@open-mercato/ui 0.6.8-develop.6889.1.af4501ca97 → 0.6.8-develop.6892.1.f179677788
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/dashboard/DashboardScreen.js +19 -5
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/injection/ComponentOverrideProvider.js +7 -1
- package/dist/backend/injection/ComponentOverrideProvider.js.map +2 -2
- package/dist/backend/injection/useRegisteredComponent.js +3 -2
- package/dist/backend/injection/useRegisteredComponent.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/dashboard/DashboardScreen.tsx +26 -5
- package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +92 -1
- package/src/backend/injection/ComponentOverrideProvider.tsx +11 -3
- package/src/backend/injection/useRegisteredComponent.tsx +3 -2
|
@@ -42,6 +42,8 @@ function generateId() {
|
|
|
42
42
|
}
|
|
43
43
|
function DashboardScreen() {
|
|
44
44
|
const t = useT();
|
|
45
|
+
const tRef = React.useRef(t);
|
|
46
|
+
tRef.current = t;
|
|
45
47
|
const organizationScopeVersion = useOrganizationScopeVersion();
|
|
46
48
|
const [loading, setLoading] = React.useState(true);
|
|
47
49
|
const [error, setError] = React.useState(null);
|
|
@@ -56,12 +58,16 @@ function DashboardScreen() {
|
|
|
56
58
|
const [settingsId, setSettingsId] = React.useState(null);
|
|
57
59
|
const pendingOpsRef = React.useRef(0);
|
|
58
60
|
const saveQueueRef = React.useRef(Promise.resolve());
|
|
61
|
+
const layoutRevisionRef = React.useRef(0);
|
|
62
|
+
const pendingLayoutRevisionsRef = React.useRef(/* @__PURE__ */ new Set());
|
|
59
63
|
const draggingIdRef = React.useRef(null);
|
|
60
64
|
const adjustSaving = React.useCallback((delta) => {
|
|
61
65
|
pendingOpsRef.current = Math.max(0, pendingOpsRef.current + delta);
|
|
62
66
|
setSaving(pendingOpsRef.current > 0);
|
|
63
67
|
}, []);
|
|
64
68
|
const load = React.useCallback(async () => {
|
|
69
|
+
const layoutRevisionAtStart = layoutRevisionRef.current;
|
|
70
|
+
const hadPendingLayoutMutationAtStart = pendingLayoutRevisionsRef.current.size > 0;
|
|
65
71
|
setLoading(true);
|
|
66
72
|
setError(null);
|
|
67
73
|
try {
|
|
@@ -72,7 +78,9 @@ function DashboardScreen() {
|
|
|
72
78
|
const data = call.result;
|
|
73
79
|
const registeredWidgetCount = getDashboardWidgets().length;
|
|
74
80
|
const normalizedLayout = sortLayout(data.layout?.items ?? []);
|
|
75
|
-
|
|
81
|
+
if (!hadPendingLayoutMutationAtStart && layoutRevisionAtStart === layoutRevisionRef.current) {
|
|
82
|
+
setLayout(normalizedLayout);
|
|
83
|
+
}
|
|
76
84
|
setWidgetCatalog(data.widgets ?? []);
|
|
77
85
|
setHasRegisteredWidgets(registeredWidgetCount > 0 || (data.widgets ?? []).length > 0);
|
|
78
86
|
setAllowedWidgetIds(data.allowedWidgetIds ?? []);
|
|
@@ -106,11 +114,11 @@ function DashboardScreen() {
|
|
|
106
114
|
setSettingsId(null);
|
|
107
115
|
return;
|
|
108
116
|
}
|
|
109
|
-
setError(
|
|
117
|
+
setError(tRef.current("dashboard.loadError"));
|
|
110
118
|
} finally {
|
|
111
119
|
setLoading(false);
|
|
112
120
|
}
|
|
113
|
-
}, [
|
|
121
|
+
}, []);
|
|
114
122
|
React.useEffect(() => {
|
|
115
123
|
load();
|
|
116
124
|
}, [load, organizationScopeVersion]);
|
|
@@ -157,6 +165,8 @@ function DashboardScreen() {
|
|
|
157
165
|
return meta.description;
|
|
158
166
|
}, [t]);
|
|
159
167
|
const queueLayoutSave = React.useCallback((items) => {
|
|
168
|
+
const layoutRevision = ++layoutRevisionRef.current;
|
|
169
|
+
pendingLayoutRevisionsRef.current.add(layoutRevision);
|
|
160
170
|
saveQueueRef.current = saveQueueRef.current.then(async () => {
|
|
161
171
|
adjustSaving(1);
|
|
162
172
|
try {
|
|
@@ -181,11 +191,12 @@ function DashboardScreen() {
|
|
|
181
191
|
logger.error("Failed to save layout", { err });
|
|
182
192
|
setError(t("dashboard.saveError"));
|
|
183
193
|
} finally {
|
|
194
|
+
pendingLayoutRevisionsRef.current.delete(layoutRevision);
|
|
184
195
|
adjustSaving(-1);
|
|
185
196
|
}
|
|
186
197
|
});
|
|
187
198
|
}, [adjustSaving, t]);
|
|
188
|
-
const patchWidgetSettings = React.useCallback(async (itemId, nextSettings) => {
|
|
199
|
+
const patchWidgetSettings = React.useCallback(async (itemId, nextSettings, layoutRevision) => {
|
|
189
200
|
adjustSaving(1);
|
|
190
201
|
try {
|
|
191
202
|
const call = await apiCall(`/api/dashboards/layout/${encodeURIComponent(itemId)}`, {
|
|
@@ -199,6 +210,7 @@ function DashboardScreen() {
|
|
|
199
210
|
logger.error("Failed to update widget settings", { err });
|
|
200
211
|
setError(t("dashboard.saveError"));
|
|
201
212
|
} finally {
|
|
213
|
+
pendingLayoutRevisionsRef.current.delete(layoutRevision);
|
|
202
214
|
adjustSaving(-1);
|
|
203
215
|
}
|
|
204
216
|
}, [adjustSaving, t]);
|
|
@@ -249,8 +261,10 @@ function DashboardScreen() {
|
|
|
249
261
|
});
|
|
250
262
|
}, [queueLayoutSave]);
|
|
251
263
|
const handleSettingsChange = React.useCallback((itemId, nextSettings) => {
|
|
264
|
+
const layoutRevision = ++layoutRevisionRef.current;
|
|
265
|
+
pendingLayoutRevisionsRef.current.add(layoutRevision);
|
|
252
266
|
setLayout((prev) => prev.map((item) => item.id === itemId ? { ...item, settings: nextSettings } : item));
|
|
253
|
-
void patchWidgetSettings(itemId, nextSettings);
|
|
267
|
+
void patchWidgetSettings(itemId, nextSettings, layoutRevision);
|
|
254
268
|
}, [patchWidgetSettings]);
|
|
255
269
|
const toggleEditing = React.useCallback(() => {
|
|
256
270
|
if (!canConfigure) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/dashboard/DashboardScreen.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { getDashboardWidgets, loadDashboardWidgetModule } from './widgetRegistry'\nimport type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { GripVertical, Plus, RefreshCw, Settings2, Trash2, X, Loader2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { InjectionSpot } from '../injection/InjectionSpot'\nimport { WidgetDataBatchProvider } from './widgetData'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'DashboardScreen' })\n\ntype DashboardWidgetSize = 'sm' | 'md' | 'lg'\n\ntype LayoutItem = {\n id: string\n widgetId: string\n order: number\n priority?: number\n size?: DashboardWidgetSize\n settings?: unknown\n}\n\ntype WidgetMeta = {\n id: string\n title: string\n description: string | null\n defaultSize: DashboardWidgetSize\n defaultEnabled: boolean\n defaultSettings: unknown\n features: string[]\n moduleId: string\n icon: string | null\n loaderKey: string\n supportsRefresh: boolean\n}\n\ntype LayoutContext = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n userName: string | null\n userEmail: string | null\n userLabel: string | null\n}\n\ntype LayoutResponse = {\n layout: { items: LayoutItem[] }\n widgets: WidgetMeta[]\n allowedWidgetIds: string[]\n canConfigure: boolean\n context: LayoutContext\n}\n\ntype WidgetModule = DashboardWidgetModule<any>\n\nfunction sizeClass(size: DashboardWidgetSize | undefined) {\n switch (size) {\n case 'lg':\n return 'md:col-span-2'\n case 'md':\n return 'md:col-span-1'\n case 'sm':\n default:\n return 'md:col-span-1'\n }\n}\n\nfunction sortLayout(items: LayoutItem[]): LayoutItem[] {\n return [...items]\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, index) => ({ ...item, order: index, priority: index }))\n}\n\nconst DEFAULT_SIZE: DashboardWidgetSize = 'md'\n\nfunction generateId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n // Fallback: timestamp + random for better uniqueness\n return Date.now().toString(36) + Math.random().toString(36).slice(2)\n}\n\nexport function DashboardScreen() {\n const t = useT()\n const organizationScopeVersion = useOrganizationScopeVersion()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [hasRegisteredWidgets, setHasRegisteredWidgets] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [layout, setLayout] = React.useState<LayoutItem[]>([])\n const [widgetCatalog, setWidgetCatalog] = React.useState<WidgetMeta[]>([])\n const [allowedWidgetIds, setAllowedWidgetIds] = React.useState<string[]>([])\n const [canConfigure, setCanConfigure] = React.useState(false)\n const [context, setContext] = React.useState<LayoutContext | null>(null)\n const [editing, setEditing] = React.useState(false)\n const [settingsId, setSettingsId] = React.useState<string | null>(null)\n const pendingOpsRef = React.useRef(0)\n const saveQueueRef = React.useRef(Promise.resolve())\n const draggingIdRef = React.useRef<string | null>(null)\n\n const adjustSaving = React.useCallback((delta: number) => {\n pendingOpsRef.current = Math.max(0, pendingOpsRef.current + delta)\n setSaving(pendingOpsRef.current > 0)\n }, [])\n\n const load = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const call = await apiCall<LayoutResponse>('/api/dashboards/layout')\n if (!call.ok || !call.result) {\n throw new Error(`Failed with status ${call.status}`)\n }\n const data = call.result\n const registeredWidgetCount = getDashboardWidgets().length\n const normalizedLayout = sortLayout(data.layout?.items ?? [])\n setLayout(normalizedLayout)\n setWidgetCatalog(data.widgets ?? [])\n setHasRegisteredWidgets(registeredWidgetCount > 0 || (data.widgets ?? []).length > 0)\n setAllowedWidgetIds(data.allowedWidgetIds ?? [])\n setCanConfigure(!!data.canConfigure)\n if (data.context) {\n setContext({\n userId: data.context.userId,\n tenantId: data.context.tenantId ?? null,\n organizationId: data.context.organizationId ?? null,\n userName: data.context.userName ?? null,\n userEmail: data.context.userEmail ?? null,\n userLabel: data.context.userLabel ?? null,\n })\n } else {\n setContext(null)\n }\n if (!data.canConfigure) {\n setEditing(false)\n setSettingsId(null)\n }\n } catch (err) {\n logger.error('Failed to load dashboard layout', { err })\n if (getDashboardWidgets().length === 0) {\n setHasRegisteredWidgets(false)\n setLayout([])\n setWidgetCatalog([])\n setAllowedWidgetIds([])\n setCanConfigure(false)\n setContext(null)\n setEditing(false)\n setSettingsId(null)\n return\n }\n setError(t('dashboard.loadError'))\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n load()\n }, [load, organizationScopeVersion])\n\n const metaById = React.useMemo(() => {\n const map = new Map<string, WidgetMeta>()\n for (const meta of widgetCatalog) map.set(meta.id, meta)\n return map\n }, [widgetCatalog])\n\n const availableWidgets = React.useMemo(() => {\n const currentIds = new Set(layout.map((item) => item.widgetId))\n return widgetCatalog.filter((meta) => !currentIds.has(meta.id))\n }, [layout, widgetCatalog])\n\n const resolveWidgetTitle = React.useCallback((meta: WidgetMeta): string => {\n const keys = [\n `${meta.id}.title`,\n `dashboard.widgets.${meta.id}.title`,\n ]\n if (meta.id.includes('.')) {\n const parts = meta.id.split('.')\n const lastPart = parts.pop()\n keys.unshift(`${parts.join('.')}.widgets.${lastPart}.title`)\n }\n for (const key of keys) {\n const translated = t(key)\n if (translated !== key) return translated\n }\n return meta.title\n }, [t])\n\n const resolveWidgetDescription = React.useCallback((meta: WidgetMeta): string | null => {\n if (!meta.description) return null\n const keys = [\n `${meta.id}.description`,\n `dashboard.widgets.${meta.id}.description`,\n ]\n if (meta.id.includes('.')) {\n const parts = meta.id.split('.')\n const lastPart = parts.pop()\n keys.unshift(`${parts.join('.')}.widgets.${lastPart}.description`)\n }\n for (const key of keys) {\n const translated = t(key)\n if (translated !== key) return translated\n }\n return meta.description\n }, [t])\n\n const queueLayoutSave = React.useCallback((items: LayoutItem[]) => {\n saveQueueRef.current = saveQueueRef.current.then(async () => {\n adjustSaving(1)\n try {\n const payload = {\n items: items.map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings ?? null,\n })),\n }\n const call = await apiCall('/api/dashboards/layout', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n if (!call.ok) throw new Error(`Failed with status ${call.status}`)\n setError(null)\n } catch (err) {\n logger.error('Failed to save layout', { err })\n setError(t('dashboard.saveError'))\n } finally {\n adjustSaving(-1)\n }\n })\n }, [adjustSaving, t])\n\n const patchWidgetSettings = React.useCallback(async (itemId: string, nextSettings: unknown) => {\n adjustSaving(1)\n try {\n const call = await apiCall(`/api/dashboards/layout/${encodeURIComponent(itemId)}`, {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ settings: nextSettings }),\n })\n if (!call.ok) throw new Error(`Failed with status ${call.status}`)\n setError(null)\n } catch (err) {\n logger.error('Failed to update widget settings', { err })\n setError(t('dashboard.saveError'))\n } finally {\n adjustSaving(-1)\n }\n }, [adjustSaving, t])\n\n const handleAddWidget = React.useCallback((widgetId: string) => {\n const meta = metaById.get(widgetId)\n if (!meta) return\n setLayout((prev) => {\n const next: LayoutItem[] = sortLayout([\n ...prev,\n {\n id: generateId(),\n widgetId: meta.id,\n order: prev.length,\n priority: prev.length,\n size: meta.defaultSize ?? DEFAULT_SIZE,\n settings: meta.defaultSettings ?? null,\n },\n ])\n queueLayoutSave(next)\n return next\n })\n setSettingsId(null)\n }, [metaById, queueLayoutSave])\n\n const handleRemoveWidget = React.useCallback((itemId: string) => {\n setLayout((prev) => {\n const next = sortLayout(prev.filter((item) => item.id !== itemId))\n queueLayoutSave(next)\n return next\n })\n if (settingsId === itemId) setSettingsId(null)\n }, [queueLayoutSave, settingsId])\n\n const handleReorder = React.useCallback((dragId: string | null, targetId: string) => {\n if (!dragId || dragId === targetId) return\n setLayout((prev) => {\n const items = [...prev]\n const from = items.findIndex((item) => item.id === dragId)\n const to = items.findIndex((item) => item.id === targetId)\n if (from === -1 || to === -1) return prev\n const [moved] = items.splice(from, 1)\n items.splice(to, 0, moved)\n const next = items.map((item, index) => ({\n ...item,\n order: index,\n priority: index,\n }))\n queueLayoutSave(next)\n return next\n })\n }, [queueLayoutSave])\n\n const handleSettingsChange = React.useCallback((itemId: string, nextSettings: unknown) => {\n setLayout((prev) => prev.map((item) => (item.id === itemId ? { ...item, settings: nextSettings } : item)))\n void patchWidgetSettings(itemId, nextSettings)\n }, [patchWidgetSettings])\n\n const toggleEditing = React.useCallback(() => {\n if (!canConfigure) return\n setEditing((prev) => {\n const next = !prev\n if (!next) setSettingsId(null)\n return next\n })\n }, [canConfigure])\n\n const handleRefresh = React.useCallback(() => {\n load()\n }, [load])\n\n const injectionContext = React.useMemo(\n () => ({\n layout,\n widgetCatalog,\n allowedWidgetIds,\n canConfigure,\n editing,\n userContext: context,\n }),\n [allowedWidgetIds, canConfigure, context, editing, layout, widgetCatalog],\n )\n const dashboardBeforeSpotId = 'dashboard:before'\n const dashboardAfterSpotId = 'dashboard:after'\n\n if (loading) {\n return (\n <div className=\"flex min-h-[320px] items-center justify-center\">\n <Spinner size=\"lg\" />\n </div>\n )\n }\n\n if (error && layout.length === 0) {\n return (\n <Alert status=\"error\">\n <AlertTitle>{t('dashboard.unavailable')}</AlertTitle>\n <AlertDescription>{error}</AlertDescription>\n <div className=\"mt-2\"><Button variant=\"outline\" onClick={handleRefresh}>{t('dashboard.retry')}</Button></div>\n </Alert>\n )\n }\n\n if (!hasRegisteredWidgets && layout.length === 0) {\n return (\n <Alert status=\"information\">\n <AlertTitle>{t('dashboard.empty.noWidgets.title', 'No dashboard widgets yet')}</AlertTitle>\n <AlertDescription>\n {t(\n 'dashboard.empty.noWidgets.description',\n 'After you add the first module that exposes dashboard widgets, they will appear here.',\n )}\n </AlertDescription>\n </Alert>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold tracking-tight\">{t('dashboard.title')}</h1>\n <p className=\"text-sm text-muted-foreground\">{t('dashboard.subtitle')}</p>\n </div>\n <div className=\"flex items-center gap-2\">\n {saving && (\n <div className=\"flex items-center gap-1 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n <span>{t('dashboard.saving')}</span>\n </div>\n )}\n {canConfigure && (\n <Button variant={editing ? 'secondary' : 'outline'} size=\"sm\" onClick={toggleEditing}>\n <Settings2 className=\"h-4 w-4\" />\n <span>{editing ? t('dashboard.action.done') : t('dashboard.action.customize')}</span>\n </Button>\n )}\n </div>\n </div>\n\n {error && layout.length > 0 && (\n <Alert status=\"error\">\n <AlertTitle>{t('dashboard.error.partial')}</AlertTitle>\n <AlertDescription>{error}</AlertDescription>\n <div className=\"mt-2\"><Button variant=\"ghost\" onClick={handleRefresh}>{t('dashboard.error.reload')}</Button></div>\n </Alert>\n )}\n\n <InjectionSpot spotId={dashboardBeforeSpotId} context={injectionContext} />\n\n {editing && availableWidgets.length > 0 && (\n <div className=\"rounded-lg border border-dashed bg-muted/50 p-4\">\n <div className=\"mb-2 text-sm font-medium text-muted-foreground\">{t('dashboard.addWidget')}</div>\n <div className=\"flex flex-wrap gap-2\">\n {availableWidgets.map((meta) => (\n <Button\n key={meta.id}\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleAddWidget(meta.id)}\n >\n <Plus className=\"h-4 w-4\" />\n {resolveWidgetTitle(meta)}\n </Button>\n ))}\n </div>\n </div>\n )}\n\n <WidgetDataBatchProvider>\n <div className={cn(\n 'grid gap-3 sm:gap-4 md:gap-6',\n 'grid-cols-1',\n 'md:grid-cols-2',\n 'xl:grid-cols-3'\n )}\n onDragOver={(event) => {\n if (!editing || !canConfigure) return\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }}\n onDrop={(event) => {\n if (!editing || !canConfigure) return\n event.preventDefault()\n const dragId = event.dataTransfer.getData('text/plain') || draggingIdRef.current\n if (!dragId) return\n setLayout((prev) => {\n const items = [...prev]\n const from = items.findIndex((entry) => entry.id === dragId)\n if (from === -1) return prev\n const [moved] = items.splice(from, 1)\n items.push(moved)\n const next = items.map((item, index) => ({\n ...item,\n order: index,\n priority: index,\n }))\n queueLayoutSave(next)\n return next\n })\n draggingIdRef.current = null\n }}>\n {layout.map((item) => {\n const meta = metaById.get(item.widgetId)\n if (!meta) return null\n const title = resolveWidgetTitle(meta)\n const description = resolveWidgetDescription(meta)\n return (\n <DashboardWidgetCard\n key={item.id}\n item={item}\n meta={meta}\n title={title}\n description={description}\n context={context}\n editing={editing && canConfigure}\n activeSettings={settingsId === item.id}\n onToggleSettings={() => setSettingsId((current) => (current === item.id ? null : item.id))}\n onRemove={() => handleRemoveWidget(item.id)}\n onSettingsChange={(settings) => handleSettingsChange(item.id, settings)}\n onDragStart={() => { draggingIdRef.current = item.id }}\n onDragEnd={() => { draggingIdRef.current = null }}\n onDrop={(event) => {\n const dragId = event.dataTransfer.getData('text/plain') || draggingIdRef.current\n handleReorder(dragId, item.id)\n draggingIdRef.current = null\n }}\n onDragEnter={() => {}}\n onDragLeave={() => {}}\n sizeClass={sizeClass(item.size)}\n />\n )\n })}\n </div>\n </WidgetDataBatchProvider>\n\n {layout.length === 0 && (\n <div className=\"rounded-lg border border-dashed bg-muted/30 p-10 text-center text-sm text-muted-foreground\">\n {!hasRegisteredWidgets\n ? t(\n 'dashboard.empty.noWidgets.description',\n 'After you add the first module that exposes dashboard widgets, they will appear here.',\n )\n : canConfigure ? t('dashboard.empty.configurable') : t('dashboard.empty.readonly')}\n </div>\n )}\n\n <InjectionSpot spotId={dashboardAfterSpotId} context={injectionContext} />\n </div>\n )\n}\n\ntype DashboardWidgetCardProps = {\n item: LayoutItem\n meta: WidgetMeta\n title: string\n description: string | null\n context: LayoutContext | null\n editing: boolean\n activeSettings: boolean\n onToggleSettings: () => void\n onRemove: () => void\n onSettingsChange: (next: unknown) => void\n onDragStart: () => void\n onDragEnd: () => void\n onDrop: (event: React.DragEvent<HTMLDivElement>) => void\n onDragEnter: () => void\n onDragLeave: () => void\n sizeClass: string\n}\n\nfunction DashboardWidgetCard({\n item,\n meta,\n title,\n description,\n context,\n editing,\n activeSettings,\n onToggleSettings,\n onRemove,\n onSettingsChange,\n onDragStart,\n onDragEnd,\n onDrop,\n onDragEnter,\n onDragLeave,\n sizeClass,\n}: DashboardWidgetCardProps) {\n const t = useT()\n const [module, setModule] = React.useState<WidgetModule | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [isDragOver, setIsDragOver] = React.useState(false)\n const [refreshToken, setRefreshToken] = React.useState(0)\n const [refreshing, setRefreshing] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n setLoading(true)\n setLoadError(null)\n loadDashboardWidgetModule(meta.loaderKey)\n .then((loaded) => {\n if (cancelled) return\n if (!loaded) {\n setModule(null)\n setLoadError(t('dashboard.widget.loadError'))\n setLoading(false)\n return\n }\n setModule(loaded)\n setLoading(false)\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('Failed to load widget module', { err })\n setLoadError(t('dashboard.widget.loadError'))\n setLoading(false)\n })\n return () => { cancelled = true }\n }, [meta.loaderKey, t])\n\n React.useEffect(() => {\n if (!meta.supportsRefresh) {\n setRefreshing(false)\n }\n }, [meta.supportsRefresh])\n\n React.useEffect(() => {\n if (activeSettings) {\n setRefreshing(false)\n }\n }, [activeSettings])\n\n React.useEffect(() => {\n if (loadError) {\n setRefreshing(false)\n }\n }, [loadError])\n\n const handleRefreshStateChange = React.useCallback((next: boolean) => {\n setRefreshing(next)\n }, [])\n\n const triggerRefresh = React.useCallback(() => {\n if (loading || !!loadError) return\n setRefreshing(true)\n setRefreshToken((current) => current + 1)\n }, [loadError, loading])\n\n const hydratedSettings = React.useMemo(() => {\n const raw = item.settings ?? meta.defaultSettings ?? null\n if (module?.hydrateSettings) {\n try {\n return module.hydrateSettings(raw)\n } catch (err) {\n logger.warn('Failed to hydrate widget settings', { err })\n return raw\n }\n }\n return raw\n }, [item.settings, meta.defaultSettings, module])\n\n const handleSettingsChange = React.useCallback((next: unknown) => {\n let raw = next\n if (module?.dehydrateSettings) {\n try {\n raw = module.dehydrateSettings(next as never)\n } catch (err) {\n logger.warn('Failed to dehydrate widget settings', { err })\n }\n }\n onSettingsChange(raw)\n }, [module, onSettingsChange])\n\n const WidgetComponent = module?.Widget\n const mode = activeSettings ? 'settings' : 'view'\n\n return (\n <div\n className={cn(\n 'group relative flex h-full flex-col rounded-lg border bg-background shadow-sm transition',\n isDragOver ? 'border-primary ring-2 ring-primary/20' : 'hover:border-primary/40',\n editing ? 'cursor-grab' : 'cursor-default',\n sizeClass\n )}\n draggable={editing}\n onDragStart={(event) => {\n if (!editing) return\n event.dataTransfer.effectAllowed = 'move'\n event.dataTransfer.setData('text/plain', item.id)\n onDragStart()\n }}\n onDragEnd={() => {\n if (!editing) return\n onDragEnd()\n }}\n onDragOver={(event) => {\n if (!editing) return\n event.preventDefault()\n event.stopPropagation()\n event.dataTransfer.dropEffect = 'move'\n if (!isDragOver) {\n setIsDragOver(true)\n onDragEnter()\n }\n }}\n onDrop={(event) => {\n if (!editing) return\n event.preventDefault()\n event.stopPropagation()\n onDrop(event)\n setIsDragOver(false)\n onDragLeave()\n }}\n onDragLeave={(event) => {\n if (!editing) return\n event.stopPropagation()\n if (event.currentTarget.contains(event.relatedTarget as Node)) return\n setIsDragOver(false)\n onDragLeave()\n }}\n >\n <div className=\"flex items-center justify-between gap-2 border-b px-3 py-2\">\n <div className=\"flex items-center gap-2\">\n {editing && <GripVertical className=\"h-4 w-4 text-muted-foreground\" />}\n <div>\n <div className=\"text-sm font-medium leading-none\">{title}</div>\n {description ? <div className=\"mt-1 text-xs text-muted-foreground\">{description}</div> : null}\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n {!editing && meta.supportsRefresh && (\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n disabled={refreshing || loading || !!loadError}\n onClick={triggerRefresh}\n aria-label={t('dashboard.widget.refresh')}\n >\n {refreshing ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <RefreshCw className=\"h-4 w-4\" />}\n </IconButton>\n )}\n {editing && (\n <>\n <IconButton\n variant={activeSettings ? 'outline' : 'ghost'}\n size=\"sm\"\n onClick={onToggleSettings}\n aria-label={activeSettings ? t('dashboard.widget.closeSettings') : t('dashboard.widget.editSettings')}\n >\n {activeSettings ? <X className=\"h-4 w-4\" /> : <Settings2 className=\"h-4 w-4\" />}\n </IconButton>\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n onClick={onRemove}\n aria-label={t('dashboard.widget.remove')}\n >\n <Trash2 className=\"h-4 w-4\" />\n </IconButton>\n </>\n )}\n </div>\n </div>\n <div className=\"flex-1 p-4\">\n {loading && (\n <div className=\"flex h-full min-h-[120px] items-center justify-center\">\n <Spinner />\n </div>\n )}\n {loadError && !loading && (\n <div className=\"text-sm text-muted-foreground\">{loadError}</div>\n )}\n {!loading && !loadError && WidgetComponent && (\n <WidgetComponent\n mode={mode as 'view' | 'settings'}\n layout={item}\n context={context ?? { userId: '', tenantId: null, organizationId: null, userName: null, userEmail: null, userLabel: null }}\n settings={hydratedSettings}\n onSettingsChange={handleSettingsChange}\n refreshToken={refreshToken}\n onRefreshStateChange={handleRefreshStateChange}\n />\n )}\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAgWQ,SAoWI,UApWJ,KAOF,YAPE;AA9VR,YAAY,WAAW;AACvB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,OAAO,kBAAkB,kBAAkB;AACpD,SAAS,eAAe;AACxB,SAAS,qBAAqB,iCAAiC;AAE/D,SAAS,UAAU;AACnB,SAAS,cAAc,MAAM,WAAW,WAAW,QAAQ,GAAG,eAAe;AAC7E,SAAS,YAAY;AACrB,SAAS,mCAAmC;AAC5C,SAAS,qBAAqB;AAC9B,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,kBAAkB,CAAC;AA8CxE,SAAS,UAAU,MAAuC;AACxD,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,WAAW,OAAmC;AACrD,SAAO,CAAC,GAAG,KAAK,EACb,KAAK,CAAC,GAAG,MAAM;AACd,UAAM,SAAS,EAAE,SAAS,EAAE,YAAY;AACxC,UAAM,SAAS,EAAE,SAAS,EAAE,YAAY;AACxC,WAAO,SAAS;AAAA,EAClB,CAAC,EACA,IAAI,CAAC,MAAM,WAAW,EAAE,GAAG,MAAM,OAAO,OAAO,UAAU,MAAM,EAAE;AACtE;AAEA,MAAM,eAAoC;AAE1C,SAAS,aAAqB;AAC5B,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,SAAO,KAAK,IAAI,EAAE,SAAS,EAAE,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC;AACrE;AAEO,SAAS,kBAAkB;AAChC,QAAM,IAAI,KAAK;AACf,QAAM,2BAA2B,4BAA4B;AAC7D,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,MAAM,SAAS,IAAI;AAC3E,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAuB,CAAC,CAAC;AAC3D,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAuB,CAAC,CAAC;AACzE,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAmB,CAAC,CAAC;AAC3E,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,KAAK;AAC5D,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAA+B,IAAI;AACvE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAwB,IAAI;AACtE,QAAM,gBAAgB,MAAM,OAAO,CAAC;AACpC,QAAM,eAAe,MAAM,OAAO,QAAQ,QAAQ,CAAC;AACnD,QAAM,gBAAgB,MAAM,OAAsB,IAAI;AAEtD,QAAM,eAAe,MAAM,YAAY,CAAC,UAAkB;AACxD,kBAAc,UAAU,KAAK,IAAI,GAAG,cAAc,UAAU,KAAK;AACjE,cAAU,cAAc,UAAU,CAAC;AAAA,EACrC,GAAG,CAAC,CAAC;AAEL,QAAM,OAAO,MAAM,YAAY,YAAY;AACzC,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,OAAO,MAAM,QAAwB,wBAAwB;AACnE,UAAI,CAAC,KAAK,MAAM,CAAC,KAAK,QAAQ;AAC5B,cAAM,IAAI,MAAM,sBAAsB,KAAK,MAAM,EAAE;AAAA,MACrD;AACA,YAAM,OAAO,KAAK;AAClB,YAAM,wBAAwB,oBAAoB,EAAE;AACpD,YAAM,mBAAmB,WAAW,KAAK,QAAQ,SAAS,CAAC,CAAC;AAC5D,gBAAU,gBAAgB;AAC1B,uBAAiB,KAAK,WAAW,CAAC,CAAC;AACnC,8BAAwB,wBAAwB,MAAM,KAAK,WAAW,CAAC,GAAG,SAAS,CAAC;AACpF,0BAAoB,KAAK,oBAAoB,CAAC,CAAC;AAC/C,sBAAgB,CAAC,CAAC,KAAK,YAAY;AACnC,UAAI,KAAK,SAAS;AAChB,mBAAW;AAAA,UACT,QAAQ,KAAK,QAAQ;AAAA,UACrB,UAAU,KAAK,QAAQ,YAAY;AAAA,UACnC,gBAAgB,KAAK,QAAQ,kBAAkB;AAAA,UAC/C,UAAU,KAAK,QAAQ,YAAY;AAAA,UACnC,WAAW,KAAK,QAAQ,aAAa;AAAA,UACrC,WAAW,KAAK,QAAQ,aAAa;AAAA,QACvC,CAAC;AAAA,MACH,OAAO;AACL,mBAAW,IAAI;AAAA,MACjB;AACA,UAAI,CAAC,KAAK,cAAc;AACtB,mBAAW,KAAK;AAChB,sBAAc,IAAI;AAAA,MACpB;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,mCAAmC,EAAE,IAAI,CAAC;AACvD,UAAI,oBAAoB,EAAE,WAAW,GAAG;AACtC,gCAAwB,KAAK;AAC7B,kBAAU,CAAC,CAAC;AACZ,yBAAiB,CAAC,CAAC;AACnB,4BAAoB,CAAC,CAAC;AACtB,wBAAgB,KAAK;AACrB,mBAAW,IAAI;AACf,mBAAW,KAAK;AAChB,sBAAc,IAAI;AAClB;AAAA,MACF;AACA,eAAS,EAAE,qBAAqB,CAAC;AAAA,IACnC,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,UAAU,MAAM;AACpB,SAAK;AAAA,EACP,GAAG,CAAC,MAAM,wBAAwB,CAAC;AAEnC,QAAM,WAAW,MAAM,QAAQ,MAAM;AACnC,UAAM,MAAM,oBAAI,IAAwB;AACxC,eAAW,QAAQ,cAAe,KAAI,IAAI,KAAK,IAAI,IAAI;AACvD,WAAO;AAAA,EACT,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,mBAAmB,MAAM,QAAQ,MAAM;AAC3C,UAAM,aAAa,IAAI,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC;AAC9D,WAAO,cAAc,OAAO,CAAC,SAAS,CAAC,WAAW,IAAI,KAAK,EAAE,CAAC;AAAA,EAChE,GAAG,CAAC,QAAQ,aAAa,CAAC;AAE1B,QAAM,qBAAqB,MAAM,YAAY,CAAC,SAA6B;AACzE,UAAM,OAAO;AAAA,MACX,GAAG,KAAK,EAAE;AAAA,MACV,qBAAqB,KAAK,EAAE;AAAA,IAC9B;AACA,QAAI,KAAK,GAAG,SAAS,GAAG,GAAG;AACzB,YAAM,QAAQ,KAAK,GAAG,MAAM,GAAG;AAC/B,YAAM,WAAW,MAAM,IAAI;AAC3B,WAAK,QAAQ,GAAG,MAAM,KAAK,GAAG,CAAC,YAAY,QAAQ,QAAQ;AAAA,IAC7D;AACA,eAAW,OAAO,MAAM;AACtB,YAAM,aAAa,EAAE,GAAG;AACxB,UAAI,eAAe,IAAK,QAAO;AAAA,IACjC;AACA,WAAO,KAAK;AAAA,EACd,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,2BAA2B,MAAM,YAAY,CAAC,SAAoC;AACtF,QAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,UAAM,OAAO;AAAA,MACX,GAAG,KAAK,EAAE;AAAA,MACV,qBAAqB,KAAK,EAAE;AAAA,IAC9B;AACA,QAAI,KAAK,GAAG,SAAS,GAAG,GAAG;AACzB,YAAM,QAAQ,KAAK,GAAG,MAAM,GAAG;AAC/B,YAAM,WAAW,MAAM,IAAI;AAC3B,WAAK,QAAQ,GAAG,MAAM,KAAK,GAAG,CAAC,YAAY,QAAQ,cAAc;AAAA,IACnE;AACA,eAAW,OAAO,MAAM;AACtB,YAAM,aAAa,EAAE,GAAG;AACxB,UAAI,eAAe,IAAK,QAAO;AAAA,IACjC;AACA,WAAO,KAAK;AAAA,EACd,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,kBAAkB,MAAM,YAAY,CAAC,UAAwB;AACjE,iBAAa,UAAU,aAAa,QAAQ,KAAK,YAAY;AAC3D,mBAAa,CAAC;AACd,UAAI;AACF,cAAM,UAAU;AAAA,UACd,OAAO,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,YACjC,IAAI,KAAK;AAAA,YACT,UAAU,KAAK;AAAA,YACf,OAAO;AAAA,YACP,UAAU;AAAA,YACV,MAAM,KAAK,QAAQ;AAAA,YACnB,UAAU,KAAK,YAAY;AAAA,UAC7B,EAAE;AAAA,QACJ;AACA,cAAM,OAAO,MAAM,QAAQ,0BAA0B;AAAA,UACnD,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,QAC9B,CAAC;AACD,YAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,sBAAsB,KAAK,MAAM,EAAE;AACjE,iBAAS,IAAI;AAAA,MACf,SAAS,KAAK;AACZ,eAAO,MAAM,yBAAyB,EAAE,IAAI,CAAC;AAC7C,iBAAS,EAAE,qBAAqB,CAAC;AAAA,MACnC,UAAE;AACA,qBAAa,EAAE;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,cAAc,CAAC,CAAC;AAEpB,QAAM,sBAAsB,MAAM,YAAY,OAAO,QAAgB,iBAA0B;AAC7F,iBAAa,CAAC;AACd,QAAI;AACF,YAAM,OAAO,MAAM,QAAQ,0BAA0B,mBAAmB,MAAM,CAAC,IAAI;AAAA,QACjF,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,aAAa,CAAC;AAAA,MACjD,CAAC;AACD,UAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,sBAAsB,KAAK,MAAM,EAAE;AACjE,eAAS,IAAI;AAAA,IACf,SAAS,KAAK;AACZ,aAAO,MAAM,oCAAoC,EAAE,IAAI,CAAC;AACxD,eAAS,EAAE,qBAAqB,CAAC;AAAA,IACnC,UAAE;AACA,mBAAa,EAAE;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,cAAc,CAAC,CAAC;AAEpB,QAAM,kBAAkB,MAAM,YAAY,CAAC,aAAqB;AAC9D,UAAM,OAAO,SAAS,IAAI,QAAQ;AAClC,QAAI,CAAC,KAAM;AACX,cAAU,CAAC,SAAS;AAClB,YAAM,OAAqB,WAAW;AAAA,QACpC,GAAG;AAAA,QACH;AAAA,UACE,IAAI,WAAW;AAAA,UACf,UAAU,KAAK;AAAA,UACf,OAAO,KAAK;AAAA,UACZ,UAAU,KAAK;AAAA,UACf,MAAM,KAAK,eAAe;AAAA,UAC1B,UAAU,KAAK,mBAAmB;AAAA,QACpC;AAAA,MACF,CAAC;AACD,sBAAgB,IAAI;AACpB,aAAO;AAAA,IACT,CAAC;AACD,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,UAAU,eAAe,CAAC;AAE9B,QAAM,qBAAqB,MAAM,YAAY,CAAC,WAAmB;AAC/D,cAAU,CAAC,SAAS;AAClB,YAAM,OAAO,WAAW,KAAK,OAAO,CAAC,SAAS,KAAK,OAAO,MAAM,CAAC;AACjE,sBAAgB,IAAI;AACpB,aAAO;AAAA,IACT,CAAC;AACD,QAAI,eAAe,OAAQ,eAAc,IAAI;AAAA,EAC/C,GAAG,CAAC,iBAAiB,UAAU,CAAC;AAEhC,QAAM,gBAAgB,MAAM,YAAY,CAAC,QAAuB,aAAqB;AACnF,QAAI,CAAC,UAAU,WAAW,SAAU;AACpC,cAAU,CAAC,SAAS;AAClB,YAAM,QAAQ,CAAC,GAAG,IAAI;AACtB,YAAM,OAAO,MAAM,UAAU,CAAC,SAAS,KAAK,OAAO,MAAM;AACzD,YAAM,KAAK,MAAM,UAAU,CAAC,SAAS,KAAK,OAAO,QAAQ;AACzD,UAAI,SAAS,MAAM,OAAO,GAAI,QAAO;AACrC,YAAM,CAAC,KAAK,IAAI,MAAM,OAAO,MAAM,CAAC;AACpC,YAAM,OAAO,IAAI,GAAG,KAAK;AACzB,YAAM,OAAO,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,QACvC,GAAG;AAAA,QACH,OAAO;AAAA,QACP,UAAU;AAAA,MACZ,EAAE;AACF,sBAAgB,IAAI;AACpB,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,eAAe,CAAC;AAEpB,QAAM,uBAAuB,MAAM,YAAY,CAAC,QAAgB,iBAA0B;AACxF,cAAU,CAAC,SAAS,KAAK,IAAI,CAAC,SAAU,KAAK,OAAO,SAAS,EAAE,GAAG,MAAM,UAAU,aAAa,IAAI,IAAK,CAAC;AACzG,SAAK,oBAAoB,QAAQ,YAAY;AAAA,EAC/C,GAAG,CAAC,mBAAmB,CAAC;AAExB,QAAM,gBAAgB,MAAM,YAAY,MAAM;AAC5C,QAAI,CAAC,aAAc;AACnB,eAAW,CAAC,SAAS;AACnB,YAAM,OAAO,CAAC;AACd,UAAI,CAAC,KAAM,eAAc,IAAI;AAC7B,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,gBAAgB,MAAM,YAAY,MAAM;AAC5C,SAAK;AAAA,EACP,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,mBAAmB,MAAM;AAAA,IAC7B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa;AAAA,IACf;AAAA,IACA,CAAC,kBAAkB,cAAc,SAAS,SAAS,QAAQ,aAAa;AAAA,EAC1E;AACA,QAAM,wBAAwB;AAC9B,QAAM,uBAAuB;AAE7B,MAAI,SAAS;AACX,WACE,oBAAC,SAAI,WAAU,kDACb,8BAAC,WAAQ,MAAK,MAAK,GACrB;AAAA,EAEJ;AAEA,MAAI,SAAS,OAAO,WAAW,GAAG;AAChC,WACE,qBAAC,SAAM,QAAO,SACZ;AAAA,0BAAC,cAAY,YAAE,uBAAuB,GAAE;AAAA,MACxC,oBAAC,oBAAkB,iBAAM;AAAA,MACzB,oBAAC,SAAI,WAAU,QAAO,8BAAC,UAAO,SAAQ,WAAU,SAAS,eAAgB,YAAE,iBAAiB,GAAE,GAAS;AAAA,OACzG;AAAA,EAEJ;AAEA,MAAI,CAAC,wBAAwB,OAAO,WAAW,GAAG;AAChD,WACE,qBAAC,SAAM,QAAO,eACZ;AAAA,0BAAC,cAAY,YAAE,mCAAmC,0BAA0B,GAAE;AAAA,MAC9E,oBAAC,oBACE;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,qDACb;AAAA,2BAAC,SACC;AAAA,4BAAC,QAAG,WAAU,yCAAyC,YAAE,iBAAiB,GAAE;AAAA,QAC5E,oBAAC,OAAE,WAAU,iCAAiC,YAAE,oBAAoB,GAAE;AAAA,SACxE;AAAA,MACA,qBAAC,SAAI,WAAU,2BACZ;AAAA,kBACC,qBAAC,SAAI,WAAU,yDACb;AAAA,8BAAC,WAAQ,WAAU,wBAAuB;AAAA,UAC1C,oBAAC,UAAM,YAAE,kBAAkB,GAAE;AAAA,WAC/B;AAAA,QAED,gBACC,qBAAC,UAAO,SAAS,UAAU,cAAc,WAAW,MAAK,MAAK,SAAS,eACrE;AAAA,8BAAC,aAAU,WAAU,WAAU;AAAA,UAC/B,oBAAC,UAAM,oBAAU,EAAE,uBAAuB,IAAI,EAAE,4BAA4B,GAAE;AAAA,WAChF;AAAA,SAEJ;AAAA,OACF;AAAA,IAEC,SAAS,OAAO,SAAS,KACxB,qBAAC,SAAM,QAAO,SACZ;AAAA,0BAAC,cAAY,YAAE,yBAAyB,GAAE;AAAA,MAC1C,oBAAC,oBAAkB,iBAAM;AAAA,MACzB,oBAAC,SAAI,WAAU,QAAO,8BAAC,UAAO,SAAQ,SAAQ,SAAS,eAAgB,YAAE,wBAAwB,GAAE,GAAS;AAAA,OAC9G;AAAA,IAGF,oBAAC,iBAAc,QAAQ,uBAAuB,SAAS,kBAAkB;AAAA,IAExE,WAAW,iBAAiB,SAAS,KACpC,qBAAC,SAAI,WAAU,mDACb;AAAA,0BAAC,SAAI,WAAU,kDAAkD,YAAE,qBAAqB,GAAE;AAAA,MAC1F,oBAAC,SAAI,WAAU,wBACZ,2BAAiB,IAAI,CAAC,SACrB;AAAA,QAAC;AAAA;AAAA,UAEC,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM,gBAAgB,KAAK,EAAE;AAAA,UAEtC;AAAA,gCAAC,QAAK,WAAU,WAAU;AAAA,YACzB,mBAAmB,IAAI;AAAA;AAAA;AAAA,QANnB,KAAK;AAAA,MAOZ,CACD,GACH;AAAA,OACF;AAAA,IAGF,oBAAC,2BACD;AAAA,MAAC;AAAA;AAAA,QAAI,WAAW;AAAA,UACd;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,YAAY,CAAC,UAAU;AACrB,cAAI,CAAC,WAAW,CAAC,aAAc;AAC/B,gBAAM,eAAe;AACrB,gBAAM,aAAa,aAAa;AAAA,QAClC;AAAA,QACA,QAAQ,CAAC,UAAU;AACjB,cAAI,CAAC,WAAW,CAAC,aAAc;AAC/B,gBAAM,eAAe;AACrB,gBAAM,SAAS,MAAM,aAAa,QAAQ,YAAY,KAAK,cAAc;AACzE,cAAI,CAAC,OAAQ;AACb,oBAAU,CAAC,SAAS;AAClB,kBAAM,QAAQ,CAAC,GAAG,IAAI;AACtB,kBAAM,OAAO,MAAM,UAAU,CAAC,UAAU,MAAM,OAAO,MAAM;AAC3D,gBAAI,SAAS,GAAI,QAAO;AACxB,kBAAM,CAAC,KAAK,IAAI,MAAM,OAAO,MAAM,CAAC;AACpC,kBAAM,KAAK,KAAK;AAChB,kBAAM,OAAO,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,cACvC,GAAG;AAAA,cACH,OAAO;AAAA,cACP,UAAU;AAAA,YACZ,EAAE;AACF,4BAAgB,IAAI;AACpB,mBAAO;AAAA,UACT,CAAC;AACD,wBAAc,UAAU;AAAA,QAC1B;AAAA,QACG,iBAAO,IAAI,CAAC,SAAS;AACpB,gBAAM,OAAO,SAAS,IAAI,KAAK,QAAQ;AACvC,cAAI,CAAC,KAAM,QAAO;AAClB,gBAAM,QAAQ,mBAAmB,IAAI;AACrC,gBAAM,cAAc,yBAAyB,IAAI;AACjD,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,SAAS,WAAW;AAAA,cACpB,gBAAgB,eAAe,KAAK;AAAA,cACpC,kBAAkB,MAAM,cAAc,CAAC,YAAa,YAAY,KAAK,KAAK,OAAO,KAAK,EAAG;AAAA,cACzF,UAAU,MAAM,mBAAmB,KAAK,EAAE;AAAA,cAC1C,kBAAkB,CAAC,aAAa,qBAAqB,KAAK,IAAI,QAAQ;AAAA,cACtE,aAAa,MAAM;AAAE,8BAAc,UAAU,KAAK;AAAA,cAAG;AAAA,cACrD,WAAW,MAAM;AAAE,8BAAc,UAAU;AAAA,cAAK;AAAA,cAChD,QAAQ,CAAC,UAAU;AACjB,sBAAM,SAAS,MAAM,aAAa,QAAQ,YAAY,KAAK,cAAc;AACzE,8BAAc,QAAQ,KAAK,EAAE;AAC7B,8BAAc,UAAU;AAAA,cAC1B;AAAA,cACA,aAAa,MAAM;AAAA,cAAC;AAAA,cACpB,aAAa,MAAM;AAAA,cAAC;AAAA,cACpB,WAAW,UAAU,KAAK,IAAI;AAAA;AAAA,YApBzB,KAAK;AAAA,UAqBZ;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH,GACA;AAAA,IAEC,OAAO,WAAW,KACjB,oBAAC,SAAI,WAAU,8FACZ,WAAC,uBACE;AAAA,MACA;AAAA,MACA;AAAA,IACF,IACE,eAAe,EAAE,8BAA8B,IAAI,EAAE,0BAA0B,GACrF;AAAA,IAGF,oBAAC,iBAAc,QAAQ,sBAAsB,SAAS,kBAAkB;AAAA,KAC1E;AAEJ;AAqBA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAAA;AACF,GAA6B;AAC3B,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA8B,IAAI;AACpE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,CAAC;AACxD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AAExD,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,eAAW,IAAI;AACf,iBAAa,IAAI;AACjB,8BAA0B,KAAK,SAAS,EACrC,KAAK,CAAC,WAAW;AAChB,UAAI,UAAW;AACf,UAAI,CAAC,QAAQ;AACX,kBAAU,IAAI;AACd,qBAAa,EAAE,4BAA4B,CAAC;AAC5C,mBAAW,KAAK;AAChB;AAAA,MACF;AACA,gBAAU,MAAM;AAChB,iBAAW,KAAK;AAAA,IAClB,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,UAAI,UAAW;AACf,aAAO,MAAM,gCAAgC,EAAE,IAAI,CAAC;AACpD,mBAAa,EAAE,4BAA4B,CAAC;AAC5C,iBAAW,KAAK;AAAA,IAClB,CAAC;AACH,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,KAAK,WAAW,CAAC,CAAC;AAEtB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAK,iBAAiB;AACzB,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,KAAK,eAAe,CAAC;AAEzB,QAAM,UAAU,MAAM;AACpB,QAAI,gBAAgB;AAClB,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,cAAc,CAAC;AAEnB,QAAM,UAAU,MAAM;AACpB,QAAI,WAAW;AACb,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,2BAA2B,MAAM,YAAY,CAAC,SAAkB;AACpE,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,QAAI,WAAW,CAAC,CAAC,UAAW;AAC5B,kBAAc,IAAI;AAClB,oBAAgB,CAAC,YAAY,UAAU,CAAC;AAAA,EAC1C,GAAG,CAAC,WAAW,OAAO,CAAC;AAEvB,QAAM,mBAAmB,MAAM,QAAQ,MAAM;AAC3C,UAAM,MAAM,KAAK,YAAY,KAAK,mBAAmB;AACrD,QAAI,QAAQ,iBAAiB;AAC3B,UAAI;AACF,eAAO,OAAO,gBAAgB,GAAG;AAAA,MACnC,SAAS,KAAK;AACZ,eAAO,KAAK,qCAAqC,EAAE,IAAI,CAAC;AACxD,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,KAAK,UAAU,KAAK,iBAAiB,MAAM,CAAC;AAEhD,QAAM,uBAAuB,MAAM,YAAY,CAAC,SAAkB;AAChE,QAAI,MAAM;AACV,QAAI,QAAQ,mBAAmB;AAC7B,UAAI;AACF,cAAM,OAAO,kBAAkB,IAAa;AAAA,MAC9C,SAAS,KAAK;AACZ,eAAO,KAAK,uCAAuC,EAAE,IAAI,CAAC;AAAA,MAC5D;AAAA,IACF;AACA,qBAAiB,GAAG;AAAA,EACtB,GAAG,CAAC,QAAQ,gBAAgB,CAAC;AAE7B,QAAM,kBAAkB,QAAQ;AAChC,QAAM,OAAO,iBAAiB,aAAa;AAE3C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,aAAa,0CAA0C;AAAA,QACvD,UAAU,gBAAgB;AAAA,QAC1BA;AAAA,MACF;AAAA,MACA,WAAW;AAAA,MACX,aAAa,CAAC,UAAU;AACtB,YAAI,CAAC,QAAS;AACd,cAAM,aAAa,gBAAgB;AACnC,cAAM,aAAa,QAAQ,cAAc,KAAK,EAAE;AAChD,oBAAY;AAAA,MACd;AAAA,MACA,WAAW,MAAM;AACf,YAAI,CAAC,QAAS;AACd,kBAAU;AAAA,MACZ;AAAA,MACA,YAAY,CAAC,UAAU;AACrB,YAAI,CAAC,QAAS;AACd,cAAM,eAAe;AACrB,cAAM,gBAAgB;AACtB,cAAM,aAAa,aAAa;AAChC,YAAI,CAAC,YAAY;AACf,wBAAc,IAAI;AAClB,sBAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,QAAQ,CAAC,UAAU;AACjB,YAAI,CAAC,QAAS;AACd,cAAM,eAAe;AACrB,cAAM,gBAAgB;AACtB,eAAO,KAAK;AACZ,sBAAc,KAAK;AACnB,oBAAY;AAAA,MACd;AAAA,MACA,aAAa,CAAC,UAAU;AACtB,YAAI,CAAC,QAAS;AACd,cAAM,gBAAgB;AACtB,YAAI,MAAM,cAAc,SAAS,MAAM,aAAqB,EAAG;AAC/D,sBAAc,KAAK;AACnB,oBAAY;AAAA,MACd;AAAA,MAEA;AAAA,6BAAC,SAAI,WAAU,8DACb;AAAA,+BAAC,SAAI,WAAU,2BACZ;AAAA,uBAAW,oBAAC,gBAAa,WAAU,iCAAgC;AAAA,YACpE,qBAAC,SACC;AAAA,kCAAC,SAAI,WAAU,oCAAoC,iBAAM;AAAA,cACxD,cAAc,oBAAC,SAAI,WAAU,sCAAsC,uBAAY,IAAS;AAAA,eAC3F;AAAA,aACF;AAAA,UACA,qBAAC,SAAI,WAAU,2BACZ;AAAA,aAAC,WAAW,KAAK,mBAChB;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,UAAU,cAAc,WAAW,CAAC,CAAC;AAAA,gBACrC,SAAS;AAAA,gBACT,cAAY,EAAE,0BAA0B;AAAA,gBAEvC,uBAAa,oBAAC,WAAQ,WAAU,wBAAuB,IAAK,oBAAC,aAAU,WAAU,WAAU;AAAA;AAAA,YAC9F;AAAA,YAED,WACC,iCACE;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,iBAAiB,YAAY;AAAA,kBACtC,MAAK;AAAA,kBACL,SAAS;AAAA,kBACT,cAAY,iBAAiB,EAAE,gCAAgC,IAAI,EAAE,+BAA+B;AAAA,kBAEnG,2BAAiB,oBAAC,KAAE,WAAU,WAAU,IAAK,oBAAC,aAAU,WAAU,WAAU;AAAA;AAAA,cAC/E;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,SAAS;AAAA,kBACT,cAAY,EAAE,yBAAyB;AAAA,kBAEvC,8BAAC,UAAO,WAAU,WAAU;AAAA;AAAA,cAC9B;AAAA,eACF;AAAA,aAEJ;AAAA,WACF;AAAA,QACA,qBAAC,SAAI,WAAU,cACZ;AAAA,qBACC,oBAAC,SAAI,WAAU,yDACb,8BAAC,WAAQ,GACX;AAAA,UAED,aAAa,CAAC,WACb,oBAAC,SAAI,WAAU,iCAAiC,qBAAU;AAAA,UAE3D,CAAC,WAAW,CAAC,aAAa,mBACzB;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,QAAQ;AAAA,cACR,SAAS,WAAW,EAAE,QAAQ,IAAI,UAAU,MAAM,gBAAgB,MAAM,UAAU,MAAM,WAAW,MAAM,WAAW,KAAK;AAAA,cACzH,UAAU;AAAA,cACV,kBAAkB;AAAA,cAClB;AAAA,cACA,sBAAsB;AAAA;AAAA,UACxB;AAAA,WAEJ;AAAA;AAAA;AAAA,EACF;AAEJ;",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { getDashboardWidgets, loadDashboardWidgetModule } from './widgetRegistry'\nimport type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { GripVertical, Plus, RefreshCw, Settings2, Trash2, X, Loader2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { InjectionSpot } from '../injection/InjectionSpot'\nimport { WidgetDataBatchProvider } from './widgetData'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'DashboardScreen' })\n\ntype DashboardWidgetSize = 'sm' | 'md' | 'lg'\n\ntype LayoutItem = {\n id: string\n widgetId: string\n order: number\n priority?: number\n size?: DashboardWidgetSize\n settings?: unknown\n}\n\ntype WidgetMeta = {\n id: string\n title: string\n description: string | null\n defaultSize: DashboardWidgetSize\n defaultEnabled: boolean\n defaultSettings: unknown\n features: string[]\n moduleId: string\n icon: string | null\n loaderKey: string\n supportsRefresh: boolean\n}\n\ntype LayoutContext = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n userName: string | null\n userEmail: string | null\n userLabel: string | null\n}\n\ntype LayoutResponse = {\n layout: { items: LayoutItem[] }\n widgets: WidgetMeta[]\n allowedWidgetIds: string[]\n canConfigure: boolean\n context: LayoutContext\n}\n\ntype WidgetModule = DashboardWidgetModule<any>\n\nfunction sizeClass(size: DashboardWidgetSize | undefined) {\n switch (size) {\n case 'lg':\n return 'md:col-span-2'\n case 'md':\n return 'md:col-span-1'\n case 'sm':\n default:\n return 'md:col-span-1'\n }\n}\n\nfunction sortLayout(items: LayoutItem[]): LayoutItem[] {\n return [...items]\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, index) => ({ ...item, order: index, priority: index }))\n}\n\nconst DEFAULT_SIZE: DashboardWidgetSize = 'md'\n\nfunction generateId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n // Fallback: timestamp + random for better uniqueness\n return Date.now().toString(36) + Math.random().toString(36).slice(2)\n}\n\nexport function DashboardScreen() {\n const t = useT()\n const tRef = React.useRef(t)\n tRef.current = t\n const organizationScopeVersion = useOrganizationScopeVersion()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [hasRegisteredWidgets, setHasRegisteredWidgets] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [layout, setLayout] = React.useState<LayoutItem[]>([])\n const [widgetCatalog, setWidgetCatalog] = React.useState<WidgetMeta[]>([])\n const [allowedWidgetIds, setAllowedWidgetIds] = React.useState<string[]>([])\n const [canConfigure, setCanConfigure] = React.useState(false)\n const [context, setContext] = React.useState<LayoutContext | null>(null)\n const [editing, setEditing] = React.useState(false)\n const [settingsId, setSettingsId] = React.useState<string | null>(null)\n const pendingOpsRef = React.useRef(0)\n const saveQueueRef = React.useRef(Promise.resolve())\n const layoutRevisionRef = React.useRef(0)\n const pendingLayoutRevisionsRef = React.useRef(new Set<number>())\n const draggingIdRef = React.useRef<string | null>(null)\n\n const adjustSaving = React.useCallback((delta: number) => {\n pendingOpsRef.current = Math.max(0, pendingOpsRef.current + delta)\n setSaving(pendingOpsRef.current > 0)\n }, [])\n\n const load = React.useCallback(async () => {\n const layoutRevisionAtStart = layoutRevisionRef.current\n const hadPendingLayoutMutationAtStart = pendingLayoutRevisionsRef.current.size > 0\n setLoading(true)\n setError(null)\n try {\n const call = await apiCall<LayoutResponse>('/api/dashboards/layout')\n if (!call.ok || !call.result) {\n throw new Error(`Failed with status ${call.status}`)\n }\n const data = call.result\n const registeredWidgetCount = getDashboardWidgets().length\n const normalizedLayout = sortLayout(data.layout?.items ?? [])\n if (\n !hadPendingLayoutMutationAtStart\n && layoutRevisionAtStart === layoutRevisionRef.current\n ) {\n setLayout(normalizedLayout)\n }\n setWidgetCatalog(data.widgets ?? [])\n setHasRegisteredWidgets(registeredWidgetCount > 0 || (data.widgets ?? []).length > 0)\n setAllowedWidgetIds(data.allowedWidgetIds ?? [])\n setCanConfigure(!!data.canConfigure)\n if (data.context) {\n setContext({\n userId: data.context.userId,\n tenantId: data.context.tenantId ?? null,\n organizationId: data.context.organizationId ?? null,\n userName: data.context.userName ?? null,\n userEmail: data.context.userEmail ?? null,\n userLabel: data.context.userLabel ?? null,\n })\n } else {\n setContext(null)\n }\n if (!data.canConfigure) {\n setEditing(false)\n setSettingsId(null)\n }\n } catch (err) {\n logger.error('Failed to load dashboard layout', { err })\n if (getDashboardWidgets().length === 0) {\n setHasRegisteredWidgets(false)\n setLayout([])\n setWidgetCatalog([])\n setAllowedWidgetIds([])\n setCanConfigure(false)\n setContext(null)\n setEditing(false)\n setSettingsId(null)\n return\n }\n setError(tRef.current('dashboard.loadError'))\n } finally {\n setLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n load()\n }, [load, organizationScopeVersion])\n\n const metaById = React.useMemo(() => {\n const map = new Map<string, WidgetMeta>()\n for (const meta of widgetCatalog) map.set(meta.id, meta)\n return map\n }, [widgetCatalog])\n\n const availableWidgets = React.useMemo(() => {\n const currentIds = new Set(layout.map((item) => item.widgetId))\n return widgetCatalog.filter((meta) => !currentIds.has(meta.id))\n }, [layout, widgetCatalog])\n\n const resolveWidgetTitle = React.useCallback((meta: WidgetMeta): string => {\n const keys = [\n `${meta.id}.title`,\n `dashboard.widgets.${meta.id}.title`,\n ]\n if (meta.id.includes('.')) {\n const parts = meta.id.split('.')\n const lastPart = parts.pop()\n keys.unshift(`${parts.join('.')}.widgets.${lastPart}.title`)\n }\n for (const key of keys) {\n const translated = t(key)\n if (translated !== key) return translated\n }\n return meta.title\n }, [t])\n\n const resolveWidgetDescription = React.useCallback((meta: WidgetMeta): string | null => {\n if (!meta.description) return null\n const keys = [\n `${meta.id}.description`,\n `dashboard.widgets.${meta.id}.description`,\n ]\n if (meta.id.includes('.')) {\n const parts = meta.id.split('.')\n const lastPart = parts.pop()\n keys.unshift(`${parts.join('.')}.widgets.${lastPart}.description`)\n }\n for (const key of keys) {\n const translated = t(key)\n if (translated !== key) return translated\n }\n return meta.description\n }, [t])\n\n const queueLayoutSave = React.useCallback((items: LayoutItem[]) => {\n const layoutRevision = ++layoutRevisionRef.current\n pendingLayoutRevisionsRef.current.add(layoutRevision)\n saveQueueRef.current = saveQueueRef.current.then(async () => {\n adjustSaving(1)\n try {\n const payload = {\n items: items.map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings ?? null,\n })),\n }\n const call = await apiCall('/api/dashboards/layout', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n if (!call.ok) throw new Error(`Failed with status ${call.status}`)\n setError(null)\n } catch (err) {\n logger.error('Failed to save layout', { err })\n setError(t('dashboard.saveError'))\n } finally {\n pendingLayoutRevisionsRef.current.delete(layoutRevision)\n adjustSaving(-1)\n }\n })\n }, [adjustSaving, t])\n\n const patchWidgetSettings = React.useCallback(async (\n itemId: string,\n nextSettings: unknown,\n layoutRevision: number,\n ) => {\n adjustSaving(1)\n try {\n const call = await apiCall(`/api/dashboards/layout/${encodeURIComponent(itemId)}`, {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ settings: nextSettings }),\n })\n if (!call.ok) throw new Error(`Failed with status ${call.status}`)\n setError(null)\n } catch (err) {\n logger.error('Failed to update widget settings', { err })\n setError(t('dashboard.saveError'))\n } finally {\n pendingLayoutRevisionsRef.current.delete(layoutRevision)\n adjustSaving(-1)\n }\n }, [adjustSaving, t])\n\n const handleAddWidget = React.useCallback((widgetId: string) => {\n const meta = metaById.get(widgetId)\n if (!meta) return\n setLayout((prev) => {\n const next: LayoutItem[] = sortLayout([\n ...prev,\n {\n id: generateId(),\n widgetId: meta.id,\n order: prev.length,\n priority: prev.length,\n size: meta.defaultSize ?? DEFAULT_SIZE,\n settings: meta.defaultSettings ?? null,\n },\n ])\n queueLayoutSave(next)\n return next\n })\n setSettingsId(null)\n }, [metaById, queueLayoutSave])\n\n const handleRemoveWidget = React.useCallback((itemId: string) => {\n setLayout((prev) => {\n const next = sortLayout(prev.filter((item) => item.id !== itemId))\n queueLayoutSave(next)\n return next\n })\n if (settingsId === itemId) setSettingsId(null)\n }, [queueLayoutSave, settingsId])\n\n const handleReorder = React.useCallback((dragId: string | null, targetId: string) => {\n if (!dragId || dragId === targetId) return\n setLayout((prev) => {\n const items = [...prev]\n const from = items.findIndex((item) => item.id === dragId)\n const to = items.findIndex((item) => item.id === targetId)\n if (from === -1 || to === -1) return prev\n const [moved] = items.splice(from, 1)\n items.splice(to, 0, moved)\n const next = items.map((item, index) => ({\n ...item,\n order: index,\n priority: index,\n }))\n queueLayoutSave(next)\n return next\n })\n }, [queueLayoutSave])\n\n const handleSettingsChange = React.useCallback((itemId: string, nextSettings: unknown) => {\n const layoutRevision = ++layoutRevisionRef.current\n pendingLayoutRevisionsRef.current.add(layoutRevision)\n setLayout((prev) => prev.map((item) => (item.id === itemId ? { ...item, settings: nextSettings } : item)))\n void patchWidgetSettings(itemId, nextSettings, layoutRevision)\n }, [patchWidgetSettings])\n\n const toggleEditing = React.useCallback(() => {\n if (!canConfigure) return\n setEditing((prev) => {\n const next = !prev\n if (!next) setSettingsId(null)\n return next\n })\n }, [canConfigure])\n\n const handleRefresh = React.useCallback(() => {\n load()\n }, [load])\n\n const injectionContext = React.useMemo(\n () => ({\n layout,\n widgetCatalog,\n allowedWidgetIds,\n canConfigure,\n editing,\n userContext: context,\n }),\n [allowedWidgetIds, canConfigure, context, editing, layout, widgetCatalog],\n )\n const dashboardBeforeSpotId = 'dashboard:before'\n const dashboardAfterSpotId = 'dashboard:after'\n\n if (loading) {\n return (\n <div className=\"flex min-h-[320px] items-center justify-center\">\n <Spinner size=\"lg\" />\n </div>\n )\n }\n\n if (error && layout.length === 0) {\n return (\n <Alert status=\"error\">\n <AlertTitle>{t('dashboard.unavailable')}</AlertTitle>\n <AlertDescription>{error}</AlertDescription>\n <div className=\"mt-2\"><Button variant=\"outline\" onClick={handleRefresh}>{t('dashboard.retry')}</Button></div>\n </Alert>\n )\n }\n\n if (!hasRegisteredWidgets && layout.length === 0) {\n return (\n <Alert status=\"information\">\n <AlertTitle>{t('dashboard.empty.noWidgets.title', 'No dashboard widgets yet')}</AlertTitle>\n <AlertDescription>\n {t(\n 'dashboard.empty.noWidgets.description',\n 'After you add the first module that exposes dashboard widgets, they will appear here.',\n )}\n </AlertDescription>\n </Alert>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold tracking-tight\">{t('dashboard.title')}</h1>\n <p className=\"text-sm text-muted-foreground\">{t('dashboard.subtitle')}</p>\n </div>\n <div className=\"flex items-center gap-2\">\n {saving && (\n <div className=\"flex items-center gap-1 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n <span>{t('dashboard.saving')}</span>\n </div>\n )}\n {canConfigure && (\n <Button variant={editing ? 'secondary' : 'outline'} size=\"sm\" onClick={toggleEditing}>\n <Settings2 className=\"h-4 w-4\" />\n <span>{editing ? t('dashboard.action.done') : t('dashboard.action.customize')}</span>\n </Button>\n )}\n </div>\n </div>\n\n {error && layout.length > 0 && (\n <Alert status=\"error\">\n <AlertTitle>{t('dashboard.error.partial')}</AlertTitle>\n <AlertDescription>{error}</AlertDescription>\n <div className=\"mt-2\"><Button variant=\"ghost\" onClick={handleRefresh}>{t('dashboard.error.reload')}</Button></div>\n </Alert>\n )}\n\n <InjectionSpot spotId={dashboardBeforeSpotId} context={injectionContext} />\n\n {editing && availableWidgets.length > 0 && (\n <div className=\"rounded-lg border border-dashed bg-muted/50 p-4\">\n <div className=\"mb-2 text-sm font-medium text-muted-foreground\">{t('dashboard.addWidget')}</div>\n <div className=\"flex flex-wrap gap-2\">\n {availableWidgets.map((meta) => (\n <Button\n key={meta.id}\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleAddWidget(meta.id)}\n >\n <Plus className=\"h-4 w-4\" />\n {resolveWidgetTitle(meta)}\n </Button>\n ))}\n </div>\n </div>\n )}\n\n <WidgetDataBatchProvider>\n <div className={cn(\n 'grid gap-3 sm:gap-4 md:gap-6',\n 'grid-cols-1',\n 'md:grid-cols-2',\n 'xl:grid-cols-3'\n )}\n onDragOver={(event) => {\n if (!editing || !canConfigure) return\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }}\n onDrop={(event) => {\n if (!editing || !canConfigure) return\n event.preventDefault()\n const dragId = event.dataTransfer.getData('text/plain') || draggingIdRef.current\n if (!dragId) return\n setLayout((prev) => {\n const items = [...prev]\n const from = items.findIndex((entry) => entry.id === dragId)\n if (from === -1) return prev\n const [moved] = items.splice(from, 1)\n items.push(moved)\n const next = items.map((item, index) => ({\n ...item,\n order: index,\n priority: index,\n }))\n queueLayoutSave(next)\n return next\n })\n draggingIdRef.current = null\n }}>\n {layout.map((item) => {\n const meta = metaById.get(item.widgetId)\n if (!meta) return null\n const title = resolveWidgetTitle(meta)\n const description = resolveWidgetDescription(meta)\n return (\n <DashboardWidgetCard\n key={item.id}\n item={item}\n meta={meta}\n title={title}\n description={description}\n context={context}\n editing={editing && canConfigure}\n activeSettings={settingsId === item.id}\n onToggleSettings={() => setSettingsId((current) => (current === item.id ? null : item.id))}\n onRemove={() => handleRemoveWidget(item.id)}\n onSettingsChange={(settings) => handleSettingsChange(item.id, settings)}\n onDragStart={() => { draggingIdRef.current = item.id }}\n onDragEnd={() => { draggingIdRef.current = null }}\n onDrop={(event) => {\n const dragId = event.dataTransfer.getData('text/plain') || draggingIdRef.current\n handleReorder(dragId, item.id)\n draggingIdRef.current = null\n }}\n onDragEnter={() => {}}\n onDragLeave={() => {}}\n sizeClass={sizeClass(item.size)}\n />\n )\n })}\n </div>\n </WidgetDataBatchProvider>\n\n {layout.length === 0 && (\n <div className=\"rounded-lg border border-dashed bg-muted/30 p-10 text-center text-sm text-muted-foreground\">\n {!hasRegisteredWidgets\n ? t(\n 'dashboard.empty.noWidgets.description',\n 'After you add the first module that exposes dashboard widgets, they will appear here.',\n )\n : canConfigure ? t('dashboard.empty.configurable') : t('dashboard.empty.readonly')}\n </div>\n )}\n\n <InjectionSpot spotId={dashboardAfterSpotId} context={injectionContext} />\n </div>\n )\n}\n\ntype DashboardWidgetCardProps = {\n item: LayoutItem\n meta: WidgetMeta\n title: string\n description: string | null\n context: LayoutContext | null\n editing: boolean\n activeSettings: boolean\n onToggleSettings: () => void\n onRemove: () => void\n onSettingsChange: (next: unknown) => void\n onDragStart: () => void\n onDragEnd: () => void\n onDrop: (event: React.DragEvent<HTMLDivElement>) => void\n onDragEnter: () => void\n onDragLeave: () => void\n sizeClass: string\n}\n\nfunction DashboardWidgetCard({\n item,\n meta,\n title,\n description,\n context,\n editing,\n activeSettings,\n onToggleSettings,\n onRemove,\n onSettingsChange,\n onDragStart,\n onDragEnd,\n onDrop,\n onDragEnter,\n onDragLeave,\n sizeClass,\n}: DashboardWidgetCardProps) {\n const t = useT()\n const [module, setModule] = React.useState<WidgetModule | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [isDragOver, setIsDragOver] = React.useState(false)\n const [refreshToken, setRefreshToken] = React.useState(0)\n const [refreshing, setRefreshing] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n setLoading(true)\n setLoadError(null)\n loadDashboardWidgetModule(meta.loaderKey)\n .then((loaded) => {\n if (cancelled) return\n if (!loaded) {\n setModule(null)\n setLoadError(t('dashboard.widget.loadError'))\n setLoading(false)\n return\n }\n setModule(loaded)\n setLoading(false)\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('Failed to load widget module', { err })\n setLoadError(t('dashboard.widget.loadError'))\n setLoading(false)\n })\n return () => { cancelled = true }\n }, [meta.loaderKey, t])\n\n React.useEffect(() => {\n if (!meta.supportsRefresh) {\n setRefreshing(false)\n }\n }, [meta.supportsRefresh])\n\n React.useEffect(() => {\n if (activeSettings) {\n setRefreshing(false)\n }\n }, [activeSettings])\n\n React.useEffect(() => {\n if (loadError) {\n setRefreshing(false)\n }\n }, [loadError])\n\n const handleRefreshStateChange = React.useCallback((next: boolean) => {\n setRefreshing(next)\n }, [])\n\n const triggerRefresh = React.useCallback(() => {\n if (loading || !!loadError) return\n setRefreshing(true)\n setRefreshToken((current) => current + 1)\n }, [loadError, loading])\n\n const hydratedSettings = React.useMemo(() => {\n const raw = item.settings ?? meta.defaultSettings ?? null\n if (module?.hydrateSettings) {\n try {\n return module.hydrateSettings(raw)\n } catch (err) {\n logger.warn('Failed to hydrate widget settings', { err })\n return raw\n }\n }\n return raw\n }, [item.settings, meta.defaultSettings, module])\n\n const handleSettingsChange = React.useCallback((next: unknown) => {\n let raw = next\n if (module?.dehydrateSettings) {\n try {\n raw = module.dehydrateSettings(next as never)\n } catch (err) {\n logger.warn('Failed to dehydrate widget settings', { err })\n }\n }\n onSettingsChange(raw)\n }, [module, onSettingsChange])\n\n const WidgetComponent = module?.Widget\n const mode = activeSettings ? 'settings' : 'view'\n\n return (\n <div\n className={cn(\n 'group relative flex h-full flex-col rounded-lg border bg-background shadow-sm transition',\n isDragOver ? 'border-primary ring-2 ring-primary/20' : 'hover:border-primary/40',\n editing ? 'cursor-grab' : 'cursor-default',\n sizeClass\n )}\n draggable={editing}\n onDragStart={(event) => {\n if (!editing) return\n event.dataTransfer.effectAllowed = 'move'\n event.dataTransfer.setData('text/plain', item.id)\n onDragStart()\n }}\n onDragEnd={() => {\n if (!editing) return\n onDragEnd()\n }}\n onDragOver={(event) => {\n if (!editing) return\n event.preventDefault()\n event.stopPropagation()\n event.dataTransfer.dropEffect = 'move'\n if (!isDragOver) {\n setIsDragOver(true)\n onDragEnter()\n }\n }}\n onDrop={(event) => {\n if (!editing) return\n event.preventDefault()\n event.stopPropagation()\n onDrop(event)\n setIsDragOver(false)\n onDragLeave()\n }}\n onDragLeave={(event) => {\n if (!editing) return\n event.stopPropagation()\n if (event.currentTarget.contains(event.relatedTarget as Node)) return\n setIsDragOver(false)\n onDragLeave()\n }}\n >\n <div className=\"flex items-center justify-between gap-2 border-b px-3 py-2\">\n <div className=\"flex items-center gap-2\">\n {editing && <GripVertical className=\"h-4 w-4 text-muted-foreground\" />}\n <div>\n <div className=\"text-sm font-medium leading-none\">{title}</div>\n {description ? <div className=\"mt-1 text-xs text-muted-foreground\">{description}</div> : null}\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n {!editing && meta.supportsRefresh && (\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n disabled={refreshing || loading || !!loadError}\n onClick={triggerRefresh}\n aria-label={t('dashboard.widget.refresh')}\n >\n {refreshing ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <RefreshCw className=\"h-4 w-4\" />}\n </IconButton>\n )}\n {editing && (\n <>\n <IconButton\n variant={activeSettings ? 'outline' : 'ghost'}\n size=\"sm\"\n onClick={onToggleSettings}\n aria-label={activeSettings ? t('dashboard.widget.closeSettings') : t('dashboard.widget.editSettings')}\n >\n {activeSettings ? <X className=\"h-4 w-4\" /> : <Settings2 className=\"h-4 w-4\" />}\n </IconButton>\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n onClick={onRemove}\n aria-label={t('dashboard.widget.remove')}\n >\n <Trash2 className=\"h-4 w-4\" />\n </IconButton>\n </>\n )}\n </div>\n </div>\n <div className=\"flex-1 p-4\">\n {loading && (\n <div className=\"flex h-full min-h-[120px] items-center justify-center\">\n <Spinner />\n </div>\n )}\n {loadError && !loading && (\n <div className=\"text-sm text-muted-foreground\">{loadError}</div>\n )}\n {!loading && !loadError && WidgetComponent && (\n <WidgetComponent\n mode={mode as 'view' | 'settings'}\n layout={item}\n context={context ?? { userId: '', tenantId: null, organizationId: null, userName: null, userEmail: null, userLabel: null }}\n settings={hydratedSettings}\n onSettingsChange={handleSettingsChange}\n refreshToken={refreshToken}\n onRefreshStateChange={handleRefreshStateChange}\n />\n )}\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAqXQ,SAoWI,UApWJ,KAOF,YAPE;AAnXR,YAAY,WAAW;AACvB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,OAAO,kBAAkB,kBAAkB;AACpD,SAAS,eAAe;AACxB,SAAS,qBAAqB,iCAAiC;AAE/D,SAAS,UAAU;AACnB,SAAS,cAAc,MAAM,WAAW,WAAW,QAAQ,GAAG,eAAe;AAC7E,SAAS,YAAY;AACrB,SAAS,mCAAmC;AAC5C,SAAS,qBAAqB;AAC9B,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,kBAAkB,CAAC;AA8CxE,SAAS,UAAU,MAAuC;AACxD,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,WAAW,OAAmC;AACrD,SAAO,CAAC,GAAG,KAAK,EACb,KAAK,CAAC,GAAG,MAAM;AACd,UAAM,SAAS,EAAE,SAAS,EAAE,YAAY;AACxC,UAAM,SAAS,EAAE,SAAS,EAAE,YAAY;AACxC,WAAO,SAAS;AAAA,EAClB,CAAC,EACA,IAAI,CAAC,MAAM,WAAW,EAAE,GAAG,MAAM,OAAO,OAAO,UAAU,MAAM,EAAE;AACtE;AAEA,MAAM,eAAoC;AAE1C,SAAS,aAAqB;AAC5B,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,SAAO,KAAK,IAAI,EAAE,SAAS,EAAE,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC;AACrE;AAEO,SAAS,kBAAkB;AAChC,QAAM,IAAI,KAAK;AACf,QAAM,OAAO,MAAM,OAAO,CAAC;AAC3B,OAAK,UAAU;AACf,QAAM,2BAA2B,4BAA4B;AAC7D,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,MAAM,SAAS,IAAI;AAC3E,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAuB,CAAC,CAAC;AAC3D,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAuB,CAAC,CAAC;AACzE,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAmB,CAAC,CAAC;AAC3E,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,KAAK;AAC5D,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAA+B,IAAI;AACvE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAwB,IAAI;AACtE,QAAM,gBAAgB,MAAM,OAAO,CAAC;AACpC,QAAM,eAAe,MAAM,OAAO,QAAQ,QAAQ,CAAC;AACnD,QAAM,oBAAoB,MAAM,OAAO,CAAC;AACxC,QAAM,4BAA4B,MAAM,OAAO,oBAAI,IAAY,CAAC;AAChE,QAAM,gBAAgB,MAAM,OAAsB,IAAI;AAEtD,QAAM,eAAe,MAAM,YAAY,CAAC,UAAkB;AACxD,kBAAc,UAAU,KAAK,IAAI,GAAG,cAAc,UAAU,KAAK;AACjE,cAAU,cAAc,UAAU,CAAC;AAAA,EACrC,GAAG,CAAC,CAAC;AAEL,QAAM,OAAO,MAAM,YAAY,YAAY;AACzC,UAAM,wBAAwB,kBAAkB;AAChD,UAAM,kCAAkC,0BAA0B,QAAQ,OAAO;AACjF,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,OAAO,MAAM,QAAwB,wBAAwB;AACnE,UAAI,CAAC,KAAK,MAAM,CAAC,KAAK,QAAQ;AAC5B,cAAM,IAAI,MAAM,sBAAsB,KAAK,MAAM,EAAE;AAAA,MACrD;AACA,YAAM,OAAO,KAAK;AAClB,YAAM,wBAAwB,oBAAoB,EAAE;AACpD,YAAM,mBAAmB,WAAW,KAAK,QAAQ,SAAS,CAAC,CAAC;AAC5D,UACE,CAAC,mCACE,0BAA0B,kBAAkB,SAC/C;AACA,kBAAU,gBAAgB;AAAA,MAC5B;AACA,uBAAiB,KAAK,WAAW,CAAC,CAAC;AACnC,8BAAwB,wBAAwB,MAAM,KAAK,WAAW,CAAC,GAAG,SAAS,CAAC;AACpF,0BAAoB,KAAK,oBAAoB,CAAC,CAAC;AAC/C,sBAAgB,CAAC,CAAC,KAAK,YAAY;AACnC,UAAI,KAAK,SAAS;AAChB,mBAAW;AAAA,UACT,QAAQ,KAAK,QAAQ;AAAA,UACrB,UAAU,KAAK,QAAQ,YAAY;AAAA,UACnC,gBAAgB,KAAK,QAAQ,kBAAkB;AAAA,UAC/C,UAAU,KAAK,QAAQ,YAAY;AAAA,UACnC,WAAW,KAAK,QAAQ,aAAa;AAAA,UACrC,WAAW,KAAK,QAAQ,aAAa;AAAA,QACvC,CAAC;AAAA,MACH,OAAO;AACL,mBAAW,IAAI;AAAA,MACjB;AACA,UAAI,CAAC,KAAK,cAAc;AACtB,mBAAW,KAAK;AAChB,sBAAc,IAAI;AAAA,MACpB;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,mCAAmC,EAAE,IAAI,CAAC;AACvD,UAAI,oBAAoB,EAAE,WAAW,GAAG;AACtC,gCAAwB,KAAK;AAC7B,kBAAU,CAAC,CAAC;AACZ,yBAAiB,CAAC,CAAC;AACnB,4BAAoB,CAAC,CAAC;AACtB,wBAAgB,KAAK;AACrB,mBAAW,IAAI;AACf,mBAAW,KAAK;AAChB,sBAAc,IAAI;AAClB;AAAA,MACF;AACA,eAAS,KAAK,QAAQ,qBAAqB,CAAC;AAAA,IAC9C,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,SAAK;AAAA,EACP,GAAG,CAAC,MAAM,wBAAwB,CAAC;AAEnC,QAAM,WAAW,MAAM,QAAQ,MAAM;AACnC,UAAM,MAAM,oBAAI,IAAwB;AACxC,eAAW,QAAQ,cAAe,KAAI,IAAI,KAAK,IAAI,IAAI;AACvD,WAAO;AAAA,EACT,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,mBAAmB,MAAM,QAAQ,MAAM;AAC3C,UAAM,aAAa,IAAI,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC;AAC9D,WAAO,cAAc,OAAO,CAAC,SAAS,CAAC,WAAW,IAAI,KAAK,EAAE,CAAC;AAAA,EAChE,GAAG,CAAC,QAAQ,aAAa,CAAC;AAE1B,QAAM,qBAAqB,MAAM,YAAY,CAAC,SAA6B;AACzE,UAAM,OAAO;AAAA,MACX,GAAG,KAAK,EAAE;AAAA,MACV,qBAAqB,KAAK,EAAE;AAAA,IAC9B;AACA,QAAI,KAAK,GAAG,SAAS,GAAG,GAAG;AACzB,YAAM,QAAQ,KAAK,GAAG,MAAM,GAAG;AAC/B,YAAM,WAAW,MAAM,IAAI;AAC3B,WAAK,QAAQ,GAAG,MAAM,KAAK,GAAG,CAAC,YAAY,QAAQ,QAAQ;AAAA,IAC7D;AACA,eAAW,OAAO,MAAM;AACtB,YAAM,aAAa,EAAE,GAAG;AACxB,UAAI,eAAe,IAAK,QAAO;AAAA,IACjC;AACA,WAAO,KAAK;AAAA,EACd,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,2BAA2B,MAAM,YAAY,CAAC,SAAoC;AACtF,QAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,UAAM,OAAO;AAAA,MACX,GAAG,KAAK,EAAE;AAAA,MACV,qBAAqB,KAAK,EAAE;AAAA,IAC9B;AACA,QAAI,KAAK,GAAG,SAAS,GAAG,GAAG;AACzB,YAAM,QAAQ,KAAK,GAAG,MAAM,GAAG;AAC/B,YAAM,WAAW,MAAM,IAAI;AAC3B,WAAK,QAAQ,GAAG,MAAM,KAAK,GAAG,CAAC,YAAY,QAAQ,cAAc;AAAA,IACnE;AACA,eAAW,OAAO,MAAM;AACtB,YAAM,aAAa,EAAE,GAAG;AACxB,UAAI,eAAe,IAAK,QAAO;AAAA,IACjC;AACA,WAAO,KAAK;AAAA,EACd,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,kBAAkB,MAAM,YAAY,CAAC,UAAwB;AACjE,UAAM,iBAAiB,EAAE,kBAAkB;AAC3C,8BAA0B,QAAQ,IAAI,cAAc;AACpD,iBAAa,UAAU,aAAa,QAAQ,KAAK,YAAY;AAC3D,mBAAa,CAAC;AACd,UAAI;AACF,cAAM,UAAU;AAAA,UACd,OAAO,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,YACjC,IAAI,KAAK;AAAA,YACT,UAAU,KAAK;AAAA,YACf,OAAO;AAAA,YACP,UAAU;AAAA,YACV,MAAM,KAAK,QAAQ;AAAA,YACnB,UAAU,KAAK,YAAY;AAAA,UAC7B,EAAE;AAAA,QACJ;AACA,cAAM,OAAO,MAAM,QAAQ,0BAA0B;AAAA,UACnD,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,QAC9B,CAAC;AACD,YAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,sBAAsB,KAAK,MAAM,EAAE;AACjE,iBAAS,IAAI;AAAA,MACf,SAAS,KAAK;AACZ,eAAO,MAAM,yBAAyB,EAAE,IAAI,CAAC;AAC7C,iBAAS,EAAE,qBAAqB,CAAC;AAAA,MACnC,UAAE;AACA,kCAA0B,QAAQ,OAAO,cAAc;AACvD,qBAAa,EAAE;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,cAAc,CAAC,CAAC;AAEpB,QAAM,sBAAsB,MAAM,YAAY,OAC5C,QACA,cACA,mBACG;AACH,iBAAa,CAAC;AACd,QAAI;AACF,YAAM,OAAO,MAAM,QAAQ,0BAA0B,mBAAmB,MAAM,CAAC,IAAI;AAAA,QACjF,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,aAAa,CAAC;AAAA,MACjD,CAAC;AACD,UAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,sBAAsB,KAAK,MAAM,EAAE;AACjE,eAAS,IAAI;AAAA,IACf,SAAS,KAAK;AACZ,aAAO,MAAM,oCAAoC,EAAE,IAAI,CAAC;AACxD,eAAS,EAAE,qBAAqB,CAAC;AAAA,IACnC,UAAE;AACA,gCAA0B,QAAQ,OAAO,cAAc;AACvD,mBAAa,EAAE;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,cAAc,CAAC,CAAC;AAEpB,QAAM,kBAAkB,MAAM,YAAY,CAAC,aAAqB;AAC9D,UAAM,OAAO,SAAS,IAAI,QAAQ;AAClC,QAAI,CAAC,KAAM;AACX,cAAU,CAAC,SAAS;AAClB,YAAM,OAAqB,WAAW;AAAA,QACpC,GAAG;AAAA,QACH;AAAA,UACE,IAAI,WAAW;AAAA,UACf,UAAU,KAAK;AAAA,UACf,OAAO,KAAK;AAAA,UACZ,UAAU,KAAK;AAAA,UACf,MAAM,KAAK,eAAe;AAAA,UAC1B,UAAU,KAAK,mBAAmB;AAAA,QACpC;AAAA,MACF,CAAC;AACD,sBAAgB,IAAI;AACpB,aAAO;AAAA,IACT,CAAC;AACD,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,UAAU,eAAe,CAAC;AAE9B,QAAM,qBAAqB,MAAM,YAAY,CAAC,WAAmB;AAC/D,cAAU,CAAC,SAAS;AAClB,YAAM,OAAO,WAAW,KAAK,OAAO,CAAC,SAAS,KAAK,OAAO,MAAM,CAAC;AACjE,sBAAgB,IAAI;AACpB,aAAO;AAAA,IACT,CAAC;AACD,QAAI,eAAe,OAAQ,eAAc,IAAI;AAAA,EAC/C,GAAG,CAAC,iBAAiB,UAAU,CAAC;AAEhC,QAAM,gBAAgB,MAAM,YAAY,CAAC,QAAuB,aAAqB;AACnF,QAAI,CAAC,UAAU,WAAW,SAAU;AACpC,cAAU,CAAC,SAAS;AAClB,YAAM,QAAQ,CAAC,GAAG,IAAI;AACtB,YAAM,OAAO,MAAM,UAAU,CAAC,SAAS,KAAK,OAAO,MAAM;AACzD,YAAM,KAAK,MAAM,UAAU,CAAC,SAAS,KAAK,OAAO,QAAQ;AACzD,UAAI,SAAS,MAAM,OAAO,GAAI,QAAO;AACrC,YAAM,CAAC,KAAK,IAAI,MAAM,OAAO,MAAM,CAAC;AACpC,YAAM,OAAO,IAAI,GAAG,KAAK;AACzB,YAAM,OAAO,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,QACvC,GAAG;AAAA,QACH,OAAO;AAAA,QACP,UAAU;AAAA,MACZ,EAAE;AACF,sBAAgB,IAAI;AACpB,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,eAAe,CAAC;AAEpB,QAAM,uBAAuB,MAAM,YAAY,CAAC,QAAgB,iBAA0B;AACxF,UAAM,iBAAiB,EAAE,kBAAkB;AAC3C,8BAA0B,QAAQ,IAAI,cAAc;AACpD,cAAU,CAAC,SAAS,KAAK,IAAI,CAAC,SAAU,KAAK,OAAO,SAAS,EAAE,GAAG,MAAM,UAAU,aAAa,IAAI,IAAK,CAAC;AACzG,SAAK,oBAAoB,QAAQ,cAAc,cAAc;AAAA,EAC/D,GAAG,CAAC,mBAAmB,CAAC;AAExB,QAAM,gBAAgB,MAAM,YAAY,MAAM;AAC5C,QAAI,CAAC,aAAc;AACnB,eAAW,CAAC,SAAS;AACnB,YAAM,OAAO,CAAC;AACd,UAAI,CAAC,KAAM,eAAc,IAAI;AAC7B,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,gBAAgB,MAAM,YAAY,MAAM;AAC5C,SAAK;AAAA,EACP,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,mBAAmB,MAAM;AAAA,IAC7B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa;AAAA,IACf;AAAA,IACA,CAAC,kBAAkB,cAAc,SAAS,SAAS,QAAQ,aAAa;AAAA,EAC1E;AACA,QAAM,wBAAwB;AAC9B,QAAM,uBAAuB;AAE7B,MAAI,SAAS;AACX,WACE,oBAAC,SAAI,WAAU,kDACb,8BAAC,WAAQ,MAAK,MAAK,GACrB;AAAA,EAEJ;AAEA,MAAI,SAAS,OAAO,WAAW,GAAG;AAChC,WACE,qBAAC,SAAM,QAAO,SACZ;AAAA,0BAAC,cAAY,YAAE,uBAAuB,GAAE;AAAA,MACxC,oBAAC,oBAAkB,iBAAM;AAAA,MACzB,oBAAC,SAAI,WAAU,QAAO,8BAAC,UAAO,SAAQ,WAAU,SAAS,eAAgB,YAAE,iBAAiB,GAAE,GAAS;AAAA,OACzG;AAAA,EAEJ;AAEA,MAAI,CAAC,wBAAwB,OAAO,WAAW,GAAG;AAChD,WACE,qBAAC,SAAM,QAAO,eACZ;AAAA,0BAAC,cAAY,YAAE,mCAAmC,0BAA0B,GAAE;AAAA,MAC9E,oBAAC,oBACE;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,qDACb;AAAA,2BAAC,SACC;AAAA,4BAAC,QAAG,WAAU,yCAAyC,YAAE,iBAAiB,GAAE;AAAA,QAC5E,oBAAC,OAAE,WAAU,iCAAiC,YAAE,oBAAoB,GAAE;AAAA,SACxE;AAAA,MACA,qBAAC,SAAI,WAAU,2BACZ;AAAA,kBACC,qBAAC,SAAI,WAAU,yDACb;AAAA,8BAAC,WAAQ,WAAU,wBAAuB;AAAA,UAC1C,oBAAC,UAAM,YAAE,kBAAkB,GAAE;AAAA,WAC/B;AAAA,QAED,gBACC,qBAAC,UAAO,SAAS,UAAU,cAAc,WAAW,MAAK,MAAK,SAAS,eACrE;AAAA,8BAAC,aAAU,WAAU,WAAU;AAAA,UAC/B,oBAAC,UAAM,oBAAU,EAAE,uBAAuB,IAAI,EAAE,4BAA4B,GAAE;AAAA,WAChF;AAAA,SAEJ;AAAA,OACF;AAAA,IAEC,SAAS,OAAO,SAAS,KACxB,qBAAC,SAAM,QAAO,SACZ;AAAA,0BAAC,cAAY,YAAE,yBAAyB,GAAE;AAAA,MAC1C,oBAAC,oBAAkB,iBAAM;AAAA,MACzB,oBAAC,SAAI,WAAU,QAAO,8BAAC,UAAO,SAAQ,SAAQ,SAAS,eAAgB,YAAE,wBAAwB,GAAE,GAAS;AAAA,OAC9G;AAAA,IAGF,oBAAC,iBAAc,QAAQ,uBAAuB,SAAS,kBAAkB;AAAA,IAExE,WAAW,iBAAiB,SAAS,KACpC,qBAAC,SAAI,WAAU,mDACb;AAAA,0BAAC,SAAI,WAAU,kDAAkD,YAAE,qBAAqB,GAAE;AAAA,MAC1F,oBAAC,SAAI,WAAU,wBACZ,2BAAiB,IAAI,CAAC,SACrB;AAAA,QAAC;AAAA;AAAA,UAEC,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM,gBAAgB,KAAK,EAAE;AAAA,UAEtC;AAAA,gCAAC,QAAK,WAAU,WAAU;AAAA,YACzB,mBAAmB,IAAI;AAAA;AAAA;AAAA,QANnB,KAAK;AAAA,MAOZ,CACD,GACH;AAAA,OACF;AAAA,IAGF,oBAAC,2BACD;AAAA,MAAC;AAAA;AAAA,QAAI,WAAW;AAAA,UACd;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,YAAY,CAAC,UAAU;AACrB,cAAI,CAAC,WAAW,CAAC,aAAc;AAC/B,gBAAM,eAAe;AACrB,gBAAM,aAAa,aAAa;AAAA,QAClC;AAAA,QACA,QAAQ,CAAC,UAAU;AACjB,cAAI,CAAC,WAAW,CAAC,aAAc;AAC/B,gBAAM,eAAe;AACrB,gBAAM,SAAS,MAAM,aAAa,QAAQ,YAAY,KAAK,cAAc;AACzE,cAAI,CAAC,OAAQ;AACb,oBAAU,CAAC,SAAS;AAClB,kBAAM,QAAQ,CAAC,GAAG,IAAI;AACtB,kBAAM,OAAO,MAAM,UAAU,CAAC,UAAU,MAAM,OAAO,MAAM;AAC3D,gBAAI,SAAS,GAAI,QAAO;AACxB,kBAAM,CAAC,KAAK,IAAI,MAAM,OAAO,MAAM,CAAC;AACpC,kBAAM,KAAK,KAAK;AAChB,kBAAM,OAAO,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,cACvC,GAAG;AAAA,cACH,OAAO;AAAA,cACP,UAAU;AAAA,YACZ,EAAE;AACF,4BAAgB,IAAI;AACpB,mBAAO;AAAA,UACT,CAAC;AACD,wBAAc,UAAU;AAAA,QAC1B;AAAA,QACG,iBAAO,IAAI,CAAC,SAAS;AACpB,gBAAM,OAAO,SAAS,IAAI,KAAK,QAAQ;AACvC,cAAI,CAAC,KAAM,QAAO;AAClB,gBAAM,QAAQ,mBAAmB,IAAI;AACrC,gBAAM,cAAc,yBAAyB,IAAI;AACjD,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,SAAS,WAAW;AAAA,cACpB,gBAAgB,eAAe,KAAK;AAAA,cACpC,kBAAkB,MAAM,cAAc,CAAC,YAAa,YAAY,KAAK,KAAK,OAAO,KAAK,EAAG;AAAA,cACzF,UAAU,MAAM,mBAAmB,KAAK,EAAE;AAAA,cAC1C,kBAAkB,CAAC,aAAa,qBAAqB,KAAK,IAAI,QAAQ;AAAA,cACtE,aAAa,MAAM;AAAE,8BAAc,UAAU,KAAK;AAAA,cAAG;AAAA,cACrD,WAAW,MAAM;AAAE,8BAAc,UAAU;AAAA,cAAK;AAAA,cAChD,QAAQ,CAAC,UAAU;AACjB,sBAAM,SAAS,MAAM,aAAa,QAAQ,YAAY,KAAK,cAAc;AACzE,8BAAc,QAAQ,KAAK,EAAE;AAC7B,8BAAc,UAAU;AAAA,cAC1B;AAAA,cACA,aAAa,MAAM;AAAA,cAAC;AAAA,cACpB,aAAa,MAAM;AAAA,cAAC;AAAA,cACpB,WAAW,UAAU,KAAK,IAAI;AAAA;AAAA,YApBzB,KAAK;AAAA,UAqBZ;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH,GACA;AAAA,IAEC,OAAO,WAAW,KACjB,oBAAC,SAAI,WAAU,8FACZ,WAAC,uBACE;AAAA,MACA;AAAA,MACA;AAAA,IACF,IACE,eAAe,EAAE,8BAA8B,IAAI,EAAE,0BAA0B,GACrF;AAAA,IAGF,oBAAC,iBAAc,QAAQ,sBAAsB,SAAS,kBAAkB;AAAA,KAC1E;AAEJ;AAqBA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAAA;AACF,GAA6B;AAC3B,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA8B,IAAI;AACpE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,CAAC;AACxD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AAExD,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,eAAW,IAAI;AACf,iBAAa,IAAI;AACjB,8BAA0B,KAAK,SAAS,EACrC,KAAK,CAAC,WAAW;AAChB,UAAI,UAAW;AACf,UAAI,CAAC,QAAQ;AACX,kBAAU,IAAI;AACd,qBAAa,EAAE,4BAA4B,CAAC;AAC5C,mBAAW,KAAK;AAChB;AAAA,MACF;AACA,gBAAU,MAAM;AAChB,iBAAW,KAAK;AAAA,IAClB,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,UAAI,UAAW;AACf,aAAO,MAAM,gCAAgC,EAAE,IAAI,CAAC;AACpD,mBAAa,EAAE,4BAA4B,CAAC;AAC5C,iBAAW,KAAK;AAAA,IAClB,CAAC;AACH,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,KAAK,WAAW,CAAC,CAAC;AAEtB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAK,iBAAiB;AACzB,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,KAAK,eAAe,CAAC;AAEzB,QAAM,UAAU,MAAM;AACpB,QAAI,gBAAgB;AAClB,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,cAAc,CAAC;AAEnB,QAAM,UAAU,MAAM;AACpB,QAAI,WAAW;AACb,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,2BAA2B,MAAM,YAAY,CAAC,SAAkB;AACpE,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,QAAI,WAAW,CAAC,CAAC,UAAW;AAC5B,kBAAc,IAAI;AAClB,oBAAgB,CAAC,YAAY,UAAU,CAAC;AAAA,EAC1C,GAAG,CAAC,WAAW,OAAO,CAAC;AAEvB,QAAM,mBAAmB,MAAM,QAAQ,MAAM;AAC3C,UAAM,MAAM,KAAK,YAAY,KAAK,mBAAmB;AACrD,QAAI,QAAQ,iBAAiB;AAC3B,UAAI;AACF,eAAO,OAAO,gBAAgB,GAAG;AAAA,MACnC,SAAS,KAAK;AACZ,eAAO,KAAK,qCAAqC,EAAE,IAAI,CAAC;AACxD,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,KAAK,UAAU,KAAK,iBAAiB,MAAM,CAAC;AAEhD,QAAM,uBAAuB,MAAM,YAAY,CAAC,SAAkB;AAChE,QAAI,MAAM;AACV,QAAI,QAAQ,mBAAmB;AAC7B,UAAI;AACF,cAAM,OAAO,kBAAkB,IAAa;AAAA,MAC9C,SAAS,KAAK;AACZ,eAAO,KAAK,uCAAuC,EAAE,IAAI,CAAC;AAAA,MAC5D;AAAA,IACF;AACA,qBAAiB,GAAG;AAAA,EACtB,GAAG,CAAC,QAAQ,gBAAgB,CAAC;AAE7B,QAAM,kBAAkB,QAAQ;AAChC,QAAM,OAAO,iBAAiB,aAAa;AAE3C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,aAAa,0CAA0C;AAAA,QACvD,UAAU,gBAAgB;AAAA,QAC1BA;AAAA,MACF;AAAA,MACA,WAAW;AAAA,MACX,aAAa,CAAC,UAAU;AACtB,YAAI,CAAC,QAAS;AACd,cAAM,aAAa,gBAAgB;AACnC,cAAM,aAAa,QAAQ,cAAc,KAAK,EAAE;AAChD,oBAAY;AAAA,MACd;AAAA,MACA,WAAW,MAAM;AACf,YAAI,CAAC,QAAS;AACd,kBAAU;AAAA,MACZ;AAAA,MACA,YAAY,CAAC,UAAU;AACrB,YAAI,CAAC,QAAS;AACd,cAAM,eAAe;AACrB,cAAM,gBAAgB;AACtB,cAAM,aAAa,aAAa;AAChC,YAAI,CAAC,YAAY;AACf,wBAAc,IAAI;AAClB,sBAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,QAAQ,CAAC,UAAU;AACjB,YAAI,CAAC,QAAS;AACd,cAAM,eAAe;AACrB,cAAM,gBAAgB;AACtB,eAAO,KAAK;AACZ,sBAAc,KAAK;AACnB,oBAAY;AAAA,MACd;AAAA,MACA,aAAa,CAAC,UAAU;AACtB,YAAI,CAAC,QAAS;AACd,cAAM,gBAAgB;AACtB,YAAI,MAAM,cAAc,SAAS,MAAM,aAAqB,EAAG;AAC/D,sBAAc,KAAK;AACnB,oBAAY;AAAA,MACd;AAAA,MAEA;AAAA,6BAAC,SAAI,WAAU,8DACb;AAAA,+BAAC,SAAI,WAAU,2BACZ;AAAA,uBAAW,oBAAC,gBAAa,WAAU,iCAAgC;AAAA,YACpE,qBAAC,SACC;AAAA,kCAAC,SAAI,WAAU,oCAAoC,iBAAM;AAAA,cACxD,cAAc,oBAAC,SAAI,WAAU,sCAAsC,uBAAY,IAAS;AAAA,eAC3F;AAAA,aACF;AAAA,UACA,qBAAC,SAAI,WAAU,2BACZ;AAAA,aAAC,WAAW,KAAK,mBAChB;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,UAAU,cAAc,WAAW,CAAC,CAAC;AAAA,gBACrC,SAAS;AAAA,gBACT,cAAY,EAAE,0BAA0B;AAAA,gBAEvC,uBAAa,oBAAC,WAAQ,WAAU,wBAAuB,IAAK,oBAAC,aAAU,WAAU,WAAU;AAAA;AAAA,YAC9F;AAAA,YAED,WACC,iCACE;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,iBAAiB,YAAY;AAAA,kBACtC,MAAK;AAAA,kBACL,SAAS;AAAA,kBACT,cAAY,iBAAiB,EAAE,gCAAgC,IAAI,EAAE,+BAA+B;AAAA,kBAEnG,2BAAiB,oBAAC,KAAE,WAAU,WAAU,IAAK,oBAAC,aAAU,WAAU,WAAU;AAAA;AAAA,cAC/E;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,SAAS;AAAA,kBACT,cAAY,EAAE,yBAAyB;AAAA,kBAEvC,8BAAC,UAAO,WAAU,WAAU;AAAA;AAAA,cAC9B;AAAA,eACF;AAAA,aAEJ;AAAA,WACF;AAAA,QACA,qBAAC,SAAI,WAAU,cACZ;AAAA,qBACC,oBAAC,SAAI,WAAU,yDACb,8BAAC,WAAQ,GACX;AAAA,UAED,aAAa,CAAC,WACb,oBAAC,SAAI,WAAU,iCAAiC,qBAAU;AAAA,UAE3D,CAAC,WAAW,CAAC,aAAa,mBACzB;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,QAAQ;AAAA,cACR,SAAS,WAAW,EAAE,QAAQ,IAAI,UAAU,MAAM,gBAAgB,MAAM,UAAU,MAAM,WAAW,MAAM,WAAW,KAAK;AAAA,cACzH,UAAU;AAAA,cACV,kBAAkB;AAAA,cAClB;AAAA,cACA,sBAAsB;AAAA;AAAA,UACxB;AAAA,WAEJ;AAAA;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": ["sizeClass"]
|
|
7
7
|
}
|
|
@@ -5,9 +5,13 @@ import { createContext, useContext } from "react";
|
|
|
5
5
|
import { registerComponentOverrides } from "@open-mercato/shared/modules/widgets/component-registry";
|
|
6
6
|
import { apiCall } from "../utils/apiCall.js";
|
|
7
7
|
const OverrideUserFeaturesContext = createContext([]);
|
|
8
|
+
const OverrideRegistryRevisionContext = createContext([]);
|
|
8
9
|
function useOverrideUserFeatures() {
|
|
9
10
|
return useContext(OverrideUserFeaturesContext);
|
|
10
11
|
}
|
|
12
|
+
function useOverrideRegistryRevision() {
|
|
13
|
+
return useContext(OverrideRegistryRevisionContext);
|
|
14
|
+
}
|
|
11
15
|
function ComponentOverrideProvider({
|
|
12
16
|
overrides,
|
|
13
17
|
children
|
|
@@ -15,6 +19,7 @@ function ComponentOverrideProvider({
|
|
|
15
19
|
const [userFeatures, setUserFeatures] = React.useState([]);
|
|
16
20
|
registerComponentOverrides(overrides);
|
|
17
21
|
React.useEffect(() => {
|
|
22
|
+
registerComponentOverrides(overrides);
|
|
18
23
|
return () => {
|
|
19
24
|
registerComponentOverrides([]);
|
|
20
25
|
};
|
|
@@ -44,12 +49,13 @@ function ComponentOverrideProvider({
|
|
|
44
49
|
cancelled = true;
|
|
45
50
|
};
|
|
46
51
|
}, [overrides]);
|
|
47
|
-
return /* @__PURE__ */ jsx(OverrideUserFeaturesContext.Provider, { value: userFeatures, children });
|
|
52
|
+
return /* @__PURE__ */ jsx(OverrideRegistryRevisionContext.Provider, { value: overrides, children: /* @__PURE__ */ jsx(OverrideUserFeaturesContext.Provider, { value: userFeatures, children }) });
|
|
48
53
|
}
|
|
49
54
|
var ComponentOverrideProvider_default = ComponentOverrideProvider;
|
|
50
55
|
export {
|
|
51
56
|
ComponentOverrideProvider,
|
|
52
57
|
ComponentOverrideProvider_default as default,
|
|
58
|
+
useOverrideRegistryRevision,
|
|
53
59
|
useOverrideUserFeatures
|
|
54
60
|
};
|
|
55
61
|
//# sourceMappingURL=ComponentOverrideProvider.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/injection/ComponentOverrideProvider.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\nimport type { ComponentOverride } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { registerComponentOverrides } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { apiCall } from '../utils/apiCall'\n\ntype FeatureCheckResponse = { granted: string[] }\n\nconst OverrideUserFeaturesContext = createContext<readonly string[]>([])\n\nexport function useOverrideUserFeatures(): readonly string[] {\n return useContext(OverrideUserFeaturesContext)\n}\n\nexport function ComponentOverrideProvider({\n overrides,\n children,\n}: {\n overrides: ComponentOverride[]\n children: React.ReactNode\n}) {\n const [userFeatures, setUserFeatures] = React.useState<readonly string[]>([])\n\n registerComponentOverrides(overrides)\n\n React.useEffect(() => {\n return () => {\n registerComponentOverrides([])\n }\n }, [overrides])\n\n React.useEffect(() => {\n const requiredFeatures = new Set<string>()\n for (const override of overrides) {\n for (const feature of override.features ?? []) {\n if (feature && feature.trim().length > 0) requiredFeatures.add(feature)\n }\n }\n if (requiredFeatures.size === 0) return\n let cancelled = false\n const fetchFeatures = async () => {\n const call = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: Array.from(requiredFeatures) }),\n })\n if (cancelled) return\n if (call.ok && call.result?.granted) {\n setUserFeatures(call.result.granted)\n }\n }\n void fetchFeatures()\n return () => {\n cancelled = true\n }\n }, [overrides])\n\n return (\n <OverrideUserFeaturesContext.Provider value={userFeatures}>\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\nimport type { ComponentOverride } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { registerComponentOverrides } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { apiCall } from '../utils/apiCall'\n\ntype FeatureCheckResponse = { granted: string[] }\n\nconst OverrideUserFeaturesContext = createContext<readonly string[]>([])\nconst OverrideRegistryRevisionContext = createContext<readonly ComponentOverride[]>([])\n\nexport function useOverrideUserFeatures(): readonly string[] {\n return useContext(OverrideUserFeaturesContext)\n}\n\nexport function useOverrideRegistryRevision(): readonly ComponentOverride[] {\n return useContext(OverrideRegistryRevisionContext)\n}\n\nexport function ComponentOverrideProvider({\n overrides,\n children,\n}: {\n overrides: ComponentOverride[]\n children: React.ReactNode\n}) {\n const [userFeatures, setUserFeatures] = React.useState<readonly string[]>([])\n\n registerComponentOverrides(overrides)\n\n React.useEffect(() => {\n registerComponentOverrides(overrides)\n return () => {\n registerComponentOverrides([])\n }\n }, [overrides])\n\n React.useEffect(() => {\n const requiredFeatures = new Set<string>()\n for (const override of overrides) {\n for (const feature of override.features ?? []) {\n if (feature && feature.trim().length > 0) requiredFeatures.add(feature)\n }\n }\n if (requiredFeatures.size === 0) return\n let cancelled = false\n const fetchFeatures = async () => {\n const call = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: Array.from(requiredFeatures) }),\n })\n if (cancelled) return\n if (call.ok && call.result?.granted) {\n setUserFeatures(call.result.granted)\n }\n }\n void fetchFeatures()\n return () => {\n cancelled = true\n }\n }, [overrides])\n\n return (\n <OverrideRegistryRevisionContext.Provider value={overrides}>\n <OverrideUserFeaturesContext.Provider value={userFeatures}>\n {children}\n </OverrideUserFeaturesContext.Provider>\n </OverrideRegistryRevisionContext.Provider>\n )\n}\n\nexport default ComponentOverrideProvider\n"],
|
|
5
|
+
"mappings": ";AAmEM;AAjEN,YAAY,WAAW;AACvB,SAAS,eAAe,kBAAkB;AAE1C,SAAS,kCAAkC;AAC3C,SAAS,eAAe;AAIxB,MAAM,8BAA8B,cAAiC,CAAC,CAAC;AACvE,MAAM,kCAAkC,cAA4C,CAAC,CAAC;AAE/E,SAAS,0BAA6C;AAC3D,SAAO,WAAW,2BAA2B;AAC/C;AAEO,SAAS,8BAA4D;AAC1E,SAAO,WAAW,+BAA+B;AACnD;AAEO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGG;AACD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAA4B,CAAC,CAAC;AAE5E,6BAA2B,SAAS;AAEpC,QAAM,UAAU,MAAM;AACpB,+BAA2B,SAAS;AACpC,WAAO,MAAM;AACX,iCAA2B,CAAC,CAAC;AAAA,IAC/B;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,UAAU,MAAM;AACpB,UAAM,mBAAmB,oBAAI,IAAY;AACzC,eAAW,YAAY,WAAW;AAChC,iBAAW,WAAW,SAAS,YAAY,CAAC,GAAG;AAC7C,YAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,EAAG,kBAAiB,IAAI,OAAO;AAAA,MACxE;AAAA,IACF;AACA,QAAI,iBAAiB,SAAS,EAAG;AACjC,QAAI,YAAY;AAChB,UAAM,gBAAgB,YAAY;AAChC,YAAM,OAAO,MAAM,QAA8B,2BAA2B;AAAA,QAC1E,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,MAAM,KAAK,gBAAgB,EAAE,CAAC;AAAA,MACjE,CAAC;AACD,UAAI,UAAW;AACf,UAAI,KAAK,MAAM,KAAK,QAAQ,SAAS;AACnC,wBAAgB,KAAK,OAAO,OAAO;AAAA,MACrC;AAAA,IACF;AACA,SAAK,cAAc;AACnB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,SACE,oBAAC,gCAAgC,UAAhC,EAAyC,OAAO,WAC/C,8BAAC,4BAA4B,UAA5B,EAAqC,OAAO,cAC1C,UACH,GACF;AAEJ;AAEA,IAAO,oCAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { getComponentEntry, getComponentOverrides } from "@open-mercato/shared/modules/widgets/component-registry";
|
|
5
|
-
import { useOverrideUserFeatures } from "./ComponentOverrideProvider.js";
|
|
5
|
+
import { useOverrideRegistryRevision, useOverrideUserFeatures } from "./ComponentOverrideProvider.js";
|
|
6
6
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
7
|
const logger = createLogger("ui").child({ component: "useRegisteredComponent" });
|
|
8
8
|
class ReplacementErrorBoundary extends React.Component {
|
|
@@ -23,6 +23,7 @@ class ReplacementErrorBoundary extends React.Component {
|
|
|
23
23
|
}
|
|
24
24
|
function useRegisteredComponent(componentId, fallback) {
|
|
25
25
|
const userFeatures = useOverrideUserFeatures();
|
|
26
|
+
const overrideRevision = useOverrideRegistryRevision();
|
|
26
27
|
return React.useMemo(() => {
|
|
27
28
|
const entry = getComponentEntry(componentId);
|
|
28
29
|
const original = entry?.component ?? fallback ?? null;
|
|
@@ -76,7 +77,7 @@ function useRegisteredComponent(componentId, fallback) {
|
|
|
76
77
|
};
|
|
77
78
|
Resolved.displayName = `RegisteredComponent(${componentId})`;
|
|
78
79
|
return Resolved;
|
|
79
|
-
}, [componentId, fallback, userFeatures]);
|
|
80
|
+
}, [componentId, fallback, overrideRevision, userFeatures]);
|
|
80
81
|
}
|
|
81
82
|
var useRegisteredComponent_default = useRegisteredComponent;
|
|
82
83
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/injection/useRegisteredComponent.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { ComponentType } from 'react'\nimport { getComponentEntry, getComponentOverrides } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useOverrideUserFeatures } from './ComponentOverrideProvider'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'useRegisteredComponent' })\n\nclass ReplacementErrorBoundary extends React.Component<\n { fallback: React.ReactNode; onError: (error: unknown) => void; children: React.ReactNode },\n { hasError: boolean }\n> {\n constructor(props: { fallback: React.ReactNode; onError: (error: unknown) => void; children: React.ReactNode }) {\n super(props)\n this.state = { hasError: false }\n }\n\n static getDerivedStateFromError(): { hasError: boolean } {\n return { hasError: true }\n }\n\n componentDidCatch(error: unknown): void {\n this.props.onError(error)\n }\n\n render() {\n if (this.state.hasError) return this.props.fallback\n return this.props.children\n }\n}\n\nexport function useRegisteredComponent<TProps>(\n componentId: string,\n fallback?: ComponentType<TProps>,\n): ComponentType<TProps> {\n const userFeatures = useOverrideUserFeatures()\n return React.useMemo(() => {\n const entry = getComponentEntry(componentId)\n const original = (entry?.component as ComponentType<TProps> | undefined) ?? fallback ?? null\n if (!original) {\n if (process.env.NODE_ENV !== 'production' && !fallback) {\n logger.warn('Component is not registered', { componentId })\n }\n const Missing = () => null\n return Missing as ComponentType<TProps>\n }\n const overrides = getComponentOverrides(componentId, userFeatures)\n const replacementOverrides = overrides.filter((override) => 'replacement' in override)\n if (process.env.NODE_ENV !== 'production' && replacementOverrides.length > 1) {\n logger.warn('Multiple replacements registered; highest-priority replacement is applied', { componentId })\n }\n\n let replacement: ComponentType<TProps> | null = null\n let replacementOverride: (typeof overrides)[number] | null = null\n const wrappers: Array<(Original: ComponentType<TProps>) => ComponentType<TProps>> = []\n const transforms: Array<(props: TProps) => TProps> = []\n\n for (const override of overrides) {\n if ('replacement' in override) {\n replacement = override.replacement as ComponentType<TProps>\n replacementOverride = override\n }\n if ('wrapper' in override) wrappers.push(override.wrapper as (Original: ComponentType<TProps>) => ComponentType<TProps>)\n if ('propsTransform' in override) transforms.push(override.propsTransform as (props: TProps) => TProps)\n }\n\n const base = replacement ?? original\n const wrapped = wrappers.reduce<ComponentType<TProps>>((acc, wrapper) => wrapper(acc), base)\n\n const Resolved = (props: TProps) => {\n const transformed = transforms.reduce((current, transform) => transform(current), props)\n const Fallback = React.createElement(original as React.ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)\n if (\n process.env.NODE_ENV !== 'production'\n && replacementOverride\n && 'replacement' in replacementOverride\n ) {\n const validation = replacementOverride.propsSchema.safeParse(transformed)\n if (!validation.success) {\n logger.error('Props schema validation failed for replacement', { componentId, module: replacementOverride.metadata?.module ?? 'unknown', issues: validation.error.format() })\n return Fallback\n }\n }\n return (\n <ReplacementErrorBoundary\n fallback={Fallback}\n onError={(error) => {\n const replacementModule = overrides.find((override) => 'replacement' in override)?.metadata?.module ?? 'unknown'\n logger.error('Component replacement failed', { componentId, module: replacementModule, err: error })\n }}\n >\n {React.createElement(wrapped as React.ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)}\n </ReplacementErrorBoundary>\n )\n }\n\n Resolved.displayName = `RegisteredComponent(${componentId})`\n return Resolved\n }, [componentId, fallback, userFeatures])\n}\n\nexport default useRegisteredComponent\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { ComponentType } from 'react'\nimport { getComponentEntry, getComponentOverrides } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useOverrideRegistryRevision, useOverrideUserFeatures } from './ComponentOverrideProvider'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'useRegisteredComponent' })\n\nclass ReplacementErrorBoundary extends React.Component<\n { fallback: React.ReactNode; onError: (error: unknown) => void; children: React.ReactNode },\n { hasError: boolean }\n> {\n constructor(props: { fallback: React.ReactNode; onError: (error: unknown) => void; children: React.ReactNode }) {\n super(props)\n this.state = { hasError: false }\n }\n\n static getDerivedStateFromError(): { hasError: boolean } {\n return { hasError: true }\n }\n\n componentDidCatch(error: unknown): void {\n this.props.onError(error)\n }\n\n render() {\n if (this.state.hasError) return this.props.fallback\n return this.props.children\n }\n}\n\nexport function useRegisteredComponent<TProps>(\n componentId: string,\n fallback?: ComponentType<TProps>,\n): ComponentType<TProps> {\n const userFeatures = useOverrideUserFeatures()\n const overrideRevision = useOverrideRegistryRevision()\n return React.useMemo(() => {\n const entry = getComponentEntry(componentId)\n const original = (entry?.component as ComponentType<TProps> | undefined) ?? fallback ?? null\n if (!original) {\n if (process.env.NODE_ENV !== 'production' && !fallback) {\n logger.warn('Component is not registered', { componentId })\n }\n const Missing = () => null\n return Missing as ComponentType<TProps>\n }\n const overrides = getComponentOverrides(componentId, userFeatures)\n const replacementOverrides = overrides.filter((override) => 'replacement' in override)\n if (process.env.NODE_ENV !== 'production' && replacementOverrides.length > 1) {\n logger.warn('Multiple replacements registered; highest-priority replacement is applied', { componentId })\n }\n\n let replacement: ComponentType<TProps> | null = null\n let replacementOverride: (typeof overrides)[number] | null = null\n const wrappers: Array<(Original: ComponentType<TProps>) => ComponentType<TProps>> = []\n const transforms: Array<(props: TProps) => TProps> = []\n\n for (const override of overrides) {\n if ('replacement' in override) {\n replacement = override.replacement as ComponentType<TProps>\n replacementOverride = override\n }\n if ('wrapper' in override) wrappers.push(override.wrapper as (Original: ComponentType<TProps>) => ComponentType<TProps>)\n if ('propsTransform' in override) transforms.push(override.propsTransform as (props: TProps) => TProps)\n }\n\n const base = replacement ?? original\n const wrapped = wrappers.reduce<ComponentType<TProps>>((acc, wrapper) => wrapper(acc), base)\n\n const Resolved = (props: TProps) => {\n const transformed = transforms.reduce((current, transform) => transform(current), props)\n const Fallback = React.createElement(original as React.ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)\n if (\n process.env.NODE_ENV !== 'production'\n && replacementOverride\n && 'replacement' in replacementOverride\n ) {\n const validation = replacementOverride.propsSchema.safeParse(transformed)\n if (!validation.success) {\n logger.error('Props schema validation failed for replacement', { componentId, module: replacementOverride.metadata?.module ?? 'unknown', issues: validation.error.format() })\n return Fallback\n }\n }\n return (\n <ReplacementErrorBoundary\n fallback={Fallback}\n onError={(error) => {\n const replacementModule = overrides.find((override) => 'replacement' in override)?.metadata?.module ?? 'unknown'\n logger.error('Component replacement failed', { componentId, module: replacementModule, err: error })\n }}\n >\n {React.createElement(wrapped as React.ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)}\n </ReplacementErrorBoundary>\n )\n }\n\n Resolved.displayName = `RegisteredComponent(${componentId})`\n return Resolved\n }, [componentId, fallback, overrideRevision, userFeatures])\n}\n\nexport default useRegisteredComponent\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6892.1.f179677788",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.8-develop.6892.1.f179677788",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
165
|
+
"@open-mercato/shared": "0.6.8-develop.6892.1.f179677788",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.0",
|
|
168
168
|
"@testing-library/react": "^16.3.1",
|
|
@@ -96,6 +96,8 @@ function generateId(): string {
|
|
|
96
96
|
|
|
97
97
|
export function DashboardScreen() {
|
|
98
98
|
const t = useT()
|
|
99
|
+
const tRef = React.useRef(t)
|
|
100
|
+
tRef.current = t
|
|
99
101
|
const organizationScopeVersion = useOrganizationScopeVersion()
|
|
100
102
|
const [loading, setLoading] = React.useState(true)
|
|
101
103
|
const [error, setError] = React.useState<string | null>(null)
|
|
@@ -110,6 +112,8 @@ export function DashboardScreen() {
|
|
|
110
112
|
const [settingsId, setSettingsId] = React.useState<string | null>(null)
|
|
111
113
|
const pendingOpsRef = React.useRef(0)
|
|
112
114
|
const saveQueueRef = React.useRef(Promise.resolve())
|
|
115
|
+
const layoutRevisionRef = React.useRef(0)
|
|
116
|
+
const pendingLayoutRevisionsRef = React.useRef(new Set<number>())
|
|
113
117
|
const draggingIdRef = React.useRef<string | null>(null)
|
|
114
118
|
|
|
115
119
|
const adjustSaving = React.useCallback((delta: number) => {
|
|
@@ -118,6 +122,8 @@ export function DashboardScreen() {
|
|
|
118
122
|
}, [])
|
|
119
123
|
|
|
120
124
|
const load = React.useCallback(async () => {
|
|
125
|
+
const layoutRevisionAtStart = layoutRevisionRef.current
|
|
126
|
+
const hadPendingLayoutMutationAtStart = pendingLayoutRevisionsRef.current.size > 0
|
|
121
127
|
setLoading(true)
|
|
122
128
|
setError(null)
|
|
123
129
|
try {
|
|
@@ -128,7 +134,12 @@ export function DashboardScreen() {
|
|
|
128
134
|
const data = call.result
|
|
129
135
|
const registeredWidgetCount = getDashboardWidgets().length
|
|
130
136
|
const normalizedLayout = sortLayout(data.layout?.items ?? [])
|
|
131
|
-
|
|
137
|
+
if (
|
|
138
|
+
!hadPendingLayoutMutationAtStart
|
|
139
|
+
&& layoutRevisionAtStart === layoutRevisionRef.current
|
|
140
|
+
) {
|
|
141
|
+
setLayout(normalizedLayout)
|
|
142
|
+
}
|
|
132
143
|
setWidgetCatalog(data.widgets ?? [])
|
|
133
144
|
setHasRegisteredWidgets(registeredWidgetCount > 0 || (data.widgets ?? []).length > 0)
|
|
134
145
|
setAllowedWidgetIds(data.allowedWidgetIds ?? [])
|
|
@@ -162,11 +173,11 @@ export function DashboardScreen() {
|
|
|
162
173
|
setSettingsId(null)
|
|
163
174
|
return
|
|
164
175
|
}
|
|
165
|
-
setError(
|
|
176
|
+
setError(tRef.current('dashboard.loadError'))
|
|
166
177
|
} finally {
|
|
167
178
|
setLoading(false)
|
|
168
179
|
}
|
|
169
|
-
}, [
|
|
180
|
+
}, [])
|
|
170
181
|
|
|
171
182
|
React.useEffect(() => {
|
|
172
183
|
load()
|
|
@@ -219,6 +230,8 @@ export function DashboardScreen() {
|
|
|
219
230
|
}, [t])
|
|
220
231
|
|
|
221
232
|
const queueLayoutSave = React.useCallback((items: LayoutItem[]) => {
|
|
233
|
+
const layoutRevision = ++layoutRevisionRef.current
|
|
234
|
+
pendingLayoutRevisionsRef.current.add(layoutRevision)
|
|
222
235
|
saveQueueRef.current = saveQueueRef.current.then(async () => {
|
|
223
236
|
adjustSaving(1)
|
|
224
237
|
try {
|
|
@@ -243,12 +256,17 @@ export function DashboardScreen() {
|
|
|
243
256
|
logger.error('Failed to save layout', { err })
|
|
244
257
|
setError(t('dashboard.saveError'))
|
|
245
258
|
} finally {
|
|
259
|
+
pendingLayoutRevisionsRef.current.delete(layoutRevision)
|
|
246
260
|
adjustSaving(-1)
|
|
247
261
|
}
|
|
248
262
|
})
|
|
249
263
|
}, [adjustSaving, t])
|
|
250
264
|
|
|
251
|
-
const patchWidgetSettings = React.useCallback(async (
|
|
265
|
+
const patchWidgetSettings = React.useCallback(async (
|
|
266
|
+
itemId: string,
|
|
267
|
+
nextSettings: unknown,
|
|
268
|
+
layoutRevision: number,
|
|
269
|
+
) => {
|
|
252
270
|
adjustSaving(1)
|
|
253
271
|
try {
|
|
254
272
|
const call = await apiCall(`/api/dashboards/layout/${encodeURIComponent(itemId)}`, {
|
|
@@ -262,6 +280,7 @@ export function DashboardScreen() {
|
|
|
262
280
|
logger.error('Failed to update widget settings', { err })
|
|
263
281
|
setError(t('dashboard.saveError'))
|
|
264
282
|
} finally {
|
|
283
|
+
pendingLayoutRevisionsRef.current.delete(layoutRevision)
|
|
265
284
|
adjustSaving(-1)
|
|
266
285
|
}
|
|
267
286
|
}, [adjustSaving, t])
|
|
@@ -316,8 +335,10 @@ export function DashboardScreen() {
|
|
|
316
335
|
}, [queueLayoutSave])
|
|
317
336
|
|
|
318
337
|
const handleSettingsChange = React.useCallback((itemId: string, nextSettings: unknown) => {
|
|
338
|
+
const layoutRevision = ++layoutRevisionRef.current
|
|
339
|
+
pendingLayoutRevisionsRef.current.add(layoutRevision)
|
|
319
340
|
setLayout((prev) => prev.map((item) => (item.id === itemId ? { ...item, settings: nextSettings } : item)))
|
|
320
|
-
void patchWidgetSettings(itemId, nextSettings)
|
|
341
|
+
void patchWidgetSettings(itemId, nextSettings, layoutRevision)
|
|
321
342
|
}, [patchWidgetSettings])
|
|
322
343
|
|
|
323
344
|
const toggleEditing = React.useCallback(() => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as React from 'react'
|
|
6
|
-
import { screen, waitFor } from '@testing-library/react'
|
|
6
|
+
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
|
7
7
|
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
8
8
|
import { DashboardScreen } from '../DashboardScreen'
|
|
9
9
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
@@ -36,6 +36,15 @@ jest.mock('next/navigation', () => ({
|
|
|
36
36
|
|
|
37
37
|
const createMockResponse = (status: number): Response => ({ status } as Response)
|
|
38
38
|
|
|
39
|
+
function successfulApiCall<T>(result: T) {
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
status: 200,
|
|
43
|
+
result,
|
|
44
|
+
response: createMockResponse(200),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
const dict = {
|
|
40
49
|
'dashboard.loadError': 'Failed to load dashboard',
|
|
41
50
|
'dashboard.widget.loadError': 'Unable to load widget.',
|
|
@@ -43,6 +52,11 @@ const dict = {
|
|
|
43
52
|
'dashboard.empty.noWidgets.description': 'Dashboard widgets will appear here after you add a module.',
|
|
44
53
|
'dashboard.widgets.foo.title': 'Widget Foo',
|
|
45
54
|
'dashboard.widgets.foo.description': 'Widget description',
|
|
55
|
+
'dashboard.widgets.bar.title': 'Widget Bar',
|
|
56
|
+
'dashboard.widgets.bar.description': 'Another widget description',
|
|
57
|
+
'dashboard.action.customize': 'Customize',
|
|
58
|
+
'dashboard.action.done': 'Done',
|
|
59
|
+
'dashboard.addWidget': 'Add a widget',
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
const widgetResponse = {
|
|
@@ -74,6 +88,28 @@ const widgetResponse = {
|
|
|
74
88
|
},
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
const secondWidget = {
|
|
92
|
+
id: 'bar',
|
|
93
|
+
title: 'Widget Bar',
|
|
94
|
+
description: 'Another widget description',
|
|
95
|
+
defaultSize: 'md',
|
|
96
|
+
defaultEnabled: false,
|
|
97
|
+
defaultSettings: null,
|
|
98
|
+
features: [],
|
|
99
|
+
moduleId: 'example',
|
|
100
|
+
icon: null,
|
|
101
|
+
loaderKey: 'bar.loader',
|
|
102
|
+
supportsRefresh: false,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function deferred<T>() {
|
|
106
|
+
let resolve!: (value: T) => void
|
|
107
|
+
const promise = new Promise<T>((promiseResolve) => {
|
|
108
|
+
resolve = promiseResolve
|
|
109
|
+
})
|
|
110
|
+
return { promise, resolve }
|
|
111
|
+
}
|
|
112
|
+
|
|
77
113
|
function MockWidget() {
|
|
78
114
|
return <div>Widget body</div>
|
|
79
115
|
}
|
|
@@ -127,6 +163,61 @@ describe('DashboardScreen', () => {
|
|
|
127
163
|
})
|
|
128
164
|
})
|
|
129
165
|
|
|
166
|
+
it('does not replace optimistic layout changes with a reload started while saves are pending', async () => {
|
|
167
|
+
const firstSave = deferred<unknown>()
|
|
168
|
+
const staleReload = deferred<unknown>()
|
|
169
|
+
let getCount = 0
|
|
170
|
+
let putCount = 0
|
|
171
|
+
const emptyLayoutResponse = {
|
|
172
|
+
...widgetResponse,
|
|
173
|
+
layout: { items: [] },
|
|
174
|
+
widgets: [widgetResponse.widgets[0], secondWidget],
|
|
175
|
+
allowedWidgetIds: ['foo', 'bar'],
|
|
176
|
+
}
|
|
177
|
+
const staleLayoutResponse = {
|
|
178
|
+
...emptyLayoutResponse,
|
|
179
|
+
layout: { items: [{ id: 'server-item-1', widgetId: 'foo', order: 0, size: 'md' as const }] },
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
;(getDashboardWidgets as jest.Mock).mockReturnValue([
|
|
183
|
+
{ key: 'foo.loader', loader: jest.fn() },
|
|
184
|
+
{ key: 'bar.loader', loader: jest.fn() },
|
|
185
|
+
])
|
|
186
|
+
;(apiCall as jest.Mock).mockImplementation((_url: string, options?: RequestInit) => {
|
|
187
|
+
if (options?.method === 'PUT') {
|
|
188
|
+
putCount += 1
|
|
189
|
+
return putCount === 1 ? firstSave.promise : Promise.resolve(successfulApiCall(null))
|
|
190
|
+
}
|
|
191
|
+
getCount += 1
|
|
192
|
+
return getCount === 1
|
|
193
|
+
? Promise.resolve(successfulApiCall(emptyLayoutResponse))
|
|
194
|
+
: staleReload.promise
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const { rerender } = renderWithProviders(<DashboardScreen />, { dict })
|
|
198
|
+
|
|
199
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Customize' }))
|
|
200
|
+
fireEvent.click(screen.getByRole('button', { name: 'Widget Foo' }))
|
|
201
|
+
fireEvent.click(screen.getByRole('button', { name: 'Widget Bar' }))
|
|
202
|
+
await waitFor(() => expect(putCount).toBe(1))
|
|
203
|
+
|
|
204
|
+
mockOrganizationScopeVersion = 1
|
|
205
|
+
rerender(<DashboardScreen />)
|
|
206
|
+
await waitFor(() => expect(getCount).toBe(2))
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
staleReload.resolve(successfulApiCall(staleLayoutResponse))
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(await screen.findByText('Widget Foo')).toBeInTheDocument()
|
|
213
|
+
expect(screen.getByText('Widget Bar')).toBeInTheDocument()
|
|
214
|
+
|
|
215
|
+
await act(async () => {
|
|
216
|
+
firstSave.resolve(successfulApiCall(null))
|
|
217
|
+
})
|
|
218
|
+
await waitFor(() => expect(putCount).toBe(2))
|
|
219
|
+
})
|
|
220
|
+
|
|
130
221
|
it('shows an error when the layout request fails', async () => {
|
|
131
222
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
132
223
|
;(apiCall as jest.Mock).mockResolvedValue({
|
|
@@ -9,11 +9,16 @@ import { apiCall } from '../utils/apiCall'
|
|
|
9
9
|
type FeatureCheckResponse = { granted: string[] }
|
|
10
10
|
|
|
11
11
|
const OverrideUserFeaturesContext = createContext<readonly string[]>([])
|
|
12
|
+
const OverrideRegistryRevisionContext = createContext<readonly ComponentOverride[]>([])
|
|
12
13
|
|
|
13
14
|
export function useOverrideUserFeatures(): readonly string[] {
|
|
14
15
|
return useContext(OverrideUserFeaturesContext)
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export function useOverrideRegistryRevision(): readonly ComponentOverride[] {
|
|
19
|
+
return useContext(OverrideRegistryRevisionContext)
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function ComponentOverrideProvider({
|
|
18
23
|
overrides,
|
|
19
24
|
children,
|
|
@@ -26,6 +31,7 @@ export function ComponentOverrideProvider({
|
|
|
26
31
|
registerComponentOverrides(overrides)
|
|
27
32
|
|
|
28
33
|
React.useEffect(() => {
|
|
34
|
+
registerComponentOverrides(overrides)
|
|
29
35
|
return () => {
|
|
30
36
|
registerComponentOverrides([])
|
|
31
37
|
}
|
|
@@ -58,9 +64,11 @@ export function ComponentOverrideProvider({
|
|
|
58
64
|
}, [overrides])
|
|
59
65
|
|
|
60
66
|
return (
|
|
61
|
-
<
|
|
62
|
-
{
|
|
63
|
-
|
|
67
|
+
<OverrideRegistryRevisionContext.Provider value={overrides}>
|
|
68
|
+
<OverrideUserFeaturesContext.Provider value={userFeatures}>
|
|
69
|
+
{children}
|
|
70
|
+
</OverrideUserFeaturesContext.Provider>
|
|
71
|
+
</OverrideRegistryRevisionContext.Provider>
|
|
64
72
|
)
|
|
65
73
|
}
|
|
66
74
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { ComponentType } from 'react'
|
|
5
5
|
import { getComponentEntry, getComponentOverrides } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
6
|
-
import { useOverrideUserFeatures } from './ComponentOverrideProvider'
|
|
6
|
+
import { useOverrideRegistryRevision, useOverrideUserFeatures } from './ComponentOverrideProvider'
|
|
7
7
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
8
8
|
|
|
9
9
|
const logger = createLogger('ui').child({ component: 'useRegisteredComponent' })
|
|
@@ -36,6 +36,7 @@ export function useRegisteredComponent<TProps>(
|
|
|
36
36
|
fallback?: ComponentType<TProps>,
|
|
37
37
|
): ComponentType<TProps> {
|
|
38
38
|
const userFeatures = useOverrideUserFeatures()
|
|
39
|
+
const overrideRevision = useOverrideRegistryRevision()
|
|
39
40
|
return React.useMemo(() => {
|
|
40
41
|
const entry = getComponentEntry(componentId)
|
|
41
42
|
const original = (entry?.component as ComponentType<TProps> | undefined) ?? fallback ?? null
|
|
@@ -98,7 +99,7 @@ export function useRegisteredComponent<TProps>(
|
|
|
98
99
|
|
|
99
100
|
Resolved.displayName = `RegisteredComponent(${componentId})`
|
|
100
101
|
return Resolved
|
|
101
|
-
}, [componentId, fallback, userFeatures])
|
|
102
|
+
}, [componentId, fallback, overrideRevision, userFeatures])
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export default useRegisteredComponent
|