@riducms/plugin 0.2.3 → 0.3.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/README.md +101 -68
- package/package.json +5 -4
- package/src/admin-extensions/documents.ts +42 -0
- package/src/admin-extensions/index.ts +81 -0
- package/src/admin-extensions/lists.ts +56 -0
- package/src/admin-extensions/shared.ts +48 -0
- package/src/admin-extensions/shell.ts +72 -0
- package/src/admin-extensions/views.ts +145 -0
- package/src/admin.ts +46 -14
- package/src/authoring/v1.ts +3 -3
- package/src/authoring.ts +27 -4
- package/src/editor/registry.ts +8 -8
- package/src/editor/types.ts +1 -1
- package/src/field.ts +8 -8
- package/src/index.ts +11 -5
- package/src/plugin-registry.ts +10 -11
- package/src/plugin.ts +70 -374
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { SchemaCollection, SchemaGlobal } from "@riducms/protocol";
|
|
2
|
+
import type { Component, Snippet } from "svelte";
|
|
3
|
+
|
|
4
|
+
import type { FieldDocument } from "../authoring";
|
|
5
|
+
import type { AdminI18n, ExtensionTranslationKey } from "../i18n";
|
|
6
|
+
import type {
|
|
7
|
+
AdminExtensionNotificationTone,
|
|
8
|
+
AdminExtensionProps,
|
|
9
|
+
AdminViewComponent,
|
|
10
|
+
} from "./shared";
|
|
11
|
+
|
|
12
|
+
/** Sign-in operations provided to a custom login screen. */
|
|
13
|
+
export interface AdminLoginExtensionHost {
|
|
14
|
+
/** Sign in, check admin access and enter the admin. Rejects if authentication/access fails. */
|
|
15
|
+
login: (credentials: { email: string; password: string }) => Promise<void>;
|
|
16
|
+
/** Show a temporary success or error message. Does not perform an operation itself. */
|
|
17
|
+
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AdminLoginComponentProps extends AdminExtensionProps {
|
|
21
|
+
/** Render with `{@render defaultView()}` to keep Ridu's sign-in screen inside your wrapper. */
|
|
22
|
+
defaultView: Snippet;
|
|
23
|
+
host: AdminLoginExtensionHost;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Add content before/after sign-in, or replace it with a custom screen. */
|
|
27
|
+
export interface AdminLoginComponent {
|
|
28
|
+
key: string;
|
|
29
|
+
component: Component<AdminLoginComponentProps>;
|
|
30
|
+
/** Defaults to after. Only one replace entry is allowed across plugins and the application. */
|
|
31
|
+
position?: "before" | "after" | "replace";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AdminAccountSurface = "profile" | "security";
|
|
35
|
+
|
|
36
|
+
/** Operations for keeping a custom account screen in sync with the signed-in session. */
|
|
37
|
+
export interface AdminAccountExtensionHost {
|
|
38
|
+
/** Reloads the signed-in document and updates the shell identity. */
|
|
39
|
+
refreshUser: () => Promise<FieldDocument | undefined>;
|
|
40
|
+
/** Ends the current session and returns to sign-in. */
|
|
41
|
+
logout: () => Promise<void>;
|
|
42
|
+
/** Show a temporary success or error message. */
|
|
43
|
+
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AdminAccountComponentProps extends AdminExtensionProps {
|
|
47
|
+
surface: AdminAccountSurface;
|
|
48
|
+
/** Render with `{@render defaultView()}` to keep Ridu's account screen inside your wrapper. */
|
|
49
|
+
defaultView: Snippet;
|
|
50
|
+
host: AdminAccountExtensionHost;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Add to or replace the profile/security screen identified by `surface`. */
|
|
54
|
+
export interface AdminAccountComponent {
|
|
55
|
+
key: string;
|
|
56
|
+
surface: AdminAccountSurface;
|
|
57
|
+
component: Component<AdminAccountComponentProps>;
|
|
58
|
+
/** Defaults to after. Each account surface permits at most one replacement. */
|
|
59
|
+
position?: "before" | "after" | "replace";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type AdminCoreViewSurface =
|
|
63
|
+
"collectionList" | "collectionCreate" | "collectionEdit" | "global" | "notFound";
|
|
64
|
+
|
|
65
|
+
/** Refresh tools for a custom collection/global screen. They do not save documents. */
|
|
66
|
+
export interface AdminCoreViewHost {
|
|
67
|
+
/** Fetch the current Go schema again and update the admin's schema-dependent UI. */
|
|
68
|
+
refreshManifest: () => Promise<void>;
|
|
69
|
+
/** Notify Ridu that your code changed documents so dependent lists/lookups can refresh. */
|
|
70
|
+
documentsChanged: () => void;
|
|
71
|
+
/** Show a temporary success or error message. */
|
|
72
|
+
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AdminCoreViewProps extends AdminExtensionProps {
|
|
76
|
+
surface: AdminCoreViewSurface;
|
|
77
|
+
collection?: SchemaCollection;
|
|
78
|
+
global?: SchemaGlobal;
|
|
79
|
+
documentID?: string;
|
|
80
|
+
/** Render with `{@render defaultView()}` to wrap the normal screen rather than rebuilding it. */
|
|
81
|
+
defaultView: Snippet;
|
|
82
|
+
host: AdminCoreViewHost;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface AdminCollectionCoreView {
|
|
86
|
+
key: string;
|
|
87
|
+
surface: "collectionList" | "collectionCreate" | "collectionEdit";
|
|
88
|
+
/** Omit to replace this surface for every collection. */
|
|
89
|
+
collection?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface AdminGlobalCoreView {
|
|
93
|
+
key: string;
|
|
94
|
+
surface: "global";
|
|
95
|
+
/** Omit to replace this surface for every global. */
|
|
96
|
+
global?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface AdminNotFoundCoreView {
|
|
100
|
+
key: string;
|
|
101
|
+
surface: "notFound";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Replace a collection/global/not-found screen. A resource-specific registration
|
|
106
|
+
* wins over an all-resources fallback for that surface; duplicate targets fail.
|
|
107
|
+
* The supplied `defaultView` lets your component wrap the existing screen.
|
|
108
|
+
*/
|
|
109
|
+
export type AdminCoreView = (
|
|
110
|
+
AdminCollectionCoreView | AdminGlobalCoreView | AdminNotFoundCoreView
|
|
111
|
+
) &
|
|
112
|
+
AdminViewComponent<AdminCoreViewProps>;
|
|
113
|
+
|
|
114
|
+
export interface AdminDashboardPanelProps {
|
|
115
|
+
manifest: AdminExtensionProps["manifest"];
|
|
116
|
+
user?: FieldDocument;
|
|
117
|
+
i18n: AdminI18n;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Add dashboard content before/after Ridu's overview, or replace that overview. */
|
|
121
|
+
interface AdminDashboardPanelRegistration {
|
|
122
|
+
/** Unique name among dashboard entries; duplicates throw. Array order controls display order. */
|
|
123
|
+
key: string;
|
|
124
|
+
/** Defaults to after. Replace hides Ridu's overview and may only be registered once. */
|
|
125
|
+
position?: "before" | "after" | "replace";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type AdminDashboardPanel = AdminDashboardPanelRegistration &
|
|
129
|
+
AdminViewComponent<AdminDashboardPanelProps>;
|
|
130
|
+
|
|
131
|
+
export interface AdminRouteNavigation {
|
|
132
|
+
/** Human-readable navigation label inside the authenticated admin shell. */
|
|
133
|
+
label: string;
|
|
134
|
+
labelKey?: ExtensionTranslationKey;
|
|
135
|
+
/** Optional grouping hint reserved for shells that render grouped plugin navigation. */
|
|
136
|
+
group?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One statically bundled route mounted beneath the authenticated admin layout. */
|
|
140
|
+
export type AdminRoute = {
|
|
141
|
+
/** Relative admin path, without a leading slash. Paired plugins must match their Go descriptor. */
|
|
142
|
+
path: string;
|
|
143
|
+
/** Optional navigation entry; omit it for a route reachable only by links or redirects. */
|
|
144
|
+
navigation?: AdminRouteNavigation;
|
|
145
|
+
} & AdminViewComponent<AdminExtensionProps>;
|
package/src/admin.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
/**
|
|
28
28
|
* Settings for your application's admin, exported from `admin/src/admin.config.ts`.
|
|
29
29
|
* Register your custom components here. Keep `generatedAdminPlugins` in `plugins`
|
|
30
|
-
* to load installed plugins such as rich text. Use `
|
|
30
|
+
* to load installed plugins such as rich text. Use `fieldEditors` to replace inputs for
|
|
31
31
|
* text, textarea, email, date, code, number and checkbox fields.
|
|
32
32
|
*/
|
|
33
33
|
export interface AdminConfig extends AdminContributions, FieldEditorConfig {
|
|
@@ -46,8 +46,8 @@ export interface AdminConfig extends AdminContributions, FieldEditorConfig {
|
|
|
46
46
|
*
|
|
47
47
|
* Export the result as the default export of `admin/src/admin.config.ts`. Keep
|
|
48
48
|
* `plugins: generatedAdminPlugins` to load installed packages such as rich text.
|
|
49
|
-
* Add `
|
|
50
|
-
* made with `defineRowLabel`, or options such as `
|
|
49
|
+
* Add `fieldEditors` for inputs made with `defineFieldEditor`, `rowLabels` for headings
|
|
50
|
+
* made with `defineRowLabel`, or options such as `dashboardPanels`, `routes`, and
|
|
51
51
|
* `documentActions` for other parts of the admin. All options are optional.
|
|
52
52
|
*
|
|
53
53
|
* Application field and row-label keys use `app:name`; select the same key in Go
|
|
@@ -70,7 +70,7 @@ export interface AdminConfig extends AdminContributions, FieldEditorConfig {
|
|
|
70
70
|
*
|
|
71
71
|
* export default defineAdmin({
|
|
72
72
|
* plugins: generatedAdminPlugins,
|
|
73
|
-
*
|
|
73
|
+
* dashboardPanels: [{ key: 'welcome', component: WelcomePanel, position: 'before' }]
|
|
74
74
|
* });
|
|
75
75
|
* ```
|
|
76
76
|
*/
|
|
@@ -81,8 +81,8 @@ export function defineAdmin(config: AdminConfig): AdminConfig {
|
|
|
81
81
|
/** Static configuration owned by one mounted admin. Changes require remount/HMR. */
|
|
82
82
|
export interface ResolvedAdminConfig {
|
|
83
83
|
readonly plugins: readonly AdminPlugin[];
|
|
84
|
-
readonly
|
|
85
|
-
readonly
|
|
84
|
+
readonly pluginFields: readonly ResolvedPluginField[];
|
|
85
|
+
readonly fieldEditors: NonNullable<AdminConfig["fieldEditors"]>;
|
|
86
86
|
readonly rowLabels: NonNullable<AdminConfig["rowLabels"]>;
|
|
87
87
|
readonly languages: readonly TranslationLanguage[];
|
|
88
88
|
readonly extensions: Readonly<ResolvedAdminExtensions>;
|
|
@@ -102,8 +102,8 @@ export function resolveAdminConfig(config: AdminConfig = {}): ResolvedAdminConfi
|
|
|
102
102
|
const plugins = Object.freeze([...(config.plugins ?? [])]);
|
|
103
103
|
return Object.freeze({
|
|
104
104
|
plugins,
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
pluginFields: resolvePluginFields(plugins),
|
|
106
|
+
fieldEditors: Object.freeze({ ...config.fieldEditors }),
|
|
107
107
|
rowLabels: Object.freeze({ ...config.rowLabels }),
|
|
108
108
|
languages: Object.freeze(config.languages?.length ? [...config.languages] : [en]),
|
|
109
109
|
extensions: Object.freeze(resolveAdminExtensions(plugins, config)),
|
|
@@ -120,22 +120,22 @@ export function resolveAdminConfig(config: AdminConfig = {}): ResolvedAdminConfi
|
|
|
120
120
|
export function validateAdminManifest(
|
|
121
121
|
config: ResolvedAdminConfig,
|
|
122
122
|
manifest: Pick<SchemaManifest, "collections" | "globals" | "blocks"> &
|
|
123
|
-
Partial<Pick<SchemaManifest, "plugins">>,
|
|
123
|
+
Partial<Pick<SchemaManifest, "plugins" | "application">>,
|
|
124
124
|
options: { completeManifest?: boolean } = {}
|
|
125
125
|
): void {
|
|
126
126
|
validateManifestPluginPairs(
|
|
127
127
|
config.plugins,
|
|
128
|
-
config.
|
|
128
|
+
config.pluginFields,
|
|
129
129
|
manifest,
|
|
130
130
|
options.completeManifest === true
|
|
131
131
|
);
|
|
132
|
-
const validatePluginField = createPluginFieldValidator(config.
|
|
132
|
+
const validatePluginField = createPluginFieldValidator(config.pluginFields);
|
|
133
133
|
const failures: string[] = [];
|
|
134
134
|
bindSchemaManifest(manifest);
|
|
135
135
|
const inspect = (fields: readonly SchemaField[], owner: string) => {
|
|
136
136
|
for (const field of fields) {
|
|
137
137
|
try {
|
|
138
|
-
validateFieldEditorSelection(config.
|
|
138
|
+
validateFieldEditorSelection(config.fieldEditors, field);
|
|
139
139
|
validatePluginField(field);
|
|
140
140
|
const selection = field.nested?.rowLabelComponent;
|
|
141
141
|
if (selection?.reference !== undefined) {
|
|
@@ -174,14 +174,46 @@ export function validateAdminManifest(
|
|
|
174
174
|
const collections = new Map(manifest.collections.map((item) => [item.slug, item]));
|
|
175
175
|
const globals = new Set((manifest.globals ?? []).map((item) => item.slug));
|
|
176
176
|
const extensions = config.extensions;
|
|
177
|
-
|
|
177
|
+
|
|
178
|
+
// Loader keys may be reused by several views, but every reference must carry the
|
|
179
|
+
// exact generated contract for the compiled Go loader. This rejects stale or
|
|
180
|
+
// hand-authored TypeScript contracts before the view can request data.
|
|
181
|
+
for (const view of [
|
|
182
|
+
...extensions.dashboardPanels,
|
|
183
|
+
...extensions.routes,
|
|
184
|
+
...extensions.coreViews,
|
|
185
|
+
]) {
|
|
186
|
+
if (view.loader === undefined) continue;
|
|
187
|
+
const contract = manifest.application?.adminLoaders?.find(
|
|
188
|
+
(loader) => loader.key === view.loader.key
|
|
189
|
+
);
|
|
190
|
+
if (contract === undefined || JSON.stringify(contract) !== JSON.stringify(view.loader.contract))
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Admin view ${"key" in view ? view.key : view.path} requires a matching generated Go loader; run ridu generate.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
for (const results of extensions.listResultsRenderers) {
|
|
196
|
+
if (!collections.has(results.collection))
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Admin list results ${results.key} selects unknown collection ${results.collection}.`
|
|
199
|
+
);
|
|
200
|
+
if (
|
|
201
|
+
extensions.coreViews.some(
|
|
202
|
+
(view) =>
|
|
203
|
+
view.surface === "collectionList" &&
|
|
204
|
+
(view.collection === undefined || view.collection === results.collection)
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
throw new Error(`Admin list results ${results.key} targets a replaced collection view.`);
|
|
208
|
+
}
|
|
209
|
+
for (const cell of extensions.listCellRenderers) {
|
|
178
210
|
const collection = collections.get(cell.collection);
|
|
179
211
|
if (!collection?.fields.some((field) => field.name === cell.field || field.path === cell.field))
|
|
180
212
|
throw new Error(
|
|
181
213
|
`Admin list cell ${cell.key} selects unknown field ${cell.collection}.${cell.field}.`
|
|
182
214
|
);
|
|
183
215
|
}
|
|
184
|
-
for (const view of extensions.
|
|
216
|
+
for (const view of extensions.coreViews) {
|
|
185
217
|
if ("collection" in view && view.collection !== undefined && !collections.has(view.collection))
|
|
186
218
|
throw new Error(`Admin core view ${view.key} selects unknown collection ${view.collection}.`);
|
|
187
219
|
if ("global" in view && view.global !== undefined && !globals.has(view.global))
|
package/src/authoring/v1.ts
CHANGED
|
@@ -26,7 +26,7 @@ const authoringAPIVersion = 1;
|
|
|
26
26
|
*
|
|
27
27
|
* Use `fields` for the default editors of new Go field types. Each map key matches
|
|
28
28
|
* a Go `PluginFieldType.Key`, and each value is returned by `definePluginField`.
|
|
29
|
-
* Use `
|
|
29
|
+
* Use `fieldEditors` for alternative editors made with `defineFieldComponent`,
|
|
30
30
|
* selected explicitly in Go with `field.PluginComponent`. The other options add
|
|
31
31
|
* pages, dashboard panels, navigation, and the UI described by `AdminContributions`.
|
|
32
32
|
*
|
|
@@ -60,9 +60,9 @@ export function defineAdminPlugin<const Plugin extends Omit<AdminPlugin, "apiVer
|
|
|
60
60
|
const result = Object.freeze({
|
|
61
61
|
...plugin,
|
|
62
62
|
...(plugin.fields === undefined ? {} : { fields: Object.freeze({ ...plugin.fields }) }),
|
|
63
|
-
...(plugin.
|
|
63
|
+
...(plugin.fieldEditors === undefined
|
|
64
64
|
? {}
|
|
65
|
-
: {
|
|
65
|
+
: { fieldEditors: Object.freeze({ ...plugin.fieldEditors }) }),
|
|
66
66
|
apiVersion: authoringAPIVersion,
|
|
67
67
|
});
|
|
68
68
|
const definition: Omit<AdminPlugin, "apiVersion"> = plugin;
|
package/src/authoring.ts
CHANGED
|
@@ -41,6 +41,11 @@ export interface FieldReferenceBrowserProps {
|
|
|
41
41
|
field: SchemaField;
|
|
42
42
|
/** The target collection's resolved schema, available from `authoring.collections`. */
|
|
43
43
|
collection: SchemaCollection;
|
|
44
|
+
/**
|
|
45
|
+
* Optional collection choices. Only readable collections are offered; switching clears the
|
|
46
|
+
* current list query and uncommitted selection.
|
|
47
|
+
*/
|
|
48
|
+
collections?: readonly SchemaCollection[];
|
|
44
49
|
/** Allow multiple selections when true; otherwise select at most one document. */
|
|
45
50
|
hasMany: boolean;
|
|
46
51
|
/** IDs currently selected by your editor. */
|
|
@@ -51,8 +56,20 @@ export interface FieldReferenceBrowserProps {
|
|
|
51
56
|
initialDocument?: FieldDocument;
|
|
52
57
|
/** Fetch and open this document when `initialDocument` is not supplied. */
|
|
53
58
|
initialDocumentID?: string;
|
|
54
|
-
/**
|
|
55
|
-
|
|
59
|
+
/** Open a new document form immediately when creation is permitted. */
|
|
60
|
+
initialCreate?: boolean;
|
|
61
|
+
/** File dropped into an upload field; opens a new metadata form before any upload is saved. */
|
|
62
|
+
initialFile?: File;
|
|
63
|
+
/**
|
|
64
|
+
* Limit selectable documents; the server still applies its own access rules. A static filter
|
|
65
|
+
* applies to every collection. Use a resolver when collection schemas need different fields.
|
|
66
|
+
*/
|
|
67
|
+
optionFilter?:
|
|
68
|
+
| FieldReferenceFilter
|
|
69
|
+
| readonly FieldReferenceFilter[]
|
|
70
|
+
| ((
|
|
71
|
+
collection: SchemaCollection
|
|
72
|
+
) => FieldReferenceFilter | readonly FieldReferenceFilter[] | undefined);
|
|
56
73
|
/** Initial values for a new related document, not changes to an existing document. */
|
|
57
74
|
defaultValues?: Readonly<Record<string, unknown>>;
|
|
58
75
|
/** Allow creation when access permits it. Set false to hide creation. */
|
|
@@ -60,12 +77,12 @@ export interface FieldReferenceBrowserProps {
|
|
|
60
77
|
/** Content locale for loading and editing related documents. */
|
|
61
78
|
locale?: string;
|
|
62
79
|
/**
|
|
63
|
-
* Called
|
|
80
|
+
* Called with selected IDs and their collection slug. Update your field here.
|
|
64
81
|
* Return false (or resolve to false) to keep the browser open. Returning void or
|
|
65
82
|
* true accepts the selection and closes it while editing remains allowed. A thrown
|
|
66
83
|
* error/rejected promise keeps it open and Ridu displays an error notification.
|
|
67
84
|
*/
|
|
68
|
-
onCommit: (ids: string[]) => void | boolean | Promise<void | boolean>;
|
|
85
|
+
onCommit: (ids: string[], collectionSlug: string) => void | boolean | Promise<void | boolean>;
|
|
69
86
|
/** Called when the browser closes; update your open/closed UI state here. */
|
|
70
87
|
onClose: () => void;
|
|
71
88
|
}
|
|
@@ -206,6 +223,12 @@ export interface FieldAuthoringHost {
|
|
|
206
223
|
readonly documentRevision: number;
|
|
207
224
|
/** The content locale being edited, separate from the admin interface language. */
|
|
208
225
|
readonly locale?: string;
|
|
226
|
+
/**
|
|
227
|
+
* Whether the current admin session may create documents in this collection.
|
|
228
|
+
* This is a presentation hint for create controls, not authorization. Access is
|
|
229
|
+
* checked again by the reference browser and operation engine when work is saved.
|
|
230
|
+
*/
|
|
231
|
+
canCreateDocument(collection: string): boolean;
|
|
209
232
|
/** Related-document picker/editor component. Its `onCommit` handler updates your field. */
|
|
210
233
|
referenceBrowser: Component<FieldReferenceBrowserProps>;
|
|
211
234
|
/**
|
package/src/editor/registry.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type FieldEditorDefinition<Type extends FieldEditorType, Config = undefin
|
|
|
37
37
|
|
|
38
38
|
const registration = Symbol("ridu-field-editor");
|
|
39
39
|
const registeredEditors = new WeakSet<RegisteredFieldEditor>();
|
|
40
|
-
/** Registration returned by `defineFieldEditor`; store it in `defineAdmin({
|
|
40
|
+
/** Registration returned by `defineFieldEditor`; store it in `defineAdmin({ fieldEditors: ... })`. */
|
|
41
41
|
export interface RegisteredFieldEditor {
|
|
42
42
|
readonly [registration]: true;
|
|
43
43
|
readonly type: FieldEditorType;
|
|
@@ -51,7 +51,7 @@ export interface RegisteredFieldEditor {
|
|
|
51
51
|
* Register a Svelte component to replace a field input in your application.
|
|
52
52
|
* Supports text, textarea, email, date, code, number, checkbox, text-list and number-list fields.
|
|
53
53
|
*
|
|
54
|
-
* Put the result under an `app:name` key in `defineAdmin({
|
|
54
|
+
* Put the result under an `app:name` key in `defineAdmin({ fieldEditors: ... })` and
|
|
55
55
|
* select that same key in Go with `field.Admin{Editor: field.Component("app:name")}`. The Go field keeps
|
|
56
56
|
* its original storage, validation and filtering behaviour.
|
|
57
57
|
*
|
|
@@ -67,7 +67,7 @@ export interface RegisteredFieldEditor {
|
|
|
67
67
|
* TypeScript infers the `Config` type from the decoder's return value.
|
|
68
68
|
*
|
|
69
69
|
* @param definition The existing field type, Svelte component, and optional settings decoder.
|
|
70
|
-
* @returns A frozen registration for `defineAdmin`'s `
|
|
70
|
+
* @returns A frozen registration for `defineAdmin`'s `fieldEditors` map. This does not
|
|
71
71
|
* render the component, select it on a Go field, or change the stored value type.
|
|
72
72
|
* @throws If the type is unsupported or the component is invalid. `ridu check`
|
|
73
73
|
* also checks the selected Go field type and its settings before deployment.
|
|
@@ -80,7 +80,7 @@ export interface RegisteredFieldEditor {
|
|
|
80
80
|
*
|
|
81
81
|
* export default defineAdmin({
|
|
82
82
|
* plugins: generatedAdminPlugins,
|
|
83
|
-
*
|
|
83
|
+
* fieldEditors: {
|
|
84
84
|
* 'app:titleCounter': defineFieldEditor({ type: 'text', component: TitleField })
|
|
85
85
|
* }
|
|
86
86
|
* });
|
|
@@ -117,12 +117,12 @@ export function defineFieldEditor<const Type extends FieldEditorType, Config = u
|
|
|
117
117
|
|
|
118
118
|
export interface FieldEditorConfig {
|
|
119
119
|
/** Custom field inputs selected by Go `field.Admin{Editor: field.Component("app:name")}`. Use `defineFieldEditor` for each entry. */
|
|
120
|
-
|
|
120
|
+
fieldEditors?: Readonly<Record<`app:${string}`, RegisteredFieldEditor>>;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** The application owns one static configuration; packaged plugins retain their pairing contracts. */
|
|
124
124
|
export function validateFieldEditorRegistrations(config: FieldEditorConfig): void {
|
|
125
|
-
for (const [reference, editor] of Object.entries(config.
|
|
125
|
+
for (const [reference, editor] of Object.entries(config.fieldEditors ?? {})) {
|
|
126
126
|
if (!localEditorReference.test(reference))
|
|
127
127
|
throw new Error(
|
|
128
128
|
`Invalid editor reference ${reference}; expected app:name in admin/src/admin.config.ts.`
|
|
@@ -134,14 +134,14 @@ export function validateFieldEditorRegistrations(config: FieldEditorConfig): voi
|
|
|
134
134
|
|
|
135
135
|
/** Check one local selection using the same decoder as the mounted editor. */
|
|
136
136
|
export function validateFieldEditorSelection(
|
|
137
|
-
|
|
137
|
+
fieldEditors: NonNullable<FieldEditorConfig["fieldEditors"]>,
|
|
138
138
|
field: SchemaField
|
|
139
139
|
): void {
|
|
140
140
|
const reference = field.admin.editor?.reference;
|
|
141
141
|
if (reference === undefined) return;
|
|
142
142
|
if (!localEditorReference.test(reference))
|
|
143
143
|
throw new Error(`Malformed editor reference ${reference}; expected app:name.`);
|
|
144
|
-
const editor =
|
|
144
|
+
const editor = fieldEditors[reference as `app:${string}`];
|
|
145
145
|
if (editor === undefined)
|
|
146
146
|
throw new Error(
|
|
147
147
|
`Editor ${reference} is not registered. Register it in admin/src/admin.config.ts or change the field component in Go.`
|
package/src/editor/types.ts
CHANGED
|
@@ -95,7 +95,7 @@ export type FieldEditorProps<Type extends FieldEditorType = FieldEditorType, Con
|
|
|
95
95
|
*/
|
|
96
96
|
authoring: Pick<
|
|
97
97
|
FieldAuthoringHost,
|
|
98
|
-
"collections" | "documentRevision" | "referenceBrowser" | "findDocument"
|
|
98
|
+
"collections" | "documentRevision" | "canCreateDocument" | "referenceBrowser" | "findDocument"
|
|
99
99
|
> & { readonly locale: string | undefined };
|
|
100
100
|
} & ([Config] extends [undefined]
|
|
101
101
|
? { config?: never }
|
package/src/field.ts
CHANGED
|
@@ -299,7 +299,7 @@ type NamedFieldTarget<Type extends FieldType, Key extends string> = Type extends
|
|
|
299
299
|
/**
|
|
300
300
|
* Register a plugin component that can replace an existing field's editor.
|
|
301
301
|
*
|
|
302
|
-
* Put the result in `defineAdminPlugin({
|
|
302
|
+
* Put the result in `defineAdminPlugin({ fieldEditors: { colorSwatch: ... } })`.
|
|
303
303
|
* Go selects that name through `field.PluginComponent(pluginKey, "colorSwatch")`
|
|
304
304
|
* in the field's `Admin.Editor` setting. This changes the input while keeping
|
|
305
305
|
* the original Go field's storage, validation, permissions, and generated types.
|
|
@@ -319,7 +319,7 @@ type NamedFieldTarget<Type extends FieldType, Key extends string> = Type extends
|
|
|
319
319
|
* errors for invalid input. Go still validates the document when it is saved.
|
|
320
320
|
*
|
|
321
321
|
* @param definition The existing field type, component, and value/settings decoders.
|
|
322
|
-
* @returns A frozen registration for the plugin's `
|
|
322
|
+
* @returns A frozen registration for the plugin's `fieldEditors` map. It preserves
|
|
323
323
|
* the selected field type and inferred value/input types; it does not create a new field type.
|
|
324
324
|
* @throws If the field type, component, decoders, or plugin field-type selection are invalid.
|
|
325
325
|
* @example
|
|
@@ -330,7 +330,7 @@ type NamedFieldTarget<Type extends FieldType, Key extends string> = Type extends
|
|
|
330
330
|
* export const editorialAdminPlugin = defineAdminPlugin({
|
|
331
331
|
* key: 'editorial-tools',
|
|
332
332
|
* pairingVersion: 1,
|
|
333
|
-
*
|
|
333
|
+
* fieldEditors: {
|
|
334
334
|
* colorSwatch: defineFieldComponent({
|
|
335
335
|
* type: 'text',
|
|
336
336
|
* component: ColorSwatch,
|
|
@@ -359,7 +359,7 @@ export function defineFieldComponent<
|
|
|
359
359
|
}
|
|
360
360
|
): PluginFieldRegistration<Value, Input, Type> & NamedFieldTarget<Type, Key>;
|
|
361
361
|
/**
|
|
362
|
-
* Name an alternative editor in your paired plugin's `
|
|
362
|
+
* Name an alternative editor in your paired plugin's `fieldEditors` map. Go selects
|
|
363
363
|
* it with `.Admin(field.Admin{Editor: field.PluginComponent(pluginKey, componentName, config)})`. Set `type` to
|
|
364
364
|
* the actual field type; also supply `fieldType` for plugin values. `decodeValue`
|
|
365
365
|
* checks reads/writes and `decodeConfig` checks settings, synchronously. Changing
|
|
@@ -379,7 +379,7 @@ export function defineFieldComponent<
|
|
|
379
379
|
}
|
|
380
380
|
): PluginFieldRegistration<Value, Value, Type> & NamedFieldTarget<Type, Key>;
|
|
381
381
|
/**
|
|
382
|
-
* Name an alternative editor without settings in your plugin's `
|
|
382
|
+
* Name an alternative editor without settings in your plugin's `fieldEditors` map.
|
|
383
383
|
* Go selects it with `field.Admin.Editor`. Set the actual schema `type`, plus
|
|
384
384
|
* `fieldType` for plugin values. Supply synchronous `decodeValue` and `decodeInput`
|
|
385
385
|
* for different saved/write shapes. Omit the Go settings argument; no config prop is
|
|
@@ -400,7 +400,7 @@ export function defineFieldComponent<
|
|
|
400
400
|
}
|
|
401
401
|
): PluginFieldRegistration<Value, Input, Type> & NamedFieldTarget<Type, Key>;
|
|
402
402
|
/**
|
|
403
|
-
* Name an alternative editor without settings in your plugin's `
|
|
403
|
+
* Name an alternative editor without settings in your plugin's `fieldEditors` map.
|
|
404
404
|
* Go selects it with `field.Admin.Editor`. Set the actual schema `type`, plus
|
|
405
405
|
* `fieldType` for plugin values. `decodeValue` synchronously checks reads/writes.
|
|
406
406
|
* Omit the Go settings argument; no config prop is passed. Any supplied component
|
|
@@ -463,10 +463,10 @@ function createRegistration(
|
|
|
463
463
|
blocks: true,
|
|
464
464
|
plugin: true,
|
|
465
465
|
};
|
|
466
|
-
if (!Object.hasOwn(supported, type)) throw new Error(`Unsupported field
|
|
466
|
+
if (!Object.hasOwn(supported, type)) throw new Error(`Unsupported field editor type ${type}.`);
|
|
467
467
|
if ("canRender" in definition || "key" in definition || "componentKey" in definition)
|
|
468
468
|
throw new Error(
|
|
469
|
-
"
|
|
469
|
+
"Field editor matching comes from its keyed registration; key/canRender/componentKey are not supported."
|
|
470
470
|
);
|
|
471
471
|
if (
|
|
472
472
|
typeof definition.component !== "function" ||
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,9 @@ export {
|
|
|
42
42
|
type AdminContributions,
|
|
43
43
|
type AdminDashboardPanel,
|
|
44
44
|
type AdminDashboardPanelProps,
|
|
45
|
+
type AdminLoaderProps,
|
|
46
|
+
type AdminViewComponent,
|
|
47
|
+
withAdminLoader,
|
|
45
48
|
type AdminExtensionProps,
|
|
46
49
|
type AdminLoginComponent,
|
|
47
50
|
type AdminLoginComponentProps,
|
|
@@ -63,9 +66,9 @@ export {
|
|
|
63
66
|
type AdminBrandComponent,
|
|
64
67
|
type AdminBrandComponentProps,
|
|
65
68
|
type AdminBrandSurface,
|
|
66
|
-
type
|
|
67
|
-
type
|
|
68
|
-
type
|
|
69
|
+
type AdminShellSlot,
|
|
70
|
+
type AdminShellSlotProps,
|
|
71
|
+
type AdminShellSlotPosition,
|
|
69
72
|
type AdminProvider,
|
|
70
73
|
type AdminProviderProps,
|
|
71
74
|
type AdminDocumentAction,
|
|
@@ -73,8 +76,11 @@ export {
|
|
|
73
76
|
type AdminDocumentExtensionProps,
|
|
74
77
|
type AdminDocumentView,
|
|
75
78
|
type AdminExtensionNotificationTone,
|
|
76
|
-
type
|
|
77
|
-
type
|
|
79
|
+
type AdminListCellRenderer,
|
|
80
|
+
type AdminListCellRendererProps,
|
|
81
|
+
type AdminCollectionList,
|
|
82
|
+
type AdminListResultsRenderer,
|
|
83
|
+
type AdminListResultsRendererProps,
|
|
78
84
|
type AdminRouteNavigation,
|
|
79
85
|
type AdminPluginPair,
|
|
80
86
|
type AdminRoute,
|
package/src/plugin-registry.ts
CHANGED
|
@@ -8,14 +8,14 @@ export interface ResolvedPluginField {
|
|
|
8
8
|
readonly owner: string;
|
|
9
9
|
/** Field-type key for a default editor, or component name for an explicitly selected editor. */
|
|
10
10
|
readonly key: string;
|
|
11
|
-
/** Present only for editors registered in `
|
|
11
|
+
/** Present only for editors registered in `fieldEditors` and selected by Go `AdminComponent`. */
|
|
12
12
|
readonly componentKey?: string;
|
|
13
13
|
readonly registration: RegisteredPluginField;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Collect field registrations, checking plugin IDs, field-type ownership, named
|
|
18
|
-
*
|
|
18
|
+
* field editors and API versions. Throws on malformed/duplicate registrations.
|
|
19
19
|
* Used by Ridu's runtime/check tooling; authors normally call the registration
|
|
20
20
|
* helpers and retain their generated plugin list instead of invoking this directly.
|
|
21
21
|
*/
|
|
@@ -37,7 +37,7 @@ export function resolvePluginFields(
|
|
|
37
37
|
owners.add(plugin.key);
|
|
38
38
|
for (const [kind, entries] of [
|
|
39
39
|
["fields", plugin.fields],
|
|
40
|
-
["
|
|
40
|
+
["fieldEditors", plugin.fieldEditors],
|
|
41
41
|
] as const) {
|
|
42
42
|
if (entries === undefined) continue;
|
|
43
43
|
if (
|
|
@@ -63,15 +63,14 @@ export function resolvePluginFields(
|
|
|
63
63
|
)
|
|
64
64
|
throw new Error(`Plugin ${plugin.key} ${kind}.${key} uses the wrong registration kind.`);
|
|
65
65
|
const identity = kind === "fields" ? `field:${key}` : `component:${plugin.key}:${key}`;
|
|
66
|
-
if (identities.has(identity))
|
|
67
|
-
throw new Error(`Duplicate admin field renderer ${identity}.`);
|
|
66
|
+
if (identities.has(identity)) throw new Error(`Duplicate admin field editor ${identity}.`);
|
|
68
67
|
identities.add(identity);
|
|
69
68
|
resolved.push(
|
|
70
69
|
Object.freeze({
|
|
71
70
|
owner: plugin.key,
|
|
72
71
|
key,
|
|
73
72
|
registration,
|
|
74
|
-
...(kind === "
|
|
73
|
+
...(kind === "fieldEditors" ? { componentKey: key } : {}),
|
|
75
74
|
})
|
|
76
75
|
);
|
|
77
76
|
}
|
|
@@ -89,7 +88,7 @@ export function validateManifestPluginPairs(
|
|
|
89
88
|
complete: boolean
|
|
90
89
|
): void {
|
|
91
90
|
// Startup may receive only public or access-filtered schemas. Build/check own
|
|
92
|
-
// completeness; selected fields below still validate their exact
|
|
91
|
+
// completeness; selected fields below still validate their exact editor.
|
|
93
92
|
if (complete && manifest.plugins !== undefined)
|
|
94
93
|
for (const item of registrations) {
|
|
95
94
|
if (
|
|
@@ -99,7 +98,7 @@ export function validateManifestPluginPairs(
|
|
|
99
98
|
)
|
|
100
99
|
)
|
|
101
100
|
throw new Error(
|
|
102
|
-
`Named
|
|
101
|
+
`Named field editor ${item.owner}:${item.key} selects undeclared field type ${item.registration.fieldType}.`
|
|
103
102
|
);
|
|
104
103
|
}
|
|
105
104
|
if (manifest.plugins !== undefined) {
|
|
@@ -136,7 +135,7 @@ export function createPluginFieldValidator(registrations: readonly ResolvedPlugi
|
|
|
136
135
|
const fields = new Map(
|
|
137
136
|
registrations.filter((item) => item.componentKey === undefined).map((item) => [item.key, item])
|
|
138
137
|
);
|
|
139
|
-
const
|
|
138
|
+
const fieldEditors = new Map(
|
|
140
139
|
registrations
|
|
141
140
|
.filter((item) => item.componentKey !== undefined)
|
|
142
141
|
.map((item) => [`${item.owner}:${item.key}`, item])
|
|
@@ -145,14 +144,14 @@ export function createPluginFieldValidator(registrations: readonly ResolvedPlugi
|
|
|
145
144
|
const selection = field.admin.component;
|
|
146
145
|
const selected =
|
|
147
146
|
selection !== undefined
|
|
148
|
-
?
|
|
147
|
+
? fieldEditors.get(`${selection.plugin}:${selection.component}`)
|
|
149
148
|
: field.type === "plugin"
|
|
150
149
|
? fields.get(field.plugin?.key ?? "")
|
|
151
150
|
: undefined;
|
|
152
151
|
if (selection !== undefined || field.type === "plugin") {
|
|
153
152
|
if (selected === undefined)
|
|
154
153
|
throw new Error(
|
|
155
|
-
`Missing
|
|
154
|
+
`Missing field editor for ${selection !== undefined ? `${selection.plugin}:${selection.component}` : field.plugin?.key}.`
|
|
156
155
|
);
|
|
157
156
|
selected.registration.decodeConfig(field);
|
|
158
157
|
}
|