// 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 { AdminDependentOptionsResult, AdminRecord, FormField, PermissionRecordLock, RelationOption } from '../../api/types' /** Read-only view of the form's current values. */ export const FORM_VALUES: InjectionKey>> = 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>> = Symbol('summer.form.locale') /** Enabled content locales for ML controls (schema meta.enabledLocales). */ export const FORM_ENABLED_LOCALES: InjectionKey>> = Symbol('summer.form.enabledLocales') /** * 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> = 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> /** Goes up after every successful save, so fields can reload their files. */ revision: Readonly> /** * 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> } /** Counter and register/release pair used by record and child forms. */ export function createUploadGate(): { activeUploads: Ref; 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 = Symbol('summer.form.session') /** * The state of each write-only password field of a settings page (Phase 18 * D-13, UI-SPEC F2): field name to `stored`, `env` or `empty`, from the * settings result's meta.secrets. SettingsFormView provides it; a record * form provides nothing, so its password fields keep their plain behaviour. */ export const SETTINGS_SECRETS: InjectionKey>>> = Symbol('summer.settings.secrets') /** * The record's permission locks (meta.permissionLocks, D-21): per * permissioneditor field name, the codes the stored record fixes with the * value and note to show. FormView and PreviewView provide it from the record * response; a control without a provider sees no record lock. */ export const FORM_PERMISSION_LOCKS: InjectionKey>>> = Symbol('summer.form.permissionLocks') /** * The notes of the fields a trigger disables now (fields.yaml `trigger`, * D-21): top-level field name to the note shown between the label and the * control. FormView provides it; without a provider no note is shown. */ export const FORM_TRIGGER_NOTES: InjectionKey>>> = Symbol('summer.form.triggerNotes') /** 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' /** * Event a plugin field type's element dispatches (bubbling) when the * administrator edits its value; detail.value is the new form value. */ export const FIELD_CHANGE_EVENT = 'summer-change' /** * Event a plugin field type's element dispatches (bubbling) to upload a file, * with detail {id, file}: the element's own request id and the File. The SPA * posts it with the admin session to the field upload route. */ export const FIELD_UPLOAD_EVENT = 'summer-upload' /** * Event the SPA dispatches on the element when an upload ends, with detail * {id, url} on success or {id, error} with a message on failure. */ export const FIELD_UPLOAD_RESULT_EVENT = 'summer-upload-result' /** How long a widget waits for its custom element to be defined. */ export const WIDGET_TIMEOUT = 5000 /** * The record form's feedback (Phase 17, D-07, D-08): errors and save notices * keyed by field name or row path (`..`), the form's * single polite live region, and its confirm dialog. FormView provides it; a * control without it (settings pages) announces nothing and never asks. */ export interface FormFeedback { errors: Readonly>> notices: Readonly>> /** Drops the error lines of one key: the administrator edited that value. */ clearError(key: string): void /** Announces text in the form's polite live region. */ announce(text: string): void /** Asks through the form's ConfirmDialog; resolves true on confirm. */ confirm(request: { message: string; confirmLabel?: string; danger?: boolean }): Promise } export const FORM_FEEDBACK: InjectionKey = Symbol('summer.form.feedback') /** * The fields a FormGrid renders and their values (D-08): a dependent * dropdown reads its sibling here. A repeater row's grid provides the row's * fields and values, and `path` names a field for its routes * (`.` in a row). */ export interface FieldScope { fields: Readonly> values: Readonly> path(name: string): string } export const FIELD_SCOPE: InjectionKey = Symbol('summer.form.fieldScope') /** * Form hosts persist a picked relation option so a remounted RelationField * can seed its id→label cache from props.labels; a host without a provider * is a no-op. */ export const FORM_REMEMBER_LABEL: InjectionKey<(field: string, option: RelationOption) => void> = Symbol('summer.form.rememberLabel') /** Dependent options per field path and sibling value, for the life of a form view. */ export const DEPENDENT_CACHE: InjectionKey> = Symbol('summer.form.dependentCache')