@riducms/plugin 0.2.3 → 0.4.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
package/src/plugin.ts
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import type { FieldType } from "@riducms/protocol";
|
|
2
2
|
import type { RegisteredPluginField } from "./field";
|
|
3
3
|
import { ADMIN_PLUGIN_API_VERSION } from "@riducms/protocol";
|
|
4
|
-
import type {
|
|
5
|
-
OperationCapabilities,
|
|
6
|
-
SchemaCollection,
|
|
7
|
-
SchemaField,
|
|
8
|
-
SchemaGlobal,
|
|
9
|
-
SchemaManifest,
|
|
10
|
-
} from "@riducms/protocol";
|
|
11
|
-
import type { Component, Snippet } from "svelte";
|
|
12
|
-
|
|
13
|
-
import type { FieldDocument } from "./authoring";
|
|
14
|
-
import type { AdminI18n, PluginMessageCatalog, ExtensionTranslationKey } from "./i18n";
|
|
4
|
+
import type { PluginMessageCatalog, ExtensionTranslationKey } from "./i18n";
|
|
15
5
|
import { freezeAdminMessages, validateAdminMessages } from "./i18n";
|
|
16
6
|
import type { RowLabelPlugin } from "./row-label";
|
|
7
|
+
import type {
|
|
8
|
+
AdminAccountComponent,
|
|
9
|
+
AdminAccountSurface,
|
|
10
|
+
AdminBrandComponent,
|
|
11
|
+
AdminContributions,
|
|
12
|
+
AdminCoreView,
|
|
13
|
+
AdminDashboardPanel,
|
|
14
|
+
AdminDocumentAction,
|
|
15
|
+
AdminDocumentView,
|
|
16
|
+
AdminListCellRenderer,
|
|
17
|
+
AdminListResultsRenderer,
|
|
18
|
+
AdminLoginComponent,
|
|
19
|
+
AdminLogoutButton,
|
|
20
|
+
AdminNavigationComponent,
|
|
21
|
+
AdminProvider,
|
|
22
|
+
AdminRoute,
|
|
23
|
+
AdminShellSlot,
|
|
24
|
+
ResolvedAdminExtensions,
|
|
25
|
+
} from "./admin-extensions";
|
|
26
|
+
|
|
27
|
+
export * from "./admin-extensions";
|
|
17
28
|
|
|
18
29
|
export { ADMIN_PLUGIN_API_VERSION };
|
|
19
30
|
|
|
@@ -43,7 +54,7 @@ export interface AdminPlugin extends AdminContributions {
|
|
|
43
54
|
* Go chooses one with `.Admin(field.Admin{Editor: field.PluginComponent(pluginKey, componentName, config)})`.
|
|
44
55
|
* These may edit built-in fields or an explicitly named plugin field type.
|
|
45
56
|
*/
|
|
46
|
-
|
|
57
|
+
fieldEditors?: Readonly<
|
|
47
58
|
Record<
|
|
48
59
|
string,
|
|
49
60
|
RegisteredPluginField &
|
|
@@ -61,315 +72,6 @@ export interface AdminPlugin extends AdminContributions {
|
|
|
61
72
|
assets?: readonly string[];
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
/**
|
|
65
|
-
* Places where a plugin or application can add admin UI. Arrays keep registration
|
|
66
|
-
* order; application entries follow plugin entries. Replacement slots allow one
|
|
67
|
-
* matching replacement. Conflicts are reported instead of silently overriding UI.
|
|
68
|
-
*/
|
|
69
|
-
export interface AdminContributions {
|
|
70
|
-
/** Authenticated admin routes contributed by this package. */
|
|
71
|
-
routes?: readonly AdminRoute[];
|
|
72
|
-
/** Panels composed into or replacing the authenticated dashboard. */
|
|
73
|
-
dashboard?: readonly AdminDashboardPanel[];
|
|
74
|
-
/** Components composed around or replacing the sign-in screen. */
|
|
75
|
-
login?: readonly AdminLoginComponent[];
|
|
76
|
-
/** Components composed around or replacing the framework account screens. */
|
|
77
|
-
account?: readonly AdminAccountComponent[];
|
|
78
|
-
/** Components composed around or replacing the framework navigation. */
|
|
79
|
-
navigation?: readonly AdminNavigationComponent[];
|
|
80
|
-
/** Replaces the default sign-out control in the account menu. */
|
|
81
|
-
logoutButton?: AdminLogoutButton;
|
|
82
|
-
/** Wraps or replaces framework collection, global, and not-found views. */
|
|
83
|
-
views?: readonly AdminCoreView[];
|
|
84
|
-
/** Replaces framework-owned brand graphics at exact shell surfaces. */
|
|
85
|
-
branding?: readonly AdminBrandComponent[];
|
|
86
|
-
/** Adds global header, action, or account-settings menu components. */
|
|
87
|
-
shell?: readonly AdminShellComponent[];
|
|
88
|
-
/** Wraps the admin in statically ordered Svelte context providers. */
|
|
89
|
-
providers?: readonly AdminProvider[];
|
|
90
|
-
/** Collection list cells rendered for exact collection/field pairs. */
|
|
91
|
-
listCells?: readonly AdminListCell[];
|
|
92
|
-
/** Actions rendered alongside framework-owned document actions. */
|
|
93
|
-
documentActions?: readonly AdminDocumentAction[];
|
|
94
|
-
/** Read-only or operational views rendered beside Edit and API. */
|
|
95
|
-
documentViews?: readonly AdminDocumentView[];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Common data supplied to admin extension components. Use the generated SDK for requests. */
|
|
99
|
-
export interface AdminExtensionProps {
|
|
100
|
-
/** Resolved Go schema available to this admin session; it may omit inaccessible resources. */
|
|
101
|
-
manifest: SchemaManifest;
|
|
102
|
-
/** Signed-in user document, absent on screens where no user is signed in yet. */
|
|
103
|
-
user?: FieldDocument;
|
|
104
|
-
/** Current admin interface translations and formatting preferences. */
|
|
105
|
-
i18n: AdminI18n;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Sign-in operations provided to a custom login screen. */
|
|
109
|
-
export interface AdminLoginExtensionHost {
|
|
110
|
-
/** Sign in, check admin access and enter the admin. Rejects if authentication/access fails. */
|
|
111
|
-
login: (credentials: { email: string; password: string }) => Promise<void>;
|
|
112
|
-
/** Show a temporary success or error message. Does not perform an operation itself. */
|
|
113
|
-
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface AdminLoginComponentProps extends AdminExtensionProps {
|
|
117
|
-
/** Render with `{@render defaultView()}` to keep Ridu's sign-in screen inside your wrapper. */
|
|
118
|
-
defaultView: Snippet;
|
|
119
|
-
host: AdminLoginExtensionHost;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Add content before/after sign-in, or replace it with a custom screen. */
|
|
123
|
-
export interface AdminLoginComponent {
|
|
124
|
-
key: string;
|
|
125
|
-
component: Component<AdminLoginComponentProps>;
|
|
126
|
-
/** Defaults to after. Only one replace entry is allowed across plugins and the application. */
|
|
127
|
-
position?: "before" | "after" | "replace";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export type AdminAccountSurface = "profile" | "security";
|
|
131
|
-
|
|
132
|
-
/** Operations for keeping a custom account screen in sync with the signed-in session. */
|
|
133
|
-
export interface AdminAccountExtensionHost {
|
|
134
|
-
/** Reloads the signed-in document and updates the shell identity. */
|
|
135
|
-
refreshUser: () => Promise<FieldDocument | undefined>;
|
|
136
|
-
/** Ends the current session and returns to sign-in. */
|
|
137
|
-
logout: () => Promise<void>;
|
|
138
|
-
/** Show a temporary success or error message. */
|
|
139
|
-
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface AdminAccountComponentProps extends AdminExtensionProps {
|
|
143
|
-
surface: AdminAccountSurface;
|
|
144
|
-
/** Render with `{@render defaultView()}` to keep Ridu's account screen inside your wrapper. */
|
|
145
|
-
defaultView: Snippet;
|
|
146
|
-
host: AdminAccountExtensionHost;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Add to or replace the profile/security screen identified by `surface`. */
|
|
150
|
-
export interface AdminAccountComponent {
|
|
151
|
-
key: string;
|
|
152
|
-
surface: AdminAccountSurface;
|
|
153
|
-
component: Component<AdminAccountComponentProps>;
|
|
154
|
-
/** Defaults to after. Each account surface permits at most one replacement. */
|
|
155
|
-
position?: "before" | "after" | "replace";
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Choose the whole navigation's boundary, the resource-link list's boundary, or replace navigation. */
|
|
159
|
-
export type AdminNavigationPosition = "before" | "beforeLinks" | "afterLinks" | "after" | "replace";
|
|
160
|
-
|
|
161
|
-
export interface AdminNavigationComponentProps extends AdminExtensionProps {
|
|
162
|
-
/** Render with `{@render defaultView()}` to include Ridu's navigation in a replacement. */
|
|
163
|
-
defaultView: Snippet;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Insert a component at a navigation position. At most one entry can replace navigation. */
|
|
167
|
-
export interface AdminNavigationComponent {
|
|
168
|
-
key: string;
|
|
169
|
-
component: Component<AdminNavigationComponentProps>;
|
|
170
|
-
position: AdminNavigationPosition;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export interface AdminLogoutExtensionHost {
|
|
174
|
-
/** End the current session and return to sign-in. Await this instead of only changing local UI. */
|
|
175
|
-
logout: () => Promise<void>;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export interface AdminLogoutButtonProps extends AdminExtensionProps {
|
|
179
|
-
host: AdminLogoutExtensionHost;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Replace the account menu's sign-out button; call the supplied `host.logout()` on activation. */
|
|
183
|
-
export interface AdminLogoutButton {
|
|
184
|
-
key: string;
|
|
185
|
-
component: Component<AdminLogoutButtonProps>;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export type AdminCoreViewSurface =
|
|
189
|
-
"collectionList" | "collectionCreate" | "collectionEdit" | "global" | "notFound";
|
|
190
|
-
|
|
191
|
-
/** Refresh tools for a custom collection/global screen. They do not save documents. */
|
|
192
|
-
export interface AdminCoreViewHost {
|
|
193
|
-
/** Fetch the current Go schema again and update the admin's schema-dependent UI. */
|
|
194
|
-
refreshManifest: () => Promise<void>;
|
|
195
|
-
/** Notify Ridu that your code changed documents so dependent lists/lookups can refresh. */
|
|
196
|
-
documentsChanged: () => void;
|
|
197
|
-
/** Show a temporary success or error message. */
|
|
198
|
-
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export interface AdminCoreViewProps extends AdminExtensionProps {
|
|
202
|
-
surface: AdminCoreViewSurface;
|
|
203
|
-
collection?: SchemaCollection;
|
|
204
|
-
global?: SchemaGlobal;
|
|
205
|
-
documentID?: string;
|
|
206
|
-
/** Render with `{@render defaultView()}` to wrap the normal screen rather than rebuilding it. */
|
|
207
|
-
defaultView: Snippet;
|
|
208
|
-
host: AdminCoreViewHost;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
interface AdminCollectionCoreView {
|
|
212
|
-
key: string;
|
|
213
|
-
surface: "collectionList" | "collectionCreate" | "collectionEdit";
|
|
214
|
-
/** Omit to replace this surface for every collection. */
|
|
215
|
-
collection?: string;
|
|
216
|
-
component: Component<AdminCoreViewProps>;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
interface AdminGlobalCoreView {
|
|
220
|
-
key: string;
|
|
221
|
-
surface: "global";
|
|
222
|
-
/** Omit to replace this surface for every global. */
|
|
223
|
-
global?: string;
|
|
224
|
-
component: Component<AdminCoreViewProps>;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
interface AdminNotFoundCoreView {
|
|
228
|
-
key: string;
|
|
229
|
-
surface: "notFound";
|
|
230
|
-
component: Component<AdminCoreViewProps>;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Replace a collection/global/not-found screen. A resource-specific registration
|
|
235
|
-
* wins over an all-resources fallback for that surface; duplicate targets fail.
|
|
236
|
-
* The supplied `defaultView` lets your component wrap the existing screen.
|
|
237
|
-
*/
|
|
238
|
-
export type AdminCoreView = AdminCollectionCoreView | AdminGlobalCoreView | AdminNotFoundCoreView;
|
|
239
|
-
|
|
240
|
-
export type AdminBrandSurface = "loginLogo" | "navigationLogo" | "accountAvatar";
|
|
241
|
-
|
|
242
|
-
export interface AdminBrandComponentProps extends AdminExtensionProps {
|
|
243
|
-
surface: AdminBrandSurface;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Replace one logo/avatar location. Only one component may claim each surface. */
|
|
247
|
-
export interface AdminBrandComponent {
|
|
248
|
-
key: string;
|
|
249
|
-
surface: AdminBrandSurface;
|
|
250
|
-
component: Component<AdminBrandComponentProps>;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export type AdminShellPosition = "header" | "actions" | "settingsMenu";
|
|
254
|
-
|
|
255
|
-
export interface AdminShellComponentProps extends AdminExtensionProps {
|
|
256
|
-
position: AdminShellPosition;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Add UI to the global header, actions area or account settings menu in registration order. */
|
|
260
|
-
export interface AdminShellComponent {
|
|
261
|
-
key: string;
|
|
262
|
-
position: AdminShellPosition;
|
|
263
|
-
component: Component<AdminShellComponentProps>;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export interface AdminProviderProps {
|
|
267
|
-
/** Render `{@render defaultView()}` after setting context so the nested providers/admin appear. */
|
|
268
|
-
defaultView: Snippet;
|
|
269
|
-
i18n: AdminI18n;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Wrap the admin in a Svelte context provider. Render the supplied `defaultView` to continue the UI. */
|
|
273
|
-
export interface AdminProvider {
|
|
274
|
-
key: string;
|
|
275
|
-
component: Component<AdminProviderProps>;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export interface AdminDashboardPanelProps {
|
|
279
|
-
manifest: SchemaManifest;
|
|
280
|
-
user?: FieldDocument;
|
|
281
|
-
i18n: AdminI18n;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** Add dashboard content before/after Ridu's overview, or replace that overview. */
|
|
285
|
-
export interface AdminDashboardPanel {
|
|
286
|
-
/** Unique name among dashboard entries; duplicates throw. Array order controls display order. */
|
|
287
|
-
key: string;
|
|
288
|
-
component: Component<AdminDashboardPanelProps>;
|
|
289
|
-
/** Defaults to after. Replace hides Ridu's overview and may only be registered once. */
|
|
290
|
-
position?: "before" | "after" | "replace";
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Data for displaying a collection table cell. This is not an editable document-form binding. */
|
|
294
|
-
export interface AdminListCellProps {
|
|
295
|
-
collection: SchemaCollection;
|
|
296
|
-
field: SchemaField;
|
|
297
|
-
document: FieldDocument;
|
|
298
|
-
/** This field's saved value. Check its type before rendering; changing it does not save a document. */
|
|
299
|
-
value: unknown;
|
|
300
|
-
i18n: AdminI18n;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Replace the cell display for one collection/field pair; duplicate targets are rejected. */
|
|
304
|
-
export interface AdminListCell {
|
|
305
|
-
key: string;
|
|
306
|
-
/** Collection slug, for example `"posts"`. */
|
|
307
|
-
collection: string;
|
|
308
|
-
/** Top-level field name/path in that collection, for example `"title"`. */
|
|
309
|
-
field: string;
|
|
310
|
-
/** Fallback column heading. */
|
|
311
|
-
label: string;
|
|
312
|
-
/** Optional translation key from this plugin's messages, or app:... for application entries. */
|
|
313
|
-
labelKey?: ExtensionTranslationKey;
|
|
314
|
-
component: Component<AdminListCellProps>;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export type AdminExtensionNotificationTone = "success" | "error";
|
|
318
|
-
|
|
319
|
-
/** Tools for a document action or extra document tab after it completes its own operation. */
|
|
320
|
-
export interface AdminDocumentExtensionHost {
|
|
321
|
-
/** Reload the current document from the server. This is not a save of unsaved form edits. */
|
|
322
|
-
refresh: () => Promise<void>;
|
|
323
|
-
/** Show a temporary success or error message. */
|
|
324
|
-
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** Saved document data for actions/tabs; use the generated SDK for application-specific operations. */
|
|
328
|
-
export interface AdminDocumentExtensionProps {
|
|
329
|
-
collection: SchemaCollection;
|
|
330
|
-
document: FieldDocument;
|
|
331
|
-
host: AdminDocumentExtensionHost;
|
|
332
|
-
i18n: AdminI18n;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** Add a component alongside the document's standard actions. Implement the operation in that component. */
|
|
336
|
-
export interface AdminDocumentAction {
|
|
337
|
-
key: string;
|
|
338
|
-
/** Omit to show the action for every non-global collection. */
|
|
339
|
-
collection?: string;
|
|
340
|
-
/** Hide unless this document operation is allowed. Visibility does not replace server authorization. */
|
|
341
|
-
requires?: keyof OperationCapabilities;
|
|
342
|
-
component: Component<AdminDocumentExtensionProps>;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** Add a tab beside Edit and API. Keys `edit` and `api` are reserved for Ridu. */
|
|
346
|
-
export interface AdminDocumentView {
|
|
347
|
-
key: string;
|
|
348
|
-
label: string;
|
|
349
|
-
labelKey?: ExtensionTranslationKey;
|
|
350
|
-
/** Collection or global slug; omit to show for every collection and global. */
|
|
351
|
-
collection?: string;
|
|
352
|
-
component: Component<AdminDocumentExtensionProps>;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export interface AdminRouteNavigation {
|
|
356
|
-
/** Human-readable navigation label inside the authenticated admin shell. */
|
|
357
|
-
label: string;
|
|
358
|
-
labelKey?: ExtensionTranslationKey;
|
|
359
|
-
/** Optional grouping hint reserved for shells that render grouped plugin navigation. */
|
|
360
|
-
group?: string;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** One statically bundled route mounted beneath the authenticated admin layout. */
|
|
364
|
-
export interface AdminRoute {
|
|
365
|
-
/** Relative admin path, without a leading slash. Paired plugins must match their Go descriptor. */
|
|
366
|
-
path: string;
|
|
367
|
-
/** Svelte route component rendered by the framework router. */
|
|
368
|
-
component: Component;
|
|
369
|
-
/** Optional navigation entry; omit it for a route reachable only by links or redirects. */
|
|
370
|
-
navigation?: AdminRouteNavigation;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
75
|
/**
|
|
374
76
|
* Go plugin metadata written into Ridu's generated admin registration file.
|
|
375
77
|
* Do not hand-maintain a second copy: `ridu generate` resolves the executable Go
|
|
@@ -400,26 +102,6 @@ export interface AdminPluginPair {
|
|
|
400
102
|
backend: BackendAdminPlugin;
|
|
401
103
|
}
|
|
402
104
|
|
|
403
|
-
/** Checked plugin registrations collected for Ridu's admin runtime. Normally consumed by generated code. */
|
|
404
|
-
export interface ResolvedAdminExtensions {
|
|
405
|
-
rowLabels: readonly RowLabelPlugin[];
|
|
406
|
-
routes: readonly AdminRoute[];
|
|
407
|
-
dashboard: readonly AdminDashboardPanel[];
|
|
408
|
-
login: readonly AdminLoginComponent[];
|
|
409
|
-
account: readonly AdminAccountComponent[];
|
|
410
|
-
navigation: readonly AdminNavigationComponent[];
|
|
411
|
-
logoutButton: AdminLogoutButton | undefined;
|
|
412
|
-
views: readonly AdminCoreView[];
|
|
413
|
-
branding: readonly AdminBrandComponent[];
|
|
414
|
-
shell: readonly AdminShellComponent[];
|
|
415
|
-
providers: readonly AdminProvider[];
|
|
416
|
-
listCells: readonly AdminListCell[];
|
|
417
|
-
documentActions: readonly AdminDocumentAction[];
|
|
418
|
-
documentViews: readonly AdminDocumentView[];
|
|
419
|
-
messages: Readonly<Record<string, PluginMessageCatalog>>;
|
|
420
|
-
applicationMessages: PluginMessageCatalog | undefined;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
105
|
/**
|
|
424
106
|
* Check that installed Go/admin plugin packages agree on identity, API version,
|
|
425
107
|
* pairing version, routes, assets and field types. Ridu's generated file calls this;
|
|
@@ -478,15 +160,16 @@ export function resolveAdminExtensions(
|
|
|
478
160
|
application?: AdminContributions & { messages?: PluginMessageCatalog }
|
|
479
161
|
): ResolvedAdminExtensions {
|
|
480
162
|
const routes: AdminRoute[] = [];
|
|
481
|
-
const
|
|
163
|
+
const dashboardPanels: AdminDashboardPanel[] = [];
|
|
482
164
|
const login: AdminLoginComponent[] = [];
|
|
483
165
|
const account: AdminAccountComponent[] = [];
|
|
484
166
|
const navigation: AdminNavigationComponent[] = [];
|
|
485
|
-
const
|
|
167
|
+
const coreViews: AdminCoreView[] = [];
|
|
486
168
|
const branding: AdminBrandComponent[] = [];
|
|
487
|
-
const
|
|
169
|
+
const shellSlots: AdminShellSlot[] = [];
|
|
488
170
|
const providers: AdminProvider[] = [];
|
|
489
|
-
const
|
|
171
|
+
const listCellRenderers: AdminListCellRenderer[] = [];
|
|
172
|
+
const listResultsRenderers: AdminListResultsRenderer[] = [];
|
|
490
173
|
const documentActions: AdminDocumentAction[] = [];
|
|
491
174
|
const documentViews: AdminDocumentView[] = [];
|
|
492
175
|
const rowLabels: RowLabelPlugin[] = [];
|
|
@@ -494,6 +177,8 @@ export function resolveAdminExtensions(
|
|
|
494
177
|
string,
|
|
495
178
|
PluginMessageCatalog
|
|
496
179
|
>;
|
|
180
|
+
// These identities represent mount slots, not package-local names. Claiming one
|
|
181
|
+
// set across every source prevents application UI from silently shadowing a plugin.
|
|
497
182
|
const identities = new Set<string>();
|
|
498
183
|
let replacementDashboard: string | undefined;
|
|
499
184
|
let replacementLogin: string | undefined;
|
|
@@ -501,6 +186,8 @@ export function resolveAdminExtensions(
|
|
|
501
186
|
let replacementNavigation: string | undefined;
|
|
502
187
|
let logoutButton: AdminLogoutButton | undefined;
|
|
503
188
|
|
|
189
|
+
// Preserve authored order within each source, with the application consistently
|
|
190
|
+
// following installed plugins for additive surfaces and provider nesting.
|
|
504
191
|
const sources = [
|
|
505
192
|
...plugins.map((plugin) => ({ ...plugin, namespace: `plugin.${plugin.key}:` })),
|
|
506
193
|
...(application === undefined
|
|
@@ -508,7 +195,7 @@ export function resolveAdminExtensions(
|
|
|
508
195
|
: [{ ...application, key: "application", namespace: "app:", rowLabels: [] }]),
|
|
509
196
|
];
|
|
510
197
|
for (const plugin of sources) {
|
|
511
|
-
validateContributions(plugin
|
|
198
|
+
validateContributions(plugin);
|
|
512
199
|
for (const rowLabel of plugin.rowLabels ?? []) {
|
|
513
200
|
if (rowLabel.key !== plugin.key) {
|
|
514
201
|
throw new Error(
|
|
@@ -536,10 +223,12 @@ export function resolveAdminExtensions(
|
|
|
536
223
|
}
|
|
537
224
|
for (const route of plugin.routes ?? []) {
|
|
538
225
|
validatePluginLabelKey(plugin, route.navigation?.labelKey, `route ${route.path}`);
|
|
226
|
+
// Treat differently cased paths as one identity rather than allowing
|
|
227
|
+
// registrations whose reachability depends on a host or proxy's normalization.
|
|
539
228
|
claim(identities, `route:${route.path.toLowerCase()}`, `Admin plugin route ${route.path}`);
|
|
540
229
|
routes.push(route);
|
|
541
230
|
}
|
|
542
|
-
for (const panel of plugin.
|
|
231
|
+
for (const panel of plugin.dashboardPanels ?? []) {
|
|
543
232
|
claim(identities, `dashboard:${panel.key}`, `Admin dashboard panel ${panel.key}`);
|
|
544
233
|
if (panel.position === "replace") {
|
|
545
234
|
if (replacementDashboard !== undefined) {
|
|
@@ -549,7 +238,7 @@ export function resolveAdminExtensions(
|
|
|
549
238
|
}
|
|
550
239
|
replacementDashboard = panel.key;
|
|
551
240
|
}
|
|
552
|
-
|
|
241
|
+
dashboardPanels.push(panel);
|
|
553
242
|
}
|
|
554
243
|
for (const component of plugin.login ?? []) {
|
|
555
244
|
claim(identities, `login:${component.key}`, `Admin login component ${component.key}`);
|
|
@@ -609,7 +298,7 @@ export function resolveAdminExtensions(
|
|
|
609
298
|
);
|
|
610
299
|
logoutButton = plugin.logoutButton;
|
|
611
300
|
}
|
|
612
|
-
for (const view of plugin.
|
|
301
|
+
for (const view of plugin.coreViews ?? []) {
|
|
613
302
|
const resource =
|
|
614
303
|
"collection" in view
|
|
615
304
|
? (view.collection ?? "*")
|
|
@@ -621,7 +310,7 @@ export function resolveAdminExtensions(
|
|
|
621
310
|
`core-view:${view.surface}:${resource}`,
|
|
622
311
|
`Admin core view ${view.surface}.${resource}`
|
|
623
312
|
);
|
|
624
|
-
|
|
313
|
+
coreViews.push(view);
|
|
625
314
|
}
|
|
626
315
|
for (const component of plugin.branding ?? []) {
|
|
627
316
|
claim(
|
|
@@ -631,26 +320,34 @@ export function resolveAdminExtensions(
|
|
|
631
320
|
);
|
|
632
321
|
branding.push(component);
|
|
633
322
|
}
|
|
634
|
-
for (const component of plugin.
|
|
323
|
+
for (const component of plugin.shellSlots ?? []) {
|
|
635
324
|
claim(
|
|
636
325
|
identities,
|
|
637
326
|
`shell:${component.position}:${component.key}`,
|
|
638
|
-
`Admin shell
|
|
327
|
+
`Admin shell slot ${component.key}`
|
|
639
328
|
);
|
|
640
|
-
|
|
329
|
+
shellSlots.push(component);
|
|
641
330
|
}
|
|
642
331
|
for (const provider of plugin.providers ?? []) {
|
|
643
332
|
claim(identities, `provider:${provider.key}`, `Admin provider ${provider.key}`);
|
|
644
333
|
providers.push(provider);
|
|
645
334
|
}
|
|
646
|
-
for (const cell of plugin.
|
|
335
|
+
for (const cell of plugin.listCellRenderers ?? []) {
|
|
647
336
|
validatePluginLabelKey(plugin, cell.labelKey, `list cell ${cell.collection}.${cell.field}`);
|
|
648
337
|
claim(
|
|
649
338
|
identities,
|
|
650
339
|
`list-cell:${cell.collection}:${cell.field}`,
|
|
651
|
-
`Admin list
|
|
340
|
+
`Admin list-cell renderer ${cell.collection}.${cell.field}`
|
|
341
|
+
);
|
|
342
|
+
listCellRenderers.push(cell);
|
|
343
|
+
}
|
|
344
|
+
for (const results of plugin.listResultsRenderers ?? []) {
|
|
345
|
+
claim(
|
|
346
|
+
identities,
|
|
347
|
+
`list-results:${results.collection}`,
|
|
348
|
+
`Admin list-results renderer ${results.collection}`
|
|
652
349
|
);
|
|
653
|
-
|
|
350
|
+
listResultsRenderers.push(results);
|
|
654
351
|
}
|
|
655
352
|
for (const action of plugin.documentActions ?? []) {
|
|
656
353
|
claim(
|
|
@@ -679,16 +376,17 @@ export function resolveAdminExtensions(
|
|
|
679
376
|
application?.messages === undefined ? undefined : freezeAdminMessages(application.messages),
|
|
680
377
|
rowLabels: Object.freeze(rowLabels),
|
|
681
378
|
routes: Object.freeze(routes),
|
|
682
|
-
|
|
379
|
+
dashboardPanels: Object.freeze(dashboardPanels),
|
|
683
380
|
login: Object.freeze(login),
|
|
684
381
|
account: Object.freeze(account),
|
|
685
382
|
navigation: Object.freeze(navigation),
|
|
686
383
|
logoutButton,
|
|
687
|
-
|
|
384
|
+
coreViews: Object.freeze(coreViews),
|
|
688
385
|
branding: Object.freeze(branding),
|
|
689
|
-
|
|
386
|
+
shellSlots: Object.freeze(shellSlots),
|
|
690
387
|
providers: Object.freeze(providers),
|
|
691
|
-
|
|
388
|
+
listCellRenderers: Object.freeze(listCellRenderers),
|
|
389
|
+
listResultsRenderers: Object.freeze(listResultsRenderers),
|
|
692
390
|
documentActions: Object.freeze(documentActions),
|
|
693
391
|
documentViews: Object.freeze(documentViews),
|
|
694
392
|
messages: Object.freeze(messages),
|
|
@@ -728,7 +426,7 @@ function assertSameValues(label: string, backend: readonly string[], admin: read
|
|
|
728
426
|
}
|
|
729
427
|
}
|
|
730
428
|
|
|
731
|
-
//
|
|
429
|
+
// Admin routes are literal paths: no router
|
|
732
430
|
// patterns, query strings, normalization aliases or framework namespace shadowing.
|
|
733
431
|
const reservedRouteRoots = new Set([
|
|
734
432
|
"account",
|
|
@@ -741,31 +439,29 @@ const reservedRouteRoots = new Set([
|
|
|
741
439
|
"request-verification",
|
|
742
440
|
"verify-email",
|
|
743
441
|
]);
|
|
744
|
-
function validateContributions(
|
|
745
|
-
source: AdminContributions & { messages?: PluginMessageCatalog },
|
|
746
|
-
local: boolean
|
|
747
|
-
) {
|
|
442
|
+
function validateContributions(source: AdminContributions & { messages?: PluginMessageCatalog }) {
|
|
748
443
|
if (source.messages !== undefined) validateAdminMessages("application", source.messages);
|
|
749
444
|
const groups = [
|
|
750
445
|
"routes",
|
|
751
|
-
"
|
|
446
|
+
"dashboardPanels",
|
|
752
447
|
"login",
|
|
753
448
|
"account",
|
|
754
449
|
"navigation",
|
|
755
|
-
"
|
|
450
|
+
"coreViews",
|
|
756
451
|
"branding",
|
|
757
|
-
"
|
|
452
|
+
"shellSlots",
|
|
758
453
|
"providers",
|
|
759
|
-
"
|
|
454
|
+
"listCellRenderers",
|
|
455
|
+
"listResultsRenderers",
|
|
760
456
|
"documentActions",
|
|
761
457
|
"documentViews",
|
|
762
458
|
] as const;
|
|
763
459
|
const positions = {
|
|
764
|
-
|
|
460
|
+
dashboardPanels: ["before", "after", "replace"],
|
|
765
461
|
login: ["before", "after", "replace"],
|
|
766
462
|
account: ["before", "after", "replace"],
|
|
767
463
|
navigation: ["before", "beforeLinks", "afterLinks", "after", "replace"],
|
|
768
|
-
|
|
464
|
+
shellSlots: ["header", "actions", "settingsMenu"],
|
|
769
465
|
} as const;
|
|
770
466
|
for (const group of groups) {
|
|
771
467
|
const entries = source[group];
|
|
@@ -785,12 +481,12 @@ function validateContributions(
|
|
|
785
481
|
const allowed: readonly string[] = positions[group as keyof typeof positions];
|
|
786
482
|
const position = "position" in entry ? entry.position : undefined;
|
|
787
483
|
if (
|
|
788
|
-
(position === undefined && (group === "navigation" || group === "
|
|
484
|
+
(position === undefined && (group === "navigation" || group === "shellSlots")) ||
|
|
789
485
|
(position !== undefined && !allowed.includes(position))
|
|
790
486
|
)
|
|
791
487
|
throw new Error(`Admin ${group} registration has an invalid position.`);
|
|
792
488
|
}
|
|
793
|
-
if (group === "account" || group === "branding" || group === "
|
|
489
|
+
if (group === "account" || group === "branding" || group === "coreViews") {
|
|
794
490
|
const allowed =
|
|
795
491
|
group === "account"
|
|
796
492
|
? ["profile", "security"]
|
|
@@ -802,7 +498,7 @@ function validateContributions(
|
|
|
802
498
|
}
|
|
803
499
|
}
|
|
804
500
|
}
|
|
805
|
-
for (const route of
|
|
501
|
+
for (const route of source.routes ?? []) {
|
|
806
502
|
if (
|
|
807
503
|
typeof route.path !== "string" ||
|
|
808
504
|
!/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(route.path) ||
|