@antelopejs/dms 0.3.7 → 0.3.8
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 +31 -0
- package/dist/implementations/dms/page.js +2 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/test/antelope.test.js +16 -5
- package/dist/test/antelope.test.js.map +1 -1
- package/dist/test/api-endpoint/index.d.ts +5 -0
- package/dist/test/api-endpoint/index.js +8 -0
- package/dist/test/api-endpoint/index.js.map +1 -0
- package/dist/test/helpers/http.d.ts +1 -0
- package/dist/test/helpers/http.js +16 -4
- package/dist/test/helpers/http.js.map +1 -1
- package/dist/test/unit/implementations/dms/notify-menu-changed.test.js +21 -0
- package/dist/test/unit/implementations/dms/notify-menu-changed.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-form-page-urls.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-page-urls.test.js +308 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-page-urls.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +241 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-route-param-filter-defaults.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-route-param-filter-defaults.test.js +254 -0
- package/dist/test/unit/interfaces/dms-base/table-view-route-param-filter-defaults.test.js.map +1 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +49 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +13 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +49 -20
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +43 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +23 -29
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +3 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +35 -25
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -1
- package/frontend-vue/pnpm-lock.yaml +4 -4
- package/frontend-vue/tests/form-page-url-fill.test.ts +63 -0
- package/frontend-vue/tests/form-redirect-placeholders.test.ts +69 -0
- package/frontend-vue/tests/route-param-filter-defaults.test.ts +59 -0
- package/frontend-vue/tests/route-param-occurrences.test.ts +109 -0
- package/package.json +2 -2
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
fillFormPageUrl,
|
|
3
|
+
type FormContainer,
|
|
4
|
+
type FormPageUrls,
|
|
5
|
+
} from "./useTableViewConfig";
|
|
2
6
|
import type { FormProps } from "../../../composables/form/types";
|
|
3
7
|
import type { QueryParamFilters } from "../../../composables/table-view/types";
|
|
4
8
|
import type {
|
|
@@ -38,6 +42,8 @@ interface TableRowActionsConfig {
|
|
|
38
42
|
view?: ComponentInfo<FormProps>;
|
|
39
43
|
};
|
|
40
44
|
formContainer?: FormContainer;
|
|
45
|
+
formPages?: FormPageUrls;
|
|
46
|
+
routeParams?: Record<string, string>;
|
|
41
47
|
componentId: string;
|
|
42
48
|
pageId: string;
|
|
43
49
|
queryParamFilters?: QueryParamFilters;
|
|
@@ -83,20 +89,6 @@ export const useTableRowActions = <T extends Data>(
|
|
|
83
89
|
return Object.keys(forwarded).length > 0 ? forwarded : undefined;
|
|
84
90
|
};
|
|
85
91
|
|
|
86
|
-
const resolveFormPageUrl = (urlSlug: string): string => {
|
|
87
|
-
if (urlSlug.startsWith("/")) return urlSlug;
|
|
88
|
-
const baseSegments = route.path.replace(/\/$/, "").split("/");
|
|
89
|
-
const parts = urlSlug.split("/");
|
|
90
|
-
let popCount = 0;
|
|
91
|
-
let partStart = 0;
|
|
92
|
-
while (partStart < parts.length && parts[partStart] === "..") {
|
|
93
|
-
popCount++;
|
|
94
|
-
partStart++;
|
|
95
|
-
}
|
|
96
|
-
const base = baseSegments.slice(0, baseSegments.length - popCount);
|
|
97
|
-
return [...base, ...parts.slice(partStart)].join("/");
|
|
98
|
-
};
|
|
99
|
-
|
|
100
92
|
const handleApiError = (error: unknown, defaultMessage: string) => {
|
|
101
93
|
const err = error as Record<string, unknown>;
|
|
102
94
|
const errData = err?.data as Record<string, unknown> | string | undefined;
|
|
@@ -335,10 +327,9 @@ export const useTableRowActions = <T extends Data>(
|
|
|
335
327
|
};
|
|
336
328
|
|
|
337
329
|
const openRowByIdImpl = (itemId: string, item?: T) => {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
navigateDms(viewUrl);
|
|
330
|
+
const viewPageUrl = config.formPages?.view;
|
|
331
|
+
if (getContainerType() === FormContainerType.page && viewPageUrl) {
|
|
332
|
+
navigateDms(fillFormPageUrl(viewPageUrl, config.routeParams, itemId));
|
|
342
333
|
return;
|
|
343
334
|
}
|
|
344
335
|
|
|
@@ -355,12 +346,15 @@ export const useTableRowActions = <T extends Data>(
|
|
|
355
346
|
const editRowImpl = (item: T, submitDefaults?: Record<string, unknown>) => {
|
|
356
347
|
const itemId = getItemId(item, config.rowIdKey ?? DEFAULT_ROW_ID_KEY);
|
|
357
348
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
349
|
+
const editPageUrl = config.formPages?.edit;
|
|
350
|
+
if (
|
|
351
|
+
getContainerType() === FormContainerType.page &&
|
|
352
|
+
itemId &&
|
|
353
|
+
editPageUrl
|
|
354
|
+
) {
|
|
361
355
|
const forwardedQuery = buildForwardedQuery();
|
|
362
356
|
navigateDms({
|
|
363
|
-
path:
|
|
357
|
+
path: fillFormPageUrl(editPageUrl, config.routeParams, itemId),
|
|
364
358
|
...(forwardedQuery ? { query: forwardedQuery } : {}),
|
|
365
359
|
});
|
|
366
360
|
return;
|
|
@@ -377,11 +371,11 @@ export const useTableRowActions = <T extends Data>(
|
|
|
377
371
|
};
|
|
378
372
|
|
|
379
373
|
const newRowImpl = (submitDefaults?: Record<string, unknown>) => {
|
|
380
|
-
|
|
381
|
-
|
|
374
|
+
const newPageUrl = config.formPages?.new;
|
|
375
|
+
if (getContainerType() === FormContainerType.page && newPageUrl) {
|
|
382
376
|
const forwardedQuery = buildForwardedQuery();
|
|
383
377
|
navigateDms({
|
|
384
|
-
path:
|
|
378
|
+
path: fillFormPageUrl(newPageUrl, config.routeParams),
|
|
385
379
|
...(forwardedQuery ? { query: forwardedQuery } : {}),
|
|
386
380
|
});
|
|
387
381
|
return;
|
|
@@ -396,10 +390,10 @@ export const useTableRowActions = <T extends Data>(
|
|
|
396
390
|
};
|
|
397
391
|
|
|
398
392
|
const duplicateRowImpl = (itemId: string) => {
|
|
399
|
-
|
|
400
|
-
|
|
393
|
+
const newPageUrl = config.formPages?.new;
|
|
394
|
+
if (getContainerType() === FormContainerType.page && newPageUrl) {
|
|
401
395
|
navigateDms({
|
|
402
|
-
path:
|
|
396
|
+
path: fillFormPageUrl(newPageUrl, config.routeParams),
|
|
403
397
|
query: { duplicate: itemId },
|
|
404
398
|
});
|
|
405
399
|
return;
|
|
@@ -88,6 +88,7 @@ const {
|
|
|
88
88
|
customButtons,
|
|
89
89
|
formComponents,
|
|
90
90
|
formContainer,
|
|
91
|
+
formPages,
|
|
91
92
|
componentId,
|
|
92
93
|
pageId,
|
|
93
94
|
defaultSort,
|
|
@@ -599,6 +600,8 @@ const {
|
|
|
599
600
|
refreshCallback: refreshAll,
|
|
600
601
|
formComponents,
|
|
601
602
|
formContainer,
|
|
603
|
+
formPages,
|
|
604
|
+
routeParams,
|
|
602
605
|
componentId: componentId!,
|
|
603
606
|
pageId: pageId!,
|
|
604
607
|
queryParamFilters,
|
|
@@ -96,18 +96,24 @@ function processFieldI18n(
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
// `{{params.id}}` takes the bare name, which on a route repeating a placeholder
|
|
100
|
+
// is its last occurrence — the row id of a form page. The `:<n>` suffix reaches
|
|
101
|
+
// a specific occurrence, `{{params.id:1}}` being the id of the page carrying the
|
|
102
|
+
// table view (see extractRouteParams).
|
|
103
|
+
const PARAM_TOKEN = /\{\{params\.(\w+(?::\d+)?)\}\}/g;
|
|
104
|
+
|
|
105
|
+
export interface ReplaceUrlVariablesContext {
|
|
100
106
|
routeParams?: Record<string, string>;
|
|
101
107
|
routeQuery: Record<string, unknown>;
|
|
102
108
|
response?: Record<string, unknown>;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
function replaceUrlVariables(
|
|
111
|
+
export function replaceUrlVariables(
|
|
106
112
|
url: string,
|
|
107
113
|
context: ReplaceUrlVariablesContext,
|
|
108
114
|
): string {
|
|
109
115
|
let processedUrl = url.replace(
|
|
110
|
-
|
|
116
|
+
PARAM_TOKEN,
|
|
111
117
|
(match, key) => context.routeParams?.[key] || match,
|
|
112
118
|
);
|
|
113
119
|
|
|
@@ -177,6 +183,30 @@ function collectSubmitData(
|
|
|
177
183
|
};
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
// `submitDefaults` string values may contain `{{query.X}}` / `{{params.X}}`
|
|
187
|
+
// tokens (e.g. a "new" form generated from `queryParamFilters`). Resolve them
|
|
188
|
+
// against the current route at submit time; drop entries whose tokens cannot
|
|
189
|
+
// be resolved so an unfiltered form doesn't submit a literal `{{...}}`.
|
|
190
|
+
export function resolveSubmitDefaults(
|
|
191
|
+
submitDefaults: Record<string, unknown> | undefined,
|
|
192
|
+
context: ReplaceUrlVariablesContext,
|
|
193
|
+
): Record<string, unknown> | undefined {
|
|
194
|
+
if (!submitDefaults) return undefined;
|
|
195
|
+
|
|
196
|
+
const resolved: Record<string, unknown> = {};
|
|
197
|
+
for (const [key, value] of Object.entries(submitDefaults)) {
|
|
198
|
+
if (typeof value === "string" && value.includes("{{")) {
|
|
199
|
+
const replaced = replaceUrlVariables(value, context);
|
|
200
|
+
if (replaced.includes("{{")) continue;
|
|
201
|
+
resolved[key] = replaced;
|
|
202
|
+
} else {
|
|
203
|
+
resolved[key] = value;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
180
210
|
export function makeFieldSchemaRequired(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
181
211
|
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) {
|
|
182
212
|
return makeFieldSchemaRequired(schema.unwrap());
|
|
@@ -346,28 +376,8 @@ export const useForm = (props: FormProps) => {
|
|
|
346
376
|
response,
|
|
347
377
|
});
|
|
348
378
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// against the current route at submit time; drop entries whose tokens cannot
|
|
352
|
-
// be resolved so an unfiltered form doesn't submit a literal `{{...}}`.
|
|
353
|
-
const effectiveSubmitDefaults = computed<Record<string, unknown> | undefined>(
|
|
354
|
-
() => {
|
|
355
|
-
if (!props.submitDefaults) return undefined;
|
|
356
|
-
|
|
357
|
-
const context = buildUrlContext();
|
|
358
|
-
const resolved: Record<string, unknown> = {};
|
|
359
|
-
for (const [key, value] of Object.entries(props.submitDefaults)) {
|
|
360
|
-
if (typeof value === "string" && value.includes("{{")) {
|
|
361
|
-
const replaced = replaceUrlVariables(value, context);
|
|
362
|
-
if (replaced.includes("{{")) continue;
|
|
363
|
-
resolved[key] = replaced;
|
|
364
|
-
} else {
|
|
365
|
-
resolved[key] = value;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
370
|
-
},
|
|
379
|
+
const effectiveSubmitDefaults = computed(() =>
|
|
380
|
+
resolveSubmitDefaults(props.submitDefaults, buildUrlContext()),
|
|
371
381
|
);
|
|
372
382
|
|
|
373
383
|
const resolvedFetchUrl = computed(() => {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
FormContainer,
|
|
3
|
+
FormPageUrls,
|
|
4
|
+
} from "../../../build/composables/table-view/useTableViewConfig";
|
|
2
5
|
import type { TableViewColumn } from "./column";
|
|
3
6
|
import type { TableViewDisplayCapabilities } from "./display";
|
|
4
7
|
import type { CustomButton } from "./custom-button";
|
|
@@ -80,6 +83,7 @@ export interface TableViewConfig<T extends Data>
|
|
|
80
83
|
view?: ComponentInfo<FormProps>;
|
|
81
84
|
};
|
|
82
85
|
formContainer?: FormContainer;
|
|
86
|
+
formPages?: FormPageUrls;
|
|
83
87
|
componentId?: string;
|
|
84
88
|
pageId?: string;
|
|
85
89
|
defaultSort?: { field: string; desc?: boolean };
|
|
@@ -210,8 +210,8 @@ packages:
|
|
|
210
210
|
'@antelopejs/core':
|
|
211
211
|
optional: true
|
|
212
212
|
|
|
213
|
-
'@antelopejs/interface-core@0.0
|
|
214
|
-
resolution: {integrity: sha512-
|
|
213
|
+
'@antelopejs/interface-core@0.1.0':
|
|
214
|
+
resolution: {integrity: sha512-49mmTXREMNNEigWpmubwQD7fSrIIvmcTb9L8L79zQcDWTToud/GIap40vaKWmkbN1oGbjOB4ael/agqZ/ejYSg==}
|
|
215
215
|
|
|
216
216
|
'@antfu/install-pkg@1.1.0':
|
|
217
217
|
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
|
|
@@ -4327,7 +4327,7 @@ snapshots:
|
|
|
4327
4327
|
|
|
4328
4328
|
'@antelopejs/core@1.8.0(@types/node@24.8.1)':
|
|
4329
4329
|
dependencies:
|
|
4330
|
-
'@antelopejs/interface-core': 0.0
|
|
4330
|
+
'@antelopejs/interface-core': 0.1.0
|
|
4331
4331
|
'@types/proper-lockfile': 4.1.4
|
|
4332
4332
|
boxen: 8.0.1
|
|
4333
4333
|
chalk: 4.1.2
|
|
@@ -4362,7 +4362,7 @@ snapshots:
|
|
|
4362
4362
|
'@oxc-parser/binding-linux-x64-gnu': 0.95.0
|
|
4363
4363
|
'@oxc-parser/binding-win32-x64-msvc': 0.95.0
|
|
4364
4364
|
|
|
4365
|
-
'@antelopejs/interface-core@0.0
|
|
4365
|
+
'@antelopejs/interface-core@0.1.0':
|
|
4366
4366
|
dependencies:
|
|
4367
4367
|
reflect-metadata: 0.2.2
|
|
4368
4368
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { fillFormPageUrl } from "../layers/dms-ui/app/build/composables/table-view/useTableViewConfig";
|
|
3
|
+
|
|
4
|
+
const ROW = "row-1";
|
|
5
|
+
|
|
6
|
+
describe("filling a serialized form page URL", () => {
|
|
7
|
+
it("substitutes the row id of a default slug", () => {
|
|
8
|
+
expect(
|
|
9
|
+
fillFormPageUrl("/settings/users/roles/table/:id/edit", {}, ROW),
|
|
10
|
+
).toBe("/settings/users/roles/table/row-1/edit");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("leaves a URL without placeholder alone", () => {
|
|
14
|
+
expect(fillFormPageUrl("/settings/users/roles/table/new", {})).toBe(
|
|
15
|
+
"/settings/users/roles/table/new",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("substitutes the row id inside the query string of an absolute slug", () => {
|
|
20
|
+
expect(
|
|
21
|
+
fillFormPageUrl("/modules/automation/builder?selected=:id", {}, ROW),
|
|
22
|
+
).toBe("/modules/automation/builder?selected=row-1");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// The page slug contributes its own `:id`, and the form slug appends another:
|
|
26
|
+
// the row takes the last, the route params fill the first.
|
|
27
|
+
it("tells the row id apart from the id of the carrying page", () => {
|
|
28
|
+
expect(
|
|
29
|
+
fillFormPageUrl("/modules/workspaces/:id/:id/view", { id: "ws-1" }, ROW),
|
|
30
|
+
).toBe("/modules/workspaces/ws-1/row-1/view");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// What a table view nested under a `:id` detail page now resolves to: the
|
|
34
|
+
// key segment sits between the two placeholders and changes nothing.
|
|
35
|
+
it("tells the ids apart across the key segment of the table view", () => {
|
|
36
|
+
expect(
|
|
37
|
+
fillFormPageUrl(
|
|
38
|
+
"/modules/workspaces/:id/invoiceTable/:id/view",
|
|
39
|
+
{ id: "ws-1" },
|
|
40
|
+
ROW,
|
|
41
|
+
),
|
|
42
|
+
).toBe("/modules/workspaces/ws-1/invoiceTable/row-1/view");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("fills the params of the carrying page when the form takes no row id", () => {
|
|
46
|
+
expect(fillFormPageUrl("/modules/workspaces/:id/new", { id: "ws-1" })).toBe(
|
|
47
|
+
"/modules/workspaces/ws-1/new",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("matches a route param on its whole name", () => {
|
|
52
|
+
expect(
|
|
53
|
+
fillFormPageUrl(
|
|
54
|
+
"/projects/:project/:projectRole/:id/view",
|
|
55
|
+
{
|
|
56
|
+
project: "p-1",
|
|
57
|
+
projectRole: "owner",
|
|
58
|
+
},
|
|
59
|
+
ROW,
|
|
60
|
+
),
|
|
61
|
+
).toBe("/projects/p-1/owner/row-1/view");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractRouteParams } from "../layers/dms-layout/app/composables/page/useSiteLayout";
|
|
3
|
+
import { resolveFormSuccessTarget } from "../layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions";
|
|
4
|
+
|
|
5
|
+
// The redirect a page-mode form of a table view registered below
|
|
6
|
+
// `/workspaces/:id` sends on submit, as the TableView factory generates it for
|
|
7
|
+
// each form page route.
|
|
8
|
+
const EDIT_PATTERN = "/workspaces/:id/invoiceTable/:id/edit";
|
|
9
|
+
const NEW_PATTERN = "/workspaces/:id/invoiceTable/new";
|
|
10
|
+
const EDIT_REDIRECT = { url: "/workspaces/{{params.id:1}}" };
|
|
11
|
+
const NEW_REDIRECT = { url: "/workspaces/{{params.id}}" };
|
|
12
|
+
|
|
13
|
+
const routeOf = (
|
|
14
|
+
pattern: string,
|
|
15
|
+
path: string,
|
|
16
|
+
query: Record<string, string> = {},
|
|
17
|
+
) => ({ params: extractRouteParams(pattern, path)!, query });
|
|
18
|
+
|
|
19
|
+
describe("page-mode form redirect", () => {
|
|
20
|
+
it("sends the edit form back to the page, not to the row", () => {
|
|
21
|
+
expect(
|
|
22
|
+
resolveFormSuccessTarget(
|
|
23
|
+
EDIT_REDIRECT,
|
|
24
|
+
routeOf(EDIT_PATTERN, "/workspaces/ws-42/invoiceTable/inv-7/edit"),
|
|
25
|
+
),
|
|
26
|
+
).toEqual({ path: "/workspaces/ws-42" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("sends the new form back to the page", () => {
|
|
30
|
+
expect(
|
|
31
|
+
resolveFormSuccessTarget(
|
|
32
|
+
NEW_REDIRECT,
|
|
33
|
+
routeOf(NEW_PATTERN, "/workspaces/ws-42/invoiceTable/new"),
|
|
34
|
+
),
|
|
35
|
+
).toEqual({ path: "/workspaces/ws-42" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("carries back the declared query params only", () => {
|
|
39
|
+
expect(
|
|
40
|
+
resolveFormSuccessTarget(
|
|
41
|
+
{ ...EDIT_REDIRECT, preserveQuery: ["status", "missing"] },
|
|
42
|
+
routeOf(EDIT_PATTERN, "/workspaces/ws-42/invoiceTable/inv-7/edit", {
|
|
43
|
+
status: "paid",
|
|
44
|
+
other: "x",
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
).toEqual({ path: "/workspaces/ws-42", query: { status: "paid" } });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("leaves the slug of a page without route parameter as is", () => {
|
|
51
|
+
const route = routeOf(
|
|
52
|
+
"/invoices/invoiceTable/:id/edit",
|
|
53
|
+
"/invoices/invoiceTable/inv-7/edit",
|
|
54
|
+
{
|
|
55
|
+
status: "paid",
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(resolveFormSuccessTarget({ url: "/invoices" }, route)).toEqual({
|
|
60
|
+
path: "/invoices",
|
|
61
|
+
});
|
|
62
|
+
expect(
|
|
63
|
+
resolveFormSuccessTarget(
|
|
64
|
+
{ url: "/invoices", preserveQuery: ["status"] },
|
|
65
|
+
route,
|
|
66
|
+
),
|
|
67
|
+
).toEqual({ path: "/invoices", query: { status: "paid" } });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractRouteParams } from "../layers/dms-layout/app/composables/page/useSiteLayout";
|
|
3
|
+
import {
|
|
4
|
+
replaceUrlVariables,
|
|
5
|
+
resolveSubmitDefaults,
|
|
6
|
+
} from "../layers/dms-ui/app/composables/form/useForm";
|
|
7
|
+
|
|
8
|
+
// The forms of a table view filtered by `routeParamFilters: { id: { field:
|
|
9
|
+
// "_instance" } }`, registered below a page whose own slug is `:id`. The tokens
|
|
10
|
+
// are what the TableView factory generates for each form page route.
|
|
11
|
+
const EDIT_PATTERN = "/workspaces/:id/invoiceTable/:id/edit";
|
|
12
|
+
const NEW_PATTERN = "/workspaces/:id/invoiceTable/new";
|
|
13
|
+
const EDIT_DEFAULTS = { _instance: "{{params.id:1}}" };
|
|
14
|
+
const NEW_DEFAULTS = { _instance: "{{params.id}}" };
|
|
15
|
+
const EDIT_SUBMIT_URL = "/api/invoices/edit?id={{params.id}}";
|
|
16
|
+
|
|
17
|
+
const contextOf = (pattern: string, path: string) => ({
|
|
18
|
+
routeParams: extractRouteParams(pattern, path)!,
|
|
19
|
+
routeQuery: {},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("route param filter defaults on a nested form page", () => {
|
|
23
|
+
it("submits the page id in the filtered field, the row id in the URL", () => {
|
|
24
|
+
const context = contextOf(
|
|
25
|
+
EDIT_PATTERN,
|
|
26
|
+
"/workspaces/ws-42/invoiceTable/inv-7/edit",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(resolveSubmitDefaults(EDIT_DEFAULTS, context)).toEqual({
|
|
30
|
+
_instance: "ws-42",
|
|
31
|
+
});
|
|
32
|
+
expect(replaceUrlVariables(EDIT_SUBMIT_URL, context)).toBe(
|
|
33
|
+
"/api/invoices/edit?id=inv-7",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("submits the page id from the new form, whose route has one :id", () => {
|
|
38
|
+
expect(
|
|
39
|
+
resolveSubmitDefaults(
|
|
40
|
+
NEW_DEFAULTS,
|
|
41
|
+
contextOf(NEW_PATTERN, "/workspaces/ws-42/invoiceTable/new"),
|
|
42
|
+
),
|
|
43
|
+
).toEqual({ _instance: "ws-42" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// A form reached at an absolute URL does not carry the page's parameters:
|
|
47
|
+
// the default is dropped rather than submitted as a literal token.
|
|
48
|
+
it("drops a default no parameter of the route answers", () => {
|
|
49
|
+
const context = contextOf(
|
|
50
|
+
"/modules/automation/builder/:id",
|
|
51
|
+
"/modules/automation/builder/p-1",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(resolveSubmitDefaults(EDIT_DEFAULTS, context)).toBe(undefined);
|
|
55
|
+
expect(
|
|
56
|
+
resolveSubmitDefaults({ ...EDIT_DEFAULTS, status: "draft" }, context),
|
|
57
|
+
).toEqual({ status: "draft" });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractRouteParams } from "../layers/dms-layout/app/composables/page/useSiteLayout";
|
|
3
|
+
import { replaceUrlVariables } from "../layers/dms-ui/app/composables/form/useForm";
|
|
4
|
+
|
|
5
|
+
// A page-mode form sub-page under a page whose own slug is `:id`: the carrying
|
|
6
|
+
// page contributes one `:id` and the form slug appends another.
|
|
7
|
+
const NESTED_FORM_PATTERN = "/workspaces/:id/invoiceTable/:id/edit";
|
|
8
|
+
const NESTED_FORM_PATH = "/workspaces/ws-1/invoiceTable/row-7/edit";
|
|
9
|
+
|
|
10
|
+
describe("extractRouteParams", () => {
|
|
11
|
+
it("keeps a single placeholder under its bare name and nothing else", () => {
|
|
12
|
+
expect(
|
|
13
|
+
extractRouteParams("/customers/:id/edit", "/customers/c-1/edit"),
|
|
14
|
+
).toEqual({ id: "c-1" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("keeps distinct placeholders under their own names and nothing else", () => {
|
|
18
|
+
expect(
|
|
19
|
+
extractRouteParams(
|
|
20
|
+
"/projects/:project/members/:id/view",
|
|
21
|
+
"/projects/p-1/members/m-2/view",
|
|
22
|
+
),
|
|
23
|
+
).toEqual({ project: "p-1", id: "m-2" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("reaches both values when one name occurs twice", () => {
|
|
27
|
+
expect(extractRouteParams(NESTED_FORM_PATTERN, NESTED_FORM_PATH)).toEqual({
|
|
28
|
+
id: "row-7",
|
|
29
|
+
"id:1": "ws-1",
|
|
30
|
+
"id:2": "row-7",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("numbers every occurrence of a name repeated more than twice", () => {
|
|
35
|
+
expect(
|
|
36
|
+
extractRouteParams(
|
|
37
|
+
"/a/:id/b/:id/c/:id/view",
|
|
38
|
+
"/a/one/b/two/c/three/view",
|
|
39
|
+
),
|
|
40
|
+
).toEqual({
|
|
41
|
+
id: "three",
|
|
42
|
+
"id:1": "one",
|
|
43
|
+
"id:2": "two",
|
|
44
|
+
"id:3": "three",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("indexes only the repeated name, leaving its neighbours untouched", () => {
|
|
49
|
+
expect(
|
|
50
|
+
extractRouteParams(
|
|
51
|
+
"/workspaces/:id/:tenant/invoiceTable/:id/edit",
|
|
52
|
+
"/workspaces/ws-1/t-9/invoiceTable/row-7/edit",
|
|
53
|
+
),
|
|
54
|
+
).toEqual({
|
|
55
|
+
id: "row-7",
|
|
56
|
+
"id:1": "ws-1",
|
|
57
|
+
"id:2": "row-7",
|
|
58
|
+
tenant: "t-9",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("still refuses a path the pattern does not match", () => {
|
|
63
|
+
expect(extractRouteParams(NESTED_FORM_PATTERN, "/workspaces/ws-1")).toBe(
|
|
64
|
+
null,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("form URL variables on a repeated placeholder", () => {
|
|
70
|
+
const context = {
|
|
71
|
+
routeParams: extractRouteParams(NESTED_FORM_PATTERN, NESTED_FORM_PATH)!,
|
|
72
|
+
routeQuery: {},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// What the TableView factory generates for the edit form: the bare name has
|
|
76
|
+
// to stay the row id, on a nested route as on a flat one.
|
|
77
|
+
it("resolves the bare name to the row id", () => {
|
|
78
|
+
expect(
|
|
79
|
+
replaceUrlVariables("/invoices/edit?id={{params.id}}", context),
|
|
80
|
+
).toBe("/invoices/edit?id=row-7");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("resolves an indexed name to the id of the carrying page", () => {
|
|
84
|
+
expect(
|
|
85
|
+
replaceUrlVariables(
|
|
86
|
+
"/workspaces/{{params.id:1}}/invoices/{{params.id}}",
|
|
87
|
+
context,
|
|
88
|
+
),
|
|
89
|
+
).toBe("/workspaces/ws-1/invoices/row-7");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("leaves an indexed token that no occurrence answers alone", () => {
|
|
93
|
+
expect(replaceUrlVariables("/invoices/{{params.id:3}}", context)).toBe(
|
|
94
|
+
"/invoices/{{params.id:3}}",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("resolves the bare name on a single-placeholder route as before", () => {
|
|
99
|
+
expect(
|
|
100
|
+
replaceUrlVariables("/customers/edit?id={{params.id}}", {
|
|
101
|
+
routeParams: extractRouteParams(
|
|
102
|
+
"/customers/:id/edit",
|
|
103
|
+
"/customers/c-1/edit",
|
|
104
|
+
)!,
|
|
105
|
+
routeQuery: {},
|
|
106
|
+
}),
|
|
107
|
+
).toBe("/customers/edit?id=c-1");
|
|
108
|
+
});
|
|
109
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/AntelopeJS/dms.git"
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"zod": "~3.24.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@antelopejs/core": ">=1.
|
|
54
|
+
"@antelopejs/core": ">=1.9.0 <2",
|
|
55
55
|
"@antelopejs/tooling-configs": ">=0.0.6 <1.0.0",
|
|
56
56
|
"@types/archiver": "^6.0.4",
|
|
57
57
|
"@types/chai": "^4.3.20",
|