@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.
Files changed (184) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/config.d.ts +2 -0
  3. package/dist/config.js +9 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/implementations/dms/api-base-url.d.ts +1 -0
  6. package/dist/implementations/dms/api-base-url.js +6 -0
  7. package/dist/implementations/dms/api-base-url.js.map +1 -0
  8. package/dist/implementations/dms/frontend-modules.js +7 -6
  9. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  10. package/dist/implementations/dms/page.js +8 -2
  11. package/dist/implementations/dms/page.js.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/test/attachment-host/capabilities.d.ts +25 -0
  15. package/dist/test/attachment-host/capabilities.js +53 -1
  16. package/dist/test/attachment-host/capabilities.js.map +1 -1
  17. package/dist/test/integration/table-view-capabilities.test.js +13 -0
  18. package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
  19. package/dist/test/unit/api-base-url.test.d.ts +1 -0
  20. package/dist/test/unit/api-base-url.test.js +31 -0
  21. package/dist/test/unit/api-base-url.test.js.map +1 -0
  22. package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
  23. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
  24. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
  25. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
  26. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  27. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
  28. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
  29. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
  30. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
  31. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
  32. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
  33. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
  34. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
  36. package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
  37. package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
  38. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
  39. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
  40. package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
  41. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
  42. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
  43. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
  44. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
  45. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  46. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
  47. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  48. package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
  49. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
  50. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
  51. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
  52. package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
  53. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
  54. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
  55. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
  56. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
  57. package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
  58. package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
  59. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
  60. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
  61. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
  62. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
  63. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
  64. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
  65. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
  66. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
  67. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
  68. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
  69. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
  70. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
  71. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
  72. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
  73. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  74. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  75. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
  76. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
  77. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
  78. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
  79. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
  80. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
  81. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
  82. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
  83. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
  84. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
  85. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
  86. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
  87. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
  88. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
  90. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
  91. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
  92. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
  93. package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
  94. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
  95. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
  96. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
  97. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
  98. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
  99. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
  100. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
  101. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
  102. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
  103. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
  104. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
  105. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
  106. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
  107. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
  108. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
  109. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
  110. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
  111. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  112. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
  113. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
  114. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
  115. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
  116. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
  117. package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
  118. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
  119. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
  120. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
  121. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
  122. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
  123. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
  124. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
  125. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
  126. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
  127. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
  128. package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
  129. package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
  130. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
  131. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
  132. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
  133. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
  134. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
  135. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
  136. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
  137. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
  138. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
  139. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
  140. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
  141. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
  142. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
  143. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
  144. package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
  145. package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
  146. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
  147. package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
  148. package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
  149. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
  150. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
  151. package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
  152. package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
  153. package/frontend-vue/tests/block-not-found.test.ts +220 -0
  154. package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
  155. package/frontend-vue/tests/component-id.test.ts +18 -0
  156. package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
  157. package/frontend-vue/tests/guard.test.ts +242 -0
  158. package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
  159. package/frontend-vue/tests/live-toggle.test.ts +87 -1
  160. package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
  161. package/frontend-vue/tests/page-live-parity.test.ts +367 -0
  162. package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
  163. package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
  164. package/frontend-vue/tests/period-selector-url.test.ts +84 -6
  165. package/frontend-vue/tests/period-url-query.test.ts +52 -0
  166. package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
  167. package/frontend-vue/tests/row-side-panel.test.ts +109 -10
  168. package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
  169. package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
  170. package/frontend-vue/tests/table-density-menu.test.ts +4 -3
  171. package/frontend-vue/tests/table-live-hold.test.ts +95 -5
  172. package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
  173. package/frontend-vue/tests/table-row-hover.test.ts +32 -0
  174. package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
  175. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
  176. package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
  177. package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
  178. package/frontend-vue/tests/table-view-views.test.ts +56 -15
  179. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
  180. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  181. package/package.json +2 -2
  182. package/skills/dms/SKILL.md +9 -3
  183. package/skills/dms-module/SKILL.md +5 -0
  184. package/skills/dms-pages/REFERENCE.md +23 -2
@@ -0,0 +1,59 @@
1
+ // The page URL's query as the server reads it for header-button availability.
2
+
3
+ /** The page query as the server reads it: one value per name, the last one. */
4
+ export function pageQueryValues(
5
+ query: Record<string, unknown> | undefined,
6
+ ): Record<string, string> {
7
+ return Object.fromEntries(
8
+ Object.entries(query ?? {}).flatMap(([name, value]) => {
9
+ const last = Array.isArray(value) ? value.at(-1) : value;
10
+ return typeof last === "string" ? [[name, last]] : [];
11
+ }),
12
+ );
13
+ }
14
+
15
+ // Query keys the DMS writes for the view itself, never for the record a page
16
+ // acts on: the row open in a side panel, the paused live mode, and a period
17
+ // selector's range (`<id>.period`, `<id>.compare`, `<id>.from`…).
18
+ const VIEW_STATE_KEYS = new Set(["record", "live"]);
19
+ const PERIOD_KEY = /\.(?:period|compare|from|to|compareFrom|compareTo)$/;
20
+
21
+ /** Whether a query key only records the view's own state. */
22
+ export function isViewStateKey(name: string): boolean {
23
+ return VIEW_STATE_KEYS.has(name) || PERIOD_KEY.test(name);
24
+ }
25
+
26
+ function withoutViewState(
27
+ query: Record<string, string>,
28
+ ): Record<string, string> {
29
+ return Object.fromEntries(
30
+ Object.entries(query).filter(([name]) => !isViewStateKey(name)),
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Whether two page queries hold the same values, in any order, leaving out
36
+ * the keys that only record the view's state: moving through rows, pausing
37
+ * the live mode or changing a period never asks the layout again.
38
+ */
39
+ export function isSamePageQuery(
40
+ left: Record<string, string>,
41
+ right: Record<string, string>,
42
+ ): boolean {
43
+ const kept = withoutViewState(left);
44
+ const other = withoutViewState(right);
45
+ const names = Object.keys(kept);
46
+ return (
47
+ names.length === Object.keys(other).length &&
48
+ names.every((name) => kept[name] === other[name])
49
+ );
50
+ }
51
+
52
+ /** The layout URL of a page, asked for the given query. */
53
+ export function layoutUrlWithQuery(
54
+ layoutUrl: string,
55
+ query: Record<string, string>,
56
+ ): string {
57
+ const search = new URLSearchParams(query).toString();
58
+ return search ? `${layoutUrl}?${search}` : layoutUrl;
59
+ }
@@ -899,6 +899,10 @@
899
899
  "position": "{index} of {total}",
900
900
  "previous": "Previous",
901
901
  "next": "Next",
902
- "close": "Close"
902
+ "close": "Close",
903
+ "not_found": {
904
+ "title": "This entry is no longer available",
905
+ "description": "It was deleted, or the link is wrong."
906
+ }
903
907
  }
904
908
  }
@@ -903,6 +903,10 @@
903
903
  "position": "{index} sur {total}",
904
904
  "previous": "Précédent",
905
905
  "next": "Suivant",
906
- "close": "Fermer"
906
+ "close": "Fermer",
907
+ "not_found": {
908
+ "title": "Cette entrée n'est plus disponible",
909
+ "description": "Elle a été supprimée, ou le lien est erroné."
910
+ }
907
911
  }
908
912
  }
@@ -18,6 +18,8 @@ export interface ActivityFeedItem {
18
18
  title: BlockText;
19
19
  /** Dimmed details, joined by "·". */
20
20
  meta?: BlockText[];
21
+ /** Third line, dimmed and mono (the entry's evidence). */
22
+ detail?: BlockText;
21
23
  /**
22
24
  * Values the `$` title and details interpolate: typed values are formatted
23
25
  * as in a composed text; a `$`-prefixed string is translated first.
@@ -0,0 +1,67 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import {
4
+ type FetchNotFound,
5
+ NOT_FOUND_TITLE_KEY,
6
+ } from "../../utils/fetchNotFound";
7
+
8
+ // The one-line status a compact block (a KPI card, a meter) shows in place of
9
+ // its value: a first load that failed, with a retry, or a source that
10
+ // answered 404 — nothing to retry there, the value does not exist.
11
+ interface BlockInlineStatusProps {
12
+ state: "error" | "not-found";
13
+ /** What the 404 said; its `emptyState.title` replaces the default text. */
14
+ notFound?: FetchNotFound | null;
15
+ }
16
+
17
+ interface BlockInlineStatusEmits {
18
+ (e: "retry"): void;
19
+ }
20
+
21
+ const props = withDefaults(defineProps<BlockInlineStatusProps>(), {
22
+ notFound: undefined,
23
+ });
24
+ const emit = defineEmits<BlockInlineStatusEmits>();
25
+
26
+ const NOT_FOUND_ICON = "i-ph-magnifying-glass";
27
+ const ERROR_ICON = "i-ph-warning-circle";
28
+
29
+ const { t } = useI18n();
30
+ const { processI18n } = useTranslation();
31
+
32
+ const isError = computed(() => props.state === "error");
33
+ const label = computed(() => {
34
+ if (isError.value) return t("dms.table.load_error_title");
35
+ const title = props.notFound?.emptyState?.title;
36
+ return title ? processI18n(title) : t(NOT_FOUND_TITLE_KEY);
37
+ });
38
+ const icon = computed(() =>
39
+ isError.value
40
+ ? ERROR_ICON
41
+ : (props.notFound?.emptyState?.icon ?? NOT_FOUND_ICON),
42
+ );
43
+ </script>
44
+
45
+ <template>
46
+ <div
47
+ class="flex items-center justify-between gap-2"
48
+ :role="isError ? 'alert' : 'status'"
49
+ >
50
+ <p
51
+ class="flex min-w-0 items-center gap-1.5 text-[12.5px]"
52
+ :class="isError ? 'text-error' : 'text-muted'"
53
+ >
54
+ <UIcon :name="icon" class="size-4 shrink-0" />
55
+ <span class="truncate">{{ label }}</span>
56
+ </p>
57
+ <UButton
58
+ v-if="isError"
59
+ :label="t('dms.table.load_error_retry')"
60
+ icon="i-ph-arrows-clockwise"
61
+ color="neutral"
62
+ variant="outline"
63
+ size="xs"
64
+ @click="emit('retry')"
65
+ />
66
+ </div>
67
+ </template>
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from "vue";
2
3
  import DmsEmptyState from "../../../components/empty-state/EmptyState.vue";
4
+ import DmsBlockActions from "./BlockActions.vue";
5
+ import { type FetchNotFound, notFoundTexts } from "../../utils/fetchNotFound";
3
6
 
4
7
  /** What a block shows when it has nothing to list (interface-dms `BlockEmptyText`). */
5
8
  export interface BlockEmptyText {
@@ -8,14 +11,22 @@ export interface BlockEmptyText {
8
11
  description?: string;
9
12
  }
10
13
 
11
- // What a data-driven block shows instead of its content: an empty list, or a
12
- // first load that failed (with a retry). Compact v2 .c-empty in a card, so it
13
- // keeps the footprint of the block it stands in for.
14
+ /**
15
+ * `empty`: nothing to show. `error`: the first load failed. `not-found`: the
16
+ * source answered 404, what the block shows does not exist.
17
+ */
18
+ export type BlockStatusState = "empty" | "error" | "not-found";
19
+
20
+ // What a data-driven block shows instead of its content: an empty list, a
21
+ // first load that failed (with a retry), or a source that answered 404.
22
+ // Compact v2 .c-empty in a card, so it keeps the footprint of the block it
23
+ // stands in for.
14
24
  interface BlockStatusProps {
15
- /** `empty`: nothing to show. `error`: the first load failed. */
16
- state: "empty" | "error";
25
+ state: BlockStatusState;
17
26
  /** Replaces the default texts of the empty state. */
18
27
  empty?: BlockEmptyText;
28
+ /** What the 404 said, laid over the default not-found texts. */
29
+ notFound?: FetchNotFound | null;
19
30
  /** Draw the card surface around it (off inside a card). */
20
31
  card?: boolean;
21
32
  }
@@ -26,33 +37,59 @@ interface BlockStatusEmits {
26
37
 
27
38
  const props = withDefaults(defineProps<BlockStatusProps>(), {
28
39
  empty: undefined,
40
+ notFound: undefined,
29
41
  card: true,
30
42
  });
31
43
  const emit = defineEmits<BlockStatusEmits>();
32
44
 
45
+ const VARIANTS = {
46
+ empty: "no-data",
47
+ error: "error",
48
+ "not-found": "no-result",
49
+ } as const;
50
+
33
51
  const { t } = useI18n();
34
52
  const { processI18n } = useTranslation();
53
+
54
+ const translate = (text: string | undefined) =>
55
+ text ? processI18n(text) : undefined;
56
+
57
+ const notFoundState = computed(() => {
58
+ const answered = props.notFound?.emptyState ?? {};
59
+ const defaults = notFoundTexts(props.notFound, t);
60
+ return {
61
+ title: translate(answered.title) ?? defaults.title,
62
+ description: translate(answered.description) ?? defaults.description,
63
+ icon: answered.icon,
64
+ tone: answered.tone,
65
+ actions: answered.actions ?? [],
66
+ };
67
+ });
68
+
69
+ const isNotFound = computed(() => props.state === "not-found");
70
+
71
+ const title = computed(() => {
72
+ if (props.state === "error") return t("dms.blocks.load_error");
73
+ if (isNotFound.value) return notFoundState.value.title;
74
+ return props.empty ? processI18n(props.empty.title) : t("dms.blocks.empty");
75
+ });
76
+
77
+ const description = computed(() => {
78
+ if (props.state === "error") return t("dms.blocks.load_error_hint");
79
+ if (isNotFound.value) return notFoundState.value.description;
80
+ return translate(props.empty?.description);
81
+ });
35
82
  </script>
36
83
 
37
84
  <template>
38
85
  <div :class="props.card && 'dms-card'">
39
86
  <DmsEmptyState
40
87
  size="sm"
41
- :variant="props.state === 'error' ? 'error' : 'no-data'"
42
- :title="
43
- props.state === 'error'
44
- ? t('dms.blocks.load_error')
45
- : props.empty
46
- ? processI18n(props.empty.title)
47
- : t('dms.blocks.empty')
48
- "
49
- :description="
50
- props.state === 'error'
51
- ? t('dms.blocks.load_error_hint')
52
- : props.empty?.description
53
- ? processI18n(props.empty.description)
54
- : undefined
55
- "
88
+ :variant="VARIANTS[props.state]"
89
+ :icon="isNotFound ? notFoundState.icon : undefined"
90
+ :tone="isNotFound ? notFoundState.tone : undefined"
91
+ :title="title"
92
+ :description="description"
56
93
  >
57
94
  <template v-if="props.state === 'error'" #actions>
58
95
  <UButton
@@ -64,6 +101,9 @@ const { processI18n } = useTranslation();
64
101
  @click="emit('retry')"
65
102
  />
66
103
  </template>
104
+ <template v-else-if="isNotFound && notFoundState.actions.length" #actions>
105
+ <DmsBlockActions :actions="notFoundState.actions" size="sm" />
106
+ </template>
67
107
  </DmsEmptyState>
68
108
  </div>
69
109
  </template>
@@ -9,7 +9,10 @@ import type { Data, TableSharedData } from "./Table.vue";
9
9
  import { KANBAN_DISPLAY_ID } from "../../../composables/table-view/types";
10
10
  import type { CustomButton } from "../../../composables/table-view/types/custom-button";
11
11
  import { useTableContext } from "../../composables/table/useTableContext";
12
- import { createTableViewShiftFShortcut } from "../../../composables/table-view/shortcuts/tableViewShiftF";
12
+ import {
13
+ createTableViewShiftFShortcut,
14
+ tableSearchShortcuts,
15
+ } from "../../../composables/table-view/shortcuts/tableViewShiftF";
13
16
  import {
14
17
  FULL_TABLE_CHROME,
15
18
  type ResolvedTableChrome,
@@ -111,6 +114,8 @@ interface TableActionsProps {
111
114
  chrome?: ResolvedTableChrome;
112
115
  /** Placeholder of the search field. */
113
116
  searchPlaceholder?: string;
117
+ /** Key opening the search (`defineShortcuts` notation); `shift_f` else. */
118
+ searchShortcut?: string;
114
119
  /** Dropdown filters writing their column's filter. */
115
120
  quickFilters?: ResolvedQuickFilter[];
116
121
  }
@@ -193,7 +198,6 @@ watch(searchAreaFocused, async (isFocused) => {
193
198
  searchActive.value = false;
194
199
  });
195
200
 
196
-
197
201
  const searchFieldRef = ref<{ $el: HTMLElement }>();
198
202
 
199
203
  const focusSearchBar = () => {
@@ -273,13 +277,16 @@ const closeSearch = () => {
273
277
  searchActive.value = false;
274
278
  };
275
279
 
276
- defineShortcuts({
277
- shift_f: createTableViewShiftFShortcut(
278
- searchActive,
279
- tableSharedData!.globalFilterState,
280
- focusSearchBar,
280
+ const openSearchFromShortcut = createTableViewShiftFShortcut(
281
+ searchActive,
282
+ tableSharedData!.globalFilterState,
283
+ focusSearchBar,
284
+ );
285
+ defineShortcuts(
286
+ computed(() =>
287
+ tableSearchShortcuts(props.searchShortcut, openSearchFromShortcut),
281
288
  ),
282
- });
289
+ );
283
290
 
284
291
  const hasCustomFilterConfig = computed(() => {
285
292
  const filters = tableSharedData?.columnFiltersState.value || [];
@@ -80,6 +80,18 @@ const customComponent = computed(() => {
80
80
  return name ? resolveDmsComponent(name) || name : undefined;
81
81
  });
82
82
  const refresh = () => tableSharedData.value?.emits("refresh");
83
+ // A custom state clears what narrows the rows as the built-in one does; each
84
+ // is unset while there is nothing of it to clear.
85
+ const clearSearch = computed(() =>
86
+ clearable.value?.search
87
+ ? () => tableSharedData.value?.clearSearch()
88
+ : undefined,
89
+ );
90
+ const clearFilters = computed(() =>
91
+ clearable.value?.filters
92
+ ? () => tableSharedData.value?.clearFilters()
93
+ : undefined,
94
+ );
83
95
 
84
96
  interface EmptyStateContent {
85
97
  title: string;
@@ -221,6 +233,8 @@ const builtInActions = computed(() => {
221
233
  :state="kind"
222
234
  :search="search"
223
235
  :refresh="refresh"
236
+ :clear-search="clearSearch"
237
+ :clear-filters="clearFilters"
224
238
  />
225
239
  <DmsEmptyState
226
240
  v-else
@@ -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 hover tint, held, and an accent rule on
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-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";
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; `undefined`
250
- // removes it, but only the row this run wrote, on the page it wrote it:
251
- // closing as the user navigates leaves the next page's `?record=` alone.
252
- const rowRecorder = (deepLink?: boolean) => {
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 (row?: Data): void => {
260
+ return (id?: string): void => {
255
261
  if (!deepLink || !config.recordScope || !config.rowNavigation) return;
256
- if (!row) {
262
+ if (!id) {
257
263
  clearWrittenRecord(written);
258
264
  written = undefined;
259
265
  return;
260
266
  }
261
- written = writeRecordId(config.recordScope, rowIdOf(row));
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 component = resolveTargetComponent(componentName);
278
- const recordRow = rowRecorder(run.deepLink && !run.selection);
279
- const componentOptions = {
280
- ...run.target.component?.options,
281
- pageId: config.pageId,
282
- componentId: config.componentId,
283
- containerId: run.containerId,
284
- selection: run.selection,
285
- onSuccessCallback: () => {
286
- closeRowSidePanel(session);
287
- config.refreshCallback?.();
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
- session,
293
- ownerKey,
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
- recordRow(row);
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
- return { handleCustomButton, handleCustomRowAction, handleBulkCustomAction };
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
- () => isFetched && !isLoading.value && data.value === null && !!error.value,
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
  }