@antelopejs/dms 0.10.0 → 0.11.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 +69 -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/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/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/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/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 +7 -7
- 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
|
@@ -5,7 +5,11 @@ import type { FormProps, FormFetchResponse, FormSubmitResponse } from "./types";
|
|
|
5
5
|
import { jsonSchemaToZod } from "json-schema-to-zod";
|
|
6
6
|
import { FormEvents } from "./types/events";
|
|
7
7
|
import type { FormData, FormFieldValue } from "./types/value";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
FieldCondition,
|
|
10
|
+
FormField,
|
|
11
|
+
FormFieldOrGroup,
|
|
12
|
+
} from "./types/field";
|
|
9
13
|
import { isFieldGroup } from "./types/field";
|
|
10
14
|
import { resolveResponseToast } from "../../build/utils/responseWarning";
|
|
11
15
|
import { processFieldI18n } from "../../build/utils/fieldOptionsI18n";
|
|
@@ -35,9 +39,11 @@ interface FormSubmitNotice {
|
|
|
35
39
|
import type { DataType } from "#dms-core/app/composables/data-types/useDataType";
|
|
36
40
|
import {
|
|
37
41
|
type ApiFieldError,
|
|
42
|
+
apiErrorStatus,
|
|
38
43
|
apiErrorText,
|
|
39
44
|
resolveFieldErrors,
|
|
40
45
|
} from "#dms-core/app/composables/useFieldErrors";
|
|
46
|
+
import { HTTP_NOT_FOUND } from "#dms-core/app/utils/http-status";
|
|
41
47
|
import { isBlankValue } from "#dms-core/app/composables/useFormValidation";
|
|
42
48
|
import {
|
|
43
49
|
isPlainObject,
|
|
@@ -472,6 +478,34 @@ export function isFieldMarkedRequired(
|
|
|
472
478
|
return !!field.required || (required?.has(field.id) ?? false);
|
|
473
479
|
}
|
|
474
480
|
|
|
481
|
+
const meetsCondition = (
|
|
482
|
+
condition: FieldCondition,
|
|
483
|
+
values: Record<string, unknown>,
|
|
484
|
+
): boolean => condition.in.includes(values[condition.field] ?? null);
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The fields the values hide (`visibleWhen` not met) or disable
|
|
488
|
+
* (`disabledWhen` met), added to those the watches hide or disable: the
|
|
489
|
+
* conditions hold from the values the form opens with, without a watch.
|
|
490
|
+
*/
|
|
491
|
+
export function withConditionFields(
|
|
492
|
+
fields: ReadonlyArray<Pick<FormField, "id" | "visibleWhen" | "disabledWhen">>,
|
|
493
|
+
values: Record<string, unknown>,
|
|
494
|
+
watched: { disabled?: Set<string>; hidden?: Set<string> },
|
|
495
|
+
): { disabled: Set<string>; hidden: Set<string> } {
|
|
496
|
+
const disabled = new Set(watched.disabled);
|
|
497
|
+
const hidden = new Set(watched.hidden);
|
|
498
|
+
for (const field of fields) {
|
|
499
|
+
if (field.visibleWhen && !meetsCondition(field.visibleWhen, values)) {
|
|
500
|
+
hidden.add(field.id);
|
|
501
|
+
}
|
|
502
|
+
if (field.disabledWhen && meetsCondition(field.disabledWhen, values)) {
|
|
503
|
+
disabled.add(field.id);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return { disabled, hidden };
|
|
507
|
+
}
|
|
508
|
+
|
|
475
509
|
function watchFieldChanges(
|
|
476
510
|
state: Ref<Record<string, unknown>>,
|
|
477
511
|
componentId: string | undefined,
|
|
@@ -612,13 +646,16 @@ export const useForm = (props: FormProps, options: UseFormOptions = {}) => {
|
|
|
612
646
|
{ deep: true },
|
|
613
647
|
);
|
|
614
648
|
|
|
615
|
-
const
|
|
616
|
-
(
|
|
649
|
+
const inactiveFields = computed(() =>
|
|
650
|
+
withConditionFields(allFields.value, state.value, {
|
|
651
|
+
disabled: state.value.disabledFields as Set<string> | undefined,
|
|
652
|
+
hidden: state.value.hiddenFields as Set<string> | undefined,
|
|
653
|
+
}),
|
|
617
654
|
);
|
|
618
655
|
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
);
|
|
656
|
+
const disabledFields = computed(() => inactiveFields.value.disabled);
|
|
657
|
+
|
|
658
|
+
const hiddenFields = computed(() => inactiveFields.value.hidden);
|
|
622
659
|
|
|
623
660
|
const requiredFields = computed(
|
|
624
661
|
() => state.value.requiredFields as Set<string> | undefined,
|
|
@@ -686,7 +723,10 @@ export const useForm = (props: FormProps, options: UseFormOptions = {}) => {
|
|
|
686
723
|
// A server render shows the failure in the form (FormLoadError) and
|
|
687
724
|
// hands it to the browser; a toast it added would show in its HTML
|
|
688
725
|
// alone, which the browser's toasts never hold.
|
|
689
|
-
|
|
726
|
+
// A 404 is no error either: the form says the record is not found.
|
|
727
|
+
if (!import.meta.env.SSR && apiErrorStatus(error) !== HTTP_NOT_FOUND) {
|
|
728
|
+
showFetchErrorToast(error);
|
|
729
|
+
}
|
|
690
730
|
throw error;
|
|
691
731
|
}
|
|
692
732
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
computed,
|
|
3
3
|
getCurrentScope,
|
|
4
|
+
onMounted,
|
|
4
5
|
onScopeDispose,
|
|
6
|
+
shallowRef,
|
|
5
7
|
type ComputedRef,
|
|
6
8
|
} from "vue";
|
|
7
9
|
import {
|
|
@@ -75,7 +77,12 @@ export interface PageLiveState {
|
|
|
75
77
|
* never paused, and its actions do nothing. A server render reads the page as
|
|
76
78
|
* live and never paused too, but `isEnabled` says whether it declares `live`:
|
|
77
79
|
* the browser hydrates the page's blocks with its controller installed, so
|
|
78
|
-
* the server draws them as it will.
|
|
80
|
+
* the server draws them as it will. The rest of the state is born in the
|
|
81
|
+
* browser (the realtime stream, the page's reads, a pause): a component
|
|
82
|
+
* hydrating the server's HTML reads it as the server did (live, not polling,
|
|
83
|
+
* never read, nothing missed) until its own `onMounted`, then follows the
|
|
84
|
+
* page. A component mounted afresh, after a client navigation, reads the page
|
|
85
|
+
* at once.
|
|
79
86
|
*
|
|
80
87
|
* ```ts
|
|
81
88
|
* const { isPaused, missedCount, resume, hold, release } = usePageLive();
|
|
@@ -86,26 +93,29 @@ export interface PageLiveState {
|
|
|
86
93
|
* `realtimeTopic` are.
|
|
87
94
|
*/
|
|
88
95
|
export function usePageLive(): PageLiveState {
|
|
89
|
-
|
|
96
|
+
// An async block hydrates after the page has installed its controller and
|
|
97
|
+
// read its data: until mounted, it draws the state the server drew.
|
|
98
|
+
const isHydrating = shallowRef(isDmsHydrating());
|
|
99
|
+
if (isHydrating.value) onMounted(() => (isHydrating.value = false));
|
|
100
|
+
const shown = () => (isHydrating.value ? null : currentPageLive());
|
|
101
|
+
const isPaused = computed(() => shown()?.isPaused.value ?? false);
|
|
90
102
|
const hold = usePageLiveHold();
|
|
91
103
|
const isServerLive = isServerPageLive();
|
|
92
104
|
return {
|
|
93
105
|
isEnabled: computed(() => currentPageLive() !== null || isServerLive),
|
|
94
106
|
isLive: computed(() => !isPaused.value),
|
|
95
107
|
isPaused,
|
|
96
|
-
pauseReason: computed(() =>
|
|
108
|
+
pauseReason: computed(() => shown()?.pauseReason.value ?? null),
|
|
97
109
|
pause: () => currentPageLive()?.pause(),
|
|
98
110
|
resume: () => currentPageLive()?.resume(),
|
|
99
111
|
toggle: () => currentPageLive()?.toggle(),
|
|
100
112
|
hold: hold.hold,
|
|
101
113
|
release: hold.release,
|
|
102
|
-
lastUpdatedAt: computed(
|
|
103
|
-
|
|
104
|
-
),
|
|
105
|
-
missedCount: computed(() => currentPageLive()?.missedCount.value ?? 0),
|
|
106
|
-
missedDetail: computed(() => currentPageLive()?.missedDetail.value ?? null),
|
|
114
|
+
lastUpdatedAt: computed(() => shown()?.lastUpdatedAt.value ?? null),
|
|
115
|
+
missedCount: computed(() => shown()?.missedCount.value ?? 0),
|
|
116
|
+
missedDetail: computed(() => shown()?.missedDetail.value ?? null),
|
|
107
117
|
setMissedDetail: (detail) => currentPageLive()?.setMissedDetail(detail),
|
|
108
|
-
isPolling: computed(() =>
|
|
118
|
+
isPolling: computed(() => shown()?.isPolling.value ?? false),
|
|
109
119
|
};
|
|
110
120
|
}
|
|
111
121
|
|
|
@@ -27,6 +27,7 @@ export interface CascaderTreeOptions {
|
|
|
27
27
|
|
|
28
28
|
export const CASCADER_PATH_SEPARATOR = " / ";
|
|
29
29
|
|
|
30
|
+
/** How many rows the picker's open menu lists to browse the tree. */
|
|
30
31
|
export const CASCADER_FETCH_LIMIT = 1000;
|
|
31
32
|
|
|
32
33
|
export const DEFAULT_CASCADER_KEY_MAPPING: CascaderKeyMapping = {
|
|
@@ -220,10 +221,100 @@ export function cascaderValuePath(
|
|
|
220
221
|
): string[] {
|
|
221
222
|
const row = asPickedRow(item);
|
|
222
223
|
const ownLabel = row ? rowValue(row, keyMapping.label) : undefined;
|
|
223
|
-
const id =
|
|
224
|
-
? rowValue(row, keyMapping.value)
|
|
225
|
-
: rowValue({ id: item }, "id");
|
|
224
|
+
const id = cascaderValueId(item, keyMapping);
|
|
226
225
|
if (!id) return ownLabel ? [ownLabel] : [];
|
|
227
226
|
const labels = rows ? buildCascaderPathLabels(rows, keyMapping, id) : [];
|
|
228
227
|
return labels.length > 0 ? labels : [ownLabel ?? id];
|
|
229
228
|
}
|
|
229
|
+
|
|
230
|
+
/** Reads the rows of the given ids from the cascader's `select` route. */
|
|
231
|
+
export type CascaderRowsReader = (ids: string[]) => Promise<CascaderRow[]>;
|
|
232
|
+
|
|
233
|
+
/** The rows a path is drawn from, and the picked ids none of them answer. */
|
|
234
|
+
export interface CascaderAncestry {
|
|
235
|
+
rows: CascaderRow[];
|
|
236
|
+
notFound: string[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The rows of the picked ids and of every ancestor above them, read with
|
|
241
|
+
* `?ids=` one level at a time: the picked rows, then their parents, then
|
|
242
|
+
* theirs, until a level names no row not read yet. A tree is as many reads as
|
|
243
|
+
* it is deep (a level is one request for all its ids), whatever its size, and
|
|
244
|
+
* a parent cycle ends because a row is never asked twice. A picked id no row
|
|
245
|
+
* answers (deleted, out of reach) is returned as not found.
|
|
246
|
+
*/
|
|
247
|
+
export async function readCascaderAncestry(
|
|
248
|
+
read: CascaderRowsReader,
|
|
249
|
+
keyMapping: CascaderKeyMapping,
|
|
250
|
+
ids: string[],
|
|
251
|
+
): Promise<CascaderAncestry> {
|
|
252
|
+
const rowsByValue = new Map<string, CascaderRow>();
|
|
253
|
+
const asked = new Set<string>();
|
|
254
|
+
let pending = [...new Set(ids)];
|
|
255
|
+
while (pending.length > 0) {
|
|
256
|
+
for (const id of pending) asked.add(id);
|
|
257
|
+
const rows = await read(pending);
|
|
258
|
+
for (const row of rows) {
|
|
259
|
+
const value = rowValue(row, keyMapping.value);
|
|
260
|
+
if (value !== undefined) rowsByValue.set(value, row);
|
|
261
|
+
}
|
|
262
|
+
pending = [
|
|
263
|
+
...new Set(
|
|
264
|
+
rows
|
|
265
|
+
.map((row) => rowValue(row, keyMapping.parent))
|
|
266
|
+
.filter(
|
|
267
|
+
(parent): parent is string =>
|
|
268
|
+
parent !== undefined && !asked.has(parent),
|
|
269
|
+
),
|
|
270
|
+
),
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
rows: [...rowsByValue.values()],
|
|
275
|
+
notFound: ids.filter((id) => !rowsByValue.has(id)),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The id of a picked value: an id, or a row carrying its id. */
|
|
280
|
+
export function cascaderValueId(
|
|
281
|
+
item: unknown,
|
|
282
|
+
keyMapping: CascaderKeyMapping,
|
|
283
|
+
): string | undefined {
|
|
284
|
+
const row = asPickedRow(item);
|
|
285
|
+
return row ? rowValue(row, keyMapping.value) : rowValue({ id: item }, "id");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** What a cell already holds of its path, and the ids it still has to read. */
|
|
289
|
+
export interface CascaderValueReads {
|
|
290
|
+
/** The rows the server joined: each one labels itself. */
|
|
291
|
+
joinedRows: CascaderRow[];
|
|
292
|
+
/** The picked ids sent plain, and the parents of the joined rows. */
|
|
293
|
+
idsToRead: string[];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Splits a cell's picked values into the rows the list joined and the ids it
|
|
298
|
+
* still reads: a plain id is read, a joined row only has its parent read
|
|
299
|
+
* (none for a root), so a list whose route joins the relation reads nothing
|
|
300
|
+
* for a pick at the top of the tree.
|
|
301
|
+
*/
|
|
302
|
+
export function cascaderValueReads(
|
|
303
|
+
values: unknown[],
|
|
304
|
+
keyMapping: CascaderKeyMapping,
|
|
305
|
+
): CascaderValueReads {
|
|
306
|
+
const joinedRows = values
|
|
307
|
+
.map(asPickedRow)
|
|
308
|
+
.filter(
|
|
309
|
+
(row): row is CascaderRow =>
|
|
310
|
+
!!row && rowValue(row, keyMapping.value) !== undefined,
|
|
311
|
+
);
|
|
312
|
+
const plainIds = values
|
|
313
|
+
.filter((item) => !asPickedRow(item))
|
|
314
|
+
.map((item) => cascaderValueId(item, keyMapping))
|
|
315
|
+
.filter((id): id is string => id !== undefined);
|
|
316
|
+
const parents = joinedRows
|
|
317
|
+
.map((row) => rowValue(row, keyMapping.parent))
|
|
318
|
+
.filter((id): id is string => id !== undefined);
|
|
319
|
+
return { joinedRows, idsToRead: [...new Set([...plainIds, ...parents])] };
|
|
320
|
+
}
|
|
@@ -290,12 +290,15 @@
|
|
|
290
290
|
"forbidden_description": "Your role doesn't let you view this entry, so the form can't open on its current values. Ask an administrator for access.",
|
|
291
291
|
"missing_title": "No entry selected",
|
|
292
292
|
"missing_description": "This form shows one entry, and the address names none. Open it from an entry to see its values.",
|
|
293
|
+
"not_found_title": "Not found",
|
|
294
|
+
"not_found_description": "This entry does not exist, or no longer does: it may have been deleted.",
|
|
293
295
|
"retry": "Try again"
|
|
294
296
|
},
|
|
295
297
|
"select_date": "Select date",
|
|
296
298
|
"relation": {
|
|
297
299
|
"selected_count": "{count} selected",
|
|
298
|
-
"add_new": "Add new entry"
|
|
300
|
+
"add_new": "Add new entry",
|
|
301
|
+
"not_found": "Not found"
|
|
299
302
|
},
|
|
300
303
|
"cascader": {
|
|
301
304
|
"placeholder": "Select an item",
|
|
@@ -291,12 +291,15 @@
|
|
|
291
291
|
"forbidden_description": "Votre rôle ne vous permet pas de voir cet élément, le formulaire ne peut donc pas s'ouvrir sur ses valeurs actuelles. Demandez l'accès à un administrateur.",
|
|
292
292
|
"missing_title": "Aucun élément sélectionné",
|
|
293
293
|
"missing_description": "Ce formulaire affiche un élément, et l'adresse n'en désigne aucun. Ouvrez-le depuis un élément pour voir ses valeurs.",
|
|
294
|
+
"not_found_title": "Introuvable",
|
|
295
|
+
"not_found_description": "Cet élément n'existe pas, ou plus : il a peut-être été supprimé.",
|
|
294
296
|
"retry": "Réessayer"
|
|
295
297
|
},
|
|
296
298
|
"select_date": "Sélectionner une date",
|
|
297
299
|
"relation": {
|
|
298
300
|
"selected_count": "{count} sélectionné(s)",
|
|
299
|
-
"add_new": "Ajouter une nouvelle entrée"
|
|
301
|
+
"add_new": "Ajouter une nouvelle entrée",
|
|
302
|
+
"not_found": "Introuvable"
|
|
300
303
|
},
|
|
301
304
|
"cascader": {
|
|
302
305
|
"placeholder": "Sélectionner un élément",
|
|
@@ -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.0",
|
|
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.0 <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,7 +77,7 @@
|
|
|
77
77
|
"zod": "~3.24.2"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@antelopejs/core": ">=1.
|
|
80
|
+
"@antelopejs/core": ">=1.17.0 <2",
|
|
81
81
|
"@antelopejs/tooling-configs": ">=0.0.9 <1.0.0",
|
|
82
82
|
"@types/archiver": "^6.0.4",
|
|
83
83
|
"@types/chai": "^4.3.20",
|
|
@@ -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": {
|
|
@@ -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
|
-
}
|