@riducms/admin 0.1.4 → 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,19 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SchemaDateFormat } from "@riducms/protocol";
|
|
2
2
|
import type { AdminI18n } from "@riducms/plugin";
|
|
3
3
|
|
|
4
4
|
export function formatDateDisplay(
|
|
5
5
|
value: unknown,
|
|
6
|
-
appearance:
|
|
6
|
+
appearance: SchemaDateFormat | undefined,
|
|
7
7
|
i18n: AdminI18n
|
|
8
8
|
) {
|
|
9
9
|
const encoded = String(value ?? "");
|
|
10
10
|
if (encoded === "") return "—";
|
|
11
|
-
if (appearance === "
|
|
11
|
+
if (appearance === "time") return encoded.slice(0, 5);
|
|
12
12
|
const date = new Date(
|
|
13
|
-
appearance === "
|
|
13
|
+
appearance === "date" || appearance === undefined ? `${encoded}T00:00:00Z` : encoded
|
|
14
14
|
);
|
|
15
15
|
if (Number.isNaN(date.valueOf())) return encoded;
|
|
16
|
-
if (appearance === "
|
|
16
|
+
if (appearance === "date-time") {
|
|
17
17
|
return i18n.formatDate(date, {
|
|
18
18
|
dateStyle: "medium",
|
|
19
19
|
timeStyle: "short",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SchemaField, ValidationIssue } from "@riducms/protocol";
|
|
3
|
+
import { fieldControlARIA } from "@riducms/ui";
|
|
4
|
+
import { onDestroy } from "svelte";
|
|
3
5
|
|
|
4
6
|
import { Checkbox } from "@admin/components/ui/checkbox";
|
|
5
7
|
import { DateValueControl } from "@admin/components/ui/date-value-control";
|
|
@@ -14,54 +16,63 @@
|
|
|
14
16
|
const runtime = getAdminRuntime();
|
|
15
17
|
const value = $derived(form.get(field.path));
|
|
16
18
|
const issues = $derived(form.issuesFor(field.path));
|
|
19
|
+
const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
|
|
17
20
|
let jsonError = $state<string>();
|
|
21
|
+
let releaseUnfinishedInput: (() => void) | undefined;
|
|
22
|
+
onDestroy(() => releaseUnfinishedInput?.());
|
|
18
23
|
const displayIssues = $derived<ValidationIssue[]>(
|
|
19
24
|
jsonError === undefined
|
|
20
25
|
? issues
|
|
21
26
|
: [{ code: "invalid_json", path: field.path, message: jsonError }]
|
|
22
27
|
);
|
|
23
|
-
const
|
|
24
|
-
|
|
28
|
+
const controlARIA = $derived(
|
|
29
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, displayIssues.length > 0)
|
|
30
|
+
);
|
|
31
|
+
const dateAppearance = $derived(field.date?.format);
|
|
25
32
|
const stringConstraints = $derived(field.type === "code" ? field.code : field.textarea);
|
|
26
33
|
|
|
27
34
|
$effect(() => form.register(field.path));
|
|
28
35
|
|
|
29
36
|
function setJSON(encoded: string) {
|
|
37
|
+
if (editingBlocked) return;
|
|
30
38
|
try {
|
|
31
39
|
form.set(field.path, JSON.parse(encoded));
|
|
40
|
+
releaseUnfinishedInput?.();
|
|
41
|
+
releaseUnfinishedInput = undefined;
|
|
32
42
|
jsonError = undefined;
|
|
33
43
|
} catch {
|
|
44
|
+
releaseUnfinishedInput?.();
|
|
45
|
+
releaseUnfinishedInput = form.setLiveInputUnavailable(field.path);
|
|
34
46
|
jsonError = runtime.i18n.t("errors:validJSON");
|
|
35
47
|
}
|
|
36
48
|
}
|
|
37
49
|
</script>
|
|
38
50
|
|
|
39
51
|
{#if field.type === "checkbox"}
|
|
40
|
-
<div
|
|
41
|
-
<label
|
|
42
|
-
class="flex min-h-6 w-fit cursor-pointer items-center gap-2 text-[13px] text-foreground-muted hover:text-foreground has-disabled:cursor-not-allowed has-disabled:text-foreground-faint"
|
|
43
|
-
for={field.id}
|
|
44
|
-
>
|
|
45
|
-
<Checkbox
|
|
46
|
-
id={field.id}
|
|
47
|
-
required={field.required}
|
|
48
|
-
aria-invalid={displayIssues.length > 0}
|
|
49
|
-
aria-describedby={hasMessage ? `${field.id}-message` : undefined}
|
|
50
|
-
aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
|
|
51
|
-
disabled={field.admin.readOnly}
|
|
52
|
-
bind:checked={() => Boolean(value), (next) => form.set(field.path, next)}
|
|
53
|
-
/>
|
|
54
|
-
<span>{field.admin.label}</span>
|
|
55
|
-
{#if field.required}<span class="ridu-field-required" aria-hidden="true">*</span>{/if}
|
|
56
|
-
{#if field.admin.readOnly}<span class="ridu-field-status">
|
|
57
|
-
{runtime.i18n.t("fields:readOnly")}
|
|
58
|
-
</span>{/if}
|
|
59
|
-
</label>
|
|
52
|
+
<div data-field-path={field.path} data-invalid={displayIssues.length > 0}>
|
|
60
53
|
<FieldMessages
|
|
61
|
-
|
|
54
|
+
controlID={field.id}
|
|
62
55
|
issues={displayIssues}
|
|
63
56
|
description={field.admin.description}
|
|
64
|
-
|
|
57
|
+
>
|
|
58
|
+
<label
|
|
59
|
+
class="flex min-h-6 w-fit cursor-pointer items-center gap-2 text-[13px] text-foreground-muted hover:text-foreground has-disabled:cursor-not-allowed has-disabled:text-foreground-faint"
|
|
60
|
+
for={field.id}
|
|
61
|
+
>
|
|
62
|
+
<Checkbox
|
|
63
|
+
id={field.id}
|
|
64
|
+
required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
|
|
65
|
+
{...controlARIA}
|
|
66
|
+
disabled={editingBlocked}
|
|
67
|
+
bind:checked={() => Boolean(value), (next) => form.set(field.path, next)}
|
|
68
|
+
/>
|
|
69
|
+
<span>{field.admin.label}</span>
|
|
70
|
+
{#if field.required}<span class="ridu-field-required" aria-hidden="true">*</span>{/if}
|
|
71
|
+
{#if field.admin.readOnly}<span class="ridu-field-status">
|
|
72
|
+
{runtime.i18n.t("fields:readOnly")}
|
|
73
|
+
</span>{/if}
|
|
74
|
+
</label>
|
|
75
|
+
</FieldMessages>
|
|
65
76
|
</div>
|
|
66
77
|
{:else}
|
|
67
78
|
<FieldShell {field} issues={displayIssues}>
|
|
@@ -70,15 +81,13 @@
|
|
|
70
81
|
id={field.id}
|
|
71
82
|
name={field.path}
|
|
72
83
|
rows={1}
|
|
73
|
-
required={field.required}
|
|
84
|
+
required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
|
|
74
85
|
minlength={stringConstraints?.minLength}
|
|
75
86
|
maxlength={stringConstraints?.maxLength}
|
|
76
87
|
placeholder={field.admin.placeholder}
|
|
77
|
-
|
|
78
|
-
aria-describedby={hasMessage ? `${field.id}-message` : undefined}
|
|
79
|
-
aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
|
|
88
|
+
{...controlARIA}
|
|
80
89
|
value={String(value ?? "")}
|
|
81
|
-
readonly={
|
|
90
|
+
readonly={editingBlocked}
|
|
82
91
|
class={field.type === "code"
|
|
83
92
|
? "min-h-14 resize-y field-sizing-fixed font-mono text-[12.5px] leading-5"
|
|
84
93
|
: undefined}
|
|
@@ -89,13 +98,11 @@
|
|
|
89
98
|
<Textarea
|
|
90
99
|
id={field.id}
|
|
91
100
|
name={field.path}
|
|
92
|
-
required={field.required}
|
|
93
|
-
|
|
94
|
-
aria-describedby={hasMessage ? `${field.id}-message` : undefined}
|
|
95
|
-
aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
|
|
101
|
+
required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
|
|
102
|
+
{...controlARIA}
|
|
96
103
|
class="min-h-40 font-mono"
|
|
97
104
|
value={JSON.stringify(value ?? {}, null, 2)}
|
|
98
|
-
readonly={
|
|
105
|
+
readonly={editingBlocked}
|
|
99
106
|
oninput={(event) => setJSON(event.currentTarget.value)}
|
|
100
107
|
/>
|
|
101
108
|
{:else if field.type === "date"}
|
|
@@ -104,28 +111,27 @@
|
|
|
104
111
|
name={field.path}
|
|
105
112
|
appearance={dateAppearance}
|
|
106
113
|
value={String(value ?? "")}
|
|
114
|
+
disabled={form.editingBlocked}
|
|
107
115
|
readonly={field.admin.readOnly}
|
|
108
|
-
required={field.required}
|
|
116
|
+
required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
|
|
109
117
|
invalid={displayIssues.length > 0}
|
|
118
|
+
hasDescription={field.admin.description !== undefined}
|
|
110
119
|
label={field.admin.label}
|
|
111
|
-
describedBy={hasMessage ? `${field.id}-message` : undefined}
|
|
112
120
|
onValueChange={(next) => form.set(field.path, next)}
|
|
113
121
|
/>
|
|
114
122
|
{:else}
|
|
115
123
|
<Input
|
|
116
124
|
id={field.id}
|
|
117
125
|
name={field.path}
|
|
118
|
-
required={field.required}
|
|
119
|
-
|
|
120
|
-
aria-describedby={hasMessage ? `${field.id}-message` : undefined}
|
|
121
|
-
aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
|
|
126
|
+
required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
|
|
127
|
+
{...controlARIA}
|
|
122
128
|
type={field.type === "number" ? "number" : field.type === "email" ? "email" : "text"}
|
|
123
129
|
min={field.type === "number" ? field.number?.min : undefined}
|
|
124
130
|
max={field.type === "number" ? field.number?.max : undefined}
|
|
125
131
|
step={field.type === "number" ? field.number?.step : undefined}
|
|
126
132
|
placeholder={field.admin.placeholder}
|
|
127
133
|
value={String(value ?? "")}
|
|
128
|
-
readonly={
|
|
134
|
+
readonly={editingBlocked}
|
|
129
135
|
oninput={(event) =>
|
|
130
136
|
form.set(
|
|
131
137
|
field.path,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SchemaField } from "@riducms/protocol";
|
|
3
|
+
import { fieldControlARIA } from "@riducms/ui";
|
|
3
4
|
import XIcon from "~icons/lucide/x";
|
|
4
5
|
|
|
5
6
|
import { Button } from "@admin/components/ui/button";
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
import FieldShell from "@admin/fields/field-shell.svelte";
|
|
10
11
|
import {
|
|
11
12
|
removeSelectValue,
|
|
12
|
-
|
|
13
|
+
selectOptionLabel,
|
|
13
14
|
selectManyValues,
|
|
14
15
|
} from "@admin/fields/select/select-value";
|
|
15
16
|
|
|
@@ -27,40 +28,38 @@
|
|
|
27
28
|
field.admin.placeholder ?? runtime.i18n.t("fields:select", { label: field.admin.label })
|
|
28
29
|
);
|
|
29
30
|
const selectedLabel = $derived(
|
|
30
|
-
field.select?.
|
|
31
|
+
field.select?.options.find((option) => option.value === value)?.label ?? placeholder
|
|
31
32
|
);
|
|
32
33
|
const selectedLabels = $derived(
|
|
33
|
-
values.map((selected) =>
|
|
34
|
+
values.map((selected) => selectOptionLabel(field.select?.options ?? [], selected))
|
|
34
35
|
);
|
|
35
36
|
const issues = $derived(form.issuesFor(field.path));
|
|
36
|
-
const
|
|
37
|
+
const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
|
|
38
|
+
const controlARIA = $derived(
|
|
39
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
40
|
+
);
|
|
37
41
|
|
|
38
42
|
$effect(() => form.register(field.path));
|
|
39
43
|
|
|
40
44
|
function setValues(next: string[]) {
|
|
45
|
+
if (editingBlocked) return;
|
|
41
46
|
form.set(field.path, [...next]);
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
function removeValue(selected: string) {
|
|
50
|
+
if (editingBlocked) return;
|
|
45
51
|
form.set(field.path, removeSelectValue(values, selected));
|
|
46
52
|
}
|
|
47
53
|
</script>
|
|
48
54
|
|
|
49
55
|
<FieldShell {field} {issues}>
|
|
50
56
|
{#if hasMany}
|
|
51
|
-
<Select
|
|
52
|
-
type="multiple"
|
|
53
|
-
value={values}
|
|
54
|
-
onValueChange={setValues}
|
|
55
|
-
disabled={field.admin.readOnly}
|
|
56
|
-
>
|
|
57
|
+
<Select type="multiple" value={values} onValueChange={setValues} disabled={editingBlocked}>
|
|
57
58
|
<SelectTrigger
|
|
58
59
|
id={field.id}
|
|
59
60
|
class="w-full"
|
|
60
|
-
|
|
61
|
+
{...controlARIA}
|
|
61
62
|
aria-required={field.required}
|
|
62
|
-
aria-describedby={hasMessage ? `${field.id}-message` : undefined}
|
|
63
|
-
aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
|
|
64
63
|
aria-label={field.admin.label}
|
|
65
64
|
>
|
|
66
65
|
<span class={values.length === 0 ? "text-foreground-placeholder" : "truncate"}>
|
|
@@ -68,8 +67,8 @@
|
|
|
68
67
|
</span>
|
|
69
68
|
</SelectTrigger>
|
|
70
69
|
<SelectContent>
|
|
71
|
-
{#each field.select?.
|
|
72
|
-
<SelectItem value={
|
|
70
|
+
{#each field.select?.options ?? [] as option (option.value)}
|
|
71
|
+
<SelectItem value={option.value} label={option.label} />
|
|
73
72
|
{/each}
|
|
74
73
|
</SelectContent>
|
|
75
74
|
</Select>
|
|
@@ -83,15 +82,16 @@
|
|
|
83
82
|
class="inline-flex max-w-full items-center gap-1 rounded-[3px] border border-control-border bg-background py-0.5 pe-0.5 ps-2 text-[12px] text-foreground-muted"
|
|
84
83
|
>
|
|
85
84
|
<span class="max-w-64 truncate">
|
|
86
|
-
{
|
|
85
|
+
{selectOptionLabel(field.select?.options ?? [], selected)}
|
|
87
86
|
</span>
|
|
88
87
|
{#if !field.admin.readOnly}
|
|
89
88
|
<Button
|
|
90
89
|
variant="ghost"
|
|
91
90
|
size="icon-xs"
|
|
91
|
+
disabled={editingBlocked}
|
|
92
92
|
onclick={() => removeValue(selected)}
|
|
93
93
|
aria-label={runtime.i18n.t("fields:remove", {
|
|
94
|
-
label:
|
|
94
|
+
label: selectOptionLabel(field.select?.options ?? [], selected),
|
|
95
95
|
})}
|
|
96
96
|
>
|
|
97
97
|
<XIcon class="size-3" aria-hidden="true" />
|
|
@@ -106,16 +106,16 @@
|
|
|
106
106
|
type="single"
|
|
107
107
|
{value}
|
|
108
108
|
allowDeselect={!field.required}
|
|
109
|
-
onValueChange={(next) =>
|
|
110
|
-
|
|
109
|
+
onValueChange={(next) => {
|
|
110
|
+
if (!editingBlocked) form.set(field.path, next);
|
|
111
|
+
}}
|
|
112
|
+
disabled={editingBlocked}
|
|
111
113
|
>
|
|
112
114
|
<SelectTrigger
|
|
113
115
|
id={field.id}
|
|
114
116
|
class="w-full"
|
|
115
|
-
|
|
117
|
+
{...controlARIA}
|
|
116
118
|
aria-required={field.required}
|
|
117
|
-
aria-describedby={hasMessage ? `${field.id}-message` : undefined}
|
|
118
|
-
aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
|
|
119
119
|
aria-label={field.admin.label}
|
|
120
120
|
>
|
|
121
121
|
<span class={value === "" ? "text-foreground-placeholder" : undefined}>
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
</span>
|
|
124
124
|
</SelectTrigger>
|
|
125
125
|
<SelectContent>
|
|
126
|
-
{#each field.select?.
|
|
127
|
-
<SelectItem value={
|
|
126
|
+
{#each field.select?.options ?? [] as option (option.value)}
|
|
127
|
+
<SelectItem value={option.value} label={option.label} />
|
|
128
128
|
{/each}
|
|
129
129
|
</SelectContent>
|
|
130
130
|
</Select>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SchemaSelectOption } from "@riducms/protocol";
|
|
2
2
|
|
|
3
3
|
export function selectManyValues(value: unknown): string[] {
|
|
4
4
|
if (!Array.isArray(value)) return [];
|
|
5
5
|
return value.filter((candidate): candidate is string => typeof candidate === "string");
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function
|
|
9
|
-
return
|
|
8
|
+
export function selectOptionLabel(options: readonly SchemaSelectOption[], value: string) {
|
|
9
|
+
return options.find((option) => option.value === value)?.label ?? value;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function removeSelectValue(values: readonly string[], value: string) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SchemaField } from "@riducms/protocol";
|
|
3
|
+
import { fieldControlARIA } from "@riducms/ui";
|
|
3
4
|
import { untrack } from "svelte";
|
|
4
5
|
|
|
5
6
|
import { Button } from "@admin/components/ui/button";
|
|
@@ -21,7 +22,10 @@
|
|
|
21
22
|
const runtime = getAdminRuntime();
|
|
22
23
|
const value = $derived(String(form.get(field.path) ?? ""));
|
|
23
24
|
const issues = $derived(form.issuesFor(field.path));
|
|
24
|
-
const
|
|
25
|
+
const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
|
|
26
|
+
const controlARIA = $derived(
|
|
27
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
28
|
+
);
|
|
25
29
|
let locked = $state(true);
|
|
26
30
|
let binding = $state<DerivedTextBinding>();
|
|
27
31
|
|
|
@@ -41,14 +45,16 @@
|
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
function generate() {
|
|
48
|
+
if (editingBlocked) return;
|
|
44
49
|
binding?.follow();
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function normalizeManualValue() {
|
|
48
|
-
if (!locked) binding?.setManual(normalizeSlug(value));
|
|
53
|
+
if (!editingBlocked && !locked) binding?.setManual(normalizeSlug(value));
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function setManualValue(value: string) {
|
|
57
|
+
if (editingBlocked) return;
|
|
52
58
|
binding?.setManual(value);
|
|
53
59
|
}
|
|
54
60
|
</script>
|
|
@@ -62,10 +68,8 @@
|
|
|
62
68
|
minlength={field.text?.minLength}
|
|
63
69
|
maxlength={field.text?.maxLength}
|
|
64
70
|
placeholder={field.admin.placeholder}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
|
|
68
|
-
readonly={field.admin.readOnly || locked}
|
|
71
|
+
{...controlARIA}
|
|
72
|
+
readonly={editingBlocked || locked}
|
|
69
73
|
class="min-w-0 flex-1 font-mono"
|
|
70
74
|
{value}
|
|
71
75
|
oninput={(event) => setManualValue(event.currentTarget.value)}
|
|
@@ -74,11 +78,23 @@
|
|
|
74
78
|
{#if !field.admin.readOnly}
|
|
75
79
|
<div class="flex shrink-0 items-center gap-1">
|
|
76
80
|
{#if !locked}
|
|
77
|
-
<Button
|
|
81
|
+
<Button
|
|
82
|
+
type="button"
|
|
83
|
+
size="xs"
|
|
84
|
+
variant="ghost"
|
|
85
|
+
disabled={editingBlocked}
|
|
86
|
+
onclick={generate}
|
|
87
|
+
>
|
|
78
88
|
{runtime.i18n.t("fields:generateSlug")}
|
|
79
89
|
</Button>
|
|
80
90
|
{/if}
|
|
81
|
-
<Button
|
|
91
|
+
<Button
|
|
92
|
+
type="button"
|
|
93
|
+
size="xs"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
disabled={editingBlocked}
|
|
96
|
+
onclick={() => (locked = !locked)}
|
|
97
|
+
>
|
|
82
98
|
{runtime.i18n.t(locked ? "fields:unlockSlug" : "fields:lockSlug")}
|
|
83
99
|
</Button>
|
|
84
100
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SchemaField } from "@riducms/protocol";
|
|
3
|
+
import { fieldControlARIA } from "@riducms/ui";
|
|
3
4
|
|
|
4
5
|
import { Input } from "@admin/components/ui/input";
|
|
5
6
|
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
@@ -14,7 +15,10 @@
|
|
|
14
15
|
let { field, form }: Props = $props();
|
|
15
16
|
const value = $derived(String(form.get(field.path) ?? ""));
|
|
16
17
|
const issues = $derived(form.issuesFor(field.path));
|
|
17
|
-
const
|
|
18
|
+
const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
|
|
19
|
+
const controlARIA = $derived(
|
|
20
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
21
|
+
);
|
|
18
22
|
|
|
19
23
|
$effect(() => form.register(field.path));
|
|
20
24
|
</script>
|
|
@@ -26,14 +30,12 @@
|
|
|
26
30
|
<Input
|
|
27
31
|
id={field.id}
|
|
28
32
|
name={field.path}
|
|
29
|
-
required={field.required}
|
|
33
|
+
required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
|
|
30
34
|
minlength={field.text?.minLength}
|
|
31
35
|
maxlength={field.text?.maxLength}
|
|
32
36
|
placeholder={field.admin.placeholder}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
|
|
36
|
-
readonly={field.admin.readOnly}
|
|
37
|
+
{...controlARIA}
|
|
38
|
+
readonly={editingBlocked}
|
|
37
39
|
{value}
|
|
38
40
|
oninput={(event) => form.set(field.path, event.currentTarget.value)}
|
|
39
41
|
/>
|
package/src/index.ts
CHANGED
|
@@ -5,26 +5,20 @@ import "@admin/app.css";
|
|
|
5
5
|
|
|
6
6
|
import { mount } from "svelte";
|
|
7
7
|
|
|
8
|
-
import type { AdminPlugin, FieldPlugin } from "@riducms/plugin";
|
|
9
8
|
import type { RiduClient, RiduConfigShape } from "@riducms/sdk";
|
|
10
|
-
import type { TranslationLanguage } from "@riducms/translations";
|
|
11
9
|
|
|
12
10
|
import App from "@admin/app.svelte";
|
|
13
11
|
import type { AdminClient } from "@admin/core/api/admin-client";
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
import type { AdminConfig } from "@riducms/plugin/admin";
|
|
14
|
+
|
|
15
|
+
export interface MountAdminOptions<Config extends RiduConfigShape> extends AdminConfig {
|
|
16
16
|
/** Existing DOM element that receives the Ridu admin application. */
|
|
17
17
|
target: Element;
|
|
18
18
|
/** Creates the generated, application-specific Fetch client. */
|
|
19
19
|
clientFactory: () => RiduClient<Config>;
|
|
20
20
|
/** Route prefix used when the admin is not mounted at /admin. */
|
|
21
21
|
adminBasePath?: string;
|
|
22
|
-
/** Statically paired packaged plugins, including fields and routes. */
|
|
23
|
-
plugins?: readonly AdminPlugin[];
|
|
24
|
-
/** Application-local field renderers that have no packaged backend pair. */
|
|
25
|
-
fieldPlugins?: readonly FieldPlugin[];
|
|
26
|
-
/** Statically imported catalogs available to the Go-configured admin languages. */
|
|
27
|
-
languages?: readonly TranslationLanguage[];
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
export function mountAdmin<Config extends RiduConfigShape>(
|
|
@@ -38,10 +32,8 @@ export function mountAdmin<Config extends RiduConfigShape>(
|
|
|
38
32
|
target: options.target,
|
|
39
33
|
props: {
|
|
40
34
|
clientFactory,
|
|
35
|
+
adminConfig: options,
|
|
41
36
|
...(options.adminBasePath === undefined ? {} : { adminBasePath: options.adminBasePath }),
|
|
42
|
-
...(options.plugins === undefined ? {} : { plugins: options.plugins }),
|
|
43
|
-
...(options.fieldPlugins === undefined ? {} : { fieldPlugins: options.fieldPlugins }),
|
|
44
|
-
...(options.languages === undefined ? {} : { languages: options.languages }),
|
|
45
37
|
},
|
|
46
38
|
});
|
|
47
39
|
}
|