@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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type App,
|
|
13
13
|
} from "vue";
|
|
14
14
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import { defineShortcuts } from "@nuxt/ui/runtime/composables/defineShortcuts.js";
|
|
15
16
|
import TableActions from "../layers/dms-ui/app/build/components/table/Actions.vue";
|
|
16
17
|
import { FULL_TABLE_CHROME } from "../layers/dms-ui/app/build/composables/table-view/utils/chrome";
|
|
17
18
|
|
|
@@ -48,7 +49,11 @@ const InputStub = defineComponent({
|
|
|
48
49
|
|
|
49
50
|
let app: App | undefined;
|
|
50
51
|
|
|
51
|
-
function mountActions(
|
|
52
|
+
function mountActions(
|
|
53
|
+
searchPlaceholder: string | undefined,
|
|
54
|
+
search = "",
|
|
55
|
+
searchShortcut?: string,
|
|
56
|
+
) {
|
|
52
57
|
const Host = defineComponent({
|
|
53
58
|
setup() {
|
|
54
59
|
provide(
|
|
@@ -76,6 +81,7 @@ function mountActions(searchPlaceholder: string | undefined, search = "") {
|
|
|
76
81
|
return () =>
|
|
77
82
|
h(TableActions, {
|
|
78
83
|
searchPlaceholder,
|
|
84
|
+
searchShortcut,
|
|
79
85
|
chrome: { ...FULL_TABLE_CHROME, refresh: false, menu: false },
|
|
80
86
|
});
|
|
81
87
|
},
|
|
@@ -166,3 +172,53 @@ describe("table toolbar search", () => {
|
|
|
166
172
|
);
|
|
167
173
|
});
|
|
168
174
|
});
|
|
175
|
+
|
|
176
|
+
describe("table search shortcut", () => {
|
|
177
|
+
const press = (
|
|
178
|
+
key: string,
|
|
179
|
+
init: KeyboardEventInit = {},
|
|
180
|
+
target: EventTarget = document.body,
|
|
181
|
+
) =>
|
|
182
|
+
target.dispatchEvent(
|
|
183
|
+
new KeyboardEvent("keydown", { key, bubbles: true, ...init }),
|
|
184
|
+
);
|
|
185
|
+
const settle = async () => {
|
|
186
|
+
await nextTick();
|
|
187
|
+
await nextTick();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
vi.stubGlobal("defineShortcuts", defineShortcuts);
|
|
192
|
+
vi.stubGlobal("nextTick", nextTick);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("opens and focuses the search on ⇧F by default", async () => {
|
|
196
|
+
const container = mountActions(undefined);
|
|
197
|
+
press("F", { shiftKey: true });
|
|
198
|
+
await settle();
|
|
199
|
+
const input = container.querySelector("input");
|
|
200
|
+
expect(input).not.toBeNull();
|
|
201
|
+
expect(document.activeElement).toBe(input);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("opens it on the table's own key, ⇧F no longer bound", async () => {
|
|
205
|
+
const container = mountActions(undefined, "", "/");
|
|
206
|
+
press("F", { shiftKey: true });
|
|
207
|
+
await settle();
|
|
208
|
+
expect(container.querySelector("input")).toBeNull();
|
|
209
|
+
press("/");
|
|
210
|
+
await settle();
|
|
211
|
+
expect(document.activeElement).toBe(container.querySelector("input"));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("leaves / to a field the user types in", async () => {
|
|
215
|
+
const container = mountActions(undefined, "", "/");
|
|
216
|
+
const field = document.createElement("input");
|
|
217
|
+
document.body.append(field);
|
|
218
|
+
field.focus();
|
|
219
|
+
press("/", {}, field);
|
|
220
|
+
await settle();
|
|
221
|
+
expect(container.querySelector("input")).toBeNull();
|
|
222
|
+
expect(document.activeElement).toBe(field);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -15,14 +15,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
15
15
|
import type { PaginationState } from "@tanstack/vue-table";
|
|
16
16
|
import type { TableFilter } from "../layers/dms-ui/app/build/components/table/Table.vue";
|
|
17
17
|
import {
|
|
18
|
+
clearTableColumnFilters,
|
|
18
19
|
clearTableFilters,
|
|
19
20
|
clearTableFiltersLabelKey,
|
|
21
|
+
clearTableSearch,
|
|
20
22
|
clearableTableFilters,
|
|
21
23
|
isTableNarrowed,
|
|
22
24
|
resetColumnFilters,
|
|
23
25
|
type TableNarrowingState,
|
|
24
26
|
} from "../layers/dms-ui/app/build/composables/table/utils/clearTableFilters";
|
|
25
27
|
import TableEmpty from "../layers/dms-ui/app/build/components/table/Empty.vue";
|
|
28
|
+
import { useTableViewEmptyState } from "../layers/dms-ui/app/composables/table-view/useTableViewEmptyState";
|
|
29
|
+
import type { TableViewEmptyStatesConfig } from "../layers/dms-ui/app/composables/table-view/types";
|
|
26
30
|
|
|
27
31
|
const pinnedStatus = (value: unknown): TableFilter => ({
|
|
28
32
|
accessorKey: "status",
|
|
@@ -173,12 +177,45 @@ describe("clearTableFilters", () => {
|
|
|
173
177
|
});
|
|
174
178
|
});
|
|
175
179
|
|
|
180
|
+
describe("clearing the search or the filters alone", () => {
|
|
181
|
+
it("empties the search, the chips kept, back to page 1", () => {
|
|
182
|
+
const state = createState({
|
|
183
|
+
columnFilters: [dueDate],
|
|
184
|
+
globalFilter: "late",
|
|
185
|
+
pageIndex: 2,
|
|
186
|
+
});
|
|
187
|
+
clearTableSearch(state);
|
|
188
|
+
expect(state.globalFilter.value).toBe("");
|
|
189
|
+
expect(state.columnFilters.value).toEqual([dueDate]);
|
|
190
|
+
expect(state.pagination.value.pageIndex).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("clears the chips, the search kept, back to page 1", () => {
|
|
194
|
+
const state = createState({
|
|
195
|
+
columnFilters: [pinnedStatus("closed"), dueDate],
|
|
196
|
+
globalFilter: "late",
|
|
197
|
+
pageIndex: 2,
|
|
198
|
+
});
|
|
199
|
+
clearTableColumnFilters(state);
|
|
200
|
+
expect(state.globalFilter.value).toBe("late");
|
|
201
|
+
expect(state.columnFilters.value).toEqual([pinnedStatus("open")]);
|
|
202
|
+
expect(state.pagination.value.pageIndex).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
176
206
|
describe("table empty state", () => {
|
|
177
207
|
let app: App | undefined;
|
|
178
208
|
|
|
179
|
-
const mountEmpty = (
|
|
209
|
+
const mountEmpty = (
|
|
210
|
+
state: Harness,
|
|
211
|
+
emptyStates?: TableViewEmptyStatesConfig,
|
|
212
|
+
) => {
|
|
180
213
|
const sharedData = shallowRef({
|
|
181
214
|
resetFilters: () => clearTableFilters(state),
|
|
215
|
+
clearSearch: () => clearTableSearch(state),
|
|
216
|
+
clearFilters: () => clearTableColumnFilters(state),
|
|
217
|
+
globalFilterState: state.globalFilter,
|
|
218
|
+
emptyStates,
|
|
182
219
|
clearableFilters: computed(() => clearableTableFilters(state)),
|
|
183
220
|
isFiltered: computed(() => isTableNarrowed(state)),
|
|
184
221
|
showArchivedState: ref(false),
|
|
@@ -266,6 +303,67 @@ describe("table empty state", () => {
|
|
|
266
303
|
expect(container.querySelector("button")).toBeNull();
|
|
267
304
|
});
|
|
268
305
|
|
|
306
|
+
it("hands a custom state a clear for the search and one for the filters", async () => {
|
|
307
|
+
const received: Record<string, unknown>[] = [];
|
|
308
|
+
vi.stubGlobal("resolveDmsComponent", () =>
|
|
309
|
+
defineComponent({
|
|
310
|
+
props: {
|
|
311
|
+
state: String,
|
|
312
|
+
clearSearch: Function,
|
|
313
|
+
clearFilters: Function,
|
|
314
|
+
},
|
|
315
|
+
setup: (props) => () => {
|
|
316
|
+
received.push({ ...props });
|
|
317
|
+
return h("p", "custom");
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
const state = createState({
|
|
322
|
+
columnFilters: [dueDate],
|
|
323
|
+
globalFilter: "late",
|
|
324
|
+
});
|
|
325
|
+
mountEmpty(state, {
|
|
326
|
+
filtered: { title: "", component: { componentName: "LogsEmpty" } },
|
|
327
|
+
});
|
|
328
|
+
const props = received.at(-1)!;
|
|
329
|
+
expect(props.state).toBe("filtered");
|
|
330
|
+
(props.clearSearch as () => void)();
|
|
331
|
+
await nextTick();
|
|
332
|
+
expect(state.globalFilter.value).toBe("");
|
|
333
|
+
expect(state.columnFilters.value).toEqual([dueDate]);
|
|
334
|
+
expect(received.at(-1)!.clearSearch).toBeUndefined();
|
|
335
|
+
(received.at(-1)!.clearFilters as () => void)();
|
|
336
|
+
expect(state.columnFilters.value).toEqual([]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("lets a component inside the table clear through useTableViewEmptyState", async () => {
|
|
340
|
+
const state = createState({
|
|
341
|
+
columnFilters: [dueDate],
|
|
342
|
+
globalFilter: "late",
|
|
343
|
+
});
|
|
344
|
+
let controls: ReturnType<typeof useTableViewEmptyState> | undefined;
|
|
345
|
+
vi.stubGlobal("resolveDmsComponent", () =>
|
|
346
|
+
defineComponent({
|
|
347
|
+
setup: () => {
|
|
348
|
+
controls = useTableViewEmptyState();
|
|
349
|
+
return () => h("p");
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
mountEmpty(state, {
|
|
354
|
+
filtered: { title: "", component: { componentName: "LogsEmpty" } },
|
|
355
|
+
});
|
|
356
|
+
expect(controls!.search.value).toBe("late");
|
|
357
|
+
expect(controls!.isFiltered.value).toBe(true);
|
|
358
|
+
expect(controls!.canClearSearch.value).toBe(true);
|
|
359
|
+
expect(controls!.canClearFilters.value).toBe(true);
|
|
360
|
+
controls!.clearFilters();
|
|
361
|
+
expect(controls!.canClearFilters.value).toBe(false);
|
|
362
|
+
expect(state.globalFilter.value).toBe("late");
|
|
363
|
+
controls!.clearSearch();
|
|
364
|
+
expect(controls!.isFiltered.value).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
269
367
|
it("offers no clear action when only default filters narrow the rows", () => {
|
|
270
368
|
const container = mountEmpty(
|
|
271
369
|
createState({ columnFilters: [pinnedStatus("open")] }),
|
|
@@ -118,11 +118,12 @@ describe("table density menu", () => {
|
|
|
118
118
|
|
|
119
119
|
it("keeps the user's density with the table state, the module's as default", () => {
|
|
120
120
|
expect(tableViewSource).toMatch(
|
|
121
|
-
/
|
|
121
|
+
/defaultDensity = props\.density \?\? DEFAULT_DENSITY/,
|
|
122
122
|
);
|
|
123
|
-
expect(tableViewSource).
|
|
124
|
-
|
|
123
|
+
expect(tableViewSource).toContain(
|
|
124
|
+
'tablePreferences.restore("density", density);',
|
|
125
125
|
);
|
|
126
|
+
expect(tableViewSource).toMatch(/\["density", density, defaultDensity\]/);
|
|
126
127
|
expect(tableViewSource).toContain('v-model:density="density"');
|
|
127
128
|
});
|
|
128
129
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
import { createApp, defineComponent, h, nextTick, type App } from "vue";
|
|
11
|
+
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
|
12
12
|
import { refreshPageBlocks } from "../layers/dms-ui/app/utils/blockRefresh";
|
|
13
13
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
14
14
|
import type { RealtimeTopicHandler } from "../layers/dms-core/app/composables/realtime/useUserRealtime";
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
LIVE_HELD_AT_QUERY_KEY,
|
|
22
22
|
sourceRouteQuery,
|
|
23
23
|
} from "../layers/dms-ui/app/build/composables/table-view/useTableRows";
|
|
24
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
TableLiveHold,
|
|
26
|
+
TableLiveHoldSignals,
|
|
27
|
+
} from "../layers/dms-ui/app/build/composables/table-view/useTableLiveHold";
|
|
28
|
+
import type { TableViewLiveHoldOptions } from "../layers/dms-ui/app/types/live";
|
|
25
29
|
|
|
26
30
|
const topicHandlers = new Map<string, RealtimeTopicHandler>();
|
|
27
31
|
|
|
@@ -53,7 +57,13 @@ let live: PageLiveController;
|
|
|
53
57
|
const publish = () =>
|
|
54
58
|
topicHandlers.get(TOPIC)?.({ topic: TOPIC, type: "request", ts: 1 });
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
interface MountedTableOptions {
|
|
61
|
+
liveHold: boolean | TableViewLiveHoldOptions;
|
|
62
|
+
isSource?: boolean;
|
|
63
|
+
signals?: TableLiveHoldSignals;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mountTable(options: MountedTableOptions) {
|
|
57
67
|
const refresh = vi.fn();
|
|
58
68
|
let hold: TableLiveHold | undefined;
|
|
59
69
|
const app = createApp(
|
|
@@ -61,9 +71,10 @@ function mountTable(options: { liveHold: boolean; isSource?: boolean }) {
|
|
|
61
71
|
setup() {
|
|
62
72
|
const gate = useRefreshTriggers(TOPIC, refresh);
|
|
63
73
|
hold = useTableLiveHold({
|
|
64
|
-
|
|
74
|
+
liveHold: options.liveHold,
|
|
65
75
|
isSource: options.isSource ?? true,
|
|
66
76
|
gate: () => gate,
|
|
77
|
+
signals: options.signals,
|
|
67
78
|
});
|
|
68
79
|
return () => h("div");
|
|
69
80
|
},
|
|
@@ -193,7 +204,13 @@ it("reads on a user's refresh while held, and holds from then", async () => {
|
|
|
193
204
|
});
|
|
194
205
|
|
|
195
206
|
it("wires the table view's rows, row events and banner to the hold", () => {
|
|
196
|
-
expect(tableViewSource).toMatch(
|
|
207
|
+
expect(tableViewSource).toMatch(
|
|
208
|
+
/liveHold\?: boolean \| TableViewLiveHoldOptions;/,
|
|
209
|
+
);
|
|
210
|
+
expect(tableViewSource).toContain(
|
|
211
|
+
"isRowOpen: () => openRowId.value !== undefined,",
|
|
212
|
+
);
|
|
213
|
+
expect(tableViewSource).toContain('ref="tableRoot"');
|
|
197
214
|
expect(tableViewSource).toContain("heldQuery: liveHold.heldQuery,");
|
|
198
215
|
expect(tableViewSource).toContain("refreshCallback: refreshByUser,");
|
|
199
216
|
expect(tableViewSource).toContain('@refresh="refreshByUser"');
|
|
@@ -202,3 +219,76 @@ it("wires the table view's rows, row events and banner to the hold", () => {
|
|
|
202
219
|
/<LiveHoldBanner[\s\S]*@show="liveHold.showNew"/,
|
|
203
220
|
);
|
|
204
221
|
});
|
|
222
|
+
|
|
223
|
+
it("holds the table alone while a row is open, the page staying live", async () => {
|
|
224
|
+
const isRowOpen = ref(false);
|
|
225
|
+
const { refresh, hold } = mountTable({
|
|
226
|
+
liveHold: { whileRowOpen: true },
|
|
227
|
+
signals: { isRowOpen: () => isRowOpen.value },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
isRowOpen.value = true;
|
|
231
|
+
await nextTick();
|
|
232
|
+
publish();
|
|
233
|
+
publish();
|
|
234
|
+
expect(live.isPaused.value).toBe(false);
|
|
235
|
+
expect(live.missedCount.value).toBe(0);
|
|
236
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
237
|
+
expect(hold.isHolding.value).toBe(true);
|
|
238
|
+
expect(hold.newCount.value).toBe(2);
|
|
239
|
+
|
|
240
|
+
// Closing the panel reads what the table missed, once.
|
|
241
|
+
isRowOpen.value = false;
|
|
242
|
+
await nextTick();
|
|
243
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
244
|
+
expect(hold.isHolding.value).toBe(false);
|
|
245
|
+
publish();
|
|
246
|
+
expect(refresh).toHaveBeenCalledTimes(2);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("keeps today's hold when liveHold is true: an open row changes nothing", async () => {
|
|
250
|
+
const { refresh, hold } = mountTable({
|
|
251
|
+
liveHold: true,
|
|
252
|
+
signals: { isRowOpen: () => true },
|
|
253
|
+
});
|
|
254
|
+
publish();
|
|
255
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(hold.isHolding.value).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("holds the table while its first row is scrolled out of view", async () => {
|
|
260
|
+
const element = document.createElement("div");
|
|
261
|
+
element.innerHTML = "<table><tbody><tr><td>1</td></tr></tbody></table>";
|
|
262
|
+
document.body.append(element);
|
|
263
|
+
const row = element.querySelector("tr")!;
|
|
264
|
+
const rect = (bottom: number) => ({ bottom, top: bottom - 40 }) as DOMRect;
|
|
265
|
+
row.getBoundingClientRect = () => rect(120);
|
|
266
|
+
vi.stubGlobal("requestAnimationFrame", (run: () => void) =>
|
|
267
|
+
setTimeout(run, 16),
|
|
268
|
+
);
|
|
269
|
+
const scroll = async () => {
|
|
270
|
+
window.dispatchEvent(new Event("scroll"));
|
|
271
|
+
vi.advanceTimersByTime(16);
|
|
272
|
+
await nextTick();
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const { refresh, hold } = mountTable({
|
|
276
|
+
liveHold: { whileScrolled: true },
|
|
277
|
+
signals: { tableElement: () => element },
|
|
278
|
+
});
|
|
279
|
+
await nextTick();
|
|
280
|
+
publish();
|
|
281
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
282
|
+
|
|
283
|
+
row.getBoundingClientRect = () => rect(-200);
|
|
284
|
+
await scroll();
|
|
285
|
+
publish();
|
|
286
|
+
expect(hold.isHolding.value).toBe(true);
|
|
287
|
+
expect(hold.newCount.value).toBe(1);
|
|
288
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
289
|
+
|
|
290
|
+
row.getBoundingClientRect = () => rect(120);
|
|
291
|
+
await scroll();
|
|
292
|
+
expect(refresh).toHaveBeenCalledTimes(2);
|
|
293
|
+
element.remove();
|
|
294
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A row's actions are named after the row: a custom action's label fills its
|
|
3
|
+
* `{field}` braces from the row ("Translate {name}"), and the ⋯ button opening
|
|
4
|
+
* the row's menu has an accessible name — "Actions for <label>" by default,
|
|
5
|
+
* or the table's own `rowActions.menuLabel`.
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
computed,
|
|
10
|
+
h,
|
|
11
|
+
ref,
|
|
12
|
+
shallowRef,
|
|
13
|
+
watch,
|
|
14
|
+
watchEffect,
|
|
15
|
+
type VNode,
|
|
16
|
+
} from "vue";
|
|
17
|
+
import { useTableColumns } from "../layers/dms-ui/app/build/composables/table/useTableColumns";
|
|
18
|
+
import * as ruleEvaluator from "../layers/dms-core/app/utils/row-action-rule-evaluator";
|
|
19
|
+
import * as colors from "../layers/dms-core/app/types/color";
|
|
20
|
+
import * as buttons from "../layers/dms-core/app/types/button";
|
|
21
|
+
import * as sizes from "../layers/dms-core/app/types/size";
|
|
22
|
+
|
|
23
|
+
vi.mock("@nuxt/ui/components/Button.vue", () => ({
|
|
24
|
+
default: { name: "UButton", render: () => null },
|
|
25
|
+
}));
|
|
26
|
+
vi.mock("@nuxt/ui/components/Checkbox.vue", () => ({
|
|
27
|
+
default: { render: () => null },
|
|
28
|
+
}));
|
|
29
|
+
vi.mock("@nuxt/ui/components/DropdownMenu.vue", () => ({
|
|
30
|
+
default: { name: "UDropdownMenu", render: () => null },
|
|
31
|
+
}));
|
|
32
|
+
vi.mock("@nuxt/ui/runtime/utils/index.js", () => ({
|
|
33
|
+
get: (row: Record<string, unknown>, key: string) => row[key],
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
type Row = Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
const ROW: Row = { _id: "fr", name: "Français", code: "fr" };
|
|
39
|
+
|
|
40
|
+
const translateKey = (key: string, params: Record<string, unknown> = {}) =>
|
|
41
|
+
Object.keys(params).length > 0 ? `${key}(${JSON.stringify(params)})` : key;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
for (const [name, value] of Object.entries({
|
|
45
|
+
...ruleEvaluator,
|
|
46
|
+
...colors,
|
|
47
|
+
...buttons,
|
|
48
|
+
...sizes,
|
|
49
|
+
})) {
|
|
50
|
+
vi.stubGlobal(name, value);
|
|
51
|
+
}
|
|
52
|
+
vi.stubGlobal("computed", computed);
|
|
53
|
+
vi.stubGlobal("ref", ref);
|
|
54
|
+
vi.stubGlobal("watch", watch);
|
|
55
|
+
vi.stubGlobal("watchEffect", watchEffect);
|
|
56
|
+
vi.stubGlobal("shallowRef", shallowRef);
|
|
57
|
+
vi.stubGlobal("isFunction", (value: unknown) => typeof value === "function");
|
|
58
|
+
vi.stubGlobal("h", h);
|
|
59
|
+
vi.stubGlobal("isString", (value: unknown) => typeof value === "string");
|
|
60
|
+
vi.stubGlobal("useI18n", () => ({ t: translateKey }));
|
|
61
|
+
vi.stubGlobal("useDmsRouter", () => ({ push: vi.fn() }));
|
|
62
|
+
vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
|
|
63
|
+
vi.stubGlobal("useDmsAppConfig", () => ({
|
|
64
|
+
ui: { icons: { ellipsis: "i-ellipsis" } },
|
|
65
|
+
}));
|
|
66
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
67
|
+
processI18n: (key: string, params?: Record<string, unknown>) =>
|
|
68
|
+
key.startsWith("$") ? translateKey(key.slice(1), params ?? {}) : key,
|
|
69
|
+
}));
|
|
70
|
+
vi.stubGlobal("useConfirm", () => ({ confirm: vi.fn() }));
|
|
71
|
+
vi.stubGlobal("useAuthFetch", () => ({ $authFetch: vi.fn() }));
|
|
72
|
+
vi.stubGlobal("useDataTypes", () => ({ getDataType: () => undefined }));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
vi.unstubAllGlobals();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
interface ActionsCellOptions {
|
|
80
|
+
custom: Array<Record<string, unknown>>;
|
|
81
|
+
menuLabel?: string;
|
|
82
|
+
labelKey?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function actionsCell({ custom, menuLabel, labelKey }: ActionsCellOptions) {
|
|
86
|
+
const { columns } = useTableColumns<Row>({
|
|
87
|
+
data: computed(() => [ROW]),
|
|
88
|
+
columns: [{ accessorKey: "name", header: "Name" }],
|
|
89
|
+
rowIdKey: "_id",
|
|
90
|
+
rowActions: {
|
|
91
|
+
add: false,
|
|
92
|
+
copyLink: false,
|
|
93
|
+
delete: false,
|
|
94
|
+
details: false,
|
|
95
|
+
duplicate: false,
|
|
96
|
+
edit: false,
|
|
97
|
+
custom: custom as never,
|
|
98
|
+
menuLabel,
|
|
99
|
+
},
|
|
100
|
+
labelKey,
|
|
101
|
+
emits: (() => undefined) as never,
|
|
102
|
+
ui: computed(() => ({ rowAction: () => "" }) as never),
|
|
103
|
+
});
|
|
104
|
+
const column = columns.value.find((entry) => entry.id === "actions");
|
|
105
|
+
const cell = column?.cell as (context: unknown) => VNode;
|
|
106
|
+
return cell({ row: { original: ROW, id: "fr" } });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const childrenOf = (node: VNode) => node.children as VNode[];
|
|
110
|
+
|
|
111
|
+
function menuButtonProps(cell: VNode): Record<string, unknown> {
|
|
112
|
+
const menu = childrenOf(cell).find(
|
|
113
|
+
(child) => (child.type as { name?: string }).name === "UDropdownMenu",
|
|
114
|
+
);
|
|
115
|
+
const slots = menu?.children as { default: () => VNode };
|
|
116
|
+
return slots.default().props ?? {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const MENU_ACTION = { label: "Remove", target: { type: "api", url: "/x" } };
|
|
120
|
+
|
|
121
|
+
it("names the ⋯ button after the row's label field by default", () => {
|
|
122
|
+
const cell = actionsCell({ custom: [MENU_ACTION], labelKey: "name" });
|
|
123
|
+
expect(menuButtonProps(cell)["aria-label"]).toBe(
|
|
124
|
+
'dms.table.row_actions_for({"label":"Français"})',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("names the ⋯ button generically without a label field", () => {
|
|
129
|
+
const cell = actionsCell({ custom: [MENU_ACTION] });
|
|
130
|
+
expect(menuButtonProps(cell)["aria-label"]).toBe("dms.table.row_actions");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("names the ⋯ button with menuLabel, filled from the row", () => {
|
|
134
|
+
const literal = actionsCell({
|
|
135
|
+
custom: [MENU_ACTION],
|
|
136
|
+
menuLabel: "Actions for {name} ({code})",
|
|
137
|
+
});
|
|
138
|
+
expect(menuButtonProps(literal)["aria-label"]).toBe(
|
|
139
|
+
"Actions for Français (fr)",
|
|
140
|
+
);
|
|
141
|
+
const key = actionsCell({
|
|
142
|
+
custom: [MENU_ACTION],
|
|
143
|
+
menuLabel: "$dms_lang.languages.menu",
|
|
144
|
+
});
|
|
145
|
+
expect(menuButtonProps(key)["aria-label"]).toContain(
|
|
146
|
+
'dms_lang.languages.menu({"_id":"fr","name":"Français"',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("fills a custom action's label from the row, inline and in the menu", () => {
|
|
151
|
+
const cell = actionsCell({
|
|
152
|
+
custom: [
|
|
153
|
+
{
|
|
154
|
+
label: "Translate {name}",
|
|
155
|
+
isVisible: true,
|
|
156
|
+
showLabel: true,
|
|
157
|
+
target: { type: "page", url: "/lang?locale={code}" },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: "$dms_lang.languages.remove",
|
|
161
|
+
target: { type: "api", url: "/x" },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
label: "Keep {unknown} as written",
|
|
165
|
+
target: { type: "api", url: "/y" },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
const [inline] = childrenOf(cell);
|
|
170
|
+
expect(inline.props?.label).toBe("Translate Français");
|
|
171
|
+
expect(inline.props?.["aria-label"]).toBe("Translate Français");
|
|
172
|
+
|
|
173
|
+
const menu = childrenOf(cell).find(
|
|
174
|
+
(child) => (child.type as { name?: string }).name === "UDropdownMenu",
|
|
175
|
+
);
|
|
176
|
+
const items = (menu?.props?.items as Array<Array<{ label: string }>>).flat();
|
|
177
|
+
expect(items.map((item) => item.label)).toEqual([
|
|
178
|
+
expect.stringContaining('dms_lang.languages.remove({"_id":"fr"'),
|
|
179
|
+
"Keep {unknown} as written",
|
|
180
|
+
]);
|
|
181
|
+
});
|
|
@@ -5,6 +5,7 @@ import { EYEBROW_CLASS } from "../layers/dms-ui/app/build/utils/eyebrow";
|
|
|
5
5
|
import { REORDER_HANDLE_CLASS } from "../layers/dms-ui/app/build/composables/table-view/useReorderHandles";
|
|
6
6
|
import {
|
|
7
7
|
resolveRowClickAction,
|
|
8
|
+
rowCustomActions,
|
|
8
9
|
type RowClickActionOptions,
|
|
9
10
|
} from "../layers/dms-ui/app/utils/rowClickAction";
|
|
10
11
|
import type { CustomRowAction } from "../layers/dms-ui/app/types/row-action";
|
|
@@ -158,3 +159,34 @@ describe("resolveRowClickAction", () => {
|
|
|
158
159
|
expect(resolveRowClickAction(options, rowFor("sent"))).toBe("details");
|
|
159
160
|
});
|
|
160
161
|
});
|
|
162
|
+
|
|
163
|
+
describe("rowCustomActions", () => {
|
|
164
|
+
const stopSelling: CustomRowAction = {
|
|
165
|
+
id: "stop_selling",
|
|
166
|
+
label: "Stop selling",
|
|
167
|
+
target: { type: "api", url: "/plans/{_id}/stop", method: "POST" },
|
|
168
|
+
rule: { field: "isActive", equals: true },
|
|
169
|
+
};
|
|
170
|
+
const remove: CustomRowAction = {
|
|
171
|
+
id: "delete",
|
|
172
|
+
label: "Delete",
|
|
173
|
+
target: { type: "api", url: "/plans/{_id}", method: "DELETE" },
|
|
174
|
+
rule: { field: "workspaces", equals: 0 },
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
it("lists the actions whose rule accepts the row, in their order", () => {
|
|
178
|
+
const actions = [detailsAction, stopSelling, remove];
|
|
179
|
+
expect(
|
|
180
|
+
rowCustomActions(actions, { isActive: true, workspaces: 0 }),
|
|
181
|
+
).toEqual(actions);
|
|
182
|
+
expect(
|
|
183
|
+
rowCustomActions(actions, { isActive: false, workspaces: 2 }).map(
|
|
184
|
+
(action) => action.label,
|
|
185
|
+
),
|
|
186
|
+
).toEqual(["Details"]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("lists nothing for a table without custom actions", () => {
|
|
190
|
+
expect(rowCustomActions(undefined, {})).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -24,7 +24,7 @@ vi.stubGlobal("useDmsLazyAsyncData", engine.useDmsLazyAsyncData);
|
|
|
24
24
|
const { useTableRows } = await import(
|
|
25
25
|
"../layers/dms-ui/app/build/composables/table-view/useTableRows"
|
|
26
26
|
);
|
|
27
|
-
const { useTableListData } = await import(
|
|
27
|
+
const { useTableCountsData, useTableListData } = await import(
|
|
28
28
|
"../layers/dms-ui/app/build/composables/table-view/useTableListData"
|
|
29
29
|
);
|
|
30
30
|
const { usePeriod } = await import(
|
|
@@ -49,6 +49,7 @@ const api = (async (url: string) => {
|
|
|
49
49
|
const mounted: App[] = [];
|
|
50
50
|
const cleanups: Array<() => void> = [];
|
|
51
51
|
let listed: { value: unknown } | undefined;
|
|
52
|
+
let counted: { value: unknown } | undefined;
|
|
52
53
|
|
|
53
54
|
const SourceTable = defineComponent({
|
|
54
55
|
setup() {
|
|
@@ -68,6 +69,15 @@ const SourceTable = defineComponent({
|
|
|
68
69
|
}).then(({ data }) => {
|
|
69
70
|
listed = data;
|
|
70
71
|
});
|
|
72
|
+
void useTableCountsData({
|
|
73
|
+
componentId: "report",
|
|
74
|
+
pageId: "reports",
|
|
75
|
+
rows,
|
|
76
|
+
queries: computed(() => [{ id: "all", query: {} }]),
|
|
77
|
+
archiveQuery: computed(() => ({})),
|
|
78
|
+
}).then(({ data }) => {
|
|
79
|
+
counted = data;
|
|
80
|
+
});
|
|
71
81
|
return () => h("div");
|
|
72
82
|
},
|
|
73
83
|
});
|
|
@@ -98,6 +108,7 @@ const periodOf = (url: string) =>
|
|
|
98
108
|
beforeEach(() => {
|
|
99
109
|
requests.length = 0;
|
|
100
110
|
listed = undefined;
|
|
111
|
+
counted = undefined;
|
|
101
112
|
});
|
|
102
113
|
|
|
103
114
|
afterEach(() => {
|
|
@@ -114,13 +125,30 @@ describe("a source table bound to a period scope", () => {
|
|
|
114
125
|
expect(listed!.value).toBeNull();
|
|
115
126
|
});
|
|
116
127
|
|
|
128
|
+
// The server never registers a scope: an empty object there drew the tabs
|
|
129
|
+
// with no skeleton, where the browser (its key carrying the period) drew
|
|
130
|
+
// one, and hydration mismatched.
|
|
131
|
+
it("keeps its tab counters' skeleton too until the scope registers", async () => {
|
|
132
|
+
mountTable();
|
|
133
|
+
await settle();
|
|
134
|
+
expect(counted).toBeDefined();
|
|
135
|
+
expect(counted!.value).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("counts its tabs with the period once the scope registers", async () => {
|
|
139
|
+
registerSelector();
|
|
140
|
+
mountTable();
|
|
141
|
+
await settle();
|
|
142
|
+
expect(counted!.value).toEqual({ all: 1 });
|
|
143
|
+
});
|
|
144
|
+
|
|
117
145
|
it("sends the period with its request once the scope registers", async () => {
|
|
118
146
|
const period = registerSelector();
|
|
119
147
|
mountTable();
|
|
120
148
|
await settle();
|
|
121
|
-
expect(requests).toHaveLength(
|
|
149
|
+
expect(requests).toHaveLength(2);
|
|
122
150
|
expect(listed!.value).toMatchObject({ total: 1 });
|
|
123
|
-
const
|
|
151
|
+
const range = period.state.value.range!;
|
|
124
152
|
expect(periodOf(requests[0]!)).toEqual({
|
|
125
153
|
from: range.from.toISOString(),
|
|
126
154
|
to: range.to.toISOString(),
|