@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/sidebar/customization-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["import { slugifySidebarId } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\n\nexport type SidebarItem = {\n id?: string\n href: string\n title: string\n defaultTitle?: string\n icon?: React.ReactNode\n iconName?: string\n iconMarkup?: string\n enabled?: boolean\n hidden?: boolean\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n children?: SidebarItem[]\n}\n\nexport type SidebarGroup = {\n id?: string\n name: string\n defaultName?: string\n items: SidebarItem[]\n}\n\nexport type SidebarCustomizationDraft = {\n order: string[]\n groupLabels: Record<string, string>\n itemLabels: Record<string, string>\n hiddenItemIds: Record<string, boolean>\n /** Per-group ordered item keys. Missing items keep their natural position at the tail. */\n itemOrder: Record<string, string[]>\n}\n\nexport type SidebarRoleTarget = {\n id: string\n name: string\n hasPreference: boolean\n}\n\nexport function resolveGroupKey(group: SidebarGroup): string {\n if (group.id && group.id.length) return group.id\n if (group.defaultName && group.defaultName.length) return slugifySidebarId(group.defaultName)\n return slugifySidebarId(group.name)\n}\n\nexport function resolveItemKey(item: { id?: string; href: string }): string {\n const candidate = item.id?.trim()\n if (candidate && candidate.length > 0) return candidate\n return item.href\n}\n\nexport function cloneSidebarGroups(groups: SidebarGroup[]): SidebarGroup[] {\n const cloneItem = (item: SidebarItem): SidebarItem => ({\n id: item.id,\n href: item.href,\n title: item.title,\n defaultTitle: item.defaultTitle,\n icon: item.icon,\n iconName: item.iconName,\n iconMarkup: item.iconMarkup,\n enabled: item.enabled,\n hidden: item.hidden,\n pageContext: item.pageContext,\n children: item.children ? item.children.map((child) => cloneItem(child)) : undefined,\n })\n return groups.map((group) => ({\n id: group.id,\n name: group.name,\n defaultName: group.defaultName,\n items: group.items.map((item) => cloneItem(item)),\n }))\n}\n\nexport function mergeGroupOrder(preferred: string[], current: string[]): string[] {\n const seen = new Set<string>()\n const merged: string[] = []\n for (const id of preferred) {\n const trimmed = id.trim()\n if (!trimmed || seen.has(trimmed) || !current.includes(trimmed)) continue\n seen.add(trimmed)\n merged.push(trimmed)\n }\n for (const id of current) {\n if (seen.has(id)) continue\n seen.add(id)\n merged.push(id)\n }\n return merged\n}\n\n/** Reorders items by preferred keys; items missing from `preferred` keep their original\n * relative order at the tail. Drops keys that no longer exist. */\nexport function applyItemOrder<T>(items: T[], keyOf: (item: T) => string, preferred: string[] | undefined): T[] {\n if (!preferred || preferred.length === 0) return items\n const byKey = new Map<string, T>()\n for (const item of items) byKey.set(keyOf(item), item)\n const seen = new Set<string>()\n const ordered: T[] = []\n for (const key of preferred) {\n if (seen.has(key)) continue\n const match = byKey.get(key)\n if (!match) continue\n ordered.push(match)\n seen.add(key)\n }\n for (const item of items) {\n const key = keyOf(item)\n if (seen.has(key)) continue\n ordered.push(item)\n seen.add(key)\n }\n return ordered\n}\n\nfunction applyItemDraft(item: SidebarItem, draft: SidebarCustomizationDraft): SidebarItem {\n const itemKey = resolveItemKey(item)\n const baseTitle = item.defaultTitle ?? item.title\n const override = draft.itemLabels[itemKey]?.trim()\n const children = item.children\n ? item.children.map((child) => applyItemDraft(child, draft))\n : undefined\n const hidden = draft.hiddenItemIds[itemKey] === true\n return {\n ...item,\n title: override && override.length > 0 ? override : baseTitle,\n hidden,\n children,\n }\n}\n\nexport function applyCustomizationDraft(\n baseGroups: SidebarGroup[],\n draft: SidebarCustomizationDraft,\n): SidebarGroup[] {\n const clones = cloneSidebarGroups(baseGroups)\n const byId = new Map<string, SidebarGroup>()\n for (const group of clones) {\n byId.set(resolveGroupKey(group), group)\n }\n const orderedIds = mergeGroupOrder(draft.order, Array.from(byId.keys()))\n const seen = new Set<string>()\n const result: SidebarGroup[] = []\n for (const id of orderedIds) {\n if (seen.has(id)) continue\n const group = byId.get(id)\n if (!group) continue\n seen.add(id)\n const baseName = group.defaultName ?? group.name\n const override = draft.groupLabels[id]?.trim()\n const orderedItems = applyItemOrder(group.items, resolveItemKey, draft.itemOrder?.[id])\n result.push({\n ...group,\n name: override && override.length > 0 ? override : baseName,\n items: orderedItems.map((item) => applyItemDraft(item, draft)),\n })\n }\n return result\n}\n\n/**\n * Filters groups to include only main sidebar items.\n * Excludes items with pageContext 'settings' or 'profile' from customization.\n * Per SPEC-007: Sidebar customization applies only to the main sidebar.\n */\nexport function filterMainSidebarGroups(groups: SidebarGroup[]): SidebarGroup[] {\n const isMainItem = (item: SidebarItem): boolean => {\n if (item.pageContext && item.pageContext !== 'main') return false\n return true\n }\n\n return groups\n .map((group) => ({\n ...group,\n items: group.items.filter(isMainItem).map((item) => ({\n ...item,\n children: item.children?.filter(isMainItem),\n })),\n }))\n .filter((group) => group.items.length > 0)\n}\n\nexport function collectSidebarDefaults(groups: SidebarGroup[]) {\n const groupDefaults = new Map<string, string>()\n const itemDefaults = new Map<string, string>()\n\n const visitItems = (items: SidebarItem[]) => {\n for (const item of items) {\n const key = resolveItemKey(item)\n const baseTitle = item.defaultTitle ?? item.title\n itemDefaults.set(key, baseTitle)\n // Backward-compatible alias for legacy stored href-based preferences.\n itemDefaults.set(item.href, baseTitle)\n if (item.children && item.children.length > 0) visitItems(item.children)\n }\n }\n\n for (const group of groups) {\n const key = resolveGroupKey(group)\n groupDefaults.set(key, group.defaultName ?? group.name)\n visitItems(group.items)\n }\n\n return { groupDefaults, itemDefaults }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { slugifySidebarId } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\n\nexport type SidebarItem = {\n id?: string\n href: string\n title: string\n defaultTitle?: string\n icon?: React.ReactNode\n iconName?: string\n iconMarkup?: string\n enabled?: boolean\n hidden?: boolean\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n children?: SidebarItem[]\n}\n\nexport type SidebarGroup = {\n id?: string\n name: string\n defaultName?: string\n items: SidebarItem[]\n}\n\nexport type SidebarCustomizationDraft = {\n order: string[]\n groupLabels: Record<string, string>\n itemLabels: Record<string, string>\n hiddenItemIds: Record<string, boolean>\n /** Per-group ordered item keys. Missing items keep their natural position at the tail. */\n itemOrder: Record<string, string[]>\n}\n\nexport type SidebarRoleTarget = {\n id: string\n name: string\n hasPreference: boolean\n}\n\nexport function resolveGroupKey(group: SidebarGroup): string {\n if (group.id && group.id.length) return group.id\n if (group.defaultName && group.defaultName.length) return slugifySidebarId(group.defaultName)\n return slugifySidebarId(group.name)\n}\n\nexport function resolveItemKey(item: { id?: string; href: string }): string {\n const candidate = item.id?.trim()\n if (candidate && candidate.length > 0) return candidate\n return item.href\n}\n\nexport function cloneSidebarGroups(groups: SidebarGroup[]): SidebarGroup[] {\n const cloneItem = (item: SidebarItem): SidebarItem => ({\n id: item.id,\n href: item.href,\n title: item.title,\n defaultTitle: item.defaultTitle,\n icon: item.icon,\n iconName: item.iconName,\n iconMarkup: item.iconMarkup,\n enabled: item.enabled,\n hidden: item.hidden,\n pageContext: item.pageContext,\n children: item.children ? item.children.map((child) => cloneItem(child)) : undefined,\n })\n return groups.map((group) => ({\n id: group.id,\n name: group.name,\n defaultName: group.defaultName,\n items: group.items.map((item) => cloneItem(item)),\n }))\n}\n\nexport function mergeGroupOrder(preferred: string[], current: string[]): string[] {\n const seen = new Set<string>()\n const merged: string[] = []\n for (const id of preferred) {\n const trimmed = id.trim()\n if (!trimmed || seen.has(trimmed) || !current.includes(trimmed)) continue\n seen.add(trimmed)\n merged.push(trimmed)\n }\n for (const id of current) {\n if (seen.has(id)) continue\n seen.add(id)\n merged.push(id)\n }\n return merged\n}\n\n/** Reorders items by preferred keys; items missing from `preferred` keep their original\n * relative order at the tail. Drops keys that no longer exist. */\nexport function applyItemOrder<T>(items: T[], keyOf: (item: T) => string, preferred: string[] | undefined): T[] {\n if (!preferred || preferred.length === 0) return items\n const byKey = new Map<string, T>()\n for (const item of items) byKey.set(keyOf(item), item)\n const seen = new Set<string>()\n const ordered: T[] = []\n for (const key of preferred) {\n if (seen.has(key)) continue\n const match = byKey.get(key)\n if (!match) continue\n ordered.push(match)\n seen.add(key)\n }\n for (const item of items) {\n const key = keyOf(item)\n if (seen.has(key)) continue\n ordered.push(item)\n seen.add(key)\n }\n return ordered\n}\n\nfunction applyItemDraft(item: SidebarItem, draft: SidebarCustomizationDraft): SidebarItem {\n const itemKey = resolveItemKey(item)\n const baseTitle = item.defaultTitle ?? item.title\n const override = draft.itemLabels[itemKey]?.trim()\n const children = item.children\n ? item.children.map((child) => applyItemDraft(child, draft))\n : undefined\n const hidden = draft.hiddenItemIds[itemKey] === true\n return {\n ...item,\n title: override && override.length > 0 ? override : baseTitle,\n hidden,\n children,\n }\n}\n\nexport function applyCustomizationDraft(\n baseGroups: SidebarGroup[],\n draft: SidebarCustomizationDraft,\n): SidebarGroup[] {\n const clones = cloneSidebarGroups(baseGroups)\n const byId = new Map<string, SidebarGroup>()\n for (const group of clones) {\n byId.set(resolveGroupKey(group), group)\n }\n const orderedIds = mergeGroupOrder(draft.order, Array.from(byId.keys()))\n const seen = new Set<string>()\n const result: SidebarGroup[] = []\n for (const id of orderedIds) {\n if (seen.has(id)) continue\n const group = byId.get(id)\n if (!group) continue\n seen.add(id)\n const baseName = group.defaultName ?? group.name\n const override = draft.groupLabels[id]?.trim()\n const orderedItems = applyItemOrder(group.items, resolveItemKey, draft.itemOrder?.[id])\n result.push({\n ...group,\n name: override && override.length > 0 ? override : baseName,\n items: orderedItems.map((item) => applyItemDraft(item, draft)),\n })\n }\n return result\n}\n\n/** Every item key a group owns, including nested children. */\nexport function collectGroupItemKeys(group: SidebarGroup): string[] {\n const keys: string[] = []\n const walk = (items: SidebarItem[]) => {\n for (const item of items) {\n keys.push(resolveItemKey(item))\n if (item.children?.length) walk(item.children)\n }\n }\n walk(group.items)\n return keys\n}\n\n/**\n * Whether a group is fully visible, fully hidden, or in between. `AppShell` drops a group once every\n * one of its items is hidden, so `'hidden'` is the state that removes the group from the sidebar.\n */\nexport function resolveGroupVisibility(\n group: SidebarGroup,\n hiddenItemIds: Record<string, boolean>,\n): 'visible' | 'hidden' | 'partial' {\n const keys = collectGroupItemKeys(group)\n if (keys.length === 0) return 'visible'\n const hiddenCount = keys.filter((key) => hiddenItemIds[key]).length\n if (hiddenCount === 0) return 'visible'\n if (hiddenCount === keys.length) return 'hidden'\n return 'partial'\n}\n\n/**\n * Hides or shows every item in a group at once. Writes the same `hiddenItemIds` keys the per-item\n * switches use, so the resulting settings are indistinguishable from reaching this state by toggling\n * each item by hand \u2014 no new persisted shape, no migration.\n */\nexport function applyGroupHidden(\n hiddenItemIds: Record<string, boolean>,\n group: SidebarGroup,\n hidden: boolean,\n): Record<string, boolean> {\n const next = { ...hiddenItemIds }\n for (const key of collectGroupItemKeys(group)) {\n if (hidden) next[key] = true\n else delete next[key]\n }\n return next\n}\n\n/**\n * Filters groups to include only main sidebar items.\n * Excludes items with pageContext 'settings' or 'profile' from customization.\n * Per SPEC-007: Sidebar customization applies only to the main sidebar.\n */\nexport function filterMainSidebarGroups(groups: SidebarGroup[]): SidebarGroup[] {\n const isMainItem = (item: SidebarItem): boolean => {\n if (item.pageContext && item.pageContext !== 'main') return false\n return true\n }\n\n return groups\n .map((group) => ({\n ...group,\n items: group.items.filter(isMainItem).map((item) => ({\n ...item,\n children: item.children?.filter(isMainItem),\n })),\n }))\n .filter((group) => group.items.length > 0)\n}\n\nexport function collectSidebarDefaults(groups: SidebarGroup[]) {\n const groupDefaults = new Map<string, string>()\n const itemDefaults = new Map<string, string>()\n\n const visitItems = (items: SidebarItem[]) => {\n for (const item of items) {\n const key = resolveItemKey(item)\n const baseTitle = item.defaultTitle ?? item.title\n itemDefaults.set(key, baseTitle)\n // Backward-compatible alias for legacy stored href-based preferences.\n itemDefaults.set(item.href, baseTitle)\n if (item.children && item.children.length > 0) visitItems(item.children)\n }\n }\n\n for (const group of groups) {\n const key = resolveGroupKey(group)\n groupDefaults.set(key, group.defaultName ?? group.name)\n visitItems(group.items)\n }\n\n return { groupDefaults, itemDefaults }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,22 @@ import { apiFetch } from "./api.js";
|
|
|
3
3
|
import { raiseCrudError, readJsonSafe } from "./serverErrors.js";
|
|
4
4
|
import { createScopedHeaderStack } from "./scopedHeaderStack.js";
|
|
5
5
|
const scopedRequestHeaders = createScopedHeaderStack();
|
|
6
|
+
function isAbortError(error) {
|
|
7
|
+
return typeof error === "object" && error !== null && error.name === "AbortError";
|
|
8
|
+
}
|
|
9
|
+
function createAbortError() {
|
|
10
|
+
if (typeof DOMException === "function") {
|
|
11
|
+
return new DOMException("[internal] The operation was aborted.", "AbortError");
|
|
12
|
+
}
|
|
13
|
+
const error = new Error("[internal] The operation was aborted.");
|
|
14
|
+
error.name = "AbortError";
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
function resolveAbortSignal(input, init) {
|
|
18
|
+
if (init?.signal) return init.signal;
|
|
19
|
+
if (typeof Request !== "undefined" && input instanceof Request) return input.signal ?? null;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
6
22
|
function mergeHeaders(base, extra) {
|
|
7
23
|
if (!base) return extra;
|
|
8
24
|
if (typeof Headers !== "undefined" && base instanceof Headers) {
|
|
@@ -31,9 +47,13 @@ async function apiCall(input, init, options) {
|
|
|
31
47
|
const source = typeof response.clone === "function" ? response.clone() : response;
|
|
32
48
|
if (parser) result = await parser(source);
|
|
33
49
|
else result = await readJsonSafe(source, fallback);
|
|
34
|
-
} catch {
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (isAbortError(err)) throw err;
|
|
35
52
|
result = fallback;
|
|
36
53
|
}
|
|
54
|
+
if (result == null && resolveAbortSignal(input, init)?.aborted) {
|
|
55
|
+
throw createAbortError();
|
|
56
|
+
}
|
|
37
57
|
return {
|
|
38
58
|
ok: response.ok,
|
|
39
59
|
status: response.status,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/apiCall.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { apiFetch } from './api'\nimport { raiseCrudError, readJsonSafe } from './serverErrors'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nexport type ApiCallOptions<TReturn> = {\n parse?: (res: Response) => Promise<TReturn | null>\n fallback?: TReturn | null\n}\n\nexport type ApiCallResult<TReturn> = {\n ok: boolean\n status: number\n result: TReturn | null\n response: Response\n cacheStatus: 'hit' | 'miss' | null\n}\n\nconst scopedRequestHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {\n if (!base) return extra\n if (typeof Headers !== 'undefined' && base instanceof Headers) {\n const merged = new Headers(base)\n Object.entries(extra).forEach(([key, value]) => merged.set(key, value))\n return merged\n }\n if (Array.isArray(base)) {\n return [...base, ...Object.entries(extra)]\n }\n return { ...(base as Record<string, string>), ...extra }\n}\n\nexport async function withScopedApiRequestHeaders<T>(\n headers: Record<string, string>,\n run: () => Promise<T>,\n): Promise<T> {\n return scopedRequestHeaders.withScopedHeaders(headers, run)\n}\n\nexport async function apiCall<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const scopedHeaders = scopedRequestHeaders.resolveScopedHeaders()\n const requestInit = Object.keys(scopedHeaders).length > 0\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scopedHeaders) }\n : init\n const response = await apiFetch(input, requestInit)\n const parser = options?.parse\n const fallback = options?.fallback ?? null\n let result: TReturn | null = null\n const rawCacheStatus =\n response.headers?.get?.('x-om-cache') ??\n response.headers?.get?.('x-cache-status') ??\n null\n const cacheStatus = rawCacheStatus === 'hit' || rawCacheStatus === 'miss' ? rawCacheStatus : null\n try {\n const source = typeof (response as Response & { clone?: () => Response }).clone === 'function'\n ? response.clone()\n : response\n if (parser) result = await parser(source)\n else result = await readJsonSafe<TReturn>(source, fallback)\n } catch {\n result = fallback\n }\n return {\n ok: response.ok,\n status: response.status,\n result,\n response,\n cacheStatus,\n }\n}\n\nexport type ApiCallOrThrowOptions<TReturn> = ApiCallOptions<TReturn> & {\n errorMessage?: string\n}\n\nexport async function apiCallOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOrThrowOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const { errorMessage, ...callOptions } = options ?? {}\n const call = await apiCall<TReturn>(input, init, callOptions)\n if (!call.ok) {\n await raiseCrudError(call.response, errorMessage)\n }\n return call\n}\n\nexport type ReadApiResultOrThrowOptions<TReturn> = ApiCallOrThrowOptions<TReturn> & {\n allowNullResult?: boolean\n emptyResultMessage?: string\n}\n\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult?: false },\n): Promise<TReturn>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init: RequestInit | undefined,\n options: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult: true },\n): Promise<TReturn | null>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn>,\n): Promise<TReturn | null> {\n const { allowNullResult = false, emptyResultMessage, ...callOptions } = options ?? {}\n const call = await apiCallOrThrow<TReturn>(input, init, callOptions)\n if (call.result == null && !allowNullResult) {\n const fallback =\n emptyResultMessage ?? callOptions.errorMessage ?? `Missing response payload (${call.status})`\n throw new Error(fallback)\n }\n return call.result\n}\n"],
|
|
5
|
-
"mappings": ";AAEA,SAAS,gBAAgB;AACzB,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,+BAA+B;AAexC,MAAM,uBAAuB,wBAAwB;AAErD,SAAS,aAAa,MAA+B,OAA4C;AAC/F,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,OAAO,YAAY,eAAe,gBAAgB,SAAS;AAC7D,UAAM,SAAS,IAAI,QAAQ,IAAI;AAC/B,WAAO,QAAQ,KAAK,EAAE,QAAQ,CAAC,CAAC,KAAK,KAAK,MAAM,OAAO,IAAI,KAAK,KAAK,CAAC;AACtE,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,CAAC,GAAG,MAAM,GAAG,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC3C;AACA,SAAO,EAAE,GAAI,MAAiC,GAAG,MAAM;AACzD;AAEA,eAAsB,4BACpB,SACA,KACY;AACZ,SAAO,qBAAqB,kBAAkB,SAAS,GAAG;AAC5D;AAEA,eAAsB,QACpB,OACA,MACA,SACiC;AACjC,QAAM,gBAAgB,qBAAqB,qBAAqB;AAChE,QAAM,cAAc,OAAO,KAAK,aAAa,EAAE,SAAS,IACpD,EAAE,GAAI,QAAQ,CAAC,GAAI,SAAS,aAAa,MAAM,SAAS,aAAa,EAAE,IACvE;AACJ,QAAM,WAAW,MAAM,SAAS,OAAO,WAAW;AAClD,QAAM,SAAS,SAAS;AACxB,QAAM,WAAW,SAAS,YAAY;AACtC,MAAI,SAAyB;AAC7B,QAAM,iBACJ,SAAS,SAAS,MAAM,YAAY,KACpC,SAAS,SAAS,MAAM,gBAAgB,KACxC;AACF,QAAM,cAAc,mBAAmB,SAAS,mBAAmB,SAAS,iBAAiB;AAC7F,MAAI;AACF,UAAM,SAAS,OAAQ,SAAmD,UAAU,aAChF,SAAS,MAAM,IACf;AACJ,QAAI,OAAQ,UAAS,MAAM,OAAO,MAAM;AAAA,QACnC,UAAS,MAAM,aAAsB,QAAQ,QAAQ;AAAA,EAC5D,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { apiFetch } from './api'\nimport { raiseCrudError, readJsonSafe } from './serverErrors'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nexport type ApiCallOptions<TReturn> = {\n parse?: (res: Response) => Promise<TReturn | null>\n fallback?: TReturn | null\n}\n\nexport type ApiCallResult<TReturn> = {\n ok: boolean\n status: number\n result: TReturn | null\n response: Response\n cacheStatus: 'hit' | 'miss' | null\n}\n\nconst scopedRequestHeaders = createScopedHeaderStack()\n\nfunction isAbortError(error: unknown): boolean {\n return typeof error === 'object'\n && error !== null\n && (error as { name?: unknown }).name === 'AbortError'\n}\n\nfunction createAbortError(): Error {\n if (typeof DOMException === 'function') {\n return new DOMException('[internal] The operation was aborted.', 'AbortError')\n }\n const error = new Error('[internal] The operation was aborted.')\n error.name = 'AbortError'\n return error\n}\n\nfunction resolveAbortSignal(input: RequestInfo | URL, init?: RequestInit): AbortSignal | null {\n if (init?.signal) return init.signal\n if (typeof Request !== 'undefined' && input instanceof Request) return input.signal ?? null\n return null\n}\n\nfunction mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {\n if (!base) return extra\n if (typeof Headers !== 'undefined' && base instanceof Headers) {\n const merged = new Headers(base)\n Object.entries(extra).forEach(([key, value]) => merged.set(key, value))\n return merged\n }\n if (Array.isArray(base)) {\n return [...base, ...Object.entries(extra)]\n }\n return { ...(base as Record<string, string>), ...extra }\n}\n\nexport async function withScopedApiRequestHeaders<T>(\n headers: Record<string, string>,\n run: () => Promise<T>,\n): Promise<T> {\n return scopedRequestHeaders.withScopedHeaders(headers, run)\n}\n\nexport async function apiCall<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const scopedHeaders = scopedRequestHeaders.resolveScopedHeaders()\n const requestInit = Object.keys(scopedHeaders).length > 0\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scopedHeaders) }\n : init\n const response = await apiFetch(input, requestInit)\n const parser = options?.parse\n const fallback = options?.fallback ?? null\n let result: TReturn | null = null\n const rawCacheStatus =\n response.headers?.get?.('x-om-cache') ??\n response.headers?.get?.('x-cache-status') ??\n null\n const cacheStatus = rawCacheStatus === 'hit' || rawCacheStatus === 'miss' ? rawCacheStatus : null\n try {\n const source = typeof (response as Response & { clone?: () => Response }).clone === 'function'\n ? response.clone()\n : response\n if (parser) result = await parser(source)\n else result = await readJsonSafe<TReturn>(source, fallback)\n } catch (err) {\n if (isAbortError(err)) throw err\n result = fallback\n }\n // `readJsonSafe` swallows the abort that cancels an in-flight body read, so a\n // request aborted after its response headers arrived would otherwise look like\n // a successful call that returned an empty payload.\n if (result == null && resolveAbortSignal(input, init)?.aborted) {\n throw createAbortError()\n }\n return {\n ok: response.ok,\n status: response.status,\n result,\n response,\n cacheStatus,\n }\n}\n\nexport type ApiCallOrThrowOptions<TReturn> = ApiCallOptions<TReturn> & {\n errorMessage?: string\n}\n\nexport async function apiCallOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOrThrowOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const { errorMessage, ...callOptions } = options ?? {}\n const call = await apiCall<TReturn>(input, init, callOptions)\n if (!call.ok) {\n await raiseCrudError(call.response, errorMessage)\n }\n return call\n}\n\nexport type ReadApiResultOrThrowOptions<TReturn> = ApiCallOrThrowOptions<TReturn> & {\n allowNullResult?: boolean\n emptyResultMessage?: string\n}\n\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult?: false },\n): Promise<TReturn>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init: RequestInit | undefined,\n options: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult: true },\n): Promise<TReturn | null>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn>,\n): Promise<TReturn | null> {\n const { allowNullResult = false, emptyResultMessage, ...callOptions } = options ?? {}\n const call = await apiCallOrThrow<TReturn>(input, init, callOptions)\n if (call.result == null && !allowNullResult) {\n const fallback =\n emptyResultMessage ?? callOptions.errorMessage ?? `Missing response payload (${call.status})`\n throw new Error(fallback)\n }\n return call.result\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/customFieldDefs.ts"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport { useQuery, type UseQueryResult, type QueryClient } from '@tanstack/react-query'\nimport { readApiResultOrThrow } from './apiCall'\nimport type { CustomFieldOptionDto } from '@open-mercato/shared/modules/entities/options'\n\nexport type CustomFieldDefDto = {\n entityId?: string\n key: string\n kind: string\n label?: string\n description?: string\n options?: CustomFieldOptionDto[]\n optionsUrl?: string\n multi?: boolean\n filterable?: boolean\n formEditable?: boolean\n listVisible?: boolean\n editor?: string\n input?: string\n priority?: number\n fieldset?: string\n fieldsets?: string[]\n group?: { code: string; title?: string; hint?: string }\n // attachments-specific config\n maxAttachmentSizeMb?: number\n acceptExtensions?: string[]\n // phone-specific config\n defaultCountryIso2?: string\n // optional validation rules\n validation?: Array<\n | { rule: 'required'; message: string }\n | { rule: 'date'; message: string }\n | { rule: 'integer'; message: string }\n | { rule: 'float'; message: string }\n | { rule: 'lt' | 'lte' | 'gt' | 'gte'; param: number; message: string }\n | { rule: 'eq' | 'ne'; param: any; message: string }\n | { rule: 'regex'; param: string; message: string }\n >\n dictionaryId?: string\n dictionaryInlineCreate?: boolean\n defaultValue?: string | number | boolean | null\n}\n\nexport type CustomFieldsetGroupDto = {\n code: string\n title?: string\n hint?: string\n}\n\nexport type CustomFieldsetDto = {\n code: string\n label: string\n icon?: string\n description?: string\n groups?: CustomFieldsetGroupDto[]\n}\n\nexport type CustomFieldDefinitionsPayload = {\n items?: CustomFieldDefDto[]\n fieldsetsByEntity?: Record<string, CustomFieldsetDto[]>\n entitySettings?: Record<string, { singleFieldsetPerRecord?: boolean }>\n}\n\nexport function normalizeEntityIds(entityIds: string | string[] | null | undefined): string[] {\n if (entityIds == null) return []\n const list = Array.isArray(entityIds) ? entityIds : [entityIds]\n const dedup = new Set<string>()\n const normalized: string[] = []\n for (const raw of list) {\n const trimmed = String(raw ?? '').trim()\n if (!trimmed || dedup.has(trimmed)) continue\n dedup.add(trimmed)\n normalized.push(trimmed)\n }\n return normalized\n}\n\nexport type CustomFieldDefinitionQueryOptions = {\n fieldset?: string\n}\n\nfunction buildDefinitionsQuery(entityIds: string[], options?: CustomFieldDefinitionQueryOptions): string {\n const params = new URLSearchParams()\n entityIds.forEach((id) => {\n if (id) params.append('entityId', id)\n })\n if (options?.fieldset) params.set('fieldset', options.fieldset)\n return params.toString()\n}\n\ntype CustomFieldDefinitionsResponse = CustomFieldDefinitionsPayload\n\nfunction normalizeRecord<T>(value: unknown): Record<string, T[]> {\n if (!value || typeof value !== 'object') return {}\n const out: Record<string, T[]> = {}\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n if (!Array.isArray(raw)) continue\n out[key] = raw as T[]\n }\n return out\n}\n\nfunction preparePayload(data: CustomFieldDefinitionsResponse | null | undefined): CustomFieldDefinitionsPayload {\n const items = Array.isArray(data?.items) ? [...data!.items] : []\n items.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))\n const fieldsetsByEntity = normalizeRecord<CustomFieldsetDto>(data?.fieldsetsByEntity)\n const entitySettings = (data?.entitySettings && typeof data.entitySettings === 'object'\n ? data.entitySettings\n : {}) as CustomFieldDefinitionsPayload['entitySettings']\n return { items, fieldsetsByEntity, entitySettings }\n}\n\nasync function readDefinitionsViaFetch(\n entityIds: string[],\n fetchImpl: typeof fetch,\n options?: CustomFieldDefinitionQueryOptions,\n): Promise<CustomFieldDefinitionsPayload> {\n const query = buildDefinitionsQuery(entityIds, options)\n const res = await fetchImpl(`/api/entities/definitions?${query}`, {\n headers: { 'content-type': 'application/json' },\n })\n const data = await res.json().catch(() => ({ items: [] }))\n return preparePayload(data)\n}\n\nasync function readDefinitionsViaApi(entityIds: string[], options?: CustomFieldDefinitionQueryOptions): Promise<CustomFieldDefinitionsPayload> {\n const query = buildDefinitionsQuery(entityIds, options)\n const payload = await readApiResultOrThrow<CustomFieldDefinitionsResponse>(\n `/api/entities/definitions?${query}`,\n { headers: { 'content-type': 'application/json' } },\n {\n errorMessage: 'Failed to load custom field definitions',\n fallback: { items: [] },\n },\n )\n return preparePayload(payload)\n}\n\nexport async function fetchCustomFieldDefinitionsPayload(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: CustomFieldDefinitionQueryOptions,\n): Promise<CustomFieldDefinitionsPayload> {\n const filtered = normalizeEntityIds(entityIds)\n if (!filtered.length) return { items: [] }\n return fetchImpl\n ? await readDefinitionsViaFetch(filtered, fetchImpl, options)\n : await readDefinitionsViaApi(filtered, options)\n}\n\nexport async function fetchCustomFieldDefs(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: CustomFieldDefinitionQueryOptions,\n): Promise<CustomFieldDefDto[]> {\n const payload = await fetchCustomFieldDefinitionsPayload(entityIds, fetchImpl, options)\n return payload.items ?? []\n}\n\nexport type UseCustomFieldDefsOptions<TData = CustomFieldDefDto[]> = {\n enabled?: boolean\n staleTime?: number\n gcTime?: number\n /** @deprecated Custom fetch implementations are no longer needed. */\n fetchImpl?: typeof fetch\n keyExtras?: Array<string | number | boolean | null | undefined>\n fieldset?: string\n select?: (data: CustomFieldDefDto[]) => TData\n}\n\nexport function useCustomFieldDefs<TData = CustomFieldDefDto[]>(\n entityIds: string | string[] | null | undefined,\n options: UseCustomFieldDefsOptions<TData> = {}\n): UseQueryResult<TData> {\n const {\n enabled: enabledOption = true,\n staleTime,\n gcTime,\n keyExtras,\n fetchImpl,\n fieldset,\n } = options\n const normalizedIds = React.useMemo(() => normalizeEntityIds(entityIds), [entityIds])\n const idsSignature = React.useMemo(() => JSON.stringify(normalizedIds), [normalizedIds])\n const extrasSignature = React.useMemo(() => JSON.stringify(keyExtras ?? []), [keyExtras])\n const normalizedFieldset = typeof fieldset === 'string' && fieldset.trim().length ? fieldset.trim() : null\n const queryKey = React.useMemo(\n () => ['customFieldDefs', ...(keyExtras ?? []), ...normalizedIds, `fieldset:${normalizedFieldset ?? 'default'}`],\n [idsSignature, extrasSignature, normalizedFieldset]\n )\n const enabled = enabledOption && normalizedIds.length > 0\n\n return useQuery<CustomFieldDefDto[], Error, TData>({\n queryKey,\n queryFn: () =>\n fetchCustomFieldDefs(\n normalizedIds,\n fetchImpl,\n normalizedFieldset ? { fieldset: normalizedFieldset } : undefined\n ),\n enabled,\n staleTime: staleTime ?? 5 * 60 * 1000,\n gcTime: gcTime ?? 30 * 60 * 1000,\n select: options.select,\n })\n}\n\nexport type CustomFieldVisibility = 'list' | 'form' | 'filter'\n\nexport function isDefVisible(def: CustomFieldDefDto, mode: CustomFieldVisibility): boolean {\n switch (mode) {\n case 'list':\n return def.listVisible !== false\n case 'form':\n return def.formEditable !== false\n case 'filter':\n return !!def.filterable\n default:\n return true\n }\n}\n\nexport function filterCustomFieldDefs(defs: CustomFieldDefDto[], mode: CustomFieldVisibility): CustomFieldDefDto[] {\n return defs\n .filter((d) => isDefVisible(d, mode))\n .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))\n}\n\nexport async function invalidateCustomFieldDefs(\n queryClient: QueryClient,\n entityIds?: string | string[] | null,\n): Promise<void> {\n const normalizedIds = normalizeEntityIds(entityIds)\n const targetPrefixes = new Set(['customFieldDefs', 'customFieldForms', 'dealFormFields'])\n if (!normalizedIds.length) {\n await queryClient.invalidateQueries({\n predicate: (query) => Array.isArray(query.queryKey) && typeof query.queryKey[0] === 'string' && targetPrefixes.has(query.queryKey[0] as string),\n })\n return\n }\n await queryClient.invalidateQueries({\n predicate: (query) => {\n if (!Array.isArray(query.queryKey)) return false\n const [prefix] = query.queryKey\n if (typeof prefix !== 'string' || !targetPrefixes.has(prefix)) return false\n return normalizedIds.every((id) => query.queryKey.includes(id))\n },\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;AACvB,SAAS,gBAAuD;AAChE,SAAS,4BAA4B;
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport { useQuery, type UseQueryResult, type QueryClient } from '@tanstack/react-query'\nimport { readApiResultOrThrow } from './apiCall'\nimport type { CustomFieldOptionDto } from '@open-mercato/shared/modules/entities/options'\n\nexport type CustomFieldDefDto = {\n entityId?: string\n key: string\n kind: string\n label?: string\n description?: string\n options?: CustomFieldOptionDto[]\n optionsUrl?: string\n multi?: boolean\n filterable?: boolean\n formEditable?: boolean\n listVisible?: boolean\n editor?: string\n input?: string\n priority?: number\n fieldset?: string\n fieldsets?: string[]\n group?: { code: string; title?: string; hint?: string }\n // attachments-specific config\n maxAttachmentSizeMb?: number\n acceptExtensions?: string[]\n // phone-specific config\n defaultCountryIso2?: string\n // optional validation rules\n validation?: Array<\n | { rule: 'required'; message: string }\n | { rule: 'date'; message: string }\n | { rule: 'integer'; message: string }\n | { rule: 'float'; message: string }\n | { rule: 'lt' | 'lte' | 'gt' | 'gte'; param: number; message: string }\n | { rule: 'eq' | 'ne'; param: any; message: string }\n | { rule: 'regex'; param: string; message: string }\n >\n dictionaryId?: string\n dictionaryInlineCreate?: boolean\n defaultValue?: string | number | boolean | null\n relatedEntityId?: string\n}\n\nexport type CustomFieldsetGroupDto = {\n code: string\n title?: string\n hint?: string\n}\n\nexport type CustomFieldsetDto = {\n code: string\n label: string\n icon?: string\n description?: string\n groups?: CustomFieldsetGroupDto[]\n}\n\nexport type CustomFieldDefinitionsPayload = {\n items?: CustomFieldDefDto[]\n fieldsetsByEntity?: Record<string, CustomFieldsetDto[]>\n entitySettings?: Record<string, { singleFieldsetPerRecord?: boolean }>\n}\n\nexport function normalizeEntityIds(entityIds: string | string[] | null | undefined): string[] {\n if (entityIds == null) return []\n const list = Array.isArray(entityIds) ? entityIds : [entityIds]\n const dedup = new Set<string>()\n const normalized: string[] = []\n for (const raw of list) {\n const trimmed = String(raw ?? '').trim()\n if (!trimmed || dedup.has(trimmed)) continue\n dedup.add(trimmed)\n normalized.push(trimmed)\n }\n return normalized\n}\n\nexport type CustomFieldDefinitionQueryOptions = {\n fieldset?: string\n}\n\nfunction buildDefinitionsQuery(entityIds: string[], options?: CustomFieldDefinitionQueryOptions): string {\n const params = new URLSearchParams()\n entityIds.forEach((id) => {\n if (id) params.append('entityId', id)\n })\n if (options?.fieldset) params.set('fieldset', options.fieldset)\n return params.toString()\n}\n\ntype CustomFieldDefinitionsResponse = CustomFieldDefinitionsPayload\n\nfunction normalizeRecord<T>(value: unknown): Record<string, T[]> {\n if (!value || typeof value !== 'object') return {}\n const out: Record<string, T[]> = {}\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n if (!Array.isArray(raw)) continue\n out[key] = raw as T[]\n }\n return out\n}\n\nfunction preparePayload(data: CustomFieldDefinitionsResponse | null | undefined): CustomFieldDefinitionsPayload {\n const items = Array.isArray(data?.items) ? [...data!.items] : []\n items.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))\n const fieldsetsByEntity = normalizeRecord<CustomFieldsetDto>(data?.fieldsetsByEntity)\n const entitySettings = (data?.entitySettings && typeof data.entitySettings === 'object'\n ? data.entitySettings\n : {}) as CustomFieldDefinitionsPayload['entitySettings']\n return { items, fieldsetsByEntity, entitySettings }\n}\n\nasync function readDefinitionsViaFetch(\n entityIds: string[],\n fetchImpl: typeof fetch,\n options?: CustomFieldDefinitionQueryOptions,\n): Promise<CustomFieldDefinitionsPayload> {\n const query = buildDefinitionsQuery(entityIds, options)\n const res = await fetchImpl(`/api/entities/definitions?${query}`, {\n headers: { 'content-type': 'application/json' },\n })\n const data = await res.json().catch(() => ({ items: [] }))\n return preparePayload(data)\n}\n\nasync function readDefinitionsViaApi(entityIds: string[], options?: CustomFieldDefinitionQueryOptions): Promise<CustomFieldDefinitionsPayload> {\n const query = buildDefinitionsQuery(entityIds, options)\n const payload = await readApiResultOrThrow<CustomFieldDefinitionsResponse>(\n `/api/entities/definitions?${query}`,\n { headers: { 'content-type': 'application/json' } },\n {\n errorMessage: 'Failed to load custom field definitions',\n fallback: { items: [] },\n },\n )\n return preparePayload(payload)\n}\n\nexport async function fetchCustomFieldDefinitionsPayload(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: CustomFieldDefinitionQueryOptions,\n): Promise<CustomFieldDefinitionsPayload> {\n const filtered = normalizeEntityIds(entityIds)\n if (!filtered.length) return { items: [] }\n return fetchImpl\n ? await readDefinitionsViaFetch(filtered, fetchImpl, options)\n : await readDefinitionsViaApi(filtered, options)\n}\n\nexport async function fetchCustomFieldDefs(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: CustomFieldDefinitionQueryOptions,\n): Promise<CustomFieldDefDto[]> {\n const payload = await fetchCustomFieldDefinitionsPayload(entityIds, fetchImpl, options)\n return payload.items ?? []\n}\n\nexport type UseCustomFieldDefsOptions<TData = CustomFieldDefDto[]> = {\n enabled?: boolean\n staleTime?: number\n gcTime?: number\n /** @deprecated Custom fetch implementations are no longer needed. */\n fetchImpl?: typeof fetch\n keyExtras?: Array<string | number | boolean | null | undefined>\n fieldset?: string\n select?: (data: CustomFieldDefDto[]) => TData\n}\n\nexport function useCustomFieldDefs<TData = CustomFieldDefDto[]>(\n entityIds: string | string[] | null | undefined,\n options: UseCustomFieldDefsOptions<TData> = {}\n): UseQueryResult<TData> {\n const {\n enabled: enabledOption = true,\n staleTime,\n gcTime,\n keyExtras,\n fetchImpl,\n fieldset,\n } = options\n const normalizedIds = React.useMemo(() => normalizeEntityIds(entityIds), [entityIds])\n const idsSignature = React.useMemo(() => JSON.stringify(normalizedIds), [normalizedIds])\n const extrasSignature = React.useMemo(() => JSON.stringify(keyExtras ?? []), [keyExtras])\n const normalizedFieldset = typeof fieldset === 'string' && fieldset.trim().length ? fieldset.trim() : null\n const queryKey = React.useMemo(\n () => ['customFieldDefs', ...(keyExtras ?? []), ...normalizedIds, `fieldset:${normalizedFieldset ?? 'default'}`],\n [idsSignature, extrasSignature, normalizedFieldset]\n )\n const enabled = enabledOption && normalizedIds.length > 0\n\n return useQuery<CustomFieldDefDto[], Error, TData>({\n queryKey,\n queryFn: () =>\n fetchCustomFieldDefs(\n normalizedIds,\n fetchImpl,\n normalizedFieldset ? { fieldset: normalizedFieldset } : undefined\n ),\n enabled,\n staleTime: staleTime ?? 5 * 60 * 1000,\n gcTime: gcTime ?? 30 * 60 * 1000,\n select: options.select,\n })\n}\n\nexport type CustomFieldVisibility = 'list' | 'form' | 'filter'\n\nexport function isDefVisible(def: CustomFieldDefDto, mode: CustomFieldVisibility): boolean {\n switch (mode) {\n case 'list':\n return def.listVisible !== false\n case 'form':\n return def.formEditable !== false\n case 'filter':\n return !!def.filterable\n default:\n return true\n }\n}\n\nexport function filterCustomFieldDefs(defs: CustomFieldDefDto[], mode: CustomFieldVisibility): CustomFieldDefDto[] {\n return defs\n .filter((d) => isDefVisible(d, mode))\n .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))\n}\n\nexport async function invalidateCustomFieldDefs(\n queryClient: QueryClient,\n entityIds?: string | string[] | null,\n): Promise<void> {\n const normalizedIds = normalizeEntityIds(entityIds)\n const targetPrefixes = new Set(['customFieldDefs', 'customFieldForms', 'dealFormFields'])\n if (!normalizedIds.length) {\n await queryClient.invalidateQueries({\n predicate: (query) => Array.isArray(query.queryKey) && typeof query.queryKey[0] === 'string' && targetPrefixes.has(query.queryKey[0] as string),\n })\n return\n }\n await queryClient.invalidateQueries({\n predicate: (query) => {\n if (!Array.isArray(query.queryKey)) return false\n const [prefix] = query.queryKey\n if (typeof prefix !== 'string' || !targetPrefixes.has(prefix)) return false\n return normalizedIds.every((id) => query.queryKey.includes(id))\n },\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
fetchCustomFieldDefinitionsPayload
|
|
4
4
|
} from "./customFieldDefs.js";
|
|
5
5
|
import { FieldRegistry, loadGeneratedFieldRegistrations } from "../fields/registry.js";
|
|
6
|
-
import {
|
|
6
|
+
import { fetchAllOffsetPages } from "./fetchAllPages.js";
|
|
7
7
|
import { normalizeCustomFieldOptions } from "@open-mercato/shared/modules/entities/options";
|
|
8
8
|
import { CURRENCY_OPTIONS_URL } from "@open-mercato/shared/modules/entities/kinds";
|
|
9
9
|
let registryReady = null;
|
|
@@ -34,14 +34,15 @@ function buildOptionsUrl(base, query) {
|
|
|
34
34
|
}
|
|
35
35
|
async function loadRemoteOptions(url) {
|
|
36
36
|
try {
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
const pages = await fetchAllOffsetPages(url);
|
|
38
|
+
if (!pages.ok) return [];
|
|
39
|
+
return pages.items.map((item) => {
|
|
40
|
+
const record = item && typeof item === "object" ? item : null;
|
|
41
|
+
return {
|
|
42
|
+
value: String(record?.value ?? item),
|
|
43
|
+
label: String(record?.label ?? record?.value ?? item)
|
|
44
|
+
};
|
|
45
|
+
});
|
|
45
46
|
} catch {
|
|
46
47
|
return [];
|
|
47
48
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/customFieldForms.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CrudField } from '../CrudForm'\nimport type {\n CustomFieldDefDto,\n CustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport {\n filterCustomFieldDefs,\n fetchCustomFieldDefs,\n fetchCustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport { FieldRegistry, loadGeneratedFieldRegistrations } from '../fields/registry'\nimport {
|
|
5
|
-
"mappings": "AAKA;AAAA,EACE;AAAA,EAEA;AAAA,OACK;AACP,SAAS,eAAe,uCAAuC;AAC/D,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { CrudField } from '../CrudForm'\nimport type {\n CustomFieldDefDto,\n CustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport {\n filterCustomFieldDefs,\n fetchCustomFieldDefs,\n fetchCustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport { FieldRegistry, loadGeneratedFieldRegistrations } from '../fields/registry'\nimport { fetchAllOffsetPages } from './fetchAllPages'\nimport { normalizeCustomFieldOptions } from '@open-mercato/shared/modules/entities/options'\nimport { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'\n\nlet registryReady: Promise<void> | null = null\n\nasync function ensureFieldRegistryReady() {\n if (!registryReady) {\n registryReady = loadGeneratedFieldRegistrations().catch((err) => {\n registryReady = null\n throw err\n })\n }\n await registryReady\n}\n\nfunction buildOptionsUrl(base: string, query?: string): string {\n if (!query) return base\n try {\n const isAbsolute = /^([a-z][a-z\\d+\\-.]*:)?\\/\\//i.test(base)\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n const url = isAbsolute ? new URL(base) : new URL(base, origin)\n if (!url.searchParams.has('query')) url.searchParams.append('query', query)\n if (!url.searchParams.has('q')) url.searchParams.append('q', query)\n if (isAbsolute) return url.toString()\n return `${url.pathname}${url.search}`\n } catch {\n const sep = base.includes('?') ? '&' : '?'\n if (base.includes('query=')) return `${base}${sep}q=${encodeURIComponent(query)}`\n return `${base}${sep}query=${encodeURIComponent(query)}`\n }\n}\n\nasync function loadRemoteOptions(url: string): Promise<Array<{ value: string; label: string }>> {\n try {\n // Options routes bound how many rows one response carries (dictionary\n // entries at 500), so walk every page instead of reading a single\n // response \u2014 a stored value past the first page must stay selectable and\n // resolvable to its label. Routes without `hasMore` still resolve in one\n // request.\n const pages = await fetchAllOffsetPages(url)\n if (!pages.ok) return []\n return pages.items.map((item) => {\n const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : null\n return {\n value: String(record?.value ?? item),\n label: String(record?.label ?? record?.value ?? item),\n }\n })\n } catch {\n return []\n }\n}\n\nexport function buildFormFieldFromCustomFieldDef(\n def: CustomFieldDefDto,\n opts?: { bareIds?: boolean }\n): CrudField | null {\n const id = opts?.bareIds ? def.key : `cf_${def.key}`\n const label = def.label || def.key\n const required = Array.isArray((def as any).validation)\n ? ((def as any).validation as any[]).some((rule) => rule && rule.rule === 'required')\n : false\n const baseProps = {\n description: def.description,\n required,\n ...(def.defaultValue !== undefined && def.defaultValue !== null ? { defaultValue: def.defaultValue } : {}),\n }\n\n switch (def.kind) {\n case 'boolean':\n return { id, label, type: 'checkbox', ...baseProps }\n case 'integer':\n case 'float':\n return { id, label, type: 'number', ...baseProps }\n case 'date':\n return { id, label, type: 'datepicker', ...baseProps }\n case 'datetime':\n return { id, label, type: 'datetime', ...baseProps }\n case 'multiline': {\n if (def.editor === 'plain') {\n return { id, label, type: 'textarea', ...baseProps }\n }\n let editor: 'simple' | 'uiw' | 'html' = 'html'\n if (def.editor === 'simpleMarkdown') editor = 'simple'\n else if (def.editor === 'htmlRichText') editor = 'html'\n return { id, label, type: 'richtext', ...baseProps, editor }\n }\n case 'select':\n case 'currency':\n case 'relation':\n return {\n id,\n label,\n type: 'select',\n ...baseProps,\n options: normalizeCustomFieldOptions(def.options || []).map((option) => ({\n value: option.value,\n label: option.label,\n })),\n multiple: !!def.multi,\n ...(def.kind === 'currency'\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(CURRENCY_OPTIONS_URL, query)\n return loadRemoteOptions(url)\n },\n }\n : def.optionsUrl\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(def.optionsUrl!, query)\n return loadRemoteOptions(url)\n },\n }\n : {}),\n ...(def.multi && def.input === 'listbox' ? ({ listbox: true } as any) : {}),\n }\n case 'dictionary': {\n if (!def.multi) {\n const input = FieldRegistry.getInput(def.kind)\n if (input) {\n return {\n id,\n label,\n type: 'custom',\n ...baseProps,\n component: (props) => input({ ...props, def }),\n }\n }\n return { id, label, type: 'text', ...baseProps }\n }\n const optionsUrl = def.optionsUrl || (def.dictionaryId ? `/api/dictionaries/${def.dictionaryId}/entries` : undefined)\n return {\n id,\n label,\n type: 'select',\n description: def.description,\n required,\n options: [],\n multiple: true,\n listbox: true,\n ...(optionsUrl\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(optionsUrl, query)\n return loadRemoteOptions(url)\n },\n }\n : {}),\n }\n }\n default: {\n if (def.kind === 'text' && def.multi) {\n const base: any = { id, label, type: 'tags', ...baseProps }\n const resolvedOptions = normalizeCustomFieldOptions(def.options || [])\n if (resolvedOptions.length > 0) {\n base.options = resolvedOptions.map((option) => ({ value: option.value, label: option.label }))\n }\n if (def.optionsUrl) {\n base.loadOptions = async (query?: string) => {\n const url = buildOptionsUrl(def.optionsUrl!, query)\n return loadRemoteOptions(url)\n }\n }\n return base\n }\n if (def.kind === 'text' && typeof def.editor === 'string' && def.editor) {\n if (def.editor === 'plain') {\n return { id, label, type: 'textarea', ...baseProps }\n }\n let editor: 'simple' | 'uiw' | 'html' = 'html'\n if (def.editor === 'simpleMarkdown') editor = 'simple'\n else if (def.editor === 'htmlRichText') editor = 'html'\n return { id, label, type: 'richtext', ...baseProps, editor }\n }\n const input = FieldRegistry.getInput(def.kind)\n if (input) {\n return {\n id,\n label,\n type: 'custom',\n ...baseProps,\n component: (props) => input({ ...props, def }),\n }\n }\n return { id, label, type: 'text', ...baseProps }\n }\n }\n}\n\nexport function buildFormFieldsFromCustomFields(\n defs: CustomFieldDefDto[],\n opts?: { bareIds?: boolean }\n): CrudField[] {\n const fields: CrudField[] = []\n const visible = filterCustomFieldDefs(defs, 'form')\n const seenKeys = new Set<string>()\n for (const def of visible) {\n const keyLower = String(def.key).toLowerCase()\n if (seenKeys.has(keyLower)) continue\n seenKeys.add(keyLower)\n const field = buildFormFieldFromCustomFieldDef(def, opts)\n if (field) fields.push(field)\n }\n return fields\n}\n\nexport async function fetchCustomFieldFormStructure(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {\n await ensureFieldRegistryReady()\n const metadata = await fetchCustomFieldDefinitionsPayload(entityIds, fetchImpl)\n const definitions = Array.isArray(metadata.items) ? metadata.items : []\n const fields = buildFormFieldsFromCustomFields(definitions, options)\n return { fields, definitions, metadata }\n}\n\nexport async function fetchCustomFieldFormFields(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<CrudField[]> {\n const { fields } = await fetchCustomFieldFormStructure(entityIds, fetchImpl, options)\n return fields\n}\n\nexport async function fetchCustomFieldFormFieldsWithDefinitions(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {\n return fetchCustomFieldFormStructure(entityIds, fetchImpl, options)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { apiCall } from "./apiCall.js";
|
|
3
|
+
const MAX_OFFSET_PAGES = 50;
|
|
4
|
+
const ABSOLUTE_URL_PATTERN = /^([a-z][a-z\d+\-.]*:)?\/\//i;
|
|
5
|
+
function parseUrl(value) {
|
|
6
|
+
try {
|
|
7
|
+
const absolute = ABSOLUTE_URL_PATTERN.test(value);
|
|
8
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
9
|
+
return { url: absolute ? new URL(value) : new URL(value, origin), absolute };
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function formatUrl(parsed) {
|
|
15
|
+
return parsed.absolute ? parsed.url.toString() : `${parsed.url.pathname}${parsed.url.search}`;
|
|
16
|
+
}
|
|
17
|
+
function readOffset(value) {
|
|
18
|
+
const parsed = parseUrl(value);
|
|
19
|
+
if (!parsed) return 0;
|
|
20
|
+
const offset = Number.parseInt(parsed.url.searchParams.get("offset") ?? "", 10);
|
|
21
|
+
return Number.isFinite(offset) && offset > 0 ? offset : 0;
|
|
22
|
+
}
|
|
23
|
+
function withOffset(value, offset) {
|
|
24
|
+
const parsed = parseUrl(value);
|
|
25
|
+
if (!parsed) {
|
|
26
|
+
const separator = value.includes("?") ? "&" : "?";
|
|
27
|
+
return `${value}${separator}offset=${offset}`;
|
|
28
|
+
}
|
|
29
|
+
parsed.url.searchParams.set("offset", String(offset));
|
|
30
|
+
return formatUrl(parsed);
|
|
31
|
+
}
|
|
32
|
+
async function fetchAllOffsetPages(url, options = {}) {
|
|
33
|
+
const maxPages = options.maxPages ?? MAX_OFFSET_PAGES;
|
|
34
|
+
const baseOffset = readOffset(url);
|
|
35
|
+
const items = [];
|
|
36
|
+
let firstPayload = {};
|
|
37
|
+
let pageCount = 0;
|
|
38
|
+
while (pageCount < maxPages) {
|
|
39
|
+
const pageUrl = pageCount === 0 ? url : withOffset(url, baseOffset + items.length);
|
|
40
|
+
const call = await apiCall(pageUrl, void 0, { fallback: {} });
|
|
41
|
+
const payload = call.result ?? {};
|
|
42
|
+
if (!call.ok) {
|
|
43
|
+
return { ok: false, items, pageCount, firstPayload, errorPayload: payload };
|
|
44
|
+
}
|
|
45
|
+
pageCount += 1;
|
|
46
|
+
if (pageCount === 1) firstPayload = payload;
|
|
47
|
+
const pageItems = Array.isArray(payload.items) ? payload.items : [];
|
|
48
|
+
items.push(...pageItems);
|
|
49
|
+
if (payload.hasMore !== true || pageItems.length === 0) break;
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, items, pageCount, firstPayload, errorPayload: null };
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
MAX_OFFSET_PAGES,
|
|
55
|
+
fetchAllOffsetPages
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=fetchAllPages.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/utils/fetchAllPages.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { apiCall } from './apiCall'\n\n// Offset-paged list routes cap how many rows one response may carry, so a\n// caller that needs the whole collection has to walk the pages. Bound the walk\n// so a route reporting hasMore forever cannot hang the caller; at the 500-row\n// dictionary ceiling this still covers 25k rows.\nexport const MAX_OFFSET_PAGES = 50\n\nexport type OffsetPagedPayload = Record<string, unknown>\n\nexport type OffsetPagedResult = {\n ok: boolean\n items: unknown[]\n pageCount: number\n firstPayload: OffsetPagedPayload\n errorPayload: OffsetPagedPayload | null\n}\n\nconst ABSOLUTE_URL_PATTERN = /^([a-z][a-z\\d+\\-.]*:)?\\/\\//i\n\ntype ParsedUrl = { url: URL; absolute: boolean }\n\nfunction parseUrl(value: string): ParsedUrl | null {\n try {\n const absolute = ABSOLUTE_URL_PATTERN.test(value)\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n return { url: absolute ? new URL(value) : new URL(value, origin), absolute }\n } catch {\n return null\n }\n}\n\nfunction formatUrl(parsed: ParsedUrl): string {\n return parsed.absolute ? parsed.url.toString() : `${parsed.url.pathname}${parsed.url.search}`\n}\n\nfunction readOffset(value: string): number {\n const parsed = parseUrl(value)\n if (!parsed) return 0\n const offset = Number.parseInt(parsed.url.searchParams.get('offset') ?? '', 10)\n return Number.isFinite(offset) && offset > 0 ? offset : 0\n}\n\nfunction withOffset(value: string, offset: number): string {\n const parsed = parseUrl(value)\n if (!parsed) {\n const separator = value.includes('?') ? '&' : '?'\n return `${value}${separator}offset=${offset}`\n }\n parsed.url.searchParams.set('offset', String(offset))\n return formatUrl(parsed)\n}\n\n/**\n * Walk every page of an offset-paged list route and return the assembled items.\n *\n * A route that does not report `hasMore` resolves after a single request, so\n * this is a drop-in replacement for a one-shot `apiCall` against any list\n * endpoint. Ordering is preserved as the route returned it, page after page \u2014\n * callers that need a global order across page boundaries must re-sort.\n */\nexport async function fetchAllOffsetPages(\n url: string,\n options: { maxPages?: number } = {},\n): Promise<OffsetPagedResult> {\n const maxPages = options.maxPages ?? MAX_OFFSET_PAGES\n const baseOffset = readOffset(url)\n const items: unknown[] = []\n let firstPayload: OffsetPagedPayload = {}\n let pageCount = 0\n\n while (pageCount < maxPages) {\n // The first request goes out untouched so a route that caches only its\n // default page still serves that page from cache.\n const pageUrl = pageCount === 0 ? url : withOffset(url, baseOffset + items.length)\n const call = await apiCall<OffsetPagedPayload>(pageUrl, undefined, { fallback: {} })\n const payload = call.result ?? {}\n if (!call.ok) {\n return { ok: false, items, pageCount, firstPayload, errorPayload: payload }\n }\n pageCount += 1\n if (pageCount === 1) firstPayload = payload\n const pageItems = Array.isArray(payload.items) ? payload.items : []\n items.push(...pageItems)\n // An empty page also stops the walk: without it a route that keeps\n // reporting hasMore while returning nothing would spin to the page cap.\n if (payload.hasMore !== true || pageItems.length === 0) break\n }\n\n return { ok: true, items, pageCount, firstPayload, errorPayload: null }\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,eAAe;AAMjB,MAAM,mBAAmB;AAYhC,MAAM,uBAAuB;AAI7B,SAAS,SAAS,OAAiC;AACjD,MAAI;AACF,UAAM,WAAW,qBAAqB,KAAK,KAAK;AAChD,UAAM,SAAS,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AACxE,WAAO,EAAE,KAAK,WAAW,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,MAAM,GAAG,SAAS;AAAA,EAC7E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,UAAU,QAA2B;AAC5C,SAAO,OAAO,WAAW,OAAO,IAAI,SAAS,IAAI,GAAG,OAAO,IAAI,QAAQ,GAAG,OAAO,IAAI,MAAM;AAC7F;AAEA,SAAS,WAAW,OAAuB;AACzC,QAAM,SAAS,SAAS,KAAK;AAC7B,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,OAAO,SAAS,OAAO,IAAI,aAAa,IAAI,QAAQ,KAAK,IAAI,EAAE;AAC9E,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,SAAS,WAAW,OAAe,QAAwB;AACzD,QAAM,SAAS,SAAS,KAAK;AAC7B,MAAI,CAAC,QAAQ;AACX,UAAM,YAAY,MAAM,SAAS,GAAG,IAAI,MAAM;AAC9C,WAAO,GAAG,KAAK,GAAG,SAAS,UAAU,MAAM;AAAA,EAC7C;AACA,SAAO,IAAI,aAAa,IAAI,UAAU,OAAO,MAAM,CAAC;AACpD,SAAO,UAAU,MAAM;AACzB;AAUA,eAAsB,oBACpB,KACA,UAAiC,CAAC,GACN;AAC5B,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,aAAa,WAAW,GAAG;AACjC,QAAM,QAAmB,CAAC;AAC1B,MAAI,eAAmC,CAAC;AACxC,MAAI,YAAY;AAEhB,SAAO,YAAY,UAAU;AAG3B,UAAM,UAAU,cAAc,IAAI,MAAM,WAAW,KAAK,aAAa,MAAM,MAAM;AACjF,UAAM,OAAO,MAAM,QAA4B,SAAS,QAAW,EAAE,UAAU,CAAC,EAAE,CAAC;AACnF,UAAM,UAAU,KAAK,UAAU,CAAC;AAChC,QAAI,CAAC,KAAK,IAAI;AACZ,aAAO,EAAE,IAAI,OAAO,OAAO,WAAW,cAAc,cAAc,QAAQ;AAAA,IAC5E;AACA,iBAAa;AACb,QAAI,cAAc,EAAG,gBAAe;AACpC,UAAM,YAAY,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAClE,UAAM,KAAK,GAAG,SAAS;AAGvB,QAAI,QAAQ,YAAY,QAAQ,UAAU,WAAW,EAAG;AAAA,EAC1D;AAEA,SAAO,EAAE,IAAI,MAAM,OAAO,WAAW,cAAc,cAAc,KAAK;AACxE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -34,6 +34,9 @@ async function fetchFeatureGrants(requestFeatures) {
|
|
|
34
34
|
}
|
|
35
35
|
return granted;
|
|
36
36
|
}
|
|
37
|
+
function legacySettingsSectionSlug(groupLabel) {
|
|
38
|
+
return groupLabel.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
39
|
+
}
|
|
37
40
|
function buildSettingsSections(entries, sectionOrder) {
|
|
38
41
|
const settingsItems = entries.filter((e) => e.pageContext === "settings");
|
|
39
42
|
const sectionMap = /* @__PURE__ */ new Map();
|
|
@@ -51,8 +54,8 @@ function buildSettingsSections(entries, sectionOrder) {
|
|
|
51
54
|
};
|
|
52
55
|
};
|
|
53
56
|
for (const item of settingsItems) {
|
|
54
|
-
const sectionId = item.
|
|
55
|
-
const order = sectionOrder[sectionId] ?? 999;
|
|
57
|
+
const sectionId = item.groupId;
|
|
58
|
+
const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999;
|
|
56
59
|
if (!sectionMap.has(sectionId)) {
|
|
57
60
|
sectionMap.set(sectionId, {
|
|
58
61
|
id: sectionId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/nav.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ReactNode } from 'react'\nimport React from 'react'\nimport type { Module, ModuleRoute, PageMetadata } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures as checkFeatures } from '@open-mercato/shared/security/features'\n\n/** Route with optional page-metadata aliases that may be merged during generation. */\ntype NavRoute = ModuleRoute & Partial<Pick<PageMetadata, 'pageTitleKey' | 'pageGroupKey'>>\n\nexport type AdminNavItem = {\n group: string\n groupId: string\n groupKey?: string\n groupDefaultName: string\n title: string\n defaultTitle: string\n titleKey?: string\n href: string\n enabled: boolean\n hidden?: boolean\n order?: number\n priority?: number\n icon?: ReactNode\n children?: AdminNavItem[]\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n}\n\nexport type AdminNavFeatureChecker = (features: string[]) => Promise<Iterable<string> | null | undefined>\n\nexport type BuildAdminNavOptions = {\n checkFeatures?: AdminNavFeatureChecker\n}\n\n/**\n * @deprecated The internal fetch-based feature check will be removed.\n * Provide `options.checkFeatures` so buildAdminNav can reuse your RBAC context.\n */\nasync function fetchFeatureGrants(requestFeatures: string[]): Promise<Set<string>> { // NOSONAR \u2014 mutable accumulator pattern; Set is populated between early return and final return\n const granted = new Set<string>()\n if (!requestFeatures.length) return granted\n let url = '/api/auth/feature-check'\n let headersInit: Record<string, string> | undefined\n if (typeof window === 'undefined') {\n // On the server, build absolute URL and forward cookies so auth is available\n try {\n const { headers: getHeaders } = await import('next/headers')\n const h = await getHeaders()\n const host = h.get('x-forwarded-host') || h.get('host') || ''\n const proto = h.get('x-forwarded-proto') || 'http'\n const cookie = h.get('cookie') || ''\n if (host) url = `${proto}://${host}/api/auth/feature-check`\n headersInit = { cookie }\n } catch {\n // ignore; fall back to relative URL without forwarded cookies\n }\n }\n try {\n const res = await fetch(url, {\n method: 'POST',\n credentials: 'include' as any,\n headers: { 'content-type': 'application/json', ...(headersInit || {}) },\n body: JSON.stringify({ features: requestFeatures }),\n } as any)\n if (res.ok) {\n const data = await res.json().catch(() => ({ granted: [] }))\n if (Array.isArray(data?.granted)) {\n data.granted.forEach((f: string) => granted.add(f))\n }\n }\n } catch {\n // ignore fetch failures and keep feature set empty\n }\n return granted\n}\n\n/**\n * @deprecated Use number directly in sectionOrder config instead\n */\nexport type SettingsSectionConfig = {\n label: string\n labelKey?: string\n order: number\n}\n\nexport type SettingsSection = {\n id: string\n label: string\n labelKey?: string\n order: number\n items: SettingsSectionItem[]\n}\n\nexport type SettingsSectionItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order: number\n children?: SettingsSectionItem[]\n}\n\nexport function buildSettingsSections(\n entries: AdminNavItem[],\n sectionOrder: Record<string, number>\n): SettingsSection[] {\n const settingsItems = entries.filter(e => e.pageContext === 'settings')\n\n const sectionMap = new Map<string, SettingsSection>()\n\n const mapSectionItem = (item: AdminNavItem): SettingsSectionItem => {\n const itemId = item.href.replace(/\\//g, '-').slice(1)\n return {\n id: itemId,\n label: item.title,\n labelKey: item.titleKey,\n href: item.href,\n icon: item.icon,\n requireFeatures: undefined,\n order: item.order ?? item.priority ?? 100,\n children: item.children?.map(mapSectionItem),\n }\n }\n\n for (const item of settingsItems) {\n const sectionId = item.group.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n const order = sectionOrder[sectionId] ?? 999\n\n if (!sectionMap.has(sectionId)) {\n sectionMap.set(sectionId, {\n id: sectionId,\n label: item.group,\n labelKey: item.groupKey,\n order,\n items: []\n })\n }\n\n const section = sectionMap.get(sectionId)!\n section.items.push(mapSectionItem(item))\n }\n\n const sections = Array.from(sectionMap.values())\n const sortItems = (items: SettingsSectionItem[]) => {\n items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n for (const item of items) {\n if (item.children?.length) sortItems(item.children)\n }\n }\n sections.sort((a, b) => a.order - b.order)\n for (const section of sections) {\n sortItems(section.items)\n }\n\n return sections\n}\n\nexport function computeSettingsPathPrefixes(sections: SettingsSection[]): string[] {\n const prefixes = new Set<string>()\n const visitItem = (item: SettingsSectionItem) => {\n const parts = item.href.split('/')\n const lastSegment = parts[parts.length - 1]\n if (parts.length > 3 && lastSegment !== 'settings') {\n prefixes.add(parts.slice(0, -1).join('/'))\n }\n prefixes.add(item.href)\n if (item.children?.length) {\n for (const child of item.children) visitItem(child)\n }\n }\n for (const section of sections) {\n for (const item of section.items) {\n visitItem(item)\n }\n }\n return Array.from(prefixes)\n}\n\nexport function convertToSectionNavGroups(\n sections: SettingsSection[],\n translate?: (key: string | undefined, fallback: string) => string\n): Array<{\n id: string\n label: string\n labelKey?: string\n order?: number\n items: ConvertedSectionNavItem[]\n}> {\n const t = translate || ((key, fallback) => fallback)\n const mapSectionItem = (item: SettingsSectionItem): ConvertedSectionNavItem => ({\n id: item.id,\n label: t(item.labelKey, item.label),\n labelKey: item.labelKey,\n href: item.href,\n icon: item.icon,\n order: item.order,\n children: item.children?.map(mapSectionItem),\n })\n\n return sections.map(section => ({\n id: section.id,\n label: t(section.labelKey, section.label),\n labelKey: section.labelKey,\n order: section.order,\n items: section.items.map(mapSectionItem),\n }))\n}\n\ntype ConvertedSectionNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order?: number\n children?: ConvertedSectionNavItem[]\n}\n\ntype BuildAdminNavModule = Pick<Module, 'id'> & { backendRoutes?: (ModuleRoute | Omit<ModuleRoute, 'Component'>)[] }\n\nexport async function buildAdminNav(\n modules: BuildAdminNavModule[],\n ctx: { auth?: { roles?: string[]; sub?: string; orgId?: string | null; tenantId?: string | null }; path?: string },\n userEntities?: Array<{ entityId: string; label: string; href: string }>,\n translate?: (key: string | undefined, fallback: string) => string,\n options?: BuildAdminNavOptions\n): Promise<AdminNavItem[]> {\n function capitalize(s: string) {\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n function deriveTitleFromPath(p: string) {\n const seg = p.split('/').filter(Boolean).pop() || ''\n return seg ? seg.split('-').map(capitalize).join(' ') : 'Home'\n }\n const entries: AdminNavItem[] = []\n\n // Collect all unique features needed across all routes first\n const allRequiredFeatures = new Set<string>()\n for (const m of modules) {\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const features = r.requireFeatures\n if (features && features.length) {\n features.forEach(f => allRequiredFeatures.add(f))\n }\n }\n }\n\n // Batch check all features in a single API call\n let userFeatures: string[] = []\n if (allRequiredFeatures.size > 0) {\n const requestFeatures = Array.from(allRequiredFeatures)\n if (options?.checkFeatures) {\n try {\n const resolved = await options.checkFeatures(requestFeatures)\n if (resolved) {\n userFeatures = Array.from(resolved).filter((feature): feature is string => typeof feature === 'string' && feature.length > 0)\n }\n } catch {\n // ignore and fall back to empty feature set\n }\n } else {\n userFeatures = Array.from(await fetchFeatureGrants(requestFeatures))\n }\n }\n\n // Helper: check if user has all required features (from cache)\n function hasAllFeatures(required: string[]): boolean {\n if (!required || required.length === 0) return true\n return checkFeatures(userFeatures, required)\n }\n\n // Icons are defined per-page in metadata; no heuristic derivation here.\n for (const m of modules) {\n const groupDefault = capitalize(m.id)\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const href = r.pattern ?? r.path ?? ''\n if (!href || href.includes('[')) continue\n if (r.navHidden) continue\n const title = r.title || deriveTitleFromPath(href)\n const titleKey = r.pageTitleKey ?? r.titleKey\n const group = r.group || groupDefault\n const groupKey = r.pageGroupKey ?? r.groupKey\n const groupId = groupKey ?? group\n const displayGroup = translate ? translate(groupKey, group) : group\n const displayTitle = translate ? translate(titleKey, title) : title\n const visible = r.visible ? await Promise.resolve(r.visible(ctx)) : true\n if (!visible) continue\n const enabled = r.enabled ? await Promise.resolve(r.enabled(ctx)) : true\n // If roles are required, check; otherwise include\n const required = r.requireRoles || []\n if (required.length) {\n const roles = ctx.auth?.roles || []\n const ok = required.some((role) => roles.includes(role))\n if (!ok) continue\n }\n // If features are required, check from cached batch result\n const features = r.requireFeatures\n if (features && features.length) {\n const ok = hasAllFeatures(features)\n if (!ok) continue\n }\n const order = r.order\n const priority = r.priority ?? order\n const icon = r.icon\n const pageContext = r.pageContext\n entries.push({\n group: displayGroup,\n groupId,\n groupKey,\n groupDefaultName: displayGroup,\n title: displayTitle,\n defaultTitle: displayTitle,\n titleKey,\n href,\n enabled,\n order,\n priority,\n icon,\n pageContext,\n })\n }\n }\n // Build hierarchy: treat routes whose href starts with a parent href + '/'\n // Sort by href length (shortest first) so potential parents are processed before children\n const sorted = [...entries].sort((a, b) => a.href.length - b.href.length)\n const byHref = new Map<string, AdminNavItem>()\n const roots: AdminNavItem[] = []\n for (const e of sorted) {\n // Walk up the href segments to find the longest matching parent in the same group\n let parent: AdminNavItem | undefined\n const segments = e.href.split('/')\n for (let i = segments.length - 1; i >= 2; i--) {\n const candidate = byHref.get(segments.slice(0, i).join('/'))\n if (candidate && candidate !== e && candidate.groupId === e.groupId) {\n parent = candidate\n break\n }\n }\n byHref.set(e.href, e)\n if (parent) {\n parent.children = parent.children || []\n parent.children.push(e)\n } else {\n roots.push(e)\n }\n }\n\n // Add dynamic user entities to the navigation\n if (userEntities && userEntities.length > 0) {\n const tableIcon = React.createElement(\n 'svg',\n { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },\n React.createElement('rect', { x: 3, y: 4, width: 18, height: 16, rx: 2 }),\n React.createElement('path', { d: 'M3 10h18M9 4v16M15 4v16' }),\n )\n const userEntitiesLegacyGroupKeys = new Set(['settings.sections.dataDesigner', 'entities.nav.group'])\n const userEntitiesItem = entries.find((entry) => entry.href === '/backend/entities/user')\n ?? entries.find((entry) =>\n entry.titleKey === 'entities.nav.userEntities' &&\n typeof entry.groupKey === 'string' &&\n userEntitiesLegacyGroupKeys.has(entry.groupKey),\n )\n if (userEntitiesItem) {\n const existingChildren = userEntitiesItem.children || []\n const dynamicUserEntities = userEntities.map((entity) => ({\n group: userEntitiesItem.group,\n groupId: userEntitiesItem.groupId,\n groupKey: userEntitiesItem.groupKey,\n groupDefaultName: userEntitiesItem.groupDefaultName,\n title: entity.label,\n defaultTitle: entity.label,\n href: entity.href,\n enabled: true,\n order: 1000, // High order to appear at the end\n priority: 1000,\n icon: tableIcon,\n }))\n // Merge and deduplicate by href to avoid duplicates coming from server or generator\n const merged = [...existingChildren, ...dynamicUserEntities]\n const byHref = new Map<string, AdminNavItem>()\n for (const it of merged) {\n if (!byHref.has(it.href)) byHref.set(it.href, it)\n }\n userEntitiesItem.children = Array.from(byHref.values())\n }\n }\n\n // Sorting: group, then priority/order, then title. Apply within children too.\n const sortItems = (arr: AdminNavItem[]) => {\n arr.sort((a, b) => {\n if (a.groupId !== b.groupId) return a.groupId.localeCompare(b.groupId)\n const ap = a.priority ?? a.order ?? 10_000\n const bp = b.priority ?? b.order ?? 10_000\n if (ap !== bp) return ap - bp\n return a.title.localeCompare(b.title)\n })\n for (const it of arr) if (it.children?.length) sortItems(it.children)\n }\n sortItems(roots)\n return roots\n}\n"],
|
|
5
|
-
"mappings": "AACA,OAAO,WAAW;AAElB,SAAS,kBAAkB,qBAAqB;AAiChD,eAAe,mBAAmB,iBAAiD;AACjF,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,CAAC,gBAAgB,OAAQ,QAAO;AACpC,MAAI,MAAM;AACV,MAAI;AACJ,MAAI,OAAO,WAAW,aAAa;AAEjC,QAAI;AACF,YAAM,EAAE,SAAS,WAAW,IAAI,MAAM,OAAO,cAAc;AAC3D,YAAM,IAAI,MAAM,WAAW;AAC3B,YAAM,OAAO,EAAE,IAAI,kBAAkB,KAAK,EAAE,IAAI,MAAM,KAAK;AAC3D,YAAM,QAAQ,EAAE,IAAI,mBAAmB,KAAK;AAC5C,YAAM,SAAS,EAAE,IAAI,QAAQ,KAAK;AAClC,UAAI,KAAM,OAAM,GAAG,KAAK,MAAM,IAAI;AAClC,oBAAc,EAAE,OAAO;AAAA,IACzB,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,oBAAoB,GAAI,eAAe,CAAC,EAAG;AAAA,MACtE,MAAM,KAAK,UAAU,EAAE,UAAU,gBAAgB,CAAC;AAAA,IACpD,CAAQ;AACR,QAAI,IAAI,IAAI;AACV,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE;AAC3D,UAAI,MAAM,QAAQ,MAAM,OAAO,GAAG;AAChC,aAAK,QAAQ,QAAQ,CAAC,MAAc,QAAQ,IAAI,CAAC,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["import type { ReactNode } from 'react'\nimport React from 'react'\nimport type { Module, ModuleRoute, PageMetadata } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures as checkFeatures } from '@open-mercato/shared/security/features'\n\n/** Route with optional page-metadata aliases that may be merged during generation. */\ntype NavRoute = ModuleRoute & Partial<Pick<PageMetadata, 'pageTitleKey' | 'pageGroupKey'>>\n\nexport type AdminNavItem = {\n group: string\n groupId: string\n groupKey?: string\n groupDefaultName: string\n title: string\n defaultTitle: string\n titleKey?: string\n href: string\n enabled: boolean\n hidden?: boolean\n order?: number\n priority?: number\n icon?: ReactNode\n children?: AdminNavItem[]\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n}\n\nexport type AdminNavFeatureChecker = (features: string[]) => Promise<Iterable<string> | null | undefined>\n\nexport type BuildAdminNavOptions = {\n checkFeatures?: AdminNavFeatureChecker\n}\n\n/**\n * @deprecated The internal fetch-based feature check will be removed.\n * Provide `options.checkFeatures` so buildAdminNav can reuse your RBAC context.\n */\nasync function fetchFeatureGrants(requestFeatures: string[]): Promise<Set<string>> { // NOSONAR \u2014 mutable accumulator pattern; Set is populated between early return and final return\n const granted = new Set<string>()\n if (!requestFeatures.length) return granted\n let url = '/api/auth/feature-check'\n let headersInit: Record<string, string> | undefined\n if (typeof window === 'undefined') {\n // On the server, build absolute URL and forward cookies so auth is available\n try {\n const { headers: getHeaders } = await import('next/headers')\n const h = await getHeaders()\n const host = h.get('x-forwarded-host') || h.get('host') || ''\n const proto = h.get('x-forwarded-proto') || 'http'\n const cookie = h.get('cookie') || ''\n if (host) url = `${proto}://${host}/api/auth/feature-check`\n headersInit = { cookie }\n } catch {\n // ignore; fall back to relative URL without forwarded cookies\n }\n }\n try {\n const res = await fetch(url, {\n method: 'POST',\n credentials: 'include' as any,\n headers: { 'content-type': 'application/json', ...(headersInit || {}) },\n body: JSON.stringify({ features: requestFeatures }),\n } as any)\n if (res.ok) {\n const data = await res.json().catch(() => ({ granted: [] }))\n if (Array.isArray(data?.granted)) {\n data.granted.forEach((f: string) => granted.add(f))\n }\n }\n } catch {\n // ignore fetch failures and keep feature set empty\n }\n return granted\n}\n\n/**\n * @deprecated Use number directly in sectionOrder config instead\n */\nexport type SettingsSectionConfig = {\n label: string\n labelKey?: string\n order: number\n}\n\nexport type SettingsSection = {\n id: string\n label: string\n labelKey?: string\n order: number\n items: SettingsSectionItem[]\n}\n\nexport type SettingsSectionItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order: number\n children?: SettingsSectionItem[]\n}\n\n/**\n * Slug of a rendered group label, used as the pre-#4843 settings section id.\n *\n * `sectionOrder` used to be keyed by these slugs, which made ordering depend on the active locale.\n * Kept only as a lookup fallback so third-party callers passing a legacy map keep their weights.\n *\n * @deprecated Key `sectionOrder` by the untranslated group id (`AdminNavItem.groupId`) instead.\n */\nfunction legacySettingsSectionSlug(groupLabel: string): string {\n return groupLabel.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n}\n\n/**\n * Groups the settings-context nav entries into ordered sections.\n *\n * `sectionOrder` is keyed by the untranslated group id (`AdminNavItem.groupId`, i.e. the page's\n * `pageGroupKey` when it declares one) \u2014 the same convention the main sidebar's `defaultGroupOrder`\n * follows. Keying off the rendered label instead made every non-English deployment miss its weights\n * and fall back to the catch-all bucket (#4843).\n */\nexport function buildSettingsSections(\n entries: AdminNavItem[],\n sectionOrder: Record<string, number>\n): SettingsSection[] {\n const settingsItems = entries.filter(e => e.pageContext === 'settings')\n\n const sectionMap = new Map<string, SettingsSection>()\n\n const mapSectionItem = (item: AdminNavItem): SettingsSectionItem => {\n const itemId = item.href.replace(/\\//g, '-').slice(1)\n return {\n id: itemId,\n label: item.title,\n labelKey: item.titleKey,\n href: item.href,\n icon: item.icon,\n requireFeatures: undefined,\n order: item.order ?? item.priority ?? 100,\n children: item.children?.map(mapSectionItem),\n }\n }\n\n for (const item of settingsItems) {\n const sectionId = item.groupId\n const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999\n\n if (!sectionMap.has(sectionId)) {\n sectionMap.set(sectionId, {\n id: sectionId,\n label: item.group,\n labelKey: item.groupKey,\n order,\n items: []\n })\n }\n\n const section = sectionMap.get(sectionId)!\n section.items.push(mapSectionItem(item))\n }\n\n const sections = Array.from(sectionMap.values())\n const sortItems = (items: SettingsSectionItem[]) => {\n items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n for (const item of items) {\n if (item.children?.length) sortItems(item.children)\n }\n }\n sections.sort((a, b) => a.order - b.order)\n for (const section of sections) {\n sortItems(section.items)\n }\n\n return sections\n}\n\nexport function computeSettingsPathPrefixes(sections: SettingsSection[]): string[] {\n const prefixes = new Set<string>()\n const visitItem = (item: SettingsSectionItem) => {\n const parts = item.href.split('/')\n const lastSegment = parts[parts.length - 1]\n if (parts.length > 3 && lastSegment !== 'settings') {\n prefixes.add(parts.slice(0, -1).join('/'))\n }\n prefixes.add(item.href)\n if (item.children?.length) {\n for (const child of item.children) visitItem(child)\n }\n }\n for (const section of sections) {\n for (const item of section.items) {\n visitItem(item)\n }\n }\n return Array.from(prefixes)\n}\n\nexport function convertToSectionNavGroups(\n sections: SettingsSection[],\n translate?: (key: string | undefined, fallback: string) => string\n): Array<{\n id: string\n label: string\n labelKey?: string\n order?: number\n items: ConvertedSectionNavItem[]\n}> {\n const t = translate || ((key, fallback) => fallback)\n const mapSectionItem = (item: SettingsSectionItem): ConvertedSectionNavItem => ({\n id: item.id,\n label: t(item.labelKey, item.label),\n labelKey: item.labelKey,\n href: item.href,\n icon: item.icon,\n order: item.order,\n children: item.children?.map(mapSectionItem),\n })\n\n return sections.map(section => ({\n id: section.id,\n label: t(section.labelKey, section.label),\n labelKey: section.labelKey,\n order: section.order,\n items: section.items.map(mapSectionItem),\n }))\n}\n\ntype ConvertedSectionNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order?: number\n children?: ConvertedSectionNavItem[]\n}\n\ntype BuildAdminNavModule = Pick<Module, 'id'> & { backendRoutes?: (ModuleRoute | Omit<ModuleRoute, 'Component'>)[] }\n\nexport async function buildAdminNav(\n modules: BuildAdminNavModule[],\n ctx: { auth?: { roles?: string[]; sub?: string; orgId?: string | null; tenantId?: string | null }; path?: string },\n userEntities?: Array<{ entityId: string; label: string; href: string }>,\n translate?: (key: string | undefined, fallback: string) => string,\n options?: BuildAdminNavOptions\n): Promise<AdminNavItem[]> {\n function capitalize(s: string) {\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n function deriveTitleFromPath(p: string) {\n const seg = p.split('/').filter(Boolean).pop() || ''\n return seg ? seg.split('-').map(capitalize).join(' ') : 'Home'\n }\n const entries: AdminNavItem[] = []\n\n // Collect all unique features needed across all routes first\n const allRequiredFeatures = new Set<string>()\n for (const m of modules) {\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const features = r.requireFeatures\n if (features && features.length) {\n features.forEach(f => allRequiredFeatures.add(f))\n }\n }\n }\n\n // Batch check all features in a single API call\n let userFeatures: string[] = []\n if (allRequiredFeatures.size > 0) {\n const requestFeatures = Array.from(allRequiredFeatures)\n if (options?.checkFeatures) {\n try {\n const resolved = await options.checkFeatures(requestFeatures)\n if (resolved) {\n userFeatures = Array.from(resolved).filter((feature): feature is string => typeof feature === 'string' && feature.length > 0)\n }\n } catch {\n // ignore and fall back to empty feature set\n }\n } else {\n userFeatures = Array.from(await fetchFeatureGrants(requestFeatures))\n }\n }\n\n // Helper: check if user has all required features (from cache)\n function hasAllFeatures(required: string[]): boolean {\n if (!required || required.length === 0) return true\n return checkFeatures(userFeatures, required)\n }\n\n // Icons are defined per-page in metadata; no heuristic derivation here.\n for (const m of modules) {\n const groupDefault = capitalize(m.id)\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const href = r.pattern ?? r.path ?? ''\n if (!href || href.includes('[')) continue\n if (r.navHidden) continue\n const title = r.title || deriveTitleFromPath(href)\n const titleKey = r.pageTitleKey ?? r.titleKey\n const group = r.group || groupDefault\n const groupKey = r.pageGroupKey ?? r.groupKey\n const groupId = groupKey ?? group\n const displayGroup = translate ? translate(groupKey, group) : group\n const displayTitle = translate ? translate(titleKey, title) : title\n const visible = r.visible ? await Promise.resolve(r.visible(ctx)) : true\n if (!visible) continue\n const enabled = r.enabled ? await Promise.resolve(r.enabled(ctx)) : true\n // If roles are required, check; otherwise include\n const required = r.requireRoles || []\n if (required.length) {\n const roles = ctx.auth?.roles || []\n const ok = required.some((role) => roles.includes(role))\n if (!ok) continue\n }\n // If features are required, check from cached batch result\n const features = r.requireFeatures\n if (features && features.length) {\n const ok = hasAllFeatures(features)\n if (!ok) continue\n }\n const order = r.order\n const priority = r.priority ?? order\n const icon = r.icon\n const pageContext = r.pageContext\n entries.push({\n group: displayGroup,\n groupId,\n groupKey,\n groupDefaultName: displayGroup,\n title: displayTitle,\n defaultTitle: displayTitle,\n titleKey,\n href,\n enabled,\n order,\n priority,\n icon,\n pageContext,\n })\n }\n }\n // Build hierarchy: treat routes whose href starts with a parent href + '/'\n // Sort by href length (shortest first) so potential parents are processed before children\n const sorted = [...entries].sort((a, b) => a.href.length - b.href.length)\n const byHref = new Map<string, AdminNavItem>()\n const roots: AdminNavItem[] = []\n for (const e of sorted) {\n // Walk up the href segments to find the longest matching parent in the same group\n let parent: AdminNavItem | undefined\n const segments = e.href.split('/')\n for (let i = segments.length - 1; i >= 2; i--) {\n const candidate = byHref.get(segments.slice(0, i).join('/'))\n if (candidate && candidate !== e && candidate.groupId === e.groupId) {\n parent = candidate\n break\n }\n }\n byHref.set(e.href, e)\n if (parent) {\n parent.children = parent.children || []\n parent.children.push(e)\n } else {\n roots.push(e)\n }\n }\n\n // Add dynamic user entities to the navigation\n if (userEntities && userEntities.length > 0) {\n const tableIcon = React.createElement(\n 'svg',\n { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },\n React.createElement('rect', { x: 3, y: 4, width: 18, height: 16, rx: 2 }),\n React.createElement('path', { d: 'M3 10h18M9 4v16M15 4v16' }),\n )\n const userEntitiesLegacyGroupKeys = new Set(['settings.sections.dataDesigner', 'entities.nav.group'])\n const userEntitiesItem = entries.find((entry) => entry.href === '/backend/entities/user')\n ?? entries.find((entry) =>\n entry.titleKey === 'entities.nav.userEntities' &&\n typeof entry.groupKey === 'string' &&\n userEntitiesLegacyGroupKeys.has(entry.groupKey),\n )\n if (userEntitiesItem) {\n const existingChildren = userEntitiesItem.children || []\n const dynamicUserEntities = userEntities.map((entity) => ({\n group: userEntitiesItem.group,\n groupId: userEntitiesItem.groupId,\n groupKey: userEntitiesItem.groupKey,\n groupDefaultName: userEntitiesItem.groupDefaultName,\n title: entity.label,\n defaultTitle: entity.label,\n href: entity.href,\n enabled: true,\n order: 1000, // High order to appear at the end\n priority: 1000,\n icon: tableIcon,\n }))\n // Merge and deduplicate by href to avoid duplicates coming from server or generator\n const merged = [...existingChildren, ...dynamicUserEntities]\n const byHref = new Map<string, AdminNavItem>()\n for (const it of merged) {\n if (!byHref.has(it.href)) byHref.set(it.href, it)\n }\n userEntitiesItem.children = Array.from(byHref.values())\n }\n }\n\n // Sorting: group, then priority/order, then title. Apply within children too.\n const sortItems = (arr: AdminNavItem[]) => {\n arr.sort((a, b) => {\n if (a.groupId !== b.groupId) return a.groupId.localeCompare(b.groupId)\n const ap = a.priority ?? a.order ?? 10_000\n const bp = b.priority ?? b.order ?? 10_000\n if (ap !== bp) return ap - bp\n return a.title.localeCompare(b.title)\n })\n for (const it of arr) if (it.children?.length) sortItems(it.children)\n }\n sortItems(roots)\n return roots\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["byHref"]
|
|
7
7
|
}
|
|
@@ -156,7 +156,7 @@ function VersionHistoryPanel({
|
|
|
156
156
|
)
|
|
157
157
|
] }),
|
|
158
158
|
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: selectedEntry ? /* @__PURE__ */ jsx(VersionHistoryDetail, { entry: selectedEntry, t }) : /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
159
|
-
shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? /* @__PURE__ */ jsx(Alert, {
|
|
159
|
+
shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? /* @__PURE__ */ jsx(Alert, { status: "information", children: /* @__PURE__ */ jsx(AlertDescription, { children: t("audit_logs.hint.view_self_only", "Showing only your own changes. Contact an administrator for broader access.") }) }) : null,
|
|
160
160
|
isInitialLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
|
|
161
161
|
/* @__PURE__ */ jsx(Loader2, { className: "mb-2 h-8 w-8 animate-spin" }),
|
|
162
162
|
/* @__PURE__ */ jsx("p", { children: t("audit_logs.version_history.loading") })
|