@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,245 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { useQuery, type UseQueryResult, type QueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { readApiResultOrThrow } from './apiCall'
|
|
4
|
+
import type { CustomFieldOptionDto } from '@open-mercato/shared/modules/entities/options'
|
|
5
|
+
|
|
6
|
+
export type CustomFieldDefDto = {
|
|
7
|
+
entityId?: string
|
|
8
|
+
key: string
|
|
9
|
+
kind: string
|
|
10
|
+
label?: string
|
|
11
|
+
description?: string
|
|
12
|
+
options?: CustomFieldOptionDto[]
|
|
13
|
+
optionsUrl?: string
|
|
14
|
+
multi?: boolean
|
|
15
|
+
filterable?: boolean
|
|
16
|
+
formEditable?: boolean
|
|
17
|
+
listVisible?: boolean
|
|
18
|
+
editor?: string
|
|
19
|
+
input?: string
|
|
20
|
+
priority?: number
|
|
21
|
+
fieldset?: string
|
|
22
|
+
group?: { code: string; title?: string; hint?: string }
|
|
23
|
+
// attachments-specific config
|
|
24
|
+
maxAttachmentSizeMb?: number
|
|
25
|
+
acceptExtensions?: string[]
|
|
26
|
+
// optional validation rules
|
|
27
|
+
validation?: Array<
|
|
28
|
+
| { rule: 'required'; message: string }
|
|
29
|
+
| { rule: 'date'; message: string }
|
|
30
|
+
| { rule: 'integer'; message: string }
|
|
31
|
+
| { rule: 'float'; message: string }
|
|
32
|
+
| { rule: 'lt' | 'lte' | 'gt' | 'gte'; param: number; message: string }
|
|
33
|
+
| { rule: 'eq' | 'ne'; param: any; message: string }
|
|
34
|
+
| { rule: 'regex'; param: string; message: string }
|
|
35
|
+
>
|
|
36
|
+
dictionaryId?: string
|
|
37
|
+
dictionaryInlineCreate?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CustomFieldsetGroupDto = {
|
|
41
|
+
code: string
|
|
42
|
+
title?: string
|
|
43
|
+
hint?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type CustomFieldsetDto = {
|
|
47
|
+
code: string
|
|
48
|
+
label: string
|
|
49
|
+
icon?: string
|
|
50
|
+
description?: string
|
|
51
|
+
groups?: CustomFieldsetGroupDto[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type CustomFieldDefinitionsPayload = {
|
|
55
|
+
items?: CustomFieldDefDto[]
|
|
56
|
+
fieldsetsByEntity?: Record<string, CustomFieldsetDto[]>
|
|
57
|
+
entitySettings?: Record<string, { singleFieldsetPerRecord?: boolean }>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeEntityIds(entityIds: string | string[] | null | undefined): string[] {
|
|
61
|
+
if (entityIds == null) return []
|
|
62
|
+
const list = Array.isArray(entityIds) ? entityIds : [entityIds]
|
|
63
|
+
const dedup = new Set<string>()
|
|
64
|
+
const normalized: string[] = []
|
|
65
|
+
for (const raw of list) {
|
|
66
|
+
const trimmed = String(raw ?? '').trim()
|
|
67
|
+
if (!trimmed || dedup.has(trimmed)) continue
|
|
68
|
+
dedup.add(trimmed)
|
|
69
|
+
normalized.push(trimmed)
|
|
70
|
+
}
|
|
71
|
+
return normalized
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type CustomFieldDefinitionQueryOptions = {
|
|
75
|
+
fieldset?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildDefinitionsQuery(entityIds: string[], options?: CustomFieldDefinitionQueryOptions): string {
|
|
79
|
+
const params = new URLSearchParams()
|
|
80
|
+
entityIds.forEach((id) => {
|
|
81
|
+
if (id) params.append('entityId', id)
|
|
82
|
+
})
|
|
83
|
+
if (options?.fieldset) params.set('fieldset', options.fieldset)
|
|
84
|
+
return params.toString()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type CustomFieldDefinitionsResponse = CustomFieldDefinitionsPayload
|
|
88
|
+
|
|
89
|
+
function normalizeRecord<T>(value: unknown): Record<string, T[]> {
|
|
90
|
+
if (!value || typeof value !== 'object') return {}
|
|
91
|
+
const out: Record<string, T[]> = {}
|
|
92
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
93
|
+
if (!Array.isArray(raw)) continue
|
|
94
|
+
out[key] = raw as T[]
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function preparePayload(data: CustomFieldDefinitionsResponse | null | undefined): CustomFieldDefinitionsPayload {
|
|
100
|
+
const items = Array.isArray(data?.items) ? [...data!.items] : []
|
|
101
|
+
items.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
|
|
102
|
+
const fieldsetsByEntity = normalizeRecord<CustomFieldsetDto>(data?.fieldsetsByEntity)
|
|
103
|
+
const entitySettings = (data?.entitySettings && typeof data.entitySettings === 'object'
|
|
104
|
+
? data.entitySettings
|
|
105
|
+
: {}) as CustomFieldDefinitionsPayload['entitySettings']
|
|
106
|
+
return { items, fieldsetsByEntity, entitySettings }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readDefinitionsViaFetch(
|
|
110
|
+
entityIds: string[],
|
|
111
|
+
fetchImpl: typeof fetch,
|
|
112
|
+
options?: CustomFieldDefinitionQueryOptions,
|
|
113
|
+
): Promise<CustomFieldDefinitionsPayload> {
|
|
114
|
+
const query = buildDefinitionsQuery(entityIds, options)
|
|
115
|
+
const res = await fetchImpl(`/api/entities/definitions?${query}`, {
|
|
116
|
+
headers: { 'content-type': 'application/json' },
|
|
117
|
+
})
|
|
118
|
+
const data = await res.json().catch(() => ({ items: [] }))
|
|
119
|
+
return preparePayload(data)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function readDefinitionsViaApi(entityIds: string[], options?: CustomFieldDefinitionQueryOptions): Promise<CustomFieldDefinitionsPayload> {
|
|
123
|
+
const query = buildDefinitionsQuery(entityIds, options)
|
|
124
|
+
const payload = await readApiResultOrThrow<CustomFieldDefinitionsResponse>(
|
|
125
|
+
`/api/entities/definitions?${query}`,
|
|
126
|
+
{ headers: { 'content-type': 'application/json' } },
|
|
127
|
+
{
|
|
128
|
+
errorMessage: 'Failed to load custom field definitions',
|
|
129
|
+
fallback: { items: [] },
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
return preparePayload(payload)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function fetchCustomFieldDefinitionsPayload(
|
|
136
|
+
entityIds: string | string[],
|
|
137
|
+
fetchImpl?: typeof fetch,
|
|
138
|
+
options?: CustomFieldDefinitionQueryOptions,
|
|
139
|
+
): Promise<CustomFieldDefinitionsPayload> {
|
|
140
|
+
const filtered = normalizeEntityIds(entityIds)
|
|
141
|
+
if (!filtered.length) return { items: [] }
|
|
142
|
+
return fetchImpl
|
|
143
|
+
? await readDefinitionsViaFetch(filtered, fetchImpl, options)
|
|
144
|
+
: await readDefinitionsViaApi(filtered, options)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function fetchCustomFieldDefs(
|
|
148
|
+
entityIds: string | string[],
|
|
149
|
+
fetchImpl?: typeof fetch,
|
|
150
|
+
options?: CustomFieldDefinitionQueryOptions,
|
|
151
|
+
): Promise<CustomFieldDefDto[]> {
|
|
152
|
+
const payload = await fetchCustomFieldDefinitionsPayload(entityIds, fetchImpl, options)
|
|
153
|
+
return payload.items ?? []
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type UseCustomFieldDefsOptions<TData = CustomFieldDefDto[]> = {
|
|
157
|
+
enabled?: boolean
|
|
158
|
+
staleTime?: number
|
|
159
|
+
gcTime?: number
|
|
160
|
+
/** @deprecated Custom fetch implementations are no longer needed. */
|
|
161
|
+
fetchImpl?: typeof fetch
|
|
162
|
+
keyExtras?: Array<string | number | boolean | null | undefined>
|
|
163
|
+
fieldset?: string
|
|
164
|
+
select?: (data: CustomFieldDefDto[]) => TData
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function useCustomFieldDefs<TData = CustomFieldDefDto[]>(
|
|
168
|
+
entityIds: string | string[] | null | undefined,
|
|
169
|
+
options: UseCustomFieldDefsOptions<TData> = {}
|
|
170
|
+
): UseQueryResult<TData> {
|
|
171
|
+
const {
|
|
172
|
+
enabled: enabledOption = true,
|
|
173
|
+
staleTime,
|
|
174
|
+
gcTime,
|
|
175
|
+
keyExtras,
|
|
176
|
+
fetchImpl,
|
|
177
|
+
fieldset,
|
|
178
|
+
} = options
|
|
179
|
+
const normalizedIds = React.useMemo(() => normalizeEntityIds(entityIds), [entityIds])
|
|
180
|
+
const idsSignature = React.useMemo(() => JSON.stringify(normalizedIds), [normalizedIds])
|
|
181
|
+
const extrasSignature = React.useMemo(() => JSON.stringify(keyExtras ?? []), [keyExtras])
|
|
182
|
+
const normalizedFieldset = typeof fieldset === 'string' && fieldset.trim().length ? fieldset.trim() : null
|
|
183
|
+
const queryKey = React.useMemo(
|
|
184
|
+
() => ['customFieldDefs', ...(keyExtras ?? []), ...normalizedIds, `fieldset:${normalizedFieldset ?? 'default'}`],
|
|
185
|
+
[idsSignature, extrasSignature, normalizedFieldset]
|
|
186
|
+
)
|
|
187
|
+
const enabled = enabledOption && normalizedIds.length > 0
|
|
188
|
+
|
|
189
|
+
return useQuery<CustomFieldDefDto[], Error, TData>({
|
|
190
|
+
queryKey,
|
|
191
|
+
queryFn: () =>
|
|
192
|
+
fetchCustomFieldDefs(
|
|
193
|
+
normalizedIds,
|
|
194
|
+
fetchImpl,
|
|
195
|
+
normalizedFieldset ? { fieldset: normalizedFieldset } : undefined
|
|
196
|
+
),
|
|
197
|
+
enabled,
|
|
198
|
+
staleTime: staleTime ?? 5 * 60 * 1000,
|
|
199
|
+
gcTime: gcTime ?? 30 * 60 * 1000,
|
|
200
|
+
select: options.select,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type CustomFieldVisibility = 'list' | 'form' | 'filter'
|
|
205
|
+
|
|
206
|
+
export function isDefVisible(def: CustomFieldDefDto, mode: CustomFieldVisibility): boolean {
|
|
207
|
+
switch (mode) {
|
|
208
|
+
case 'list':
|
|
209
|
+
return def.listVisible !== false
|
|
210
|
+
case 'form':
|
|
211
|
+
return def.formEditable !== false
|
|
212
|
+
case 'filter':
|
|
213
|
+
return !!def.filterable
|
|
214
|
+
default:
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function filterCustomFieldDefs(defs: CustomFieldDefDto[], mode: CustomFieldVisibility): CustomFieldDefDto[] {
|
|
220
|
+
return defs
|
|
221
|
+
.filter((d) => isDefVisible(d, mode))
|
|
222
|
+
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function invalidateCustomFieldDefs(
|
|
226
|
+
queryClient: QueryClient,
|
|
227
|
+
entityIds?: string | string[] | null,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const normalizedIds = normalizeEntityIds(entityIds)
|
|
230
|
+
const targetPrefixes = new Set(['customFieldDefs', 'customFieldForms', 'dealFormFields'])
|
|
231
|
+
if (!normalizedIds.length) {
|
|
232
|
+
await queryClient.invalidateQueries({
|
|
233
|
+
predicate: (query) => Array.isArray(query.queryKey) && typeof query.queryKey[0] === 'string' && targetPrefixes.has(query.queryKey[0] as string),
|
|
234
|
+
})
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
await queryClient.invalidateQueries({
|
|
238
|
+
predicate: (query) => {
|
|
239
|
+
if (!Array.isArray(query.queryKey)) return false
|
|
240
|
+
const [prefix] = query.queryKey
|
|
241
|
+
if (typeof prefix !== 'string' || !targetPrefixes.has(prefix)) return false
|
|
242
|
+
return normalizedIds.every((id) => query.queryKey.includes(id))
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { useCustomFieldDefs, type UseCustomFieldDefsOptions } from './customFieldDefs'
|
|
3
|
+
import { Filter } from '@open-mercato/shared/lib/query/types'
|
|
4
|
+
import type { FilterDef } from '../FilterOverlay'
|
|
5
|
+
import type { CustomFieldDefDto } from './customFieldDefs'
|
|
6
|
+
export type { CustomFieldDefDto }
|
|
7
|
+
import { filterCustomFieldDefs, fetchCustomFieldDefs as loadCustomFieldDefs } from './customFieldDefs'
|
|
8
|
+
import { type UseQueryResult } from '@tanstack/react-query'
|
|
9
|
+
import { apiCall } from './apiCall'
|
|
10
|
+
import { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'
|
|
11
|
+
|
|
12
|
+
function buildOptionsUrl(base: string, query?: string): string {
|
|
13
|
+
if (!query) return base
|
|
14
|
+
try {
|
|
15
|
+
const isAbsolute = /^([a-z][a-z\d+\-.]*:)?\/\//i.test(base)
|
|
16
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
|
17
|
+
const url = isAbsolute ? new URL(base) : new URL(base, origin)
|
|
18
|
+
if (!url.searchParams.has('query')) url.searchParams.append('query', query)
|
|
19
|
+
if (!url.searchParams.has('q')) url.searchParams.append('q', query)
|
|
20
|
+
if (isAbsolute) return url.toString()
|
|
21
|
+
return `${url.pathname}${url.search}`
|
|
22
|
+
} catch {
|
|
23
|
+
const sep = base.includes('?') ? '&' : '?'
|
|
24
|
+
if (base.includes('query=')) return `${base}${sep}q=${encodeURIComponent(query)}`
|
|
25
|
+
return `${base}${sep}query=${encodeURIComponent(query)}`
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type OptionsResponse = { items?: unknown[] }
|
|
30
|
+
|
|
31
|
+
async function loadRemoteOptions(url: string): Promise<Array<{ value: string; label: string }>> {
|
|
32
|
+
try {
|
|
33
|
+
const call = await apiCall<OptionsResponse>(url, undefined, { fallback: { items: [] } })
|
|
34
|
+
if (!call.ok) return []
|
|
35
|
+
const payload = call.result ?? { items: [] }
|
|
36
|
+
const items = Array.isArray(payload?.items) ? payload.items : []
|
|
37
|
+
return items.map((it: any) => ({
|
|
38
|
+
value: String(it?.value ?? it),
|
|
39
|
+
label: String(it?.label ?? it?.value ?? it),
|
|
40
|
+
}))
|
|
41
|
+
} catch {
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type RawOption = string | number | { value?: unknown; label?: unknown }
|
|
47
|
+
|
|
48
|
+
function normalizeOptions(options?: RawOption[]): Array<{ value: string; label: string }> {
|
|
49
|
+
if (!Array.isArray(options)) return []
|
|
50
|
+
return options.map((option) => {
|
|
51
|
+
if (option && typeof option === 'object' && 'value' in option) {
|
|
52
|
+
const rawValue = (option as any).value
|
|
53
|
+
const rawLabel = (option as any).label ?? rawValue
|
|
54
|
+
const value = String(rawValue)
|
|
55
|
+
const label = typeof rawLabel === 'string' ? rawLabel : String(rawLabel)
|
|
56
|
+
return { value, label }
|
|
57
|
+
}
|
|
58
|
+
const value = String(option)
|
|
59
|
+
return { value, label: value.charAt(0).toUpperCase() + value.slice(1) }
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildFilterDefsFromCustomFields(defs: CustomFieldDefDto[]): FilterDef[] {
|
|
64
|
+
const f: FilterDef[] = []
|
|
65
|
+
const visible = filterCustomFieldDefs(defs, 'filter')
|
|
66
|
+
const seenKeys = new Set<string>() // case-insensitive de-dupe by key
|
|
67
|
+
for (const d of visible) {
|
|
68
|
+
const keyLower = String(d.key).toLowerCase()
|
|
69
|
+
if (seenKeys.has(keyLower)) continue
|
|
70
|
+
seenKeys.add(keyLower)
|
|
71
|
+
const id = `cf_${d.key}`
|
|
72
|
+
const label = d.label || d.key
|
|
73
|
+
if (d.kind === 'boolean') {
|
|
74
|
+
f.push({ id, label, type: 'checkbox' })
|
|
75
|
+
} else if (d.kind === 'select' || d.kind === 'currency' || d.kind === 'relation' || d.kind === 'dictionary') {
|
|
76
|
+
const options = normalizeOptions(d.options)
|
|
77
|
+
const base: FilterDef = { id: d.multi ? `${id}In` : id, label, type: 'select', multiple: !!d.multi, options }
|
|
78
|
+
// When optionsUrl is provided, allow async options loading for filters too
|
|
79
|
+
const optionsUrl = d.kind === 'currency' ? CURRENCY_OPTIONS_URL : d.optionsUrl
|
|
80
|
+
if (optionsUrl) {
|
|
81
|
+
;(base as FilterDef).loadOptions = async (query?: string) => {
|
|
82
|
+
const url = buildOptionsUrl(optionsUrl, query)
|
|
83
|
+
return loadRemoteOptions(url)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
f.push(base)
|
|
87
|
+
} else if (d.kind === 'text' && d.multi) {
|
|
88
|
+
// Multi-text custom field → use tags input in filters
|
|
89
|
+
const base: FilterDef = {
|
|
90
|
+
id: `${id}In`,
|
|
91
|
+
label,
|
|
92
|
+
type: 'tags',
|
|
93
|
+
// If static options provided, pass them for suggestions
|
|
94
|
+
options: normalizeOptions(d.options),
|
|
95
|
+
} as any
|
|
96
|
+
// Enable async suggestions when optionsUrl provided
|
|
97
|
+
if (d.optionsUrl) {
|
|
98
|
+
;(base as any).loadOptions = async (query?: string) => {
|
|
99
|
+
const url = buildOptionsUrl(d.optionsUrl!, query)
|
|
100
|
+
return loadRemoteOptions(url)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
f.push(base)
|
|
104
|
+
} else {
|
|
105
|
+
f.push({ id, label, type: 'text' })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// De-duplicate by id in case of overlaps; keep first occurrence
|
|
109
|
+
const out: FilterDef[] = []
|
|
110
|
+
const seen = new Set<string>()
|
|
111
|
+
for (const item of f) {
|
|
112
|
+
if (seen.has(item.id)) continue
|
|
113
|
+
seen.add(item.id)
|
|
114
|
+
out.push(item)
|
|
115
|
+
}
|
|
116
|
+
// Preserve the original visible order (already sorted by priority) by mapping back
|
|
117
|
+
const order = new Map(visible.map((v, idx) => [v.key, idx]))
|
|
118
|
+
out.sort((a, b) => (order.get(a.id.replace(/^cf_/, '').replace(/In$/, '')) ?? 0) - (order.get(b.id.replace(/^cf_/, '').replace(/In$/, '')) ?? 0))
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function fetchCustomFieldFilterDefs(
|
|
123
|
+
entityIds: string | string[],
|
|
124
|
+
fetchImpl?: typeof fetch,
|
|
125
|
+
options?: { fieldset?: string },
|
|
126
|
+
): Promise<FilterDef[]> {
|
|
127
|
+
const defs: CustomFieldDefDto[] = await loadCustomFieldDefs(
|
|
128
|
+
entityIds,
|
|
129
|
+
fetchImpl,
|
|
130
|
+
options?.fieldset ? { fieldset: options.fieldset } : undefined,
|
|
131
|
+
)
|
|
132
|
+
return buildFilterDefsFromCustomFields(defs)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function useCustomFieldFilterDefs(
|
|
136
|
+
entityIds: string | string[] | null | undefined,
|
|
137
|
+
options: UseCustomFieldDefsOptions<FilterDef[]> = {}
|
|
138
|
+
): UseQueryResult<FilterDef[]> {
|
|
139
|
+
const { select, ...rest } = options
|
|
140
|
+
const selectFn = React.useCallback(
|
|
141
|
+
(defs: CustomFieldDefDto[]) => (select ? select(defs) : buildFilterDefsFromCustomFields(defs)),
|
|
142
|
+
[select]
|
|
143
|
+
)
|
|
144
|
+
return useCustomFieldDefs<FilterDef[]>(entityIds, { ...rest, select: selectFn })
|
|
145
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { CrudField } from '../CrudForm'
|
|
2
|
+
import type {
|
|
3
|
+
CustomFieldDefDto,
|
|
4
|
+
CustomFieldDefinitionsPayload,
|
|
5
|
+
} from './customFieldDefs'
|
|
6
|
+
import {
|
|
7
|
+
filterCustomFieldDefs,
|
|
8
|
+
fetchCustomFieldDefs,
|
|
9
|
+
fetchCustomFieldDefinitionsPayload,
|
|
10
|
+
} from './customFieldDefs'
|
|
11
|
+
import { FieldRegistry, loadGeneratedFieldRegistrations } from '../fields/registry'
|
|
12
|
+
import { apiCall } from './apiCall'
|
|
13
|
+
import { normalizeCustomFieldOptions } from '@open-mercato/shared/modules/entities/options'
|
|
14
|
+
import { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'
|
|
15
|
+
|
|
16
|
+
let registryReady: Promise<void> | null = null
|
|
17
|
+
|
|
18
|
+
async function ensureFieldRegistryReady() {
|
|
19
|
+
if (!registryReady) {
|
|
20
|
+
registryReady = loadGeneratedFieldRegistrations().catch((err) => {
|
|
21
|
+
registryReady = null
|
|
22
|
+
throw err
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
await registryReady
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildOptionsUrl(base: string, query?: string): string {
|
|
29
|
+
if (!query) return base
|
|
30
|
+
try {
|
|
31
|
+
const isAbsolute = /^([a-z][a-z\d+\-.]*:)?\/\//i.test(base)
|
|
32
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
|
33
|
+
const url = isAbsolute ? new URL(base) : new URL(base, origin)
|
|
34
|
+
if (!url.searchParams.has('query')) url.searchParams.append('query', query)
|
|
35
|
+
if (!url.searchParams.has('q')) url.searchParams.append('q', query)
|
|
36
|
+
if (isAbsolute) return url.toString()
|
|
37
|
+
return `${url.pathname}${url.search}`
|
|
38
|
+
} catch {
|
|
39
|
+
const sep = base.includes('?') ? '&' : '?'
|
|
40
|
+
if (base.includes('query=')) return `${base}${sep}q=${encodeURIComponent(query)}`
|
|
41
|
+
return `${base}${sep}query=${encodeURIComponent(query)}`
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type OptionsResponse = { items?: unknown[] }
|
|
46
|
+
|
|
47
|
+
async function loadRemoteOptions(url: string): Promise<Array<{ value: string; label: string }>> {
|
|
48
|
+
try {
|
|
49
|
+
const call = await apiCall<OptionsResponse>(url, undefined, { fallback: { items: [] } })
|
|
50
|
+
if (!call.ok) return []
|
|
51
|
+
const payload = call.result ?? { items: [] }
|
|
52
|
+
const items = Array.isArray(payload?.items) ? payload.items : []
|
|
53
|
+
return items.map((it: any) => ({
|
|
54
|
+
value: String(it?.value ?? it),
|
|
55
|
+
label: String(it?.label ?? it?.value ?? it),
|
|
56
|
+
}))
|
|
57
|
+
} catch {
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildFormFieldFromCustomFieldDef(
|
|
63
|
+
def: CustomFieldDefDto,
|
|
64
|
+
opts?: { bareIds?: boolean }
|
|
65
|
+
): CrudField | null {
|
|
66
|
+
const id = opts?.bareIds ? def.key : `cf_${def.key}`
|
|
67
|
+
const label = def.label || def.key
|
|
68
|
+
const required = Array.isArray((def as any).validation)
|
|
69
|
+
? ((def as any).validation as any[]).some((rule) => rule && rule.rule === 'required')
|
|
70
|
+
: false
|
|
71
|
+
|
|
72
|
+
switch (def.kind) {
|
|
73
|
+
case 'boolean':
|
|
74
|
+
return { id, label, type: 'checkbox', description: def.description, required }
|
|
75
|
+
case 'integer':
|
|
76
|
+
case 'float':
|
|
77
|
+
return { id, label, type: 'number', description: def.description, required }
|
|
78
|
+
case 'multiline': {
|
|
79
|
+
let editor: 'simple' | 'uiw' | 'html' = 'uiw'
|
|
80
|
+
if (def.editor === 'simpleMarkdown') editor = 'simple'
|
|
81
|
+
else if (def.editor === 'htmlRichText') editor = 'html'
|
|
82
|
+
return { id, label, type: 'richtext', description: def.description, editor, required }
|
|
83
|
+
}
|
|
84
|
+
case 'select':
|
|
85
|
+
case 'currency':
|
|
86
|
+
case 'relation':
|
|
87
|
+
return {
|
|
88
|
+
id,
|
|
89
|
+
label,
|
|
90
|
+
type: 'select',
|
|
91
|
+
description: def.description,
|
|
92
|
+
options: normalizeCustomFieldOptions(def.options || []).map((option) => ({
|
|
93
|
+
value: option.value,
|
|
94
|
+
label: option.label,
|
|
95
|
+
})),
|
|
96
|
+
multiple: !!def.multi,
|
|
97
|
+
required,
|
|
98
|
+
...(def.kind === 'currency'
|
|
99
|
+
? {
|
|
100
|
+
loadOptions: async (query?: string) => {
|
|
101
|
+
const url = buildOptionsUrl(CURRENCY_OPTIONS_URL, query)
|
|
102
|
+
return loadRemoteOptions(url)
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
: def.optionsUrl
|
|
106
|
+
? {
|
|
107
|
+
loadOptions: async (query?: string) => {
|
|
108
|
+
const url = buildOptionsUrl(def.optionsUrl!, query)
|
|
109
|
+
return loadRemoteOptions(url)
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
...(def.multi && def.input === 'listbox' ? ({ listbox: true } as any) : {}),
|
|
114
|
+
}
|
|
115
|
+
default: {
|
|
116
|
+
if (def.kind === 'text' && def.multi) {
|
|
117
|
+
const base: any = { id, label, type: 'tags', description: def.description, required }
|
|
118
|
+
const resolvedOptions = normalizeCustomFieldOptions(def.options || [])
|
|
119
|
+
if (resolvedOptions.length > 0) {
|
|
120
|
+
base.options = resolvedOptions.map((option) => ({ value: option.value, label: option.label }))
|
|
121
|
+
}
|
|
122
|
+
if (def.optionsUrl) {
|
|
123
|
+
base.loadOptions = async (query?: string) => {
|
|
124
|
+
const url = buildOptionsUrl(def.optionsUrl!, query)
|
|
125
|
+
return loadRemoteOptions(url)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return base
|
|
129
|
+
}
|
|
130
|
+
if (def.kind === 'text' && typeof def.editor === 'string' && def.editor) {
|
|
131
|
+
let editor: 'simple' | 'uiw' | 'html' = 'uiw'
|
|
132
|
+
if (def.editor === 'simpleMarkdown') editor = 'simple'
|
|
133
|
+
else if (def.editor === 'htmlRichText') editor = 'html'
|
|
134
|
+
return { id, label, type: 'richtext', description: def.description, editor, required }
|
|
135
|
+
}
|
|
136
|
+
const input = FieldRegistry.getInput(def.kind)
|
|
137
|
+
if (input) {
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
label,
|
|
141
|
+
type: 'custom',
|
|
142
|
+
required,
|
|
143
|
+
description: def.description,
|
|
144
|
+
component: (props) => input({ ...props, def }),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { id, label, type: 'text', description: def.description, required }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function buildFormFieldsFromCustomFields(
|
|
153
|
+
defs: CustomFieldDefDto[],
|
|
154
|
+
opts?: { bareIds?: boolean }
|
|
155
|
+
): CrudField[] {
|
|
156
|
+
const fields: CrudField[] = []
|
|
157
|
+
const visible = filterCustomFieldDefs(defs, 'form')
|
|
158
|
+
const seenKeys = new Set<string>()
|
|
159
|
+
for (const def of visible) {
|
|
160
|
+
const keyLower = String(def.key).toLowerCase()
|
|
161
|
+
if (seenKeys.has(keyLower)) continue
|
|
162
|
+
seenKeys.add(keyLower)
|
|
163
|
+
const field = buildFormFieldFromCustomFieldDef(def, opts)
|
|
164
|
+
if (field) fields.push(field)
|
|
165
|
+
}
|
|
166
|
+
return fields
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function fetchCustomFieldFormStructure(
|
|
170
|
+
entityIds: string | string[],
|
|
171
|
+
fetchImpl?: typeof fetch,
|
|
172
|
+
options?: { bareIds?: boolean },
|
|
173
|
+
): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {
|
|
174
|
+
await ensureFieldRegistryReady()
|
|
175
|
+
const metadata = await fetchCustomFieldDefinitionsPayload(entityIds, fetchImpl)
|
|
176
|
+
const definitions = Array.isArray(metadata.items) ? metadata.items : []
|
|
177
|
+
const fields = buildFormFieldsFromCustomFields(definitions, options)
|
|
178
|
+
return { fields, definitions, metadata }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function fetchCustomFieldFormFields(
|
|
182
|
+
entityIds: string | string[],
|
|
183
|
+
fetchImpl?: typeof fetch,
|
|
184
|
+
options?: { bareIds?: boolean },
|
|
185
|
+
): Promise<CrudField[]> {
|
|
186
|
+
const { fields } = await fetchCustomFieldFormStructure(entityIds, fetchImpl, options)
|
|
187
|
+
return fields
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function fetchCustomFieldFormFieldsWithDefinitions(
|
|
191
|
+
entityIds: string | string[],
|
|
192
|
+
fetchImpl?: typeof fetch,
|
|
193
|
+
options?: { bareIds?: boolean },
|
|
194
|
+
): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {
|
|
195
|
+
return fetchCustomFieldFormStructure(entityIds, fetchImpl, options)
|
|
196
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type CollectCustomFieldOptions = {
|
|
2
|
+
prefixes?: string[]
|
|
3
|
+
stripPrefix?: boolean
|
|
4
|
+
transform?: (value: unknown, fieldId: string, rawKey: string) => unknown
|
|
5
|
+
accept?: (fieldId: string, rawKey: string, value: unknown) => boolean
|
|
6
|
+
omitUndefined?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PREFIXES = ['cf_', 'cf:']
|
|
10
|
+
|
|
11
|
+
export function collectCustomFieldValues(
|
|
12
|
+
values: Record<string, unknown>,
|
|
13
|
+
options: CollectCustomFieldOptions = {},
|
|
14
|
+
): Record<string, unknown> {
|
|
15
|
+
const {
|
|
16
|
+
prefixes = DEFAULT_PREFIXES,
|
|
17
|
+
stripPrefix = true,
|
|
18
|
+
transform,
|
|
19
|
+
accept,
|
|
20
|
+
omitUndefined = true,
|
|
21
|
+
} = options
|
|
22
|
+
|
|
23
|
+
const result: Record<string, unknown> = {}
|
|
24
|
+
|
|
25
|
+
for (const [rawKey, rawValue] of Object.entries(values)) {
|
|
26
|
+
const prefix = prefixes.find((candidate) => rawKey.startsWith(candidate))
|
|
27
|
+
if (!prefix) continue
|
|
28
|
+
|
|
29
|
+
const fieldId = stripPrefix ? rawKey.slice(prefix.length) : rawKey
|
|
30
|
+
if (!fieldId) continue
|
|
31
|
+
|
|
32
|
+
if (accept && !accept(fieldId, rawKey, rawValue)) continue
|
|
33
|
+
|
|
34
|
+
const nextValue = transform ? transform(rawValue, fieldId, rawKey) : rawValue
|
|
35
|
+
if (omitUndefined && nextValue === undefined) continue
|
|
36
|
+
|
|
37
|
+
result[fieldId] = nextValue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type FlashType = 'success' | 'error' | 'warning' | 'info'
|
|
2
|
+
|
|
3
|
+
// Append flash message and type to a URL (relative or absolute) and return a relative URL string.
|
|
4
|
+
export function withFlash(url: string, message: string, type: FlashType = 'success'): string {
|
|
5
|
+
const base = typeof window !== 'undefined' && window.location ? window.location.origin : 'http://localhost'
|
|
6
|
+
const u = new URL(url, base)
|
|
7
|
+
u.searchParams.set('flash', message)
|
|
8
|
+
u.searchParams.set('type', type)
|
|
9
|
+
const qs = u.searchParams.toString()
|
|
10
|
+
return `${u.pathname}${qs ? `?${qs}` : ''}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Helper to push a URL with flash via Next.js router
|
|
14
|
+
export function pushWithFlash(router: { push: (href: string) => any }, url: string, message: string, type: FlashType = 'success') {
|
|
15
|
+
router.push(withFlash(url, message, type))
|
|
16
|
+
}
|
|
17
|
+
|