@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.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- 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>
|