@riducms/plugin 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +346 -32
- package/package.json +9 -4
- package/src/admin.ts +171 -0
- package/src/authoring/v1.ts +71 -0
- package/src/authoring.ts +160 -2
- package/src/component-config.ts +51 -0
- package/src/editor/field.svelte +21 -0
- package/src/editor/field.ts +9 -0
- package/src/editor/index.ts +3 -0
- package/src/editor/registry.ts +174 -0
- package/src/editor/types.ts +104 -0
- package/src/field.ts +596 -16
- package/src/form.ts +33 -4
- package/src/i18n.ts +29 -4
- package/src/index.ts +19 -7
- package/src/local-row-label.ts +87 -0
- package/src/plugin-registry.ts +188 -0
- package/src/plugin.ts +265 -74
- package/src/row-label.ts +15 -4
package/src/plugin.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FieldType } from "@riducms/protocol";
|
|
2
|
+
import type { RegisteredPluginField } from "./field";
|
|
3
|
+
import { resolvePluginFields, type ResolvedPluginField } from "./plugin-registry";
|
|
2
4
|
import { ADMIN_PLUGIN_API_VERSION } from "@riducms/protocol";
|
|
3
5
|
import type {
|
|
4
6
|
OperationCapabilities,
|
|
@@ -10,32 +12,64 @@ import type {
|
|
|
10
12
|
import type { Component, Snippet } from "svelte";
|
|
11
13
|
|
|
12
14
|
import type { FieldDocument } from "./authoring";
|
|
13
|
-
import type { AdminI18n, PluginMessageCatalog,
|
|
15
|
+
import type { AdminI18n, PluginMessageCatalog, ExtensionTranslationKey } from "./i18n";
|
|
14
16
|
import { freezeAdminMessages, validateAdminMessages } from "./i18n";
|
|
15
17
|
import type { RowLabelPlugin } from "./row-label";
|
|
16
18
|
|
|
17
19
|
export { ADMIN_PLUGIN_API_VERSION };
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* The browser UI supplied by an installed Go/admin plugin pair.
|
|
23
|
+
* Create this with `defineAdminPlugin` from `@riducms/plugin/authoring/v1`,
|
|
24
|
+
* which supplies the framework API version. Go declares the plugin's server
|
|
25
|
+
* behaviour; this object connects its fields and other admin UI components.
|
|
23
26
|
*/
|
|
24
|
-
export interface AdminPlugin {
|
|
25
|
-
/**
|
|
27
|
+
export interface AdminPlugin extends AdminContributions {
|
|
28
|
+
/** Supplied by the versioned authoring import. Authors must not set this themselves. */
|
|
26
29
|
apiVersion: typeof ADMIN_PLUGIN_API_VERSION;
|
|
27
|
-
/**
|
|
30
|
+
/** Owning Go plugin's key, for example `"editorial-tools"`; not necessarily a field-type key. */
|
|
28
31
|
key: string;
|
|
29
|
-
/**
|
|
32
|
+
/** Positive integer matching the Go descriptor; bump it when the two halves become incompatible. */
|
|
30
33
|
pairingVersion: number;
|
|
31
|
-
/**
|
|
32
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Default editors for field types declared by this Go plugin. Each map key is a
|
|
36
|
+
* globally unique field-type name; each value comes from `definePluginField`.
|
|
37
|
+
* For example, one plugin may provide both `"review-note"` and `"color-swatch"`.
|
|
38
|
+
*/
|
|
39
|
+
fields?: Readonly<
|
|
40
|
+
Record<string, RegisteredPluginField & { readonly type: "plugin"; readonly fieldType?: never }>
|
|
41
|
+
>;
|
|
42
|
+
/**
|
|
43
|
+
* Alternative editors made with `defineFieldComponent`, keyed by component name.
|
|
44
|
+
* Go chooses one with `.Admin(field.Admin{Editor: field.PluginComponent(pluginKey, componentName, config)})`.
|
|
45
|
+
* These may edit built-in fields or an explicitly named plugin field type.
|
|
46
|
+
*/
|
|
47
|
+
components?: Readonly<
|
|
48
|
+
Record<
|
|
49
|
+
string,
|
|
50
|
+
RegisteredPluginField &
|
|
51
|
+
(
|
|
52
|
+
| { readonly type: Exclude<FieldType, "plugin"> }
|
|
53
|
+
| { readonly type: "plugin"; readonly fieldType: string }
|
|
54
|
+
)
|
|
55
|
+
>
|
|
56
|
+
>;
|
|
33
57
|
/** Array and blocks row-label components contributed by this package. */
|
|
34
58
|
rowLabels?: readonly RowLabelPlugin[];
|
|
35
59
|
/** Statically bundled, plugin-owned interface messages. */
|
|
36
60
|
messages?: PluginMessageCatalog;
|
|
61
|
+
/** Package-relative modules/styles to import at build time; must match the Go descriptor's list. */
|
|
62
|
+
assets?: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Places where a plugin or application can add admin UI. Arrays keep registration
|
|
67
|
+
* order; application entries follow plugin entries. Replacement slots allow one
|
|
68
|
+
* matching replacement. Conflicts are reported instead of silently overriding UI.
|
|
69
|
+
*/
|
|
70
|
+
export interface AdminContributions {
|
|
37
71
|
/** Authenticated admin routes contributed by this package. */
|
|
38
|
-
routes?: readonly
|
|
72
|
+
routes?: readonly AdminRoute[];
|
|
39
73
|
/** Panels composed into or replacing the authenticated dashboard. */
|
|
40
74
|
dashboard?: readonly AdminDashboardPanel[];
|
|
41
75
|
/** Components composed around or replacing the sign-in screen. */
|
|
@@ -60,65 +94,77 @@ export interface AdminPlugin {
|
|
|
60
94
|
documentActions?: readonly AdminDocumentAction[];
|
|
61
95
|
/** Read-only or operational views rendered beside Edit and API. */
|
|
62
96
|
documentViews?: readonly AdminDocumentView[];
|
|
63
|
-
/** Package-relative static modules imported by the generated admin registry. */
|
|
64
|
-
assets?: readonly string[];
|
|
65
97
|
}
|
|
66
98
|
|
|
99
|
+
/** Common data supplied to admin extension components. Use the generated SDK for requests. */
|
|
67
100
|
export interface AdminExtensionProps {
|
|
101
|
+
/** Resolved Go schema available to this admin session; it may omit inaccessible resources. */
|
|
68
102
|
manifest: SchemaManifest;
|
|
103
|
+
/** Signed-in user document, absent on screens where no user is signed in yet. */
|
|
69
104
|
user?: FieldDocument;
|
|
105
|
+
/** Current admin interface translations and formatting preferences. */
|
|
70
106
|
i18n: AdminI18n;
|
|
71
107
|
}
|
|
72
108
|
|
|
109
|
+
/** Sign-in operations provided to a custom login screen. */
|
|
73
110
|
export interface AdminLoginExtensionHost {
|
|
74
|
-
/**
|
|
111
|
+
/** Sign in, check admin access and enter the admin. Rejects if authentication/access fails. */
|
|
75
112
|
login: (credentials: { email: string; password: string }) => Promise<void>;
|
|
113
|
+
/** Show a temporary success or error message. Does not perform an operation itself. */
|
|
76
114
|
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
77
115
|
}
|
|
78
116
|
|
|
79
117
|
export interface AdminLoginComponentProps extends AdminExtensionProps {
|
|
80
|
-
/**
|
|
118
|
+
/** Render with `{@render defaultView()}` to keep Ridu's sign-in screen inside your wrapper. */
|
|
81
119
|
defaultView: Snippet;
|
|
82
120
|
host: AdminLoginExtensionHost;
|
|
83
121
|
}
|
|
84
122
|
|
|
123
|
+
/** Add content before/after sign-in, or replace it with a custom screen. */
|
|
85
124
|
export interface AdminLoginComponent {
|
|
86
125
|
key: string;
|
|
87
126
|
component: Component<AdminLoginComponentProps>;
|
|
127
|
+
/** Defaults to after. Only one replace entry is allowed across plugins and the application. */
|
|
88
128
|
position?: "before" | "after" | "replace";
|
|
89
129
|
}
|
|
90
130
|
|
|
91
131
|
export type AdminAccountSurface = "profile" | "security";
|
|
92
132
|
|
|
133
|
+
/** Operations for keeping a custom account screen in sync with the signed-in session. */
|
|
93
134
|
export interface AdminAccountExtensionHost {
|
|
94
135
|
/** Reloads the signed-in document and updates the shell identity. */
|
|
95
136
|
refreshUser: () => Promise<FieldDocument | undefined>;
|
|
96
137
|
/** Ends the current session and returns to sign-in. */
|
|
97
138
|
logout: () => Promise<void>;
|
|
139
|
+
/** Show a temporary success or error message. */
|
|
98
140
|
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
99
141
|
}
|
|
100
142
|
|
|
101
143
|
export interface AdminAccountComponentProps extends AdminExtensionProps {
|
|
102
144
|
surface: AdminAccountSurface;
|
|
103
|
-
/**
|
|
145
|
+
/** Render with `{@render defaultView()}` to keep Ridu's account screen inside your wrapper. */
|
|
104
146
|
defaultView: Snippet;
|
|
105
147
|
host: AdminAccountExtensionHost;
|
|
106
148
|
}
|
|
107
149
|
|
|
150
|
+
/** Add to or replace the profile/security screen identified by `surface`. */
|
|
108
151
|
export interface AdminAccountComponent {
|
|
109
152
|
key: string;
|
|
110
153
|
surface: AdminAccountSurface;
|
|
111
154
|
component: Component<AdminAccountComponentProps>;
|
|
155
|
+
/** Defaults to after. Each account surface permits at most one replacement. */
|
|
112
156
|
position?: "before" | "after" | "replace";
|
|
113
157
|
}
|
|
114
158
|
|
|
159
|
+
/** Choose the whole navigation's boundary, the resource-link list's boundary, or replace navigation. */
|
|
115
160
|
export type AdminNavigationPosition = "before" | "beforeLinks" | "afterLinks" | "after" | "replace";
|
|
116
161
|
|
|
117
162
|
export interface AdminNavigationComponentProps extends AdminExtensionProps {
|
|
118
|
-
/**
|
|
163
|
+
/** Render with `{@render defaultView()}` to include Ridu's navigation in a replacement. */
|
|
119
164
|
defaultView: Snippet;
|
|
120
165
|
}
|
|
121
166
|
|
|
167
|
+
/** Insert a component at a navigation position. At most one entry can replace navigation. */
|
|
122
168
|
export interface AdminNavigationComponent {
|
|
123
169
|
key: string;
|
|
124
170
|
component: Component<AdminNavigationComponentProps>;
|
|
@@ -126,6 +172,7 @@ export interface AdminNavigationComponent {
|
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
export interface AdminLogoutExtensionHost {
|
|
175
|
+
/** End the current session and return to sign-in. Await this instead of only changing local UI. */
|
|
129
176
|
logout: () => Promise<void>;
|
|
130
177
|
}
|
|
131
178
|
|
|
@@ -133,6 +180,7 @@ export interface AdminLogoutButtonProps extends AdminExtensionProps {
|
|
|
133
180
|
host: AdminLogoutExtensionHost;
|
|
134
181
|
}
|
|
135
182
|
|
|
183
|
+
/** Replace the account menu's sign-out button; call the supplied `host.logout()` on activation. */
|
|
136
184
|
export interface AdminLogoutButton {
|
|
137
185
|
key: string;
|
|
138
186
|
component: Component<AdminLogoutButtonProps>;
|
|
@@ -141,9 +189,13 @@ export interface AdminLogoutButton {
|
|
|
141
189
|
export type AdminCoreViewSurface =
|
|
142
190
|
"collectionList" | "collectionCreate" | "collectionEdit" | "global" | "notFound";
|
|
143
191
|
|
|
192
|
+
/** Refresh tools for a custom collection/global screen. They do not save documents. */
|
|
144
193
|
export interface AdminCoreViewHost {
|
|
194
|
+
/** Fetch the current Go schema again and update the admin's schema-dependent UI. */
|
|
145
195
|
refreshManifest: () => Promise<void>;
|
|
196
|
+
/** Notify Ridu that your code changed documents so dependent lists/lookups can refresh. */
|
|
146
197
|
documentsChanged: () => void;
|
|
198
|
+
/** Show a temporary success or error message. */
|
|
147
199
|
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
148
200
|
}
|
|
149
201
|
|
|
@@ -152,7 +204,7 @@ export interface AdminCoreViewProps extends AdminExtensionProps {
|
|
|
152
204
|
collection?: SchemaCollection;
|
|
153
205
|
global?: SchemaGlobal;
|
|
154
206
|
documentID?: string;
|
|
155
|
-
/**
|
|
207
|
+
/** Render with `{@render defaultView()}` to wrap the normal screen rather than rebuilding it. */
|
|
156
208
|
defaultView: Snippet;
|
|
157
209
|
host: AdminCoreViewHost;
|
|
158
210
|
}
|
|
@@ -179,6 +231,11 @@ interface AdminNotFoundCoreView {
|
|
|
179
231
|
component: Component<AdminCoreViewProps>;
|
|
180
232
|
}
|
|
181
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Replace a collection/global/not-found screen. A resource-specific registration
|
|
236
|
+
* wins over an all-resources fallback for that surface; duplicate targets fail.
|
|
237
|
+
* The supplied `defaultView` lets your component wrap the existing screen.
|
|
238
|
+
*/
|
|
182
239
|
export type AdminCoreView = AdminCollectionCoreView | AdminGlobalCoreView | AdminNotFoundCoreView;
|
|
183
240
|
|
|
184
241
|
export type AdminBrandSurface = "loginLogo" | "navigationLogo" | "accountAvatar";
|
|
@@ -187,6 +244,7 @@ export interface AdminBrandComponentProps extends AdminExtensionProps {
|
|
|
187
244
|
surface: AdminBrandSurface;
|
|
188
245
|
}
|
|
189
246
|
|
|
247
|
+
/** Replace one logo/avatar location. Only one component may claim each surface. */
|
|
190
248
|
export interface AdminBrandComponent {
|
|
191
249
|
key: string;
|
|
192
250
|
surface: AdminBrandSurface;
|
|
@@ -199,6 +257,7 @@ export interface AdminShellComponentProps extends AdminExtensionProps {
|
|
|
199
257
|
position: AdminShellPosition;
|
|
200
258
|
}
|
|
201
259
|
|
|
260
|
+
/** Add UI to the global header, actions area or account settings menu in registration order. */
|
|
202
261
|
export interface AdminShellComponent {
|
|
203
262
|
key: string;
|
|
204
263
|
position: AdminShellPosition;
|
|
@@ -206,11 +265,12 @@ export interface AdminShellComponent {
|
|
|
206
265
|
}
|
|
207
266
|
|
|
208
267
|
export interface AdminProviderProps {
|
|
209
|
-
/**
|
|
268
|
+
/** Render `{@render defaultView()}` after setting context so the nested providers/admin appear. */
|
|
210
269
|
defaultView: Snippet;
|
|
211
270
|
i18n: AdminI18n;
|
|
212
271
|
}
|
|
213
272
|
|
|
273
|
+
/** Wrap the admin in a Svelte context provider. Render the supplied `defaultView` to continue the UI. */
|
|
214
274
|
export interface AdminProvider {
|
|
215
275
|
key: string;
|
|
216
276
|
component: Component<AdminProviderProps>;
|
|
@@ -222,38 +282,50 @@ export interface AdminDashboardPanelProps {
|
|
|
222
282
|
i18n: AdminI18n;
|
|
223
283
|
}
|
|
224
284
|
|
|
285
|
+
/** Add dashboard content before/after Ridu's overview, or replace that overview. */
|
|
225
286
|
export interface AdminDashboardPanel {
|
|
226
|
-
/**
|
|
287
|
+
/** Unique name among dashboard entries; duplicates throw. Array order controls display order. */
|
|
227
288
|
key: string;
|
|
228
289
|
component: Component<AdminDashboardPanelProps>;
|
|
229
|
-
/** Replace
|
|
290
|
+
/** Defaults to after. Replace hides Ridu's overview and may only be registered once. */
|
|
230
291
|
position?: "before" | "after" | "replace";
|
|
231
292
|
}
|
|
232
293
|
|
|
294
|
+
/** Data for displaying a collection table cell. This is not an editable document-form binding. */
|
|
233
295
|
export interface AdminListCellProps {
|
|
234
296
|
collection: SchemaCollection;
|
|
235
297
|
field: SchemaField;
|
|
236
298
|
document: FieldDocument;
|
|
299
|
+
/** This field's saved value. Check its type before rendering; changing it does not save a document. */
|
|
237
300
|
value: unknown;
|
|
238
301
|
i18n: AdminI18n;
|
|
239
302
|
}
|
|
240
303
|
|
|
304
|
+
/** Replace the cell display for one collection/field pair; duplicate targets are rejected. */
|
|
241
305
|
export interface AdminListCell {
|
|
242
306
|
key: string;
|
|
307
|
+
/** Collection slug, for example `"posts"`. */
|
|
243
308
|
collection: string;
|
|
309
|
+
/** Top-level field name/path in that collection, for example `"title"`. */
|
|
244
310
|
field: string;
|
|
311
|
+
/** Fallback column heading. */
|
|
245
312
|
label: string;
|
|
246
|
-
|
|
313
|
+
/** Optional translation key from this plugin's messages, or app:... for application entries. */
|
|
314
|
+
labelKey?: ExtensionTranslationKey;
|
|
247
315
|
component: Component<AdminListCellProps>;
|
|
248
316
|
}
|
|
249
317
|
|
|
250
318
|
export type AdminExtensionNotificationTone = "success" | "error";
|
|
251
319
|
|
|
320
|
+
/** Tools for a document action or extra document tab after it completes its own operation. */
|
|
252
321
|
export interface AdminDocumentExtensionHost {
|
|
322
|
+
/** Reload the current document from the server. This is not a save of unsaved form edits. */
|
|
253
323
|
refresh: () => Promise<void>;
|
|
324
|
+
/** Show a temporary success or error message. */
|
|
254
325
|
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
255
326
|
}
|
|
256
327
|
|
|
328
|
+
/** Saved document data for actions/tabs; use the generated SDK for application-specific operations. */
|
|
257
329
|
export interface AdminDocumentExtensionProps {
|
|
258
330
|
collection: SchemaCollection;
|
|
259
331
|
document: FieldDocument;
|
|
@@ -261,43 +333,49 @@ export interface AdminDocumentExtensionProps {
|
|
|
261
333
|
i18n: AdminI18n;
|
|
262
334
|
}
|
|
263
335
|
|
|
336
|
+
/** Add a component alongside the document's standard actions. Implement the operation in that component. */
|
|
264
337
|
export interface AdminDocumentAction {
|
|
265
338
|
key: string;
|
|
266
339
|
/** Omit to show the action for every non-global collection. */
|
|
267
340
|
collection?: string;
|
|
268
|
-
/** Hide
|
|
341
|
+
/** Hide unless this document operation is allowed. Visibility does not replace server authorization. */
|
|
269
342
|
requires?: keyof OperationCapabilities;
|
|
270
343
|
component: Component<AdminDocumentExtensionProps>;
|
|
271
344
|
}
|
|
272
345
|
|
|
346
|
+
/** Add a tab beside Edit and API. Keys `edit` and `api` are reserved for Ridu. */
|
|
273
347
|
export interface AdminDocumentView {
|
|
274
348
|
key: string;
|
|
275
349
|
label: string;
|
|
276
|
-
labelKey?:
|
|
277
|
-
/**
|
|
350
|
+
labelKey?: ExtensionTranslationKey;
|
|
351
|
+
/** Collection or global slug; omit to show for every collection and global. */
|
|
278
352
|
collection?: string;
|
|
279
353
|
component: Component<AdminDocumentExtensionProps>;
|
|
280
354
|
}
|
|
281
355
|
|
|
282
|
-
export interface
|
|
356
|
+
export interface AdminRouteNavigation {
|
|
283
357
|
/** Human-readable navigation label inside the authenticated admin shell. */
|
|
284
358
|
label: string;
|
|
285
|
-
labelKey?:
|
|
359
|
+
labelKey?: ExtensionTranslationKey;
|
|
286
360
|
/** Optional grouping hint reserved for shells that render grouped plugin navigation. */
|
|
287
361
|
group?: string;
|
|
288
362
|
}
|
|
289
363
|
|
|
290
364
|
/** One statically bundled route mounted beneath the authenticated admin layout. */
|
|
291
|
-
export interface
|
|
292
|
-
/** Relative
|
|
365
|
+
export interface AdminRoute {
|
|
366
|
+
/** Relative admin path, without a leading slash. Paired plugins must match their Go descriptor. */
|
|
293
367
|
path: string;
|
|
294
368
|
/** Svelte route component rendered by the framework router. */
|
|
295
369
|
component: Component;
|
|
296
370
|
/** Optional navigation entry; omit it for a route reachable only by links or redirects. */
|
|
297
|
-
navigation?:
|
|
371
|
+
navigation?: AdminRouteNavigation;
|
|
298
372
|
}
|
|
299
373
|
|
|
300
|
-
/**
|
|
374
|
+
/**
|
|
375
|
+
* Go plugin metadata written into Ridu's generated admin registration file.
|
|
376
|
+
* Do not hand-maintain a second copy: `ridu generate` resolves the executable Go
|
|
377
|
+
* configuration and emits the matching imports and compatibility checks.
|
|
378
|
+
*/
|
|
301
379
|
export interface BackendAdminPlugin {
|
|
302
380
|
/** Framework admin-plugin API required by the compiled backend. */
|
|
303
381
|
apiVersion: number;
|
|
@@ -313,18 +391,22 @@ export interface BackendAdminPlugin {
|
|
|
313
391
|
routes?: readonly string[];
|
|
314
392
|
/** Exact ordered package-relative static assets declared by the backend. */
|
|
315
393
|
assets?: readonly string[];
|
|
394
|
+
/** Exact field-type keys declared by this backend plugin. */
|
|
395
|
+
fieldTypes?: readonly string[];
|
|
316
396
|
}
|
|
317
397
|
|
|
398
|
+
/** One imported admin plugin and its generated Go metadata, compared before use. */
|
|
318
399
|
export interface AdminPluginPair {
|
|
319
400
|
admin: AdminPlugin;
|
|
320
401
|
backend: BackendAdminPlugin;
|
|
321
402
|
}
|
|
322
403
|
|
|
404
|
+
/** Checked plugin registrations collected for Ridu's admin runtime. Normally consumed by generated code. */
|
|
323
405
|
export interface ResolvedAdminPluginPairs {
|
|
324
406
|
plugins: readonly AdminPlugin[];
|
|
325
|
-
fields: readonly
|
|
407
|
+
fields: readonly ResolvedPluginField[];
|
|
326
408
|
rowLabels: readonly RowLabelPlugin[];
|
|
327
|
-
routes: readonly
|
|
409
|
+
routes: readonly AdminRoute[];
|
|
328
410
|
dashboard: readonly AdminDashboardPanel[];
|
|
329
411
|
login: readonly AdminLoginComponent[];
|
|
330
412
|
account: readonly AdminAccountComponent[];
|
|
@@ -338,27 +420,22 @@ export interface ResolvedAdminPluginPairs {
|
|
|
338
420
|
documentActions: readonly AdminDocumentAction[];
|
|
339
421
|
documentViews: readonly AdminDocumentView[];
|
|
340
422
|
messages: Readonly<Record<string, PluginMessageCatalog>>;
|
|
423
|
+
applicationMessages: PluginMessageCatalog | undefined;
|
|
341
424
|
}
|
|
342
425
|
|
|
343
|
-
export type
|
|
344
|
-
|
|
345
|
-
/** Defines one package export consumed by Ridu's generated static registry. */
|
|
346
|
-
export function defineAdminPlugin<const Plugin extends AdminPlugin>(plugin: Plugin): Plugin {
|
|
347
|
-
return plugin;
|
|
348
|
-
}
|
|
426
|
+
export type ResolvedAdminExtensions = Omit<ResolvedAdminPluginPairs, "plugins" | "fields">;
|
|
349
427
|
|
|
350
428
|
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
429
|
+
* Check that installed Go/admin plugin packages agree on identity, API version,
|
|
430
|
+
* pairing version, routes, assets and field types. Ridu's generated file calls this;
|
|
431
|
+
* authors should fix mismatched packages rather than editing generated metadata.
|
|
432
|
+
* Missing imports fail during compilation; incompatible pairs throw here.
|
|
354
433
|
*/
|
|
355
434
|
export function resolveAdminPluginPairs(
|
|
356
435
|
pairs: readonly AdminPluginPair[]
|
|
357
436
|
): ResolvedAdminPluginPairs {
|
|
358
437
|
const keys = new Set<string>();
|
|
359
|
-
const fieldIdentities = new Set<string>();
|
|
360
438
|
const plugins: AdminPlugin[] = [];
|
|
361
|
-
const fields: FieldPlugin[] = [];
|
|
362
439
|
|
|
363
440
|
for (const { admin, backend } of pairs) {
|
|
364
441
|
const identity = `${backend.package}#${backend.export}`;
|
|
@@ -390,39 +467,33 @@ export function resolveAdminPluginPairs(
|
|
|
390
467
|
(admin.routes ?? []).map((route) => route.path)
|
|
391
468
|
);
|
|
392
469
|
assertSameValues(`${backend.key} assets`, backend.assets ?? [], admin.assets ?? []);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
throw new Error(
|
|
399
|
-
`Admin plugin ${backend.key} registered plugin field ${field.key ?? "<missing>"}`
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
const fieldIdentity = `${field.type}:${field.key ?? ""}:${field.componentKey ?? ""}`;
|
|
403
|
-
if (fieldIdentities.has(fieldIdentity)) {
|
|
404
|
-
throw new Error(`Admin field renderer ${fieldIdentity} is registered more than once`);
|
|
405
|
-
}
|
|
406
|
-
fieldIdentities.add(fieldIdentity);
|
|
407
|
-
fields.push(field);
|
|
408
|
-
}
|
|
470
|
+
assertSameValues(
|
|
471
|
+
`${backend.key} field types`,
|
|
472
|
+
[...(backend.fieldTypes ?? [])].sort(),
|
|
473
|
+
Object.keys(admin.fields ?? {}).sort()
|
|
474
|
+
);
|
|
409
475
|
keys.add(backend.key);
|
|
410
476
|
plugins.push(admin);
|
|
411
477
|
}
|
|
412
|
-
const extensions =
|
|
478
|
+
const extensions = resolveAdminExtensions(plugins);
|
|
413
479
|
|
|
414
480
|
return {
|
|
415
481
|
plugins: Object.freeze(plugins),
|
|
416
|
-
fields:
|
|
482
|
+
fields: resolvePluginFields(plugins),
|
|
417
483
|
...extensions,
|
|
418
484
|
};
|
|
419
485
|
}
|
|
420
486
|
|
|
421
|
-
/**
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
487
|
+
/**
|
|
488
|
+
* Collect plugin UI first, then application UI, preserving order and rejecting
|
|
489
|
+
* duplicate identities or competing replacements. Used by Ridu's admin runtime;
|
|
490
|
+
* application authors normally provide `defineAdmin` configuration instead.
|
|
491
|
+
*/
|
|
492
|
+
export function resolveAdminExtensions(
|
|
493
|
+
plugins: readonly AdminPlugin[],
|
|
494
|
+
application?: AdminContributions & { messages?: PluginMessageCatalog }
|
|
495
|
+
): ResolvedAdminExtensions {
|
|
496
|
+
const routes: AdminRoute[] = [];
|
|
426
497
|
const dashboard: AdminDashboardPanel[] = [];
|
|
427
498
|
const login: AdminLoginComponent[] = [];
|
|
428
499
|
const account: AdminAccountComponent[] = [];
|
|
@@ -446,7 +517,14 @@ export function resolveAdminPluginExtensions(
|
|
|
446
517
|
let replacementNavigation: string | undefined;
|
|
447
518
|
let logoutButton: AdminLogoutButton | undefined;
|
|
448
519
|
|
|
449
|
-
|
|
520
|
+
const sources = [
|
|
521
|
+
...plugins.map((plugin) => ({ ...plugin, namespace: `plugin.${plugin.key}:` })),
|
|
522
|
+
...(application === undefined
|
|
523
|
+
? []
|
|
524
|
+
: [{ ...application, key: "application", namespace: "app:", rowLabels: [] }]),
|
|
525
|
+
];
|
|
526
|
+
for (const plugin of sources) {
|
|
527
|
+
validateContributions(plugin, plugin.namespace === "app:");
|
|
450
528
|
for (const rowLabel of plugin.rowLabels ?? []) {
|
|
451
529
|
if (rowLabel.key !== plugin.key) {
|
|
452
530
|
throw new Error(
|
|
@@ -465,7 +543,7 @@ export function resolveAdminPluginExtensions(
|
|
|
465
543
|
);
|
|
466
544
|
rowLabels.push(rowLabel);
|
|
467
545
|
}
|
|
468
|
-
if (plugin.messages !== undefined) {
|
|
546
|
+
if (plugin.messages !== undefined && plugin.namespace !== "app:") {
|
|
469
547
|
if (messages[plugin.key] !== undefined) {
|
|
470
548
|
throw new Error(`Admin plugin messages for ${plugin.key} are registered more than once`);
|
|
471
549
|
}
|
|
@@ -474,7 +552,7 @@ export function resolveAdminPluginExtensions(
|
|
|
474
552
|
}
|
|
475
553
|
for (const route of plugin.routes ?? []) {
|
|
476
554
|
validatePluginLabelKey(plugin, route.navigation?.labelKey, `route ${route.path}`);
|
|
477
|
-
claim(identities, `route:${route.path}`, `Admin plugin route ${route.path}`);
|
|
555
|
+
claim(identities, `route:${route.path.toLowerCase()}`, `Admin plugin route ${route.path}`);
|
|
478
556
|
routes.push(route);
|
|
479
557
|
}
|
|
480
558
|
for (const panel of plugin.dashboard ?? []) {
|
|
@@ -613,6 +691,8 @@ export function resolveAdminPluginExtensions(
|
|
|
613
691
|
}
|
|
614
692
|
|
|
615
693
|
return {
|
|
694
|
+
applicationMessages:
|
|
695
|
+
application?.messages === undefined ? undefined : freezeAdminMessages(application.messages),
|
|
616
696
|
rowLabels: Object.freeze(rowLabels),
|
|
617
697
|
routes: Object.freeze(routes),
|
|
618
698
|
dashboard: Object.freeze(dashboard),
|
|
@@ -634,12 +714,12 @@ export function resolveAdminPluginExtensions(
|
|
|
634
714
|
const adminComponentKeyPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
635
715
|
|
|
636
716
|
function validatePluginLabelKey(
|
|
637
|
-
plugin:
|
|
638
|
-
labelKey:
|
|
717
|
+
plugin: { key: string; namespace: string; messages?: PluginMessageCatalog },
|
|
718
|
+
labelKey: ExtensionTranslationKey | undefined,
|
|
639
719
|
surface: string
|
|
640
720
|
) {
|
|
641
721
|
if (labelKey === undefined) return;
|
|
642
|
-
const prefix =
|
|
722
|
+
const prefix = plugin.namespace;
|
|
643
723
|
if (!labelKey.startsWith(prefix)) {
|
|
644
724
|
throw new Error(
|
|
645
725
|
`Admin plugin ${plugin.key} ${surface} label key must use its own ${prefix} namespace`
|
|
@@ -663,3 +743,114 @@ function assertSameValues(label: string, backend: readonly string[], admin: read
|
|
|
663
743
|
throw new Error(`Admin plugin ${label} do not match the compiled backend declaration`);
|
|
664
744
|
}
|
|
665
745
|
}
|
|
746
|
+
|
|
747
|
+
// Application routes are literal paths: no router
|
|
748
|
+
// patterns, query strings, normalization aliases or framework namespace shadowing.
|
|
749
|
+
const reservedRouteRoots = new Set([
|
|
750
|
+
"account",
|
|
751
|
+
"collections",
|
|
752
|
+
"globals",
|
|
753
|
+
"login",
|
|
754
|
+
"create-first-user",
|
|
755
|
+
"forgot-password",
|
|
756
|
+
"reset-password",
|
|
757
|
+
"request-verification",
|
|
758
|
+
"verify-email",
|
|
759
|
+
]);
|
|
760
|
+
function validateContributions(
|
|
761
|
+
source: AdminContributions & { messages?: PluginMessageCatalog },
|
|
762
|
+
local: boolean
|
|
763
|
+
) {
|
|
764
|
+
if (source.messages !== undefined) validateAdminMessages("application", source.messages);
|
|
765
|
+
const groups = [
|
|
766
|
+
"routes",
|
|
767
|
+
"dashboard",
|
|
768
|
+
"login",
|
|
769
|
+
"account",
|
|
770
|
+
"navigation",
|
|
771
|
+
"views",
|
|
772
|
+
"branding",
|
|
773
|
+
"shell",
|
|
774
|
+
"providers",
|
|
775
|
+
"listCells",
|
|
776
|
+
"documentActions",
|
|
777
|
+
"documentViews",
|
|
778
|
+
] as const;
|
|
779
|
+
const positions = {
|
|
780
|
+
dashboard: ["before", "after", "replace"],
|
|
781
|
+
login: ["before", "after", "replace"],
|
|
782
|
+
account: ["before", "after", "replace"],
|
|
783
|
+
navigation: ["before", "beforeLinks", "afterLinks", "after", "replace"],
|
|
784
|
+
shell: ["header", "actions", "settingsMenu"],
|
|
785
|
+
} as const;
|
|
786
|
+
for (const group of groups) {
|
|
787
|
+
const entries = source[group];
|
|
788
|
+
if (entries === undefined) continue;
|
|
789
|
+
if (!Array.isArray(entries)) throw new Error(`Admin ${group} registrations must be an array.`);
|
|
790
|
+
for (const entry of entries) {
|
|
791
|
+
if (entry === null || typeof entry !== "object" || typeof entry.component !== "function")
|
|
792
|
+
throw new Error(`Admin ${group} registration requires a Svelte component.`);
|
|
793
|
+
if (
|
|
794
|
+
"key" in entry &&
|
|
795
|
+
(typeof entry.key !== "string" || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(entry.key))
|
|
796
|
+
)
|
|
797
|
+
throw new Error(`Admin ${group} registration has an invalid key.`);
|
|
798
|
+
if (group !== "routes" && !("key" in entry))
|
|
799
|
+
throw new Error(`Admin ${group} registration requires a key.`);
|
|
800
|
+
if (group in positions) {
|
|
801
|
+
const allowed: readonly string[] = positions[group as keyof typeof positions];
|
|
802
|
+
const position = "position" in entry ? entry.position : undefined;
|
|
803
|
+
if (
|
|
804
|
+
(position === undefined && (group === "navigation" || group === "shell")) ||
|
|
805
|
+
(position !== undefined && !allowed.includes(position))
|
|
806
|
+
)
|
|
807
|
+
throw new Error(`Admin ${group} registration has an invalid position.`);
|
|
808
|
+
}
|
|
809
|
+
if (group === "account" || group === "branding" || group === "views") {
|
|
810
|
+
const allowed =
|
|
811
|
+
group === "account"
|
|
812
|
+
? ["profile", "security"]
|
|
813
|
+
: group === "branding"
|
|
814
|
+
? ["loginLogo", "navigationLogo", "accountAvatar"]
|
|
815
|
+
: ["collectionList", "collectionCreate", "collectionEdit", "global", "notFound"];
|
|
816
|
+
if (!("surface" in entry) || !allowed.includes(entry.surface))
|
|
817
|
+
throw new Error(`Admin ${group} registration has an invalid surface.`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const route of local ? (source.routes ?? []) : []) {
|
|
822
|
+
if (
|
|
823
|
+
typeof route.path !== "string" ||
|
|
824
|
+
!/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(route.path) ||
|
|
825
|
+
reservedRouteRoots.has(route.path.split("/")[0]!.toLowerCase())
|
|
826
|
+
)
|
|
827
|
+
throw new Error(
|
|
828
|
+
`Admin route ${route.path} must be a literal relative path outside framework namespaces.`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
if (
|
|
832
|
+
source.logoutButton !== undefined &&
|
|
833
|
+
(typeof source.logoutButton.component !== "function" || !source.logoutButton.key)
|
|
834
|
+
)
|
|
835
|
+
throw new Error("Admin logout button requires a key and Svelte component.");
|
|
836
|
+
for (const action of source.documentActions ?? []) {
|
|
837
|
+
if (
|
|
838
|
+
action.requires !== undefined &&
|
|
839
|
+
![
|
|
840
|
+
"admin",
|
|
841
|
+
"create",
|
|
842
|
+
"read",
|
|
843
|
+
"readVersions",
|
|
844
|
+
"update",
|
|
845
|
+
"delete",
|
|
846
|
+
"duplicate",
|
|
847
|
+
"publish",
|
|
848
|
+
"unpublish",
|
|
849
|
+
"restoreDeleted",
|
|
850
|
+
"deletePermanent",
|
|
851
|
+
"selectAll",
|
|
852
|
+
].includes(action.requires)
|
|
853
|
+
)
|
|
854
|
+
throw new Error(`Admin document action ${action.key} has an invalid required operation.`);
|
|
855
|
+
}
|
|
856
|
+
}
|