@ngbplatform/ui 1.3.0

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.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. package/vite-public-assets.js +68 -0
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ import { useSlots } from 'vue'
3
+ import NgbDrawer from '../components/NgbDrawer.vue'
4
+ import NgbRegisterGrid from '../components/register/NgbRegisterGrid.vue'
5
+ import type { RegisterColumn, RegisterDataRow } from '../components/register/registerTypes'
6
+ import NgbEntityListPageHeader from './NgbEntityListPageHeader.vue'
7
+
8
+ const props = withDefaults(defineProps<{
9
+ title: string
10
+ canBack?: boolean
11
+ itemsCount?: number | null
12
+ total?: number | null
13
+ loading?: boolean
14
+ error?: string | null
15
+ warning?: string | null
16
+ disableCreate?: boolean
17
+ showFilter?: boolean
18
+ disableFilter?: boolean
19
+ filterActive?: boolean
20
+ disablePrev?: boolean
21
+ disableNext?: boolean
22
+ columns: RegisterColumn[]
23
+ rows: RegisterDataRow[]
24
+ storageKey: string
25
+ drawerOpen?: boolean
26
+ drawerTitle?: string
27
+ drawerSubtitle?: string
28
+ drawerHideHeader?: boolean
29
+ drawerFlushBody?: boolean
30
+ beforeClose?: (() => boolean | Promise<boolean>) | null
31
+ }>(), {
32
+ canBack: true,
33
+ loading: false,
34
+ error: null,
35
+ warning: null,
36
+ showFilter: true,
37
+ disableCreate: false,
38
+ disableFilter: false,
39
+ filterActive: false,
40
+ disablePrev: false,
41
+ disableNext: false,
42
+ drawerOpen: undefined,
43
+ drawerTitle: '',
44
+ drawerSubtitle: '',
45
+ drawerHideHeader: false,
46
+ drawerFlushBody: false,
47
+ beforeClose: null,
48
+ })
49
+
50
+ const emit = defineEmits<{
51
+ (e: 'back'): void
52
+ (e: 'refresh'): void
53
+ (e: 'create'): void
54
+ (e: 'filter'): void
55
+ (e: 'prev'): void
56
+ (e: 'next'): void
57
+ (e: 'rowActivate', id: string): void
58
+ (e: 'update:drawerOpen', value: boolean): void
59
+ }>()
60
+
61
+ const slots = useSlots()
62
+ </script>
63
+
64
+ <template>
65
+ <div class="h-full min-h-0 flex flex-col">
66
+ <NgbEntityListPageHeader
67
+ :title="title"
68
+ :can-back="canBack"
69
+ :items-count="itemsCount"
70
+ :total="total"
71
+ :loading="loading"
72
+ :disable-create="disableCreate"
73
+ :show-filter="showFilter"
74
+ :disable-filter="disableFilter"
75
+ :filter-active="filterActive"
76
+ :disable-prev="disablePrev"
77
+ :disable-next="disableNext"
78
+ @back="emit('back')"
79
+ @refresh="emit('refresh')"
80
+ @create="emit('create')"
81
+ @filter="emit('filter')"
82
+ @prev="emit('prev')"
83
+ @next="emit('next')"
84
+ >
85
+ <template #filters>
86
+ <slot name="filters" />
87
+ </template>
88
+ </NgbEntityListPageHeader>
89
+
90
+ <div class="p-6 flex-1 min-h-0 flex flex-col">
91
+ <div
92
+ v-if="error"
93
+ class="mb-4 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-100"
94
+ >
95
+ {{ error }}
96
+ </div>
97
+
98
+ <div
99
+ v-if="warning"
100
+ class="mb-4 rounded-[var(--ngb-radius)] border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/20 dark:text-amber-100"
101
+ >
102
+ {{ warning }}
103
+ </div>
104
+
105
+ <slot name="beforeGrid" />
106
+
107
+ <slot name="grid">
108
+ <NgbRegisterGrid
109
+ class="flex-1 min-h-0"
110
+ fill-height
111
+ :show-panel="false"
112
+ :columns="columns"
113
+ :rows="rows"
114
+ :show-totals="false"
115
+ :storage-key="storageKey"
116
+ :activate-on-row-click="true"
117
+ @rowActivate="emit('rowActivate', String($event))"
118
+ />
119
+ </slot>
120
+
121
+ <div v-if="loading" class="mt-3 text-sm text-ngb-muted">Loading…</div>
122
+ </div>
123
+
124
+ <slot name="filterDrawer" />
125
+
126
+ <NgbDrawer
127
+ v-if="drawerOpen !== undefined && slots.drawerContent"
128
+ :open="drawerOpen"
129
+ :title="drawerTitle"
130
+ :subtitle="drawerSubtitle"
131
+ :hide-header="drawerHideHeader"
132
+ :flush-body="drawerFlushBody"
133
+ :before-close="beforeClose ?? undefined"
134
+ @update:open="emit('update:drawerOpen', $event)"
135
+ >
136
+ <template v-if="slots.drawerActions" #actions>
137
+ <slot name="drawerActions" />
138
+ </template>
139
+
140
+ <slot name="drawerContent" />
141
+ </NgbDrawer>
142
+ </div>
143
+ </template>
@@ -0,0 +1,28 @@
1
+ import type { CatalogTypeMetadata, DocumentTypeMetadata, MetadataFormBehavior } from './types'
2
+
3
+ export type MetadataFrameworkConfig = {
4
+ loadCatalogTypeMetadata: (catalogType: string) => Promise<CatalogTypeMetadata>
5
+ loadDocumentTypeMetadata: (documentType: string) => Promise<DocumentTypeMetadata>
6
+ formBehavior?: MetadataFormBehavior
7
+ }
8
+
9
+ let metadataFrameworkConfig: MetadataFrameworkConfig | null = null
10
+
11
+ export function configureNgbMetadata(config: MetadataFrameworkConfig): void {
12
+ metadataFrameworkConfig = config
13
+ }
14
+
15
+ export function getConfiguredNgbMetadata(): MetadataFrameworkConfig {
16
+ if (!metadataFrameworkConfig) {
17
+ throw new Error('NGB metadata framework is not configured. Call configureNgbMetadata(...) during app bootstrap.')
18
+ }
19
+
20
+ return metadataFrameworkConfig
21
+ }
22
+
23
+ export function resolveNgbMetadataFormBehavior(override?: MetadataFormBehavior): MetadataFormBehavior {
24
+ return {
25
+ ...(metadataFrameworkConfig?.formBehavior ?? {}),
26
+ ...(override ?? {}),
27
+ }
28
+ }
@@ -0,0 +1,27 @@
1
+ export function dataTypeKind(dataType: unknown): string {
2
+ const normalized = String(dataType ?? '').trim()
3
+ return normalized || 'Unknown'
4
+ }
5
+
6
+ export function isGuidType(dataType: unknown): boolean {
7
+ return dataTypeKind(dataType) === 'Guid'
8
+ }
9
+
10
+ export function isBooleanType(dataType: unknown): boolean {
11
+ return dataTypeKind(dataType) === 'Boolean'
12
+ }
13
+
14
+ export function isNumberType(dataType: unknown): boolean {
15
+ const kind = dataTypeKind(dataType)
16
+ return kind === 'Int32' || kind === 'Decimal' || kind === 'Money'
17
+ }
18
+
19
+ export function isDateType(dataType: unknown): boolean {
20
+ const kind = dataTypeKind(dataType)
21
+ return kind === 'Date' || kind === 'DateOnly'
22
+ }
23
+
24
+ export function isDateTimeType(dataType: unknown): boolean {
25
+ const kind = dataTypeKind(dataType)
26
+ return kind === 'DateTime' || kind === 'DateTimeOffset'
27
+ }
@@ -0,0 +1,114 @@
1
+ import { dataTypeKind } from './dataTypes'
2
+ import { resolveNgbMetadataFormBehavior } from './config'
3
+ import { tryExtractReferenceId } from './entityModel'
4
+ import type { EntityFormModel, FieldHiddenArgs, FieldMetadata, FieldReadonlyArgs, FormMetadata, JsonValue, MetadataFormBehavior, RecordFields } from './types'
5
+
6
+ export function flattenFormFields(form?: FormMetadata | null): FieldMetadata[] {
7
+ if (!form?.sections) return []
8
+
9
+ const out: FieldMetadata[] = []
10
+ for (const section of form.sections) {
11
+ for (const row of section.rows ?? []) {
12
+ for (const field of row.fields ?? []) out.push(field)
13
+ }
14
+ }
15
+
16
+ return out
17
+ }
18
+
19
+ export function defaultFindDisplayField(form: FormMetadata): FieldMetadata | null {
20
+ for (const field of flattenFormFields(form)) {
21
+ if (field.key === 'display') return field
22
+ }
23
+
24
+ return null
25
+ }
26
+
27
+ export function defaultIsFieldReadonly(args: FieldReadonlyArgs): boolean {
28
+ if (args.forceReadonly) return true
29
+ if (args.field.isReadOnly) return true
30
+ if (args.status !== undefined && args.field.readOnlyWhenStatusIn?.includes(args.status)) return true
31
+ return false
32
+ }
33
+
34
+ export function defaultIsFieldHidden(_args: FieldHiddenArgs): boolean {
35
+ return false
36
+ }
37
+
38
+ function tryFlattenRef(value: unknown): unknown {
39
+ const referenceId = tryExtractReferenceId(value)
40
+ return referenceId ?? value
41
+ }
42
+
43
+ export function ensureModelKeys(form: FormMetadata | null | undefined, model: EntityFormModel): void {
44
+ for (const field of flattenFormFields(form)) {
45
+ if (!(field.key in model) || model[field.key] === undefined) {
46
+ model[field.key] = dataTypeKind(field.dataType) === 'Boolean' || field.uiControl === 5 ? false : null
47
+ }
48
+ }
49
+ }
50
+
51
+ function normalizeValue(field: FieldMetadata, value: unknown): JsonValue {
52
+ let normalized = value
53
+ if (normalized === '') normalized = null
54
+ if (normalized === undefined) normalized = null
55
+
56
+ normalized = tryFlattenRef(normalized)
57
+
58
+ if (normalized === null) return null
59
+
60
+ switch (field.uiControl) {
61
+ case 5:
62
+ return !!normalized
63
+ case 3:
64
+ case 4:
65
+ if (typeof normalized === 'number') return normalized
66
+ if (typeof normalized === 'string') {
67
+ const number = Number(normalized)
68
+ return Number.isFinite(number) ? number : normalized
69
+ }
70
+ return normalized as JsonValue
71
+ case 6:
72
+ case 7:
73
+ return normalized as JsonValue
74
+ }
75
+
76
+ switch (dataTypeKind(field.dataType)) {
77
+ case 'Int32': {
78
+ if (typeof normalized === 'number') return Math.trunc(normalized)
79
+ if (typeof normalized === 'string') {
80
+ const number = Number.parseInt(normalized, 10)
81
+ return Number.isFinite(number) ? number : normalized
82
+ }
83
+ return normalized as JsonValue
84
+ }
85
+ case 'Decimal':
86
+ case 'Money': {
87
+ if (typeof normalized === 'number') return normalized
88
+ if (typeof normalized === 'string') {
89
+ const number = Number(normalized)
90
+ return Number.isFinite(number) ? number : normalized
91
+ }
92
+ return normalized as JsonValue
93
+ }
94
+ case 'Boolean':
95
+ return !!normalized
96
+ default:
97
+ return normalized as JsonValue
98
+ }
99
+ }
100
+
101
+ export function buildFieldsPayload(form: FormMetadata | null | undefined, model: EntityFormModel): RecordFields {
102
+ const fields = flattenFormFields(form)
103
+ const payload: RecordFields = {}
104
+
105
+ for (const field of fields) {
106
+ payload[field.key] = normalizeValue(field, model[field.key])
107
+ }
108
+
109
+ return payload
110
+ }
111
+
112
+ export function resolveMetadataFormBehavior(override?: MetadataFormBehavior): MetadataFormBehavior {
113
+ return resolveNgbMetadataFormBehavior(override)
114
+ }
@@ -0,0 +1,34 @@
1
+ import type { ReferenceValue } from './types'
2
+
3
+ export type { EntityFormModel, ReferenceValue } from './types'
4
+
5
+ export function isReferenceValue(value: unknown): value is ReferenceValue {
6
+ return !!value
7
+ && typeof value === 'object'
8
+ && typeof (value as ReferenceValue).id === 'string'
9
+ && typeof (value as ReferenceValue).display === 'string'
10
+ }
11
+
12
+ export function tryExtractReferenceId(value: unknown): string | null {
13
+ if (typeof value === 'string') {
14
+ const normalized = value.trim()
15
+ return normalized || null
16
+ }
17
+
18
+ if (isReferenceValue(value)) {
19
+ const normalized = value.id.trim()
20
+ return normalized || null
21
+ }
22
+
23
+ return null
24
+ }
25
+
26
+ export function tryExtractReferenceDisplay(value: unknown): string | null {
27
+ if (!isReferenceValue(value)) return null
28
+ const normalized = value.display.trim()
29
+ return normalized || null
30
+ }
31
+
32
+ export function asTrimmedString(value: unknown): string {
33
+ return value == null ? '' : String(value).trim()
34
+ }
@@ -0,0 +1,83 @@
1
+ import { dataTypeKind } from './dataTypes'
2
+ import { isReferenceValue } from './entityModel'
3
+
4
+ const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/
5
+
6
+ export function formatDateOnlyValue(value: string): string {
7
+ if (!DATE_ONLY_RE.test(value)) return value
8
+
9
+ const [year, month, day] = value.split('-').map((part) => Number(part))
10
+ const date = new Date(year, month - 1, day, 12, 0, 0)
11
+ if (Number.isNaN(date.getTime())) return value
12
+ return date.toLocaleDateString()
13
+ }
14
+
15
+ export function formatDateTimeValue(value: string): string {
16
+ const date = new Date(value)
17
+ if (Number.isNaN(date.getTime())) return value
18
+ return date.toLocaleString()
19
+ }
20
+
21
+ export function formatNumberValue(kind: string, value: unknown): string {
22
+ const numeric = typeof value === 'number' ? value : Number(value)
23
+ if (!Number.isFinite(numeric)) return String(value ?? '—')
24
+
25
+ if (kind === 'Int32') {
26
+ return Math.trunc(numeric).toLocaleString()
27
+ }
28
+
29
+ if (kind === 'Money') {
30
+ return numeric.toLocaleString(undefined, {
31
+ minimumFractionDigits: 2,
32
+ maximumFractionDigits: 2,
33
+ })
34
+ }
35
+
36
+ return numeric.toLocaleString(undefined, {
37
+ minimumFractionDigits: 0,
38
+ maximumFractionDigits: 4,
39
+ })
40
+ }
41
+
42
+ function formatObjectValue(value: Record<string, unknown>): string {
43
+ const entries = Object.entries(value)
44
+ .filter(([, item]) => item !== undefined)
45
+ .map(([key, item]) => `${humanizeEntityKey(key)}: ${formatLooseEntityValue(item)}`)
46
+ .filter((item) => item.length > 0)
47
+
48
+ return entries.length > 0 ? entries.join(' · ') : '—'
49
+ }
50
+
51
+ export function formatLooseEntityValue(value: unknown): string {
52
+ if (value == null) return '—'
53
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No'
54
+ if (typeof value === 'number') return Number.isFinite(value) ? value.toLocaleString() : String(value)
55
+ if (typeof value === 'string') return value.trim() || '—'
56
+ if (Array.isArray(value)) return value.map((item) => formatLooseEntityValue(item)).join(' · ')
57
+ if (isReferenceValue(value)) return value.display.trim() || value.id
58
+ if (typeof value === 'object') return formatObjectValue(value as Record<string, unknown>)
59
+ return String(value)
60
+ }
61
+
62
+ export function formatTypedEntityValue(dataType: unknown, value: unknown): string {
63
+ if (value == null) return '—'
64
+
65
+ const kind = dataTypeKind(dataType)
66
+ if (kind === 'Boolean') return !!value ? 'Yes' : 'No'
67
+ if (kind === 'Date' && typeof value === 'string') return formatDateOnlyValue(value)
68
+ if (kind === 'DateTime' && typeof value === 'string') return formatDateTimeValue(value)
69
+ if (kind === 'Money' || kind === 'Decimal' || kind === 'Int32') return formatNumberValue(kind, value)
70
+
71
+ return formatLooseEntityValue(value)
72
+ }
73
+
74
+ export function humanizeEntityKey(value: string): string {
75
+ const normalized = String(value ?? '').trim().replace(/[._]/g, ' ')
76
+ if (!normalized) return 'Field'
77
+
78
+ return normalized
79
+ .split(/\s+/)
80
+ .filter(Boolean)
81
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
82
+ .join(' ')
83
+ }
@@ -0,0 +1,84 @@
1
+ import { resolveNgbMetadataFormBehavior } from './config'
2
+ import { dataTypeKind } from './dataTypes'
3
+ import { isReferenceValue } from './entityModel'
4
+ import { resolveLookupHint } from './lookup'
5
+ import type { EntityFormModel, FieldMetadata, FieldOption, LookupHint, MetadataFormBehavior } from './types'
6
+
7
+ export type FieldRenderMode =
8
+ | 'select'
9
+ | 'lookup'
10
+ | 'checkbox'
11
+ | 'textarea'
12
+ | 'reference-display'
13
+ | 'date'
14
+ | 'input'
15
+
16
+ export type FieldInputType = 'text' | 'number' | 'datetime-local'
17
+
18
+ export type FieldRendererState = {
19
+ mode: FieldRenderMode
20
+ inputType: FieldInputType
21
+ fieldOptions: FieldOption[] | null
22
+ hint: LookupHint | null
23
+ }
24
+
25
+ export function resolveFieldRendererState(args: {
26
+ entityTypeCode: string
27
+ model: EntityFormModel
28
+ field: FieldMetadata
29
+ modelValue: unknown
30
+ behavior?: MetadataFormBehavior
31
+ }): FieldRendererState {
32
+ const behavior = resolveNgbMetadataFormBehavior(args.behavior)
33
+ const fieldOptions = behavior.resolveFieldOptions?.({
34
+ entityTypeCode: args.entityTypeCode,
35
+ model: args.model,
36
+ field: args.field,
37
+ }) ?? args.field.options ?? null
38
+ const hint = resolveLookupHint({
39
+ entityTypeCode: args.entityTypeCode,
40
+ model: args.model,
41
+ field: args.field,
42
+ behavior,
43
+ })
44
+ const dataType = dataTypeKind(args.field.dataType)
45
+
46
+ const isLookup = !!hint || dataType === 'Lookup'
47
+ const isCheckbox = args.field.uiControl === 5 || dataType === 'Boolean'
48
+ const isTextArea = args.field.uiControl === 2
49
+ const isDate = args.field.uiControl === 6 || dataType === 'Date'
50
+ const isDateTime = args.field.uiControl === 7 || dataType === 'DateTime'
51
+ const isNumber = args.field.uiControl === 3 || dataType === 'Int32' || dataType === 'Decimal'
52
+ const isMoney = args.field.uiControl === 4 || dataType === 'Money'
53
+
54
+ if (fieldOptions) {
55
+ return { mode: 'select', inputType: 'text', fieldOptions, hint }
56
+ }
57
+
58
+ if (isLookup && hint) {
59
+ return { mode: 'lookup', inputType: 'text', fieldOptions, hint }
60
+ }
61
+
62
+ if (isCheckbox) {
63
+ return { mode: 'checkbox', inputType: 'text', fieldOptions, hint }
64
+ }
65
+
66
+ if (isTextArea) {
67
+ return { mode: 'textarea', inputType: 'text', fieldOptions, hint }
68
+ }
69
+
70
+ if (isReferenceValue(args.modelValue) && !hint) {
71
+ return { mode: 'reference-display', inputType: 'text', fieldOptions, hint }
72
+ }
73
+
74
+ if (isDate) {
75
+ return { mode: 'date', inputType: 'text', fieldOptions, hint }
76
+ }
77
+
78
+ return {
79
+ mode: 'input',
80
+ inputType: isDateTime ? 'datetime-local' : (isNumber || isMoney ? 'number' : 'text'),
81
+ fieldOptions,
82
+ hint,
83
+ }
84
+ }
@@ -0,0 +1,153 @@
1
+ import { isNonEmptyGuid } from '../utils/guid'
2
+ import type { FilterFieldLike, FilterFieldOption, FilterFieldState, FilterLookupItem, LookupStoreApi, ResolvedLookupSource } from './types'
3
+
4
+ export type {
5
+ FilterFieldLike,
6
+ FilterFieldOption,
7
+ FilterFieldState,
8
+ FilterLookupItem,
9
+ LookupStoreApi,
10
+ ResolvedLookupSource,
11
+ } from './types'
12
+
13
+ export function splitFilterValues(raw: string): string[] {
14
+ return raw
15
+ .split(',')
16
+ .map((part) => part.trim())
17
+ .filter((part) => part.length > 0)
18
+ }
19
+
20
+ export function joinFilterValues(values: string[]): string {
21
+ return values
22
+ .map((value) => value.trim())
23
+ .filter((value) => value.length > 0)
24
+ .join(',')
25
+ }
26
+
27
+ export function summarizeFilterValues(values: string[]): string | null {
28
+ if (values.length === 0) return null
29
+ const first = values[0]
30
+ const remaining = values.length - 1
31
+ return remaining > 0 ? `${first} (+${remaining})` : first
32
+ }
33
+
34
+ export function optionLabelForFilter(field: Pick<FilterFieldLike, 'options'>, rawValue: string): string {
35
+ const normalized = rawValue.trim()
36
+ if (!normalized) return ''
37
+ const option = (field.options ?? []).find((entry) => String(entry.value ?? '').trim() === normalized)
38
+ return String(option?.label ?? normalized).trim()
39
+ }
40
+
41
+ type FilterOptionLabelField = {
42
+ key: string
43
+ options?: readonly FilterFieldOption[] | null
44
+ }
45
+
46
+ export function buildFilterOptionLabelsByKey<TField extends FilterOptionLabelField>(
47
+ fields: readonly TField[],
48
+ ): Record<string, Map<string, string>> {
49
+ const result: Record<string, Map<string, string>> = {}
50
+
51
+ for (const field of fields) {
52
+ if (!field.key || !field.options?.length) continue
53
+
54
+ const options = new Map<string, string>()
55
+ for (const option of field.options) {
56
+ const value = String(option?.value ?? '').trim()
57
+ const label = String(option?.label ?? '').trim()
58
+ if (!value || !label) continue
59
+ options.set(value, label)
60
+ }
61
+
62
+ if (options.size > 0) result[field.key] = options
63
+ }
64
+
65
+ return result
66
+ }
67
+
68
+ export function filterInputType(dataType: unknown): 'number' | 'text' {
69
+ const normalized = String(dataType ?? '').trim().toLowerCase()
70
+ if (normalized === 'decimal' || normalized === 'money' || normalized === 'int32' || normalized === '2' || normalized === '3') {
71
+ return 'number'
72
+ }
73
+ if (normalized.includes('decimal') || normalized.includes('number') || normalized.includes('int')) return 'number'
74
+ return 'text'
75
+ }
76
+
77
+ export function filterPlaceholder(field: Pick<FilterFieldLike, 'label' | 'lookup' | 'isMulti'>): string {
78
+ if (field.lookup) return `Type ${field.label.toLowerCase()}…`
79
+ if (field.isMulti) return 'Comma-separated values…'
80
+ return field.label
81
+ }
82
+
83
+ export function filterSelectOptions(
84
+ field: Pick<FilterFieldLike, 'options'>,
85
+ emptyLabel = 'Any',
86
+ ): FilterFieldOption[] {
87
+ return [{ value: '', label: emptyLabel }, ...((field.options ?? []) as FilterFieldOption[])]
88
+ }
89
+
90
+ export function extractLookupIds(value: unknown): string[] {
91
+ if (Array.isArray(value)) return value.filter(isNonEmptyGuid)
92
+ return isNonEmptyGuid(value) ? [value] : []
93
+ }
94
+
95
+ export function labelForResolvedLookup(
96
+ lookupStore: LookupStoreApi,
97
+ lookup: ResolvedLookupSource,
98
+ id: string,
99
+ ): string {
100
+ if (lookup.kind === 'catalog') return lookupStore.labelForCatalog(lookup.catalogType, id)
101
+ if (lookup.kind === 'coa') return lookupStore.labelForCoa(id)
102
+ return lookupStore.labelForAnyDocument(lookup.documentTypes, id)
103
+ }
104
+
105
+ export async function ensureResolvedLookupLabels(
106
+ lookupStore: LookupStoreApi,
107
+ lookup: ResolvedLookupSource,
108
+ ids: string[],
109
+ ): Promise<void> {
110
+ if (lookup.kind === 'catalog') {
111
+ await lookupStore.ensureCatalogLabels(lookup.catalogType, ids)
112
+ return
113
+ }
114
+
115
+ if (lookup.kind === 'coa') {
116
+ await lookupStore.ensureCoaLabels(ids)
117
+ return
118
+ }
119
+
120
+ await lookupStore.ensureAnyDocumentLabels(lookup.documentTypes, ids)
121
+ }
122
+
123
+ export async function searchResolvedLookupItems<TItem extends FilterLookupItem>(
124
+ lookupStore: LookupStoreApi<TItem>,
125
+ lookup: ResolvedLookupSource,
126
+ query: string,
127
+ ): Promise<TItem[]> {
128
+ if (lookup.kind === 'catalog') {
129
+ return await lookupStore.searchCatalog(
130
+ lookup.catalogType,
131
+ query,
132
+ 'filters' in lookup && lookup.filters ? { filters: lookup.filters } : undefined,
133
+ )
134
+ }
135
+
136
+ if (lookup.kind === 'coa') return await lookupStore.searchCoa(query)
137
+ return await lookupStore.searchDocuments(lookup.documentTypes, query)
138
+ }
139
+
140
+ export async function hydrateResolvedLookupItems<TItem extends FilterLookupItem>(
141
+ lookupStore: LookupStoreApi<TItem>,
142
+ lookup: ResolvedLookupSource,
143
+ ids: string[],
144
+ ): Promise<TItem[]> {
145
+ if (ids.length === 0) return []
146
+
147
+ await ensureResolvedLookupLabels(lookupStore, lookup, ids)
148
+
149
+ return ids.map((id) => ({
150
+ id,
151
+ label: labelForResolvedLookup(lookupStore, lookup, id),
152
+ }) as TItem)
153
+ }
@@ -0,0 +1,25 @@
1
+ import { resolveNgbMetadataFormBehavior } from './config'
2
+ import type { EntityFormModel, FieldMetadata, LookupHint, LookupSource, MetadataFormBehavior } from './types'
3
+
4
+ export function lookupHintFromSource(source?: LookupSource | null): LookupHint | null {
5
+ if (!source) return null
6
+
7
+ if (source.kind === 'catalog') return { kind: 'catalog', catalogType: source.catalogType }
8
+ if (source.kind === 'document') return { kind: 'document', documentTypes: [...source.documentTypes] }
9
+ return { kind: 'coa' }
10
+ }
11
+
12
+ export function resolveLookupHint(args: {
13
+ entityTypeCode: string
14
+ model: EntityFormModel
15
+ field: FieldMetadata
16
+ behavior?: MetadataFormBehavior
17
+ }): LookupHint | null {
18
+ const behavior = resolveNgbMetadataFormBehavior(args.behavior)
19
+
20
+ return behavior.resolveLookupHint?.({
21
+ entityTypeCode: args.entityTypeCode,
22
+ model: args.model,
23
+ field: args.field,
24
+ }) ?? lookupHintFromSource(args.field.lookup)
25
+ }