@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,7 +1,11 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { RowLabelComponentProps } from "@riducms/plugin";
|
|
3
|
+
import type { AdminConfig } from "@riducms/plugin/admin";
|
|
1
4
|
import type { RowLabelPlugin, RowLabelSnapshot, RowLabelValue } from "@riducms/plugin";
|
|
2
5
|
import type { SchemaField } from "@riducms/protocol";
|
|
3
6
|
|
|
4
7
|
export class RowLabelRegistry {
|
|
8
|
+
constructor(private readonly local: NonNullable<AdminConfig["rowLabels"]> = {}) {}
|
|
5
9
|
#plugins = new Map<string, RowLabelPlugin>();
|
|
6
10
|
|
|
7
11
|
register(plugin: RowLabelPlugin) {
|
|
@@ -12,22 +16,38 @@ export class RowLabelRegistry {
|
|
|
12
16
|
this.#plugins.set(identity, plugin);
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
resolve(
|
|
19
|
+
resolve(
|
|
20
|
+
field: SchemaField
|
|
21
|
+
): { component: Component<RowLabelComponentProps>; config: unknown } | undefined {
|
|
16
22
|
const component = field.nested?.rowLabelComponent;
|
|
17
23
|
if (component === undefined) return undefined;
|
|
18
|
-
|
|
24
|
+
if (component.reference !== undefined) {
|
|
25
|
+
const local = this.local[component.reference as `app:${string}`];
|
|
26
|
+
if (local === undefined)
|
|
27
|
+
throw new Error(
|
|
28
|
+
`No local row label ${component.reference} is registered for ${field.path}.`
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
component: local.component as Component<RowLabelComponentProps>,
|
|
32
|
+
config: local.decode(field),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const identity = rowLabelIdentity(component.plugin ?? "", component.component ?? "");
|
|
19
36
|
const plugin = this.#plugins.get(identity);
|
|
20
37
|
if (plugin === undefined) {
|
|
21
38
|
throw new Error(
|
|
22
39
|
`No admin row label component can render field ${field.path} (plugin "${component.plugin}", component "${component.component}")`
|
|
23
40
|
);
|
|
24
41
|
}
|
|
25
|
-
return plugin;
|
|
42
|
+
return { component: plugin.component, config: component.config };
|
|
26
43
|
}
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
export function createRowLabelRegistry(
|
|
30
|
-
|
|
46
|
+
export function createRowLabelRegistry(
|
|
47
|
+
plugins: readonly RowLabelPlugin[] = [],
|
|
48
|
+
local: AdminConfig["rowLabels"] = {}
|
|
49
|
+
): RowLabelRegistry {
|
|
50
|
+
const registry = new RowLabelRegistry(local);
|
|
31
51
|
for (const plugin of plugins) registry.register(plugin);
|
|
32
52
|
return registry;
|
|
33
53
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { validateAdminConfig, type AdminConfig } from "@riducms/plugin/admin";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
+
resolveAdminExtensions,
|
|
3
4
|
type AdminDashboardPanel,
|
|
4
5
|
type AdminLoginComponent,
|
|
5
6
|
type AdminAccountComponent,
|
|
@@ -13,8 +14,7 @@ import {
|
|
|
13
14
|
type AdminDocumentView,
|
|
14
15
|
type AdminListCell,
|
|
15
16
|
type AdminPlugin,
|
|
16
|
-
type
|
|
17
|
-
type FieldPlugin,
|
|
17
|
+
type AdminRoute,
|
|
18
18
|
} from "@riducms/plugin";
|
|
19
19
|
import type { AuthSession, OperationCapabilities, SchemaManifest } from "@riducms/protocol";
|
|
20
20
|
import { RiduError } from "@riducms/sdk";
|
|
@@ -48,7 +48,7 @@ class StalePreferenceSessionError extends Error {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export class AdminRuntime {
|
|
51
|
-
manifest = $state<SchemaManifest>();
|
|
51
|
+
manifest = $state.raw<SchemaManifest>();
|
|
52
52
|
manifestRevision = $state(0);
|
|
53
53
|
documentRevision = $state(0);
|
|
54
54
|
session = $state<AuthSession<AdminDocument>>();
|
|
@@ -150,7 +150,7 @@ export class AdminRuntime {
|
|
|
150
150
|
return true;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
readonly pluginRoutes: readonly
|
|
153
|
+
readonly pluginRoutes: readonly AdminRoute[];
|
|
154
154
|
readonly dashboardPanels: readonly AdminDashboardPanel[];
|
|
155
155
|
readonly loginComponents: readonly AdminLoginComponent[];
|
|
156
156
|
readonly accountComponents: readonly AdminAccountComponent[];
|
|
@@ -169,14 +169,12 @@ export class AdminRuntime {
|
|
|
169
169
|
constructor(
|
|
170
170
|
readonly client: AdminClient,
|
|
171
171
|
readonly plugins: readonly AdminPlugin[] = [],
|
|
172
|
-
fieldPlugins: readonly FieldPlugin[] = [],
|
|
173
172
|
languages?: readonly TranslationLanguage[],
|
|
174
|
-
readonly fields: FieldRegistry = createCoreFieldRegistry(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
])
|
|
173
|
+
readonly fields: FieldRegistry = createCoreFieldRegistry(plugins),
|
|
174
|
+
readonly editors: NonNullable<AdminConfig["fields"]> = {},
|
|
175
|
+
readonly application: AdminConfig = {}
|
|
178
176
|
) {
|
|
179
|
-
const extensions =
|
|
177
|
+
const extensions = resolveAdminExtensions(plugins, application);
|
|
180
178
|
this.pluginRoutes = extensions.routes;
|
|
181
179
|
this.dashboardPanels = extensions.dashboard;
|
|
182
180
|
this.loginComponents = extensions.login;
|
|
@@ -190,8 +188,14 @@ export class AdminRuntime {
|
|
|
190
188
|
this.listCells = extensions.listCells;
|
|
191
189
|
this.documentActions = extensions.documentActions;
|
|
192
190
|
this.documentViews = extensions.documentViews;
|
|
193
|
-
this.rowLabels = createRowLabelRegistry(extensions.rowLabels);
|
|
194
|
-
this.i18n = new AdminI18nController(
|
|
191
|
+
this.rowLabels = createRowLabelRegistry(extensions.rowLabels, application.rowLabels);
|
|
192
|
+
this.i18n = new AdminI18nController(
|
|
193
|
+
client,
|
|
194
|
+
() => this.session,
|
|
195
|
+
languages,
|
|
196
|
+
extensions.messages,
|
|
197
|
+
extensions.applicationMessages
|
|
198
|
+
);
|
|
195
199
|
this.#applyTheme();
|
|
196
200
|
if (typeof window !== "undefined") {
|
|
197
201
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
@@ -435,6 +439,10 @@ export class AdminRuntime {
|
|
|
435
439
|
try {
|
|
436
440
|
const manifest = await this.client.schema();
|
|
437
441
|
if (request !== this.#manifestRequest) return;
|
|
442
|
+
validateAdminConfig(
|
|
443
|
+
{ ...this.application, plugins: this.plugins, fields: this.editors },
|
|
444
|
+
manifest
|
|
445
|
+
);
|
|
438
446
|
this.manifest = manifest;
|
|
439
447
|
this.manifestRevision += 1;
|
|
440
448
|
this.#configureContentLocale();
|
|
@@ -487,6 +495,10 @@ export class AdminRuntime {
|
|
|
487
495
|
try {
|
|
488
496
|
const manifest = await this.client.schema();
|
|
489
497
|
if (request !== this.#manifestRequest) return;
|
|
498
|
+
validateAdminConfig(
|
|
499
|
+
{ ...this.application, plugins: this.plugins, fields: this.editors },
|
|
500
|
+
manifest
|
|
501
|
+
);
|
|
490
502
|
this.manifest = manifest;
|
|
491
503
|
this.manifestRevision += 1;
|
|
492
504
|
this.#configureContentLocale();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from "svelte";
|
|
3
|
+
import { connectDocumentLiveValidation } from "@admin/core/forms/live-validation.svelte";
|
|
2
4
|
import { useNavigate } from "@hvniel/svelte-router";
|
|
3
5
|
import PaletteIcon from "~icons/lucide/palette";
|
|
4
6
|
import RotateCcwIcon from "~icons/lucide/rotate-ccw";
|
|
@@ -19,6 +21,8 @@
|
|
|
19
21
|
const notifications = getNotificationCenter();
|
|
20
22
|
const navigate = useNavigate();
|
|
21
23
|
const form = new FormController({}, runtime.i18n);
|
|
24
|
+
connectDocumentLiveValidation(form, runtime.client);
|
|
25
|
+
$effect(() => () => form.disposeBindings());
|
|
22
26
|
let loading = $state(true);
|
|
23
27
|
let error = $state<string>();
|
|
24
28
|
let profilePending = $state(false);
|
|
@@ -69,13 +73,15 @@
|
|
|
69
73
|
const locale = contentLocale;
|
|
70
74
|
if (slug === undefined || id === undefined) return;
|
|
71
75
|
const abort = new AbortController();
|
|
72
|
-
|
|
76
|
+
untrack(() => load(slug, id, locale, abort.signal));
|
|
73
77
|
return () => abort.abort();
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
$effect(() => runtime.registerContentLocaleBlocker(() => form.dirty || form.submitting));
|
|
77
81
|
|
|
78
82
|
async function load(slug: string, id: string, locale: string | undefined, signal: AbortSignal) {
|
|
83
|
+
form.setResource({ collection: slug, id });
|
|
84
|
+
form.setLocalization(locale);
|
|
79
85
|
loading = true;
|
|
80
86
|
error = undefined;
|
|
81
87
|
try {
|
|
@@ -84,7 +90,7 @@
|
|
|
84
90
|
runtime.client.collectionAccess(slug, { id, signal, locale }),
|
|
85
91
|
]);
|
|
86
92
|
if (signal.aborted) return;
|
|
87
|
-
form.reset(document);
|
|
93
|
+
form.reset(document, collection?.fields ?? []);
|
|
88
94
|
form.setLocalization(locale, document._localization?.sources);
|
|
89
95
|
form.setAccess(access, "update");
|
|
90
96
|
} catch (cause) {
|
|
@@ -249,10 +255,9 @@
|
|
|
249
255
|
class="justify-self-start"
|
|
250
256
|
type="submit"
|
|
251
257
|
disabled={profilePending || !form.dirty || form.access?.operations.update !== true}
|
|
258
|
+
aria-busy={form.submitting}
|
|
252
259
|
>
|
|
253
|
-
{
|
|
254
|
-
? runtime.i18n.t("account:savingProfile")
|
|
255
|
-
: runtime.i18n.t("account:saveProfile")}
|
|
260
|
+
{runtime.i18n.t("account:saveProfile")}
|
|
256
261
|
</Button>
|
|
257
262
|
</form>
|
|
258
263
|
{/if}
|
|
@@ -333,7 +333,7 @@
|
|
|
333
333
|
<span class="ridu-field-label">{runtime.i18n.t("account:expiration")}</span>
|
|
334
334
|
<DateValueControl
|
|
335
335
|
id="api-key-expiry"
|
|
336
|
-
appearance="
|
|
336
|
+
appearance="date-time"
|
|
337
337
|
value={apiKeyExpiry}
|
|
338
338
|
label={runtime.i18n.t("account:expiration")}
|
|
339
339
|
onValueChange={(next) => (apiKeyExpiry = next)}
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
let wasOpen = $state(false);
|
|
37
37
|
const field = $derived(fields.find((candidate) => candidate.name === fieldName));
|
|
38
38
|
const fieldLabel = $derived(field?.admin.label ?? runtime.i18n.t("collections:chooseField"));
|
|
39
|
-
const
|
|
40
|
-
field?.select?.
|
|
39
|
+
const selectedOptionLabel = $derived(
|
|
40
|
+
field?.select?.options.find((option) => option.value === value)?.label ??
|
|
41
41
|
(value === "" ? runtime.i18n.t("collections:clearValue") : value)
|
|
42
42
|
);
|
|
43
43
|
|
|
@@ -104,13 +104,13 @@
|
|
|
104
104
|
<Select type="single" {value} onValueChange={(next) => (value = next)}>
|
|
105
105
|
<SelectTrigger class="w-full" aria-label={runtime.i18n.t("collections:value")}>
|
|
106
106
|
<span class={value === "" ? "text-foreground-placeholder" : undefined}>
|
|
107
|
-
{
|
|
107
|
+
{selectedOptionLabel}
|
|
108
108
|
</span>
|
|
109
109
|
</SelectTrigger>
|
|
110
110
|
<SelectContent>
|
|
111
111
|
<SelectItem value="" label={runtime.i18n.t("collections:clearValue")} />
|
|
112
|
-
{#each field.select?.
|
|
113
|
-
<SelectItem value={
|
|
112
|
+
{#each field.select?.options ?? [] as option (option.value)}
|
|
113
|
+
<SelectItem value={option.value} label={option.label} />
|
|
114
114
|
{/each}
|
|
115
115
|
</SelectContent>
|
|
116
116
|
</Select>
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
{:else if field?.type === "date"}
|
|
130
130
|
<DateValueControl
|
|
131
131
|
id={`bulk-edit-${field.id}`}
|
|
132
|
-
appearance={field.date?.
|
|
132
|
+
appearance={field.date?.format}
|
|
133
133
|
{value}
|
|
134
134
|
label={runtime.i18n.t("collections:value")}
|
|
135
135
|
onValueChange={(next) => (value = next)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccessCapabilitiesEnvelope, Pagination,
|
|
1
|
+
import type { AccessCapabilitiesEnvelope, Pagination, SchemaSelectOption } from "@riducms/protocol";
|
|
2
2
|
import type { AdminI18n } from "@riducms/translations";
|
|
3
3
|
|
|
4
4
|
import type { AdminClient, AdminDocument } from "@admin/core/api/admin-client";
|
|
@@ -15,7 +15,7 @@ interface CollectionListControllerOptions {
|
|
|
15
15
|
get titleField(): string | undefined;
|
|
16
16
|
get status(): string;
|
|
17
17
|
get statusField(): string | undefined;
|
|
18
|
-
get
|
|
18
|
+
get statusOptions(): readonly SchemaSelectOption[];
|
|
19
19
|
get versioned(): boolean;
|
|
20
20
|
get trashOnly(): boolean;
|
|
21
21
|
get folderField(): string | undefined;
|
|
@@ -92,7 +92,7 @@ export class CollectionListController {
|
|
|
92
92
|
titleField: this.options.titleField,
|
|
93
93
|
status: this.options.status,
|
|
94
94
|
statusField: this.options.statusField,
|
|
95
|
-
statusValues: this.options.
|
|
95
|
+
statusValues: this.options.statusOptions.map((option) => option.value),
|
|
96
96
|
trashOnly: this.options.trashOnly,
|
|
97
97
|
folderField: this.options.folderField,
|
|
98
98
|
folderID: this.options.folderID,
|
|
@@ -278,7 +278,7 @@ export class CollectionListController {
|
|
|
278
278
|
};
|
|
279
279
|
|
|
280
280
|
statusLabel = (value: string) => {
|
|
281
|
-
const configured = this.options.
|
|
281
|
+
const configured = this.options.statusOptions.find((option) => option.value === value)?.label;
|
|
282
282
|
if (configured !== undefined) return configured;
|
|
283
283
|
const normalized = value.toLocaleLowerCase(this.options.i18n.language);
|
|
284
284
|
if (normalized === "published") return this.options.i18n.t("documents:published");
|
|
@@ -296,7 +296,7 @@ export class CollectionListController {
|
|
|
296
296
|
updated = (document: AdminDocument) => formatUpdated(document.updatedAt, this.options.i18n);
|
|
297
297
|
created = (document: AdminDocument) => formatUpdated(document.createdAt, this.options.i18n);
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
optionCount = (option: SchemaSelectOption) => this.statusCounts[option.value];
|
|
300
300
|
|
|
301
301
|
toggleDocument = (id: string, checked: boolean) => {
|
|
302
302
|
if (!this.selectionControlsReady || !this.docs.some((document) => document.id === id)) return;
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
function addFilter() {
|
|
76
76
|
if (field === undefined) return;
|
|
77
77
|
const filterValue = operator === "exists" ? value || "true" : value;
|
|
78
|
-
if (operator !== "exists" && filterValue === "") return;
|
|
78
|
+
if (operator !== "exists" && filterValue === "" && field?.type !== "text-list") return;
|
|
79
79
|
onFiltersChange([...filters, { field: field.path, operator, value: filterValue }]);
|
|
80
80
|
value = "";
|
|
81
81
|
}
|
|
@@ -163,21 +163,21 @@
|
|
|
163
163
|
aria-label={i18n.t("collections:filterValue")}
|
|
164
164
|
>
|
|
165
165
|
<span class={value === "" ? "text-foreground-placeholder" : undefined}>
|
|
166
|
-
{field.select?.
|
|
166
|
+
{field.select?.options.find((option) => option.value === value)?.label ??
|
|
167
167
|
i18n.t("collections:chooseValue")}
|
|
168
168
|
</span>
|
|
169
169
|
</SelectTrigger>
|
|
170
170
|
<SelectContent>
|
|
171
171
|
<SelectItem value="" label={i18n.t("collections:chooseValue")} />
|
|
172
|
-
{#each field.select?.
|
|
173
|
-
<SelectItem value={
|
|
172
|
+
{#each field.select?.options ?? [] as option}
|
|
173
|
+
<SelectItem value={option.value} label={option.label} />
|
|
174
174
|
{/each}
|
|
175
175
|
</SelectContent>
|
|
176
176
|
</Select>
|
|
177
177
|
{:else if field.type === "date"}
|
|
178
178
|
<DateValueControl
|
|
179
179
|
id={`collection-filter-${field.id}`}
|
|
180
|
-
appearance={field.date?.
|
|
180
|
+
appearance={field.date?.format}
|
|
181
181
|
{value}
|
|
182
182
|
label={i18n.t("collections:filterValue")}
|
|
183
183
|
size="toolbar"
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
{:else}
|
|
187
187
|
<Input
|
|
188
188
|
aria-label={i18n.t("collections:filterValue")}
|
|
189
|
-
type={field.type === "number" ? "number" : "text"}
|
|
189
|
+
type={field.type === "number" || field.type === "number-list" ? "number" : "text"}
|
|
190
190
|
class="h-8"
|
|
191
191
|
bind:value
|
|
192
192
|
/>
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
const statusField = $derived(
|
|
66
66
|
collection?.fields.find((field) => field.type === "select" && field.name === "status")
|
|
67
67
|
);
|
|
68
|
-
const
|
|
68
|
+
const statusOptions = $derived(statusField?.select?.options ?? []);
|
|
69
69
|
const searchParams = $derived(new URLSearchParams(location.search));
|
|
70
70
|
const localization = $derived(runtime.manifest?.application.localization);
|
|
71
71
|
const requestedLocale = $derived(searchParams.get("locale"));
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
let referenceLabels = $state.raw<Record<string, string>>({});
|
|
96
96
|
let searchInput = $state<HTMLInputElement | null>(null);
|
|
97
97
|
const activeStatus = $derived(
|
|
98
|
-
|
|
98
|
+
statusOptions.some((option) => option.value === requestedStatus) ? requestedStatus : ""
|
|
99
99
|
);
|
|
100
100
|
const bulkEditableFields = $derived(
|
|
101
101
|
collection?.fields.filter(
|
|
@@ -183,8 +183,8 @@
|
|
|
183
183
|
get statusField() {
|
|
184
184
|
return statusField?.name;
|
|
185
185
|
},
|
|
186
|
-
get
|
|
187
|
-
return
|
|
186
|
+
get statusOptions() {
|
|
187
|
+
return statusOptions;
|
|
188
188
|
},
|
|
189
189
|
get versioned() {
|
|
190
190
|
return collection?.capabilities.versions === true;
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
statusColumnAvailable,
|
|
225
225
|
} = $derived(controller);
|
|
226
226
|
const {
|
|
227
|
-
|
|
227
|
+
optionCount,
|
|
228
228
|
canReadField,
|
|
229
229
|
deletePermanent,
|
|
230
230
|
bulkDelete,
|
|
@@ -512,13 +512,12 @@
|
|
|
512
512
|
return value === true ? runtime.i18n.t("general:yes") : runtime.i18n.t("general:no");
|
|
513
513
|
}
|
|
514
514
|
if (field.type === "select" || field.type === "radio") {
|
|
515
|
-
return field.select?.
|
|
515
|
+
return field.select?.options.find((option) => option.value === value)?.label ?? String(value);
|
|
516
516
|
}
|
|
517
517
|
if (field.type === "array") return summarizeRows(value, "row");
|
|
518
518
|
if (field.type === "blocks") return summarizeRows(value, "block");
|
|
519
519
|
if (field.type === "json" || field.plugin !== undefined) return summarizeStructured(value);
|
|
520
|
-
if (field.type === "date")
|
|
521
|
-
return formatDateDisplay(value, field.date?.pickerAppearance, runtime.i18n);
|
|
520
|
+
if (field.type === "date") return formatDateDisplay(value, field.date?.format, runtime.i18n);
|
|
522
521
|
if (referenceField(field)) {
|
|
523
522
|
return runtime.i18n.formatList(
|
|
524
523
|
fieldReferences(field, value).map(
|
|
@@ -769,7 +768,7 @@
|
|
|
769
768
|
</div>
|
|
770
769
|
</header>
|
|
771
770
|
|
|
772
|
-
{#if
|
|
771
|
+
{#if statusOptions.length > 0 && !trashOnly}
|
|
773
772
|
<div
|
|
774
773
|
class="mt-6 flex h-9 w-full items-center justify-start gap-5 border-b border-control-border"
|
|
775
774
|
role="group"
|
|
@@ -786,15 +785,15 @@
|
|
|
786
785
|
{runtime.i18n.formatNumber(allDocuments)}
|
|
787
786
|
</span>
|
|
788
787
|
</button>
|
|
789
|
-
{#each
|
|
790
|
-
{const statusCount = $derived(
|
|
788
|
+
{#each statusOptions as option (option.value)}
|
|
789
|
+
{const statusCount = $derived(optionCount(option))}
|
|
791
790
|
<button
|
|
792
791
|
type="button"
|
|
793
792
|
class="relative h-9 flex-none cursor-pointer px-px pb-2.5 text-[13px] text-foreground-muted outline-none after:absolute after:inset-x-0 after:bottom-[-1px] after:h-0.5 after:bg-primary after:opacity-0 after:transition-opacity hover:text-foreground-strong focus-visible:outline-2 focus-visible:outline-primary/60 focus-visible:outline-offset-2 aria-pressed:text-foreground-strong aria-pressed:after:opacity-100"
|
|
794
|
-
aria-pressed={activeStatus ===
|
|
795
|
-
onclick={() => selectStatus(
|
|
793
|
+
aria-pressed={activeStatus === option.value}
|
|
794
|
+
onclick={() => selectStatus(option.value)}
|
|
796
795
|
>
|
|
797
|
-
{
|
|
796
|
+
{option.label}
|
|
798
797
|
{#if statusCount !== undefined}
|
|
799
798
|
<span class="font-mono ms-1 text-[9.5px] text-foreground-faint">
|
|
800
799
|
{runtime.i18n.formatNumber(statusCount)}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
1
2
|
import type { SchemaCollection, SchemaField } from "@riducms/protocol";
|
|
2
3
|
import type { AdminI18n } from "@riducms/translations";
|
|
3
4
|
|
|
@@ -13,7 +14,8 @@ export type ListFilterOperator =
|
|
|
13
14
|
| "greaterThanEqual"
|
|
14
15
|
| "lessThan"
|
|
15
16
|
| "lessThanEqual"
|
|
16
|
-
| "exists"
|
|
17
|
+
| "exists"
|
|
18
|
+
| "in";
|
|
17
19
|
|
|
18
20
|
export interface ListFilter {
|
|
19
21
|
field: string;
|
|
@@ -58,6 +60,7 @@ export function listColumnFields(collection: SchemaCollection | undefined, title
|
|
|
58
60
|
|
|
59
61
|
export function filterableFields(fields: readonly SchemaField[]) {
|
|
60
62
|
return fields.flatMap((field) => {
|
|
63
|
+
if (field.queryRestricted) return [];
|
|
61
64
|
if (field.type === "group" || field.type === "array" || field.type === "blocks") {
|
|
62
65
|
return nestedFields(field, "filters");
|
|
63
66
|
}
|
|
@@ -67,8 +70,11 @@ export function filterableFields(fields: readonly SchemaField[]) {
|
|
|
67
70
|
|
|
68
71
|
export function sortableField(field: SchemaField) {
|
|
69
72
|
return (
|
|
73
|
+
field.queryRestricted !== true &&
|
|
70
74
|
field.virtual === undefined &&
|
|
71
75
|
field.join === undefined &&
|
|
76
|
+
field.type !== "text-list" &&
|
|
77
|
+
field.type !== "number-list" &&
|
|
72
78
|
field.type !== "array" &&
|
|
73
79
|
field.type !== "blocks" &&
|
|
74
80
|
field.type !== "group" &&
|
|
@@ -96,6 +102,7 @@ export function defaultListColumns(
|
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
export function filterOperatorsFor(field: SchemaField): readonly ListFilterOperator[] {
|
|
105
|
+
if (field.type === "text-list" || field.type === "number-list") return ["in", "exists"];
|
|
99
106
|
if (field.type === "number" || field.type === "date") return orderedOperators;
|
|
100
107
|
if (field.type === "text" || field.type === "textarea" || field.type === "email") {
|
|
101
108
|
return textOperators;
|
|
@@ -105,6 +112,7 @@ export function filterOperatorsFor(field: SchemaField): readonly ListFilterOpera
|
|
|
105
112
|
|
|
106
113
|
export function filterOperatorLabel(operator: ListFilterOperator, i18n: AdminI18n) {
|
|
107
114
|
const labels: Record<ListFilterOperator, Parameters<AdminI18n["t"]>[0]> = {
|
|
115
|
+
in: "collections:operatorIncludesItem",
|
|
108
116
|
equals: "collections:operatorEquals",
|
|
109
117
|
notEquals: "collections:operatorNotEquals",
|
|
110
118
|
like: "collections:operatorLike",
|
|
@@ -165,19 +173,19 @@ export function buildListFilterWhere(
|
|
|
165
173
|
const byName = new Map(fields.map((field) => [field.path, field]));
|
|
166
174
|
return filters.flatMap((filter): Record<string, unknown>[] => {
|
|
167
175
|
const field = byName.get(filter.field);
|
|
168
|
-
if (field === undefined) return [];
|
|
176
|
+
if (field === undefined || !filterOperatorsFor(field).includes(filter.operator)) return [];
|
|
169
177
|
if (filter.operator === "exists") {
|
|
170
178
|
return [{ [filter.field]: { exists: filter.value !== "false" } }];
|
|
171
179
|
}
|
|
172
|
-
if (filter.value === "") return [];
|
|
180
|
+
if (filter.value === "" && field.type !== "text-list") return [];
|
|
173
181
|
let value: string | number | boolean = filter.value;
|
|
174
|
-
if (field.type === "number") {
|
|
182
|
+
if (field.type === "number" || field.type === "number-list") {
|
|
175
183
|
value = Number(filter.value);
|
|
176
184
|
if (!Number.isFinite(value)) return [];
|
|
177
185
|
} else if (field.type === "checkbox") {
|
|
178
186
|
value = filter.value === "true";
|
|
179
187
|
}
|
|
180
|
-
return [{ [filter.field]: { [filter.operator]: value } }];
|
|
188
|
+
return [{ [filter.field]: { [filter.operator]: filter.operator === "in" ? [value] : value } }];
|
|
181
189
|
});
|
|
182
190
|
}
|
|
183
191
|
|
|
@@ -222,16 +230,21 @@ export function visibleColumnNames(encoded: string | null, fallback: readonly st
|
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
function nestedFields(field: SchemaField, mode: "columns" | "filters"): SchemaField[] {
|
|
233
|
+
if (mode === "filters" && field.queryRestricted) return [];
|
|
225
234
|
if (field.type === "blocks") {
|
|
226
235
|
if (mode === "columns") return [];
|
|
227
|
-
return (field.blocks
|
|
236
|
+
return (resolveBlockTypes(field.blocks) ?? []).flatMap((block) =>
|
|
228
237
|
block.fields.flatMap((child) =>
|
|
229
|
-
withListLabel(child, `${field.admin.label} > ${block.
|
|
238
|
+
withListLabel(child, `${field.admin.label} > ${block.labels.singular}`, mode)
|
|
230
239
|
)
|
|
231
240
|
);
|
|
232
241
|
}
|
|
233
242
|
return (field.nested?.fields ?? []).flatMap((child) =>
|
|
234
|
-
withListLabel(
|
|
243
|
+
withListLabel(
|
|
244
|
+
field.queryRestricted ? { ...child, queryRestricted: true } : child,
|
|
245
|
+
field.admin.label,
|
|
246
|
+
mode
|
|
247
|
+
)
|
|
235
248
|
);
|
|
236
249
|
}
|
|
237
250
|
|
|
@@ -254,6 +267,7 @@ function withListLabel(
|
|
|
254
267
|
|
|
255
268
|
function filterableLeaf(field: SchemaField) {
|
|
256
269
|
return (
|
|
270
|
+
field.queryRestricted !== true &&
|
|
257
271
|
field.virtual === undefined &&
|
|
258
272
|
field.join === undefined &&
|
|
259
273
|
field.plugin === undefined &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectDocumentLiveValidation } from "@admin/core/forms/live-validation.svelte";
|
|
1
2
|
import type { DocumentLockEnvelope, SchemaCollection } from "@riducms/protocol";
|
|
2
3
|
import { RiduError } from "@riducms/sdk";
|
|
3
4
|
import { tick, untrack } from "svelte";
|
|
@@ -85,6 +86,7 @@ export class DocumentController {
|
|
|
85
86
|
|
|
86
87
|
constructor(readonly options: DocumentControllerOptions) {
|
|
87
88
|
this.form = new FormController({}, options.runtime.i18n);
|
|
89
|
+
connectDocumentLiveValidation(this.form, options.runtime.client);
|
|
88
90
|
$effect(() => {
|
|
89
91
|
const slug = this.options.slug;
|
|
90
92
|
const documentID = this.options.global ? slug : this.options.documentID;
|
|
@@ -146,6 +148,7 @@ export class DocumentController {
|
|
|
146
148
|
});
|
|
147
149
|
|
|
148
150
|
$effect(() => () => {
|
|
151
|
+
this.form.disposeBindings();
|
|
149
152
|
this.#saveRequest?.abort();
|
|
150
153
|
this.#imageRequest?.abort();
|
|
151
154
|
if (this.#copyResetTimer !== undefined) window.clearTimeout(this.#copyResetTimer);
|
|
@@ -929,7 +932,7 @@ export class DocumentController {
|
|
|
929
932
|
this.imageOutcomeUncertain = false;
|
|
930
933
|
this.form.setAccess(undefined, documentID === undefined ? "create" : "update");
|
|
931
934
|
if (documentID === undefined) {
|
|
932
|
-
this.form.reset(initialFormValues(collection.fields));
|
|
935
|
+
this.form.reset(initialFormValues(collection.fields), collection.fields);
|
|
933
936
|
this.form.setLocalization(locale);
|
|
934
937
|
this.versions = [];
|
|
935
938
|
this.currentDocument = undefined;
|
|
@@ -1179,7 +1182,10 @@ export class DocumentController {
|
|
|
1179
1182
|
|
|
1180
1183
|
#applyDocument(document: AdminDocument, recoverDraft = false) {
|
|
1181
1184
|
this.currentDocument = document;
|
|
1182
|
-
this.form.reset(
|
|
1185
|
+
this.form.reset(
|
|
1186
|
+
documentFormValues(this.collection?.fields ?? [], document),
|
|
1187
|
+
this.collection?.fields ?? []
|
|
1188
|
+
);
|
|
1183
1189
|
this.form.setLocalization(this.contentLocale, document._localization?.sources);
|
|
1184
1190
|
this.currentRevision = typeof document._revision === "number" ? document._revision : 0;
|
|
1185
1191
|
this.currentStatus = document._status === "draft" ? "draft" : "published";
|