@antelopejs/dms 0.10.0 → 0.11.1
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 +113 -0
- package/dist/config.d.ts +0 -2
- package/dist/config.js +0 -4
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.js +17 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/manifest.d.ts +6 -0
- package/dist/implementations/dms/manifest.js +10 -7
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/implementations/dms/page.js +45 -16
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +4 -7
- package/dist/index.js.map +1 -1
- package/dist/store/listing.js +1 -2
- package/dist/store/listing.js.map +1 -1
- package/dist/store/operations.js +1 -2
- package/dist/store/operations.js.map +1 -1
- package/dist/utils/ensure-default-tenant.d.ts +2 -1
- package/dist/utils/ensure-default-tenant.js +13 -1
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/dist/utils/export-jobs.js +2 -1
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +1 -2
- package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +3 -0
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +5 -37
- package/frontend-vue/layers/dms-core/app/utils/menu.ts +66 -70
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
- package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
- package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +3 -11
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +2 -106
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +0 -8
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +0 -10
- package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +18 -3
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +24 -4
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +10 -2
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -0
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +9 -9
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +9 -9
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
- package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +6 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +6 -4
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +2 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +26 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +22 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +17 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +30 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +13 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
- package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +27 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +14 -7
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +3 -9
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +16 -1
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +47 -7
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +19 -9
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -1
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -1
- package/frontend-vue/package.json +6 -2
- package/package.json +8 -11
- package/skills/dms-module/SKILL.md +1 -1
- package/skills/dms-pages/REFERENCE.md +7 -3
- package/dist/hooks/tenant-instances.d.ts +0 -1
- package/dist/hooks/tenant-instances.js +0 -13
- package/dist/hooks/tenant-instances.js.map +0 -1
- package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +0 -29
- package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +0 -50
- package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +0 -71
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"packageManager": "pnpm@10.6.5",
|
|
13
13
|
"engines": {
|
|
14
|
-
"@antelopejs/dms-frontend": ">=0.
|
|
14
|
+
"@antelopejs/dms-frontend": ">=0.9.0 <0.10.0"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"typecheck": "ajs dms verify-source --layer \"$PWD\" --local-package @antelopejs/dms=.. --local-package @antelopejs/interface-dms=../../interface-dms",
|
|
@@ -37,7 +37,11 @@
|
|
|
37
37
|
"@iconify-json/ph": "^1.2.2",
|
|
38
38
|
"@internationalized/date": "^3.10.0",
|
|
39
39
|
"@lezer/common": "^1.5.3",
|
|
40
|
+
"@lezer/css": "^1.3.8",
|
|
40
41
|
"@lezer/highlight": "^1.2.5",
|
|
42
|
+
"@lezer/html": "^1.3.13",
|
|
43
|
+
"@lezer/javascript": "^1.5.6",
|
|
44
|
+
"@lezer/json": "^1.0.3",
|
|
41
45
|
"@tailwindcss/typography": "^0.5.19",
|
|
42
46
|
"@tanstack/vue-table": "^8.21.3",
|
|
43
47
|
"@tiptap/core": "^3.30.2",
|
|
@@ -75,7 +79,7 @@
|
|
|
75
79
|
},
|
|
76
80
|
"devDependencies": {
|
|
77
81
|
"@antelopejs/core": ">=1.16.0 <2",
|
|
78
|
-
"@antelopejs/dms-frontend": ">=0.
|
|
82
|
+
"@antelopejs/dms-frontend": ">=0.9.0 <0.10.0",
|
|
79
83
|
"@eslint/js": "^8.57.0",
|
|
80
84
|
"@inertiajs/vue3": "^3.7.0",
|
|
81
85
|
"@nuxt/ui": "4.10.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "The AntelopeJS dashboard management system: admin dashboards declared in TypeScript, with auth and tenancy, data management, file uploads, notifications and realtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"@antelopejs/interface-api": ">=0.0.15 <1.0.0",
|
|
59
59
|
"@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
|
|
60
60
|
"@antelopejs/interface-core": ">=0.2.4 <1.0.0",
|
|
61
|
-
"@antelopejs/interface-data-api": ">=0.
|
|
62
|
-
"@antelopejs/interface-database": ">=0.1.
|
|
63
|
-
"@antelopejs/interface-database-decorators": ">=0.1.
|
|
64
|
-
"@antelopejs/interface-dms": ">=0.
|
|
61
|
+
"@antelopejs/interface-data-api": ">=0.4.0 <1.0.0",
|
|
62
|
+
"@antelopejs/interface-database": ">=0.1.11 <1.0.0",
|
|
63
|
+
"@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0",
|
|
64
|
+
"@antelopejs/interface-dms": ">=0.9.1 <0.10.0",
|
|
65
65
|
"@antelopejs/interface-email": ">=0.0.6 <1.0.0",
|
|
66
66
|
"@antelopejs/interface-file-storage": ">=0.1.4 <1.0.0",
|
|
67
67
|
"archiver": "^7.0.1",
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"zod": "~3.24.2"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@antelopejs/core": ">=1.
|
|
81
|
-
"@antelopejs/tooling-configs": "
|
|
80
|
+
"@antelopejs/core": ">=1.17.0 <2",
|
|
81
|
+
"@antelopejs/tooling-configs": "0.2.0",
|
|
82
82
|
"@types/archiver": "^6.0.4",
|
|
83
83
|
"@types/chai": "^4.3.20",
|
|
84
84
|
"@types/jsonwebtoken": "^9.0.10",
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"@antelopejs/interface-redis": ">=0.0.9 <1.0.0"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
|
-
"@antelopejs/core": ">=1.
|
|
123
|
+
"@antelopejs/core": ">=1.17.0 <2"
|
|
124
124
|
},
|
|
125
125
|
"peerDependenciesMeta": {
|
|
126
126
|
"@antelopejs/core": {
|
|
@@ -129,11 +129,8 @@
|
|
|
129
129
|
},
|
|
130
130
|
"scripts": {
|
|
131
131
|
"build": "pnpm --filter @antelopejs/interface-dms build && rimraf dist && tsc && tsc-alias",
|
|
132
|
-
"format": "oxfmt . && pnpm --dir frontend-vue format",
|
|
133
132
|
"dev": "ajs project run --inspect -w -p playground",
|
|
134
133
|
"frontend:dev": "pnpm --dir playground frontend:dev",
|
|
135
|
-
"lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt . && pnpm --dir frontend-vue lint:fix",
|
|
136
|
-
"lint": "oxlint && oxfmt --check . && pnpm --dir frontend-vue lint && antelopejs-check-interface-ranges",
|
|
137
134
|
"typecheck": "node scripts/unless-prebuilt.mjs pnpm --filter @antelopejs/interface-dms build && tsc --noEmit",
|
|
138
135
|
"test": "node scripts/unless-prebuilt.mjs pnpm run build && ajs module test .",
|
|
139
136
|
"test:unit": "node scripts/unless-prebuilt.mjs pnpm run build && DMS_TEST_DIR=dist/test/unit ajs module test .",
|
|
@@ -85,7 +85,7 @@ module's frontend; without it the module is backend-only.
|
|
|
85
85
|
|
|
86
86
|
`renderer` names the framework (Vue 3), not a loader release. The `@antelopejs/dms-frontend`
|
|
87
87
|
releases the frontend module runs on go in the frontend module's own `package.json`, as
|
|
88
|
-
`"engines": { "@antelopejs/dms-frontend": ">=0.
|
|
88
|
+
`"engines": { "@antelopejs/dms-frontend": ">=0.9.0 <0.10.0" }`: the loader refuses the module
|
|
89
89
|
outside that range. Cap it below the next loader minor while the loader is 0.x.
|
|
90
90
|
|
|
91
91
|
An absolute link (a public file, a snippet to copy, a mail) is built on `await GetApiBaseUrl()`
|
|
@@ -206,9 +206,13 @@ row's ⋯ button — `docs/04.components/08.tables/0.index.md#pinned-rows`.
|
|
|
206
206
|
`{ label, tone?, variant: "soft" | "outline" }`.
|
|
207
207
|
- A `TableView` over a shared controller hides a column on one page with
|
|
208
208
|
`columns: { hidden: ["workspace"] }` — no view just to hide it.
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
- A field shown, required or locked by another column's value goes on its `@Column`:
|
|
210
|
+
`visibleWhen: { field: "stage", in: ["lost"] }`, `disabledWhen: { field: "stage", in: ["won"] }`.
|
|
211
|
+
The generated forms and the data routes both apply them (hidden: not mandatory, value ignored,
|
|
212
|
+
stored value cleared on edit; disabled: sent value ignored, stored one kept) — no watch, no custom page.
|
|
213
|
+
- `forms: { new?, edit?, view? }` hands each generated form's builder to a callback
|
|
214
|
+
(`new: (form) => form.watch(FormEvents.FIELD_CHANGE, …)`) for input comfort only — a value filled
|
|
215
|
+
in from another, a field one table's forms leave out. No data rule there: the routes never see it.
|
|
212
216
|
- A table of more than a few columns that phones will read offers the cards and opens them there:
|
|
213
217
|
`displays: [{ id: "table" }, { id: "cards" }], mobileDisplay: "cards"`. Only a display the user
|
|
214
218
|
saved or the open view's own `display` overrides it: a `views.defaultView` that sets no display
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function registerTenantInstanceProvisioning(): void;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.registerTenantInstanceProvisioning = registerTenantInstanceProvisioning;
|
|
4
|
-
const hooks_1 = require("@antelopejs/interface-dms/hooks");
|
|
5
|
-
const tenant_instances_1 = require("../utils/tenant-instances");
|
|
6
|
-
async function onTenantBeingProvisioned(payload) {
|
|
7
|
-
await (0, tenant_instances_1.registerTenantInstance)(payload.tenantId);
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
function registerTenantInstanceProvisioning() {
|
|
11
|
-
(0, hooks_1.RegisterHook)(hooks_1.Hook.TENANT_BEING_PROVISIONED, onTenantBeingProvisioned);
|
|
12
|
-
}
|
|
13
|
-
//# sourceMappingURL=tenant-instances.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-instances.js","sourceRoot":"","sources":["../../src/hooks/tenant-instances.ts"],"names":[],"mappings":";;AAqBA,gFAEC;AAvBD,2DAIyC;AACzC,gEAAmE;AAEnE,KAAK,UAAU,wBAAwB,CACrC,OAAsC;IAEtC,MAAM,IAAA,yCAAsB,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/C,OAAO,SAAS,CAAC;AACnB,CAAC;AASD,SAAgB,kCAAkC;IAChD,IAAA,oBAAY,EAAC,YAAI,CAAC,wBAAwB,EAAE,wBAAwB,CAAC,CAAC;AACxE,CAAC"}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { getCurrentInstance } from "vue";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Whether the component setting up or rendering now is hydrating the
|
|
5
|
-
* server's HTML.
|
|
6
|
-
*
|
|
7
|
-
* Vue binds a hydrating component's vnode to its server-rendered node before
|
|
8
|
-
* its setup runs, where a fresh mount has no node until it is mounted; once
|
|
9
|
-
* mounted, the component no longer hydrates. So this is true from the setup
|
|
10
|
-
* to the first render of a component the browser takes over from the server,
|
|
11
|
-
* and false in a client navigation, on a later re-render, on the server, and
|
|
12
|
-
* outside a component.
|
|
13
|
-
*
|
|
14
|
-
* Read it where the browser must draw what the server drew: a part drawn
|
|
15
|
-
* in the browser only, or a value only the browser knows (local storage, the
|
|
16
|
-
* clock), waits until mounted while hydrating, and shows at once anywhere
|
|
17
|
-
* else. Call it before the setup's first `await`, while the component is
|
|
18
|
-
* still Vue's current instance. Data needs no check: `useApiData` hands the
|
|
19
|
-
* server's answer to the browser on its own.
|
|
20
|
-
*
|
|
21
|
-
* ```ts
|
|
22
|
-
* const isDrawn = ref(!import.meta.env.SSR && !isHydratingComponent());
|
|
23
|
-
* onMounted(() => (isDrawn.value = true));
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export function isHydratingComponent(): boolean {
|
|
27
|
-
const instance = getCurrentInstance();
|
|
28
|
-
return !!instance && !instance.isMounted && !!instance.vnode.el;
|
|
29
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { computed, type ComputedRef } from "vue";
|
|
2
|
-
import { resolveUrlVariables } from "#dms-ui/app/build/utils/urlVariables";
|
|
3
|
-
import type { PageRecord } from "../../utils/pageRecord";
|
|
4
|
-
|
|
5
|
-
/** The record a page shows, as the layout declares it (`record`). */
|
|
6
|
-
export interface PageRecordSource {
|
|
7
|
-
/** `{{params.X}}` and `{{query.X}}` are filled in from the page URL. */
|
|
8
|
-
fetchUrl: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The record the page shows, for the header buttons acting on it: loading
|
|
13
|
-
* until it answers, missing when it does not, or when its URL names a value
|
|
14
|
-
* the page does not carry. Undefined for a page declaring no record.
|
|
15
|
-
*
|
|
16
|
-
* It is read through `useApiData`, keyed on its URL like a block's
|
|
17
|
-
* `fetchUrl`: a block of the page reading the same URL shares the one
|
|
18
|
-
* request, and a server render hands the record to the browser with the page.
|
|
19
|
-
*/
|
|
20
|
-
export function usePageRecord(
|
|
21
|
-
source: () => PageRecordSource | undefined,
|
|
22
|
-
): ComputedRef<PageRecord | undefined> {
|
|
23
|
-
const route = useDmsRoute();
|
|
24
|
-
const siteLayout = useSiteLayout();
|
|
25
|
-
|
|
26
|
-
// Null while the URL names a value the page does not carry.
|
|
27
|
-
const url = computed(() => {
|
|
28
|
-
const declared = source();
|
|
29
|
-
if (!declared) return undefined;
|
|
30
|
-
return (
|
|
31
|
-
resolveUrlVariables(declared.fetchUrl, {
|
|
32
|
-
routeParams: siteLayout.findMatchingRoute(route.path)?.params,
|
|
33
|
-
routeQuery: route.query as Record<string, unknown>,
|
|
34
|
-
}) ?? null
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
// The options a block's read takes (`useChartFetch`): a block never holds
|
|
38
|
-
// a navigation, and neither does the header.
|
|
39
|
-
const { data, error } = useApiData<Record<string, unknown>>(
|
|
40
|
-
() => url.value ?? null,
|
|
41
|
-
{ lazy: true },
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
return computed<PageRecord | undefined>(() => {
|
|
45
|
-
if (url.value === undefined) return undefined;
|
|
46
|
-
if (url.value === null || error.value) return { status: "missing" };
|
|
47
|
-
if (data.value) return { status: "loaded", values: data.value };
|
|
48
|
-
return { status: "loading" };
|
|
49
|
-
});
|
|
50
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
2
|
-
import type { ActionTarget } from "#dms-ui/app/composables/table-view/types/action-target";
|
|
3
|
-
|
|
4
|
-
// The record a page shows (`DefaultLayout({ record })`), as its header
|
|
5
|
-
// buttons read it: `{{record.X}}` in a target, a button acting on it.
|
|
6
|
-
|
|
7
|
-
/** The record of the page as the header knows it. */
|
|
8
|
-
export type PageRecord =
|
|
9
|
-
| { status: "loading" }
|
|
10
|
-
| { status: "loaded"; values: Record<string, unknown> }
|
|
11
|
-
/** Not found, refused, or its URL names a value the page does not carry. */
|
|
12
|
-
| { status: "missing" };
|
|
13
|
-
|
|
14
|
-
/** What the header needs of a button to bind it to the page's record. */
|
|
15
|
-
export interface RecordBoundButton {
|
|
16
|
-
target?: ActionTarget;
|
|
17
|
-
requiresRecord?: boolean;
|
|
18
|
-
disabled?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const RECORD_TOKEN = /\{\{\s*record\.([\w.-]+)\s*\}\}/g;
|
|
22
|
-
const NAMES_RECORD = /\{\{\s*record\./;
|
|
23
|
-
|
|
24
|
-
const targetUrl = (target: ActionTarget | undefined): string | undefined => {
|
|
25
|
-
const url = (target as { url?: unknown } | undefined)?.url;
|
|
26
|
-
return typeof url === "string" ? url : undefined;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/** Whether a button acts on the page's record. */
|
|
30
|
-
export function isRecordBound(button: RecordBoundButton): boolean {
|
|
31
|
-
if (button.requiresRecord) return true;
|
|
32
|
-
const url = targetUrl(button.target);
|
|
33
|
-
return !!url && NAMES_RECORD.test(url);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A URL with its `{{record.X}}` tokens filled in, each value percent-encoded
|
|
38
|
-
* (an address is one query value). A token the record has no value for is
|
|
39
|
-
* left as written.
|
|
40
|
-
*/
|
|
41
|
-
export function fillRecordTokens(
|
|
42
|
-
url: string,
|
|
43
|
-
record: Record<string, unknown>,
|
|
44
|
-
): string {
|
|
45
|
-
return url.replace(RECORD_TOKEN, (match, key: string) => {
|
|
46
|
-
const value: unknown = get(record, key);
|
|
47
|
-
return typeof value === "string" || typeof value === "number"
|
|
48
|
-
? encodeURIComponent(String(value))
|
|
49
|
-
: match;
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The button as the page's record leaves it: unchanged when it does not act
|
|
55
|
-
* on the record; disabled while the record loads; left out (`null`) when the
|
|
56
|
-
* record is missing; its target's `{{record.X}}` filled in once loaded.
|
|
57
|
-
*/
|
|
58
|
-
export function withPageRecord<T extends RecordBoundButton>(
|
|
59
|
-
button: T,
|
|
60
|
-
record: PageRecord | undefined,
|
|
61
|
-
): T | null {
|
|
62
|
-
if (!record || !isRecordBound(button)) return button;
|
|
63
|
-
if (record.status === "missing") return null;
|
|
64
|
-
if (record.status === "loading") return { ...button, disabled: true };
|
|
65
|
-
const url = targetUrl(button.target);
|
|
66
|
-
if (!url) return button;
|
|
67
|
-
return {
|
|
68
|
-
...button,
|
|
69
|
-
target: { ...button.target, url: fillRecordTokens(url, record.values) },
|
|
70
|
-
} as T;
|
|
71
|
-
}
|