Files
summercms/admin/src/components/form/formContext.ts
Jakub Zych c6f68e4639 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
2026-10-06 11:16:46 +02:00

94 lines
3.7 KiB
TypeScript

// What a record form shares with extension controls (D-05, D-08). FormView
// provides these; a widget reads the current values and the locale, and
// writes action results back through patch, which marks the form dirty and
// clears that field's errors exactly like typing into it. A form without a
// provider (settings pages) gets the defaults of the injecting control.
import { ref, type InjectionKey, type Ref } from 'vue'
import type { FileRoutes } from '../../api/files'
import type { AdminRecord } from '../../api/types'
/** Read-only view of the form's current values. */
export const FORM_VALUES: InjectionKey<Readonly<Ref<AdminRecord>>> = Symbol('summer.form.values')
/** Sets one field's value as if the admin had edited it. Nothing is saved. */
export const FORM_PATCH: InjectionKey<(name: string, value: unknown) => void> = Symbol('summer.form.patch')
/** The form schema's locale (meta.locale). */
export const FORM_LOCALE: InjectionKey<Readonly<Ref<string>>> = Symbol('summer.form.locale')
/**
* The form controller's plugin assets: resolves once its scripts have loaded
* or failed, with the failed URLs (pluginAssets.loadControllerAssets).
*/
export const FORM_ASSETS: InjectionKey<() => Promise<string[]>> = Symbol('summer.form.assets')
/**
* The form's deferred-binding session (D-02, D-03). FormView makes one per
* mount; a relation child modal provides its own for the fields inside it.
* Uploads, removals and deferred relation calls go out with `key` (and
* `childKey` in a child modal) and are committed by the form's next save.
*/
export interface FormSession {
/** The record form's session key (X-Session-Key). */
key: string
/** The child modal's own key (X-Child-Session-Key); absent on a record form. */
childKey?: string
/** Id of the record the files belong to; 0 while it is not saved yet. */
recordId: number
/** The file routes of one fileupload field of this form. */
routes(field: string): FileRoutes
/**
* Records a change held against the session (an upload, a removal, a
* cancelled upload, a deferred relation change): the form becomes dirty.
*/
markDirty(): void
/** How many such changes the form holds; 0 after a successful save. */
pendingChanges: Readonly<Ref<number>>
/** Goes up after every successful save, so fields can reload their files. */
revision: Readonly<Ref<number>>
/**
* Registers an in-flight upload. Call the returned function in `finally`
* when the request reaches a terminal state. The form will not submit
* while any upload is still open.
*/
beginUpload(): () => void
/** How many uploads have not yet reached a terminal state. */
activeUploads: Readonly<Ref<number>>
}
/** Counter and register/release pair used by record and child forms. */
export function createUploadGate(): { activeUploads: Ref<number>; beginUpload: () => () => void } {
const activeUploads = ref(0)
return {
activeUploads,
beginUpload() {
activeUploads.value++
let released = false
return () => {
if (released) {
return
}
released = true
if (activeUploads.value > 0) {
activeUploads.value--
}
}
},
}
}
export const FORM_SESSION: InjectionKey<FormSession> = Symbol('summer.form.session')
/** 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