- FORM_REMEMBER_LABEL inject so hosts keep picked option names - RelationField choose() writes the label before emit - Vitest backstops remount and tab-switch labeling Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
7.7 KiB
TypeScript
184 lines
7.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 { AdminDependentOptionsResult, AdminRecord, FormField, PermissionRecordLock, RelationOption } 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')
|
|
|
|
/** Enabled content locales for ML controls (schema meta.enabledLocales). */
|
|
export const FORM_ENABLED_LOCALES: InjectionKey<Readonly<Ref<readonly string[]>>> = 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<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')
|
|
|
|
/**
|
|
* 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<Readonly<Ref<Record<string, string>>>> = 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<Readonly<Ref<Record<string, PermissionRecordLock[]>>>> =
|
|
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<Readonly<Ref<Record<string, string>>>> = 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 (`<repeater>.<index>.<nested>`), 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<Ref<Record<string, string[]>>>
|
|
notices: Readonly<Ref<Record<string, string[]>>>
|
|
/** 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<boolean>
|
|
}
|
|
|
|
export const FORM_FEEDBACK: InjectionKey<FormFeedback> = 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
|
|
* (`<repeater>.<field>` in a row).
|
|
*/
|
|
export interface FieldScope {
|
|
fields: Readonly<Ref<readonly FormField[]>>
|
|
values: Readonly<Ref<AdminRecord>>
|
|
path(name: string): string
|
|
}
|
|
|
|
export const FIELD_SCOPE: InjectionKey<FieldScope> = 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<Map<string, AdminDependentOptionsResult>> = Symbol('summer.form.dependentCache')
|