@riducms/admin 0.1.5 → 0.2.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/package.json +11 -9
- package/src/app.css +63 -4
- package/src/app.svelte +12 -14
- package/src/components/ui/date-value-control/date-value-control-loader.svelte +15 -4
- package/src/components/ui/date-value-control/date-value-control.svelte +17 -17
- package/src/components/ui/select/select-trigger.svelte +1 -1
- package/src/core/forms/embedded-fields.ts +132 -0
- package/src/core/forms/embedded-schema-draft.svelte.ts +335 -0
- package/src/core/forms/field-editor-authoring.ts +47 -0
- package/src/core/forms/field-editor-binding.ts +228 -0
- package/src/core/forms/field-issue-focus.ts +42 -0
- package/src/core/forms/field-occurrence.ts +122 -0
- package/src/core/forms/form-controller.svelte.ts +475 -29
- package/src/core/forms/form-issue-correlation.ts +159 -0
- package/src/core/forms/form-schema.ts +211 -17
- package/src/core/forms/form-validation.ts +183 -17
- package/src/core/forms/live-validation.svelte.ts +317 -0
- package/src/core/forms/plugin-field-authoring.ts +160 -0
- package/src/core/forms/plugin-field-binding.ts +258 -0
- package/src/core/forms/plugin-field-write.ts +57 -0
- package/src/core/i18n/admin-i18n.svelte.ts +8 -1
- package/src/core/i18n/localized-schema.ts +33 -10
- package/src/core/plugins/field-registry.ts +29 -49
- package/src/core/plugins/row-label-registry.ts +25 -5
- package/src/core/runtime/admin-runtime.svelte.ts +25 -13
- package/src/features/auth/account-route.svelte +10 -5
- package/src/features/auth/account-security-route.svelte +1 -1
- package/src/features/collections/collection-list-bulk-editor.svelte +6 -6
- package/src/features/collections/collection-list-controller.svelte.ts +5 -5
- package/src/features/collections/collection-list-filter-popover.svelte +6 -6
- package/src/features/collections/collection-list-route.svelte +13 -14
- package/src/features/collections/list-workspace.ts +22 -8
- package/src/features/documents/document-controller.svelte.ts +8 -2
- package/src/features/documents/document-route.svelte +36 -43
- package/src/features/reference-browser/reference-browser-workflow.svelte.ts +30 -15
- package/src/features/reference-browser/reference-browser.svelte +215 -189
- package/src/features/uploads/upload-metadata-fields.svelte +6 -6
- package/src/features/versions/version-history-route.svelte +1 -1
- package/src/fields/embedded-schema-draft-editor.svelte +74 -0
- package/src/fields/embedded-schema-fields.svelte +44 -0
- package/src/fields/field-clipboard.ts +76 -16
- package/src/fields/field-collapsible.svelte +49 -0
- package/src/fields/field-layout.svelte +2 -18
- package/src/fields/field-messages.svelte +13 -18
- package/src/fields/field-renderer.svelte +53 -14
- package/src/fields/join/join-field.svelte +8 -1
- package/src/fields/live-validation-feedback.svelte +21 -0
- package/src/fields/local-field-editor.svelte +52 -0
- package/src/fields/nested/nested-field.svelte +159 -91
- package/src/fields/nested/scoped-field.ts +7 -9
- package/src/fields/plugin-field.svelte +156 -0
- package/src/fields/point/point-field.svelte +12 -2
- package/src/fields/primitive-list/primitive-list-field.svelte +170 -0
- package/src/fields/primitive-list/primitive-number-input.ts +6 -0
- package/src/fields/radio/radio-field.svelte +13 -9
- package/src/fields/relationship/relationship-field.svelte +36 -12
- package/src/fields/relationship/relationship-query.ts +4 -2
- package/src/fields/relationship/relationship-quick-picker.svelte +38 -10
- package/src/fields/scalar/date-control-value.ts +5 -5
- package/src/fields/scalar/date-value.ts +5 -5
- package/src/fields/scalar/scalar-field.svelte +47 -41
- package/src/fields/select/select-field.svelte +24 -24
- package/src/fields/select/select-value.ts +3 -3
- package/src/fields/text/slug-field.svelte +24 -8
- package/src/fields/text/text-field.svelte +8 -6
- package/src/index.ts +4 -12
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
3
|
+
|
|
2
4
|
import type { SchemaField } from "@riducms/protocol";
|
|
3
5
|
|
|
4
6
|
function fieldUsesLocalization(field: SchemaField): boolean {
|
|
5
7
|
if (field.localized === true) return true;
|
|
6
8
|
if (field.nested?.fields.some(fieldUsesLocalization)) return true;
|
|
7
|
-
return
|
|
9
|
+
return (
|
|
10
|
+
resolveBlockTypes(field.blocks).some((block) => block.fields.some(fieldUsesLocalization)) ===
|
|
11
|
+
true
|
|
12
|
+
);
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
function withContentLocale(path: string, locale: string | undefined) {
|
|
@@ -14,6 +19,7 @@
|
|
|
14
19
|
</script>
|
|
15
20
|
|
|
16
21
|
<script lang="ts">
|
|
22
|
+
import { focusFieldIssue } from "@admin/core/forms/field-issue-focus";
|
|
17
23
|
import type { AdminDocumentExtensionHost } from "@riducms/plugin";
|
|
18
24
|
import { Link, useBlocker, useLocation, useNavigate, useParams } from "@hvniel/svelte-router";
|
|
19
25
|
import { tick, untrack } from "svelte";
|
|
@@ -242,6 +248,18 @@
|
|
|
242
248
|
let previousLivePreviewRouteKey: string | undefined;
|
|
243
249
|
let dismissedLock = $state<string>();
|
|
244
250
|
const livePreview = $derived(collection?.admin.livePreview);
|
|
251
|
+
$effect(() => {
|
|
252
|
+
const state = location.state as { openLivePreview?: boolean } | null;
|
|
253
|
+
if (routeDocumentView !== "edit" || state?.openLivePreview !== true) return;
|
|
254
|
+
livePreviewOpen = true;
|
|
255
|
+
// API and edit views have separate route instances. Consume the transient
|
|
256
|
+
// intent on arrival so Back or reload cannot reopen a dismissed preview.
|
|
257
|
+
const { openLivePreview: _, ...remainingState } = state;
|
|
258
|
+
navigate(`${location.pathname}${location.search}${location.hash}`, {
|
|
259
|
+
replace: true,
|
|
260
|
+
state: remainingState,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
245
263
|
const creatingAuthUser = $derived(
|
|
246
264
|
creating && !globalResource && collection?.capabilities.auth === true
|
|
247
265
|
);
|
|
@@ -321,10 +339,13 @@
|
|
|
321
339
|
}
|
|
322
340
|
|
|
323
341
|
function toggleLivePreview() {
|
|
342
|
+
if (!livePreviewOpen && routeDocumentView === "api") {
|
|
343
|
+
navigate(editPath, { replace: true, state: { openLivePreview: true } });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
324
346
|
livePreviewOpen = !livePreviewOpen;
|
|
325
347
|
if (livePreviewOpen) {
|
|
326
348
|
controller.documentView = "edit";
|
|
327
|
-
navigate(editPath, { replace: true });
|
|
328
349
|
}
|
|
329
350
|
}
|
|
330
351
|
|
|
@@ -413,7 +434,7 @@
|
|
|
413
434
|
const issuePath = controller.revealFirstIssue();
|
|
414
435
|
if (issuePath === undefined) return;
|
|
415
436
|
await tick();
|
|
416
|
-
|
|
437
|
+
await focusFieldIssue(issuePath);
|
|
417
438
|
}
|
|
418
439
|
|
|
419
440
|
async function handleSave(event: Event) {
|
|
@@ -464,24 +485,6 @@
|
|
|
464
485
|
return runtime.i18n.t("documents:passwordsDoNotMatch");
|
|
465
486
|
return undefined;
|
|
466
487
|
}
|
|
467
|
-
|
|
468
|
-
function focusIssue(path: string) {
|
|
469
|
-
const segments = path.split(".");
|
|
470
|
-
while (segments.length > 0) {
|
|
471
|
-
const candidate = segments.join(".");
|
|
472
|
-
const container = document.querySelector<HTMLElement>(
|
|
473
|
-
`[data-field-path="${CSS.escape(candidate)}"]`
|
|
474
|
-
);
|
|
475
|
-
if (container !== null) {
|
|
476
|
-
container.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
477
|
-
container
|
|
478
|
-
.querySelector<HTMLElement>("input, textarea, button, [tabindex]:not([tabindex='-1'])")
|
|
479
|
-
?.focus({ preventScroll: true });
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
segments.pop();
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
488
|
</script>
|
|
486
489
|
|
|
487
490
|
<section
|
|
@@ -634,14 +637,10 @@
|
|
|
634
637
|
aria-busy={form.submitting}
|
|
635
638
|
onclick={handleSave}
|
|
636
639
|
>
|
|
637
|
-
{
|
|
638
|
-
? runtime.i18n.t("documents:saving")
|
|
639
|
-
: runtime.i18n.t("documents:saveDraft")}
|
|
640
|
+
{runtime.i18n.t("documents:saveDraft")}
|
|
640
641
|
</Button>
|
|
641
642
|
<Button size="sm" disabled={!canSave} aria-busy={form.submitting} onclick={handlePublish}>
|
|
642
|
-
{
|
|
643
|
-
? runtime.i18n.t("documents:publishing")
|
|
644
|
-
: runtime.i18n.t("documents:publish")}
|
|
643
|
+
{runtime.i18n.t("documents:publish")}
|
|
645
644
|
</Button>
|
|
646
645
|
{:else if creating && versionedCollection && !draftsCollection}
|
|
647
646
|
<Button
|
|
@@ -650,9 +649,7 @@
|
|
|
650
649
|
aria-busy={form.submitting}
|
|
651
650
|
onclick={handlePublish}
|
|
652
651
|
>
|
|
653
|
-
{
|
|
654
|
-
? runtime.i18n.t("documents:publishing")
|
|
655
|
-
: runtime.i18n.t("documents:publish")}
|
|
652
|
+
{runtime.i18n.t("documents:publish")}
|
|
656
653
|
</Button>
|
|
657
654
|
{:else}
|
|
658
655
|
<Button
|
|
@@ -662,19 +659,13 @@
|
|
|
662
659
|
aria-busy={form.submitting}
|
|
663
660
|
onclick={handleSave}
|
|
664
661
|
>
|
|
665
|
-
{
|
|
666
|
-
? runtime.i18n.t(
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
? runtime.i18n.t("documents:saveDraft")
|
|
673
|
-
: versionedCollection
|
|
674
|
-
? currentStatus === "published"
|
|
675
|
-
? runtime.i18n.t("documents:publishChanges")
|
|
676
|
-
: runtime.i18n.t("documents:saveDraft")
|
|
677
|
-
: runtime.i18n.t("documents:save")}
|
|
662
|
+
{creating && draftsCollection
|
|
663
|
+
? runtime.i18n.t("documents:saveDraft")
|
|
664
|
+
: versionedCollection
|
|
665
|
+
? currentStatus === "published"
|
|
666
|
+
? runtime.i18n.t("documents:publishChanges")
|
|
667
|
+
: runtime.i18n.t("documents:saveDraft")
|
|
668
|
+
: runtime.i18n.t("documents:save")}
|
|
678
669
|
</Button>
|
|
679
670
|
{/if}
|
|
680
671
|
{#if !creating && versionedCollection && currentStatus === "draft" && canPublish}
|
|
@@ -812,6 +803,8 @@
|
|
|
812
803
|
<Skeleton class="mt-5 h-10.5" />
|
|
813
804
|
<Skeleton class="h-28" />
|
|
814
805
|
</div>
|
|
806
|
+
{:else if !creating && currentDocument === undefined}
|
|
807
|
+
<!-- A failed read has no editable document; the recovery/error banner remains visible. -->
|
|
815
808
|
{:else if documentView === "api"}
|
|
816
809
|
<DocumentAPIView
|
|
817
810
|
resourceSlug={slug}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
2
|
+
import { connectDocumentLiveValidation } from "@admin/core/forms/live-validation.svelte";
|
|
1
3
|
import type { SchemaCollection, SchemaField } from "@riducms/protocol";
|
|
2
4
|
import { RiduError } from "@riducms/sdk";
|
|
3
5
|
|
|
@@ -90,6 +92,7 @@ export class ReferenceBrowserWorkflow {
|
|
|
90
92
|
|
|
91
93
|
constructor(readonly options: ReferenceBrowserWorkflowOptions) {
|
|
92
94
|
this.form = new FormController({}, options.runtime.i18n);
|
|
95
|
+
connectDocumentLiveValidation(this.form, options.runtime.client);
|
|
93
96
|
this.#lookup = new RelationshipLookupController(options.runtime.client);
|
|
94
97
|
this.form.setLocalization(options.locale);
|
|
95
98
|
this.pageSize = options.collection.capabilities.upload ? 8 : 10;
|
|
@@ -134,6 +137,7 @@ export class ReferenceBrowserWorkflow {
|
|
|
134
137
|
});
|
|
135
138
|
|
|
136
139
|
$effect(() => () => {
|
|
140
|
+
this.form.disposeBindings();
|
|
137
141
|
this.#editorLoad.abort();
|
|
138
142
|
this.#editorAccess?.abort();
|
|
139
143
|
this.#saveRequest?.abort();
|
|
@@ -225,8 +229,9 @@ export class ReferenceBrowserWorkflow {
|
|
|
225
229
|
|
|
226
230
|
get canCommitSelection() {
|
|
227
231
|
return (
|
|
228
|
-
this.
|
|
229
|
-
this.workingSelection.
|
|
232
|
+
!this.options.readOnly &&
|
|
233
|
+
(this.workingSelection.length !== this.options.selectedIDs.length ||
|
|
234
|
+
this.workingSelection.some((id) => !this.options.selectedIDs.includes(id)))
|
|
230
235
|
);
|
|
231
236
|
}
|
|
232
237
|
|
|
@@ -271,11 +276,11 @@ export class ReferenceBrowserWorkflow {
|
|
|
271
276
|
};
|
|
272
277
|
|
|
273
278
|
commitSelection = async () => {
|
|
274
|
-
if (this.committing) return;
|
|
279
|
+
if (this.committing || this.options.readOnly) return;
|
|
275
280
|
this.committing = true;
|
|
276
281
|
try {
|
|
277
282
|
const committed = await this.options.onCommit([...this.workingSelection]);
|
|
278
|
-
if (committed !== false) this.#closeBrowser();
|
|
283
|
+
if (committed !== false && !this.options.readOnly) this.#closeBrowser();
|
|
279
284
|
} catch (cause) {
|
|
280
285
|
this.options.notifications.error({
|
|
281
286
|
title: this.options.runtime.i18n.t("reference:updateNotConfirmed"),
|
|
@@ -304,11 +309,12 @@ export class ReferenceBrowserWorkflow {
|
|
|
304
309
|
openNewDocument = () => {
|
|
305
310
|
if (this.form.submitting || !this.canCreateDocument) return;
|
|
306
311
|
this.editorDocument = undefined;
|
|
312
|
+
this.form.setResource({ collection: this.options.collection.slug });
|
|
307
313
|
const values = {
|
|
308
314
|
...initialFormValues(this.options.collection.fields),
|
|
309
315
|
...this.options.defaultValues,
|
|
310
316
|
};
|
|
311
|
-
this.form.reset(values);
|
|
317
|
+
this.form.reset(values, this.options.collection.fields);
|
|
312
318
|
this.form.setAccess(undefined, "create");
|
|
313
319
|
this.selectedFiles = undefined;
|
|
314
320
|
this.newUserPassword = "";
|
|
@@ -338,6 +344,7 @@ export class ReferenceBrowserWorkflow {
|
|
|
338
344
|
this.credentialIssue = this.#validateNewUserPassword();
|
|
339
345
|
if (this.credentialIssue !== undefined) return false;
|
|
340
346
|
const saved = await this.form.submit(this.validationFields, this.creating, async (values) => {
|
|
347
|
+
if (this.options.readOnly) throw new Error("This field is read-only.");
|
|
341
348
|
if (this.editorDocument !== undefined) {
|
|
342
349
|
return this.options.runtime.client.update(
|
|
343
350
|
this.options.collection.slug,
|
|
@@ -376,7 +383,7 @@ export class ReferenceBrowserWorkflow {
|
|
|
376
383
|
locale: this.options.locale,
|
|
377
384
|
});
|
|
378
385
|
});
|
|
379
|
-
if (request.signal.aborted) return false;
|
|
386
|
+
if (request.signal.aborted || this.options.readOnly) return false;
|
|
380
387
|
const wasCreating = this.editorDocument === undefined;
|
|
381
388
|
this.editorDocument = saved;
|
|
382
389
|
this.#lookup.remember(saved);
|
|
@@ -384,7 +391,10 @@ export class ReferenceBrowserWorkflow {
|
|
|
384
391
|
? [...new Set([...this.workingSelection, saved.id])]
|
|
385
392
|
: [saved.id];
|
|
386
393
|
this.options.runtime.documentsChanged();
|
|
387
|
-
this.form.reset(
|
|
394
|
+
this.form.reset(
|
|
395
|
+
documentFormValues(this.options.collection.fields, saved),
|
|
396
|
+
this.options.collection.fields
|
|
397
|
+
);
|
|
388
398
|
this.form.setLocalization(this.options.locale, saved._localization?.sources);
|
|
389
399
|
this.options.notifications.success({
|
|
390
400
|
title: wasCreating
|
|
@@ -402,7 +412,7 @@ export class ReferenceBrowserWorkflow {
|
|
|
402
412
|
this.credentialIssue = undefined;
|
|
403
413
|
return true;
|
|
404
414
|
} catch (cause) {
|
|
405
|
-
if (request.signal.aborted) return false;
|
|
415
|
+
if (request.signal.aborted || this.options.readOnly) return false;
|
|
406
416
|
if (
|
|
407
417
|
creatingUpload &&
|
|
408
418
|
!(cause instanceof FormValidationError) &&
|
|
@@ -550,8 +560,12 @@ export class ReferenceBrowserWorkflow {
|
|
|
550
560
|
}
|
|
551
561
|
|
|
552
562
|
#openEditor(document: AdminDocument) {
|
|
563
|
+
this.form.setResource({ collection: this.options.collection.slug, id: document.id });
|
|
553
564
|
this.editorDocument = document;
|
|
554
|
-
this.form.reset(
|
|
565
|
+
this.form.reset(
|
|
566
|
+
documentFormValues(this.options.collection.fields, document),
|
|
567
|
+
this.options.collection.fields
|
|
568
|
+
);
|
|
555
569
|
this.form.setLocalization(this.options.locale, document._localization?.sources);
|
|
556
570
|
this.form.setAccess(undefined, "update");
|
|
557
571
|
this.selectedFiles = undefined;
|
|
@@ -612,6 +626,7 @@ export class ReferenceBrowserWorkflow {
|
|
|
612
626
|
}
|
|
613
627
|
|
|
614
628
|
#closeBrowser() {
|
|
629
|
+
this.form.disposeBindings();
|
|
615
630
|
this.options.setOpen(false);
|
|
616
631
|
this.options.onClose();
|
|
617
632
|
}
|
|
@@ -645,11 +660,11 @@ function createCollectionFilters(collection: SchemaCollection, i18n: AdminRuntim
|
|
|
645
660
|
const select =
|
|
646
661
|
collection.fields.find((field) => field.name === "status" && field.select !== undefined) ??
|
|
647
662
|
collection.fields.find((field) => field.select !== undefined);
|
|
648
|
-
for (const
|
|
663
|
+
for (const option of select?.select?.options ?? []) {
|
|
649
664
|
result.push({
|
|
650
|
-
key:
|
|
651
|
-
label: i18n.text(
|
|
652
|
-
filter: { field: select?.name ?? "", operator: "equals", value:
|
|
665
|
+
key: option.value,
|
|
666
|
+
label: i18n.text(option.label, option.labelTranslations),
|
|
667
|
+
filter: { field: select?.name ?? "", operator: "equals", value: option.value },
|
|
653
668
|
});
|
|
654
669
|
}
|
|
655
670
|
return result;
|
|
@@ -675,9 +690,9 @@ function scopeField(field: SchemaField, prefix: string): SchemaField {
|
|
|
675
690
|
? {}
|
|
676
691
|
: {
|
|
677
692
|
blocks: {
|
|
678
|
-
types: field.blocks.
|
|
693
|
+
types: resolveBlockTypes(field.blocks).map((block) => ({
|
|
679
694
|
...block,
|
|
680
|
-
fields: block.fields.map((child) => scopeField(child, `${prefix}-${block.
|
|
695
|
+
fields: block.fields.map((child) => scopeField(child, `${prefix}-${block.slug}`)),
|
|
681
696
|
})),
|
|
682
697
|
},
|
|
683
698
|
}),
|