@antelopejs/dms 0.3.6 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/implementations/dms/page.js +2 -2
  3. package/dist/implementations/dms/page.js.map +1 -1
  4. package/dist/test/antelope.test.js +16 -5
  5. package/dist/test/antelope.test.js.map +1 -1
  6. package/dist/test/api-endpoint/index.d.ts +5 -0
  7. package/dist/test/api-endpoint/index.js +8 -0
  8. package/dist/test/api-endpoint/index.js.map +1 -0
  9. package/dist/test/helpers/http.d.ts +1 -0
  10. package/dist/test/helpers/http.js +16 -4
  11. package/dist/test/helpers/http.js.map +1 -1
  12. package/dist/test/unit/implementations/dms/notify-menu-changed.test.js +21 -0
  13. package/dist/test/unit/implementations/dms/notify-menu-changed.test.js.map +1 -1
  14. package/dist/test/unit/interfaces/dms-base/table-view-form-page-urls.test.d.ts +1 -0
  15. package/dist/test/unit/interfaces/dms-base/table-view-form-page-urls.test.js +308 -0
  16. package/dist/test/unit/interfaces/dms-base/table-view-form-page-urls.test.js.map +1 -0
  17. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.d.ts +1 -0
  18. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +241 -0
  19. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -0
  20. package/dist/test/unit/interfaces/dms-base/table-view-route-param-filter-defaults.test.d.ts +1 -0
  21. package/dist/test/unit/interfaces/dms-base/table-view-route-param-filter-defaults.test.js +254 -0
  22. package/dist/test/unit/interfaces/dms-base/table-view-route-param-filter-defaults.test.js.map +1 -0
  23. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +49 -20
  24. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +6 -1
  25. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +2 -0
  26. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +13 -7
  27. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +49 -20
  28. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +43 -5
  29. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +23 -29
  30. package/frontend-vue/layers/dms-ui/app/components/grid/Grid.vue +10 -1
  31. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +16 -6
  32. package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +29 -0
  33. package/frontend-vue/layers/dms-ui/app/components/grid/constants.ts +1 -0
  34. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +3 -0
  35. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +35 -25
  36. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -1
  37. package/frontend-vue/pnpm-lock.yaml +4 -4
  38. package/frontend-vue/tests/form-page-url-fill.test.ts +63 -0
  39. package/frontend-vue/tests/form-redirect-placeholders.test.ts +69 -0
  40. package/frontend-vue/tests/grid-responsive-columns.test.ts +105 -0
  41. package/frontend-vue/tests/route-param-filter-defaults.test.ts +59 -0
  42. package/frontend-vue/tests/route-param-occurrences.test.ts +109 -0
  43. package/package.json +2 -2
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ const interface_api_1 = require("@antelopejs/interface-api");
46
+ const interface_core_1 = require("@antelopejs/interface-core");
47
+ const interface_data_api_1 = require("@antelopejs/interface-data-api");
48
+ const metadata_1 = require("@antelopejs/interface-data-api/metadata");
49
+ const interface_database_decorators_1 = require("@antelopejs/interface-database-decorators");
50
+ const chai_1 = require("chai");
51
+ const pageImpl = __importStar(require("../../../../implementations/dms/page"));
52
+ const permissionsImpl = __importStar(require("../../../../implementations/dms/permissions"));
53
+ const realtimeImpl = __importStar(require("../../../../implementations/dms/realtime"));
54
+ const constants_1 = require("@antelopejs/interface-dms/constants");
55
+ const pageInterface = __importStar(require("@antelopejs/interface-dms/page"));
56
+ const page_1 = require("@antelopejs/interface-dms/page");
57
+ const registry_1 = require("@antelopejs/interface-dms/page/registry");
58
+ const permissionsInterface = __importStar(require("@antelopejs/interface-dms/permissions"));
59
+ const realtimeInterface = __importStar(require("@antelopejs/interface-dms/realtime"));
60
+ const default_types_1 = require("@antelopejs/interface-dms/base/data-types/default-types");
61
+ const table_view_1 = require("@antelopejs/interface-dms/base/table-view");
62
+ const TABLE = "route-param-filter-documents";
63
+ const LOCATION = "/api/route-param-filter-defaults";
64
+ function settle() {
65
+ return new Promise((resolve) => setImmediate(resolve));
66
+ }
67
+ let Document = class Document extends interface_database_decorators_1.Table {
68
+ };
69
+ __decorate([
70
+ (0, interface_database_decorators_1.Field)("string"),
71
+ __metadata("design:type", String)
72
+ ], Document.prototype, "name", void 0);
73
+ __decorate([
74
+ (0, interface_database_decorators_1.Field)("string"),
75
+ __metadata("design:type", String)
76
+ ], Document.prototype, "_instance", void 0);
77
+ Document = __decorate([
78
+ (0, interface_database_decorators_1.RegisterTable)(TABLE, constants_1.CORE_SCHEMA_NAME)
79
+ ], Document);
80
+ class DocumentModel extends (0, interface_database_decorators_1.BasicDataModel)(Document, TABLE) {
81
+ }
82
+ let DocumentAPI = class DocumentAPI extends (0, interface_data_api_1.DataController)(Document, { new: table_view_1.TableViewRoutes.New, edit: table_view_1.TableViewRoutes.Edit }, (0, interface_api_1.Controller)(LOCATION)) {
83
+ };
84
+ __decorate([
85
+ (0, metadata_1.ModelReference)(),
86
+ (0, interface_database_decorators_1.Model)(DocumentModel),
87
+ __metadata("design:type", DocumentModel)
88
+ ], DocumentAPI.prototype, "model", void 0);
89
+ __decorate([
90
+ (0, table_view_1.Column)({ name: "Name", type: new default_types_1.DefaultDataTypes.StringType() }),
91
+ (0, metadata_1.Access)(metadata_1.AccessMode.ReadWrite),
92
+ __metadata("design:type", String)
93
+ ], DocumentAPI.prototype, "name", void 0);
94
+ DocumentAPI = __decorate([
95
+ (0, interface_data_api_1.RegisterDataController)()
96
+ ], DocumentAPI);
97
+ const ROUTE_FILTER = { id: { field: "_instance" } };
98
+ const QUERY_FILTER = { status: { field: "status" } };
99
+ const detailTable = (0, table_view_1.TableView)(DocumentAPI, {
100
+ formContainer: { type: "page" },
101
+ routeParamFilters: ROUTE_FILTER,
102
+ queryParamFilters: QUERY_FILTER,
103
+ realtime: false,
104
+ });
105
+ const namedParamTable = (0, table_view_1.TableView)(DocumentAPI, {
106
+ formContainer: { type: "page" },
107
+ routeParamFilters: { workspace: { field: "_instance" } },
108
+ realtime: false,
109
+ });
110
+ const deepTable = (0, table_view_1.TableView)(DocumentAPI, {
111
+ formContainer: { type: "page" },
112
+ routeParamFilters: ROUTE_FILTER,
113
+ realtime: false,
114
+ });
115
+ const topLevelTable = (0, table_view_1.TableView)(DocumentAPI, {
116
+ formContainer: { type: "page" },
117
+ routeParamFilters: ROUTE_FILTER,
118
+ realtime: false,
119
+ });
120
+ const unfilteredTable = (0, table_view_1.TableView)(DocumentAPI, {
121
+ formContainer: { type: "page" },
122
+ realtime: false,
123
+ });
124
+ const drawerTable = (0, table_view_1.TableView)(DocumentAPI, {
125
+ formContainer: { type: "drawer" },
126
+ routeParamFilters: ROUTE_FILTER,
127
+ realtime: false,
128
+ });
129
+ let DetailPage = class DetailPage extends (0, page_1.RootPageController)("rpf-detail", {
130
+ displayName: "Detail of a workspace",
131
+ urlSlug: "rpf-detail/:id",
132
+ }) {
133
+ static rows = detailTable;
134
+ };
135
+ DetailPage = __decorate([
136
+ (0, page_1.RegisterPage)()
137
+ ], DetailPage);
138
+ let NamedParamPage = class NamedParamPage extends (0, page_1.RootPageController)("rpf-named", {
139
+ displayName: "Detail with a placeholder of its own name",
140
+ urlSlug: "rpf-named/:workspace",
141
+ }) {
142
+ static rows = namedParamTable;
143
+ };
144
+ NamedParamPage = __decorate([
145
+ (0, page_1.RegisterPage)()
146
+ ], NamedParamPage);
147
+ let DeepPage = class DeepPage extends (0, page_1.RootPageController)("rpf-deep", {
148
+ displayName: "Page repeating its own placeholder",
149
+ urlSlug: "rpf-deep/:id/sub/:id",
150
+ }) {
151
+ static rows = deepTable;
152
+ };
153
+ DeepPage = __decorate([
154
+ (0, page_1.RegisterPage)()
155
+ ], DeepPage);
156
+ let TopLevelPage = class TopLevelPage extends (0, page_1.RootPageController)("rpf-top", {
157
+ displayName: "Page without a route parameter",
158
+ }) {
159
+ static rows = topLevelTable;
160
+ };
161
+ TopLevelPage = __decorate([
162
+ (0, page_1.RegisterPage)()
163
+ ], TopLevelPage);
164
+ let UnfilteredPage = class UnfilteredPage extends (0, page_1.RootPageController)("rpf-unfiltered", {
165
+ displayName: "Table view without URL filters",
166
+ }) {
167
+ static rows = unfilteredTable;
168
+ };
169
+ UnfilteredPage = __decorate([
170
+ (0, page_1.RegisterPage)()
171
+ ], UnfilteredPage);
172
+ let DrawerPage = class DrawerPage extends (0, page_1.RootPageController)("rpf-drawer", {
173
+ displayName: "Drawer form container",
174
+ urlSlug: "rpf-drawer/:id",
175
+ }) {
176
+ static rows = drawerTable;
177
+ };
178
+ DrawerPage = __decorate([
179
+ (0, page_1.RegisterPage)()
180
+ ], DrawerPage);
181
+ const pages = [
182
+ DetailPage,
183
+ NamedParamPage,
184
+ DeepPage,
185
+ TopLevelPage,
186
+ UnfilteredPage,
187
+ DrawerPage,
188
+ ];
189
+ async function formPageOptions(formPageFullId) {
190
+ const form = registry_1.pageMetadataByFullId.get(formPageFullId)?.components.form;
191
+ if (!form)
192
+ throw new Error(`No form page registered as ${formPageFullId}`);
193
+ return (await form.serialize()).options;
194
+ }
195
+ async function formComponents(table) {
196
+ return (await table.serialize()).options?.formComponents;
197
+ }
198
+ describe("[unit] interfaces/dms-base — route param filter form defaults", () => {
199
+ before(async () => {
200
+ (0, interface_core_1.ImplementInterface)(permissionsInterface, permissionsImpl);
201
+ (0, interface_core_1.ImplementInterface)(realtimeInterface, realtimeImpl);
202
+ (0, interface_core_1.ImplementInterface)(pageInterface, pageImpl);
203
+ await settle();
204
+ });
205
+ after(() => {
206
+ for (const page of pages) {
207
+ const { pageInfo } = (0, interface_core_1.GetMetadata)(page, page_1.PageMetadata);
208
+ if (pageInfo)
209
+ pageInterface.internal.RegisterPage.unregister(pageInfo);
210
+ }
211
+ });
212
+ it("numbers the page parameter on an edit route repeating its name", async () => {
213
+ const edit = await formPageOptions("rpf-detail.rows.edit");
214
+ (0, chai_1.expect)(edit?.submitDefaults).to.deep.equal({
215
+ status: "{{query.status}}",
216
+ _instance: "{{params.id:1}}",
217
+ });
218
+ (0, chai_1.expect)(edit?.submitUrl).to.equal(`${LOCATION}/edit?id={{params.id}}`);
219
+ });
220
+ it("keeps the bare name on a new route carrying the name once", async () => {
221
+ (0, chai_1.expect)((await formPageOptions("rpf-detail.rows.new"))?.submitDefaults).to.deep.equal({
222
+ status: "{{query.status}}",
223
+ _instance: "{{params.id}}",
224
+ });
225
+ });
226
+ it("gives the view form no defaults", async () => {
227
+ (0, chai_1.expect)((await formPageOptions("rpf-detail.rows.view"))?.submitDefaults).to.equal(undefined);
228
+ });
229
+ it("keeps the bare name when the page parameter is not named id", async () => {
230
+ (0, chai_1.expect)((await formPageOptions("rpf-named.rows.edit"))?.submitDefaults).to.deep.equal({ _instance: "{{params.workspace}}" });
231
+ });
232
+ it("numbers the last occurrence of a name the page slug repeats", async () => {
233
+ (0, chai_1.expect)((await formPageOptions("rpf-deep.rows.new"))?.submitDefaults).to.deep.equal({ _instance: "{{params.id:2}}" });
234
+ (0, chai_1.expect)((await formPageOptions("rpf-deep.rows.edit"))?.submitDefaults).to.deep.equal({ _instance: "{{params.id:2}}" });
235
+ });
236
+ it("leaves the forms of a page without route parameter as they were", async () => {
237
+ for (const kind of ["new", "edit"]) {
238
+ (0, chai_1.expect)((await formPageOptions(`rpf-top.rows.${kind}`))?.submitDefaults).to.deep.equal({ _instance: "{{params.id}}" });
239
+ }
240
+ const unfiltered = await formPageOptions("rpf-unfiltered.rows.edit");
241
+ (0, chai_1.expect)(unfiltered?.submitDefaults).to.equal(undefined);
242
+ });
243
+ it("keeps the bare name on the forms the table view embeds", async () => {
244
+ const embedded = await formComponents(drawerTable);
245
+ (0, chai_1.expect)(embedded?.edit?.options?.submitDefaults).to.deep.equal({
246
+ _instance: "{{params.id}}",
247
+ });
248
+ (0, chai_1.expect)((await formComponents(detailTable))?.edit?.options?.submitDefaults).to.deep.equal({
249
+ status: "{{query.status}}",
250
+ _instance: "{{params.id}}",
251
+ });
252
+ });
253
+ });
254
+ //# sourceMappingURL=table-view-route-param-filter-defaults.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-view-route-param-filter-defaults.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/table-view-route-param-filter-defaults.test.ts"],"names":[],"mappings":"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"}
@@ -37,36 +37,65 @@ interface MatchRouteResult {
37
37
  params: Record<string, string>;
38
38
  }
39
39
 
40
- function extractRouteParams(
40
+ // A route pattern can carry the same placeholder name twice: a page-mode form
41
+ // sub-page under a page whose own slug is `:id` resolves to
42
+ // `/workspaces/:id/invoiceTable/:id/edit`. The bare name holds the LAST
43
+ // occurrence, so `params.id` keeps meaning the row id the way it does on every
44
+ // single-placeholder form route, and each occurrence of a repeated name is
45
+ // exposed as well under `<name>:<n>`, numbered from 1 in pattern order, so the
46
+ // id of the carrying page stays reachable as `id:1`. A name that occurs only
47
+ // once gets no indexed key: those bags are exactly what they were.
48
+ const OCCURRENCE_KEY_SEPARATOR = ":";
49
+
50
+ const occurrenceKey = (name: string, occurrence: number): string =>
51
+ `${name}${OCCURRENCE_KEY_SEPARATOR}${occurrence}`;
52
+
53
+ function countByName(names: string[]): Record<string, number> {
54
+ const totals: Record<string, number> = {};
55
+ for (const name of names) {
56
+ totals[name] = (totals[name] ?? 0) + 1;
57
+ }
58
+ return totals;
59
+ }
60
+
61
+ function buildParamBag(
62
+ names: string[],
63
+ values: string[],
64
+ ): Record<string, string> {
65
+ const totals = countByName(names);
66
+ const seen: Record<string, number> = {};
67
+ const params: Record<string, string> = {};
68
+
69
+ names.forEach((name, index) => {
70
+ const value = values[index]!;
71
+ const occurrence = (seen[name] ?? 0) + 1;
72
+ seen[name] = occurrence;
73
+ params[name] = value;
74
+ if (totals[name]! > 1) {
75
+ params[occurrenceKey(name, occurrence)] = value;
76
+ }
77
+ });
78
+
79
+ return params;
80
+ }
81
+
82
+ export function extractRouteParams(
41
83
  pattern: string,
42
84
  path: string,
43
85
  ): Record<string, string> | null {
44
- const regexPattern = pattern
45
- .split("/")
46
- .map((segment) => {
47
- if (segment.startsWith(":")) {
48
- return "([^/]+)";
49
- }
50
- return segment;
51
- })
86
+ const segments = pattern.split("/");
87
+ const regexPattern = segments
88
+ .map((segment) => (segment.startsWith(":") ? "([^/]+)" : segment))
52
89
  .join("/");
53
90
 
54
- const regex = new RegExp(`^${regexPattern}$`);
55
- const match = path.match(regex);
56
-
91
+ const match = path.match(new RegExp(`^${regexPattern}$`));
57
92
  if (!match) return null;
58
93
 
59
- const paramNames = pattern
60
- .split("/")
94
+ const paramNames = segments
61
95
  .filter((segment) => segment.startsWith(":"))
62
96
  .map((segment) => segment.substring(1));
63
97
 
64
- const params: Record<string, string> = {};
65
- paramNames.forEach((name, index) => {
66
- params[name] = match[index + 1]!;
67
- });
68
-
69
- return params;
98
+ return buildParamBag(paramNames, match.slice(1));
70
99
  }
71
100
 
72
101
  export const useSiteLayout = () => {
@@ -26,7 +26,10 @@ import type {
26
26
  CustomButton,
27
27
  TableViewDisplayCapabilities,
28
28
  } from "../../../composables/table-view/types";
29
- import type { FormContainer } from "../../composables/table-view/useTableViewConfig";
29
+ import type {
30
+ FormContainer,
31
+ FormPageUrls,
32
+ } from "../../composables/table-view/useTableViewConfig";
30
33
  import type { CustomRowAction } from "../../../types/row-action";
31
34
  import type { RowActionConfig } from "#dms-core/app/types/row-action";
32
35
  import type { TableTabItem } from "./Tabs.vue";
@@ -97,6 +100,8 @@ export interface TableProps<T> {
97
100
  onCustomRowAction?: (action: CustomRowAction, rowData: T) => void;
98
101
  componentId?: string;
99
102
  formContainer?: FormContainer;
103
+ formPages?: FormPageUrls;
104
+ routeParams?: Record<string, string>;
100
105
  defaultSort?: { field: string; desc?: boolean };
101
106
  initialColumnVisibility?: VisibilityState;
102
107
  presenceByRow?: TableRowPresenceMap;
@@ -92,6 +92,8 @@ export const useTable = <T extends Data>(props: UseTableProps<T>) => {
92
92
  ui: props.ui,
93
93
  componentId: props.tableProps.componentId,
94
94
  formContainer: props.tableProps.formContainer,
95
+ formPages: props.tableProps.formPages,
96
+ routeParams: props.tableProps.routeParams,
95
97
  onCustomRowAction: props.tableProps.onCustomRowAction,
96
98
  });
97
99
 
@@ -28,7 +28,11 @@ import {
28
28
  FormContainerType,
29
29
  DEFAULT_FORM_CONTAINER_TYPE,
30
30
  } from "../table-view/types";
31
- import type { FormContainer } from "../table-view/useTableViewConfig";
31
+ import {
32
+ fillFormPageUrl,
33
+ type FormContainer,
34
+ type FormPageUrls,
35
+ } from "../table-view/useTableViewConfig";
32
36
  import type { DmsAppConfig } from "#dms-core/shared/types/app-config";
33
37
 
34
38
  const SORT_DIRECTION_ICON: Record<SortDirection, string> = {
@@ -84,6 +88,8 @@ interface ColumnConfig<T> {
84
88
  ui: ComputedRef<TableStyleSlots>;
85
89
  componentId?: string;
86
90
  formContainer?: FormContainer;
91
+ formPages?: FormPageUrls;
92
+ routeParams?: Record<string, string>;
87
93
  onCustomRowAction?: (action: CustomRowAction, rowData: T) => void;
88
94
  }
89
95
 
@@ -353,13 +359,13 @@ export const useTableColumns = <T extends Data>(config: ColumnConfig<T>) => {
353
359
 
354
360
  const containerType =
355
361
  config.formContainer?.type ?? DEFAULT_FORM_CONTAINER_TYPE;
356
- if (containerType === FormContainerType.page) {
362
+ const viewPageUrl =
363
+ containerType === FormContainerType.page
364
+ ? config.formPages?.view
365
+ : undefined;
366
+ if (viewPageUrl) {
357
367
  const itemId = get(rowData, config.rowIdKey);
358
- const viewSlug =
359
- config.formContainer?.pages?.view?.urlSlug ||
360
- `${currentRoute.path}/${itemId}/view`;
361
- const viewUrl = viewSlug.replace(":id", itemId);
362
- itemUrl = `${window.location.origin}${viewUrl}`;
368
+ itemUrl = `${window.location.origin}${fillFormPageUrl(viewPageUrl, config.routeParams, itemId)}`;
363
369
  } else {
364
370
  const updatedQuery = {
365
371
  ...currentRoute.query,
@@ -1,32 +1,61 @@
1
1
  import type { LocationQueryValue } from "#dms/frontend-module";
2
+ import { replaceUrlVariables } from "../../../composables/form/useForm";
3
+
4
+ type RouteQuery = Record<string, LocationQueryValue | LocationQueryValue[]>;
5
+
6
+ export interface FormSuccessParams {
7
+ url: string;
8
+ preserveQuery?: string[];
9
+ }
10
+
11
+ export interface FormSuccessRoute {
12
+ query: RouteQuery;
13
+ params?: Record<string, string>;
14
+ }
15
+
16
+ /**
17
+ * Where a page-mode form sends the user on submit: the page carrying the table
18
+ * view. Its URL holds a `{{params.X}}` token per placeholder of that page's
19
+ * slug, resolved against the parameters of the form route being left, the way
20
+ * `redirectOnSuccess` is.
21
+ */
22
+ export function resolveFormSuccessTarget(
23
+ { url, preserveQuery }: FormSuccessParams,
24
+ route: FormSuccessRoute,
25
+ ): { path: string; query?: RouteQuery } {
26
+ const path = replaceUrlVariables(url, {
27
+ routeParams: route.params,
28
+ routeQuery: route.query,
29
+ });
30
+
31
+ // Restore the filtered table view: carry back the query params the form
32
+ // was opened with (those declared in `queryParamFilters`).
33
+ const query: RouteQuery = {};
34
+ for (const key of preserveQuery ?? []) {
35
+ if (route.query[key] !== undefined) {
36
+ query[key] = route.query[key];
37
+ }
38
+ }
39
+
40
+ return { path, ...(Object.keys(query).length > 0 ? { query } : {}) };
41
+ }
2
42
 
3
43
  export function registerDefaultFunctions() {
4
44
  const { registerFunction } = useDefinedFunctions();
5
45
  const route = useDmsRoute();
46
+ const { findMatchingRoute } = useSiteLayout();
6
47
 
7
48
  registerFunction(
8
49
  TableViewFunctions.CUSTOM_PAGE_FORM_SUCCESS,
9
50
  (event: CustomEvent) => {
10
- const { url, preserveQuery } = (
11
- event as CustomEvent & {
12
- params: { url: string; preserveQuery?: string[] };
13
- }
14
- ).params;
15
-
16
- // Restore the filtered table view: carry back the query params the form
17
- // was opened with (those declared in `queryParamFilters`).
18
- const query: Record<string, LocationQueryValue | LocationQueryValue[]> =
19
- {};
20
- for (const key of preserveQuery ?? []) {
21
- if (route.query[key] !== undefined) {
22
- query[key] = route.query[key];
23
- }
24
- }
25
-
26
- navigateDms({
27
- path: url,
28
- ...(Object.keys(query).length > 0 ? { query } : {}),
29
- });
51
+ const { params } = event as CustomEvent & { params: FormSuccessParams };
52
+
53
+ navigateDms(
54
+ resolveFormSuccessTarget(params, {
55
+ query: route.query,
56
+ params: findMatchingRoute(route.path)?.params,
57
+ }),
58
+ );
30
59
  },
31
60
  );
32
61
  }
@@ -35,16 +35,51 @@ const withBuiltinDefaults = (
35
35
  };
36
36
  };
37
37
 
38
+ // The per-page slugs the table view declares are resolved server-side and
39
+ // arrive as `formPages`; the frontend never reads them.
38
40
  export interface FormContainer {
39
41
  type: FormContainerType;
40
42
  size?: ModalSize;
41
- pages?: {
42
- new?: { urlSlug?: string; displayName?: string; description?: string };
43
- edit?: { urlSlug?: string; displayName?: string; description?: string };
44
- view?: { urlSlug?: string; displayName?: string; description?: string };
45
- };
46
43
  }
47
44
 
45
+ /**
46
+ * Page-mode form URLs resolved by the server, placeholders included: the `:id`
47
+ * of the row, and those the slug of the carrying page contributes.
48
+ */
49
+ export interface FormPageUrls {
50
+ new: string;
51
+ edit: string;
52
+ view: string;
53
+ }
54
+
55
+ const ROW_ID_PLACEHOLDER = ":id";
56
+
57
+ /**
58
+ * Turn a serialized form page URL into the one to navigate to. The row id
59
+ * takes the last `:id` — the form slug contributes it after the slug of the
60
+ * carrying page, so an earlier one belongs to that page — and the params of
61
+ * the current route fill whatever placeholder is left.
62
+ */
63
+ export const fillFormPageUrl = (
64
+ url: string,
65
+ routeParams?: Record<string, string>,
66
+ itemId?: string,
67
+ ): string => {
68
+ let filled = url;
69
+ const rowIdAt =
70
+ itemId === undefined ? -1 : url.lastIndexOf(ROW_ID_PLACEHOLDER);
71
+ if (rowIdAt >= 0) {
72
+ filled =
73
+ url.slice(0, rowIdAt) +
74
+ itemId +
75
+ url.slice(rowIdAt + ROW_ID_PLACEHOLDER.length);
76
+ }
77
+ for (const [param, value] of Object.entries(routeParams ?? {})) {
78
+ filled = filled.replace(new RegExp(`:${param}(?![\\w])`, "g"), value);
79
+ }
80
+ return filled;
81
+ };
82
+
48
83
  export const useTableViewConfig = <T extends Data>(
49
84
  config: TableViewConfig<T>,
50
85
  ) => {
@@ -90,6 +125,8 @@ export const useTableViewConfig = <T extends Data>(
90
125
  customNavItems: config.customNavItems,
91
126
  componentId: config.componentId,
92
127
  formContainer: config.formContainer,
128
+ formPages: config.formPages,
129
+ routeParams: config.routeParams,
93
130
  data: config.data,
94
131
  loading: config.loading,
95
132
  orderOptions: config.orderOptions,
@@ -117,6 +154,7 @@ export const useTableViewConfig = <T extends Data>(
117
154
  customButtons: config.customButtons,
118
155
  formComponents: config.formComponents,
119
156
  formContainer: config.formContainer,
157
+ formPages: config.formPages,
120
158
  componentId: config.componentId,
121
159
  pageId: config.pageId,
122
160
  defaultSort: config.defaultSort,