@lattice-php/lattice 0.24.0 → 0.25.0

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.
Files changed (52) hide show
  1. package/dist/action/components/action-form.js +36 -4
  2. package/dist/action/components/action-form.js.map +1 -1
  3. package/dist/form/components/form.js +8 -2
  4. package/dist/form/components/form.js.map +1 -1
  5. package/dist/form/components/index.d.ts +1 -0
  6. package/dist/form/components/index.js +2 -1
  7. package/dist/form/components/wizard.d.ts +3 -0
  8. package/dist/form/components/wizard.js +148 -0
  9. package/dist/form/components/wizard.js.map +1 -0
  10. package/dist/form/embed.d.ts +1 -1
  11. package/dist/form/embed.js +2 -2
  12. package/dist/form/hooks/context.d.ts +7 -2
  13. package/dist/form/hooks/context.js +4 -1
  14. package/dist/form/hooks/context.js.map +1 -1
  15. package/dist/form/index.js +2 -1
  16. package/dist/form/lib/field-errors.d.ts +2 -0
  17. package/dist/form/lib/field-errors.js +5 -1
  18. package/dist/form/lib/field-errors.js.map +1 -1
  19. package/dist/form/lib/field-props.d.ts +6 -0
  20. package/dist/form/lib/field-props.js +7 -1
  21. package/dist/form/lib/field-props.js.map +1 -1
  22. package/dist/form/lib/prefill-targets.js +2 -3
  23. package/dist/form/lib/prefill-targets.js.map +1 -1
  24. package/dist/form/lib/wizard-steps.d.ts +5 -0
  25. package/dist/form/lib/wizard-steps.js +44 -0
  26. package/dist/form/lib/wizard-steps.js.map +1 -0
  27. package/dist/form/plugin.js +4 -1
  28. package/dist/form/plugin.js.map +1 -1
  29. package/dist/i18n/translatable.d.ts +1 -0
  30. package/dist/i18n/translatable.js +4 -1
  31. package/dist/i18n/translatable.js.map +1 -1
  32. package/dist/layout/components/callouts.js +3 -2
  33. package/dist/layout/components/callouts.js.map +1 -1
  34. package/dist/notifications/components/notification-item.js +7 -4
  35. package/dist/notifications/components/notification-item.js.map +1 -1
  36. package/dist/table/components/filter-controls.js +4 -1
  37. package/dist/table/components/filter-controls.js.map +1 -1
  38. package/dist/toast/callout.js +6 -3
  39. package/dist/toast/callout.js.map +1 -1
  40. package/dist/toast/toaster.js +2 -2
  41. package/dist/toast/toaster.js.map +1 -1
  42. package/dist/types/generated.d.ts +17 -7
  43. package/dist/vite-typescript-refresh.d.ts +9 -0
  44. package/dist/vite-typescript-refresh.js +47 -0
  45. package/dist/vite-typescript-refresh.js.map +1 -0
  46. package/dist/vite.d.ts +22 -1
  47. package/dist/vite.js +60 -7
  48. package/dist/vite.js.map +1 -1
  49. package/dist-standalone/lattice.css +1 -1
  50. package/dist-standalone/lattice.js +14 -14
  51. package/dist-standalone/manifest.json +2 -2
  52. package/package.json +1 -1
@@ -14,7 +14,7 @@ import { FormValuesProvider, useFormValues } from "../../form/hooks/values.js";
14
14
  import "../../form/lib/form-transport.js";
15
15
  import { useFormResolver } from "../../form/hooks/use-form-resolver.js";
16
16
  import { collectFields } from "../../form/lib/collect-fields.js";
17
- import { firstErrors } from "../../form/lib/field-errors.js";
17
+ import { errorKeyBelongsTo, firstErrors } from "../../form/lib/field-errors.js";
18
18
  import { useDebouncedCallback } from "../../lib/use-debounced-callback.js";
19
19
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
20
20
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -67,6 +67,7 @@ function ActionFormBody({ cancelLabel, componentRef, endpoint, extraData, fieldL
67
67
  const dispatch = useEffectDispatcher();
68
68
  const [errors, setErrors] = useState({});
69
69
  const [processing, setProcessing] = useState(false);
70
+ const [validating, setValidating] = useState(false);
70
71
  const request = useCallback((extraHeaders) => apiFetch(endpoint, {
71
72
  body: JSON.stringify({
72
73
  ...valuesRef.current,
@@ -106,6 +107,31 @@ function ActionFormBody({ cancelLabel, componentRef, endpoint, extraData, fieldL
106
107
  const validate = useCallback((field) => {
107
108
  if (precognitive) runValidation(field);
108
109
  }, [precognitive, runValidation]);
110
+ const touch = useCallback(() => {}, []);
111
+ const validateFields = useCallback((fields, options) => {
112
+ setValidating(true);
113
+ request({
114
+ Precognition: "true",
115
+ "Precognition-Validate-Only": fields.join(",")
116
+ }).then(async (response) => {
117
+ if (response.status === 422) {
118
+ const body = await response.json();
119
+ setErrors((current) => ({
120
+ ...current,
121
+ ...firstErrors(body.errors)
122
+ }));
123
+ options?.onValidationError?.();
124
+ return;
125
+ }
126
+ if (!response.ok) {
127
+ options?.onValidationError?.();
128
+ return;
129
+ }
130
+ const cleared = fields.filter((field) => !field.includes("*"));
131
+ setErrors((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !cleared.some((name) => errorKeyBelongsTo(key, name)))));
132
+ options?.onSuccess?.();
133
+ }).catch(() => options?.onValidationError?.()).finally(() => setValidating(false));
134
+ }, [request]);
109
135
  const submit = useCallback(() => {
110
136
  setProcessing(true);
111
137
  request().then(async (response) => {
@@ -132,7 +158,10 @@ function ActionFormBody({ cancelLabel, componentRef, endpoint, extraData, fieldL
132
158
  fieldLabels,
133
159
  precognitive,
134
160
  processing,
135
- validate
161
+ touch,
162
+ validate,
163
+ validateFields,
164
+ validating
136
165
  }), [
137
166
  clearErrors,
138
167
  componentRef,
@@ -141,7 +170,10 @@ function ActionFormBody({ cancelLabel, componentRef, endpoint, extraData, fieldL
141
170
  fieldLabels,
142
171
  precognitive,
143
172
  processing,
144
- validate
173
+ touch,
174
+ validate,
175
+ validateFields,
176
+ validating
145
177
  ]),
146
178
  children: /* @__PURE__ */ jsxs("form", {
147
179
  className: "flex flex-col gap-6",
@@ -164,7 +196,7 @@ function ActionFormBody({ cancelLabel, componentRef, endpoint, extraData, fieldL
164
196
  type: "button",
165
197
  variant: "ghost",
166
198
  children: cancelLabel
167
- }), /* @__PURE__ */ jsxs(Button, {
199
+ }), formNode.props?.submitButton !== false && /* @__PURE__ */ jsxs(Button, {
168
200
  "data-test": "action-form-submit",
169
201
  disabled: processing,
170
202
  type: "submit",
@@ -1 +1 @@
1
- {"version":3,"file":"action-form.js","names":[],"sources":["../../../resources/js/action/components/action-form.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { apiFetch } from \"@lattice-php/lattice/core/api\";\nimport { Button } from \"@lattice-php/lattice/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n type DialogPlacement,\n} from \"@lattice-php/lattice/ui/dialog\";\nimport { Skeleton } from \"@lattice-php/lattice/ui/skeleton\";\nimport { Spinner } from \"@lattice-php/lattice/ui/spinner\";\nimport { Renderer } from \"@lattice-php/lattice/core/renderer\";\nimport type { Node } from \"@lattice-php/lattice/core/types\";\nimport type { ModalWidth } from \"@lattice-php/lattice/types/generated\";\nimport {\n collectFields,\n FORM_DEBOUNCE_MS,\n FormProvider,\n FormValuesProvider,\n firstErrors,\n PrefillProvider,\n ResolvedNodesProvider,\n useFormResolver,\n useFormValues,\n} from \"@lattice-php/lattice/form/embed\";\nimport type { FieldErrors } from \"@lattice-php/lattice/form/embed\";\nimport { useDebouncedCallback } from \"@lattice-php/lattice/lib/use-debounced-callback\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport { dispatchActionError, getActionEffects } from \"@lattice-php/lattice/effects/dispatch\";\nimport type { ActionResponse } from \"@lattice-php/lattice/effects/dispatch\";\nimport { useEffectDispatcher } from \"@lattice-php/lattice/effects/use-effect-dispatcher\";\n\ntype ActionFormProps = {\n cancelLabel: string;\n componentRef: string;\n description?: string;\n endpoint: string;\n /** Extra payload merged into every request, e.g. a bulk action's selection. */\n extraData?: Record<string, unknown>;\n /** The form to render; null while a lazy schema is still being fetched. */\n formNode: Node | null;\n method: string;\n onClose: () => void;\n onSuccess: (response: ActionResponse) => void;\n /** Dialog placement for the form modal; sheets dock to a viewport edge. */\n placement?: DialogPlacement;\n submitLabel: string;\n title: string;\n width?: ModalWidth;\n};\n\n/**\n * Fetch a lazily-served form schema from the action endpoint while `enabled`,\n * so it can be prefilled per record. Returns null until it arrives.\n */\nexport function useLazyActionForm(\n endpoint: string,\n componentRef: string,\n enabled: boolean,\n): Node | null {\n const [node, setNode] = useState<Node | null>(null);\n\n useEffect(() => {\n if (!enabled) {\n setNode(null);\n\n return;\n }\n\n const controller = new AbortController();\n\n void apiFetch(endpoint, {\n body: JSON.stringify({ _form: true }),\n ref: componentRef,\n method: \"POST\",\n signal: controller.signal,\n throwOnError: false,\n })\n .then((response) => (response.ok ? (response.json() as Promise<Node>) : null))\n .then((fetched) => setNode(fetched))\n .catch(() => {});\n\n return () => controller.abort();\n }, [enabled, endpoint, componentRef]);\n\n return node;\n}\n\nfunction ActionFormSkeleton() {\n return (\n <div className=\"space-y-4\" data-lattice-action-form-loading>\n <Skeleton className=\"h-4 w-24\" />\n <Skeleton className=\"h-10 w-full\" />\n <Skeleton className=\"h-10 w-full\" />\n </div>\n );\n}\n\nfunction ActionFormBody({\n cancelLabel,\n componentRef,\n endpoint,\n extraData,\n fieldLabels,\n formNode,\n method,\n onClose,\n onSuccess,\n precognitive,\n submitLabel,\n}: Omit<ActionFormProps, \"description\" | \"title\"> & {\n fieldLabels: Record<string, string>;\n formNode: Node;\n precognitive: boolean;\n}) {\n const values = useFormValues();\n const valuesRef = useRef(values);\n valuesRef.current = values;\n const extraDataRef = useRef(extraData);\n extraDataRef.current = extraData;\n const { nodes: resolvedNodes, markUserEdit } = useFormResolver(\n endpoint,\n componentRef,\n formNode.schema,\n );\n\n const dispatch = useEffectDispatcher();\n const [errors, setErrors] = useState<FieldErrors>({});\n const [processing, setProcessing] = useState(false);\n\n const request = useCallback(\n (extraHeaders?: Record<string, string>): Promise<Response> =>\n apiFetch(endpoint, {\n body: JSON.stringify({ ...valuesRef.current, ...extraDataRef.current }),\n method,\n ref: componentRef,\n headers: extraHeaders,\n throwOnError: false,\n }),\n [componentRef, endpoint, method],\n );\n\n const clearErrors = useCallback((field: string) => {\n setErrors((current) =>\n current[field] === undefined ? current : { ...current, [field]: undefined },\n );\n }, []);\n\n const runValidation = useDebouncedCallback((field: string) => {\n void request({ Precognition: \"true\", \"Precognition-Validate-Only\": field })\n .then(async (response) => {\n if (response.status === 422) {\n const body = (await response.json()) as { errors?: Record<string, string[]> };\n setErrors((current) => ({ ...current, ...firstErrors(body.errors) }));\n\n return;\n }\n\n clearErrors(field);\n })\n .catch(() => {});\n }, FORM_DEBOUNCE_MS);\n\n const validate = useCallback(\n (field: string) => {\n if (precognitive) {\n runValidation(field);\n }\n },\n [precognitive, runValidation],\n );\n\n const submit = useCallback(() => {\n setProcessing(true);\n\n void request()\n .then(async (response) => {\n const body = (await response.json().catch(() => ({}))) as ActionResponse & {\n errors?: Record<string, string[]>;\n };\n\n dispatch(getActionEffects(body.effects));\n\n if (response.status === 422 && body.errors) {\n setErrors(firstErrors(body.errors));\n\n return;\n }\n\n if (!response.ok) {\n return;\n }\n\n onSuccess(body);\n })\n .catch((error: unknown) => dispatchActionError(error))\n .finally(() => setProcessing(false));\n }, [dispatch, onSuccess, request]);\n\n const context = useMemo(\n () => ({\n action: endpoint,\n clearErrors,\n componentRef,\n errors,\n fieldLabels,\n precognitive,\n processing,\n validate,\n }),\n [clearErrors, componentRef, endpoint, errors, fieldLabels, precognitive, processing, validate],\n );\n\n return (\n <FormProvider value={context}>\n <form\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n <PrefillProvider value={{ markUserEdit }}>\n <ResolvedNodesProvider nodes={resolvedNodes}>\n <Renderer nodes={formNode.schema ?? []} />\n </ResolvedNodesProvider>\n </PrefillProvider>\n\n <div className=\"flex justify-end gap-3\">\n <Button\n data-test=\"action-form-cancel\"\n disabled={processing}\n onClick={onClose}\n type=\"button\"\n variant=\"ghost\"\n >\n {cancelLabel}\n </Button>\n\n <Button data-test=\"action-form-submit\" disabled={processing} type=\"submit\">\n {processing && <Spinner />}\n {submitLabel}\n </Button>\n </div>\n </form>\n </FormProvider>\n );\n}\n\nfunction ActionFormContent({\n formNode,\n ...rest\n}: Omit<ActionFormProps, \"description\" | \"title\"> & { formNode: Node }) {\n const precognitive = Boolean(formNode.props?.precognitive);\n const { labels: fieldLabels, values: initialValues } = useMemo(() => {\n const { labels, values } = collectFields(formNode.schema);\n\n return {\n labels,\n values: { ...values, ...(formNode.props?.state as Record<string, unknown> | undefined) },\n };\n }, [formNode]);\n\n return (\n <FormValuesProvider initial={initialValues}>\n <ActionFormBody\n fieldLabels={fieldLabels}\n formNode={formNode}\n precognitive={precognitive}\n {...rest}\n />\n </FormValuesProvider>\n );\n}\n\nexport function ActionForm({\n description,\n formNode,\n onClose,\n placement,\n title,\n width,\n ...rest\n}: ActionFormProps) {\n const { t } = useT(\"lattice\");\n\n return (\n <Dialog\n open\n onOpenChange={(open) => {\n if (!open) {\n onClose();\n }\n }}\n >\n <DialogContent\n {...(description ? {} : { \"aria-describedby\": undefined })}\n placement={placement}\n width={width}\n >\n <DialogHeader\n closeLabel={t(\"common.close\", \"Close\")}\n description={description}\n title={title}\n />\n\n <div className=\"mt-6\">\n {formNode ? (\n <ActionFormContent formNode={formNode} onClose={onClose} {...rest} />\n ) : (\n <ActionFormSkeleton />\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"action-form.js","names":[],"sources":["../../../resources/js/action/components/action-form.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { apiFetch } from \"@lattice-php/lattice/core/api\";\nimport { Button } from \"@lattice-php/lattice/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n type DialogPlacement,\n} from \"@lattice-php/lattice/ui/dialog\";\nimport { Skeleton } from \"@lattice-php/lattice/ui/skeleton\";\nimport { Spinner } from \"@lattice-php/lattice/ui/spinner\";\nimport { Renderer } from \"@lattice-php/lattice/core/renderer\";\nimport type { Node } from \"@lattice-php/lattice/core/types\";\nimport type { ModalWidth } from \"@lattice-php/lattice/types/generated\";\nimport {\n collectFields,\n FORM_DEBOUNCE_MS,\n FormProvider,\n FormValuesProvider,\n errorKeyBelongsTo,\n firstErrors,\n PrefillProvider,\n ResolvedNodesProvider,\n useFormResolver,\n useFormValues,\n} from \"@lattice-php/lattice/form/embed\";\nimport type { FieldErrors } from \"@lattice-php/lattice/form/embed\";\nimport { useDebouncedCallback } from \"@lattice-php/lattice/lib/use-debounced-callback\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport { dispatchActionError, getActionEffects } from \"@lattice-php/lattice/effects/dispatch\";\nimport type { ActionResponse } from \"@lattice-php/lattice/effects/dispatch\";\nimport { useEffectDispatcher } from \"@lattice-php/lattice/effects/use-effect-dispatcher\";\n\ntype ActionFormProps = {\n cancelLabel: string;\n componentRef: string;\n description?: string;\n endpoint: string;\n /** Extra payload merged into every request, e.g. a bulk action's selection. */\n extraData?: Record<string, unknown>;\n /** The form to render; null while a lazy schema is still being fetched. */\n formNode: Node | null;\n method: string;\n onClose: () => void;\n onSuccess: (response: ActionResponse) => void;\n /** Dialog placement for the form modal; sheets dock to a viewport edge. */\n placement?: DialogPlacement;\n submitLabel: string;\n title: string;\n width?: ModalWidth;\n};\n\n/**\n * Fetch a lazily-served form schema from the action endpoint while `enabled`,\n * so it can be prefilled per record. Returns null until it arrives.\n */\nexport function useLazyActionForm(\n endpoint: string,\n componentRef: string,\n enabled: boolean,\n): Node | null {\n const [node, setNode] = useState<Node | null>(null);\n\n useEffect(() => {\n if (!enabled) {\n setNode(null);\n\n return;\n }\n\n const controller = new AbortController();\n\n void apiFetch(endpoint, {\n body: JSON.stringify({ _form: true }),\n ref: componentRef,\n method: \"POST\",\n signal: controller.signal,\n throwOnError: false,\n })\n .then((response) => (response.ok ? (response.json() as Promise<Node>) : null))\n .then((fetched) => setNode(fetched))\n .catch(() => {});\n\n return () => controller.abort();\n }, [enabled, endpoint, componentRef]);\n\n return node;\n}\n\nfunction ActionFormSkeleton() {\n return (\n <div className=\"space-y-4\" data-lattice-action-form-loading>\n <Skeleton className=\"h-4 w-24\" />\n <Skeleton className=\"h-10 w-full\" />\n <Skeleton className=\"h-10 w-full\" />\n </div>\n );\n}\n\nfunction ActionFormBody({\n cancelLabel,\n componentRef,\n endpoint,\n extraData,\n fieldLabels,\n formNode,\n method,\n onClose,\n onSuccess,\n precognitive,\n submitLabel,\n}: Omit<ActionFormProps, \"description\" | \"title\"> & {\n fieldLabels: Record<string, string>;\n formNode: Node;\n precognitive: boolean;\n}) {\n const values = useFormValues();\n const valuesRef = useRef(values);\n valuesRef.current = values;\n const extraDataRef = useRef(extraData);\n extraDataRef.current = extraData;\n const { nodes: resolvedNodes, markUserEdit } = useFormResolver(\n endpoint,\n componentRef,\n formNode.schema,\n );\n\n const dispatch = useEffectDispatcher();\n const [errors, setErrors] = useState<FieldErrors>({});\n const [processing, setProcessing] = useState(false);\n const [validating, setValidating] = useState(false);\n\n const request = useCallback(\n (extraHeaders?: Record<string, string>): Promise<Response> =>\n apiFetch(endpoint, {\n body: JSON.stringify({ ...valuesRef.current, ...extraDataRef.current }),\n method,\n ref: componentRef,\n headers: extraHeaders,\n throwOnError: false,\n }),\n [componentRef, endpoint, method],\n );\n\n const clearErrors = useCallback((field: string) => {\n setErrors((current) =>\n current[field] === undefined ? current : { ...current, [field]: undefined },\n );\n }, []);\n\n const runValidation = useDebouncedCallback((field: string) => {\n void request({ Precognition: \"true\", \"Precognition-Validate-Only\": field })\n .then(async (response) => {\n if (response.status === 422) {\n const body = (await response.json()) as { errors?: Record<string, string[]> };\n setErrors((current) => ({ ...current, ...firstErrors(body.errors) }));\n\n return;\n }\n\n clearErrors(field);\n })\n .catch(() => {});\n }, FORM_DEBOUNCE_MS);\n\n const validate = useCallback(\n (field: string) => {\n if (precognitive) {\n runValidation(field);\n }\n },\n [precognitive, runValidation],\n );\n\n const touch = useCallback(() => {}, []);\n\n const validateFields = useCallback(\n (fields: string[], options?: { onSuccess?: () => void; onValidationError?: () => void }) => {\n setValidating(true);\n\n void request({ Precognition: \"true\", \"Precognition-Validate-Only\": fields.join(\",\") })\n .then(async (response) => {\n if (response.status === 422) {\n const body = (await response.json()) as { errors?: Record<string, string[]> };\n setErrors((current) => ({ ...current, ...firstErrors(body.errors) }));\n options?.onValidationError?.();\n\n return;\n }\n\n if (!response.ok) {\n options?.onValidationError?.();\n\n return;\n }\n\n const cleared = fields.filter((field) => !field.includes(\"*\"));\n setErrors((current) =>\n Object.fromEntries(\n Object.entries(current).filter(\n ([key]) => !cleared.some((name) => errorKeyBelongsTo(key, name)),\n ),\n ),\n );\n options?.onSuccess?.();\n })\n .catch(() => options?.onValidationError?.())\n .finally(() => setValidating(false));\n },\n [request],\n );\n\n const submit = useCallback(() => {\n setProcessing(true);\n\n void request()\n .then(async (response) => {\n const body = (await response.json().catch(() => ({}))) as ActionResponse & {\n errors?: Record<string, string[]>;\n };\n\n dispatch(getActionEffects(body.effects));\n\n if (response.status === 422 && body.errors) {\n setErrors(firstErrors(body.errors));\n\n return;\n }\n\n if (!response.ok) {\n return;\n }\n\n onSuccess(body);\n })\n .catch((error: unknown) => dispatchActionError(error))\n .finally(() => setProcessing(false));\n }, [dispatch, onSuccess, request]);\n\n const context = useMemo(\n () => ({\n action: endpoint,\n clearErrors,\n componentRef,\n errors,\n fieldLabels,\n precognitive,\n processing,\n touch,\n validate,\n validateFields,\n validating,\n }),\n [\n clearErrors,\n componentRef,\n endpoint,\n errors,\n fieldLabels,\n precognitive,\n processing,\n touch,\n validate,\n validateFields,\n validating,\n ],\n );\n\n return (\n <FormProvider value={context}>\n <form\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n <PrefillProvider value={{ markUserEdit }}>\n <ResolvedNodesProvider nodes={resolvedNodes}>\n <Renderer nodes={formNode.schema ?? []} />\n </ResolvedNodesProvider>\n </PrefillProvider>\n\n <div className=\"flex justify-end gap-3\">\n <Button\n data-test=\"action-form-cancel\"\n disabled={processing}\n onClick={onClose}\n type=\"button\"\n variant=\"ghost\"\n >\n {cancelLabel}\n </Button>\n\n {formNode.props?.submitButton !== false && (\n <Button data-test=\"action-form-submit\" disabled={processing} type=\"submit\">\n {processing && <Spinner />}\n {submitLabel}\n </Button>\n )}\n </div>\n </form>\n </FormProvider>\n );\n}\n\nfunction ActionFormContent({\n formNode,\n ...rest\n}: Omit<ActionFormProps, \"description\" | \"title\"> & { formNode: Node }) {\n const precognitive = Boolean(formNode.props?.precognitive);\n const { labels: fieldLabels, values: initialValues } = useMemo(() => {\n const { labels, values } = collectFields(formNode.schema);\n\n return {\n labels,\n values: { ...values, ...(formNode.props?.state as Record<string, unknown> | undefined) },\n };\n }, [formNode]);\n\n return (\n <FormValuesProvider initial={initialValues}>\n <ActionFormBody\n fieldLabels={fieldLabels}\n formNode={formNode}\n precognitive={precognitive}\n {...rest}\n />\n </FormValuesProvider>\n );\n}\n\nexport function ActionForm({\n description,\n formNode,\n onClose,\n placement,\n title,\n width,\n ...rest\n}: ActionFormProps) {\n const { t } = useT(\"lattice\");\n\n return (\n <Dialog\n open\n onOpenChange={(open) => {\n if (!open) {\n onClose();\n }\n }}\n >\n <DialogContent\n {...(description ? {} : { \"aria-describedby\": undefined })}\n placement={placement}\n width={width}\n >\n <DialogHeader\n closeLabel={t(\"common.close\", \"Close\")}\n description={description}\n title={title}\n />\n\n <div className=\"mt-6\">\n {formNode ? (\n <ActionFormContent formNode={formNode} onClose={onClose} {...rest} />\n ) : (\n <ActionFormSkeleton />\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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"}
@@ -88,7 +88,7 @@ var FormComponent = ({ children, node }) => {
88
88
  validationTimeout: precognitive ? validationTimeout : void 0,
89
89
  headers: withHeaders(componentRef),
90
90
  className: "mx-auto flex w-full max-w-2xl flex-col gap-6",
91
- children: ({ clearErrors, errors, processing, reset, validate }) => /* @__PURE__ */ jsxs(FormProvider, {
91
+ children: ({ clearErrors, errors, processing, reset, touch, validate, validating }) => /* @__PURE__ */ jsxs(FormProvider, {
92
92
  value: {
93
93
  action,
94
94
  clearErrors: (field) => clearErrors(field),
@@ -98,7 +98,13 @@ var FormComponent = ({ children, node }) => {
98
98
  fieldLabels,
99
99
  precognitive,
100
100
  processing,
101
- validate: (field) => validate(field)
101
+ touch: (fields) => touch(...fields),
102
+ validate: (field) => validate(field),
103
+ validateFields: (fields, options) => validate({
104
+ only: fields,
105
+ ...options
106
+ }),
107
+ validating
102
108
  },
103
109
  children: [
104
110
  /* @__PURE__ */ jsx(FormResetListener, {
@@ -1 +1 @@
1
- {"version":3,"file":"form.js","names":[],"sources":["../../../resources/js/form/components/form.tsx"],"sourcesContent":["import { Form as InertiaForm } from \"@inertiajs/react\";\nimport { withHeaders } from \"@lattice-php/lattice/core/headers\";\nimport { LATTICE_EVENT } from \"@lattice-php/lattice/core/event-names\";\nimport { useWindowEvent } from \"@lattice-php/lattice/core/hooks/use-window-event\";\nimport { nodeKey } from \"@lattice-php/lattice/core/nodes\";\nimport { RenderNode } from \"@lattice-php/lattice/core/renderer\";\nimport type { Node, RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport type { ButtonVariant, Justify } from \"@lattice-php/lattice/types/generated\";\nimport { useMemo } from \"react\";\nimport { FormSubmitButton } from \"./base/submit-button\";\nimport { FormProvider } from \"@lattice-php/lattice/form/hooks/context\";\nimport { collectFields } from \"@lattice-php/lattice/form/lib/collect-fields\";\nimport { PrefillProvider } from \"@lattice-php/lattice/form/hooks/prefill-context\";\nimport { ResolvedNodesProvider } from \"@lattice-php/lattice/form/hooks/resolved-nodes\";\nimport { useFormResolver } from \"@lattice-php/lattice/form/hooks/use-form-resolver\";\nimport { FormValuesProvider } from \"@lattice-php/lattice/form/hooks/values\";\n\nconst JUSTIFY_CLASS: Record<Justify, string> = {\n start: \"justify-start\",\n center: \"justify-center\",\n end: \"justify-end\",\n between: \"justify-between\",\n around: \"justify-around\",\n evenly: \"justify-evenly\",\n};\n\nfunction FormResetListener({\n componentId,\n reset,\n}: {\n componentId?: string;\n reset: (...fields: string[]) => void;\n}) {\n useWindowEvent(LATTICE_EVENT.resetForm, (event) => {\n const detail = (event as CustomEvent<{ form: string | null }>).detail;\n\n if (!detail?.form || detail.form === componentId) {\n reset();\n }\n });\n\n return null;\n}\n\nfunction FormBody({\n action,\n children,\n componentRef,\n nodes,\n shouldRenderSubmitButton,\n submitButtons,\n submitJustify,\n submitLabel,\n submitVariant,\n summaryLabel,\n}: {\n action: string;\n children: React.ReactNode;\n componentRef: string;\n nodes: Node[] | undefined;\n shouldRenderSubmitButton: boolean;\n submitButtons: Node<\"button\">[] | undefined;\n submitJustify: Justify | undefined;\n submitLabel: string;\n submitVariant: ButtonVariant | undefined;\n summaryLabel: string;\n}) {\n const { nodes: resolvedNodes, markUserEdit } = useFormResolver(action, componentRef, nodes);\n\n return (\n <PrefillProvider value={{ markUserEdit }}>\n <ResolvedNodesProvider nodes={resolvedNodes}>\n <div className=\"flex flex-col gap-6\">\n {children}\n\n {shouldRenderSubmitButton && (\n <div className={`flex gap-3 ${JUSTIFY_CLASS[submitJustify ?? \"end\"]}`}>\n {submitButtons?.length ? (\n submitButtons.map((button, index) =>\n button.props.buttonType === \"submit\" ? (\n <FormSubmitButton\n key={nodeKey(button, index)}\n label={button.props.label ?? submitLabel}\n summaryLabel={summaryLabel}\n variant={button.props.variant ?? submitVariant ?? \"default\"}\n />\n ) : (\n <RenderNode key={nodeKey(button, index)} node={button} />\n ),\n )\n ) : (\n <FormSubmitButton\n label={submitLabel}\n summaryLabel={summaryLabel}\n variant={submitVariant ?? \"default\"}\n />\n )}\n </div>\n )}\n </div>\n </ResolvedNodesProvider>\n </PrefillProvider>\n );\n}\n\nexport const FormComponent: RendererComponent<\"form\"> = ({ children, node }) => {\n const { t } = useT(\"lattice\");\n const props = node.props;\n const action = props.action ?? \"#\";\n const errorBag = props.errorBag;\n const componentRef = props.ref ?? \"\";\n const method = props.method ?? \"post\";\n const precognitive = props.precognitive;\n const resetOnError = props.resetOnError ?? false;\n const resetOnSuccess = props.resetOnSuccess ?? [];\n const state = props.state;\n const { labels: fieldLabels, values: fieldValues } = useMemo(\n () => collectFields(node.schema),\n [node.schema],\n );\n const initialValues = useMemo(() => ({ ...fieldValues, ...state }), [fieldValues, state]);\n const shouldRenderSubmitButton = props.submitButton;\n const submitButtons = props.submitButtons ?? undefined;\n const submitJustify = props.submitJustify ?? undefined;\n const submitLabel = props.submitLabel ?? t(\"form.submit\", \"Submit\");\n const submitVariant = props.submitVariant ?? undefined;\n const summaryLabel = props.validationSummaryLabel;\n const validationTimeout = props.validationTimeout ?? undefined;\n\n return (\n <InertiaForm\n action={action}\n data-slot=\"form\"\n data-lattice-component={node.id}\n errorBag={errorBag}\n method={method}\n resetOnError={resetOnError}\n resetOnSuccess={resetOnSuccess}\n validationTimeout={precognitive ? validationTimeout : undefined}\n headers={withHeaders(componentRef)}\n className=\"mx-auto flex w-full max-w-2xl flex-col gap-6\"\n >\n {({ clearErrors, errors, processing, reset, validate }) => (\n <FormProvider\n value={{\n action,\n clearErrors: (field) => clearErrors(field),\n componentId: node.id,\n componentRef,\n errors: errors as Record<string, string | undefined>,\n fieldLabels,\n precognitive,\n processing,\n validate: (field) => validate(field),\n }}\n >\n <FormResetListener componentId={node.id} reset={reset} />\n\n {props.status && (\n <div className=\"text-center text-sm font-medium text-lt-success\">{props.status}</div>\n )}\n\n <FormValuesProvider initial={initialValues}>\n <FormBody\n action={action}\n componentRef={componentRef}\n nodes={node.schema}\n shouldRenderSubmitButton={shouldRenderSubmitButton}\n submitButtons={submitButtons}\n submitJustify={submitJustify}\n submitLabel={submitLabel}\n submitVariant={submitVariant}\n summaryLabel={summaryLabel}\n >\n {children}\n </FormBody>\n </FormValuesProvider>\n </FormProvider>\n )}\n </InertiaForm>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"form.js","names":[],"sources":["../../../resources/js/form/components/form.tsx"],"sourcesContent":["import { Form as InertiaForm } from \"@inertiajs/react\";\nimport { withHeaders } from \"@lattice-php/lattice/core/headers\";\nimport { LATTICE_EVENT } from \"@lattice-php/lattice/core/event-names\";\nimport { useWindowEvent } from \"@lattice-php/lattice/core/hooks/use-window-event\";\nimport { nodeKey } from \"@lattice-php/lattice/core/nodes\";\nimport { RenderNode } from \"@lattice-php/lattice/core/renderer\";\nimport type { Node, RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport type { ButtonVariant, Justify } from \"@lattice-php/lattice/types/generated\";\nimport { useMemo } from \"react\";\nimport { FormSubmitButton } from \"./base/submit-button\";\nimport { FormProvider } from \"@lattice-php/lattice/form/hooks/context\";\nimport { collectFields } from \"@lattice-php/lattice/form/lib/collect-fields\";\nimport { PrefillProvider } from \"@lattice-php/lattice/form/hooks/prefill-context\";\nimport { ResolvedNodesProvider } from \"@lattice-php/lattice/form/hooks/resolved-nodes\";\nimport { useFormResolver } from \"@lattice-php/lattice/form/hooks/use-form-resolver\";\nimport { FormValuesProvider } from \"@lattice-php/lattice/form/hooks/values\";\n\nconst JUSTIFY_CLASS: Record<Justify, string> = {\n start: \"justify-start\",\n center: \"justify-center\",\n end: \"justify-end\",\n between: \"justify-between\",\n around: \"justify-around\",\n evenly: \"justify-evenly\",\n};\n\nfunction FormResetListener({\n componentId,\n reset,\n}: {\n componentId?: string;\n reset: (...fields: string[]) => void;\n}) {\n useWindowEvent(LATTICE_EVENT.resetForm, (event) => {\n const detail = (event as CustomEvent<{ form: string | null }>).detail;\n\n if (!detail?.form || detail.form === componentId) {\n reset();\n }\n });\n\n return null;\n}\n\nfunction FormBody({\n action,\n children,\n componentRef,\n nodes,\n shouldRenderSubmitButton,\n submitButtons,\n submitJustify,\n submitLabel,\n submitVariant,\n summaryLabel,\n}: {\n action: string;\n children: React.ReactNode;\n componentRef: string;\n nodes: Node[] | undefined;\n shouldRenderSubmitButton: boolean;\n submitButtons: Node<\"button\">[] | undefined;\n submitJustify: Justify | undefined;\n submitLabel: string;\n submitVariant: ButtonVariant | undefined;\n summaryLabel: string;\n}) {\n const { nodes: resolvedNodes, markUserEdit } = useFormResolver(action, componentRef, nodes);\n\n return (\n <PrefillProvider value={{ markUserEdit }}>\n <ResolvedNodesProvider nodes={resolvedNodes}>\n <div className=\"flex flex-col gap-6\">\n {children}\n\n {shouldRenderSubmitButton && (\n <div className={`flex gap-3 ${JUSTIFY_CLASS[submitJustify ?? \"end\"]}`}>\n {submitButtons?.length ? (\n submitButtons.map((button, index) =>\n button.props.buttonType === \"submit\" ? (\n <FormSubmitButton\n key={nodeKey(button, index)}\n label={button.props.label ?? submitLabel}\n summaryLabel={summaryLabel}\n variant={button.props.variant ?? submitVariant ?? \"default\"}\n />\n ) : (\n <RenderNode key={nodeKey(button, index)} node={button} />\n ),\n )\n ) : (\n <FormSubmitButton\n label={submitLabel}\n summaryLabel={summaryLabel}\n variant={submitVariant ?? \"default\"}\n />\n )}\n </div>\n )}\n </div>\n </ResolvedNodesProvider>\n </PrefillProvider>\n );\n}\n\nexport const FormComponent: RendererComponent<\"form\"> = ({ children, node }) => {\n const { t } = useT(\"lattice\");\n const props = node.props;\n const action = props.action ?? \"#\";\n const errorBag = props.errorBag;\n const componentRef = props.ref ?? \"\";\n const method = props.method ?? \"post\";\n const precognitive = props.precognitive;\n const resetOnError = props.resetOnError ?? false;\n const resetOnSuccess = props.resetOnSuccess ?? [];\n const state = props.state;\n const { labels: fieldLabels, values: fieldValues } = useMemo(\n () => collectFields(node.schema),\n [node.schema],\n );\n const initialValues = useMemo(() => ({ ...fieldValues, ...state }), [fieldValues, state]);\n const shouldRenderSubmitButton = props.submitButton;\n const submitButtons = props.submitButtons ?? undefined;\n const submitJustify = props.submitJustify ?? undefined;\n const submitLabel = props.submitLabel ?? t(\"form.submit\", \"Submit\");\n const submitVariant = props.submitVariant ?? undefined;\n const summaryLabel = props.validationSummaryLabel;\n const validationTimeout = props.validationTimeout ?? undefined;\n\n return (\n <InertiaForm\n action={action}\n data-slot=\"form\"\n data-lattice-component={node.id}\n errorBag={errorBag}\n method={method}\n resetOnError={resetOnError}\n resetOnSuccess={resetOnSuccess}\n validationTimeout={precognitive ? validationTimeout : undefined}\n headers={withHeaders(componentRef)}\n className=\"mx-auto flex w-full max-w-2xl flex-col gap-6\"\n >\n {({ clearErrors, errors, processing, reset, touch, validate, validating }) => (\n <FormProvider\n value={{\n action,\n clearErrors: (field) => clearErrors(field),\n componentId: node.id,\n componentRef,\n errors: errors as Record<string, string | undefined>,\n fieldLabels,\n precognitive,\n processing,\n touch: (fields) => touch(...fields),\n validate: (field) => validate(field),\n validateFields: (fields, options) => validate({ only: fields, ...options }),\n validating,\n }}\n >\n <FormResetListener componentId={node.id} reset={reset} />\n\n {props.status && (\n <div className=\"text-center text-sm font-medium text-lt-success\">{props.status}</div>\n )}\n\n <FormValuesProvider initial={initialValues}>\n <FormBody\n action={action}\n componentRef={componentRef}\n nodes={node.schema}\n shouldRenderSubmitButton={shouldRenderSubmitButton}\n submitButtons={submitButtons}\n submitJustify={submitJustify}\n submitLabel={submitLabel}\n submitVariant={submitVariant}\n summaryLabel={summaryLabel}\n >\n {children}\n </FormBody>\n </FormValuesProvider>\n </FormProvider>\n )}\n </InertiaForm>\n );\n};\n"],"mappings":"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"}
@@ -16,3 +16,4 @@ export { TextareaComponent } from './fields/textarea.js';
16
16
  export { TextInputComponent } from './fields/text-input.js';
17
17
  export { TimeInputComponent } from './fields/time-input.js';
18
18
  export { ToggleComponent } from './fields/toggle.js';
19
+ export { WizardComponent, WizardStepComponent } from './wizard.js';
@@ -16,4 +16,5 @@ import { TextareaComponent } from "./fields/textarea.js";
16
16
  import { TextInputComponent } from "./fields/text-input.js";
17
17
  import { TimeInputComponent } from "./fields/time-input.js";
18
18
  import { ToggleComponent } from "./fields/toggle.js";
19
- export { BuilderComponent, CheckboxComponent, ChoiceComponent, ColorPickerFieldComponent, DateInputComponent, DateTimeInputComponent, FileUploadComponent, FormComponent, HiddenInputComponent, NumberInputComponent, OtpInputComponent, PasswordInputComponent, RepeaterComponent, SelectComponent, TextInputComponent, TextareaComponent, TimeInputComponent, ToggleComponent };
19
+ import { WizardComponent, WizardStepComponent } from "./wizard.js";
20
+ export { BuilderComponent, CheckboxComponent, ChoiceComponent, ColorPickerFieldComponent, DateInputComponent, DateTimeInputComponent, FileUploadComponent, FormComponent, HiddenInputComponent, NumberInputComponent, OtpInputComponent, PasswordInputComponent, RepeaterComponent, SelectComponent, TextInputComponent, TextareaComponent, TimeInputComponent, ToggleComponent, WizardComponent, WizardStepComponent };
@@ -0,0 +1,3 @@
1
+ import { RendererComponent } from '../../core/types.js';
2
+ export declare const WizardComponent: RendererComponent<"wizard">;
3
+ export declare const WizardStepComponent: RendererComponent<"wizard-step">;
@@ -0,0 +1,148 @@
1
+ import { useT } from "../../i18n/instance.js";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { Icon } from "../../icons/sprite.js";
4
+ import { Button } from "../../ui/button.js";
5
+ import { Spinner } from "../../ui/spinner.js";
6
+ import { useFormContext } from "../hooks/context.js";
7
+ import { firstErroredStep, stepFieldNames, stepValidationPaths, stepsWithErrors } from "../lib/wizard-steps.js";
8
+ import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ //#region resources/js/form/components/wizard.tsx
11
+ var WizardContext = createContext({ activeName: "" });
12
+ function getSteps(node) {
13
+ const nodes = (node.schema ?? []).filter((child) => child.type === "wizard-step");
14
+ return {
15
+ items: nodes.map((child) => child.props),
16
+ nodes
17
+ };
18
+ }
19
+ var WizardComponent = ({ children, node }) => {
20
+ const { t } = useT("lattice");
21
+ const { errors, processing, touch, validateFields, validating } = useFormContext();
22
+ const { items, nodes } = useMemo(() => getSteps(node), [node]);
23
+ const stepNames = useMemo(() => nodes.map((step) => stepFieldNames(step)), [nodes]);
24
+ const isVertical = node.props.orientation === "vertical";
25
+ const [activeIndex, setActiveIndex] = useState(0);
26
+ const [visited, setVisited] = useState(() => new Set([0]));
27
+ const [completed, setCompleted] = useState(() => /* @__PURE__ */ new Set());
28
+ const erroredSteps = useMemo(() => stepsWithErrors(stepNames, errors), [stepNames, errors]);
29
+ const isLast = activeIndex === items.length - 1;
30
+ const goTo = (index) => {
31
+ setActiveIndex(index);
32
+ setVisited((previous) => new Set(previous).add(index));
33
+ };
34
+ const advance = () => {
35
+ setCompleted((previous) => new Set(previous).add(activeIndex));
36
+ if (!isLast) goTo(activeIndex + 1);
37
+ };
38
+ const onNext = () => {
39
+ const step = nodes[activeIndex];
40
+ const paths = step ? stepValidationPaths(step) : [];
41
+ if (paths.length === 0) {
42
+ advance();
43
+ return;
44
+ }
45
+ touch(paths);
46
+ validateFields(paths, { onSuccess: advance });
47
+ };
48
+ const wasProcessing = useRef(false);
49
+ useEffect(() => {
50
+ if (wasProcessing.current && !processing) {
51
+ const target = firstErroredStep(stepNames, errors);
52
+ if (target !== null && target !== activeIndex) goTo(target);
53
+ }
54
+ wasProcessing.current = processing;
55
+ }, [
56
+ processing,
57
+ errors,
58
+ stepNames,
59
+ activeIndex
60
+ ]);
61
+ const contextValue = useMemo(() => ({ activeName: items[activeIndex]?.name ?? "" }), [items, activeIndex]);
62
+ return /* @__PURE__ */ jsx(WizardContext.Provider, {
63
+ value: contextValue,
64
+ children: /* @__PURE__ */ jsxs("div", {
65
+ className: cn("gap-6", isVertical ? "flex" : "grid"),
66
+ "data-slot": "wizard",
67
+ children: [/* @__PURE__ */ jsx("ol", {
68
+ "aria-label": t("form.wizard.steps", "Steps"),
69
+ className: cn("gap-1", isVertical ? "flex w-56 shrink-0 flex-col" : "flex flex-wrap"),
70
+ children: items.map((step, index) => {
71
+ const isActive = index === activeIndex;
72
+ const isDone = completed.has(index);
73
+ const hasError = erroredSteps.has(index);
74
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
75
+ "aria-current": isActive ? "step" : void 0,
76
+ className: cn("flex w-full items-center gap-2 rounded-lt px-3 py-2 text-left text-sm", isActive ? "bg-lt-muted font-medium text-lt-fg" : "text-lt-muted-fg", !visited.has(index) && "cursor-not-allowed opacity-60"),
77
+ "data-error": hasError || void 0,
78
+ "data-test": `wizard-rail-${step.name}`,
79
+ disabled: !visited.has(index),
80
+ id: `wizard-step-${step.name}-trigger`,
81
+ onClick: () => goTo(index),
82
+ type: "button",
83
+ children: [/* @__PURE__ */ jsx("span", {
84
+ className: cn("flex size-5 shrink-0 items-center justify-center rounded-full border text-xs", hasError ? "border-lt-danger text-lt-danger" : isDone ? "border-lt-primary bg-lt-primary text-lt-primary-fg" : "border-lt-border"),
85
+ children: isDone && !hasError ? /* @__PURE__ */ jsx(Icon, {
86
+ className: "size-lt-icon-sm",
87
+ name: "check"
88
+ }) : index + 1
89
+ }), /* @__PURE__ */ jsxs("span", {
90
+ className: "min-w-0",
91
+ children: [/* @__PURE__ */ jsx("span", {
92
+ className: "block truncate",
93
+ children: step.label
94
+ }), isVertical && step.description && /* @__PURE__ */ jsx("span", {
95
+ className: "block truncate text-xs text-lt-muted-fg",
96
+ children: step.description
97
+ })]
98
+ })]
99
+ }) }, step.name);
100
+ })
101
+ }), /* @__PURE__ */ jsxs("div", {
102
+ className: "min-w-0 flex-1 space-y-6",
103
+ children: [children, /* @__PURE__ */ jsxs("div", {
104
+ className: "flex items-center justify-between gap-3",
105
+ children: [/* @__PURE__ */ jsx(Button, {
106
+ "data-test": "wizard-back",
107
+ disabled: activeIndex === 0 || processing,
108
+ onClick: () => goTo(activeIndex - 1),
109
+ type: "button",
110
+ variant: "outline",
111
+ children: t("form.wizard.back", "Back")
112
+ }), isLast ? /* @__PURE__ */ jsxs(Button, {
113
+ "data-test": "wizard-finish",
114
+ disabled: processing,
115
+ type: "submit",
116
+ children: [processing && /* @__PURE__ */ jsx(Spinner, {}), t("form.wizard.finish", "Finish")]
117
+ }) : /* @__PURE__ */ jsxs(Button, {
118
+ "data-test": "wizard-next",
119
+ disabled: processing || validating,
120
+ onClick: onNext,
121
+ type: "button",
122
+ children: [validating && /* @__PURE__ */ jsx(Spinner, {}), t("form.wizard.next", "Next")]
123
+ })]
124
+ })]
125
+ })]
126
+ })
127
+ });
128
+ };
129
+ var WizardStepComponent = ({ children, node }) => {
130
+ const { activeName } = useContext(WizardContext);
131
+ const name = node.props.name;
132
+ const isActive = activeName === name;
133
+ const [hasOpened, setHasOpened] = useState(isActive);
134
+ useEffect(() => {
135
+ if (isActive) setHasOpened(true);
136
+ }, [isActive]);
137
+ return /* @__PURE__ */ jsx("section", {
138
+ "aria-labelledby": `wizard-step-${name}-trigger`,
139
+ className: cn("space-y-8", !isActive && "hidden"),
140
+ hidden: !isActive,
141
+ id: `wizard-step-${name}-panel`,
142
+ children: hasOpened ? children : null
143
+ });
144
+ };
145
+ //#endregion
146
+ export { WizardComponent, WizardStepComponent };
147
+
148
+ //# sourceMappingURL=wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizard.js","names":[],"sources":["../../../resources/js/form/components/wizard.tsx"],"sourcesContent":["import { createContext, useContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { Node, RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport type { WizardStep } from \"@lattice-php/lattice/types/generated\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport { Icon } from \"@lattice-php/lattice/icons\";\nimport { Button } from \"@lattice-php/lattice/ui/button\";\nimport { Spinner } from \"@lattice-php/lattice/ui/spinner\";\nimport { useFormContext } from \"@lattice-php/lattice/form/hooks/context\";\nimport {\n firstErroredStep,\n stepFieldNames,\n stepsWithErrors,\n stepValidationPaths,\n} from \"@lattice-php/lattice/form/lib/wizard-steps\";\n\ntype WizardContextValue = { activeName: string };\n\nconst WizardContext = createContext<WizardContextValue>({ activeName: \"\" });\n\nfunction getSteps(node: Node<\"wizard\">): { items: WizardStep[]; nodes: Node[] } {\n const nodes = (node.schema ?? []).filter((child) => child.type === \"wizard-step\");\n const items = nodes.map((child) => child.props as unknown as WizardStep);\n\n return { items, nodes };\n}\n\nexport const WizardComponent: RendererComponent<\"wizard\"> = ({ children, node }) => {\n const { t } = useT(\"lattice\");\n const { errors, processing, touch, validateFields, validating } = useFormContext();\n const { items, nodes } = useMemo(() => getSteps(node), [node]);\n const stepNames = useMemo(() => nodes.map((step) => stepFieldNames(step)), [nodes]);\n const isVertical = node.props.orientation === \"vertical\";\n\n const [activeIndex, setActiveIndex] = useState(0);\n const [visited, setVisited] = useState<Set<number>>(() => new Set([0]));\n const [completed, setCompleted] = useState<Set<number>>(() => new Set());\n const erroredSteps = useMemo(() => stepsWithErrors(stepNames, errors), [stepNames, errors]);\n const isLast = activeIndex === items.length - 1;\n\n const goTo = (index: number): void => {\n setActiveIndex(index);\n setVisited((previous) => new Set(previous).add(index));\n };\n\n const advance = (): void => {\n setCompleted((previous) => new Set(previous).add(activeIndex));\n\n if (!isLast) {\n goTo(activeIndex + 1);\n }\n };\n\n const onNext = (): void => {\n const step = nodes[activeIndex];\n const paths = step ? stepValidationPaths(step) : [];\n\n if (paths.length === 0) {\n advance();\n\n return;\n }\n\n touch(paths);\n validateFields(paths, { onSuccess: advance });\n };\n\n const wasProcessing = useRef(false);\n useEffect(() => {\n if (wasProcessing.current && !processing) {\n const target = firstErroredStep(stepNames, errors);\n\n if (target !== null && target !== activeIndex) {\n goTo(target);\n }\n }\n\n wasProcessing.current = processing;\n }, [processing, errors, stepNames, activeIndex]);\n\n const contextValue = useMemo(\n () => ({ activeName: items[activeIndex]?.name ?? \"\" }),\n [items, activeIndex],\n );\n\n return (\n <WizardContext.Provider value={contextValue}>\n <div className={cn(\"gap-6\", isVertical ? \"flex\" : \"grid\")} data-slot=\"wizard\">\n <ol\n aria-label={t(\"form.wizard.steps\", \"Steps\")}\n className={cn(\"gap-1\", isVertical ? \"flex w-56 shrink-0 flex-col\" : \"flex flex-wrap\")}\n >\n {items.map((step, index) => {\n const isActive = index === activeIndex;\n const isDone = completed.has(index);\n const hasError = erroredSteps.has(index);\n\n return (\n <li key={step.name}>\n <button\n aria-current={isActive ? \"step\" : undefined}\n className={cn(\n \"flex w-full items-center gap-2 rounded-lt px-3 py-2 text-left text-sm\",\n isActive ? \"bg-lt-muted font-medium text-lt-fg\" : \"text-lt-muted-fg\",\n !visited.has(index) && \"cursor-not-allowed opacity-60\",\n )}\n data-error={hasError || undefined}\n data-test={`wizard-rail-${step.name}`}\n disabled={!visited.has(index)}\n id={`wizard-step-${step.name}-trigger`}\n onClick={() => goTo(index)}\n type=\"button\"\n >\n <span\n className={cn(\n \"flex size-5 shrink-0 items-center justify-center rounded-full border text-xs\",\n hasError\n ? \"border-lt-danger text-lt-danger\"\n : isDone\n ? \"border-lt-primary bg-lt-primary text-lt-primary-fg\"\n : \"border-lt-border\",\n )}\n >\n {isDone && !hasError ? (\n <Icon className=\"size-lt-icon-sm\" name=\"check\" />\n ) : (\n index + 1\n )}\n </span>\n <span className=\"min-w-0\">\n <span className=\"block truncate\">{step.label}</span>\n {isVertical && step.description && (\n <span className=\"block truncate text-xs text-lt-muted-fg\">\n {step.description}\n </span>\n )}\n </span>\n </button>\n </li>\n );\n })}\n </ol>\n\n <div className=\"min-w-0 flex-1 space-y-6\">\n {children}\n\n <div className=\"flex items-center justify-between gap-3\">\n <Button\n data-test=\"wizard-back\"\n disabled={activeIndex === 0 || processing}\n onClick={() => goTo(activeIndex - 1)}\n type=\"button\"\n variant=\"outline\"\n >\n {t(\"form.wizard.back\", \"Back\")}\n </Button>\n\n {isLast ? (\n <Button data-test=\"wizard-finish\" disabled={processing} type=\"submit\">\n {processing && <Spinner />}\n {t(\"form.wizard.finish\", \"Finish\")}\n </Button>\n ) : (\n <Button\n data-test=\"wizard-next\"\n disabled={processing || validating}\n onClick={onNext}\n type=\"button\"\n >\n {validating && <Spinner />}\n {t(\"form.wizard.next\", \"Next\")}\n </Button>\n )}\n </div>\n </div>\n </div>\n </WizardContext.Provider>\n );\n};\n\nexport const WizardStepComponent: RendererComponent<\"wizard-step\"> = ({ children, node }) => {\n const { activeName } = useContext(WizardContext);\n const name = node.props.name;\n const isActive = activeName === name;\n const [hasOpened, setHasOpened] = useState(isActive);\n\n useEffect(() => {\n if (isActive) {\n setHasOpened(true);\n }\n }, [isActive]);\n\n return (\n <section\n aria-labelledby={`wizard-step-${name}-trigger`}\n className={cn(\"space-y-8\", !isActive && \"hidden\")}\n hidden={!isActive}\n id={`wizard-step-${name}-panel`}\n >\n {hasOpened ? (children as ReactNode) : null}\n </section>\n );\n};\n"],"mappings":"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"}
@@ -16,7 +16,7 @@ export { FormValuesProvider, useFormValues, useSetFormValue } from './hooks/valu
16
16
  export { walkFields } from './lib/field-props.js';
17
17
  export { collectFields } from './lib/collect-fields.js';
18
18
  export type { CollectedFields } from './lib/collect-fields.js';
19
- export { firstErrors } from './lib/field-errors.js';
19
+ export { errorKeyBelongsTo, firstErrors } from './lib/field-errors.js';
20
20
  export type { FieldErrors } from './lib/field-errors.js';
21
21
  export { appendPath, getPath, setPath } from './lib/form-path.js';
22
22
  export { FORM_DEBOUNCE_MS } from './lib/form-transport.js';
@@ -9,5 +9,5 @@ import { TableCellProvider } from "./hooks/row-layout-context.js";
9
9
  import { FORM_DEBOUNCE_MS } from "./lib/form-transport.js";
10
10
  import { useFormResolver } from "./hooks/use-form-resolver.js";
11
11
  import { collectFields } from "./lib/collect-fields.js";
12
- import { firstErrors } from "./lib/field-errors.js";
13
- export { FORM_DEBOUNCE_MS, FieldCommitOverrideProvider, FormProvider, FormValuesProvider, PrefillProvider, ResolvedNodesProvider, TableCellProvider, appendPath, collectFields, firstErrors, getPath, setPath, useFormResolver, useFormValues, useSetFormValue, walkFields };
12
+ import { errorKeyBelongsTo, firstErrors } from "./lib/field-errors.js";
13
+ export { FORM_DEBOUNCE_MS, FieldCommitOverrideProvider, FormProvider, FormValuesProvider, PrefillProvider, ResolvedNodesProvider, TableCellProvider, appendPath, collectFields, errorKeyBelongsTo, firstErrors, getPath, setPath, useFormResolver, useFormValues, useSetFormValue, walkFields };
@@ -1,5 +1,5 @@
1
1
  import { Option } from '../../core/types.js';
2
- type FormContextValue = {
2
+ export type FormContextValue = {
3
3
  action: string;
4
4
  clearErrors: (field: string) => void;
5
5
  componentId?: string;
@@ -10,11 +10,16 @@ type FormContextValue = {
10
10
  precognitive: boolean;
11
11
  processing: boolean;
12
12
  searchOptions?: (field: string, query: string, values: Record<string, unknown>, signal: AbortSignal) => Promise<Option[]>;
13
+ touch: (fields: string[]) => void;
13
14
  validate: (field: string) => void;
15
+ validateFields: (fields: string[], options?: {
16
+ onSuccess?: () => void;
17
+ onValidationError?: () => void;
18
+ }) => void;
19
+ validating: boolean;
14
20
  };
15
21
  export declare function FormProvider({ children, value, }: {
16
22
  children: React.ReactNode;
17
23
  value: FormContextValue;
18
24
  }): import("react").JSX.Element;
19
25
  export declare function useFormContext(): FormContextValue;
20
- export {};
@@ -11,7 +11,10 @@ var FormContext = createContext({
11
11
  fieldLabels: {},
12
12
  precognitive: false,
13
13
  processing: false,
14
- validate: () => {}
14
+ touch: () => {},
15
+ validate: () => {},
16
+ validateFields: () => {},
17
+ validating: false
15
18
  });
16
19
  function FormProvider({ children, value }) {
17
20
  return /* @__PURE__ */ jsx(FormContext.Provider, {
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../../../resources/js/form/hooks/context.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { Option } from \"@lattice-php/lattice/core/types\";\n\ntype FormContextValue = {\n action: string;\n clearErrors: (field: string) => void;\n componentId?: string;\n componentRef: string;\n errors: Record<string, string | undefined>;\n fieldIdPrefix?: string;\n fieldLabels: Record<string, string>;\n precognitive: boolean;\n processing: boolean;\n searchOptions?: (\n field: string,\n query: string,\n values: Record<string, unknown>,\n signal: AbortSignal,\n ) => Promise<Option[]>;\n validate: (field: string) => void;\n};\n\nconst FormContext = createContext<FormContextValue>({\n action: \"#\",\n clearErrors: () => {},\n componentId: undefined,\n componentRef: \"\",\n errors: {},\n fieldIdPrefix: undefined,\n fieldLabels: {},\n precognitive: false,\n processing: false,\n validate: () => {},\n});\n\nexport function FormProvider({\n children,\n value,\n}: {\n children: React.ReactNode;\n value: FormContextValue;\n}) {\n return <FormContext.Provider value={value}>{children}</FormContext.Provider>;\n}\n\nexport function useFormContext() {\n return useContext(FormContext);\n}\n"],"mappings":";;;AAsBA,IAAM,cAAc,cAAgC;CAClD,QAAQ;CACR,mBAAmB,CAAC;CACpB,aAAa,KAAA;CACb,cAAc;CACd,QAAQ,CAAC;CACT,eAAe,KAAA;CACf,aAAa,CAAC;CACd,cAAc;CACd,YAAY;CACZ,gBAAgB,CAAC;AACnB,CAAC;AAED,SAAgB,aAAa,EAC3B,UACA,SAIC;CACD,OAAO,oBAAC,YAAY,UAAb;EAA6B;EAAQ;CAA+B,CAAA;AAC7E;AAEA,SAAgB,iBAAiB;CAC/B,OAAO,WAAW,WAAW;AAC/B"}
1
+ {"version":3,"file":"context.js","names":[],"sources":["../../../resources/js/form/hooks/context.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { Option } from \"@lattice-php/lattice/core/types\";\n\nexport type FormContextValue = {\n action: string;\n clearErrors: (field: string) => void;\n componentId?: string;\n componentRef: string;\n errors: Record<string, string | undefined>;\n fieldIdPrefix?: string;\n fieldLabels: Record<string, string>;\n precognitive: boolean;\n processing: boolean;\n searchOptions?: (\n field: string,\n query: string,\n values: Record<string, unknown>,\n signal: AbortSignal,\n ) => Promise<Option[]>;\n touch: (fields: string[]) => void;\n validate: (field: string) => void;\n validateFields: (\n fields: string[],\n options?: { onSuccess?: () => void; onValidationError?: () => void },\n ) => void;\n validating: boolean;\n};\n\nconst FormContext = createContext<FormContextValue>({\n action: \"#\",\n clearErrors: () => {},\n componentId: undefined,\n componentRef: \"\",\n errors: {},\n fieldIdPrefix: undefined,\n fieldLabels: {},\n precognitive: false,\n processing: false,\n touch: () => {},\n validate: () => {},\n validateFields: () => {},\n validating: false,\n});\n\nexport function FormProvider({\n children,\n value,\n}: {\n children: React.ReactNode;\n value: FormContextValue;\n}) {\n return <FormContext.Provider value={value}>{children}</FormContext.Provider>;\n}\n\nexport function useFormContext() {\n return useContext(FormContext);\n}\n"],"mappings":";;;AA4BA,IAAM,cAAc,cAAgC;CAClD,QAAQ;CACR,mBAAmB,CAAC;CACpB,aAAa,KAAA;CACb,cAAc;CACd,QAAQ,CAAC;CACT,eAAe,KAAA;CACf,aAAa,CAAC;CACd,cAAc;CACd,YAAY;CACZ,aAAa,CAAC;CACd,gBAAgB,CAAC;CACjB,sBAAsB,CAAC;CACvB,YAAY;AACd,CAAC;AAED,SAAgB,aAAa,EAC3B,UACA,SAIC;CACD,OAAO,oBAAC,YAAY,UAAb;EAA6B;EAAQ;CAA+B,CAAA;AAC7E;AAEA,SAAgB,iBAAiB;CAC/B,OAAO,WAAW,WAAW;AAC/B"}
@@ -17,5 +17,6 @@ import { TextareaComponent } from "./components/fields/textarea.js";
17
17
  import { TextInputComponent } from "./components/fields/text-input.js";
18
18
  import { TimeInputComponent } from "./components/fields/time-input.js";
19
19
  import { ToggleComponent } from "./components/fields/toggle.js";
20
+ import { WizardComponent, WizardStepComponent } from "./components/wizard.js";
20
21
  import { formComponents } from "./plugin.js";
21
- export { BuilderComponent, CheckboxComponent, ChoiceComponent, ColorPickerFieldComponent, DateInputComponent, DateTimeInputComponent, FileUploadComponent, FormComponent, FormValuesProvider, HiddenInputComponent, NumberInputComponent, OtpInputComponent, PasswordInputComponent, RepeaterComponent, SelectComponent, TextInputComponent, TextareaComponent, TimeInputComponent, ToggleComponent, formComponents };
22
+ export { BuilderComponent, CheckboxComponent, ChoiceComponent, ColorPickerFieldComponent, DateInputComponent, DateTimeInputComponent, FileUploadComponent, FormComponent, FormValuesProvider, HiddenInputComponent, NumberInputComponent, OtpInputComponent, PasswordInputComponent, RepeaterComponent, SelectComponent, TextInputComponent, TextareaComponent, TimeInputComponent, ToggleComponent, WizardComponent, WizardStepComponent, formComponents };
@@ -1,3 +1,5 @@
1
1
  export type FieldErrors = Record<string, string | undefined>;
2
+ /** Whether an error-bag key targets the named field itself or a path nested under it. */
3
+ export declare function errorKeyBelongsTo(key: string, name: string): boolean;
2
4
  /** Reduce a Laravel 422 error bag (arrays of messages) to the first per field. */
3
5
  export declare function firstErrors(errors: Record<string, string[] | string> | undefined): FieldErrors;
@@ -1,4 +1,8 @@
1
1
  //#region resources/js/form/lib/field-errors.ts
2
+ /** Whether an error-bag key targets the named field itself or a path nested under it. */
3
+ function errorKeyBelongsTo(key, name) {
4
+ return key === name || key.startsWith(`${name}.`);
5
+ }
2
6
  /** Reduce a Laravel 422 error bag (arrays of messages) to the first per field. */
3
7
  function firstErrors(errors) {
4
8
  const result = {};
@@ -6,6 +10,6 @@ function firstErrors(errors) {
6
10
  return result;
7
11
  }
8
12
  //#endregion
9
- export { firstErrors };
13
+ export { errorKeyBelongsTo, firstErrors };
10
14
 
11
15
  //# sourceMappingURL=field-errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-errors.js","names":[],"sources":["../../../resources/js/form/lib/field-errors.ts"],"sourcesContent":["export type FieldErrors = Record<string, string | undefined>;\n\n/** Reduce a Laravel 422 error bag (arrays of messages) to the first per field. */\nexport function firstErrors(errors: Record<string, string[] | string> | undefined): FieldErrors {\n const result: FieldErrors = {};\n\n for (const [key, value] of Object.entries(errors ?? {})) {\n result[key] = Array.isArray(value) ? value[0] : value;\n }\n\n return result;\n}\n"],"mappings":";;AAGA,SAAgB,YAAY,QAAoE;CAC9F,MAAM,SAAsB,CAAC;CAE7B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,CAAC,CAAC,GACpD,OAAO,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;CAGlD,OAAO;AACT"}
1
+ {"version":3,"file":"field-errors.js","names":[],"sources":["../../../resources/js/form/lib/field-errors.ts"],"sourcesContent":["export type FieldErrors = Record<string, string | undefined>;\n\n/** Whether an error-bag key targets the named field itself or a path nested under it. */\nexport function errorKeyBelongsTo(key: string, name: string): boolean {\n return key === name || key.startsWith(`${name}.`);\n}\n\n/** Reduce a Laravel 422 error bag (arrays of messages) to the first per field. */\nexport function firstErrors(errors: Record<string, string[] | string> | undefined): FieldErrors {\n const result: FieldErrors = {};\n\n for (const [key, value] of Object.entries(errors ?? {})) {\n result[key] = Array.isArray(value) ? value[0] : value;\n }\n\n return result;\n}\n"],"mappings":";;AAGA,SAAgB,kBAAkB,KAAa,MAAuB;CACpE,OAAO,QAAQ,QAAQ,IAAI,WAAW,GAAG,KAAK,EAAE;AAClD;;AAGA,SAAgB,YAAY,QAAoE;CAC9F,MAAM,SAAsB,CAAC;CAE7B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,CAAC,CAAC,GACpD,OAAO,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;CAGlD,OAAO;AACT"}
@@ -8,6 +8,12 @@ import { ComponentPropsMap } from '../../types/generated.js';
8
8
  * a generated field type (every field bakes the base in) rather than hand-written.
9
9
  */
10
10
  type FieldProps = Partial<Pick<ComponentPropsMap["field.text-input"], "conditions" | "dependsOnAny" | "dependsOnKeys" | "disabled" | "editablePrefill" | "helperText" | "label" | "name" | "prefillRefreshOn" | "prefillResetOn" | "readOnly" | "required" | "tooltip" | "value">>;
11
+ /**
12
+ * Field types whose value is a collection of rows. Schema walkers must not
13
+ * descend into their child schemas as top-level fields; children live under
14
+ * `name.<index>.` paths instead.
15
+ */
16
+ export declare const ROW_FIELD_TYPES: Set<string>;
11
17
  export declare function fieldProps(node: Node): FieldProps;
12
18
  export declare function walkFields(nodes: Node[] | undefined, visit: (props: FieldProps, node: Node) => void): void;
13
19
  export {};
@@ -1,4 +1,10 @@
1
1
  //#region resources/js/form/lib/field-props.ts
2
+ /**
3
+ * Field types whose value is a collection of rows. Schema walkers must not
4
+ * descend into their child schemas as top-level fields; children live under
5
+ * `name.<index>.` paths instead.
6
+ */
7
+ var ROW_FIELD_TYPES = new Set(["field.builder", "field.repeater"]);
2
8
  function fieldProps(node) {
3
9
  return node.props;
4
10
  }
@@ -9,6 +15,6 @@ function walkFields(nodes, visit) {
9
15
  }
10
16
  }
11
17
  //#endregion
12
- export { fieldProps, walkFields };
18
+ export { ROW_FIELD_TYPES, fieldProps, walkFields };
13
19
 
14
20
  //# sourceMappingURL=field-props.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-props.js","names":[],"sources":["../../../resources/js/form/lib/field-props.ts"],"sourcesContent":["import type { Node } from \"@lattice-php/lattice/core/types\";\nimport type { ComponentPropsMap } from \"@lattice-php/lattice/types/generated\";\n\n/**\n * The props every form-field node shares (the PHP Field base). Nodes flow through\n * the form framework loosely typed via the generic schema, so this is the typed\n * lens the shared hooks read them through. Everything is optional because the\n * lens is also applied to non-field nodes while walking the schema. Derived from\n * a generated field type (every field bakes the base in) rather than hand-written.\n */\ntype FieldProps = Partial<\n Pick<\n ComponentPropsMap[\"field.text-input\"],\n | \"conditions\"\n | \"dependsOnAny\"\n | \"dependsOnKeys\"\n | \"disabled\"\n | \"editablePrefill\"\n | \"helperText\"\n | \"label\"\n | \"name\"\n | \"prefillRefreshOn\"\n | \"prefillResetOn\"\n | \"readOnly\"\n | \"required\"\n | \"tooltip\"\n | \"value\"\n >\n>;\n\nexport function fieldProps(node: Node): FieldProps {\n return node.props as FieldProps;\n}\n\nexport function walkFields(\n nodes: Node[] | undefined,\n visit: (props: FieldProps, node: Node) => void,\n): void {\n for (const child of nodes ?? []) {\n visit(fieldProps(child), child);\n walkFields(child.schema, visit);\n }\n}\n"],"mappings":";AA8BA,SAAgB,WAAW,MAAwB;CACjD,OAAO,KAAK;AACd;AAEA,SAAgB,WACd,OACA,OACM;CACN,KAAK,MAAM,SAAS,SAAS,CAAC,GAAG;EAC/B,MAAM,WAAW,KAAK,GAAG,KAAK;EAC9B,WAAW,MAAM,QAAQ,KAAK;CAChC;AACF"}
1
+ {"version":3,"file":"field-props.js","names":[],"sources":["../../../resources/js/form/lib/field-props.ts"],"sourcesContent":["import type { Node } from \"@lattice-php/lattice/core/types\";\nimport type { ComponentPropsMap } from \"@lattice-php/lattice/types/generated\";\n\n/**\n * The props every form-field node shares (the PHP Field base). Nodes flow through\n * the form framework loosely typed via the generic schema, so this is the typed\n * lens the shared hooks read them through. Everything is optional because the\n * lens is also applied to non-field nodes while walking the schema. Derived from\n * a generated field type (every field bakes the base in) rather than hand-written.\n */\ntype FieldProps = Partial<\n Pick<\n ComponentPropsMap[\"field.text-input\"],\n | \"conditions\"\n | \"dependsOnAny\"\n | \"dependsOnKeys\"\n | \"disabled\"\n | \"editablePrefill\"\n | \"helperText\"\n | \"label\"\n | \"name\"\n | \"prefillRefreshOn\"\n | \"prefillResetOn\"\n | \"readOnly\"\n | \"required\"\n | \"tooltip\"\n | \"value\"\n >\n>;\n\n/**\n * Field types whose value is a collection of rows. Schema walkers must not\n * descend into their child schemas as top-level fields; children live under\n * `name.<index>.` paths instead.\n */\nexport const ROW_FIELD_TYPES = new Set([\"field.builder\", \"field.repeater\"]);\n\nexport function fieldProps(node: Node): FieldProps {\n return node.props as FieldProps;\n}\n\nexport function walkFields(\n nodes: Node[] | undefined,\n visit: (props: FieldProps, node: Node) => void,\n): void {\n for (const child of nodes ?? []) {\n visit(fieldProps(child), child);\n walkFields(child.schema, visit);\n }\n}\n"],"mappings":";;;;;;AAmCA,IAAa,kBAAkB,IAAI,IAAI,CAAC,iBAAiB,gBAAgB,CAAC;AAE1E,SAAgB,WAAW,MAAwB;CACjD,OAAO,KAAK;AACd;AAEA,SAAgB,WACd,OACA,OACM;CACN,KAAK,MAAM,SAAS,SAAS,CAAC,GAAG;EAC/B,MAAM,WAAW,KAAK,GAAG,KAAK;EAC9B,WAAW,MAAM,QAAQ,KAAK;CAChC;AACF"}
@@ -1,9 +1,8 @@
1
- import { fieldProps } from "./field-props.js";
1
+ import { ROW_FIELD_TYPES, fieldProps } from "./field-props.js";
2
2
  import { appendPath, getPath } from "./form-path.js";
3
3
  import { buildOverrideKey, rowIdFrom } from "./override-keys.js";
4
4
  import { rowSchemaFor } from "../components/fields/row-templates.js";
5
5
  //#region resources/js/form/lib/prefill-targets.ts
6
- var ROW_COLLECTION_TYPES = new Set(["field.builder", "field.repeater"]);
7
6
  function mapDep(dep, rowPath) {
8
7
  if (dep.startsWith("@")) return dep.slice(1);
9
8
  return rowPath === null ? dep : appendPath(rowPath, dep);
@@ -23,7 +22,7 @@ function collectPrefillTargets(nodes, values) {
23
22
  const targets = [];
24
23
  const walk = (list, rowPath = null, identityRowPath = null, identityCollectionPath = null, index = 0, row = {}) => {
25
24
  for (const node of list ?? []) {
26
- if (ROW_COLLECTION_TYPES.has(node.type)) {
25
+ if (ROW_FIELD_TYPES.has(node.type)) {
27
26
  const name = fieldProps(node).name;
28
27
  if (name) {
29
28
  const childCollectionPath = appendPath(rowPath, name);