feat(admin): widget field payload and summer-result data channel (quick-261006-eyj)
- WidgetField posts the summer-action event's detail.payload as payload only
when the detail carries one; a payload-less post body is unchanged
- after a successful action the response data is set on the element as the
data attribute (removed when the answer has none) and announced with a
summer-result event carrying {data, fill, message}; failures dispatch nothing
- WIDGET_RESULT_EVENT exported from formContext; unit tests for both directions
- modules/boardwalk/dist rebuilt
This commit is contained in:
@@ -6,16 +6,20 @@ import { currentLocale, t } from '../../../app/i18n'
|
||||
import { showToast } from '../../../state/useToasts'
|
||||
import ExtensionFailure from '../../ui/ExtensionFailure.vue'
|
||||
import type { FieldControlProps } from '../control'
|
||||
import { FORM_ASSETS, FORM_LOCALE, FORM_PATCH, FORM_VALUES, WIDGET_EVENT, WIDGET_TIMEOUT } from '../formContext'
|
||||
import { FORM_ASSETS, FORM_LOCALE, FORM_PATCH, FORM_VALUES, WIDGET_EVENT, WIDGET_RESULT_EVENT, WIDGET_TIMEOUT } from '../formContext'
|
||||
|
||||
// A `type: widget` field (D-04, D-05, D-07, D-08; UI-SPEC S3). The plugin's
|
||||
// custom element is created imperatively inside a node Vue never renders
|
||||
// children into. It gets attributes only: record id, field name, locale, the
|
||||
// current fill values as JSON and its labels; never a token, a cookie, a Vue
|
||||
// current fill values as JSON and its labels, and after a successful action
|
||||
// the serialized response data as `data`; never a token, a cookie, a Vue
|
||||
// instance or a function. It asks for its action with a bubbling
|
||||
// summer-action event; the SPA posts it with the admin session, writes back
|
||||
// only the declared fill keys the server returned and shows the message.
|
||||
// Nothing is saved until the admin presses Save.
|
||||
// summer-action event whose optional detail.payload (any JSON value) the SPA
|
||||
// posts as `payload`; the SPA posts with the admin session, writes back only
|
||||
// the declared fill keys the server returned, sets or removes the `data`
|
||||
// attribute from the response data, dispatches summer-result on the element
|
||||
// with detail {data, fill, message} and shows the message. Nothing is saved
|
||||
// until the admin presses Save.
|
||||
const props = defineProps<FieldControlProps>()
|
||||
|
||||
const values = inject(FORM_VALUES, null)
|
||||
@@ -73,28 +77,44 @@ function fail(text?: string): void {
|
||||
element?.setAttribute('state', 'error')
|
||||
}
|
||||
|
||||
async function onAction(): Promise<void> {
|
||||
async function onAction(payload: unknown): Promise<void> {
|
||||
const source = props.source
|
||||
if (!element || busy || !source) {
|
||||
const target = element
|
||||
if (!target || busy || !source) {
|
||||
return
|
||||
}
|
||||
busy = true
|
||||
element.setAttribute('busy', '')
|
||||
element.removeAttribute('state')
|
||||
target.setAttribute('busy', '')
|
||||
target.removeAttribute('state')
|
||||
try {
|
||||
const result = await api.POST('/{vendor}/{plugin}/{controller}/widgets/{field}', {
|
||||
params: { path: { ...source, field: props.field.name } },
|
||||
body: { record_id: props.recordId ?? undefined, values: fillValues() },
|
||||
body: {
|
||||
record_id: props.recordId ?? undefined,
|
||||
values: fillValues(),
|
||||
...(payload === undefined ? {} : { payload }),
|
||||
},
|
||||
})
|
||||
if (result.data) {
|
||||
const fill = result.data.data.fill ?? {}
|
||||
const answer = result.data.data
|
||||
const fill = answer.fill ?? {}
|
||||
for (const name of props.field.fill ?? []) {
|
||||
if (Object.hasOwn(fill, name)) {
|
||||
patch(name, fill[name])
|
||||
}
|
||||
}
|
||||
if (result.data.data.message) {
|
||||
showToast(result.data.data.message)
|
||||
if (!unmounted) {
|
||||
if (Object.hasOwn(answer, 'data')) {
|
||||
target.setAttribute('data', JSON.stringify(answer.data))
|
||||
} else {
|
||||
target.removeAttribute('data')
|
||||
}
|
||||
target.dispatchEvent(
|
||||
new CustomEvent(WIDGET_RESULT_EVENT, { detail: { data: answer.data, fill, message: answer.message } }),
|
||||
)
|
||||
}
|
||||
if (answer.message) {
|
||||
showToast(answer.message)
|
||||
}
|
||||
} else {
|
||||
fail(result.error?.error.message)
|
||||
@@ -107,8 +127,17 @@ async function onAction(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function listener(): void {
|
||||
void onAction()
|
||||
/** The event's own payload property, or undefined when the detail has none. */
|
||||
function payloadOf(event: Event): unknown {
|
||||
const detail: unknown = (event as CustomEvent<unknown>).detail
|
||||
if (detail !== null && typeof detail === 'object' && Object.hasOwn(detail, 'payload')) {
|
||||
return (detail as { payload: unknown }).payload
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function listener(event: Event): void {
|
||||
void onAction(payloadOf(event))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -82,5 +82,12 @@ export const FORM_SESSION: InjectionKey<FormSession> = Symbol('summer.form.sessi
|
||||
/** Event a widget element dispatches to run its action (bubbles, composed). */
|
||||
export const WIDGET_EVENT = 'summer-action'
|
||||
|
||||
/**
|
||||
* Event the SPA dispatches on the plugin element after each successful action,
|
||||
* with detail {data, fill, message}: the response data as returned (undefined
|
||||
* when the answer carried none), the fill object and the message.
|
||||
*/
|
||||
export const WIDGET_RESULT_EVENT = 'summer-result'
|
||||
|
||||
/** How long a widget waits for its custom element to be defined. */
|
||||
export const WIDGET_TIMEOUT = 5000
|
||||
|
||||
Reference in New Issue
Block a user