@antelopejs/dms 0.7.9 → 0.7.11
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 +41 -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/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 +2 -0
- 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/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 +75 -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 +104 -98
- 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/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- 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/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/tab-slot-spacing.test.ts +8 -0
- 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-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
|
@@ -21,6 +21,17 @@ export const ROW_SIDE_PANEL_ID = "dms:row-side-panel";
|
|
|
21
21
|
/** Registered name of `RowSidePanel.vue` (the layer's `Dms` prefix applied). */
|
|
22
22
|
const ROW_SIDE_PANEL_COMPONENT = "DmsRowSidePanel";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The state the panel draws in place of the component on a row that is gone,
|
|
26
|
+
* its texts translated.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export interface RowSidePanelNotFound {
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* What the row side panel shows: the header the DMS draws, the module's
|
|
26
37
|
* component and the row it receives.
|
|
@@ -32,7 +43,12 @@ export interface RowSidePanelContent {
|
|
|
32
43
|
session: symbol;
|
|
33
44
|
/** The table (or button bar) that opened the panel; see {@link rowSidePanelOwnerKey}. */
|
|
34
45
|
ownerKey: string;
|
|
46
|
+
/** The panel's title: its accessible name, and its header without a row. */
|
|
35
47
|
title: string;
|
|
48
|
+
/** The header's title while `row` is open (the target's `rowTitle`). */
|
|
49
|
+
rowTitle?: string;
|
|
50
|
+
/** The row the link names is gone: this state replaces the component. */
|
|
51
|
+
notFound?: RowSidePanelNotFound;
|
|
36
52
|
/** i18n key (`$`-prefixed) of the position, receiving `{ index, total }`. */
|
|
37
53
|
positionLabel?: string;
|
|
38
54
|
component: Component;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { computed, shallowRef, watch, type ComputedRef } from "vue";
|
|
2
|
+
import type { LayoutHeaderAction } from "../../build/components/layout/PageHeaderActionBar.vue";
|
|
3
|
+
import type { PageLayout } from "../../types/page";
|
|
4
|
+
import {
|
|
5
|
+
isSamePageQuery,
|
|
6
|
+
layoutUrlWithQuery,
|
|
7
|
+
pageQueryValues,
|
|
8
|
+
} from "../../utils/pageQuery";
|
|
9
|
+
|
|
10
|
+
interface ServedHeaderActionsOptions {
|
|
11
|
+
/** The buttons the page layout was served with. */
|
|
12
|
+
actions: () => LayoutHeaderAction[];
|
|
13
|
+
/**
|
|
14
|
+
* The page query their availability was resolved for, set when one of them
|
|
15
|
+
* declares an `availability` (`headerActionsQuery`).
|
|
16
|
+
*/
|
|
17
|
+
servedQuery: () => Record<string, string> | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The page header's buttons for the page query of the moment. A button whose
|
|
22
|
+
* `availability` depends on the query (`?workspace=` names the record the
|
|
23
|
+
* page acts on) was resolved for the query the layout was loaded with: when
|
|
24
|
+
* the URL's query moves away from it, the header asks the page layout again
|
|
25
|
+
* for that query and takes its buttons, keeping the last ones meanwhile.
|
|
26
|
+
* Buttons without an availability never cause a request.
|
|
27
|
+
*/
|
|
28
|
+
export function useServedHeaderActions({
|
|
29
|
+
actions,
|
|
30
|
+
servedQuery,
|
|
31
|
+
}: ServedHeaderActionsOptions): ComputedRef<LayoutHeaderAction[]> {
|
|
32
|
+
const route = useDmsRoute();
|
|
33
|
+
const siteLayout = useSiteLayout();
|
|
34
|
+
const refreshed = shallowRef<LayoutHeaderAction[] | null>(null);
|
|
35
|
+
let latestRequest = 0;
|
|
36
|
+
|
|
37
|
+
const layoutUrl = () =>
|
|
38
|
+
siteLayout.findMatchingRoute(route.path)?.metadata?.layoutUrl;
|
|
39
|
+
|
|
40
|
+
async function askFor(url: string, query: Record<string, string>) {
|
|
41
|
+
const request = ++latestRequest;
|
|
42
|
+
const { $authFetch } = useAuthFetch();
|
|
43
|
+
try {
|
|
44
|
+
const layout = await $authFetch<PageLayout>(
|
|
45
|
+
layoutUrlWithQuery(url, query),
|
|
46
|
+
);
|
|
47
|
+
if (request !== latestRequest) return;
|
|
48
|
+
const options = layout.layout.options as
|
|
49
|
+
| { headerActions?: LayoutHeaderAction[] }
|
|
50
|
+
| undefined;
|
|
51
|
+
refreshed.value = options?.headerActions ?? [];
|
|
52
|
+
} catch {
|
|
53
|
+
// The buttons resolved for the previous query stay: the operation
|
|
54
|
+
// behind each one still refuses on its own.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function follow() {
|
|
59
|
+
const served = servedQuery();
|
|
60
|
+
const current = pageQueryValues(route.query as Record<string, unknown>);
|
|
61
|
+
const url = layoutUrl();
|
|
62
|
+
if (!served || !url || isSamePageQuery(served, current)) {
|
|
63
|
+
latestRequest++;
|
|
64
|
+
refreshed.value = null;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
void askFor(url, current);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!import.meta.env.SSR) {
|
|
71
|
+
watch([() => route.query, servedQuery], follow, { immediate: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return computed(() => refreshed.value ?? actions());
|
|
75
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { pageQueryValues } from "../../utils/pageQuery";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Reads the same page cache that Inertia hydrates before navigation. Missing
|
|
3
5
|
* layouts (including newly registered dev pages) use the shared deduped loader.
|
|
@@ -17,7 +19,12 @@ export async function usePageLayout() {
|
|
|
17
19
|
);
|
|
18
20
|
const load = async () => {
|
|
19
21
|
if (layoutUrl.value && !pageLayout.value) {
|
|
20
|
-
|
|
22
|
+
// Asked with the page query, for the header buttons whose availability
|
|
23
|
+
// depends on it; cached for the page whatever its query.
|
|
24
|
+
await siteLayout.loadPageLayout(
|
|
25
|
+
layoutUrl.value,
|
|
26
|
+
pageQueryValues(route.query),
|
|
27
|
+
);
|
|
21
28
|
}
|
|
22
29
|
};
|
|
23
30
|
watch([layoutUrl, () => siteLayout.pageLayouts.value], load);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { layoutUrlWithQuery } from "../../utils/pageQuery";
|
|
1
2
|
import { withoutTrailingSlash } from "ufo";
|
|
2
3
|
|
|
3
4
|
const STALE_THRESHOLD_MS = 300_000;
|
|
@@ -282,7 +283,15 @@ export const useSiteLayout = () => {
|
|
|
282
283
|
await refresh();
|
|
283
284
|
};
|
|
284
285
|
|
|
285
|
-
|
|
286
|
+
/**
|
|
287
|
+
* The layout of a page, cached by its layout URL. `query` is the page
|
|
288
|
+
* URL's, which the first request carries for the header buttons whose
|
|
289
|
+
* availability reads it (the header asks again for another query).
|
|
290
|
+
*/
|
|
291
|
+
const loadPageLayout = async (
|
|
292
|
+
slug: string,
|
|
293
|
+
query?: Record<string, string>,
|
|
294
|
+
): Promise<PageLayout> => {
|
|
286
295
|
if (pageLayouts.value[slug]) {
|
|
287
296
|
return pageLayouts.value[slug];
|
|
288
297
|
}
|
|
@@ -294,7 +303,9 @@ export const useSiteLayout = () => {
|
|
|
294
303
|
}
|
|
295
304
|
|
|
296
305
|
const { $authFetch } = useAuthFetch();
|
|
297
|
-
const request = $authFetch<PageLayout>(
|
|
306
|
+
const request = $authFetch<PageLayout>(
|
|
307
|
+
layoutUrlWithQuery(slug, query ?? {}),
|
|
308
|
+
).then((result) => {
|
|
298
309
|
if (pageLayouts.value === cache) cache[slug] = result;
|
|
299
310
|
return result;
|
|
300
311
|
});
|
|
@@ -12,6 +12,11 @@ interface Props {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
14
14
|
headerActions?: LayoutHeaderAction[];
|
|
15
|
+
/**
|
|
16
|
+
* The page query the server resolved the buttons' availability for, set
|
|
17
|
+
* when one declares an `availability` (`headerActionsQuery`).
|
|
18
|
+
*/
|
|
19
|
+
headerActionsQuery?: Record<string, string>;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -22,6 +27,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
22
27
|
title: undefined,
|
|
23
28
|
description: undefined,
|
|
24
29
|
headerActions: () => [],
|
|
30
|
+
headerActionsQuery: undefined,
|
|
25
31
|
});
|
|
26
32
|
</script>
|
|
27
33
|
|
|
@@ -33,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
33
39
|
:title="props.title"
|
|
34
40
|
:description="props.description"
|
|
35
41
|
:header-actions="props.headerActions"
|
|
42
|
+
:header-actions-query="props.headerActionsQuery"
|
|
36
43
|
>
|
|
37
44
|
<slot />
|
|
38
45
|
</DashboardPageBody>
|
|
@@ -12,6 +12,11 @@ interface Props {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
14
14
|
headerActions?: LayoutHeaderAction[];
|
|
15
|
+
/**
|
|
16
|
+
* The page query the server resolved the buttons' availability for, set
|
|
17
|
+
* when one declares an `availability` (`headerActionsQuery`).
|
|
18
|
+
*/
|
|
19
|
+
headerActionsQuery?: Record<string, string>;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -21,6 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
21
26
|
title: undefined,
|
|
22
27
|
description: undefined,
|
|
23
28
|
headerActions: () => [],
|
|
29
|
+
headerActionsQuery: undefined,
|
|
24
30
|
});
|
|
25
31
|
</script>
|
|
26
32
|
|
|
@@ -33,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
33
39
|
:title="props.title"
|
|
34
40
|
:description="props.description"
|
|
35
41
|
:header-actions="props.headerActions"
|
|
42
|
+
:header-actions-query="props.headerActionsQuery"
|
|
36
43
|
header-class="pb-5"
|
|
37
44
|
>
|
|
38
45
|
<slot />
|
|
@@ -218,10 +218,13 @@ const realtimePageId = computed(() => pagelayoutMetadata.value?.fullId ?? "");
|
|
|
218
218
|
const realtime = usePageRealtime(realtimePageId);
|
|
219
219
|
providePageRealtime(realtime);
|
|
220
220
|
// Before the blocks set up: their refetches go through the page's live gate.
|
|
221
|
-
usePageLiveHost(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
usePageLiveHost(
|
|
222
|
+
() => ({
|
|
223
|
+
pageId: pagelayoutMetadata.value?.fullId,
|
|
224
|
+
live: pagelayoutMetadata.value?.live,
|
|
225
|
+
}),
|
|
226
|
+
{ isStreamConnected: () => !!realtime.sessionId.value },
|
|
227
|
+
);
|
|
225
228
|
|
|
226
229
|
function buildPageSetupContext(
|
|
227
230
|
pageInfo: PageInfo,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The page URL's query as the server reads it for header-button availability.
|
|
2
|
+
|
|
3
|
+
/** The page query as the server reads it: one value per name, the last one. */
|
|
4
|
+
export function pageQueryValues(
|
|
5
|
+
query: Record<string, unknown> | undefined,
|
|
6
|
+
): Record<string, string> {
|
|
7
|
+
return Object.fromEntries(
|
|
8
|
+
Object.entries(query ?? {}).flatMap(([name, value]) => {
|
|
9
|
+
const last = Array.isArray(value) ? value.at(-1) : value;
|
|
10
|
+
return typeof last === "string" ? [[name, last]] : [];
|
|
11
|
+
}),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Query keys the DMS writes for the view itself, never for the record a page
|
|
16
|
+
// acts on: the row open in a side panel, the paused live mode, and a period
|
|
17
|
+
// selector's range (`<id>.period`, `<id>.compare`, `<id>.from`…).
|
|
18
|
+
const VIEW_STATE_KEYS = new Set(["record", "live"]);
|
|
19
|
+
const PERIOD_KEY = /\.(?:period|compare|from|to|compareFrom|compareTo)$/;
|
|
20
|
+
|
|
21
|
+
/** Whether a query key only records the view's own state. */
|
|
22
|
+
export function isViewStateKey(name: string): boolean {
|
|
23
|
+
return VIEW_STATE_KEYS.has(name) || PERIOD_KEY.test(name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function withoutViewState(
|
|
27
|
+
query: Record<string, string>,
|
|
28
|
+
): Record<string, string> {
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.entries(query).filter(([name]) => !isViewStateKey(name)),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether two page queries hold the same values, in any order, leaving out
|
|
36
|
+
* the keys that only record the view's state: moving through rows, pausing
|
|
37
|
+
* the live mode or changing a period never asks the layout again.
|
|
38
|
+
*/
|
|
39
|
+
export function isSamePageQuery(
|
|
40
|
+
left: Record<string, string>,
|
|
41
|
+
right: Record<string, string>,
|
|
42
|
+
): boolean {
|
|
43
|
+
const kept = withoutViewState(left);
|
|
44
|
+
const other = withoutViewState(right);
|
|
45
|
+
const names = Object.keys(kept);
|
|
46
|
+
return (
|
|
47
|
+
names.length === Object.keys(other).length &&
|
|
48
|
+
names.every((name) => kept[name] === other[name])
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The layout URL of a page, asked for the given query. */
|
|
53
|
+
export function layoutUrlWithQuery(
|
|
54
|
+
layoutUrl: string,
|
|
55
|
+
query: Record<string, string>,
|
|
56
|
+
): string {
|
|
57
|
+
const search = new URLSearchParams(query).toString();
|
|
58
|
+
return search ? `${layoutUrl}?${search}` : layoutUrl;
|
|
59
|
+
}
|
|
@@ -899,6 +899,10 @@
|
|
|
899
899
|
"position": "{index} of {total}",
|
|
900
900
|
"previous": "Previous",
|
|
901
901
|
"next": "Next",
|
|
902
|
-
"close": "Close"
|
|
902
|
+
"close": "Close",
|
|
903
|
+
"not_found": {
|
|
904
|
+
"title": "This entry is no longer available",
|
|
905
|
+
"description": "It was deleted, or the link is wrong."
|
|
906
|
+
}
|
|
903
907
|
}
|
|
904
908
|
}
|
|
@@ -903,6 +903,10 @@
|
|
|
903
903
|
"position": "{index} sur {total}",
|
|
904
904
|
"previous": "Précédent",
|
|
905
905
|
"next": "Suivant",
|
|
906
|
-
"close": "Fermer"
|
|
906
|
+
"close": "Fermer",
|
|
907
|
+
"not_found": {
|
|
908
|
+
"title": "Cette entrée n'est plus disponible",
|
|
909
|
+
"description": "Elle a été supprimée, ou le lien est erroné."
|
|
910
|
+
}
|
|
907
911
|
}
|
|
908
912
|
}
|
|
@@ -18,6 +18,8 @@ export interface ActivityFeedItem {
|
|
|
18
18
|
title: BlockText;
|
|
19
19
|
/** Dimmed details, joined by "·". */
|
|
20
20
|
meta?: BlockText[];
|
|
21
|
+
/** Third line, dimmed and mono (the entry's evidence). */
|
|
22
|
+
detail?: BlockText;
|
|
21
23
|
/**
|
|
22
24
|
* Values the `$` title and details interpolate: typed values are formatted
|
|
23
25
|
* as in a composed text; a `$`-prefixed string is translated first.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
type FetchNotFound,
|
|
5
|
+
NOT_FOUND_TITLE_KEY,
|
|
6
|
+
} from "../../utils/fetchNotFound";
|
|
7
|
+
|
|
8
|
+
// The one-line status a compact block (a KPI card, a meter) shows in place of
|
|
9
|
+
// its value: a first load that failed, with a retry, or a source that
|
|
10
|
+
// answered 404 — nothing to retry there, the value does not exist.
|
|
11
|
+
interface BlockInlineStatusProps {
|
|
12
|
+
state: "error" | "not-found";
|
|
13
|
+
/** What the 404 said; its `emptyState.title` replaces the default text. */
|
|
14
|
+
notFound?: FetchNotFound | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BlockInlineStatusEmits {
|
|
18
|
+
(e: "retry"): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<BlockInlineStatusProps>(), {
|
|
22
|
+
notFound: undefined,
|
|
23
|
+
});
|
|
24
|
+
const emit = defineEmits<BlockInlineStatusEmits>();
|
|
25
|
+
|
|
26
|
+
const NOT_FOUND_ICON = "i-ph-magnifying-glass";
|
|
27
|
+
const ERROR_ICON = "i-ph-warning-circle";
|
|
28
|
+
|
|
29
|
+
const { t } = useI18n();
|
|
30
|
+
const { processI18n } = useTranslation();
|
|
31
|
+
|
|
32
|
+
const isError = computed(() => props.state === "error");
|
|
33
|
+
const label = computed(() => {
|
|
34
|
+
if (isError.value) return t("dms.table.load_error_title");
|
|
35
|
+
const title = props.notFound?.emptyState?.title;
|
|
36
|
+
return title ? processI18n(title) : t(NOT_FOUND_TITLE_KEY);
|
|
37
|
+
});
|
|
38
|
+
const icon = computed(() =>
|
|
39
|
+
isError.value
|
|
40
|
+
? ERROR_ICON
|
|
41
|
+
: (props.notFound?.emptyState?.icon ?? NOT_FOUND_ICON),
|
|
42
|
+
);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div
|
|
47
|
+
class="flex items-center justify-between gap-2"
|
|
48
|
+
:role="isError ? 'alert' : 'status'"
|
|
49
|
+
>
|
|
50
|
+
<p
|
|
51
|
+
class="flex min-w-0 items-center gap-1.5 text-[12.5px]"
|
|
52
|
+
:class="isError ? 'text-error' : 'text-muted'"
|
|
53
|
+
>
|
|
54
|
+
<UIcon :name="icon" class="size-4 shrink-0" />
|
|
55
|
+
<span class="truncate">{{ label }}</span>
|
|
56
|
+
</p>
|
|
57
|
+
<UButton
|
|
58
|
+
v-if="isError"
|
|
59
|
+
:label="t('dms.table.load_error_retry')"
|
|
60
|
+
icon="i-ph-arrows-clockwise"
|
|
61
|
+
color="neutral"
|
|
62
|
+
variant="outline"
|
|
63
|
+
size="xs"
|
|
64
|
+
@click="emit('retry')"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import DmsEmptyState from "../../../components/empty-state/EmptyState.vue";
|
|
4
|
+
import DmsBlockActions from "./BlockActions.vue";
|
|
5
|
+
import { type FetchNotFound, notFoundTexts } from "../../utils/fetchNotFound";
|
|
3
6
|
|
|
4
7
|
/** What a block shows when it has nothing to list (interface-dms `BlockEmptyText`). */
|
|
5
8
|
export interface BlockEmptyText {
|
|
@@ -8,14 +11,22 @@ export interface BlockEmptyText {
|
|
|
8
11
|
description?: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* `empty`: nothing to show. `error`: the first load failed. `not-found`: the
|
|
16
|
+
* source answered 404, what the block shows does not exist.
|
|
17
|
+
*/
|
|
18
|
+
export type BlockStatusState = "empty" | "error" | "not-found";
|
|
19
|
+
|
|
20
|
+
// What a data-driven block shows instead of its content: an empty list, a
|
|
21
|
+
// first load that failed (with a retry), or a source that answered 404.
|
|
22
|
+
// Compact v2 .c-empty in a card, so it keeps the footprint of the block it
|
|
23
|
+
// stands in for.
|
|
14
24
|
interface BlockStatusProps {
|
|
15
|
-
|
|
16
|
-
state: "empty" | "error";
|
|
25
|
+
state: BlockStatusState;
|
|
17
26
|
/** Replaces the default texts of the empty state. */
|
|
18
27
|
empty?: BlockEmptyText;
|
|
28
|
+
/** What the 404 said, laid over the default not-found texts. */
|
|
29
|
+
notFound?: FetchNotFound | null;
|
|
19
30
|
/** Draw the card surface around it (off inside a card). */
|
|
20
31
|
card?: boolean;
|
|
21
32
|
}
|
|
@@ -26,33 +37,59 @@ interface BlockStatusEmits {
|
|
|
26
37
|
|
|
27
38
|
const props = withDefaults(defineProps<BlockStatusProps>(), {
|
|
28
39
|
empty: undefined,
|
|
40
|
+
notFound: undefined,
|
|
29
41
|
card: true,
|
|
30
42
|
});
|
|
31
43
|
const emit = defineEmits<BlockStatusEmits>();
|
|
32
44
|
|
|
45
|
+
const VARIANTS = {
|
|
46
|
+
empty: "no-data",
|
|
47
|
+
error: "error",
|
|
48
|
+
"not-found": "no-result",
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
33
51
|
const { t } = useI18n();
|
|
34
52
|
const { processI18n } = useTranslation();
|
|
53
|
+
|
|
54
|
+
const translate = (text: string | undefined) =>
|
|
55
|
+
text ? processI18n(text) : undefined;
|
|
56
|
+
|
|
57
|
+
const notFoundState = computed(() => {
|
|
58
|
+
const answered = props.notFound?.emptyState ?? {};
|
|
59
|
+
const defaults = notFoundTexts(props.notFound, t);
|
|
60
|
+
return {
|
|
61
|
+
title: translate(answered.title) ?? defaults.title,
|
|
62
|
+
description: translate(answered.description) ?? defaults.description,
|
|
63
|
+
icon: answered.icon,
|
|
64
|
+
tone: answered.tone,
|
|
65
|
+
actions: answered.actions ?? [],
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const isNotFound = computed(() => props.state === "not-found");
|
|
70
|
+
|
|
71
|
+
const title = computed(() => {
|
|
72
|
+
if (props.state === "error") return t("dms.blocks.load_error");
|
|
73
|
+
if (isNotFound.value) return notFoundState.value.title;
|
|
74
|
+
return props.empty ? processI18n(props.empty.title) : t("dms.blocks.empty");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const description = computed(() => {
|
|
78
|
+
if (props.state === "error") return t("dms.blocks.load_error_hint");
|
|
79
|
+
if (isNotFound.value) return notFoundState.value.description;
|
|
80
|
+
return translate(props.empty?.description);
|
|
81
|
+
});
|
|
35
82
|
</script>
|
|
36
83
|
|
|
37
84
|
<template>
|
|
38
85
|
<div :class="props.card && 'dms-card'">
|
|
39
86
|
<DmsEmptyState
|
|
40
87
|
size="sm"
|
|
41
|
-
:variant="props.state
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
? processI18n(props.empty.title)
|
|
47
|
-
: t('dms.blocks.empty')
|
|
48
|
-
"
|
|
49
|
-
:description="
|
|
50
|
-
props.state === 'error'
|
|
51
|
-
? t('dms.blocks.load_error_hint')
|
|
52
|
-
: props.empty?.description
|
|
53
|
-
? processI18n(props.empty.description)
|
|
54
|
-
: undefined
|
|
55
|
-
"
|
|
88
|
+
:variant="VARIANTS[props.state]"
|
|
89
|
+
:icon="isNotFound ? notFoundState.icon : undefined"
|
|
90
|
+
:tone="isNotFound ? notFoundState.tone : undefined"
|
|
91
|
+
:title="title"
|
|
92
|
+
:description="description"
|
|
56
93
|
>
|
|
57
94
|
<template v-if="props.state === 'error'" #actions>
|
|
58
95
|
<UButton
|
|
@@ -64,6 +101,9 @@ const { processI18n } = useTranslation();
|
|
|
64
101
|
@click="emit('retry')"
|
|
65
102
|
/>
|
|
66
103
|
</template>
|
|
104
|
+
<template v-else-if="isNotFound && notFoundState.actions.length" #actions>
|
|
105
|
+
<DmsBlockActions :actions="notFoundState.actions" size="sm" />
|
|
106
|
+
</template>
|
|
67
107
|
</DmsEmptyState>
|
|
68
108
|
</div>
|
|
69
109
|
</template>
|
|
@@ -9,7 +9,10 @@ import type { Data, TableSharedData } from "./Table.vue";
|
|
|
9
9
|
import { KANBAN_DISPLAY_ID } from "../../../composables/table-view/types";
|
|
10
10
|
import type { CustomButton } from "../../../composables/table-view/types/custom-button";
|
|
11
11
|
import { useTableContext } from "../../composables/table/useTableContext";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createTableViewShiftFShortcut,
|
|
14
|
+
tableSearchShortcuts,
|
|
15
|
+
} from "../../../composables/table-view/shortcuts/tableViewShiftF";
|
|
13
16
|
import {
|
|
14
17
|
FULL_TABLE_CHROME,
|
|
15
18
|
type ResolvedTableChrome,
|
|
@@ -111,6 +114,8 @@ interface TableActionsProps {
|
|
|
111
114
|
chrome?: ResolvedTableChrome;
|
|
112
115
|
/** Placeholder of the search field. */
|
|
113
116
|
searchPlaceholder?: string;
|
|
117
|
+
/** Key opening the search (`defineShortcuts` notation); `shift_f` else. */
|
|
118
|
+
searchShortcut?: string;
|
|
114
119
|
/** Dropdown filters writing their column's filter. */
|
|
115
120
|
quickFilters?: ResolvedQuickFilter[];
|
|
116
121
|
}
|
|
@@ -193,7 +198,6 @@ watch(searchAreaFocused, async (isFocused) => {
|
|
|
193
198
|
searchActive.value = false;
|
|
194
199
|
});
|
|
195
200
|
|
|
196
|
-
|
|
197
201
|
const searchFieldRef = ref<{ $el: HTMLElement }>();
|
|
198
202
|
|
|
199
203
|
const focusSearchBar = () => {
|
|
@@ -273,13 +277,16 @@ const closeSearch = () => {
|
|
|
273
277
|
searchActive.value = false;
|
|
274
278
|
};
|
|
275
279
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
280
|
+
const openSearchFromShortcut = createTableViewShiftFShortcut(
|
|
281
|
+
searchActive,
|
|
282
|
+
tableSharedData!.globalFilterState,
|
|
283
|
+
focusSearchBar,
|
|
284
|
+
);
|
|
285
|
+
defineShortcuts(
|
|
286
|
+
computed(() =>
|
|
287
|
+
tableSearchShortcuts(props.searchShortcut, openSearchFromShortcut),
|
|
281
288
|
),
|
|
282
|
-
|
|
289
|
+
);
|
|
283
290
|
|
|
284
291
|
const hasCustomFilterConfig = computed(() => {
|
|
285
292
|
const filters = tableSharedData?.columnFiltersState.value || [];
|
|
@@ -80,6 +80,18 @@ const customComponent = computed(() => {
|
|
|
80
80
|
return name ? resolveDmsComponent(name) || name : undefined;
|
|
81
81
|
});
|
|
82
82
|
const refresh = () => tableSharedData.value?.emits("refresh");
|
|
83
|
+
// A custom state clears what narrows the rows as the built-in one does; each
|
|
84
|
+
// is unset while there is nothing of it to clear.
|
|
85
|
+
const clearSearch = computed(() =>
|
|
86
|
+
clearable.value?.search
|
|
87
|
+
? () => tableSharedData.value?.clearSearch()
|
|
88
|
+
: undefined,
|
|
89
|
+
);
|
|
90
|
+
const clearFilters = computed(() =>
|
|
91
|
+
clearable.value?.filters
|
|
92
|
+
? () => tableSharedData.value?.clearFilters()
|
|
93
|
+
: undefined,
|
|
94
|
+
);
|
|
83
95
|
|
|
84
96
|
interface EmptyStateContent {
|
|
85
97
|
title: string;
|
|
@@ -221,6 +233,8 @@ const builtInActions = computed(() => {
|
|
|
221
233
|
:state="kind"
|
|
222
234
|
:search="search"
|
|
223
235
|
:refresh="refresh"
|
|
236
|
+
:clear-search="clearSearch"
|
|
237
|
+
:clear-filters="clearFilters"
|
|
224
238
|
/>
|
|
225
239
|
<DmsEmptyState
|
|
226
240
|
v-else
|