@antelopejs/dms 0.7.18 → 0.7.20
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 +16 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRecordLink.ts +93 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +8 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/urlFilters.ts +18 -0
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +29 -48
- package/frontend-vue/tests/table-view-bulk-drawer.test.ts +3 -0
- package/frontend-vue/tests/table-view-record-link.test.ts +207 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.7.20
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.7.19...v0.7.20)
|
|
6
|
+
|
|
7
|
+
### 🩹 Fixes
|
|
8
|
+
|
|
9
|
+
- **table-view:** Reopen a record link to a closed row and keep chips from restoring it ([#249](https://github.com/AntelopeJS/dms/pull/249))
|
|
10
|
+
|
|
11
|
+
### ❤️ Contributors
|
|
12
|
+
|
|
13
|
+
- Antony Rizzitelli <rizzitelli.antony@pm.me>
|
|
14
|
+
|
|
15
|
+
## v0.7.19
|
|
16
|
+
|
|
17
|
+
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.7.18...v0.7.19)
|
|
18
|
+
|
|
3
19
|
## v0.7.18
|
|
4
20
|
|
|
5
21
|
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.7.17...v0.7.18)
|
|
@@ -110,6 +110,11 @@ export interface ActionTargetsConfig {
|
|
|
110
110
|
rowNavigation?: RowNavigationSource;
|
|
111
111
|
/** Where a `deepLink` action writes the row it has open in the URL. */
|
|
112
112
|
recordScope?: TableUrlScope;
|
|
113
|
+
/**
|
|
114
|
+
* Told the row a `deepLink` action shows each time it writes it in the URL,
|
|
115
|
+
* and `undefined` once its container closed and removed it.
|
|
116
|
+
*/
|
|
117
|
+
onRecordShown?: (id?: string) => void;
|
|
113
118
|
/**
|
|
114
119
|
* The field a row's id is read from (`_id` by default): it fills `{id}` in
|
|
115
120
|
* the `from` URL of a row action's confirmation, as on a built-in action.
|
|
@@ -326,11 +331,12 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
326
331
|
return (id?: string): void => {
|
|
327
332
|
if (!deepLink || !config.recordScope || !config.rowNavigation) return;
|
|
328
333
|
if (!id) {
|
|
329
|
-
clearWrittenRecord(written);
|
|
334
|
+
if (clearWrittenRecord(written)) config.onRecordShown?.(undefined);
|
|
330
335
|
written = undefined;
|
|
331
336
|
return;
|
|
332
337
|
}
|
|
333
338
|
written = writeRecordId(config.recordScope, id);
|
|
339
|
+
config.onRecordShown?.(id);
|
|
334
340
|
};
|
|
335
341
|
};
|
|
336
342
|
const rowRecorder = (deepLink?: boolean) => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { router as inertiaRouter } from "@inertiajs/vue3";
|
|
2
|
+
import { onBeforeUnmount, onMounted, watch } from "vue";
|
|
3
|
+
import type { CustomRowAction } from "../../../types/row-action";
|
|
4
|
+
import {
|
|
5
|
+
deepLinkOutcome,
|
|
6
|
+
type DeepLinkedRow,
|
|
7
|
+
readRecordId,
|
|
8
|
+
writeRecordId,
|
|
9
|
+
} from "./utils/recordLink";
|
|
10
|
+
import { queryOfUrl } from "./utils/urlFilters";
|
|
11
|
+
import type { TableUrlScope } from "./utils/views";
|
|
12
|
+
|
|
13
|
+
/** What `useTableRecordLink` follows the URL's `?record=` with. */
|
|
14
|
+
export interface TableRecordLinkOptions<T> {
|
|
15
|
+
route: { query: Record<string, unknown> };
|
|
16
|
+
urlScope: TableUrlScope;
|
|
17
|
+
/** The table's `deepLink` action: the container a link opens. */
|
|
18
|
+
action: CustomRowAction;
|
|
19
|
+
/** Finds the row a link names, once the list is loaded. */
|
|
20
|
+
resolve: (id: string) => Promise<DeepLinkedRow<T>>;
|
|
21
|
+
/** Opens the row found in the action's container. */
|
|
22
|
+
open: (row: T) => void;
|
|
23
|
+
/** Opens a side panel's own "no longer available" state on the id. */
|
|
24
|
+
showMissing: (id: string) => void;
|
|
25
|
+
/** Tells the user the link led nowhere, or could not be followed. */
|
|
26
|
+
notify: (outcome: "notify" | "notifyFailed") => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What the table tells the link about the row its container shows. */
|
|
30
|
+
export interface TableRecordLink {
|
|
31
|
+
/**
|
|
32
|
+
* The row the action's container shows, as written in the URL, or
|
|
33
|
+
* `undefined` once it closed: a link to the row shown opens nothing again,
|
|
34
|
+
* a link to a row closed reopens it.
|
|
35
|
+
*/
|
|
36
|
+
shown: (id?: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Opens the row the URL's `?record=` names: on arrival, and on each visit
|
|
41
|
+
* that keeps the page mounted (a link to the same page naming a row, back and
|
|
42
|
+
* forward), as a fresh load would. The visit is followed rather than the
|
|
43
|
+
* route's value: the table writes and removes the key in place, so a link to
|
|
44
|
+
* a row closed meanwhile leaves the route's value unchanged. A link naming no
|
|
45
|
+
* row removes the key and tells the user.
|
|
46
|
+
*/
|
|
47
|
+
export function useTableRecordLink<T>(
|
|
48
|
+
options: TableRecordLinkOptions<T>,
|
|
49
|
+
): TableRecordLink {
|
|
50
|
+
const { action, urlScope } = options;
|
|
51
|
+
// The row opened or being opened: arrival and the visit that mounted the
|
|
52
|
+
// table name it twice, a visit while it is open names it again.
|
|
53
|
+
let linkedId: string | undefined;
|
|
54
|
+
|
|
55
|
+
const settle = async (id: string) => {
|
|
56
|
+
const found = await options.resolve(id);
|
|
57
|
+
const outcome = deepLinkOutcome(found, action);
|
|
58
|
+
if (outcome === "open") return options.open(found.row as T);
|
|
59
|
+
if (outcome === "missingPanel") return options.showMissing(id);
|
|
60
|
+
writeRecordId(urlScope);
|
|
61
|
+
if (linkedId === id) linkedId = undefined;
|
|
62
|
+
options.notify(outcome);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const follow = (query: Record<string, unknown>) => {
|
|
66
|
+
const id = readRecordId(query, urlScope);
|
|
67
|
+
if (!id || id === linkedId) return;
|
|
68
|
+
linkedId = id;
|
|
69
|
+
void settle(id);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let stopFollowingVisits: (() => void) | undefined;
|
|
73
|
+
onMounted(() => {
|
|
74
|
+
follow(options.route.query);
|
|
75
|
+
stopFollowingVisits = inertiaRouter.on("navigate", (event) =>
|
|
76
|
+
follow(queryOfUrl(event.detail.page.url)),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
onBeforeUnmount(() => stopFollowingVisits?.());
|
|
80
|
+
|
|
81
|
+
// A visit Inertia does not announce (a `router.replace`) still moves the
|
|
82
|
+
// route: a new value there is followed as well.
|
|
83
|
+
watch(
|
|
84
|
+
() => readRecordId(options.route.query, urlScope),
|
|
85
|
+
() => follow(options.route.query),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
shown: (id) => {
|
|
90
|
+
linkedId = id;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -76,6 +76,8 @@ interface TableRowActionsConfig {
|
|
|
76
76
|
rowNavigation?: RowNavigationSource;
|
|
77
77
|
/** Where a `deepLink` action writes its open row in the URL. */
|
|
78
78
|
recordScope?: TableUrlScope;
|
|
79
|
+
/** Told the row a `deepLink` action shows, `undefined` once closed. */
|
|
80
|
+
onRecordShown?: (id?: string) => void;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
interface BulkActionConfig {
|
|
@@ -814,6 +816,7 @@ export const useTableRowActions = <T extends Data>(
|
|
|
814
816
|
handleApiError,
|
|
815
817
|
rowNavigation: config.rowNavigation,
|
|
816
818
|
recordScope: config.recordScope,
|
|
819
|
+
onRecordShown: config.onRecordShown,
|
|
817
820
|
rowIdKey: config.rowIdKey,
|
|
818
821
|
});
|
|
819
822
|
|
|
@@ -58,15 +58,18 @@ export interface WrittenRecord {
|
|
|
58
58
|
/**
|
|
59
59
|
* Removes the row a table wrote in the URL, only while the URL still holds
|
|
60
60
|
* it: a container closing as the user leaves the page must not erase the
|
|
61
|
-
* `?record=` of the page reached.
|
|
61
|
+
* `?record=` of the page reached. Returns whether it removed it.
|
|
62
62
|
*/
|
|
63
|
-
export function clearWrittenRecord(
|
|
64
|
-
|
|
63
|
+
export function clearWrittenRecord(
|
|
64
|
+
written: WrittenRecord | undefined,
|
|
65
|
+
): boolean {
|
|
66
|
+
if (typeof window === "undefined" || !written) return false;
|
|
65
67
|
const url = new URL(window.location.href);
|
|
66
|
-
if (url.pathname !== written.pathname) return;
|
|
67
|
-
if (url.searchParams.get(written.key) !== written.id) return;
|
|
68
|
+
if (url.pathname !== written.pathname) return false;
|
|
69
|
+
if (url.searchParams.get(written.key) !== written.id) return false;
|
|
68
70
|
url.searchParams.delete(written.key);
|
|
69
71
|
window.history.replaceState(window.history.state, "", url);
|
|
72
|
+
return true;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/** Where a deep link finds its row: among the rows listed, or by id. */
|
|
@@ -27,3 +27,21 @@ export function withoutQueryParam<Query extends Record<string, unknown>>(
|
|
|
27
27
|
const { [param]: _removed, ...rest } = query;
|
|
28
28
|
return rest as Query;
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/** The query of a URL, absolute or relative to the page, one value per key. */
|
|
32
|
+
export function queryOfUrl(url: string): Record<string, string> {
|
|
33
|
+
return Object.fromEntries(new URL(url, "http://dms.invalid").searchParams);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The query the browser shows, else the route's on a server render. A table
|
|
38
|
+
* writes some keys in place (the open `?record=`, removed on close), so the
|
|
39
|
+
* route, which follows the last visit, can still hold a key the browser no
|
|
40
|
+
* longer shows: a visit rebuilt from it would bring the key back.
|
|
41
|
+
*/
|
|
42
|
+
export function currentQuery(
|
|
43
|
+
routeQuery: Record<string, unknown>,
|
|
44
|
+
): Record<string, unknown> {
|
|
45
|
+
if (typeof window === "undefined") return routeQuery;
|
|
46
|
+
return queryOfUrl(window.location.href);
|
|
47
|
+
}
|
|
@@ -78,12 +78,8 @@ import { selectedRowIds } from "../../build/composables/table-view/utils/bulkAct
|
|
|
78
78
|
import { isBulkAction } from "../../build/composables/actions/bulkSelection";
|
|
79
79
|
import { selectionScopeKey } from "../../build/utils/selectionScope";
|
|
80
80
|
import {
|
|
81
|
-
type DeepLinkOutcome,
|
|
82
|
-
deepLinkOutcome,
|
|
83
81
|
isDeepLinkAction,
|
|
84
|
-
readRecordId,
|
|
85
82
|
resolveDeepLinkedRow,
|
|
86
|
-
writeRecordId,
|
|
87
83
|
} from "../../build/composables/table-view/utils/recordLink";
|
|
88
84
|
import {
|
|
89
85
|
rowSidePanelOwnerKey,
|
|
@@ -119,6 +115,7 @@ import type {
|
|
|
119
115
|
TableViewSwitcherItem,
|
|
120
116
|
} from "../../build/components/table/Table.vue";
|
|
121
117
|
import {
|
|
118
|
+
currentQuery,
|
|
122
119
|
visibleUrlFilters,
|
|
123
120
|
withoutQueryParam,
|
|
124
121
|
} from "../../build/composables/table-view/utils/urlFilters";
|
|
@@ -155,6 +152,7 @@ import {
|
|
|
155
152
|
readTableUrlTab,
|
|
156
153
|
useTableUrlTab,
|
|
157
154
|
} from "../../build/composables/table-view/useTableUrlTab";
|
|
155
|
+
import { useTableRecordLink } from "../../build/composables/table-view/useTableRecordLink";
|
|
158
156
|
import TableViews, {
|
|
159
157
|
type TableViewItem,
|
|
160
158
|
} from "../../build/components/table/Views.vue";
|
|
@@ -411,9 +409,11 @@ const shownUrlFilters = computed<TableUrlFilter[]>(() =>
|
|
|
411
409
|
visibleUrlFilters(queryParamFilters, route.query as Record<string, unknown>),
|
|
412
410
|
);
|
|
413
411
|
|
|
412
|
+
// Rebuilt from the URL the browser shows: a `?record=` a closed drawer
|
|
413
|
+
// removed in place must not come back with the visit.
|
|
414
414
|
const removeUrlFilter = (param: string) =>
|
|
415
415
|
router.replace({
|
|
416
|
-
query: withoutQueryParam(route.query, param),
|
|
416
|
+
query: withoutQueryParam(currentQuery(route.query), param),
|
|
417
417
|
});
|
|
418
418
|
|
|
419
419
|
const routeParamHiddenFilters = computed<TableFilter[]>(() => {
|
|
@@ -1298,6 +1298,7 @@ const {
|
|
|
1298
1298
|
rowIdKey: props.rowIdKey ?? ROW_ID_DEFAULT_KEY,
|
|
1299
1299
|
},
|
|
1300
1300
|
recordScope: urlScope,
|
|
1301
|
+
onRecordShown: (id) => recordLink?.shown(id),
|
|
1301
1302
|
});
|
|
1302
1303
|
|
|
1303
1304
|
// Custom actions offered on the selection bar. "Select all N matching"
|
|
@@ -1704,59 +1705,40 @@ watch(rowSelect, (selection) => {
|
|
|
1704
1705
|
const deepLinkAction = (tableProps.value.rowActions?.custom ?? []).find(
|
|
1705
1706
|
isDeepLinkAction,
|
|
1706
1707
|
);
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
};
|
|
1708
|
+
const recordLink = deepLinkAction
|
|
1709
|
+
? useTableRecordLink<T>({
|
|
1710
|
+
route,
|
|
1711
|
+
urlScope,
|
|
1712
|
+
action: deepLinkAction,
|
|
1713
|
+
resolve: async (id) => {
|
|
1714
|
+
await until(isListLoading).toBe(false);
|
|
1715
|
+
return resolveDeepLinkedRow(id, {
|
|
1716
|
+
listed: shownResults.value ?? undefined,
|
|
1717
|
+
idOf: getRowId,
|
|
1718
|
+
getRow: tableRows.getRow,
|
|
1719
|
+
});
|
|
1720
|
+
},
|
|
1721
|
+
open: (row) => handleCustomRowAction(deepLinkAction, row as Data),
|
|
1722
|
+
showMissing: (id) => handleMissingRow(deepLinkAction, id),
|
|
1723
|
+
notify: (outcome) => toast.add(recordLinkToasts[outcome]()),
|
|
1724
|
+
})
|
|
1725
|
+
: undefined;
|
|
1719
1726
|
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
toast.add({
|
|
1727
|
+
const recordLinkToasts = {
|
|
1728
|
+
notify: () => ({
|
|
1723
1729
|
title: t("dms.record_link.not_found_title"),
|
|
1724
1730
|
description: t("dms.record_link.not_found_description"),
|
|
1725
1731
|
color: Color.warning,
|
|
1726
1732
|
icon: "i-ph-link-break",
|
|
1727
|
-
})
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
const notifyFailedRecordLink = () => {
|
|
1731
|
-
writeRecordId(urlScope);
|
|
1732
|
-
toast.add({
|
|
1733
|
+
}),
|
|
1734
|
+
notifyFailed: () => ({
|
|
1733
1735
|
title: t("dms.record_link.failed_title"),
|
|
1734
1736
|
description: t("dms.record_link.failed_description"),
|
|
1735
1737
|
color: Color.error,
|
|
1736
1738
|
icon: "i-ph-warning-circle",
|
|
1737
|
-
})
|
|
1738
|
-
};
|
|
1739
|
-
|
|
1740
|
-
const deepLinkOutcomes: Record<
|
|
1741
|
-
DeepLinkOutcome,
|
|
1742
|
-
(action: CustomRowAction, id: string, row: Data) => void
|
|
1743
|
-
> = {
|
|
1744
|
-
open: (action, _id, row) => handleCustomRowAction(action, row),
|
|
1745
|
-
missingPanel: (action, id) => handleMissingRow(action, id),
|
|
1746
|
-
notify: notifyUnresolvedRecordLink,
|
|
1747
|
-
notifyFailed: notifyFailedRecordLink,
|
|
1739
|
+
}),
|
|
1748
1740
|
};
|
|
1749
1741
|
|
|
1750
|
-
// A visit that keeps the page mounted (a link to the same page with another
|
|
1751
|
-
// `?record=`) opens the row it names, as a fresh load would. The table's own
|
|
1752
|
-
// writes replace the URL in place and never reach the route.
|
|
1753
|
-
watch(
|
|
1754
|
-
() => readRecordId(route.query, urlScope),
|
|
1755
|
-
(id, previous) => {
|
|
1756
|
-
if (id && id !== previous) void openRecordFromUrl();
|
|
1757
|
-
},
|
|
1758
|
-
);
|
|
1759
|
-
|
|
1760
1742
|
const selectedIds = computed(() => selectedRowIds(rowSelect.value));
|
|
1761
1743
|
|
|
1762
1744
|
const isRowActionEnabled = (
|
|
@@ -2086,7 +2068,6 @@ const checkUrlParameter = () => {
|
|
|
2086
2068
|
|
|
2087
2069
|
onMounted(() => {
|
|
2088
2070
|
checkUrlParameter();
|
|
2089
|
-
void openRecordFromUrl();
|
|
2090
2071
|
void loadRelationQuickFilters();
|
|
2091
2072
|
void refreshLinkTabCounts();
|
|
2092
2073
|
// The registry is client-only; once hydrated, degrade to the table display if
|
|
@@ -271,9 +271,11 @@ describe("action targets on a selection and on a listed row", () => {
|
|
|
271
271
|
it("steps a row's drawer through the rows shown and keeps it in the URL", async () => {
|
|
272
272
|
window.history.replaceState(null, "", "/runs");
|
|
273
273
|
const rows = [{ _id: "r1" }, { _id: "r2" }];
|
|
274
|
+
const onRecordShown = vi.fn();
|
|
274
275
|
const targets = mountTargets({
|
|
275
276
|
rowNavigation: { rows: () => rows, rowIdKey: "_id" },
|
|
276
277
|
recordScope: { tableId: "runs", isSoleTableView: true },
|
|
278
|
+
onRecordShown,
|
|
277
279
|
});
|
|
278
280
|
targets.handleCustomRowAction(
|
|
279
281
|
{
|
|
@@ -306,6 +308,7 @@ describe("action targets on a selection and on a listed row", () => {
|
|
|
306
308
|
await nextTick();
|
|
307
309
|
await Promise.resolve();
|
|
308
310
|
expect(window.location.search).toBe("");
|
|
311
|
+
expect(onRecordShown.mock.calls).toEqual([["r1"], ["r2"], [undefined]]);
|
|
309
312
|
window.dispatchEvent(new KeyboardEvent("keydown", { key: "k" }));
|
|
310
313
|
expect(drawer!.patches).toHaveLength(1);
|
|
311
314
|
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* A table with a `deepLink` action opens the row `?record=` names on arrival
|
|
4
|
+
* and on each visit that keeps the page mounted, including a link to a row
|
|
5
|
+
* closed meanwhile: the table removes the key in place, so the route still
|
|
6
|
+
* holds the same value and only the visit tells the link was followed again.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { createApp, defineComponent, h, nextTick, reactive } from "vue";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { CustomRowAction } from "../layers/dms-ui/app/types/row-action";
|
|
13
|
+
import type { DeepLinkedRow } from "../layers/dms-ui/app/build/composables/table-view/utils/recordLink";
|
|
14
|
+
import {
|
|
15
|
+
clearWrittenRecord,
|
|
16
|
+
writeRecordId,
|
|
17
|
+
} from "../layers/dms-ui/app/build/composables/table-view/utils/recordLink";
|
|
18
|
+
import {
|
|
19
|
+
currentQuery,
|
|
20
|
+
withoutQueryParam,
|
|
21
|
+
} from "../layers/dms-ui/app/build/composables/table-view/utils/urlFilters";
|
|
22
|
+
|
|
23
|
+
type NavigateListener = (event: { detail: { page: { url: string } } }) => void;
|
|
24
|
+
const navigateListeners = new Set<NavigateListener>();
|
|
25
|
+
|
|
26
|
+
vi.mock("@inertiajs/vue3", () => ({
|
|
27
|
+
router: {
|
|
28
|
+
on: (type: string, listener: NavigateListener) => {
|
|
29
|
+
if (type !== "navigate") return () => {};
|
|
30
|
+
navigateListeners.add(listener);
|
|
31
|
+
return () => navigateListeners.delete(listener);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const { useTableRecordLink } = await import(
|
|
37
|
+
"../layers/dms-ui/app/build/composables/table-view/useTableRecordLink"
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
interface Row {
|
|
41
|
+
id: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ROWS: Row[] = [{ id: "r1" }, { id: "r2" }];
|
|
45
|
+
const SCOPE = { tableId: "sends", isSoleTableView: true };
|
|
46
|
+
const DRAWER = {
|
|
47
|
+
id: "details",
|
|
48
|
+
deepLink: true,
|
|
49
|
+
target: { type: "drawer" },
|
|
50
|
+
} as unknown as CustomRowAction;
|
|
51
|
+
|
|
52
|
+
const mounted: Array<{ unmount: () => void }> = [];
|
|
53
|
+
|
|
54
|
+
const flush = () => new Promise<void>((done) => setTimeout(done));
|
|
55
|
+
|
|
56
|
+
/** A visit of the page: Inertia pushes its URL, then announces it. */
|
|
57
|
+
async function visit(route: { query: Record<string, unknown> }, url: string) {
|
|
58
|
+
window.history.pushState({}, "", url);
|
|
59
|
+
route.query = Object.fromEntries(new URL(url, "http://x").searchParams);
|
|
60
|
+
await nextTick();
|
|
61
|
+
navigateListeners.forEach((listener) =>
|
|
62
|
+
listener({ detail: { page: { url } } }),
|
|
63
|
+
);
|
|
64
|
+
await flush();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mountTable(url: string) {
|
|
68
|
+
window.history.replaceState({}, "", url);
|
|
69
|
+
const route = reactive({
|
|
70
|
+
query: Object.fromEntries(new URL(url, "http://x").searchParams) as Record<
|
|
71
|
+
string,
|
|
72
|
+
unknown
|
|
73
|
+
>,
|
|
74
|
+
});
|
|
75
|
+
const opened: string[] = [];
|
|
76
|
+
const notify = vi.fn();
|
|
77
|
+
let link: { shown: (id?: string) => void } | undefined;
|
|
78
|
+
const app = createApp(
|
|
79
|
+
defineComponent({
|
|
80
|
+
setup() {
|
|
81
|
+
link = useTableRecordLink<Row>({
|
|
82
|
+
route,
|
|
83
|
+
urlScope: SCOPE,
|
|
84
|
+
action: DRAWER,
|
|
85
|
+
resolve: async (id): Promise<DeepLinkedRow<Row>> => {
|
|
86
|
+
const row = ROWS.find((candidate) => candidate.id === id);
|
|
87
|
+
return row ? { row, isMissing: false } : { isMissing: true };
|
|
88
|
+
},
|
|
89
|
+
// The drawer writes the row it opens, as the action's targets do.
|
|
90
|
+
open: (row) => {
|
|
91
|
+
opened.push(row.id);
|
|
92
|
+
writeRecordId(SCOPE, row.id);
|
|
93
|
+
link?.shown(row.id);
|
|
94
|
+
},
|
|
95
|
+
showMissing: () => {},
|
|
96
|
+
notify,
|
|
97
|
+
});
|
|
98
|
+
return () => h("div");
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
app.mount(document.createElement("div"));
|
|
103
|
+
mounted.push(app);
|
|
104
|
+
// Closing the drawer removes the row from the URL in place.
|
|
105
|
+
const close = () => {
|
|
106
|
+
writeRecordId(SCOPE);
|
|
107
|
+
link?.shown(undefined);
|
|
108
|
+
};
|
|
109
|
+
return { route, opened, notify, close };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
beforeEach(() => navigateListeners.clear());
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
mounted.splice(0).forEach((app) => app.unmount());
|
|
116
|
+
window.history.replaceState({}, "", "/");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("a record link on a mounted table", () => {
|
|
120
|
+
it("opens the row a link names on arrival, once", async () => {
|
|
121
|
+
const { route, opened } = mountTable("/sends?record=r1");
|
|
122
|
+
await flush();
|
|
123
|
+
// The visit that mounted the table is announced after it mounted.
|
|
124
|
+
navigateListeners.forEach((listener) =>
|
|
125
|
+
listener({ detail: { page: { url: "/sends?record=r1" } } }),
|
|
126
|
+
);
|
|
127
|
+
await flush();
|
|
128
|
+
expect(opened).toEqual(["r1"]);
|
|
129
|
+
expect(route.query.record).toBe("r1");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("opens the row a visit to the same page names", async () => {
|
|
133
|
+
const { route, opened } = mountTable("/sends?record=r1");
|
|
134
|
+
await flush();
|
|
135
|
+
await visit(route, "/sends?record=r2");
|
|
136
|
+
expect(opened).toEqual(["r1", "r2"]);
|
|
137
|
+
expect(window.location.search).toBe("?record=r2");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("opens the same row again after its drawer closed", async () => {
|
|
141
|
+
const { route, opened, close } = mountTable("/sends?record=r1");
|
|
142
|
+
await flush();
|
|
143
|
+
// A link to the row shown opens nothing more.
|
|
144
|
+
await visit(route, "/sends?record=r1");
|
|
145
|
+
expect(opened).toEqual(["r1"]);
|
|
146
|
+
|
|
147
|
+
close();
|
|
148
|
+
expect(window.location.search).toBe("");
|
|
149
|
+
await visit(route, "/sends?record=r1");
|
|
150
|
+
expect(opened).toEqual(["r1", "r1"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("tells the user a link names no row and removes the key", async () => {
|
|
154
|
+
const { route, opened, notify } = mountTable("/sends?tab=all&record=gone");
|
|
155
|
+
await flush();
|
|
156
|
+
expect(opened).toEqual([]);
|
|
157
|
+
expect(notify).toHaveBeenCalledWith("notify");
|
|
158
|
+
expect(window.location.search).toBe("?tab=all");
|
|
159
|
+
|
|
160
|
+
// Following the same link again tells it again.
|
|
161
|
+
await visit(route, "/sends?tab=all&record=gone");
|
|
162
|
+
expect(notify).toHaveBeenCalledTimes(2);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("stops following visits once unmounted", async () => {
|
|
166
|
+
const { route, opened } = mountTable("/sends");
|
|
167
|
+
mounted.splice(0).forEach((app) => app.unmount());
|
|
168
|
+
await visit(route, "/sends?record=r2");
|
|
169
|
+
expect(opened).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("the URL a closed drawer leaves", () => {
|
|
174
|
+
it("removes only the row this drawer wrote, and says so", () => {
|
|
175
|
+
window.history.replaceState({}, "", "/sends");
|
|
176
|
+
const written = writeRecordId(SCOPE, "r1");
|
|
177
|
+
writeRecordId(SCOPE, "r2");
|
|
178
|
+
expect(clearWrittenRecord(written)).toBe(false);
|
|
179
|
+
expect(window.location.search).toBe("?record=r2");
|
|
180
|
+
expect(clearWrittenRecord(writeRecordId(SCOPE, "r2"))).toBe(true);
|
|
181
|
+
expect(window.location.search).toBe("");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("rebuilds a removed filter's visit from the URL the browser shows", () => {
|
|
185
|
+
window.history.replaceState({}, "", "/sends?email=a%40b.test&tab=all");
|
|
186
|
+
// The route still holds the row of the drawer closed in place.
|
|
187
|
+
const routeQuery = { email: "a@b.test", tab: "all", record: "r1" };
|
|
188
|
+
expect(withoutQueryParam(currentQuery(routeQuery), "email")).toEqual({
|
|
189
|
+
tab: "all",
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("wires the record link and the filter chips into the table view", () => {
|
|
195
|
+
const source = readFileSync(
|
|
196
|
+
resolve(
|
|
197
|
+
__dirname,
|
|
198
|
+
"../layers/dms-ui/app/components/table-view/TableView.vue",
|
|
199
|
+
),
|
|
200
|
+
"utf8",
|
|
201
|
+
);
|
|
202
|
+
expect(source).toContain("useTableRecordLink<T>({");
|
|
203
|
+
expect(source).toContain("onRecordShown: (id) => recordLink?.shown(id),");
|
|
204
|
+
expect(source).toContain(
|
|
205
|
+
"withoutQueryParam(currentQuery(route.query), param)",
|
|
206
|
+
);
|
|
207
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.20",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/AntelopeJS/dms.git"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
|
|
38
38
|
"@antelopejs/interface-database": ">=0.1.8 <1.0.0",
|
|
39
39
|
"@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
|
|
40
|
-
"@antelopejs/interface-dms": ">=0.5.
|
|
40
|
+
"@antelopejs/interface-dms": ">=0.5.12 <0.6.0",
|
|
41
41
|
"@antelopejs/interface-email": ">=0.0.5 <1.0.0",
|
|
42
42
|
"@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
|
|
43
43
|
"archiver": "^7.0.1",
|