@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/version-history/VersionHistoryPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Undo failed', { err })\n } finally {\n setUndoingToken(null)\n }\n }, [t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Redo failed', { err })\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Alert variant=\"info\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/50 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-overline uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Undo failed', { err })\n } finally {\n setUndoingToken(null)\n }\n }, [t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Redo failed', { err })\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Alert status=\"information\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/50 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-overline uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,7 +22,8 @@ function LanguageSwitcher() {
|
|
|
22
22
|
en: t("common.languages.english", "English"),
|
|
23
23
|
pl: t("common.languages.polish", "Polski"),
|
|
24
24
|
es: t("common.languages.spanish", "Espa\xF1ol"),
|
|
25
|
-
de: t("common.languages.german", "Deutsch")
|
|
25
|
+
de: t("common.languages.german", "Deutsch"),
|
|
26
|
+
ko: t("common.languages.korean", "\uD55C\uAD6D\uC5B4")
|
|
26
27
|
};
|
|
27
28
|
async function setLocale(locale) {
|
|
28
29
|
if (locale === current) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/frontend/LanguageSwitcher.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useId, useTransition } from 'react'\nimport { useLocale, useLocaleLocked, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\n\nexport function LanguageSwitcher() {\n const current = useLocale()\n const localeLocked = useLocaleLocked()\n const t = useT()\n const router = useRouter()\n const [pending, startTransition] = useTransition()\n const selectId = useId()\n\n const languageLabels: Record<Locale, string> = {\n en: t('common.languages.english', 'English'),\n pl: t('common.languages.polish', 'Polski'),\n es: t('common.languages.spanish', 'Espa\u00F1ol'),\n de: t('common.languages.german', 'Deutsch'),\n }\n\n async function setLocale(locale: Locale) {\n if (locale === current) return\n try {\n const res = await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n if (!res.ok) return\n startTransition(() => router.refresh())\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {\n // Ignore if window is unavailable\n }\n } catch {\n // Ignore network errors; UX fallback keeps previous locale\n }\n }\n\n // Locale is pinned via OM_FORCE_LOCALE \u2014 switching is a no-op, so hide the control.\n if (localeLocked) return null\n\n return (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <label htmlFor={selectId}>{t('common.language')}</label>\n <Select\n value={current}\n onValueChange={(value) => setLocale(value as Locale)}\n disabled={pending}\n >\n <SelectTrigger id={selectId} size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {locales.map((locale) => (\n <SelectItem key={locale} value={locale}>\n {languageLabels[locale]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useId, useTransition } from 'react'\nimport { useLocale, useLocaleLocked, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\n\nexport function LanguageSwitcher() {\n const current = useLocale()\n const localeLocked = useLocaleLocked()\n const t = useT()\n const router = useRouter()\n const [pending, startTransition] = useTransition()\n const selectId = useId()\n\n const languageLabels: Record<Locale, string> = {\n en: t('common.languages.english', 'English'),\n pl: t('common.languages.polish', 'Polski'),\n es: t('common.languages.spanish', 'Espa\u00F1ol'),\n de: t('common.languages.german', 'Deutsch'),\n ko: t('common.languages.korean', '\uD55C\uAD6D\uC5B4'),\n }\n\n async function setLocale(locale: Locale) {\n if (locale === current) return\n try {\n const res = await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n if (!res.ok) return\n startTransition(() => router.refresh())\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {\n // Ignore if window is unavailable\n }\n } catch {\n // Ignore network errors; UX fallback keeps previous locale\n }\n }\n\n // Locale is pinned via OM_FORCE_LOCALE \u2014 switching is a no-op, so hide the control.\n if (localeLocked) return null\n\n return (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <label htmlFor={selectId}>{t('common.language')}</label>\n <Select\n value={current}\n onValueChange={(value) => setLocale(value as Locale)}\n disabled={pending}\n >\n <SelectTrigger id={selectId} size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {locales.map((locale) => (\n <SelectItem key={locale} value={locale}>\n {languageLabels[locale]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAsDM,cACA,YADA;AArDN,SAAS,OAAO,qBAAqB;AACrC,SAAS,WAAW,iBAAiB,YAAY;AACjD,SAAS,iBAAiB;AAC1B,SAAS,eAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,SAAS,mBAAmB;AACjC,QAAM,UAAU,UAAU;AAC1B,QAAM,eAAe,gBAAgB;AACrC,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,SAAS,eAAe,IAAI,cAAc;AACjD,QAAM,WAAW,MAAM;AAEvB,QAAM,iBAAyC;AAAA,IAC7C,IAAI,EAAE,4BAA4B,SAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,QAAQ;AAAA,IACzC,IAAI,EAAE,4BAA4B,YAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,SAAS;AAAA,IAC1C,IAAI,EAAE,2BAA2B,oBAAK;AAAA,EACxC;AAEA,iBAAe,UAAU,QAAgB;AACvC,QAAI,WAAW,QAAS;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,oBAAoB;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;AAAA,MACjC,CAAC;AACD,UAAI,CAAC,IAAI,GAAI;AACb,sBAAgB,MAAM,OAAO,QAAQ,CAAC;AACtC,UAAI;AACF,eAAO,cAAc,IAAI,MAAM,oBAAoB,CAAC;AAAA,MACtD,QAAQ;AAAA,MAER;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,MAAI,aAAc,QAAO;AAEzB,SACE,qBAAC,SAAI,WAAU,yDACb;AAAA,wBAAC,WAAM,SAAS,UAAW,YAAE,iBAAiB,GAAE;AAAA,IAChD;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,eAAe,CAAC,UAAU,UAAU,KAAe;AAAA,QACnD,UAAU;AAAA,QAEV;AAAA,8BAAC,iBAAc,IAAI,UAAU,MAAK,MAChC,8BAAC,eAAY,GACf;AAAA,UACA,oBAAC,iBACE,kBAAQ,IAAI,CAAC,WACZ,oBAAC,cAAwB,OAAO,QAC7B,yBAAe,MAAM,KADP,MAEjB,CACD,GACH;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -33,7 +33,7 @@ function PortalProvider({ orgSlug, children, initialAuth, initialTenant }) {
|
|
|
33
33
|
},
|
|
34
34
|
roles: [],
|
|
35
35
|
resolvedFeatures: initialAuth.resolvedFeatures,
|
|
36
|
-
isPortalAdmin: initialAuth.
|
|
36
|
+
isPortalAdmin: initialAuth.isPortalAdmin === true,
|
|
37
37
|
loading: false,
|
|
38
38
|
error: null
|
|
39
39
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/portal/PortalContext.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport {\n type ReactNode,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport type { CustomerAuthResult } from '@open-mercato/shared/modules/customer-auth'\nimport type { CustomerAuthContext } from '@open-mercato/shared/modules/customer-auth'\nimport { apiCall } from '../backend/utils/apiCall'\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\ntype TenantState = {\n tenantId: string | undefined\n organizationId: string | undefined\n organizationName: string | undefined\n loading: boolean\n error: string | null\n}\n\ntype PortalContextValue = {\n auth: CustomerAuthResult & { logout: () => Promise<void> }\n tenant: TenantState\n orgSlug: string\n}\n\nconst PortalCtx = createContext<PortalContextValue | null>(null)\n\n/* ------------------------------------------------------------------ */\n/* Hook */\n/* ------------------------------------------------------------------ */\n\nexport function usePortalContext(): PortalContextValue {\n const ctx = useContext(PortalCtx)\n if (!ctx) throw new Error('usePortalContext must be used inside <PortalProvider>')\n return ctx\n}\n\n/* ------------------------------------------------------------------ */\n/* Provider */\n/* ------------------------------------------------------------------ */\n\ntype InitialTenant = {\n tenantId?: string\n organizationId?: string\n organizationName?: string\n}\n\ntype PortalProviderProps = {\n orgSlug: string\n children: ReactNode\n /** Server-resolved customer auth. When provided, skips client-side fetching. */\n initialAuth?: CustomerAuthContext | null\n /** Server-resolved tenant data. When provided, skips client-side fetching. */\n initialTenant?: InitialTenant\n}\n\n/**\n * Provides auth + tenant state for portal pages.\n *\n * When `initialAuth` and `initialTenant` are provided (from server layout),\n * the context initializes with that data immediately \u2014 no loading state,\n * no client-side fetching, no blink.\n *\n * Falls back to client-side fetching only when initial data is not provided.\n */\nexport function PortalProvider({ orgSlug, children, initialAuth, initialTenant }: PortalProviderProps) {\n const hasServerData = initialAuth !== undefined\n\n /* ---- Auth state ---- */\n const [authState, setAuthState] = useState<CustomerAuthResult>(() => {\n if (hasServerData && initialAuth) {\n // Server-resolved auth \u2014 start fully loaded, no client-side fetch needed\n return {\n user: {\n id: initialAuth.sub,\n email: initialAuth.email,\n displayName: initialAuth.displayName,\n emailVerified: false,\n customerEntityId: initialAuth.customerEntityId ?? null,\n personEntityId: initialAuth.personEntityId ?? null,\n isActive: true,\n lastLoginAt: null,\n createdAt: '',\n },\n roles: [],\n resolvedFeatures: initialAuth.resolvedFeatures,\n isPortalAdmin: initialAuth.
|
|
5
|
-
"mappings": ";AA0LS;AAzLT;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,eAAe;AAoBxB,MAAM,YAAY,cAAyC,IAAI;AAMxD,SAAS,mBAAuC;AACrD,QAAM,MAAM,WAAW,SAAS;AAChC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uDAAuD;AACjF,SAAO;AACT;AA8BO,SAAS,eAAe,EAAE,SAAS,UAAU,aAAa,cAAc,GAAwB;AACrG,QAAM,gBAAgB,gBAAgB;AAGtC,QAAM,CAAC,WAAW,YAAY,IAAI,SAA6B,MAAM;AACnE,QAAI,iBAAiB,aAAa;AAEhC,aAAO;AAAA,QACL,MAAM;AAAA,UACJ,IAAI,YAAY;AAAA,UAChB,OAAO,YAAY;AAAA,UACnB,aAAa,YAAY;AAAA,UACzB,eAAe;AAAA,UACf,kBAAkB,YAAY,oBAAoB;AAAA,UAClD,gBAAgB,YAAY,kBAAkB;AAAA,UAC9C,UAAU;AAAA,UACV,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,QACA,OAAO,CAAC;AAAA,QACR,kBAAkB,YAAY;AAAA,QAC9B,eAAe,YAAY,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport {\n type ReactNode,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport type { CustomerAuthResult } from '@open-mercato/shared/modules/customer-auth'\nimport type { CustomerAuthContext } from '@open-mercato/shared/modules/customer-auth'\nimport { apiCall } from '../backend/utils/apiCall'\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\ntype TenantState = {\n tenantId: string | undefined\n organizationId: string | undefined\n organizationName: string | undefined\n loading: boolean\n error: string | null\n}\n\ntype PortalContextValue = {\n auth: CustomerAuthResult & { logout: () => Promise<void> }\n tenant: TenantState\n orgSlug: string\n}\n\nconst PortalCtx = createContext<PortalContextValue | null>(null)\n\n/* ------------------------------------------------------------------ */\n/* Hook */\n/* ------------------------------------------------------------------ */\n\nexport function usePortalContext(): PortalContextValue {\n const ctx = useContext(PortalCtx)\n if (!ctx) throw new Error('usePortalContext must be used inside <PortalProvider>')\n return ctx\n}\n\n/* ------------------------------------------------------------------ */\n/* Provider */\n/* ------------------------------------------------------------------ */\n\ntype InitialTenant = {\n tenantId?: string\n organizationId?: string\n organizationName?: string\n}\n\ntype PortalProviderProps = {\n orgSlug: string\n children: ReactNode\n /** Server-resolved customer auth. When provided, skips client-side fetching. */\n initialAuth?: CustomerAuthContext | null\n /** Server-resolved tenant data. When provided, skips client-side fetching. */\n initialTenant?: InitialTenant\n}\n\n/**\n * Provides auth + tenant state for portal pages.\n *\n * When `initialAuth` and `initialTenant` are provided (from server layout),\n * the context initializes with that data immediately \u2014 no loading state,\n * no client-side fetching, no blink.\n *\n * Falls back to client-side fetching only when initial data is not provided.\n */\nexport function PortalProvider({ orgSlug, children, initialAuth, initialTenant }: PortalProviderProps) {\n const hasServerData = initialAuth !== undefined\n\n /* ---- Auth state ---- */\n const [authState, setAuthState] = useState<CustomerAuthResult>(() => {\n if (hasServerData && initialAuth) {\n // Server-resolved auth \u2014 start fully loaded, no client-side fetch needed\n return {\n user: {\n id: initialAuth.sub,\n email: initialAuth.email,\n displayName: initialAuth.displayName,\n emailVerified: false,\n customerEntityId: initialAuth.customerEntityId ?? null,\n personEntityId: initialAuth.personEntityId ?? null,\n isActive: true,\n lastLoginAt: null,\n createdAt: '',\n },\n roles: [],\n resolvedFeatures: initialAuth.resolvedFeatures,\n isPortalAdmin: initialAuth.isPortalAdmin === true,\n loading: false,\n error: null,\n }\n }\n if (hasServerData && !initialAuth) {\n // Server confirmed: no auth cookie \u2192 not authenticated\n return { user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: false, error: null }\n }\n // No server data \u2192 need client-side fetch\n return { user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: true, error: null }\n })\n\n // Guard against re-fetching when the provider persists across navigations\n // and `initialAuth` arrives as a new object reference on each server render.\n const profileEnrichedRef = useRef(false)\n\n // Client-side profile fetch \u2014 only runs when NO server data was provided,\n // or to enrich server-provided auth with full profile (roles, etc.)\n useEffect(() => {\n if (hasServerData && !initialAuth) return // Server said: not authenticated, nothing to fetch\n if (profileEnrichedRef.current) return\n let cancelled = false\n\n async function fetchProfile() {\n try {\n const { ok, status, result: data } = await apiCall<{ ok: boolean; user: CustomerAuthResult['user']; roles: CustomerAuthResult['roles']; resolvedFeatures: string[]; isPortalAdmin: boolean }>('/api/customer_accounts/portal/profile')\n if (cancelled) return\n if (status === 401) {\n setAuthState((prev) => ({ ...prev, loading: false, user: prev.user }))\n return\n }\n if (!ok || !data?.ok) {\n setAuthState((prev) => ({ ...prev, loading: false }))\n return\n }\n profileEnrichedRef.current = true\n setAuthState({\n user: data!.user,\n roles: data!.roles || [],\n resolvedFeatures: data!.resolvedFeatures || [],\n isPortalAdmin: data!.isPortalAdmin || false,\n loading: false,\n error: null,\n })\n } catch {\n if (!cancelled) {\n setAuthState((prev) => ({ ...prev, loading: false }))\n }\n }\n }\n\n fetchProfile()\n return () => { cancelled = true }\n }, [hasServerData, initialAuth])\n\n const logout = useCallback(async () => {\n try {\n await apiCall('/api/customer_accounts/portal/logout', { method: 'POST' })\n } catch {\n // Best-effort logout \u2014 redirect regardless\n }\n profileEnrichedRef.current = false\n setAuthState({ user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: false, error: null })\n window.location.assign(`/${orgSlug}/portal/login`)\n }, [orgSlug])\n\n /* ---- Tenant state ---- */\n const [tenantState] = useState<TenantState>(() => {\n if (initialTenant) {\n return {\n tenantId: initialTenant.tenantId,\n organizationId: initialTenant.organizationId,\n organizationName: initialTenant.organizationName,\n loading: false,\n error: null,\n }\n }\n return { tenantId: undefined, organizationId: undefined, organizationName: undefined, loading: true, error: null }\n })\n\n // Client-side tenant fetch \u2014 only when no server data\n useEffect(() => {\n if (initialTenant) return // Server already resolved\n // Fallback: kept for backward compat but shouldn't be hit when layout provides data\n }, [initialTenant])\n\n const value: PortalContextValue = {\n auth: { ...authState, logout },\n tenant: tenantState,\n orgSlug,\n }\n\n return <PortalCtx.Provider value={value}>{children}</PortalCtx.Provider>\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const PORTAL_BRIDGE_STATUS_DOM_NAME = "om:portal-bridge:status";
|
|
2
|
+
function readPortalBridgeHealth() {
|
|
3
|
+
if (typeof window === "undefined") return void 0;
|
|
4
|
+
return window.__portalBridgeHealthy;
|
|
5
|
+
}
|
|
6
|
+
function publishPortalBridgeHealth(healthy) {
|
|
7
|
+
if (typeof window === "undefined") return;
|
|
8
|
+
window.__portalBridgeHealthy = healthy;
|
|
9
|
+
window.dispatchEvent(
|
|
10
|
+
new CustomEvent(PORTAL_BRIDGE_STATUS_DOM_NAME, {
|
|
11
|
+
detail: { healthy }
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
function clearPortalBridgeHealth() {
|
|
16
|
+
if (typeof window === "undefined") return;
|
|
17
|
+
delete window.__portalBridgeHealthy;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
PORTAL_BRIDGE_STATUS_DOM_NAME,
|
|
21
|
+
clearPortalBridgeHealth,
|
|
22
|
+
publishPortalBridgeHealth,
|
|
23
|
+
readPortalBridgeHealth
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=portalBridgeStatus.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/portal/hooks/portalBridgeStatus.ts"],
|
|
4
|
+
"sourcesContent": ["export const PORTAL_BRIDGE_STATUS_DOM_NAME = 'om:portal-bridge:status'\n\nexport type PortalBridgeStatusDetail = {\n healthy: boolean\n}\n\ntype PortalBridgeWindow = Window & {\n __portalBridgeHealthy?: boolean\n}\n\nexport function readPortalBridgeHealth(): boolean | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as PortalBridgeWindow).__portalBridgeHealthy\n}\n\nexport function publishPortalBridgeHealth(healthy: boolean): void {\n if (typeof window === 'undefined') return\n ;(window as PortalBridgeWindow).__portalBridgeHealthy = healthy\n window.dispatchEvent(\n new CustomEvent<PortalBridgeStatusDetail>(PORTAL_BRIDGE_STATUS_DOM_NAME, {\n detail: { healthy },\n }),\n )\n}\n\nexport function clearPortalBridgeHealth(): void {\n if (typeof window === 'undefined') return\n delete (window as PortalBridgeWindow).__portalBridgeHealthy\n}\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,gCAAgC;AAUtC,SAAS,yBAA8C;AAC5D,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAQ,OAA8B;AACxC;AAEO,SAAS,0BAA0B,SAAwB;AAChE,MAAI,OAAO,WAAW,YAAa;AAClC,EAAC,OAA8B,wBAAwB;AACxD,SAAO;AAAA,IACL,IAAI,YAAsC,+BAA+B;AAAA,MACvE,QAAQ,EAAE,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH;AACF;AAEO,SAAS,0BAAgC;AAC9C,MAAI,OAAO,WAAW,YAAa;AACnC,SAAQ,OAA8B;AACxC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { PORTAL_EVENT_DOM_NAME } from "./usePortalAppEvent.js";
|
|
4
4
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
5
|
+
import { publishPortalBridgeHealth } from "./portalBridgeStatus.js";
|
|
5
6
|
const logger = createLogger("ui").child({ component: "PortalEventBridge" });
|
|
6
7
|
const PORTAL_SSE_ENDPOINT = "/api/customer_accounts/portal/events/stream";
|
|
7
8
|
const HEARTBEAT_TIMEOUT = 45e3;
|
|
@@ -36,6 +37,7 @@ function usePortalEventBridge() {
|
|
|
36
37
|
if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current);
|
|
37
38
|
heartbeatTimer.current = setTimeout(() => {
|
|
38
39
|
logger.warn("Heartbeat timeout \u2014 reconnecting");
|
|
40
|
+
publishPortalBridgeHealth(false);
|
|
39
41
|
disconnect();
|
|
40
42
|
scheduleReconnect();
|
|
41
43
|
}, HEARTBEAT_TIMEOUT);
|
|
@@ -51,6 +53,7 @@ function usePortalEventBridge() {
|
|
|
51
53
|
hasEverConnected.current = true;
|
|
52
54
|
reconnectPending.current = false;
|
|
53
55
|
reconnectAttempts.current = 0;
|
|
56
|
+
publishPortalBridgeHealth(true);
|
|
54
57
|
resetHeartbeatTimer();
|
|
55
58
|
if (shouldEmitReconnect) {
|
|
56
59
|
window.dispatchEvent(
|
|
@@ -82,6 +85,7 @@ function usePortalEventBridge() {
|
|
|
82
85
|
if (hasEverConnected.current) {
|
|
83
86
|
reconnectPending.current = true;
|
|
84
87
|
}
|
|
88
|
+
publishPortalBridgeHealth(false);
|
|
85
89
|
disconnect();
|
|
86
90
|
if (mounted) scheduleReconnect();
|
|
87
91
|
};
|
|
@@ -89,6 +93,7 @@ function usePortalEventBridge() {
|
|
|
89
93
|
if (hasEverConnected.current) {
|
|
90
94
|
reconnectPending.current = true;
|
|
91
95
|
}
|
|
96
|
+
publishPortalBridgeHealth(false);
|
|
92
97
|
if (mounted) scheduleReconnect();
|
|
93
98
|
}
|
|
94
99
|
}
|
|
@@ -117,6 +122,7 @@ function usePortalEventBridge() {
|
|
|
117
122
|
connect();
|
|
118
123
|
return () => {
|
|
119
124
|
mounted = false;
|
|
125
|
+
publishPortalBridgeHealth(false);
|
|
120
126
|
disconnect();
|
|
121
127
|
if (reconnectTimer.current) {
|
|
122
128
|
clearTimeout(reconnectTimer.current);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/portal/hooks/usePortalEventBridge.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { PORTAL_EVENT_DOM_NAME } from './usePortalAppEvent'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'PortalEventBridge' })\n\nconst PORTAL_SSE_ENDPOINT = '/api/customer_accounts/portal/events/stream'\nconst HEARTBEAT_TIMEOUT = 45_000\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\nconst DEDUP_WINDOW_MS = 500\nconst PORTAL_BRIDGE_RECONNECTED_EVENT_ID = 'om:portal-bridge:reconnected'\n\n/**\n * React hook that establishes a singleton SSE connection to the portal event bridge.\n *\n * Mount once in the portal shell/layout. Receives server-side events with\n * `portalBroadcast: true` and dispatches them as `om:portal-event` CustomEvents\n * on the window object for consumption by `usePortalAppEvent`.\n *\n * Uses customer auth (cookie-based JWT) instead of staff auth.\n *\n * @example\n * ```tsx\n * import { usePortalEventBridge } from '@open-mercato/ui/portal/hooks/usePortalEventBridge'\n *\n * function PortalShell({ children }) {\n * usePortalEventBridge()\n * return <div>{children}</div>\n * }\n * ```\n */\nexport function usePortalEventBridge(): void {\n const sourceRef = useRef<EventSource | null>(null)\n const reconnectAttempts = useRef(0)\n const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const heartbeatTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const recentEvents = useRef<Map<string, number>>(new Map())\n const hasEverConnected = useRef(false)\n const reconnectPending = useRef(false)\n\n useEffect(() => {\n let mounted = true\n\n function isDuplicate(eventPayload: AppEventPayload): boolean {\n const key = `${eventPayload.id}:${JSON.stringify(eventPayload.payload ?? {})}`\n const lastSeen = recentEvents.current.get(key)\n if (lastSeen && Date.now() - lastSeen < DEDUP_WINDOW_MS) return true\n recentEvents.current.set(key, Date.now())\n if (recentEvents.current.size > 100) {\n const now = Date.now()\n for (const [k, v] of recentEvents.current) {\n if (now - v > DEDUP_WINDOW_MS * 2) recentEvents.current.delete(k)\n }\n }\n return false\n }\n\n function resetHeartbeatTimer() {\n if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = setTimeout(() => {\n logger.warn('Heartbeat timeout \u2014 reconnecting')\n disconnect()\n scheduleReconnect()\n }, HEARTBEAT_TIMEOUT)\n }\n\n function connect() {\n if (!mounted) return\n if (sourceRef.current) return\n\n try {\n const source = new EventSource(PORTAL_SSE_ENDPOINT, { withCredentials: true })\n sourceRef.current = source\n\n source.onopen = () => {\n const shouldEmitReconnect = hasEverConnected.current && reconnectPending.current\n hasEverConnected.current = true\n reconnectPending.current = false\n reconnectAttempts.current = 0\n resetHeartbeatTimer()\n if (shouldEmitReconnect) {\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, {\n detail: {\n id: PORTAL_BRIDGE_RECONNECTED_EVENT_ID,\n payload: {},\n timestamp: Date.now(),\n organizationId: '',\n } satisfies AppEventPayload,\n }),\n )\n }\n }\n\n source.onmessage = (event) => {\n resetHeartbeatTimer()\n if (!event.data || event.data === ':heartbeat') return\n\n try {\n const parsed = JSON.parse(event.data) as AppEventPayload\n if (!parsed.id || typeof parsed.id !== 'string') return\n if (isDuplicate(parsed)) return\n\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, { detail: parsed }),\n )\n } catch {\n // Ignore malformed events\n }\n }\n\n source.onerror = () => {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n disconnect()\n if (mounted) scheduleReconnect()\n }\n } catch {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n if (mounted) scheduleReconnect()\n }\n }\n\n function disconnect() {\n if (sourceRef.current) {\n sourceRef.current.close()\n sourceRef.current = null\n }\n if (heartbeatTimer.current) {\n clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = null\n }\n }\n\n function scheduleReconnect() {\n if (reconnectTimer.current) return\n const delay = Math.min(\n RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts.current),\n RECONNECT_MAX_MS,\n )\n reconnectAttempts.current++\n reconnectTimer.current = setTimeout(() => {\n reconnectTimer.current = null\n connect()\n }, delay)\n }\n\n connect()\n\n return () => {\n mounted = false\n disconnect()\n if (reconnectTimer.current) {\n clearTimeout(reconnectTimer.current)\n reconnectTimer.current = null\n }\n }\n }, [])\n}\n"],
|
|
5
|
-
"mappings": ";AACA,SAAS,WAAW,cAAc;AAElC,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { PORTAL_EVENT_DOM_NAME } from './usePortalAppEvent'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { publishPortalBridgeHealth } from './portalBridgeStatus'\n\nconst logger = createLogger('ui').child({ component: 'PortalEventBridge' })\n\nconst PORTAL_SSE_ENDPOINT = '/api/customer_accounts/portal/events/stream'\nconst HEARTBEAT_TIMEOUT = 45_000\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\nconst DEDUP_WINDOW_MS = 500\nconst PORTAL_BRIDGE_RECONNECTED_EVENT_ID = 'om:portal-bridge:reconnected'\n\n/**\n * React hook that establishes a singleton SSE connection to the portal event bridge.\n *\n * Mount once in the portal shell/layout. Receives server-side events with\n * `portalBroadcast: true` and dispatches them as `om:portal-event` CustomEvents\n * on the window object for consumption by `usePortalAppEvent`.\n *\n * Uses customer auth (cookie-based JWT) instead of staff auth.\n *\n * @example\n * ```tsx\n * import { usePortalEventBridge } from '@open-mercato/ui/portal/hooks/usePortalEventBridge'\n *\n * function PortalShell({ children }) {\n * usePortalEventBridge()\n * return <div>{children}</div>\n * }\n * ```\n */\nexport function usePortalEventBridge(): void {\n const sourceRef = useRef<EventSource | null>(null)\n const reconnectAttempts = useRef(0)\n const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const heartbeatTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const recentEvents = useRef<Map<string, number>>(new Map())\n const hasEverConnected = useRef(false)\n const reconnectPending = useRef(false)\n\n useEffect(() => {\n let mounted = true\n\n function isDuplicate(eventPayload: AppEventPayload): boolean {\n const key = `${eventPayload.id}:${JSON.stringify(eventPayload.payload ?? {})}`\n const lastSeen = recentEvents.current.get(key)\n if (lastSeen && Date.now() - lastSeen < DEDUP_WINDOW_MS) return true\n recentEvents.current.set(key, Date.now())\n if (recentEvents.current.size > 100) {\n const now = Date.now()\n for (const [k, v] of recentEvents.current) {\n if (now - v > DEDUP_WINDOW_MS * 2) recentEvents.current.delete(k)\n }\n }\n return false\n }\n\n function resetHeartbeatTimer() {\n if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = setTimeout(() => {\n logger.warn('Heartbeat timeout \u2014 reconnecting')\n publishPortalBridgeHealth(false)\n disconnect()\n scheduleReconnect()\n }, HEARTBEAT_TIMEOUT)\n }\n\n function connect() {\n if (!mounted) return\n if (sourceRef.current) return\n\n try {\n const source = new EventSource(PORTAL_SSE_ENDPOINT, { withCredentials: true })\n sourceRef.current = source\n\n source.onopen = () => {\n const shouldEmitReconnect = hasEverConnected.current && reconnectPending.current\n hasEverConnected.current = true\n reconnectPending.current = false\n reconnectAttempts.current = 0\n publishPortalBridgeHealth(true)\n resetHeartbeatTimer()\n if (shouldEmitReconnect) {\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, {\n detail: {\n id: PORTAL_BRIDGE_RECONNECTED_EVENT_ID,\n payload: {},\n timestamp: Date.now(),\n organizationId: '',\n } satisfies AppEventPayload,\n }),\n )\n }\n }\n\n source.onmessage = (event) => {\n resetHeartbeatTimer()\n if (!event.data || event.data === ':heartbeat') return\n\n try {\n const parsed = JSON.parse(event.data) as AppEventPayload\n if (!parsed.id || typeof parsed.id !== 'string') return\n if (isDuplicate(parsed)) return\n\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, { detail: parsed }),\n )\n } catch {\n // Ignore malformed events\n }\n }\n\n source.onerror = () => {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n publishPortalBridgeHealth(false)\n disconnect()\n if (mounted) scheduleReconnect()\n }\n } catch {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n publishPortalBridgeHealth(false)\n if (mounted) scheduleReconnect()\n }\n }\n\n function disconnect() {\n if (sourceRef.current) {\n sourceRef.current.close()\n sourceRef.current = null\n }\n if (heartbeatTimer.current) {\n clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = null\n }\n }\n\n function scheduleReconnect() {\n if (reconnectTimer.current) return\n const delay = Math.min(\n RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts.current),\n RECONNECT_MAX_MS,\n )\n reconnectAttempts.current++\n reconnectTimer.current = setTimeout(() => {\n reconnectTimer.current = null\n connect()\n }, delay)\n }\n\n connect()\n\n return () => {\n mounted = false\n publishPortalBridgeHealth(false)\n disconnect()\n if (reconnectTimer.current) {\n clearTimeout(reconnectTimer.current)\n reconnectTimer.current = null\n }\n }\n }, [])\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { apiCall } from "../../backend/utils/apiCall.js";
|
|
4
|
+
import {
|
|
5
|
+
PORTAL_BRIDGE_STATUS_DOM_NAME,
|
|
6
|
+
readPortalBridgeHealth
|
|
7
|
+
} from "./portalBridgeStatus.js";
|
|
4
8
|
const POLL_INTERVAL = 8e3;
|
|
5
9
|
const BASE = "/api/customer_accounts/portal/notifications";
|
|
6
10
|
async function fetchJson(url, init) {
|
|
@@ -37,21 +41,55 @@ function usePortalNotifications() {
|
|
|
37
41
|
}
|
|
38
42
|
setIsLoading(false);
|
|
39
43
|
}, []);
|
|
44
|
+
const [usePolling, setUsePolling] = React.useState(() => {
|
|
45
|
+
if (typeof window === "undefined" || !("EventSource" in window)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return readPortalBridgeHealth() !== true;
|
|
49
|
+
});
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (typeof window === "undefined" || !("EventSource" in window)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const handleStatusChange = (event) => {
|
|
55
|
+
const detail = event.detail;
|
|
56
|
+
if (detail && typeof detail.healthy === "boolean") {
|
|
57
|
+
setUsePolling(!detail.healthy);
|
|
58
|
+
if (!detail.healthy) {
|
|
59
|
+
fetchAll();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange);
|
|
64
|
+
return () => {
|
|
65
|
+
window.removeEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange);
|
|
66
|
+
};
|
|
67
|
+
}, [fetchAll]);
|
|
40
68
|
React.useEffect(() => {
|
|
41
69
|
fetchAll();
|
|
70
|
+
}, [fetchAll]);
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (!usePolling) return;
|
|
42
73
|
const interval = setInterval(fetchAll, POLL_INTERVAL);
|
|
43
74
|
return () => clearInterval(interval);
|
|
44
|
-
}, [fetchAll]);
|
|
75
|
+
}, [fetchAll, usePolling]);
|
|
45
76
|
React.useEffect(() => {
|
|
46
|
-
const handler = (
|
|
47
|
-
const detail =
|
|
48
|
-
if (detail?.id === "notifications.notification.created" || detail?.id === "notifications.notification.batch_created") {
|
|
77
|
+
const handler = (event) => {
|
|
78
|
+
const detail = event.detail;
|
|
79
|
+
if (detail?.id === "notifications.notification.created" || detail?.id === "notifications.notification.batch_created" || detail?.id === "om:portal-bridge:reconnected") {
|
|
49
80
|
fetchAll();
|
|
50
81
|
}
|
|
51
82
|
};
|
|
52
83
|
window.addEventListener("om:portal-event", handler);
|
|
53
84
|
return () => window.removeEventListener("om:portal-event", handler);
|
|
54
85
|
}, [fetchAll]);
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
const onFocus = () => {
|
|
88
|
+
fetchAll();
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener("focus", onFocus);
|
|
91
|
+
return () => window.removeEventListener("focus", onFocus);
|
|
92
|
+
}, [fetchAll]);
|
|
55
93
|
const markAsRead = React.useCallback(async (id) => {
|
|
56
94
|
await fetchJson(`${BASE}/${id}/read`, { method: "PUT" });
|
|
57
95
|
setNotifications(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/portal/hooks/usePortalNotifications.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport { apiCall } from '../../backend/utils/apiCall'\n\nexport type UsePortalNotificationsResult = {\n notifications: NotificationDto[]\n unreadCount: number\n hasNew: boolean\n isLoading: boolean\n refresh: () => void\n markAsRead: (id: string) => Promise<void>\n dismiss: (id: string) => Promise<void>\n markAllRead: () => Promise<void>\n}\n\nconst POLL_INTERVAL = 8000\nconst BASE = '/api/customer_accounts/portal/notifications'\n\nasync function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null> {\n try {\n const { ok, result } = await apiCall<T>(url, init)\n if (!ok) return null\n return result\n } catch {\n return null\n }\n}\n\n/**\n * Portal notification hook
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AAEvB,SAAS,eAAe;
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport { apiCall } from '../../backend/utils/apiCall'\nimport {\n PORTAL_BRIDGE_STATUS_DOM_NAME,\n readPortalBridgeHealth,\n type PortalBridgeStatusDetail,\n} from './portalBridgeStatus'\n\nexport type UsePortalNotificationsResult = {\n notifications: NotificationDto[]\n unreadCount: number\n hasNew: boolean\n isLoading: boolean\n refresh: () => void\n markAsRead: (id: string) => Promise<void>\n dismiss: (id: string) => Promise<void>\n markAllRead: () => Promise<void>\n}\n\nconst POLL_INTERVAL = 8000\nconst BASE = '/api/customer_accounts/portal/notifications'\n\nasync function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null> {\n try {\n const { ok, result } = await apiCall<T>(url, init)\n if (!ok) return null\n return result\n } catch {\n return null\n }\n}\n\n/**\n * Portal notification hook with SSE-first delivery and polling fallback.\n *\n * Performs an initial list/count reconciliation, refreshes on notification,\n * reconnect, and focus events, and polls every 8 seconds until the portal\n * event bridge explicitly reports a healthy connection.\n */\nexport function usePortalNotifications(): UsePortalNotificationsResult {\n const [notifications, setNotifications] = React.useState<NotificationDto[]>([])\n const [unreadCount, setUnreadCount] = React.useState(0)\n const [hasNew, setHasNew] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const lastIdRef = React.useRef<string | null>(null)\n\n const fetchAll = React.useCallback(async () => {\n const [listData, countData] = await Promise.all([\n fetchJson<{ ok: boolean; items: NotificationDto[] }>(`${BASE}?pageSize=50`),\n fetchJson<{ ok: boolean; unreadCount: number }>(`${BASE}/unread-count`),\n ])\n\n if (listData?.ok && listData.items) {\n const items = listData.items\n if (lastIdRef.current && items.length > 0 && items[0].id !== lastIdRef.current) {\n setHasNew(true)\n setTimeout(() => setHasNew(false), 3000)\n }\n if (items.length > 0) lastIdRef.current = items[0].id\n setNotifications(items)\n }\n\n if (countData?.ok) {\n setUnreadCount(countData.unreadCount)\n }\n\n setIsLoading(false)\n }, [])\n\n const [usePolling, setUsePolling] = React.useState(() => {\n if (typeof window === 'undefined' || !('EventSource' in window)) {\n return true\n }\n return readPortalBridgeHealth() !== true\n })\n\n React.useEffect(() => {\n if (typeof window === 'undefined' || !('EventSource' in window)) {\n return\n }\n const handleStatusChange = (event: Event) => {\n const detail = (event as CustomEvent<PortalBridgeStatusDetail>).detail\n if (detail && typeof detail.healthy === 'boolean') {\n setUsePolling(!detail.healthy)\n if (!detail.healthy) {\n fetchAll()\n }\n }\n }\n window.addEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)\n return () => {\n window.removeEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)\n }\n }, [fetchAll])\n\n React.useEffect(() => {\n fetchAll()\n }, [fetchAll])\n\n React.useEffect(() => {\n if (!usePolling) return\n const interval = setInterval(fetchAll, POLL_INTERVAL)\n return () => clearInterval(interval)\n }, [fetchAll, usePolling])\n\n React.useEffect(() => {\n const handler = (event: Event) => {\n const detail = (event as CustomEvent<{ id?: string }>).detail\n if (\n detail?.id === 'notifications.notification.created' ||\n detail?.id === 'notifications.notification.batch_created' ||\n detail?.id === 'om:portal-bridge:reconnected'\n ) {\n fetchAll()\n }\n }\n window.addEventListener('om:portal-event', handler)\n return () => window.removeEventListener('om:portal-event', handler)\n }, [fetchAll])\n\n React.useEffect(() => {\n const onFocus = () => {\n fetchAll()\n }\n window.addEventListener('focus', onFocus)\n return () => window.removeEventListener('focus', onFocus)\n }, [fetchAll])\n\n const markAsRead = React.useCallback(async (id: string) => {\n await fetchJson(`${BASE}/${id}/read`, { method: 'PUT' })\n setNotifications((prev) =>\n prev.map((n) => (n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n)),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }, [])\n\n const dismiss = React.useCallback(async (id: string) => {\n await fetchJson(`${BASE}/${id}/dismiss`, { method: 'PUT' })\n setNotifications((prev) => prev.filter((n) => n.id !== id))\n setUnreadCount((prev) => {\n const wasDismissedUnread = notifications.find((n) => n.id === id)?.status === 'unread'\n return wasDismissedUnread ? Math.max(0, prev - 1) : prev\n })\n }, [notifications])\n\n const markAllRead = React.useCallback(async () => {\n await fetchJson(`${BASE}/mark-all-read`, { method: 'PUT' })\n setNotifications((prev) =>\n prev.map((n) => (n.status === 'unread' ? { ...n, status: 'read', readAt: new Date().toISOString() } : n)),\n )\n setUnreadCount(0)\n }, [])\n\n const refresh = React.useCallback(() => { fetchAll() }, [fetchAll])\n\n return { notifications, unreadCount, hasNew, isLoading, refresh, markAsRead, dismiss, markAllRead }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/portal/utils/nav.js
CHANGED
|
@@ -26,8 +26,7 @@ function pickPreferredRoute(existing, candidate) {
|
|
|
26
26
|
function buildPortalNav({
|
|
27
27
|
routes,
|
|
28
28
|
orgSlug,
|
|
29
|
-
grantedFeatures
|
|
30
|
-
isPortalAdmin = false
|
|
29
|
+
grantedFeatures
|
|
31
30
|
}) {
|
|
32
31
|
const mainItems = [];
|
|
33
32
|
const accountItems = [];
|
|
@@ -45,7 +44,7 @@ function buildPortalNav({
|
|
|
45
44
|
const pattern = route.pattern ?? route.path;
|
|
46
45
|
const nav = route.nav;
|
|
47
46
|
const requireFeatures = route.requireCustomerFeatures ?? [];
|
|
48
|
-
if (
|
|
47
|
+
if (requireFeatures.length) {
|
|
49
48
|
if (!hasAllFeatures(grantedFeatures, requireFeatures)) continue;
|
|
50
49
|
}
|
|
51
50
|
const href = resolveHref(pattern, orgSlug);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/portal/utils/nav.ts"],
|
|
4
|
-
"sourcesContent": ["import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\n\nexport type PortalNavGroupId = 'main' | 'account'\n\nexport type PortalNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: string\n order: number\n}\n\nexport type PortalNavGroup = {\n id: PortalNavGroupId\n items: PortalNavItem[]\n}\n\nexport type BuildPortalNavOptions = {\n /** Route manifest to inspect (typically `getFrontendRouteManifests()`). */\n routes: readonly FrontendRouteManifestEntry[]\n /** Current customer org slug \u2014 substituted into `[orgSlug]` patterns. */\n orgSlug: string\n /**
|
|
5
|
-
"mappings": "AACA,SAAS,sBAAsB;AA6B/B,SAAS,gBAAgB,SAAgD;AACvE,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,WAAW,oBAAoB,KAAK,YAAY;AACjE;AAEA,SAAS,sBAAsB,MAAuB;AACpD,SAAO,CAAC,KAAK,SAAS,GAAG;AAC3B;AAEA,SAAS,YAAY,SAAiB,SAAyB;AAC7D,SAAO,QAAQ,QAAQ,aAAa,OAAO;AAC7C;AAEA,SAAS,UAAU,OAAkC;AACnD,MAAI,UAAU,UAAU,UAAU,UAAW,QAAO;AACpD,SAAO;AACT;AAEA,SAAS,mBACP,UACA,WAC4B;AAC5B,QAAM,mBAAmB,SAAS,2BAA2B,CAAC;AAC9D,QAAM,oBAAoB,UAAU,2BAA2B,CAAC;AAChE,MAAI,iBAAiB,WAAW,KAAK,kBAAkB,SAAS,EAAG,QAAO;AAC1E,MAAI,kBAAkB,WAAW,KAAK,iBAAiB,SAAS,EAAG,QAAO;AAC1E,MAAI,iBAAiB,WAAW,kBAAkB,QAAQ;AACxD,WAAO,kBAAkB,SAAS,iBAAiB,SAAS,YAAY;AAAA,EAC1E;AACA,SAAO;AACT;AAaO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;
|
|
4
|
+
"sourcesContent": ["import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\n\nexport type PortalNavGroupId = 'main' | 'account'\n\nexport type PortalNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: string\n order: number\n}\n\nexport type PortalNavGroup = {\n id: PortalNavGroupId\n items: PortalNavItem[]\n}\n\nexport type BuildPortalNavOptions = {\n /** Route manifest to inspect (typically `getFrontendRouteManifests()`). */\n routes: readonly FrontendRouteManifestEntry[]\n /** Current customer org slug \u2014 substituted into `[orgSlug]` patterns. */\n orgSlug: string\n /** Concrete effective features granted to the current customer. */\n grantedFeatures: readonly string[]\n /** @deprecated Portal admins must receive concrete effective features. */\n isPortalAdmin?: boolean\n}\n\nfunction isPortalPattern(pattern: string | undefined): pattern is string {\n if (!pattern) return false\n return pattern.startsWith('/[orgSlug]/portal/') || pattern === '/[orgSlug]/portal'\n}\n\nfunction hasNoUnresolvedParams(href: string): boolean {\n return !href.includes('[')\n}\n\nfunction resolveHref(pattern: string, orgSlug: string): string {\n return pattern.replace('[orgSlug]', orgSlug)\n}\n\nfunction pickGroup(group: unknown): PortalNavGroupId {\n if (group === 'main' || group === 'account') return group\n return 'main'\n}\n\nfunction pickPreferredRoute(\n existing: FrontendRouteManifestEntry,\n candidate: FrontendRouteManifestEntry,\n): FrontendRouteManifestEntry {\n const existingFeatures = existing.requireCustomerFeatures ?? []\n const candidateFeatures = candidate.requireCustomerFeatures ?? []\n if (existingFeatures.length === 0 && candidateFeatures.length > 0) return candidate\n if (candidateFeatures.length === 0 && existingFeatures.length > 0) return existing\n if (existingFeatures.length !== candidateFeatures.length) {\n return candidateFeatures.length > existingFeatures.length ? candidate : existing\n }\n return existing\n}\n\n/**\n * Build the portal sidebar from the frontend route manifest.\n *\n * Mirrors `buildAdminNav()` for the portal surface: selects routes under\n * `/[orgSlug]/portal/*` that declare a `nav` block, applies\n * `requireCustomerFeatures` against the caller's grants (wildcards honored),\n * and returns ordered sidebar groups.\n *\n * Absence of `nav` on a metadata file means the page is routable but not\n * auto-listed \u2014 useful for detail/create pages.\n */\nexport function buildPortalNav({\n routes,\n orgSlug,\n grantedFeatures,\n}: BuildPortalNavOptions): PortalNavGroup[] {\n const mainItems: PortalNavItem[] = []\n const accountItems: PortalNavItem[] = []\n\n const dedupedByPattern = new Map<string, FrontendRouteManifestEntry>()\n for (const route of routes) {\n const pattern = route.pattern ?? route.path\n if (!isPortalPattern(pattern)) continue\n if (route.navHidden) continue\n const nav = route.nav\n if (!nav || typeof nav.label !== 'string' || nav.label.length === 0) continue\n const existing = dedupedByPattern.get(pattern)\n dedupedByPattern.set(pattern, existing ? pickPreferredRoute(existing, route) : route)\n }\n\n for (const route of dedupedByPattern.values()) {\n const pattern = (route.pattern ?? route.path) as string\n const nav = route.nav!\n\n const requireFeatures = route.requireCustomerFeatures ?? []\n if (requireFeatures.length) {\n if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue\n }\n\n const href = resolveHref(pattern, orgSlug)\n if (!hasNoUnresolvedParams(href)) continue\n\n const group = pickGroup(nav.group)\n const item: PortalNavItem = {\n id: `portal-nav:${pattern}`,\n label: nav.label,\n labelKey: nav.labelKey,\n href,\n icon: nav.icon,\n order: typeof nav.order === 'number' ? nav.order : 100,\n }\n if (group === 'account') accountItems.push(item)\n else mainItems.push(item)\n }\n\n const sortItems = (items: PortalNavItem[]) =>\n items.sort((a, b) => {\n if (a.order !== b.order) return a.order - b.order\n return a.label.localeCompare(b.label)\n })\n\n sortItems(mainItems)\n sortItems(accountItems)\n\n const groups: PortalNavGroup[] = []\n if (mainItems.length) groups.push({ id: 'main', items: mainItems })\n if (accountItems.length) groups.push({ id: 'account', items: accountItems })\n return groups\n}\n\n/**\n * Merge sidebar groups from the portal nav endpoint with items contributed via\n * `usePortalInjectedMenuItems`. Auto-discovered entries take precedence \u2014\n * injected items with matching `id` or `href` are dropped as duplicates.\n */\nexport function mergePortalSidebarGroupsWithInjected<TInjected extends { id: string; href?: string }>(\n discovered: readonly PortalNavGroup[],\n injected: {\n main: readonly TInjected[]\n account: readonly TInjected[]\n },\n): {\n main: Array<PortalNavItem | TInjected>\n account: Array<PortalNavItem | TInjected>\n} {\n const mergeGroup = <T extends PortalNavItem | TInjected>(\n base: readonly PortalNavItem[],\n extra: readonly TInjected[],\n ): Array<PortalNavItem | TInjected> => {\n const knownIds = new Set(base.map((item) => item.id))\n const knownHrefs = new Set(base.map((item) => item.href).filter((href): href is string => Boolean(href)))\n const merged: Array<PortalNavItem | TInjected> = [...base]\n for (const item of extra) {\n if (knownIds.has(item.id)) continue\n if (item.href && knownHrefs.has(item.href)) continue\n merged.push(item)\n knownIds.add(item.id)\n if (item.href) knownHrefs.add(item.href)\n }\n return merged\n }\n\n const mainBase = discovered.find((g) => g.id === 'main')?.items ?? []\n const accountBase = discovered.find((g) => g.id === 'account')?.items ?? []\n return {\n main: mergeGroup(mainBase, injected.main),\n account: mergeGroup(accountBase, injected.account),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,7 +6,7 @@ function ErrorNotice({ title, message, action }) {
|
|
|
6
6
|
const t = useT();
|
|
7
7
|
const resolvedTitle = title ?? t("ui.errors.defaultTitle", "Something went wrong");
|
|
8
8
|
const resolvedMessage = message ?? t("ui.errors.defaultMessage", "Unable to load data. Please try again.");
|
|
9
|
-
return /* @__PURE__ */ jsxs(Alert, {
|
|
9
|
+
return /* @__PURE__ */ jsxs(Alert, { status: "error", children: [
|
|
10
10
|
/* @__PURE__ */ jsx(AlertTitle, { children: resolvedTitle }),
|
|
11
11
|
/* @__PURE__ */ jsx(AlertDescription, { children: resolvedMessage }),
|
|
12
12
|
action ? /* @__PURE__ */ jsx("div", { className: "mt-2", children: action }) : null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/ErrorNotice.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from './alert'\n\nexport function ErrorNotice({ title, message, action }: {\n title?: string\n message?: string\n action?: React.ReactNode\n}) {\n const t = useT()\n const resolvedTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')\n const resolvedMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')\n return (\n <Alert
|
|
5
|
-
"mappings": ";AAcI,SACE,KADF;AAZJ,SAAS,YAAY;AACrB,SAAS,OAAO,kBAAkB,kBAAkB;AAE7C,SAAS,YAAY,EAAE,OAAO,SAAS,OAAO,GAIlD;AACD,QAAM,IAAI,KAAK;AACf,QAAM,gBAAgB,SAAS,EAAE,0BAA0B,sBAAsB;AACjF,QAAM,kBAAkB,WAAW,EAAE,4BAA4B,wCAAwC;AACzG,SACE,qBAAC,SAAM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from './alert'\n\nexport function ErrorNotice({ title, message, action }: {\n title?: string\n message?: string\n action?: React.ReactNode\n}) {\n const t = useT()\n const resolvedTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')\n const resolvedMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')\n return (\n <Alert status=\"error\">\n <AlertTitle>{resolvedTitle}</AlertTitle>\n <AlertDescription>{resolvedMessage}</AlertDescription>\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </Alert>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAcI,SACE,KADF;AAZJ,SAAS,YAAY;AACrB,SAAS,OAAO,kBAAkB,kBAAkB;AAE7C,SAAS,YAAY,EAAE,OAAO,SAAS,OAAO,GAIlD;AACD,QAAM,IAAI,KAAK;AACf,QAAM,gBAAgB,SAAS,EAAE,0BAA0B,sBAAsB;AACjF,QAAM,kBAAkB,WAAW,EAAE,4BAA4B,wCAAwC;AACzG,SACE,qBAAC,SAAM,QAAO,SACZ;AAAA,wBAAC,cAAY,yBAAc;AAAA,IAC3B,oBAAC,oBAAkB,2BAAgB;AAAA,IAClC,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,KACnD;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -27,7 +27,7 @@ function Notice({
|
|
|
27
27
|
compact = false
|
|
28
28
|
}) {
|
|
29
29
|
if (process.env.NODE_ENV === "development") {
|
|
30
|
-
logger.warn('<Notice> is deprecated. Use <Alert
|
|
30
|
+
logger.warn('<Notice> is deprecated. Use <Alert status="error|warning|information"> instead. See: docs/design-system/migration-tables.md');
|
|
31
31
|
}
|
|
32
32
|
const styles = variantStyles[variant];
|
|
33
33
|
if (compact || !title && !action && (children || message)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/Notice.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\n/**\n * @deprecated Use <Alert
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\n/**\n * @deprecated Use <Alert status=\"error|warning|information\"> instead.\n * Migration guide: docs/design-system/migration-tables.md#j3-component-mapping\n *\n * Notice variant=\"error\" \u2192 Alert status=\"error\"\n * Notice variant=\"warning\" \u2192 Alert status=\"warning\"\n * Notice variant=\"info\" \u2192 Alert status=\"information\"\n */\n\nconst variantStyles = {\n error: {\n container: 'border-status-error-border bg-status-error-bg text-status-error-text',\n icon: 'border-status-error-icon',\n },\n info: {\n container: 'border-status-info-border bg-status-info-bg text-status-info-text',\n icon: 'border-status-info-icon',\n },\n warning: {\n container: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n icon: 'border-status-warning-icon',\n },\n} as const\n\nexport type NoticeVariant = keyof typeof variantStyles\n\nexport type NoticeProps = {\n variant?: NoticeVariant\n title?: string\n message?: string\n action?: React.ReactNode\n children?: React.ReactNode\n className?: string\n compact?: boolean\n}\n\nexport function Notice({\n variant = 'info',\n title,\n message,\n action,\n children,\n className,\n compact = false,\n}: NoticeProps) {\n if (process.env.NODE_ENV === 'development') {\n logger.warn('<Notice> is deprecated. Use <Alert status=\"error|warning|information\"> instead. See: docs/design-system/migration-tables.md')\n }\n\n const styles = variantStyles[variant]\n\n if (compact || (!title && !action && (children || message))) {\n return (\n <div className={cn('rounded-md border px-3 py-2 text-sm', styles.container, className)}>\n {children ?? message}\n </div>\n )\n }\n\n return (\n <div className={cn('rounded-md border p-4', styles.container, className)}>\n <div className=\"flex items-start gap-3\">\n <span\n className={cn('inline-block mt-0.5 h-4 w-4 rounded-full border-2 shrink-0', styles.icon)}\n aria-hidden\n />\n <div className=\"space-y-1\">\n {title ? <div className=\"text-sm font-medium\">{title}</div> : null}\n {message ? <div className=\"text-sm opacity-90\">{message}</div> : null}\n {children}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
5
|
"mappings": ";AA4DM,cAaE,YAbF;AA1DN,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI;AAWhC,MAAM,gBAAgB;AAAA,EACpB,OAAO;AAAA,IACL,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AACF;AAcO,SAAS,OAAO;AAAA,EACrB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAAgB;AACd,MAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,WAAO,KAAK,6HAA6H;AAAA,EAC3I;AAEA,QAAM,SAAS,cAAc,OAAO;AAEpC,MAAI,WAAY,CAAC,SAAS,CAAC,WAAW,YAAY,UAAW;AAC3D,WACE,oBAAC,SAAI,WAAW,GAAG,uCAAuC,OAAO,WAAW,SAAS,GAClF,sBAAY,SACf;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,yBAAyB,OAAO,WAAW,SAAS,GACrE,+BAAC,SAAI,WAAU,0BACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,8DAA8D,OAAO,IAAI;AAAA,QACvF,eAAW;AAAA;AAAA,IACb;AAAA,IACA,qBAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,oBAAC,SAAI,WAAU,uBAAuB,iBAAM,IAAS;AAAA,MAC7D,UAAU,oBAAC,SAAI,WAAU,sBAAsB,mBAAQ,IAAS;AAAA,MAChE;AAAA,MACA,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,OACnD;AAAA,KACF,GACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|