From a5e7dac10f2ebf10b606e3de1a9c1dc89c0803a1 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 29 Sep 2026 02:13:26 +0200 Subject: [PATCH] feat(10.1-02): run registered toolbar actions and scope plugin CSS per controller - ListToolbar renders server-filtered actions after delete as outline buttons, busy during their POST - ListView posts {} to toolbar/{action}, toasts, reloads the list and refetches the header partial - Lists load controller assets too, so other controllers' stylesheet links are disabled on every open - Vite dev server proxies {prefix}/assets to summer serve; dist rebuilt --- admin/src/api/types.ts | 2 + admin/src/app/pluginAssets.ts | 2 +- admin/src/components/list/ListToolbar.vue | 43 +++++-- admin/src/views/ListView.vue | 44 ++++++- .../tests/fixtures/extension.list-schema.json | 10 +- admin/tests/smoke/extension.smoke.test.ts | 113 +++++++++++++++++- admin/vite.config.ts | 6 +- .../boardwalk/dist/assets/index-D9vrCBt0.js | 4 - .../boardwalk/dist/assets/index-DMFEtOWd.js | 4 + modules/boardwalk/dist/index.html | 2 +- 10 files changed, 207 insertions(+), 23 deletions(-) delete mode 100644 modules/boardwalk/dist/assets/index-D9vrCBt0.js create mode 100644 modules/boardwalk/dist/assets/index-DMFEtOWd.js diff --git a/admin/src/api/types.ts b/admin/src/api/types.ts index 5da4ba1..2db99e0 100644 --- a/admin/src/api/types.ts +++ b/admin/src/api/types.ts @@ -40,6 +40,8 @@ export type AdminActionRequest = Schemas['cabana.AdminActionRequest'] export type AdminActionResult = Schemas['cabana.AdminActionResult'] /** A controller's plugin script and stylesheet URLs under {base}/assets/. */ export type ControllerAssets = Schemas['cabana.ControllerAssets'] +/** A registered toolbar action the admin may run, with its localized label. */ +export type ToolbarAction = Schemas['cabana.ToolbarAction'] /** One node of a rendered partial: an allowlisted element or a text node. */ export type PartialNode = Schemas['cabana.PartialNode'] /** A rendered header or form partial. */ diff --git a/admin/src/app/pluginAssets.ts b/admin/src/app/pluginAssets.ts index 76ca15f..01d3a29 100644 --- a/admin/src/app/pluginAssets.ts +++ b/admin/src/app/pluginAssets.ts @@ -102,8 +102,8 @@ export async function loadControllerAssets( controllerId: string, assets: ControllerAssets | null | undefined, ): Promise { - activateStyles(controllerId) loadStyles(controllerId, assets?.styles ?? []) + activateStyles(controllerId) const urls = assets?.scripts ?? [] const results = await Promise.allSettled(urls.map((url) => loadScript(url))) return urls.filter((_, index) => results[index]?.status === 'rejected') diff --git a/admin/src/components/list/ListToolbar.vue b/admin/src/components/list/ListToolbar.vue index 5fe7b8e..761124c 100644 --- a/admin/src/components/list/ListToolbar.vue +++ b/admin/src/components/list/ListToolbar.vue @@ -1,21 +1,33 @@ diff --git a/admin/src/views/ListView.vue b/admin/src/views/ListView.vue index 2ad3a49..5ab2405 100644 --- a/admin/src/views/ListView.vue +++ b/admin/src/views/ListView.vue @@ -7,6 +7,7 @@ import type { AdminRecord, ListMeta, ListQuery, ListSchema } from '../api/types' import { controllerIdFromParams, controllerPath } from '../app/controllerRoutes' import { message, t } from '../app/i18n' import { listQueryKey, parseListQuery, toListQuery, type RowId } from '../app/listQuery' +import { loadControllerAssets } from '../app/pluginAssets' import { mapWinterUrl } from '../app/winterUrl' import DataTable from '../components/list/DataTable.vue' import FilterBar from '../components/list/FilterBar.vue' @@ -70,7 +71,13 @@ const subtitle = computed(() => (meta.value ? message(messages.value?.recordCoun const buttons = computed(() => schema.value?.toolbarButtons ?? []) const headingButtons = computed(() => buttons.value.filter((button) => button === 'create')) -const toolbarButtons = computed(() => buttons.value.filter((button) => button === 'delete')) +// Delete and the registered actions the admin may run, in declared order +// (D-12). An action missing from toolbarActions is not rendered at all. +const toolbarActions = computed(() => schema.value?.toolbarActions ?? []) +const toolbarButtons = computed(() => + buttons.value.filter((button) => button === 'delete' || toolbarActions.value.some((action) => action.name === button)), +) +const busyAction = ref(null) const perPageOptions = computed(() => { const options = schema.value?.perPageOptions ?? [] @@ -116,7 +123,10 @@ async function load(): Promise { const result = await api.GET('/{vendor}/{plugin}/{controller}/schema/list', { params: { path } }) schema.value = result.data?.data ?? null schemaFailed.value = !schema.value - + if (schema.value) { + // Plugin files load in the background; the table never waits (D-14). + void loadControllerAssets(controllerId, schema.value.assets) + } } void load() @@ -216,6 +226,33 @@ async function onDelete(): Promise { deleting.value = false } } + +/** Runs a registered toolbar action (D-12): no confirmation, body {}. */ +async function onAction(name: string): Promise { + if (busyAction.value !== null) { + return + } + busyAction.value = name + try { + const result = await api.POST('/{vendor}/{plugin}/{controller}/toolbar/{action}', { + params: { path: { ...path, action: name } }, + body: {}, + }) + if (result.data) { + if (result.data.data.message) { + showToast(result.data.data.message) + } + await loadList() + partialReload.value += 1 + return + } + showToast(result.error?.error.message || t('backend::lang.extension.action_failed'), 'danger') + } catch { + showToast(t('backend::lang.extension.action_failed'), 'danger') + } finally { + busyAction.value = null + } +}