@antelopejs/dms 0.7.8 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/api-base-url.d.ts +1 -0
- package/dist/implementations/dms/api-base-url.js +6 -0
- package/dist/implementations/dms/api-base-url.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.js +7 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/page.js +8 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +25 -0
- package/dist/test/attachment-host/capabilities.js +53 -1
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +13 -0
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/api-base-url.test.d.ts +1 -0
- package/dist/test/unit/api-base-url.test.js +31 -0
- package/dist/test/unit/api-base-url.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
- package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
- package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
- package/frontend-vue/tests/block-not-found.test.ts +220 -0
- package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
- package/frontend-vue/tests/component-id.test.ts +18 -0
- package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
- package/frontend-vue/tests/guard.test.ts +242 -0
- package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
- package/frontend-vue/tests/live-toggle.test.ts +87 -1
- package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
- package/frontend-vue/tests/page-live-parity.test.ts +367 -0
- package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
- package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
- package/frontend-vue/tests/period-selector-url.test.ts +84 -6
- package/frontend-vue/tests/period-url-query.test.ts +52 -0
- package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
- package/frontend-vue/tests/row-side-panel.test.ts +109 -10
- package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
- package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
- package/frontend-vue/tests/table-density-menu.test.ts +4 -3
- package/frontend-vue/tests/table-live-hold.test.ts +95 -5
- package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
- package/frontend-vue/tests/table-row-hover.test.ts +32 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
- package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
- package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
- package/frontend-vue/tests/table-view-views.test.ts +56 -15
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -3
- package/skills/dms-module/SKILL.md +5 -0
- package/skills/dms-pages/REFERENCE.md +23 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { usePermissionPreview } from "#dms-core/app/build/composables/auth/usePermissionPreview";
|
|
3
3
|
import { childLayoutPath } from "#dms-core/app/build/utils/permission-preview";
|
|
4
|
+
import { childComponentId } from "../utils/componentId";
|
|
4
5
|
import {
|
|
5
6
|
GRID_DECLARED_COLUMNS,
|
|
6
7
|
type GridDeclaredColumns,
|
|
@@ -111,7 +112,7 @@ provide<GridDeclaredColumns>(GRID_DECLARED_COLUMNS, {
|
|
|
111
112
|
<DmsRecursiveComponent
|
|
112
113
|
:component="child"
|
|
113
114
|
:page-id="pageId"
|
|
114
|
-
:component-id="
|
|
115
|
+
:component-id="childComponentId(componentId, child.id)"
|
|
115
116
|
:route-params="routeParams"
|
|
116
117
|
:layout-path="childLayoutPath(layoutPath, child.id)"
|
|
117
118
|
/>
|
|
@@ -120,7 +121,7 @@ provide<GridDeclaredColumns>(GRID_DECLARED_COLUMNS, {
|
|
|
120
121
|
v-else
|
|
121
122
|
:component="child"
|
|
122
123
|
:page-id="pageId"
|
|
123
|
-
:component-id="
|
|
124
|
+
:component-id="childComponentId(componentId, child.id)"
|
|
124
125
|
:route-params="routeParams"
|
|
125
126
|
:layout-path="childLayoutPath(layoutPath, child.id)"
|
|
126
127
|
/>
|
|
@@ -135,7 +136,7 @@ provide<GridDeclaredColumns>(GRID_DECLARED_COLUMNS, {
|
|
|
135
136
|
<DmsRecursiveComponent
|
|
136
137
|
:component="child"
|
|
137
138
|
:page-id="pageId"
|
|
138
|
-
:component-id="
|
|
139
|
+
:component-id="childComponentId(componentId, child.id)"
|
|
139
140
|
:route-params="routeParams"
|
|
140
141
|
:layout-path="childLayoutPath(layoutPath, child.id)"
|
|
141
142
|
/>
|
|
@@ -144,7 +145,7 @@ provide<GridDeclaredColumns>(GRID_DECLARED_COLUMNS, {
|
|
|
144
145
|
v-else
|
|
145
146
|
:component="child"
|
|
146
147
|
:page-id="pageId"
|
|
147
|
-
:component-id="
|
|
148
|
+
:component-id="childComponentId(componentId, child.id)"
|
|
148
149
|
:route-params="routeParams"
|
|
149
150
|
:layout-path="childLayoutPath(layoutPath, child.id)"
|
|
150
151
|
/>
|
|
@@ -14,6 +14,8 @@ interface Props {
|
|
|
14
14
|
iconColor?: Tone;
|
|
15
15
|
title?: string;
|
|
16
16
|
subtitle?: string;
|
|
17
|
+
/** Third line, dimmed and mono (an entry's evidence). */
|
|
18
|
+
detail?: string;
|
|
17
19
|
/** Trailing meta, usually the time. */
|
|
18
20
|
trailing?: string;
|
|
19
21
|
/** Render title/subtitle in mono (for paths, queries…). */
|
|
@@ -37,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
37
39
|
iconColor: "neutral",
|
|
38
40
|
title: undefined,
|
|
39
41
|
subtitle: undefined,
|
|
42
|
+
detail: undefined,
|
|
40
43
|
trailing: undefined,
|
|
41
44
|
mono: false,
|
|
42
45
|
unread: false,
|
|
@@ -58,6 +61,7 @@ const hasTitle = computed(() => !!props.title || !!slots.default);
|
|
|
58
61
|
:icon="props.icon"
|
|
59
62
|
:tone="props.iconColor"
|
|
60
63
|
:meta="meta"
|
|
64
|
+
:detail="props.detail"
|
|
61
65
|
:mono="props.mono"
|
|
62
66
|
:unread="props.unread || undefined"
|
|
63
67
|
:trailing="props.trailing"
|
|
@@ -19,13 +19,19 @@ import {
|
|
|
19
19
|
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
20
20
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
21
21
|
import type { BlockText } from "../../../../dms-core/app/types/composed-text";
|
|
22
|
-
import
|
|
22
|
+
import DmsBlockStatus, {
|
|
23
|
+
type BlockEmptyText,
|
|
24
|
+
} from "../../build/components/blocks/BlockStatus.vue";
|
|
23
25
|
|
|
24
26
|
// The renderer's props are optional: DmsActivityFeed is both the backend
|
|
25
27
|
// `ActivityFeed` block (`dms-activity-feed-block`) and a template component.
|
|
26
28
|
interface ActivityFeedProps extends Partial<DefaultComponentProps> {
|
|
27
29
|
/** Eyebrow title of the card head ("Activity"). */
|
|
28
30
|
title?: string;
|
|
31
|
+
/** Count chip beside the title; a `count` the source answers wins. */
|
|
32
|
+
count?: number;
|
|
33
|
+
/** Soft badge in the card head, before the actions ("Last 24 h"). */
|
|
34
|
+
badge?: BlockText;
|
|
29
35
|
/** Entries shown when no data source is set. */
|
|
30
36
|
items?: ActivityFeedItem[];
|
|
31
37
|
/** Endpoint answering `{ items }`, newest first. */
|
|
@@ -65,6 +71,8 @@ interface ActivityFeedProps extends Partial<DefaultComponentProps> {
|
|
|
65
71
|
|
|
66
72
|
const props = withDefaults(defineProps<ActivityFeedProps>(), {
|
|
67
73
|
title: undefined,
|
|
74
|
+
count: undefined,
|
|
75
|
+
badge: undefined,
|
|
68
76
|
items: undefined,
|
|
69
77
|
fetchUrl: undefined,
|
|
70
78
|
fetchUrlMethod: undefined,
|
|
@@ -91,8 +99,10 @@ const { processText } = useComposedText();
|
|
|
91
99
|
|
|
92
100
|
const {
|
|
93
101
|
items: list,
|
|
102
|
+
count: fetchedCount,
|
|
94
103
|
isPending,
|
|
95
104
|
hasError,
|
|
105
|
+
notFound,
|
|
96
106
|
refresh,
|
|
97
107
|
} = useBlockItems<ActivityFeedItem>({
|
|
98
108
|
items: () => props.items,
|
|
@@ -129,6 +139,17 @@ const trailingOf = (item: ActivityFeedItem): string | undefined => {
|
|
|
129
139
|
const textOf = (item: ActivityFeedItem, text: BlockText): string =>
|
|
130
140
|
processText(activityText(text, item.params));
|
|
131
141
|
|
|
142
|
+
const detailOf = (item: ActivityFeedItem): string | undefined =>
|
|
143
|
+
item.detail ? textOf(item, item.detail) : undefined;
|
|
144
|
+
|
|
145
|
+
const headCount = computed(() => fetchedCount.value ?? props.count);
|
|
146
|
+
const badgeText = computed(() =>
|
|
147
|
+
props.badge ? processText(props.badge) : undefined,
|
|
148
|
+
);
|
|
149
|
+
const hasHeadActions = computed(
|
|
150
|
+
() => props.actions.length > 0 || !!badgeText.value,
|
|
151
|
+
);
|
|
152
|
+
|
|
132
153
|
const metaOf = (item: ActivityFeedItem): string | undefined =>
|
|
133
154
|
item.meta?.length
|
|
134
155
|
? item.meta.map((entry) => textOf(item, entry)).join(" · ")
|
|
@@ -186,6 +207,7 @@ const wrapperProps = computed(() =>
|
|
|
186
207
|
? {
|
|
187
208
|
padded: false,
|
|
188
209
|
title: props.title ? processI18n(props.title) : undefined,
|
|
210
|
+
count: headCount.value,
|
|
189
211
|
class: "min-w-0 overflow-hidden",
|
|
190
212
|
}
|
|
191
213
|
: { class: "min-w-0" },
|
|
@@ -194,8 +216,16 @@ const wrapperProps = computed(() =>
|
|
|
194
216
|
|
|
195
217
|
<template>
|
|
196
218
|
<component :is="Wrapper" v-bind="wrapperProps">
|
|
197
|
-
<template v-if="props.card &&
|
|
219
|
+
<template v-if="props.card && hasHeadActions" #actions>
|
|
220
|
+
<UBadge
|
|
221
|
+
v-if="badgeText"
|
|
222
|
+
:label="badgeText"
|
|
223
|
+
color="primary"
|
|
224
|
+
variant="soft"
|
|
225
|
+
size="sm"
|
|
226
|
+
/>
|
|
198
227
|
<DmsBlockActions
|
|
228
|
+
v-if="props.actions.length"
|
|
199
229
|
:actions="props.actions"
|
|
200
230
|
size="xs"
|
|
201
231
|
lead-variant="ghost"
|
|
@@ -251,6 +281,13 @@ const wrapperProps = computed(() =>
|
|
|
251
281
|
]"
|
|
252
282
|
/>
|
|
253
283
|
|
|
284
|
+
<DmsBlockStatus
|
|
285
|
+
v-else-if="notFound"
|
|
286
|
+
state="not-found"
|
|
287
|
+
:card="false"
|
|
288
|
+
:not-found="notFound"
|
|
289
|
+
/>
|
|
290
|
+
|
|
254
291
|
<DmsEmptyState
|
|
255
292
|
v-else-if="!entries.length"
|
|
256
293
|
icon="i-ph-tray"
|
|
@@ -287,6 +324,7 @@ const wrapperProps = computed(() =>
|
|
|
287
324
|
:icon-color="item.tone ?? 'neutral'"
|
|
288
325
|
:title="textOf(item, item.title)"
|
|
289
326
|
:subtitle="metaOf(item)"
|
|
327
|
+
:detail="detailOf(item)"
|
|
290
328
|
:trailing="trailingOf(item)"
|
|
291
329
|
:unread="item.unread"
|
|
292
330
|
:mono="props.mono"
|
|
@@ -3,6 +3,7 @@ import { computed } from "vue";
|
|
|
3
3
|
import DmsCodeSnippet from "../code-snippet/CodeSnippet.vue";
|
|
4
4
|
import DmsBlockStatus from "../../build/components/blocks/BlockStatus.vue";
|
|
5
5
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
6
|
+
import { readFetchNotFound } from "../../build/utils/fetchNotFound";
|
|
6
7
|
import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
|
|
7
8
|
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
8
9
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
@@ -62,10 +63,14 @@ const title = computed(() =>
|
|
|
62
63
|
props.title ? processText(props.title) : undefined,
|
|
63
64
|
);
|
|
64
65
|
const showSkeleton = computed(() => isLoading.value && !data.value);
|
|
66
|
+
const notFound = computed(() =>
|
|
67
|
+
isLoading.value ? null : readFetchNotFound(error.value),
|
|
68
|
+
);
|
|
65
69
|
// A failed first load shows a retry in place of the code; a failed refetch
|
|
66
70
|
// keeps the code on screen.
|
|
67
71
|
const hasError = computed(
|
|
68
|
-
() =>
|
|
72
|
+
() =>
|
|
73
|
+
!isLoading.value && !data.value && Boolean(error.value) && !notFound.value,
|
|
69
74
|
);
|
|
70
75
|
|
|
71
76
|
const { t } = useI18n();
|
|
@@ -73,6 +78,11 @@ const { t } = useI18n();
|
|
|
73
78
|
|
|
74
79
|
<template>
|
|
75
80
|
<DmsBlockStatus v-if="hasError" state="error" @retry="refresh()" />
|
|
81
|
+
<DmsBlockStatus
|
|
82
|
+
v-else-if="notFound"
|
|
83
|
+
state="not-found"
|
|
84
|
+
:not-found="notFound"
|
|
85
|
+
/>
|
|
76
86
|
<div
|
|
77
87
|
v-else-if="showSkeleton"
|
|
78
88
|
class="border-default grid gap-2 rounded-[10px] border px-3.5 py-3"
|
|
@@ -69,7 +69,7 @@ const TRANSLATED_TYPES = new Set([undefined, "text", "status", "link"]);
|
|
|
69
69
|
|
|
70
70
|
const { processText } = useComposedText();
|
|
71
71
|
|
|
72
|
-
const { items, isPending, hasError, refresh } =
|
|
72
|
+
const { items, isPending, hasError, notFound, refresh } =
|
|
73
73
|
useBlockItems<KeyValueListBlockItem>({
|
|
74
74
|
items: () => props.items,
|
|
75
75
|
fetchUrl: props.fetchUrl,
|
|
@@ -104,7 +104,11 @@ const title = computed(() =>
|
|
|
104
104
|
props.title ? processText(props.title) : undefined,
|
|
105
105
|
);
|
|
106
106
|
const isEmpty = computed(
|
|
107
|
-
() =>
|
|
107
|
+
() =>
|
|
108
|
+
!isPending.value &&
|
|
109
|
+
!hasError.value &&
|
|
110
|
+
!notFound.value &&
|
|
111
|
+
resolvedItems.value.length === 0,
|
|
108
112
|
);
|
|
109
113
|
// A card with a head gets its body padding from the list itself, so the rows'
|
|
110
114
|
// hairlines run to the same inset as the head's.
|
|
@@ -122,6 +126,12 @@ const listClass = computed(() => (props.card ? "px-[18px] py-2" : undefined));
|
|
|
122
126
|
:card="false"
|
|
123
127
|
@retry="refresh()"
|
|
124
128
|
/>
|
|
129
|
+
<DmsBlockStatus
|
|
130
|
+
v-else-if="notFound"
|
|
131
|
+
state="not-found"
|
|
132
|
+
:card="false"
|
|
133
|
+
:not-found="notFound"
|
|
134
|
+
/>
|
|
125
135
|
<DmsBlockStatus
|
|
126
136
|
v-else-if="isEmpty"
|
|
127
137
|
state="empty"
|
|
@@ -4,17 +4,29 @@ import DmsSegmented from "../segmented/Segmented.vue";
|
|
|
4
4
|
import { usePageLive } from "../../composables/live/usePageLive";
|
|
5
5
|
import { formatElapsed } from "../../build/composables/live/formatElapsed";
|
|
6
6
|
import { useLiveToggleShortcut } from "../../build/composables/live/useLiveToggleShortcut";
|
|
7
|
+
import { HIDDEN_TAB_NOTE } from "../../build/composables/live/pageLiveController";
|
|
8
|
+
import {
|
|
9
|
+
resolveConfirmText,
|
|
10
|
+
type ConfirmTranslate,
|
|
11
|
+
} from "../../build/composables/confirm/confirmDialogTexts";
|
|
12
|
+
import type { PageLiveNote, PageLiveNoteTone } from "../../types/live";
|
|
7
13
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
8
14
|
|
|
9
15
|
interface LiveToggleBlockProps extends DefaultComponentProps {
|
|
10
16
|
align?: "left" | "center" | "right";
|
|
11
17
|
/** Shows "updated N s ago". */
|
|
12
18
|
showUpdatedAt?: boolean;
|
|
19
|
+
/** `segmented` (Live / Pause segments) or `pill` (a pulsing Live pill). */
|
|
20
|
+
variant?: "segmented" | "pill";
|
|
21
|
+
/** What the paused toggle calls the missed events, given `{count}`. */
|
|
22
|
+
missedLabel?: string;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
const props = withDefaults(defineProps<LiveToggleBlockProps>(), {
|
|
16
26
|
align: "right",
|
|
17
27
|
showUpdatedAt: true,
|
|
28
|
+
variant: "segmented",
|
|
29
|
+
missedLabel: undefined,
|
|
18
30
|
});
|
|
19
31
|
|
|
20
32
|
const DIRECT = "direct";
|
|
@@ -28,8 +40,17 @@ const ALIGN_CLASSES: Record<
|
|
|
28
40
|
center: "justify-center",
|
|
29
41
|
right: "justify-end",
|
|
30
42
|
};
|
|
43
|
+
const TONE_CLASSES: Record<PageLiveNoteTone, string> = {
|
|
44
|
+
neutral: "",
|
|
45
|
+
warning: "text-warning",
|
|
46
|
+
error: "text-error",
|
|
47
|
+
};
|
|
48
|
+
const PILL_BUTTON_CLASS =
|
|
49
|
+
"focus-visible:outline-primary flex items-center gap-1.5 rounded-[5px] px-2 py-1 text-[12px] font-medium transition-colors focus-visible:outline-2";
|
|
50
|
+
const PILL_IDLE_CLASS = "text-muted hover:text-default hover:bg-elevated";
|
|
31
51
|
|
|
32
52
|
const { t } = useI18n();
|
|
53
|
+
const translate = t as ConfirmTranslate;
|
|
33
54
|
const live = usePageLive();
|
|
34
55
|
|
|
35
56
|
// The state lives in the browser (its storage, its clock): the server and the
|
|
@@ -63,30 +84,134 @@ const updatedLabel = computed(() => {
|
|
|
63
84
|
const at = live.lastUpdatedAt.value;
|
|
64
85
|
if (at === null) return "";
|
|
65
86
|
const { unit, count } = formatElapsed(now.value - at);
|
|
66
|
-
|
|
87
|
+
const updated = t(`dms.live.updated_${unit}`, { count }, count);
|
|
88
|
+
return live.isPolling.value
|
|
89
|
+
? `${updated} · ${t("dms.live.polling")}`
|
|
90
|
+
: updated;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const noteText = (note: PageLiveNote) =>
|
|
94
|
+
resolveConfirmText(note.text, note.params ?? {}, translate);
|
|
95
|
+
|
|
96
|
+
const missedText = computed(() => {
|
|
97
|
+
const count = live.missedCount.value;
|
|
98
|
+
if (count === 0) return "";
|
|
99
|
+
return props.missedLabel
|
|
100
|
+
? resolveConfirmText(props.missedLabel, { count }, translate)
|
|
101
|
+
: t("dms.live.new_rows", { count }, count);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// "Paused · typing in the Tester · 40 new requests", the detail apart: it
|
|
105
|
+
// takes its own tone.
|
|
106
|
+
const pausedText = computed(() => {
|
|
107
|
+
const reason = live.pauseReason.value;
|
|
108
|
+
return [
|
|
109
|
+
t("dms.live.paused"),
|
|
110
|
+
reason ? noteText(reason) : "",
|
|
111
|
+
missedText.value,
|
|
112
|
+
]
|
|
113
|
+
.filter((part) => part !== "")
|
|
114
|
+
.join(" · ");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const detail = computed(() => {
|
|
118
|
+
const note = live.missedDetail.value;
|
|
119
|
+
if (!note || live.missedCount.value === 0) return null;
|
|
120
|
+
return {
|
|
121
|
+
text: noteText(note),
|
|
122
|
+
toneClass: TONE_CLASSES[note.tone ?? "neutral"],
|
|
123
|
+
};
|
|
67
124
|
});
|
|
68
125
|
|
|
126
|
+
// A hidden tab resumes by itself when it shows again: no button for it.
|
|
127
|
+
const canResume = computed(
|
|
128
|
+
() =>
|
|
129
|
+
live.isPaused.value &&
|
|
130
|
+
live.pauseReason.value?.text !== HIDDEN_TAB_NOTE.text,
|
|
131
|
+
);
|
|
132
|
+
|
|
69
133
|
useLiveToggleShortcut(() => live.toggle());
|
|
70
134
|
</script>
|
|
71
135
|
|
|
72
136
|
<template>
|
|
73
137
|
<div
|
|
74
138
|
v-if="isMounted && live.isEnabled.value"
|
|
75
|
-
class="dms-live-toggle flex flex-wrap items-center gap-2.5"
|
|
76
|
-
:class="ALIGN_CLASSES[align]"
|
|
139
|
+
class="dms-live-toggle flex flex-wrap items-center gap-x-2.5 gap-y-1"
|
|
140
|
+
:class="[ALIGN_CLASSES[align], `dms-live-toggle--${variant}`]"
|
|
77
141
|
>
|
|
78
142
|
<span
|
|
79
|
-
v-if="
|
|
143
|
+
v-if="live.isPaused.value"
|
|
144
|
+
class="dms-live-toggle__status text-muted min-w-0 text-[12.5px] tabular-nums"
|
|
145
|
+
aria-live="polite"
|
|
146
|
+
>
|
|
147
|
+
{{ pausedText }}
|
|
148
|
+
<span v-if="detail" :class="detail.toneClass">· {{ detail.text }}</span>
|
|
149
|
+
</span>
|
|
150
|
+
<span
|
|
151
|
+
v-else-if="showUpdatedAt"
|
|
80
152
|
class="text-muted text-[12.5px] tabular-nums"
|
|
81
153
|
aria-live="off"
|
|
82
154
|
>
|
|
83
155
|
{{ updatedLabel }}
|
|
84
156
|
</span>
|
|
157
|
+
<div
|
|
158
|
+
v-if="variant === 'pill'"
|
|
159
|
+
role="group"
|
|
160
|
+
:aria-label="t('dms.live.aria_label')"
|
|
161
|
+
class="border-default bg-default inline-flex shrink-0 items-center gap-0.5 rounded-md border p-0.5"
|
|
162
|
+
>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
data-value="direct"
|
|
166
|
+
:aria-pressed="live.isLive.value"
|
|
167
|
+
:class="[
|
|
168
|
+
PILL_BUTTON_CLASS,
|
|
169
|
+
live.isLive.value ? 'bg-success/10 text-success' : PILL_IDLE_CLASS,
|
|
170
|
+
]"
|
|
171
|
+
@click="live.resume()"
|
|
172
|
+
>
|
|
173
|
+
<span
|
|
174
|
+
class="dms-live-toggle__dot size-2 rounded-full"
|
|
175
|
+
:class="
|
|
176
|
+
live.isLive.value
|
|
177
|
+
? 'bg-success motion-safe:animate-pulse'
|
|
178
|
+
: 'bg-(--ui-border-accented)'
|
|
179
|
+
"
|
|
180
|
+
/>
|
|
181
|
+
{{ t("dms.live.direct") }}
|
|
182
|
+
</button>
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
data-value="pause"
|
|
186
|
+
:aria-pressed="live.isPaused.value"
|
|
187
|
+
:class="[
|
|
188
|
+
PILL_BUTTON_CLASS,
|
|
189
|
+
live.isPaused.value
|
|
190
|
+
? 'bg-elevated text-highlighted'
|
|
191
|
+
: PILL_IDLE_CLASS,
|
|
192
|
+
]"
|
|
193
|
+
@click="live.pause()"
|
|
194
|
+
>
|
|
195
|
+
<UIcon name="i-ph-pause-fill" class="size-3" />
|
|
196
|
+
{{ t("dms.live.pause") }}
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
85
199
|
<DmsSegmented
|
|
200
|
+
v-else
|
|
86
201
|
v-model="mode"
|
|
87
202
|
:items="items"
|
|
88
203
|
:aria-label="t('dms.live.aria_label')"
|
|
89
204
|
size="sm"
|
|
90
205
|
/>
|
|
206
|
+
<UButton
|
|
207
|
+
v-if="variant === 'pill' && canResume"
|
|
208
|
+
data-action="resume"
|
|
209
|
+
size="xs"
|
|
210
|
+
color="neutral"
|
|
211
|
+
variant="soft"
|
|
212
|
+
icon="i-ph-play-fill"
|
|
213
|
+
:label="t('dms.live.resume')"
|
|
214
|
+
@click="live.resume()"
|
|
215
|
+
/>
|
|
91
216
|
</div>
|
|
92
217
|
</template>
|
|
@@ -94,6 +94,7 @@ const {
|
|
|
94
94
|
items: blockItems,
|
|
95
95
|
isPending,
|
|
96
96
|
hasError,
|
|
97
|
+
notFound,
|
|
97
98
|
refresh,
|
|
98
99
|
} = useBlockItems<NavCardGridItem>({
|
|
99
100
|
items: () => props.items,
|
|
@@ -161,6 +162,11 @@ const { t } = useI18n();
|
|
|
161
162
|
"
|
|
162
163
|
/>
|
|
163
164
|
<DmsBlockStatus v-if="hasError" state="error" @retry="refresh()" />
|
|
165
|
+
<DmsBlockStatus
|
|
166
|
+
v-else-if="notFound"
|
|
167
|
+
state="not-found"
|
|
168
|
+
:not-found="notFound"
|
|
169
|
+
/>
|
|
164
170
|
<DmsBlockStatus
|
|
165
171
|
v-else-if="!isPending && cards.length === 0"
|
|
166
172
|
state="empty"
|
|
@@ -61,7 +61,7 @@ const DEFAULT_SKELETON_COUNT = 4;
|
|
|
61
61
|
|
|
62
62
|
const { processText } = useComposedText();
|
|
63
63
|
|
|
64
|
-
const { items, isPending, hasError, refresh } =
|
|
64
|
+
const { items, isPending, hasError, notFound, refresh } =
|
|
65
65
|
useBlockItems<StatGroupBlockItem>({
|
|
66
66
|
items: () => props.items,
|
|
67
67
|
fetchUrl: props.fetchUrl,
|
|
@@ -85,6 +85,11 @@ const resolvedItems = computed<StatGroupItem[]>(() =>
|
|
|
85
85
|
|
|
86
86
|
<template>
|
|
87
87
|
<DmsBlockStatus v-if="hasError" state="error" @retry="refresh()" />
|
|
88
|
+
<DmsBlockStatus
|
|
89
|
+
v-else-if="notFound"
|
|
90
|
+
state="not-found"
|
|
91
|
+
:not-found="notFound"
|
|
92
|
+
/>
|
|
88
93
|
<DmsBlockStatus
|
|
89
94
|
v-else-if="!isPending && resolvedItems.length === 0"
|
|
90
95
|
state="empty"
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ConfirmImpact,
|
|
13
13
|
type ConfirmNotice,
|
|
14
14
|
type ConfirmPartialOutcome,
|
|
15
|
+
type ConfirmSecondaryAction,
|
|
15
16
|
ConfirmTextError,
|
|
16
17
|
type ConfirmValues,
|
|
17
18
|
} from "../../composables/confirm/types";
|
|
@@ -52,6 +53,11 @@ interface ConfirmModalProps {
|
|
|
52
53
|
fields?: ConfirmDialogField[];
|
|
53
54
|
/** A component drawn in the body, bound to the fields' values. */
|
|
54
55
|
component?: ComponentInfo;
|
|
56
|
+
/**
|
|
57
|
+
* A button next to cancel and confirm that leaves the modal open: its
|
|
58
|
+
* `run` is awaited, the button loading; a rejection shows in the alert.
|
|
59
|
+
*/
|
|
60
|
+
secondaryAction?: ConfirmSecondaryAction;
|
|
55
61
|
/** Checks the body fields first; `false` keeps the modal open. */
|
|
56
62
|
validate?: () => boolean | Promise<boolean>;
|
|
57
63
|
/**
|
|
@@ -88,6 +94,7 @@ const props = withDefaults(defineProps<ConfirmModalProps>(), {
|
|
|
88
94
|
confirmText: undefined,
|
|
89
95
|
fields: () => [],
|
|
90
96
|
component: undefined,
|
|
97
|
+
secondaryAction: undefined,
|
|
91
98
|
validate: undefined,
|
|
92
99
|
onConfirm: undefined,
|
|
93
100
|
});
|
|
@@ -153,6 +160,7 @@ const bodyComponent = computed(() => resolveComponentRef(props.component));
|
|
|
153
160
|
const isOpen = ref(true);
|
|
154
161
|
const typedText = ref("");
|
|
155
162
|
const isPending = ref(false);
|
|
163
|
+
const isSecondaryPending = ref(false);
|
|
156
164
|
// The failure (or partial outcome) of the last confirm, above the buttons.
|
|
157
165
|
const notice = ref<ConfirmNotice & { tone: "error" | "warning" }>();
|
|
158
166
|
// The action went part of the way: nothing left to confirm, closing it
|
|
@@ -315,6 +323,20 @@ async function handleConfirm() {
|
|
|
315
323
|
close(true);
|
|
316
324
|
}
|
|
317
325
|
|
|
326
|
+
async function handleSecondary() {
|
|
327
|
+
const action = props.secondaryAction;
|
|
328
|
+
if (!action || isPending.value || isSecondaryPending.value) return;
|
|
329
|
+
isSecondaryPending.value = true;
|
|
330
|
+
notice.value = undefined;
|
|
331
|
+
try {
|
|
332
|
+
await action.run();
|
|
333
|
+
} catch (error) {
|
|
334
|
+
notice.value = { ...errorNotice(error), tone: "error" };
|
|
335
|
+
} finally {
|
|
336
|
+
isSecondaryPending.value = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
318
340
|
function handleCancel() {
|
|
319
341
|
if (isPending.value) return;
|
|
320
342
|
close(isSettled.value);
|
|
@@ -463,6 +485,17 @@ function handleCancel() {
|
|
|
463
485
|
|
|
464
486
|
<template #footer>
|
|
465
487
|
<div class="flex w-full items-center justify-end gap-2">
|
|
488
|
+
<UButton
|
|
489
|
+
v-if="props.secondaryAction"
|
|
490
|
+
:label="props.secondaryAction.label"
|
|
491
|
+
:icon="props.secondaryAction.icon"
|
|
492
|
+
variant="ghost"
|
|
493
|
+
color="neutral"
|
|
494
|
+
:loading="isSecondaryPending"
|
|
495
|
+
:disabled="isPending"
|
|
496
|
+
data-confirm-action="secondary"
|
|
497
|
+
@click="handleSecondary"
|
|
498
|
+
/>
|
|
466
499
|
<DmsModalEscHint
|
|
467
500
|
class="max-sm:hidden"
|
|
468
501
|
:label="
|
|
@@ -17,17 +17,35 @@ interface ChoiceCardsProps {
|
|
|
17
17
|
/** Several cards picked: the value is the list of theirs. */
|
|
18
18
|
multiple?: boolean;
|
|
19
19
|
disabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Cards per row once the field is 480px wide; one column below it.
|
|
22
|
+
* Defaults to 2: three options then wrap 2 + 1, `3` keeps them on one row.
|
|
23
|
+
*/
|
|
24
|
+
columns?: number;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
const
|
|
27
|
+
const DEFAULT_COLUMNS = 2;
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<ChoiceCardsProps>(), {
|
|
30
|
+
id: undefined,
|
|
31
|
+
multiple: false,
|
|
32
|
+
disabled: false,
|
|
33
|
+
columns: DEFAULT_COLUMNS,
|
|
34
|
+
});
|
|
23
35
|
const model = defineModel<ChoiceValue | ChoiceValue[] | null | undefined>();
|
|
24
36
|
|
|
25
37
|
// Nuxt UI's choice cards, as the v2 theme draws them: the options side by
|
|
26
|
-
// side, wrapping, each with its description.
|
|
38
|
+
// side, wrapping, each with its description. The column count is a custom
|
|
39
|
+
// property, so any `columns` needs no class of its own.
|
|
27
40
|
const CARDS_UI = {
|
|
28
|
-
fieldset:
|
|
41
|
+
fieldset:
|
|
42
|
+
"grid gap-2.5 @min-[480px]:grid-cols-[repeat(var(--dms-choice-columns),minmax(0,1fr))]",
|
|
29
43
|
};
|
|
30
44
|
|
|
45
|
+
const columnsStyle = computed(() => ({
|
|
46
|
+
"--dms-choice-columns": String(Math.max(1, Math.round(props.columns))),
|
|
47
|
+
}));
|
|
48
|
+
|
|
31
49
|
const items = computed(() => props.items as unknown as RadioGroupItem[]);
|
|
32
50
|
const pickedMany = computed<ChoiceValue[]>({
|
|
33
51
|
get: () => (Array.isArray(model.value) ? model.value : []),
|
|
@@ -41,7 +59,7 @@ const pickedOne = computed<ChoiceValue | undefined>({
|
|
|
41
59
|
</script>
|
|
42
60
|
|
|
43
61
|
<template>
|
|
44
|
-
<div class="@container">
|
|
62
|
+
<div class="@container" :style="columnsStyle">
|
|
45
63
|
<UCheckboxGroup
|
|
46
64
|
v-if="props.multiple"
|
|
47
65
|
:id="props.id"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
4
|
+
import DmsEmptyStateBlock from "../blocks/EmptyStateBlock.vue";
|
|
5
|
+
import {
|
|
6
|
+
type GuardCondition,
|
|
7
|
+
type GuardFallback,
|
|
8
|
+
type GuardState,
|
|
9
|
+
useGuard,
|
|
10
|
+
} from "../../build/composables/blocks/useGuard";
|
|
11
|
+
import { notFoundTexts } from "../../build/utils/fetchNotFound";
|
|
12
|
+
|
|
13
|
+
// `Guard` block (interface-dms `base/guard`): renders its children unchanged
|
|
14
|
+
// unless a route says there is nothing for them, then one EmptyState (the
|
|
15
|
+
// `fallback`, with the answer's `emptyState` laid over it) in their place.
|
|
16
|
+
// `display: contents`, so the children lay out as if the guard were not there.
|
|
17
|
+
interface GuardProps extends DefaultComponentProps {
|
|
18
|
+
fetchUrl?: string;
|
|
19
|
+
fetchUrlMethod?: string;
|
|
20
|
+
periodScope?: string;
|
|
21
|
+
realtimeTopic?: string | string[];
|
|
22
|
+
when?: GuardCondition;
|
|
23
|
+
/** Dot path of the value `empty` checks. */
|
|
24
|
+
field?: string;
|
|
25
|
+
fallback?: GuardFallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<GuardProps>(), {
|
|
29
|
+
fetchUrl: undefined,
|
|
30
|
+
fetchUrlMethod: undefined,
|
|
31
|
+
periodScope: undefined,
|
|
32
|
+
realtimeTopic: undefined,
|
|
33
|
+
when: "empty",
|
|
34
|
+
field: undefined,
|
|
35
|
+
fallback: undefined,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const { t } = useI18n();
|
|
39
|
+
|
|
40
|
+
const state = useGuard(
|
|
41
|
+
() => props,
|
|
42
|
+
() => props.routeParams,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// `notFound` draws what a block whose source answers 404 draws: the same
|
|
46
|
+
// title, the 404's message (or the same hint) and the same icon.
|
|
47
|
+
const DEFAULT_FALLBACKS: Record<
|
|
48
|
+
GuardCondition,
|
|
49
|
+
(state: GuardState) => GuardFallback
|
|
50
|
+
> = {
|
|
51
|
+
empty: () => ({ title: t("dms.blocks.empty") }),
|
|
52
|
+
notFound: ({ notFound }) => ({
|
|
53
|
+
...notFoundTexts(notFound, t),
|
|
54
|
+
variant: "no-result",
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const fallback = computed<GuardFallback | null>(() => {
|
|
59
|
+
const current = state.value;
|
|
60
|
+
if (!current) return null;
|
|
61
|
+
return {
|
|
62
|
+
...DEFAULT_FALLBACKS[current.condition](current),
|
|
63
|
+
...props.fallback,
|
|
64
|
+
...current.override,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div class="contents" data-dms-guard>
|
|
71
|
+
<DmsEmptyStateBlock v-if="fallback" v-bind="fallback" />
|
|
72
|
+
<slot v-else />
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|