@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/README.md
CHANGED
|
@@ -7,14 +7,22 @@ the browser, without a JavaScript server in production.
|
|
|
7
7
|
|
|
8
8
|
## Choose the API for your job
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
Collection cards can use `listResultsRenderers` without replacing Ridu's list controller. Custom dashboards
|
|
11
|
+
can use `withAdminLoader` with a generated Go loader reference and `AdminLoaderProps<Data>`.
|
|
12
|
+
The same helper pairs loaders with literal custom routes and collection/global/not-found replacement
|
|
13
|
+
views. Results renderers own their links through `list.documentHref(document)` and `Link` from
|
|
14
|
+
`@riducms/admin/routing`.
|
|
15
|
+
See [custom admin views and data](https://riducms.com/docs/custom-components/custom-views/) for the complete
|
|
16
|
+
Go-to-Svelte contract and its deliberate limits.
|
|
17
|
+
|
|
18
|
+
| Job | Import | Register/select it |
|
|
19
|
+
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| Add a field type with its own Go rules and structured value | `defineAdminPlugin`, `definePluginField` from `@riducms/plugin/authoring/v1` | Plugin `fields` map; matching field-type key declared in Go |
|
|
21
|
+
| Supply an alternative advanced field editor in a paired plugin | `defineFieldComponent` from `@riducms/plugin/authoring/v1` | Plugin `fieldEditors` map; Go `.Admin(field.Admin{Editor: field.PluginComponent(owner, name, config)})` |
|
|
22
|
+
| Change how one application's text/number/checkbox-style field looks | `defineFieldEditor` from `@riducms/plugin/editor` | `defineAdmin({ fieldEditors })`; Go `.Admin(field.Admin{Editor: field.Component("app:name", config)})` |
|
|
23
|
+
| Add application routes, dashboard panels or other admin UI | `defineAdmin` from `@riducms/plugin/admin` | `admin/src/admin.config.ts` |
|
|
24
|
+
| Customize an application's array/block row headings | `defineRowLabel` from `@riducms/plugin/admin` | `defineAdmin({ rowLabels })`; Go `.Admin(field.Admin{RowLabel: field.Component("app:name", config)})` |
|
|
25
|
+
| Use the shared visual controls | `@riducms/ui` | Compose controls in your component |
|
|
18
26
|
|
|
19
27
|
Public component/host types are exported from `@riducms/plugin`. The versioned authoring import
|
|
20
28
|
also exports the plugin field types. Do not import private `admin/src` implementations.
|
|
@@ -54,27 +62,39 @@ unknown data, returns the shape your component expects, or throws a useful error
|
|
|
54
62
|
|
|
55
63
|
```ts
|
|
56
64
|
export interface NoteValue {
|
|
57
|
-
|
|
65
|
+
text: string;
|
|
58
66
|
}
|
|
59
67
|
export interface NoteConfig {
|
|
60
|
-
|
|
68
|
+
copyTo: string;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
export function decodeNote(raw: unknown): NoteValue {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
if (
|
|
73
|
+
typeof raw !== "object" ||
|
|
74
|
+
raw === null ||
|
|
75
|
+
Array.isArray(raw) ||
|
|
76
|
+
!("text" in raw) ||
|
|
77
|
+
typeof raw.text !== "string" ||
|
|
78
|
+
Object.keys(raw).length !== 1
|
|
79
|
+
) {
|
|
80
|
+
throw new Error("A note must be an object containing only a text string.");
|
|
81
|
+
}
|
|
82
|
+
return { text: raw.text };
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
export function decodeNoteConfig(raw: unknown): NoteConfig {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
if (
|
|
87
|
+
typeof raw !== "object" ||
|
|
88
|
+
raw === null ||
|
|
89
|
+
Array.isArray(raw) ||
|
|
90
|
+
!("copyTo" in raw) ||
|
|
91
|
+
typeof raw.copyTo !== "string" ||
|
|
92
|
+
!raw.copyTo.trim() ||
|
|
93
|
+
Object.keys(raw).length !== 1
|
|
94
|
+
) {
|
|
95
|
+
throw new Error("Note settings must contain a nonempty copyTo field path.");
|
|
96
|
+
}
|
|
97
|
+
return { copyTo: raw.copyTo };
|
|
78
98
|
}
|
|
79
99
|
```
|
|
80
100
|
|
|
@@ -82,35 +102,34 @@ Then write `note-field.svelte`:
|
|
|
82
102
|
|
|
83
103
|
```svelte
|
|
84
104
|
<script lang="ts">
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
import type { PluginFieldProps } from "@riducms/plugin/authoring/v1";
|
|
106
|
+
import type { NoteValue, NoteConfig } from "./value";
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
let { field, config, form }: PluginFieldProps<NoteValue, NoteConfig> = $props();
|
|
109
|
+
// Remember this document's Title field while this note editor is open.
|
|
110
|
+
// svelte-ignore state_referenced_locally
|
|
111
|
+
const title = form.bind(config.copyTo);
|
|
92
112
|
</script>
|
|
93
113
|
|
|
94
114
|
<label for={field.schema.id}>{field.schema.admin.label}</label>
|
|
95
115
|
<textarea
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
></textarea>
|
|
116
|
+
id={field.schema.id}
|
|
117
|
+
name={field.schema.path}
|
|
118
|
+
required={field.schema.required}
|
|
119
|
+
readonly={field.readOnly}
|
|
120
|
+
aria-invalid={field.issues.length > 0}
|
|
121
|
+
aria-describedby={`${field.schema.id}-issues`}
|
|
122
|
+
value={field.value?.text ?? ""}
|
|
123
|
+
oninput={(event) => field.set({ text: event.currentTarget.value })}></textarea>
|
|
105
124
|
<div id={`${field.schema.id}-issues`} aria-live="polite">
|
|
106
|
-
|
|
125
|
+
{#each field.issues as issue}<p>{issue.message}</p>{/each}
|
|
107
126
|
</div>
|
|
108
127
|
<button
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
type="button"
|
|
129
|
+
disabled={field.readOnly || title.readOnly || !field.value?.text}
|
|
130
|
+
onclick={() => title.set(field.value?.text ?? null)}
|
|
112
131
|
>
|
|
113
|
-
|
|
132
|
+
Use note as title
|
|
114
133
|
</button>
|
|
115
134
|
```
|
|
116
135
|
|
|
@@ -122,15 +141,15 @@ import NoteField from "./note-field.svelte";
|
|
|
122
141
|
import { decodeNote, decodeNoteConfig } from "./value";
|
|
123
142
|
|
|
124
143
|
export const editorialAdminPlugin = defineAdminPlugin({
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
key: "editorial-tools",
|
|
145
|
+
pairingVersion: 1,
|
|
146
|
+
fields: {
|
|
147
|
+
"review-note": definePluginField({
|
|
148
|
+
component: NoteField,
|
|
149
|
+
decodeValue: decodeNote,
|
|
150
|
+
decodeConfig: decodeNoteConfig,
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
134
153
|
});
|
|
135
154
|
```
|
|
136
155
|
|
|
@@ -185,14 +204,14 @@ expects. This is different from your own `field`, which has your registered deco
|
|
|
185
204
|
|
|
186
205
|
## Values, config and validation
|
|
187
206
|
|
|
188
|
-
| API
|
|
189
|
-
|
|
|
190
|
-
| `field.value`
|
|
191
|
-
| `field.rawValue`
|
|
207
|
+
| API | Meaning |
|
|
208
|
+
| ----------------- | ------------------------------------------------------------------------------------------ |
|
|
209
|
+
| `field.value` | Latest checked form value, including unsaved edits. Objects/arrays are copies. |
|
|
210
|
+
| `field.rawValue` | Copied data before decoding, useful for recovery UI when old data is unsupported. |
|
|
192
211
|
| `field.set(next)` | Replace this value in the normal unsaved form. Use `null` to clear; undefined is rejected. |
|
|
193
|
-
| `field.issues`
|
|
194
|
-
| `form.get(path)`
|
|
195
|
-
| `form.snapshot()` | Copy current form values once. It is neither live state nor a server fetch.
|
|
212
|
+
| `field.issues` | Current form/server validation issues for the field and its children. |
|
|
213
|
+
| `form.get(path)` | Copy a value at its current document-root path; does not remember a row's identity. |
|
|
214
|
+
| `form.snapshot()` | Copy current form values once. It is neither live state nor a server fetch. |
|
|
196
215
|
|
|
197
216
|
Changing an object returned by a read does not edit the form. Call `set` with a replacement value.
|
|
198
217
|
Ridu copies values supplied to `set` too, so mutating your original object afterward does not
|
|
@@ -226,11 +245,17 @@ final checks on saving; UI customization does not change storage, filtering or o
|
|
|
226
245
|
- `collections`: available collection definitions, not the documents themselves.
|
|
227
246
|
- `locale`: content locale, separate from the admin interface language.
|
|
228
247
|
- `documentRevision`: an admin change counter for refreshing UI, not a saved `_revision` number.
|
|
248
|
+
- `canCreateDocument(collection)`: a live presentation hint for showing collection create controls.
|
|
249
|
+
The reference browser and operation engine still authorize the actual create operation.
|
|
229
250
|
- `findDocument(collection, id, signal?)`: fetch a saved document in the current locale. It does
|
|
230
251
|
not read the current form's unsaved changes.
|
|
231
252
|
- `referenceBrowser`: the related-document picker/editor. Render it and update your field in
|
|
232
|
-
`onCommit(ids)`.
|
|
233
|
-
|
|
253
|
+
`onCommit(ids, collectionSlug)`. Supply `collections` to offer a collection selector; switching
|
|
254
|
+
clears the list query and uncommitted selection. The admin offers only collections the current
|
|
255
|
+
session can read and does not browse or commit when none are available. A static `optionFilter`
|
|
256
|
+
applies to every offered collection; pass a collection-aware resolver when their filter fields
|
|
257
|
+
differ. Return false to keep it open; otherwise it closes after acceptance. Saving a related
|
|
258
|
+
document inside the picker is a separate server operation.
|
|
234
259
|
- `requestPlugin<Result>(path, body, signal?)`: POST to this renderer's paired Go plugin endpoint,
|
|
235
260
|
using a relative path such as `generate-title`. The response generic does not validate data;
|
|
236
261
|
request `unknown` and decode it when needed. This does not update your form automatically.
|
|
@@ -250,7 +275,7 @@ To edit an existing item's fields directly in the parent form, render:
|
|
|
250
275
|
|
|
251
276
|
```svelte
|
|
252
277
|
{#if selectedIdentity !== undefined && authoring.schemaForm !== undefined}
|
|
253
|
-
|
|
278
|
+
{@render authoring.schemaForm({ treeKey: "widgets", identity: selectedIdentity })}
|
|
254
279
|
{/if}
|
|
255
280
|
```
|
|
256
281
|
|
|
@@ -264,11 +289,11 @@ name input and its feedback:
|
|
|
264
289
|
|
|
265
290
|
```svelte
|
|
266
291
|
{#if authoring.schemaHeader !== undefined}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
292
|
+
{@render authoring.schemaHeader({
|
|
293
|
+
treeKey: "widgets",
|
|
294
|
+
identity: selectedIdentity,
|
|
295
|
+
onChange: ({ field, value }) => updateWidgetField(selectedIdentity, field, value),
|
|
296
|
+
})}
|
|
272
297
|
{/if}
|
|
273
298
|
```
|
|
274
299
|
|
|
@@ -305,15 +330,20 @@ embedded-form example and the [rich-text package](../plugin-richtext/) for an ed
|
|
|
305
330
|
|
|
306
331
|
## Other admin UI
|
|
307
332
|
|
|
308
|
-
Both paired plugins and `defineAdmin` can register `routes`, `
|
|
309
|
-
`navigation`, `logoutButton`, `
|
|
310
|
-
`documentActions` and `documentViews`.
|
|
333
|
+
Both paired plugins and `defineAdmin` can register `routes`, `dashboardPanels`, `login`, `account`,
|
|
334
|
+
`navigation`, `logoutButton`, `coreViews`, `branding`, `shellSlots`, `providers`,
|
|
335
|
+
`listCellRenderers`, `listResultsRenderers`, `documentActions` and `documentViews`.
|
|
311
336
|
|
|
312
337
|
Replacement login/account/navigation/core-view components receive a `defaultView` snippet.
|
|
313
338
|
Render `{@render defaultView()}` to keep the normal screen inside your wrapper. Providers must
|
|
314
339
|
render their `defaultView` to include the nested admin. Host methods perform login/logout,
|
|
315
340
|
refreshes and notifications; application-specific operations use the generated SDK.
|
|
316
341
|
|
|
342
|
+
`logoutButton` replaces the sign-out control in the navigation footer and receives `host.logout()`.
|
|
343
|
+
The Payload-style shell places this control there rather than in the account menu. A full navigation
|
|
344
|
+
replacement should render its `defaultView` when it wants to retain the framework navigation and
|
|
345
|
+
logout control.
|
|
346
|
+
|
|
317
347
|
Collection/global view replacements may target one resource or act as a fallback. An exact
|
|
318
348
|
resource match wins over its fallback; duplicate targets and exclusive replacements fail.
|
|
319
349
|
Table cells and document action/view props contain document data, not a field-form binding.
|
|
@@ -367,6 +397,9 @@ interactions as well.
|
|
|
367
397
|
For custom UI, use the documented editor contracts and run the checks above in the consuming
|
|
368
398
|
application so its Svelte and TypeScript configuration checks the complete integration.
|
|
369
399
|
|
|
400
|
+
The [custom components guide](https://riducms.com/docs/custom-components/) explains when a
|
|
401
|
+
component is a routed view, form-bound editor, read-only renderer, shell slot, or reusable primitive.
|
|
402
|
+
|
|
370
403
|
- [Plugin fields and editors](https://riducms.com/docs/fields/plugin/)
|
|
371
404
|
- [Building plugins](https://riducms.com/docs/plugins/)
|
|
372
405
|
- [Application-local field components](https://riducms.com/docs/custom-components/field-components/)
|
package/package.json
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
"url": "https://github.com/riducms/ridu/issues"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@riducms/protocol": "0.
|
|
7
|
-
"@riducms/
|
|
8
|
-
"@riducms/
|
|
6
|
+
"@riducms/protocol": "0.4.0",
|
|
7
|
+
"@riducms/sdk": "0.4.0",
|
|
8
|
+
"@riducms/translations": "0.4.0",
|
|
9
|
+
"@riducms/ui": "0.4.0"
|
|
9
10
|
},
|
|
10
11
|
"description": "Public TypeScript contracts for statically registered Ridu admin plugins.",
|
|
11
12
|
"exports": {
|
|
@@ -39,5 +40,5 @@
|
|
|
39
40
|
"test": "bun test"
|
|
40
41
|
},
|
|
41
42
|
"type": "module",
|
|
42
|
-
"version": "0.
|
|
43
|
+
"version": "0.4.0"
|
|
43
44
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OperationCapabilities, SchemaCollection } from "@riducms/protocol";
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
|
|
4
|
+
import type { FieldDocument } from "../authoring";
|
|
5
|
+
import type { AdminI18n, ExtensionTranslationKey } from "../i18n";
|
|
6
|
+
import type { AdminExtensionNotificationTone } from "./shared";
|
|
7
|
+
|
|
8
|
+
/** Tools for a document action or extra document view after it completes its own operation. */
|
|
9
|
+
export interface AdminDocumentExtensionHost {
|
|
10
|
+
/** Reload the current document from the server. This is not a save of unsaved form edits. */
|
|
11
|
+
refresh: () => Promise<void>;
|
|
12
|
+
/** Show a temporary success or error message. */
|
|
13
|
+
notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Saved document data for actions/views; use the generated SDK for application-specific operations. */
|
|
17
|
+
export interface AdminDocumentExtensionProps {
|
|
18
|
+
collection: SchemaCollection;
|
|
19
|
+
document: FieldDocument;
|
|
20
|
+
host: AdminDocumentExtensionHost;
|
|
21
|
+
i18n: AdminI18n;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Add a component alongside the document's standard actions. Implement the operation in that component. */
|
|
25
|
+
export interface AdminDocumentAction {
|
|
26
|
+
key: string;
|
|
27
|
+
/** Omit to show the action for every non-global collection. */
|
|
28
|
+
collection?: string;
|
|
29
|
+
/** Hide unless this document operation is allowed. Visibility does not replace server authorization. */
|
|
30
|
+
requires?: keyof OperationCapabilities;
|
|
31
|
+
component: Component<AdminDocumentExtensionProps>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Add a tab beside Edit and API. Keys `edit` and `api` are reserved for Ridu. */
|
|
35
|
+
export interface AdminDocumentView {
|
|
36
|
+
key: string;
|
|
37
|
+
label: string;
|
|
38
|
+
labelKey?: ExtensionTranslationKey;
|
|
39
|
+
/** Collection or global slug; omit to show for every collection and global. */
|
|
40
|
+
collection?: string;
|
|
41
|
+
component: Component<AdminDocumentExtensionProps>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { PluginMessageCatalog } from "../i18n";
|
|
2
|
+
import type { RowLabelPlugin } from "../row-label";
|
|
3
|
+
import type { AdminDocumentAction, AdminDocumentView } from "./documents";
|
|
4
|
+
import type { AdminListCellRenderer, AdminListResultsRenderer } from "./lists";
|
|
5
|
+
import type {
|
|
6
|
+
AdminBrandComponent,
|
|
7
|
+
AdminLogoutButton,
|
|
8
|
+
AdminNavigationComponent,
|
|
9
|
+
AdminProvider,
|
|
10
|
+
AdminShellSlot,
|
|
11
|
+
} from "./shell";
|
|
12
|
+
import type {
|
|
13
|
+
AdminAccountComponent,
|
|
14
|
+
AdminCoreView,
|
|
15
|
+
AdminDashboardPanel,
|
|
16
|
+
AdminLoginComponent,
|
|
17
|
+
AdminRoute,
|
|
18
|
+
} from "./views";
|
|
19
|
+
|
|
20
|
+
export * from "./documents";
|
|
21
|
+
export * from "./lists";
|
|
22
|
+
export * from "./shared";
|
|
23
|
+
export * from "./shell";
|
|
24
|
+
export * from "./views";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Places where a plugin or application can add admin UI. Arrays keep registration
|
|
28
|
+
* order; application entries follow plugin entries. Replacement slots allow one
|
|
29
|
+
* matching replacement. Conflicts are reported instead of silently overriding UI.
|
|
30
|
+
*/
|
|
31
|
+
export interface AdminContributions {
|
|
32
|
+
/** Authenticated admin routes contributed by this package. */
|
|
33
|
+
routes?: readonly AdminRoute[];
|
|
34
|
+
/** Panels composed into or replacing the authenticated dashboard. */
|
|
35
|
+
dashboardPanels?: readonly AdminDashboardPanel[];
|
|
36
|
+
/** Components composed around or replacing the sign-in screen. */
|
|
37
|
+
login?: readonly AdminLoginComponent[];
|
|
38
|
+
/** Components composed around or replacing the framework account screens. */
|
|
39
|
+
account?: readonly AdminAccountComponent[];
|
|
40
|
+
/** Components composed around or replacing the framework navigation. */
|
|
41
|
+
navigation?: readonly AdminNavigationComponent[];
|
|
42
|
+
/** Replaces the default sign-out control in the navigation footer. */
|
|
43
|
+
logoutButton?: AdminLogoutButton;
|
|
44
|
+
/** Wraps or replaces framework collection, global, and not-found views. */
|
|
45
|
+
coreViews?: readonly AdminCoreView[];
|
|
46
|
+
/** Replaces framework-owned brand graphics at exact shell surfaces. */
|
|
47
|
+
branding?: readonly AdminBrandComponent[];
|
|
48
|
+
/** Adds components to explicit global shell slots. */
|
|
49
|
+
shellSlots?: readonly AdminShellSlot[];
|
|
50
|
+
/** Wraps the admin in statically ordered Svelte context providers. */
|
|
51
|
+
providers?: readonly AdminProvider[];
|
|
52
|
+
/** Collection list cells rendered for exact collection/field pairs. */
|
|
53
|
+
listCellRenderers?: readonly AdminListCellRenderer[];
|
|
54
|
+
/** Replace only the results layout; Ridu retains list loading, controls and pagination. */
|
|
55
|
+
listResultsRenderers?: readonly AdminListResultsRenderer[];
|
|
56
|
+
/** Actions rendered alongside framework-owned document actions. */
|
|
57
|
+
documentActions?: readonly AdminDocumentAction[];
|
|
58
|
+
/** Read-only or operational views rendered beside Edit and API. */
|
|
59
|
+
documentViews?: readonly AdminDocumentView[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Checked registrations collected for Ridu's admin runtime. Normally consumed by generated code. */
|
|
63
|
+
export interface ResolvedAdminExtensions {
|
|
64
|
+
rowLabels: readonly RowLabelPlugin[];
|
|
65
|
+
routes: readonly AdminRoute[];
|
|
66
|
+
dashboardPanels: readonly AdminDashboardPanel[];
|
|
67
|
+
login: readonly AdminLoginComponent[];
|
|
68
|
+
account: readonly AdminAccountComponent[];
|
|
69
|
+
navigation: readonly AdminNavigationComponent[];
|
|
70
|
+
logoutButton: AdminLogoutButton | undefined;
|
|
71
|
+
coreViews: readonly AdminCoreView[];
|
|
72
|
+
branding: readonly AdminBrandComponent[];
|
|
73
|
+
shellSlots: readonly AdminShellSlot[];
|
|
74
|
+
providers: readonly AdminProvider[];
|
|
75
|
+
listCellRenderers: readonly AdminListCellRenderer[];
|
|
76
|
+
listResultsRenderers: readonly AdminListResultsRenderer[];
|
|
77
|
+
documentActions: readonly AdminDocumentAction[];
|
|
78
|
+
documentViews: readonly AdminDocumentView[];
|
|
79
|
+
messages: Readonly<Record<string, PluginMessageCatalog>>;
|
|
80
|
+
applicationMessages: PluginMessageCatalog | undefined;
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SchemaCollection, SchemaField } from "@riducms/protocol";
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
|
|
4
|
+
import type { FieldDocument } from "../authoring";
|
|
5
|
+
import type { AdminI18n, ExtensionTranslationKey } from "../i18n";
|
|
6
|
+
|
|
7
|
+
/** Live, read-only projections and selection commands from Ridu's list controller. */
|
|
8
|
+
export interface AdminCollectionList {
|
|
9
|
+
readonly documents: readonly FieldDocument[];
|
|
10
|
+
readonly selectedIDs: ReadonlySet<string>;
|
|
11
|
+
readonly selectionEnabled: boolean;
|
|
12
|
+
readonly allOnPageSelected: boolean;
|
|
13
|
+
readonly pageSelectionIndeterminate: boolean;
|
|
14
|
+
toggleDocument(id: string, selected: boolean): void;
|
|
15
|
+
togglePage(selected: boolean): void;
|
|
16
|
+
title(document: FieldDocument): string;
|
|
17
|
+
/** Router-root destination with an encoded ID and the current content locale. */
|
|
18
|
+
documentHref(document: FieldDocument): string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AdminListResultsRendererProps {
|
|
22
|
+
collection: SchemaCollection;
|
|
23
|
+
list: AdminCollectionList;
|
|
24
|
+
i18n: AdminI18n;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Collection-specific results layout. Trash retains its framework restore/delete controls. */
|
|
28
|
+
export interface AdminListResultsRenderer {
|
|
29
|
+
key: string;
|
|
30
|
+
collection: string;
|
|
31
|
+
component: Component<AdminListResultsRendererProps>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Data for displaying a collection table cell. This is not an editable document-form binding. */
|
|
35
|
+
export interface AdminListCellRendererProps {
|
|
36
|
+
collection: SchemaCollection;
|
|
37
|
+
field: SchemaField;
|
|
38
|
+
document: FieldDocument;
|
|
39
|
+
/** This field's saved value. Check its type before rendering; changing it does not save a document. */
|
|
40
|
+
value: unknown;
|
|
41
|
+
i18n: AdminI18n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Replace the cell display for one collection/field pair; duplicate targets are rejected. */
|
|
45
|
+
export interface AdminListCellRenderer {
|
|
46
|
+
key: string;
|
|
47
|
+
/** Collection slug, for example `"posts"`. */
|
|
48
|
+
collection: string;
|
|
49
|
+
/** Top-level field name/path in that collection, for example `"title"`. */
|
|
50
|
+
field: string;
|
|
51
|
+
/** Fallback column heading. */
|
|
52
|
+
label: string;
|
|
53
|
+
/** Optional translation key from this plugin's messages, or app:... for application entries. */
|
|
54
|
+
labelKey?: ExtensionTranslationKey;
|
|
55
|
+
component: Component<AdminListCellRendererProps>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SchemaManifest } from "@riducms/protocol";
|
|
2
|
+
import type { AdminLoader } from "@riducms/sdk";
|
|
3
|
+
import type { Component } from "svelte";
|
|
4
|
+
|
|
5
|
+
import type { FieldDocument } from "../authoring";
|
|
6
|
+
import type { AdminI18n } from "../i18n";
|
|
7
|
+
|
|
8
|
+
/** Common data supplied to admin extension components. Use the generated SDK for requests. */
|
|
9
|
+
export interface AdminExtensionProps {
|
|
10
|
+
/** Resolved Go schema available to this admin session; it may omit inaccessible resources. */
|
|
11
|
+
manifest: SchemaManifest;
|
|
12
|
+
/** Signed-in user document, absent on screens where no user is signed in yet. */
|
|
13
|
+
user?: FieldDocument;
|
|
14
|
+
/** Current admin interface translations and formatting preferences. */
|
|
15
|
+
i18n: AdminI18n;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type AdminExtensionNotificationTone = "success" | "error";
|
|
19
|
+
|
|
20
|
+
/** Prepared application data and the mounted view's explicit refresh operation. */
|
|
21
|
+
export interface AdminLoaderProps<Data> {
|
|
22
|
+
data: Data;
|
|
23
|
+
refresh: () => Promise<void>;
|
|
24
|
+
refreshing: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AdminViewComponent<Props extends object> =
|
|
28
|
+
| { component: Component<Props> | Component; loader?: never }
|
|
29
|
+
| {
|
|
30
|
+
component: Component<Props & AdminLoaderProps<unknown>>;
|
|
31
|
+
loader: AdminLoader<never, unknown>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Spread this pairing into a dashboard panel, custom route or core-view registration. */
|
|
35
|
+
export function withAdminLoader<Input, Data, Props extends AdminLoaderProps<NoInfer<Data>>>(
|
|
36
|
+
loader: AdminLoader<Input, Data>,
|
|
37
|
+
component: Component<Props>
|
|
38
|
+
): {
|
|
39
|
+
loader: AdminLoader<never, unknown>;
|
|
40
|
+
component: Component<Omit<Props, keyof AdminLoaderProps<unknown>> & AdminLoaderProps<unknown>>;
|
|
41
|
+
} {
|
|
42
|
+
// Erase the paired generic only after checking both sides. The host decodes
|
|
43
|
+
// with this same loader before handing its output to this same component.
|
|
44
|
+
return { loader, component } as unknown as {
|
|
45
|
+
loader: AdminLoader<never, unknown>;
|
|
46
|
+
component: Component<Omit<Props, keyof AdminLoaderProps<unknown>> & AdminLoaderProps<unknown>>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
|
+
|
|
3
|
+
import type { AdminI18n } from "../i18n";
|
|
4
|
+
import type { AdminExtensionProps } from "./shared";
|
|
5
|
+
|
|
6
|
+
/** Choose the whole navigation's boundary, the resource-link list's boundary, or replace navigation. */
|
|
7
|
+
export type AdminNavigationPosition = "before" | "beforeLinks" | "afterLinks" | "after" | "replace";
|
|
8
|
+
|
|
9
|
+
export interface AdminNavigationComponentProps extends AdminExtensionProps {
|
|
10
|
+
/** Render with `{@render defaultView()}` to include Ridu's navigation in a replacement. */
|
|
11
|
+
defaultView: Snippet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Insert a component at a navigation position. At most one entry can replace navigation. */
|
|
15
|
+
export interface AdminNavigationComponent {
|
|
16
|
+
key: string;
|
|
17
|
+
component: Component<AdminNavigationComponentProps>;
|
|
18
|
+
position: AdminNavigationPosition;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AdminLogoutExtensionHost {
|
|
22
|
+
/** End the current session and return to sign-in. Await this instead of only changing local UI. */
|
|
23
|
+
logout: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AdminLogoutButtonProps extends AdminExtensionProps {
|
|
27
|
+
host: AdminLogoutExtensionHost;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Replace the navigation footer's sign-out button; call the supplied `host.logout()` on activation. */
|
|
31
|
+
export interface AdminLogoutButton {
|
|
32
|
+
key: string;
|
|
33
|
+
component: Component<AdminLogoutButtonProps>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type AdminBrandSurface = "loginLogo" | "navigationLogo" | "accountAvatar";
|
|
37
|
+
|
|
38
|
+
export interface AdminBrandComponentProps extends AdminExtensionProps {
|
|
39
|
+
surface: AdminBrandSurface;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Replace one logo/avatar location. Only one component may claim each surface. */
|
|
43
|
+
export interface AdminBrandComponent {
|
|
44
|
+
key: string;
|
|
45
|
+
surface: AdminBrandSurface;
|
|
46
|
+
component: Component<AdminBrandComponentProps>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type AdminShellSlotPosition = "header" | "actions" | "settingsMenu";
|
|
50
|
+
|
|
51
|
+
export interface AdminShellSlotProps extends AdminExtensionProps {
|
|
52
|
+
position: AdminShellSlotPosition;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Add UI to the global header, actions area or account settings menu in registration order. */
|
|
56
|
+
export interface AdminShellSlot {
|
|
57
|
+
key: string;
|
|
58
|
+
position: AdminShellSlotPosition;
|
|
59
|
+
component: Component<AdminShellSlotProps>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AdminProviderProps {
|
|
63
|
+
/** Render `{@render defaultView()}` after setting context so the nested providers/admin appear. */
|
|
64
|
+
defaultView: Snippet;
|
|
65
|
+
i18n: AdminI18n;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Wrap the admin in a Svelte context provider. Render the supplied `defaultView` to continue the UI. */
|
|
69
|
+
export interface AdminProvider {
|
|
70
|
+
key: string;
|
|
71
|
+
component: Component<AdminProviderProps>;
|
|
72
|
+
}
|