@antelopejs/dms 0.7.20 → 0.8.0
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 +62 -0
- package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
- package/dist/implementations/dms-auth/account-emails.js +62 -0
- package/dist/implementations/dms-auth/account-emails.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +1 -3
- package/dist/implementations/dms-auth/index.js +15 -61
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/pages/settings/users/email-change.js +3 -4
- package/dist/pages/settings/users/email-change.js.map +1 -1
- package/dist/pages/settings/users/invites.js +1 -1
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.js +1 -1
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.js +1 -0
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js +27 -0
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +18 -4
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
- package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
- package/dist/test/unit/utils/email-subjects.test.js +27 -0
- package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
- package/dist/utils/admin-invite-email.js +2 -4
- package/dist/utils/admin-invite-email.js.map +1 -1
- package/dist/utils/email-change-mail.d.ts +1 -1
- package/dist/utils/email-change-mail.js +11 -6
- package/dist/utils/email-change-mail.js.map +1 -1
- package/dist/utils/email-subjects.d.ts +3 -0
- package/dist/utils/email-subjects.js +32 -0
- package/dist/utils/email-subjects.js.map +1 -0
- package/dist/utils/export-jobs.js +15 -10
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
- package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
- package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
- package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +14 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +90 -43
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableUrlTab.ts +42 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +10 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
- package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
- package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
- package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +14 -6
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
- package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
- package/frontend-vue/tests/builtin-emails.test.ts +141 -0
- package/frontend-vue/tests/cascader.test.ts +27 -0
- package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
- package/frontend-vue/tests/chart-colors.test.ts +8 -0
- package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
- package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
- package/frontend-vue/tests/check-list-marker.test.ts +58 -0
- package/frontend-vue/tests/confirm-from.test.ts +1 -1
- package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
- package/frontend-vue/tests/context-links.test.ts +2 -2
- package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
- package/frontend-vue/tests/download-action-target.test.ts +2 -2
- package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
- package/frontend-vue/tests/footer-config.test.ts +135 -0
- package/frontend-vue/tests/form-actions.test.ts +7 -0
- package/frontend-vue/tests/form-entry-order.test.ts +45 -0
- package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
- package/frontend-vue/tests/form-load-error.test.ts +38 -1
- package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
- package/frontend-vue/tests/header-actions.test.ts +55 -9
- package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
- package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
- package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
- package/frontend-vue/tests/row-side-panel.test.ts +93 -2
- package/frontend-vue/tests/save-bar.test.ts +15 -3
- package/frontend-vue/tests/save-status-error.test.ts +2 -2
- package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
- package/frontend-vue/tests/table-row-hover.test.ts +2 -2
- package/frontend-vue/tests/table-view-bulk-drawer.test.ts +114 -0
- package/frontend-vue/tests/table-view-cards-reread.test.ts +77 -0
- package/frontend-vue/tests/table-view-sources.test.ts +1 -1
- package/frontend-vue/tests/table-view-url-tab.test.ts +84 -2
- package/frontend-vue/tests/template-tokens.test.ts +235 -0
- package/frontend-vue/tests/url-interpolation.test.ts +6 -6
- package/package.json +2 -2
- package/skills/dms/SKILL.md +2 -1
- package/skills/dms-pages/REFERENCE.md +2 -2
- package/skills/dms-project/SKILL.md +0 -1
|
@@ -25,10 +25,12 @@ import {
|
|
|
25
25
|
type RowNavigation,
|
|
26
26
|
type RowNavigationSource,
|
|
27
27
|
rowNavigation,
|
|
28
|
+
type ShowRow,
|
|
28
29
|
} from "./rowNavigation";
|
|
29
30
|
import {
|
|
30
31
|
clearWrittenRecord,
|
|
31
32
|
isDeepLinkAction,
|
|
33
|
+
resolveDeepLinkedRow,
|
|
32
34
|
writeRecordId,
|
|
33
35
|
type WrittenRecord,
|
|
34
36
|
} from "../table-view/utils/recordLink";
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
closeRowSidePanelOf,
|
|
38
40
|
openRowSidePanel,
|
|
39
41
|
rowSidePanelOwnerKey,
|
|
42
|
+
useRowSidePanelContent,
|
|
40
43
|
} from "#dms-layout/app/build/composables/layout/rowSidePanel";
|
|
41
44
|
import type { TableUrlScope } from "../table-view/utils/views";
|
|
42
45
|
import { useActionConfirm } from "../confirm/useActionConfirm";
|
|
@@ -49,52 +52,47 @@ import {
|
|
|
49
52
|
findServedQuickAction,
|
|
50
53
|
} from "../../utils/dispatchQuickActionTarget";
|
|
51
54
|
import {
|
|
52
|
-
|
|
55
|
+
namesPageValue,
|
|
53
56
|
resolveLinkVariables,
|
|
54
57
|
resolveTextVariables,
|
|
55
58
|
} from "../../utils/urlVariables";
|
|
56
|
-
import {
|
|
59
|
+
import { currentPageUrlValues } from "../page/pageUrlValues";
|
|
57
60
|
import { type DownloadFetch, saveDownload } from "../../utils/fileDownload";
|
|
61
|
+
import { interpolateRowText } from "#dms-core/app/utils/url-interpolation";
|
|
58
62
|
|
|
59
63
|
/** Runs a confirmed action from inside its dialog (see `onConfirm`). */
|
|
60
64
|
type ConfirmedRun = NonNullable<ConfirmOptions["onConfirm"]>;
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
* A
|
|
64
|
-
*
|
|
67
|
+
* A target's URL with the page's `{{params.X}}` / `{{query.X}}` filled (a
|
|
68
|
+
* query pair without a value left out), then the row's `{{row.field}}`: an
|
|
65
69
|
* action keeps the page's context (`?website={{query.website}}`) as a page
|
|
66
|
-
* header button does
|
|
67
|
-
* as it is then.
|
|
70
|
+
* header button does, whatever its type and wherever it sits. Read when the
|
|
71
|
+
* action runs, so it carries the URL as it is then.
|
|
68
72
|
*/
|
|
69
|
-
function
|
|
70
|
-
const withPageValues =
|
|
73
|
+
function targetUrl(url: string, rowData?: Data): string {
|
|
74
|
+
const withPageValues = namesPageValue(url)
|
|
71
75
|
? resolveLinkVariables(url, currentPageUrlValues())
|
|
72
76
|
: url;
|
|
73
77
|
return rowData ? interpolateUrl(withPageValues, rowData) : withPageValues;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
/**
|
|
77
|
-
* A download target's file name with the
|
|
78
|
-
* (a name, not a URL: nothing is encoded)
|
|
79
|
-
*
|
|
81
|
+
* A download target's file name with the row's `{{row.field}}` and the
|
|
82
|
+
* page's tokens filled as they are (a name, not a URL: nothing is encoded). A
|
|
83
|
+
* token without a value leaves no name: the server's one is used.
|
|
80
84
|
*/
|
|
81
85
|
function downloadTargetFilename(
|
|
82
86
|
filename: string | undefined,
|
|
83
87
|
rowData?: Data,
|
|
84
88
|
): string | undefined {
|
|
85
89
|
if (!filename) return undefined;
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
return interpolateLiteral(withPageValues, rowData);
|
|
90
|
+
const withRow = rowData ? interpolateRowText(filename, rowData) : filename;
|
|
91
|
+
return resolveTextVariables(withRow, currentPageUrlValues());
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
routeParams: useSiteLayout().findMatchingRoute(route.path)?.params,
|
|
95
|
-
routeQuery: route.query as Record<string, unknown>,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
94
|
+
// The id an export job's start answered, in its `statusUrl` / `downloadUrl`.
|
|
95
|
+
const JOB_ID_TOKEN = /\{\{\s*job\.id\s*\}\}/g;
|
|
98
96
|
|
|
99
97
|
/** Where the buttons run: the requests they send, what they refresh. */
|
|
100
98
|
export interface ActionTargetsConfig {
|
|
@@ -116,7 +114,7 @@ export interface ActionTargetsConfig {
|
|
|
116
114
|
*/
|
|
117
115
|
onRecordShown?: (id?: string) => void;
|
|
118
116
|
/**
|
|
119
|
-
* The field a row's id is read from (`_id` by default): it fills `{id}` in
|
|
117
|
+
* The field a row's id is read from (`_id` by default): it fills `{{row.id}}` in
|
|
120
118
|
* the `from` URL of a row action's confirmation, as on a built-in action.
|
|
121
119
|
*/
|
|
122
120
|
rowIdKey?: string;
|
|
@@ -145,6 +143,18 @@ interface SidePanelTargetRun extends TargetRunExtras {
|
|
|
145
143
|
missingId?: string;
|
|
146
144
|
}
|
|
147
145
|
|
|
146
|
+
/**
|
|
147
|
+
* `show`, run only while the row side panel still shows `session`: a row read
|
|
148
|
+
* after the panel closed, or moved to another run, leaves it alone.
|
|
149
|
+
*/
|
|
150
|
+
function whileShown<V>(session: symbol, show: (value: V) => void) {
|
|
151
|
+
return (value: V): void => {
|
|
152
|
+
if (useRowSidePanelContent().value?.session === session) show(value);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const NO_ROW_ROUTE = async (): Promise<undefined> => undefined;
|
|
157
|
+
|
|
148
158
|
const NOT_FOUND_TITLE_KEY = "side_panel.not_found.title";
|
|
149
159
|
const NOT_FOUND_DESCRIPTION_KEY = "side_panel.not_found.description";
|
|
150
160
|
|
|
@@ -260,7 +270,10 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
260
270
|
const statusUrlTemplate = target.statusUrl;
|
|
261
271
|
const downloadUrlTemplate = target.downloadUrl;
|
|
262
272
|
const interpolate = (template: string, ticket: ExportJobTicket): string =>
|
|
263
|
-
|
|
273
|
+
targetUrl(
|
|
274
|
+
template.replace(JOB_ID_TOKEN, encodeURIComponent(ticket.jobId)),
|
|
275
|
+
rowData,
|
|
276
|
+
);
|
|
264
277
|
await runExportJob({
|
|
265
278
|
startUrl: url,
|
|
266
279
|
startMethod: target.method ?? "POST",
|
|
@@ -284,7 +297,7 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
284
297
|
try {
|
|
285
298
|
await saveDownload(
|
|
286
299
|
{
|
|
287
|
-
url:
|
|
300
|
+
url: targetUrl(target.url, rowData),
|
|
288
301
|
filename: downloadTargetFilename(target.filename, rowData),
|
|
289
302
|
},
|
|
290
303
|
fetchRaw,
|
|
@@ -339,6 +352,31 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
339
352
|
config.onRecordShown?.(id);
|
|
340
353
|
};
|
|
341
354
|
};
|
|
355
|
+
// `showRow` given an id: the listed row, else the one the table's route
|
|
356
|
+
// reads. A row its route answered 404 for goes to `showMissing` when the
|
|
357
|
+
// container has a state for it; any other row not found rejects. Only the
|
|
358
|
+
// latest call applies: a read that resolves after a later call is dropped.
|
|
359
|
+
const rowShower = (
|
|
360
|
+
show: (row: Data) => void,
|
|
361
|
+
showMissing?: (id: string) => void,
|
|
362
|
+
): ShowRow => {
|
|
363
|
+
let latest = 0;
|
|
364
|
+
return async (rowOrId) => {
|
|
365
|
+
const call = ++latest;
|
|
366
|
+
if (typeof rowOrId !== "string") return show(rowOrId);
|
|
367
|
+
const source = config.rowNavigation;
|
|
368
|
+
const found = await resolveDeepLinkedRow(rowOrId, {
|
|
369
|
+
listed: source?.rows(),
|
|
370
|
+
idOf: rowIdOf,
|
|
371
|
+
getRow: source?.getRow ?? NO_ROW_ROUTE,
|
|
372
|
+
});
|
|
373
|
+
if (call !== latest) return;
|
|
374
|
+
if (found.row) return show(found.row);
|
|
375
|
+
if (found.isMissing && showMissing) return showMissing(rowOrId);
|
|
376
|
+
throw new Error(`The row "${rowOrId}" could not be read`);
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
|
|
342
380
|
const rowRecorder = (deepLink?: boolean) => {
|
|
343
381
|
const recordId = recordRecorder(deepLink);
|
|
344
382
|
return (row?: Data): void => recordId(rowIdOf(row));
|
|
@@ -382,6 +420,10 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
382
420
|
componentId: config.componentId,
|
|
383
421
|
containerId: run.containerId,
|
|
384
422
|
selection: run.selection,
|
|
423
|
+
showRow: rowShower(
|
|
424
|
+
whileShown(session, showRow),
|
|
425
|
+
whileShown(session, showMissing),
|
|
426
|
+
),
|
|
385
427
|
onSuccessCallback: () => {
|
|
386
428
|
closeRowSidePanel(session);
|
|
387
429
|
config.refreshCallback?.();
|
|
@@ -389,7 +431,7 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
389
431
|
},
|
|
390
432
|
onClose: () => recordId(undefined),
|
|
391
433
|
};
|
|
392
|
-
|
|
434
|
+
function showRow(row?: Data) {
|
|
393
435
|
openRowSidePanel({
|
|
394
436
|
...panel,
|
|
395
437
|
rowTitle: rowTitleOf(run.target, row),
|
|
@@ -399,10 +441,13 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
399
441
|
showRow,
|
|
400
442
|
});
|
|
401
443
|
recordId(rowIdOf(row));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
444
|
+
}
|
|
445
|
+
function showMissing(id: string) {
|
|
446
|
+
openRowSidePanel({ ...panel, notFound: notFoundOf(run.target), showRow });
|
|
447
|
+
recordId(id);
|
|
448
|
+
}
|
|
449
|
+
if (run.missingId) return showMissing(run.missingId);
|
|
450
|
+
showRow(run.rowData);
|
|
406
451
|
};
|
|
407
452
|
|
|
408
453
|
// A container opened on a listed row steps through the rows shown (its
|
|
@@ -415,6 +460,12 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
415
460
|
const source = config.rowNavigation;
|
|
416
461
|
let instance: ContainerInstance | undefined;
|
|
417
462
|
let navigation: RowNavigation | undefined;
|
|
463
|
+
// Set once the container closes: a row read that resolves later neither
|
|
464
|
+
// writes `?record=` back nor patches the closed container.
|
|
465
|
+
let closed = false;
|
|
466
|
+
const showRowById = rowShower((row) => {
|
|
467
|
+
if (!closed) showRow(row);
|
|
468
|
+
});
|
|
418
469
|
const optionsFor = (row?: Data) => {
|
|
419
470
|
navigation =
|
|
420
471
|
row && source ? rowNavigation(source, row, showRow) : undefined;
|
|
@@ -425,6 +476,7 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
425
476
|
containerId: run.containerId,
|
|
426
477
|
rowData: row,
|
|
427
478
|
navigation,
|
|
479
|
+
showRow: showRowById,
|
|
428
480
|
selection: run.selection,
|
|
429
481
|
onSuccessCallback: () => {
|
|
430
482
|
instance?.close();
|
|
@@ -436,14 +488,16 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
436
488
|
function showRow(row: Data) {
|
|
437
489
|
instance?.patch({
|
|
438
490
|
componentOptions: optionsFor(row),
|
|
439
|
-
componentKey:
|
|
491
|
+
componentKey: rowIdOf(row),
|
|
440
492
|
});
|
|
441
493
|
recordRow(row);
|
|
442
494
|
}
|
|
443
495
|
instance = openContainer(run, component, optionsFor(run.rowData));
|
|
444
496
|
recordRow(run.rowData);
|
|
445
|
-
|
|
497
|
+
// `showRow` may move an unlisted row's container onto a listed one.
|
|
498
|
+
const stopSteps = source ? listenRowSteps(() => navigation) : undefined;
|
|
446
499
|
const settle = () => {
|
|
500
|
+
closed = true;
|
|
447
501
|
stopSteps?.();
|
|
448
502
|
recordRow(undefined);
|
|
449
503
|
};
|
|
@@ -482,11 +536,11 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
482
536
|
const targetHandlers: Record<string, TargetHandler> = {
|
|
483
537
|
page: (target, _label, rowData) => {
|
|
484
538
|
const pageTarget = target as ActionTarget & { type: "page" };
|
|
485
|
-
navigateDms(
|
|
539
|
+
navigateDms(targetUrl(pageTarget.url, rowData));
|
|
486
540
|
},
|
|
487
541
|
external: (target, _label, rowData) => {
|
|
488
542
|
const externalTarget = target as ActionTarget & { type: "external" };
|
|
489
|
-
const url =
|
|
543
|
+
const url = targetUrl(externalTarget.url, rowData);
|
|
490
544
|
if (typeof window === "undefined" || !url) return;
|
|
491
545
|
if (externalTarget.newTab) {
|
|
492
546
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
@@ -496,16 +550,12 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
496
550
|
},
|
|
497
551
|
api: async (target, label, rowData) => {
|
|
498
552
|
const apiTarget = target as ActionTarget & { type: "api" };
|
|
499
|
-
const url = rowData
|
|
500
|
-
? interpolateUrl(apiTarget.url, rowData)
|
|
501
|
-
: apiTarget.url;
|
|
553
|
+
const url = targetUrl(apiTarget.url, rowData);
|
|
502
554
|
await handleApiTarget(apiTarget, url, label, rowData);
|
|
503
555
|
},
|
|
504
556
|
exportJob: async (target, _label, rowData) => {
|
|
505
557
|
const jobTarget = target as ActionTarget & { type: "exportJob" };
|
|
506
|
-
const url = rowData
|
|
507
|
-
? interpolateUrl(jobTarget.url, rowData)
|
|
508
|
-
: jobTarget.url;
|
|
558
|
+
const url = targetUrl(jobTarget.url, rowData);
|
|
509
559
|
await handleExportJobTarget(jobTarget, url, rowData);
|
|
510
560
|
},
|
|
511
561
|
download: (target, label, rowData) =>
|
|
@@ -525,9 +575,6 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
525
575
|
sidePanel: sidePanelHandler,
|
|
526
576
|
};
|
|
527
577
|
|
|
528
|
-
const targetUrl = (url: string, rowData?: Data): string =>
|
|
529
|
-
rowData ? interpolateUrl(url, rowData) : url;
|
|
530
|
-
|
|
531
578
|
// A target making a request runs it from inside its confirmation, which
|
|
532
579
|
// shows a refusal (under its field when it names one) and stays open; any
|
|
533
580
|
// other runs once confirmed.
|
|
@@ -548,7 +595,7 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
548
595
|
},
|
|
549
596
|
};
|
|
550
597
|
|
|
551
|
-
// `{id}` of a confirmation's `from` URL, when the row carries an id.
|
|
598
|
+
// `{{row.id}}` of a confirmation's `from` URL, when the row carries an id.
|
|
552
599
|
const rowUrlParams = (rowData?: Data) => {
|
|
553
600
|
const id = rowData
|
|
554
601
|
? get(rowData, config.rowIdKey ?? DEFAULT_ROW_ID_KEY)
|
|
@@ -18,9 +18,19 @@ const DATA_LABEL_FONT_SIZE = "11px";
|
|
|
18
18
|
const DATA_LABEL_FONT_WEIGHT = 600;
|
|
19
19
|
const TOOLTIP_DATETIME_FORMAT = "dd MMM yyyy";
|
|
20
20
|
const HOVER_MARKER_SIZE = 5;
|
|
21
|
+
/** Grows a visible point on hover instead of setting a size below its own. */
|
|
22
|
+
const HOVER_POINT_SIZE_OFFSET = 2;
|
|
23
|
+
/** A scatter is nothing but its points, so it draws them without `pointSize`. */
|
|
24
|
+
const DEFAULT_POINT_SIZE = 6;
|
|
25
|
+
const POINT_MARKER_TYPES: ChartType[] = ["scatter"];
|
|
26
|
+
/** Half a data label's box and a gap: lifts the label off the point it names. */
|
|
27
|
+
const POINT_LABEL_CLEARANCE = 11;
|
|
28
|
+
const HALF = 2;
|
|
21
29
|
const HEAD_MARKER_SIZE = 5;
|
|
22
30
|
const SOLID_GRID = 0;
|
|
23
31
|
const HEAD_MARKER_TYPES: ChartType[] = ["line", "area"];
|
|
32
|
+
/** A radar draws its own polygon grid; the cartesian one would cross the card. */
|
|
33
|
+
const OWN_GRID_TYPES: ChartType[] = ["radar"];
|
|
24
34
|
|
|
25
35
|
/** Theme colors every section of an Apex config reads once per build. */
|
|
26
36
|
export interface ApexThemeColors {
|
|
@@ -83,7 +93,10 @@ export function buildGrid(
|
|
|
83
93
|
theme: ApexThemeColors,
|
|
84
94
|
): Record<string, unknown> {
|
|
85
95
|
return {
|
|
86
|
-
show:
|
|
96
|
+
show:
|
|
97
|
+
!isCircular &&
|
|
98
|
+
!OWN_GRID_TYPES.includes(input.type) &&
|
|
99
|
+
(input.showGrid ?? true),
|
|
87
100
|
borderColor: theme.border,
|
|
88
101
|
strokeDashArray: SOLID_GRID,
|
|
89
102
|
padding: GRID_PADDING,
|
|
@@ -140,6 +153,13 @@ export function buildMarkers(
|
|
|
140
153
|
colors: string[],
|
|
141
154
|
theme: ApexThemeColors,
|
|
142
155
|
): Record<string, unknown> {
|
|
156
|
+
if (POINT_MARKER_TYPES.includes(input.type)) {
|
|
157
|
+
return {
|
|
158
|
+
size: pointSize(input),
|
|
159
|
+
strokeWidth: 0,
|
|
160
|
+
hover: { sizeOffset: HOVER_POINT_SIZE_OFFSET },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
143
163
|
const showsHead = HEAD_MARKER_TYPES.includes(input.type) && !input.stacked;
|
|
144
164
|
return {
|
|
145
165
|
size: 0,
|
|
@@ -151,18 +171,55 @@ export function buildMarkers(
|
|
|
151
171
|
};
|
|
152
172
|
}
|
|
153
173
|
|
|
154
|
-
|
|
174
|
+
function pointSize(input: UseApexChartInput): number {
|
|
175
|
+
return input.pointSize ?? DEFAULT_POINT_SIZE;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* How far a data label moves up: Apex centres it on its point, which hides a
|
|
180
|
+
* scatter's point under its own label.
|
|
181
|
+
*/
|
|
182
|
+
export function dataLabelOffset(input: UseApexChartInput): number {
|
|
183
|
+
if (!POINT_MARKER_TYPES.includes(input.type)) return 0;
|
|
184
|
+
return -(pointSize(input) / HALF + POINT_LABEL_CLEARANCE);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Where Apex is drawing a data label. */
|
|
188
|
+
interface DataLabelContext {
|
|
189
|
+
seriesIndex: number;
|
|
190
|
+
dataPointIndex: number;
|
|
191
|
+
w: { config: { series: ChartSeries[] } };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A point's own `label` when it has one, else the value Apex would print. */
|
|
195
|
+
function pointLabel(value: unknown, context: DataLabelContext): unknown {
|
|
196
|
+
const point =
|
|
197
|
+
context.w.config.series[context.seriesIndex]?.data[context.dataPointIndex];
|
|
198
|
+
if (typeof point !== "object" || point === null || !("label" in point)) {
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
return point.label || value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Value labels in mono, without Apex's default drop shadow. A circular chart
|
|
206
|
+
* keeps Apex's own percent formatter: its series hold bare numbers.
|
|
207
|
+
*/
|
|
155
208
|
export function buildDataLabels(
|
|
156
209
|
enabled: boolean,
|
|
157
210
|
theme: ApexThemeColors,
|
|
211
|
+
isCircular = false,
|
|
212
|
+
offsetY = 0,
|
|
158
213
|
): Record<string, unknown> {
|
|
159
214
|
return {
|
|
160
215
|
enabled,
|
|
216
|
+
offsetY,
|
|
161
217
|
dropShadow: { enabled: false },
|
|
162
218
|
style: {
|
|
163
219
|
fontSize: DATA_LABEL_FONT_SIZE,
|
|
164
220
|
fontFamily: theme.monoFont,
|
|
165
221
|
fontWeight: DATA_LABEL_FONT_WEIGHT,
|
|
166
222
|
},
|
|
223
|
+
...(isCircular ? {} : { formatter: pointLabel }),
|
|
167
224
|
};
|
|
168
225
|
}
|
|
@@ -3,7 +3,10 @@ import type {
|
|
|
3
3
|
ConfirmDialogSecondaryAction,
|
|
4
4
|
ConfirmDialogSecondaryTarget,
|
|
5
5
|
} from "#dms-core/app/types/confirm-dialog";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
interpolateRowText,
|
|
8
|
+
interpolateUrl,
|
|
9
|
+
} from "#dms-core/app/utils/url-interpolation";
|
|
7
10
|
import { readTranslationKey } from "#dms-core/app/composables/translation/useTranslation";
|
|
8
11
|
import type {
|
|
9
12
|
ConfirmOptions,
|
|
@@ -11,6 +14,12 @@ import type {
|
|
|
11
14
|
} from "../../../composables/confirm/types";
|
|
12
15
|
import { processFieldI18n } from "../../utils/fieldOptionsI18n";
|
|
13
16
|
import { interpolateLiteral } from "../../utils/literalText";
|
|
17
|
+
import {
|
|
18
|
+
namesPageValue,
|
|
19
|
+
resolveLinkVariables,
|
|
20
|
+
resolveTextVariables,
|
|
21
|
+
} from "../../utils/urlVariables";
|
|
22
|
+
import { currentPageUrlValues } from "../page/pageUrlValues";
|
|
14
23
|
|
|
15
24
|
/** `t` as vue-i18n gives it: named parameters, then the plural count. */
|
|
16
25
|
export type ConfirmTranslate = (
|
|
@@ -41,22 +50,36 @@ export type SecondaryTargetRun = (
|
|
|
41
50
|
target: ConfirmDialogSecondaryTarget,
|
|
42
51
|
) => Promise<void> | void;
|
|
43
52
|
|
|
53
|
+
// The page's `{{params.X}}` / `{{query.X}}` come first, as on an action's own
|
|
54
|
+
// target; the route is only read for a text holding such a token.
|
|
55
|
+
function withPageLink(url: string): string {
|
|
56
|
+
return namesPageValue(url)
|
|
57
|
+
? resolveLinkVariables(url, currentPageUrlValues())
|
|
58
|
+
: url;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withPageText(text: string): string | undefined {
|
|
62
|
+
return namesPageValue(text)
|
|
63
|
+
? resolveTextVariables(text, currentPageUrlValues())
|
|
64
|
+
: text;
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
/**
|
|
45
68
|
* A dialog's secondary target with its URL (and file name) filled in with
|
|
46
|
-
* the same parameters as its texts.
|
|
69
|
+
* the page's values and, as `{{row.X}}`, the same parameters as its texts.
|
|
47
70
|
*/
|
|
48
71
|
function resolveSecondaryTarget(
|
|
49
72
|
target: ConfirmDialogSecondaryTarget,
|
|
50
73
|
params: Record<string, unknown>,
|
|
51
74
|
): ConfirmDialogSecondaryTarget {
|
|
52
|
-
const url = interpolateUrl(target.url, params);
|
|
75
|
+
const url = interpolateUrl(withPageLink(target.url), params);
|
|
53
76
|
if (target.type !== "download" || target.filename === undefined) {
|
|
54
77
|
return { ...target, url };
|
|
55
78
|
}
|
|
56
79
|
return {
|
|
57
80
|
...target,
|
|
58
81
|
url,
|
|
59
|
-
filename:
|
|
82
|
+
filename: withPageText(interpolateRowText(target.filename, params)),
|
|
60
83
|
};
|
|
61
84
|
}
|
|
62
85
|
|
|
@@ -15,12 +15,17 @@ import {
|
|
|
15
15
|
} from "./confirmDialogTexts";
|
|
16
16
|
import type { DownloadFetch } from "../../utils/fileDownload";
|
|
17
17
|
import { secondaryTargetRunner } from "./secondaryTarget";
|
|
18
|
+
import {
|
|
19
|
+
namesPageValue,
|
|
20
|
+
resolveLinkVariables,
|
|
21
|
+
} from "../../utils/urlVariables";
|
|
22
|
+
import { currentPageUrlValues } from "../page/pageUrlValues";
|
|
18
23
|
|
|
19
24
|
/** What an action asks its confirmation with. */
|
|
20
25
|
export interface ActionConfirmRequest {
|
|
21
26
|
/** The row the action targets: its fields fill the texts and `from` URL. */
|
|
22
27
|
row?: Record<string, unknown>;
|
|
23
|
-
/** Fill-ins of a `from` URL besides the row's fields (`{id}`). */
|
|
28
|
+
/** Fill-ins of a `from` URL besides the row's fields (`{{row.id}}`). */
|
|
24
29
|
urlParams?: Record<string, unknown>;
|
|
25
30
|
/**
|
|
26
31
|
* The look of the action, where the dialog leaves it out (an archive's
|
|
@@ -55,7 +60,10 @@ export function useActionConfirm() {
|
|
|
55
60
|
request: ActionConfirmRequest,
|
|
56
61
|
): Promise<ConfirmDialog | undefined> => {
|
|
57
62
|
if (!isConfirmFrom(declared)) return declared;
|
|
58
|
-
const
|
|
63
|
+
const from = namesPageValue(declared.from)
|
|
64
|
+
? resolveLinkVariables(declared.from, currentPageUrlValues())
|
|
65
|
+
: declared.from;
|
|
66
|
+
const url = interpolateUrl(from, {
|
|
59
67
|
...request.row,
|
|
60
68
|
...request.urlParams,
|
|
61
69
|
});
|
|
@@ -322,13 +322,8 @@ function renderRelation(value: unknown, options: unknown) {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
function renderCascaderRelation(value: unknown, options: unknown) {
|
|
325
|
-
const { t } = useI18n();
|
|
326
325
|
const opts = options as CascaderRelationOptions | undefined;
|
|
327
326
|
|
|
328
|
-
if (opts?.multiple && Array.isArray(value)) {
|
|
329
|
-
return t("dms.form.cascader.selected_count", { count: value.length });
|
|
330
|
-
}
|
|
331
|
-
|
|
332
327
|
if (!value) return value;
|
|
333
328
|
|
|
334
329
|
return h(CascaderPath, {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FormFieldOrGroup } from "../../../composables/form/types/field";
|
|
2
|
+
import { isFieldGroup } from "../../../composables/form/types/field";
|
|
3
|
+
|
|
4
|
+
function entryOrder(entry: FormFieldOrGroup): number | undefined {
|
|
5
|
+
return isFieldGroup(entry) ? entry.order : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A form's entries with its groups' `order` applied: the groups giving one
|
|
10
|
+
* trade the places they were declared at, sorted on it (equal orders keep
|
|
11
|
+
* their declaration order); every other entry keeps its place.
|
|
12
|
+
*/
|
|
13
|
+
export function orderFormEntries<T extends FormFieldOrGroup>(
|
|
14
|
+
entries: readonly T[],
|
|
15
|
+
): T[] {
|
|
16
|
+
const ordered = entries
|
|
17
|
+
.filter((entry) => entryOrder(entry) !== undefined)
|
|
18
|
+
.sort((left, right) => entryOrder(left)! - entryOrder(right)!);
|
|
19
|
+
let next = 0;
|
|
20
|
+
return entries.map((entry) =>
|
|
21
|
+
entryOrder(entry) === undefined ? entry : ordered[next++]!,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* it has somewhere to go back to, then "Unsaved changes" with Discard and
|
|
7
7
|
* Save.
|
|
8
8
|
* - `action`: a form that does something each time (send a notification,
|
|
9
|
-
* invite someone, run a job).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* successful submit.
|
|
9
|
+
* invite someone, run a job). Its own submit label, always ready: what it
|
|
10
|
+
* holds untouched, its defaults included, is a payload too. Beside it,
|
|
11
|
+
* Reset once a value changed, Cancel before that in a drawer or a modal.
|
|
12
|
+
* It empties after a successful submit.
|
|
13
13
|
*/
|
|
14
14
|
export type FormKind = "record" | "action";
|
|
15
15
|
|
|
@@ -83,21 +83,25 @@ function recordSaveBar(context: SaveBarContext): SaveBarState {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* An action form's bar:
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
86
|
+
* An action form's bar: its submit, never held, so a pre-filled form sends as
|
|
87
|
+
* it is. Beside it, Reset once a value changed; before that, Cancel in a
|
|
88
|
+
* drawer or a modal, its only way out. A form nothing in which can be changed
|
|
89
|
+
* has nothing to reset.
|
|
90
90
|
*/
|
|
91
91
|
function actionSaveBar(context: SaveBarContext): SaveBarState {
|
|
92
|
-
const bar = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
const bar: SaveBarState = {
|
|
93
|
+
showsStatus: false,
|
|
94
|
+
showsSubmit: true,
|
|
95
|
+
isSubmitHeld: false,
|
|
96
|
+
isHidden: false,
|
|
97
|
+
};
|
|
98
|
+
if (context.dirty && context.resettable) {
|
|
99
|
+
return { ...bar, secondary: "discard" };
|
|
96
100
|
}
|
|
97
|
-
if (!context.cancellable) {
|
|
98
|
-
return { ...
|
|
101
|
+
if (!context.dirty && context.cancellable) {
|
|
102
|
+
return { ...bar, secondary: "cancel" };
|
|
99
103
|
}
|
|
100
|
-
return
|
|
104
|
+
return bar;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
const SAVE_BARS: Record<FormKind, (context: SaveBarContext) => SaveBarState> = {
|
|
@@ -5,9 +5,33 @@ import { HTTP_FORBIDDEN } from "#dms-core/app/utils/http-status";
|
|
|
5
5
|
/**
|
|
6
6
|
* Why a form could not load the record it edits: `forbidden` when the server
|
|
7
7
|
* refused the caller the read (a role that may edit rows but not view them),
|
|
8
|
-
* `
|
|
8
|
+
* `missing` when its address lacks the record's id (a page reached without
|
|
9
|
+
* its `?id=`), `error` for anything else (a network failure, a server error,
|
|
10
|
+
* a row gone).
|
|
9
11
|
*/
|
|
10
|
-
export type FormLoadFailure = "forbidden" | "error";
|
|
12
|
+
export type FormLoadFailure = "forbidden" | "missing" | "error";
|
|
13
|
+
|
|
14
|
+
/** What a form knows of the record it is about, as far as `missing` goes. */
|
|
15
|
+
export interface FormRecordAddress {
|
|
16
|
+
requiresRecord?: boolean;
|
|
17
|
+
fetchUrl?: string;
|
|
18
|
+
/** `fetchUrl` with its route values filled; unset while one is missing. */
|
|
19
|
+
resolvedFetchUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `missing` for a form about one record whose address names no record yet:
|
|
24
|
+
* its fields would only hold their defaults, which read as a record.
|
|
25
|
+
*/
|
|
26
|
+
export function missingRecordFailure(
|
|
27
|
+
address: FormRecordAddress,
|
|
28
|
+
): FormLoadFailure | null {
|
|
29
|
+
const isMissing =
|
|
30
|
+
!!address.requiresRecord &&
|
|
31
|
+
!!address.fetchUrl &&
|
|
32
|
+
!address.resolvedFetchUrl;
|
|
33
|
+
return isMissing ? "missing" : null;
|
|
34
|
+
}
|
|
11
35
|
|
|
12
36
|
/** The cached load of a form's record (`useDmsAsyncData`), as far as it is read. */
|
|
13
37
|
export interface FormLoadSource<T> {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
namesPageValue,
|
|
3
|
+
type ReplaceUrlVariablesContext,
|
|
4
|
+
resolveLinkVariables,
|
|
5
|
+
resolveUrlVariables,
|
|
6
|
+
} from "../../utils/urlVariables";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The values of the page being shown that a `{{params.X}}` / `{{query.X}}`
|
|
10
|
+
* token names: the route parameters of the DMS page matching the URL, and its
|
|
11
|
+
* query. Read when called, so it follows the URL as it changes.
|
|
12
|
+
*/
|
|
13
|
+
export function currentPageUrlValues(): ReplaceUrlVariablesContext {
|
|
14
|
+
const route = useDmsRoute();
|
|
15
|
+
return {
|
|
16
|
+
routeParams: useSiteLayout().findMatchingRoute(route.path)?.params,
|
|
17
|
+
routeQuery: route.query as Record<string, unknown>,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A link declared on a block (`to`) with the page's tokens filled, as a page
|
|
23
|
+
* header button's: a query pair the page has no value for is left out. The
|
|
24
|
+
* route is only read for a link holding a token.
|
|
25
|
+
*/
|
|
26
|
+
export function pageLink(to: string): string;
|
|
27
|
+
export function pageLink(to: string | undefined): string | undefined;
|
|
28
|
+
export function pageLink(to: string | undefined): string | undefined {
|
|
29
|
+
if (!to || !namesPageValue(to)) return to;
|
|
30
|
+
return resolveLinkVariables(to, currentPageUrlValues());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A route a field or block requests (`searchUrl`, `rotateUrl`, …) with the
|
|
35
|
+
* page's tokens filled, or `undefined` while one has no value, as a block's
|
|
36
|
+
* `fetchUrl`.
|
|
37
|
+
*/
|
|
38
|
+
export function pageRequestUrl(url: string | undefined): string | undefined {
|
|
39
|
+
if (!url || !namesPageValue(url)) return url;
|
|
40
|
+
return resolveUrlVariables(url, currentPageUrlValues());
|
|
41
|
+
}
|