@riducms/admin 0.1.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/LICENSE +21 -0
- package/README.md +31 -0
- package/THIRD_PARTY_NOTICES.md +217 -0
- package/package.json +62 -0
- package/src/app/admin-core-view-route.svelte +89 -0
- package/src/app/admin-layout.svelte +457 -0
- package/src/app/admin-navigation-progress.svelte +204 -0
- package/src/app/admin-page-title.svelte +94 -0
- package/src/app/admin-provider-layer.svelte +26 -0
- package/src/app/admin-router-root.svelte +9 -0
- package/src/app/admin-routes.svelte +116 -0
- package/src/app/deferred-redirect.svelte +23 -0
- package/src/app/loading-boundary.svelte +70 -0
- package/src/app/not-found-route.svelte +29 -0
- package/src/app-meta.ts +2 -0
- package/src/app.css +467 -0
- package/src/app.svelte +88 -0
- package/src/assets/ridu-logo-arch.svg +3 -0
- package/src/assets/ridu-word-logo.svg +7 -0
- package/src/components/brand/ridu-logo.svelte +24 -0
- package/src/components/code-block/code-block.svelte +107 -0
- package/src/components/json-tree/json-tree-node.svelte +77 -0
- package/src/components/json-tree/json-viewer.svelte +113 -0
- package/src/components/ui/banner/banner.svelte +41 -0
- package/src/components/ui/banner/index.ts +5 -0
- package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
- package/src/components/ui/bulk-action-bar/index.ts +1 -0
- package/src/components/ui/button/index.ts +8 -0
- package/src/components/ui/calendar/calendar-caption.svelte +72 -0
- package/src/components/ui/calendar/calendar-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-day.svelte +27 -0
- package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
- package/src/components/ui/calendar/calendar-grid.svelte +17 -0
- package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-header.svelte +20 -0
- package/src/components/ui/calendar/calendar-heading.svelte +17 -0
- package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
- package/src/components/ui/calendar/calendar-month.svelte +16 -0
- package/src/components/ui/calendar/calendar-months.svelte +20 -0
- package/src/components/ui/calendar/calendar-nav.svelte +20 -0
- package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
- package/src/components/ui/calendar/calendar.svelte +118 -0
- package/src/components/ui/calendar/index.ts +1 -0
- package/src/components/ui/checkbox/checkbox.svelte +35 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
- package/src/components/ui/confirmation-dialog/index.ts +1 -0
- package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
- package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
- package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
- package/src/components/ui/date-value-control/index.ts +3 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +52 -0
- package/src/components/ui/dialog/dialog-description.svelte +20 -0
- package/src/components/ui/dialog/dialog-footer.svelte +20 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +8 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +5 -0
- package/src/components/ui/empty-state/empty-state.svelte +34 -0
- package/src/components/ui/empty-state/index.ts +1 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/kbd/index.ts +1 -0
- package/src/components/ui/kbd/kbd.svelte +24 -0
- package/src/components/ui/pagination/index.ts +1 -0
- package/src/components/ui/pagination/pagination.svelte +88 -0
- package/src/components/ui/popover/index.ts +3 -0
- package/src/components/ui/popover/popover-content.svelte +29 -0
- package/src/components/ui/popover/popover-trigger.svelte +11 -0
- package/src/components/ui/popover/popover.svelte +7 -0
- package/src/components/ui/radio-group/index.ts +3 -0
- package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
- package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
- package/src/components/ui/radio-group/radio-group.svelte +19 -0
- package/src/components/ui/select/index.ts +4 -0
- package/src/components/ui/select/select-content.svelte +38 -0
- package/src/components/ui/select/select-item.svelte +40 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/sheet/index.ts +6 -0
- package/src/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/components/ui/sheet/sheet-content.svelte +77 -0
- package/src/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
- package/src/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/components/ui/sheet/sheet.svelte +7 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/skeleton/skeleton.svelte +31 -0
- package/src/components/ui/slider/index.ts +3 -0
- package/src/components/ui/slider/slider-content.svelte +59 -0
- package/src/components/ui/slider/slider.svelte +35 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +55 -0
- package/src/components/ui/status-indicator/index.ts +5 -0
- package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
- package/src/components/ui/table/index.ts +6 -0
- package/src/components/ui/table/table-body.svelte +20 -0
- package/src/components/ui/table/table-cell.svelte +23 -0
- package/src/components/ui/table/table-head.svelte +23 -0
- package/src/components/ui/table/table-header.svelte +20 -0
- package/src/components/ui/table/table-row.svelte +23 -0
- package/src/components/ui/table/table.svelte +22 -0
- package/src/components/ui/tabs/index.ts +8 -0
- package/src/components/ui/tabs/tabs-content.svelte +17 -0
- package/src/components/ui/tabs/tabs-list.svelte +40 -0
- package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/src/components/ui/tabs/tabs.svelte +19 -0
- package/src/components/ui/textarea/index.ts +1 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
- package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/core/api/admin-client.ts +45 -0
- package/src/core/forms/field-condition.ts +63 -0
- package/src/core/forms/form-controller.svelte.ts +369 -0
- package/src/core/forms/form-draft-recovery.ts +80 -0
- package/src/core/forms/form-schema.ts +609 -0
- package/src/core/forms/form-validation.ts +750 -0
- package/src/core/i18n/admin-i18n.svelte.ts +364 -0
- package/src/core/i18n/localized-schema.ts +111 -0
- package/src/core/notifications/notification-center.svelte.ts +69 -0
- package/src/core/notifications/validation-toast.svelte +18 -0
- package/src/core/plugins/field-registry.ts +60 -0
- package/src/core/plugins/row-label-registry.ts +62 -0
- package/src/core/preferences/preference-write-queue.ts +107 -0
- package/src/core/routing/admin-paths.ts +137 -0
- package/src/core/runtime/admin-runtime.svelte.ts +625 -0
- package/src/features/auth/account-menu.svelte +159 -0
- package/src/features/auth/account-navigation.svelte +44 -0
- package/src/features/auth/account-route.svelte +395 -0
- package/src/features/auth/account-security-controller.svelte.ts +119 -0
- package/src/features/auth/account-security-route.svelte +460 -0
- package/src/features/auth/auth-brand.svelte +17 -0
- package/src/features/auth/auth-frame.svelte +18 -0
- package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
- package/src/features/auth/create-first-user-route.svelte +174 -0
- package/src/features/auth/forgot-password-route.svelte +78 -0
- package/src/features/auth/login-route.svelte +221 -0
- package/src/features/auth/request-verification-route.svelte +73 -0
- package/src/features/auth/reset-password-route.svelte +107 -0
- package/src/features/auth/verify-email-route.svelte +60 -0
- package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
- package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
- package/src/features/collections/collection-list-controller.svelte.ts +717 -0
- package/src/features/collections/collection-list-filter-popover.svelte +238 -0
- package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
- package/src/features/collections/collection-list-results.svelte +410 -0
- package/src/features/collections/collection-list-route.svelte +951 -0
- package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
- package/src/features/collections/collection-table-skeleton.svelte +44 -0
- package/src/features/collections/list-workspace.ts +265 -0
- package/src/features/dashboard/dashboard-route.svelte +141 -0
- package/src/features/development/development-tools.svelte +217 -0
- package/src/features/documents/document-api-view-loader.svelte +31 -0
- package/src/features/documents/document-api-view.svelte +547 -0
- package/src/features/documents/document-controller.svelte.ts +1252 -0
- package/src/features/documents/document-field-partition.ts +18 -0
- package/src/features/documents/document-field-sections.svelte +46 -0
- package/src/features/documents/document-heading.ts +20 -0
- package/src/features/documents/document-route-dirty.ts +43 -0
- package/src/features/documents/document-route-loader.svelte +22 -0
- package/src/features/documents/document-route.svelte +1124 -0
- package/src/features/documents/live-preview-panel.svelte +390 -0
- package/src/features/documents/live-preview.ts +61 -0
- package/src/features/documents/upload-document-preview-loader.svelte +40 -0
- package/src/features/documents/upload-document-preview.svelte +525 -0
- package/src/features/localization/content-locale-switcher.svelte +93 -0
- package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
- package/src/features/navigation/admin-command-menu.svelte +245 -0
- package/src/features/navigation/admin-resource-groups.ts +72 -0
- package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
- package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
- package/src/features/reference-browser/reference-browser.svelte +844 -0
- package/src/features/uploads/bulk-upload-route.svelte +624 -0
- package/src/features/uploads/bulk-upload.ts +39 -0
- package/src/features/uploads/upload-metadata-fields.svelte +112 -0
- package/src/features/versions/version-diff.ts +72 -0
- package/src/features/versions/version-history-route.svelte +581 -0
- package/src/fields/field-clipboard.ts +132 -0
- package/src/fields/field-layout.svelte +60 -0
- package/src/fields/field-layout.ts +57 -0
- package/src/fields/field-messages.svelte +29 -0
- package/src/fields/field-renderer.svelte +152 -0
- package/src/fields/field-shell.svelte +30 -0
- package/src/fields/field-tab-layout.svelte +99 -0
- package/src/fields/join/join-field.svelte +358 -0
- package/src/fields/localized-field-presentation.ts +17 -0
- package/src/fields/nested/nested-field.svelte +602 -0
- package/src/fields/nested/scoped-field.ts +78 -0
- package/src/fields/nested/sortable-row.svelte +32 -0
- package/src/fields/point/point-field.svelte +52 -0
- package/src/fields/radio/radio-field.svelte +34 -0
- package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
- package/src/fields/relationship/relationship-field.svelte +258 -0
- package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
- package/src/fields/relationship/relationship-query.ts +88 -0
- package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
- package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
- package/src/fields/relationship/relationship-value.ts +47 -0
- package/src/fields/scalar/date-control-value.ts +60 -0
- package/src/fields/scalar/date-value.ts +26 -0
- package/src/fields/scalar/scalar-field.svelte +137 -0
- package/src/fields/select/select-field.svelte +132 -0
- package/src/fields/select/select-value.ts +14 -0
- package/src/fields/text/slug-field.svelte +87 -0
- package/src/fields/text/slug.ts +38 -0
- package/src/fields/text/text-field.svelte +41 -0
- package/src/fields/ui/ui-field.svelte +12 -0
- package/src/fields/virtual/virtual-field.svelte +28 -0
- package/src/index.ts +49 -0
- package/src/main.ts +12 -0
- package/src/vite-env.d.ts +9 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { SchemaField } from "@riducms/protocol";
|
|
2
|
+
|
|
3
|
+
export type FieldClipboardKind = "field" | "row";
|
|
4
|
+
|
|
5
|
+
export interface FieldClipboardPayload {
|
|
6
|
+
version: 1;
|
|
7
|
+
kind: FieldClipboardKind;
|
|
8
|
+
signature: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const clipboardPrefix = "ridu-field-clipboard:";
|
|
13
|
+
const maximumClipboardLength = 1_000_000;
|
|
14
|
+
let memoryClipboard: FieldClipboardPayload | undefined;
|
|
15
|
+
|
|
16
|
+
export function fieldClipboardSignature(field: SchemaField): string {
|
|
17
|
+
return JSON.stringify(fieldShape(field));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createFieldClipboardPayload(
|
|
21
|
+
field: SchemaField,
|
|
22
|
+
kind: FieldClipboardKind,
|
|
23
|
+
value: unknown
|
|
24
|
+
): FieldClipboardPayload {
|
|
25
|
+
return {
|
|
26
|
+
version: 1,
|
|
27
|
+
kind,
|
|
28
|
+
signature: fieldClipboardSignature(field),
|
|
29
|
+
value: cloneJSON(value),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseFieldClipboardPayload(encoded: string): FieldClipboardPayload | undefined {
|
|
34
|
+
if (!encoded.startsWith(clipboardPrefix) || encoded.length > maximumClipboardLength)
|
|
35
|
+
return undefined;
|
|
36
|
+
try {
|
|
37
|
+
const candidate = JSON.parse(
|
|
38
|
+
encoded.slice(clipboardPrefix.length)
|
|
39
|
+
) as Partial<FieldClipboardPayload>;
|
|
40
|
+
if (
|
|
41
|
+
candidate.version !== 1 ||
|
|
42
|
+
(candidate.kind !== "field" && candidate.kind !== "row") ||
|
|
43
|
+
typeof candidate.signature !== "string" ||
|
|
44
|
+
!("value" in candidate)
|
|
45
|
+
)
|
|
46
|
+
return undefined;
|
|
47
|
+
return candidate as FieldClipboardPayload;
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function compatibleClipboardValue(
|
|
54
|
+
payload: FieldClipboardPayload | undefined,
|
|
55
|
+
field: SchemaField,
|
|
56
|
+
kind: FieldClipboardKind
|
|
57
|
+
) {
|
|
58
|
+
if (
|
|
59
|
+
payload === undefined ||
|
|
60
|
+
payload.kind !== kind ||
|
|
61
|
+
payload.signature !== fieldClipboardSignature(field)
|
|
62
|
+
)
|
|
63
|
+
return undefined;
|
|
64
|
+
return cloneForPaste(payload.value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function writeFieldClipboard(payload: FieldClipboardPayload) {
|
|
68
|
+
memoryClipboard = payload;
|
|
69
|
+
try {
|
|
70
|
+
await navigator.clipboard.writeText(clipboardPrefix + JSON.stringify(payload));
|
|
71
|
+
} catch {
|
|
72
|
+
// The in-memory copy keeps same-admin workflows available when a browser
|
|
73
|
+
// denies clipboard permission or the app is served from an insecure origin.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function readFieldClipboard() {
|
|
78
|
+
try {
|
|
79
|
+
const encoded = await navigator.clipboard.readText();
|
|
80
|
+
const parsed = parseFieldClipboardPayload(encoded);
|
|
81
|
+
if (parsed !== undefined) {
|
|
82
|
+
memoryClipboard = parsed;
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Fall through to the last trusted in-app copy.
|
|
87
|
+
}
|
|
88
|
+
return memoryClipboard;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cloneForPaste(value: unknown): unknown {
|
|
92
|
+
if (Array.isArray(value)) return value.map(cloneForPaste);
|
|
93
|
+
if (value === null || typeof value !== "object") return value;
|
|
94
|
+
return Object.fromEntries(
|
|
95
|
+
Object.entries(value).map(([name, nested]) => [
|
|
96
|
+
name,
|
|
97
|
+
name === "_key" ? crypto.randomUUID() : cloneForPaste(nested),
|
|
98
|
+
])
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cloneJSON(value: unknown) {
|
|
103
|
+
return value === undefined ? null : JSON.parse(JSON.stringify(value));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fieldShape(field: SchemaField): unknown {
|
|
107
|
+
return {
|
|
108
|
+
type: field.type,
|
|
109
|
+
category: field.category,
|
|
110
|
+
select: field.select?.choices.map((choice) => choice.value),
|
|
111
|
+
relationship: field.relationship && {
|
|
112
|
+
collection: field.relationship.collectionSlug,
|
|
113
|
+
targets: field.relationship.targets?.map((target) => target.collectionSlug),
|
|
114
|
+
hasMany: field.relationship.hasMany === true,
|
|
115
|
+
polymorphic: field.relationship.polymorphic === true,
|
|
116
|
+
},
|
|
117
|
+
upload: field.upload && {
|
|
118
|
+
collection: field.upload.collectionSlug,
|
|
119
|
+
hasMany: field.upload.hasMany === true,
|
|
120
|
+
},
|
|
121
|
+
nested: field.nested && {
|
|
122
|
+
minRows: field.nested.minRows ?? 0,
|
|
123
|
+
maxRows: field.nested.maxRows ?? 0,
|
|
124
|
+
fields: field.nested.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
|
|
125
|
+
},
|
|
126
|
+
blocks: field.blocks?.types.map((block) => ({
|
|
127
|
+
key: block.key,
|
|
128
|
+
fields: block.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
|
|
129
|
+
})),
|
|
130
|
+
plugin: field.plugin && { key: field.plugin.key, config: field.plugin.config },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SchemaField } from "@riducms/protocol";
|
|
3
|
+
|
|
4
|
+
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
5
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
6
|
+
import FieldRenderer from "@admin/fields/field-renderer.svelte";
|
|
7
|
+
import { createFieldLayout } from "@admin/fields/field-layout";
|
|
8
|
+
import FieldTabLayout from "@admin/fields/field-tab-layout.svelte";
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
fields,
|
|
12
|
+
form,
|
|
13
|
+
suppressedTabGroupID = "",
|
|
14
|
+
}: {
|
|
15
|
+
fields: readonly SchemaField[];
|
|
16
|
+
form: FormController;
|
|
17
|
+
suppressedTabGroupID?: string;
|
|
18
|
+
} = $props();
|
|
19
|
+
const runtime = getAdminRuntime();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#snippet fieldGrid(tabFields: readonly SchemaField[])}
|
|
23
|
+
<div class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-12">
|
|
24
|
+
{#each createFieldLayout(tabFields, suppressedTabGroupID) as group (group.key)}
|
|
25
|
+
{#if group.tabGroup !== undefined}
|
|
26
|
+
<FieldTabLayout fields={group.fields} {form} tabGroup={group.tabGroup} />
|
|
27
|
+
{:else if group.collapsible !== undefined}
|
|
28
|
+
<details
|
|
29
|
+
class="col-span-1 rounded-[4px] border border-control-border bg-background sm:col-span-12"
|
|
30
|
+
open={!group.collapsible.initiallyCollapsed}
|
|
31
|
+
data-field-collapsible={group.collapsible.id}
|
|
32
|
+
>
|
|
33
|
+
<summary class="cursor-pointer px-5 py-4 text-[13.5px] font-medium text-foreground">
|
|
34
|
+
{runtime.i18n.text(group.collapsible.label, group.collapsible.labelTranslations)}
|
|
35
|
+
</summary>
|
|
36
|
+
<div
|
|
37
|
+
class="grid grid-cols-1 gap-x-4 gap-y-6 border-t border-control-border p-5 sm:grid-cols-12"
|
|
38
|
+
>
|
|
39
|
+
{#each group.fields as field (field.id)}
|
|
40
|
+
<FieldRenderer {field} {form} />
|
|
41
|
+
{/each}
|
|
42
|
+
</div>
|
|
43
|
+
</details>
|
|
44
|
+
{:else if group.row !== undefined}
|
|
45
|
+
<div
|
|
46
|
+
class="col-span-1 grid grid-cols-1 gap-x-4 gap-y-6 sm:col-span-12 sm:grid-cols-12"
|
|
47
|
+
data-field-row={group.row.id}
|
|
48
|
+
>
|
|
49
|
+
{#each group.fields as field (field.id)}
|
|
50
|
+
<FieldRenderer {field} {form} />
|
|
51
|
+
{/each}
|
|
52
|
+
</div>
|
|
53
|
+
{:else}
|
|
54
|
+
<FieldRenderer field={group.fields[0]} {form} />
|
|
55
|
+
{/if}
|
|
56
|
+
{/each}
|
|
57
|
+
</div>
|
|
58
|
+
{/snippet}
|
|
59
|
+
|
|
60
|
+
{@render fieldGrid(fields)}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SchemaField,
|
|
3
|
+
SchemaFieldCollapsible,
|
|
4
|
+
SchemaFieldRow,
|
|
5
|
+
SchemaFieldTabGroup,
|
|
6
|
+
} from "@riducms/protocol";
|
|
7
|
+
|
|
8
|
+
export interface FieldLayoutGroup {
|
|
9
|
+
key: string;
|
|
10
|
+
row?: SchemaFieldRow;
|
|
11
|
+
collapsible?: SchemaFieldCollapsible;
|
|
12
|
+
tabGroup?: SchemaFieldTabGroup;
|
|
13
|
+
fields: [SchemaField, ...SchemaField[]];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Row metadata is deliberately carried by ordinary data fields. Grouping it
|
|
17
|
+
// here preserves the manifest's field order without introducing a form value.
|
|
18
|
+
export function createFieldLayout(
|
|
19
|
+
fields: readonly SchemaField[],
|
|
20
|
+
suppressedTabGroupID = ""
|
|
21
|
+
): FieldLayoutGroup[] {
|
|
22
|
+
const groups: FieldLayoutGroup[] = [];
|
|
23
|
+
for (const field of fields) {
|
|
24
|
+
const row = field.admin.row;
|
|
25
|
+
const collapsible = field.admin.collapsible;
|
|
26
|
+
const tabGroup =
|
|
27
|
+
field.admin.tabGroup?.id === suppressedTabGroupID ? undefined : field.admin.tabGroup;
|
|
28
|
+
const previous = groups.at(-1);
|
|
29
|
+
if (tabGroup !== undefined && previous?.tabGroup?.id === tabGroup.id) {
|
|
30
|
+
previous.fields.push(field);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (collapsible !== undefined && previous?.collapsible?.id === collapsible.id) {
|
|
34
|
+
previous.fields.push(field);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (row !== undefined && previous?.row?.id === row.id) {
|
|
38
|
+
previous.fields.push(field);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
groups.push({
|
|
42
|
+
key:
|
|
43
|
+
tabGroup !== undefined
|
|
44
|
+
? `tabs:${tabGroup.id}:${field.id}`
|
|
45
|
+
: collapsible !== undefined
|
|
46
|
+
? `collapsible:${collapsible.id}`
|
|
47
|
+
: row === undefined
|
|
48
|
+
? `field:${field.id}`
|
|
49
|
+
: `row:${row.id}`,
|
|
50
|
+
...(row === undefined ? {} : { row }),
|
|
51
|
+
...(collapsible === undefined ? {} : { collapsible }),
|
|
52
|
+
...(tabGroup === undefined ? {} : { tabGroup }),
|
|
53
|
+
fields: [field],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return groups;
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ValidationIssue } from "@riducms/protocol";
|
|
3
|
+
import CircleAlertIcon from "~icons/lucide/circle-alert";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
id,
|
|
7
|
+
issues = [],
|
|
8
|
+
description,
|
|
9
|
+
class: className,
|
|
10
|
+
}: {
|
|
11
|
+
id: string;
|
|
12
|
+
issues?: readonly ValidationIssue[];
|
|
13
|
+
description?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
} = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if issues.length > 0}
|
|
19
|
+
<div {id} class={["flex items-start gap-1.5", className]} role="alert" aria-atomic="true">
|
|
20
|
+
<CircleAlertIcon class="mt-0.5 size-3.5 shrink-0 text-destructive" aria-hidden="true" />
|
|
21
|
+
<ul class="grid gap-0.5">
|
|
22
|
+
{#each issues as issue (`${issue.path}:${issue.code}:${issue.message}`)}
|
|
23
|
+
<li class="ridu-field-error">{issue.message}</li>
|
|
24
|
+
{/each}
|
|
25
|
+
</ul>
|
|
26
|
+
</div>
|
|
27
|
+
{:else if description !== undefined}
|
|
28
|
+
<p {id} class={["ridu-field-help", className]}>{description}</p>
|
|
29
|
+
{/if}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldAuthoringHost } from "@riducms/plugin";
|
|
3
|
+
import type { SchemaField } from "@riducms/protocol";
|
|
4
|
+
|
|
5
|
+
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
6
|
+
import { evaluateFieldCondition } from "@admin/core/forms/field-condition";
|
|
7
|
+
import { localizeSchemaCollection, localizeSchemaField } from "@admin/core/i18n/localized-schema";
|
|
8
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
9
|
+
import { fieldAccessPath } from "@admin/fields/nested/scoped-field";
|
|
10
|
+
import ReferenceBrowser from "@admin/features/reference-browser/reference-browser-loader.svelte";
|
|
11
|
+
import {
|
|
12
|
+
contentLocaleLabel,
|
|
13
|
+
withContentLocaleLabel,
|
|
14
|
+
} from "@admin/fields/localized-field-presentation";
|
|
15
|
+
|
|
16
|
+
import SelectField from "@admin/fields/select/select-field.svelte";
|
|
17
|
+
import RadioField from "@admin/fields/radio/radio-field.svelte";
|
|
18
|
+
import PointField from "@admin/fields/point/point-field.svelte";
|
|
19
|
+
import UIField from "@admin/fields/ui/ui-field.svelte";
|
|
20
|
+
import JoinField from "@admin/fields/join/join-field.svelte";
|
|
21
|
+
import VirtualField from "@admin/fields/virtual/virtual-field.svelte";
|
|
22
|
+
import ScalarField from "@admin/fields/scalar/scalar-field.svelte";
|
|
23
|
+
import NestedField from "@admin/fields/nested/nested-field.svelte";
|
|
24
|
+
import RelationshipField from "@admin/fields/relationship/relationship-field.svelte";
|
|
25
|
+
import TextField from "@admin/fields/text/text-field.svelte";
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
field: SchemaField;
|
|
29
|
+
form: FormController;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let { field, form }: Props = $props();
|
|
33
|
+
const runtime = getAdminRuntime();
|
|
34
|
+
const authoring: FieldAuthoringHost = {
|
|
35
|
+
get collections() {
|
|
36
|
+
return (
|
|
37
|
+
runtime.manifest?.collections.map((collection) =>
|
|
38
|
+
localizeSchemaCollection(collection, runtime.i18n)
|
|
39
|
+
) ?? []
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
get documentRevision() {
|
|
43
|
+
return runtime.documentRevision;
|
|
44
|
+
},
|
|
45
|
+
get locale() {
|
|
46
|
+
return form.contentLocale;
|
|
47
|
+
},
|
|
48
|
+
referenceBrowser: ReferenceBrowser,
|
|
49
|
+
findDocument: (collection, id, signal) =>
|
|
50
|
+
runtime.client.find(collection, id, { signal, locale: form.contentLocale }),
|
|
51
|
+
async requestPlugin<Result>(path: string, body: unknown, signal?: AbortSignal) {
|
|
52
|
+
const owner = field.admin.component?.plugin ?? field.plugin?.key;
|
|
53
|
+
if (owner === undefined) {
|
|
54
|
+
throw new Error("This field renderer does not have an owning plugin");
|
|
55
|
+
}
|
|
56
|
+
return runtime.client.requestPlugin<Result>(owner, path, body, { signal });
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const plugin = $derived(runtime.fields.resolve(field));
|
|
60
|
+
const accessPath = $derived(fieldAccessPath(field));
|
|
61
|
+
const visible = $derived(
|
|
62
|
+
field.admin.hidden !== true &&
|
|
63
|
+
form.canRead(field.path, accessPath) &&
|
|
64
|
+
(field.admin.condition === undefined ||
|
|
65
|
+
evaluateFieldCondition(field.admin.condition, field.path, (path) => form.get(path)))
|
|
66
|
+
);
|
|
67
|
+
const renderedField = $derived(
|
|
68
|
+
field.type === "join" || field.admin.readOnly || form.canWrite(field.path, accessPath)
|
|
69
|
+
? field
|
|
70
|
+
: { ...field, admin: { ...field.admin, readOnly: true } }
|
|
71
|
+
);
|
|
72
|
+
const localizedField = $derived(localizeSchemaField(renderedField, runtime.i18n));
|
|
73
|
+
const activeContentLocaleLabel = $derived(
|
|
74
|
+
contentLocaleLabel(runtime.contentLocales, form.contentLocale)
|
|
75
|
+
);
|
|
76
|
+
const presentedField = $derived(withContentLocaleLabel(localizedField, activeContentLocaleLabel));
|
|
77
|
+
const columnClasses: Record<number, string> = {
|
|
78
|
+
1: "col-span-1 sm:col-span-1",
|
|
79
|
+
2: "col-span-1 sm:col-span-2",
|
|
80
|
+
3: "col-span-1 sm:col-span-3",
|
|
81
|
+
4: "col-span-1 sm:col-span-4",
|
|
82
|
+
5: "col-span-1 sm:col-span-5",
|
|
83
|
+
6: "col-span-1 sm:col-span-6",
|
|
84
|
+
7: "col-span-1 sm:col-span-7",
|
|
85
|
+
8: "col-span-1 sm:col-span-8",
|
|
86
|
+
9: "col-span-1 sm:col-span-9",
|
|
87
|
+
10: "col-span-1 sm:col-span-10",
|
|
88
|
+
11: "col-span-1 sm:col-span-11",
|
|
89
|
+
12: "col-span-1 sm:col-span-12",
|
|
90
|
+
};
|
|
91
|
+
const columns = $derived(Math.min(12, Math.max(1, field.admin.columns ?? 12)));
|
|
92
|
+
const columnClass = $derived(columnClasses[columns] ?? columnClasses[12]);
|
|
93
|
+
const inheritedFrom = $derived(
|
|
94
|
+
form.isInherited(field.path) ? form.localizationSource(field.path) : undefined
|
|
95
|
+
);
|
|
96
|
+
const inheritedFromLabel = $derived(contentLocaleLabel(runtime.contentLocales, inheritedFrom));
|
|
97
|
+
const missingLocale = $derived(
|
|
98
|
+
field.localized === true &&
|
|
99
|
+
form.contentLocale !== undefined &&
|
|
100
|
+
form.get(field.path) === undefined
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
function renderField() {
|
|
104
|
+
if (plugin.component !== undefined) return plugin.component;
|
|
105
|
+
|
|
106
|
+
switch (plugin.type) {
|
|
107
|
+
case "text":
|
|
108
|
+
return TextField;
|
|
109
|
+
case "select":
|
|
110
|
+
return SelectField;
|
|
111
|
+
case "radio":
|
|
112
|
+
return RadioField;
|
|
113
|
+
case "point":
|
|
114
|
+
return PointField;
|
|
115
|
+
case "ui":
|
|
116
|
+
return UIField;
|
|
117
|
+
case "join":
|
|
118
|
+
return JoinField;
|
|
119
|
+
case "virtual":
|
|
120
|
+
return VirtualField;
|
|
121
|
+
case "group":
|
|
122
|
+
case "array":
|
|
123
|
+
case "blocks":
|
|
124
|
+
return NestedField;
|
|
125
|
+
case "relationship":
|
|
126
|
+
case "upload":
|
|
127
|
+
return RelationshipField;
|
|
128
|
+
default:
|
|
129
|
+
return ScalarField;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
{#if visible}<div class={columnClass}>
|
|
135
|
+
{const RenderField = $derived(renderField())}
|
|
136
|
+
<RenderField field={presentedField} {form} {authoring} i18n={runtime.i18n} />
|
|
137
|
+
{#if inheritedFrom !== undefined}<p
|
|
138
|
+
class="mt-1 font-mono text-[10px] text-foreground-faint"
|
|
139
|
+
data-localization-source={inheritedFrom}
|
|
140
|
+
>
|
|
141
|
+
{runtime.i18n.t("documents:inheritedFrom", {
|
|
142
|
+
locale: inheritedFromLabel ?? inheritedFrom,
|
|
143
|
+
})}
|
|
144
|
+
</p>{:else if missingLocale}<p
|
|
145
|
+
class="mt-1 font-mono text-[10px] text-foreground-faint"
|
|
146
|
+
data-localization-missing={form.contentLocale}
|
|
147
|
+
>
|
|
148
|
+
{runtime.i18n.t("documents:missingTranslation", {
|
|
149
|
+
locale: activeContentLocaleLabel ?? form.contentLocale ?? "",
|
|
150
|
+
})}
|
|
151
|
+
</p>{/if}
|
|
152
|
+
</div>{/if}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SchemaField, ValidationIssue } from "@riducms/protocol";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
|
|
5
|
+
import { FieldFrame } from "@riducms/ui";
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
field,
|
|
9
|
+
issues = [],
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
field: SchemaField;
|
|
13
|
+
issues?: ValidationIssue[];
|
|
14
|
+
children: Snippet;
|
|
15
|
+
} = $props();
|
|
16
|
+
const errorMessages = $derived(issues.map((issue) => issue.message));
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div data-field-path={field.path}>
|
|
20
|
+
<FieldFrame
|
|
21
|
+
controlID={field.id}
|
|
22
|
+
label={field.admin.label}
|
|
23
|
+
required={field.required}
|
|
24
|
+
readOnly={field.admin.readOnly}
|
|
25
|
+
description={field.admin.description}
|
|
26
|
+
errors={errorMessages}
|
|
27
|
+
>
|
|
28
|
+
{@render children()}
|
|
29
|
+
</FieldFrame>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SchemaField, SchemaFieldTabGroup } from "@riducms/protocol";
|
|
3
|
+
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@admin/components/ui/tabs";
|
|
5
|
+
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
6
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
7
|
+
import FieldLayout from "@admin/fields/field-layout.svelte";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
fields,
|
|
11
|
+
form,
|
|
12
|
+
tabGroup,
|
|
13
|
+
}: {
|
|
14
|
+
fields: readonly SchemaField[];
|
|
15
|
+
form: FormController;
|
|
16
|
+
tabGroup: SchemaFieldTabGroup;
|
|
17
|
+
} = $props();
|
|
18
|
+
const runtime = getAdminRuntime();
|
|
19
|
+
const fallbackTab = $derived(runtime.i18n.t("fields:content"));
|
|
20
|
+
const tabs = $derived.by(() => {
|
|
21
|
+
const entries: Array<{ key: string; label: string }> = [];
|
|
22
|
+
for (const field of fields) {
|
|
23
|
+
const key = field.admin.tab ?? "";
|
|
24
|
+
if (entries.some((entry) => entry.key === key)) continue;
|
|
25
|
+
entries.push({
|
|
26
|
+
key,
|
|
27
|
+
label:
|
|
28
|
+
field.admin.tab === undefined
|
|
29
|
+
? fallbackTab
|
|
30
|
+
: runtime.i18n.text(field.admin.tab, field.admin.tabTranslations),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return entries;
|
|
34
|
+
});
|
|
35
|
+
const accessibleLabel = $derived(
|
|
36
|
+
runtime.i18n.t("fields:sections", {
|
|
37
|
+
labels: runtime.i18n.formatList(tabs.map((tab) => tab.label)),
|
|
38
|
+
count: tabs.length,
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
let selectedTab = $state("");
|
|
42
|
+
let revealedIssueSignature = $state("");
|
|
43
|
+
const activeTab = $derived(
|
|
44
|
+
tabs.some((tab) => tab.key === selectedTab) ? selectedTab : (tabs[0]?.key ?? "")
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function issueCountForTab(tab: string) {
|
|
48
|
+
return fields.filter(
|
|
49
|
+
(field) => (field.admin.tab ?? "") === tab && form.issuesFor(field.path).length > 0
|
|
50
|
+
).length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
$effect(() => {
|
|
54
|
+
const signature = form.issues.map((issue) => `${issue.code}:${issue.path}`).join("|");
|
|
55
|
+
if (signature === "" || signature === revealedIssueSignature) return;
|
|
56
|
+
revealedIssueSignature = signature;
|
|
57
|
+
const issueTab = tabs.find((tab) => issueCountForTab(tab.key) > 0);
|
|
58
|
+
if (issueTab !== undefined) selectedTab = issueTab.key;
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<Tabs
|
|
63
|
+
class="col-span-1 mb-1 gap-0 sm:col-span-12"
|
|
64
|
+
value={activeTab}
|
|
65
|
+
onValueChange={(tab) => (selectedTab = tab)}
|
|
66
|
+
data-field-tab-group={tabGroup.id}
|
|
67
|
+
>
|
|
68
|
+
<TabsList
|
|
69
|
+
variant="line"
|
|
70
|
+
class="-ms-5 h-12 w-[calc(100%+2.5rem)] justify-start gap-6 overflow-x-auto overflow-y-hidden border-b border-control-border px-5 sm:-ms-8 sm:w-[calc(100%+4rem)] sm:px-8 lg:-ms-[60px] lg:w-[calc(100%+120px)] lg:px-[60px]"
|
|
71
|
+
aria-label={accessibleLabel}
|
|
72
|
+
>
|
|
73
|
+
{#each tabs as tab (tab.key)}
|
|
74
|
+
{const issueCount = $derived(issueCountForTab(tab.key))}
|
|
75
|
+
<TabsTrigger
|
|
76
|
+
class="h-12 flex-none rounded-none px-px text-[14px] font-medium focus-visible:bg-control focus-visible:outline-none data-[state=active]:border-b-foreground-strong!"
|
|
77
|
+
value={tab.key}
|
|
78
|
+
>
|
|
79
|
+
{tab.label}{#if issueCount > 0}<span
|
|
80
|
+
class="font-mono grid min-w-4 place-items-center rounded-full bg-destructive/14 px-1 text-[9px] text-destructive"
|
|
81
|
+
aria-label={runtime.i18n.t("fields:invalidCount", { count: issueCount })}
|
|
82
|
+
>
|
|
83
|
+
{runtime.i18n.formatNumber(issueCount)}
|
|
84
|
+
</span>{/if}
|
|
85
|
+
</TabsTrigger>
|
|
86
|
+
{/each}
|
|
87
|
+
</TabsList>
|
|
88
|
+
{#each tabs as tab (tab.key)}
|
|
89
|
+
<TabsContent value={tab.key} class="pt-6">
|
|
90
|
+
{#if activeTab === tab.key}
|
|
91
|
+
<FieldLayout
|
|
92
|
+
fields={fields.filter((field) => (field.admin.tab ?? "") === tab.key)}
|
|
93
|
+
{form}
|
|
94
|
+
suppressedTabGroupID={tabGroup.id}
|
|
95
|
+
/>
|
|
96
|
+
{/if}
|
|
97
|
+
</TabsContent>
|
|
98
|
+
{/each}
|
|
99
|
+
</Tabs>
|