@riducms/admin 0.1.5 → 0.2.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/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,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import LiveValidationFeedback from "@admin/fields/live-validation-feedback.svelte";
|
|
2
3
|
import type { FieldReferenceBrowserProps } from "@riducms/plugin";
|
|
4
|
+
import { fieldControlARIA } from "@riducms/ui";
|
|
3
5
|
import { tick } from "svelte";
|
|
4
6
|
import ArrowLeftIcon from "~icons/lucide/arrow-left";
|
|
5
7
|
import ChevronLeftIcon from "~icons/lucide/chevron-left";
|
|
@@ -62,8 +64,9 @@
|
|
|
62
64
|
const lowerSingularLabel = $derived(singularLabel.toLocaleLowerCase(runtime.i18n.language));
|
|
63
65
|
const lowerPluralLabel = $derived(pluralLabel.toLocaleLowerCase(runtime.i18n.language));
|
|
64
66
|
let contentElement = $state<HTMLElement | null>(null);
|
|
67
|
+
let searchElement = $state<HTMLInputElement | null>(null);
|
|
65
68
|
// The field mounts a fresh browser for each drawer session, so workflow inputs other than the
|
|
66
|
-
// controlled open state intentionally describe that session rather than later prop replacements.
|
|
69
|
+
// controlled open/editability state intentionally describe that session rather than later prop replacements.
|
|
67
70
|
// svelte-ignore state_referenced_locally
|
|
68
71
|
const controller = new ReferenceBrowserWorkflow({
|
|
69
72
|
runtime,
|
|
@@ -72,7 +75,9 @@
|
|
|
72
75
|
collection,
|
|
73
76
|
hasMany,
|
|
74
77
|
selectedIDs,
|
|
75
|
-
readOnly
|
|
78
|
+
get readOnly() {
|
|
79
|
+
return readOnly;
|
|
80
|
+
},
|
|
76
81
|
initialDocument,
|
|
77
82
|
initialDocumentID,
|
|
78
83
|
optionFilter,
|
|
@@ -137,6 +142,12 @@
|
|
|
137
142
|
);
|
|
138
143
|
const visibleResultIdentity = $derived(docs.map((document) => document.id).join("\u001f"));
|
|
139
144
|
|
|
145
|
+
function focusBrowserSearch(event: Event) {
|
|
146
|
+
if (screen !== "list" || searchElement === null) return;
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
searchElement.focus();
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
async function handleSave(event: Event) {
|
|
141
152
|
event.preventDefault();
|
|
142
153
|
if (await saveEditor()) return;
|
|
@@ -318,6 +329,7 @@
|
|
|
318
329
|
class="border-control-border bg-background-surface p-0"
|
|
319
330
|
style="width: min(760px, 96vw); max-width: 760px"
|
|
320
331
|
showCloseButton={false}
|
|
332
|
+
onOpenAutoFocus={focusBrowserSearch}
|
|
321
333
|
aria-label={screen === "list"
|
|
322
334
|
? runtime.i18n.t("reference:selectLabel", { label: fieldLabel })
|
|
323
335
|
: editorTitle}
|
|
@@ -379,6 +391,7 @@
|
|
|
379
391
|
{runtime.i18n.t("reference:search", { label: pluralLabel })}
|
|
380
392
|
</label>
|
|
381
393
|
<Input
|
|
394
|
+
bind:ref={searchElement}
|
|
382
395
|
id="{field.id}-relationship-search"
|
|
383
396
|
type="search"
|
|
384
397
|
disabled={committing}
|
|
@@ -611,207 +624,220 @@
|
|
|
611
624
|
</div>
|
|
612
625
|
{:else}
|
|
613
626
|
<form class="mx-auto max-w-[620px] px-6 py-8" onsubmit={handleSave}>
|
|
614
|
-
{
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
627
|
+
<fieldset class="contents" disabled={form.submitting}>
|
|
628
|
+
{#if readOnly}
|
|
629
|
+
<Banner class="mb-6" tone="warning">
|
|
630
|
+
{runtime.i18n.t("reference:readOnlyDescription")}
|
|
631
|
+
</Banner>
|
|
632
|
+
{/if}
|
|
633
|
+
{#if editorError !== undefined}
|
|
634
|
+
<Banner class="mb-6" tone="destructive">{editorError}</Banner>
|
|
635
|
+
{/if}
|
|
636
|
+
{#if !readOnly && form.access !== undefined && !form.access.operations[creating ? "create" : "update"]}
|
|
637
|
+
<Banner class="mb-6" tone="warning">
|
|
638
|
+
{runtime.i18n.t(
|
|
639
|
+
creating ? "reference:cannotCreate" : "reference:cannotUpdate",
|
|
640
|
+
{
|
|
641
|
+
label: lowerSingularLabel,
|
|
642
|
+
}
|
|
643
|
+
)}
|
|
644
|
+
</Banner>
|
|
645
|
+
{/if}
|
|
629
646
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
>
|
|
634
|
-
<div class="grid min-h-48 place-items-center bg-background-layer">
|
|
635
|
-
{#if isImage(editorDocument) && mediaURL(editorDocument) !== undefined}
|
|
636
|
-
<img
|
|
637
|
-
class="max-h-72 w-full object-contain"
|
|
638
|
-
src={mediaURL(editorDocument)}
|
|
639
|
-
alt={typeof editorDocument.alt === "string" ? editorDocument.alt : ""}
|
|
640
|
-
/>
|
|
641
|
-
{:else}
|
|
642
|
-
<FileIcon class="size-9 text-foreground-faint" />
|
|
643
|
-
{/if}
|
|
644
|
-
</div>
|
|
645
|
-
<dl
|
|
646
|
-
class="grid grid-cols-[90px_1fr] gap-x-4 gap-y-2 border-t border-control-border px-4 py-3 text-[11.5px]"
|
|
647
|
+
{#if collection.capabilities.upload && editorDocument !== undefined}
|
|
648
|
+
<div
|
|
649
|
+
class="mb-7 overflow-hidden rounded-[4px] border border-control-border bg-control"
|
|
647
650
|
>
|
|
648
|
-
<
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
651
|
+
<div class="grid min-h-48 place-items-center bg-background-layer">
|
|
652
|
+
{#if isImage(editorDocument) && mediaURL(editorDocument) !== undefined}
|
|
653
|
+
<img
|
|
654
|
+
class="max-h-72 w-full object-contain"
|
|
655
|
+
src={mediaURL(editorDocument)}
|
|
656
|
+
alt={typeof editorDocument.alt === "string" ? editorDocument.alt : ""}
|
|
657
|
+
/>
|
|
658
|
+
{:else}
|
|
659
|
+
<FileIcon class="size-9 text-foreground-faint" />
|
|
660
|
+
{/if}
|
|
661
|
+
</div>
|
|
662
|
+
<dl
|
|
663
|
+
class="grid grid-cols-[90px_1fr] gap-x-4 gap-y-2 border-t border-control-border px-4 py-3 text-[11.5px]"
|
|
664
|
+
>
|
|
665
|
+
<dt class="text-foreground-faint">{runtime.i18n.t("uploads:filename")}</dt>
|
|
666
|
+
<dd class="min-w-0 truncate text-foreground-muted">
|
|
667
|
+
{String(editorDocument.filename ?? "—")}
|
|
668
|
+
</dd>
|
|
669
|
+
<dt class="text-foreground-faint">{runtime.i18n.t("uploads:type")}</dt>
|
|
670
|
+
<dd class="font-mono text-[10.5px] text-foreground-muted">
|
|
671
|
+
{String(editorDocument.mimeType ?? "—")}
|
|
672
|
+
</dd>
|
|
673
|
+
<dt class="text-foreground-faint">{runtime.i18n.t("uploads:size")}</dt>
|
|
674
|
+
<dd class="font-mono text-[10.5px] text-foreground-muted">
|
|
675
|
+
{formatBytes(editorDocument.filesize) || "—"}
|
|
676
|
+
</dd>
|
|
677
|
+
<dt class="text-foreground-faint">
|
|
678
|
+
{runtime.i18n.t("uploads:dimensions")}
|
|
679
|
+
</dt>
|
|
680
|
+
<dd class="font-mono text-[10.5px] text-foreground-muted">
|
|
681
|
+
{typeof editorDocument.width === "number" &&
|
|
682
|
+
typeof editorDocument.height === "number"
|
|
683
|
+
? `${runtime.i18n.formatNumber(editorDocument.width)} × ${runtime.i18n.formatNumber(editorDocument.height)}`
|
|
684
|
+
: "—"}
|
|
685
|
+
</dd>
|
|
686
|
+
</dl>
|
|
687
|
+
</div>
|
|
688
|
+
{/if}
|
|
672
689
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
690
|
+
{#if creating && collection.capabilities.upload}
|
|
691
|
+
<label
|
|
692
|
+
class="mb-7 grid min-h-44 cursor-pointer place-items-center rounded-[4px] border border-dashed border-control-border-hover bg-control px-6 py-8 text-center transition-colors hover:border-primary/45 hover:bg-primary/[0.025]"
|
|
693
|
+
>
|
|
694
|
+
<input
|
|
695
|
+
class="sr-only"
|
|
696
|
+
type="file"
|
|
697
|
+
accept={collection.uploadSettings?.mimeTypes.join(",")}
|
|
698
|
+
required
|
|
699
|
+
disabled={form.submitting || form.access?.operations.create !== true}
|
|
700
|
+
bind:files={controller.selectedFiles}
|
|
701
|
+
/>
|
|
702
|
+
<span>
|
|
703
|
+
<FileUpIcon class="mx-auto size-6 text-foreground-faint" />
|
|
704
|
+
<span class="mt-3 block text-[13.5px] font-medium text-foreground-muted">
|
|
705
|
+
{selectedFile?.name ?? runtime.i18n.t("uploads:chooseFileToUpload")}
|
|
706
|
+
</span>
|
|
707
|
+
<span class="font-mono mt-1.5 block text-[9.5px] text-foreground-faint">
|
|
708
|
+
{runtime.i18n.formatList(collection.uploadSettings?.mimeTypes ?? [], {
|
|
709
|
+
style: "short",
|
|
710
|
+
type: "disjunction",
|
|
711
|
+
})}
|
|
712
|
+
</span>
|
|
695
713
|
</span>
|
|
696
|
-
</
|
|
697
|
-
|
|
698
|
-
{/if}
|
|
714
|
+
</label>
|
|
715
|
+
{/if}
|
|
699
716
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
717
|
+
{#if controller.creatingAuthUser}
|
|
718
|
+
<fieldset
|
|
719
|
+
class="mb-7 grid gap-4 rounded-[4px] border border-control-border bg-control p-4.5"
|
|
720
|
+
>
|
|
721
|
+
<legend class="px-1 text-[13px] font-semibold text-foreground">
|
|
722
|
+
{runtime.i18n.t("reference:credentials")}
|
|
723
|
+
</legend>
|
|
724
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
725
|
+
<label class="grid gap-2" for="ridu-reference-new-user-password">
|
|
726
|
+
<span class="ridu-field-label">{runtime.i18n.t("reference:password")}</span>
|
|
727
|
+
<input
|
|
728
|
+
id="ridu-reference-new-user-password"
|
|
729
|
+
type="password"
|
|
730
|
+
autocomplete="new-password"
|
|
731
|
+
required
|
|
732
|
+
disabled={form.submitting || form.access?.operations.create !== true}
|
|
733
|
+
aria-invalid={controller.credentialIssue !== undefined}
|
|
734
|
+
aria-describedby="ridu-reference-new-user-password-help"
|
|
735
|
+
bind:value={controller.newUserPassword}
|
|
736
|
+
oninput={() => (controller.credentialIssue = undefined)}
|
|
737
|
+
/>
|
|
738
|
+
</label>
|
|
739
|
+
<label class="grid gap-2" for="ridu-reference-new-user-password-confirmation">
|
|
740
|
+
<span class="ridu-field-label">
|
|
741
|
+
{runtime.i18n.t("reference:confirmPassword")}
|
|
742
|
+
</span>
|
|
743
|
+
<input
|
|
744
|
+
id="ridu-reference-new-user-password-confirmation"
|
|
745
|
+
type="password"
|
|
746
|
+
autocomplete="new-password"
|
|
747
|
+
required
|
|
748
|
+
disabled={form.submitting || form.access?.operations.create !== true}
|
|
749
|
+
aria-invalid={controller.credentialIssue !== undefined}
|
|
750
|
+
aria-describedby="ridu-reference-new-user-password-help"
|
|
751
|
+
bind:value={controller.newUserPasswordConfirmation}
|
|
752
|
+
oninput={() => (controller.credentialIssue = undefined)}
|
|
753
|
+
/>
|
|
754
|
+
</label>
|
|
755
|
+
</div>
|
|
756
|
+
<p
|
|
757
|
+
id="ridu-reference-new-user-password-help"
|
|
758
|
+
class={controller.credentialIssue === undefined
|
|
759
|
+
? "ridu-field-help"
|
|
760
|
+
: "ridu-field-error"}
|
|
761
|
+
role={controller.credentialIssue === undefined ? undefined : "alert"}
|
|
762
|
+
>
|
|
763
|
+
{controller.credentialIssue ??
|
|
764
|
+
runtime.i18n.t("reference:passwordMinimum", {
|
|
765
|
+
minimum: runtime.i18n.formatNumber(
|
|
766
|
+
collection.authSettings?.passwordMinLength ?? 8
|
|
767
|
+
),
|
|
768
|
+
})}
|
|
769
|
+
</p>
|
|
770
|
+
</fieldset>
|
|
771
|
+
{/if}
|
|
772
|
+
|
|
773
|
+
<p class="font-mono text-[9.5px] tracking-[0.13em] text-foreground-faint uppercase">
|
|
774
|
+
{creating
|
|
775
|
+
? runtime.i18n.t("reference:newMetadata", { label: singularLabel })
|
|
776
|
+
: `${singularLabel} · ${editorDocument?.id ?? ""}`}
|
|
777
|
+
</p>
|
|
778
|
+
{#if headingField !== undefined}
|
|
779
|
+
<div data-field-path={headingField.path}>
|
|
780
|
+
<label class="sr-only" for={headingField.id}>
|
|
781
|
+
{runtime.i18n.text(
|
|
782
|
+
headingField.admin.label,
|
|
783
|
+
headingField.admin.labelTranslations
|
|
784
|
+
)}
|
|
721
785
|
</label>
|
|
722
|
-
<
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
786
|
+
<FieldMessages
|
|
787
|
+
controlID={headingField.id}
|
|
788
|
+
issues={headingIssues}
|
|
789
|
+
description={headingField.admin.description}
|
|
790
|
+
class="mt-3"
|
|
791
|
+
>
|
|
726
792
|
<input
|
|
727
|
-
id=
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
793
|
+
id={headingField.id}
|
|
794
|
+
class="font-serif w-full border-x-0 border-t-0 border-b border-b-transparent bg-transparent p-0 pb-1 text-[34px] leading-[1.08] text-foreground caret-primary outline-none placeholder:text-foreground-soft aria-invalid:!border-b-destructive/65"
|
|
795
|
+
value={String(form.get(headingField.path) ?? "")}
|
|
796
|
+
placeholder={runtime.i18n.t("reference:untitledLabel", {
|
|
797
|
+
label: lowerSingularLabel,
|
|
798
|
+
})}
|
|
799
|
+
required={headingField.required &&
|
|
800
|
+
(!headingField.dynamicDefault ||
|
|
801
|
+
form.get(headingField.path) !== undefined)}
|
|
802
|
+
readonly={readOnly || !form.canWrite(headingField.path)}
|
|
803
|
+
{...fieldControlARIA(
|
|
804
|
+
headingField.id,
|
|
805
|
+
headingField.admin.description !== undefined,
|
|
806
|
+
headingIssues.length > 0
|
|
807
|
+
)}
|
|
808
|
+
oninput={(event) => form.set(headingField.path, event.currentTarget.value)}
|
|
809
|
+
onblur={() => form.liveValidation.flush(headingField.path)}
|
|
736
810
|
/>
|
|
737
|
-
</
|
|
811
|
+
</FieldMessages>
|
|
812
|
+
<LiveValidationFeedback
|
|
813
|
+
feedback={form.liveValidation.forField(headingField.path)}
|
|
814
|
+
i18n={runtime.i18n}
|
|
815
|
+
path={headingField.path}
|
|
816
|
+
/>
|
|
738
817
|
</div>
|
|
739
|
-
|
|
740
|
-
id="ridu-reference-new-user-password-help"
|
|
741
|
-
class={controller.credentialIssue === undefined
|
|
742
|
-
? "ridu-field-help"
|
|
743
|
-
: "ridu-field-error"}
|
|
744
|
-
role={controller.credentialIssue === undefined ? undefined : "alert"}
|
|
745
|
-
>
|
|
746
|
-
{controller.credentialIssue ??
|
|
747
|
-
runtime.i18n.t("reference:passwordMinimum", {
|
|
748
|
-
minimum: runtime.i18n.formatNumber(
|
|
749
|
-
collection.authSettings?.passwordMinLength ?? 8
|
|
750
|
-
),
|
|
751
|
-
})}
|
|
752
|
-
</p>
|
|
753
|
-
</fieldset>
|
|
754
|
-
{/if}
|
|
818
|
+
{/if}
|
|
755
819
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
<div data-field-path={headingField.path}>
|
|
763
|
-
<label class="sr-only" for={headingField.id}>
|
|
764
|
-
{runtime.i18n.text(
|
|
765
|
-
headingField.admin.label,
|
|
766
|
-
headingField.admin.labelTranslations
|
|
820
|
+
<div class="mt-7">
|
|
821
|
+
<FieldLayout
|
|
822
|
+
fields={editorFields.map((candidate) =>
|
|
823
|
+
readOnly
|
|
824
|
+
? { ...candidate, admin: { ...candidate.admin, readOnly: true } }
|
|
825
|
+
: candidate
|
|
767
826
|
)}
|
|
768
|
-
|
|
769
|
-
<input
|
|
770
|
-
id={headingField.id}
|
|
771
|
-
class="font-serif mt-3 w-full border-x-0 border-t-0 border-b border-b-transparent bg-transparent p-0 pb-1 text-[34px] leading-[1.08] text-foreground caret-primary outline-none placeholder:text-foreground-soft aria-invalid:border-b-destructive/65"
|
|
772
|
-
value={String(form.get(headingField.path) ?? "")}
|
|
773
|
-
placeholder={runtime.i18n.t("reference:untitledLabel", {
|
|
774
|
-
label: lowerSingularLabel,
|
|
775
|
-
})}
|
|
776
|
-
required={headingField.required}
|
|
777
|
-
readonly={readOnly || form.submitting || !form.canWrite(headingField.path)}
|
|
778
|
-
aria-invalid={headingIssues.length > 0}
|
|
779
|
-
aria-describedby={headingIssues.length > 0
|
|
780
|
-
? `${headingField.id}-message`
|
|
781
|
-
: undefined}
|
|
782
|
-
aria-errormessage={headingIssues.length > 0
|
|
783
|
-
? `${headingField.id}-message`
|
|
784
|
-
: undefined}
|
|
785
|
-
oninput={(event) => form.set(headingField.path, event.currentTarget.value)}
|
|
786
|
-
/>
|
|
787
|
-
<FieldMessages
|
|
788
|
-
id="{headingField.id}-message"
|
|
789
|
-
issues={headingIssues}
|
|
790
|
-
class="mt-2"
|
|
827
|
+
{form}
|
|
791
828
|
/>
|
|
792
829
|
</div>
|
|
793
|
-
{/if}
|
|
794
830
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
</
|
|
805
|
-
|
|
806
|
-
{#if !readOnly}
|
|
807
|
-
<div class="mt-8 flex justify-end">
|
|
808
|
-
<Button type="submit" disabled={!canSave}>
|
|
809
|
-
{creating
|
|
810
|
-
? runtime.i18n.t("reference:createLabel", { label: lowerSingularLabel })
|
|
811
|
-
: runtime.i18n.t("reference:saveLabel", { label: lowerSingularLabel })}
|
|
812
|
-
</Button>
|
|
813
|
-
</div>
|
|
814
|
-
{/if}
|
|
831
|
+
{#if !readOnly}
|
|
832
|
+
<div class="mt-8 flex justify-end">
|
|
833
|
+
<Button type="submit" disabled={!canSave}>
|
|
834
|
+
{creating
|
|
835
|
+
? runtime.i18n.t("reference:createLabel", { label: lowerSingularLabel })
|
|
836
|
+
: runtime.i18n.t("reference:saveLabel", { label: lowerSingularLabel })}
|
|
837
|
+
</Button>
|
|
838
|
+
</div>
|
|
839
|
+
{/if}
|
|
840
|
+
</fieldset>
|
|
815
841
|
</form>
|
|
816
842
|
{/if}
|
|
817
843
|
</div>
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
function selectedLabel(field: SchemaField, value: string) {
|
|
45
|
-
const
|
|
46
|
-
return
|
|
45
|
+
const option = field.select?.options.find((candidate) => candidate.value === value);
|
|
46
|
+
return option === undefined
|
|
47
47
|
? i18n.t("uploads:chooseValue")
|
|
48
|
-
: i18n.text(
|
|
48
|
+
: i18n.text(option.label, option.labelTranslations);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function controlLabel(field: SchemaField) {
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
</SelectTrigger>
|
|
84
84
|
<SelectContent>
|
|
85
85
|
<SelectItem value="" label={i18n.t("uploads:chooseValue")} />
|
|
86
|
-
{#each entry.field.select?.
|
|
86
|
+
{#each entry.field.select?.options ?? [] as option (option.value)}
|
|
87
87
|
<SelectItem
|
|
88
|
-
value={
|
|
89
|
-
label={i18n.text(
|
|
88
|
+
value={option.value}
|
|
89
|
+
label={i18n.text(option.label, option.labelTranslations)}
|
|
90
90
|
/>
|
|
91
91
|
{/each}
|
|
92
92
|
</SelectContent>
|
|
@@ -480,7 +480,7 @@
|
|
|
480
480
|
{runtime.i18n.t("versions:publishDateTime")}
|
|
481
481
|
<DateValueControl
|
|
482
482
|
id="scheduled-publish-at"
|
|
483
|
-
appearance="
|
|
483
|
+
appearance="date-time"
|
|
484
484
|
value={scheduleAt}
|
|
485
485
|
label={runtime.i18n.t("versions:publishDateTime")}
|
|
486
486
|
onValueChange={(next) => (scheduleAt = next)}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getAdminI18n, type EmbeddedSchemaDraftEditorProps } from "@riducms/plugin";
|
|
3
|
+
import type { HostedSchemaDraft } from "@admin/core/forms/embedded-schema-draft.svelte";
|
|
4
|
+
import { Sheet, SheetContent, SheetTitle, SheetDescription } from "@admin/components/ui/sheet";
|
|
5
|
+
import { Button } from "@admin/components/ui/button";
|
|
6
|
+
import { focusFieldIssue } from "@admin/core/forms/field-issue-focus";
|
|
7
|
+
import FieldLayout from "@admin/fields/field-layout.svelte";
|
|
8
|
+
import { onDestroy } from "svelte";
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
session,
|
|
12
|
+
options,
|
|
13
|
+
}: { session: HostedSchemaDraft; options: EmbeddedSchemaDraftEditorProps } = $props();
|
|
14
|
+
// A mounted drawer owns exactly one detached edit and releases its parent blocker.
|
|
15
|
+
// svelte-ignore state_referenced_locally
|
|
16
|
+
const draft = session.draft;
|
|
17
|
+
const i18n = getAdminI18n();
|
|
18
|
+
onDestroy(() => draft.discard());
|
|
19
|
+
|
|
20
|
+
function cancel() {
|
|
21
|
+
draft.discard();
|
|
22
|
+
options.onCancel();
|
|
23
|
+
}
|
|
24
|
+
async function apply() {
|
|
25
|
+
if (!draft.validate()) {
|
|
26
|
+
const issue = draft.issues[0];
|
|
27
|
+
if (issue !== undefined) await focusFieldIssue(issue.path);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
options.onApply(draft.payload());
|
|
31
|
+
draft.discard();
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<Sheet
|
|
36
|
+
open
|
|
37
|
+
onOpenChange={(open) => {
|
|
38
|
+
if (!open) cancel();
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<SheetContent
|
|
42
|
+
class="w-full sm:max-w-2xl"
|
|
43
|
+
showCloseButton={false}
|
|
44
|
+
onOpenAutoFocus={async (event) => {
|
|
45
|
+
const issue = draft.issues[0];
|
|
46
|
+
if (issue !== undefined) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
await focusFieldIssue(issue.path);
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<div class="border-b border-border px-6 py-4">
|
|
53
|
+
<SheetTitle>{options.title ?? i18n.t("fields:embeddedEditTitle")}</SheetTitle>
|
|
54
|
+
<SheetDescription>{i18n.t("fields:embeddedEditDescription")}</SheetDescription>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-6 py-5" data-schema-draft={draft.id}>
|
|
57
|
+
{#if draft.stale}
|
|
58
|
+
<p role="alert" class="mb-4 text-destructive">
|
|
59
|
+
{i18n.t("errors:staleEmbeddedEdit")}
|
|
60
|
+
</p>
|
|
61
|
+
{/if}
|
|
62
|
+
{#if draft.issues.length > 0}
|
|
63
|
+
<div role="alert" class="mb-4 text-destructive">
|
|
64
|
+
{#each draft.issues as issue}<p>{issue.message}</p>{/each}
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
{#if !draft.stale}<FieldLayout fields={session.fields} form={session.form} />{/if}
|
|
68
|
+
</div>
|
|
69
|
+
<div class="flex justify-end gap-2 border-t border-border px-6 py-4">
|
|
70
|
+
<Button variant="outline" onclick={cancel}>{i18n.t("general:cancel")}</Button>
|
|
71
|
+
<Button onclick={apply} disabled={draft.stale}>{i18n.t("general:apply")}</Button>
|
|
72
|
+
</div>
|
|
73
|
+
</SheetContent>
|
|
74
|
+
</Sheet>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EmbeddedSchemaFormScope } from "@riducms/plugin";
|
|
3
|
+
import type { SchemaField, ValidationIssue } from "@riducms/protocol";
|
|
4
|
+
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
5
|
+
import type { EmbeddedOccurrence } from "@admin/core/forms/embedded-fields";
|
|
6
|
+
import { fieldAccessPath, scopeRepeatedRowField } from "@admin/fields/nested/scoped-field";
|
|
7
|
+
import FieldLayout from "@admin/fields/field-layout.svelte";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
field,
|
|
11
|
+
form,
|
|
12
|
+
scope,
|
|
13
|
+
occurrence,
|
|
14
|
+
issues,
|
|
15
|
+
}: {
|
|
16
|
+
field: SchemaField;
|
|
17
|
+
form: FormController;
|
|
18
|
+
scope: EmbeddedSchemaFormScope;
|
|
19
|
+
occurrence: EmbeddedOccurrence | undefined;
|
|
20
|
+
issues: readonly ValidationIssue[];
|
|
21
|
+
} = $props();
|
|
22
|
+
const children = $derived(
|
|
23
|
+
occurrence?.block.fields.map((child) =>
|
|
24
|
+
scopeRepeatedRowField(
|
|
25
|
+
child,
|
|
26
|
+
occurrence.path,
|
|
27
|
+
`${field.id}-${scope.treeKey}-${scope.identity}`,
|
|
28
|
+
scope.readOnly === true ||
|
|
29
|
+
field.admin.readOnly === true ||
|
|
30
|
+
!form.canWrite(field.path, fieldAccessPath(field))
|
|
31
|
+
)
|
|
32
|
+
) ?? []
|
|
33
|
+
);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
{#if issues.length > 0}
|
|
37
|
+
<p role="alert">{issues[0]?.message}</p>
|
|
38
|
+
{:else if occurrence === undefined}
|
|
39
|
+
<p role="alert">
|
|
40
|
+
The embedded field occurrence is no longer available. Close this editor and reopen it.
|
|
41
|
+
</p>
|
|
42
|
+
{:else}
|
|
43
|
+
<FieldLayout fields={children} {form} />
|
|
44
|
+
{/if}
|