mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
Feat/plugin instance actions (#2209)
* feat(shared): scope plugin action descriptors * feat(plugin-sdk): instance-scoped settings actions * feat(plugins): run instance-scoped actions from the admin settings dialog * feat(admin): instance-scoped plugin actions in the settings dialog * fix(plugins): surface instance actions without instance fields and harden the action gate * docs(plugin-sdk): note that hosts before 4.2.0 ignore action scope * fix(admin): open the dialog for instance actions, explain inactive 404s, guard the save window * docs(wiki): plugin pages for instance actions and the admin panel changes
This commit is contained in:
@@ -813,13 +813,15 @@ export interface PluginDefinition {
|
||||
exportUserData?(input: { userId: number }, ctx: PluginContext): Promise<unknown> | unknown;
|
||||
events?: PluginEventSubscription[];
|
||||
/**
|
||||
* Buttons on the plugin's own settings page ("Test connection", "Sync now"). The key
|
||||
* must match an entry in the manifest's `actions`.
|
||||
* Buttons on the plugin's settings forms ("Test connection", "Sync now", "Purge
|
||||
* cache"). The key must match an entry in the manifest's `actions`; that entry's
|
||||
* `scope` decides WHERE the button renders — `'user'` (default) on the user Settings
|
||||
* tab, `'instance'` in the admin instance-settings dialog.
|
||||
*
|
||||
* USER-INITIATED, so unlike the notificationChannel hook there IS an acting user — the
|
||||
* person who clicked. `ctx.settings.get()` returns THEIR value and trip reads are
|
||||
* membership-checked against them, which is what makes a "test my credentials" button
|
||||
* possible at all.
|
||||
* USER-INITIATED either way, so unlike the notificationChannel hook there IS an acting
|
||||
* user — the person who clicked (a user, or an admin for an instance action).
|
||||
* `ctx.settings.get()` returns THEIR value, `ctx.config` is the instance config, and
|
||||
* trip reads are membership-checked against them.
|
||||
*/
|
||||
actions?: Record<string, (ctx: PluginContext) => Promise<PluginActionResult | void> | PluginActionResult | void>;
|
||||
hooks?: {
|
||||
|
||||
@@ -60,6 +60,10 @@ export interface ManifestAction {
|
||||
label?: string;
|
||||
hint?: string;
|
||||
danger?: boolean;
|
||||
/** Which settings form renders the button: `'user'` (default) on the user Settings
|
||||
* tab, run as the clicking user; `'instance'` in the admin instance-settings dialog,
|
||||
* run as the clicking admin. Unlike settings fields, the default is `'user'`. */
|
||||
scope?: 'user' | 'instance';
|
||||
}
|
||||
export interface ManifestCapabilities {
|
||||
settingsUi?: boolean;
|
||||
@@ -446,6 +450,9 @@ export function validateManifest(raw: unknown): ValidationResult {
|
||||
if (seen.has(key)) errors.push(`duplicate action "${key}"`);
|
||||
seen.add(key);
|
||||
if (a.label !== undefined && typeof a.label !== 'string') errors.push(`action "${key}" label must be a string`);
|
||||
if (a.scope !== undefined && a.scope !== 'user' && a.scope !== 'instance') {
|
||||
errors.push(`action "${key}".scope must be "user" or "instance"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +72,12 @@ export interface MockHostOptions {
|
||||
* refuses it (and the SDK client swallows the rejection, so subscribers silently
|
||||
* never see it). Unset = record every emit, as before. */
|
||||
declaredEmits?: string[];
|
||||
/** Action keys this plugin declares in its manifest `actions`. When set, driving an
|
||||
/** Action keys this plugin declares in its manifest `actions`, as plain keys or
|
||||
* `{ key, scope }` entries (the scope is accepted for fixture fidelity; the mock runs
|
||||
* both scopes with the acting-user ctx, as the host does). When set, driving an
|
||||
* undeclared key throws — production refuses it before the child is ever woken.
|
||||
* Unset = any key the plugin implements can be driven. */
|
||||
declaredActions?: string[];
|
||||
declaredActions?: Array<string | { key: string; scope?: 'user' | 'instance' }>;
|
||||
/** Hosts an ADMIN supplies at runtime to an `operatorEgress: true` plugin, which by
|
||||
* definition cannot name them in its manifest. Only `trek-plugin dev` reads this (to
|
||||
* widen its egress guard); the mock ctx makes no network calls of its own. It lives
|
||||
@@ -1580,7 +1582,8 @@ export function createMockHost(opts: MockHostOptions = {}): MockHost {
|
||||
return impl[fn](...args, name === 'notificationChannel' ? userlessCtx : ctx) as never;
|
||||
},
|
||||
action: async (key) => {
|
||||
if (opts.declaredActions && !opts.declaredActions.includes(key)) {
|
||||
const declared = opts.declaredActions?.map((a) => (typeof a === 'string' ? a : a.key));
|
||||
if (declared && !declared.includes(key)) {
|
||||
throw new Error(`RESOURCE_FORBIDDEN: plugin did not declare action "${key}"`);
|
||||
}
|
||||
const fn = def.actions?.[key];
|
||||
|
||||
Reference in New Issue
Block a user