fix(32): CR-04 take widget actions dispatched from connectedCallback
WidgetField assigned its element after append(), so an action the element dispatched from connectedCallback (inside append) found no target and was dropped without a result or an error state; widgets that load on connect never started. The element is now the action target before it is connected. Rebuilt the embedded admin dist.
This commit is contained in:
@@ -158,8 +158,10 @@ onMounted(async () => {
|
||||
created.setAttribute('label', props.field.actionLabel || props.field.label || props.field.name)
|
||||
created.setAttribute('busy-label', t('backend::lang.extension.busy'))
|
||||
created.addEventListener(WIDGET_EVENT, listener)
|
||||
mountPoint.value.append(created)
|
||||
// The element is the action target before it is connected: an action
|
||||
// its connectedCallback dispatches (inside append) is taken, not dropped.
|
||||
element = created
|
||||
mountPoint.value.append(created)
|
||||
status.value = 'ready'
|
||||
} catch {
|
||||
if (!unmounted) {
|
||||
|
||||
@@ -49,6 +49,7 @@ interface Options {
|
||||
assets?: () => Promise<string[]>
|
||||
source?: typeof SOURCE | null
|
||||
routes?: Record<string, Route>
|
||||
attachTo?: HTMLElement
|
||||
}
|
||||
|
||||
function mountWidget(options: Options = {}) {
|
||||
@@ -59,6 +60,7 @@ function mountWidget(options: Options = {}) {
|
||||
})
|
||||
const calls = mockApi(options.routes ?? {})
|
||||
const wrapper = mount(WidgetField, {
|
||||
attachTo: options.attachTo,
|
||||
props: {
|
||||
field: options.field ?? lookupField(),
|
||||
modelValue: undefined,
|
||||
@@ -367,6 +369,44 @@ describe('summer-action (D-05, D-07)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('an action from connectedCallback (CR-04)', () => {
|
||||
it('posts the action an element dispatches while it is being connected', async () => {
|
||||
const tag = 'acme-demo-eager'
|
||||
if (!customElements.get(tag)) {
|
||||
customElements.define(
|
||||
tag,
|
||||
class extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.dispatchEvent(new CustomEvent(WIDGET_EVENT, { bubbles: true, composed: true, detail: { payload: { op: 'context' } } }))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
const action = deferred()
|
||||
const host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
const { wrapper, calls } = mountWidget({
|
||||
field: lookupField({ widget: tag }),
|
||||
attachTo: host,
|
||||
routes: { [`POST ${ACTION}`]: action.route },
|
||||
})
|
||||
await flushPromises()
|
||||
const target = wrapper.find(tag).element as HTMLElement
|
||||
expect(target.isConnected).toBe(true)
|
||||
expect(target.hasAttribute('busy')).toBe(true)
|
||||
const posts = requestsTo(calls, 'POST', ACTION)
|
||||
expect(posts).toHaveLength(1)
|
||||
const body = (await posts[0]!.json()) as Record<string, unknown>
|
||||
expect(body.payload).toEqual({ op: 'context' })
|
||||
const seen = results(target)
|
||||
action.release({ body: { data: { message: '', fill: {} }, meta: {} } })
|
||||
await flushPromises()
|
||||
expect(target.hasAttribute('busy')).toBe(false)
|
||||
expect(seen).toHaveLength(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('payload and data (summer-result)', () => {
|
||||
const OK: Reply = { body: { data: { message: '', fill: {} }, meta: {} } }
|
||||
const DATA_REPLY: Reply = {
|
||||
|
||||
@@ -83,7 +83,7 @@ The SPA talks to the plugin element through attributes and two events, nothing e
|
||||
|
||||
The events:
|
||||
|
||||
- `summer-action` is dispatched by the element (with `bubbles` and `composed`) to ask for its action. Its optional `detail.payload` is any JSON value; the SPA posts it as `payload`. A detail without a `payload` property sends none.
|
||||
- `summer-action` is dispatched by the element (with `bubbles` and `composed`) to ask for its action. Its optional `detail.payload` is any JSON value; the SPA posts it as `payload`. A detail without a `payload` property sends none. The SPA listens before it connects the element, so an action dispatched from `connectedCallback` (a first load, for example) is taken; one dispatched while `busy` is set is dropped, so wait for `busy` to clear before asking again.
|
||||
- `summer-result` is dispatched by the SPA on the element after a successful action, with `detail` `{data, fill, message}`: the response data as returned (`undefined` when none), the fill object and the message. A failed action dispatches nothing.
|
||||
|
||||
The element never receives a token, a cookie, a Vue instance or a function, and it must not make requests of its own: the SPA owns HTTP and the admin session. Render server data with `textContent`, not `innerHTML`: `data` is the plugin's own answer, but it still came over the wire. The `acme-demo-lookup` fixture under `modules/cabana/testdata/extension/assets/js/lookup.js` is a complete reorder widget: a click sends the new id order as the payload and the returned item list is rendered from the `data` attribute and the `summer-result` event.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
modules/boardwalk/dist/index.html
vendored
2
modules/boardwalk/dist/index.html
vendored
@@ -7,7 +7,7 @@
|
||||
<meta name="summer-admin-base" content="__SUMMER_ADMIN_BASE__" />
|
||||
<link rel="icon" href="./favicon.ico" sizes="48x48" />
|
||||
<title>SummerCMS</title>
|
||||
<script type="module" crossorigin src="./assets/index-BgYjF4nQ.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-BNWI_4R0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Bakgr9jB.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user