@open-mercato/ui 0.4.2-canary-c02407ff85
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/build.mjs +62 -0
- package/dist/backend/AppShell.js +902 -0
- package/dist/backend/AppShell.js.map +7 -0
- package/dist/backend/ConfirmDialog.js +17 -0
- package/dist/backend/ConfirmDialog.js.map +7 -0
- package/dist/backend/ContextHelp.js +31 -0
- package/dist/backend/ContextHelp.js.map +7 -0
- package/dist/backend/CrudForm.js +2028 -0
- package/dist/backend/CrudForm.js.map +7 -0
- package/dist/backend/DataTable.js +1363 -0
- package/dist/backend/DataTable.js.map +7 -0
- package/dist/backend/EmptyState.js +52 -0
- package/dist/backend/EmptyState.js.map +7 -0
- package/dist/backend/FilterBar.js +140 -0
- package/dist/backend/FilterBar.js.map +7 -0
- package/dist/backend/FilterOverlay.js +279 -0
- package/dist/backend/FilterOverlay.js.map +7 -0
- package/dist/backend/FlashMessages.js +66 -0
- package/dist/backend/FlashMessages.js.map +7 -0
- package/dist/backend/JsonBuilder.js +322 -0
- package/dist/backend/JsonBuilder.js.map +7 -0
- package/dist/backend/JsonDisplay.js +203 -0
- package/dist/backend/JsonDisplay.js.map +7 -0
- package/dist/backend/Page.js +27 -0
- package/dist/backend/Page.js.map +7 -0
- package/dist/backend/PerspectiveSidebar.js +282 -0
- package/dist/backend/PerspectiveSidebar.js.map +7 -0
- package/dist/backend/RowActions.js +148 -0
- package/dist/backend/RowActions.js.map +7 -0
- package/dist/backend/TruncatedCell.js +92 -0
- package/dist/backend/TruncatedCell.js.map +7 -0
- package/dist/backend/UserMenu.js +107 -0
- package/dist/backend/UserMenu.js.map +7 -0
- package/dist/backend/ValueIcons.js +34 -0
- package/dist/backend/ValueIcons.js.map +7 -0
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
- package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
- package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
- package/dist/backend/dashboard/DashboardScreen.js +578 -0
- package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
- package/dist/backend/dashboard/index.js +5 -0
- package/dist/backend/dashboard/index.js.map +7 -0
- package/dist/backend/dashboard/widgetRegistry.js +55 -0
- package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
- package/dist/backend/detail/ActivitiesSection.js +962 -0
- package/dist/backend/detail/ActivitiesSection.js.map +7 -0
- package/dist/backend/detail/AddressEditor.js +413 -0
- package/dist/backend/detail/AddressEditor.js.map +7 -0
- package/dist/backend/detail/AddressTiles.js +437 -0
- package/dist/backend/detail/AddressTiles.js.map +7 -0
- package/dist/backend/detail/AddressesSection.js +264 -0
- package/dist/backend/detail/AddressesSection.js.map +7 -0
- package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
- package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
- package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
- package/dist/backend/detail/AttachmentsSection.js +367 -0
- package/dist/backend/detail/AttachmentsSection.js.map +7 -0
- package/dist/backend/detail/CustomDataSection.js +433 -0
- package/dist/backend/detail/CustomDataSection.js.map +7 -0
- package/dist/backend/detail/DetailFieldsSection.js +75 -0
- package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
- package/dist/backend/detail/ErrorMessage.js +28 -0
- package/dist/backend/detail/ErrorMessage.js.map +7 -0
- package/dist/backend/detail/InlineEditors.js +681 -0
- package/dist/backend/detail/InlineEditors.js.map +7 -0
- package/dist/backend/detail/LoadingMessage.js +14 -0
- package/dist/backend/detail/LoadingMessage.js.map +7 -0
- package/dist/backend/detail/NotesSection.js +1032 -0
- package/dist/backend/detail/NotesSection.js.map +7 -0
- package/dist/backend/detail/TabEmptyState.js +25 -0
- package/dist/backend/detail/TabEmptyState.js.map +7 -0
- package/dist/backend/detail/TagsSection.js +254 -0
- package/dist/backend/detail/TagsSection.js.map +7 -0
- package/dist/backend/detail/addressFormat.js +77 -0
- package/dist/backend/detail/addressFormat.js.map +7 -0
- package/dist/backend/detail/index.js +34 -0
- package/dist/backend/detail/index.js.map +7 -0
- package/dist/backend/fields/registry.generated.js +8 -0
- package/dist/backend/fields/registry.generated.js.map +7 -0
- package/dist/backend/fields/registry.js +29 -0
- package/dist/backend/fields/registry.js.map +7 -0
- package/dist/backend/indexes/PartialIndexBanner.js +58 -0
- package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
- package/dist/backend/indexes/store.js +62 -0
- package/dist/backend/indexes/store.js.map +7 -0
- package/dist/backend/injection/InjectionSpot.js +179 -0
- package/dist/backend/injection/InjectionSpot.js.map +7 -0
- package/dist/backend/injection/PageInjectionBoundary.js +26 -0
- package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
- package/dist/backend/injection/helpers.js +26 -0
- package/dist/backend/injection/helpers.js.map +7 -0
- package/dist/backend/injection/widgetRegistry.js +55 -0
- package/dist/backend/injection/widgetRegistry.js.map +7 -0
- package/dist/backend/inputs/ComboboxInput.js +225 -0
- package/dist/backend/inputs/ComboboxInput.js.map +7 -0
- package/dist/backend/inputs/LookupSelect.js +191 -0
- package/dist/backend/inputs/LookupSelect.js.map +7 -0
- package/dist/backend/inputs/PhoneNumberField.js +100 -0
- package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
- package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
- package/dist/backend/inputs/TagsInput.js +222 -0
- package/dist/backend/inputs/TagsInput.js.map +7 -0
- package/dist/backend/inputs/index.js +6 -0
- package/dist/backend/inputs/index.js.map +7 -0
- package/dist/backend/operations/LastOperationBanner.js +80 -0
- package/dist/backend/operations/LastOperationBanner.js.map +7 -0
- package/dist/backend/operations/store.js +183 -0
- package/dist/backend/operations/store.js.map +7 -0
- package/dist/backend/schedule/ScheduleAgenda.js +107 -0
- package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
- package/dist/backend/schedule/ScheduleGrid.js +107 -0
- package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
- package/dist/backend/schedule/ScheduleToolbar.js +166 -0
- package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
- package/dist/backend/schedule/ScheduleView.js +165 -0
- package/dist/backend/schedule/ScheduleView.js.map +7 -0
- package/dist/backend/schedule/index.js +6 -0
- package/dist/backend/schedule/index.js.map +7 -0
- package/dist/backend/schedule/recurrence.js +83 -0
- package/dist/backend/schedule/recurrence.js.map +7 -0
- package/dist/backend/schedule/types.js +1 -0
- package/dist/backend/schedule/types.js.map +7 -0
- package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
- package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
- package/dist/backend/utils/api.js +127 -0
- package/dist/backend/utils/api.js.map +7 -0
- package/dist/backend/utils/apiCall.js +48 -0
- package/dist/backend/utils/apiCall.js.map +7 -0
- package/dist/backend/utils/crud.js +126 -0
- package/dist/backend/utils/crud.js.map +7 -0
- package/dist/backend/utils/customFieldColumns.js +56 -0
- package/dist/backend/utils/customFieldColumns.js.map +7 -0
- package/dist/backend/utils/customFieldDefs.js +143 -0
- package/dist/backend/utils/customFieldDefs.js.map +7 -0
- package/dist/backend/utils/customFieldFilters.js +126 -0
- package/dist/backend/utils/customFieldFilters.js.map +7 -0
- package/dist/backend/utils/customFieldForms.js +162 -0
- package/dist/backend/utils/customFieldForms.js.map +7 -0
- package/dist/backend/utils/customFieldValues.js +26 -0
- package/dist/backend/utils/customFieldValues.js.map +7 -0
- package/dist/backend/utils/flash.js +16 -0
- package/dist/backend/utils/flash.js.map +7 -0
- package/dist/backend/utils/nav.js +185 -0
- package/dist/backend/utils/nav.js.map +7 -0
- package/dist/backend/utils/serverErrors.js +230 -0
- package/dist/backend/utils/serverErrors.js.map +7 -0
- package/dist/frontend/AuthFooter.js +23 -0
- package/dist/frontend/AuthFooter.js.map +7 -0
- package/dist/frontend/LanguageSwitcher.js +57 -0
- package/dist/frontend/LanguageSwitcher.js.map +7 -0
- package/dist/frontend/Layout.js +14 -0
- package/dist/frontend/Layout.js.map +7 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +7 -0
- package/dist/primitives/DataLoader.js +67 -0
- package/dist/primitives/DataLoader.js.map +7 -0
- package/dist/primitives/ErrorNotice.js +20 -0
- package/dist/primitives/ErrorNotice.js.map +7 -0
- package/dist/primitives/alert.js +38 -0
- package/dist/primitives/alert.js.map +7 -0
- package/dist/primitives/badge.js +28 -0
- package/dist/primitives/badge.js.map +7 -0
- package/dist/primitives/button.js +44 -0
- package/dist/primitives/button.js.map +7 -0
- package/dist/primitives/card.js +91 -0
- package/dist/primitives/card.js.map +7 -0
- package/dist/primitives/checkbox.js +28 -0
- package/dist/primitives/checkbox.js.map +7 -0
- package/dist/primitives/dialog.js +90 -0
- package/dist/primitives/dialog.js.map +7 -0
- package/dist/primitives/input.js +22 -0
- package/dist/primitives/input.js.map +7 -0
- package/dist/primitives/label.js +21 -0
- package/dist/primitives/label.js.map +7 -0
- package/dist/primitives/separator.js +9 -0
- package/dist/primitives/separator.js.map +7 -0
- package/dist/primitives/spinner.js +24 -0
- package/dist/primitives/spinner.js.map +7 -0
- package/dist/primitives/switch.js +80 -0
- package/dist/primitives/switch.js.map +7 -0
- package/dist/primitives/table.js +29 -0
- package/dist/primitives/table.js.map +7 -0
- package/dist/primitives/tabs.js +87 -0
- package/dist/primitives/tabs.js.map +7 -0
- package/dist/primitives/textarea.js +21 -0
- package/dist/primitives/textarea.js.map +7 -0
- package/dist/primitives/tooltip.js +60 -0
- package/dist/primitives/tooltip.js.map +7 -0
- package/dist/theme/QueryProvider.js +44 -0
- package/dist/theme/QueryProvider.js.map +7 -0
- package/dist/theme/ThemeProvider.js +95 -0
- package/dist/theme/ThemeProvider.js.map +7 -0
- package/dist/theme/ThemeToggle.js +88 -0
- package/dist/theme/ThemeToggle.js.map +7 -0
- package/dist/theme/index.js +10 -0
- package/dist/theme/index.js.map +7 -0
- package/dist/types/react-big-calendar.d.js +1 -0
- package/dist/types/react-big-calendar.d.js.map +7 -0
- package/jest.config.cjs +23 -0
- package/jest.setup.ts +55 -0
- package/package.json +105 -0
- package/src/backend/AppShell.tsx +1096 -0
- package/src/backend/ConfirmDialog.tsx +19 -0
- package/src/backend/ContextHelp.tsx +38 -0
- package/src/backend/CrudForm.tsx +2503 -0
- package/src/backend/DataTable.tsx +1730 -0
- package/src/backend/EmptyState.tsx +65 -0
- package/src/backend/FilterBar.tsx +161 -0
- package/src/backend/FilterOverlay.tsx +328 -0
- package/src/backend/FlashMessages.tsx +82 -0
- package/src/backend/JsonBuilder.tsx +362 -0
- package/src/backend/JsonDisplay.tsx +254 -0
- package/src/backend/Page.tsx +30 -0
- package/src/backend/PerspectiveSidebar.tsx +337 -0
- package/src/backend/RowActions.tsx +151 -0
- package/src/backend/TruncatedCell.tsx +133 -0
- package/src/backend/UserMenu.tsx +118 -0
- package/src/backend/ValueIcons.tsx +48 -0
- package/src/backend/__tests__/AppShell.test.tsx +115 -0
- package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
- package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
- package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
- package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
- package/src/backend/__tests__/serverErrors.test.ts +83 -0
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
- package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
- package/src/backend/dashboard/DashboardScreen.tsx +684 -0
- package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
- package/src/backend/dashboard/index.ts +1 -0
- package/src/backend/dashboard/widgetRegistry.ts +68 -0
- package/src/backend/detail/ActivitiesSection.tsx +1284 -0
- package/src/backend/detail/AddressEditor.tsx +472 -0
- package/src/backend/detail/AddressTiles.tsx +587 -0
- package/src/backend/detail/AddressesSection.tsx +346 -0
- package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
- package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
- package/src/backend/detail/AttachmentsSection.tsx +414 -0
- package/src/backend/detail/CustomDataSection.tsx +530 -0
- package/src/backend/detail/DetailFieldsSection.tsx +147 -0
- package/src/backend/detail/ErrorMessage.tsx +32 -0
- package/src/backend/detail/InlineEditors.tsx +877 -0
- package/src/backend/detail/LoadingMessage.tsx +14 -0
- package/src/backend/detail/NotesSection.tsx +1275 -0
- package/src/backend/detail/TabEmptyState.tsx +48 -0
- package/src/backend/detail/TagsSection.tsx +314 -0
- package/src/backend/detail/addressFormat.tsx +121 -0
- package/src/backend/detail/index.ts +44 -0
- package/src/backend/fields/registry.generated.ts +8 -0
- package/src/backend/fields/registry.ts +38 -0
- package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
- package/src/backend/indexes/store.ts +88 -0
- package/src/backend/injection/InjectionSpot.tsx +236 -0
- package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
- package/src/backend/injection/helpers.ts +35 -0
- package/src/backend/injection/widgetRegistry.ts +68 -0
- package/src/backend/inputs/ComboboxInput.tsx +269 -0
- package/src/backend/inputs/LookupSelect.tsx +247 -0
- package/src/backend/inputs/PhoneNumberField.tsx +129 -0
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
- package/src/backend/inputs/TagsInput.tsx +259 -0
- package/src/backend/inputs/index.ts +5 -0
- package/src/backend/operations/LastOperationBanner.tsx +85 -0
- package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
- package/src/backend/operations/store.ts +230 -0
- package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
- package/src/backend/schedule/ScheduleGrid.tsx +136 -0
- package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
- package/src/backend/schedule/ScheduleView.tsx +198 -0
- package/src/backend/schedule/index.ts +5 -0
- package/src/backend/schedule/recurrence.ts +99 -0
- package/src/backend/schedule/types.ts +26 -0
- package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
- package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
- package/src/backend/utils/__tests__/crud.test.ts +87 -0
- package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
- package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
- package/src/backend/utils/api.ts +149 -0
- package/src/backend/utils/apiCall.ts +96 -0
- package/src/backend/utils/crud.ts +174 -0
- package/src/backend/utils/customFieldColumns.ts +71 -0
- package/src/backend/utils/customFieldDefs.ts +245 -0
- package/src/backend/utils/customFieldFilters.ts +145 -0
- package/src/backend/utils/customFieldForms.ts +196 -0
- package/src/backend/utils/customFieldValues.ts +41 -0
- package/src/backend/utils/flash.ts +17 -0
- package/src/backend/utils/nav.ts +238 -0
- package/src/backend/utils/serverErrors.ts +302 -0
- package/src/frontend/AuthFooter.tsx +29 -0
- package/src/frontend/LanguageSwitcher.tsx +66 -0
- package/src/frontend/Layout.tsx +13 -0
- package/src/index.ts +32 -0
- package/src/primitives/DataLoader.tsx +92 -0
- package/src/primitives/ErrorNotice.tsx +26 -0
- package/src/primitives/alert.tsx +52 -0
- package/src/primitives/badge.tsx +31 -0
- package/src/primitives/button.tsx +47 -0
- package/src/primitives/card.tsx +92 -0
- package/src/primitives/checkbox.tsx +28 -0
- package/src/primitives/dialog.tsx +110 -0
- package/src/primitives/input.tsx +20 -0
- package/src/primitives/label.tsx +18 -0
- package/src/primitives/separator.tsx +7 -0
- package/src/primitives/spinner.tsx +27 -0
- package/src/primitives/switch.tsx +86 -0
- package/src/primitives/table.tsx +27 -0
- package/src/primitives/tabs.tsx +128 -0
- package/src/primitives/textarea.tsx +20 -0
- package/src/primitives/tooltip.tsx +85 -0
- package/src/theme/QueryProvider.tsx +46 -0
- package/src/theme/ThemeProvider.tsx +120 -0
- package/src/theme/ThemeToggle.tsx +88 -0
- package/src/theme/index.ts +3 -0
- package/src/types/react-big-calendar.d.ts +16 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
export type PartialIndexNotice = {
|
|
5
|
+
entity: string
|
|
6
|
+
entityLabel: string
|
|
7
|
+
baseCount: number | null
|
|
8
|
+
indexedCount: number | null
|
|
9
|
+
scope: 'scoped' | 'global'
|
|
10
|
+
receivedAt: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PartialIndexInput = {
|
|
14
|
+
entity: string
|
|
15
|
+
entityLabel?: string
|
|
16
|
+
baseCount?: number | null
|
|
17
|
+
indexedCount?: number | null
|
|
18
|
+
scope?: 'scoped' | 'global'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TTL_MS = 120_000
|
|
22
|
+
|
|
23
|
+
let current: PartialIndexNotice | null = null
|
|
24
|
+
const emitter = new EventTarget()
|
|
25
|
+
|
|
26
|
+
function now(): number {
|
|
27
|
+
return typeof performance !== 'undefined' && performance.now
|
|
28
|
+
? Math.round(performance.timeOrigin + performance.now())
|
|
29
|
+
: Date.now()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function subscribe(listener: () => void) {
|
|
33
|
+
const wrapped = () => listener()
|
|
34
|
+
emitter.addEventListener('change', wrapped)
|
|
35
|
+
return () => emitter.removeEventListener('change', wrapped)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emit() {
|
|
39
|
+
emitter.dispatchEvent(new Event('change'))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeInput(input: PartialIndexInput): PartialIndexNotice {
|
|
43
|
+
const label = typeof input.entityLabel === 'string' && input.entityLabel.trim()
|
|
44
|
+
? input.entityLabel.trim()
|
|
45
|
+
: input.entity
|
|
46
|
+
return {
|
|
47
|
+
entity: input.entity,
|
|
48
|
+
entityLabel: label,
|
|
49
|
+
baseCount: typeof input.baseCount === 'number' ? input.baseCount : null,
|
|
50
|
+
indexedCount: typeof input.indexedCount === 'number' ? input.indexedCount : null,
|
|
51
|
+
scope: input.scope === 'global' ? 'global' : 'scoped',
|
|
52
|
+
receivedAt: now(),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function prune(stale: PartialIndexNotice | null): PartialIndexNotice | null {
|
|
57
|
+
if (!stale) return null
|
|
58
|
+
const age = now() - stale.receivedAt
|
|
59
|
+
if (!Number.isFinite(age) || age > TTL_MS) return null
|
|
60
|
+
return stale
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getSnapshot(): PartialIndexNotice | null {
|
|
64
|
+
current = prune(current)
|
|
65
|
+
return current
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function usePartialIndexWarning(): PartialIndexNotice | null {
|
|
69
|
+
return React.useSyncExternalStore(
|
|
70
|
+
subscribe,
|
|
71
|
+
() => getSnapshot(),
|
|
72
|
+
() => null,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pushPartialIndexWarning(input: PartialIndexInput) {
|
|
77
|
+
if (typeof window === 'undefined') return
|
|
78
|
+
if (!input.entity) return
|
|
79
|
+
const next = normalizeInput(input)
|
|
80
|
+
current = next
|
|
81
|
+
emit()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function dismissPartialIndexWarning() {
|
|
85
|
+
if (typeof window === 'undefined') return
|
|
86
|
+
current = null
|
|
87
|
+
emit()
|
|
88
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import type {
|
|
4
|
+
InjectionSpotId,
|
|
5
|
+
InjectionWidgetModule,
|
|
6
|
+
WidgetInjectionEventHandlers,
|
|
7
|
+
WidgetBeforeSaveResult,
|
|
8
|
+
} from '@open-mercato/shared/modules/widgets/injection'
|
|
9
|
+
import { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'
|
|
10
|
+
|
|
11
|
+
export type InjectionSpotProps<TContext = unknown, TData = unknown> = {
|
|
12
|
+
spotId: InjectionSpotId
|
|
13
|
+
context: TContext
|
|
14
|
+
data?: TData
|
|
15
|
+
onDataChange?: (data: TData) => void
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
onEvent?: (event: 'onLoad' | 'onBeforeSave' | 'onSave' | 'onAfterSave', widgetId: string) => void
|
|
18
|
+
widgetsOverride?: LoadedWidget[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type LoadedWidget = {
|
|
22
|
+
widgetId: string
|
|
23
|
+
module: InjectionWidgetModule<any, any>
|
|
24
|
+
moduleId: string
|
|
25
|
+
key: string
|
|
26
|
+
placement?: LoadedInjectionWidget['placement']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useInjectionWidgets<TContext = unknown>(
|
|
30
|
+
spotId: InjectionSpotId | null | undefined,
|
|
31
|
+
options?: {
|
|
32
|
+
context?: TContext
|
|
33
|
+
triggerOnLoad?: boolean
|
|
34
|
+
onEvent?: (event: 'onLoad', widgetId: string) => void
|
|
35
|
+
}
|
|
36
|
+
) {
|
|
37
|
+
const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])
|
|
38
|
+
const [loading, setLoading] = React.useState(true)
|
|
39
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
40
|
+
const loadedRef = React.useRef(false)
|
|
41
|
+
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
if (!spotId) {
|
|
44
|
+
setWidgets([])
|
|
45
|
+
setLoading(false)
|
|
46
|
+
setError(null)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
let mounted = true
|
|
50
|
+
const load = async () => {
|
|
51
|
+
try {
|
|
52
|
+
setLoading(true)
|
|
53
|
+
setError(null)
|
|
54
|
+
const loaded = await loadInjectionWidgetsForSpot(spotId)
|
|
55
|
+
if (!mounted) return
|
|
56
|
+
const widgetList: LoadedWidget[] = loaded.map((w) => ({
|
|
57
|
+
widgetId: w.metadata.id,
|
|
58
|
+
module: w,
|
|
59
|
+
moduleId: w.moduleId,
|
|
60
|
+
key: w.key,
|
|
61
|
+
placement: w.placement,
|
|
62
|
+
}))
|
|
63
|
+
setWidgets(widgetList)
|
|
64
|
+
|
|
65
|
+
// Trigger onLoad for all widgets
|
|
66
|
+
if (!loadedRef.current && options?.triggerOnLoad) {
|
|
67
|
+
loadedRef.current = true
|
|
68
|
+
for (const widget of widgetList) {
|
|
69
|
+
if (widget.module.eventHandlers?.onLoad) {
|
|
70
|
+
try {
|
|
71
|
+
await widget.module.eventHandlers.onLoad(options.context as TContext)
|
|
72
|
+
options.onEvent?.('onLoad', widget.widgetId)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (!mounted) return
|
|
81
|
+
console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)
|
|
82
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
83
|
+
} finally {
|
|
84
|
+
if (mounted) setLoading(false)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
load()
|
|
88
|
+
return () => {
|
|
89
|
+
mounted = false
|
|
90
|
+
}
|
|
91
|
+
}, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent])
|
|
92
|
+
|
|
93
|
+
return { widgets, loading, error }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function InjectionSpot<TContext = unknown, TData = unknown>({
|
|
97
|
+
spotId,
|
|
98
|
+
context,
|
|
99
|
+
data,
|
|
100
|
+
onDataChange,
|
|
101
|
+
disabled,
|
|
102
|
+
onEvent,
|
|
103
|
+
widgetsOverride,
|
|
104
|
+
}: InjectionSpotProps<TContext, TData>) {
|
|
105
|
+
const useSpotId = widgetsOverride ? null : spotId
|
|
106
|
+
const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {
|
|
107
|
+
context,
|
|
108
|
+
triggerOnLoad: !widgetsOverride,
|
|
109
|
+
onEvent: onEvent ? (event, id) => onEvent(event, id) : undefined,
|
|
110
|
+
})
|
|
111
|
+
const effectiveWidgets = widgetsOverride ?? widgets
|
|
112
|
+
const effectiveLoading = widgetsOverride ? false : loading
|
|
113
|
+
const effectiveError = widgetsOverride ? null : error
|
|
114
|
+
|
|
115
|
+
if (effectiveLoading) {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (effectiveError) {
|
|
120
|
+
console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (effectiveWidgets.length === 0) {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
{effectiveWidgets.map((widget) => {
|
|
131
|
+
const { Widget } = widget.module
|
|
132
|
+
return (
|
|
133
|
+
<Widget
|
|
134
|
+
key={widget.widgetId}
|
|
135
|
+
context={context}
|
|
136
|
+
data={data}
|
|
137
|
+
onDataChange={onDataChange}
|
|
138
|
+
disabled={disabled}
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
})}
|
|
142
|
+
</>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Hook to trigger injection widget events imperatively
|
|
148
|
+
*/
|
|
149
|
+
export function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {
|
|
150
|
+
const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])
|
|
151
|
+
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
if (prefetchedWidgets && prefetchedWidgets.length) {
|
|
154
|
+
setWidgets(prefetchedWidgets)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
let mounted = true
|
|
158
|
+
const load = async () => {
|
|
159
|
+
try {
|
|
160
|
+
const loaded = await loadInjectionWidgetsForSpot(spotId)
|
|
161
|
+
if (!mounted) return
|
|
162
|
+
setWidgets(
|
|
163
|
+
loaded.map((w) => ({
|
|
164
|
+
widgetId: w.metadata.id,
|
|
165
|
+
module: w,
|
|
166
|
+
moduleId: w.moduleId,
|
|
167
|
+
key: w.key,
|
|
168
|
+
placement: w.placement,
|
|
169
|
+
}))
|
|
170
|
+
)
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
load()
|
|
176
|
+
return () => {
|
|
177
|
+
mounted = false
|
|
178
|
+
}
|
|
179
|
+
}, [spotId, prefetchedWidgets])
|
|
180
|
+
|
|
181
|
+
const triggerEvent = React.useCallback(
|
|
182
|
+
async (
|
|
183
|
+
event: keyof WidgetInjectionEventHandlers<TContext, TData>,
|
|
184
|
+
data: TData,
|
|
185
|
+
context: TContext
|
|
186
|
+
): Promise<{ ok: boolean; message?: string; fieldErrors?: Record<string, string> }> => {
|
|
187
|
+
const normalizeBeforeSave = (result: WidgetBeforeSaveResult): { ok: boolean; message?: string; fieldErrors?: Record<string, string> } => {
|
|
188
|
+
if (result === false) return { ok: false }
|
|
189
|
+
if (result === true || typeof result === 'undefined') return { ok: true }
|
|
190
|
+
if (result && typeof result === 'object') {
|
|
191
|
+
const ok = typeof result.ok === 'boolean' ? result.ok : true
|
|
192
|
+
const message = typeof result.message === 'string' ? result.message : undefined
|
|
193
|
+
const fieldErrors =
|
|
194
|
+
result.fieldErrors && typeof result.fieldErrors === 'object'
|
|
195
|
+
? Object.fromEntries(
|
|
196
|
+
Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),
|
|
197
|
+
)
|
|
198
|
+
: undefined
|
|
199
|
+
return { ok, message, fieldErrors }
|
|
200
|
+
}
|
|
201
|
+
return { ok: true }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const widget of widgets) {
|
|
205
|
+
const handler = widget.module.eventHandlers?.[event]
|
|
206
|
+
if (handler) {
|
|
207
|
+
try {
|
|
208
|
+
const result = await (handler as any)(data, context)
|
|
209
|
+
if (event === 'onBeforeSave') {
|
|
210
|
+
const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)
|
|
211
|
+
if (!normalized.ok) {
|
|
212
|
+
console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)
|
|
213
|
+
return normalized
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)
|
|
218
|
+
if (event === 'onBeforeSave') {
|
|
219
|
+
const message =
|
|
220
|
+
err instanceof Error
|
|
221
|
+
? err.message || 'Validation blocked'
|
|
222
|
+
: typeof err === 'string'
|
|
223
|
+
? err
|
|
224
|
+
: undefined
|
|
225
|
+
return { ok: false, message }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { ok: true }
|
|
231
|
+
},
|
|
232
|
+
[widgets]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return { triggerEvent, widgets }
|
|
236
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { InjectionSpot } from './InjectionSpot'
|
|
3
|
+
|
|
4
|
+
function normalizePath(path: string): string {
|
|
5
|
+
const trimmed = path.replace(/\?.*$/, '').replace(/\/+$/, '')
|
|
6
|
+
const withoutLeading = trimmed.startsWith('/') ? trimmed.slice(1) : trimmed
|
|
7
|
+
const safe = withoutLeading.length ? withoutLeading : 'root'
|
|
8
|
+
return safe.replace(/[^a-zA-Z0-9]+/g, ':')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function PageInjectionBoundary({
|
|
12
|
+
path,
|
|
13
|
+
context,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
path: string
|
|
17
|
+
context?: Record<string, unknown>
|
|
18
|
+
children: ReactNode
|
|
19
|
+
}) {
|
|
20
|
+
const handle = normalizePath(path || '/')
|
|
21
|
+
const beforeSpotId = `admin.page:${handle}:before`
|
|
22
|
+
const afterSpotId = `admin.page:${handle}:after`
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<InjectionSpot spotId={beforeSpotId} context={context ?? { path }} />
|
|
27
|
+
{children}
|
|
28
|
+
<InjectionSpot spotId={afterSpotId} context={context ?? { path }} />
|
|
29
|
+
</>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a standard injection spot ID for CRUD forms
|
|
5
|
+
* @param formName The name/identifier of the form (e.g., 'catalog.product', 'catalog.variant')
|
|
6
|
+
* @returns A standardized injection spot ID
|
|
7
|
+
*/
|
|
8
|
+
export function generateCrudFormInjectionSpotId(formName: string): InjectionSpotId {
|
|
9
|
+
return `crud-form:${formName}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate injection spot IDs for common CRUD form locations
|
|
14
|
+
*/
|
|
15
|
+
export const CrudFormInjectionSpots = {
|
|
16
|
+
/**
|
|
17
|
+
* Generate injection spot ID for before the form fields
|
|
18
|
+
*/
|
|
19
|
+
beforeFields: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:before-fields`,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate injection spot ID for after the form fields
|
|
23
|
+
*/
|
|
24
|
+
afterFields: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:after-fields`,
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate injection spot ID for the header area
|
|
28
|
+
*/
|
|
29
|
+
header: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:header`,
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate injection spot ID for the footer/actions area
|
|
33
|
+
*/
|
|
34
|
+
footer: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:footer`,
|
|
35
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ModuleInjectionWidgetEntry } from '@open-mercato/shared/modules/registry'
|
|
2
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
3
|
+
|
|
4
|
+
type Entry = ModuleInjectionWidgetEntry
|
|
5
|
+
|
|
6
|
+
// Registration pattern for publishable packages
|
|
7
|
+
let _injectionWidgetEntries: Entry[] | null = null
|
|
8
|
+
|
|
9
|
+
export function registerInjectionWidgets(entries: Entry[]) {
|
|
10
|
+
if (_injectionWidgetEntries !== null && process.env.NODE_ENV === 'development') {
|
|
11
|
+
console.debug('[Bootstrap] Injection widgets re-registered (this may occur during HMR)')
|
|
12
|
+
}
|
|
13
|
+
_injectionWidgetEntries = entries
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getInjectionWidgets(): Entry[] {
|
|
17
|
+
if (!_injectionWidgetEntries) {
|
|
18
|
+
// On client-side, bootstrap doesn't run - return empty array gracefully
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
throw new Error('[Bootstrap] Injection widgets not registered. Call registerInjectionWidgets() at bootstrap.')
|
|
23
|
+
}
|
|
24
|
+
return _injectionWidgetEntries
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let entriesPromise: Promise<Entry[]> | null = null
|
|
28
|
+
|
|
29
|
+
async function getEntries(): Promise<Entry[]> {
|
|
30
|
+
if (!entriesPromise) {
|
|
31
|
+
const promise = Promise.resolve().then(() => getInjectionWidgets())
|
|
32
|
+
entriesPromise = promise.catch((err) => {
|
|
33
|
+
// Clear cache on error so next call can retry after registration
|
|
34
|
+
if (entriesPromise === promise) {
|
|
35
|
+
entriesPromise = null
|
|
36
|
+
}
|
|
37
|
+
throw err
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
return entriesPromise
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type LoadedWidgetModule = InjectionWidgetModule<any, any>
|
|
44
|
+
|
|
45
|
+
const cache = new Map<string, Promise<LoadedWidgetModule>>()
|
|
46
|
+
|
|
47
|
+
async function findEntry(loaderKey: string): Promise<Entry | undefined> {
|
|
48
|
+
const entries = await getEntries()
|
|
49
|
+
return entries.find((entry) => entry.key === loaderKey)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function loadInjectionWidgetModule(loaderKey: string): Promise<LoadedWidgetModule | null> {
|
|
53
|
+
const entry = await findEntry(loaderKey)
|
|
54
|
+
if (!entry) return null
|
|
55
|
+
if (!cache.has(loaderKey)) {
|
|
56
|
+
cache.set(
|
|
57
|
+
loaderKey,
|
|
58
|
+
entry
|
|
59
|
+
.loader()
|
|
60
|
+
.then((mod) => {
|
|
61
|
+
const candidate = mod as LoadedWidgetModule
|
|
62
|
+
const maybeDefault = (mod as { default?: LoadedWidgetModule }).default
|
|
63
|
+
return maybeDefault ?? candidate
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
return cache.get(loaderKey) ?? null
|
|
68
|
+
}
|