@antelopejs/dms 0.7.6 → 0.7.8
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 +35 -0
- 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-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/form/FormSectionNav.vue +30 -15
- 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/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 +90 -12
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +1 -1
- 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/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 +5 -2
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +44 -6
- 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/field-row.test.ts +14 -0
- package/frontend-vue/tests/form-section-nav-groups.test.ts +75 -14
- 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-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 +11 -3
- package/skills/dms-pages/REFERENCE.md +5 -0
|
@@ -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,
|
|
@@ -110,6 +123,9 @@ import { useAccumulatedPages } from "../../build/composables/table-view/useAccum
|
|
|
110
123
|
import { usePersistedPagination } from "../../build/composables/table-view/usePersistedPagination";
|
|
111
124
|
import { useTableRows } from "../../build/composables/table-view/useTableRows";
|
|
112
125
|
import { useRefreshTriggers } from "../../build/composables/blocks/useRefreshTriggers";
|
|
126
|
+
import { useTableLiveHold } from "../../build/composables/table-view/useTableLiveHold";
|
|
127
|
+
import { usePageLive } from "../../composables/live/usePageLive";
|
|
128
|
+
import LiveHoldBanner from "../../build/components/table-view/LiveHoldBanner.vue";
|
|
113
129
|
import { useTableReorder } from "../../build/composables/table-view/useTableReorder";
|
|
114
130
|
import {
|
|
115
131
|
groupedOptionsFor,
|
|
@@ -179,6 +195,8 @@ interface TableViewProps<T extends Data> extends TableViewConfig<T> {
|
|
|
179
195
|
isSoleTableView?: boolean;
|
|
180
196
|
/** Topics whose events make the table read its rows again. */
|
|
181
197
|
realtimeTopic?: string | string[];
|
|
198
|
+
/** Keeps the rows while the live page is paused (backend `liveHold`). */
|
|
199
|
+
liveHold?: boolean;
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
const props = defineProps<TableViewProps<T>>();
|
|
@@ -801,6 +819,14 @@ const archiveQuery = computed(() =>
|
|
|
801
819
|
archiveMode ? { showArchived: showArchived.value } : {},
|
|
802
820
|
);
|
|
803
821
|
|
|
822
|
+
// The gate is set up with the other refresh triggers below; the hold only
|
|
823
|
+
// reads it once the table is on screen.
|
|
824
|
+
const liveHold = useTableLiveHold({
|
|
825
|
+
isEnabled: !!props.liveHold,
|
|
826
|
+
isSource: tableRows.isSource,
|
|
827
|
+
gate: () => liveGate,
|
|
828
|
+
});
|
|
829
|
+
|
|
804
830
|
const { data, status, error, refresh } = await useTableListData<T>({
|
|
805
831
|
componentId,
|
|
806
832
|
pageId,
|
|
@@ -808,6 +834,7 @@ const { data, status, error, refresh } = await useTableListData<T>({
|
|
|
808
834
|
query: queryRequest,
|
|
809
835
|
archiveQuery,
|
|
810
836
|
isSelfManaged: isActiveDisplaySelfManaged,
|
|
837
|
+
heldQuery: liveHold.heldQuery,
|
|
811
838
|
});
|
|
812
839
|
|
|
813
840
|
// Nothing listed yet (a client navigation paints before the first page
|
|
@@ -949,6 +976,7 @@ const { data: tabCountsData, refresh: refreshTabCounts } =
|
|
|
949
976
|
rows: tableRows,
|
|
950
977
|
queries: tabCountsQuery,
|
|
951
978
|
archiveQuery,
|
|
979
|
+
heldQuery: liveHold.heldQuery,
|
|
952
980
|
});
|
|
953
981
|
|
|
954
982
|
const viewItems = computed<TableViewItem[]>(() =>
|
|
@@ -1125,8 +1153,19 @@ const refreshAll = async () => {
|
|
|
1125
1153
|
|
|
1126
1154
|
// The rows can move without the table seeing it (a batch, a job, another
|
|
1127
1155
|
// 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
|
-
|
|
1156
|
+
// keeping the current rows on screen until the answer lands. On a live page
|
|
1157
|
+
// the row events below go through the same gate.
|
|
1158
|
+
const liveGate = useRefreshTriggers(
|
|
1159
|
+
props.realtimeTopic,
|
|
1160
|
+
() => void refreshAll(),
|
|
1161
|
+
);
|
|
1162
|
+
// What the user asks for (a row action, the refresh button, ⌘R, a display's
|
|
1163
|
+
// own action) reads at once, even while the live page is paused, and brings
|
|
1164
|
+
// a held table up to date.
|
|
1165
|
+
const refreshByUser = () => {
|
|
1166
|
+
liveGate.acknowledge();
|
|
1167
|
+
return refreshAll();
|
|
1168
|
+
};
|
|
1130
1169
|
|
|
1131
1170
|
// "Preview as role": a link tab to a page the previewed role could not open
|
|
1132
1171
|
// is drawn locked, one to a page it opens partially is drawn partially
|
|
@@ -1203,7 +1242,7 @@ const {
|
|
|
1203
1242
|
caption,
|
|
1204
1243
|
labelKey,
|
|
1205
1244
|
rowIdKey: props.rowIdKey,
|
|
1206
|
-
refreshCallback:
|
|
1245
|
+
refreshCallback: refreshByUser,
|
|
1207
1246
|
formComponents,
|
|
1208
1247
|
formContainer,
|
|
1209
1248
|
formPages,
|
|
@@ -1395,12 +1434,28 @@ const realtimeEventHandlers: Record<
|
|
|
1395
1434
|
},
|
|
1396
1435
|
};
|
|
1397
1436
|
|
|
1437
|
+
// On a live page a row event is one more event behind the table: in Direct
|
|
1438
|
+
// it reads the rows again (at most once per `maxRate`), paused it is counted
|
|
1439
|
+
// and the rows stay as they are. Rows gone elsewhere still leave a selection
|
|
1440
|
+
// the page shows moving.
|
|
1441
|
+
const { isEnabled: isPageLive, isPaused: isPagePaused } = usePageLive();
|
|
1442
|
+
const gateRowEvent = (type: string, ids: string[], event: object) => {
|
|
1443
|
+
if (type === REALTIME_EVENT_TYPE.DELETED && !isPagePaused.value) {
|
|
1444
|
+
deselectRows(ids);
|
|
1445
|
+
}
|
|
1446
|
+
liveGate.trigger(event);
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1398
1449
|
useRealtimeTopic(realtimeRowTopic, (event) => {
|
|
1399
1450
|
const payload = event as { type: string; payload?: { ids?: string[] } };
|
|
1400
1451
|
const handler = realtimeEventHandlers[payload.type];
|
|
1401
1452
|
if (!handler) return;
|
|
1402
1453
|
const ids = payload.payload?.ids ?? [];
|
|
1403
1454
|
if (ids.length === 0 && payload.type !== REALTIME_EVENT_TYPE.CREATED) return;
|
|
1455
|
+
if (isPageLive.value) {
|
|
1456
|
+
gateRowEvent(payload.type, ids, event);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1404
1459
|
void handler(ids);
|
|
1405
1460
|
});
|
|
1406
1461
|
|
|
@@ -1599,20 +1654,36 @@ watch(rowSelect, (selection) => {
|
|
|
1599
1654
|
if (Object.keys(selection).length === 0) allMatching.value = false;
|
|
1600
1655
|
});
|
|
1601
1656
|
|
|
1602
|
-
// A deep-linked action reopens on the row the URL names, listed or not.
|
|
1657
|
+
// A deep-linked action reopens on the row the URL names, listed or not. A
|
|
1658
|
+
// list still loading is waited for first: a row it lists keeps its position
|
|
1659
|
+
// among the rows shown, and a source table without `fetchRowUrl` has no
|
|
1660
|
+
// route to read one row from.
|
|
1603
1661
|
const deepLinkAction = (tableProps.value.rowActions?.custom ?? []).find(
|
|
1604
|
-
|
|
1662
|
+
isDeepLinkAction,
|
|
1605
1663
|
);
|
|
1606
1664
|
const openRecordFromUrl = async () => {
|
|
1607
1665
|
const id = deepLinkAction ? readRecordId(route.query, urlScope) : undefined;
|
|
1608
1666
|
if (!deepLinkAction || !id) return;
|
|
1609
|
-
|
|
1610
|
-
const row =
|
|
1667
|
+
await until(isListLoading).toBe(false);
|
|
1668
|
+
const row = await resolveDeepLinkedRow(id, {
|
|
1669
|
+
listed: shownResults.value ?? undefined,
|
|
1670
|
+
idOf: getRowId,
|
|
1671
|
+
getRow: tableRows.getRow,
|
|
1672
|
+
});
|
|
1611
1673
|
if (row) handleCustomRowAction(deepLinkAction, row as Data);
|
|
1612
1674
|
};
|
|
1613
1675
|
|
|
1614
1676
|
const selectedIds = computed(() => selectedRowIds(rowSelect.value));
|
|
1615
1677
|
|
|
1678
|
+
// The row a side panel has open, which the grid highlights.
|
|
1679
|
+
const rowSidePanel = useRowSidePanelContent();
|
|
1680
|
+
const rowSidePanelOwner = rowSidePanelOwnerKey(pageId!, componentId!);
|
|
1681
|
+
const openRowId = computed(() =>
|
|
1682
|
+
rowSidePanel.value?.ownerKey === rowSidePanelOwner
|
|
1683
|
+
? rowSidePanel.value.rowKey
|
|
1684
|
+
: undefined,
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1616
1687
|
const isRowActionEnabled = (
|
|
1617
1688
|
config: boolean | RowActionConfig | undefined,
|
|
1618
1689
|
item: T,
|
|
@@ -1746,7 +1817,7 @@ const displayContext = computed(
|
|
|
1746
1817
|
componentId,
|
|
1747
1818
|
pageId,
|
|
1748
1819
|
options: optionsForDisplay(activeDisplayId.value),
|
|
1749
|
-
refresh:
|
|
1820
|
+
refresh: refreshByUser,
|
|
1750
1821
|
reorder: reorderState.value,
|
|
1751
1822
|
table: displayTable.value,
|
|
1752
1823
|
}),
|
|
@@ -1833,7 +1904,7 @@ onBeforeUnmount(() => unregisterQuickActionTarget?.());
|
|
|
1833
1904
|
|
|
1834
1905
|
defineShortcuts(
|
|
1835
1906
|
buildTableViewShortcuts({
|
|
1836
|
-
refresh:
|
|
1907
|
+
refresh: refreshByUser,
|
|
1837
1908
|
tableProps,
|
|
1838
1909
|
newRow: () => newRow(queryParamDefaults.value),
|
|
1839
1910
|
globalFilter,
|
|
@@ -2004,8 +2075,9 @@ onMounted(() => {
|
|
|
2004
2075
|
:bulk-actions="bulkCustomActions"
|
|
2005
2076
|
v-model:all-matching="allMatching"
|
|
2006
2077
|
:presence-by-row="presenceByRowFiltered"
|
|
2078
|
+
:open-row-id="openRowId"
|
|
2007
2079
|
@add="handleRowAdd"
|
|
2008
|
-
@refresh="
|
|
2080
|
+
@refresh="refreshByUser"
|
|
2009
2081
|
@details="handleRowClick"
|
|
2010
2082
|
@delete="(e) => deleteRows(e)"
|
|
2011
2083
|
@archive="(e) => archiveRows(e)"
|
|
@@ -2016,6 +2088,12 @@ onMounted(() => {
|
|
|
2016
2088
|
@duplicate="handleRowDuplicate"
|
|
2017
2089
|
@edit="handleRowEdit"
|
|
2018
2090
|
>
|
|
2091
|
+
<template v-if="liveHold.newCount.value > 0" #notice>
|
|
2092
|
+
<LiveHoldBanner
|
|
2093
|
+
:count="liveHold.newCount.value"
|
|
2094
|
+
@show="liveHold.showNew"
|
|
2095
|
+
/>
|
|
2096
|
+
</template>
|
|
2019
2097
|
<template v-if="hasViews" #views>
|
|
2020
2098
|
<TableViews
|
|
2021
2099
|
:items="viewItems"
|
|
@@ -54,7 +54,7 @@ export interface FormSection {
|
|
|
54
54
|
label: string;
|
|
55
55
|
description?: string;
|
|
56
56
|
icon?: string;
|
|
57
|
-
/** Heading of the side
|
|
57
|
+
/** Heading of the navigation (side list or chips) the section's entry sits under. */
|
|
58
58
|
navGroup?: string;
|
|
59
59
|
fieldIds: string[];
|
|
60
60
|
}
|
|
@@ -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> {
|
|
@@ -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
|
+
}
|
|
@@ -24,7 +24,10 @@ export interface CustomRowAction {
|
|
|
24
24
|
isVisible?: boolean;
|
|
25
25
|
/** Legacy spelling of `isVisible`, still honoured. */
|
|
26
26
|
visible?: boolean;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Preferred row-click target when its rule accepts the row: a double-click,
|
|
29
|
+
* or a single click on a table with `rowClick: "single"`.
|
|
30
|
+
*/
|
|
28
31
|
isDefault?: boolean;
|
|
29
32
|
/** Color of the inline button or menu entry. */
|
|
30
33
|
color?: ColorValue;
|
|
@@ -34,6 +37,6 @@ export interface CustomRowAction {
|
|
|
34
37
|
showLabel?: boolean;
|
|
35
38
|
/** Offered on the selection bar too. */
|
|
36
39
|
bulk?: CustomRowActionBulk;
|
|
37
|
-
/** The drawer or
|
|
40
|
+
/** The drawer, modal or side panel it opens is reflected in the URL (`?record=`). */
|
|
38
41
|
deepLink?: boolean;
|
|
39
42
|
}
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const BLOCK_REFRESH_EVENT = "DmsComponent.Block.Refresh";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The window event of a refresh nobody asked for (a timer, a feed): on a page
|
|
10
|
+
* declared with `live` it goes through the Direct / Pause gate.
|
|
11
|
+
*/
|
|
12
|
+
export const LIVE_BLOCK_REFRESH_EVENT = "DmsComponent.Block.LiveRefresh";
|
|
13
|
+
|
|
8
14
|
/** The watch functions every block can name (see interface-dms). */
|
|
9
15
|
export const BlockFunctions = {
|
|
10
16
|
REFRESH_PAGE: "DmsComponent.Block.RefreshPage",
|
|
@@ -12,17 +18,49 @@ export const BlockFunctions = {
|
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Asks every data block mounted on the page to read its data again: what a
|
|
15
|
-
* component calls once an action of
|
|
16
|
-
* shows. The page header calls it after a button changed something
|
|
21
|
+
* component calls once an action of the user changed the record the page
|
|
22
|
+
* shows. The page header calls it after a button changed something, a form
|
|
23
|
+
* through `BlockFunctions.REFRESH_PAGE` once it saved. A user action always
|
|
24
|
+
* refreshes: on a page declared with `live` it reads even while paused, and
|
|
25
|
+
* the blocks it reads no longer count what they had missed.
|
|
17
26
|
*/
|
|
18
27
|
export function refreshPageBlocks(): void {
|
|
19
28
|
if (typeof window === "undefined") return;
|
|
20
29
|
window.dispatchEvent(new CustomEvent(BLOCK_REFRESH_EVENT));
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Asks every data block mounted on the page to read its data again because
|
|
34
|
+
* time passed or a feed moved, not because the user did something: on a page
|
|
35
|
+
* declared with `live` it goes through the page's Direct / Pause gate like a
|
|
36
|
+
* realtime event — capped by `maxRate` in Direct, counted while paused and
|
|
37
|
+
* read once on resume. Without `live`, it is `refreshPageBlocks()`.
|
|
38
|
+
*/
|
|
39
|
+
export function refreshLivePageBlocks(): void {
|
|
40
|
+
if (typeof window === "undefined") return;
|
|
41
|
+
window.dispatchEvent(new CustomEvent(LIVE_BLOCK_REFRESH_EVENT));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calls `handler` on each `refreshPageBlocks()`; returns the unsubscribe. Every
|
|
46
|
+
* handler receives the same event for one call, which tells one refresh
|
|
47
|
+
* reaching several blocks from several refreshes.
|
|
48
|
+
*/
|
|
49
|
+
export function onPageBlocksRefresh(
|
|
50
|
+
handler: (event: Event) => void,
|
|
51
|
+
): () => void {
|
|
52
|
+
return listen(BLOCK_REFRESH_EVENT, handler);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Calls `handler` on each `refreshLivePageBlocks()`; returns the unsubscribe. */
|
|
56
|
+
export function onLivePageBlocksRefresh(
|
|
57
|
+
handler: (event: Event) => void,
|
|
58
|
+
): () => void {
|
|
59
|
+
return listen(LIVE_BLOCK_REFRESH_EVENT, handler);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function listen(name: string, handler: (event: Event) => void): () => void {
|
|
25
63
|
if (typeof window === "undefined") return () => {};
|
|
26
|
-
window.addEventListener(
|
|
27
|
-
return () => window.removeEventListener(
|
|
64
|
+
window.addEventListener(name, handler);
|
|
65
|
+
return () => window.removeEventListener(name, handler);
|
|
28
66
|
}
|
|
@@ -469,6 +469,19 @@
|
|
|
469
469
|
"delete": {
|
|
470
470
|
"description": "Delete hovered row",
|
|
471
471
|
"condition": "Only when enableRowDelete is enabled"
|
|
472
|
+
},
|
|
473
|
+
"j": {
|
|
474
|
+
"description": "Next row"
|
|
475
|
+
},
|
|
476
|
+
"k": {
|
|
477
|
+
"description": "Previous row"
|
|
478
|
+
},
|
|
479
|
+
"row_open": {
|
|
480
|
+
"condition": "While a row is open in a side panel, a drawer or a modal, outside a text field"
|
|
481
|
+
},
|
|
482
|
+
"panel_escape": {
|
|
483
|
+
"description": "Close the row side panel",
|
|
484
|
+
"condition": "While a row is open in a side panel, once the table has no search or selection to clear"
|
|
472
485
|
}
|
|
473
486
|
},
|
|
474
487
|
"tab": {
|
|
@@ -492,6 +505,11 @@
|
|
|
492
505
|
"escape": {
|
|
493
506
|
"description": "Close dialogs, menus and the palette"
|
|
494
507
|
}
|
|
508
|
+
},
|
|
509
|
+
"livetoggle": {
|
|
510
|
+
"l": {
|
|
511
|
+
"description": "Switch the page between Direct and Pause"
|
|
512
|
+
}
|
|
495
513
|
}
|
|
496
514
|
},
|
|
497
515
|
"notifications": {
|
|
@@ -709,6 +727,16 @@
|
|
|
709
727
|
"action_error": {
|
|
710
728
|
"forbidden": "You don’t have permission to do this.",
|
|
711
729
|
"generic": "Something went wrong and the action couldn’t be completed. Try again."
|
|
730
|
+
},
|
|
731
|
+
"live": {
|
|
732
|
+
"direct": "Direct",
|
|
733
|
+
"pause": "Pause",
|
|
734
|
+
"aria_label": "Live updates",
|
|
735
|
+
"updated_seconds": "Updated {count} s ago",
|
|
736
|
+
"updated_minutes": "Updated {count} min ago",
|
|
737
|
+
"updated_hours": "Updated {count} h ago",
|
|
738
|
+
"new_rows": "{count} new",
|
|
739
|
+
"show_new": "Show"
|
|
712
740
|
}
|
|
713
741
|
}
|
|
714
742
|
}
|
|
@@ -470,6 +470,19 @@
|
|
|
470
470
|
"delete": {
|
|
471
471
|
"description": "Supprimer la ligne survolée",
|
|
472
472
|
"condition": "Uniquement quand enableRowDelete est activé"
|
|
473
|
+
},
|
|
474
|
+
"j": {
|
|
475
|
+
"description": "Ligne suivante"
|
|
476
|
+
},
|
|
477
|
+
"k": {
|
|
478
|
+
"description": "Ligne précédente"
|
|
479
|
+
},
|
|
480
|
+
"row_open": {
|
|
481
|
+
"condition": "Quand une ligne est ouverte dans un panneau latéral, un tiroir ou une fenêtre, hors d'un champ de saisie"
|
|
482
|
+
},
|
|
483
|
+
"panel_escape": {
|
|
484
|
+
"description": "Fermer le panneau latéral de la ligne",
|
|
485
|
+
"condition": "Quand une ligne est ouverte dans un panneau latéral, une fois que la table n'a plus de recherche ni de sélection à effacer"
|
|
473
486
|
}
|
|
474
487
|
},
|
|
475
488
|
"tab": {
|
|
@@ -493,6 +506,11 @@
|
|
|
493
506
|
"escape": {
|
|
494
507
|
"description": "Fermer les dialogues, les menus et la palette"
|
|
495
508
|
}
|
|
509
|
+
},
|
|
510
|
+
"livetoggle": {
|
|
511
|
+
"l": {
|
|
512
|
+
"description": "Basculer la page entre Direct et Pause"
|
|
513
|
+
}
|
|
496
514
|
}
|
|
497
515
|
},
|
|
498
516
|
"notifications": {
|
|
@@ -710,6 +728,16 @@
|
|
|
710
728
|
"action_error": {
|
|
711
729
|
"forbidden": "Vous n’avez pas l’autorisation d’effectuer cette action.",
|
|
712
730
|
"generic": "Une erreur est survenue et l’action n’a pas pu aboutir. Réessayez."
|
|
731
|
+
},
|
|
732
|
+
"live": {
|
|
733
|
+
"direct": "Direct",
|
|
734
|
+
"pause": "Pause",
|
|
735
|
+
"aria_label": "Mises à jour en direct",
|
|
736
|
+
"updated_seconds": "Mis à jour il y a {count} s",
|
|
737
|
+
"updated_minutes": "Mis à jour il y a {count} min",
|
|
738
|
+
"updated_hours": "Mis à jour il y a {count} h",
|
|
739
|
+
"new_rows": "{count} nouvelle | {count} nouvelles",
|
|
740
|
+
"show_new": "Afficher"
|
|
713
741
|
}
|
|
714
742
|
}
|
|
715
743
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* A chart on a paused live page sends no request on its events or on a page
|
|
4
|
+
* refresh, and reads once when the page resumes.
|
|
5
|
+
*/
|
|
6
|
+
import { createApp, defineComponent, h, ref } from "vue";
|
|
7
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
refreshLivePageBlocks,
|
|
10
|
+
refreshPageBlocks,
|
|
11
|
+
} from "../layers/dms-ui/app/utils/blockRefresh";
|
|
12
|
+
import { createPageLiveController } from "../layers/dms-ui/app/build/composables/live/pageLiveController";
|
|
13
|
+
import { setCurrentPageLive } from "../layers/dms-ui/app/build/composables/live/currentPageLive";
|
|
14
|
+
|
|
15
|
+
const requests: string[] = [];
|
|
16
|
+
|
|
17
|
+
vi.stubGlobal("useAuthFetch", () => ({
|
|
18
|
+
$authFetch: async (url: string) => {
|
|
19
|
+
requests.push(url);
|
|
20
|
+
return { value: requests.length };
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
vi.stubGlobal("useDmsRoute", () => ({ query: {} }));
|
|
24
|
+
vi.stubGlobal("ref", ref);
|
|
25
|
+
|
|
26
|
+
const { useChartFetch } = await import(
|
|
27
|
+
"../layers/dms-ui/app/composables/chart/useChartFetch"
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// useChartFetch debounces every refresh past the first by 180ms.
|
|
31
|
+
const SETTLE_MS = 250;
|
|
32
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
|
|
33
|
+
|
|
34
|
+
const apps: Array<() => void> = [];
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
apps.splice(0).forEach((unmount) => unmount());
|
|
38
|
+
setCurrentPageLive(null);
|
|
39
|
+
window.localStorage.clear();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("requests nothing while paused and reads once on resume", async () => {
|
|
43
|
+
const live = createPageLiveController("api.overview", {});
|
|
44
|
+
setCurrentPageLive(live);
|
|
45
|
+
const app = createApp(
|
|
46
|
+
defineComponent({
|
|
47
|
+
setup() {
|
|
48
|
+
useChartFetch<{ value: number }>({ fetchUrl: "/api/overview/chart" });
|
|
49
|
+
return () => h("div");
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
app.mount(document.createElement("div"));
|
|
54
|
+
apps.push(() => app.unmount());
|
|
55
|
+
await settle();
|
|
56
|
+
expect(requests).toHaveLength(1);
|
|
57
|
+
|
|
58
|
+
live.pause();
|
|
59
|
+
refreshLivePageBlocks();
|
|
60
|
+
refreshLivePageBlocks();
|
|
61
|
+
await settle();
|
|
62
|
+
expect(requests).toHaveLength(1);
|
|
63
|
+
|
|
64
|
+
live.resume();
|
|
65
|
+
await settle();
|
|
66
|
+
expect(requests).toHaveLength(2);
|
|
67
|
+
|
|
68
|
+
// An action of the user refreshes even while paused.
|
|
69
|
+
live.pause();
|
|
70
|
+
refreshPageBlocks();
|
|
71
|
+
await settle();
|
|
72
|
+
expect(requests).toHaveLength(3);
|
|
73
|
+
live.dispose();
|
|
74
|
+
});
|