@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,134 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The body of the side panel a `sidePanel` action target opens on a row (see
|
|
4
|
+
* `rowSidePanel.ts`). The DMS draws the header: the title, the row's position
|
|
5
|
+
* among the rows the table shows, previous and next (J and K too) and close
|
|
6
|
+
* (Escape too). Below it, the module's component receives the row as a
|
|
7
|
+
* drawer's component does, mounted anew on each row. The position follows the
|
|
8
|
+
* table: a refresh that moves or drops the row updates it.
|
|
9
|
+
*/
|
|
10
|
+
import { computed, onBeforeUnmount, onMounted } from "vue";
|
|
11
|
+
import {
|
|
12
|
+
listenRowPanelClose,
|
|
13
|
+
listenRowSteps,
|
|
14
|
+
rowNavigation,
|
|
15
|
+
} from "#dms-ui/app/build/composables/actions/rowNavigation";
|
|
16
|
+
import {
|
|
17
|
+
closeRowSidePanel,
|
|
18
|
+
useRowSidePanelContent,
|
|
19
|
+
} from "../../composables/layout/rowSidePanel";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_POSITION_KEY = "side_panel.position";
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
const { processI18n } = useTranslation();
|
|
25
|
+
const content = useRowSidePanelContent();
|
|
26
|
+
|
|
27
|
+
const navigation = computed(() => {
|
|
28
|
+
const current = content.value;
|
|
29
|
+
if (!current?.row || !current.source) return undefined;
|
|
30
|
+
return rowNavigation(current.source, current.row, current.showRow);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const position = computed(() => {
|
|
34
|
+
if (!navigation.value) return undefined;
|
|
35
|
+
const params = {
|
|
36
|
+
index: navigation.value.index + 1,
|
|
37
|
+
total: navigation.value.total,
|
|
38
|
+
};
|
|
39
|
+
const label = content.value?.positionLabel;
|
|
40
|
+
return label ? processI18n(label, params) : t(DEFAULT_POSITION_KEY, params);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const componentProps = computed(() => ({
|
|
44
|
+
...content.value?.componentOptions,
|
|
45
|
+
rowData: content.value?.row,
|
|
46
|
+
navigation: navigation.value,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const close = () => closeRowSidePanel();
|
|
50
|
+
|
|
51
|
+
let stopKeys: Array<() => void> = [];
|
|
52
|
+
|
|
53
|
+
onMounted(() => {
|
|
54
|
+
stopKeys = [
|
|
55
|
+
listenRowSteps(() => navigation.value),
|
|
56
|
+
listenRowPanelClose(close),
|
|
57
|
+
];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Another side panel took its place, or the user signed out: the row is no
|
|
61
|
+
// longer open.
|
|
62
|
+
onBeforeUnmount(() => {
|
|
63
|
+
stopKeys.forEach((stop) => stop());
|
|
64
|
+
close();
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<div v-if="content" data-dms-row-side-panel class="flex min-h-0 flex-col">
|
|
70
|
+
<header
|
|
71
|
+
class="border-default flex shrink-0 items-start gap-2 border-b py-3 ps-4 pe-3"
|
|
72
|
+
>
|
|
73
|
+
<div class="min-w-0 flex-1 self-center">
|
|
74
|
+
<h2
|
|
75
|
+
class="text-highlighted truncate text-[15px] leading-[1.3] font-[650] tracking-[-0.015em]"
|
|
76
|
+
>
|
|
77
|
+
{{ content.title }}
|
|
78
|
+
</h2>
|
|
79
|
+
<p
|
|
80
|
+
v-if="position"
|
|
81
|
+
data-dms-row-side-panel-position
|
|
82
|
+
class="text-muted mt-0.5 text-[12.5px] tabular-nums"
|
|
83
|
+
>
|
|
84
|
+
{{ position }}
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex shrink-0 items-center gap-0.5">
|
|
88
|
+
<template v-if="navigation">
|
|
89
|
+
<UButton
|
|
90
|
+
data-dms-row-side-panel-prev
|
|
91
|
+
icon="i-ph-caret-up"
|
|
92
|
+
color="neutral"
|
|
93
|
+
variant="ghost"
|
|
94
|
+
size="sm"
|
|
95
|
+
square
|
|
96
|
+
:aria-label="t('side_panel.previous')"
|
|
97
|
+
:disabled="!navigation.hasPrev"
|
|
98
|
+
@click="navigation.prev()"
|
|
99
|
+
/>
|
|
100
|
+
<UButton
|
|
101
|
+
data-dms-row-side-panel-next
|
|
102
|
+
icon="i-ph-caret-down"
|
|
103
|
+
color="neutral"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
size="sm"
|
|
106
|
+
square
|
|
107
|
+
:aria-label="t('side_panel.next')"
|
|
108
|
+
:disabled="!navigation.hasNext"
|
|
109
|
+
@click="navigation.next()"
|
|
110
|
+
/>
|
|
111
|
+
</template>
|
|
112
|
+
<UButton
|
|
113
|
+
data-dms-row-side-panel-close
|
|
114
|
+
icon="i-ph-x"
|
|
115
|
+
color="neutral"
|
|
116
|
+
variant="ghost"
|
|
117
|
+
size="sm"
|
|
118
|
+
square
|
|
119
|
+
:aria-label="t('side_panel.close')"
|
|
120
|
+
@click="close"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
</header>
|
|
124
|
+
<div class="min-h-0 flex-1 overflow-y-auto p-4">
|
|
125
|
+
<Suspense>
|
|
126
|
+
<component
|
|
127
|
+
:is="content.component"
|
|
128
|
+
:key="content.rowKey"
|
|
129
|
+
v-bind="componentProps"
|
|
130
|
+
/>
|
|
131
|
+
</Suspense>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type Component, type ShallowRef, shallowRef } from "vue";
|
|
2
|
+
import type { RowNavigationSource } from "#dms-ui/app/build/composables/actions/rowNavigation";
|
|
3
|
+
import {
|
|
4
|
+
registerSidePanel,
|
|
5
|
+
unregisterSidePanel,
|
|
6
|
+
} from "../../../composables/useAppSidePanels";
|
|
7
|
+
import {
|
|
8
|
+
isSidePanelOpen,
|
|
9
|
+
setOpenSidePanel,
|
|
10
|
+
useSidePanelPreferences,
|
|
11
|
+
} from "./sidePanelState";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Id of the side panel a `sidePanel` action target opens. It is registered
|
|
15
|
+
* only while a row is open, so a side panel cookie still naming it after a
|
|
16
|
+
* reload shows nothing.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export const ROW_SIDE_PANEL_ID = "dms:row-side-panel";
|
|
21
|
+
/** Registered name of `RowSidePanel.vue` (the layer's `Dms` prefix applied). */
|
|
22
|
+
const ROW_SIDE_PANEL_COMPONENT = "DmsRowSidePanel";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the row side panel shows: the header the DMS draws, the module's
|
|
26
|
+
* component and the row it receives.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export interface RowSidePanelContent {
|
|
31
|
+
/** One run of an action: rows shown by stepping share it. */
|
|
32
|
+
session: symbol;
|
|
33
|
+
/** The table (or button bar) that opened the panel; see {@link rowSidePanelOwnerKey}. */
|
|
34
|
+
ownerKey: string;
|
|
35
|
+
title: string;
|
|
36
|
+
/** i18n key (`$`-prefixed) of the position, receiving `{ index, total }`. */
|
|
37
|
+
positionLabel?: string;
|
|
38
|
+
component: Component;
|
|
39
|
+
/** Props of the component besides `rowData` and `navigation`. */
|
|
40
|
+
componentOptions: Record<string, unknown>;
|
|
41
|
+
row?: Data;
|
|
42
|
+
/** Id of `row`: the key the component is mounted under, the row the table highlights. */
|
|
43
|
+
rowKey?: string;
|
|
44
|
+
/** The rows shown, which the panel steps through (J / K). */
|
|
45
|
+
source?: RowNavigationSource;
|
|
46
|
+
/** Shows another row of the same run in place. */
|
|
47
|
+
showRow: (row: Data) => void;
|
|
48
|
+
/** Runs once the panel closes, whatever closed it. */
|
|
49
|
+
onClose?: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Written on the client only (a click, a table mounting on a deep link), never
|
|
53
|
+
// while the server renders a request, so a module-level ref is safe here. It
|
|
54
|
+
// holds a component and callbacks, which no shared state may serialize.
|
|
55
|
+
const content = shallowRef<RowSidePanelContent | null>(null);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The key a table's open row is kept under, which the table compares to
|
|
59
|
+
* highlight it.
|
|
60
|
+
*
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
export function rowSidePanelOwnerKey(
|
|
64
|
+
pageId: string,
|
|
65
|
+
componentId: string,
|
|
66
|
+
): string {
|
|
67
|
+
return `${pageId}-${componentId}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @internal */
|
|
71
|
+
export function useRowSidePanelContent(): Readonly<
|
|
72
|
+
ShallowRef<RowSidePanelContent | null>
|
|
73
|
+
> {
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Shows `next` in the row side panel, opening it (and closing any other side
|
|
79
|
+
* panel) when it is closed. Another run's content gives way: its `onClose`
|
|
80
|
+
* runs first.
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
export function openRowSidePanel(next: RowSidePanelContent): void {
|
|
85
|
+
const current = content.value;
|
|
86
|
+
if (current && current.session !== next.session) current.onClose?.();
|
|
87
|
+
content.value = next;
|
|
88
|
+
if (current?.title !== next.title) {
|
|
89
|
+
registerSidePanel({
|
|
90
|
+
id: ROW_SIDE_PANEL_ID,
|
|
91
|
+
component: ROW_SIDE_PANEL_COMPONENT,
|
|
92
|
+
ariaLabel: next.title,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
setOpenSidePanel(useSidePanelPreferences(), ROW_SIDE_PANEL_ID);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function closeContent(current: RowSidePanelContent): void {
|
|
99
|
+
content.value = null;
|
|
100
|
+
unregisterSidePanel(ROW_SIDE_PANEL_ID);
|
|
101
|
+
const preferences = useSidePanelPreferences();
|
|
102
|
+
if (isSidePanelOpen(preferences, ROW_SIDE_PANEL_ID)) {
|
|
103
|
+
setOpenSidePanel(preferences, null);
|
|
104
|
+
}
|
|
105
|
+
current.onClose?.();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Closes the row side panel; with `session`, only while it still shows that
|
|
110
|
+
* run.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function closeRowSidePanel(session?: symbol): void {
|
|
115
|
+
const current = content.value;
|
|
116
|
+
if (!current || (session && current.session !== session)) return;
|
|
117
|
+
closeContent(current);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Closes the row side panel when `ownerKey` opened it: the table it belongs
|
|
122
|
+
* to is gone.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
export function closeRowSidePanelOf(ownerKey: string): void {
|
|
127
|
+
const current = content.value;
|
|
128
|
+
if (current?.ownerKey === ownerKey) closeContent(current);
|
|
129
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { withoutTrailingSlash } from "ufo";
|
|
3
3
|
import { holdPageHeaderActions } from "../composables/layout/usePageHeaderActions";
|
|
4
4
|
import { useDevReloading } from "../build/composables/dev-reload/useDevReloadHolder";
|
|
5
|
+
import { usePageLiveHost } from "#dms-ui/app/build/composables/live/usePageLiveHost";
|
|
5
6
|
|
|
6
7
|
defineOptions({ inheritAttrs: false });
|
|
7
8
|
|
|
@@ -216,6 +217,11 @@ const { permissions: userPermissions } = usePermissions();
|
|
|
216
217
|
const realtimePageId = computed(() => pagelayoutMetadata.value?.fullId ?? "");
|
|
217
218
|
const realtime = usePageRealtime(realtimePageId);
|
|
218
219
|
providePageRealtime(realtime);
|
|
220
|
+
// Before the blocks set up: their refetches go through the page's live gate.
|
|
221
|
+
usePageLiveHost(() => ({
|
|
222
|
+
pageId: pagelayoutMetadata.value?.fullId,
|
|
223
|
+
live: pagelayoutMetadata.value?.live,
|
|
224
|
+
}));
|
|
219
225
|
|
|
220
226
|
function buildPageSetupContext(
|
|
221
227
|
pageInfo: PageInfo,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BlockText } from "#dms-core/app/types/composed-text";
|
|
2
2
|
import type { BlockAction } from "#dms-ui/app/build/components/blocks/BlockActions.vue";
|
|
3
|
+
import type { PageLiveOptions } from "#dms-ui/app/types/live";
|
|
3
4
|
|
|
4
5
|
export type MenuItemType = "label" | "link";
|
|
5
6
|
// MenuItemVariant / MenuItemStatus / MenuItemQuery live in dms-core, the layer
|
|
@@ -28,6 +29,8 @@ export interface MenuOptions {
|
|
|
28
29
|
status?: MenuItemStatus;
|
|
29
30
|
badge?: string;
|
|
30
31
|
bypassTenantAccessGate?: boolean;
|
|
32
|
+
/** The page's Direct / Pause valve (see `PageLiveOptions`). */
|
|
33
|
+
live?: PageLiveOptions;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface CategoryInfo extends Omit<MenuOptions, "category"> {
|
|
@@ -899,6 +899,10 @@
|
|
|
899
899
|
"close": "Ne plus afficher"
|
|
900
900
|
},
|
|
901
901
|
"side_panel": {
|
|
902
|
-
"resize": "Redimensionner le panneau"
|
|
902
|
+
"resize": "Redimensionner le panneau",
|
|
903
|
+
"position": "{index} sur {total}",
|
|
904
|
+
"previous": "Précédent",
|
|
905
|
+
"next": "Suivant",
|
|
906
|
+
"close": "Fermer"
|
|
903
907
|
}
|
|
904
908
|
}
|
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
FormPageUrls,
|
|
38
38
|
} from "../../composables/table-view/useTableViewConfig";
|
|
39
39
|
import type { CustomRowAction } from "../../../types/row-action";
|
|
40
|
+
import type { TableViewRowClick } from "../../utils/rowClickTrigger";
|
|
40
41
|
import type { RowActionConfig } from "#dms-core/app/types/row-action";
|
|
41
42
|
import type { TableTabItem } from "./Tabs.vue";
|
|
42
43
|
import type { ColumnDisplay } from "../../composables/data-types/useColumnValueRenderer";
|
|
@@ -149,6 +150,13 @@ export interface TableProps<T> {
|
|
|
149
150
|
|
|
150
151
|
rowIdKey?: string;
|
|
151
152
|
rowActions?: TableRowActionOptions;
|
|
153
|
+
/**
|
|
154
|
+
* What runs a row's click action: a double-click (the default) or a single
|
|
155
|
+
* click (`"single"`). See `isRowClickTrigger`.
|
|
156
|
+
*/
|
|
157
|
+
rowClick?: TableViewRowClick;
|
|
158
|
+
/** Id of the row a side panel has open: the grid highlights it. */
|
|
159
|
+
openRowId?: string;
|
|
152
160
|
customNavItems?: NavItem[];
|
|
153
161
|
tabs?: TableTabItem[];
|
|
154
162
|
customButtons?: CustomButton[];
|
|
@@ -412,6 +420,7 @@ import {
|
|
|
412
420
|
} from "../../composables/table/constants";
|
|
413
421
|
import { mergeColumnOrder } from "../../composables/table/utils/columnOrder";
|
|
414
422
|
import { EYEBROW_CLASS } from "../../utils/eyebrow";
|
|
423
|
+
import { isRowClickTrigger } from "../../utils/rowClickTrigger";
|
|
415
424
|
|
|
416
425
|
// Same color as the surrounding card frame (.dms-card) so sticky
|
|
417
426
|
// rail/pinned cells blend in instead of showing a contrasting block.
|
|
@@ -435,6 +444,12 @@ const ROW_SELECTED_BG = "data-[selected=true]:bg-primary/10";
|
|
|
435
444
|
const ROW_SELECTED_CELL_BG =
|
|
436
445
|
"group-data-[selected=true]:bg-[color-mix(in_srgb,var(--ui-primary)_10%,var(--dms-surface-card))]";
|
|
437
446
|
|
|
447
|
+
// The row a side panel has open: the hover tint, held, and an accent rule on
|
|
448
|
+
// its leading edge.
|
|
449
|
+
const ROW_OPEN_BG =
|
|
450
|
+
"data-[open=true]:bg-elevated data-[open=true]:shadow-[inset_2px_0_0_0_var(--ui-primary)]";
|
|
451
|
+
const ROW_OPEN_CELL_BG = "group-data-[open=true]:bg-elevated";
|
|
452
|
+
|
|
438
453
|
// An expanded row keeps the hover tint and hands its bottom rule to the detail
|
|
439
454
|
// band below it.
|
|
440
455
|
const ROW_EXPANDED_BG = "data-[expanded=true]:bg-elevated";
|
|
@@ -501,7 +516,7 @@ const theme = tv({
|
|
|
501
516
|
colResizer:
|
|
502
517
|
"bg-primary/40 absolute right-0 top-0 z-10 h-full w-1 cursor-col-resize touch-none select-none opacity-0 hover:opacity-100",
|
|
503
518
|
|
|
504
|
-
row: `group ${ROW_SELECTED_BG} ${ROW_EXPANDED_BG}`,
|
|
519
|
+
row: `group ${ROW_SELECTED_BG} ${ROW_EXPANDED_BG} ${ROW_OPEN_BG}`,
|
|
505
520
|
rowCell: `${FIRST_ROW_CELL_GUTTER} border-b-muted border-b h-11 px-3.5 py-0 text-[13px] last:pe-2.5 in-[tr:last-child]:border-b-0 group-data-[expanded=true]:border-b-transparent`,
|
|
506
521
|
rowInternal: "transition-opacity duration-150",
|
|
507
522
|
rowContainer: "relative",
|
|
@@ -560,11 +575,11 @@ const theme = tv({
|
|
|
560
575
|
pinned: {
|
|
561
576
|
left: {
|
|
562
577
|
headCell: `${HEADER_MATCH_BG} sticky z-10 shadow-[2px_0_0_0_rgba(0,0,0,0.06)]`,
|
|
563
|
-
rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[2px_0_0_0_rgba(0,0,0,0.06)]`,
|
|
578
|
+
rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} ${ROW_OPEN_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[2px_0_0_0_rgba(0,0,0,0.06)]`,
|
|
564
579
|
},
|
|
565
580
|
right: {
|
|
566
581
|
headCell: `${HEADER_MATCH_BG} sticky z-10 shadow-[-2px_0_0_0_rgba(0,0,0,0.06)]`,
|
|
567
|
-
rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[-2px_0_0_0_rgba(0,0,0,0.06)]`,
|
|
582
|
+
rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} ${ROW_OPEN_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[-2px_0_0_0_rgba(0,0,0,0.06)]`,
|
|
568
583
|
},
|
|
569
584
|
},
|
|
570
585
|
// Dense lists: 36px rows under a 32px header band.
|
|
@@ -1205,7 +1220,7 @@ const rowClickAction = (row: T): RowClickAction | undefined =>
|
|
|
1205
1220
|
// lights up always opens something.
|
|
1206
1221
|
const isRowClickable = (row: T): boolean => rowClickAction(row) !== undefined;
|
|
1207
1222
|
|
|
1208
|
-
const
|
|
1223
|
+
const runRowClickAction = (row: T) => {
|
|
1209
1224
|
const action = rowClickAction(row);
|
|
1210
1225
|
if (!action) return;
|
|
1211
1226
|
if (typeof action === "string") {
|
|
@@ -1215,6 +1230,19 @@ const handleRowDoubleClick = (row: T) => {
|
|
|
1215
1230
|
props.onCustomRowAction?.(action, row);
|
|
1216
1231
|
};
|
|
1217
1232
|
|
|
1233
|
+
// Both events reach the row: `rowClick` tells which one runs its action.
|
|
1234
|
+
const handleRowPointer = (event: MouseEvent, row: T) => {
|
|
1235
|
+
if (!isRowClickTrigger(props.rowClick, event)) return;
|
|
1236
|
+
runRowClickAction(row);
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
const isOpenRow = (row: T): boolean =>
|
|
1240
|
+
props.openRowId !== undefined &&
|
|
1241
|
+
String(get(row, rowIdKey)) === props.openRowId;
|
|
1242
|
+
|
|
1243
|
+
const isSingleClickRow = (row: T): boolean =>
|
|
1244
|
+
props.rowClick === "single" && isRowClickable(row);
|
|
1245
|
+
|
|
1218
1246
|
const presenceRailBackground = (row: T): string => {
|
|
1219
1247
|
if (getRowPresence(row)) return PRESENCE_RAIL_ACTIVE_BG;
|
|
1220
1248
|
if (props.loading || !isRowClickable(row)) return PANEL_MATCH_BG;
|
|
@@ -1339,6 +1367,10 @@ defineShortcuts({
|
|
|
1339
1367
|
/>
|
|
1340
1368
|
</template>
|
|
1341
1369
|
|
|
1370
|
+
<!-- A strip the table view adds over its rows (the live hold's "N new"),
|
|
1371
|
+
shown whichever chrome the display keeps. -->
|
|
1372
|
+
<slot name="notice" />
|
|
1373
|
+
|
|
1342
1374
|
<slot name="body" :table="table">
|
|
1343
1375
|
<section
|
|
1344
1376
|
ref="tableRoot"
|
|
@@ -1472,20 +1504,24 @@ defineShortcuts({
|
|
|
1472
1504
|
<tr
|
|
1473
1505
|
:data-selected="row.getIsSelected()"
|
|
1474
1506
|
:data-expanded="row.getIsExpanded()"
|
|
1507
|
+
:data-open="isOpenRow(row.original) || undefined"
|
|
1508
|
+
:aria-current="isOpenRow(row.original) || undefined"
|
|
1475
1509
|
:data-presence="!!getRowPresence(row.original)"
|
|
1476
1510
|
:title="
|
|
1477
1511
|
getRowPresence(row.original)
|
|
1478
1512
|
? presenceTooltipText(getRowPresence(row.original)!)
|
|
1479
1513
|
: undefined
|
|
1480
1514
|
"
|
|
1481
|
-
:class="
|
|
1515
|
+
:class="[
|
|
1482
1516
|
uiTable.row({
|
|
1483
1517
|
loading,
|
|
1484
1518
|
rowClickable: isRowClickable(row.original),
|
|
1485
1519
|
presence: !!getRowPresence(row.original),
|
|
1486
|
-
})
|
|
1487
|
-
|
|
1488
|
-
|
|
1520
|
+
}),
|
|
1521
|
+
{ 'cursor-pointer': isSingleClickRow(row.original) },
|
|
1522
|
+
]"
|
|
1523
|
+
@click="handleRowPointer($event, row.original)"
|
|
1524
|
+
@dblclick="handleRowPointer($event, row.original)"
|
|
1489
1525
|
@mouseenter="handleRowHover(row)"
|
|
1490
1526
|
@mouseleave="handleRowLeave"
|
|
1491
1527
|
@dragover="reorderHandles.allowDrop"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The strip a `liveHold` table shows while its page is paused and events came
|
|
4
|
+
* in: "N new · Show". Show reads the rows once; the page stays paused.
|
|
5
|
+
*/
|
|
6
|
+
defineProps<{
|
|
7
|
+
/** Events the table received since its rows were held or last shown. */
|
|
8
|
+
count: number;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{ show: [] }>();
|
|
12
|
+
|
|
13
|
+
const { t } = useI18n();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div
|
|
18
|
+
role="status"
|
|
19
|
+
class="dms-live-hold-banner text-highlighted flex items-center gap-2.5 border-b border-(--dms-info-line) bg-(--dms-info-tint) py-[7px] ps-[18px] pe-3.5 text-[13px]"
|
|
20
|
+
>
|
|
21
|
+
<UIcon name="i-ph-pause-circle" class="text-info size-4 shrink-0" />
|
|
22
|
+
<span class="min-w-0">
|
|
23
|
+
{{ t("dms.live.new_rows", { count }, count) }}
|
|
24
|
+
</span>
|
|
25
|
+
<span aria-hidden="true" class="text-muted">·</span>
|
|
26
|
+
<UButton
|
|
27
|
+
:label="t('dms.live.show_new')"
|
|
28
|
+
color="neutral"
|
|
29
|
+
variant="link"
|
|
30
|
+
size="xs"
|
|
31
|
+
class="p-0 font-medium"
|
|
32
|
+
@click="emit('show')"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
@@ -83,3 +83,20 @@ export function listenRowSteps(current: () => RowNavigation | undefined) {
|
|
|
83
83
|
window.addEventListener("keydown", onKey);
|
|
84
84
|
return () => window.removeEventListener("keydown", onKey);
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
const CLOSE_KEY = "Escape";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Escape closes a row's side panel, unless the user types in a field or a
|
|
91
|
+
* layer above it (a dialog, a menu) already handled the key. Returns the
|
|
92
|
+
* listener's removal.
|
|
93
|
+
*/
|
|
94
|
+
export function listenRowPanelClose(close: () => void) {
|
|
95
|
+
const onKey = (event: KeyboardEvent) => {
|
|
96
|
+
if (event.key !== CLOSE_KEY || event.defaultPrevented) return;
|
|
97
|
+
if (isTypingTarget(event.target)) return;
|
|
98
|
+
close();
|
|
99
|
+
};
|
|
100
|
+
window.addEventListener("keydown", onKey);
|
|
101
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
102
|
+
}
|