@open-mercato/ui 0.4.2-canary-c02407ff85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.mjs +62 -0
- package/dist/backend/AppShell.js +902 -0
- package/dist/backend/AppShell.js.map +7 -0
- package/dist/backend/ConfirmDialog.js +17 -0
- package/dist/backend/ConfirmDialog.js.map +7 -0
- package/dist/backend/ContextHelp.js +31 -0
- package/dist/backend/ContextHelp.js.map +7 -0
- package/dist/backend/CrudForm.js +2028 -0
- package/dist/backend/CrudForm.js.map +7 -0
- package/dist/backend/DataTable.js +1363 -0
- package/dist/backend/DataTable.js.map +7 -0
- package/dist/backend/EmptyState.js +52 -0
- package/dist/backend/EmptyState.js.map +7 -0
- package/dist/backend/FilterBar.js +140 -0
- package/dist/backend/FilterBar.js.map +7 -0
- package/dist/backend/FilterOverlay.js +279 -0
- package/dist/backend/FilterOverlay.js.map +7 -0
- package/dist/backend/FlashMessages.js +66 -0
- package/dist/backend/FlashMessages.js.map +7 -0
- package/dist/backend/JsonBuilder.js +322 -0
- package/dist/backend/JsonBuilder.js.map +7 -0
- package/dist/backend/JsonDisplay.js +203 -0
- package/dist/backend/JsonDisplay.js.map +7 -0
- package/dist/backend/Page.js +27 -0
- package/dist/backend/Page.js.map +7 -0
- package/dist/backend/PerspectiveSidebar.js +282 -0
- package/dist/backend/PerspectiveSidebar.js.map +7 -0
- package/dist/backend/RowActions.js +148 -0
- package/dist/backend/RowActions.js.map +7 -0
- package/dist/backend/TruncatedCell.js +92 -0
- package/dist/backend/TruncatedCell.js.map +7 -0
- package/dist/backend/UserMenu.js +107 -0
- package/dist/backend/UserMenu.js.map +7 -0
- package/dist/backend/ValueIcons.js +34 -0
- package/dist/backend/ValueIcons.js.map +7 -0
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
- package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
- package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
- package/dist/backend/dashboard/DashboardScreen.js +578 -0
- package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
- package/dist/backend/dashboard/index.js +5 -0
- package/dist/backend/dashboard/index.js.map +7 -0
- package/dist/backend/dashboard/widgetRegistry.js +55 -0
- package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
- package/dist/backend/detail/ActivitiesSection.js +962 -0
- package/dist/backend/detail/ActivitiesSection.js.map +7 -0
- package/dist/backend/detail/AddressEditor.js +413 -0
- package/dist/backend/detail/AddressEditor.js.map +7 -0
- package/dist/backend/detail/AddressTiles.js +437 -0
- package/dist/backend/detail/AddressTiles.js.map +7 -0
- package/dist/backend/detail/AddressesSection.js +264 -0
- package/dist/backend/detail/AddressesSection.js.map +7 -0
- package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
- package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
- package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
- package/dist/backend/detail/AttachmentsSection.js +367 -0
- package/dist/backend/detail/AttachmentsSection.js.map +7 -0
- package/dist/backend/detail/CustomDataSection.js +433 -0
- package/dist/backend/detail/CustomDataSection.js.map +7 -0
- package/dist/backend/detail/DetailFieldsSection.js +75 -0
- package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
- package/dist/backend/detail/ErrorMessage.js +28 -0
- package/dist/backend/detail/ErrorMessage.js.map +7 -0
- package/dist/backend/detail/InlineEditors.js +681 -0
- package/dist/backend/detail/InlineEditors.js.map +7 -0
- package/dist/backend/detail/LoadingMessage.js +14 -0
- package/dist/backend/detail/LoadingMessage.js.map +7 -0
- package/dist/backend/detail/NotesSection.js +1032 -0
- package/dist/backend/detail/NotesSection.js.map +7 -0
- package/dist/backend/detail/TabEmptyState.js +25 -0
- package/dist/backend/detail/TabEmptyState.js.map +7 -0
- package/dist/backend/detail/TagsSection.js +254 -0
- package/dist/backend/detail/TagsSection.js.map +7 -0
- package/dist/backend/detail/addressFormat.js +77 -0
- package/dist/backend/detail/addressFormat.js.map +7 -0
- package/dist/backend/detail/index.js +34 -0
- package/dist/backend/detail/index.js.map +7 -0
- package/dist/backend/fields/registry.generated.js +8 -0
- package/dist/backend/fields/registry.generated.js.map +7 -0
- package/dist/backend/fields/registry.js +29 -0
- package/dist/backend/fields/registry.js.map +7 -0
- package/dist/backend/indexes/PartialIndexBanner.js +58 -0
- package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
- package/dist/backend/indexes/store.js +62 -0
- package/dist/backend/indexes/store.js.map +7 -0
- package/dist/backend/injection/InjectionSpot.js +179 -0
- package/dist/backend/injection/InjectionSpot.js.map +7 -0
- package/dist/backend/injection/PageInjectionBoundary.js +26 -0
- package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
- package/dist/backend/injection/helpers.js +26 -0
- package/dist/backend/injection/helpers.js.map +7 -0
- package/dist/backend/injection/widgetRegistry.js +55 -0
- package/dist/backend/injection/widgetRegistry.js.map +7 -0
- package/dist/backend/inputs/ComboboxInput.js +225 -0
- package/dist/backend/inputs/ComboboxInput.js.map +7 -0
- package/dist/backend/inputs/LookupSelect.js +191 -0
- package/dist/backend/inputs/LookupSelect.js.map +7 -0
- package/dist/backend/inputs/PhoneNumberField.js +100 -0
- package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
- package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
- package/dist/backend/inputs/TagsInput.js +222 -0
- package/dist/backend/inputs/TagsInput.js.map +7 -0
- package/dist/backend/inputs/index.js +6 -0
- package/dist/backend/inputs/index.js.map +7 -0
- package/dist/backend/operations/LastOperationBanner.js +80 -0
- package/dist/backend/operations/LastOperationBanner.js.map +7 -0
- package/dist/backend/operations/store.js +183 -0
- package/dist/backend/operations/store.js.map +7 -0
- package/dist/backend/schedule/ScheduleAgenda.js +107 -0
- package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
- package/dist/backend/schedule/ScheduleGrid.js +107 -0
- package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
- package/dist/backend/schedule/ScheduleToolbar.js +166 -0
- package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
- package/dist/backend/schedule/ScheduleView.js +165 -0
- package/dist/backend/schedule/ScheduleView.js.map +7 -0
- package/dist/backend/schedule/index.js +6 -0
- package/dist/backend/schedule/index.js.map +7 -0
- package/dist/backend/schedule/recurrence.js +83 -0
- package/dist/backend/schedule/recurrence.js.map +7 -0
- package/dist/backend/schedule/types.js +1 -0
- package/dist/backend/schedule/types.js.map +7 -0
- package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
- package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
- package/dist/backend/utils/api.js +127 -0
- package/dist/backend/utils/api.js.map +7 -0
- package/dist/backend/utils/apiCall.js +48 -0
- package/dist/backend/utils/apiCall.js.map +7 -0
- package/dist/backend/utils/crud.js +126 -0
- package/dist/backend/utils/crud.js.map +7 -0
- package/dist/backend/utils/customFieldColumns.js +56 -0
- package/dist/backend/utils/customFieldColumns.js.map +7 -0
- package/dist/backend/utils/customFieldDefs.js +143 -0
- package/dist/backend/utils/customFieldDefs.js.map +7 -0
- package/dist/backend/utils/customFieldFilters.js +126 -0
- package/dist/backend/utils/customFieldFilters.js.map +7 -0
- package/dist/backend/utils/customFieldForms.js +162 -0
- package/dist/backend/utils/customFieldForms.js.map +7 -0
- package/dist/backend/utils/customFieldValues.js +26 -0
- package/dist/backend/utils/customFieldValues.js.map +7 -0
- package/dist/backend/utils/flash.js +16 -0
- package/dist/backend/utils/flash.js.map +7 -0
- package/dist/backend/utils/nav.js +185 -0
- package/dist/backend/utils/nav.js.map +7 -0
- package/dist/backend/utils/serverErrors.js +230 -0
- package/dist/backend/utils/serverErrors.js.map +7 -0
- package/dist/frontend/AuthFooter.js +23 -0
- package/dist/frontend/AuthFooter.js.map +7 -0
- package/dist/frontend/LanguageSwitcher.js +57 -0
- package/dist/frontend/LanguageSwitcher.js.map +7 -0
- package/dist/frontend/Layout.js +14 -0
- package/dist/frontend/Layout.js.map +7 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +7 -0
- package/dist/primitives/DataLoader.js +67 -0
- package/dist/primitives/DataLoader.js.map +7 -0
- package/dist/primitives/ErrorNotice.js +20 -0
- package/dist/primitives/ErrorNotice.js.map +7 -0
- package/dist/primitives/alert.js +38 -0
- package/dist/primitives/alert.js.map +7 -0
- package/dist/primitives/badge.js +28 -0
- package/dist/primitives/badge.js.map +7 -0
- package/dist/primitives/button.js +44 -0
- package/dist/primitives/button.js.map +7 -0
- package/dist/primitives/card.js +91 -0
- package/dist/primitives/card.js.map +7 -0
- package/dist/primitives/checkbox.js +28 -0
- package/dist/primitives/checkbox.js.map +7 -0
- package/dist/primitives/dialog.js +90 -0
- package/dist/primitives/dialog.js.map +7 -0
- package/dist/primitives/input.js +22 -0
- package/dist/primitives/input.js.map +7 -0
- package/dist/primitives/label.js +21 -0
- package/dist/primitives/label.js.map +7 -0
- package/dist/primitives/separator.js +9 -0
- package/dist/primitives/separator.js.map +7 -0
- package/dist/primitives/spinner.js +24 -0
- package/dist/primitives/spinner.js.map +7 -0
- package/dist/primitives/switch.js +80 -0
- package/dist/primitives/switch.js.map +7 -0
- package/dist/primitives/table.js +29 -0
- package/dist/primitives/table.js.map +7 -0
- package/dist/primitives/tabs.js +87 -0
- package/dist/primitives/tabs.js.map +7 -0
- package/dist/primitives/textarea.js +21 -0
- package/dist/primitives/textarea.js.map +7 -0
- package/dist/primitives/tooltip.js +60 -0
- package/dist/primitives/tooltip.js.map +7 -0
- package/dist/theme/QueryProvider.js +44 -0
- package/dist/theme/QueryProvider.js.map +7 -0
- package/dist/theme/ThemeProvider.js +95 -0
- package/dist/theme/ThemeProvider.js.map +7 -0
- package/dist/theme/ThemeToggle.js +88 -0
- package/dist/theme/ThemeToggle.js.map +7 -0
- package/dist/theme/index.js +10 -0
- package/dist/theme/index.js.map +7 -0
- package/dist/types/react-big-calendar.d.js +1 -0
- package/dist/types/react-big-calendar.d.js.map +7 -0
- package/jest.config.cjs +23 -0
- package/jest.setup.ts +55 -0
- package/package.json +105 -0
- package/src/backend/AppShell.tsx +1096 -0
- package/src/backend/ConfirmDialog.tsx +19 -0
- package/src/backend/ContextHelp.tsx +38 -0
- package/src/backend/CrudForm.tsx +2503 -0
- package/src/backend/DataTable.tsx +1730 -0
- package/src/backend/EmptyState.tsx +65 -0
- package/src/backend/FilterBar.tsx +161 -0
- package/src/backend/FilterOverlay.tsx +328 -0
- package/src/backend/FlashMessages.tsx +82 -0
- package/src/backend/JsonBuilder.tsx +362 -0
- package/src/backend/JsonDisplay.tsx +254 -0
- package/src/backend/Page.tsx +30 -0
- package/src/backend/PerspectiveSidebar.tsx +337 -0
- package/src/backend/RowActions.tsx +151 -0
- package/src/backend/TruncatedCell.tsx +133 -0
- package/src/backend/UserMenu.tsx +118 -0
- package/src/backend/ValueIcons.tsx +48 -0
- package/src/backend/__tests__/AppShell.test.tsx +115 -0
- package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
- package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
- package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
- package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
- package/src/backend/__tests__/serverErrors.test.ts +83 -0
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
- package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
- package/src/backend/dashboard/DashboardScreen.tsx +684 -0
- package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
- package/src/backend/dashboard/index.ts +1 -0
- package/src/backend/dashboard/widgetRegistry.ts +68 -0
- package/src/backend/detail/ActivitiesSection.tsx +1284 -0
- package/src/backend/detail/AddressEditor.tsx +472 -0
- package/src/backend/detail/AddressTiles.tsx +587 -0
- package/src/backend/detail/AddressesSection.tsx +346 -0
- package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
- package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
- package/src/backend/detail/AttachmentsSection.tsx +414 -0
- package/src/backend/detail/CustomDataSection.tsx +530 -0
- package/src/backend/detail/DetailFieldsSection.tsx +147 -0
- package/src/backend/detail/ErrorMessage.tsx +32 -0
- package/src/backend/detail/InlineEditors.tsx +877 -0
- package/src/backend/detail/LoadingMessage.tsx +14 -0
- package/src/backend/detail/NotesSection.tsx +1275 -0
- package/src/backend/detail/TabEmptyState.tsx +48 -0
- package/src/backend/detail/TagsSection.tsx +314 -0
- package/src/backend/detail/addressFormat.tsx +121 -0
- package/src/backend/detail/index.ts +44 -0
- package/src/backend/fields/registry.generated.ts +8 -0
- package/src/backend/fields/registry.ts +38 -0
- package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
- package/src/backend/indexes/store.ts +88 -0
- package/src/backend/injection/InjectionSpot.tsx +236 -0
- package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
- package/src/backend/injection/helpers.ts +35 -0
- package/src/backend/injection/widgetRegistry.ts +68 -0
- package/src/backend/inputs/ComboboxInput.tsx +269 -0
- package/src/backend/inputs/LookupSelect.tsx +247 -0
- package/src/backend/inputs/PhoneNumberField.tsx +129 -0
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
- package/src/backend/inputs/TagsInput.tsx +259 -0
- package/src/backend/inputs/index.ts +5 -0
- package/src/backend/operations/LastOperationBanner.tsx +85 -0
- package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
- package/src/backend/operations/store.ts +230 -0
- package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
- package/src/backend/schedule/ScheduleGrid.tsx +136 -0
- package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
- package/src/backend/schedule/ScheduleView.tsx +198 -0
- package/src/backend/schedule/index.ts +5 -0
- package/src/backend/schedule/recurrence.ts +99 -0
- package/src/backend/schedule/types.ts +26 -0
- package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
- package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
- package/src/backend/utils/__tests__/crud.test.ts +87 -0
- package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
- package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
- package/src/backend/utils/api.ts +149 -0
- package/src/backend/utils/apiCall.ts +96 -0
- package/src/backend/utils/crud.ts +174 -0
- package/src/backend/utils/customFieldColumns.ts +71 -0
- package/src/backend/utils/customFieldDefs.ts +245 -0
- package/src/backend/utils/customFieldFilters.ts +145 -0
- package/src/backend/utils/customFieldForms.ts +196 -0
- package/src/backend/utils/customFieldValues.ts +41 -0
- package/src/backend/utils/flash.ts +17 -0
- package/src/backend/utils/nav.ts +238 -0
- package/src/backend/utils/serverErrors.ts +302 -0
- package/src/frontend/AuthFooter.tsx +29 -0
- package/src/frontend/LanguageSwitcher.tsx +66 -0
- package/src/frontend/Layout.tsx +13 -0
- package/src/index.ts +32 -0
- package/src/primitives/DataLoader.tsx +92 -0
- package/src/primitives/ErrorNotice.tsx +26 -0
- package/src/primitives/alert.tsx +52 -0
- package/src/primitives/badge.tsx +31 -0
- package/src/primitives/button.tsx +47 -0
- package/src/primitives/card.tsx +92 -0
- package/src/primitives/checkbox.tsx +28 -0
- package/src/primitives/dialog.tsx +110 -0
- package/src/primitives/input.tsx +20 -0
- package/src/primitives/label.tsx +18 -0
- package/src/primitives/separator.tsx +7 -0
- package/src/primitives/spinner.tsx +27 -0
- package/src/primitives/switch.tsx +86 -0
- package/src/primitives/table.tsx +27 -0
- package/src/primitives/tabs.tsx +128 -0
- package/src/primitives/textarea.tsx +20 -0
- package/src/primitives/tooltip.tsx +85 -0
- package/src/theme/QueryProvider.tsx +46 -0
- package/src/theme/ThemeProvider.tsx +120 -0
- package/src/theme/ThemeToggle.tsx +88 -0
- package/src/theme/index.ts +3 -0
- package/src/types/react-big-calendar.d.ts +16 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { collectCustomFieldValues } from '../customFieldValues'
|
|
2
|
+
|
|
3
|
+
describe('collectCustomFieldValues', () => {
|
|
4
|
+
it('strips cf_ prefix by default', () => {
|
|
5
|
+
const input = { cf_name: 'Alice', cf_age: 30, email: 'alice@example.com' }
|
|
6
|
+
expect(collectCustomFieldValues(input)).toEqual({ name: 'Alice', age: 30 })
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('handles cf: prefix and keeps both by default', () => {
|
|
10
|
+
const input = { 'cf:city': 'Berlin', cf_country: 'DE' }
|
|
11
|
+
expect(collectCustomFieldValues(input)).toEqual({ city: 'Berlin', country: 'DE' })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('applies transform and accept hooks', () => {
|
|
15
|
+
const input = { cf_name: 'Alice', cf_notes: '', cf_skip: 'value' }
|
|
16
|
+
const result = collectCustomFieldValues(input, {
|
|
17
|
+
transform: (value) => (typeof value === 'string' ? value.trim() : value),
|
|
18
|
+
accept: (fieldId) => fieldId !== 'skip',
|
|
19
|
+
})
|
|
20
|
+
expect(result).toEqual({ name: 'Alice', notes: '' })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('retains prefix when stripPrefix is false', () => {
|
|
24
|
+
const input = { cf_name: 'Alice' }
|
|
25
|
+
expect(collectCustomFieldValues(input, { stripPrefix: false })).toEqual({ cf_name: 'Alice' })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('omits undefined results when requested', () => {
|
|
29
|
+
const input = { cf_name: 'Alice', cf_age: 30, cf_extra: null }
|
|
30
|
+
const result = collectCustomFieldValues(input, {
|
|
31
|
+
transform: (value, key) => (key === 'extra' ? undefined : value),
|
|
32
|
+
})
|
|
33
|
+
expect(result).toEqual({ name: 'Alice', age: 30 })
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)
|
|
3
|
+
// Used across UI data utilities to avoid duplication.
|
|
4
|
+
import { flash } from '../FlashMessages'
|
|
5
|
+
import { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
6
|
+
import { pushOperation } from '../operations/store'
|
|
7
|
+
import { pushPartialIndexWarning } from '../indexes/store'
|
|
8
|
+
export class UnauthorizedError extends Error {
|
|
9
|
+
readonly status = 401
|
|
10
|
+
constructor(message = 'Unauthorized') {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'UnauthorizedError'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function redirectToSessionRefresh() {
|
|
17
|
+
if (typeof window === 'undefined') return
|
|
18
|
+
const current = window.location.pathname + window.location.search
|
|
19
|
+
// Avoid redirect loops if already on an auth/session route
|
|
20
|
+
if (window.location.pathname.startsWith('/api/auth')) return
|
|
21
|
+
try {
|
|
22
|
+
flash('Session expired. Redirecting to sign in…', 'warning')
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`
|
|
25
|
+
}, 20)
|
|
26
|
+
} catch {
|
|
27
|
+
// no-op
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ForbiddenError extends Error {
|
|
32
|
+
readonly status = 403
|
|
33
|
+
constructor(message = 'Forbidden') {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = 'ForbiddenError'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']
|
|
40
|
+
|
|
41
|
+
export function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {
|
|
42
|
+
if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {
|
|
43
|
+
DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function redirectToForbiddenLogin(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {
|
|
48
|
+
if (typeof window === 'undefined') return
|
|
49
|
+
// We don't know required roles from the API response; use a generic hint.
|
|
50
|
+
if (window.location.pathname.startsWith('/login')) return
|
|
51
|
+
try {
|
|
52
|
+
const current = window.location.pathname + window.location.search
|
|
53
|
+
const features = options?.requiredFeatures?.filter(Boolean) ?? []
|
|
54
|
+
const roles = options?.requiredRoles?.filter(Boolean) ?? []
|
|
55
|
+
const query = features.length
|
|
56
|
+
? `requireFeature=${encodeURIComponent(features.join(','))}`
|
|
57
|
+
: `requireRole=${encodeURIComponent((roles.length ? roles : DEFAULT_FORBIDDEN_ROLES).map(String).join(','))}`
|
|
58
|
+
const url = `/login?${query}&redirect=${encodeURIComponent(current)}`
|
|
59
|
+
flash('Insufficient permissions. Redirecting to login…', 'warning')
|
|
60
|
+
setTimeout(() => { window.location.href = url }, 60)
|
|
61
|
+
} catch {
|
|
62
|
+
// no-op
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
67
|
+
type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
68
|
+
const baseFetch: FetchType = (typeof window !== 'undefined' && (window as any).__omOriginalFetch)
|
|
69
|
+
? ((window as any).__omOriginalFetch as FetchType)
|
|
70
|
+
: fetch;
|
|
71
|
+
const res = await baseFetch(input, init);
|
|
72
|
+
const onLoginPage = typeof window !== 'undefined' && window.location.pathname.startsWith('/login')
|
|
73
|
+
if (res.status === 401) {
|
|
74
|
+
// Trigger same redirect flow as protected pages
|
|
75
|
+
if (!onLoginPage) {
|
|
76
|
+
redirectToSessionRefresh()
|
|
77
|
+
// Throw a typed error for callers that might still handle it
|
|
78
|
+
throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))
|
|
79
|
+
}
|
|
80
|
+
return res
|
|
81
|
+
}
|
|
82
|
+
if (res.status === 403) {
|
|
83
|
+
// Try to read requiredRoles from JSON body; ignore if not JSON
|
|
84
|
+
let roles: string[] | null = null
|
|
85
|
+
let features: string[] | null = null
|
|
86
|
+
let payload: unknown = null
|
|
87
|
+
try {
|
|
88
|
+
const clone = res.clone()
|
|
89
|
+
const data = await clone.json()
|
|
90
|
+
if (Array.isArray(data?.requiredRoles)) roles = data.requiredRoles.map((r: any) => String(r))
|
|
91
|
+
if (Array.isArray(data?.requiredFeatures)) features = data.requiredFeatures.map((f: any) => String(f))
|
|
92
|
+
if (data && typeof data === 'object') payload = data
|
|
93
|
+
} catch {}
|
|
94
|
+
// Only redirect if not already on login page
|
|
95
|
+
if (!onLoginPage) {
|
|
96
|
+
const target =
|
|
97
|
+
typeof input === 'string'
|
|
98
|
+
? input
|
|
99
|
+
: input instanceof URL
|
|
100
|
+
? input.toString()
|
|
101
|
+
: (typeof Request !== 'undefined' && input instanceof Request)
|
|
102
|
+
? input.url
|
|
103
|
+
: 'unknown'
|
|
104
|
+
try {
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.warn('[apiFetch] Forbidden response', {
|
|
107
|
+
url: target,
|
|
108
|
+
status: res.status,
|
|
109
|
+
requiredRoles: roles,
|
|
110
|
+
requiredFeatures: features,
|
|
111
|
+
details: payload,
|
|
112
|
+
})
|
|
113
|
+
} catch {}
|
|
114
|
+
redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features })
|
|
115
|
+
const msg = await res.text().catch(() => 'Forbidden')
|
|
116
|
+
throw new ForbiddenError(msg)
|
|
117
|
+
}
|
|
118
|
+
// If already on login, just return the response for the caller to handle
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const header = res.headers.get('x-om-operation')
|
|
122
|
+
const metadata = deserializeOperationMetadata(header)
|
|
123
|
+
if (metadata) pushOperation(metadata)
|
|
124
|
+
} catch {
|
|
125
|
+
// ignore malformed headers
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const warningRaw = res.headers.get('x-om-partial-index')
|
|
129
|
+
if (warningRaw) {
|
|
130
|
+
const parsed = JSON.parse(warningRaw) as Record<string, unknown>
|
|
131
|
+
if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {
|
|
132
|
+
const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')
|
|
133
|
+
if (entity) {
|
|
134
|
+
const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null
|
|
135
|
+
const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null
|
|
136
|
+
const scope = parsed.scope === 'global' ? 'global' : 'scoped'
|
|
137
|
+
const entityLabel =
|
|
138
|
+
typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()
|
|
139
|
+
? parsed.entityLabel.trim()
|
|
140
|
+
: entity
|
|
141
|
+
pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore malformed headers
|
|
147
|
+
}
|
|
148
|
+
return res
|
|
149
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { apiFetch } from './api'
|
|
4
|
+
import { raiseCrudError, readJsonSafe } from './serverErrors'
|
|
5
|
+
|
|
6
|
+
export type ApiCallOptions<TReturn> = {
|
|
7
|
+
parse?: (res: Response) => Promise<TReturn | null>
|
|
8
|
+
fallback?: TReturn | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ApiCallResult<TReturn> = {
|
|
12
|
+
ok: boolean
|
|
13
|
+
status: number
|
|
14
|
+
result: TReturn | null
|
|
15
|
+
response: Response
|
|
16
|
+
cacheStatus: 'hit' | 'miss' | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function apiCall<TReturn = Record<string, unknown>>(
|
|
20
|
+
input: RequestInfo | URL,
|
|
21
|
+
init?: RequestInit,
|
|
22
|
+
options?: ApiCallOptions<TReturn>,
|
|
23
|
+
): Promise<ApiCallResult<TReturn>> {
|
|
24
|
+
const response = await apiFetch(input, init)
|
|
25
|
+
const parser = options?.parse
|
|
26
|
+
const fallback = options?.fallback ?? null
|
|
27
|
+
let result: TReturn | null = null
|
|
28
|
+
const rawCacheStatus =
|
|
29
|
+
response.headers?.get?.('x-om-cache') ??
|
|
30
|
+
response.headers?.get?.('x-cache-status') ??
|
|
31
|
+
null
|
|
32
|
+
const cacheStatus = rawCacheStatus === 'hit' || rawCacheStatus === 'miss' ? rawCacheStatus : null
|
|
33
|
+
try {
|
|
34
|
+
const source = typeof (response as Response & { clone?: () => Response }).clone === 'function'
|
|
35
|
+
? response.clone()
|
|
36
|
+
: response
|
|
37
|
+
if (parser) result = await parser(source)
|
|
38
|
+
else result = await readJsonSafe<TReturn>(source, fallback)
|
|
39
|
+
} catch {
|
|
40
|
+
result = fallback
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ok: response.ok,
|
|
44
|
+
status: response.status,
|
|
45
|
+
result,
|
|
46
|
+
response,
|
|
47
|
+
cacheStatus,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ApiCallOrThrowOptions<TReturn> = ApiCallOptions<TReturn> & {
|
|
52
|
+
errorMessage?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function apiCallOrThrow<TReturn = Record<string, unknown>>(
|
|
56
|
+
input: RequestInfo | URL,
|
|
57
|
+
init?: RequestInit,
|
|
58
|
+
options?: ApiCallOrThrowOptions<TReturn>,
|
|
59
|
+
): Promise<ApiCallResult<TReturn>> {
|
|
60
|
+
const { errorMessage, ...callOptions } = options ?? {}
|
|
61
|
+
const call = await apiCall<TReturn>(input, init, callOptions)
|
|
62
|
+
if (!call.ok) {
|
|
63
|
+
await raiseCrudError(call.response, errorMessage)
|
|
64
|
+
}
|
|
65
|
+
return call
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ReadApiResultOrThrowOptions<TReturn> = ApiCallOrThrowOptions<TReturn> & {
|
|
69
|
+
allowNullResult?: boolean
|
|
70
|
+
emptyResultMessage?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function readApiResultOrThrow<TReturn = Record<string, unknown>>(
|
|
74
|
+
input: RequestInfo | URL,
|
|
75
|
+
init?: RequestInit,
|
|
76
|
+
options?: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult?: false },
|
|
77
|
+
): Promise<TReturn>
|
|
78
|
+
export async function readApiResultOrThrow<TReturn = Record<string, unknown>>(
|
|
79
|
+
input: RequestInfo | URL,
|
|
80
|
+
init: RequestInit | undefined,
|
|
81
|
+
options: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult: true },
|
|
82
|
+
): Promise<TReturn | null>
|
|
83
|
+
export async function readApiResultOrThrow<TReturn = Record<string, unknown>>(
|
|
84
|
+
input: RequestInfo | URL,
|
|
85
|
+
init?: RequestInit,
|
|
86
|
+
options?: ReadApiResultOrThrowOptions<TReturn>,
|
|
87
|
+
): Promise<TReturn | null> {
|
|
88
|
+
const { allowNullResult = false, emptyResultMessage, ...callOptions } = options ?? {}
|
|
89
|
+
const call = await apiCallOrThrow<TReturn>(input, init, callOptions)
|
|
90
|
+
if (call.result == null && !allowNullResult) {
|
|
91
|
+
const fallback =
|
|
92
|
+
emptyResultMessage ?? callOptions.errorMessage ?? `Missing response payload (${call.status})`
|
|
93
|
+
throw new Error(fallback)
|
|
94
|
+
}
|
|
95
|
+
return call.result
|
|
96
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export type SortDir = 'asc' | 'desc'
|
|
2
|
+
|
|
3
|
+
export type ListResponse<T> = {
|
|
4
|
+
items: T[]
|
|
5
|
+
total: number
|
|
6
|
+
page: number
|
|
7
|
+
pageSize: number
|
|
8
|
+
totalPages: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type CrudExportFormat = 'csv' | 'json' | 'xml' | 'markdown'
|
|
12
|
+
|
|
13
|
+
function toQuery(params: Record<string, any>) {
|
|
14
|
+
const sp = new URLSearchParams()
|
|
15
|
+
for (const [k, v] of Object.entries(params)) {
|
|
16
|
+
if (v === undefined || v === null) continue
|
|
17
|
+
if (Array.isArray(v)) {
|
|
18
|
+
if (v.length === 0) continue
|
|
19
|
+
sp.set(k, v.join(','))
|
|
20
|
+
} else {
|
|
21
|
+
sp.set(k, String(v))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return sp.toString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildCrudQuery(params: Record<string, any>): string {
|
|
28
|
+
return toQuery(params)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
import { apiCall, readApiResultOrThrow, type ApiCallResult } from './apiCall'
|
|
32
|
+
import { raiseCrudError } from './serverErrors'
|
|
33
|
+
|
|
34
|
+
function mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {
|
|
35
|
+
if (!base) return extra
|
|
36
|
+
const hasHeadersCtor = typeof Headers !== 'undefined'
|
|
37
|
+
if (hasHeadersCtor && base instanceof Headers) {
|
|
38
|
+
const merged = new Headers(base)
|
|
39
|
+
Object.entries(extra).forEach(([key, value]) => merged.set(key, value))
|
|
40
|
+
return merged
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(base)) {
|
|
43
|
+
return [...base, ...Object.entries(extra)]
|
|
44
|
+
}
|
|
45
|
+
return { ...(base as Record<string, string>), ...extra }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type CrudRequestExtras<TReturn> = {
|
|
49
|
+
parseResult?: (res: Response) => Promise<TReturn | null>
|
|
50
|
+
fallbackResult?: TReturn | null
|
|
51
|
+
errorMessage?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type CrudRequestInit<TReturn> = Omit<RequestInit, 'body' | 'method'> & CrudRequestExtras<TReturn>
|
|
55
|
+
type CrudDeleteOptions<TReturn> = Omit<RequestInit, 'method' | 'body'> &
|
|
56
|
+
CrudRequestExtras<TReturn> & {
|
|
57
|
+
body?: unknown
|
|
58
|
+
id?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type CrudResponse<TReturn> = ApiCallResult<TReturn>
|
|
62
|
+
|
|
63
|
+
export async function fetchCrudList<T>(apiPath: string, params: Record<string, any>, init?: RequestInit): Promise<ListResponse<T>> {
|
|
64
|
+
const qs = buildCrudQuery(params)
|
|
65
|
+
return readApiResultOrThrow<ListResponse<T>>(`/api/${apiPath}?${qs}`, init, {
|
|
66
|
+
errorMessage: 'Failed to fetch list',
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildCrudExportUrl(apiPath: string, params: Record<string, any>, format: CrudExportFormat): string {
|
|
71
|
+
const qs = buildCrudQuery({ ...params, format })
|
|
72
|
+
return `/api/${apiPath}?${qs}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildCrudCsvUrl(apiPath: string, params: Record<string, any>): string {
|
|
76
|
+
return buildCrudExportUrl(apiPath, params, 'csv')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function createCrud<TReturn = Record<string, unknown>>(
|
|
80
|
+
apiPath: string,
|
|
81
|
+
body: any,
|
|
82
|
+
init?: CrudRequestInit<TReturn>,
|
|
83
|
+
): Promise<CrudResponse<TReturn>> {
|
|
84
|
+
const { parseResult, fallbackResult, errorMessage, headers, ...rest } = init ?? {}
|
|
85
|
+
const call = await apiCall<TReturn>(
|
|
86
|
+
`/api/${apiPath}`,
|
|
87
|
+
{
|
|
88
|
+
...rest,
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: mergeHeaders(headers, { 'content-type': 'application/json' }),
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
parse: parseResult,
|
|
95
|
+
fallback: fallbackResult ?? null,
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
if (!call.ok) await raiseCrudError(call.response, errorMessage ?? 'Failed to create')
|
|
99
|
+
return call
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function updateCrud<TReturn = Record<string, unknown>>(
|
|
103
|
+
apiPath: string,
|
|
104
|
+
body: any,
|
|
105
|
+
init?: CrudRequestInit<TReturn>,
|
|
106
|
+
): Promise<CrudResponse<TReturn>> {
|
|
107
|
+
const { parseResult, fallbackResult, errorMessage, headers, ...rest } = init ?? {}
|
|
108
|
+
const call = await apiCall<TReturn>(
|
|
109
|
+
`/api/${apiPath}`,
|
|
110
|
+
{
|
|
111
|
+
...rest,
|
|
112
|
+
method: 'PUT',
|
|
113
|
+
headers: mergeHeaders(headers, { 'content-type': 'application/json' }),
|
|
114
|
+
body: JSON.stringify(body),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
parse: parseResult,
|
|
118
|
+
fallback: fallbackResult ?? null,
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
if (!call.ok) await raiseCrudError(call.response, errorMessage ?? 'Failed to update')
|
|
122
|
+
return call
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function deleteCrud<TReturn = Record<string, unknown>>(
|
|
126
|
+
apiPath: string,
|
|
127
|
+
id: string,
|
|
128
|
+
init?: CrudRequestInit<TReturn>,
|
|
129
|
+
): Promise<CrudResponse<TReturn>>
|
|
130
|
+
export async function deleteCrud<TReturn = Record<string, unknown>>(
|
|
131
|
+
apiPath: string,
|
|
132
|
+
options: CrudDeleteOptions<TReturn>,
|
|
133
|
+
): Promise<CrudResponse<TReturn>>
|
|
134
|
+
export async function deleteCrud<TReturn = Record<string, unknown>>(
|
|
135
|
+
apiPath: string,
|
|
136
|
+
idOrOptions: string | CrudDeleteOptions<TReturn>,
|
|
137
|
+
maybeInit?: CrudRequestInit<TReturn>,
|
|
138
|
+
): Promise<CrudResponse<TReturn>> {
|
|
139
|
+
if (typeof idOrOptions === 'string') {
|
|
140
|
+
const { parseResult, fallbackResult, errorMessage, ...rest } = maybeInit ?? {}
|
|
141
|
+
const call = await apiCall<TReturn>(
|
|
142
|
+
`/api/${apiPath}?id=${encodeURIComponent(idOrOptions)}`,
|
|
143
|
+
{
|
|
144
|
+
...rest,
|
|
145
|
+
method: 'DELETE',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
parse: parseResult,
|
|
149
|
+
fallback: fallbackResult ?? null,
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
if (!call.ok) await raiseCrudError(call.response, errorMessage ?? 'Failed to delete')
|
|
153
|
+
return call
|
|
154
|
+
}
|
|
155
|
+
const { parseResult, fallbackResult, errorMessage, headers, body, id, ...rest } = idOrOptions
|
|
156
|
+
const payload = body ?? (id ? { id } : undefined)
|
|
157
|
+
const requestHeaders =
|
|
158
|
+
payload !== undefined ? mergeHeaders(headers, { 'content-type': 'application/json' }) : headers
|
|
159
|
+
const call = await apiCall<TReturn>(
|
|
160
|
+
`/api/${apiPath}`,
|
|
161
|
+
{
|
|
162
|
+
...rest,
|
|
163
|
+
method: 'DELETE',
|
|
164
|
+
headers: requestHeaders,
|
|
165
|
+
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
parse: parseResult,
|
|
169
|
+
fallback: fallbackResult ?? null,
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
if (!call.ok) await raiseCrudError(call.response, errorMessage ?? 'Failed to delete')
|
|
173
|
+
return call
|
|
174
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
2
|
+
import type { CustomFieldDefDto, CustomFieldVisibility } from './customFieldDefs'
|
|
3
|
+
import { isDefVisible } from './customFieldDefs'
|
|
4
|
+
|
|
5
|
+
// Filters and annotates columns with custom-field definitions:
|
|
6
|
+
// - Drops cf_* columns when no definition exists or listVisible === false
|
|
7
|
+
// - Uses definition label as header when header is missing
|
|
8
|
+
export function applyCustomFieldVisibility<T>(columns: ColumnDef<T, any>[], defs: CustomFieldDefDto[], mode: CustomFieldVisibility = 'list'): ColumnDef<T, any>[] {
|
|
9
|
+
const byKey = new Map(defs.map((d) => [d.key, d]))
|
|
10
|
+
// First, filter and annotate headers
|
|
11
|
+
const filtered = columns.filter((c) => {
|
|
12
|
+
const key = String((c as any).accessorKey || '')
|
|
13
|
+
if (!key.startsWith('cf_')) return true
|
|
14
|
+
const cfKey = key.slice(3)
|
|
15
|
+
const def = byKey.get(cfKey)
|
|
16
|
+
if (!def) return false
|
|
17
|
+
if (!isDefVisible(def, mode)) return false
|
|
18
|
+
const currentHeader = (c as any).header
|
|
19
|
+
const fallbackHeader = typeof currentHeader === 'string' && currentHeader.trim().length ? currentHeader : key
|
|
20
|
+
const label = def.label && def.label.trim().length ? def.label : fallbackHeader
|
|
21
|
+
if (currentHeader == null || typeof currentHeader === 'string') {
|
|
22
|
+
(c as any).header = label
|
|
23
|
+
}
|
|
24
|
+
const existingMeta = ((c as any).meta || {}) as Record<string, unknown>
|
|
25
|
+
const nextMeta = Object.assign({}, existingMeta, { label })
|
|
26
|
+
;(c as any).meta = nextMeta
|
|
27
|
+
return true
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Then, reorder only the cf_* columns by definition priority while preserving
|
|
31
|
+
// the positions of non-cf columns and the count of cf slots.
|
|
32
|
+
const cfEntries: Array<{ col: ColumnDef<T, any>; key: string; prio: number }> = []
|
|
33
|
+
filtered.forEach((c) => {
|
|
34
|
+
const key = String((c as any).accessorKey || '')
|
|
35
|
+
if (key.startsWith('cf_')) {
|
|
36
|
+
const cfKey = key.slice(3)
|
|
37
|
+
const def = byKey.get(cfKey)
|
|
38
|
+
cfEntries.push({ col: c, key: cfKey, prio: def?.priority ?? 0 })
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
cfEntries.sort((a, b) => a.prio - b.prio)
|
|
42
|
+
let cfIdx = 0
|
|
43
|
+
const result = filtered.map((c) => {
|
|
44
|
+
const key = String((c as any).accessorKey || '')
|
|
45
|
+
if (!key.startsWith('cf_')) return c
|
|
46
|
+
const next = cfEntries[cfIdx++]?.col ?? c
|
|
47
|
+
return next
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Append any missing cf columns (defs visible but not present in incoming columns)
|
|
51
|
+
const existingCfKeys = new Set<string>(result
|
|
52
|
+
.map((c) => String((c as any).accessorKey || ''))
|
|
53
|
+
.filter((k) => k.startsWith('cf_'))
|
|
54
|
+
.map((k) => k.slice(3)))
|
|
55
|
+
|
|
56
|
+
const visibleSorted = defs
|
|
57
|
+
.filter((d) => isDefVisible(d, mode))
|
|
58
|
+
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
|
|
59
|
+
|
|
60
|
+
const missing = visibleSorted.filter((d) => !existingCfKeys.has(d.key))
|
|
61
|
+
for (const d of missing) {
|
|
62
|
+
const col: ColumnDef<T, any> = {
|
|
63
|
+
accessorKey: `cf_${d.key}` as any,
|
|
64
|
+
header: d.label || `cf_${d.key}`,
|
|
65
|
+
// Respect responsive priority when provided; default leaves it visible
|
|
66
|
+
meta: { priority: (d as any).priority, label: d.label || `cf_${d.key}` } as any,
|
|
67
|
+
}
|
|
68
|
+
result.push(col)
|
|
69
|
+
}
|
|
70
|
+
return result
|
|
71
|
+
}
|