@open-mercato/ui 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -2
- package/agentic/standalone-guide.md +1 -1
- package/build.mjs +2 -54
- package/dist/ai/AiChat.js +26 -7
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/LoopDisabledBanner.js +1 -1
- package/dist/ai/LoopDisabledBanner.js.map +2 -2
- package/dist/ai/parts/ConfirmationCard.js +4 -4
- package/dist/ai/parts/ConfirmationCard.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +2 -2
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +3 -3
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
- package/dist/backend/AppShell.js +43 -4
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/AuthSessionGuard.js +14 -0
- package/dist/backend/AuthSessionGuard.js.map +2 -2
- package/dist/backend/BackendChromeProvider.js +6 -1
- package/dist/backend/BackendChromeProvider.js.map +2 -2
- package/dist/backend/CrudForm.js +6 -4
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +20 -18
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/JsonBuilder.js +3 -3
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/NextStepCallout.js +4 -4
- package/dist/backend/NextStepCallout.js.map +1 -1
- package/dist/backend/PerspectiveSidebar.js +1 -1
- package/dist/backend/PerspectiveSidebar.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +2 -1
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/RowActions.js +2 -2
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +13 -1
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/crud/useGroupOrder.js +14 -21
- package/dist/backend/crud/useGroupOrder.js.map +2 -2
- package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
- package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
- package/dist/backend/dashboard/DashboardScreen.js +3 -3
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/detail/AddressTiles.js +4 -5
- package/dist/backend/detail/AddressTiles.js.map +2 -2
- package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
- package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
- package/dist/backend/detail/AttachmentsSection.js +1 -1
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +1 -1
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/detail/TagsSection.js +1 -1
- package/dist/backend/detail/TagsSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
- package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
- package/dist/backend/forms/FormHeader.js +1 -2
- package/dist/backend/forms/FormHeader.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +1 -25
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.js +3 -3
- package/dist/backend/icons/lucideRegistry.js.map +2 -2
- package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
- package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
- package/dist/backend/injection/spotIds.js +23 -18
- package/dist/backend/injection/spotIds.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js.map +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
- package/dist/backend/perspectiveState.js +34 -0
- package/dist/backend/perspectiveState.js.map +7 -0
- package/dist/backend/schedule/ScheduleAgenda.js +3 -3
- package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
- package/dist/backend/schedule/ScheduleGrid.js +3 -3
- package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
- package/dist/backend/schedule/ScheduleToolbar.js +87 -70
- package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/dist/backend/sidebar/customization-helpers.js +30 -0
- package/dist/backend/sidebar/customization-helpers.js.map +2 -2
- package/dist/backend/utils/apiCall.js +21 -1
- package/dist/backend/utils/apiCall.js.map +2 -2
- package/dist/backend/utils/customFieldDefs.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +10 -9
- package/dist/backend/utils/customFieldForms.js.map +2 -2
- package/dist/backend/utils/fetchAllPages.js +57 -0
- package/dist/backend/utils/fetchAllPages.js.map +7 -0
- package/dist/backend/utils/nav.js +5 -2
- package/dist/backend/utils/nav.js.map +2 -2
- package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
- package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +2 -1
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/dist/portal/PortalContext.js +1 -1
- package/dist/portal/PortalContext.js.map +2 -2
- package/dist/portal/hooks/portalBridgeStatus.js +25 -0
- package/dist/portal/hooks/portalBridgeStatus.js.map +7 -0
- package/dist/portal/hooks/usePortalEventBridge.js +6 -0
- package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
- package/dist/portal/hooks/usePortalNotifications.js +42 -4
- package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
- package/dist/portal/utils/nav.js +2 -3
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/ErrorNotice.js +1 -1
- package/dist/primitives/ErrorNotice.js.map +2 -2
- package/dist/primitives/Notice.js +1 -1
- package/dist/primitives/Notice.js.map +1 -1
- package/dist/primitives/button.js +9 -4
- package/dist/primitives/button.js.map +2 -2
- package/dist/theme/ThemeProvider.js +1 -1
- package/dist/theme/ThemeProvider.js.map +2 -2
- package/dist/theme/index.js +3 -0
- package/dist/theme/index.js.map +2 -2
- package/dist/theme/theme-init-script.js +17 -0
- package/dist/theme/theme-init-script.js.map +7 -0
- package/figma/alert.figma.tsx +35 -0
- package/figma/badge.figma.tsx +33 -0
- package/figma/button.figma.tsx +33 -0
- package/figma/checkbox.figma.tsx +28 -0
- package/figma/drawer.figma.tsx +40 -0
- package/figma/input-variants.figma.tsx +58 -0
- package/figma/radio.figma.tsx +16 -0
- package/figma/select.figma.tsx +24 -0
- package/figma/status-badge.figma.tsx +24 -0
- package/figma/switch.figma.tsx +28 -0
- package/figma/tabs.figma.tsx +22 -0
- package/figma/tag.figma.tsx +27 -0
- package/figma.config.json +6 -0
- package/jest.config.cjs +2 -1
- package/package.json +21 -18
- package/scripts/lucideRegistrySource.cjs +133 -0
- package/src/ai/AiChat.tsx +36 -7
- package/src/ai/LoopDisabledBanner.tsx +1 -1
- package/src/ai/__tests__/AiChat.test.tsx +31 -0
- package/src/ai/parts/ConfirmationCard.tsx +4 -4
- package/src/ai/parts/FieldDiffCard.tsx +2 -2
- package/src/ai/parts/MutationResultCard.tsx +3 -3
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
- package/src/backend/AppShell.tsx +55 -4
- package/src/backend/AuthSessionGuard.tsx +29 -0
- package/src/backend/BackendChromeProvider.tsx +19 -1
- package/src/backend/CrudForm.tsx +18 -4
- package/src/backend/DataTable.tsx +36 -19
- package/src/backend/JsonBuilder.tsx +3 -3
- package/src/backend/NextStepCallout.tsx +4 -4
- package/src/backend/PerspectiveSidebar.tsx +1 -1
- package/src/backend/ProfileDropdown.tsx +1 -0
- package/src/backend/RowActions.tsx +2 -2
- package/src/backend/__tests__/AppShell.test.tsx +65 -7
- package/src/backend/__tests__/AuthSessionGuard.test.tsx +52 -0
- package/src/backend/__tests__/BackendChromeProvider.test.tsx +98 -2
- package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +158 -0
- package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
- package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
- package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +62 -1
- package/src/backend/__tests__/nav-utils.test.ts +139 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -1
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +68 -1
- package/src/backend/crud/__tests__/useGroupOrder.test.ts +39 -1
- package/src/backend/crud/useGroupOrder.ts +30 -25
- package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
- package/src/backend/dashboard/DashboardScreen.tsx +3 -3
- package/src/backend/detail/AddressTiles.tsx +3 -6
- package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/AttachmentsSection.tsx +1 -1
- package/src/backend/detail/NotesSection.tsx +1 -1
- package/src/backend/detail/TagsSection.tsx +1 -1
- package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
- package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
- package/src/backend/forms/FormHeader.tsx +1 -2
- package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
- package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
- package/src/backend/icons/lucideRegistry.ts +2 -2
- package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
- package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +38 -0
- package/src/backend/injection/spotIds.ts +23 -18
- package/src/backend/inputs/ComboboxInput.tsx +1 -1
- package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
- package/src/backend/perspectiveState.ts +44 -0
- package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
- package/src/backend/schedule/ScheduleGrid.tsx +3 -3
- package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +42 -3
- package/src/backend/sidebar/__tests__/group-visibility.test.ts +131 -0
- package/src/backend/sidebar/customization-helpers.ts +47 -0
- package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
- package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +95 -0
- package/src/backend/utils/__tests__/fetchAllPages.test.ts +103 -0
- package/src/backend/utils/apiCall.ts +29 -1
- package/src/backend/utils/customFieldDefs.ts +1 -0
- package/src/backend/utils/customFieldForms.ts +15 -11
- package/src/backend/utils/fetchAllPages.ts +93 -0
- package/src/backend/utils/nav.ts +22 -2
- package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
- package/src/frontend/LanguageSwitcher.tsx +1 -0
- package/src/portal/PortalContext.tsx +1 -1
- package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +66 -0
- package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +155 -0
- package/src/portal/hooks/portalBridgeStatus.ts +29 -0
- package/src/portal/hooks/usePortalEventBridge.ts +6 -0
- package/src/portal/hooks/usePortalNotifications.ts +55 -12
- package/src/portal/utils/__tests__/nav.test.ts +2 -2
- package/src/portal/utils/nav.ts +3 -4
- package/src/primitives/ErrorNotice.tsx +1 -1
- package/src/primitives/Notice.tsx +5 -5
- package/src/primitives/__tests__/button.test.tsx +70 -0
- package/src/primitives/button.tsx +10 -4
- package/src/theme/ThemeProvider.tsx +1 -2
- package/src/theme/__tests__/theme-init-script.test.ts +70 -0
- package/src/theme/index.ts +1 -0
- package/src/theme/theme-init-script.ts +23 -0
|
@@ -357,7 +357,7 @@ export function DashboardScreen() {
|
|
|
357
357
|
|
|
358
358
|
if (error && layout.length === 0) {
|
|
359
359
|
return (
|
|
360
|
-
<Alert
|
|
360
|
+
<Alert status="error">
|
|
361
361
|
<AlertTitle>{t('dashboard.unavailable')}</AlertTitle>
|
|
362
362
|
<AlertDescription>{error}</AlertDescription>
|
|
363
363
|
<div className="mt-2"><Button variant="outline" onClick={handleRefresh}>{t('dashboard.retry')}</Button></div>
|
|
@@ -367,7 +367,7 @@ export function DashboardScreen() {
|
|
|
367
367
|
|
|
368
368
|
if (!hasRegisteredWidgets && layout.length === 0) {
|
|
369
369
|
return (
|
|
370
|
-
<Alert
|
|
370
|
+
<Alert status="information">
|
|
371
371
|
<AlertTitle>{t('dashboard.empty.noWidgets.title', 'No dashboard widgets yet')}</AlertTitle>
|
|
372
372
|
<AlertDescription>
|
|
373
373
|
{t(
|
|
@@ -403,7 +403,7 @@ export function DashboardScreen() {
|
|
|
403
403
|
</div>
|
|
404
404
|
|
|
405
405
|
{error && layout.length > 0 && (
|
|
406
|
-
<Alert
|
|
406
|
+
<Alert status="error">
|
|
407
407
|
<AlertTitle>{t('dashboard.error.partial')}</AlertTitle>
|
|
408
408
|
<AlertDescription>{error}</AlertDescription>
|
|
409
409
|
<div className="mt-2"><Button variant="ghost" onClick={handleRefresh}>{t('dashboard.error.reload')}</Button></div>
|
|
@@ -7,7 +7,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
7
7
|
import { TabEmptyState } from '@open-mercato/ui/backend/detail'
|
|
8
8
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
9
9
|
import { normalizeCoordinateInput, validateCoordinateInput } from '@open-mercato/shared/lib/location/coordinates'
|
|
10
|
-
import { AddressView,
|
|
10
|
+
import { AddressView, type AddressFormatStrategy } from './addressFormat'
|
|
11
11
|
import AddressEditor, { type AddressTypesAdapter } from './AddressEditor'
|
|
12
12
|
import {
|
|
13
13
|
Dialog,
|
|
@@ -176,7 +176,7 @@ export function AddressTiles<C = unknown>({
|
|
|
176
176
|
t,
|
|
177
177
|
emptyLabel,
|
|
178
178
|
isSubmitting = false,
|
|
179
|
-
gridClassName = 'grid grid-cols-
|
|
179
|
+
gridClassName = 'grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2 sm:gap-4',
|
|
180
180
|
hideAddButton = false,
|
|
181
181
|
onAddActionChange,
|
|
182
182
|
emptyStateTitle,
|
|
@@ -480,7 +480,7 @@ export function AddressTiles<C = unknown>({
|
|
|
480
480
|
addressTypesAdapter={addressTypesAdapter}
|
|
481
481
|
addressTypesContext={addressTypesContext}
|
|
482
482
|
/>
|
|
483
|
-
{generalError ? <p className="text-xs text-
|
|
483
|
+
{generalError ? <p className="text-xs text-status-error-text">{generalError}</p> : null}
|
|
484
484
|
<div className="flex flex-wrap justify-end gap-2">
|
|
485
485
|
<Button type="button" variant="outline" onClick={handleCancel} disabled={disableActions}>
|
|
486
486
|
{label('cancel', 'Cancel')}
|
|
@@ -569,9 +569,6 @@ export function AddressTiles<C = unknown>({
|
|
|
569
569
|
</p>
|
|
570
570
|
) : null}
|
|
571
571
|
<AddressView address={address} format={format} className="text-sm text-foreground" />
|
|
572
|
-
<p className="text-xs text-muted-foreground">
|
|
573
|
-
{formatAddressString(address, format)}
|
|
574
|
-
</p>
|
|
575
572
|
</div>
|
|
576
573
|
<div className="flex items-center gap-1 opacity-100 md:opacity-0 transition-opacity md:group-hover:opacity-100 focus-within:opacity-100">
|
|
577
574
|
<Button
|
|
@@ -38,7 +38,7 @@ export function AttachmentDeleteDialog({ open, onOpenChange, fileName, onConfirm
|
|
|
38
38
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isDeleting}>
|
|
39
39
|
{t('attachments.library.metadata.cancel', 'Cancel')}
|
|
40
40
|
</Button>
|
|
41
|
-
<Button type="button" variant="destructive" onClick={onConfirm} disabled={isDeleting}>
|
|
41
|
+
<Button type="button" variant="destructive-solid" onClick={onConfirm} disabled={isDeleting}>
|
|
42
42
|
{t('attachments.library.actions.delete', 'Delete')}
|
|
43
43
|
</Button>
|
|
44
44
|
</DialogFooter>
|
|
@@ -256,7 +256,7 @@ function AttachmentsSectionImpl({
|
|
|
256
256
|
</div>
|
|
257
257
|
)}
|
|
258
258
|
|
|
259
|
-
{error ? <p className="text-xs font-medium text-
|
|
259
|
+
{error ? <p className="text-xs font-medium text-status-error-text">{error}</p> : null}
|
|
260
260
|
|
|
261
261
|
{loading ? (
|
|
262
262
|
<div className="text-sm text-muted-foreground">{t('attachments.library.loading', 'Loading attachments…')}</div>
|
|
@@ -1256,7 +1256,7 @@ function NotesSectionImpl<C = unknown>({
|
|
|
1256
1256
|
editorWrapperClassName="w-full rounded-md border border-muted-foreground/20 bg-background p-2"
|
|
1257
1257
|
remarkPlugins={markdownPlugins}
|
|
1258
1258
|
/>
|
|
1259
|
-
{contentError ? <p className="text-xs text-
|
|
1259
|
+
{contentError ? <p className="text-xs text-status-error-text">{contentError}</p> : null}
|
|
1260
1260
|
<div className="flex flex-wrap items-center gap-2">
|
|
1261
1261
|
<Button type="button" size="sm" onClick={handleContentSave} disabled={contentSavingId === note.id}>
|
|
1262
1262
|
{contentSavingId === note.id ? (
|
|
@@ -387,7 +387,7 @@ function TagsSectionImpl({
|
|
|
387
387
|
loadSuggestions={loadSuggestions}
|
|
388
388
|
autoFocus={!autoSave}
|
|
389
389
|
/>
|
|
390
|
-
{error ? <p className="text-xs text-
|
|
390
|
+
{error ? <p className="text-xs text-status-error-text">{error}</p> : null}
|
|
391
391
|
{autoSave ? null : (
|
|
392
392
|
<div className="flex items-center gap-2 mt-3 mb-2">
|
|
393
393
|
<Button type="button" size="sm" onClick={handleSave} disabled={saving || isSubmitting}>
|
|
@@ -75,4 +75,25 @@ describe('AttachmentAssignmentsEditor (metadata dialog)', () => {
|
|
|
75
75
|
fireEvent.click(screen.getByRole('button', { name: 'Remove assignment' }))
|
|
76
76
|
expect(screen.queryByPlaceholderText('catalog.product')).toBeNull()
|
|
77
77
|
})
|
|
78
|
+
|
|
79
|
+
it('keeps the surviving assignment values after removing a middle row', () => {
|
|
80
|
+
render(
|
|
81
|
+
<EditorHarness
|
|
82
|
+
initial={[
|
|
83
|
+
{ type: 'catalog.product', id: 'p-1', href: '', label: '' },
|
|
84
|
+
{ type: 'sales.order', id: 'o-2', href: '', label: '' },
|
|
85
|
+
{ type: 'customers.person', id: 'c-3', href: '', label: '' },
|
|
86
|
+
]}
|
|
87
|
+
/>,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
fireEvent.click(screen.getAllByRole('button', { name: labels.remove })[1])
|
|
91
|
+
|
|
92
|
+
const typeInputs = screen.getAllByPlaceholderText('catalog.product') as HTMLInputElement[]
|
|
93
|
+
const idInputs = screen.getAllByPlaceholderText('Record ID') as HTMLInputElement[]
|
|
94
|
+
expect(typeInputs[0]).toHaveValue('catalog.product')
|
|
95
|
+
expect(idInputs[0]).toHaveValue('p-1')
|
|
96
|
+
expect(typeInputs[1]).toHaveValue('customers.person')
|
|
97
|
+
expect(idInputs[1]).toHaveValue('c-3')
|
|
98
|
+
})
|
|
78
99
|
})
|
|
@@ -444,7 +444,7 @@ export function AdvancedFilterPanel(props: AdvancedFilterPanelProps) {
|
|
|
444
444
|
)}
|
|
445
445
|
{props.pendingErrors.length > 0 ? (
|
|
446
446
|
<div className="p-3 border-t border-border" data-testid="filter-validation-banner">
|
|
447
|
-
<Alert
|
|
447
|
+
<Alert status="error">
|
|
448
448
|
<AlertDescription>
|
|
449
449
|
{props.pendingErrors.length === 1
|
|
450
450
|
? t('ui.advancedFilter.banner.incomplete', '{count} filter is incomplete — filters won\'t apply until all values are picked.', { count: 1 })
|
|
@@ -181,11 +181,10 @@ function DetailHeader({
|
|
|
181
181
|
{onDelete ? (
|
|
182
182
|
<Button
|
|
183
183
|
type="button"
|
|
184
|
-
variant="
|
|
184
|
+
variant="destructive"
|
|
185
185
|
size="sm"
|
|
186
186
|
onClick={onDelete}
|
|
187
187
|
disabled={isDeleting}
|
|
188
|
-
className="text-red-600 border-red-200 hover:bg-red-50 rounded"
|
|
189
188
|
>
|
|
190
189
|
{isDeleting ? (
|
|
191
190
|
<Loader2 className="size-4 mr-2 animate-spin" />
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { Project, ScriptKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from 'ts-morph'
|
|
5
|
+
import { buildLucideRegistrySource } from '../../../../scripts/lucideRegistrySource.cjs'
|
|
6
|
+
|
|
7
|
+
const packageDir = join(__dirname, '..', '..', '..', '..')
|
|
8
|
+
const iconsDir = join(packageDir, 'src', 'backend', 'icons')
|
|
9
|
+
|
|
10
|
+
type ResolvedIcon = { kebab: string; exportName: string }
|
|
11
|
+
|
|
12
|
+
const populatedFixture: ResolvedIcon[] = [
|
|
13
|
+
{ kebab: 'bell', exportName: 'Bell' },
|
|
14
|
+
{ kebab: 'alert-circle', exportName: 'AlertCircle' },
|
|
15
|
+
{ kebab: 'bar-chart-2', exportName: 'BarChart2' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const duplicateExportFixture: ResolvedIcon[] = [
|
|
19
|
+
{ kebab: 'trash', exportName: 'Trash' },
|
|
20
|
+
{ kebab: 'trash-can', exportName: 'Trash' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function parse(source: string): SourceFile {
|
|
24
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
25
|
+
return project.createSourceFile('lucideRegistry.generated.tsx', source, {
|
|
26
|
+
overwrite: true,
|
|
27
|
+
scriptKind: ScriptKind.TSX,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function syntacticDiagnostics(source: string): string[] {
|
|
32
|
+
const sourceFile = parse(source)
|
|
33
|
+
return sourceFile
|
|
34
|
+
.getProject()
|
|
35
|
+
.getProgram()
|
|
36
|
+
.getSyntacticDiagnostics(sourceFile)
|
|
37
|
+
.map((diagnostic) => `${diagnostic.getLineNumber() ?? 0}: ${diagnostic.getMessageText()}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registryObject(source: string): ObjectLiteralExpression {
|
|
41
|
+
const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
|
|
42
|
+
return declaration.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function registryEntries(source: string): Record<string, string> {
|
|
46
|
+
const entries: Record<string, string> = {}
|
|
47
|
+
for (const property of registryObject(source).getProperties()) {
|
|
48
|
+
const assignment = property.asKindOrThrow(SyntaxKind.PropertyAssignment)
|
|
49
|
+
entries[assignment.getSymbolOrThrow().getName()] = assignment.getInitializerOrThrow().getText()
|
|
50
|
+
}
|
|
51
|
+
return entries
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function valueImportNames(source: string, moduleSpecifier: string): string[] {
|
|
55
|
+
return parse(source)
|
|
56
|
+
.getImportDeclarations()
|
|
57
|
+
.filter((declaration) => declaration.getModuleSpecifierValue() === moduleSpecifier)
|
|
58
|
+
.filter((declaration) => !declaration.isTypeOnly())
|
|
59
|
+
.flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('buildLucideRegistrySource', () => {
|
|
63
|
+
it('maps every discovered kebab name to its lucide export', () => {
|
|
64
|
+
expect(registryEntries(buildLucideRegistrySource(populatedFixture))).toEqual({
|
|
65
|
+
'alert-circle': 'AlertCircle',
|
|
66
|
+
'bar-chart-2': 'BarChart2',
|
|
67
|
+
bell: 'Bell',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('emits the registry keys sorted by kebab name', () => {
|
|
72
|
+
const keys = registryObject(buildLucideRegistrySource(populatedFixture))
|
|
73
|
+
.getProperties()
|
|
74
|
+
.map((property) => property.asKindOrThrow(SyntaxKind.PropertyAssignment).getSymbolOrThrow().getName())
|
|
75
|
+
expect(keys).toEqual(['alert-circle', 'bar-chart-2', 'bell'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('imports each lucide export exactly once', () => {
|
|
79
|
+
expect(valueImportNames(buildLucideRegistrySource(populatedFixture), 'lucide-react')).toEqual([
|
|
80
|
+
'AlertCircle',
|
|
81
|
+
'BarChart2',
|
|
82
|
+
'Bell',
|
|
83
|
+
])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('quotes keys that are not valid identifiers', () => {
|
|
87
|
+
const property = registryObject(buildLucideRegistrySource(populatedFixture))
|
|
88
|
+
.getPropertyOrThrow("'bar-chart-2'")
|
|
89
|
+
.asKindOrThrow(SyntaxKind.PropertyAssignment)
|
|
90
|
+
expect(property.getNameNode().getKind()).toBe(SyntaxKind.StringLiteral)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('de-duplicates the value import when two names share one export', () => {
|
|
94
|
+
const source = buildLucideRegistrySource(duplicateExportFixture)
|
|
95
|
+
expect(valueImportNames(source, 'lucide-react')).toEqual(['Trash'])
|
|
96
|
+
expect(registryEntries(source)).toEqual({ trash: 'Trash', 'trash-can': 'Trash' })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('emits a valid module with an empty registry when no icons are discovered', () => {
|
|
100
|
+
const source = buildLucideRegistrySource([])
|
|
101
|
+
expect(registryObject(source).getProperties()).toHaveLength(0)
|
|
102
|
+
expect(valueImportNames(source, 'lucide-react')).toEqual([])
|
|
103
|
+
expect(source).not.toContain("import {} from 'lucide-react'")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('keeps the registry a plain mutable object literal', () => {
|
|
107
|
+
const source = buildLucideRegistrySource(populatedFixture)
|
|
108
|
+
const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
|
|
109
|
+
expect(declaration.getTypeNodeOrThrow().getText()).toBe('Record<string, LucideIcon>')
|
|
110
|
+
expect(declaration.getVariableStatementOrThrow().isExported()).toBe(true)
|
|
111
|
+
expect(source).not.toContain('as const')
|
|
112
|
+
expect(source).not.toContain('Object.freeze')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('keeps the LucideIcon import type-only', () => {
|
|
116
|
+
const typeOnly = parse(buildLucideRegistrySource(populatedFixture))
|
|
117
|
+
.getImportDeclarations()
|
|
118
|
+
.filter((declaration) => declaration.isTypeOnly())
|
|
119
|
+
.flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
|
|
120
|
+
expect(typeOnly).toEqual(['LucideIcon'])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it.each([
|
|
124
|
+
['populated', populatedFixture],
|
|
125
|
+
['empty', [] as ResolvedIcon[]],
|
|
126
|
+
['duplicate exports', duplicateExportFixture],
|
|
127
|
+
])('emits %s output with zero syntactic diagnostics', (_label, fixture) => {
|
|
128
|
+
expect(syntacticDiagnostics(buildLucideRegistrySource(fixture))).toEqual([])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('fails loudly when an export name would produce an unparseable module', () => {
|
|
132
|
+
expect(() => buildLucideRegistrySource([{ kebab: 'broken', exportName: 'not a valid export' }])).toThrow(
|
|
133
|
+
/not syntactically valid/
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('lucideRegistry barrel', () => {
|
|
139
|
+
it('keeps its public export list unchanged', () => {
|
|
140
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
141
|
+
const barrel = project.createSourceFile(
|
|
142
|
+
'lucideRegistry.ts',
|
|
143
|
+
readFileSync(join(iconsDir, 'lucideRegistry.ts'), 'utf-8'),
|
|
144
|
+
{ overwrite: true }
|
|
145
|
+
)
|
|
146
|
+
expect([...barrel.getExportedDeclarations().keys()].sort()).toEqual([
|
|
147
|
+
'LUCIDE_ICON_REGISTRY',
|
|
148
|
+
'registerAdditionalIcons',
|
|
149
|
+
'resolveRegisteredLucideIcon',
|
|
150
|
+
'resolveRegisteredLucideIconNode',
|
|
151
|
+
])
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('lucideRegistry.generated importers', () => {
|
|
156
|
+
it('is imported only from within src/backend/icons', () => {
|
|
157
|
+
const repoRoot = join(packageDir, '..', '..')
|
|
158
|
+
let matches: string[] = []
|
|
159
|
+
try {
|
|
160
|
+
matches = execFileSync(
|
|
161
|
+
'git',
|
|
162
|
+
['grep', '-l', 'lucideRegistry.generated', '--', '*.ts', '*.tsx'],
|
|
163
|
+
{ cwd: repoRoot, encoding: 'utf-8' }
|
|
164
|
+
)
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const status = (error as { status?: number }).status
|
|
169
|
+
if (status !== 1) throw error
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const outsideIconsFolder = matches.filter(
|
|
173
|
+
(path) => !path.startsWith('packages/ui/src/backend/icons/')
|
|
174
|
+
)
|
|
175
|
+
expect(outsideIconsFolder).toEqual([])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
|
|
3
3
|
// Generated from discovered icon string usages across the repo.
|
|
4
4
|
|
|
5
|
-
import type
|
|
6
|
-
import type { LucideIcon } from 'lucide-react'
|
|
5
|
+
import type { LucideIcon } from 'lucide-react';
|
|
7
6
|
import {
|
|
8
7
|
Activity,
|
|
9
8
|
AlertCircle,
|
|
@@ -150,7 +149,7 @@ import {
|
|
|
150
149
|
Wrench,
|
|
151
150
|
X,
|
|
152
151
|
XCircle,
|
|
153
|
-
} from 'lucide-react'
|
|
152
|
+
} from 'lucide-react';
|
|
154
153
|
|
|
155
154
|
export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
156
155
|
'activity': Activity,
|
|
@@ -298,37 +297,4 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
298
297
|
'wrench': Wrench,
|
|
299
298
|
'x': X,
|
|
300
299
|
'x-circle': XCircle,
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function normalizeKebabIconName(input: string): string {
|
|
304
|
-
const trimmed = input.trim()
|
|
305
|
-
if (!trimmed) return ''
|
|
306
|
-
const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
|
|
307
|
-
if (!withoutPrefix) return ''
|
|
308
|
-
if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
|
|
309
|
-
return withoutPrefix
|
|
310
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
311
|
-
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
312
|
-
.toLowerCase()
|
|
313
|
-
}
|
|
314
|
-
return withoutPrefix
|
|
315
|
-
.replace(/[_\s]+/g, '-')
|
|
316
|
-
.replace(/-+/g, '-')
|
|
317
|
-
.toLowerCase()
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
|
|
321
|
-
if (!name) return null
|
|
322
|
-
const normalized = normalizeKebabIconName(name)
|
|
323
|
-
if (!normalized) return null
|
|
324
|
-
return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function resolveRegisteredLucideIconNode(
|
|
328
|
-
name: string | undefined,
|
|
329
|
-
className: string
|
|
330
|
-
): React.ReactNode | null {
|
|
331
|
-
const Icon = resolveRegisteredLucideIcon(name)
|
|
332
|
-
if (!Icon) return null
|
|
333
|
-
return <Icon className={className} />
|
|
334
|
-
}
|
|
300
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
2
|
resolveRegisteredLucideIcon,
|
|
3
3
|
resolveRegisteredLucideIconNode,
|
|
4
|
-
|
|
5
|
-
} from './lucideRegistry.generated'
|
|
4
|
+
} from './lucideRegistryRuntime'
|
|
5
|
+
export { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
6
6
|
|
|
7
7
|
import type { LucideIcon } from 'lucide-react'
|
|
8
8
|
import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import type { LucideIcon } from 'lucide-react'
|
|
3
|
+
import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
4
|
+
|
|
5
|
+
function normalizeKebabIconName(input: string): string {
|
|
6
|
+
const trimmed = input.trim()
|
|
7
|
+
if (!trimmed) return ''
|
|
8
|
+
const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
|
|
9
|
+
if (!withoutPrefix) return ''
|
|
10
|
+
if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
|
|
11
|
+
return withoutPrefix
|
|
12
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
13
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
}
|
|
16
|
+
return withoutPrefix
|
|
17
|
+
.replace(/[_\s]+/g, '-')
|
|
18
|
+
.replace(/-+/g, '-')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
|
|
23
|
+
if (!name) return null
|
|
24
|
+
const normalized = normalizeKebabIconName(name)
|
|
25
|
+
if (!normalized) return null
|
|
26
|
+
return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveRegisteredLucideIconNode(
|
|
30
|
+
name: string | undefined,
|
|
31
|
+
className: string
|
|
32
|
+
): React.ReactNode | null {
|
|
33
|
+
const Icon = resolveRegisteredLucideIcon(name)
|
|
34
|
+
if (!Icon) return null
|
|
35
|
+
return <Icon className={className} />
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CRUD_FORM_EXTENSION_SURFACES,
|
|
3
|
+
DATA_TABLE_EXTENSION_SURFACES,
|
|
4
|
+
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
5
|
+
import { CrudFormInjectionSpots, DataTableInjectionSpots } from '../spotIds'
|
|
6
|
+
|
|
7
|
+
describe('standard extension point spot IDs', () => {
|
|
8
|
+
it('keeps every bound DataTable suffix aligned with the runtime helper', () => {
|
|
9
|
+
const tableId = 'catalog.products.list'
|
|
10
|
+
const boundSuffixes = DATA_TABLE_EXTENSION_SURFACES
|
|
11
|
+
.filter((surface) => surface.bound && surface.suffix)
|
|
12
|
+
.map((surface) => surface.suffix)
|
|
13
|
+
|
|
14
|
+
expect(boundSuffixes).toEqual([
|
|
15
|
+
'header',
|
|
16
|
+
'footer',
|
|
17
|
+
'toolbar',
|
|
18
|
+
'search-trailing',
|
|
19
|
+
'columns',
|
|
20
|
+
'row-actions',
|
|
21
|
+
'bulk-actions',
|
|
22
|
+
'filters',
|
|
23
|
+
])
|
|
24
|
+
expect(DataTableInjectionSpots.emptyState(tableId)).toBe('data-table:catalog.products.list:empty-state')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('keeps bound CrudForm spots distinct from helper-only spots', () => {
|
|
28
|
+
const entityId = 'catalog.product'
|
|
29
|
+
const boundKeys = CRUD_FORM_EXTENSION_SURFACES
|
|
30
|
+
.filter((surface) => surface.bound)
|
|
31
|
+
.map((surface) => surface.key)
|
|
32
|
+
|
|
33
|
+
expect(boundKeys).toEqual(['base', 'header', 'fields', 'replacement'])
|
|
34
|
+
expect(CrudFormInjectionSpots.base(entityId)).toBe('crud-form:catalog.product')
|
|
35
|
+
expect(CrudFormInjectionSpots.header(entityId)).toBe('crud-form:catalog.product:header')
|
|
36
|
+
expect(CrudFormInjectionSpots.fields(entityId)).toBe('crud-form:catalog.product:fields')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import {
|
|
3
|
+
crudFormExtensionSpotId,
|
|
4
|
+
dataTableExtensionSpotId,
|
|
5
|
+
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
2
6
|
|
|
3
7
|
export const BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID: InjectionSpotId = 'backend:record:current'
|
|
4
8
|
export const BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:top'
|
|
@@ -18,32 +22,33 @@ export const GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID: InjectionSpotId
|
|
|
18
22
|
|
|
19
23
|
// Standardized pattern helpers
|
|
20
24
|
export const CrudFormInjectionSpots = {
|
|
21
|
-
base: (entityId: string): InjectionSpotId =>
|
|
22
|
-
beforeFields: (entityId: string): InjectionSpotId =>
|
|
23
|
-
afterFields: (entityId: string): InjectionSpotId =>
|
|
24
|
-
header: (entityId: string): InjectionSpotId =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
base: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId),
|
|
26
|
+
beforeFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'before-fields'),
|
|
27
|
+
afterFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'after-fields'),
|
|
28
|
+
header: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'header'),
|
|
29
|
+
fields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'fields'),
|
|
30
|
+
footer: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'footer'),
|
|
31
|
+
sidebar: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'sidebar'),
|
|
32
|
+
group: (entityId: string, groupId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `group:${groupId}`),
|
|
33
|
+
fieldBefore: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:before`),
|
|
34
|
+
fieldAfter: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:after`),
|
|
30
35
|
} as const
|
|
31
36
|
|
|
32
37
|
export const DataTableInjectionSpots = {
|
|
33
|
-
header: (tableId: string): InjectionSpotId =>
|
|
34
|
-
footer: (tableId: string): InjectionSpotId =>
|
|
35
|
-
toolbar: (tableId: string): InjectionSpotId =>
|
|
38
|
+
header: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'header'),
|
|
39
|
+
footer: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'footer'),
|
|
40
|
+
toolbar: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'toolbar'),
|
|
36
41
|
// Slot rendered immediately after the search input on the same row as the
|
|
37
42
|
// FilterBar — intended for compact, icon-sized triggers (AI assistants,
|
|
38
43
|
// saved view shortcuts, etc.). Hosts pass the resolved spot ID through to
|
|
39
44
|
// FilterBar's `searchTrailing` prop. Stays empty when the table has no
|
|
40
45
|
// search input.
|
|
41
|
-
searchTrailing: (tableId: string): InjectionSpotId =>
|
|
42
|
-
emptyState: (tableId: string): InjectionSpotId =>
|
|
43
|
-
columns: (tableId: string): InjectionSpotId =>
|
|
44
|
-
rowActions: (tableId: string): InjectionSpotId =>
|
|
45
|
-
bulkActions: (tableId: string): InjectionSpotId =>
|
|
46
|
-
filters: (tableId: string): InjectionSpotId =>
|
|
46
|
+
searchTrailing: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'search-trailing'),
|
|
47
|
+
emptyState: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'empty-state'),
|
|
48
|
+
columns: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'columns'),
|
|
49
|
+
rowActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'row-actions'),
|
|
50
|
+
bulkActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'bulk-actions'),
|
|
51
|
+
filters: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'filters'),
|
|
47
52
|
} as const
|
|
48
53
|
|
|
49
54
|
// Portal chrome spot IDs (FROZEN once shipped)
|
|
@@ -241,7 +241,7 @@ export function ComboboxInput({
|
|
|
241
241
|
.finally(() => { if (!cancelled) setLoading(false) })
|
|
242
242
|
}
|
|
243
243
|
return () => { cancelled = true }
|
|
244
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
244
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- resolveDescription intentionally excluded:
|
|
245
245
|
// including it would re-run the effect on every render when the prop is an inline function
|
|
246
246
|
}, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])
|
|
247
247
|
|
|
@@ -141,7 +141,7 @@ export function EmailThreadsPanel({
|
|
|
141
141
|
{!canCompose && composeDisabledHint ? <div>{composeDisabledHint}</div> : null}
|
|
142
142
|
|
|
143
143
|
{error ? (
|
|
144
|
-
<Alert
|
|
144
|
+
<Alert status="error">
|
|
145
145
|
<AlertDescription>{error}</AlertDescription>
|
|
146
146
|
</Alert>
|
|
147
147
|
) : null}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const PERSPECTIVE_COOKIE_PREFIX = 'om_table_perspective'
|
|
2
|
+
export const PERSPECTIVE_STORAGE_PREFIX = 'om_table_perspective_snapshot'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Purge every browser-local DataTable perspective snapshot and active-view cookie
|
|
6
|
+
* across all tables in this origin. Snapshots persist the unsaved/live table state
|
|
7
|
+
* (column widths, order, visibility) keyed only by `tableId`, so they are shared by
|
|
8
|
+
* every account that signs in on the same browser profile. Call this at the auth
|
|
9
|
+
* identity boundary so one user's unsaved layout never carries over to a different
|
|
10
|
+
* user — including a different tenant — reusing the same browser tab (#4185).
|
|
11
|
+
*
|
|
12
|
+
* Lives here rather than in DataTable so the backend shell can call it without
|
|
13
|
+
* pulling the whole table implementation into the bundle of every backend page.
|
|
14
|
+
* `@open-mercato/ui/backend/DataTable` re-exports it, which is the published
|
|
15
|
+
* import path and must stay stable.
|
|
16
|
+
*/
|
|
17
|
+
export function clearAllPerspectiveState(): void {
|
|
18
|
+
if (typeof window !== 'undefined') {
|
|
19
|
+
try {
|
|
20
|
+
const storage = window.localStorage
|
|
21
|
+
const staleKeys: string[] = []
|
|
22
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
23
|
+
const key = storage.key(index)
|
|
24
|
+
if (key && key.startsWith(`${PERSPECTIVE_STORAGE_PREFIX}:`)) staleKeys.push(key)
|
|
25
|
+
}
|
|
26
|
+
for (const key of staleKeys) storage.removeItem(key)
|
|
27
|
+
} catch {
|
|
28
|
+
// private mode / quota errors are non-fatal
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (typeof document !== 'undefined') {
|
|
32
|
+
try {
|
|
33
|
+
const cookies = document.cookie ? document.cookie.split(';') : []
|
|
34
|
+
for (const cookie of cookies) {
|
|
35
|
+
const name = cookie.split('=')[0]?.trim()
|
|
36
|
+
if (name && name.startsWith(`${PERSPECTIVE_COOKIE_PREFIX}:`)) {
|
|
37
|
+
document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore — cookie access can throw in sandboxed contexts
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -57,9 +57,9 @@ function getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallbac
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function getKindStyles(kind: ScheduleItem['kind']): string {
|
|
60
|
-
if (kind === 'event') return 'border-
|
|
61
|
-
if (kind === 'exception') return 'border-
|
|
62
|
-
return 'border-
|
|
60
|
+
if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'
|
|
61
|
+
if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'
|
|
62
|
+
return 'border-status-success-border bg-status-success-bg text-status-success-text'
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export type ScheduleAgendaProps = {
|
|
@@ -57,9 +57,9 @@ function getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallbac
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function getKindStyles(kind: ScheduleItem['kind']): string {
|
|
60
|
-
if (kind === 'event') return 'border-
|
|
61
|
-
if (kind === 'exception') return 'border-
|
|
62
|
-
return 'border-
|
|
60
|
+
if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'
|
|
61
|
+
if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'
|
|
62
|
+
return 'border-status-success-border bg-status-success-bg text-status-success-text'
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export type ScheduleGridProps = {
|