@alfakit/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/components.css +347 -0
- package/dist/actions.d.ts +9 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +25 -5
- package/dist/actions.js.map +1 -1
- package/dist/cms-live-preview.d.ts.map +1 -1
- package/dist/cms-live-preview.js +7 -1
- package/dist/cms-live-preview.js.map +1 -1
- package/dist/file-size.js +1 -1
- package/dist/file-size.js.map +1 -1
- package/dist/form.d.ts +33 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +48 -1
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/page-actions.d.ts +8 -0
- package/dist/page-actions.d.ts.map +1 -1
- package/dist/page-actions.js +11 -1
- package/dist/page-actions.js.map +1 -1
- package/dist/schema/cms.d.ts +2 -0
- package/dist/schema/cms.d.ts.map +1 -1
- package/dist/schema/components.d.ts +5 -0
- package/dist/schema/components.d.ts.map +1 -1
- package/dist/schema/components.js.map +1 -1
- package/dist/schema/panel.d.ts +6 -0
- package/dist/schema/panel.d.ts.map +1 -1
- package/dist/schema/panel.js.map +1 -1
- package/dist/schema/table-grouping.d.ts +5 -1
- package/dist/schema/table-grouping.d.ts.map +1 -1
- package/package.json +5 -3
- package/themes.css +2 -334
package/dist/actions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.js","names":[],"sources":["../src/actions.ts"],"sourcesContent":["import type { Page } from '@inertiajs/core'\nimport { revealAutofocusField } from './autofocus'\nimport {\n CONFLICT_ERROR_KEY,\n currentRecordVersion,\n markRecordSubmitted,\n RECORD_VERSION_INPUT,\n reportSaveConflict,\n} from './conflicts'\nimport { isFieldActionContext } from './field-actions'\nimport { pageFlash } from './flash'\nimport { isLeavingVisit, type FormStore } from './form'\nimport { HttpError, inertiaPost, requestJson, router } from './http'\nimport { ACTION_PATH_SEPARATOR, actionPath, PASSWORD_CONFIRMATION_INPUT } from './modal-actions'\nimport { openDatabaseNotifications } from './notifications'\nimport { submitFormById } from './page-actions'\nimport { currentPageQuery } from './page-query'\nimport type {\n ActionModalCancelledResponse,\n ActionModalSchema,\n ActionSchema,\n AnyActionSchema,\n} from './schema/actions'\nimport { isActionGroup } from './schema/actions'\nimport type { RecordKey, Size } from './schema/common'\nimport type { FieldActionResponse } from './schema/form'\nimport type { NotificationSchema } from './schema/notifications'\nimport { createStore, type Store } from './store'\n\nexport interface ActionContext {\n /** Encrypted page context token. */\n token: string\n /** `page`, `table`, `relationManager:Class`, `widget:Class`. */\n component?: string\n record?: RecordKey | null\n /** Selected record keys for bulk actions. */\n records?: RecordKey[]\n /** \"All filtered records\" bulk selection. */\n allRecords?: boolean\n arguments?: Record<string, unknown>\n /**\n * Names of the actions whose modals this action sits in, outermost first (actions in a\n * modal's footer). The server resolves the action by its path, `parent>child`.\n */\n parents?: string[]\n /**\n * Field actions (`component` = `field:{schema}::{key}`): the form the field belongs to.\n * Its state is sent with the action and the refreshed form is merged back into it.\n */\n form?: FormStore\n}\n\nexport interface ExecuteOptions {\n /** The current password, for actions with `requiresPasswordConfirmation()`. */\n password?: string\n /**\n * \"Create & create another\": run with the `another` argument and, when the server keeps\n * the modal open, reload it with a fresh form (keeping `preserveFormData` fields).\n */\n createAnother?: boolean\n}\n\n/** The argument `CreateAction` reads for \"Create & create another\". */\nexport const CREATE_ANOTHER_ARGUMENT = 'another'\n\n/**\n * The state of the fresh form after \"Create & create another\": the server's defaults,\n * plus the previous values of the fields `preserveFormData` names (all of them for `true`).\n */\nexport function createAnotherState(\n fresh: Record<string, unknown>,\n previous: Record<string, unknown>,\n preserve: boolean | string[] | undefined,\n): Record<string, unknown> {\n if (!preserve) {\n return fresh\n }\n\n const keys = preserve === true ? Object.keys(previous) : preserve\n const state = { ...fresh }\n\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(previous, key)) {\n state[key] = previous[key]\n }\n }\n\n return state\n}\n\nexport interface OpenModal {\n action: ActionSchema\n context: ActionContext\n modal: ActionModalSchema\n /** Current form values of the modal form. */\n errors: Record<string, string[]>\n processing: boolean\n /** Field actions: settles `runField()` with the action's result (undefined when closed). */\n resolve?: (result: unknown) => void\n /**\n * Bumped by `resetModal()` (\"Create & create another\"): the renderer focuses the form's\n * `autofocus()` field again (`focusAutofocusField()`) once the fresh form rendered.\n */\n refocus?: number\n}\n\nexport interface ActionRunnerState {\n /** Stack of open modals (nested modals are possible). */\n modals: OpenModal[]\n /** Name of the action currently loading its modal. */\n loading: string | null\n}\n\nexport interface ActionRunnerOptions {\n endpoints: { action: string; modal: string }\n onError?: (error: unknown) => void\n /** Called after a successful run so the UI can e.g. clear a table selection. */\n onSuccess?: (action: ActionSchema, context: ActionContext) => void\n /**\n * Shows notifications that arrive in JSON responses (a modal that `mountUsing()`\n * cancelled, field actions). Renderers can also set it with `setNotificationHandler()`.\n */\n onNotifications?: (notifications: NotificationSchema[]) => void\n}\n\n/**\n * Drives action buttons: plain links navigate, actions without a modal post straight to\n * the server, and actions with a modal fetch the modal schema first.\n */\nexport class ActionRunner {\n public readonly store: Store<ActionRunnerState>\n\n /** Runs in flight, so a double click or a repeated shortcut doesn't submit twice. */\n private readonly running = new Set<string>()\n\n /** The form of each open modal (registered by the renderer), by modal path. */\n private readonly modalForms = new Map<string, FormStore>()\n\n constructor(private readonly options: ActionRunnerOptions) {\n this.store = createStore<ActionRunnerState>({ modals: [], loading: null })\n }\n\n get state(): ActionRunnerState {\n return this.store.get()\n }\n\n /** Show notifications from JSON responses with this handler (e.g. the renderer's toasts). */\n setNotificationHandler(\n handler: ((notifications: NotificationSchema[]) => void) | undefined,\n ): void {\n this.options.onNotifications = handler\n }\n\n private notify(notifications: NotificationSchema[] | undefined): void {\n if (notifications && notifications.length > 0) {\n this.options.onNotifications?.(notifications)\n }\n }\n\n /** Point the runner at the current panel's endpoints (they change per tenant/panel). */\n setEndpoints(endpoints: Partial<ActionRunnerOptions['endpoints']>): void {\n Object.assign(this.options.endpoints, endpoints)\n }\n\n /**\n * The renderer registers the form of an open modal, so nested actions can send its state\n * (`$mountedActions`, `getRawData()`) and `fillData()` can write into it. Returns the\n * function that unregisters it.\n */\n registerModalForm(action: ActionSchema, context: ActionContext, form: FormStore): () => void {\n const key = actionPath(action, context)\n this.modalForms.set(key, form)\n\n return () => {\n if (this.modalForms.get(key) === form) this.modalForms.delete(key)\n }\n }\n\n /** The open modals of the actions a nested action sits in, outermost first. */\n parentModals(context: ActionContext): Array<OpenModal | undefined> {\n const parents = context.parents ?? []\n\n return parents.map((_, index) => {\n const path = parents.slice(0, index + 1).join(ACTION_PATH_SEPARATOR)\n\n return this.state.modals.find(\n (modal) =>\n modal.context.component === context.component &&\n actionPath(modal.action, modal.context) === path,\n )\n })\n }\n\n /**\n * The form state of the modals a nested action sits in, outermost first (undefined when\n * none of them has a form).\n */\n mountedData(context: ActionContext): Array<Record<string, unknown>> | undefined {\n const parents = context.parents ?? []\n\n if (parents.length === 0) {\n return undefined\n }\n\n const forms = parents.map((_, index) =>\n this.modalForms.get(parents.slice(0, index + 1).join(ACTION_PATH_SEPARATOR)),\n )\n\n // Parents without a form have nothing to send.\n if (forms.every((form) => !form)) {\n return undefined\n }\n\n return forms.map((form) => form?.state.values ?? {})\n }\n\n /** Entry point for a click on an action button. */\n async trigger(action: ActionSchema, context: ActionContext): Promise<void> {\n if (action.disabled) {\n return\n }\n\n // `openDatabaseNotifications()`: a client-side button.\n if (action.opensDatabaseNotifications) {\n openDatabaseNotifications()\n\n return\n }\n\n // `makeModalSubmitAction()`: submit the modal this button sits in, with its arguments.\n if (action.submitsModal) {\n this.submitParentModal(action, context)\n\n return\n }\n\n // `formId()`: the page form submits itself with its state, wherever the button is.\n if (submitFormById(action, context)) {\n return\n }\n\n // `postToUrl()`: a POST request to the URL (e.g. \"lock session\"), not a visit.\n if (action.url && action.postToUrl) {\n const { router } = await import('@inertiajs/core')\n router.post(action.url)\n\n return\n }\n\n if (action.url && !action.hasAction && !action.hasModal) {\n if (action.openUrlInNewTab) {\n window.open(action.url, '_blank', 'noopener')\n } else {\n const { router } = await import('@inertiajs/core')\n router.visit(action.url)\n }\n\n return\n }\n\n if (isFieldActionContext(context)) {\n await this.runField(action, context)\n\n return\n }\n\n if (action.hasModal) {\n await this.openModal(action, context)\n\n return\n }\n\n this.execute(action, context)\n }\n\n /**\n * Run a field action (opening its modal first, if it has one) and resolve with its\n * result once it ran, or with undefined when the modal is closed without running it.\n */\n async runField(action: ActionSchema, context: ActionContext): Promise<unknown> {\n if (action.disabled) {\n return undefined\n }\n\n if (action.hasModal) {\n return new Promise<unknown>((resolve) => {\n void this.openModal(action, context, resolve)\n })\n }\n\n return this.executeField(action, context)\n }\n\n /**\n * Run the action whose modal `button` sits in (a `makeModalSubmitAction()` button) with\n * that modal's form state and the button's arguments.\n */\n submitParentModal(button: ActionSchema, context: ActionContext): void {\n const parents = context.parents ?? []\n const open = this.parentModals(context).at(-1)\n\n if (!open || parents.length === 0) {\n return\n }\n\n const form = this.modalForms.get(parents.join(ACTION_PATH_SEPARATOR))\n\n this.execute(\n open.action,\n { ...open.context, arguments: { ...open.context.arguments, ...button.arguments } },\n form?.state.values ?? {},\n )\n }\n\n /**\n * Close a modal the user dismissed (cancel, Escape, clicking away, close button). With\n * `cancelParentActionsOnClose()` its parents close too.\n */\n cancelModal(name: string): void {\n const open = this.state.modals.find((modal) => modal.action.name === name)\n const cancel = open?.modal.cancelParentActionsOnClose\n\n this.closeModal(name)\n\n if (open && cancel) {\n this.closeParents(open.context, cancel)\n }\n }\n\n /** Close the modals `context` sits in: all of them, or up to and including `until`. */\n closeParents(context: ActionContext, until: true | string): void {\n const parents = [...(context.parents ?? [])].reverse()\n\n for (const parent of parents) {\n this.closeModal(parent)\n\n if (until !== true && parent === until) {\n break\n }\n }\n }\n\n async openModal(\n action: ActionSchema,\n context: ActionContext,\n resolve?: (result: unknown) => void,\n ): Promise<void> {\n const key = `modal:${runKey(action, context)}`\n\n if (this.running.has(key)) {\n resolve?.(undefined)\n\n return\n }\n\n this.running.add(key)\n this.store.update({ loading: action.name })\n\n try {\n const modal = await this.fetchModal(action, context)\n\n // `mountUsing()` halted or cancelled the action: the modal stays closed.\n if (isCancelledModal(modal)) {\n this.store.update({ loading: null })\n this.notify(modal.notifications)\n resolve?.(undefined)\n\n return\n }\n\n this.store.set((state) => ({\n loading: null,\n modals: [\n ...state.modals,\n { action, context, modal, errors: {}, processing: false, resolve },\n ],\n }))\n } catch (error) {\n this.store.update({ loading: null })\n resolve?.(undefined)\n this.options.onError?.(error)\n } finally {\n this.running.delete(key)\n }\n }\n\n private fetchModal(\n action: ActionSchema,\n context: ActionContext,\n ): Promise<ActionModalSchema | ActionModalCancelledResponse> {\n const params = {\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n arguments: context.arguments,\n }\n const mountedData = this.mountedData(context)\n\n // Field actions send the form state, and nested actions their parents' form state,\n // which needs a request body.\n if (isFieldActionContext(context)) {\n return requestJson<ActionModalSchema | ActionModalCancelledResponse>(\n this.options.endpoints.modal,\n { method: 'POST', body: { ...params, state: context.form?.state.values ?? {} } },\n )\n }\n\n return requestJson<ActionModalSchema | ActionModalCancelledResponse>(\n this.options.endpoints.modal,\n mountedData ? { method: 'POST', body: { ...params, mountedData } } : { query: params },\n )\n }\n\n /**\n * After \"Create & create another\": load the modal again so its form starts from the\n * server's defaults, keeping the `preserveFormData` fields from the values just sent.\n */\n async resetModal(name: string, previous: Record<string, unknown> = {}): Promise<void> {\n const open = this.state.modals.find((modal) => modal.action.name === name)\n\n if (!open) {\n return\n }\n\n try {\n const modal = await this.fetchModal(open.action, open.context)\n\n if (isCancelledModal(modal)) {\n this.notify(modal.notifications)\n this.closeModal(name)\n\n return\n }\n\n if (modal.form) {\n modal.form = {\n ...modal.form,\n // A wizard goes back to the step holding the `autofocus()` field.\n components: revealAutofocusField(modal.form.components),\n state: createAnotherState(modal.form.state, previous, modal.preserveFormData),\n }\n }\n\n this.store.set((state) => ({\n ...state,\n modals: state.modals.map((entry) =>\n entry === open || entry.action.name === name\n ? { ...entry, modal, errors: {}, refocus: (entry.refocus ?? 0) + 1 }\n : entry,\n ),\n }))\n } catch (error) {\n this.options.onError?.(error)\n }\n }\n\n /** Whether this action is currently being submitted for this context. */\n isRunning(action: ActionSchema, context: ActionContext): boolean {\n return this.running.has(runKey(action, context))\n }\n\n closeModal(name?: string): void {\n const modals = this.state.modals\n const remaining = name\n ? modals.filter((modal) => modal.action.name !== name)\n : modals.slice(0, -1)\n\n this.store.set((state) => ({ ...state, modals: remaining }))\n\n // A field action's caller learns that its modal closed without running.\n for (const modal of modals) {\n if (!remaining.includes(modal)) modal.resolve?.(undefined)\n }\n }\n\n /** Submit the topmost modal (or run a modal-less action) through Inertia. */\n execute(\n action: ActionSchema,\n context: ActionContext,\n data: Record<string, unknown> = {},\n options: ExecuteOptions = {},\n ): void {\n if (isFieldActionContext(context)) {\n void this.executeField(action, context, data)\n\n return\n }\n\n const key = runKey(action, context)\n\n if (this.running.has(key)) {\n return\n }\n\n this.running.add(key)\n this.setProcessing(action.name, true)\n\n // Edit pages (`detectConflicts()`): page actions send the version the form was loaded\n // from, so the server refuses to save over somebody else's changes.\n const version = context.component === 'page' ? currentRecordVersion() : null\n\n if (version !== null) markRecordSubmitted()\n\n inertiaPost(\n actionEndpoint(this.options.endpoints.action, context),\n {\n ...(version !== null ? { [RECORD_VERSION_INPUT]: version } : {}),\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n records: context.records,\n allRecords: context.allRecords ? 1 : undefined,\n bulk: context.records !== undefined ? 1 : undefined,\n arguments: options.createAnother\n ? { ...context.arguments, [CREATE_ANOTHER_ARGUMENT]: true }\n : context.arguments,\n tableQuery: currentPageQuery(),\n data,\n mountedData: this.mountedData(context),\n [PASSWORD_CONFIRMATION_INPUT]: options.password || undefined,\n },\n {\n onError: (errors) => {\n if (CONFLICT_ERROR_KEY in (errors as Record<string, unknown>)) reportSaveConflict()\n\n // A nested action without a modal of its own (e.g. `getValidatedData()` on its\n // parent failed): the parent's modal shows the errors.\n const hasModal = this.state.modals.some((modal) => modal.action.name === action.name)\n const parent = hasModal ? undefined : this.parentModals(context).at(-1)\n\n this.setErrors(\n parent ? parent.action.name : action.name,\n errors as Record<string, string>,\n )\n },\n onSuccess: (page) => {\n const flash = pageFlash(page as Page).alfakitAction\n\n // `fillData()`: values written into the parents' forms.\n for (const filled of flash?.parentData ?? []) {\n const parents = context.parents ?? []\n const index = parents.lastIndexOf(filled.name)\n const form =\n index >= 0\n ? this.modalForms.get(parents.slice(0, index + 1).join(ACTION_PATH_SEPARATOR))\n : undefined\n\n for (const [path, value] of Object.entries(filled.data)) {\n form?.setValue(path, value, { silent: true })\n }\n }\n\n if (\n !flash ||\n flash.status === 'cancelled' ||\n (flash.status === 'success' && flash.closeModal !== false)\n ) {\n this.closeModal(action.name)\n }\n\n // `cancelParentActions()`: the modals this nested action was opened from close too.\n if (flash?.status === 'success' && flash.cancelParentActions) {\n this.closeParents(\n context,\n typeof flash.cancelParentActions === 'string' ? flash.cancelParentActions : true,\n )\n }\n\n // `replaceMountedAction()`: the next action opens (or runs) in place of this one.\n const replacement = flash?.status === 'success' ? flash.replaceMountedAction : undefined\n\n if (replacement) {\n const next: ActionSchema = {\n type: 'Action',\n name: replacement.name,\n hasModal: replacement.hasModal || undefined,\n hasAction: true,\n }\n const nextContext: ActionContext = {\n ...context,\n arguments: replacement.arguments ?? undefined,\n }\n\n if (replacement.hasModal) {\n void this.openModal(next, nextContext)\n } else {\n this.execute(next, nextContext)\n }\n }\n\n if (flash?.status === 'success' && flash.closeModal === false && options.createAnother) {\n void this.resetModal(action.name, data)\n }\n\n if (!flash || flash.status === 'success') {\n this.options.onSuccess?.(action, context)\n }\n },\n onFinish: () => {\n this.running.delete(key)\n this.setProcessing(action.name, false)\n },\n },\n )\n }\n\n /**\n * Run an action and resolve with its JSON result (for callers that don't want an\n * Inertia visit, e.g. a custom widget).\n */\n async executeJson<T = unknown>(\n action: ActionSchema,\n context: ActionContext,\n data: Record<string, unknown> = {},\n ): Promise<T> {\n try {\n const response = await requestJson<{ result: T }>(\n actionEndpoint(this.options.endpoints.action, context),\n {\n method: 'POST',\n body: {\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n records: context.records,\n allRecords: context.allRecords ? 1 : undefined,\n bulk: context.records !== undefined ? 1 : undefined,\n arguments: context.arguments,\n tableQuery: currentPageQuery(),\n data,\n },\n },\n )\n\n return response.result\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.setErrors(\n action.name,\n Object.fromEntries(\n Object.entries(error.errors).map(([key, messages]) => [key, messages[0] ?? '']),\n ),\n )\n }\n\n throw error\n }\n }\n\n /**\n * Run a field action as JSON with its form's state; merge the refreshed form from the\n * response, settle the modal's `runField()` promise and close the modal.\n */\n async executeField(\n action: ActionSchema,\n context: ActionContext,\n data: Record<string, unknown> = {},\n ): Promise<unknown> {\n const key = runKey(action, context)\n\n if (this.running.has(key)) {\n return undefined\n }\n\n this.running.add(key)\n this.setProcessing(action.name, true)\n\n const form = context.form\n const sent = form?.state.values ?? {}\n\n try {\n const response = await requestJson<FieldActionResponse>(\n actionEndpoint(this.options.endpoints.action, context),\n {\n method: 'POST',\n body: {\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n arguments: context.arguments,\n data,\n state: sent,\n fingerprints: form?.currentFingerprints,\n },\n },\n )\n\n // A partial response that doesn't fit the tree falls back to a full refresh.\n if (response.form && form && !form.applyResponse(response.form, sent)) {\n void form.refresh()\n }\n\n this.notify(response.notifications)\n\n if (response.status === 'halted') {\n return undefined\n }\n\n if (response.status === 'success') {\n const modal = this.state.modals.find((open) => open.action.name === action.name)\n modal?.resolve?.(response.result)\n }\n\n if (response.status === 'cancelled' || response.closeModal !== false) {\n this.closeModal(action.name)\n }\n\n if (response.status === 'success') {\n this.options.onSuccess?.(action, context)\n\n if (response.redirect) {\n const { router } = await import('@inertiajs/core')\n router.visit(response.redirect)\n }\n }\n\n return response.status === 'success' ? response.result : undefined\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.setErrors(\n action.name,\n Object.fromEntries(\n Object.entries(error.errors).map(([field, messages]) => [field, messages[0] ?? '']),\n ),\n )\n } else {\n this.options.onError?.(error)\n }\n\n return undefined\n } finally {\n this.running.delete(key)\n this.setProcessing(action.name, false)\n }\n }\n\n private setProcessing(name: string, processing: boolean): void {\n this.store.set((state) => ({\n ...state,\n modals: state.modals.map((modal) =>\n modal.action.name === name\n ? { ...modal, processing, errors: processing ? {} : modal.errors }\n : modal,\n ),\n }))\n }\n\n private setErrors(name: string, errors: Record<string, string>): void {\n const normalized: Record<string, string[]> = {}\n\n for (const [key, message] of Object.entries(errors)) {\n normalized[key] = [message]\n }\n\n this.store.set((state) => ({\n ...state,\n modals: state.modals.map((modal) =>\n modal.action.name === name ? { ...modal, errors: normalized } : modal,\n ),\n }))\n }\n}\n\n/**\n * Whether an action button should bind its key bindings globally. Record actions repeat\n * on every row, so a page-wide shortcut would run them once per record; they only bind\n * when rendered outside a record (header, page and bulk actions).\n */\nexport function bindsKeyBindings(\n action: Pick<ActionSchema, 'keyBindings' | 'disabled'>,\n context: Pick<ActionContext, 'record'>,\n): boolean {\n return Boolean(action.keyBindings?.length) && !action.disabled && context.record == null\n}\n\n/** Button sizes every renderer's shadcn `Button` understands. */\nexport type ActionButtonSize = 'xs' | 'sm' | 'default' | 'lg'\n\n/**\n * The button size for an action: its own `size()` wins over the size the surrounding\n * list asks for (`md` is the default button, `xl` the largest one).\n */\nexport function resolveActionSize(\n size: Size | null | undefined,\n fallback: ActionButtonSize = 'default',\n): ActionButtonSize {\n switch (size) {\n case 'xs':\n return 'xs'\n case 'sm':\n return 'sm'\n case 'md':\n return 'default'\n case 'lg':\n case 'xl':\n return 'lg'\n default:\n return fallback\n }\n}\n\n/** The square icon-only button that matches a button size. */\nexport function iconButtonSize(size: ActionButtonSize): 'icon-xs' | 'icon-sm' | 'icon' {\n return size === 'xs' ? 'icon-xs' : size === 'sm' ? 'icon-sm' : 'icon'\n}\n\nexport interface DropdownPlacement {\n side: 'top' | 'right' | 'bottom' | 'left'\n align: 'start' | 'center' | 'end'\n}\n\n/**\n * Parse an ActionGroup `dropdownPlacement()` (`bottom-start`, `top-end`, `left`, …) into the\n * side/align pair dropdown primitives take. Defaults to `bottom` / `end`.\n */\nexport function parseDropdownPlacement(placement: string | null | undefined): DropdownPlacement {\n const [side, align] = (placement ?? '').split('-')\n const sides = ['top', 'right', 'bottom', 'left'] as const\n const aligns = ['start', 'center', 'end'] as const\n\n const knownSide = sides.find((candidate) => candidate === side)\n const resolvedAlign =\n aligns.find((candidate) => candidate === align) ?? (knownSide && !align ? 'center' : 'end')\n\n return { side: knownSide ?? 'bottom', align: resolvedAlign }\n}\n\n/** Every action inside a group, nested groups flattened. */\nexport function flattenActionGroup(actions: AnyActionSchema[]): ActionSchema[] {\n return actions.flatMap((action) =>\n isActionGroup(action) ? flattenActionGroup(action.actions) : [action],\n )\n}\n\n/**\n * Whether an open modal is hidden because a nested modal was opened from it. Nested modals\n * hide their parents unless they opt into `overlayParentActions()`; a hidden parent shows\n * again when the child closes.\n */\nexport function isModalHiddenByChild(modals: OpenModal[], index: number): boolean {\n const modal = modals[index]\n\n if (!modal) {\n return false\n }\n\n const path = actionPath(modal.action, modal.context)\n\n return modals.slice(index + 1).some((child) => {\n if (child.modal.overlayParent === true) return false\n\n const parents = child.context.parents ?? []\n\n return parents.some(\n (_, position) => parents.slice(0, position + 1).join(ACTION_PATH_SEPARATOR) === path,\n )\n })\n}\n\n/** Justify class keyword for modal footer buttons: `footerActionsAlignment`, else `alignment`. */\nexport function modalFooterAlignment(\n modal: Pick<ActionModalSchema, 'alignment' | 'footerActionsAlignment'>,\n): 'start' | 'center' | 'end' | 'between' {\n const alignment = modal.footerActionsAlignment ?? modal.alignment\n\n switch (alignment) {\n case 'start':\n case 'center':\n case 'between':\n return alignment\n default:\n return 'end'\n }\n}\n\n/**\n * The physical side a slide-over enters from: `end` (default) is the right edge in\n * left-to-right documents and the left edge in right-to-left ones.\n */\nexport function slideOverSide(\n position: 'start' | 'end' | null | undefined,\n direction: 'ltr' | 'rtl' = 'ltr',\n): 'left' | 'right' {\n const atStart = position === 'start'\n\n return atStart === (direction === 'rtl') ? 'right' : 'left'\n}\n\nfunction isCancelledModal(\n modal: ActionModalSchema | ActionModalCancelledResponse,\n): modal is ActionModalCancelledResponse {\n return (modal as Partial<ActionModalCancelledResponse>).cancelled === true\n}\n\n/**\n * Whether closing a modal has to ask first: its form changed and the modal has\n * `unsavedChangesAlert` (the action's own setting or the panel's `unsavedChangesAlerts()`).\n */\nexport function shouldConfirmModalClose(\n modal: Pick<ActionModalSchema, 'unsavedChangesAlert'>,\n form: { isDirty: boolean } | null | undefined,\n): boolean {\n return Boolean(modal.unsavedChangesAlert && form?.isDirty)\n}\n\n/**\n * Ask whether to close a modal with unsaved changes (`window.confirm`); true when it may\n * close. Without changes (or the alert) it never asks.\n */\nexport function confirmModalClose(\n modal: Pick<ActionModalSchema, 'unsavedChangesAlert'>,\n form: { isDirty: boolean } | null | undefined,\n message: string,\n): boolean {\n if (!shouldConfirmModalClose(modal, form) || typeof window === 'undefined') {\n return true\n }\n\n return window.confirm(message)\n}\n\n/**\n * Warn before leaving the page (a reload, or an Inertia visit when `message` is given)\n * while a modal form with `unsavedChangesAlert` has unsaved changes. Returns a function\n * that removes the listeners.\n */\nexport function guardModalUnload(\n modal: Pick<ActionModalSchema, 'unsavedChangesAlert'>,\n form: { isDirty: boolean } | null | undefined,\n message?: string,\n): () => void {\n if (!modal.unsavedChangesAlert || !form || typeof window === 'undefined') {\n return () => {}\n }\n\n const listener = (event: BeforeUnloadEvent) => {\n if (form.isDirty) {\n event.preventDefault()\n event.returnValue = ''\n }\n }\n\n window.addEventListener('beforeunload', listener)\n\n const removeVisitGuard = message\n ? router.on('before', (event) => {\n if (form.isDirty && isLeavingVisit(event.detail.visit) && !window.confirm(message)) {\n event.preventDefault()\n }\n })\n : () => {}\n\n return () => {\n window.removeEventListener('beforeunload', listener)\n removeVisitGuard()\n }\n}\n\n/** The context of a user or tenant menu action: the page's token and the menu's component. */\nexport function menuActionContext(token: string, menu: 'userMenu' | 'tenantMenu'): ActionContext {\n return { token, component: menu }\n}\n\n/**\n * Split menu entries into their groups (`group` index, nested arrays in\n * `userMenuItems()`), in order; renderers draw a separator between groups.\n */\nexport function groupMenuItems<T extends { group?: number }>(items: T[]): T[][] {\n const groups: T[][] = []\n let current: number | null = null\n\n for (const item of items) {\n const group = item.group ?? 0\n\n if (current === null || group !== current) {\n groups.push([])\n current = group\n }\n\n groups[groups.length - 1]!.push(item)\n }\n\n return groups\n}\n\nfunction runKey(action: ActionSchema, context: ActionContext): string {\n return [context.component ?? 'page', actionPath(action, context), context.record ?? ''].join(\n '\\u0000',\n )\n}\n\n/**\n * The action endpoint for a run. \"All records\" bulk actions carry the page's query string\n * (filters, search, list tab), so the server selects exactly the records the table shows.\n */\nexport function actionEndpoint(\n endpoint: string,\n context: Pick<ActionContext, 'allRecords'>,\n search: string = typeof window !== 'undefined' ? window.location.search : '',\n): string {\n if (!context.allRecords || search.length <= 1) {\n return endpoint\n }\n\n return endpoint + (endpoint.includes('?') ? '&' : '?') + search.replace(/^\\?/, '')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"actions.js","names":[],"sources":["../src/actions.ts"],"sourcesContent":["import type { Page } from '@inertiajs/core'\nimport { revealAutofocusField } from './autofocus'\nimport {\n CONFLICT_ERROR_KEY,\n currentRecordVersion,\n markRecordSubmitted,\n RECORD_VERSION_INPUT,\n reportSaveConflict,\n} from './conflicts'\nimport { isFieldActionContext } from './field-actions'\nimport { pageFlash } from './flash'\nimport { isLeavingVisit, type FormStore } from './form'\nimport { HttpError, inertiaPost, requestJson, router } from './http'\nimport { ACTION_PATH_SEPARATOR, actionPath, PASSWORD_CONFIRMATION_INPUT } from './modal-actions'\nimport { openDatabaseNotifications } from './notifications'\nimport { submitFormById } from './page-actions'\nimport { currentPageQuery } from './page-query'\nimport type {\n ActionModalCancelledResponse,\n ActionModalSchema,\n ActionSchema,\n AnyActionSchema,\n} from './schema/actions'\nimport { isActionGroup } from './schema/actions'\nimport type { RecordKey, Size } from './schema/common'\nimport type { FieldActionResponse } from './schema/form'\nimport type { NotificationSchema } from './schema/notifications'\nimport { createStore, type Store } from './store'\n\nexport interface ActionContext {\n /** Encrypted page context token. */\n token: string\n /** `page`, `table`, `relationManager:Class`, `widget:Class`. */\n component?: string\n record?: RecordKey | null\n /** Selected record keys for bulk actions. */\n records?: RecordKey[]\n /** \"All filtered records\" bulk selection. */\n allRecords?: boolean\n arguments?: Record<string, unknown>\n /**\n * Names of the actions whose modals this action sits in, outermost first (actions in a\n * modal's footer). The server resolves the action by its path, `parent>child`.\n */\n parents?: string[]\n /**\n * Field actions (`component` = `field:{schema}::{key}`): the form the field belongs to.\n * Its state is sent with the action and the refreshed form is merged back into it.\n */\n form?: FormStore\n}\n\nexport interface ExecuteOptions {\n /** The current password, for actions with `requiresPasswordConfirmation()`. */\n password?: string\n /**\n * \"Create & create another\": run with the `another` argument and, when the server keeps\n * the modal open, reload it with a fresh form (keeping `preserveFormData` fields).\n */\n createAnother?: boolean\n}\n\n/** The argument `CreateAction` reads for \"Create & create another\". */\nexport const CREATE_ANOTHER_ARGUMENT = 'another'\n\n/**\n * The state of the fresh form after \"Create & create another\": the server's defaults,\n * plus the previous values of the fields `preserveFormData` names (all of them for `true`).\n */\nexport function createAnotherState(\n fresh: Record<string, unknown>,\n previous: Record<string, unknown>,\n preserve: boolean | string[] | undefined,\n): Record<string, unknown> {\n if (!preserve) {\n return fresh\n }\n\n const keys = preserve === true ? Object.keys(previous) : preserve\n const state = { ...fresh }\n\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(previous, key)) {\n state[key] = previous[key]\n }\n }\n\n return state\n}\n\nexport interface OpenModal {\n action: ActionSchema\n context: ActionContext\n modal: ActionModalSchema\n /** Current form values of the modal form. */\n errors: Record<string, string[]>\n processing: boolean\n /** Field actions: settles `runField()` with the action's result (undefined when closed). */\n resolve?: (result: unknown) => void\n /**\n * Bumped by `resetModal()` (\"Create & create another\"): the renderer focuses the form's\n * `autofocus()` field again (`focusAutofocusField()`) once the fresh form rendered.\n */\n refocus?: number\n}\n\nexport interface ActionRunnerState {\n /** Stack of open modals (nested modals are possible). */\n modals: OpenModal[]\n /** Name of the action currently loading its modal. */\n loading: string | null\n /**\n * Which run of it (component, action path and record): a table renders the same action\n * once per row, and only the clicked row's button is loading (`isActionLoading()`).\n */\n loadingKey?: string | null\n}\n\nexport interface ActionRunnerOptions {\n endpoints: { action: string; modal: string }\n onError?: (error: unknown) => void\n /** Called after a successful run so the UI can e.g. clear a table selection. */\n onSuccess?: (action: ActionSchema, context: ActionContext) => void\n /**\n * Shows notifications that arrive in JSON responses (a modal that `mountUsing()`\n * cancelled, field actions). Renderers can also set it with `setNotificationHandler()`.\n */\n onNotifications?: (notifications: NotificationSchema[]) => void\n}\n\n/**\n * Drives action buttons: plain links navigate, actions without a modal post straight to\n * the server, and actions with a modal fetch the modal schema first.\n */\nexport class ActionRunner {\n public readonly store: Store<ActionRunnerState>\n\n /** Runs in flight, so a double click or a repeated shortcut doesn't submit twice. */\n private readonly running = new Set<string>()\n\n /** The form of each open modal (registered by the renderer), by modal path. */\n private readonly modalForms = new Map<string, FormStore>()\n\n constructor(private readonly options: ActionRunnerOptions) {\n this.store = createStore<ActionRunnerState>({ modals: [], loading: null, loadingKey: null })\n }\n\n get state(): ActionRunnerState {\n return this.store.get()\n }\n\n /** Show notifications from JSON responses with this handler (e.g. the renderer's toasts). */\n setNotificationHandler(\n handler: ((notifications: NotificationSchema[]) => void) | undefined,\n ): void {\n this.options.onNotifications = handler\n }\n\n private notify(notifications: NotificationSchema[] | undefined): void {\n if (notifications && notifications.length > 0) {\n this.options.onNotifications?.(notifications)\n }\n }\n\n /** Point the runner at the current panel's endpoints (they change per tenant/panel). */\n setEndpoints(endpoints: Partial<ActionRunnerOptions['endpoints']>): void {\n Object.assign(this.options.endpoints, endpoints)\n }\n\n /**\n * The renderer registers the form of an open modal, so nested actions can send its state\n * (`$mountedActions`, `getRawData()`) and `fillData()` can write into it. Returns the\n * function that unregisters it.\n */\n registerModalForm(action: ActionSchema, context: ActionContext, form: FormStore): () => void {\n const key = actionPath(action, context)\n this.modalForms.set(key, form)\n\n return () => {\n if (this.modalForms.get(key) === form) this.modalForms.delete(key)\n }\n }\n\n /** The open modals of the actions a nested action sits in, outermost first. */\n parentModals(context: ActionContext): Array<OpenModal | undefined> {\n const parents = context.parents ?? []\n\n return parents.map((_, index) => {\n const path = parents.slice(0, index + 1).join(ACTION_PATH_SEPARATOR)\n\n return this.state.modals.find(\n (modal) =>\n modal.context.component === context.component &&\n actionPath(modal.action, modal.context) === path,\n )\n })\n }\n\n /**\n * The form state of the modals a nested action sits in, outermost first (undefined when\n * none of them has a form).\n */\n mountedData(context: ActionContext): Array<Record<string, unknown>> | undefined {\n const parents = context.parents ?? []\n\n if (parents.length === 0) {\n return undefined\n }\n\n const forms = parents.map((_, index) =>\n this.modalForms.get(parents.slice(0, index + 1).join(ACTION_PATH_SEPARATOR)),\n )\n\n // Parents without a form have nothing to send.\n if (forms.every((form) => !form)) {\n return undefined\n }\n\n return forms.map((form) => form?.state.values ?? {})\n }\n\n /** Entry point for a click on an action button. */\n async trigger(action: ActionSchema, context: ActionContext): Promise<void> {\n if (action.disabled) {\n return\n }\n\n // `openDatabaseNotifications()`: a client-side button.\n if (action.opensDatabaseNotifications) {\n openDatabaseNotifications()\n\n return\n }\n\n // `makeModalSubmitAction()`: submit the modal this button sits in, with its arguments.\n if (action.submitsModal) {\n this.submitParentModal(action, context)\n\n return\n }\n\n // `formId()`: the page form submits itself with its state, wherever the button is.\n if (submitFormById(action, context)) {\n return\n }\n\n // `postToUrl()`: a POST request to the URL (e.g. \"lock session\"), not a visit.\n if (action.url && action.postToUrl) {\n const { router } = await import('@inertiajs/core')\n router.post(action.url)\n\n return\n }\n\n if (action.url && !action.hasAction && !action.hasModal) {\n if (action.openUrlInNewTab) {\n window.open(action.url, '_blank', 'noopener')\n } else {\n const { router } = await import('@inertiajs/core')\n router.visit(action.url)\n }\n\n return\n }\n\n if (isFieldActionContext(context)) {\n await this.runField(action, context)\n\n return\n }\n\n if (action.hasModal) {\n await this.openModal(action, context)\n\n return\n }\n\n this.execute(action, context)\n }\n\n /**\n * Run a field action (opening its modal first, if it has one) and resolve with its\n * result once it ran, or with undefined when the modal is closed without running it.\n */\n async runField(action: ActionSchema, context: ActionContext): Promise<unknown> {\n if (action.disabled) {\n return undefined\n }\n\n if (action.hasModal) {\n return new Promise<unknown>((resolve) => {\n void this.openModal(action, context, resolve)\n })\n }\n\n return this.executeField(action, context)\n }\n\n /**\n * Run the action whose modal `button` sits in (a `makeModalSubmitAction()` button) with\n * that modal's form state and the button's arguments.\n */\n submitParentModal(button: ActionSchema, context: ActionContext): void {\n const parents = context.parents ?? []\n const open = this.parentModals(context).at(-1)\n\n if (!open || parents.length === 0) {\n return\n }\n\n const form = this.modalForms.get(parents.join(ACTION_PATH_SEPARATOR))\n\n this.execute(\n open.action,\n { ...open.context, arguments: { ...open.context.arguments, ...button.arguments } },\n form?.state.values ?? {},\n )\n }\n\n /**\n * Close a modal the user dismissed (cancel, Escape, clicking away, close button). With\n * `cancelParentActionsOnClose()` its parents close too.\n */\n cancelModal(name: string): void {\n const open = this.state.modals.find((modal) => modal.action.name === name)\n const cancel = open?.modal.cancelParentActionsOnClose\n\n this.closeModal(name)\n\n if (open && cancel) {\n this.closeParents(open.context, cancel)\n }\n }\n\n /** Close the modals `context` sits in: all of them, or up to and including `until`. */\n closeParents(context: ActionContext, until: true | string): void {\n const parents = [...(context.parents ?? [])].reverse()\n\n for (const parent of parents) {\n this.closeModal(parent)\n\n if (until !== true && parent === until) {\n break\n }\n }\n }\n\n async openModal(\n action: ActionSchema,\n context: ActionContext,\n resolve?: (result: unknown) => void,\n ): Promise<void> {\n const key = `modal:${runKey(action, context)}`\n\n if (this.running.has(key)) {\n resolve?.(undefined)\n\n return\n }\n\n this.running.add(key)\n this.store.update({ loading: action.name, loadingKey: runKey(action, context) })\n\n try {\n const modal = await this.fetchModal(action, context)\n\n // `mountUsing()` halted or cancelled the action: the modal stays closed.\n if (isCancelledModal(modal)) {\n this.store.update({ loading: null, loadingKey: null })\n this.notify(modal.notifications)\n resolve?.(undefined)\n\n return\n }\n\n this.store.set((state) => ({\n loading: null,\n loadingKey: null,\n modals: [\n ...state.modals,\n { action, context, modal, errors: {}, processing: false, resolve },\n ],\n }))\n } catch (error) {\n this.store.update({ loading: null, loadingKey: null })\n resolve?.(undefined)\n this.options.onError?.(error)\n } finally {\n this.running.delete(key)\n }\n }\n\n private fetchModal(\n action: ActionSchema,\n context: ActionContext,\n ): Promise<ActionModalSchema | ActionModalCancelledResponse> {\n const params = {\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n arguments: context.arguments,\n }\n const mountedData = this.mountedData(context)\n\n // Field actions send the form state, and nested actions their parents' form state,\n // which needs a request body.\n if (isFieldActionContext(context)) {\n return requestJson<ActionModalSchema | ActionModalCancelledResponse>(\n this.options.endpoints.modal,\n { method: 'POST', body: { ...params, state: context.form?.state.values ?? {} } },\n )\n }\n\n return requestJson<ActionModalSchema | ActionModalCancelledResponse>(\n this.options.endpoints.modal,\n mountedData ? { method: 'POST', body: { ...params, mountedData } } : { query: params },\n )\n }\n\n /**\n * After \"Create & create another\": load the modal again so its form starts from the\n * server's defaults, keeping the `preserveFormData` fields from the values just sent.\n */\n async resetModal(name: string, previous: Record<string, unknown> = {}): Promise<void> {\n const open = this.state.modals.find((modal) => modal.action.name === name)\n\n if (!open) {\n return\n }\n\n try {\n const modal = await this.fetchModal(open.action, open.context)\n\n if (isCancelledModal(modal)) {\n this.notify(modal.notifications)\n this.closeModal(name)\n\n return\n }\n\n if (modal.form) {\n modal.form = {\n ...modal.form,\n // A wizard goes back to the step holding the `autofocus()` field.\n components: revealAutofocusField(modal.form.components),\n state: createAnotherState(modal.form.state, previous, modal.preserveFormData),\n }\n }\n\n this.store.set((state) => ({\n ...state,\n modals: state.modals.map((entry) =>\n entry === open || entry.action.name === name\n ? { ...entry, modal, errors: {}, refocus: (entry.refocus ?? 0) + 1 }\n : entry,\n ),\n }))\n } catch (error) {\n this.options.onError?.(error)\n }\n }\n\n /** Whether this action is currently being submitted for this context. */\n isRunning(action: ActionSchema, context: ActionContext): boolean {\n return this.running.has(runKey(action, context))\n }\n\n closeModal(name?: string): void {\n const modals = this.state.modals\n const remaining = name\n ? modals.filter((modal) => modal.action.name !== name)\n : modals.slice(0, -1)\n\n this.store.set((state) => ({ ...state, modals: remaining }))\n\n // A field action's caller learns that its modal closed without running.\n for (const modal of modals) {\n if (!remaining.includes(modal)) modal.resolve?.(undefined)\n }\n }\n\n /** Submit the topmost modal (or run a modal-less action) through Inertia. */\n execute(\n action: ActionSchema,\n context: ActionContext,\n data: Record<string, unknown> = {},\n options: ExecuteOptions = {},\n ): void {\n if (isFieldActionContext(context)) {\n void this.executeField(action, context, data)\n\n return\n }\n\n const key = runKey(action, context)\n\n if (this.running.has(key)) {\n return\n }\n\n this.running.add(key)\n this.setProcessing(action.name, true)\n\n // Edit pages (`detectConflicts()`): page actions send the version the form was loaded\n // from, so the server refuses to save over somebody else's changes.\n const version = context.component === 'page' ? currentRecordVersion() : null\n\n if (version !== null) markRecordSubmitted()\n\n inertiaPost(\n actionEndpoint(this.options.endpoints.action, context),\n {\n ...(version !== null ? { [RECORD_VERSION_INPUT]: version } : {}),\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n records: context.records,\n allRecords: context.allRecords ? 1 : undefined,\n bulk: context.records !== undefined ? 1 : undefined,\n arguments: options.createAnother\n ? { ...context.arguments, [CREATE_ANOTHER_ARGUMENT]: true }\n : context.arguments,\n tableQuery: currentPageQuery(),\n data,\n mountedData: this.mountedData(context),\n [PASSWORD_CONFIRMATION_INPUT]: options.password || undefined,\n },\n {\n onError: (errors) => {\n if (CONFLICT_ERROR_KEY in (errors as Record<string, unknown>)) reportSaveConflict()\n\n // A nested action without a modal of its own (e.g. `getValidatedData()` on its\n // parent failed): the parent's modal shows the errors.\n const hasModal = this.state.modals.some((modal) => modal.action.name === action.name)\n const parent = hasModal ? undefined : this.parentModals(context).at(-1)\n\n this.setErrors(\n parent ? parent.action.name : action.name,\n errors as Record<string, string>,\n )\n },\n onSuccess: (page) => {\n const flash = pageFlash(page as Page).alfakitAction\n\n // `fillData()`: values written into the parents' forms.\n for (const filled of flash?.parentData ?? []) {\n const parents = context.parents ?? []\n const index = parents.lastIndexOf(filled.name)\n const form =\n index >= 0\n ? this.modalForms.get(parents.slice(0, index + 1).join(ACTION_PATH_SEPARATOR))\n : undefined\n\n for (const [path, value] of Object.entries(filled.data)) {\n form?.setValue(path, value, { silent: true })\n }\n }\n\n if (\n !flash ||\n flash.status === 'cancelled' ||\n (flash.status === 'success' && flash.closeModal !== false)\n ) {\n this.closeModal(action.name)\n }\n\n // `cancelParentActions()`: the modals this nested action was opened from close too.\n if (flash?.status === 'success' && flash.cancelParentActions) {\n this.closeParents(\n context,\n typeof flash.cancelParentActions === 'string' ? flash.cancelParentActions : true,\n )\n }\n\n // `replaceMountedAction()`: the next action opens (or runs) in place of this one.\n const replacement = flash?.status === 'success' ? flash.replaceMountedAction : undefined\n\n if (replacement) {\n const next: ActionSchema = {\n type: 'Action',\n name: replacement.name,\n hasModal: replacement.hasModal || undefined,\n hasAction: true,\n }\n const nextContext: ActionContext = {\n ...context,\n arguments: replacement.arguments ?? undefined,\n }\n\n if (replacement.hasModal) {\n void this.openModal(next, nextContext)\n } else {\n this.execute(next, nextContext)\n }\n }\n\n if (flash?.status === 'success' && flash.closeModal === false && options.createAnother) {\n void this.resetModal(action.name, data)\n }\n\n if (!flash || flash.status === 'success') {\n this.options.onSuccess?.(action, context)\n }\n },\n onFinish: () => {\n this.running.delete(key)\n this.setProcessing(action.name, false)\n },\n },\n )\n }\n\n /**\n * Run an action and resolve with its JSON result (for callers that don't want an\n * Inertia visit, e.g. a custom widget).\n */\n async executeJson<T = unknown>(\n action: ActionSchema,\n context: ActionContext,\n data: Record<string, unknown> = {},\n ): Promise<T> {\n try {\n const response = await requestJson<{ result: T }>(\n actionEndpoint(this.options.endpoints.action, context),\n {\n method: 'POST',\n body: {\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n records: context.records,\n allRecords: context.allRecords ? 1 : undefined,\n bulk: context.records !== undefined ? 1 : undefined,\n arguments: context.arguments,\n tableQuery: currentPageQuery(),\n data,\n },\n },\n )\n\n return response.result\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.setErrors(\n action.name,\n Object.fromEntries(\n Object.entries(error.errors).map(([key, messages]) => [key, messages[0] ?? '']),\n ),\n )\n }\n\n throw error\n }\n }\n\n /**\n * Run a field action as JSON with its form's state; merge the refreshed form from the\n * response, settle the modal's `runField()` promise and close the modal.\n */\n async executeField(\n action: ActionSchema,\n context: ActionContext,\n data: Record<string, unknown> = {},\n ): Promise<unknown> {\n const key = runKey(action, context)\n\n if (this.running.has(key)) {\n return undefined\n }\n\n this.running.add(key)\n this.setProcessing(action.name, true)\n\n const form = context.form\n const sent = form?.state.values ?? {}\n\n try {\n const response = await requestJson<FieldActionResponse>(\n actionEndpoint(this.options.endpoints.action, context),\n {\n method: 'POST',\n body: {\n token: context.token,\n component: context.component,\n action: actionPath(action, context),\n record: context.record ?? undefined,\n arguments: context.arguments,\n data,\n state: sent,\n fingerprints: form?.currentFingerprints,\n },\n },\n )\n\n // A partial response that doesn't fit the tree falls back to a full refresh.\n if (response.form && form && !form.applyResponse(response.form, sent)) {\n void form.refresh()\n }\n\n this.notify(response.notifications)\n\n if (response.status === 'halted') {\n return undefined\n }\n\n if (response.status === 'success') {\n const modal = this.state.modals.find((open) => open.action.name === action.name)\n modal?.resolve?.(response.result)\n }\n\n if (response.status === 'cancelled' || response.closeModal !== false) {\n this.closeModal(action.name)\n }\n\n if (response.status === 'success') {\n this.options.onSuccess?.(action, context)\n\n if (response.redirect) {\n const { router } = await import('@inertiajs/core')\n router.visit(response.redirect)\n }\n }\n\n return response.status === 'success' ? response.result : undefined\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.setErrors(\n action.name,\n Object.fromEntries(\n Object.entries(error.errors).map(([field, messages]) => [field, messages[0] ?? '']),\n ),\n )\n } else {\n this.options.onError?.(error)\n }\n\n return undefined\n } finally {\n this.running.delete(key)\n this.setProcessing(action.name, false)\n }\n }\n\n private setProcessing(name: string, processing: boolean): void {\n this.store.set((state) => ({\n ...state,\n modals: state.modals.map((modal) =>\n modal.action.name === name\n ? { ...modal, processing, errors: processing ? {} : modal.errors }\n : modal,\n ),\n }))\n }\n\n private setErrors(name: string, errors: Record<string, string>): void {\n const normalized: Record<string, string[]> = {}\n\n for (const [key, message] of Object.entries(errors)) {\n normalized[key] = [message]\n }\n\n this.store.set((state) => ({\n ...state,\n modals: state.modals.map((modal) =>\n modal.action.name === name ? { ...modal, errors: normalized } : modal,\n ),\n }))\n }\n}\n\n/**\n * Whether an action button should bind its key bindings globally. Record actions repeat\n * on every row, so a page-wide shortcut would run them once per record; they only bind\n * when rendered outside a record (header, page and bulk actions).\n */\nexport function bindsKeyBindings(\n action: Pick<ActionSchema, 'keyBindings' | 'disabled'>,\n context: Pick<ActionContext, 'record'>,\n): boolean {\n return Boolean(action.keyBindings?.length) && !action.disabled && context.record == null\n}\n\n/** Button sizes every renderer's shadcn `Button` understands. */\nexport type ActionButtonSize = 'xs' | 'sm' | 'default' | 'lg'\n\n/**\n * The button size for an action: its own `size()` wins over the size the surrounding\n * list asks for (`md` is the default button, `xl` the largest one).\n */\nexport function resolveActionSize(\n size: Size | null | undefined,\n fallback: ActionButtonSize = 'default',\n): ActionButtonSize {\n switch (size) {\n case 'xs':\n return 'xs'\n case 'sm':\n return 'sm'\n case 'md':\n return 'default'\n case 'lg':\n case 'xl':\n return 'lg'\n default:\n return fallback\n }\n}\n\n/** The square icon-only button that matches a button size. */\nexport function iconButtonSize(size: ActionButtonSize): 'icon-xs' | 'icon-sm' | 'icon' {\n return size === 'xs' ? 'icon-xs' : size === 'sm' ? 'icon-sm' : 'icon'\n}\n\nexport interface DropdownPlacement {\n side: 'top' | 'right' | 'bottom' | 'left'\n align: 'start' | 'center' | 'end'\n}\n\n/**\n * Parse an ActionGroup `dropdownPlacement()` (`bottom-start`, `top-end`, `left`, …) into the\n * side/align pair dropdown primitives take. Defaults to `bottom` / `end`.\n */\nexport function parseDropdownPlacement(placement: string | null | undefined): DropdownPlacement {\n const [side, align] = (placement ?? '').split('-')\n const sides = ['top', 'right', 'bottom', 'left'] as const\n const aligns = ['start', 'center', 'end'] as const\n\n const knownSide = sides.find((candidate) => candidate === side)\n const resolvedAlign =\n aligns.find((candidate) => candidate === align) ?? (knownSide && !align ? 'center' : 'end')\n\n return { side: knownSide ?? 'bottom', align: resolvedAlign }\n}\n\n/** Every action inside a group, nested groups flattened. */\nexport function flattenActionGroup(actions: AnyActionSchema[]): ActionSchema[] {\n return actions.flatMap((action) =>\n isActionGroup(action) ? flattenActionGroup(action.actions) : [action],\n )\n}\n\n/**\n * Whether an open modal is hidden because a nested modal was opened from it. Nested modals\n * hide their parents unless they opt into `overlayParentActions()`; a hidden parent shows\n * again when the child closes.\n */\nexport function isModalHiddenByChild(modals: OpenModal[], index: number): boolean {\n const modal = modals[index]\n\n if (!modal) {\n return false\n }\n\n const path = actionPath(modal.action, modal.context)\n\n return modals.slice(index + 1).some((child) => {\n if (child.modal.overlayParent === true) return false\n\n const parents = child.context.parents ?? []\n\n return parents.some(\n (_, position) => parents.slice(0, position + 1).join(ACTION_PATH_SEPARATOR) === path,\n )\n })\n}\n\n/** Justify class keyword for modal footer buttons: `footerActionsAlignment`, else `alignment`. */\nexport function modalFooterAlignment(\n modal: Pick<ActionModalSchema, 'alignment' | 'footerActionsAlignment'>,\n): 'start' | 'center' | 'end' | 'between' {\n const alignment = modal.footerActionsAlignment ?? modal.alignment\n\n switch (alignment) {\n case 'start':\n case 'center':\n case 'between':\n return alignment\n default:\n return 'end'\n }\n}\n\n/**\n * The physical side a slide-over enters from: `end` (default) is the right edge in\n * left-to-right documents and the left edge in right-to-left ones.\n */\nexport function slideOverSide(\n position: 'start' | 'end' | null | undefined,\n direction: 'ltr' | 'rtl' = 'ltr',\n): 'left' | 'right' {\n const atStart = position === 'start'\n\n return atStart === (direction === 'rtl') ? 'right' : 'left'\n}\n\nfunction isCancelledModal(\n modal: ActionModalSchema | ActionModalCancelledResponse,\n): modal is ActionModalCancelledResponse {\n return (modal as Partial<ActionModalCancelledResponse>).cancelled === true\n}\n\n/**\n * Whether closing a modal has to ask first: its form changed and the modal has\n * `unsavedChangesAlert` (the action's own setting or the panel's `unsavedChangesAlerts()`).\n */\nexport function shouldConfirmModalClose(\n modal: Pick<ActionModalSchema, 'unsavedChangesAlert'>,\n form: { isDirty: boolean } | null | undefined,\n): boolean {\n return Boolean(modal.unsavedChangesAlert && form?.isDirty)\n}\n\n/**\n * Ask whether to close a modal with unsaved changes (`window.confirm`); true when it may\n * close. Without changes (or the alert) it never asks.\n */\nexport function confirmModalClose(\n modal: Pick<ActionModalSchema, 'unsavedChangesAlert'>,\n form: { isDirty: boolean } | null | undefined,\n message: string,\n): boolean {\n if (!shouldConfirmModalClose(modal, form) || typeof window === 'undefined') {\n return true\n }\n\n return window.confirm(message)\n}\n\n/**\n * Warn before leaving the page (a reload, or an Inertia visit when `message` is given)\n * while a modal form with `unsavedChangesAlert` has unsaved changes. Returns a function\n * that removes the listeners.\n */\nexport function guardModalUnload(\n modal: Pick<ActionModalSchema, 'unsavedChangesAlert'>,\n form: { isDirty: boolean } | null | undefined,\n message?: string,\n): () => void {\n if (!modal.unsavedChangesAlert || !form || typeof window === 'undefined') {\n return () => {}\n }\n\n const listener = (event: BeforeUnloadEvent) => {\n if (form.isDirty) {\n event.preventDefault()\n event.returnValue = ''\n }\n }\n\n window.addEventListener('beforeunload', listener)\n\n const removeVisitGuard = message\n ? router.on('before', (event) => {\n if (form.isDirty && isLeavingVisit(event.detail.visit) && !window.confirm(message)) {\n event.preventDefault()\n }\n })\n : () => {}\n\n return () => {\n window.removeEventListener('beforeunload', listener)\n removeVisitGuard()\n }\n}\n\n/** The context of a user or tenant menu action: the page's token and the menu's component. */\nexport function menuActionContext(token: string, menu: 'userMenu' | 'tenantMenu'): ActionContext {\n return { token, component: menu }\n}\n\n/**\n * Split menu entries into their groups (`group` index, nested arrays in\n * `userMenuItems()`), in order; renderers draw a separator between groups.\n */\nexport function groupMenuItems<T extends { group?: number }>(items: T[]): T[][] {\n const groups: T[][] = []\n let current: number | null = null\n\n for (const item of items) {\n const group = item.group ?? 0\n\n if (current === null || group !== current) {\n groups.push([])\n current = group\n }\n\n groups[groups.length - 1]!.push(item)\n }\n\n return groups\n}\n\n/** Whether this button's run (this action for this record) is fetching its modal. */\nexport function isActionLoading(\n state: ActionRunnerState,\n action: ActionSchema,\n context: ActionContext,\n): boolean {\n return state.loadingKey === runKey(action, context)\n}\n\n/** Whether this button's run (this action for this record) has a modal that is submitting. */\nexport function isActionProcessing(\n state: ActionRunnerState,\n action: ActionSchema,\n context: ActionContext,\n): boolean {\n const key = runKey(action, context)\n\n return state.modals.some(\n (modal) => modal.processing && runKey(modal.action, modal.context) === key,\n )\n}\n\nfunction runKey(action: Pick<ActionSchema, 'name'>, context: ActionContext): string {\n return [context.component ?? 'page', actionPath(action, context), context.record ?? ''].join(\n '\\u0000',\n )\n}\n\n/**\n * The action endpoint for a run. \"All records\" bulk actions carry the page's query string\n * (filters, search, list tab), so the server selects exactly the records the table shows.\n */\nexport function actionEndpoint(\n endpoint: string,\n context: Pick<ActionContext, 'allRecords'>,\n search: string = typeof window !== 'undefined' ? window.location.search : '',\n): string {\n if (!context.allRecords || search.length <= 1) {\n return endpoint\n }\n\n return endpoint + (endpoint.includes('?') ? '&' : '?') + search.replace(/^\\?/, '')\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-live-preview.d.ts","names":[],"sources":["../src/cms-live-preview.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"cms-live-preview.d.ts","names":[],"sources":["../src/cms-live-preview.ts"],"mappings":";;;;;;;;YAWY;iBAEK;EACf,QAAQ;EACR;EACA;;EAEA;;EAEA;;iBAGe;;EAEf,OAAO,OAAO,4BAA4B,QAAQ;EAClD;EACA;EACA;EACA;EACA,YAAY,UAAU;;EAEtB;;;qBAIW;;;;;YAMD,gCAAgC;qBAE/B;mBAOkB;UANrB;UACA;UACA;UACA;UACA;EAEqB,YAAA,SAAS;MAUlC,YAAY;;EAKhB,SAAS,OAAO;;EAWV,SAAS;EAyCf;UAMQ;;;;;;;;wBAYM,uBAAuB,MAAM,gBAAgB;;;;;;wBA0B7C,yBACd,2BACA,gBACA;;wBASc;;;;;;;;wBAeA,qBAAqB,MAAM,gBAAgB;;;;;;;;wBAkB3C,sBAAsB,MAAM,gBAAgB;;qBAe/C;;wBAGG,qBAAqB;;wBAYf,gBACpB,gBACA,OAAO,yBACP;EAAW;EAAe;IACzB,QAAQ"}
|
package/dist/cms-live-preview.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentRecordVersion, setRecordVersion } from "./conflicts.js";
|
|
1
2
|
import { callPageMethod } from "./page-methods.js";
|
|
2
3
|
//#region src/cms-live-preview.ts
|
|
3
4
|
/** Preview widths: `null` fills the pane. */
|
|
@@ -152,7 +153,12 @@ function rememberedCmsPreview(value) {
|
|
|
152
153
|
}
|
|
153
154
|
/** Call the editor page's autosave method with the form state. */
|
|
154
155
|
async function callCmsAutosave(method, state, options) {
|
|
155
|
-
|
|
156
|
+
const response = await callPageMethod(method, {
|
|
157
|
+
state,
|
|
158
|
+
recordVersion: currentRecordVersion()
|
|
159
|
+
}, options);
|
|
160
|
+
if (response.result.recordVersion) setRecordVersion(response.result.recordVersion);
|
|
161
|
+
return response.result;
|
|
156
162
|
}
|
|
157
163
|
//#endregion
|
|
158
164
|
export { CMS_PREVIEW_DEVICES, CMS_PREVIEW_STORAGE_KEY, CmsAutosave, callCmsAutosave, cmsPreviewFitsBeside, cmsPreviewOpensByDefault, cmsPreviewOpensOnLoad, cmsPreviewPaneWidth, rememberedCmsPreview, reserveCmsPreviewSpace };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-live-preview.js","names":[],"sources":["../src/cms-live-preview.ts"],"sourcesContent":["import { callPageMethod } from './page-methods'\nimport type { CmsAutosaveResult } from './schema/cms'\n\n/**\n * The framework-agnostic half of the page editor's live preview (alfasix/alfakit-cms):\n * debounced autosaves of the form state through the page's exposed `cmsAutosave` method,\n * and the preview URL and reload counter the renderers bind an iframe to. The server\n * stores the draft and signs the preview; nothing here renders or validates content.\n */\n\nexport type CmsAutosaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'\n\nexport interface CmsAutosaveSnapshot {\n status: CmsAutosaveStatus\n savedAt: string | null\n previewUrl: string | null\n /** Bumped after every successful save: remount the preview iframe on change. */\n version: number\n /** The saved draft differs from what visitors see (`hasPendingChanges()`). */\n hasChanges: boolean\n}\n\nexport interface CmsAutosaveOptions {\n /** Send the state (the editor page's `cmsAutosave` method). */\n save: (state: Record<string, unknown>) => Promise<CmsAutosaveResult>\n debounce: number\n previewUrl: string | null\n savedAt?: string | null\n hasChanges?: boolean\n onChange?: (snapshot: CmsAutosaveSnapshot) => void\n /** Retry delay after a failed save (milliseconds). */\n retryAfter?: number\n}\n\n/** Preview widths: `null` fills the pane. */\nexport const CMS_PREVIEW_DEVICES = {\n desktop: null,\n tablet: 768,\n phone: 390,\n} as const\n\nexport type CmsPreviewDevice = keyof typeof CMS_PREVIEW_DEVICES\n\nexport class CmsAutosave {\n private timer: ReturnType<typeof setTimeout> | null = null\n private pending: Record<string, unknown> | null = null\n private running = false\n private disposed = false\n private current: CmsAutosaveSnapshot\n\n constructor(private readonly options: CmsAutosaveOptions) {\n this.current = {\n status: options.savedAt ? 'saved' : 'idle',\n savedAt: options.savedAt ?? null,\n previewUrl: options.previewUrl,\n version: 0,\n hasChanges: options.hasChanges ?? false,\n }\n }\n\n get snapshot(): CmsAutosaveSnapshot {\n return this.current\n }\n\n /** The form changed: save its state once it stays unchanged for `debounce` ms. */\n schedule(state: Record<string, unknown>): void {\n if (this.disposed) return\n\n this.pending = state\n this.update({ status: 'pending' })\n\n if (this.timer) clearTimeout(this.timer)\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n\n /** Save the latest state now (when one is waiting). */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n\n if (this.disposed || this.running || this.pending === null) return\n\n const state = this.pending\n this.pending = null\n this.running = true\n this.update({ status: 'saving' })\n\n try {\n const result = await this.options.save(state)\n\n if (this.disposed) return\n\n this.update({\n status: this.pending === null ? 'saved' : 'pending',\n savedAt: result.savedAt,\n previewUrl: result.previewUrl ?? this.current.previewUrl,\n version: this.current.version + 1,\n hasChanges: result.hasChanges,\n })\n } catch {\n if (this.disposed) return\n\n // Keep the newest state and try again later.\n this.pending ??= state\n this.update({ status: 'error' })\n this.timer = setTimeout(() => void this.flush(), this.options.retryAfter ?? 5000)\n } finally {\n this.running = false\n }\n\n if (this.pending !== null && this.timer === null) {\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n }\n\n dispose(): void {\n this.disposed = true\n\n if (this.timer) clearTimeout(this.timer)\n }\n\n private update(patch: Partial<CmsAutosaveSnapshot>): void {\n this.current = { ...this.current, ...patch }\n this.options.onChange?.(this.current)\n }\n}\n\n/**\n * While the side-by-side preview is open, make room for it: the panel's content area\n * (`[data-slot=\"content\"]`) keeps clear of the pane with an inline end margin of the pane's\n * width (it may be centred with a maximum width, so padding would only squeeze the form),\n * restored when it closes. Returns the function that restores it.\n */\nexport function reserveCmsPreviewSpace(from: Element | null, width: string): () => void {\n const content = from?.closest<HTMLElement>('[data-slot=\"content\"]')\n\n if (\n !content ||\n typeof window === 'undefined' ||\n !window.matchMedia?.('(min-width: 1024px)').matches\n ) {\n return () => {}\n }\n\n const previous = { margin: content.style.marginInlineEnd, width: content.style.width }\n content.style.marginInlineEnd = width\n content.style.width = 'auto'\n\n return () => {\n content.style.marginInlineEnd = previous.margin\n content.style.width = previous.width\n }\n}\n\n/**\n * Whether the side-by-side preview should open on its own: a wide enough screen for the\n * form and the pane (`minWidth`, 1536px by default), unless this viewer closed it last\n * time (`remembered === 'closed'`).\n */\nexport function cmsPreviewOpensByDefault(\n remembered: string | null,\n width: number = typeof window === 'undefined' ? 0 : window.innerWidth,\n minWidth = 1536,\n): boolean {\n if (remembered === 'open') return width >= 1024\n if (remembered === 'closed') return false\n\n return width >= minWidth\n}\n\n/** Width of the side-by-side pane: half the screen, at most 56rem (matches `lg:w-[min(50vw,56rem)]`). */\nexport function cmsPreviewPaneWidth(): number {\n if (typeof window === 'undefined') return 0\n\n const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16\n\n return Math.min(window.innerWidth / 2, 56 * rem)\n}\n\n/**\n * Whether the preview fits beside the form: at least the `lg` breakpoint, and the column\n * holding the panel's content area (the parent of `[data-slot=\"content\"]` around `from`,\n * i.e. the screen minus the sidebar) keeps `minFormWidth` pixels (640) for the form next to\n * the pane. When it doesn't, renderers open the preview as a full-screen overlay instead, so\n * the form never runs under the pane.\n */\nexport function cmsPreviewFitsBeside(from: Element | null, minFormWidth = 640): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width = column?.clientWidth || window.innerWidth\n\n return width - cmsPreviewPaneWidth() >= minFormWidth\n}\n\n/**\n * Whether the side-by-side preview opens when the editor loads: never below the `lg`\n * breakpoint (phones and tablets keep it closed) or when it wouldn't fit beside the form\n * (`cmsPreviewFitsBeside()`); when this viewer opened or closed it last time, their choice;\n * otherwise when the column holding the panel's content area (the parent of\n * `[data-slot=\"content\"]` around `from`) is at least `minWidth` wide (1440px).\n */\nexport function cmsPreviewOpensOnLoad(from: Element | null, minWidth = 1440): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n if (!cmsPreviewFitsBeside(from)) return false\n\n const remembered = rememberedCmsPreview()\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width =\n remembered === 'open' ? window.innerWidth : (column?.clientWidth ?? window.innerWidth)\n\n return cmsPreviewOpensByDefault(remembered, width, minWidth)\n}\n/** The storage key remembering whether the viewer keeps the preview open. */\nexport const CMS_PREVIEW_STORAGE_KEY = 'alfakit:cms-live-preview'\n\n/** Read / write the viewer's preview choice; storage may be unavailable. */\nexport function rememberedCmsPreview(value?: 'open' | 'closed'): string | null {\n try {\n if (typeof localStorage === 'undefined') return null\n if (value) localStorage.setItem(CMS_PREVIEW_STORAGE_KEY, value)\n\n return localStorage.getItem(CMS_PREVIEW_STORAGE_KEY)\n } catch {\n return null\n }\n}\n\n/** Call the editor page's autosave method with the form state. */\nexport async function callCmsAutosave(\n method: string,\n state: Record<string, unknown>,\n options: { token: string; endpoint: string },\n): Promise<CmsAutosaveResult> {\n const response = await callPageMethod<CmsAutosaveResult>(method, { state }, options)\n\n return response.result\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cms-live-preview.js","names":[],"sources":["../src/cms-live-preview.ts"],"sourcesContent":["import { currentRecordVersion, setRecordVersion } from './conflicts'\nimport { callPageMethod } from './page-methods'\nimport type { CmsAutosaveResult } from './schema/cms'\n\n/**\n * The framework-agnostic half of the page editor's live preview (alfasix/alfakit-cms):\n * debounced autosaves of the form state through the page's exposed `cmsAutosave` method,\n * and the preview URL and reload counter the renderers bind an iframe to. The server\n * stores the draft and signs the preview; nothing here renders or validates content.\n */\n\nexport type CmsAutosaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'\n\nexport interface CmsAutosaveSnapshot {\n status: CmsAutosaveStatus\n savedAt: string | null\n previewUrl: string | null\n /** Bumped after every successful save: remount the preview iframe on change. */\n version: number\n /** The saved draft differs from what visitors see (`hasPendingChanges()`). */\n hasChanges: boolean\n}\n\nexport interface CmsAutosaveOptions {\n /** Send the state (the editor page's `cmsAutosave` method). */\n save: (state: Record<string, unknown>) => Promise<CmsAutosaveResult>\n debounce: number\n previewUrl: string | null\n savedAt?: string | null\n hasChanges?: boolean\n onChange?: (snapshot: CmsAutosaveSnapshot) => void\n /** Retry delay after a failed save (milliseconds). */\n retryAfter?: number\n}\n\n/** Preview widths: `null` fills the pane. */\nexport const CMS_PREVIEW_DEVICES = {\n desktop: null,\n tablet: 768,\n phone: 390,\n} as const\n\nexport type CmsPreviewDevice = keyof typeof CMS_PREVIEW_DEVICES\n\nexport class CmsAutosave {\n private timer: ReturnType<typeof setTimeout> | null = null\n private pending: Record<string, unknown> | null = null\n private running = false\n private disposed = false\n private current: CmsAutosaveSnapshot\n\n constructor(private readonly options: CmsAutosaveOptions) {\n this.current = {\n status: options.savedAt ? 'saved' : 'idle',\n savedAt: options.savedAt ?? null,\n previewUrl: options.previewUrl,\n version: 0,\n hasChanges: options.hasChanges ?? false,\n }\n }\n\n get snapshot(): CmsAutosaveSnapshot {\n return this.current\n }\n\n /** The form changed: save its state once it stays unchanged for `debounce` ms. */\n schedule(state: Record<string, unknown>): void {\n if (this.disposed) return\n\n this.pending = state\n this.update({ status: 'pending' })\n\n if (this.timer) clearTimeout(this.timer)\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n\n /** Save the latest state now (when one is waiting). */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n\n if (this.disposed || this.running || this.pending === null) return\n\n const state = this.pending\n this.pending = null\n this.running = true\n this.update({ status: 'saving' })\n\n try {\n const result = await this.options.save(state)\n\n if (this.disposed) return\n\n this.update({\n status: this.pending === null ? 'saved' : 'pending',\n savedAt: result.savedAt,\n previewUrl: result.previewUrl ?? this.current.previewUrl,\n version: this.current.version + 1,\n hasChanges: result.hasChanges,\n })\n } catch {\n if (this.disposed) return\n\n // Keep the newest state and try again later.\n this.pending ??= state\n this.update({ status: 'error' })\n this.timer = setTimeout(() => void this.flush(), this.options.retryAfter ?? 5000)\n } finally {\n this.running = false\n }\n\n if (this.pending !== null && this.timer === null) {\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n }\n\n dispose(): void {\n this.disposed = true\n\n if (this.timer) clearTimeout(this.timer)\n }\n\n private update(patch: Partial<CmsAutosaveSnapshot>): void {\n this.current = { ...this.current, ...patch }\n this.options.onChange?.(this.current)\n }\n}\n\n/**\n * While the side-by-side preview is open, make room for it: the panel's content area\n * (`[data-slot=\"content\"]`) keeps clear of the pane with an inline end margin of the pane's\n * width (it may be centred with a maximum width, so padding would only squeeze the form),\n * restored when it closes. Returns the function that restores it.\n */\nexport function reserveCmsPreviewSpace(from: Element | null, width: string): () => void {\n const content = from?.closest<HTMLElement>('[data-slot=\"content\"]')\n\n if (\n !content ||\n typeof window === 'undefined' ||\n !window.matchMedia?.('(min-width: 1024px)').matches\n ) {\n return () => {}\n }\n\n const previous = { margin: content.style.marginInlineEnd, width: content.style.width }\n content.style.marginInlineEnd = width\n content.style.width = 'auto'\n\n return () => {\n content.style.marginInlineEnd = previous.margin\n content.style.width = previous.width\n }\n}\n\n/**\n * Whether the side-by-side preview should open on its own: a wide enough screen for the\n * form and the pane (`minWidth`, 1536px by default), unless this viewer closed it last\n * time (`remembered === 'closed'`).\n */\nexport function cmsPreviewOpensByDefault(\n remembered: string | null,\n width: number = typeof window === 'undefined' ? 0 : window.innerWidth,\n minWidth = 1536,\n): boolean {\n if (remembered === 'open') return width >= 1024\n if (remembered === 'closed') return false\n\n return width >= minWidth\n}\n\n/** Width of the side-by-side pane: half the screen, at most 56rem (matches `lg:w-[min(50vw,56rem)]`). */\nexport function cmsPreviewPaneWidth(): number {\n if (typeof window === 'undefined') return 0\n\n const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16\n\n return Math.min(window.innerWidth / 2, 56 * rem)\n}\n\n/**\n * Whether the preview fits beside the form: at least the `lg` breakpoint, and the column\n * holding the panel's content area (the parent of `[data-slot=\"content\"]` around `from`,\n * i.e. the screen minus the sidebar) keeps `minFormWidth` pixels (640) for the form next to\n * the pane. When it doesn't, renderers open the preview as a full-screen overlay instead, so\n * the form never runs under the pane.\n */\nexport function cmsPreviewFitsBeside(from: Element | null, minFormWidth = 640): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width = column?.clientWidth || window.innerWidth\n\n return width - cmsPreviewPaneWidth() >= minFormWidth\n}\n\n/**\n * Whether the side-by-side preview opens when the editor loads: never below the `lg`\n * breakpoint (phones and tablets keep it closed) or when it wouldn't fit beside the form\n * (`cmsPreviewFitsBeside()`); when this viewer opened or closed it last time, their choice;\n * otherwise when the column holding the panel's content area (the parent of\n * `[data-slot=\"content\"]` around `from`) is at least `minWidth` wide (1440px).\n */\nexport function cmsPreviewOpensOnLoad(from: Element | null, minWidth = 1440): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n if (!cmsPreviewFitsBeside(from)) return false\n\n const remembered = rememberedCmsPreview()\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width =\n remembered === 'open' ? window.innerWidth : (column?.clientWidth ?? window.innerWidth)\n\n return cmsPreviewOpensByDefault(remembered, width, minWidth)\n}\n/** The storage key remembering whether the viewer keeps the preview open. */\nexport const CMS_PREVIEW_STORAGE_KEY = 'alfakit:cms-live-preview'\n\n/** Read / write the viewer's preview choice; storage may be unavailable. */\nexport function rememberedCmsPreview(value?: 'open' | 'closed'): string | null {\n try {\n if (typeof localStorage === 'undefined') return null\n if (value) localStorage.setItem(CMS_PREVIEW_STORAGE_KEY, value)\n\n return localStorage.getItem(CMS_PREVIEW_STORAGE_KEY)\n } catch {\n return null\n }\n}\n\n/** Call the editor page's autosave method with the form state. */\nexport async function callCmsAutosave(\n method: string,\n state: Record<string, unknown>,\n options: { token: string; endpoint: string },\n): Promise<CmsAutosaveResult> {\n const response = await callPageMethod<CmsAutosaveResult>(\n method,\n { state, recordVersion: currentRecordVersion() },\n options,\n )\n\n // The draft changed the record: the next save sends the new version, so it is not\n // refused as a conflict with this tab's own autosave.\n if (response.result.recordVersion) setRecordVersion(response.result.recordVersion)\n\n return response.result\n}\n"],"mappings":"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"}
|
package/dist/file-size.js
CHANGED
package/dist/file-size.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-size.js","names":[],"sources":["../src/file-size.ts"],"sourcesContent":["import type { LocaleFormatter } from './i18n'\n\nconst SIZE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'] as const\n\n/** A human file size (`1.4 MB`) in the formatter's locale. Kept apart from file-manager.ts so pickers don't pull the whole controller. */\nexport function fileManagerSize(\n bytes: number | null | undefined,\n formatter: Pick<LocaleFormatter, 'number'>,\n): string {\n let value = typeof bytes === 'number' && Number.isFinite(bytes) && bytes > 0 ? bytes : 0\n let unit = 0\n\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024\n unit++\n }\n\n return formatter.number(value, {\n style: 'unit',\n unit: SIZE_UNITS[unit],\n unitDisplay: 'short',\n maximumFractionDigits: unit === 0 ? 0 : 1,\n })\n}\n"],"mappings":";AAEA,MAAM,aAAa;CAAC;CAAQ;CAAY;CAAY;CAAY;AAAU;;AAG1E,SAAgB,gBACd,OACA,WACQ;CACR,IAAI,QAAQ,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ;CACvF,IAAI,OAAO;CAEX,OAAO,SAAS,QAAQ,OAAO,WAAW,SAAS,GAAG;EACpD,SAAS;EACT;CACF;CAEA,OAAO,UAAU,OAAO,OAAO;EAC7B,OAAO;EACP,MAAM,WAAW;
|
|
1
|
+
{"version":3,"file":"file-size.js","names":[],"sources":["../src/file-size.ts"],"sourcesContent":["import type { LocaleFormatter } from './i18n'\n\nconst SIZE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'] as const\n\n/** A human file size (`1.4 MB`) in the formatter's locale. Kept apart from file-manager.ts so pickers don't pull the whole controller. */\nexport function fileManagerSize(\n bytes: number | null | undefined,\n formatter: Pick<LocaleFormatter, 'number'>,\n): string {\n let value = typeof bytes === 'number' && Number.isFinite(bytes) && bytes > 0 ? bytes : 0\n let unit = 0\n\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024\n unit++\n }\n\n return formatter.number(value, {\n style: 'unit',\n unit: SIZE_UNITS[unit],\n // Bytes have no short form in most locales (\"11 byte\"): the long one is pluralised.\n unitDisplay: unit === 0 ? 'long' : 'short',\n maximumFractionDigits: unit === 0 ? 0 : 1,\n })\n}\n"],"mappings":";AAEA,MAAM,aAAa;CAAC;CAAQ;CAAY;CAAY;CAAY;AAAU;;AAG1E,SAAgB,gBACd,OACA,WACQ;CACR,IAAI,QAAQ,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ;CACvF,IAAI,OAAO;CAEX,OAAO,SAAS,QAAQ,OAAO,WAAW,SAAS,GAAG;EACpD,SAAS;EACT;CACF;CAEA,OAAO,UAAU,OAAO,OAAO;EAC7B,OAAO;EACP,MAAM,WAAW;EAEjB,aAAa,SAAS,IAAI,SAAS;EACnC,uBAAuB,SAAS,IAAI,IAAI;CAC1C,CAAC;AACH"}
|
package/dist/form.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NotificationSchema } from "./schema/notifications.js";
|
|
2
2
|
import { FormPayload, SchemaRefreshResponse } from "./schema/form.js";
|
|
3
3
|
import { IconPickerResponse } from "./schema/icon-picker.js";
|
|
4
|
-
import { FieldSchema, SchemaComponent } from "./schema/components.js";
|
|
4
|
+
import { FieldSchema, SchemaComponent, TabSchema } from "./schema/components.js";
|
|
5
5
|
import { ModalTableSelectResponse } from "./schema/advanced-fields.js";
|
|
6
6
|
import { Store } from "./store.js";
|
|
7
7
|
import { TableQuery } from "./table.js";
|
|
@@ -40,7 +40,16 @@ export interface FormStoreOptions {
|
|
|
40
40
|
onNotifications?: (notifications: NotificationSchema[]) => void;
|
|
41
41
|
/** Called after a live refresh was merged (e.g. to reload what depends on the form). */
|
|
42
42
|
onRefreshed?: (path: string | undefined) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Prefix of the fields' DOM ids (`{idPrefix}-{statePath}`), default `alfakit`. Forms shown
|
|
45
|
+
* over another form (action modals) use their own, so ids never repeat on the page.
|
|
46
|
+
*/
|
|
47
|
+
idPrefix?: string;
|
|
43
48
|
}
|
|
49
|
+
/** A DOM-safe id prefix for a form, e.g. `action:field.category_id.createOption` -> `alfakit-action-field-category_id-createOption`. */
|
|
50
|
+
export declare function formIdPrefix(scope: string): string;
|
|
51
|
+
/** The DOM id of a field: `{prefix}-{statePath}` with unsafe characters replaced. */
|
|
52
|
+
export declare function fieldDomId(prefix: string, statePath: string): string;
|
|
44
53
|
/**
|
|
45
54
|
* Walk a component tree and collect every field (including those inside Repeater items).
|
|
46
55
|
*/
|
|
@@ -59,6 +68,27 @@ export declare function findFieldByPath(components: SchemaComponent[], path: str
|
|
|
59
68
|
* still move between steps while errors are shown.
|
|
60
69
|
*/
|
|
61
70
|
export declare function wizardStepToReveal(steps: SchemaComponent[], previousErrors: Record<string, unknown>, errors: Record<string, unknown>): number | null;
|
|
71
|
+
/**
|
|
72
|
+
* Whether a layout component (a tab, a section) holds a field with a validation error:
|
|
73
|
+
* one of its fields, a nested repeater/builder item field (`items.0.sku`), or, while the
|
|
74
|
+
* component is deferred and its fields aren't loaded, one of its `deferredPaths`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function componentHasErrors(component: Pick<SchemaComponent, 'schema' | 'deferred' | 'deferredPaths'>, errors: Record<string, unknown>): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The value a tab is written to the query string as (`persistTabInQueryString()`): its
|
|
79
|
+
* readable `queryValue` (`billing-address`), else its key.
|
|
80
|
+
*/
|
|
81
|
+
export declare function tabQueryValue(tab: Pick<TabSchema, 'key' | 'queryValue'>): string;
|
|
82
|
+
/**
|
|
83
|
+
* The key of the tab a query-string value names: a tab's `queryValue`, or its key (links
|
|
84
|
+
* written before tabs had readable values, `?tab=tabs-0.tab-1`). Null when none matches.
|
|
85
|
+
*/
|
|
86
|
+
export declare function tabFromQuery(tabs: Array<Pick<TabSchema, 'key' | 'queryValue'>>, value: string | null | undefined): string | null;
|
|
87
|
+
/**
|
|
88
|
+
* The tab to show after the errors change: when new error keys arrived (a submit or
|
|
89
|
+
* validation response) and the active tab has none, the first tab with an error.
|
|
90
|
+
*/
|
|
91
|
+
export declare function tabToReveal(tabs: Array<Pick<SchemaComponent, 'key' | 'schema' | 'deferred' | 'deferredPaths'>>, activeKey: string, previousErrors: Record<string, unknown>, errors: Record<string, unknown>): string | null;
|
|
62
92
|
/**
|
|
63
93
|
* Framework-agnostic form state: values addressed by `statePath`, validation errors,
|
|
64
94
|
* dirty tracking and server round-trips for `live()` fields.
|
|
@@ -90,6 +120,8 @@ export declare class FormStore {
|
|
|
90
120
|
setToken(token: string): void;
|
|
91
121
|
/** Context component name of the schema (`form`, `action:table.edit`, …). */
|
|
92
122
|
get component(): string;
|
|
123
|
+
/** Prefix of the fields' DOM ids (see `FormStoreOptions.idPrefix`). */
|
|
124
|
+
get idPrefix(): string;
|
|
93
125
|
get record(): string | number | null;
|
|
94
126
|
get token(): string;
|
|
95
127
|
get optionsUrl(): string | undefined;
|
package/dist/form.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.d.ts","names":[],"sources":["../src/form.ts"],"mappings":";;;;;;;;iBAqBiB;EACf,YAAY;EACZ,QAAQ;EACR,QAAQ;;EAER,OAAO;;EAEP;EACA;EACA,SAAS;;iBAGM;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,eAAe;EACf,WAAW;;EAEX,mBAAmB,eAAe;;EAElC,eAAe;;;;;
|
|
1
|
+
{"version":3,"file":"form.d.ts","names":[],"sources":["../src/form.ts"],"mappings":";;;;;;;;iBAqBiB;EACf,YAAY;EACZ,QAAQ;EACR,QAAQ;;EAER,OAAO;;EAEP;EACA;EACA,SAAS;;iBAGM;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,eAAe;EACf,WAAW;;EAEX,mBAAmB,eAAe;;EAElC,eAAe;;;;;EAKf;;;wBAIc,aAAa;;wBAKb,WAAW,gBAAgB;;;;wBAO3B,cACd,YAAY,mBACZ,OAAM,gBACL;;;;;;;;wBA2Ba,gBACd,YAAY,mBACZ,cACA,SAAQ,0BACP;;;;;;wBAmEa,mBACd,OAAO,mBACP,gBAAgB,yBAChB,QAAQ;;;;;;wBAkBM,mBACd,WAAW,KAAK,2DAChB,QAAQ;;;;;wBAkBM,cAAc,KAAK,KAAK;;;;;wBAQxB,aACd,MAAM,MAAM,KAAK,mCACjB;;;;;wBAcc,YACd,MAAM,MAAM,KAAK,oEACjB,mBACA,gBAAgB,yBAChB,QAAQ;;;;;qBAiBG;UAyBD;WAxBM,OAAO,MAAM;mBAEZ;UAET;;;;;UAMA;;;;;;UAOA;;UAGA;EAGN,YAAA,SAAS,aACD,SAAS;MAkBf,SAAS;;;;;EAQb,SAAS;;MAKL;;MAKA;MAIA;MAIA;MAIA;MAIA;;MAKA,uBAAuB;;EAK3B,kBAAkB;;;;;;EAclB,gBAAgB;EAahB,UAAU,WAAW,OAAO;EAM5B,SAAS,aAAa,cAAc,WAAW,IAAI;;;;;EAQnD,SAAS,cAAc,gBAAgB;IAAW;;;EA+BlD,KAAK;;EASL,KAAK,SAAS,aAAa;IAAW;;EAYtC,UAAU,QAAQ;EAUlB;EAIA,SAAS;EAIT,cAAc;MAIV;EAIJ,UAAU,eAAe;UAMjB;;;;;EAiBF,QAAQ,gBAAgB;;;;;;;EAoE9B,cACE,UAAU,uBACV,MAAM,yBACN,QAAO;;;;;;EAqCH,aAAa,iBAAiB,uBAAuB;;EA8CrD,cACJ,cACA,iBACC,QAAQ;IAAQ;IAAe;;EAe5B,aACJ,cACA,QAAQ,yBACP,QAAQ;;EAgBL,YACJ,cACA;IAAS;IAAiB;IAAqB;KAC/C,SAAS,cACR,QAAQ;;;;;EAqBL,WACJ,cACA,oBACA,OAAO,YACP,SAAS,cACR,QAAQ;EAgBX;;;;;;;;wBAkCc,aACd;EAAQ;EAAgB;sBACxB;EAAS;EAAa;EAAmB;;;iBAY1B;EACf;EACA,KAAK;EACL;EACA;EACA;;;;;;;;wBASc,eAAe,OAAO,cAAc;iBAuBnC;EACf;EACA,OAAO;;;wBAIO,mBACd,kBACA,eAAe;wBAKD,mBAAmB,kBAAkB;wBAIrC,iBAAiB,kBAAkB,cAAc;wBAYjD,kBAAkB,kBAAkB;;;;;YAWxC,aAAa,KAAK,eAAe;wBAiB7B,iBAAiB,GAC/B,OAAO,aAAa,IACpB,cACA,aACC,aAAa;wBAYA,mBAAmB,GAAG,OAAO,aAAa,IAAI,kBAAkB,aAAa;wBAM7E,kBAAkB,GAAG,OAAO,aAAa,IAAI,iBAAiB,aAAa"}
|
package/dist/form.js
CHANGED
|
@@ -9,6 +9,14 @@ import { mergeRefreshResponse } from "./partial.js";
|
|
|
9
9
|
import { isField } from "./schema/components.js";
|
|
10
10
|
import { debounce } from "./utils/debounce.js";
|
|
11
11
|
//#region src/form.ts
|
|
12
|
+
/** A DOM-safe id prefix for a form, e.g. `action:field.category_id.createOption` -> `alfakit-action-field-category_id-createOption`. */
|
|
13
|
+
function formIdPrefix(scope) {
|
|
14
|
+
return `alfakit-${scope.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
15
|
+
}
|
|
16
|
+
/** The DOM id of a field: `{prefix}-{statePath}` with unsafe characters replaced. */
|
|
17
|
+
function fieldDomId(prefix, statePath) {
|
|
18
|
+
return `${prefix}-${statePath.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
19
|
+
}
|
|
12
20
|
/**
|
|
13
21
|
* Walk a component tree and collect every field (including those inside Repeater items).
|
|
14
22
|
*/
|
|
@@ -64,6 +72,41 @@ function wizardStepToReveal(steps, previousErrors, errors) {
|
|
|
64
72
|
return index === -1 ? null : index;
|
|
65
73
|
}
|
|
66
74
|
/**
|
|
75
|
+
* Whether a layout component (a tab, a section) holds a field with a validation error:
|
|
76
|
+
* one of its fields, a nested repeater/builder item field (`items.0.sku`), or, while the
|
|
77
|
+
* component is deferred and its fields aren't loaded, one of its `deferredPaths`.
|
|
78
|
+
*/
|
|
79
|
+
function componentHasErrors(component, errors) {
|
|
80
|
+
const keys = Object.keys(errors).filter((key) => errors[key]);
|
|
81
|
+
if (keys.length === 0) return false;
|
|
82
|
+
return (component.deferred && !component.schema?.length ? component.deferredPaths ?? [] : collectFields(component.schema ?? []).map((field) => field.statePath)).some((path) => keys.some((key) => key === path || key.startsWith(`${path}.`)));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The value a tab is written to the query string as (`persistTabInQueryString()`): its
|
|
86
|
+
* readable `queryValue` (`billing-address`), else its key.
|
|
87
|
+
*/
|
|
88
|
+
function tabQueryValue(tab) {
|
|
89
|
+
return tab.queryValue ?? tab.key;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The key of the tab a query-string value names: a tab's `queryValue`, or its key (links
|
|
93
|
+
* written before tabs had readable values, `?tab=tabs-0.tab-1`). Null when none matches.
|
|
94
|
+
*/
|
|
95
|
+
function tabFromQuery(tabs, value) {
|
|
96
|
+
if (!value) return null;
|
|
97
|
+
return (tabs.find((tab) => tab.queryValue === value) ?? tabs.find((tab) => tab.key === value))?.key ?? null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The tab to show after the errors change: when new error keys arrived (a submit or
|
|
101
|
+
* validation response) and the active tab has none, the first tab with an error.
|
|
102
|
+
*/
|
|
103
|
+
function tabToReveal(tabs, activeKey, previousErrors, errors) {
|
|
104
|
+
if (!Object.keys(errors).some((key) => !(key in previousErrors))) return null;
|
|
105
|
+
const active = tabs.find((tab) => tab.key === activeKey);
|
|
106
|
+
if (active && componentHasErrors(active, errors)) return null;
|
|
107
|
+
return tabs.find((tab) => componentHasErrors(tab, errors))?.key ?? null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
67
110
|
* Framework-agnostic form state: values addressed by `statePath`, validation errors,
|
|
68
111
|
* dirty tracking and server round-trips for `live()` fields.
|
|
69
112
|
*/
|
|
@@ -117,6 +160,10 @@ var FormStore = class {
|
|
|
117
160
|
get component() {
|
|
118
161
|
return this.options.component;
|
|
119
162
|
}
|
|
163
|
+
/** Prefix of the fields' DOM ids (see `FormStoreOptions.idPrefix`). */
|
|
164
|
+
get idPrefix() {
|
|
165
|
+
return this.options.idPrefix ?? "alfakit";
|
|
166
|
+
}
|
|
120
167
|
get record() {
|
|
121
168
|
return this.options.record ?? null;
|
|
122
169
|
}
|
|
@@ -468,6 +515,6 @@ function cloneIndexedState(state, cloned) {
|
|
|
468
515
|
return result;
|
|
469
516
|
}
|
|
470
517
|
//#endregion
|
|
471
|
-
export { FormStore, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, collectFields, findFieldByPath, formIdentity, isLeavingVisit, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, wizardStepToReveal };
|
|
518
|
+
export { FormStore, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, collectFields, componentHasErrors, fieldDomId, findFieldByPath, formIdPrefix, formIdentity, isLeavingVisit, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, tabFromQuery, tabQueryValue, tabToReveal, wizardStepToReveal };
|
|
472
519
|
|
|
473
520
|
//# sourceMappingURL=form.js.map
|