@antelopejs/dms 0.7.9 → 0.7.11
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 +41 -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/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 +2 -0
- 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/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 +75 -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 +104 -98
- 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/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- 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/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/tab-slot-spacing.test.ts +8 -0
- 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-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
|
@@ -113,6 +113,8 @@ export interface TableRowActionOptions {
|
|
|
113
113
|
copyLink?: boolean | RowActionConfig;
|
|
114
114
|
hasSelection?: boolean;
|
|
115
115
|
custom?: CustomRowAction[];
|
|
116
|
+
/** Name of a row's ⋯ menu button, its `{field}` braces filled from the row. */
|
|
117
|
+
menuLabel?: string;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
export interface NavItem {
|
|
@@ -149,6 +151,8 @@ export interface TableProps<T> {
|
|
|
149
151
|
caption?: string;
|
|
150
152
|
|
|
151
153
|
rowIdKey?: string;
|
|
154
|
+
/** Field naming a row: the default name of its ⋯ menu button. */
|
|
155
|
+
labelKey?: string;
|
|
152
156
|
rowActions?: TableRowActionOptions;
|
|
153
157
|
/**
|
|
154
158
|
* What runs a row's click action: a double-click (the default) or a single
|
|
@@ -203,6 +207,8 @@ export interface TableProps<T> {
|
|
|
203
207
|
chrome?: ResolvedTableChrome;
|
|
204
208
|
/** Placeholder of the search field. */
|
|
205
209
|
searchPlaceholder?: string;
|
|
210
|
+
/** Key opening the search (`defineShortcuts` notation); `shift_f` else. */
|
|
211
|
+
searchShortcut?: string;
|
|
206
212
|
/**
|
|
207
213
|
* One-click dropdown filters of the toolbar, writing the column filters
|
|
208
214
|
* (the `columnFilters` model).
|
|
@@ -342,6 +348,10 @@ export interface TableSharedData<T> {
|
|
|
342
348
|
* included) and the toolbar search — and goes back to the first page.
|
|
343
349
|
*/
|
|
344
350
|
resetFilters: () => void;
|
|
351
|
+
/** Empties the toolbar search alone (the toolbar folds it). */
|
|
352
|
+
clearSearch: () => void;
|
|
353
|
+
/** Clears the filter chips alone, the search kept. */
|
|
354
|
+
clearFilters: () => void;
|
|
345
355
|
/** What `resetFilters` would clear (nothing: no clear action to offer). */
|
|
346
356
|
clearableFilters: ComputedRef<ClearableTableFilters>;
|
|
347
357
|
/** A search, a filter chip or a hidden filter narrows the rows. */
|
|
@@ -409,7 +419,9 @@ import TableTabs from "./Tabs.vue";
|
|
|
409
419
|
import { createTableViewDeleteShortcut } from "../../../composables/table-view/shortcuts/tableViewDelete";
|
|
410
420
|
import { FULL_TABLE_CHROME } from "../../composables/table-view/utils/chrome";
|
|
411
421
|
import {
|
|
422
|
+
clearTableColumnFilters,
|
|
412
423
|
clearTableFilters,
|
|
424
|
+
clearTableSearch,
|
|
413
425
|
clearableTableFilters,
|
|
414
426
|
isTableNarrowed,
|
|
415
427
|
type TableNarrowingState,
|
|
@@ -444,11 +456,14 @@ const ROW_SELECTED_BG = "data-[selected=true]:bg-primary/10";
|
|
|
444
456
|
const ROW_SELECTED_CELL_BG =
|
|
445
457
|
"group-data-[selected=true]:bg-[color-mix(in_srgb,var(--ui-primary)_10%,var(--dms-surface-card))]";
|
|
446
458
|
|
|
447
|
-
// The row a side panel has open: the
|
|
448
|
-
// its leading edge
|
|
459
|
+
// The row a side panel has open: the accent tint of a selected row and a 2px
|
|
460
|
+
// accent bar on its leading edge, the "current item" of the design system
|
|
461
|
+
// (a list row, a settings role). The bar is drawn by the first cell: a
|
|
462
|
+
// table row's own box shadow is not painted under its cells' backgrounds.
|
|
449
463
|
const ROW_OPEN_BG =
|
|
450
|
-
"data-[open=true]:bg-
|
|
451
|
-
const ROW_OPEN_CELL_BG =
|
|
464
|
+
"data-[open=true]:bg-primary/10 data-[open=true]:[&>td:first-child]:shadow-[inset_2px_0_0_0_var(--ui-primary)]";
|
|
465
|
+
const ROW_OPEN_CELL_BG =
|
|
466
|
+
"group-data-[open=true]:bg-[color-mix(in_srgb,var(--ui-primary)_10%,var(--dms-surface-card))]";
|
|
452
467
|
|
|
453
468
|
// An expanded row keeps the hover tint and hands its bottom rule to the detail
|
|
454
469
|
// band below it.
|
|
@@ -1079,6 +1094,13 @@ const resetFilters = () => {
|
|
|
1079
1094
|
clearTableFilters(narrowingState);
|
|
1080
1095
|
void filtersCleared.trigger();
|
|
1081
1096
|
};
|
|
1097
|
+
// Clearing the search folds the toolbar's field, as a full clear does; the
|
|
1098
|
+
// filters alone leave it as it is.
|
|
1099
|
+
const clearSearch = () => {
|
|
1100
|
+
clearTableSearch(narrowingState);
|
|
1101
|
+
void filtersCleared.trigger();
|
|
1102
|
+
};
|
|
1103
|
+
const clearFilters = () => clearTableColumnFilters(narrowingState);
|
|
1082
1104
|
const clearableFilters = computed(() => clearableTableFilters(narrowingState));
|
|
1083
1105
|
const isFiltered = computed(() => isTableNarrowed(narrowingState));
|
|
1084
1106
|
|
|
@@ -1162,6 +1184,8 @@ watchEffect(() => {
|
|
|
1162
1184
|
hasCustomColumns,
|
|
1163
1185
|
deleteFilter,
|
|
1164
1186
|
resetFilters,
|
|
1187
|
+
clearSearch,
|
|
1188
|
+
clearFilters,
|
|
1165
1189
|
clearableFilters,
|
|
1166
1190
|
isFiltered,
|
|
1167
1191
|
onFiltersCleared: filtersCleared.on,
|
|
@@ -1305,6 +1329,7 @@ defineShortcuts({
|
|
|
1305
1329
|
:on-custom-button="onCustomButton"
|
|
1306
1330
|
:chrome="resolvedChrome"
|
|
1307
1331
|
:search-placeholder="searchPlaceholder"
|
|
1332
|
+
:search-shortcut="searchShortcut"
|
|
1308
1333
|
:quick-filters="quickFilters"
|
|
1309
1334
|
:class="{
|
|
1310
1335
|
'ms-auto': !resolvedChrome.caption && !tabsInline,
|
|
@@ -91,8 +91,13 @@ interface SidePanelTargetRun extends TargetRunExtras {
|
|
|
91
91
|
title: string;
|
|
92
92
|
containerId: string;
|
|
93
93
|
rowData?: Data;
|
|
94
|
+
/** Id of a row the link names but the route answered 404 for. */
|
|
95
|
+
missingId?: string;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
const NOT_FOUND_TITLE_KEY = "side_panel.not_found.title";
|
|
99
|
+
const NOT_FOUND_DESCRIPTION_KEY = "side_panel.not_found.description";
|
|
100
|
+
|
|
96
101
|
/** A drawer or modal to open, and on what. */
|
|
97
102
|
interface ComponentTargetRun extends TargetRunExtras {
|
|
98
103
|
target: ActionTarget & { type: "modal" | "drawer" };
|
|
@@ -246,21 +251,39 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
246
251
|
return id === undefined || id === null ? undefined : String(id);
|
|
247
252
|
};
|
|
248
253
|
|
|
249
|
-
// A deep-linked action keeps the row it has open in the URL;
|
|
250
|
-
// removes it, but only the row this run wrote, on the page it
|
|
251
|
-
// closing as the user navigates leaves the next page's
|
|
252
|
-
|
|
254
|
+
// A deep-linked action keeps the row it has open in the URL, by id;
|
|
255
|
+
// `undefined` removes it, but only the row this run wrote, on the page it
|
|
256
|
+
// wrote it: closing as the user navigates leaves the next page's
|
|
257
|
+
// `?record=` alone.
|
|
258
|
+
const recordRecorder = (deepLink?: boolean) => {
|
|
253
259
|
let written: WrittenRecord | undefined;
|
|
254
|
-
return (
|
|
260
|
+
return (id?: string): void => {
|
|
255
261
|
if (!deepLink || !config.recordScope || !config.rowNavigation) return;
|
|
256
|
-
if (!
|
|
262
|
+
if (!id) {
|
|
257
263
|
clearWrittenRecord(written);
|
|
258
264
|
written = undefined;
|
|
259
265
|
return;
|
|
260
266
|
}
|
|
261
|
-
written = writeRecordId(config.recordScope,
|
|
267
|
+
written = writeRecordId(config.recordScope, id);
|
|
262
268
|
};
|
|
263
269
|
};
|
|
270
|
+
const rowRecorder = (deepLink?: boolean) => {
|
|
271
|
+
const recordId = recordRecorder(deepLink);
|
|
272
|
+
return (row?: Data): void => recordId(rowIdOf(row));
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// The header names the open row when the target words it from its fields.
|
|
276
|
+
const rowTitleOf = (target: SidePanelTarget, row?: Data) =>
|
|
277
|
+
row && target.rowTitle
|
|
278
|
+
? resolveConfirmText(target.rowTitle, row, t as ConfirmTranslate)
|
|
279
|
+
: undefined;
|
|
280
|
+
|
|
281
|
+
const notFoundOf = (target: SidePanelTarget) => ({
|
|
282
|
+
title: processI18n(target.notFound?.title || `$${NOT_FOUND_TITLE_KEY}`),
|
|
283
|
+
description: processI18n(
|
|
284
|
+
target.notFound?.description || `$${NOT_FOUND_DESCRIPTION_KEY}`,
|
|
285
|
+
),
|
|
286
|
+
});
|
|
264
287
|
|
|
265
288
|
const ownerKey = rowSidePanelOwnerKey(config.pageId, config.componentId);
|
|
266
289
|
|
|
@@ -274,36 +297,40 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
274
297
|
const componentName = run.target.component?.componentName;
|
|
275
298
|
if (!componentName) return;
|
|
276
299
|
const session = Symbol(componentName);
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
config.
|
|
300
|
+
const recordId = recordRecorder(run.deepLink && !run.selection);
|
|
301
|
+
const panel = {
|
|
302
|
+
session,
|
|
303
|
+
ownerKey,
|
|
304
|
+
title: run.title,
|
|
305
|
+
positionLabel: run.target.positionLabel,
|
|
306
|
+
component: resolveTargetComponent(componentName),
|
|
307
|
+
componentOptions: {
|
|
308
|
+
...run.target.component?.options,
|
|
309
|
+
pageId: config.pageId,
|
|
310
|
+
componentId: config.componentId,
|
|
311
|
+
containerId: run.containerId,
|
|
312
|
+
selection: run.selection,
|
|
313
|
+
onSuccessCallback: () => {
|
|
314
|
+
closeRowSidePanel(session);
|
|
315
|
+
config.refreshCallback?.();
|
|
316
|
+
},
|
|
288
317
|
},
|
|
318
|
+
onClose: () => recordId(undefined),
|
|
289
319
|
};
|
|
290
320
|
const showRow = (row?: Data) => {
|
|
291
321
|
openRowSidePanel({
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
title: run.title,
|
|
295
|
-
positionLabel: run.target.positionLabel,
|
|
296
|
-
component,
|
|
297
|
-
componentOptions,
|
|
322
|
+
...panel,
|
|
323
|
+
rowTitle: rowTitleOf(run.target, row),
|
|
298
324
|
row,
|
|
299
325
|
rowKey: rowIdOf(row),
|
|
300
326
|
source: row ? config.rowNavigation : undefined,
|
|
301
327
|
showRow,
|
|
302
|
-
onClose: () => recordRow(undefined),
|
|
303
328
|
});
|
|
304
|
-
|
|
329
|
+
recordId(rowIdOf(row));
|
|
305
330
|
};
|
|
306
|
-
showRow(run.rowData);
|
|
331
|
+
if (!run.missingId) return showRow(run.rowData);
|
|
332
|
+
openRowSidePanel({ ...panel, notFound: notFoundOf(run.target), showRow });
|
|
333
|
+
recordId(run.missingId);
|
|
307
334
|
};
|
|
308
335
|
|
|
309
336
|
// A container opened on a listed row steps through the rows shown (its
|
|
@@ -543,5 +570,26 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
543
570
|
);
|
|
544
571
|
};
|
|
545
572
|
|
|
546
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Opens a side panel action on a row its link names but its route answered
|
|
575
|
+
* 404 for: the panel says the row is gone, and closing it removes the link.
|
|
576
|
+
* Another target opens nothing: no row, no drawer.
|
|
577
|
+
*/
|
|
578
|
+
const handleMissingRow = (action: CustomRowAction, id: string) => {
|
|
579
|
+
if (action.target.type !== "sidePanel") return;
|
|
580
|
+
handleSidePanelTarget({
|
|
581
|
+
target: action.target,
|
|
582
|
+
title: processI18n(action.target.title || action.label),
|
|
583
|
+
containerId: `${config.pageId}-${config.componentId}-${action.target.type}`,
|
|
584
|
+
deepLink: isDeepLinkAction(action),
|
|
585
|
+
missingId: id,
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
handleCustomButton,
|
|
591
|
+
handleCustomRowAction,
|
|
592
|
+
handleBulkCustomAction,
|
|
593
|
+
handleMissingRow,
|
|
594
|
+
};
|
|
547
595
|
}
|
|
@@ -3,10 +3,16 @@ import { useChartFetch } from "../../../composables/chart/useChartFetch";
|
|
|
3
3
|
import { useComponentEvent } from "../../../../../dms-core/app/composables/components/useComponentEvent";
|
|
4
4
|
import { useWatch } from "../../../../../dms-core/app/composables/watch/useWatch";
|
|
5
5
|
import type { WatchAction } from "../../../../../dms-core/app/types/watch";
|
|
6
|
+
import {
|
|
7
|
+
type FetchNotFound,
|
|
8
|
+
readFetchNotFound,
|
|
9
|
+
} from "../../utils/fetchNotFound";
|
|
6
10
|
|
|
7
11
|
/** What a list block's `fetchUrl` answers with. */
|
|
8
12
|
export interface BlockItemsResponse<T> {
|
|
9
13
|
items?: T[];
|
|
14
|
+
/** How many items the source holds in all (an ActivityFeed's head count). */
|
|
15
|
+
count?: number;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
export interface UseBlockItemsOptions<T> {
|
|
@@ -31,10 +37,17 @@ export interface UseBlockItemsOptions<T> {
|
|
|
31
37
|
|
|
32
38
|
export interface UseBlockItemsReturn<T> {
|
|
33
39
|
items: ComputedRef<T[]>;
|
|
40
|
+
/** The `count` the source answered with, if it answered one. */
|
|
41
|
+
count: ComputedRef<number | undefined>;
|
|
34
42
|
/** First load of a fetched list: nothing to show yet. */
|
|
35
43
|
isPending: ComputedRef<boolean>;
|
|
36
44
|
/** The first load failed; a refetch failure keeps the last items. */
|
|
37
45
|
hasError: ComputedRef<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* The source answered 404: what the block lists does not exist (its last
|
|
48
|
+
* items are dropped). `null` otherwise. Never also `hasError`.
|
|
49
|
+
*/
|
|
50
|
+
notFound: ComputedRef<FetchNotFound | null>;
|
|
38
51
|
refresh: () => Promise<void>;
|
|
39
52
|
}
|
|
40
53
|
|
|
@@ -71,12 +84,23 @@ export function useBlockItems<T>(
|
|
|
71
84
|
const items = computed<T[]>(() =>
|
|
72
85
|
isFetched ? (data.value?.items ?? []) : (options.items() ?? []),
|
|
73
86
|
);
|
|
87
|
+
const count = computed<number | undefined>(() =>
|
|
88
|
+
typeof data.value?.count === "number" ? data.value.count : undefined,
|
|
89
|
+
);
|
|
74
90
|
const isPending = computed(
|
|
75
91
|
() => isFetched && isLoading.value && data.value === null,
|
|
76
92
|
);
|
|
93
|
+
const notFound = computed(() =>
|
|
94
|
+
isFetched && !isLoading.value ? readFetchNotFound(error.value) : null,
|
|
95
|
+
);
|
|
77
96
|
const hasError = computed(
|
|
78
|
-
() =>
|
|
97
|
+
() =>
|
|
98
|
+
isFetched &&
|
|
99
|
+
!isLoading.value &&
|
|
100
|
+
data.value === null &&
|
|
101
|
+
!!error.value &&
|
|
102
|
+
!notFound.value,
|
|
79
103
|
);
|
|
80
104
|
|
|
81
|
-
return { items, isPending, hasError, refresh };
|
|
105
|
+
return { items, count, isPending, hasError, notFound, refresh };
|
|
82
106
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from "vue";
|
|
2
|
+
import { useChartFetch } from "../../../composables/chart/useChartFetch";
|
|
3
|
+
import {
|
|
4
|
+
type FetchNotFound,
|
|
5
|
+
readFetchNotFound,
|
|
6
|
+
} from "../../utils/fetchNotFound";
|
|
7
|
+
import type { BlockAction } from "../../components/blocks/BlockActions.vue";
|
|
8
|
+
import type {
|
|
9
|
+
EmptyStateSize,
|
|
10
|
+
EmptyStateVariant,
|
|
11
|
+
} from "../../../components/empty-state/EmptyState.vue";
|
|
12
|
+
import type { Tone } from "../../../types/tone";
|
|
13
|
+
|
|
14
|
+
/** The empty state a guard shows in place of its children (interface-dms `GuardFallback`). */
|
|
15
|
+
export interface GuardFallback {
|
|
16
|
+
title?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
variant?: EmptyStateVariant;
|
|
19
|
+
icon?: string;
|
|
20
|
+
tone?: Tone;
|
|
21
|
+
hatched?: boolean;
|
|
22
|
+
size?: EmptyStateSize;
|
|
23
|
+
actions?: BlockAction[];
|
|
24
|
+
code?: { content: string; language?: string; copy?: boolean };
|
|
25
|
+
card?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What a guard checks its route for (interface-dms `GuardCondition`). */
|
|
29
|
+
export type GuardCondition = "empty" | "notFound";
|
|
30
|
+
|
|
31
|
+
/** The route a guard reads and what it checks it for (interface-dms `GuardOptions`). */
|
|
32
|
+
export interface GuardSource {
|
|
33
|
+
fetchUrl?: string;
|
|
34
|
+
fetchUrlMethod?: string;
|
|
35
|
+
periodScope?: string;
|
|
36
|
+
realtimeTopic?: string | string[];
|
|
37
|
+
when?: GuardCondition;
|
|
38
|
+
/** Dot path of the value `empty` checks; the whole answer without it. */
|
|
39
|
+
field?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the route answers: any value, maybe with an `emptyState` laid over the fallback. */
|
|
43
|
+
export interface GuardResponse {
|
|
44
|
+
emptyState?: GuardFallback;
|
|
45
|
+
[field: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Which condition held: the fallback to show, before its defaults. */
|
|
49
|
+
export interface GuardState {
|
|
50
|
+
condition: GuardCondition;
|
|
51
|
+
/** The answer's `emptyState`, when the route sent one. */
|
|
52
|
+
override?: GuardFallback;
|
|
53
|
+
/** What the 404 said, for `notFound`: its message sets the default texts. */
|
|
54
|
+
notFound?: FetchNotFound;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const FIELD_PATH_SEPARATOR = ".";
|
|
58
|
+
|
|
59
|
+
/** The value at a dot path of an answer, `undefined` where the path stops. */
|
|
60
|
+
export function readFieldPath(source: unknown, path: string): unknown {
|
|
61
|
+
return path.split(FIELD_PATH_SEPARATOR).reduce<unknown>((value, key) => {
|
|
62
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
63
|
+
return (value as Record<string, unknown>)[key];
|
|
64
|
+
}, source);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Absent, `null`, `false`, `0`, `""` or an empty list: nothing to show. */
|
|
68
|
+
export function isEmptyValue(value: unknown): boolean {
|
|
69
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
70
|
+
return !value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function answerOverride(answer: unknown): GuardFallback | undefined {
|
|
74
|
+
if (!answer || typeof answer !== "object" || Array.isArray(answer)) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const { emptyState } = answer as GuardResponse;
|
|
78
|
+
return emptyState && typeof emptyState === "object" ? emptyState : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The condition that holds, or `null` while the children show: while the
|
|
83
|
+
* route loads, after any failure other than the condition, and whenever the
|
|
84
|
+
* value is set. The route is read like a block's `fetchUrl` (URL tokens,
|
|
85
|
+
* period scope, realtime topics, page refresh); nothing is requested while
|
|
86
|
+
* a token has no value.
|
|
87
|
+
*/
|
|
88
|
+
export function useGuard(
|
|
89
|
+
options: () => GuardSource,
|
|
90
|
+
routeParams: () => Record<string, string> | undefined,
|
|
91
|
+
): ComputedRef<GuardState | null> {
|
|
92
|
+
const initial = options();
|
|
93
|
+
const { data, error } = useChartFetch<unknown>({
|
|
94
|
+
fetchUrl: initial.fetchUrl,
|
|
95
|
+
fetchUrlMethod: initial.fetchUrlMethod,
|
|
96
|
+
periodScope: initial.periodScope,
|
|
97
|
+
realtimeTopic: initial.realtimeTopic,
|
|
98
|
+
routeParams,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return computed(() => {
|
|
102
|
+
const { when = "empty", field } = options();
|
|
103
|
+
const failure = error.value;
|
|
104
|
+
if (when === "notFound") {
|
|
105
|
+
const notFound = readFetchNotFound(failure);
|
|
106
|
+
if (!notFound) return null;
|
|
107
|
+
return { condition: when, override: notFound.emptyState, notFound };
|
|
108
|
+
}
|
|
109
|
+
const answer = data.value;
|
|
110
|
+
if (failure || answer === null || answer === undefined) return null;
|
|
111
|
+
const value = field ? readFieldPath(answer, field) : answer;
|
|
112
|
+
if (!isEmptyValue(value)) return null;
|
|
113
|
+
return { condition: when, override: answerOverride(answer) };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -68,7 +68,7 @@ const isTechnical = (text: string) =>
|
|
|
68
68
|
* as it came. Undefined when it is not user-facing (an untranslated key, JSON,
|
|
69
69
|
* a stack trace…), for the caller to fall back on a generic message.
|
|
70
70
|
*/
|
|
71
|
-
const userFacingText = (
|
|
71
|
+
export const userFacingText = (
|
|
72
72
|
text: string | undefined,
|
|
73
73
|
translate: I18nTranslate,
|
|
74
74
|
): string | undefined => {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ConfirmDialog,
|
|
3
|
+
ConfirmDialogSecondaryAction,
|
|
4
|
+
ConfirmDialogSecondaryTarget,
|
|
5
|
+
} from "#dms-core/app/types/confirm-dialog";
|
|
6
|
+
import { interpolateUrl } from "#dms-core/app/utils/url-interpolation";
|
|
2
7
|
import { readTranslationKey } from "#dms-core/app/composables/translation/useTranslation";
|
|
3
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ConfirmOptions,
|
|
10
|
+
ConfirmSecondaryAction,
|
|
11
|
+
} from "../../../composables/confirm/types";
|
|
4
12
|
import { processFieldI18n } from "../../utils/fieldOptionsI18n";
|
|
5
13
|
import { interpolateLiteral } from "../../utils/literalText";
|
|
6
14
|
|
|
@@ -28,21 +36,68 @@ export function resolveConfirmText(
|
|
|
28
36
|
: t(key, params);
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
/** Runs a dialog's secondary target, its URL already filled in. */
|
|
40
|
+
export type SecondaryTargetRun = (
|
|
41
|
+
target: ConfirmDialogSecondaryTarget,
|
|
42
|
+
) => Promise<void> | void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A dialog's secondary target with its URL (and file name) filled in with
|
|
46
|
+
* the same parameters as its texts.
|
|
47
|
+
*/
|
|
48
|
+
function resolveSecondaryTarget(
|
|
49
|
+
target: ConfirmDialogSecondaryTarget,
|
|
50
|
+
params: Record<string, unknown>,
|
|
51
|
+
): ConfirmDialogSecondaryTarget {
|
|
52
|
+
const url = interpolateUrl(target.url, params);
|
|
53
|
+
if (target.type !== "download" || target.filename === undefined) {
|
|
54
|
+
return { ...target, url };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...target,
|
|
58
|
+
url,
|
|
59
|
+
filename: interpolateLiteral(target.filename, params),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveSecondaryAction(
|
|
64
|
+
action: ConfirmDialogSecondaryAction | undefined,
|
|
65
|
+
params: Record<string, unknown>,
|
|
66
|
+
t: ConfirmTranslate,
|
|
67
|
+
runTarget: SecondaryTargetRun | undefined,
|
|
68
|
+
): ConfirmSecondaryAction | undefined {
|
|
69
|
+
if (!action || !runTarget) return undefined;
|
|
70
|
+
const target = resolveSecondaryTarget(action.target, params);
|
|
71
|
+
return {
|
|
72
|
+
label: resolveConfirmText(action.label, params, t),
|
|
73
|
+
icon: action.icon,
|
|
74
|
+
run: () => runTarget(target),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
31
78
|
/**
|
|
32
79
|
* A confirmation dialog with its texts resolved, ready for `useConfirm()`:
|
|
33
80
|
* interpolated with the row's fields (`row`) and the dialog's own `params`.
|
|
81
|
+
* Its secondary action is drawn only when given `runTarget` to run it with.
|
|
34
82
|
*/
|
|
35
83
|
export function resolveConfirmDialog(
|
|
36
84
|
dialog: ConfirmDialog,
|
|
37
85
|
row: Record<string, unknown> | undefined,
|
|
38
86
|
t: ConfirmTranslate,
|
|
87
|
+
runTarget?: SecondaryTargetRun,
|
|
39
88
|
): ConfirmOptions {
|
|
40
89
|
const params = { ...row, ...dialog.params };
|
|
41
90
|
const text = (value: string | undefined) =>
|
|
42
91
|
value === undefined ? undefined : resolveConfirmText(value, params, t);
|
|
43
|
-
const { params: _params, ...rest } = dialog;
|
|
92
|
+
const { params: _params, secondaryAction, ...rest } = dialog;
|
|
44
93
|
return {
|
|
45
94
|
...rest,
|
|
95
|
+
secondaryAction: resolveSecondaryAction(
|
|
96
|
+
secondaryAction,
|
|
97
|
+
params,
|
|
98
|
+
t,
|
|
99
|
+
runTarget,
|
|
100
|
+
),
|
|
46
101
|
title: text(dialog.title) ?? "",
|
|
47
102
|
description: text(dialog.description),
|
|
48
103
|
confirmLabel: text(dialog.confirmLabel),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfirmDialogDownloadTarget,
|
|
3
|
+
ConfirmDialogLinkTarget,
|
|
4
|
+
ConfirmDialogSecondaryTarget,
|
|
5
|
+
} from "#dms-core/app/types/confirm-dialog";
|
|
6
|
+
import { downloadFile } from "#dms-core/app/utils/downloadFile";
|
|
7
|
+
import type { SecondaryTargetRun } from "./confirmDialogTexts";
|
|
8
|
+
|
|
9
|
+
/** The response a download reads: its body and its headers. */
|
|
10
|
+
export interface DownloadResponse {
|
|
11
|
+
_data?: Blob;
|
|
12
|
+
headers: Headers;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Fetches a download with the user's session (`$authFetch.raw`). */
|
|
16
|
+
export type DownloadFetch = (
|
|
17
|
+
url: string,
|
|
18
|
+
options: { responseType: "blob" },
|
|
19
|
+
) => Promise<DownloadResponse>;
|
|
20
|
+
|
|
21
|
+
const DISPOSITION_NAME = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i;
|
|
22
|
+
const FALLBACK_FILENAME = "download";
|
|
23
|
+
|
|
24
|
+
// A malformed escape is kept as written rather than failing the download.
|
|
25
|
+
function decodeName(name: string): string {
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(name);
|
|
28
|
+
} catch {
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The name a download is saved under: the declared one, else the server's
|
|
35
|
+
* `Content-Disposition` one, else the URL's last path segment.
|
|
36
|
+
*/
|
|
37
|
+
export function downloadFilename(
|
|
38
|
+
target: ConfirmDialogDownloadTarget,
|
|
39
|
+
disposition: string | null,
|
|
40
|
+
): string {
|
|
41
|
+
if (target.filename) return target.filename;
|
|
42
|
+
const served = disposition ? DISPOSITION_NAME.exec(disposition)?.[1] : "";
|
|
43
|
+
if (served) return decodeName(served.trim());
|
|
44
|
+
const path = target.url.split(/[?#]/)[0] ?? "";
|
|
45
|
+
return decodeName(path.split("/").pop() || FALLBACK_FILENAME);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function openLink(target: ConfirmDialogLinkTarget): void {
|
|
49
|
+
if (typeof window === "undefined" || !target.url) return;
|
|
50
|
+
if (target.newTab) {
|
|
51
|
+
window.open(target.url, "_blank", "noopener,noreferrer");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
window.location.href = target.url;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function saveDownload(
|
|
58
|
+
target: ConfirmDialogDownloadTarget,
|
|
59
|
+
fetchRaw: DownloadFetch,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
if (!target.url) return;
|
|
62
|
+
const response = await fetchRaw(target.url, { responseType: "blob" });
|
|
63
|
+
const blob = response._data ?? new Blob([]);
|
|
64
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
65
|
+
try {
|
|
66
|
+
downloadFile(
|
|
67
|
+
objectUrl,
|
|
68
|
+
downloadFilename(target, response.headers.get("content-disposition")),
|
|
69
|
+
);
|
|
70
|
+
} finally {
|
|
71
|
+
URL.revokeObjectURL(objectUrl);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Runs the target of a confirmation's secondary action, the dialog left
|
|
77
|
+
* open: a link opens, a download is fetched with `fetchRaw` and saved.
|
|
78
|
+
*/
|
|
79
|
+
export function secondaryTargetRunner(
|
|
80
|
+
fetchRaw: DownloadFetch,
|
|
81
|
+
): SecondaryTargetRun {
|
|
82
|
+
return (target: ConfirmDialogSecondaryTarget) =>
|
|
83
|
+
target.type === "download"
|
|
84
|
+
? saveDownload(target, fetchRaw)
|
|
85
|
+
: openLink(target);
|
|
86
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ConfirmTranslate,
|
|
14
14
|
resolveConfirmDialog,
|
|
15
15
|
} from "./confirmDialogTexts";
|
|
16
|
+
import { type DownloadFetch, secondaryTargetRunner } from "./secondaryTarget";
|
|
16
17
|
|
|
17
18
|
/** What an action asks its confirmation with. */
|
|
18
19
|
export interface ActionConfirmRequest {
|
|
@@ -44,6 +45,9 @@ export function useActionConfirm() {
|
|
|
44
45
|
const { $authFetch } = useAuthFetch();
|
|
45
46
|
const { t } = useI18n();
|
|
46
47
|
const toast = useToast();
|
|
48
|
+
const runSecondaryTarget = secondaryTargetRunner(
|
|
49
|
+
$authFetch.raw as unknown as DownloadFetch,
|
|
50
|
+
);
|
|
47
51
|
|
|
48
52
|
const loadDialog = async (
|
|
49
53
|
declared: ActionConfirm,
|
|
@@ -81,6 +85,7 @@ export function useActionConfirm() {
|
|
|
81
85
|
dialog,
|
|
82
86
|
request.row,
|
|
83
87
|
t as ConfirmTranslate,
|
|
88
|
+
runSecondaryTarget,
|
|
84
89
|
);
|
|
85
90
|
const isConfirmed = await confirm({
|
|
86
91
|
...request.fallback,
|
|
@@ -48,6 +48,11 @@ export interface LiveGate {
|
|
|
48
48
|
catchUp(): void;
|
|
49
49
|
/** Records a read the caller makes itself (see LiveSubscription). */
|
|
50
50
|
acknowledge(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Holds the block alone while the page stays live (see LiveSubscription).
|
|
53
|
+
* Does nothing on a page without `live`.
|
|
54
|
+
*/
|
|
55
|
+
setHeld(isHeld: boolean): void;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
interface BoundSubscription {
|
|
@@ -103,5 +108,6 @@ export function useLiveGate(refresh: () => void): LiveGate {
|
|
|
103
108
|
else refresh();
|
|
104
109
|
},
|
|
105
110
|
acknowledge: () => resolve()?.acknowledge(),
|
|
111
|
+
setHeld: (isHeld) => resolve()?.setHeld(isHeld),
|
|
106
112
|
};
|
|
107
113
|
}
|