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:
jubnl
2026-09-02 23:06:03 +02:00
committed by GitHub
parent b1ce7c303d
commit b82962f95b
59 changed files with 1133 additions and 128 deletions
@@ -27,7 +27,7 @@ function ctrl() {
updateUserConfig: vi.fn((_id: string, _uid: number, patch: Record<string, unknown>) => ({ ...patch, apiKey: '••••••••' })),
} as unknown as PluginsService;
// The controller now also takes the runtime (for settings-page actions).
const runtime = { actionsOf: () => [], invokeAction: vi.fn() } as unknown as PluginRuntimeService;
const runtime = { actionsOf: vi.fn(() => []), invokeAction: vi.fn(async () => ({ ok: true })) } as unknown as PluginRuntimeService;
return { c: new PluginUserSettingsController(svc, runtime, new DatabaseService(dbConn)), svc, runtime };
}
@@ -55,4 +55,12 @@ describe('PluginUserSettingsController', () => {
getMock.mockReturnValue(undefined as never);
expect(c.update('p', { config: { units: 'metric' } }, req(5))).toEqual({ config: {} });
});
it('GET lists USER-scope actions only, and POST runs one as the caller in the user scope', async () => {
const { c, runtime } = ctrl();
c.get('p', req(5));
expect(runtime.actionsOf).toHaveBeenCalledWith('p', 'user');
expect(await c.runAction('p', 'sync', req(5))).toEqual({ ok: true });
expect(runtime.invokeAction).toHaveBeenCalledWith('p', 'sync', 5, 'user');
});
});