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
This commit is contained in:
Jakub Zych
2026-09-29 02:13:26 +02:00
parent 9df9fae930
commit a5e7dac10f
10 changed files with 207 additions and 23 deletions

View File

@@ -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<string | null>(null)
const perPageOptions = computed(() => {
const options = schema.value?.perPageOptions ?? []
@@ -116,7 +123,10 @@ async function load(): Promise<void> {
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<void> {
deleting.value = false
}
}
/** Runs a registered toolbar action (D-12): no confirmation, body {}. */
async function onAction(name: string): Promise<void> {
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
}
}
</script>
<template>
@@ -255,8 +292,11 @@ async function onDelete(): Promise<void> {
:selected-count="selected.length"
:selected-label="message(messages?.selected, selected.length)"
:delete-label="message(messages?.deleteSelected, selected.length)"
:actions="toolbarActions"
:busy-action="busyAction"
@update:search="onSearch"
@delete="onDelete"
@action="onAction"
/>
<FilterBar
v-if="schema && schema.filters.length > 0"