@antelopejs/dms 0.7.7 → 0.7.9
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 +39 -0
- 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/interfaces/dms-base/column-display.test.js +15 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +66 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +6 -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 +16 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +23 -3
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -1
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +134 -0
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +129 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -0
- package/frontend-vue/layers/dms-layout/app/types/page.ts +3 -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/table/Table.vue +44 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +35 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +107 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +26 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/cellHelpers.ts +24 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +58 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +30 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +107 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/formatElapsed.ts +25 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +229 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +8 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLiveToggleShortcut.ts +25 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +46 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +330 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +189 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +15 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +77 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +53 -18
- 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/utils/recordLink.ts +61 -2
- package/frontend-vue/layers/dms-ui/app/build/utils/rowClickTrigger.ts +73 -0
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +92 -0
- package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +10 -4
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +19 -0
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +99 -13
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +52 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/index.ts +8 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewRowPanel.ts +30 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +15 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/config/shortcuts-registry.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +8 -0
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +7 -2
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +44 -6
- 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 +28 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +28 -0
- package/frontend-vue/tests/chart-live-pause.test.ts +74 -0
- package/frontend-vue/tests/component-id.test.ts +18 -0
- package/frontend-vue/tests/field-row.test.ts +14 -0
- package/frontend-vue/tests/live-hold-banner.test.ts +37 -0
- package/frontend-vue/tests/live-toggle.test.ts +162 -0
- package/frontend-vue/tests/page-live-gate.test.ts +241 -0
- package/frontend-vue/tests/period-live-pause.test.ts +120 -0
- package/frontend-vue/tests/period-selector-url.test.ts +268 -0
- package/frontend-vue/tests/period-url-query.test.ts +351 -0
- package/frontend-vue/tests/row-side-panel.test.ts +653 -0
- package/frontend-vue/tests/table-cell-tones.test.ts +176 -0
- package/frontend-vue/tests/table-live-hold.test.ts +204 -0
- package/frontend-vue/tests/table-row-hover.test.ts +32 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +144 -0
- package/frontend-vue/tests/table-view-refresh.test.ts +2 -2
- package/frontend-vue/tests/table-view-sources.test.ts +40 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -2
- package/skills/dms-pages/REFERENCE.md +5 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
3
|
+
import DmsSegmented from "../segmented/Segmented.vue";
|
|
4
|
+
import { usePageLive } from "../../composables/live/usePageLive";
|
|
5
|
+
import { formatElapsed } from "../../build/composables/live/formatElapsed";
|
|
6
|
+
import { useLiveToggleShortcut } from "../../build/composables/live/useLiveToggleShortcut";
|
|
7
|
+
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
8
|
+
|
|
9
|
+
interface LiveToggleBlockProps extends DefaultComponentProps {
|
|
10
|
+
align?: "left" | "center" | "right";
|
|
11
|
+
/** Shows "updated N s ago". */
|
|
12
|
+
showUpdatedAt?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<LiveToggleBlockProps>(), {
|
|
16
|
+
align: "right",
|
|
17
|
+
showUpdatedAt: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const DIRECT = "direct";
|
|
21
|
+
const PAUSE = "pause";
|
|
22
|
+
const TICK_MS = 1000;
|
|
23
|
+
const ALIGN_CLASSES: Record<
|
|
24
|
+
NonNullable<LiveToggleBlockProps["align"]>,
|
|
25
|
+
string
|
|
26
|
+
> = {
|
|
27
|
+
left: "justify-start",
|
|
28
|
+
center: "justify-center",
|
|
29
|
+
right: "justify-end",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const { t } = useI18n();
|
|
33
|
+
const live = usePageLive();
|
|
34
|
+
|
|
35
|
+
// The state lives in the browser (its storage, its clock): the server and the
|
|
36
|
+
// hydrating render draw nothing, the mounted block draws the real state.
|
|
37
|
+
const isMounted = ref(false);
|
|
38
|
+
const now = ref(Date.now());
|
|
39
|
+
let ticker: ReturnType<typeof setInterval> | undefined;
|
|
40
|
+
|
|
41
|
+
onMounted(() => {
|
|
42
|
+
isMounted.value = true;
|
|
43
|
+
if (props.showUpdatedAt) {
|
|
44
|
+
ticker = setInterval(() => (now.value = Date.now()), TICK_MS);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
onBeforeUnmount(() => clearInterval(ticker));
|
|
48
|
+
|
|
49
|
+
const mode = computed({
|
|
50
|
+
get: () => (live.isPaused.value ? PAUSE : DIRECT),
|
|
51
|
+
set: (next: string | number) => {
|
|
52
|
+
if (next === PAUSE) live.pause();
|
|
53
|
+
else live.resume();
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const items = computed(() => [
|
|
58
|
+
{ value: DIRECT, label: t("dms.live.direct"), icon: "i-ph-broadcast" },
|
|
59
|
+
{ value: PAUSE, label: t("dms.live.pause"), icon: "i-ph-pause" },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const updatedLabel = computed(() => {
|
|
63
|
+
const at = live.lastUpdatedAt.value;
|
|
64
|
+
if (at === null) return "";
|
|
65
|
+
const { unit, count } = formatElapsed(now.value - at);
|
|
66
|
+
return t(`dms.live.updated_${unit}`, { count }, count);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
useLiveToggleShortcut(() => live.toggle());
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<div
|
|
74
|
+
v-if="isMounted && live.isEnabled.value"
|
|
75
|
+
class="dms-live-toggle flex flex-wrap items-center gap-2.5"
|
|
76
|
+
:class="ALIGN_CLASSES[align]"
|
|
77
|
+
>
|
|
78
|
+
<span
|
|
79
|
+
v-if="showUpdatedAt"
|
|
80
|
+
class="text-muted text-[12.5px] tabular-nums"
|
|
81
|
+
aria-live="off"
|
|
82
|
+
>
|
|
83
|
+
{{ updatedLabel }}
|
|
84
|
+
</span>
|
|
85
|
+
<DmsSegmented
|
|
86
|
+
v-model="mode"
|
|
87
|
+
:items="items"
|
|
88
|
+
:aria-label="t('dms.live.aria_label')"
|
|
89
|
+
size="sm"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
@@ -107,15 +107,24 @@ const theme = tv({
|
|
|
107
107
|
disabled: {
|
|
108
108
|
true: { root: "opacity-55" },
|
|
109
109
|
},
|
|
110
|
+
// Without a label cell, the control is the grid's only child: it spans
|
|
111
|
+
// both columns instead of falling into the form's 240px label column.
|
|
112
|
+
labeled: {
|
|
113
|
+
false: { control: "col-span-full" },
|
|
114
|
+
},
|
|
110
115
|
},
|
|
111
116
|
});
|
|
112
117
|
|
|
118
|
+
const hasText = computed(
|
|
119
|
+
() => !!(props.label || props.description || slots.label || slots.details),
|
|
120
|
+
);
|
|
113
121
|
const ui = computed(() =>
|
|
114
122
|
theme({
|
|
115
123
|
layout: props.layout,
|
|
116
124
|
inset: props.inset,
|
|
117
125
|
spacing: props.spacing,
|
|
118
126
|
disabled: props.disabled,
|
|
127
|
+
labeled: hasText.value,
|
|
119
128
|
}),
|
|
120
129
|
);
|
|
121
130
|
const labelTag = computed(() => (props.labelFor ? "label" : "div"));
|
|
@@ -127,10 +136,7 @@ const { processI18n } = useTranslation();
|
|
|
127
136
|
<template>
|
|
128
137
|
<div :class="ui.root()">
|
|
129
138
|
<div :class="ui.body()">
|
|
130
|
-
<div
|
|
131
|
-
v-if="props.label || props.description || slots.label || slots.details"
|
|
132
|
-
:class="ui.text()"
|
|
133
|
-
>
|
|
139
|
+
<div v-if="hasText" :class="ui.text()">
|
|
134
140
|
<component
|
|
135
141
|
:is="labelTag"
|
|
136
142
|
v-if="props.label || slots.label"
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "#dms-core/app/composables/period/types";
|
|
13
13
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
14
14
|
import { usePeriodDraft } from "../../build/composables/period/usePeriodDraft";
|
|
15
|
+
import { usePeriodLocation } from "../../build/composables/period/usePeriodLocation";
|
|
15
16
|
import {
|
|
16
17
|
type CalendarRange,
|
|
17
18
|
calendarToRange,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
} from "../../build/composables/period/periodDisplay";
|
|
22
23
|
import PeriodRangeCalendar from "../../build/components/period/PeriodRangeCalendar.vue";
|
|
23
24
|
import { useUserRegionalPreferences } from "#dms-core/app/composables/user/useUserRegionalPreferences";
|
|
25
|
+
import { usePageLive } from "../../composables/live/usePageLive";
|
|
24
26
|
|
|
25
27
|
interface Props extends DefaultComponentProps {
|
|
26
28
|
id: string;
|
|
@@ -93,11 +95,28 @@ useWatch(props.watchActions || [], props.componentId);
|
|
|
93
95
|
|
|
94
96
|
const { t, locale } = useI18n();
|
|
95
97
|
|
|
98
|
+
// A paused live page keeps its period where it was: a relative preset
|
|
99
|
+
// sliding every minute would refetch every block reading it.
|
|
100
|
+
const { isPaused: isPagePaused } = usePageLive();
|
|
96
101
|
const period = usePeriod({
|
|
97
102
|
defaultPreset: props.defaultPreset,
|
|
98
103
|
defaultComparison: props.defaultComparison,
|
|
99
104
|
presets: props.presets,
|
|
100
105
|
comparisons: props.comparisons,
|
|
106
|
+
holdClock: () => isPagePaused.value,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Before the scope registers, so the cards' first request already carries
|
|
110
|
+
// the period of the URL (or of the user's last choice), never the default.
|
|
111
|
+
usePeriodLocation(period, {
|
|
112
|
+
id: props.id,
|
|
113
|
+
pageId: props.pageId,
|
|
114
|
+
fallback: {
|
|
115
|
+
preset: period.preset.value,
|
|
116
|
+
comparison: period.comparison.value,
|
|
117
|
+
customRange: null,
|
|
118
|
+
customCompareRange: null,
|
|
119
|
+
},
|
|
101
120
|
});
|
|
102
121
|
|
|
103
122
|
const cleanup = registerPeriodScope(props.id, period.state);
|
|
@@ -8,7 +8,12 @@ import type {
|
|
|
8
8
|
Table,
|
|
9
9
|
VisibilityState,
|
|
10
10
|
} from "@tanstack/vue-table";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
refDebounced,
|
|
13
|
+
until,
|
|
14
|
+
useActiveElement,
|
|
15
|
+
watchDebounced,
|
|
16
|
+
} from "@vueuse/core";
|
|
12
17
|
import {
|
|
13
18
|
QUICK_ACTION_BUTTON_KEY,
|
|
14
19
|
QUICK_ACTION_COMPONENT_KEY,
|
|
@@ -71,7 +76,15 @@ import {
|
|
|
71
76
|
import { selectedRowIds } from "../../build/composables/table-view/utils/bulkActions";
|
|
72
77
|
import { isBulkAction } from "../../build/composables/actions/bulkSelection";
|
|
73
78
|
import { selectionScopeKey } from "../../build/utils/selectionScope";
|
|
74
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
isDeepLinkAction,
|
|
81
|
+
readRecordId,
|
|
82
|
+
resolveDeepLinkedRow,
|
|
83
|
+
} from "../../build/composables/table-view/utils/recordLink";
|
|
84
|
+
import {
|
|
85
|
+
rowSidePanelOwnerKey,
|
|
86
|
+
useRowSidePanelContent,
|
|
87
|
+
} from "#dms-layout/app/build/composables/layout/rowSidePanel";
|
|
75
88
|
import type { CustomRowAction } from "../../types/row-action";
|
|
76
89
|
import {
|
|
77
90
|
type QuickFilterItem,
|
|
@@ -82,7 +95,10 @@ import {
|
|
|
82
95
|
staticQuickFilterItems,
|
|
83
96
|
} from "../../build/composables/table-view/utils/quickFilters";
|
|
84
97
|
import { useNavBadges } from "../../build/composables/navigation/useNavBadges";
|
|
85
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
resolveRowClickAction,
|
|
100
|
+
rowCustomActions,
|
|
101
|
+
} from "../../utils/rowClickAction";
|
|
86
102
|
import { usePermissionPreview } from "#dms-core/app/build/composables/auth/usePermissionPreview";
|
|
87
103
|
|
|
88
104
|
import UTable from "../../build/components/table/Table.vue";
|
|
@@ -110,6 +126,9 @@ import { useAccumulatedPages } from "../../build/composables/table-view/useAccum
|
|
|
110
126
|
import { usePersistedPagination } from "../../build/composables/table-view/usePersistedPagination";
|
|
111
127
|
import { useTableRows } from "../../build/composables/table-view/useTableRows";
|
|
112
128
|
import { useRefreshTriggers } from "../../build/composables/blocks/useRefreshTriggers";
|
|
129
|
+
import { useTableLiveHold } from "../../build/composables/table-view/useTableLiveHold";
|
|
130
|
+
import { usePageLive } from "../../composables/live/usePageLive";
|
|
131
|
+
import LiveHoldBanner from "../../build/components/table-view/LiveHoldBanner.vue";
|
|
113
132
|
import { useTableReorder } from "../../build/composables/table-view/useTableReorder";
|
|
114
133
|
import {
|
|
115
134
|
groupedOptionsFor,
|
|
@@ -179,6 +198,8 @@ interface TableViewProps<T extends Data> extends TableViewConfig<T> {
|
|
|
179
198
|
isSoleTableView?: boolean;
|
|
180
199
|
/** Topics whose events make the table read its rows again. */
|
|
181
200
|
realtimeTopic?: string | string[];
|
|
201
|
+
/** Keeps the rows while the live page is paused (backend `liveHold`). */
|
|
202
|
+
liveHold?: boolean;
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
const props = defineProps<TableViewProps<T>>();
|
|
@@ -801,6 +822,14 @@ const archiveQuery = computed(() =>
|
|
|
801
822
|
archiveMode ? { showArchived: showArchived.value } : {},
|
|
802
823
|
);
|
|
803
824
|
|
|
825
|
+
// The gate is set up with the other refresh triggers below; the hold only
|
|
826
|
+
// reads it once the table is on screen.
|
|
827
|
+
const liveHold = useTableLiveHold({
|
|
828
|
+
isEnabled: !!props.liveHold,
|
|
829
|
+
isSource: tableRows.isSource,
|
|
830
|
+
gate: () => liveGate,
|
|
831
|
+
});
|
|
832
|
+
|
|
804
833
|
const { data, status, error, refresh } = await useTableListData<T>({
|
|
805
834
|
componentId,
|
|
806
835
|
pageId,
|
|
@@ -808,6 +837,7 @@ const { data, status, error, refresh } = await useTableListData<T>({
|
|
|
808
837
|
query: queryRequest,
|
|
809
838
|
archiveQuery,
|
|
810
839
|
isSelfManaged: isActiveDisplaySelfManaged,
|
|
840
|
+
heldQuery: liveHold.heldQuery,
|
|
811
841
|
});
|
|
812
842
|
|
|
813
843
|
// Nothing listed yet (a client navigation paints before the first page
|
|
@@ -949,6 +979,7 @@ const { data: tabCountsData, refresh: refreshTabCounts } =
|
|
|
949
979
|
rows: tableRows,
|
|
950
980
|
queries: tabCountsQuery,
|
|
951
981
|
archiveQuery,
|
|
982
|
+
heldQuery: liveHold.heldQuery,
|
|
952
983
|
});
|
|
953
984
|
|
|
954
985
|
const viewItems = computed<TableViewItem[]>(() =>
|
|
@@ -1125,8 +1156,19 @@ const refreshAll = async () => {
|
|
|
1125
1156
|
|
|
1126
1157
|
// The rows can move without the table seeing it (a batch, a job, another
|
|
1127
1158
|
// block's action): a topic of the backend or a page refresh reads them again,
|
|
1128
|
-
// keeping the current rows on screen until the answer lands.
|
|
1129
|
-
|
|
1159
|
+
// keeping the current rows on screen until the answer lands. On a live page
|
|
1160
|
+
// the row events below go through the same gate.
|
|
1161
|
+
const liveGate = useRefreshTriggers(
|
|
1162
|
+
props.realtimeTopic,
|
|
1163
|
+
() => void refreshAll(),
|
|
1164
|
+
);
|
|
1165
|
+
// What the user asks for (a row action, the refresh button, ⌘R, a display's
|
|
1166
|
+
// own action) reads at once, even while the live page is paused, and brings
|
|
1167
|
+
// a held table up to date.
|
|
1168
|
+
const refreshByUser = () => {
|
|
1169
|
+
liveGate.acknowledge();
|
|
1170
|
+
return refreshAll();
|
|
1171
|
+
};
|
|
1130
1172
|
|
|
1131
1173
|
// "Preview as role": a link tab to a page the previewed role could not open
|
|
1132
1174
|
// is drawn locked, one to a page it opens partially is drawn partially
|
|
@@ -1203,7 +1245,7 @@ const {
|
|
|
1203
1245
|
caption,
|
|
1204
1246
|
labelKey,
|
|
1205
1247
|
rowIdKey: props.rowIdKey,
|
|
1206
|
-
refreshCallback:
|
|
1248
|
+
refreshCallback: refreshByUser,
|
|
1207
1249
|
formComponents,
|
|
1208
1250
|
formContainer,
|
|
1209
1251
|
formPages,
|
|
@@ -1395,12 +1437,28 @@ const realtimeEventHandlers: Record<
|
|
|
1395
1437
|
},
|
|
1396
1438
|
};
|
|
1397
1439
|
|
|
1440
|
+
// On a live page a row event is one more event behind the table: in Direct
|
|
1441
|
+
// it reads the rows again (at most once per `maxRate`), paused it is counted
|
|
1442
|
+
// and the rows stay as they are. Rows gone elsewhere still leave a selection
|
|
1443
|
+
// the page shows moving.
|
|
1444
|
+
const { isEnabled: isPageLive, isPaused: isPagePaused } = usePageLive();
|
|
1445
|
+
const gateRowEvent = (type: string, ids: string[], event: object) => {
|
|
1446
|
+
if (type === REALTIME_EVENT_TYPE.DELETED && !isPagePaused.value) {
|
|
1447
|
+
deselectRows(ids);
|
|
1448
|
+
}
|
|
1449
|
+
liveGate.trigger(event);
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1398
1452
|
useRealtimeTopic(realtimeRowTopic, (event) => {
|
|
1399
1453
|
const payload = event as { type: string; payload?: { ids?: string[] } };
|
|
1400
1454
|
const handler = realtimeEventHandlers[payload.type];
|
|
1401
1455
|
if (!handler) return;
|
|
1402
1456
|
const ids = payload.payload?.ids ?? [];
|
|
1403
1457
|
if (ids.length === 0 && payload.type !== REALTIME_EVENT_TYPE.CREATED) return;
|
|
1458
|
+
if (isPageLive.value) {
|
|
1459
|
+
gateRowEvent(payload.type, ids, event);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1404
1462
|
void handler(ids);
|
|
1405
1463
|
});
|
|
1406
1464
|
|
|
@@ -1599,20 +1657,36 @@ watch(rowSelect, (selection) => {
|
|
|
1599
1657
|
if (Object.keys(selection).length === 0) allMatching.value = false;
|
|
1600
1658
|
});
|
|
1601
1659
|
|
|
1602
|
-
// A deep-linked action reopens on the row the URL names, listed or not.
|
|
1660
|
+
// A deep-linked action reopens on the row the URL names, listed or not. A
|
|
1661
|
+
// list still loading is waited for first: a row it lists keeps its position
|
|
1662
|
+
// among the rows shown, and a source table without `fetchRowUrl` has no
|
|
1663
|
+
// route to read one row from.
|
|
1603
1664
|
const deepLinkAction = (tableProps.value.rowActions?.custom ?? []).find(
|
|
1604
|
-
|
|
1665
|
+
isDeepLinkAction,
|
|
1605
1666
|
);
|
|
1606
1667
|
const openRecordFromUrl = async () => {
|
|
1607
1668
|
const id = deepLinkAction ? readRecordId(route.query, urlScope) : undefined;
|
|
1608
1669
|
if (!deepLinkAction || !id) return;
|
|
1609
|
-
|
|
1610
|
-
const row =
|
|
1670
|
+
await until(isListLoading).toBe(false);
|
|
1671
|
+
const row = await resolveDeepLinkedRow(id, {
|
|
1672
|
+
listed: shownResults.value ?? undefined,
|
|
1673
|
+
idOf: getRowId,
|
|
1674
|
+
getRow: tableRows.getRow,
|
|
1675
|
+
});
|
|
1611
1676
|
if (row) handleCustomRowAction(deepLinkAction, row as Data);
|
|
1612
1677
|
};
|
|
1613
1678
|
|
|
1614
1679
|
const selectedIds = computed(() => selectedRowIds(rowSelect.value));
|
|
1615
1680
|
|
|
1681
|
+
// The row a side panel has open, which the grid highlights.
|
|
1682
|
+
const rowSidePanel = useRowSidePanelContent();
|
|
1683
|
+
const rowSidePanelOwner = rowSidePanelOwnerKey(pageId!, componentId!);
|
|
1684
|
+
const openRowId = computed(() =>
|
|
1685
|
+
rowSidePanel.value?.ownerKey === rowSidePanelOwner
|
|
1686
|
+
? rowSidePanel.value.rowKey
|
|
1687
|
+
: undefined,
|
|
1688
|
+
);
|
|
1689
|
+
|
|
1616
1690
|
const isRowActionEnabled = (
|
|
1617
1691
|
config: boolean | RowActionConfig | undefined,
|
|
1618
1692
|
item: T,
|
|
@@ -1707,6 +1781,11 @@ const displayRowActions = {
|
|
|
1707
1781
|
isRowActionEnabled(tableProps.value.rowActions?.edit, item),
|
|
1708
1782
|
canDeleteRow: (item: T) =>
|
|
1709
1783
|
isRowActionEnabled(tableProps.value.rowActions?.delete, item),
|
|
1784
|
+
customFor: (item: T) =>
|
|
1785
|
+
rowCustomActions(
|
|
1786
|
+
tableProps.value.rowActions?.custom,
|
|
1787
|
+
item as Record<string, unknown>,
|
|
1788
|
+
),
|
|
1710
1789
|
};
|
|
1711
1790
|
|
|
1712
1791
|
// The TanStack instance is only available as a #body slot prop; captured here so
|
|
@@ -1746,7 +1825,7 @@ const displayContext = computed(
|
|
|
1746
1825
|
componentId,
|
|
1747
1826
|
pageId,
|
|
1748
1827
|
options: optionsForDisplay(activeDisplayId.value),
|
|
1749
|
-
refresh:
|
|
1828
|
+
refresh: refreshByUser,
|
|
1750
1829
|
reorder: reorderState.value,
|
|
1751
1830
|
table: displayTable.value,
|
|
1752
1831
|
}),
|
|
@@ -1833,7 +1912,7 @@ onBeforeUnmount(() => unregisterQuickActionTarget?.());
|
|
|
1833
1912
|
|
|
1834
1913
|
defineShortcuts(
|
|
1835
1914
|
buildTableViewShortcuts({
|
|
1836
|
-
refresh:
|
|
1915
|
+
refresh: refreshByUser,
|
|
1837
1916
|
tableProps,
|
|
1838
1917
|
newRow: () => newRow(queryParamDefaults.value),
|
|
1839
1918
|
globalFilter,
|
|
@@ -2004,8 +2083,9 @@ onMounted(() => {
|
|
|
2004
2083
|
:bulk-actions="bulkCustomActions"
|
|
2005
2084
|
v-model:all-matching="allMatching"
|
|
2006
2085
|
:presence-by-row="presenceByRowFiltered"
|
|
2086
|
+
:open-row-id="openRowId"
|
|
2007
2087
|
@add="handleRowAdd"
|
|
2008
|
-
@refresh="
|
|
2088
|
+
@refresh="refreshByUser"
|
|
2009
2089
|
@details="handleRowClick"
|
|
2010
2090
|
@delete="(e) => deleteRows(e)"
|
|
2011
2091
|
@archive="(e) => archiveRows(e)"
|
|
@@ -2016,6 +2096,12 @@ onMounted(() => {
|
|
|
2016
2096
|
@duplicate="handleRowDuplicate"
|
|
2017
2097
|
@edit="handleRowEdit"
|
|
2018
2098
|
>
|
|
2099
|
+
<template v-if="liveHold.newCount.value > 0" #notice>
|
|
2100
|
+
<LiveHoldBanner
|
|
2101
|
+
:count="liveHold.newCount.value"
|
|
2102
|
+
@show="liveHold.showNew"
|
|
2103
|
+
/>
|
|
2104
|
+
</template>
|
|
2019
2105
|
<template v-if="hasViews" #views>
|
|
2020
2106
|
<TableViews
|
|
2021
2107
|
:items="viewItems"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from "vue";
|
|
2
|
+
import { currentPageLive } from "../../build/composables/live/currentPageLive";
|
|
3
|
+
|
|
4
|
+
/** The Direct / Pause state of the page on screen, for a module's component. */
|
|
5
|
+
export interface PageLiveState {
|
|
6
|
+
/** The page declares `live`: it has a Direct / Pause state at all. */
|
|
7
|
+
isEnabled: ComputedRef<boolean>;
|
|
8
|
+
/** The page's data follows its events: Direct, or a page without `live`. */
|
|
9
|
+
isLive: ComputedRef<boolean>;
|
|
10
|
+
/** The page is paused: its blocks request nothing on their events. */
|
|
11
|
+
isPaused: ComputedRef<boolean>;
|
|
12
|
+
pause: () => void;
|
|
13
|
+
/** Leaves the pause: each block that missed an event refetches once. */
|
|
14
|
+
resume: () => void;
|
|
15
|
+
toggle: () => void;
|
|
16
|
+
/** When the page last read its data (ms since the epoch), `null` without `live`. */
|
|
17
|
+
lastUpdatedAt: ComputedRef<number | null>;
|
|
18
|
+
/** Distinct events the page received since the pause. */
|
|
19
|
+
missedCount: ComputedRef<number>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Follows the Direct / Pause state of the page on screen (see the page option
|
|
24
|
+
* `live` and the `LiveToggle()` block), so a custom block stays in step with
|
|
25
|
+
* the built-in ones without reading the DOM: stop a client-side ticker while
|
|
26
|
+
* paused, show what was missed, offer its own switch.
|
|
27
|
+
*
|
|
28
|
+
* On a page without `live`, or while server-rendering, `isEnabled` is false,
|
|
29
|
+
* the page reads as live and never paused, and `pause` / `resume` do nothing.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const { isPaused, missedCount, resume } = usePageLive();
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* A component that refetches on its own realtime topic need not use it:
|
|
36
|
+
* built-in blocks with a `realtimeTopic` already follow the state.
|
|
37
|
+
*/
|
|
38
|
+
export function usePageLive(): PageLiveState {
|
|
39
|
+
const isPaused = computed(() => currentPageLive()?.isPaused.value ?? false);
|
|
40
|
+
return {
|
|
41
|
+
isEnabled: computed(() => currentPageLive() !== null),
|
|
42
|
+
isLive: computed(() => !isPaused.value),
|
|
43
|
+
isPaused,
|
|
44
|
+
pause: () => currentPageLive()?.pause(),
|
|
45
|
+
resume: () => currentPageLive()?.resume(),
|
|
46
|
+
toggle: () => currentPageLive()?.toggle(),
|
|
47
|
+
lastUpdatedAt: computed(
|
|
48
|
+
() => currentPageLive()?.lastUpdatedAt.value ?? null,
|
|
49
|
+
),
|
|
50
|
+
missedCount: computed(() => currentPageLive()?.missedCount.value ?? 0),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
} from "./tableViewShiftA";
|
|
24
24
|
import { TABLE_VIEW_DELETE_METADATA } from "./tableViewDelete";
|
|
25
25
|
import { TABLE_VIEW_SHIFT_F_METADATA } from "./tableViewShiftF";
|
|
26
|
+
import {
|
|
27
|
+
TABLE_VIEW_J_METADATA,
|
|
28
|
+
TABLE_VIEW_K_METADATA,
|
|
29
|
+
TABLE_VIEW_PANEL_ESCAPE_METADATA,
|
|
30
|
+
} from "./tableViewRowPanel";
|
|
26
31
|
|
|
27
32
|
export const TABLE_VIEW_SHORTCUTS_METADATA = [
|
|
28
33
|
TABLE_VIEW_META_R_METADATA,
|
|
@@ -31,6 +36,9 @@ export const TABLE_VIEW_SHORTCUTS_METADATA = [
|
|
|
31
36
|
TABLE_VIEW_SHIFT_A_METADATA,
|
|
32
37
|
TABLE_VIEW_DELETE_METADATA,
|
|
33
38
|
TABLE_VIEW_SHIFT_F_METADATA,
|
|
39
|
+
TABLE_VIEW_J_METADATA,
|
|
40
|
+
TABLE_VIEW_K_METADATA,
|
|
41
|
+
TABLE_VIEW_PANEL_ESCAPE_METADATA,
|
|
34
42
|
];
|
|
35
43
|
|
|
36
44
|
interface BuildTableViewShortcutsParams<T extends Data> {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The keys of a row opened from the table (see `listenRowSteps` and
|
|
2
|
+
// `listenRowPanelClose`): listed in the shortcuts help, handled by the panel,
|
|
3
|
+
// drawer or modal while it is open.
|
|
4
|
+
|
|
5
|
+
const ROW_OPEN_CONDITION = {
|
|
6
|
+
descriptionKey: "$dms.shortcuts.tableview.row_open.condition",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const TABLE_VIEW_J_METADATA: ShortcutMetadata = {
|
|
10
|
+
key: ["j"],
|
|
11
|
+
descriptionKey: "$dms.shortcuts.tableview.j.description",
|
|
12
|
+
component: "$dms.components.tableview",
|
|
13
|
+
condition: ROW_OPEN_CONDITION,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const TABLE_VIEW_K_METADATA: ShortcutMetadata = {
|
|
17
|
+
key: ["k"],
|
|
18
|
+
descriptionKey: "$dms.shortcuts.tableview.k.description",
|
|
19
|
+
component: "$dms.components.tableview",
|
|
20
|
+
condition: ROW_OPEN_CONDITION,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const TABLE_VIEW_PANEL_ESCAPE_METADATA: ShortcutMetadata = {
|
|
24
|
+
key: ["$keyboard.escape"],
|
|
25
|
+
descriptionKey: "$dms.shortcuts.tableview.panel_escape.description",
|
|
26
|
+
component: "$dms.components.tableview",
|
|
27
|
+
condition: {
|
|
28
|
+
descriptionKey: "$dms.shortcuts.tableview.panel_escape.condition",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { DrawerDirection } from "../../containers/types";
|
|
2
2
|
|
|
3
|
+
/** What a `sidePanel` target sets besides its component (backend `SidePanelTargetOptions`). */
|
|
4
|
+
export interface SidePanelTargetOptions {
|
|
5
|
+
/** Title of the panel header (`$`: an i18n key); the action's label else. */
|
|
6
|
+
title?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The row's position under the title: an i18n key (`$`-prefixed) receiving
|
|
9
|
+
* `{ index }` (from 1) and `{ total }`.
|
|
10
|
+
*/
|
|
11
|
+
positionLabel?: string;
|
|
12
|
+
/** The open row is kept in the URL (`?record=`): on unless `false`. */
|
|
13
|
+
deepLink?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
export type ActionTarget =
|
|
4
17
|
| {
|
|
5
18
|
type: "drawer";
|
|
@@ -16,6 +29,8 @@ export type ActionTarget =
|
|
|
16
29
|
title?: string;
|
|
17
30
|
description?: string;
|
|
18
31
|
}
|
|
32
|
+
/** A non-modal panel docked on the right of the page, on a row. */
|
|
33
|
+
| ({ type: "sidePanel"; component: ComponentInfo } & SidePanelTargetOptions)
|
|
19
34
|
| { type: "page"; url: string }
|
|
20
35
|
| { type: "external"; url: string; newTab?: boolean }
|
|
21
36
|
/** Runs a quick action, by its `category:id` key or bare id. */
|
|
@@ -219,7 +219,11 @@ export interface TableViewSourceCapabilities {
|
|
|
219
219
|
/** Where a `TableView.fromSource` table reads its rows (backend `source`). */
|
|
220
220
|
export interface TableViewSourceConfig {
|
|
221
221
|
fetchUrl: string;
|
|
222
|
+
/** Route answering one row, `{id}` its id (backend `fetchRowUrl`). */
|
|
223
|
+
fetchRowUrl?: string;
|
|
222
224
|
capabilities: TableViewSourceCapabilities;
|
|
225
|
+
/** Id of the `PeriodSelector` whose period is sent to `fetchUrl`. */
|
|
226
|
+
periodScope?: string;
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
export interface TableViewListResponse<T> {
|
|
@@ -92,7 +92,14 @@ export interface TableViewDisplayActions<T> {
|
|
|
92
92
|
details: (item: T) => void;
|
|
93
93
|
/** What a click on the row does in the grid: its default action. */
|
|
94
94
|
open: (item: T) => void;
|
|
95
|
-
custom
|
|
95
|
+
/** Runs a custom row action on the row, as its grid menu entry does. */
|
|
96
|
+
custom: (action: CustomRowAction, item?: T) => void;
|
|
97
|
+
/**
|
|
98
|
+
* The table's custom row actions the row offers: those whose `rule` accepts
|
|
99
|
+
* it, in their declared order. A card drawing its own menu lists these and
|
|
100
|
+
* runs one with `custom`; an action's `id` finds a given one.
|
|
101
|
+
*/
|
|
102
|
+
customFor: (item: T) => CustomRowAction[];
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GLOBAL_SHORTCUTS_METADATA } from "../build/composables/global/shortcuts";
|
|
2
|
+
import { LIVE_TOGGLE_SHORTCUTS_METADATA } from "../build/composables/live/shortcuts";
|
|
2
3
|
import { TAB_SHORTCUTS_METADATA } from "../composables/tab/shortcuts";
|
|
3
4
|
import { TABLE_VIEW_SHORTCUTS_METADATA } from "../composables/table-view/shortcuts";
|
|
4
5
|
import type { ComponentShortcuts } from "../types/shortcuts";
|
|
@@ -16,4 +17,8 @@ export default [
|
|
|
16
17
|
component: "TableView",
|
|
17
18
|
shortcuts: TABLE_VIEW_SHORTCUTS_METADATA,
|
|
18
19
|
},
|
|
20
|
+
{
|
|
21
|
+
component: "LiveToggle",
|
|
22
|
+
shortcuts: LIVE_TOGGLE_SHORTCUTS_METADATA,
|
|
23
|
+
},
|
|
19
24
|
] satisfies ComponentShortcuts[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror of `PageLiveOptions` from `@antelopejs/interface-dms/page`: the
|
|
3
|
+
* Direct / Pause valve a page declares with `live`.
|
|
4
|
+
*/
|
|
5
|
+
export interface PageLiveOptions {
|
|
6
|
+
/** Shortest interval, in seconds, between two refetches of one block. */
|
|
7
|
+
maxRate?: number;
|
|
8
|
+
}
|
|
@@ -14,6 +14,8 @@ export interface CustomRowActionBulkOptions {
|
|
|
14
14
|
export type CustomRowActionBulk = true | CustomRowActionBulkOptions;
|
|
15
15
|
|
|
16
16
|
export interface CustomRowAction {
|
|
17
|
+
/** Names the action for a card or display that places it itself. */
|
|
18
|
+
id?: string;
|
|
17
19
|
label: string;
|
|
18
20
|
icon?: string;
|
|
19
21
|
target: ActionTarget;
|
|
@@ -24,7 +26,10 @@ export interface CustomRowAction {
|
|
|
24
26
|
isVisible?: boolean;
|
|
25
27
|
/** Legacy spelling of `isVisible`, still honoured. */
|
|
26
28
|
visible?: boolean;
|
|
27
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Preferred row-click target when its rule accepts the row: a double-click,
|
|
31
|
+
* or a single click on a table with `rowClick: "single"`.
|
|
32
|
+
*/
|
|
28
33
|
isDefault?: boolean;
|
|
29
34
|
/** Color of the inline button or menu entry. */
|
|
30
35
|
color?: ColorValue;
|
|
@@ -34,6 +39,6 @@ export interface CustomRowAction {
|
|
|
34
39
|
showLabel?: boolean;
|
|
35
40
|
/** Offered on the selection bar too. */
|
|
36
41
|
bulk?: CustomRowActionBulk;
|
|
37
|
-
/** The drawer or
|
|
42
|
+
/** The drawer, modal or side panel it opens is reflected in the URL (`?record=`). */
|
|
38
43
|
deepLink?: boolean;
|
|
39
44
|
}
|