@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,246 @@
1
+ import { computed, ref, watch, type ComputedRef } from 'vue';
2
+ import type { RouteLocationNormalizedLoaded } from 'vue-router';
3
+
4
+ import { prefetchLookupsForPage } from '../lookup/prefetch';
5
+ import { normalizeSingleQueryValue } from '../router/queryParams';
6
+ import { isGuidString } from '../utils/guid';
7
+ import { stableStringify } from '../utils/stableValue';
8
+ import { buildFilterOptionLabelsByKey, labelForResolvedLookup } from './filtering';
9
+ import { isReferenceValue } from './entityModel';
10
+ import { buildMetadataRegisterColumns, buildMetadataRegisterRows } from './register';
11
+ import type {
12
+ ColumnMetadata,
13
+ JsonValue,
14
+ ListFilterField,
15
+ LookupHint,
16
+ LookupSource,
17
+ LookupStoreApi,
18
+ RecordFields,
19
+ } from './types';
20
+
21
+ export type MetadataRegisterPageItem = {
22
+ id: string;
23
+ payload?: {
24
+ fields?: RecordFields | null;
25
+ } | null;
26
+ isDeleted?: boolean;
27
+ isMarkedForDeletion?: boolean;
28
+ status?: unknown;
29
+ };
30
+
31
+ export type MetadataRegisterPageResponse<TItem extends MetadataRegisterPageItem = MetadataRegisterPageItem> = {
32
+ items: readonly TItem[];
33
+ total?: number | null;
34
+ };
35
+
36
+ export type MetadataRegisterPageMetadata<TField extends ListFilterField = ListFilterField> = {
37
+ displayName: string;
38
+ list?: {
39
+ columns: readonly ColumnMetadata[];
40
+ filters?: readonly TField[] | null;
41
+ } | null;
42
+ };
43
+
44
+ export type MetadataRegisterCellArgs<TItem extends MetadataRegisterPageItem = MetadataRegisterPageItem> = {
45
+ column: ColumnMetadata;
46
+ item: TItem;
47
+ rawValue: JsonValue | undefined;
48
+ defaultValue: unknown;
49
+ entityTypeCode: string;
50
+ };
51
+
52
+ export type UseMetadataPageReloadKeyArgs = {
53
+ route: RouteLocationNormalizedLoaded;
54
+ entityTypeCode: ComputedRef<string>;
55
+ ignoreQueryKey?: (key: string) => boolean;
56
+ };
57
+
58
+ export type UseMetadataRegisterPageDataArgs<
59
+ TField extends ListFilterField = ListFilterField,
60
+ TMeta extends MetadataRegisterPageMetadata<TField> = MetadataRegisterPageMetadata<TField>,
61
+ TItem extends MetadataRegisterPageItem = MetadataRegisterPageItem,
62
+ TPage extends MetadataRegisterPageResponse<TItem> = MetadataRegisterPageResponse<TItem>,
63
+ > = {
64
+ route: RouteLocationNormalizedLoaded;
65
+ entityTypeCode: ComputedRef<string>;
66
+ reloadKey: ComputedRef<string>;
67
+ loadMetadata: (entityTypeCode: string) => Promise<TMeta>;
68
+ loadPage: (args: {
69
+ entityTypeCode: string;
70
+ metadata: TMeta;
71
+ }) => Promise<TPage>;
72
+ lookupStore?: LookupStoreApi;
73
+ resolveLookupHint?: (args: {
74
+ entityTypeCode: string;
75
+ fieldKey: string;
76
+ lookup?: LookupSource | null;
77
+ }) => LookupHint | null;
78
+ mapFieldValue?: (args: MetadataRegisterCellArgs<TItem>) => unknown;
79
+ formatError?: (cause: unknown) => string;
80
+ };
81
+
82
+ function defaultErrorMessage(cause: unknown): string {
83
+ return cause instanceof Error ? cause.message : String(cause);
84
+ }
85
+
86
+ export function useMetadataPageReloadKey(args: UseMetadataPageReloadKeyArgs) {
87
+ return computed(() =>
88
+ stableStringify({
89
+ path: args.route.path,
90
+ entityTypeCode: args.entityTypeCode.value,
91
+ query: Object.entries(args.route.query)
92
+ .filter(([key]) => !args.ignoreQueryKey?.(key))
93
+ .map(([key, value]) => [key, normalizeSingleQueryValue(value)] as const)
94
+ .sort(([left], [right]) => left.localeCompare(right)),
95
+ }),
96
+ );
97
+ }
98
+
99
+ export function useMetadataRegisterPageData<
100
+ TField extends ListFilterField = ListFilterField,
101
+ TMeta extends MetadataRegisterPageMetadata<TField> = MetadataRegisterPageMetadata<TField>,
102
+ TItem extends MetadataRegisterPageItem = MetadataRegisterPageItem,
103
+ TPage extends MetadataRegisterPageResponse<TItem> = MetadataRegisterPageResponse<TItem>,
104
+ >(
105
+ args: UseMetadataRegisterPageDataArgs<TField, TMeta, TItem, TPage>,
106
+ ) {
107
+ const loading = ref(false);
108
+ const error = ref<string | null>(null);
109
+ const metadata = ref<TMeta | null>(null);
110
+ const page = ref<TPage | null>(null);
111
+ const loadSeq = ref(0);
112
+
113
+ const listFilters = computed<readonly TField[]>(() => metadata.value?.list?.filters ?? []);
114
+ const hasListFilters = computed(() => listFilters.value.length > 0);
115
+ const optionLabelsByColumnKey = computed(() => ({
116
+ ...buildFilterOptionLabelsByKey(metadata.value?.list?.columns ?? []),
117
+ ...buildFilterOptionLabelsByKey(listFilters.value),
118
+ }));
119
+
120
+ function resolveDefaultFieldValue(column: ColumnMetadata, rawValue: JsonValue | undefined): unknown {
121
+ if (isReferenceValue(rawValue)) return rawValue;
122
+ if (!args.lookupStore || !args.resolveLookupHint || !isGuidString(rawValue)) return rawValue;
123
+
124
+ const hint = args.resolveLookupHint({
125
+ entityTypeCode: args.entityTypeCode.value,
126
+ fieldKey: column.key,
127
+ lookup: column.lookup,
128
+ });
129
+ if (!hint) return rawValue;
130
+
131
+ return {
132
+ id: rawValue,
133
+ display: labelForResolvedLookup(args.lookupStore, hint, rawValue),
134
+ };
135
+ }
136
+
137
+ const columns = computed(() =>
138
+ buildMetadataRegisterColumns({
139
+ columns: metadata.value?.list?.columns ?? [],
140
+ optionLabelsByColumnKey: optionLabelsByColumnKey.value,
141
+ }),
142
+ );
143
+
144
+ const rows = computed(() =>
145
+ buildMetadataRegisterRows({
146
+ items: page.value?.items ?? [],
147
+ columns: metadata.value?.list?.columns ?? [],
148
+ mapFieldValue: (column, rawValue, item) => {
149
+ const defaultValue = resolveDefaultFieldValue(column, rawValue);
150
+ return args.mapFieldValue?.({
151
+ column,
152
+ item,
153
+ rawValue,
154
+ defaultValue,
155
+ entityTypeCode: args.entityTypeCode.value,
156
+ }) ?? defaultValue;
157
+ },
158
+ }),
159
+ );
160
+
161
+ async function prefetchReferenceLabels(): Promise<void> {
162
+ if (!args.lookupStore || !args.resolveLookupHint) return;
163
+
164
+ const entityTypeCode = String(args.entityTypeCode.value ?? '').trim();
165
+ const columns = metadata.value?.list?.columns ?? [];
166
+ const items = page.value?.items ?? [];
167
+ if (!entityTypeCode || columns.length === 0 || items.length === 0) return;
168
+
169
+ await prefetchLookupsForPage({
170
+ entityTypeCode,
171
+ columns,
172
+ items,
173
+ lookupStore: args.lookupStore,
174
+ resolveLookupHint: (nextEntityTypeCode, fieldKey, lookup) =>
175
+ args.resolveLookupHint?.({
176
+ entityTypeCode: nextEntityTypeCode,
177
+ fieldKey,
178
+ lookup,
179
+ }) ?? null,
180
+ });
181
+ }
182
+
183
+ async function load(): Promise<boolean> {
184
+ const entityTypeCode = String(args.entityTypeCode.value ?? '').trim();
185
+ const seq = ++loadSeq.value;
186
+
187
+ if (!entityTypeCode) {
188
+ if (seq === loadSeq.value) {
189
+ loading.value = false;
190
+ error.value = null;
191
+ metadata.value = null;
192
+ page.value = null;
193
+ }
194
+ return false;
195
+ }
196
+
197
+ loading.value = true;
198
+ error.value = null;
199
+
200
+ try {
201
+ const nextMetadata = await args.loadMetadata(entityTypeCode);
202
+ if (seq !== loadSeq.value) return false;
203
+ metadata.value = nextMetadata;
204
+
205
+ const nextPage = await args.loadPage({
206
+ entityTypeCode,
207
+ metadata: nextMetadata,
208
+ });
209
+ if (seq !== loadSeq.value) return false;
210
+ page.value = nextPage;
211
+ return true;
212
+ } catch (cause) {
213
+ if (seq !== loadSeq.value) return false;
214
+ error.value = args.formatError?.(cause) ?? defaultErrorMessage(cause);
215
+ return false;
216
+ } finally {
217
+ if (seq === loadSeq.value) loading.value = false;
218
+ }
219
+ }
220
+
221
+ watch(args.reloadKey, () => {
222
+ void load();
223
+ }, { immediate: true });
224
+
225
+ watch(
226
+ () => [args.entityTypeCode.value, metadata.value?.list?.columns, page.value?.items],
227
+ () => {
228
+ void prefetchReferenceLabels();
229
+ },
230
+ { immediate: true, deep: true },
231
+ );
232
+
233
+ return {
234
+ loading,
235
+ error,
236
+ metadata,
237
+ page,
238
+ listFilters,
239
+ hasListFilters,
240
+ optionLabelsByColumnKey,
241
+ columns,
242
+ rows,
243
+ load,
244
+ prefetchReferenceLabels,
245
+ };
246
+ }
@@ -0,0 +1,49 @@
1
+ import type { Ref } from 'vue'
2
+
3
+ type UseValidationFocusOptions = {
4
+ attribute: string
5
+ }
6
+
7
+ function focusableElement(container: HTMLElement | null): HTMLElement | null {
8
+ if (!container) return null
9
+ return (
10
+ container.querySelector<HTMLElement>('input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])') ??
11
+ null
12
+ )
13
+ }
14
+
15
+ export function useValidationFocus(
16
+ rootRef: Ref<HTMLElement | null>,
17
+ options: UseValidationFocusOptions,
18
+ ) {
19
+ function containerFor(key: string): HTMLElement | null {
20
+ const root = rootRef.value
21
+ if (!root) return null
22
+
23
+ const selector = `[${options.attribute}]`
24
+ const nodes = Array.from(root.querySelectorAll<HTMLElement>(selector))
25
+ return nodes.find((node) => node.getAttribute(options.attribute) === key) ?? null
26
+ }
27
+
28
+ function scrollTo(key: string): boolean {
29
+ const container = containerFor(key)
30
+ if (!container) return false
31
+ container.scrollIntoView({ block: 'center', behavior: 'smooth' })
32
+ return true
33
+ }
34
+
35
+ function focus(key: string): boolean {
36
+ const container = containerFor(key)
37
+ if (!container) return false
38
+ container.scrollIntoView({ block: 'center', behavior: 'smooth' })
39
+ const target = focusableElement(container)
40
+ target?.focus({ preventScroll: true } as FocusOptions)
41
+ return true
42
+ }
43
+
44
+ return {
45
+ containerFor,
46
+ scrollTo,
47
+ focus,
48
+ }
49
+ }
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <span :class="classes">
3
+ <slot />
4
+ </span>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { computed } from 'vue'
9
+
10
+ type BadgeTone = 'neutral' | 'dev' | 'success' | 'warn' | 'danger'
11
+
12
+ const props = withDefaults(defineProps<{
13
+ tone?: BadgeTone
14
+ }>(), {
15
+ tone: 'neutral',
16
+ })
17
+
18
+ const classes = computed(() => {
19
+ const base = 'inline-flex shrink-0 whitespace-nowrap items-center rounded-full border px-2 py-0.5 text-[11px] font-semibold tracking-[.02em]'
20
+
21
+ const tone = {
22
+ neutral: 'border-ngb-border bg-ngb-neutral-subtle text-ngb-text',
23
+ dev: 'border-ngb-border bg-ngb-card text-ngb-text',
24
+ success: 'border-ngb-success-border bg-ngb-success-subtle text-ngb-success',
25
+ warn: 'border-ngb-warn-border bg-ngb-warn-subtle text-ngb-warn',
26
+ danger: 'border-ngb-danger-border bg-ngb-danger-subtle text-ngb-danger',
27
+ }[props.tone] ?? 'border-ngb-border bg-ngb-neutral-subtle text-ngb-text'
28
+
29
+ return [base, tone].join(' ')
30
+ })
31
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <button :type="type" :disabled="disabled || loading" :class="classes">
3
+ <span v-if="loading" class="ngb-spinner" aria-hidden="true"></span>
4
+ <slot />
5
+ </button>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { computed } from 'vue'
10
+
11
+ type ButtonVariant = 'secondary' | 'primary' | 'ghost' | 'danger'
12
+ type ButtonSize = 'sm' | 'md' | 'lg'
13
+ type NativeButtonType = 'button' | 'submit' | 'reset'
14
+
15
+ const props = withDefaults(defineProps<{
16
+ variant?: ButtonVariant
17
+ size?: ButtonSize
18
+ disabled?: boolean
19
+ loading?: boolean
20
+ type?: NativeButtonType
21
+ }>(), {
22
+ variant: 'secondary',
23
+ size: 'md',
24
+ disabled: false,
25
+ loading: false,
26
+ type: 'button',
27
+ })
28
+
29
+ const classes = computed(() => {
30
+ const base =
31
+ 'inline-flex items-center justify-center gap-2 rounded-[var(--ngb-radius)] border text-sm font-medium select-none ' +
32
+ 'transition-shadow transition-colors shadow-none ngb-focus';
33
+
34
+ const sizes = {
35
+ sm: 'h-8 px-3',
36
+ md: 'h-9 px-3.5',
37
+ lg: 'h-10 px-4',
38
+ }[props.size] ?? 'h-9 px-3.5'
39
+
40
+ const common = (props.disabled || props.loading)
41
+ ? 'opacity-60 cursor-not-allowed'
42
+ : 'cursor-pointer';
43
+
44
+ const variants = {
45
+ secondary: 'bg-ngb-card border-ngb-border text-ngb-text hover:bg-ngb-bg hover:border-[rgba(11,60,93,.35)] hover:shadow-[0_2px_10px_rgba(0,0,0,.06)]',
46
+ primary: 'bg-ngb-primary border-ngb-primary text-white hover:bg-ngb-primary-hover hover:border-ngb-primary-hover hover:shadow-[0_8px_22px_rgba(0,0,0,.10)]',
47
+ ghost: 'bg-transparent border-transparent text-ngb-text hover:bg-ngb-bg hover:border-ngb-border',
48
+ danger: 'bg-ngb-card border-ngb-border text-ngb-danger hover:bg-ngb-bg hover:border-[rgba(155,28,28,.35)] hover:shadow-[0_2px_10px_rgba(0,0,0,.06)]',
49
+ }[props.variant] ?? 'bg-ngb-card border-ngb-border text-ngb-text'
50
+
51
+ return [base, sizes, common, variants].join(' ')
52
+ })
53
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <label class="inline-flex items-center gap-2 select-none" :class="disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'">
3
+ <input
4
+ class="sr-only"
5
+ type="checkbox"
6
+ :checked="modelValue"
7
+ :disabled="disabled"
8
+ @change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
9
+ />
10
+
11
+ <span
12
+ class="h-4 w-4 border border-ngb-border bg-ngb-card flex items-center justify-center"
13
+ :class="modelValue ? 'border-[rgba(11,60,93,.55)] bg-[rgba(11,60,93,.06)] dark:border-[rgba(147,197,253,.55)] dark:bg-[rgba(147,197,253,.10)]' : ''"
14
+ aria-hidden="true"
15
+ >
16
+ <span v-if="modelValue" class="text-[color:var(--ngb-logo)] text-[12px] leading-none">✓</span>
17
+ </span>
18
+
19
+ <span v-if="label" class="text-sm text-ngb-text">{{ label }}</span>
20
+ </label>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ defineProps<{
25
+ modelValue: boolean;
26
+ label?: string;
27
+ disabled?: boolean;
28
+ }>();
29
+
30
+ defineEmits<{
31
+ (e: 'update:modelValue', v: boolean): void;
32
+ }>();
33
+ </script>
@@ -0,0 +1,141 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import NgbPickerPopover from './NgbPickerPopover.vue'
4
+ import NgbPickerNavButton from './NgbPickerNavButton.vue'
5
+ import { parseDateOnlyValue, toDateOnlyValue } from '../utils/dateValues'
6
+
7
+ const props = defineProps<{
8
+ modelValue: string | null | undefined
9
+ placeholder?: string
10
+ disabled?: boolean
11
+ readonly?: boolean
12
+ grouped?: boolean
13
+ }>()
14
+
15
+ const emit = defineEmits<{
16
+ (e: 'update:modelValue', v: string | null): void
17
+ }>()
18
+
19
+ function toDisplay(v: string | null | undefined): string {
20
+ if (!v) return ''
21
+ const dt = parseDateOnlyValue(v)
22
+ if (!dt) return String(v)
23
+ const mm = String(dt.getMonth() + 1).padStart(2, '0')
24
+ const dd = String(dt.getDate()).padStart(2, '0')
25
+ const yy = String(dt.getFullYear())
26
+ return `${mm}/${dd}/${yy}`
27
+ }
28
+
29
+ const selected = computed(() => parseDateOnlyValue(props.modelValue ?? null))
30
+
31
+ const today = () => {
32
+ const n = new Date()
33
+ return new Date(n.getFullYear(), n.getMonth(), n.getDate())
34
+ }
35
+
36
+ const viewYear = ref<number>(selected.value?.getFullYear() ?? today().getFullYear())
37
+ const viewMonth = ref<number>(selected.value?.getMonth() ?? today().getMonth())
38
+
39
+ watch(
40
+ () => props.modelValue,
41
+ (v) => {
42
+ const dt = parseDateOnlyValue(v ?? null)
43
+ if (!dt) return
44
+ viewYear.value = dt.getFullYear()
45
+ viewMonth.value = dt.getMonth()
46
+ }
47
+ )
48
+
49
+ const monthLabel = computed(() => {
50
+ const dt = new Date(viewYear.value, viewMonth.value, 1)
51
+ return dt.toLocaleString(undefined, { month: 'long', year: 'numeric' })
52
+ })
53
+
54
+ const grid = computed(() => {
55
+ const first = new Date(viewYear.value, viewMonth.value, 1)
56
+ const firstDow = first.getDay()
57
+ const daysInMonth = new Date(viewYear.value, viewMonth.value + 1, 0).getDate()
58
+
59
+ const cells: Array<{ dt: Date | null; label: string }> = []
60
+ for (let i = 0; i < firstDow; i++) cells.push({ dt: null, label: '' })
61
+ for (let d = 1; d <= daysInMonth; d++) cells.push({ dt: new Date(viewYear.value, viewMonth.value, d), label: String(d) })
62
+ while (cells.length % 7 !== 0) cells.push({ dt: null, label: '' })
63
+ return cells
64
+ })
65
+
66
+ const isSameDay = (a: Date, b: Date) =>
67
+ a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
68
+
69
+ function prevMonth() {
70
+ const dt = new Date(viewYear.value, viewMonth.value - 1, 1)
71
+ viewYear.value = dt.getFullYear()
72
+ viewMonth.value = dt.getMonth()
73
+ }
74
+
75
+ function nextMonth() {
76
+ const dt = new Date(viewYear.value, viewMonth.value + 1, 1)
77
+ viewYear.value = dt.getFullYear()
78
+ viewMonth.value = dt.getMonth()
79
+ }
80
+
81
+ function pick(dt: Date, close: () => void) {
82
+ emit('update:modelValue', toDateOnlyValue(dt))
83
+ close()
84
+ }
85
+
86
+ function clear(close: () => void) {
87
+ emit('update:modelValue', null)
88
+ close()
89
+ }
90
+
91
+ function setToday(close: () => void) {
92
+ emit('update:modelValue', toDateOnlyValue(today()))
93
+ close()
94
+ }
95
+
96
+ const displayValue = computed(() => toDisplay(props.modelValue ?? null))
97
+ </script>
98
+
99
+ <template>
100
+ <NgbPickerPopover
101
+ :display-value="displayValue"
102
+ :placeholder="placeholder ?? 'mm/dd/yyyy'"
103
+ :disabled="disabled"
104
+ :readonly="readonly"
105
+ :grouped="grouped"
106
+ >
107
+ <template #header>
108
+ <NgbPickerNavButton direction="prev" @click="prevMonth" />
109
+ <div class="text-sm font-semibold text-ngb-text">{{ monthLabel }}</div>
110
+ <NgbPickerNavButton direction="next" @click="nextMonth" />
111
+ </template>
112
+
113
+ <template #default="{ close }">
114
+ <div class="grid grid-cols-7 gap-1 text-center text-xs text-ngb-muted">
115
+ <div>Su</div><div>Mo</div><div>Tu</div><div>We</div><div>Th</div><div>Fr</div><div>Sa</div>
116
+ </div>
117
+
118
+ <div class="mt-2 grid grid-cols-7 gap-1">
119
+ <button
120
+ v-for="(c, i) in grid"
121
+ :key="i"
122
+ type="button"
123
+ class="h-9 rounded-md text-sm"
124
+ :class="[
125
+ !c.dt ? 'pointer-events-none opacity-0' : '',
126
+ c.dt && selected && isSameDay(c.dt, selected) ? 'bg-ngb-primary/20 text-ngb-text' : 'text-ngb-text hover:bg-ngb-muted/10',
127
+ c.dt && isSameDay(c.dt, today()) ? 'ring-1 ring-ngb-primary/40' : ''
128
+ ]"
129
+ @click="c.dt && pick(c.dt, close)"
130
+ >
131
+ {{ c.label }}
132
+ </button>
133
+ </div>
134
+ </template>
135
+
136
+ <template #footer="{ close }">
137
+ <button type="button" class="text-ngb-primary hover:underline" @click="clear(close)">Clear</button>
138
+ <button type="button" class="text-ngb-primary hover:underline" @click="setToday(close)">Today</button>
139
+ </template>
140
+ </NgbPickerPopover>
141
+ </template>