@antelopejs/dms 0.7.1 → 0.7.3
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 +47 -0
- package/dist/hooks/tenant-instances.d.ts +1 -0
- package/dist/hooks/tenant-instances.js +13 -0
- package/dist/hooks/tenant-instances.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-topics.test.js +67 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js +1 -0
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js +11 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +31 -0
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
- package/dist/test/unit/utils/tenant-instances.test.d.ts +1 -0
- package/dist/test/unit/utils/tenant-instances.test.js +53 -0
- package/dist/test/unit/utils/tenant-instances.test.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/tenant-instances.d.ts +2 -0
- package/dist/utils/tenant-instances.js +38 -0
- package/dist/utils/tenant-instances.js.map +1 -0
- package/dist/validation/auth.schema.d.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +4 -0
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +35 -23
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockActions.vue +34 -6
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +26 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/MonoCell.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +24 -13
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +12 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +40 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +85 -22
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +94 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +3 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeEditor.ts +2 -13
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +59 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeLanguages.ts +36 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codePalette.ts +32 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +77 -20
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +15 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +111 -25
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +95 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +18 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +21 -9
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +97 -0
- package/frontend-vue/layers/dms-ui/app/components/empty-state/EmptyState.vue +13 -4
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +14 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +2 -8
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +19 -0
- package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +54 -48
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +24 -9
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +66 -74
- package/frontend-vue/layers/dms-ui/app/components/top-list/internal/TopListRow.vue +4 -3
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +5 -2
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +2 -29
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +2 -2
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/package.json +4 -3
- package/frontend-vue/pnpm-lock.yaml +8 -5
- package/frontend-vue/tests/activity-feed-days.test.ts +26 -12
- package/frontend-vue/tests/banner-block-fetch.test.ts +162 -0
- package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +100 -0
- package/frontend-vue/tests/block-items-period-realtime.test.ts +147 -0
- package/frontend-vue/tests/block-route-params.test.ts +48 -0
- package/frontend-vue/tests/code-block.test.ts +155 -0
- package/frontend-vue/tests/code-highlight.test.ts +72 -0
- package/frontend-vue/tests/composed-text-blocks.test.ts +5 -0
- package/frontend-vue/tests/composed-text-more-blocks.test.ts +391 -0
- package/frontend-vue/tests/drawer-target-direction.test.ts +205 -0
- package/frontend-vue/tests/empty-state-late-slots.test.ts +53 -0
- package/frontend-vue/tests/flow-canvas-delete-badge.test.ts +88 -0
- package/frontend-vue/tests/form-redirect-placeholders.test.ts +11 -0
- package/frontend-vue/tests/key-value-list-copy.test.ts +114 -0
- package/frontend-vue/tests/master-detail-layout.test.ts +53 -0
- package/frontend-vue/tests/mono-cell-truncate.test.ts +50 -0
- package/frontend-vue/tests/nav-card-grid-empty-category.test.ts +1 -0
- package/frontend-vue/tests/page-fill-height.test.ts +20 -3
- package/frontend-vue/tests/route-param-occurrences.test.ts +9 -0
- package/frontend-vue/tests/table-actions-search-label.test.ts +147 -0
- package/frontend-vue/tests/table-clear-filters.test.ts +22 -0
- package/frontend-vue/tests/table-empty-body-viewport.test.ts +25 -0
- package/frontend-vue/tests/table-metric-cells.test.ts +22 -0
- package/frontend-vue/tests/table-pills-cell.test.ts +126 -0
- package/frontend-vue/tests/table-source-navigation.test.ts +123 -0
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +137 -0
- package/frontend-vue/tests/table-view-refresh.test.ts +84 -0
- package/frontend-vue/tests/table-view-sources.test.ts +67 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +14 -3
- package/skills/dms-pages/REFERENCE.md +17 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createApp, h, type App } from "vue";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import FlowCanvas from "../layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue";
|
|
5
|
+
|
|
6
|
+
vi.mock("@vue-flow/core", async () => {
|
|
7
|
+
const vue = await import("vue");
|
|
8
|
+
const selectedNodes = vue.ref([
|
|
9
|
+
{
|
|
10
|
+
id: "users",
|
|
11
|
+
computedPosition: { x: 10, y: 20 },
|
|
12
|
+
dimensions: { width: 100, height: 40 },
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
const Slotted = vue.defineComponent({
|
|
16
|
+
setup:
|
|
17
|
+
(_, { slots }) =>
|
|
18
|
+
() =>
|
|
19
|
+
vue.h("div", slots.default?.()),
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
VueFlow: Slotted,
|
|
23
|
+
Panel: Slotted,
|
|
24
|
+
PanelPosition: {
|
|
25
|
+
TopLeft: "top-left",
|
|
26
|
+
TopCenter: "top-center",
|
|
27
|
+
TopRight: "top-right",
|
|
28
|
+
BottomLeft: "bottom-left",
|
|
29
|
+
BottomCenter: "bottom-center",
|
|
30
|
+
BottomRight: "bottom-right",
|
|
31
|
+
},
|
|
32
|
+
useVueFlow: () => ({
|
|
33
|
+
viewport: vue.ref({ x: 0, y: 0, zoom: 1 }),
|
|
34
|
+
getSelectedNodes: selectedNodes,
|
|
35
|
+
zoomTo: () => undefined,
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
vi.mock("@vue-flow/background", () => ({
|
|
41
|
+
Background: { render: () => null },
|
|
42
|
+
}));
|
|
43
|
+
vi.mock("@vue-flow/controls", () => ({
|
|
44
|
+
Controls: { render: () => null },
|
|
45
|
+
ControlButton: { render: () => null },
|
|
46
|
+
}));
|
|
47
|
+
vi.mock("@vue-flow/minimap", () => ({ MiniMap: { render: () => null } }));
|
|
48
|
+
|
|
49
|
+
let app: App | undefined;
|
|
50
|
+
|
|
51
|
+
function mountCanvas(props: Record<string, unknown>) {
|
|
52
|
+
const deleted: string[][] = [];
|
|
53
|
+
app = createApp({
|
|
54
|
+
render: () =>
|
|
55
|
+
h(FlowCanvas, {
|
|
56
|
+
deletableNodes: true,
|
|
57
|
+
...props,
|
|
58
|
+
onDeleteNodes: (ids: string[]) => deleted.push(ids),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
const container = document.createElement("div");
|
|
62
|
+
app.mount(container);
|
|
63
|
+
return { root: container.firstElementChild as HTMLElement, deleted };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
app?.unmount();
|
|
68
|
+
app = undefined;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const BADGE = ".dms-flow-canvas__delete-button";
|
|
72
|
+
|
|
73
|
+
describe("flow canvas delete badge", () => {
|
|
74
|
+
it("draws a delete badge on a selected node by default", () => {
|
|
75
|
+
const { root, deleted } = mountCanvas({});
|
|
76
|
+
const badge = root.querySelector<HTMLButtonElement>(BADGE);
|
|
77
|
+
expect(badge).not.toBeNull();
|
|
78
|
+
badge!.click();
|
|
79
|
+
expect(deleted).toEqual([["users"]]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("hides the badge on request and keeps keyboard deletion", () => {
|
|
83
|
+
const { root, deleted } = mountCanvas({ deleteBadge: false });
|
|
84
|
+
expect(root.querySelector(BADGE)).toBeNull();
|
|
85
|
+
root.dispatchEvent(new KeyboardEvent("keydown", { key: "Delete" }));
|
|
86
|
+
expect(deleted).toEqual([["users"]]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -35,6 +35,17 @@ describe("page-mode form redirect", () => {
|
|
|
35
35
|
).toEqual({ path: "/workspaces/ws-42" });
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
// The page path the router hands over is already percent-encoded: the path
|
|
39
|
+
// sent back is the one the page was reached at.
|
|
40
|
+
it("sends back an encoded page path as it was", () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveFormSuccessTarget(
|
|
43
|
+
EDIT_REDIRECT,
|
|
44
|
+
routeOf(EDIT_PATTERN, "/workspaces/ws%2042/invoiceTable/inv-7/edit"),
|
|
45
|
+
),
|
|
46
|
+
).toEqual({ path: "/workspaces/ws%2042" });
|
|
47
|
+
});
|
|
48
|
+
|
|
38
49
|
it("carries back the declared query params only", () => {
|
|
39
50
|
expect(
|
|
40
51
|
resolveFormSuccessTarget(
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* A KeyValueList row with `copy` draws a copy button next to its value, from
|
|
4
|
+
* static items as from the items a `fetchUrl` answers; `copyValue` copies
|
|
5
|
+
* another text than the one shown.
|
|
6
|
+
*/
|
|
7
|
+
import { createApp, defineComponent, h, reactive, ref } from "vue";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { createI18n } from "vue-i18n";
|
|
10
|
+
|
|
11
|
+
const i18n = createI18n({
|
|
12
|
+
legacy: false,
|
|
13
|
+
locale: "en",
|
|
14
|
+
missingWarn: false,
|
|
15
|
+
fallbackWarn: false,
|
|
16
|
+
messages: { en: { dms: { button: { copy: "Copy", copied: "Copied" } } } },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const copied: string[] = [];
|
|
20
|
+
|
|
21
|
+
// jsdom has no clipboard: the button's copy lands in `copied` instead.
|
|
22
|
+
vi.mock("@vueuse/core", async (importOriginal) => ({
|
|
23
|
+
...(await importOriginal<typeof import("@vueuse/core")>()),
|
|
24
|
+
useClipboard: () => ({
|
|
25
|
+
copy: async (text: string) => {
|
|
26
|
+
copied.push(text);
|
|
27
|
+
},
|
|
28
|
+
copied: ref(false),
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
let fetchedItems: unknown[] = [];
|
|
33
|
+
|
|
34
|
+
vi.stubGlobal("useAuthFetch", () => ({
|
|
35
|
+
$authFetch: async () => ({ items: fetchedItems }),
|
|
36
|
+
}));
|
|
37
|
+
vi.stubGlobal("useDmsRoute", () => reactive({ query: {} }));
|
|
38
|
+
|
|
39
|
+
vi.mock("../layers/dms-core/app/composables/watch/useWatch", () => ({
|
|
40
|
+
useWatch: () => ({ state: ref({}) }),
|
|
41
|
+
}));
|
|
42
|
+
vi.mock(
|
|
43
|
+
"../layers/dms-core/app/composables/components/useComponentEvent",
|
|
44
|
+
() => ({ useComponentEvent: () => ({}) }),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const KeyValueListBlock = (
|
|
48
|
+
await import("../layers/dms-ui/app/components/blocks/KeyValueListBlock.vue")
|
|
49
|
+
).default;
|
|
50
|
+
|
|
51
|
+
const SETTLE_MS = 50;
|
|
52
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
|
|
53
|
+
const mounted: Array<{ unmount: () => void }> = [];
|
|
54
|
+
|
|
55
|
+
function render(props: Record<string, unknown>): HTMLElement {
|
|
56
|
+
const root = document.createElement("div");
|
|
57
|
+
const app = createApp(KeyValueListBlock, { card: false, ...props });
|
|
58
|
+
app.component("UIcon", defineComponent({ render: () => h("i") }));
|
|
59
|
+
app.component("USkeleton", defineComponent({ render: () => h("span") }));
|
|
60
|
+
app.mount(root);
|
|
61
|
+
mounted.push(app);
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function copyButtons(root: HTMLElement): HTMLButtonElement[] {
|
|
66
|
+
return [...root.querySelectorAll<HTMLButtonElement>("button[title]")];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
fetchedItems = [];
|
|
71
|
+
copied.splice(0);
|
|
72
|
+
vi.stubGlobal("useI18n", () => ({
|
|
73
|
+
t: i18n.global.t,
|
|
74
|
+
locale: i18n.global.locale,
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
mounted.splice(0).forEach((app) => app.unmount());
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("KeyValueList copy button", () => {
|
|
83
|
+
it("copies the shown value of a row that asks for it, and only those", async () => {
|
|
84
|
+
const root = render({
|
|
85
|
+
items: [
|
|
86
|
+
{ label: "Endpoint", value: "https://api.example.com", copy: true },
|
|
87
|
+
{ label: "Plan", value: "Business" },
|
|
88
|
+
{ label: "Token", value: null, copy: true },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const buttons = copyButtons(root);
|
|
93
|
+
expect(buttons).toHaveLength(1);
|
|
94
|
+
buttons[0]!.click();
|
|
95
|
+
await settle();
|
|
96
|
+
expect(copied).toEqual(["https://api.example.com"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("copies copyValue rather than the shown text, from fetched items", async () => {
|
|
100
|
+
fetchedItems = [
|
|
101
|
+
{
|
|
102
|
+
label: "Thumbnail",
|
|
103
|
+
value: "thumb.webp",
|
|
104
|
+
copyValue: "https://cdn.example.com/media/42/thumb.webp",
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
const root = render({ fetchUrl: "/api/media/42/urls" });
|
|
108
|
+
await settle();
|
|
109
|
+
|
|
110
|
+
copyButtons(root)[0]!.click();
|
|
111
|
+
await settle();
|
|
112
|
+
expect(copied).toEqual(["https://cdn.example.com/media/42/thumb.webp"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, expect, it } from "vitest";
|
|
3
|
+
import { type App, createApp, h, nextTick } from "vue";
|
|
4
|
+
|
|
5
|
+
const ITEMS = [
|
|
6
|
+
{ value: "a", label: "Alpha" },
|
|
7
|
+
{ value: "b", label: "Beta" },
|
|
8
|
+
];
|
|
9
|
+
// The former `lg` viewport breakpoint, now read off the component's own width.
|
|
10
|
+
const SIDE_BY_SIDE_COLUMNS = "@5xl:grid-cols-[380px_minmax(0,1fr)]";
|
|
11
|
+
const VIEWPORT_VARIANT = /(?:^|\s)(?:sm|md|lg|xl|2xl):/;
|
|
12
|
+
|
|
13
|
+
let app: App | undefined;
|
|
14
|
+
let host: HTMLDivElement;
|
|
15
|
+
|
|
16
|
+
async function mount() {
|
|
17
|
+
const { default: MasterDetail } = await import(
|
|
18
|
+
"../layers/dms-ui/app/components/master-detail/MasterDetail.vue"
|
|
19
|
+
);
|
|
20
|
+
app = createApp({
|
|
21
|
+
setup: () => () =>
|
|
22
|
+
h(
|
|
23
|
+
MasterDetail,
|
|
24
|
+
{ items: ITEMS, modelValue: "a" },
|
|
25
|
+
{ default: () => h("p", "detail") },
|
|
26
|
+
),
|
|
27
|
+
});
|
|
28
|
+
app.component("UIcon", () => h("i"));
|
|
29
|
+
app.mount(host);
|
|
30
|
+
await nextTick();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
host = document.createElement("div");
|
|
35
|
+
document.body.append(host);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
app?.unmount();
|
|
40
|
+
host.remove();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("lays the panes out from its own width, not the viewport's", async () => {
|
|
44
|
+
await mount();
|
|
45
|
+
const container = host.firstElementChild as HTMLElement;
|
|
46
|
+
const grid = container.firstElementChild as HTMLElement;
|
|
47
|
+
expect(container.classList).toContain("@container");
|
|
48
|
+
expect(grid.classList).toContain("grid-cols-1");
|
|
49
|
+
expect(grid.classList).toContain(SIDE_BY_SIDE_COLUMNS);
|
|
50
|
+
for (const element of host.querySelectorAll("*")) {
|
|
51
|
+
expect(element.getAttribute("class") ?? "").not.toMatch(VIEWPORT_VARIANT);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createApp, defineComponent, h, type App } from "vue";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import MonoCell from "../layers/dms-ui/app/build/components/table/MonoCell.vue";
|
|
5
|
+
|
|
6
|
+
const ButtonStub = defineComponent({
|
|
7
|
+
setup: () => () => h("button"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
let app: App | undefined;
|
|
11
|
+
|
|
12
|
+
function mountCell(copy: boolean) {
|
|
13
|
+
app = createApp({
|
|
14
|
+
render: () => h(MonoCell, { value: "a-very-long-identifier", copy }),
|
|
15
|
+
});
|
|
16
|
+
app.component("UButton", ButtonStub);
|
|
17
|
+
const container = document.createElement("div");
|
|
18
|
+
app.mount(container);
|
|
19
|
+
return container.firstElementChild as HTMLElement;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
|
|
24
|
+
vi.stubGlobal("useToast", () => ({ add: () => undefined }));
|
|
25
|
+
vi.stubGlobal("Color", { success: "success" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
app?.unmount();
|
|
30
|
+
app = undefined;
|
|
31
|
+
vi.unstubAllGlobals();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("mono cell", () => {
|
|
35
|
+
it("holds its root to the cell's width so a long value can truncate", () => {
|
|
36
|
+
const root = mountCell(true);
|
|
37
|
+
expect(root.classList).toContain("flex");
|
|
38
|
+
expect(root.classList).not.toContain("inline-flex");
|
|
39
|
+
expect(root.classList).toContain("max-w-full");
|
|
40
|
+
expect(root.classList).toContain("min-w-0");
|
|
41
|
+
const value = root.querySelector("span");
|
|
42
|
+
expect(value?.classList).toContain("truncate");
|
|
43
|
+
expect(value?.getAttribute("title")).toBe("a-very-long-identifier");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps the copy button at its size beside the value", () => {
|
|
47
|
+
const root = mountCell(true);
|
|
48
|
+
expect(root.querySelector("button")?.classList).toContain("shrink-0");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -90,9 +90,9 @@ const FLOW_PAGE_MARKUP =
|
|
|
90
90
|
'<h1 class="text-highlighted text-2xl font-[650] leading-[1.2] tracking-[-0.03em]"><!--[-->Explorer<!--]--></h1><!--v-if--></div><!--[--><!--[--><!-- eslint-disable vue/no-v-html --><!--v-if--><!--]--><!--]--></section>' +
|
|
91
91
|
'<div class="contents [html[data-dms-role-preview=pending]_&]:hidden"><!--[--><!--[--><!--[-->' +
|
|
92
92
|
'<div class="dms-page-stack space-y-6">' +
|
|
93
|
-
'<div class="">' +
|
|
93
|
+
'<div class="empty:hidden">' +
|
|
94
94
|
'<section data-component="stats" page-id="tools.explorer" layout-path="stats"></section></div>' +
|
|
95
|
-
'<div class="">' +
|
|
95
|
+
'<div class="empty:hidden">' +
|
|
96
96
|
'<section data-component="explorer" page-id="tools.explorer" layout-path="explorer"></section></div></div><!----><!--]--><!--]--><!--]--></div>' +
|
|
97
97
|
PAGE_SKELETON_MARKUP +
|
|
98
98
|
"<!----><!--]--><!--]--><!--]--></div></div><!--[--><!----><!--]--></div><!----></div>";
|
|
@@ -124,8 +124,12 @@ const Container: FunctionalComponent = (_, { attrs, slots }) =>
|
|
|
124
124
|
h("div", { ...attrs, class: ["w-full", attrs.class] }, slots.default?.());
|
|
125
125
|
Container.inheritAttrs = false;
|
|
126
126
|
|
|
127
|
+
// A component id starting with `hidden` stands for a block that has nothing
|
|
128
|
+
// to show (a fetched Banner the route answered `null`): it renders nothing.
|
|
127
129
|
const PageComponent: FunctionalComponent<{ componentId: string }> = (props) =>
|
|
128
|
-
|
|
130
|
+
props.componentId.startsWith("hidden")
|
|
131
|
+
? null
|
|
132
|
+
: h("section", { "data-component": props.componentId });
|
|
129
133
|
PageComponent.props = ["componentId"];
|
|
130
134
|
|
|
131
135
|
const FillHeightProbe: FunctionalComponent = () =>
|
|
@@ -299,6 +303,19 @@ describe("DefaultLayout without fillHeight", () => {
|
|
|
299
303
|
});
|
|
300
304
|
});
|
|
301
305
|
|
|
306
|
+
describe("a block that renders nothing", () => {
|
|
307
|
+
it("leaves an empty wrapper the stack hides, so it takes no gap", async () => {
|
|
308
|
+
pageLayout.value = withComponents("hiddenBanner", "stats");
|
|
309
|
+
const host = parse(await renderInLayout({}, CatchAllPage));
|
|
310
|
+
const [banner, stats] = Array.from(
|
|
311
|
+
host.querySelectorAll(".dms-page-stack > div"),
|
|
312
|
+
);
|
|
313
|
+
expect(banner?.matches(":empty")).toBe(true);
|
|
314
|
+
expectClasses(banner, "empty:hidden");
|
|
315
|
+
expect(stats?.matches(":empty")).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
302
319
|
describe("DefaultLayout with fillHeight", () => {
|
|
303
320
|
it("makes the page region a flex column with a minimum height", async () => {
|
|
304
321
|
const host = parse(
|
|
@@ -89,6 +89,15 @@ describe("form URL variables on a repeated placeholder", () => {
|
|
|
89
89
|
).toBe("/workspaces/ws-1/invoices/row-7");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
it("encodes an occurrence like any parameter", () => {
|
|
93
|
+
expect(
|
|
94
|
+
replaceUrlVariables("/workspaces/{{params.id:1}}/rows", {
|
|
95
|
+
routeParams: { id: "row-7", "id:1": "ws 1/2", "id:2": "row-7" },
|
|
96
|
+
routeQuery: {},
|
|
97
|
+
}),
|
|
98
|
+
).toBe("/workspaces/ws%201%2F2/rows");
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
it("leaves an indexed token that no occurrence answers alone", () => {
|
|
93
102
|
expect(replaceUrlVariables("/invoices/{{params.id:3}}", context)).toBe(
|
|
94
103
|
"/invoices/{{params.id:3}}",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
createApp,
|
|
5
|
+
defineComponent,
|
|
6
|
+
h,
|
|
7
|
+
nextTick,
|
|
8
|
+
provide,
|
|
9
|
+
ref,
|
|
10
|
+
shallowRef,
|
|
11
|
+
watch,
|
|
12
|
+
type App,
|
|
13
|
+
} from "vue";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import TableActions from "../layers/dms-ui/app/build/components/table/Actions.vue";
|
|
16
|
+
import { FULL_TABLE_CHROME } from "../layers/dms-ui/app/build/composables/table-view/utils/chrome";
|
|
17
|
+
|
|
18
|
+
vi.mock("../layers/dms-ui/app/build/components/table/Menu.vue", () => ({
|
|
19
|
+
default: { render: () => null },
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("../layers/dms-ui/app/build/components/form/SearchInput.vue", () => ({
|
|
23
|
+
default: { render: () => null },
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const PassThrough = defineComponent({
|
|
27
|
+
setup:
|
|
28
|
+
(_, { slots }) =>
|
|
29
|
+
() =>
|
|
30
|
+
h("div", slots.default?.()),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ButtonStub = defineComponent({
|
|
34
|
+
setup:
|
|
35
|
+
(_, { slots }) =>
|
|
36
|
+
() =>
|
|
37
|
+
h("button", slots.default?.()),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const InputStub = defineComponent({
|
|
41
|
+
setup: () => () => h("input"),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let app: App | undefined;
|
|
45
|
+
|
|
46
|
+
function mountActions(searchPlaceholder: string | undefined, search = "") {
|
|
47
|
+
const Host = defineComponent({
|
|
48
|
+
setup() {
|
|
49
|
+
provide(
|
|
50
|
+
"tableSharedData",
|
|
51
|
+
shallowRef({
|
|
52
|
+
activeCapabilities: ref({
|
|
53
|
+
search: true,
|
|
54
|
+
filters: false,
|
|
55
|
+
sorting: false,
|
|
56
|
+
}),
|
|
57
|
+
labeledColumns: ref([]),
|
|
58
|
+
globalFilterState: ref(search),
|
|
59
|
+
columnFiltersState: ref([]),
|
|
60
|
+
paginationState: ref({ pageIndex: 0, pageSize: 10 }),
|
|
61
|
+
filtersRowOpenState: ref(false),
|
|
62
|
+
hasCustomSort: ref(false),
|
|
63
|
+
displays: [],
|
|
64
|
+
activeDisplayState: ref("table"),
|
|
65
|
+
kanbanGroupByOptions: [],
|
|
66
|
+
kanbanGroupByState: ref(undefined),
|
|
67
|
+
onFiltersCleared: () => undefined,
|
|
68
|
+
emits: () => undefined,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return () =>
|
|
72
|
+
h(TableActions, {
|
|
73
|
+
searchPlaceholder,
|
|
74
|
+
chrome: { ...FULL_TABLE_CHROME, refresh: false, menu: false },
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
app = createApp(Host);
|
|
79
|
+
for (const [name, stub] of Object.entries({
|
|
80
|
+
UButton: ButtonStub,
|
|
81
|
+
UInput: InputStub,
|
|
82
|
+
UKbd: PassThrough,
|
|
83
|
+
UPopover: PassThrough,
|
|
84
|
+
UDropdownMenu: PassThrough,
|
|
85
|
+
UTooltip: PassThrough,
|
|
86
|
+
UIcon: PassThrough,
|
|
87
|
+
})) {
|
|
88
|
+
app.component(name, stub);
|
|
89
|
+
}
|
|
90
|
+
const container = document.createElement("div");
|
|
91
|
+
app.mount(container);
|
|
92
|
+
return container;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
vi.stubGlobal("computed", computed);
|
|
97
|
+
vi.stubGlobal("ref", ref);
|
|
98
|
+
vi.stubGlobal("watch", watch);
|
|
99
|
+
vi.stubGlobal("defineShortcuts", () => undefined);
|
|
100
|
+
vi.stubGlobal("useDmsAppConfig", () => ({
|
|
101
|
+
ui: { icons: { close: "close", plus: "plus", ellipsis: "ellipsis" } },
|
|
102
|
+
}));
|
|
103
|
+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
|
|
104
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
105
|
+
processI18n: (value: string) =>
|
|
106
|
+
value.startsWith("$") ? `translated:${value.slice(1)}` : value,
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
app?.unmount();
|
|
112
|
+
app = undefined;
|
|
113
|
+
vi.unstubAllGlobals();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("table toolbar search", () => {
|
|
117
|
+
it("names the button opening the search", () => {
|
|
118
|
+
const container = mountActions(undefined);
|
|
119
|
+
expect(container.querySelector("button")?.getAttribute("aria-label")).toBe(
|
|
120
|
+
"dms.table.search_open",
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("uses the configured placeholder, which also names the field", async () => {
|
|
125
|
+
const container = mountActions("$page.members.search", "ada");
|
|
126
|
+
await nextTick();
|
|
127
|
+
const input = container.querySelector("input");
|
|
128
|
+
expect(input?.getAttribute("placeholder")).toBe(
|
|
129
|
+
"translated:page.members.search",
|
|
130
|
+
);
|
|
131
|
+
expect(input?.getAttribute("aria-label")).toBe(
|
|
132
|
+
"translated:page.members.search",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("falls back to the default placeholder as the field's name", async () => {
|
|
137
|
+
const container = mountActions(undefined, "ada");
|
|
138
|
+
await nextTick();
|
|
139
|
+
const input = container.querySelector("input");
|
|
140
|
+
expect(input?.getAttribute("placeholder")).toBe(
|
|
141
|
+
"dms.table.search_placeholder",
|
|
142
|
+
);
|
|
143
|
+
expect(input?.getAttribute("aria-label")).toBe(
|
|
144
|
+
"dms.table.search_placeholder",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -148,6 +148,18 @@ describe("clearTableFilters", () => {
|
|
|
148
148
|
).toBe(true);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
it("counts the hidden filters of the page URL as filters", () => {
|
|
152
|
+
const state = { ...createState(), hiddenFiltersApply: ref(true) };
|
|
153
|
+
expect(isTableNarrowed(state)).toBe(true);
|
|
154
|
+
// Nothing the user could clear: no clear action is offered.
|
|
155
|
+
expect(clearableTableFilters(state)).toEqual({
|
|
156
|
+
filters: false,
|
|
157
|
+
search: false,
|
|
158
|
+
});
|
|
159
|
+
state.hiddenFiltersApply.value = false;
|
|
160
|
+
expect(isTableNarrowed(state)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
151
163
|
it("ignores a search the active display does not apply", () => {
|
|
152
164
|
const state = {
|
|
153
165
|
...createState({ globalFilter: "a" }),
|
|
@@ -244,6 +256,16 @@ describe("table empty state", () => {
|
|
|
244
256
|
expect(state.globalFilter.value).toBe("");
|
|
245
257
|
});
|
|
246
258
|
|
|
259
|
+
it("says no row matches, not that there are none yet, under hidden filters", () => {
|
|
260
|
+
const container = mountEmpty({
|
|
261
|
+
...createState(),
|
|
262
|
+
hiddenFiltersApply: ref(true),
|
|
263
|
+
});
|
|
264
|
+
expect(container.textContent).toContain("dms.table.no_results_title");
|
|
265
|
+
expect(container.textContent).not.toContain("dms.table.empty_title");
|
|
266
|
+
expect(container.querySelector("button")).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
|
|
247
269
|
it("offers no clear action when only default filters narrow the rows", () => {
|
|
248
270
|
const container = mountEmpty(
|
|
249
271
|
createState({ columnFilters: [pinnedStatus("open")] }),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
const tableSource = readFileSync(
|
|
6
|
+
resolve(__dirname, "../layers/dms-ui/app/build/components/table/Table.vue"),
|
|
7
|
+
"utf8",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe("table empty body", () => {
|
|
11
|
+
it("centres on the visible part of a table wider than its card", () => {
|
|
12
|
+
expect(tableSource).toMatch(
|
|
13
|
+
/emptyBody:\s*"sticky start-0 w-\[var\(--dms-table-viewport,auto\)\]"/,
|
|
14
|
+
);
|
|
15
|
+
expect(tableSource).toMatch(
|
|
16
|
+
/<div :class="uiTable\.emptyBody\(\)">\s*<TableEmpty/,
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("knows the visible width of every table, not only expandable ones", () => {
|
|
21
|
+
expect(tableSource).toMatch(
|
|
22
|
+
/"--dms-table-viewport":\s*tableViewportWidth\.value > 0/,
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -103,6 +103,28 @@ describe("metric cells", () => {
|
|
|
103
103
|
expect(cell.innerHTML).toContain("text-error");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
it("tones a status pill by the row's tone field before the column's map", () => {
|
|
107
|
+
const options = { tones: { "404": "info" }, toneField: "statusTone" };
|
|
108
|
+
const pill = (row: Record<string, unknown>) =>
|
|
109
|
+
render(formatters.get("status_pill")!(404, "en-GB", options, row))
|
|
110
|
+
.innerHTML;
|
|
111
|
+
|
|
112
|
+
expect(pill({ statusTone: "warning" })).toContain("text-warning");
|
|
113
|
+
app?.unmount();
|
|
114
|
+
expect(pill({ statusTone: "not-a-tone" })).toContain("text-info");
|
|
115
|
+
app?.unmount();
|
|
116
|
+
expect(
|
|
117
|
+
render(
|
|
118
|
+
formatters.get("status_pill")!(
|
|
119
|
+
503,
|
|
120
|
+
"en-GB",
|
|
121
|
+
{ toneField: "statusTone" },
|
|
122
|
+
{ statusTone: "error" },
|
|
123
|
+
),
|
|
124
|
+
).innerHTML,
|
|
125
|
+
).toContain("text-error");
|
|
126
|
+
});
|
|
127
|
+
|
|
106
128
|
it("draws a progress bar with its figures", () => {
|
|
107
129
|
const cell = render(
|
|
108
130
|
formatters.get("progress")!(
|