@lattice-php/lattice 0.21.0 → 0.22.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 (38) hide show
  1. package/README.md +10 -0
  2. package/dist/action/components/action-form.js +14 -9
  3. package/dist/action/components/action-form.js.map +1 -1
  4. package/dist/action/components/action.js +2 -1
  5. package/dist/action/components/action.js.map +1 -1
  6. package/dist/action/hooks/use-action.js +42 -35
  7. package/dist/action/hooks/use-action.js.map +1 -1
  8. package/dist/action/lib/action-label.d.ts +2 -0
  9. package/dist/action/lib/action-label.js +9 -0
  10. package/dist/action/lib/action-label.js.map +1 -0
  11. package/dist/action/lib/run-action.d.ts +7 -7
  12. package/dist/action/lib/run-action.js +8 -7
  13. package/dist/action/lib/run-action.js.map +1 -1
  14. package/dist/form/components/form.js +3 -1
  15. package/dist/form/components/form.js.map +1 -1
  16. package/dist/format/number.d.ts +1 -1
  17. package/dist/format/number.js.map +1 -1
  18. package/dist/format/value.d.ts +1 -1
  19. package/dist/format/value.js.map +1 -1
  20. package/dist/notifications/components/notifications.js +1 -0
  21. package/dist/notifications/components/notifications.js.map +1 -1
  22. package/dist/table/components/bulk-bar.js +15 -14
  23. package/dist/table/components/bulk-bar.js.map +1 -1
  24. package/dist/table/components/cells/numeric-cell.d.ts +1 -1
  25. package/dist/table/components/cells/numeric-cell.js.map +1 -1
  26. package/dist/table/lib/bulk.js +2 -1
  27. package/dist/table/lib/bulk.js.map +1 -1
  28. package/dist/toast/index.d.ts +1 -1
  29. package/dist/toast/index.js +2 -2
  30. package/dist/types/generated.d.ts +1 -1
  31. package/dist/ui/modal.js +3 -1
  32. package/dist/ui/modal.js.map +1 -1
  33. package/dist-standalone/chunks/{subscriptions-B_ISgA7V.js → subscriptions-DK_s_F2g.js} +1 -1
  34. package/dist-standalone/chunks/use-effect-dispatcher-EVZxmfoo.js +97 -0
  35. package/dist-standalone/lattice.js +15 -15
  36. package/dist-standalone/manifest.json +4 -4
  37. package/package.json +3 -3
  38. package/dist-standalone/chunks/use-effect-dispatcher-D1wPPwHj.js +0 -97
package/README.md CHANGED
@@ -36,6 +36,16 @@ See [Installation](https://latticephp.com/introduction/installation/) for the fu
36
36
 
37
37
  Full documentation, guides, and examples live at **[latticephp.com](https://latticephp.com)**.
38
38
 
39
+ ## Generating types for custom wire classes
40
+
41
+ If you define your own Lattice components, fields, columns, filters, or effects in PHP, regenerate the matching TypeScript so the React side stays in lockstep:
42
+
43
+ ```bash
44
+ php artisan lattice:typescript
45
+ ```
46
+
47
+ Projects that use only Lattice's bundled components never need this — the command detects it and no-ops. Generating custom types requires the dev dependency `spatie/laravel-typescript-transformer`.
48
+
39
49
  ## License
40
50
 
41
51
  The MIT License (MIT). See [LICENSE.md](LICENSE.md) for details.
@@ -1,6 +1,7 @@
1
1
  import { Renderer } from "../../core/renderer.js";
2
2
  import { useT } from "../../i18n/instance.js";
3
- import { dispatchActionError } from "../../effects/dispatch.js";
3
+ import { dispatchActionError, getActionEffects } from "../../effects/dispatch.js";
4
+ import { useEffectDispatcher } from "../../effects/use-effect-dispatcher.js";
4
5
  import { Button } from "../../ui/button.js";
5
6
  import { Dialog, DialogContent, DialogHeader } from "../../ui/dialog.js";
6
7
  import { Spinner } from "../../ui/spinner.js";
@@ -63,6 +64,7 @@ function ActionFormBody({ cancelLabel, componentRef, endpoint, extraData, fieldL
63
64
  const extraDataRef = useRef(extraData);
64
65
  extraDataRef.current = extraData;
65
66
  const { nodes: resolvedNodes, markUserEdit } = useFormResolver(endpoint, componentRef, formNode.schema);
67
+ const dispatch = useEffectDispatcher();
66
68
  const [errors, setErrors] = useState({});
67
69
  const [processing, setProcessing] = useState(false);
68
70
  const request = useCallback((extraHeaders) => apiFetch(endpoint, {
@@ -107,17 +109,20 @@ function ActionFormBody({ cancelLabel, componentRef, endpoint, extraData, fieldL
107
109
  const submit = useCallback(() => {
108
110
  setProcessing(true);
109
111
  request().then(async (response) => {
110
- if (response.status === 422) {
111
- setErrors(firstErrors((await response.json()).errors));
112
- return;
113
- }
114
- if (!response.ok) {
115
- dispatchActionError(/* @__PURE__ */ new Error(`Action request failed with status ${response.status}`));
112
+ const body = await response.json().catch(() => ({}));
113
+ dispatch(getActionEffects(body.effects));
114
+ if (response.status === 422 && body.errors) {
115
+ setErrors(firstErrors(body.errors));
116
116
  return;
117
117
  }
118
- onSuccess(await response.json());
118
+ if (!response.ok) return;
119
+ onSuccess(body);
119
120
  }).catch((error) => dispatchActionError(error)).finally(() => setProcessing(false));
120
- }, [onSuccess, request]);
121
+ }, [
122
+ dispatch,
123
+ onSuccess,
124
+ request
125
+ ]);
121
126
  return /* @__PURE__ */ jsx(FormProvider, {
122
127
  value: useMemo(() => ({
123
128
  action: endpoint,
@@ -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 } from \"@lattice-php/lattice/effects/dispatch\";\nimport type { ActionResponse } from \"@lattice-php/lattice/effects/dispatch\";\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 [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 if (response.status === 422) {\n const body = (await response.json()) as { errors?: Record<string, string[]> };\n setErrors(firstErrors(body.errors));\n\n return;\n }\n\n if (!response.ok) {\n dispatchActionError(new Error(`Action request failed with status ${response.status}`));\n\n return;\n }\n\n onSuccess((await response.json()) as ActionResponse);\n })\n .catch((error: unknown) => dispatchActionError(error))\n .finally(() => setProcessing(false));\n }, [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 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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"}
@@ -2,6 +2,7 @@ import { IconRenderer } from "../../icons/icon-renderer.js";
2
2
  import { prefixedTestId } from "../../core/test-id.js";
3
3
  import { Button } from "../../ui/button.js";
4
4
  import { Spinner } from "../../ui/spinner.js";
5
+ import { actionLabel } from "../lib/action-label.js";
5
6
  import { useAction } from "../hooks/use-action.js";
6
7
  import { actionMenuItemClassName, useActionMenu } from "../../ui/action-menu-context.js";
7
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -9,7 +10,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
10
  var ActionComponent = ({ node }) => {
10
11
  const endpoint = node.props.endpoint ?? "";
11
12
  const icon = node.props.icon;
12
- const label = node.props.label ?? "Run action";
13
+ const label = actionLabel(node);
13
14
  const isMenuItem = useActionMenu();
14
15
  const variant = node.props.variant ?? "default";
15
16
  const { processing, requestSubmit, overlays } = useAction(node);
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","names":[],"sources":["../../../resources/js/action/components/action.tsx"],"sourcesContent":["import { Button } from \"@lattice-php/lattice/ui/button\";\nimport { Spinner } from \"@lattice-php/lattice/ui/spinner\";\nimport { prefixedTestId } from \"@lattice-php/lattice/core/test-id\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { IconRenderer } from \"@lattice-php/lattice/icons\";\nimport {\n actionMenuItemClassName,\n useActionMenu,\n} from \"@lattice-php/lattice/ui/action-menu-context\";\nimport { useAction } from \"@lattice-php/lattice/action/hooks/use-action\";\n\nconst ActionComponent: RendererComponent<\"action\"> = ({ node }) => {\n const endpoint = node.props.endpoint ?? \"\";\n const icon = node.props.icon;\n const label = node.props.label ?? \"Run action\";\n const isMenuItem = useActionMenu();\n const variant = node.props.variant ?? \"default\";\n const { processing, requestSubmit, overlays } = useAction(node);\n const testId = node.key ?? prefixedTestId(\"action\", node.id);\n\n return (\n <>\n <Button\n className={isMenuItem ? actionMenuItemClassName : undefined}\n data-lattice-component={node.id}\n data-test={testId}\n disabled={processing || !endpoint}\n onClick={requestSubmit}\n type=\"button\"\n variant={isMenuItem ? \"ghost\" : variant}\n >\n {processing ? (\n <Spinner className={isMenuItem ? \"size-lt-icon-sm\" : undefined} />\n ) : (\n icon && (\n <IconRenderer\n className={isMenuItem ? \"size-lt-icon-sm\" : \"size-lt-icon-md\"}\n icon={icon}\n />\n )\n )}\n {label}\n </Button>\n\n {overlays}\n </>\n );\n};\n\nexport default ActionComponent;\n"],"mappings":";;;;;;;;AAWA,IAAM,mBAAgD,EAAE,WAAW;CACjE,MAAM,WAAW,KAAK,MAAM,YAAY;CACxC,MAAM,OAAO,KAAK,MAAM;CACxB,MAAM,QAAQ,KAAK,MAAM,SAAS;CAClC,MAAM,aAAa,cAAc;CACjC,MAAM,UAAU,KAAK,MAAM,WAAW;CACtC,MAAM,EAAE,YAAY,eAAe,aAAa,UAAU,IAAI;CAC9D,MAAM,SAAS,KAAK,OAAO,eAAe,UAAU,KAAK,EAAE;CAE3D,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,QAAD;EACE,WAAW,aAAa,0BAA0B,KAAA;EAClD,0BAAwB,KAAK;EAC7B,aAAW;EACX,UAAU,cAAc,CAAC;EACzB,SAAS;EACT,MAAK;EACL,SAAS,aAAa,UAAU;YAPlC,CASG,aACC,oBAAC,SAAD,EAAS,WAAW,aAAa,oBAAoB,KAAA,EAAY,CAAA,IAEjE,QACE,oBAAC,cAAD;GACE,WAAW,aAAa,oBAAoB;GACtC;EACP,CAAA,GAGJ,KACK;KAEP,QACD,EAAA,CAAA;AAEN"}
1
+ {"version":3,"file":"action.js","names":[],"sources":["../../../resources/js/action/components/action.tsx"],"sourcesContent":["import { Button } from \"@lattice-php/lattice/ui/button\";\nimport { Spinner } from \"@lattice-php/lattice/ui/spinner\";\nimport { prefixedTestId } from \"@lattice-php/lattice/core/test-id\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { IconRenderer } from \"@lattice-php/lattice/icons\";\nimport {\n actionMenuItemClassName,\n useActionMenu,\n} from \"@lattice-php/lattice/ui/action-menu-context\";\nimport { useAction } from \"@lattice-php/lattice/action/hooks/use-action\";\nimport { actionLabel } from \"@lattice-php/lattice/action/lib/action-label\";\n\nconst ActionComponent: RendererComponent<\"action\"> = ({ node }) => {\n const endpoint = node.props.endpoint ?? \"\";\n const icon = node.props.icon;\n const label = actionLabel(node);\n const isMenuItem = useActionMenu();\n const variant = node.props.variant ?? \"default\";\n const { processing, requestSubmit, overlays } = useAction(node);\n const testId = node.key ?? prefixedTestId(\"action\", node.id);\n\n return (\n <>\n <Button\n className={isMenuItem ? actionMenuItemClassName : undefined}\n data-lattice-component={node.id}\n data-test={testId}\n disabled={processing || !endpoint}\n onClick={requestSubmit}\n type=\"button\"\n variant={isMenuItem ? \"ghost\" : variant}\n >\n {processing ? (\n <Spinner className={isMenuItem ? \"size-lt-icon-sm\" : undefined} />\n ) : (\n icon && (\n <IconRenderer\n className={isMenuItem ? \"size-lt-icon-sm\" : \"size-lt-icon-md\"}\n icon={icon}\n />\n )\n )}\n {label}\n </Button>\n\n {overlays}\n </>\n );\n};\n\nexport default ActionComponent;\n"],"mappings":";;;;;;;;;AAYA,IAAM,mBAAgD,EAAE,WAAW;CACjE,MAAM,WAAW,KAAK,MAAM,YAAY;CACxC,MAAM,OAAO,KAAK,MAAM;CACxB,MAAM,QAAQ,YAAY,IAAI;CAC9B,MAAM,aAAa,cAAc;CACjC,MAAM,UAAU,KAAK,MAAM,WAAW;CACtC,MAAM,EAAE,YAAY,eAAe,aAAa,UAAU,IAAI;CAC9D,MAAM,SAAS,KAAK,OAAO,eAAe,UAAU,KAAK,EAAE;CAE3D,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,QAAD;EACE,WAAW,aAAa,0BAA0B,KAAA;EAClD,0BAAwB,KAAK;EAC7B,aAAW;EACX,UAAU,cAAc,CAAC;EACzB,SAAS;EACT,MAAK;EACL,SAAS,aAAa,UAAU;YAPlC,CASG,aACC,oBAAC,SAAD,EAAS,WAAW,aAAa,oBAAoB,KAAA,EAAY,CAAA,IAEjE,QACE,oBAAC,cAAD;GACE,WAAW,aAAa,oBAAoB;GACtC;EACP,CAAA,GAGJ,KACK;KAEP,QACD,EAAA,CAAA;AAEN"}
@@ -1,10 +1,12 @@
1
1
  import { withHeaders } from "../../core/headers.js";
2
- import { getActionEffects } from "../../effects/dispatch.js";
2
+ import { translate } from "../../i18n/instance.js";
3
3
  import { useEffectDispatcher } from "../../effects/use-effect-dispatcher.js";
4
4
  import { ConfirmDialog } from "../../ui/confirm-dialog.js";
5
+ import { apiFetch } from "../../core/api.js";
5
6
  import { runAction } from "../lib/run-action.js";
6
7
  import { ActionForm, useLazyActionForm } from "../components/action-form.js";
7
- import { router, useHttp } from "@inertiajs/react";
8
+ import { actionLabel } from "../lib/action-label.js";
9
+ import { router } from "@inertiajs/react";
8
10
  import { useState } from "react";
9
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
12
  //#region resources/js/action/hooks/use-action.tsx
@@ -18,13 +20,13 @@ function useAction(node) {
18
20
  const endpoint = node.props.endpoint ?? "";
19
21
  const componentRef = node.props.ref ?? "";
20
22
  const method = node.props.method ?? "post";
21
- const label = node.props.label ?? "Run action";
23
+ const label = actionLabel(node);
22
24
  const variant = node.props.variant ?? "default";
23
25
  const confirmation = node.props.confirmation;
24
26
  const inlineForm = node.props.form;
25
27
  const lazyForm = node.props.lazyForm === true;
26
28
  const hasForm = Boolean(inlineForm) || lazyForm;
27
- const http = useHttp({});
29
+ const [processing, setProcessing] = useState(false);
28
30
  const dispatch = useEffectDispatcher();
29
31
  const [isConfirming, setIsConfirming] = useState(false);
30
32
  const [isFilling, setIsFilling] = useState(false);
@@ -37,7 +39,14 @@ function useAction(node) {
37
39
  setIsConfirming(false);
38
40
  return;
39
41
  }
40
- if (await runAction(() => http[method](endpoint, { headers: withHeaders(componentRef) }), dispatch)) setIsConfirming(false);
42
+ setProcessing(true);
43
+ const ok = await runAction(() => apiFetch(endpoint, {
44
+ method,
45
+ ref: componentRef,
46
+ throwOnError: false
47
+ }), dispatch);
48
+ setProcessing(false);
49
+ if (ok) setIsConfirming(false);
41
50
  };
42
51
  const requestSubmit = () => {
43
52
  if (hasForm) {
@@ -52,38 +61,36 @@ function useAction(node) {
52
61
  };
53
62
  const confirmationTitle = confirmation?.title ?? label;
54
63
  const confirmationConfirmLabel = confirmation?.confirmLabel ?? label;
55
- const confirmationCancelLabel = confirmation?.cancelLabel ?? "Cancel";
56
- const overlays = /* @__PURE__ */ jsxs(Fragment$1, { children: [isConfirming && confirmation && /* @__PURE__ */ jsx(ConfirmDialog, {
57
- title: confirmationTitle,
58
- description: confirmation.description ?? void 0,
59
- confirmLabel: confirmationConfirmLabel,
60
- cancelLabel: confirmationCancelLabel,
61
- confirmVariant: variant,
62
- processing: http.processing,
63
- confirmDisabled: !endpoint,
64
- onConfirm: () => void submit(),
65
- onCancel: () => setIsConfirming(false)
66
- }), isFilling && hasForm && /* @__PURE__ */ jsx(ActionForm, {
67
- cancelLabel: confirmationCancelLabel,
68
- componentRef,
69
- description: confirmation?.description ?? void 0,
70
- endpoint,
71
- formNode,
72
- method,
73
- onClose: () => setIsFilling(false),
74
- onSuccess: (response) => {
75
- dispatch(getActionEffects(response.effects));
76
- setIsFilling(false);
77
- },
78
- placement: node.props.modalSide ?? "center",
79
- submitLabel: confirmationConfirmLabel,
80
- title: confirmationTitle,
81
- width: node.props.modalWidth ?? void 0
82
- })] });
64
+ const confirmationCancelLabel = confirmation?.cancelLabel ?? translate("lattice", "common.cancel", "Cancel");
83
65
  return {
84
- processing: http.processing,
66
+ processing,
85
67
  requestSubmit,
86
- overlays
68
+ overlays: /* @__PURE__ */ jsxs(Fragment$1, { children: [isConfirming && confirmation && /* @__PURE__ */ jsx(ConfirmDialog, {
69
+ title: confirmationTitle,
70
+ description: confirmation.description ?? void 0,
71
+ confirmLabel: confirmationConfirmLabel,
72
+ cancelLabel: confirmationCancelLabel,
73
+ confirmVariant: variant,
74
+ processing,
75
+ confirmDisabled: !endpoint,
76
+ onConfirm: () => void submit(),
77
+ onCancel: () => setIsConfirming(false)
78
+ }), isFilling && hasForm && /* @__PURE__ */ jsx(ActionForm, {
79
+ cancelLabel: confirmationCancelLabel,
80
+ componentRef,
81
+ description: confirmation?.description ?? void 0,
82
+ endpoint,
83
+ formNode,
84
+ method,
85
+ onClose: () => setIsFilling(false),
86
+ onSuccess: () => {
87
+ setIsFilling(false);
88
+ },
89
+ placement: node.props.modalSide ?? "center",
90
+ submitLabel: confirmationConfirmLabel,
91
+ title: confirmationTitle,
92
+ width: node.props.modalWidth ?? void 0
93
+ })] })
87
94
  };
88
95
  }
89
96
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-action.js","names":[],"sources":["../../../resources/js/action/hooks/use-action.tsx"],"sourcesContent":["import { router, useHttp } from \"@inertiajs/react\";\nimport type { Method } from \"@inertiajs/core\";\nimport { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ConfirmDialog } from \"@lattice-php/lattice/ui/confirm-dialog\";\nimport { withHeaders } from \"@lattice-php/lattice/core/headers\";\nimport type { Node } from \"@lattice-php/lattice/core/types\";\nimport { 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\";\nimport { runAction } from \"@lattice-php/lattice/action/lib/run-action\";\nimport { ActionForm, useLazyActionForm } from \"@lattice-php/lattice/action/components/action-form\";\n\ntype UseAction = {\n /** Whether the action request is in flight. */\n processing: boolean;\n /** Gate then run the action: open the form, confirm, or dispatch directly. */\n requestSubmit: () => void;\n /** The confirm dialog and action form rendered next to the trigger. */\n overlays: ReactNode;\n};\n\n/**\n * The shared action machinery behind the Action button, action menu items, and\n * action links: it gates submission (form → modal, confirmation → confirm,\n * otherwise dispatch) and renders the matching overlays. The host owns the\n * trigger element so each surface keeps its own styling.\n */\nexport function useAction(node: Node<\"action\" | \"action.bulk\">): UseAction {\n const endpoint = node.props.endpoint ?? \"\";\n const componentRef = node.props.ref ?? \"\";\n const method: Method = node.props.method ?? \"post\";\n const label = node.props.label ?? \"Run action\";\n const variant = node.props.variant ?? \"default\";\n const confirmation = node.props.confirmation;\n const inlineForm = node.props.form;\n const lazyForm = node.props.lazyForm === true;\n const hasForm = Boolean(inlineForm) || lazyForm;\n\n const http = useHttp<Record<string, never>, ActionResponse>({});\n const dispatch = useEffectDispatcher();\n const [isConfirming, setIsConfirming] = useState(false);\n const [isFilling, setIsFilling] = useState(false);\n const lazyNode = useLazyActionForm(endpoint, componentRef, isFilling && lazyForm);\n const formNode = lazyForm ? lazyNode : inlineForm;\n\n const submit = async (): Promise<void> => {\n if (!endpoint) {\n return;\n }\n\n if (method === \"get\") {\n router.visit(endpoint, { headers: withHeaders(componentRef) });\n setIsConfirming(false);\n\n return;\n }\n\n const ok = await runAction(\n () => http[method](endpoint, { headers: withHeaders(componentRef) }),\n dispatch,\n );\n\n if (ok) {\n setIsConfirming(false);\n }\n };\n\n const requestSubmit = (): void => {\n if (hasForm) {\n setIsFilling(true);\n\n return;\n }\n\n if (confirmation) {\n setIsConfirming(true);\n\n return;\n }\n\n void submit();\n };\n\n const confirmationTitle = confirmation?.title ?? label;\n const confirmationConfirmLabel = confirmation?.confirmLabel ?? label;\n const confirmationCancelLabel = confirmation?.cancelLabel ?? \"Cancel\";\n\n const overlays = (\n <>\n {isConfirming && confirmation && (\n <ConfirmDialog\n title={confirmationTitle}\n description={confirmation.description ?? undefined}\n confirmLabel={confirmationConfirmLabel}\n cancelLabel={confirmationCancelLabel}\n confirmVariant={variant}\n processing={http.processing}\n confirmDisabled={!endpoint}\n onConfirm={() => void submit()}\n onCancel={() => setIsConfirming(false)}\n />\n )}\n\n {isFilling && hasForm && (\n <ActionForm\n cancelLabel={confirmationCancelLabel}\n componentRef={componentRef}\n description={confirmation?.description ?? undefined}\n endpoint={endpoint}\n formNode={formNode}\n method={method}\n onClose={() => setIsFilling(false)}\n onSuccess={(response) => {\n dispatch(getActionEffects(response.effects));\n setIsFilling(false);\n }}\n placement={node.props.modalSide ?? \"center\"}\n submitLabel={confirmationConfirmLabel}\n title={confirmationTitle}\n width={node.props.modalWidth ?? undefined}\n />\n )}\n </>\n );\n\n return { processing: http.processing, requestSubmit, overlays };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA4BA,SAAgB,UAAU,MAAiD;CACzE,MAAM,WAAW,KAAK,MAAM,YAAY;CACxC,MAAM,eAAe,KAAK,MAAM,OAAO;CACvC,MAAM,SAAiB,KAAK,MAAM,UAAU;CAC5C,MAAM,QAAQ,KAAK,MAAM,SAAS;CAClC,MAAM,UAAU,KAAK,MAAM,WAAW;CACtC,MAAM,eAAe,KAAK,MAAM;CAChC,MAAM,aAAa,KAAK,MAAM;CAC9B,MAAM,WAAW,KAAK,MAAM,aAAa;CACzC,MAAM,UAAU,QAAQ,UAAU,KAAK;CAEvC,MAAM,OAAO,QAA+C,CAAC,CAAC;CAC9D,MAAM,WAAW,oBAAoB;CACrC,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,WAAW,kBAAkB,UAAU,cAAc,aAAa,QAAQ;CAChF,MAAM,WAAW,WAAW,WAAW;CAEvC,MAAM,SAAS,YAA2B;EACxC,IAAI,CAAC,UACH;EAGF,IAAI,WAAW,OAAO;GACpB,OAAO,MAAM,UAAU,EAAE,SAAS,YAAY,YAAY,EAAE,CAAC;GAC7D,gBAAgB,KAAK;GAErB;EACF;EAOA,IAAI,MALa,gBACT,KAAK,QAAQ,UAAU,EAAE,SAAS,YAAY,YAAY,EAAE,CAAC,GACnE,QACF,GAGE,gBAAgB,KAAK;CAEzB;CAEA,MAAM,sBAA4B;EAChC,IAAI,SAAS;GACX,aAAa,IAAI;GAEjB;EACF;EAEA,IAAI,cAAc;GAChB,gBAAgB,IAAI;GAEpB;EACF;EAEA,OAAY;CACd;CAEA,MAAM,oBAAoB,cAAc,SAAS;CACjD,MAAM,2BAA2B,cAAc,gBAAgB;CAC/D,MAAM,0BAA0B,cAAc,eAAe;CAE7D,MAAM,WACJ,qBAAA,YAAA,EAAA,UAAA,CACG,gBAAgB,gBACf,oBAAC,eAAD;EACE,OAAO;EACP,aAAa,aAAa,eAAe,KAAA;EACzC,cAAc;EACd,aAAa;EACb,gBAAgB;EAChB,YAAY,KAAK;EACjB,iBAAiB,CAAC;EAClB,iBAAiB,KAAK,OAAO;EAC7B,gBAAgB,gBAAgB,KAAK;CACtC,CAAA,GAGF,aAAa,WACZ,oBAAC,YAAD;EACE,aAAa;EACC;EACd,aAAa,cAAc,eAAe,KAAA;EAChC;EACA;EACF;EACR,eAAe,aAAa,KAAK;EACjC,YAAY,aAAa;GACvB,SAAS,iBAAiB,SAAS,OAAO,CAAC;GAC3C,aAAa,KAAK;EACpB;EACA,WAAW,KAAK,MAAM,aAAa;EACnC,aAAa;EACb,OAAO;EACP,OAAO,KAAK,MAAM,cAAc,KAAA;CACjC,CAAA,CAEH,EAAA,CAAA;CAGJ,OAAO;EAAE,YAAY,KAAK;EAAY;EAAe;CAAS;AAChE"}
1
+ {"version":3,"file":"use-action.js","names":[],"sources":["../../../resources/js/action/hooks/use-action.tsx"],"sourcesContent":["import { router } from \"@inertiajs/react\";\nimport type { Method } from \"@inertiajs/core\";\nimport { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ConfirmDialog } from \"@lattice-php/lattice/ui/confirm-dialog\";\nimport { apiFetch } from \"@lattice-php/lattice/core/api\";\nimport { withHeaders } from \"@lattice-php/lattice/core/headers\";\nimport type { Node } from \"@lattice-php/lattice/core/types\";\nimport { translate } from \"@lattice-php/lattice/i18n\";\nimport { useEffectDispatcher } from \"@lattice-php/lattice/effects/use-effect-dispatcher\";\nimport { runAction } from \"@lattice-php/lattice/action/lib/run-action\";\nimport { ActionForm, useLazyActionForm } from \"@lattice-php/lattice/action/components/action-form\";\nimport { actionLabel } from \"@lattice-php/lattice/action/lib/action-label\";\n\ntype UseAction = {\n /** Whether the action request is in flight. */\n processing: boolean;\n /** Gate then run the action: open the form, confirm, or dispatch directly. */\n requestSubmit: () => void;\n /** The confirm dialog and action form rendered next to the trigger. */\n overlays: ReactNode;\n};\n\n/**\n * The shared action machinery behind the Action button, action menu items, and\n * action links: it gates submission (form → modal, confirmation → confirm,\n * otherwise dispatch) and renders the matching overlays. The host owns the\n * trigger element so each surface keeps its own styling.\n */\nexport function useAction(node: Node<\"action\" | \"action.bulk\">): UseAction {\n const endpoint = node.props.endpoint ?? \"\";\n const componentRef = node.props.ref ?? \"\";\n const method: Method = node.props.method ?? \"post\";\n const label = actionLabel(node);\n const variant = node.props.variant ?? \"default\";\n const confirmation = node.props.confirmation;\n const inlineForm = node.props.form;\n const lazyForm = node.props.lazyForm === true;\n const hasForm = Boolean(inlineForm) || lazyForm;\n\n const [processing, setProcessing] = useState(false);\n const dispatch = useEffectDispatcher();\n const [isConfirming, setIsConfirming] = useState(false);\n const [isFilling, setIsFilling] = useState(false);\n const lazyNode = useLazyActionForm(endpoint, componentRef, isFilling && lazyForm);\n const formNode = lazyForm ? lazyNode : inlineForm;\n\n const submit = async (): Promise<void> => {\n if (!endpoint) {\n return;\n }\n\n if (method === \"get\") {\n router.visit(endpoint, { headers: withHeaders(componentRef) });\n setIsConfirming(false);\n\n return;\n }\n\n setProcessing(true);\n\n const ok = await runAction(\n () => apiFetch(endpoint, { method, ref: componentRef, throwOnError: false }),\n dispatch,\n );\n\n setProcessing(false);\n\n if (ok) {\n setIsConfirming(false);\n }\n };\n\n const requestSubmit = (): void => {\n if (hasForm) {\n setIsFilling(true);\n\n return;\n }\n\n if (confirmation) {\n setIsConfirming(true);\n\n return;\n }\n\n void submit();\n };\n\n const confirmationTitle = confirmation?.title ?? label;\n const confirmationConfirmLabel = confirmation?.confirmLabel ?? label;\n const confirmationCancelLabel =\n confirmation?.cancelLabel ?? translate(\"lattice\", \"common.cancel\", \"Cancel\");\n\n const overlays = (\n <>\n {isConfirming && confirmation && (\n <ConfirmDialog\n title={confirmationTitle}\n description={confirmation.description ?? undefined}\n confirmLabel={confirmationConfirmLabel}\n cancelLabel={confirmationCancelLabel}\n confirmVariant={variant}\n processing={processing}\n confirmDisabled={!endpoint}\n onConfirm={() => void submit()}\n onCancel={() => setIsConfirming(false)}\n />\n )}\n\n {isFilling && hasForm && (\n <ActionForm\n cancelLabel={confirmationCancelLabel}\n componentRef={componentRef}\n description={confirmation?.description ?? undefined}\n endpoint={endpoint}\n formNode={formNode}\n method={method}\n onClose={() => setIsFilling(false)}\n onSuccess={() => {\n setIsFilling(false);\n }}\n placement={node.props.modalSide ?? \"center\"}\n submitLabel={confirmationConfirmLabel}\n title={confirmationTitle}\n width={node.props.modalWidth ?? undefined}\n />\n )}\n </>\n );\n\n return { processing, requestSubmit, overlays };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAgB,UAAU,MAAiD;CACzE,MAAM,WAAW,KAAK,MAAM,YAAY;CACxC,MAAM,eAAe,KAAK,MAAM,OAAO;CACvC,MAAM,SAAiB,KAAK,MAAM,UAAU;CAC5C,MAAM,QAAQ,YAAY,IAAI;CAC9B,MAAM,UAAU,KAAK,MAAM,WAAW;CACtC,MAAM,eAAe,KAAK,MAAM;CAChC,MAAM,aAAa,KAAK,MAAM;CAC9B,MAAM,WAAW,KAAK,MAAM,aAAa;CACzC,MAAM,UAAU,QAAQ,UAAU,KAAK;CAEvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,KAAK;CAClD,MAAM,WAAW,oBAAoB;CACrC,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,WAAW,kBAAkB,UAAU,cAAc,aAAa,QAAQ;CAChF,MAAM,WAAW,WAAW,WAAW;CAEvC,MAAM,SAAS,YAA2B;EACxC,IAAI,CAAC,UACH;EAGF,IAAI,WAAW,OAAO;GACpB,OAAO,MAAM,UAAU,EAAE,SAAS,YAAY,YAAY,EAAE,CAAC;GAC7D,gBAAgB,KAAK;GAErB;EACF;EAEA,cAAc,IAAI;EAElB,MAAM,KAAK,MAAM,gBACT,SAAS,UAAU;GAAE;GAAQ,KAAK;GAAc,cAAc;EAAM,CAAC,GAC3E,QACF;EAEA,cAAc,KAAK;EAEnB,IAAI,IACF,gBAAgB,KAAK;CAEzB;CAEA,MAAM,sBAA4B;EAChC,IAAI,SAAS;GACX,aAAa,IAAI;GAEjB;EACF;EAEA,IAAI,cAAc;GAChB,gBAAgB,IAAI;GAEpB;EACF;EAEA,OAAY;CACd;CAEA,MAAM,oBAAoB,cAAc,SAAS;CACjD,MAAM,2BAA2B,cAAc,gBAAgB;CAC/D,MAAM,0BACJ,cAAc,eAAe,UAAU,WAAW,iBAAiB,QAAQ;CAuC7E,OAAO;EAAE;EAAY;EAAe,UApClC,qBAAA,YAAA,EAAA,UAAA,CACG,gBAAgB,gBACf,oBAAC,eAAD;GACE,OAAO;GACP,aAAa,aAAa,eAAe,KAAA;GACzC,cAAc;GACd,aAAa;GACb,gBAAgB;GACJ;GACZ,iBAAiB,CAAC;GAClB,iBAAiB,KAAK,OAAO;GAC7B,gBAAgB,gBAAgB,KAAK;EACtC,CAAA,GAGF,aAAa,WACZ,oBAAC,YAAD;GACE,aAAa;GACC;GACd,aAAa,cAAc,eAAe,KAAA;GAChC;GACA;GACF;GACR,eAAe,aAAa,KAAK;GACjC,iBAAiB;IACf,aAAa,KAAK;GACpB;GACA,WAAW,KAAK,MAAM,aAAa;GACnC,aAAa;GACb,OAAO;GACP,OAAO,KAAK,MAAM,cAAc,KAAA;EACjC,CAAA,CAEH,EAAA,CAGgC;CAAS;AAC/C"}
@@ -0,0 +1,2 @@
1
+ import { Node } from '../../core/types.js';
2
+ export declare function actionLabel(node: Node<"action" | "action.bulk">): string;
@@ -0,0 +1,9 @@
1
+ import { translate } from "../../i18n/instance.js";
2
+ //#region resources/js/action/lib/action-label.ts
3
+ function actionLabel(node) {
4
+ return node.props.label ?? translate("lattice", "common.action.run", "Run action");
5
+ }
6
+ //#endregion
7
+ export { actionLabel };
8
+
9
+ //# sourceMappingURL=action-label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-label.js","names":[],"sources":["../../../resources/js/action/lib/action-label.ts"],"sourcesContent":["import { translate } from \"@lattice-php/lattice/i18n\";\nimport type { Node } from \"@lattice-php/lattice/core/types\";\n\nexport function actionLabel(node: Node<\"action\" | \"action.bulk\">): string {\n return node.props.label ?? translate(\"lattice\", \"common.action.run\", \"Run action\");\n}\n"],"mappings":";;AAGA,SAAgB,YAAY,MAA8C;CACxE,OAAO,KAAK,MAAM,SAAS,UAAU,WAAW,qBAAqB,YAAY;AACnF"}
@@ -1,9 +1,9 @@
1
- import { ActionEffect, ActionResponse } from '../../effects/dispatch.js';
1
+ import { ActionEffect } from '../../effects/dispatch.js';
2
2
  /**
3
- * Runs an action request and dispatches the effects from its response, routing
4
- * any failure through dispatchActionError. Returns whether the request
5
- * succeeded so callers run their own post-success cleanup (closing a dialog,
6
- * reloading a table). Shared by the single action button and the bulk bar so
7
- * the invocation contract stays in one place.
3
+ * Runs an action request and dispatches the effects from its response body,
4
+ * whether the action succeeded or was rejected (non-2xx). Returns whether the
5
+ * response was ok so callers run their own post-success cleanup (closing a
6
+ * dialog, reloading a table) only on success; a rejected action leaves the
7
+ * dialog open. A thrown/network error routes through dispatchActionError.
8
8
  */
9
- export declare function runAction(request: () => Promise<ActionResponse>, dispatch: (effects: ActionEffect[]) => void): Promise<boolean>;
9
+ export declare function runAction(request: () => Promise<Response>, dispatch: (effects: ActionEffect[]) => void): Promise<boolean>;
@@ -1,16 +1,17 @@
1
1
  import { dispatchActionError, getActionEffects } from "../../effects/dispatch.js";
2
2
  //#region resources/js/action/lib/run-action.ts
3
3
  /**
4
- * Runs an action request and dispatches the effects from its response, routing
5
- * any failure through dispatchActionError. Returns whether the request
6
- * succeeded so callers run their own post-success cleanup (closing a dialog,
7
- * reloading a table). Shared by the single action button and the bulk bar so
8
- * the invocation contract stays in one place.
4
+ * Runs an action request and dispatches the effects from its response body,
5
+ * whether the action succeeded or was rejected (non-2xx). Returns whether the
6
+ * response was ok so callers run their own post-success cleanup (closing a
7
+ * dialog, reloading a table) only on success; a rejected action leaves the
8
+ * dialog open. A thrown/network error routes through dispatchActionError.
9
9
  */
10
10
  async function runAction(request, dispatch) {
11
11
  try {
12
- dispatch(getActionEffects((await request()).effects));
13
- return true;
12
+ const response = await request();
13
+ dispatch(getActionEffects((await response.json().catch(() => ({}))).effects));
14
+ return response.ok;
14
15
  } catch (error) {
15
16
  dispatchActionError(error);
16
17
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"run-action.js","names":[],"sources":["../../../resources/js/action/lib/run-action.ts"],"sourcesContent":["import {\n type ActionEffect,\n type ActionResponse,\n dispatchActionError,\n getActionEffects,\n} from \"@lattice-php/lattice/effects/dispatch\";\n\n/**\n * Runs an action request and dispatches the effects from its response, routing\n * any failure through dispatchActionError. Returns whether the request\n * succeeded so callers run their own post-success cleanup (closing a dialog,\n * reloading a table). Shared by the single action button and the bulk bar so\n * the invocation contract stays in one place.\n */\nexport async function runAction(\n request: () => Promise<ActionResponse>,\n dispatch: (effects: ActionEffect[]) => void,\n): Promise<boolean> {\n try {\n const response = await request();\n dispatch(getActionEffects(response.effects));\n\n return true;\n } catch (error) {\n dispatchActionError(error);\n\n return false;\n }\n}\n"],"mappings":";;;;;;;;;AAcA,eAAsB,UACpB,SACA,UACkB;CAClB,IAAI;EAEF,SAAS,kBAAiB,MADH,QAAQ,GACI,OAAO,CAAC;EAE3C,OAAO;CACT,SAAS,OAAO;EACd,oBAAoB,KAAK;EAEzB,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"run-action.js","names":[],"sources":["../../../resources/js/action/lib/run-action.ts"],"sourcesContent":["import {\n type ActionEffect,\n type ActionResponse,\n dispatchActionError,\n getActionEffects,\n} from \"@lattice-php/lattice/effects/dispatch\";\n\n/**\n * Runs an action request and dispatches the effects from its response body,\n * whether the action succeeded or was rejected (non-2xx). Returns whether the\n * response was ok so callers run their own post-success cleanup (closing a\n * dialog, reloading a table) only on success; a rejected action leaves the\n * dialog open. A thrown/network error routes through dispatchActionError.\n */\nexport async function runAction(\n request: () => Promise<Response>,\n dispatch: (effects: ActionEffect[]) => void,\n): Promise<boolean> {\n try {\n const response = await request();\n const body = (await response.json().catch(() => ({}))) as ActionResponse;\n dispatch(getActionEffects(body.effects));\n\n return response.ok;\n } catch (error) {\n dispatchActionError(error);\n\n return false;\n }\n}\n"],"mappings":";;;;;;;;;AAcA,eAAsB,UACpB,SACA,UACkB;CAClB,IAAI;EACF,MAAM,WAAW,MAAM,QAAQ;EAE/B,SAAS,kBAAiB,MADN,SAAS,KAAK,EAAE,aAAa,CAAC,EAAE,GACrB,OAAO,CAAC;EAEvC,OAAO,SAAS;CAClB,SAAS,OAAO;EACd,oBAAoB,KAAK;EAEzB,OAAO;CACT;AACF"}
@@ -1,6 +1,7 @@
1
1
  import { LATTICE_EVENT } from "../../core/event-names.js";
2
2
  import { useWindowEvent } from "../../core/hooks/use-window-event.js";
3
3
  import { withHeaders } from "../../core/headers.js";
4
+ import { useT } from "../../i18n/instance.js";
4
5
  import { FormProvider } from "../hooks/context.js";
5
6
  import { PrefillProvider } from "../hooks/prefill-context.js";
6
7
  import { ResolvedNodesProvider } from "../hooks/resolved-nodes.js";
@@ -39,6 +40,7 @@ function FormBody({ action, children, componentRef, nodes, shouldRenderSubmitBut
39
40
  });
40
41
  }
41
42
  var FormComponent = ({ children, node }) => {
43
+ const { t } = useT("lattice");
42
44
  const props = node.props;
43
45
  const action = props.action ?? "#";
44
46
  const errorBag = props.errorBag;
@@ -54,7 +56,7 @@ var FormComponent = ({ children, node }) => {
54
56
  ...state
55
57
  }), [fieldValues, state]);
56
58
  const shouldRenderSubmitButton = props.submitButton;
57
- const submitLabel = props.submitLabel ?? "Submit";
59
+ const submitLabel = props.submitLabel ?? t("form.submit", "Submit");
58
60
  const summaryLabel = props.validationSummaryLabel;
59
61
  const validationTimeout = props.validationTimeout ?? void 0;
60
62
  return /* @__PURE__ */ jsx(Form, {
@@ -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 type { Node, RendererComponent } from \"@lattice-php/lattice/core/types\";\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\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 submitLabel,\n summaryLabel,\n}: {\n action: string;\n children: React.ReactNode;\n componentRef: string;\n nodes: Node[] | undefined;\n shouldRenderSubmitButton: boolean;\n submitLabel: string;\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 justify-end rounded-lt border border-lt-border bg-lt-surface px-lt-gutter py-4 shadow-lt-sm\">\n <FormSubmitButton label={submitLabel} summaryLabel={summaryLabel} />\n </div>\n )}\n </div>\n </ResolvedNodesProvider>\n </PrefillProvider>\n );\n}\n\nexport const FormComponent: RendererComponent<\"form\"> = ({ children, node }) => {\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 submitLabel = props.submitLabel ?? \"Submit\";\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 submitLabel={submitLabel}\n summaryLabel={summaryLabel}\n >\n {children}\n </FormBody>\n </FormValuesProvider>\n </FormProvider>\n )}\n </InertiaForm>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAS,kBAAkB,EACzB,aACA,SAIC;CACD,eAAe,cAAc,YAAY,UAAU;EACjD,MAAM,SAAU,MAA+C;EAE/D,IAAI,CAAC,QAAQ,QAAQ,OAAO,SAAS,aACnC,MAAM;CAEV,CAAC;CAED,OAAO;AACT;AAEA,SAAS,SAAS,EAChB,QACA,UACA,cACA,OACA,0BACA,aACA,gBASC;CACD,MAAM,EAAE,OAAO,eAAe,iBAAiB,gBAAgB,QAAQ,cAAc,KAAK;CAE1F,OACE,oBAAC,iBAAD;EAAiB,OAAO,EAAE,aAAa;YACrC,oBAAC,uBAAD;GAAuB,OAAO;aAC5B,qBAAC,OAAD;IAAK,WAAU;cAAf,CACG,UAEA,4BACC,oBAAC,OAAD;KAAK,WAAU;eACb,oBAAC,kBAAD;MAAkB,OAAO;MAA2B;KAAe,CAAA;IAChE,CAAA,CAEJ;;EACgB,CAAA;CACR,CAAA;AAErB;AAEA,IAAa,iBAA4C,EAAE,UAAU,WAAW;CAC9E,MAAM,QAAQ,KAAK;CACnB,MAAM,SAAS,MAAM,UAAU;CAC/B,MAAM,WAAW,MAAM;CACvB,MAAM,eAAe,MAAM,OAAO;CAClC,MAAM,SAAS,MAAM,UAAU;CAC/B,MAAM,eAAe,MAAM;CAC3B,MAAM,eAAe,MAAM,gBAAgB;CAC3C,MAAM,iBAAiB,MAAM,kBAAkB,CAAC;CAChD,MAAM,QAAQ,MAAM;CACpB,MAAM,EAAE,QAAQ,aAAa,QAAQ,gBAAgB,cAC7C,cAAc,KAAK,MAAM,GAC/B,CAAC,KAAK,MAAM,CACd;CACA,MAAM,gBAAgB,eAAe;EAAE,GAAG;EAAa,GAAG;CAAM,IAAI,CAAC,aAAa,KAAK,CAAC;CACxF,MAAM,2BAA2B,MAAM;CACvC,MAAM,cAAc,MAAM,eAAe;CACzC,MAAM,eAAe,MAAM;CAC3B,MAAM,oBAAoB,MAAM,qBAAqB,KAAA;CAErD,OACE,oBAAC,MAAD;EACU;EACR,aAAU;EACV,0BAAwB,KAAK;EACnB;EACF;EACM;EACE;EAChB,mBAAmB,eAAe,oBAAoB,KAAA;EACtD,SAAS,YAAY,YAAY;EACjC,WAAU;aAER,EAAE,aAAa,QAAQ,YAAY,OAAO,eAC1C,qBAAC,cAAD;GACE,OAAO;IACL;IACA,cAAc,UAAU,YAAY,KAAK;IACzC,aAAa,KAAK;IAClB;IACQ;IACR;IACA;IACA;IACA,WAAW,UAAU,SAAS,KAAK;GACrC;aAXF;IAaE,oBAAC,mBAAD;KAAmB,aAAa,KAAK;KAAW;IAAQ,CAAA;IAEvD,MAAM,UACL,oBAAC,OAAD;KAAK,WAAU;eAAmD,MAAM;IAAY,CAAA;IAGtF,oBAAC,oBAAD;KAAoB,SAAS;eAC3B,oBAAC,UAAD;MACU;MACM;MACd,OAAO,KAAK;MACc;MACb;MACC;MAEb;KACO,CAAA;IACQ,CAAA;GACR;;CAEL,CAAA;AAEjB"}
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 type { Node, RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\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\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 submitLabel,\n summaryLabel,\n}: {\n action: string;\n children: React.ReactNode;\n componentRef: string;\n nodes: Node[] | undefined;\n shouldRenderSubmitButton: boolean;\n submitLabel: string;\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 justify-end rounded-lt border border-lt-border bg-lt-surface px-lt-gutter py-4 shadow-lt-sm\">\n <FormSubmitButton label={submitLabel} summaryLabel={summaryLabel} />\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 submitLabel = props.submitLabel ?? t(\"form.submit\", \"Submit\");\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 submitLabel={submitLabel}\n summaryLabel={summaryLabel}\n >\n {children}\n </FormBody>\n </FormValuesProvider>\n </FormProvider>\n )}\n </InertiaForm>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAS,kBAAkB,EACzB,aACA,SAIC;CACD,eAAe,cAAc,YAAY,UAAU;EACjD,MAAM,SAAU,MAA+C;EAE/D,IAAI,CAAC,QAAQ,QAAQ,OAAO,SAAS,aACnC,MAAM;CAEV,CAAC;CAED,OAAO;AACT;AAEA,SAAS,SAAS,EAChB,QACA,UACA,cACA,OACA,0BACA,aACA,gBASC;CACD,MAAM,EAAE,OAAO,eAAe,iBAAiB,gBAAgB,QAAQ,cAAc,KAAK;CAE1F,OACE,oBAAC,iBAAD;EAAiB,OAAO,EAAE,aAAa;YACrC,oBAAC,uBAAD;GAAuB,OAAO;aAC5B,qBAAC,OAAD;IAAK,WAAU;cAAf,CACG,UAEA,4BACC,oBAAC,OAAD;KAAK,WAAU;eACb,oBAAC,kBAAD;MAAkB,OAAO;MAA2B;KAAe,CAAA;IAChE,CAAA,CAEJ;;EACgB,CAAA;CACR,CAAA;AAErB;AAEA,IAAa,iBAA4C,EAAE,UAAU,WAAW;CAC9E,MAAM,EAAE,MAAM,KAAK,SAAS;CAC5B,MAAM,QAAQ,KAAK;CACnB,MAAM,SAAS,MAAM,UAAU;CAC/B,MAAM,WAAW,MAAM;CACvB,MAAM,eAAe,MAAM,OAAO;CAClC,MAAM,SAAS,MAAM,UAAU;CAC/B,MAAM,eAAe,MAAM;CAC3B,MAAM,eAAe,MAAM,gBAAgB;CAC3C,MAAM,iBAAiB,MAAM,kBAAkB,CAAC;CAChD,MAAM,QAAQ,MAAM;CACpB,MAAM,EAAE,QAAQ,aAAa,QAAQ,gBAAgB,cAC7C,cAAc,KAAK,MAAM,GAC/B,CAAC,KAAK,MAAM,CACd;CACA,MAAM,gBAAgB,eAAe;EAAE,GAAG;EAAa,GAAG;CAAM,IAAI,CAAC,aAAa,KAAK,CAAC;CACxF,MAAM,2BAA2B,MAAM;CACvC,MAAM,cAAc,MAAM,eAAe,EAAE,eAAe,QAAQ;CAClE,MAAM,eAAe,MAAM;CAC3B,MAAM,oBAAoB,MAAM,qBAAqB,KAAA;CAErD,OACE,oBAAC,MAAD;EACU;EACR,aAAU;EACV,0BAAwB,KAAK;EACnB;EACF;EACM;EACE;EAChB,mBAAmB,eAAe,oBAAoB,KAAA;EACtD,SAAS,YAAY,YAAY;EACjC,WAAU;aAER,EAAE,aAAa,QAAQ,YAAY,OAAO,eAC1C,qBAAC,cAAD;GACE,OAAO;IACL;IACA,cAAc,UAAU,YAAY,KAAK;IACzC,aAAa,KAAK;IAClB;IACQ;IACR;IACA;IACA;IACA,WAAW,UAAU,SAAS,KAAK;GACrC;aAXF;IAaE,oBAAC,mBAAD;KAAmB,aAAa,KAAK;KAAW;IAAQ,CAAA;IAEvD,MAAM,UACL,oBAAC,OAAD;KAAK,WAAU;eAAmD,MAAM;IAAY,CAAA;IAGtF,oBAAC,oBAAD;KAAoB,SAAS;eAC3B,oBAAC,UAAD;MACU;MACM;MACd,OAAO,KAAK;MACc;MACb;MACC;MAEb;KACO,CAAA;IACQ,CAAA;GACR;;CAEL,CAAA;AAEjB"}
@@ -1,2 +1,2 @@
1
- import { NumberFormat } from '../types/index.js';
1
+ import { NumberFormat } from '../types/generated.js';
2
2
  export declare function formatNumber(value: unknown, format: NumberFormat, locale: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"number.js","names":[],"sources":["../../resources/js/format/number.ts"],"sourcesContent":["import type { NumberFormat } from \"@lattice-php/lattice/types\";\nimport { numericValue } from \"./numeric\";\n\nexport function formatNumber(value: unknown, format: NumberFormat, locale: string): string {\n const number = numericValue(value);\n\n if (number === null) {\n return String(value ?? \"\");\n }\n\n const options: Intl.NumberFormatOptions = {\n notation: format.notation as Intl.NumberFormatOptions[\"notation\"],\n minimumFractionDigits: format.minimumFractionDigits ?? undefined,\n maximumFractionDigits: format.maximumFractionDigits ?? undefined,\n };\n\n if (format.currency) {\n options.style = \"currency\";\n options.currency = format.currency;\n } else if (format.unit) {\n options.style = \"unit\";\n options.unit = format.unit;\n }\n\n return new Intl.NumberFormat(locale, options).format(number);\n}\n"],"mappings":";;AAGA,SAAgB,aAAa,OAAgB,QAAsB,QAAwB;CACzF,MAAM,SAAS,aAAa,KAAK;CAEjC,IAAI,WAAW,MACb,OAAO,OAAO,SAAS,EAAE;CAG3B,MAAM,UAAoC;EACxC,UAAU,OAAO;EACjB,uBAAuB,OAAO,yBAAyB,KAAA;EACvD,uBAAuB,OAAO,yBAAyB,KAAA;CACzD;CAEA,IAAI,OAAO,UAAU;EACnB,QAAQ,QAAQ;EAChB,QAAQ,WAAW,OAAO;CAC5B,OAAO,IAAI,OAAO,MAAM;EACtB,QAAQ,QAAQ;EAChB,QAAQ,OAAO,OAAO;CACxB;CAEA,OAAO,IAAI,KAAK,aAAa,QAAQ,OAAO,EAAE,OAAO,MAAM;AAC7D"}
1
+ {"version":3,"file":"number.js","names":[],"sources":["../../resources/js/format/number.ts"],"sourcesContent":["import type { NumberFormat } from \"@lattice-php/lattice/types/generated\";\nimport { numericValue } from \"./numeric\";\n\nexport function formatNumber(value: unknown, format: NumberFormat, locale: string): string {\n const number = numericValue(value);\n\n if (number === null) {\n return String(value ?? \"\");\n }\n\n const options: Intl.NumberFormatOptions = {\n notation: format.notation as Intl.NumberFormatOptions[\"notation\"],\n minimumFractionDigits: format.minimumFractionDigits ?? undefined,\n maximumFractionDigits: format.maximumFractionDigits ?? undefined,\n };\n\n if (format.currency) {\n options.style = \"currency\";\n options.currency = format.currency;\n } else if (format.unit) {\n options.style = \"unit\";\n options.unit = format.unit;\n }\n\n return new Intl.NumberFormat(locale, options).format(number);\n}\n"],"mappings":";;AAGA,SAAgB,aAAa,OAAgB,QAAsB,QAAwB;CACzF,MAAM,SAAS,aAAa,KAAK;CAEjC,IAAI,WAAW,MACb,OAAO,OAAO,SAAS,EAAE;CAG3B,MAAM,UAAoC;EACxC,UAAU,OAAO;EACjB,uBAAuB,OAAO,yBAAyB,KAAA;EACvD,uBAAuB,OAAO,yBAAyB,KAAA;CACzD;CAEA,IAAI,OAAO,UAAU;EACnB,QAAQ,QAAQ;EAChB,QAAQ,WAAW,OAAO;CAC5B,OAAO,IAAI,OAAO,MAAM;EACtB,QAAQ,QAAQ;EAChB,QAAQ,OAAO,OAAO;CACxB;CAEA,OAAO,IAAI,KAAK,aAAa,QAAQ,OAAO,EAAE,OAAO,MAAM;AAC7D"}
@@ -1,4 +1,4 @@
1
- import { DateFormat, NumberFormat } from '../types/index.js';
1
+ import { DateFormat, NumberFormat } from '../types/generated.js';
2
2
  export type Format = NumberFormat | DateFormat;
3
3
  export declare function formatValue(value: unknown, format: Format | null, ctx: {
4
4
  locale: string;
@@ -1 +1 @@
1
- {"version":3,"file":"value.js","names":[],"sources":["../../resources/js/format/value.ts"],"sourcesContent":["import type { DateFormat, NumberFormat } from \"@lattice-php/lattice/types\";\nimport { formatDateValue } from \"./date-time\";\nimport { formatNumber } from \"./number\";\n\nexport type Format = NumberFormat | DateFormat;\n\nfunction isDateFormat(format: Format): format is DateFormat {\n return format.kind === \"date\";\n}\n\nexport function formatValue(\n value: unknown,\n format: Format | null,\n ctx: { locale: string; timezone: string },\n): string {\n if (format === null) {\n return String(value ?? \"\");\n }\n\n return isDateFormat(format)\n ? formatDateValue(value, format, { locale: ctx.locale, timeZone: ctx.timezone })\n : formatNumber(value, format, ctx.locale);\n}\n"],"mappings":";;;AAMA,SAAS,aAAa,QAAsC;CAC1D,OAAO,OAAO,SAAS;AACzB;AAEA,SAAgB,YACd,OACA,QACA,KACQ;CACR,IAAI,WAAW,MACb,OAAO,OAAO,SAAS,EAAE;CAG3B,OAAO,aAAa,MAAM,IACtB,gBAAgB,OAAO,QAAQ;EAAE,QAAQ,IAAI;EAAQ,UAAU,IAAI;CAAS,CAAC,IAC7E,aAAa,OAAO,QAAQ,IAAI,MAAM;AAC5C"}
1
+ {"version":3,"file":"value.js","names":[],"sources":["../../resources/js/format/value.ts"],"sourcesContent":["import type { DateFormat, NumberFormat } from \"@lattice-php/lattice/types/generated\";\nimport { formatDateValue } from \"./date-time\";\nimport { formatNumber } from \"./number\";\n\nexport type Format = NumberFormat | DateFormat;\n\nfunction isDateFormat(format: Format): format is DateFormat {\n return format.kind === \"date\";\n}\n\nexport function formatValue(\n value: unknown,\n format: Format | null,\n ctx: { locale: string; timezone: string },\n): string {\n if (format === null) {\n return String(value ?? \"\");\n }\n\n return isDateFormat(format)\n ? formatDateValue(value, format, { locale: ctx.locale, timeZone: ctx.timezone })\n : formatNumber(value, format, ctx.locale);\n}\n"],"mappings":";;;AAMA,SAAS,aAAa,QAAsC;CAC1D,OAAO,OAAO,SAAS;AACzB;AAEA,SAAgB,YACd,OACA,QACA,KACQ;CACR,IAAI,WAAW,MACb,OAAO,OAAO,SAAS,EAAE;CAG3B,OAAO,aAAa,MAAM,IACtB,gBAAgB,OAAO,QAAQ;EAAE,QAAQ,IAAI;EAAQ,UAAU,IAAI;CAAS,CAAC,IAC7E,aAAa,OAAO,QAAQ,IAAI,MAAM;AAC5C"}
@@ -79,6 +79,7 @@ var NotificationsComponent = ({ node }) => {
79
79
  open: store.open,
80
80
  onOpenChange: store.setOpen,
81
81
  children: /* @__PURE__ */ jsxs(DialogContent, {
82
+ "aria-describedby": void 0,
82
83
  className: "p-0",
83
84
  placement: "end",
84
85
  width: "sm",
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.js","names":[],"sources":["../../../resources/js/notifications/components/notifications.tsx"],"sourcesContent":["import { Component, lazy, Suspense, type ReactNode } from \"react\";\nimport { Badge } from \"@lattice-php/lattice/ui/badge\";\nimport { Dialog, DialogContent, DialogTitle } from \"@lattice-php/lattice/ui/dialog\";\nimport {\n Popover as PopoverRoot,\n PopoverContent,\n PopoverTrigger,\n} from \"@lattice-php/lattice/ui/popover\";\nimport { Icon } from \"@lattice-php/lattice/icons\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { useNotifications } from \"@lattice-php/lattice/notifications/store\";\nimport type { NotificationItem } from \"@lattice-php/lattice/notifications/types\";\nimport { NotificationList } from \"./notification-list\";\n\nconst NotificationsEcho = lazy(() =>\n import(\"./notifications-echo\").then((m) => ({ default: m.NotificationsEcho })),\n);\n\nclass EchoBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {\n state = { failed: false };\n\n static getDerivedStateFromError(): { failed: boolean } {\n return { failed: true };\n }\n\n componentDidCatch(): void {\n console.warn(\n \"[lattice] The notifications bell declares a realtime channel but Echo is unavailable. Install @laravel/echo-react and call configureEcho().\",\n );\n }\n\n render(): ReactNode {\n return this.state.failed ? null : this.props.children;\n }\n}\n\nconst NotificationsComponent: RendererComponent<\"notifications\"> = ({ node }) => {\n const { t } = useT(\"lattice\");\n const store = useNotifications({\n endpoint: node.props.endpoint,\n pollingInterval: node.props.pollingInterval,\n });\n\n const label = t(\"notifications.label\", \"Notifications\");\n\n const trigger = (\n <span className=\"relative inline-flex items-center justify-center rounded-lt-sm p-2 hover:bg-lt-muted\">\n <Icon name=\"bell\" className=\"size-lt-icon-md\" />\n {store.unreadCount > 0 ? (\n <Badge\n variant=\"destructive\"\n data-test=\"notifications-badge\"\n className=\"absolute -right-0.5 -top-0.5 min-w-4 px-1 py-0 text-[10px]\"\n >\n {store.unreadCount}\n </Badge>\n ) : null}\n </span>\n );\n\n const panel = (\n <div className=\"w-80\" data-test=\"notifications-panel\">\n <div className=\"flex items-center justify-between border-b border-lt-border px-3 py-2\">\n <span className=\"text-sm font-medium\">{t(\"notifications.heading\", \"Notifications\")}</span>\n {store.unreadCount > 0 ? (\n <button\n type=\"button\"\n className=\"text-xs text-lt-muted-fg hover:text-lt-fg\"\n onClick={store.markAllRead}\n >\n {t(\"notifications.mark-all-read\", \"Mark all read\")}\n </button>\n ) : null}\n </div>\n <NotificationList\n notifications={store.notifications}\n status={store.status}\n hasMore={store.hasMore}\n onMarkRead={store.markRead}\n onDismiss={store.dismiss}\n onLoadMore={store.loadMore}\n />\n </div>\n );\n\n return (\n <>\n {node.props.channel ? (\n <EchoBoundary>\n <Suspense fallback={null}>\n <NotificationsEcho\n channel={node.props.channel}\n onReceive={(item: NotificationItem) => store.receive(item)}\n />\n </Suspense>\n </EchoBoundary>\n ) : null}\n\n {node.props.slideOut ? (\n <>\n <button\n type=\"button\"\n aria-label={label}\n data-test=\"notifications-trigger\"\n onClick={() => store.setOpen(true)}\n >\n {trigger}\n </button>\n <Dialog open={store.open} onOpenChange={store.setOpen}>\n <DialogContent className=\"p-0\" placement=\"end\" width=\"sm\">\n <DialogTitle className=\"sr-only\">{label}</DialogTitle>\n {panel}\n </DialogContent>\n </Dialog>\n </>\n ) : (\n <PopoverRoot open={store.open} onOpenChange={store.setOpen}>\n <PopoverTrigger asChild>\n <button type=\"button\" aria-label={label} data-test=\"notifications-trigger\">\n {trigger}\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"p-0\">\n {panel}\n </PopoverContent>\n </PopoverRoot>\n )}\n </>\n );\n};\n\nexport default NotificationsComponent;\n"],"mappings":";;;;;;;;;;AAeA,IAAM,oBAAoB,WACxB,OAAO,2BAAwB,MAAM,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAC/E;AAEA,IAAM,eAAN,cAA2B,UAAwD;CACjF,QAAQ,EAAE,QAAQ,MAAM;CAExB,OAAO,2BAAgD;EACrD,OAAO,EAAE,QAAQ,KAAK;CACxB;CAEA,oBAA0B;EACxB,QAAQ,KACN,6IACF;CACF;CAEA,SAAoB;EAClB,OAAO,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM;CAC/C;AACF;AAEA,IAAM,0BAA8D,EAAE,WAAW;CAC/E,MAAM,EAAE,MAAM,KAAK,SAAS;CAC5B,MAAM,QAAQ,iBAAiB;EAC7B,UAAU,KAAK,MAAM;EACrB,iBAAiB,KAAK,MAAM;CAC9B,CAAC;CAED,MAAM,QAAQ,EAAE,uBAAuB,eAAe;CAEtD,MAAM,UACJ,qBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,oBAAC,MAAD;GAAM,MAAK;GAAO,WAAU;EAAmB,CAAA,GAC9C,MAAM,cAAc,IACnB,oBAAC,OAAD;GACE,SAAQ;GACR,aAAU;GACV,WAAU;aAET,MAAM;EACF,CAAA,IACL,IACA;;CAGR,MAAM,QACJ,qBAAC,OAAD;EAAK,WAAU;EAAO,aAAU;YAAhC,CACE,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,oBAAC,QAAD;IAAM,WAAU;cAAuB,EAAE,yBAAyB,eAAe;GAAQ,CAAA,GACxF,MAAM,cAAc,IACnB,oBAAC,UAAD;IACE,MAAK;IACL,WAAU;IACV,SAAS,MAAM;cAEd,EAAE,+BAA+B,eAAe;GAC3C,CAAA,IACN,IACD;MACL,oBAAC,kBAAD;GACE,eAAe,MAAM;GACrB,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,YAAY,MAAM;GAClB,WAAW,MAAM;GACjB,YAAY,MAAM;EACnB,CAAA,CACE;;CAGP,OACE,qBAAA,YAAA,EAAA,UAAA,CACG,KAAK,MAAM,UACV,oBAAC,cAAD,EAAA,UACE,oBAAC,UAAD;EAAU,UAAU;YAClB,oBAAC,mBAAD;GACE,SAAS,KAAK,MAAM;GACpB,YAAY,SAA2B,MAAM,QAAQ,IAAI;EAC1D,CAAA;CACO,CAAA,EACE,CAAA,IACZ,MAEH,KAAK,MAAM,WACV,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,UAAD;EACE,MAAK;EACL,cAAY;EACZ,aAAU;EACV,eAAe,MAAM,QAAQ,IAAI;YAEhC;CACK,CAAA,GACR,oBAAC,QAAD;EAAQ,MAAM,MAAM;EAAM,cAAc,MAAM;YAC5C,qBAAC,eAAD;GAAe,WAAU;GAAM,WAAU;GAAM,OAAM;aAArD,CACE,oBAAC,aAAD;IAAa,WAAU;cAAW;GAAmB,CAAA,GACpD,KACY;;CACT,CAAA,CACR,EAAA,CAAA,IAEF,qBAAC,SAAD;EAAa,MAAM,MAAM;EAAM,cAAc,MAAM;YAAnD,CACE,oBAAC,gBAAD;GAAgB,SAAA;aACd,oBAAC,UAAD;IAAQ,MAAK;IAAS,cAAY;IAAO,aAAU;cAChD;GACK,CAAA;EACM,CAAA,GAChB,oBAAC,gBAAD;GAAgB,OAAM;GAAM,WAAU;aACnC;EACa,CAAA,CACL;GAEf,EAAA,CAAA;AAEN"}
1
+ {"version":3,"file":"notifications.js","names":[],"sources":["../../../resources/js/notifications/components/notifications.tsx"],"sourcesContent":["import { Component, lazy, Suspense, type ReactNode } from \"react\";\nimport { Badge } from \"@lattice-php/lattice/ui/badge\";\nimport { Dialog, DialogContent, DialogTitle } from \"@lattice-php/lattice/ui/dialog\";\nimport {\n Popover as PopoverRoot,\n PopoverContent,\n PopoverTrigger,\n} from \"@lattice-php/lattice/ui/popover\";\nimport { Icon } from \"@lattice-php/lattice/icons\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { useNotifications } from \"@lattice-php/lattice/notifications/store\";\nimport type { NotificationItem } from \"@lattice-php/lattice/notifications/types\";\nimport { NotificationList } from \"./notification-list\";\n\nconst NotificationsEcho = lazy(() =>\n import(\"./notifications-echo\").then((m) => ({ default: m.NotificationsEcho })),\n);\n\nclass EchoBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {\n state = { failed: false };\n\n static getDerivedStateFromError(): { failed: boolean } {\n return { failed: true };\n }\n\n componentDidCatch(): void {\n console.warn(\n \"[lattice] The notifications bell declares a realtime channel but Echo is unavailable. Install @laravel/echo-react and call configureEcho().\",\n );\n }\n\n render(): ReactNode {\n return this.state.failed ? null : this.props.children;\n }\n}\n\nconst NotificationsComponent: RendererComponent<\"notifications\"> = ({ node }) => {\n const { t } = useT(\"lattice\");\n const store = useNotifications({\n endpoint: node.props.endpoint,\n pollingInterval: node.props.pollingInterval,\n });\n\n const label = t(\"notifications.label\", \"Notifications\");\n\n const trigger = (\n <span className=\"relative inline-flex items-center justify-center rounded-lt-sm p-2 hover:bg-lt-muted\">\n <Icon name=\"bell\" className=\"size-lt-icon-md\" />\n {store.unreadCount > 0 ? (\n <Badge\n variant=\"destructive\"\n data-test=\"notifications-badge\"\n className=\"absolute -right-0.5 -top-0.5 min-w-4 px-1 py-0 text-[10px]\"\n >\n {store.unreadCount}\n </Badge>\n ) : null}\n </span>\n );\n\n const panel = (\n <div className=\"w-80\" data-test=\"notifications-panel\">\n <div className=\"flex items-center justify-between border-b border-lt-border px-3 py-2\">\n <span className=\"text-sm font-medium\">{t(\"notifications.heading\", \"Notifications\")}</span>\n {store.unreadCount > 0 ? (\n <button\n type=\"button\"\n className=\"text-xs text-lt-muted-fg hover:text-lt-fg\"\n onClick={store.markAllRead}\n >\n {t(\"notifications.mark-all-read\", \"Mark all read\")}\n </button>\n ) : null}\n </div>\n <NotificationList\n notifications={store.notifications}\n status={store.status}\n hasMore={store.hasMore}\n onMarkRead={store.markRead}\n onDismiss={store.dismiss}\n onLoadMore={store.loadMore}\n />\n </div>\n );\n\n return (\n <>\n {node.props.channel ? (\n <EchoBoundary>\n <Suspense fallback={null}>\n <NotificationsEcho\n channel={node.props.channel}\n onReceive={(item: NotificationItem) => store.receive(item)}\n />\n </Suspense>\n </EchoBoundary>\n ) : null}\n\n {node.props.slideOut ? (\n <>\n <button\n type=\"button\"\n aria-label={label}\n data-test=\"notifications-trigger\"\n onClick={() => store.setOpen(true)}\n >\n {trigger}\n </button>\n <Dialog open={store.open} onOpenChange={store.setOpen}>\n <DialogContent aria-describedby={undefined} className=\"p-0\" placement=\"end\" width=\"sm\">\n <DialogTitle className=\"sr-only\">{label}</DialogTitle>\n {panel}\n </DialogContent>\n </Dialog>\n </>\n ) : (\n <PopoverRoot open={store.open} onOpenChange={store.setOpen}>\n <PopoverTrigger asChild>\n <button type=\"button\" aria-label={label} data-test=\"notifications-trigger\">\n {trigger}\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"p-0\">\n {panel}\n </PopoverContent>\n </PopoverRoot>\n )}\n </>\n );\n};\n\nexport default NotificationsComponent;\n"],"mappings":";;;;;;;;;;AAeA,IAAM,oBAAoB,WACxB,OAAO,2BAAwB,MAAM,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAC/E;AAEA,IAAM,eAAN,cAA2B,UAAwD;CACjF,QAAQ,EAAE,QAAQ,MAAM;CAExB,OAAO,2BAAgD;EACrD,OAAO,EAAE,QAAQ,KAAK;CACxB;CAEA,oBAA0B;EACxB,QAAQ,KACN,6IACF;CACF;CAEA,SAAoB;EAClB,OAAO,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM;CAC/C;AACF;AAEA,IAAM,0BAA8D,EAAE,WAAW;CAC/E,MAAM,EAAE,MAAM,KAAK,SAAS;CAC5B,MAAM,QAAQ,iBAAiB;EAC7B,UAAU,KAAK,MAAM;EACrB,iBAAiB,KAAK,MAAM;CAC9B,CAAC;CAED,MAAM,QAAQ,EAAE,uBAAuB,eAAe;CAEtD,MAAM,UACJ,qBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,oBAAC,MAAD;GAAM,MAAK;GAAO,WAAU;EAAmB,CAAA,GAC9C,MAAM,cAAc,IACnB,oBAAC,OAAD;GACE,SAAQ;GACR,aAAU;GACV,WAAU;aAET,MAAM;EACF,CAAA,IACL,IACA;;CAGR,MAAM,QACJ,qBAAC,OAAD;EAAK,WAAU;EAAO,aAAU;YAAhC,CACE,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,oBAAC,QAAD;IAAM,WAAU;cAAuB,EAAE,yBAAyB,eAAe;GAAQ,CAAA,GACxF,MAAM,cAAc,IACnB,oBAAC,UAAD;IACE,MAAK;IACL,WAAU;IACV,SAAS,MAAM;cAEd,EAAE,+BAA+B,eAAe;GAC3C,CAAA,IACN,IACD;MACL,oBAAC,kBAAD;GACE,eAAe,MAAM;GACrB,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,YAAY,MAAM;GAClB,WAAW,MAAM;GACjB,YAAY,MAAM;EACnB,CAAA,CACE;;CAGP,OACE,qBAAA,YAAA,EAAA,UAAA,CACG,KAAK,MAAM,UACV,oBAAC,cAAD,EAAA,UACE,oBAAC,UAAD;EAAU,UAAU;YAClB,oBAAC,mBAAD;GACE,SAAS,KAAK,MAAM;GACpB,YAAY,SAA2B,MAAM,QAAQ,IAAI;EAC1D,CAAA;CACO,CAAA,EACE,CAAA,IACZ,MAEH,KAAK,MAAM,WACV,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,UAAD;EACE,MAAK;EACL,cAAY;EACZ,aAAU;EACV,eAAe,MAAM,QAAQ,IAAI;YAEhC;CACK,CAAA,GACR,oBAAC,QAAD;EAAQ,MAAM,MAAM;EAAM,cAAc,MAAM;YAC5C,qBAAC,eAAD;GAAe,oBAAkB,KAAA;GAAW,WAAU;GAAM,WAAU;GAAM,OAAM;aAAlF,CACE,oBAAC,aAAD;IAAa,WAAU;cAAW;GAAmB,CAAA,GACpD,KACY;;CACT,CAAA,CACR,EAAA,CAAA,IAEF,qBAAC,SAAD;EAAa,MAAM,MAAM;EAAM,cAAc,MAAM;YAAnD,CACE,oBAAC,gBAAD;GAAgB,SAAA;aACd,oBAAC,UAAD;IAAQ,MAAK;IAAS,cAAY;IAAO,aAAU;cAChD;GACK,CAAA;EACM,CAAA,GAChB,oBAAC,gBAAD;GAAgB,OAAM;GAAM,WAAU;aACnC;EACa,CAAA,CACL;GAEf,EAAA,CAAA;AAEN"}
@@ -1,20 +1,18 @@
1
- import { withHeaders } from "../../core/headers.js";
2
1
  import { useT } from "../../i18n/instance.js";
3
2
  import { prefixedTestId } from "../../core/test-id.js";
4
- import { getActionEffects } from "../../effects/dispatch.js";
5
3
  import { useEffectDispatcher } from "../../effects/use-effect-dispatcher.js";
6
4
  import { Button } from "../../ui/button.js";
7
5
  import { Spinner } from "../../ui/spinner.js";
8
6
  import { ConfirmDialog } from "../../ui/confirm-dialog.js";
7
+ import { apiFetch } from "../../core/api.js";
9
8
  import { runAction } from "../../action/lib/run-action.js";
10
9
  import { ActionForm } from "../../action/components/action-form.js";
11
- import { useHttp } from "@inertiajs/react";
12
10
  import { useState } from "react";
13
11
  import { jsx, jsxs } from "react/jsx-runtime";
14
12
  //#region resources/js/table/components/bulk-bar.tsx
15
13
  function BulkBar({ actions, selectedKeys, allMatching, total, query, canSelectAllMatching, onSelectAllMatching, onCompleted }) {
16
14
  const { t } = useT("lattice");
17
- const http = useHttp({});
15
+ const [processing, setProcessing] = useState(false);
18
16
  const dispatch = useEffectDispatcher();
19
17
  const [confirming, setConfirming] = useState(null);
20
18
  const [filling, setFilling] = useState(null);
@@ -23,11 +21,15 @@ function BulkBar({ actions, selectedKeys, allMatching, total, query, canSelectAl
23
21
  ...query
24
22
  } : { selected: selectedKeys };
25
23
  async function submit(action) {
26
- http.transform((data) => ({
27
- ...data,
28
- ...selectionPayload()
29
- }));
30
- if (await runAction(() => http[action.method](action.endpoint, { headers: withHeaders(action.ref ?? "") }), dispatch)) {
24
+ setProcessing(true);
25
+ const ok = await runAction(() => apiFetch(action.endpoint, {
26
+ method: action.method,
27
+ ref: action.ref,
28
+ body: JSON.stringify(selectionPayload()),
29
+ throwOnError: false
30
+ }), dispatch);
31
+ setProcessing(false);
32
+ if (ok) {
31
33
  setConfirming(null);
32
34
  onCompleted();
33
35
  }
@@ -64,9 +66,9 @@ function BulkBar({ actions, selectedKeys, allMatching, total, query, canSelectAl
64
66
  type: "button",
65
67
  "data-test": prefixedTestId("bulk-action", action.id),
66
68
  variant: action.variant,
67
- disabled: http.processing,
69
+ disabled: processing,
68
70
  onClick: () => run(action),
69
- children: [http.processing && /* @__PURE__ */ jsx(Spinner, {}), action.label]
71
+ children: [processing && /* @__PURE__ */ jsx(Spinner, {}), action.label]
70
72
  }, action.id))
71
73
  }),
72
74
  confirming?.confirmation && /* @__PURE__ */ jsx(ConfirmDialog, {
@@ -75,7 +77,7 @@ function BulkBar({ actions, selectedKeys, allMatching, total, query, canSelectAl
75
77
  confirmLabel: confirming.confirmation.confirmLabel ?? confirming.label,
76
78
  cancelLabel: confirming.confirmation.cancelLabel ?? t("common.cancel", "Cancel"),
77
79
  confirmVariant: confirming.variant,
78
- processing: http.processing,
80
+ processing,
79
81
  onConfirm: () => void submit(confirming),
80
82
  onCancel: () => setConfirming(null)
81
83
  }),
@@ -88,8 +90,7 @@ function BulkBar({ actions, selectedKeys, allMatching, total, query, canSelectAl
88
90
  formNode: filling.form,
89
91
  method: filling.method,
90
92
  onClose: () => setFilling(null),
91
- onSuccess: (response) => {
92
- dispatch(getActionEffects(response.effects));
93
+ onSuccess: () => {
93
94
  setFilling(null);
94
95
  onCompleted();
95
96
  },