Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
160
packages/core/src/widgets/index.ts
Normal file
160
packages/core/src/widgets/index.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { getDb } from "../loader.js";
|
||||
import { getWidgetComponents as getComponentRegistry } from "./components.js";
|
||||
import type { Widget, WidgetArea, WidgetRow, WidgetComponentDef } from "./types.js";
|
||||
|
||||
export type {
|
||||
Widget,
|
||||
WidgetArea,
|
||||
WidgetType,
|
||||
WidgetComponentDef,
|
||||
PropDef,
|
||||
CreateWidgetAreaInput,
|
||||
CreateWidgetInput,
|
||||
UpdateWidgetInput,
|
||||
ReorderWidgetsInput,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* Get a widget area by name, with all its widgets.
|
||||
*
|
||||
* Single query with a left join rather than area-then-widgets so the
|
||||
* common case costs one round-trip. An area with no widgets yields one
|
||||
* row with null widget columns, which we skip when mapping.
|
||||
*/
|
||||
export async function getWidgetArea(name: string): Promise<WidgetArea | null> {
|
||||
const db = await getDb();
|
||||
const rows = await db
|
||||
.selectFrom("_emdash_widget_areas as a")
|
||||
.leftJoin("_emdash_widgets as w", "w.area_id", "a.id")
|
||||
.select([
|
||||
"a.id as a_id",
|
||||
"a.name as a_name",
|
||||
"a.label as a_label",
|
||||
"a.description as a_description",
|
||||
"w.id as w_id",
|
||||
"w.type as w_type",
|
||||
"w.title as w_title",
|
||||
"w.content as w_content",
|
||||
"w.menu_name as w_menu_name",
|
||||
"w.component_id as w_component_id",
|
||||
"w.component_props as w_component_props",
|
||||
"w.area_id as w_area_id",
|
||||
"w.sort_order as w_sort_order",
|
||||
"w.created_at as w_created_at",
|
||||
])
|
||||
.where("a.name", "=", name)
|
||||
.orderBy("w.sort_order", "asc")
|
||||
.execute();
|
||||
|
||||
const first = rows[0];
|
||||
if (!first) return null;
|
||||
const widgets: Widget[] = [];
|
||||
for (const row of rows) {
|
||||
if (row.w_id === null) continue; // area has no widgets (left-join null row)
|
||||
// Left-join makes every w_* column nullable in the type; at runtime
|
||||
// they're all non-null once w_id is (we match on widgets.area_id, so
|
||||
// a widget row always has the not-null columns filled). Cast is the
|
||||
// price of that structural fact.
|
||||
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- left-join row is non-null when w_id is set; see above
|
||||
const widgetRow = {
|
||||
id: row.w_id,
|
||||
type: row.w_type,
|
||||
title: row.w_title,
|
||||
content: row.w_content,
|
||||
menu_name: row.w_menu_name,
|
||||
component_id: row.w_component_id,
|
||||
component_props: row.w_component_props,
|
||||
area_id: row.w_area_id,
|
||||
sort_order: row.w_sort_order,
|
||||
created_at: row.w_created_at,
|
||||
} as WidgetRow;
|
||||
widgets.push(rowToWidget(widgetRow));
|
||||
}
|
||||
|
||||
return {
|
||||
id: first.a_id,
|
||||
name: first.a_name,
|
||||
label: first.a_label,
|
||||
description: first.a_description ?? undefined,
|
||||
widgets,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all widget areas with their widgets
|
||||
*/
|
||||
export async function getWidgetAreas(): Promise<WidgetArea[]> {
|
||||
const db = await getDb();
|
||||
// Get all areas
|
||||
const areaRows = await db.selectFrom("_emdash_widget_areas").selectAll().execute();
|
||||
|
||||
// Get all widgets
|
||||
const widgetRows = await db
|
||||
.selectFrom("_emdash_widgets")
|
||||
.selectAll()
|
||||
.$castTo<WidgetRow>()
|
||||
.orderBy("sort_order", "asc")
|
||||
.execute();
|
||||
|
||||
// Group widgets by area
|
||||
const widgetsByArea = new Map<string, Widget[]>();
|
||||
for (const row of widgetRows) {
|
||||
if (!widgetsByArea.has(row.area_id)) {
|
||||
widgetsByArea.set(row.area_id, []);
|
||||
}
|
||||
widgetsByArea.get(row.area_id)!.push(rowToWidget(row));
|
||||
}
|
||||
|
||||
// Combine
|
||||
return areaRows.map((areaRow) => ({
|
||||
id: areaRow.id,
|
||||
name: areaRow.name,
|
||||
label: areaRow.label,
|
||||
description: areaRow.description ?? undefined,
|
||||
widgets: widgetsByArea.get(areaRow.id) || [],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available widget components (for admin UI)
|
||||
*/
|
||||
export function getWidgetComponents(): WidgetComponentDef[] {
|
||||
return getComponentRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a widget row to the API type
|
||||
*/
|
||||
export function rowToWidget(row: WidgetRow): Widget {
|
||||
const widget: Widget = {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
title: row.title ?? undefined,
|
||||
};
|
||||
|
||||
// Type-specific fields
|
||||
if (row.type === "content" && row.content) {
|
||||
try {
|
||||
widget.content = JSON.parse(row.content);
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (row.type === "menu" && row.menu_name) {
|
||||
widget.menuName = row.menu_name;
|
||||
}
|
||||
|
||||
if (row.type === "component" && row.component_id) {
|
||||
widget.componentId = row.component_id;
|
||||
if (row.component_props) {
|
||||
try {
|
||||
widget.componentProps = JSON.parse(row.component_props);
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widget;
|
||||
}
|
||||
Reference in New Issue
Block a user