@antelopejs/dms 0.7.8 → 0.7.10
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/CHANGELOG.md +45 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/api-base-url.d.ts +1 -0
- package/dist/implementations/dms/api-base-url.js +6 -0
- package/dist/implementations/dms/api-base-url.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.js +7 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/page.js +8 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +25 -0
- package/dist/test/attachment-host/capabilities.js +53 -1
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +13 -0
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/api-base-url.test.d.ts +1 -0
- package/dist/test/unit/api-base-url.test.js +31 -0
- package/dist/test/unit/api-base-url.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
- package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
- package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
- package/frontend-vue/tests/block-not-found.test.ts +220 -0
- package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
- package/frontend-vue/tests/component-id.test.ts +18 -0
- package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
- package/frontend-vue/tests/guard.test.ts +242 -0
- package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
- package/frontend-vue/tests/live-toggle.test.ts +87 -1
- package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
- package/frontend-vue/tests/page-live-parity.test.ts +367 -0
- package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
- package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
- package/frontend-vue/tests/period-selector-url.test.ts +84 -6
- package/frontend-vue/tests/period-url-query.test.ts +52 -0
- package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
- package/frontend-vue/tests/row-side-panel.test.ts +109 -10
- package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
- package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
- package/frontend-vue/tests/table-density-menu.test.ts +4 -3
- package/frontend-vue/tests/table-live-hold.test.ts +95 -5
- package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
- package/frontend-vue/tests/table-row-hover.test.ts +32 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
- package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
- package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
- package/frontend-vue/tests/table-view-views.test.ts +56 -15
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -3
- package/skills/dms-module/SKILL.md +5 -0
- package/skills/dms-pages/REFERENCE.md +23 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { getCurrentInstance, onMounted, type Ref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
type CompactTableEntry,
|
|
4
|
+
decodeServerField,
|
|
5
|
+
encodeServerField,
|
|
6
|
+
isDefaultValue,
|
|
7
|
+
isServerTableField,
|
|
8
|
+
type LocalTableField,
|
|
9
|
+
readLocalTableEntry,
|
|
10
|
+
SERVER_TABLE_FIELDS,
|
|
11
|
+
type ServerTableField,
|
|
12
|
+
type TableField,
|
|
13
|
+
TABLES_PREFERENCE_KEY,
|
|
14
|
+
tablePreferenceId,
|
|
15
|
+
writeLocalTableEntry,
|
|
16
|
+
} from "#dms-core/app/build/composables/user/tablePreferences";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The preferences of one table view. A field the server render needs
|
|
20
|
+
* (`ServerTableField`) is read in setup from the cookie, on the server too; a
|
|
21
|
+
* field only the browser draws (`LocalTableField`) reads as its default in
|
|
22
|
+
* setup, on both sides, and is restored from `localStorage` once mounted.
|
|
23
|
+
*/
|
|
24
|
+
export interface TablePreferences {
|
|
25
|
+
/** A server field's stored value, else `defaultValue`; a local field's default. */
|
|
26
|
+
get: <T>(field: TableField, defaultValue: T) => T;
|
|
27
|
+
/** Stores `value`, or forgets the field when `value` is its default. */
|
|
28
|
+
set: <T>(field: TableField, value: T, defaultValue: T) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Sets `state` to the local field's stored value once the table is
|
|
31
|
+
* mounted, unless `isValid` refuses it: the server and the hydrating
|
|
32
|
+
* client render the default, so no mismatch.
|
|
33
|
+
*/
|
|
34
|
+
restore: <T>(
|
|
35
|
+
field: LocalTableField,
|
|
36
|
+
state: Ref<T>,
|
|
37
|
+
isValid?: (value: T) => boolean,
|
|
38
|
+
) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PendingRestore {
|
|
42
|
+
field: LocalTableField;
|
|
43
|
+
apply: (value: unknown) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The table's entry in the cookie: `tables.<id>`, its server fields. */
|
|
47
|
+
function useServerTableEntry(id: string) {
|
|
48
|
+
const { getPreference, setPreference, removePreference } = usePreferences();
|
|
49
|
+
const path = `${TABLES_PREFERENCE_KEY}.${id}`;
|
|
50
|
+
|
|
51
|
+
const entry = (): CompactTableEntry | undefined =>
|
|
52
|
+
getPreference<CompactTableEntry | undefined>(path, undefined);
|
|
53
|
+
const tables = () =>
|
|
54
|
+
getPreference<Record<string, unknown>>(TABLES_PREFERENCE_KEY, {});
|
|
55
|
+
|
|
56
|
+
// The last entry takes the collection with it: no empty branch left over.
|
|
57
|
+
const forget = () => {
|
|
58
|
+
const isLastEntry = Object.keys(tables()).every((key) => key === id);
|
|
59
|
+
removePreference(isLastEntry ? TABLES_PREFERENCE_KEY : path);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const set = (
|
|
63
|
+
field: ServerTableField,
|
|
64
|
+
value: unknown,
|
|
65
|
+
defaultValue: unknown,
|
|
66
|
+
) => {
|
|
67
|
+
const { key } = SERVER_TABLE_FIELDS[field];
|
|
68
|
+
const next = { ...entry() };
|
|
69
|
+
const encoded = encodeServerField(field, value, defaultValue);
|
|
70
|
+
if (JSON.stringify(next[key]) === JSON.stringify(encoded)) return;
|
|
71
|
+
if (encoded === undefined) Reflect.deleteProperty(next, key);
|
|
72
|
+
else next[key] = encoded;
|
|
73
|
+
if (Object.keys(next).length === 0) forget();
|
|
74
|
+
else setPreference(path, next);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// The table used last is evicted last: opening it counts as a write.
|
|
78
|
+
const markUsed = () => {
|
|
79
|
+
const current = entry();
|
|
80
|
+
if (current && Object.keys(tables()).at(-1) !== id) {
|
|
81
|
+
setPreference(path, current);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return { entry, set, markUsed };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const setLocalField = (
|
|
89
|
+
id: string,
|
|
90
|
+
field: LocalTableField,
|
|
91
|
+
value: unknown,
|
|
92
|
+
defaultValue: unknown,
|
|
93
|
+
) => {
|
|
94
|
+
const entry = readLocalTableEntry(id);
|
|
95
|
+
if (isDefaultValue(value, defaultValue)) {
|
|
96
|
+
Reflect.deleteProperty(entry, field);
|
|
97
|
+
} else {
|
|
98
|
+
entry[field] = JSON.parse(JSON.stringify(value));
|
|
99
|
+
}
|
|
100
|
+
writeLocalTableEntry(id, entry);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const restoreLocalFields = (id: string, pending: PendingRestore[]) => {
|
|
104
|
+
const stored = readLocalTableEntry(id);
|
|
105
|
+
pending
|
|
106
|
+
.filter(({ field }) => stored[field] !== undefined)
|
|
107
|
+
.forEach(({ field, apply }) => apply(stored[field]));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The preferences of the table view `componentId` on page `pageId`, under a
|
|
112
|
+
* short hash of both: the cookie entry `tables.<id>`, and the
|
|
113
|
+
* `localStorage` key `dms-table-preferences:v1:<id>`.
|
|
114
|
+
*/
|
|
115
|
+
export function useTablePreferences(
|
|
116
|
+
componentId: string | undefined,
|
|
117
|
+
pageId: string | undefined,
|
|
118
|
+
): TablePreferences {
|
|
119
|
+
const id = tablePreferenceId(componentId ?? "", pageId ?? "");
|
|
120
|
+
const server = useServerTableEntry(id);
|
|
121
|
+
const pending: PendingRestore[] = [];
|
|
122
|
+
|
|
123
|
+
if (getCurrentInstance()) {
|
|
124
|
+
onMounted(() => {
|
|
125
|
+
restoreLocalFields(id, pending);
|
|
126
|
+
server.markUsed();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
get: (field, defaultValue) =>
|
|
132
|
+
isServerTableField(field)
|
|
133
|
+
? decodeServerField(field, server.entry(), defaultValue)
|
|
134
|
+
: defaultValue,
|
|
135
|
+
set: (field, value, defaultValue) =>
|
|
136
|
+
isServerTableField(field)
|
|
137
|
+
? server.set(field, value, defaultValue)
|
|
138
|
+
: setLocalField(id, field, value, defaultValue),
|
|
139
|
+
restore: (field, state, isValid) => {
|
|
140
|
+
pending.push({
|
|
141
|
+
field,
|
|
142
|
+
apply: (value) => {
|
|
143
|
+
const restored = value as typeof state.value;
|
|
144
|
+
if (!isValid || isValid(restored)) state.value = restored;
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type ReplaceUrlVariablesContext,
|
|
10
10
|
resolveUrlVariables,
|
|
11
11
|
} from "../../utils/urlVariables";
|
|
12
|
+
import type { RowActionRule } from "#dms-core/app/types/row-action";
|
|
13
|
+
import { pinRows } from "./utils/pinnedRows";
|
|
12
14
|
import {
|
|
13
15
|
appendPeriodToUrl,
|
|
14
16
|
usePeriodScope,
|
|
@@ -34,6 +36,11 @@ export interface TableRowsOptions {
|
|
|
34
36
|
* read again on every request.
|
|
35
37
|
*/
|
|
36
38
|
route?: () => ReplaceUrlVariablesContext;
|
|
39
|
+
/**
|
|
40
|
+
* The rows listed first whatever the sort (backend `pinnedRows`), read on
|
|
41
|
+
* every request: `undefined` while the display pins nothing.
|
|
42
|
+
*/
|
|
43
|
+
pinnedRows?: () => RowActionRule | undefined;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
const FILTER_PREFIX = "filter_";
|
|
@@ -94,13 +101,15 @@ const compareValues = (a: unknown, b: unknown): number => {
|
|
|
94
101
|
/**
|
|
95
102
|
* The rows the browser narrows, sorts and pages itself, for what the source
|
|
96
103
|
* route left to it: a search over the listed columns' text, the sort on one
|
|
97
|
-
* column, the page.
|
|
104
|
+
* column, the page. The pinned rows lead before the page is cut, so they
|
|
105
|
+
* open the first one.
|
|
98
106
|
*/
|
|
99
107
|
export function processSourceRows<T extends Record<string, unknown>>(
|
|
100
108
|
rows: T[],
|
|
101
109
|
query: Query,
|
|
102
110
|
capabilities: SourceCapabilities,
|
|
103
111
|
columns: TableViewColumn[],
|
|
112
|
+
pinnedRows?: RowActionRule,
|
|
104
113
|
): TableViewListResponse<T> {
|
|
105
114
|
let processed = rows;
|
|
106
115
|
const search = typeof query.search === "string" ? query.search.trim() : "";
|
|
@@ -120,6 +129,7 @@ export function processSourceRows<T extends Record<string, unknown>>(
|
|
|
120
129
|
(a, b) => direction * compareValues(get(a, sortKey), get(b, sortKey)),
|
|
121
130
|
);
|
|
122
131
|
}
|
|
132
|
+
processed = pinRows(processed, pinnedRows);
|
|
123
133
|
const offset = Number(query.offset) || 0;
|
|
124
134
|
const limit = Number(query.limit) || processed.length;
|
|
125
135
|
return {
|
|
@@ -151,7 +161,8 @@ function emptySourcePage<T>(query: Query): TableViewListResponse<T> {
|
|
|
151
161
|
* source's `periodScope` adds the period of its selector to the URL, like a
|
|
152
162
|
* chart's; until that selector registers, nothing is requested and the list
|
|
153
163
|
* answers `null`, which the table draws as its skeleton. `sourceUrl` is the
|
|
154
|
-
* URL requested, for the caller to list again when it changes.
|
|
164
|
+
* URL requested, for the caller to list again when it changes. The rows
|
|
165
|
+
* `pinnedRows` accepts lead each list answered.
|
|
155
166
|
*/
|
|
156
167
|
export function useTableRows<T extends Record<string, unknown>>(
|
|
157
168
|
options: TableRowsOptions,
|
|
@@ -189,13 +200,26 @@ export function useTableRows<T extends Record<string, unknown>>(
|
|
|
189
200
|
limit: Number(query.limit) || 0,
|
|
190
201
|
};
|
|
191
202
|
}
|
|
192
|
-
return processSourceRows(
|
|
203
|
+
return processSourceRows(
|
|
204
|
+
response.results,
|
|
205
|
+
query,
|
|
206
|
+
capabilities,
|
|
207
|
+
columns,
|
|
208
|
+
options.pinnedRows?.(),
|
|
209
|
+
);
|
|
193
210
|
};
|
|
194
211
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
const withPinnedRows = (
|
|
213
|
+
page: TableViewListResponse<T> | null,
|
|
214
|
+
): TableViewListResponse<T> | null =>
|
|
215
|
+
page && { ...page, results: pinRows(page.results, options.pinnedRows?.()) };
|
|
216
|
+
|
|
217
|
+
const list = async (query: Query): Promise<TableViewListResponse<T> | null> =>
|
|
218
|
+
withPinnedRows(
|
|
219
|
+
source
|
|
220
|
+
? await listFromSource(source, query)
|
|
221
|
+
: await api<TableViewListResponse<T>>(`${location}/list`, { query }),
|
|
222
|
+
);
|
|
199
223
|
|
|
200
224
|
// A source answers its totals one list at a time.
|
|
201
225
|
const countBatch = async (
|
|
@@ -242,5 +266,12 @@ export function useTableRows<T extends Record<string, unknown>>(
|
|
|
242
266
|
? getSourceRow(source, id)
|
|
243
267
|
: api<T>(`${location}/get`, { query: { id } });
|
|
244
268
|
|
|
245
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
isSource: !!source,
|
|
271
|
+
sourceUrl,
|
|
272
|
+
isPeriodPending,
|
|
273
|
+
list,
|
|
274
|
+
countBatch,
|
|
275
|
+
getRow,
|
|
276
|
+
};
|
|
246
277
|
}
|
|
@@ -122,6 +122,7 @@ export const useTableViewConfig = <T extends Data>(
|
|
|
122
122
|
caption: processI18n(config.caption ?? ""),
|
|
123
123
|
maxHeight: config.maxHeight,
|
|
124
124
|
rowIdKey: config.rowIdKey,
|
|
125
|
+
labelKey: config.labelKey,
|
|
125
126
|
rowActions: config.rowActions,
|
|
126
127
|
rowClick: config.rowClick,
|
|
127
128
|
customNavItems: config.customNavItems,
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -801,17 +801,21 @@ export const useTableRowActions = <T extends Data>(
|
|
|
801
801
|
await proceed();
|
|
802
802
|
};
|
|
803
803
|
|
|
804
|
-
const {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
804
|
+
const {
|
|
805
|
+
handleCustomButton,
|
|
806
|
+
handleCustomRowAction,
|
|
807
|
+
handleBulkCustomAction,
|
|
808
|
+
handleMissingRow,
|
|
809
|
+
} = useActionTargets({
|
|
810
|
+
api: config.api,
|
|
811
|
+
pageId: config.pageId,
|
|
812
|
+
componentId: config.componentId,
|
|
813
|
+
refreshCallback: config.refreshCallback,
|
|
814
|
+
handleApiError,
|
|
815
|
+
rowNavigation: config.rowNavigation,
|
|
816
|
+
recordScope: config.recordScope,
|
|
817
|
+
rowIdKey: config.rowIdKey,
|
|
818
|
+
});
|
|
815
819
|
|
|
816
820
|
return {
|
|
817
821
|
createFormContainer,
|
|
@@ -826,6 +830,7 @@ export const useTableRowActions = <T extends Data>(
|
|
|
826
830
|
handleCustomButton,
|
|
827
831
|
handleCustomRowAction,
|
|
828
832
|
handleBulkCustomAction,
|
|
833
|
+
handleMissingRow,
|
|
829
834
|
handleApiError,
|
|
830
835
|
runConfirmedBuiltIn,
|
|
831
836
|
};
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type UserTableView,
|
|
24
24
|
viewSnapshot,
|
|
25
25
|
} from "./utils/views";
|
|
26
|
+
import type { TablePreferences } from "./useTablePreferences";
|
|
26
27
|
|
|
27
28
|
/** The table's state a view reads and sets. */
|
|
28
29
|
export interface TableViewsState {
|
|
@@ -40,8 +41,8 @@ export interface TableViewsOptions {
|
|
|
40
41
|
views: TableViewViewsConfig | undefined;
|
|
41
42
|
defaults: TableStateDefaults;
|
|
42
43
|
state: TableViewsState;
|
|
43
|
-
/**
|
|
44
|
-
|
|
44
|
+
/** The table's preferences (`useTablePreferences`). */
|
|
45
|
+
preferences: TablePreferences;
|
|
45
46
|
urlScope: TableUrlScope;
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -52,8 +53,6 @@ export interface ResolvedTableView extends TableViewViewConfig {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export const VIEW_URL_KEY = "view";
|
|
55
|
-
const ACTIVE_VIEW_PREFERENCE = "activeView";
|
|
56
|
-
const USER_VIEWS_PREFERENCE = "userViews";
|
|
57
56
|
const NO_VIEW = "";
|
|
58
57
|
const VIEW_NAME_FIELD = "label";
|
|
59
58
|
|
|
@@ -64,8 +63,7 @@ const VIEW_NAME_FIELD = "label";
|
|
|
64
63
|
* a change made to a view is not kept when another is opened.
|
|
65
64
|
*/
|
|
66
65
|
export function useTableViews(options: TableViewsOptions) {
|
|
67
|
-
const { views, defaults, state,
|
|
68
|
-
const { getPreference, setPreference } = usePreferences();
|
|
66
|
+
const { views, defaults, state, preferences, urlScope } = options;
|
|
69
67
|
const { user } = useUserSession();
|
|
70
68
|
const { t } = useI18n();
|
|
71
69
|
const { processI18n } = useTranslation();
|
|
@@ -74,9 +72,9 @@ export function useTableViews(options: TableViewsOptions) {
|
|
|
74
72
|
const route = useDmsRoute();
|
|
75
73
|
|
|
76
74
|
const userId = computed(() => user.value?._id as string | undefined);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
75
|
+
// Kept in the browser: none on the server, restored once mounted.
|
|
76
|
+
const storedUserViews = ref<UserTableView[]>([]);
|
|
77
|
+
preferences.restore("userViews", storedUserViews, Array.isArray);
|
|
80
78
|
const ownUserViews = computed(() =>
|
|
81
79
|
views?.userViews && userId.value
|
|
82
80
|
? storedUserViews.value.filter((view) => view.ownerId === userId.value)
|
|
@@ -132,25 +130,50 @@ export function useTableViews(options: TableViewsOptions) {
|
|
|
132
130
|
|
|
133
131
|
// Read once, on arrival: a view named by the URL wins over the state kept
|
|
134
132
|
// from the last visit; a view the caller may not open falls back to the
|
|
135
|
-
// default one, with a word (on the client, once mounted).
|
|
133
|
+
// default one, with a word (on the client, once mounted). The user's views
|
|
134
|
+
// are kept in the browser, so a view that is not the module's is resolved
|
|
135
|
+
// once mounted.
|
|
136
136
|
const urlView = readTableUrlKey(route.query, urlScope, VIEW_URL_KEY);
|
|
137
|
-
const persistedViewId =
|
|
138
|
-
|
|
137
|
+
const persistedViewId = preferences.get<string | undefined>(
|
|
138
|
+
"activeView",
|
|
139
139
|
undefined,
|
|
140
140
|
);
|
|
141
141
|
let isUrlViewUnavailable = false;
|
|
142
|
+
let isUrlViewPending = false;
|
|
143
|
+
let isOpenedOnArrival = false;
|
|
142
144
|
if (urlView.value && findView(urlView.value)) {
|
|
143
145
|
openView(urlView.value);
|
|
146
|
+
isOpenedOnArrival = true;
|
|
144
147
|
} else if (urlView.value) {
|
|
145
|
-
|
|
146
|
-
openView(views?.defaultView);
|
|
148
|
+
isUrlViewPending = true;
|
|
147
149
|
} else if (persistedViewId === undefined) {
|
|
148
150
|
if (views?.defaultView) openView(views.defaultView);
|
|
151
|
+
isOpenedOnArrival = !!views?.defaultView;
|
|
149
152
|
} else {
|
|
150
153
|
activeViewId.value = findView(persistedViewId)?.id;
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
// The browser-only state (order, density) is restored over the view opened
|
|
157
|
+
// in setup: the view's own wins, as it did when everything was read there.
|
|
158
|
+
const reapplyBrowserState = () => {
|
|
159
|
+
const snapshot = viewSnapshot(activeView.value, defaults);
|
|
160
|
+
state.columnOrder.value = snapshot.order;
|
|
161
|
+
state.density.value = snapshot.density;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const arriveOnceMounted = () => {
|
|
165
|
+
if (isUrlViewPending) {
|
|
166
|
+
isUrlViewUnavailable = !findView(urlView.value);
|
|
167
|
+
openView(isUrlViewUnavailable ? views?.defaultView : urlView.value);
|
|
168
|
+
} else if (isOpenedOnArrival) {
|
|
169
|
+
reapplyBrowserState();
|
|
170
|
+
} else if (persistedViewId && !activeViewId.value) {
|
|
171
|
+
activeViewId.value = findView(persistedViewId)?.id;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
153
175
|
onMounted(() => {
|
|
176
|
+
arriveOnceMounted();
|
|
154
177
|
if (urlView.ignoredShortKey && import.meta.env.DEV) {
|
|
155
178
|
console.warn(
|
|
156
179
|
`[DMS] "?${VIEW_URL_KEY}=" is ignored on a page with several table views: name the table, "?${urlScope.tableId}.${VIEW_URL_KEY}=".`,
|
|
@@ -166,7 +189,7 @@ export function useTableViews(options: TableViewsOptions) {
|
|
|
166
189
|
});
|
|
167
190
|
|
|
168
191
|
watch(activeViewId, (id) => {
|
|
169
|
-
|
|
192
|
+
preferences.set<string | undefined>("activeView", id ?? NO_VIEW, undefined);
|
|
170
193
|
});
|
|
171
194
|
|
|
172
195
|
const isModified = computed(() => {
|
|
@@ -181,7 +204,7 @@ export function useTableViews(options: TableViewsOptions) {
|
|
|
181
204
|
|
|
182
205
|
const writeUserViews = (next: UserTableView[]) => {
|
|
183
206
|
storedUserViews.value = next;
|
|
184
|
-
|
|
207
|
+
preferences.set("userViews", next, []);
|
|
185
208
|
};
|
|
186
209
|
|
|
187
210
|
/** Back to the open view's own state. */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RowActionRule } from "#dms-core/app/types/row-action";
|
|
2
|
+
import { evaluateRowActionRule } from "#dms-core/app/utils/row-action-rule-evaluator";
|
|
3
|
+
|
|
4
|
+
interface PartitionedRows<T> {
|
|
5
|
+
pinned: T[];
|
|
6
|
+
rest: T[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The rows a table view's `pinnedRows` accepts first, then the others, each
|
|
11
|
+
* in the order they came in: the sort keeps ordering both, it no longer
|
|
12
|
+
* decides which rows lead.
|
|
13
|
+
*/
|
|
14
|
+
export function pinRows<T extends Record<string, unknown>>(
|
|
15
|
+
rows: T[],
|
|
16
|
+
pinnedRows: RowActionRule | undefined,
|
|
17
|
+
): T[] {
|
|
18
|
+
if (!pinnedRows) return rows;
|
|
19
|
+
const { pinned, rest } = rows.reduce<PartitionedRows<T>>(
|
|
20
|
+
(parts, row) => {
|
|
21
|
+
(evaluateRowActionRule(pinnedRows, row) ? parts.pinned : parts.rest).push(
|
|
22
|
+
row,
|
|
23
|
+
);
|
|
24
|
+
return parts;
|
|
25
|
+
},
|
|
26
|
+
{ pinned: [], rest: [] },
|
|
27
|
+
);
|
|
28
|
+
return pinned.length === 0 ? rows : [...pinned, ...rest];
|
|
29
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CustomRowAction } from "../../../../types/row-action";
|
|
2
2
|
import { readTableUrlKey, type TableUrlScope } from "./views";
|
|
3
|
+
import { readFetchNotFound } from "../../../utils/fetchNotFound";
|
|
3
4
|
|
|
4
5
|
/** Query key of the row a `deepLink` action has open. */
|
|
5
6
|
export const RECORD_URL_KEY = "record";
|
|
@@ -76,16 +77,28 @@ export interface DeepLinkedRowSource<T> {
|
|
|
76
77
|
getRow: (id: string) => Promise<T | undefined>;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/** What a deep link found: its row, or a route answering it is gone. */
|
|
81
|
+
export interface DeepLinkedRow<T> {
|
|
82
|
+
row?: T;
|
|
83
|
+
/** The route answered 404: the row the link names no longer exists. */
|
|
84
|
+
isMissing: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
/**
|
|
80
88
|
* The row a deep link names: the listed one, else the one read by id — a row
|
|
81
|
-
* beyond the first page. A
|
|
82
|
-
* nothing.
|
|
89
|
+
* beyond the first page. A route answering 404 marks it missing; any other
|
|
90
|
+
* failure (a refusal, the network) or no route at all finds nothing.
|
|
83
91
|
*/
|
|
84
92
|
export async function resolveDeepLinkedRow<T>(
|
|
85
93
|
id: string,
|
|
86
94
|
source: DeepLinkedRowSource<T>,
|
|
87
|
-
): Promise<T
|
|
95
|
+
): Promise<DeepLinkedRow<T>> {
|
|
88
96
|
const listed = source.listed?.find((row) => source.idOf(row) === id);
|
|
89
|
-
if (listed) return listed;
|
|
90
|
-
|
|
97
|
+
if (listed) return { row: listed, isMissing: false };
|
|
98
|
+
try {
|
|
99
|
+
const row = await source.getRow(id);
|
|
100
|
+
return { row: row ?? undefined, isMissing: false };
|
|
101
|
+
} catch (error: unknown) {
|
|
102
|
+
return { isMissing: readFetchNotFound(error) !== null };
|
|
103
|
+
}
|
|
91
104
|
}
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts
CHANGED
|
@@ -54,6 +54,23 @@ export const sanitizeSorting = (
|
|
|
54
54
|
return [{ id: first.id, desc: first.desc === true }];
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The sort the list route can serve ({@link sanitizeSorting}), or the
|
|
59
|
+
* default sort when that leaves none: a sort the user cleared, or one naming
|
|
60
|
+
* a column gone, goes back to the table's default rather than to the
|
|
61
|
+
* database's natural order — and is remembered so.
|
|
62
|
+
*/
|
|
63
|
+
export const sortingOrDefault = (
|
|
64
|
+
sorting: SortingState | null | undefined,
|
|
65
|
+
sortable: ReadonlySet<string>,
|
|
66
|
+
defaultSorting: SortingState,
|
|
67
|
+
): SortingState => {
|
|
68
|
+
const sanitized = sanitizeSorting(sorting, sortable);
|
|
69
|
+
return sanitized.length > 0
|
|
70
|
+
? sanitized
|
|
71
|
+
: sanitizeSorting(defaultSorting, sortable);
|
|
72
|
+
};
|
|
73
|
+
|
|
57
74
|
/** Whether two sorting states request the same order. */
|
|
58
75
|
export const isSameSorting = (a: SortingState, b: SortingState): boolean =>
|
|
59
76
|
a.length === b.length &&
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HTTP_NOT_FOUND } from "../../../../dms-core/app/utils/http-status";
|
|
2
|
+
import type { I18nTranslate } from "#dms-core/app/composables/translation/useTranslation";
|
|
3
|
+
import type { BlockAction } from "../components/blocks/BlockActions.vue";
|
|
4
|
+
import { userFacingText } from "../composables/confirm/actionError";
|
|
5
|
+
import type { Tone } from "../../types/tone";
|
|
6
|
+
|
|
7
|
+
/** Translation key of the not-found title, shared by blocks and guards. */
|
|
8
|
+
export const NOT_FOUND_TITLE_KEY = "dms.blocks.not_found";
|
|
9
|
+
/** Translation key of the not-found hint, when the 404 says nothing usable. */
|
|
10
|
+
export const NOT_FOUND_HINT_KEY = "dms.blocks.not_found_hint";
|
|
11
|
+
|
|
12
|
+
/** What the body of a 404 may lay over a not-found state. */
|
|
13
|
+
export interface NotFoundEmptyState {
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
tone?: Tone;
|
|
18
|
+
actions?: BlockAction[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What a data source answering 404 says about it, read from the error body:
|
|
23
|
+
* `{ message?, emptyState? }`, both optional.
|
|
24
|
+
*/
|
|
25
|
+
export interface FetchNotFound {
|
|
26
|
+
/** The body's `message`: an i18n key or a sentence for the user. */
|
|
27
|
+
message?: string;
|
|
28
|
+
/** Laid over the not-found state drawn (title, icon, actions…). */
|
|
29
|
+
emptyState?: NotFoundEmptyState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FetchFailure {
|
|
33
|
+
statusCode?: number;
|
|
34
|
+
status?: number;
|
|
35
|
+
response?: { status?: number; _data?: unknown };
|
|
36
|
+
data?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
40
|
+
typeof value === "object" && value !== null;
|
|
41
|
+
|
|
42
|
+
const statusOf = (error: FetchFailure): number | undefined =>
|
|
43
|
+
error.statusCode ?? error.status ?? error.response?.status;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The not-found answer a failed request carries, or `null` when it failed any
|
|
47
|
+
* other way (network, refusal, crash): a 404 is the server's final word that
|
|
48
|
+
* what is read does not exist, not a load error to retry.
|
|
49
|
+
*/
|
|
50
|
+
export function readFetchNotFound(error: unknown): FetchNotFound | null {
|
|
51
|
+
if (!isObject(error)) return null;
|
|
52
|
+
const failure = error as FetchFailure;
|
|
53
|
+
if (statusOf(failure) !== HTTP_NOT_FOUND) return null;
|
|
54
|
+
const data = failure.data ?? failure.response?._data;
|
|
55
|
+
const body = isObject(data) ? data : {};
|
|
56
|
+
return {
|
|
57
|
+
message: typeof body.message === "string" ? body.message : undefined,
|
|
58
|
+
emptyState: isObject(body.emptyState)
|
|
59
|
+
? (body.emptyState as NotFoundEmptyState)
|
|
60
|
+
: undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The default texts of a not-found state, before the 404's `emptyState` is
|
|
66
|
+
* laid over them: a translated "Not found", and the body's `message` when it
|
|
67
|
+
* is user-facing, else a translated hint. A block and a `Guard` draw the same.
|
|
68
|
+
*/
|
|
69
|
+
export function notFoundTexts(
|
|
70
|
+
notFound: FetchNotFound | null | undefined,
|
|
71
|
+
translate: I18nTranslate,
|
|
72
|
+
): { title: string; description: string } {
|
|
73
|
+
return {
|
|
74
|
+
title: translate(NOT_FOUND_TITLE_KEY, {}),
|
|
75
|
+
description:
|
|
76
|
+
userFacingText(notFound?.message, translate) ??
|
|
77
|
+
translate(NOT_FOUND_HINT_KEY, {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// a specific occurrence, `{{params.id:1}}` being the id of the page carrying the
|
|
4
4
|
// table view (see extractRouteParams).
|
|
5
5
|
const PARAM_TOKEN = /\{\{params\.(\w+(?::\d+)?)\}\}/g;
|
|
6
|
-
|
|
6
|
+
// A query key may carry dots and dashes: a period selector writes `<id>.period`.
|
|
7
|
+
const QUERY_TOKEN = /\{\{query\.([\w.-]+)\}\}/g;
|
|
7
8
|
const RESPONSE_TOKEN = /\{\{response\.(\w+)\}\}/g;
|
|
8
9
|
const TOKEN_OPENING = "{{";
|
|
9
10
|
|
|
@@ -118,3 +119,47 @@ export function resolveTextVariables(
|
|
|
118
119
|
const resolved = fillRouteTokens(text, context, TEXT_ENCODING);
|
|
119
120
|
return hasUrlVariables(resolved) ? undefined : resolved;
|
|
120
121
|
}
|
|
122
|
+
|
|
123
|
+
// A route token left without a value: `{{params.id}}`, `{{query.workspace}}`.
|
|
124
|
+
const UNRESOLVED_ROUTE_TOKEN = /\{\{(?:params|query)\.\w+(?::\d+)?\}\}/g;
|
|
125
|
+
const UNRESOLVED_ROUTE_VALUE = /^\{\{(?:params|query)\.\w+(?::\d+)?\}\}$/;
|
|
126
|
+
const QUERY_SEPARATOR = "?";
|
|
127
|
+
const FRAGMENT_SEPARATOR = "#";
|
|
128
|
+
const PAIR_SEPARATOR = "&";
|
|
129
|
+
const VALUE_SEPARATOR = "=";
|
|
130
|
+
|
|
131
|
+
/** The query of a URL without the pairs whose whole value is a token left empty. */
|
|
132
|
+
function dropUnresolvedPairs(search: string): string {
|
|
133
|
+
return search
|
|
134
|
+
.split(PAIR_SEPARATOR)
|
|
135
|
+
.filter((pair) => {
|
|
136
|
+
const value = pair.slice(pair.indexOf(VALUE_SEPARATOR) + 1);
|
|
137
|
+
return (
|
|
138
|
+
!pair.includes(VALUE_SEPARATOR) || !UNRESOLVED_ROUTE_VALUE.test(value)
|
|
139
|
+
);
|
|
140
|
+
})
|
|
141
|
+
.join(PAIR_SEPARATOR);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A link's URL with the `{{params.X}}` / `{{query.X}}` tokens of the page it
|
|
146
|
+
* sits on filled, for a button that always leads somewhere: a query pair
|
|
147
|
+
* whose value is a token the page has no value for is left out
|
|
148
|
+
* (`?workspace={{query.workspace}}` on a page without `?workspace=` leads to
|
|
149
|
+
* the target's default), any other such token is left empty.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveLinkVariables(
|
|
152
|
+
url: string,
|
|
153
|
+
context: ReplaceUrlVariablesContext,
|
|
154
|
+
): string {
|
|
155
|
+
const filled = replaceUrlVariables(url, context);
|
|
156
|
+
if (!hasUrlVariables(filled)) return filled;
|
|
157
|
+
const [beforeFragment = "", ...fragment] = filled.split(FRAGMENT_SEPARATOR);
|
|
158
|
+
const [path = "", ...search] = beforeFragment.split(QUERY_SEPARATOR);
|
|
159
|
+
const query = dropUnresolvedPairs(search.join(QUERY_SEPARATOR));
|
|
160
|
+
const rebuilt = [
|
|
161
|
+
query ? `${path}${QUERY_SEPARATOR}${query}` : path,
|
|
162
|
+
...fragment,
|
|
163
|
+
].join(FRAGMENT_SEPARATOR);
|
|
164
|
+
return rebuilt.replace(UNRESOLVED_ROUTE_TOKEN, "");
|
|
165
|
+
}
|