@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,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
|
|
@@ -113,6 +113,8 @@ export interface TableRowActionOptions {
|
|
|
113
113
|
copyLink?: boolean | RowActionConfig;
|
|
114
114
|
hasSelection?: boolean;
|
|
115
115
|
custom?: CustomRowAction[];
|
|
116
|
+
/** Name of a row's ⋯ menu button, its `{field}` braces filled from the row. */
|
|
117
|
+
menuLabel?: string;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
export interface NavItem {
|
|
@@ -149,6 +151,8 @@ export interface TableProps<T> {
|
|
|
149
151
|
caption?: string;
|
|
150
152
|
|
|
151
153
|
rowIdKey?: string;
|
|
154
|
+
/** Field naming a row: the default name of its ⋯ menu button. */
|
|
155
|
+
labelKey?: string;
|
|
152
156
|
rowActions?: TableRowActionOptions;
|
|
153
157
|
/**
|
|
154
158
|
* What runs a row's click action: a double-click (the default) or a single
|
|
@@ -203,6 +207,8 @@ export interface TableProps<T> {
|
|
|
203
207
|
chrome?: ResolvedTableChrome;
|
|
204
208
|
/** Placeholder of the search field. */
|
|
205
209
|
searchPlaceholder?: string;
|
|
210
|
+
/** Key opening the search (`defineShortcuts` notation); `shift_f` else. */
|
|
211
|
+
searchShortcut?: string;
|
|
206
212
|
/**
|
|
207
213
|
* One-click dropdown filters of the toolbar, writing the column filters
|
|
208
214
|
* (the `columnFilters` model).
|
|
@@ -342,6 +348,10 @@ export interface TableSharedData<T> {
|
|
|
342
348
|
* included) and the toolbar search — and goes back to the first page.
|
|
343
349
|
*/
|
|
344
350
|
resetFilters: () => void;
|
|
351
|
+
/** Empties the toolbar search alone (the toolbar folds it). */
|
|
352
|
+
clearSearch: () => void;
|
|
353
|
+
/** Clears the filter chips alone, the search kept. */
|
|
354
|
+
clearFilters: () => void;
|
|
345
355
|
/** What `resetFilters` would clear (nothing: no clear action to offer). */
|
|
346
356
|
clearableFilters: ComputedRef<ClearableTableFilters>;
|
|
347
357
|
/** A search, a filter chip or a hidden filter narrows the rows. */
|
|
@@ -409,7 +419,9 @@ import TableTabs from "./Tabs.vue";
|
|
|
409
419
|
import { createTableViewDeleteShortcut } from "../../../composables/table-view/shortcuts/tableViewDelete";
|
|
410
420
|
import { FULL_TABLE_CHROME } from "../../composables/table-view/utils/chrome";
|
|
411
421
|
import {
|
|
422
|
+
clearTableColumnFilters,
|
|
412
423
|
clearTableFilters,
|
|
424
|
+
clearTableSearch,
|
|
413
425
|
clearableTableFilters,
|
|
414
426
|
isTableNarrowed,
|
|
415
427
|
type TableNarrowingState,
|
|
@@ -444,11 +456,14 @@ const ROW_SELECTED_BG = "data-[selected=true]:bg-primary/10";
|
|
|
444
456
|
const ROW_SELECTED_CELL_BG =
|
|
445
457
|
"group-data-[selected=true]:bg-[color-mix(in_srgb,var(--ui-primary)_10%,var(--dms-surface-card))]";
|
|
446
458
|
|
|
447
|
-
// The row a side panel has open: the
|
|
448
|
-
// its leading edge
|
|
459
|
+
// The row a side panel has open: the accent tint of a selected row and a 2px
|
|
460
|
+
// accent bar on its leading edge, the "current item" of the design system
|
|
461
|
+
// (a list row, a settings role). The bar is drawn by the first cell: a
|
|
462
|
+
// table row's own box shadow is not painted under its cells' backgrounds.
|
|
449
463
|
const ROW_OPEN_BG =
|
|
450
|
-
"data-[open=true]:bg-
|
|
451
|
-
const ROW_OPEN_CELL_BG =
|
|
464
|
+
"data-[open=true]:bg-primary/10 data-[open=true]:[&>td:first-child]:shadow-[inset_2px_0_0_0_var(--ui-primary)]";
|
|
465
|
+
const ROW_OPEN_CELL_BG =
|
|
466
|
+
"group-data-[open=true]:bg-[color-mix(in_srgb,var(--ui-primary)_10%,var(--dms-surface-card))]";
|
|
452
467
|
|
|
453
468
|
// An expanded row keeps the hover tint and hands its bottom rule to the detail
|
|
454
469
|
// band below it.
|
|
@@ -1079,6 +1094,13 @@ const resetFilters = () => {
|
|
|
1079
1094
|
clearTableFilters(narrowingState);
|
|
1080
1095
|
void filtersCleared.trigger();
|
|
1081
1096
|
};
|
|
1097
|
+
// Clearing the search folds the toolbar's field, as a full clear does; the
|
|
1098
|
+
// filters alone leave it as it is.
|
|
1099
|
+
const clearSearch = () => {
|
|
1100
|
+
clearTableSearch(narrowingState);
|
|
1101
|
+
void filtersCleared.trigger();
|
|
1102
|
+
};
|
|
1103
|
+
const clearFilters = () => clearTableColumnFilters(narrowingState);
|
|
1082
1104
|
const clearableFilters = computed(() => clearableTableFilters(narrowingState));
|
|
1083
1105
|
const isFiltered = computed(() => isTableNarrowed(narrowingState));
|
|
1084
1106
|
|
|
@@ -1162,6 +1184,8 @@ watchEffect(() => {
|
|
|
1162
1184
|
hasCustomColumns,
|
|
1163
1185
|
deleteFilter,
|
|
1164
1186
|
resetFilters,
|
|
1187
|
+
clearSearch,
|
|
1188
|
+
clearFilters,
|
|
1165
1189
|
clearableFilters,
|
|
1166
1190
|
isFiltered,
|
|
1167
1191
|
onFiltersCleared: filtersCleared.on,
|
|
@@ -1305,6 +1329,7 @@ defineShortcuts({
|
|
|
1305
1329
|
:on-custom-button="onCustomButton"
|
|
1306
1330
|
:chrome="resolvedChrome"
|
|
1307
1331
|
:search-placeholder="searchPlaceholder"
|
|
1332
|
+
:search-shortcut="searchShortcut"
|
|
1308
1333
|
:quick-filters="quickFilters"
|
|
1309
1334
|
:class="{
|
|
1310
1335
|
'ms-auto': !resolvedChrome.caption && !tabsInline,
|
|
@@ -91,8 +91,13 @@ interface SidePanelTargetRun extends TargetRunExtras {
|
|
|
91
91
|
title: string;
|
|
92
92
|
containerId: string;
|
|
93
93
|
rowData?: Data;
|
|
94
|
+
/** Id of a row the link names but the route answered 404 for. */
|
|
95
|
+
missingId?: string;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
const NOT_FOUND_TITLE_KEY = "side_panel.not_found.title";
|
|
99
|
+
const NOT_FOUND_DESCRIPTION_KEY = "side_panel.not_found.description";
|
|
100
|
+
|
|
96
101
|
/** A drawer or modal to open, and on what. */
|
|
97
102
|
interface ComponentTargetRun extends TargetRunExtras {
|
|
98
103
|
target: ActionTarget & { type: "modal" | "drawer" };
|
|
@@ -246,21 +251,39 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
246
251
|
return id === undefined || id === null ? undefined : String(id);
|
|
247
252
|
};
|
|
248
253
|
|
|
249
|
-
// A deep-linked action keeps the row it has open in the URL;
|
|
250
|
-
// removes it, but only the row this run wrote, on the page it
|
|
251
|
-
// closing as the user navigates leaves the next page's
|
|
252
|
-
|
|
254
|
+
// A deep-linked action keeps the row it has open in the URL, by id;
|
|
255
|
+
// `undefined` removes it, but only the row this run wrote, on the page it
|
|
256
|
+
// wrote it: closing as the user navigates leaves the next page's
|
|
257
|
+
// `?record=` alone.
|
|
258
|
+
const recordRecorder = (deepLink?: boolean) => {
|
|
253
259
|
let written: WrittenRecord | undefined;
|
|
254
|
-
return (
|
|
260
|
+
return (id?: string): void => {
|
|
255
261
|
if (!deepLink || !config.recordScope || !config.rowNavigation) return;
|
|
256
|
-
if (!
|
|
262
|
+
if (!id) {
|
|
257
263
|
clearWrittenRecord(written);
|
|
258
264
|
written = undefined;
|
|
259
265
|
return;
|
|
260
266
|
}
|
|
261
|
-
written = writeRecordId(config.recordScope,
|
|
267
|
+
written = writeRecordId(config.recordScope, id);
|
|
262
268
|
};
|
|
263
269
|
};
|
|
270
|
+
const rowRecorder = (deepLink?: boolean) => {
|
|
271
|
+
const recordId = recordRecorder(deepLink);
|
|
272
|
+
return (row?: Data): void => recordId(rowIdOf(row));
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// The header names the open row when the target words it from its fields.
|
|
276
|
+
const rowTitleOf = (target: SidePanelTarget, row?: Data) =>
|
|
277
|
+
row && target.rowTitle
|
|
278
|
+
? resolveConfirmText(target.rowTitle, row, t as ConfirmTranslate)
|
|
279
|
+
: undefined;
|
|
280
|
+
|
|
281
|
+
const notFoundOf = (target: SidePanelTarget) => ({
|
|
282
|
+
title: processI18n(target.notFound?.title || `$${NOT_FOUND_TITLE_KEY}`),
|
|
283
|
+
description: processI18n(
|
|
284
|
+
target.notFound?.description || `$${NOT_FOUND_DESCRIPTION_KEY}`,
|
|
285
|
+
),
|
|
286
|
+
});
|
|
264
287
|
|
|
265
288
|
const ownerKey = rowSidePanelOwnerKey(config.pageId, config.componentId);
|
|
266
289
|
|
|
@@ -274,36 +297,40 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
274
297
|
const componentName = run.target.component?.componentName;
|
|
275
298
|
if (!componentName) return;
|
|
276
299
|
const session = Symbol(componentName);
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
config.
|
|
300
|
+
const recordId = recordRecorder(run.deepLink && !run.selection);
|
|
301
|
+
const panel = {
|
|
302
|
+
session,
|
|
303
|
+
ownerKey,
|
|
304
|
+
title: run.title,
|
|
305
|
+
positionLabel: run.target.positionLabel,
|
|
306
|
+
component: resolveTargetComponent(componentName),
|
|
307
|
+
componentOptions: {
|
|
308
|
+
...run.target.component?.options,
|
|
309
|
+
pageId: config.pageId,
|
|
310
|
+
componentId: config.componentId,
|
|
311
|
+
containerId: run.containerId,
|
|
312
|
+
selection: run.selection,
|
|
313
|
+
onSuccessCallback: () => {
|
|
314
|
+
closeRowSidePanel(session);
|
|
315
|
+
config.refreshCallback?.();
|
|
316
|
+
},
|
|
288
317
|
},
|
|
318
|
+
onClose: () => recordId(undefined),
|
|
289
319
|
};
|
|
290
320
|
const showRow = (row?: Data) => {
|
|
291
321
|
openRowSidePanel({
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
title: run.title,
|
|
295
|
-
positionLabel: run.target.positionLabel,
|
|
296
|
-
component,
|
|
297
|
-
componentOptions,
|
|
322
|
+
...panel,
|
|
323
|
+
rowTitle: rowTitleOf(run.target, row),
|
|
298
324
|
row,
|
|
299
325
|
rowKey: rowIdOf(row),
|
|
300
326
|
source: row ? config.rowNavigation : undefined,
|
|
301
327
|
showRow,
|
|
302
|
-
onClose: () => recordRow(undefined),
|
|
303
328
|
});
|
|
304
|
-
|
|
329
|
+
recordId(rowIdOf(row));
|
|
305
330
|
};
|
|
306
|
-
showRow(run.rowData);
|
|
331
|
+
if (!run.missingId) return showRow(run.rowData);
|
|
332
|
+
openRowSidePanel({ ...panel, notFound: notFoundOf(run.target), showRow });
|
|
333
|
+
recordId(run.missingId);
|
|
307
334
|
};
|
|
308
335
|
|
|
309
336
|
// A container opened on a listed row steps through the rows shown (its
|
|
@@ -543,5 +570,26 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
543
570
|
);
|
|
544
571
|
};
|
|
545
572
|
|
|
546
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Opens a side panel action on a row its link names but its route answered
|
|
575
|
+
* 404 for: the panel says the row is gone, and closing it removes the link.
|
|
576
|
+
* Another target opens nothing: no row, no drawer.
|
|
577
|
+
*/
|
|
578
|
+
const handleMissingRow = (action: CustomRowAction, id: string) => {
|
|
579
|
+
if (action.target.type !== "sidePanel") return;
|
|
580
|
+
handleSidePanelTarget({
|
|
581
|
+
target: action.target,
|
|
582
|
+
title: processI18n(action.target.title || action.label),
|
|
583
|
+
containerId: `${config.pageId}-${config.componentId}-${action.target.type}`,
|
|
584
|
+
deepLink: isDeepLinkAction(action),
|
|
585
|
+
missingId: id,
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
handleCustomButton,
|
|
591
|
+
handleCustomRowAction,
|
|
592
|
+
handleBulkCustomAction,
|
|
593
|
+
handleMissingRow,
|
|
594
|
+
};
|
|
547
595
|
}
|
|
@@ -3,10 +3,16 @@ import { useChartFetch } from "../../../composables/chart/useChartFetch";
|
|
|
3
3
|
import { useComponentEvent } from "../../../../../dms-core/app/composables/components/useComponentEvent";
|
|
4
4
|
import { useWatch } from "../../../../../dms-core/app/composables/watch/useWatch";
|
|
5
5
|
import type { WatchAction } from "../../../../../dms-core/app/types/watch";
|
|
6
|
+
import {
|
|
7
|
+
type FetchNotFound,
|
|
8
|
+
readFetchNotFound,
|
|
9
|
+
} from "../../utils/fetchNotFound";
|
|
6
10
|
|
|
7
11
|
/** What a list block's `fetchUrl` answers with. */
|
|
8
12
|
export interface BlockItemsResponse<T> {
|
|
9
13
|
items?: T[];
|
|
14
|
+
/** How many items the source holds in all (an ActivityFeed's head count). */
|
|
15
|
+
count?: number;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
export interface UseBlockItemsOptions<T> {
|
|
@@ -31,10 +37,17 @@ export interface UseBlockItemsOptions<T> {
|
|
|
31
37
|
|
|
32
38
|
export interface UseBlockItemsReturn<T> {
|
|
33
39
|
items: ComputedRef<T[]>;
|
|
40
|
+
/** The `count` the source answered with, if it answered one. */
|
|
41
|
+
count: ComputedRef<number | undefined>;
|
|
34
42
|
/** First load of a fetched list: nothing to show yet. */
|
|
35
43
|
isPending: ComputedRef<boolean>;
|
|
36
44
|
/** The first load failed; a refetch failure keeps the last items. */
|
|
37
45
|
hasError: ComputedRef<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* The source answered 404: what the block lists does not exist (its last
|
|
48
|
+
* items are dropped). `null` otherwise. Never also `hasError`.
|
|
49
|
+
*/
|
|
50
|
+
notFound: ComputedRef<FetchNotFound | null>;
|
|
38
51
|
refresh: () => Promise<void>;
|
|
39
52
|
}
|
|
40
53
|
|
|
@@ -71,12 +84,23 @@ export function useBlockItems<T>(
|
|
|
71
84
|
const items = computed<T[]>(() =>
|
|
72
85
|
isFetched ? (data.value?.items ?? []) : (options.items() ?? []),
|
|
73
86
|
);
|
|
87
|
+
const count = computed<number | undefined>(() =>
|
|
88
|
+
typeof data.value?.count === "number" ? data.value.count : undefined,
|
|
89
|
+
);
|
|
74
90
|
const isPending = computed(
|
|
75
91
|
() => isFetched && isLoading.value && data.value === null,
|
|
76
92
|
);
|
|
93
|
+
const notFound = computed(() =>
|
|
94
|
+
isFetched && !isLoading.value ? readFetchNotFound(error.value) : null,
|
|
95
|
+
);
|
|
77
96
|
const hasError = computed(
|
|
78
|
-
() =>
|
|
97
|
+
() =>
|
|
98
|
+
isFetched &&
|
|
99
|
+
!isLoading.value &&
|
|
100
|
+
data.value === null &&
|
|
101
|
+
!!error.value &&
|
|
102
|
+
!notFound.value,
|
|
79
103
|
);
|
|
80
104
|
|
|
81
|
-
return { items, isPending, hasError, refresh };
|
|
105
|
+
return { items, count, isPending, hasError, notFound, refresh };
|
|
82
106
|
}
|