@lattice-php/lattice 0.26.0 → 0.27.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.
- package/dist/action/components/action-form.js +2 -2
- package/dist/action/components/action-form.js.map +1 -1
- package/dist/action/hooks/use-action.js +2 -2
- package/dist/chat/components/chat-box.js +1 -1
- package/dist/core/api.d.ts +13 -0
- package/dist/core/api.js +38 -4
- package/dist/core/api.js.map +1 -1
- package/dist/core/component-ref.d.ts +3 -0
- package/dist/core/component-ref.js +20 -2
- package/dist/core/component-ref.js.map +1 -1
- package/dist/core/headers.js +1 -1
- package/dist/core/index.js +4 -4
- package/dist/create-app.js +6 -1
- package/dist/create-app.js.map +1 -1
- package/dist/dnd/index.d.ts +16 -0
- package/dist/dnd/index.js +13 -0
- package/dist/form/components/fields/date-picker-field.js +1 -1
- package/dist/form/components/fields/date-picker-field.js.map +1 -1
- package/dist/form/components/fields/file-upload.js +3 -2
- package/dist/form/components/fields/file-upload.js.map +1 -1
- package/dist/form/components/fields/select.js +4 -3
- package/dist/form/components/fields/select.js.map +1 -1
- package/dist/form/components/form.js +63 -27
- package/dist/form/components/form.js.map +1 -1
- package/dist/form/hooks/use-form-resolver.js +1 -1
- package/dist/form/hooks/use-form-resolver.js.map +1 -1
- package/dist/form/hooks/values.d.ts +8 -0
- package/dist/form/hooks/values.js +18 -1
- package/dist/form/hooks/values.js.map +1 -1
- package/dist/i18n/backend.js +1 -1
- package/dist/i18n/instance.js +1 -1
- package/dist/i18n/timezone.js +1 -1
- package/dist/index.js +3 -3
- package/dist/inertia.d.ts +7 -0
- package/dist/inertia.js +19 -1
- package/dist/inertia.js.map +1 -1
- package/dist/lattice.css +38 -33
- package/dist/remote/components/data-list.js +2 -2
- package/dist/table/components/bulk-bar.js +1 -1
- package/dist/table/hooks/use-table.js +3 -2
- package/dist/table/hooks/use-table.js.map +1 -1
- package/dist/ui/fragment.js +1 -1
- package/dist-standalone/chunks/{chart-view-8xEKTUJV.js → chart-view-CpYsLvzR.js} +4 -4
- package/dist-standalone/chunks/date-picker-field-lX8N40Ii.js +1 -0
- package/dist-standalone/chunks/{page-props-D3FnL614.js → page-props-ClpVjpOc.js} +1 -1
- package/dist-standalone/chunks/{rich-editor-field-47ov3Bny.js → rich-editor-field-C71csVJv.js} +2 -2
- package/dist-standalone/chunks/{subscriptions-C-0lSr7X.js → subscriptions-Bt2s3IF3.js} +1 -1
- package/dist-standalone/chunks/use-effect-dispatcher-DGFXSzA6.js +97 -0
- package/dist-standalone/lattice.css +1 -1
- package/dist-standalone/lattice.js +15 -15
- package/dist-standalone/manifest.json +8 -8
- package/package.json +9 -1
- package/dist-standalone/chunks/date-picker-field-cdhxtVSG.js +0 -1
- package/dist-standalone/chunks/use-effect-dispatcher-EVZxmfoo.js +0 -97
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { apiFetch } from "../../core/api.js";
|
|
1
2
|
import { useT } from "../../i18n/instance.js";
|
|
2
3
|
import { Renderer } from "../../core/renderer.js";
|
|
3
4
|
import { dispatchActionError, getActionEffects } from "../../effects/dispatch.js";
|
|
@@ -5,7 +6,6 @@ import { useEffectDispatcher } from "../../effects/use-effect-dispatcher.js";
|
|
|
5
6
|
import { Button } from "../../ui/button.js";
|
|
6
7
|
import { Dialog, DialogContent, DialogHeader } from "../../ui/dialog.js";
|
|
7
8
|
import { Spinner } from "../../ui/spinner.js";
|
|
8
|
-
import { apiFetch } from "../../core/api.js";
|
|
9
9
|
import { Skeleton } from "../../ui/skeleton.js";
|
|
10
10
|
import { FormProvider } from "../../form/hooks/context.js";
|
|
11
11
|
import { PrefillProvider } from "../../form/hooks/prefill-context.js";
|
|
@@ -32,7 +32,7 @@ function useLazyActionForm(endpoint, componentRef, enabled) {
|
|
|
32
32
|
}
|
|
33
33
|
const controller = new AbortController();
|
|
34
34
|
apiFetch(endpoint, {
|
|
35
|
-
body: JSON.stringify({
|
|
35
|
+
body: JSON.stringify({ _sub: "schema" }),
|
|
36
36
|
ref: componentRef,
|
|
37
37
|
method: "POST",
|
|
38
38
|
signal: controller.signal,
|
|
@@ -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 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 emphasis=\"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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"}
|
|
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({ _sub: \"schema\" }),\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 emphasis=\"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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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { translate } from "../../i18n/instance.js";
|
|
2
1
|
import { withHeaders } from "../../core/headers.js";
|
|
2
|
+
import { apiFetch } from "../../core/api.js";
|
|
3
|
+
import { translate } from "../../i18n/instance.js";
|
|
3
4
|
import { useEffectDispatcher } from "../../effects/use-effect-dispatcher.js";
|
|
4
5
|
import { ConfirmDialog } from "../../ui/confirm-dialog.js";
|
|
5
|
-
import { apiFetch } from "../../core/api.js";
|
|
6
6
|
import { runAction } from "../lib/run-action.js";
|
|
7
7
|
import { ActionForm, useLazyActionForm } from "../components/action-form.js";
|
|
8
8
|
import { actionLabel } from "../lib/action-label.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { apiFetch, remoteJson } from "../../core/api.js";
|
|
1
2
|
import { useT } from "../../i18n/instance.js";
|
|
2
3
|
import { cn } from "../../lib/utils.js";
|
|
3
4
|
import { testIdentity } from "../../core/test-id.js";
|
|
4
|
-
import { apiFetch, remoteJson } from "../../core/api.js";
|
|
5
5
|
import { createRemoteNdjsonChatTransport, ndjsonChatTransport } from "../lib/transport.js";
|
|
6
6
|
import { useChat } from "../hooks/use-chat.js";
|
|
7
7
|
import { MessageList } from "./message-list.js";
|
package/dist/core/api.d.ts
CHANGED
|
@@ -16,6 +16,19 @@ export type RemoteInit = Omit<RequestInit, "credentials" | "headers"> & {
|
|
|
16
16
|
retryOnUnauthorized?: boolean;
|
|
17
17
|
throwOnError?: boolean;
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Boot paths call this with the server-minted URL so subdirectory installs
|
|
21
|
+
* refresh against the right path; the default covers root installs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function setRefRefreshEndpoint(url: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Trade an expired-but-authentic ref for a fresh one. Deduped per original ref
|
|
26
|
+
* so a burst of 403s from one component costs a single round-trip. The renewed
|
|
27
|
+
* token lands in the component-ref map, so every later request that resolves
|
|
28
|
+
* its ref through withHeaders picks it up — this is the transport-agnostic
|
|
29
|
+
* primitive both the fetch funnel below and the Inertia form retry build on.
|
|
30
|
+
*/
|
|
31
|
+
export declare function refreshRef(componentRef: string): Promise<string | null>;
|
|
19
32
|
export declare function apiFetch(url: string, init?: ApiInit): Promise<Response>;
|
|
20
33
|
export declare function apiJson<T>(url: string, init?: ApiInit): Promise<T>;
|
|
21
34
|
export declare function clearRemoteTokenCache(): void;
|
package/dist/core/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { latestRef, storeRefreshedRef } from "./component-ref.js";
|
|
1
2
|
import { localeHeader } from "../i18n/locale.js";
|
|
2
3
|
import { withHeaders } from "./headers.js";
|
|
3
4
|
//#region resources/js/core/api.ts
|
|
@@ -31,18 +32,51 @@ function defaultHeaders(method) {
|
|
|
31
32
|
"X-XSRF-TOKEN": xsrfToken()
|
|
32
33
|
};
|
|
33
34
|
}
|
|
35
|
+
var refRefreshEndpoint = "/lattice/refs/refresh";
|
|
36
|
+
/**
|
|
37
|
+
* Boot paths call this with the server-minted URL so subdirectory installs
|
|
38
|
+
* refresh against the right path; the default covers root installs.
|
|
39
|
+
*/
|
|
40
|
+
function setRefRefreshEndpoint(url) {
|
|
41
|
+
refRefreshEndpoint = url;
|
|
42
|
+
}
|
|
43
|
+
var pendingRefRefreshes = /* @__PURE__ */ new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Trade an expired-but-authentic ref for a fresh one. Deduped per original ref
|
|
46
|
+
* so a burst of 403s from one component costs a single round-trip. The renewed
|
|
47
|
+
* token lands in the component-ref map, so every later request that resolves
|
|
48
|
+
* its ref through withHeaders picks it up — this is the transport-agnostic
|
|
49
|
+
* primitive both the fetch funnel below and the Inertia form retry build on.
|
|
50
|
+
*/
|
|
51
|
+
async function refreshRef(componentRef) {
|
|
52
|
+
const pending = pendingRefRefreshes.get(componentRef);
|
|
53
|
+
if (pending) return pending;
|
|
54
|
+
const request = apiJson(refRefreshEndpoint, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: JSON.stringify({ ref: latestRef(componentRef) })
|
|
57
|
+
}).then((data) => {
|
|
58
|
+
storeRefreshedRef(componentRef, data.ref);
|
|
59
|
+
return data.ref;
|
|
60
|
+
}).catch(() => null).finally(() => {
|
|
61
|
+
pendingRefRefreshes.delete(componentRef);
|
|
62
|
+
});
|
|
63
|
+
pendingRefRefreshes.set(componentRef, request);
|
|
64
|
+
return request;
|
|
65
|
+
}
|
|
34
66
|
async function apiFetch(url, init = {}) {
|
|
35
|
-
const { ref, headers, throwOnError = true, method, ...rest } = init;
|
|
67
|
+
const { ref = "", headers, throwOnError = true, method, ...rest } = init;
|
|
36
68
|
const normalizedMethod = method?.toUpperCase();
|
|
37
|
-
const
|
|
69
|
+
const request = () => fetch(url, {
|
|
38
70
|
credentials: "same-origin",
|
|
39
71
|
...rest,
|
|
40
72
|
method: normalizedMethod,
|
|
41
|
-
headers: withHeaders(ref
|
|
73
|
+
headers: withHeaders(ref, {
|
|
42
74
|
...defaultHeaders(normalizedMethod),
|
|
43
75
|
...headers
|
|
44
76
|
})
|
|
45
77
|
});
|
|
78
|
+
let response = await request();
|
|
79
|
+
if (ref !== "" && response.status === 403 && await refreshRef(ref) !== null) response = await request();
|
|
46
80
|
if (throwOnError && !response.ok) throw new ApiError(response);
|
|
47
81
|
return response;
|
|
48
82
|
}
|
|
@@ -122,6 +156,6 @@ async function remoteJson(url, init) {
|
|
|
122
156
|
return (await remoteFetch(url, init)).json();
|
|
123
157
|
}
|
|
124
158
|
//#endregion
|
|
125
|
-
export { ApiError, apiFetch, apiJson, clearRemoteTokenCache, invalidateRemoteToken, remoteFetch, remoteJson, remoteToken };
|
|
159
|
+
export { ApiError, apiFetch, apiJson, clearRemoteTokenCache, invalidateRemoteToken, refreshRef, remoteFetch, remoteJson, remoteToken, setRefRefreshEndpoint };
|
|
126
160
|
|
|
127
161
|
//# sourceMappingURL=api.js.map
|
package/dist/core/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","names":[],"sources":["../../resources/js/core/api.ts"],"sourcesContent":["/**\n * The single funnel for raw-fetch HTTP, so the cross-cutting header, credential,\n * and error policy has one home and call sites only declare intent. Returns a\n * Response (not parsed data) so one primitive serves JSON, the chat's streaming\n * getReader(), and multipart alike. Not for the Inertia world (router/useHttp),\n * which carries its own headers.\n */\n\nimport { withHeaders } from \"./headers\";\nimport { localeHeader } from \"@lattice-php/lattice/i18n/locale\";\nimport type {\n BrowserToken as GeneratedBrowserToken,\n RemoteAccess as GeneratedRemoteAccess,\n} from \"@lattice-php/lattice/types/generated\";\n\nexport class ApiError extends Error {\n constructor(readonly response: Response) {\n super(`HTTP ${response.status}`);\n }\n}\n\nfunction xsrfToken(): string {\n const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);\n\n return match ? decodeURIComponent(match[1]) : \"\";\n}\n\nexport type ApiInit = Omit<RequestInit, \"headers\"> & {\n ref?: string;\n headers?: Record<string, string>;\n throwOnError?: boolean;\n};\n\nexport type BrowserToken = GeneratedBrowserToken;\nexport type RemoteAccess = GeneratedRemoteAccess;\n\nexport type RemoteInit = Omit<RequestInit, \"credentials\" | \"headers\"> & {\n headers?: Record<string, string>;\n remote: RemoteAccess;\n retryOnUnauthorized?: boolean;\n throwOnError?: boolean;\n};\n\ntype CachedBrowserToken = {\n expiresAt: number;\n token: BrowserToken;\n};\n\nconst tokenRefreshSkewMs = 10_000;\nconst remoteTokenCache = new Map<string, CachedBrowserToken>();\nconst pendingRemoteTokens = new Map<string, Promise<BrowserToken>>();\n\nfunction defaultHeaders(method: string | undefined): Record<string, string> {\n const isWrite = method !== undefined && method !== \"GET\" && method !== \"HEAD\";\n\n if (!isWrite) {\n return { Accept: \"application/json\" };\n }\n\n return {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"X-Requested-With\": \"XMLHttpRequest\",\n \"X-XSRF-TOKEN\": xsrfToken(),\n };\n}\n\nexport async function apiFetch(url: string, init: ApiInit = {}): Promise<Response> {\n const { ref, headers, throwOnError = true, method, ...rest } = init;\n // Some servers reject a lower-case method line, and fetch only normalizes the\n // standard verbs, so upper-case it here.\n const normalizedMethod = method?.toUpperCase();\n const response = await fetch(url, {\n credentials: \"same-origin\",\n ...rest,\n method: normalizedMethod,\n headers: withHeaders(ref ?? \"\", { ...defaultHeaders(normalizedMethod), ...headers }),\n });\n\n if (throwOnError && !response.ok) {\n throw new ApiError(response);\n }\n\n return response;\n}\n\nexport async function apiJson<T>(url: string, init?: ApiInit): Promise<T> {\n return (await apiFetch(url, init)).json() as Promise<T>;\n}\n\nfunction remoteTokenKey(remote: RemoteAccess): string {\n return [remote.source, remote.audience, [...remote.scopes].sort().join(\" \")].join(\"\\u001f\");\n}\n\nexport function clearRemoteTokenCache(): void {\n remoteTokenCache.clear();\n pendingRemoteTokens.clear();\n}\n\nexport function invalidateRemoteToken(remote: RemoteAccess): void {\n remoteTokenCache.delete(remoteTokenKey(remote));\n}\n\nexport async function remoteToken(remote: RemoteAccess): Promise<BrowserToken> {\n const key = remoteTokenKey(remote);\n const cached = remoteTokenCache.get(key);\n\n if (cached && cached.expiresAt > Date.now() + tokenRefreshSkewMs) {\n return cached.token;\n }\n\n const pending = pendingRemoteTokens.get(key);\n\n if (pending) {\n return pending;\n }\n\n const request = apiJson<BrowserToken>(remote.tokenEndpoint, {\n method: \"POST\",\n ref: remote.ref,\n body: JSON.stringify({\n nodeId: remote.nodeId,\n nodeType: remote.nodeType,\n audience: remote.audience,\n scopes: remote.scopes,\n }),\n })\n .then((token) => {\n remoteTokenCache.set(key, {\n token,\n expiresAt: Date.now() + Math.max(0, token.expiresIn) * 1000,\n });\n\n return token;\n })\n .finally(() => {\n pendingRemoteTokens.delete(key);\n });\n\n pendingRemoteTokens.set(key, request);\n\n return request;\n}\n\nfunction isUnauthorized(response: Response): boolean {\n return response.status === 401 || response.status === 403;\n}\n\nasync function fetchRemoteWithToken(\n url: string,\n init: Omit<RemoteInit, \"remote\" | \"retryOnUnauthorized\" | \"throwOnError\">,\n token: BrowserToken,\n): Promise<Response> {\n const { headers, ...rest } = init;\n\n return fetch(url, {\n ...rest,\n credentials: \"omit\",\n headers: {\n Accept: \"application/json\",\n ...localeHeader(),\n ...headers,\n Authorization: `${token.tokenType} ${token.accessToken}`,\n },\n });\n}\n\nexport async function remoteFetch(url: string, init: RemoteInit): Promise<Response> {\n const { remote, retryOnUnauthorized = true, throwOnError = true, ...request } = init;\n let token = await remoteToken(remote);\n let response = await fetchRemoteWithToken(url, request, token);\n\n if (retryOnUnauthorized && isUnauthorized(response)) {\n invalidateRemoteToken(remote);\n token = await remoteToken(remote);\n response = await fetchRemoteWithToken(url, request, token);\n }\n\n if (throwOnError && !response.ok) {\n throw new ApiError(response);\n }\n\n return response;\n}\n\nexport async function remoteJson<T>(url: string, init: RemoteInit): Promise<T> {\n return (await remoteFetch(url, init)).json() as Promise<T>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api.js","names":[],"sources":["../../resources/js/core/api.ts"],"sourcesContent":["/**\n * The single funnel for raw-fetch HTTP, so the cross-cutting header, credential,\n * and error policy has one home and call sites only declare intent. Returns a\n * Response (not parsed data) so one primitive serves JSON, the chat's streaming\n * getReader(), and multipart alike. Not for the Inertia world (router/useHttp),\n * which carries its own headers.\n */\n\nimport { latestRef, storeRefreshedRef } from \"./component-ref\";\nimport { withHeaders } from \"./headers\";\nimport { localeHeader } from \"@lattice-php/lattice/i18n/locale\";\nimport type {\n BrowserToken as GeneratedBrowserToken,\n RemoteAccess as GeneratedRemoteAccess,\n} from \"@lattice-php/lattice/types/generated\";\n\nexport class ApiError extends Error {\n constructor(readonly response: Response) {\n super(`HTTP ${response.status}`);\n }\n}\n\nfunction xsrfToken(): string {\n const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);\n\n return match ? decodeURIComponent(match[1]) : \"\";\n}\n\nexport type ApiInit = Omit<RequestInit, \"headers\"> & {\n ref?: string;\n headers?: Record<string, string>;\n throwOnError?: boolean;\n};\n\nexport type BrowserToken = GeneratedBrowserToken;\nexport type RemoteAccess = GeneratedRemoteAccess;\n\nexport type RemoteInit = Omit<RequestInit, \"credentials\" | \"headers\"> & {\n headers?: Record<string, string>;\n remote: RemoteAccess;\n retryOnUnauthorized?: boolean;\n throwOnError?: boolean;\n};\n\ntype CachedBrowserToken = {\n expiresAt: number;\n token: BrowserToken;\n};\n\nconst tokenRefreshSkewMs = 10_000;\nconst remoteTokenCache = new Map<string, CachedBrowserToken>();\nconst pendingRemoteTokens = new Map<string, Promise<BrowserToken>>();\n\nfunction defaultHeaders(method: string | undefined): Record<string, string> {\n const isWrite = method !== undefined && method !== \"GET\" && method !== \"HEAD\";\n\n if (!isWrite) {\n return { Accept: \"application/json\" };\n }\n\n return {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"X-Requested-With\": \"XMLHttpRequest\",\n \"X-XSRF-TOKEN\": xsrfToken(),\n };\n}\n\nlet refRefreshEndpoint = \"/lattice/refs/refresh\";\n\n/**\n * Boot paths call this with the server-minted URL so subdirectory installs\n * refresh against the right path; the default covers root installs.\n */\nexport function setRefRefreshEndpoint(url: string): void {\n refRefreshEndpoint = url;\n}\n\nconst pendingRefRefreshes = new Map<string, Promise<string | null>>();\n\n/**\n * Trade an expired-but-authentic ref for a fresh one. Deduped per original ref\n * so a burst of 403s from one component costs a single round-trip. The renewed\n * token lands in the component-ref map, so every later request that resolves\n * its ref through withHeaders picks it up — this is the transport-agnostic\n * primitive both the fetch funnel below and the Inertia form retry build on.\n */\nexport async function refreshRef(componentRef: string): Promise<string | null> {\n const pending = pendingRefRefreshes.get(componentRef);\n\n if (pending) {\n return pending;\n }\n\n const request = apiJson<{ ref: string }>(refRefreshEndpoint, {\n method: \"POST\",\n body: JSON.stringify({ ref: latestRef(componentRef) }),\n })\n .then((data) => {\n storeRefreshedRef(componentRef, data.ref);\n\n return data.ref;\n })\n .catch(() => null)\n .finally(() => {\n pendingRefRefreshes.delete(componentRef);\n });\n\n pendingRefRefreshes.set(componentRef, request);\n\n return request;\n}\n\nexport async function apiFetch(url: string, init: ApiInit = {}): Promise<Response> {\n const { ref = \"\", headers, throwOnError = true, method, ...rest } = init;\n // Some servers reject a lower-case method line, and fetch only normalizes the\n // standard verbs, so upper-case it here.\n const normalizedMethod = method?.toUpperCase();\n const request = () =>\n fetch(url, {\n credentials: \"same-origin\",\n ...rest,\n method: normalizedMethod,\n // withHeaders resolves the latest refreshed token for the ref, so the\n // retry below picks up the renewal without threading it through.\n headers: withHeaders(ref, { ...defaultHeaders(normalizedMethod), ...headers }),\n });\n\n let response = await request();\n\n if (ref !== \"\" && response.status === 403 && (await refreshRef(ref)) !== null) {\n response = await request();\n }\n\n if (throwOnError && !response.ok) {\n throw new ApiError(response);\n }\n\n return response;\n}\n\nexport async function apiJson<T>(url: string, init?: ApiInit): Promise<T> {\n return (await apiFetch(url, init)).json() as Promise<T>;\n}\n\nfunction remoteTokenKey(remote: RemoteAccess): string {\n return [remote.source, remote.audience, [...remote.scopes].sort().join(\" \")].join(\"\\u001f\");\n}\n\nexport function clearRemoteTokenCache(): void {\n remoteTokenCache.clear();\n pendingRemoteTokens.clear();\n}\n\nexport function invalidateRemoteToken(remote: RemoteAccess): void {\n remoteTokenCache.delete(remoteTokenKey(remote));\n}\n\nexport async function remoteToken(remote: RemoteAccess): Promise<BrowserToken> {\n const key = remoteTokenKey(remote);\n const cached = remoteTokenCache.get(key);\n\n if (cached && cached.expiresAt > Date.now() + tokenRefreshSkewMs) {\n return cached.token;\n }\n\n const pending = pendingRemoteTokens.get(key);\n\n if (pending) {\n return pending;\n }\n\n const request = apiJson<BrowserToken>(remote.tokenEndpoint, {\n method: \"POST\",\n ref: remote.ref,\n body: JSON.stringify({\n nodeId: remote.nodeId,\n nodeType: remote.nodeType,\n audience: remote.audience,\n scopes: remote.scopes,\n }),\n })\n .then((token) => {\n remoteTokenCache.set(key, {\n token,\n expiresAt: Date.now() + Math.max(0, token.expiresIn) * 1000,\n });\n\n return token;\n })\n .finally(() => {\n pendingRemoteTokens.delete(key);\n });\n\n pendingRemoteTokens.set(key, request);\n\n return request;\n}\n\nfunction isUnauthorized(response: Response): boolean {\n return response.status === 401 || response.status === 403;\n}\n\nasync function fetchRemoteWithToken(\n url: string,\n init: Omit<RemoteInit, \"remote\" | \"retryOnUnauthorized\" | \"throwOnError\">,\n token: BrowserToken,\n): Promise<Response> {\n const { headers, ...rest } = init;\n\n return fetch(url, {\n ...rest,\n credentials: \"omit\",\n headers: {\n Accept: \"application/json\",\n ...localeHeader(),\n ...headers,\n Authorization: `${token.tokenType} ${token.accessToken}`,\n },\n });\n}\n\nexport async function remoteFetch(url: string, init: RemoteInit): Promise<Response> {\n const { remote, retryOnUnauthorized = true, throwOnError = true, ...request } = init;\n let token = await remoteToken(remote);\n let response = await fetchRemoteWithToken(url, request, token);\n\n if (retryOnUnauthorized && isUnauthorized(response)) {\n invalidateRemoteToken(remote);\n token = await remoteToken(remote);\n response = await fetchRemoteWithToken(url, request, token);\n }\n\n if (throwOnError && !response.ok) {\n throw new ApiError(response);\n }\n\n return response;\n}\n\nexport async function remoteJson<T>(url: string, init: RemoteInit): Promise<T> {\n return (await remoteFetch(url, init)).json() as Promise<T>;\n}\n"],"mappings":"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"}
|
|
@@ -5,4 +5,7 @@
|
|
|
5
5
|
* header.
|
|
6
6
|
*/
|
|
7
7
|
export declare const LATTICE_REF_HEADER = "X-Lattice-Ref";
|
|
8
|
+
export declare function latestRef(componentRef: string): string;
|
|
9
|
+
export declare function storeRefreshedRef(componentRef: string, refreshed: string): void;
|
|
10
|
+
export declare function clearRefreshedRefs(): void;
|
|
8
11
|
export declare function withRefHeader(componentRef: string): Record<string, string>;
|
|
@@ -6,10 +6,28 @@
|
|
|
6
6
|
* header.
|
|
7
7
|
*/
|
|
8
8
|
var LATTICE_REF_HEADER = "X-Lattice-Ref";
|
|
9
|
+
/**
|
|
10
|
+
* Refs are sealed with a lifetime and baked into node props at render time, so
|
|
11
|
+
* a long-lived tab cannot pick up a renewed token through React state. Renewed
|
|
12
|
+
* tokens are instead kept here, keyed by the original prop value, and resolved
|
|
13
|
+
* whenever a ref travels — every consumer keeps passing the ref it was rendered
|
|
14
|
+
* with.
|
|
15
|
+
*/
|
|
16
|
+
var refreshedRefs = /* @__PURE__ */ new Map();
|
|
17
|
+
function latestRef(componentRef) {
|
|
18
|
+
return refreshedRefs.get(componentRef) ?? componentRef;
|
|
19
|
+
}
|
|
20
|
+
function storeRefreshedRef(componentRef, refreshed) {
|
|
21
|
+
refreshedRefs.set(componentRef, refreshed);
|
|
22
|
+
}
|
|
23
|
+
function clearRefreshedRefs() {
|
|
24
|
+
refreshedRefs.clear();
|
|
25
|
+
}
|
|
9
26
|
function withRefHeader(componentRef) {
|
|
10
|
-
|
|
27
|
+
const ref = latestRef(componentRef);
|
|
28
|
+
return ref ? { [LATTICE_REF_HEADER]: ref } : {};
|
|
11
29
|
}
|
|
12
30
|
//#endregion
|
|
13
|
-
export { LATTICE_REF_HEADER, withRefHeader };
|
|
31
|
+
export { LATTICE_REF_HEADER, clearRefreshedRefs, latestRef, storeRefreshedRef, withRefHeader };
|
|
14
32
|
|
|
15
33
|
//# sourceMappingURL=component-ref.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-ref.js","names":[],"sources":["../../resources/js/core/component-ref.ts"],"sourcesContent":["/**\n * The signed component reference travels to the server as the `X-Lattice-Ref`\n * request header on every interactive request (GET and writes alike). This must\n * match ComponentReferenceSigner::token() on the PHP side, which reads the same\n * header.\n */\nexport const LATTICE_REF_HEADER = \"X-Lattice-Ref\";\n\nexport function withRefHeader(componentRef: string): Record<string, string> {\n
|
|
1
|
+
{"version":3,"file":"component-ref.js","names":[],"sources":["../../resources/js/core/component-ref.ts"],"sourcesContent":["/**\n * The signed component reference travels to the server as the `X-Lattice-Ref`\n * request header on every interactive request (GET and writes alike). This must\n * match ComponentReferenceSigner::token() on the PHP side, which reads the same\n * header.\n */\nexport const LATTICE_REF_HEADER = \"X-Lattice-Ref\";\n\n/**\n * Refs are sealed with a lifetime and baked into node props at render time, so\n * a long-lived tab cannot pick up a renewed token through React state. Renewed\n * tokens are instead kept here, keyed by the original prop value, and resolved\n * whenever a ref travels — every consumer keeps passing the ref it was rendered\n * with.\n */\nconst refreshedRefs = new Map<string, string>();\n\nexport function latestRef(componentRef: string): string {\n return refreshedRefs.get(componentRef) ?? componentRef;\n}\n\nexport function storeRefreshedRef(componentRef: string, refreshed: string): void {\n refreshedRefs.set(componentRef, refreshed);\n}\n\nexport function clearRefreshedRefs(): void {\n refreshedRefs.clear();\n}\n\nexport function withRefHeader(componentRef: string): Record<string, string> {\n const ref = latestRef(componentRef);\n\n return ref ? { [LATTICE_REF_HEADER]: ref } : {};\n}\n"],"mappings":";;;;;;;AAMA,IAAa,qBAAqB;;;;;;;;AASlC,IAAM,gCAAgB,IAAI,IAAoB;AAE9C,SAAgB,UAAU,cAA8B;CACtD,OAAO,cAAc,IAAI,YAAY,KAAK;AAC5C;AAEA,SAAgB,kBAAkB,cAAsB,WAAyB;CAC/E,cAAc,IAAI,cAAc,SAAS;AAC3C;AAEA,SAAgB,qBAA2B;CACzC,cAAc,MAAM;AACtB;AAEA,SAAgB,cAAc,cAA8C;CAC1E,MAAM,MAAM,UAAU,YAAY;CAElC,OAAO,MAAM,GAAG,qBAAqB,IAAI,IAAI,CAAC;AAChD"}
|
package/dist/core/headers.js
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { createPlugin, createRegistry, eagerComponent, extendRegistry, lazyComponent } from "./registry.js";
|
|
2
|
-
import { RegistryContext, setDefaultRegistry, useColumnRegistry, useComponentRegistry, useEffectHandlerRegistry } from "./registry-context.js";
|
|
3
|
-
import { dataBindings, isRecord, materializeNode, materializeProps, materializeSchema, rowValue } from "./materialize.js";
|
|
4
1
|
import { LATTICE_REF_HEADER, withRefHeader } from "./component-ref.js";
|
|
5
2
|
import { withHeaders } from "./headers.js";
|
|
3
|
+
import { ApiError, apiFetch, apiJson, clearRemoteTokenCache, invalidateRemoteToken, remoteFetch, remoteJson, remoteToken } from "./api.js";
|
|
4
|
+
import { dataBindings, isRecord, materializeNode, materializeProps, materializeSchema, rowValue } from "./materialize.js";
|
|
5
|
+
import { createPlugin, createRegistry, eagerComponent, extendRegistry, lazyComponent } from "./registry.js";
|
|
6
|
+
import { RegistryContext, setDefaultRegistry, useColumnRegistry, useComponentRegistry, useEffectHandlerRegistry } from "./registry-context.js";
|
|
6
7
|
import { cn } from "../lib/utils.js";
|
|
7
8
|
import { toNodes } from "./nodes.js";
|
|
8
9
|
import { CollapsedContext, useCollapsed } from "./collapsed-context.js";
|
|
9
10
|
import { RenderNode, Renderer } from "./renderer.js";
|
|
10
11
|
import { nodeIdentity } from "./test-id.js";
|
|
11
12
|
import { usePersistentState } from "../lib/use-persistent-state.js";
|
|
12
|
-
import { ApiError, apiFetch, apiJson, clearRemoteTokenCache, invalidateRemoteToken, remoteFetch, remoteJson, remoteToken } from "./api.js";
|
|
13
13
|
export { ApiError, CollapsedContext, LATTICE_REF_HEADER, RegistryContext, RenderNode, Renderer, apiFetch, apiJson, clearRemoteTokenCache, cn, createPlugin, createRegistry, dataBindings, eagerComponent, extendRegistry, invalidateRemoteToken, isRecord, lazyComponent, materializeNode, materializeProps, materializeSchema, nodeIdentity, remoteFetch, remoteJson, remoteToken, rowValue, setDefaultRegistry, toNodes, useCollapsed, useColumnRegistry, useComponentRegistry, useEffectHandlerRegistry, usePersistentState, withHeaders, withRefHeader };
|
package/dist/create-app.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { initializeAppearance } from "./appearance/index.js";
|
|
2
|
+
import { setRefRefreshEndpoint } from "./core/api.js";
|
|
3
|
+
import { isRecord } from "./core/materialize.js";
|
|
2
4
|
import { extendRegistry } from "./core/registry.js";
|
|
3
5
|
import { setDefaultRegistry } from "./core/registry-context.js";
|
|
4
6
|
import "./i18n/instance.js";
|
|
5
7
|
import { LocaleReload } from "./i18n/locale-reload.js";
|
|
6
8
|
import { i18nConfigFromPageProps } from "./i18n/shared-props.js";
|
|
7
|
-
import { createLayoutResolver, createPageResolver, withVisitHeaders } from "./inertia.js";
|
|
9
|
+
import { createLayoutResolver, createPageResolver, registerRefRenewal, withVisitHeaders } from "./inertia.js";
|
|
8
10
|
import { ProviderBase } from "./provider-base.js";
|
|
9
11
|
import { registry } from "./registry.js";
|
|
10
12
|
import { createInertiaApp } from "@inertiajs/react";
|
|
@@ -44,6 +46,7 @@ function createLatticeApp({ registry: registry$1, plugins, sprite, pages = {}, d
|
|
|
44
46
|
const baseRegistry = registry$1 ?? registry;
|
|
45
47
|
const activeRegistry = plugins && plugins.length > 0 ? extendRegistry(baseRegistry, ...plugins) : baseRegistry;
|
|
46
48
|
setDefaultRegistry(activeRegistry);
|
|
49
|
+
registerRefRenewal();
|
|
47
50
|
const i18nEnabled = i18n !== false;
|
|
48
51
|
const pluginI18n = plugins?.flatMap((plugin) => plugin.i18n ? [plugin.i18n] : []) ?? [];
|
|
49
52
|
const i18nOptions = i18nEnabled ? withPluginNamespaces(i18n ?? {}, pluginI18n) : {};
|
|
@@ -61,6 +64,8 @@ function createLatticeApp({ registry: registry$1, plugins, sprite, pages = {}, d
|
|
|
61
64
|
withApp: (node, { ssr, page }) => {
|
|
62
65
|
const pending = [];
|
|
63
66
|
if (!ssr) {
|
|
67
|
+
const shared = page.props.lattice;
|
|
68
|
+
if (isRecord(shared) && isRecord(shared.urls) && typeof shared.urls.refreshRef === "string") setRefRefreshEndpoint(shared.urls.refreshRef);
|
|
64
69
|
if (i18nEnabled && i18nConfigFromPageProps(page.props) !== void 0) pending.push(import("./i18n/page-props.js").then((module) => module.configureI18nFromPageProps(page.props, i18nOptions)));
|
|
65
70
|
const booted = boot?.({ page });
|
|
66
71
|
if (booted) pending.push(booted);
|
package/dist/create-app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-app.js","names":[],"sources":["../resources/js/create-app.tsx"],"sourcesContent":["import type { Page as InertiaPage, VisitOptions } from \"@inertiajs/core\";\nimport { createInertiaApp } from \"@inertiajs/react\";\nimport { useEffect, useState, type ReactElement, type ReactNode } from \"react\";\nimport { initializeAppearance } from \"./appearance\";\nimport { extendRegistry, type Plugin, type PluginI18n, type Registry } from \"./core/registry\";\nimport { setDefaultRegistry } from \"./core/registry-context\";\nimport type { SpriteValue } from \"./icons/sprite\";\nimport { DEFAULT_NAMESPACE } from \"./i18n/instance\";\nimport { LocaleReload } from \"./i18n/locale-reload\";\nimport { i18nConfigFromPageProps } from \"./i18n/shared-props\";\nimport {\n createLayoutResolver,\n createPageResolver,\n withVisitHeaders,\n type CreateLayoutResolverOptions,\n type PageModules,\n} from \"./inertia\";\nimport { ProviderBase } from \"./provider-base\";\nimport { registry as defaultRegistry } from \"./registry\";\n\ntype InertiaAppOptions = NonNullable<Parameters<typeof createInertiaApp>[0]>;\n\nexport type CreateLatticeAppI18nOptions = {\n /**\n * i18next namespaces to load, e.g. `[\"lattice\", \"app\"]`. Defaults to the\n * package's own. Namespaces declared by plugins are merged in on top.\n */\n namespaces?: string[];\n};\n\nexport type CreateLatticeAppOptions = Omit<\n InertiaAppOptions,\n \"resolve\" | \"layout\" | \"setup\" | \"withApp\" | \"pages\"\n> & {\n registry?: Registry;\n /**\n * Component-package plugins to merge onto the registry — pass the\n * Composer-discovered `virtual:lattice/plugins` here to register vendor\n * components with no manual `extendRegistry` call. A plugin's declared\n * i18n namespace joins the i18n bootstrap automatically.\n */\n plugins?: Plugin[];\n sprite?: SpriteValue;\n /** Normal Inertia page modules, e.g. `import.meta.glob('./pages/**\\/*.tsx')`. */\n pages?: PageModules;\n defaultLayout?: CreateLayoutResolverOptions[\"defaultLayout\"];\n /**\n * Lattice's i18n bootstrap, on by default: when the backend shares the\n * `lattice.i18n` prop, the first render waits for the translation setup (no\n * flash of untranslated fallbacks), `LocaleReload` re-fetches the page after\n * a locale switch, and every visit carries the locale/timezone headers. The\n * i18next chunk only loads when the backend actually shares the prop. Pass\n * `false` to opt out entirely.\n */\n i18n?: CreateLatticeAppI18nOptions | false;\n /**\n * App bootstrap that needs the initial page — e.g. `configureEcho` from a\n * shared connection prop. Runs on the client before the first render; a\n * returned promise delays that render until it resolves.\n */\n boot?: (context: { page: InertiaPage }) => void | Promise<void>;\n /**\n * Compose providers or siblings around the app. Applied inside the Lattice\n * Provider, so the registry, sprite, and toaster contexts are available.\n */\n wrap?: (app: ReactElement) => ReactElement;\n};\n\nfunction withPluginNamespaces(\n options: CreateLatticeAppI18nOptions,\n entries: PluginI18n[],\n): CreateLatticeAppI18nOptions {\n if (entries.length === 0) {\n return options;\n }\n\n const namespaces = new Set([\n ...(options.namespaces ?? [DEFAULT_NAMESPACE]),\n ...entries.map((entry) => entry.namespace),\n ]);\n\n return { ...options, namespaces: [...namespaces] };\n}\n\n// Fail-open: a failed bootstrap renders the app anyway (with fallback strings)\n// rather than a blank page.\nfunction AwaitReady({ ready, children }: { ready: Promise<unknown>; children: ReactNode }) {\n const [isReady, setReady] = useState(false);\n\n useEffect(() => {\n let active = true;\n const reveal = (): void => {\n if (active) {\n setReady(true);\n }\n };\n\n void ready.then(reveal, reveal);\n\n return () => {\n active = false;\n };\n }, [ready]);\n\n return isReady ? children : null;\n}\n\n/**\n * Bootstrap an Inertia app with the Lattice shell: the page/layout resolvers\n * (server-driven Lattice pages and normal Inertia pages alike), the Provider —\n * registry, sprite, flash toasts via Lattice's own Toaster — theme\n * initialization, and the i18n bootstrap. Forwards any other createInertiaApp\n * option.\n */\nexport function createLatticeApp({\n registry,\n plugins,\n sprite,\n pages = {},\n defaultLayout,\n strictMode = true,\n i18n,\n boot,\n wrap,\n ...inertiaOptions\n}: CreateLatticeAppOptions = {}) {\n const baseRegistry = registry ?? defaultRegistry;\n const activeRegistry =\n plugins && plugins.length > 0 ? extendRegistry(baseRegistry, ...plugins) : baseRegistry;\n\n setDefaultRegistry(activeRegistry);\n\n const i18nEnabled = i18n !== false;\n const pluginI18n = plugins?.flatMap((plugin) => (plugin.i18n ? [plugin.i18n] : [])) ?? [];\n const i18nOptions = i18nEnabled ? withPluginNamespaces(i18n ?? {}, pluginI18n) : {};\n const userVisitOptions = inertiaOptions.defaults?.visitOptions;\n const defaults = i18nEnabled\n ? {\n ...inertiaOptions.defaults,\n visitOptions: (href: string, options: VisitOptions): VisitOptions =>\n withVisitHeaders(href, userVisitOptions?.(href, options) ?? options),\n }\n : inertiaOptions.defaults;\n\n const app = createInertiaApp({\n ...inertiaOptions,\n defaults,\n strictMode,\n resolve: createPageResolver(pages),\n layout: createLayoutResolver({ defaultLayout }),\n withApp: (node: ReactElement, { ssr, page }: { ssr: boolean; page: InertiaPage }) => {\n const pending: Promise<unknown>[] = [];\n\n if (!ssr) {\n // Loaded on demand so apps without the shared i18n prop never ship the\n // i18next backend; the disabled-config call still stores the timezone.\n if (i18nEnabled && i18nConfigFromPageProps(page.props) !== undefined) {\n pending.push(\n import(\"./i18n/page-props\").then((module) =>\n module.configureI18nFromPageProps(page.props, i18nOptions),\n ),\n );\n }\n\n const booted = boot?.({ page });\n\n if (booted) {\n pending.push(booted);\n }\n }\n\n const inner = (\n <>\n {node}\n {i18nEnabled ? <LocaleReload /> : null}\n </>\n );\n\n // The whole shell waits, not just the page: anything rendering a\n // translated string (the Toaster included) would otherwise initialize\n // i18next before the backend can register — init runs exactly once,\n // first caller wins.\n const shell = (\n <ProviderBase registry={activeRegistry} sprite={sprite}>\n {wrap ? wrap(inner) : inner}\n </ProviderBase>\n );\n\n return pending.length > 0 ? (\n <AwaitReady ready={Promise.all(pending)}>{shell}</AwaitReady>\n ) : (\n shell\n );\n },\n });\n\n initializeAppearance();\n\n return app;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"create-app.js","names":[],"sources":["../resources/js/create-app.tsx"],"sourcesContent":["import type { Page as InertiaPage, VisitOptions } from \"@inertiajs/core\";\nimport { createInertiaApp } from \"@inertiajs/react\";\nimport { useEffect, useState, type ReactElement, type ReactNode } from \"react\";\nimport { initializeAppearance } from \"./appearance\";\nimport { setRefRefreshEndpoint } from \"./core/api\";\nimport { isRecord } from \"./core/materialize\";\nimport { extendRegistry, type Plugin, type PluginI18n, type Registry } from \"./core/registry\";\nimport { setDefaultRegistry } from \"./core/registry-context\";\nimport type { SpriteValue } from \"./icons/sprite\";\nimport { DEFAULT_NAMESPACE } from \"./i18n/instance\";\nimport { LocaleReload } from \"./i18n/locale-reload\";\nimport { i18nConfigFromPageProps } from \"./i18n/shared-props\";\nimport {\n createLayoutResolver,\n createPageResolver,\n registerRefRenewal,\n withVisitHeaders,\n type CreateLayoutResolverOptions,\n type PageModules,\n} from \"./inertia\";\nimport { ProviderBase } from \"./provider-base\";\nimport { registry as defaultRegistry } from \"./registry\";\n\ntype InertiaAppOptions = NonNullable<Parameters<typeof createInertiaApp>[0]>;\n\nexport type CreateLatticeAppI18nOptions = {\n /**\n * i18next namespaces to load, e.g. `[\"lattice\", \"app\"]`. Defaults to the\n * package's own. Namespaces declared by plugins are merged in on top.\n */\n namespaces?: string[];\n};\n\nexport type CreateLatticeAppOptions = Omit<\n InertiaAppOptions,\n \"resolve\" | \"layout\" | \"setup\" | \"withApp\" | \"pages\"\n> & {\n registry?: Registry;\n /**\n * Component-package plugins to merge onto the registry — pass the\n * Composer-discovered `virtual:lattice/plugins` here to register vendor\n * components with no manual `extendRegistry` call. A plugin's declared\n * i18n namespace joins the i18n bootstrap automatically.\n */\n plugins?: Plugin[];\n sprite?: SpriteValue;\n /** Normal Inertia page modules, e.g. `import.meta.glob('./pages/**\\/*.tsx')`. */\n pages?: PageModules;\n defaultLayout?: CreateLayoutResolverOptions[\"defaultLayout\"];\n /**\n * Lattice's i18n bootstrap, on by default: when the backend shares the\n * `lattice.i18n` prop, the first render waits for the translation setup (no\n * flash of untranslated fallbacks), `LocaleReload` re-fetches the page after\n * a locale switch, and every visit carries the locale/timezone headers. The\n * i18next chunk only loads when the backend actually shares the prop. Pass\n * `false` to opt out entirely.\n */\n i18n?: CreateLatticeAppI18nOptions | false;\n /**\n * App bootstrap that needs the initial page — e.g. `configureEcho` from a\n * shared connection prop. Runs on the client before the first render; a\n * returned promise delays that render until it resolves.\n */\n boot?: (context: { page: InertiaPage }) => void | Promise<void>;\n /**\n * Compose providers or siblings around the app. Applied inside the Lattice\n * Provider, so the registry, sprite, and toaster contexts are available.\n */\n wrap?: (app: ReactElement) => ReactElement;\n};\n\nfunction withPluginNamespaces(\n options: CreateLatticeAppI18nOptions,\n entries: PluginI18n[],\n): CreateLatticeAppI18nOptions {\n if (entries.length === 0) {\n return options;\n }\n\n const namespaces = new Set([\n ...(options.namespaces ?? [DEFAULT_NAMESPACE]),\n ...entries.map((entry) => entry.namespace),\n ]);\n\n return { ...options, namespaces: [...namespaces] };\n}\n\n// Fail-open: a failed bootstrap renders the app anyway (with fallback strings)\n// rather than a blank page.\nfunction AwaitReady({ ready, children }: { ready: Promise<unknown>; children: ReactNode }) {\n const [isReady, setReady] = useState(false);\n\n useEffect(() => {\n let active = true;\n const reveal = (): void => {\n if (active) {\n setReady(true);\n }\n };\n\n void ready.then(reveal, reveal);\n\n return () => {\n active = false;\n };\n }, [ready]);\n\n return isReady ? children : null;\n}\n\n/**\n * Bootstrap an Inertia app with the Lattice shell: the page/layout resolvers\n * (server-driven Lattice pages and normal Inertia pages alike), the Provider —\n * registry, sprite, flash toasts via Lattice's own Toaster — theme\n * initialization, and the i18n bootstrap. Forwards any other createInertiaApp\n * option.\n */\nexport function createLatticeApp({\n registry,\n plugins,\n sprite,\n pages = {},\n defaultLayout,\n strictMode = true,\n i18n,\n boot,\n wrap,\n ...inertiaOptions\n}: CreateLatticeAppOptions = {}) {\n const baseRegistry = registry ?? defaultRegistry;\n const activeRegistry =\n plugins && plugins.length > 0 ? extendRegistry(baseRegistry, ...plugins) : baseRegistry;\n\n setDefaultRegistry(activeRegistry);\n registerRefRenewal();\n\n const i18nEnabled = i18n !== false;\n const pluginI18n = plugins?.flatMap((plugin) => (plugin.i18n ? [plugin.i18n] : [])) ?? [];\n const i18nOptions = i18nEnabled ? withPluginNamespaces(i18n ?? {}, pluginI18n) : {};\n const userVisitOptions = inertiaOptions.defaults?.visitOptions;\n const defaults = i18nEnabled\n ? {\n ...inertiaOptions.defaults,\n visitOptions: (href: string, options: VisitOptions): VisitOptions =>\n withVisitHeaders(href, userVisitOptions?.(href, options) ?? options),\n }\n : inertiaOptions.defaults;\n\n const app = createInertiaApp({\n ...inertiaOptions,\n defaults,\n strictMode,\n resolve: createPageResolver(pages),\n layout: createLayoutResolver({ defaultLayout }),\n withApp: (node: ReactElement, { ssr, page }: { ssr: boolean; page: InertiaPage }) => {\n const pending: Promise<unknown>[] = [];\n\n if (!ssr) {\n const shared = page.props.lattice;\n\n if (\n isRecord(shared) &&\n isRecord(shared.urls) &&\n typeof shared.urls.refreshRef === \"string\"\n ) {\n setRefRefreshEndpoint(shared.urls.refreshRef);\n }\n\n // Loaded on demand so apps without the shared i18n prop never ship the\n // i18next backend; the disabled-config call still stores the timezone.\n if (i18nEnabled && i18nConfigFromPageProps(page.props) !== undefined) {\n pending.push(\n import(\"./i18n/page-props\").then((module) =>\n module.configureI18nFromPageProps(page.props, i18nOptions),\n ),\n );\n }\n\n const booted = boot?.({ page });\n\n if (booted) {\n pending.push(booted);\n }\n }\n\n const inner = (\n <>\n {node}\n {i18nEnabled ? <LocaleReload /> : null}\n </>\n );\n\n // The whole shell waits, not just the page: anything rendering a\n // translated string (the Toaster included) would otherwise initialize\n // i18next before the backend can register — init runs exactly once,\n // first caller wins.\n const shell = (\n <ProviderBase registry={activeRegistry} sprite={sprite}>\n {wrap ? wrap(inner) : inner}\n </ProviderBase>\n );\n\n return pending.length > 0 ? (\n <AwaitReady ready={Promise.all(pending)}>{shell}</AwaitReady>\n ) : (\n shell\n );\n },\n });\n\n initializeAppearance();\n\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuEA,SAAS,qBACP,SACA,SAC6B;CAC7B,IAAI,QAAQ,WAAW,GACrB,OAAO;CAGT,MAAM,aAAa,IAAI,IAAI,CACzB,GAAI,QAAQ,cAAc,CAAA,SAAkB,GAC5C,GAAG,QAAQ,KAAK,UAAU,MAAM,SAAS,CAC3C,CAAC;CAED,OAAO;EAAE,GAAG;EAAS,YAAY,CAAC,GAAG,UAAU;CAAE;AACnD;AAIA,SAAS,WAAW,EAAE,OAAO,YAA8D;CACzF,MAAM,CAAC,SAAS,YAAY,SAAS,KAAK;CAE1C,gBAAgB;EACd,IAAI,SAAS;EACb,MAAM,eAAqB;GACzB,IAAI,QACF,SAAS,IAAI;EAEjB;EAEA,MAAW,KAAK,QAAQ,MAAM;EAE9B,aAAa;GACX,SAAS;EACX;CACF,GAAG,CAAC,KAAK,CAAC;CAEV,OAAO,UAAU,WAAW;AAC9B;;;;;;;;AASA,SAAgB,iBAAiB,EAC/B,UAAA,YACA,SACA,QACA,QAAQ,CAAC,GACT,eACA,aAAa,MACb,MACA,MACA,MACA,GAAG,mBACwB,CAAC,GAAG;CAC/B,MAAM,eAAe,cAAY;CACjC,MAAM,iBACJ,WAAW,QAAQ,SAAS,IAAI,eAAe,cAAc,GAAG,OAAO,IAAI;CAE7E,mBAAmB,cAAc;CACjC,mBAAmB;CAEnB,MAAM,cAAc,SAAS;CAC7B,MAAM,aAAa,SAAS,SAAS,WAAY,OAAO,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC,CAAE,KAAK,CAAC;CACxF,MAAM,cAAc,cAAc,qBAAqB,QAAQ,CAAC,GAAG,UAAU,IAAI,CAAC;CAClF,MAAM,mBAAmB,eAAe,UAAU;CAClD,MAAM,WAAW,cACb;EACE,GAAG,eAAe;EAClB,eAAe,MAAc,YAC3B,iBAAiB,MAAM,mBAAmB,MAAM,OAAO,KAAK,OAAO;CACvE,IACA,eAAe;CAEnB,MAAM,MAAM,iBAAiB;EAC3B,GAAG;EACH;EACA;EACA,SAAS,mBAAmB,KAAK;EACjC,QAAQ,qBAAqB,EAAE,cAAc,CAAC;EAC9C,UAAU,MAAoB,EAAE,KAAK,WAAgD;GACnF,MAAM,UAA8B,CAAC;GAErC,IAAI,CAAC,KAAK;IACR,MAAM,SAAS,KAAK,MAAM;IAE1B,IACE,SAAS,MAAM,KACf,SAAS,OAAO,IAAI,KACpB,OAAO,OAAO,KAAK,eAAe,UAElC,sBAAsB,OAAO,KAAK,UAAU;IAK9C,IAAI,eAAe,wBAAwB,KAAK,KAAK,MAAM,KAAA,GACzD,QAAQ,KACN,OAAO,wBAAqB,MAAM,WAChC,OAAO,2BAA2B,KAAK,OAAO,WAAW,CAC3D,CACF;IAGF,MAAM,SAAS,OAAO,EAAE,KAAK,CAAC;IAE9B,IAAI,QACF,QAAQ,KAAK,MAAM;GAEvB;GAEA,MAAM,QACJ,qBAAA,YAAA,EAAA,UAAA,CACG,MACA,cAAc,oBAAC,cAAD,CAAe,CAAA,IAAI,IAClC,EAAA,CAAA;GAOJ,MAAM,QACJ,oBAAC,cAAD;IAAc,UAAU;IAAwB;cAC7C,OAAO,KAAK,KAAK,IAAI;GACV,CAAA;GAGhB,OAAO,QAAQ,SAAS,IACtB,oBAAC,YAAD;IAAY,OAAO,QAAQ,IAAI,OAAO;cAAI;GAAkB,CAAA,IAE5D;EAEJ;CACF,CAAC;CAED,qBAAqB;CAErB,OAAO;AACT"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { draggable, dropTargetForElements, monitorForElements, } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
2
|
+
export type { ElementDragPayload, ElementEventBasePayload, ElementDropTargetEventBasePayload, ElementGetFeedbackArgs, ElementDropTargetGetFeedbackArgs, ElementMonitorGetFeedbackArgs, } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
3
|
+
export { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
|
4
|
+
export { reorder } from '@atlaskit/pragmatic-drag-and-drop/reorder';
|
|
5
|
+
export { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
|
6
|
+
export { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
|
|
7
|
+
export type { AllDragTypes, BaseEventPayload, CleanupFn, DragLocation, DragLocationHistory, DropTargetRecord, ElementDragType, Input, MonitorArgs, Position, } from '@atlaskit/pragmatic-drag-and-drop/types';
|
|
8
|
+
export { attachClosestEdge, extractClosestEdge, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
9
|
+
export type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
10
|
+
export { attachInstruction as attachTreeItemInstruction, extractInstruction as extractTreeItemInstruction, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
11
|
+
export type { Instruction as TreeItemInstruction, ItemMode as TreeItemMode, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
12
|
+
export { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index';
|
|
13
|
+
export { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge';
|
|
14
|
+
export { announce, cleanup as cleanupLiveRegion, } from '@atlaskit/pragmatic-drag-and-drop-live-region';
|
|
15
|
+
export { autoScrollForElements, autoScrollWindowForElements, } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
|
16
|
+
export { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { draggable, dropTargetForElements, monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
2
|
+
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
3
|
+
import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder";
|
|
4
|
+
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
|
5
|
+
import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source";
|
|
6
|
+
import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
7
|
+
import { attachInstruction as attachTreeItemInstruction, extractInstruction as extractTreeItemInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
|
8
|
+
import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index";
|
|
9
|
+
import { reorderWithEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge";
|
|
10
|
+
import { announce, cleanup as cleanupLiveRegion } from "@atlaskit/pragmatic-drag-and-drop-live-region";
|
|
11
|
+
import { autoScrollForElements, autoScrollWindowForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
|
12
|
+
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
|
|
13
|
+
export { announce, attachClosestEdge, attachTreeItemInstruction, autoScrollForElements, autoScrollWindowForElements, cleanupLiveRegion, combine, draggable, dropTargetForElements, extractClosestEdge, extractTreeItemInstruction, getReorderDestinationIndex, monitorForElements, pointerOutsideOfPreview, preserveOffsetOnSource, reorder, reorderWithEdge, setCustomNativeDragPreview };
|
|
@@ -88,7 +88,7 @@ function DatePickerField({ mode, label, name, testId, value, min, max, step, dis
|
|
|
88
88
|
disabled: disabled || readOnly,
|
|
89
89
|
size: "icon",
|
|
90
90
|
type: "button",
|
|
91
|
-
|
|
91
|
+
variant: "secondary",
|
|
92
92
|
children: /* @__PURE__ */ jsx(Icon, {
|
|
93
93
|
name: "calendar",
|
|
94
94
|
className: "size-lt-icon-md",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-picker-field.js","names":[],"sources":["../../../../resources/js/form/components/fields/date-picker-field.tsx"],"sourcesContent":["import type { DateValue } from \"@internationalized/date\";\nimport * as datePicker from \"@zag-js/date-picker\";\nimport { normalizeProps, useMachine } from \"@zag-js/react\";\nimport { useId, useMemo } from \"react\";\nimport { Button } from \"@lattice-php/lattice/ui/button\";\nimport { Icon } from \"@lattice-php/lattice/icons\";\nimport { useLocale } from \"@lattice-php/lattice/i18n\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { Input } from \"@lattice-php/lattice/ui/input\";\nimport {\n formatDateDisplayValue,\n formatDateTimeDisplayValue,\n formatDateTimeValue,\n formatDateValue,\n formatTimeInputValue,\n parseDateDisplayValue,\n parseDateTimeDisplayValue,\n parseDateTimeValue,\n parseDateValue,\n} from \"./date-picker-value\";\nimport { TimePicker } from \"./time-picker\";\nimport { parseTimeString } from \"./time-picker-columns\";\n\nexport type DatePickerFieldProps = {\n mode: \"date\" | \"date-time\";\n label: string;\n name: string;\n testId: string;\n value: unknown;\n min?: string | null;\n max?: string | null;\n step?: number | null;\n disabled: boolean;\n readOnly: boolean;\n autoFocus?: boolean;\n tabIndex?: number | null;\n timezone?: string;\n onChange: (value: string) => void;\n onBlur?: () => void;\n};\n\nexport function DatePickerField({\n mode,\n label,\n name,\n testId,\n value,\n min,\n max,\n step,\n disabled,\n readOnly,\n autoFocus = false,\n tabIndex,\n timezone = \"UTC\",\n onChange,\n onBlur,\n}: DatePickerFieldProps) {\n const id = useId();\n const { locale } = useLocale();\n const selected = useMemo(\n () =>\n [mode === \"date\" ? parseDateValue(value) : parseDateTimeValue(value, timezone)].filter(\n Boolean,\n ) as DateValue[],\n [mode, timezone, value],\n );\n const service = useMachine(datePicker.machine, {\n id,\n name,\n value: selected.length > 0 ? selected : undefined,\n min: min ? parseDateValue(min) : undefined,\n max: max ? parseDateValue(max) : undefined,\n disabled,\n readOnly,\n locale,\n selectionMode: \"single\",\n timeZone: timezone,\n closeOnSelect: mode === \"date\",\n format(date) {\n return mode === \"date\"\n ? formatDateDisplayValue(date, locale)\n : formatDateTimeDisplayValue(date, locale, timezone);\n },\n parse(text) {\n return mode === \"date\"\n ? parseDateDisplayValue(text, locale)\n : parseDateTimeDisplayValue(text, locale, timezone);\n },\n onValueChange(details) {\n const next = details.value[0];\n\n onChange(mode === \"date\" ? formatDateValue(next) : formatDateTimeValue(next, timezone));\n },\n onOpenChange(details) {\n if (!details.open) {\n onBlur?.();\n }\n },\n });\n const api = datePicker.connect(service, normalizeProps);\n const { name: _inputName, onInput, ...inputProps } = api.getInputProps();\n const submittedValue =\n mode === \"date\" ? formatDateValue(selected[0]) : formatDateTimeValue(selected[0], timezone);\n\n return (\n <div {...api.getRootProps()} className={cn(\"relative\", api.open && \"z-lt-popover\")}>\n <input type=\"hidden\" name={name} value={submittedValue} data-test={`${testId}-value`} />\n <div {...api.getControlProps()} className=\"flex gap-2\">\n <Input\n {...inputProps}\n aria-label={label}\n autoFocus={autoFocus}\n data-test={testId}\n disabled={disabled}\n id={name}\n onInput={(event) => {\n onInput?.(event);\n\n if (mode !== \"date\") {\n return;\n }\n\n const normalized = normalizeDateInputValue(event.currentTarget.value);\n\n if (!normalized) {\n return;\n }\n\n const next = parseDateValue(normalized);\n\n if (!next) {\n return;\n }\n\n event.currentTarget.value = normalized;\n api.setValue([next]);\n event.currentTarget.value = formatDateDisplayValue(next, locale);\n onChange(formatDateValue(next));\n }}\n readOnly={readOnly}\n tabIndex={tabIndex ?? undefined}\n />\n <Button\n {...api.getTriggerProps()}\n aria-label={`Open ${label || name} calendar`}\n disabled={disabled || readOnly}\n size=\"icon\"\n type=\"button\"\n color=\"secondary\"\n >\n <Icon name=\"calendar\" className=\"size-lt-icon-md\" aria-hidden=\"true\" />\n </Button>\n </div>\n {api.open ? (\n <div\n {...api.getPositionerProps()}\n className=\"absolute z-lt-popover mt-2 rounded-lt-sm border border-lt-border bg-lt-popover p-3 text-lt-popover-fg shadow-lt-md\"\n >\n <div {...api.getContentProps()} className=\"grid gap-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <Button {...api.getPrevTriggerProps()} emphasis=\"ghost\" size=\"icon\" type=\"button\">\n <Icon name=\"chevron-left\" className=\"size-lt-icon-md\" aria-hidden=\"true\" />\n </Button>\n <div {...api.getRangeTextProps()} className=\"text-sm font-medium text-lt-fg\" />\n <Button {...api.getNextTriggerProps()} emphasis=\"ghost\" size=\"icon\" type=\"button\">\n <Icon name=\"chevron-right\" className=\"size-lt-icon-md\" aria-hidden=\"true\" />\n </Button>\n </div>\n <table {...api.getTableProps()} className=\"w-full border-collapse text-sm\">\n <thead {...api.getTableHeadProps()}>\n <tr {...api.getTableRowProps()}>\n {api.weekDays.map((day) => (\n <th\n {...api.getTableHeaderProps()}\n aria-label={day.long}\n key={day.value.toString()}\n className=\"size-8 text-center text-xs font-medium text-lt-muted-fg\"\n >\n {day.narrow}\n </th>\n ))}\n </tr>\n </thead>\n <tbody {...api.getTableBodyProps()}>\n {api.weeks.map((week, weekIndex) => (\n <tr {...api.getTableRowProps()} key={weekIndex}>\n {week.map((day) => {\n const state = api.getDayTableCellState({ value: day });\n\n return (\n <td\n {...api.getDayTableCellProps({ value: day })}\n key={day.toString()}\n className=\"p-0 text-center\"\n >\n <button\n {...api.getDayTableCellTriggerProps({ value: day })}\n type=\"button\"\n className={cn(\n \"size-8 rounded-lt-sm text-sm text-lt-fg hover:bg-lt-muted\",\n state.selected && \"bg-lt-primary text-lt-primary-fg\",\n state.outsideRange && \"text-lt-muted-fg\",\n state.disabled && \"cursor-not-allowed opacity-40\",\n )}\n >\n {day.day}\n </button>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n {mode === \"date-time\" ? (\n <TimePicker\n value={parseTimeString(formatTimeInputValue(selected[0], timezone))}\n onChange={(next) =>\n api.setTime({ hour: next.hour, minute: next.minute, second: next.second })\n }\n step={step}\n disabled={disabled}\n readOnly={readOnly}\n testId={`${testId}-time`}\n />\n ) : null}\n </div>\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction normalizeDateInputValue(value: string): string | undefined {\n const compact = value.replace(/\\D/g, \"\");\n\n if (compact.length !== 8) {\n return undefined;\n }\n\n return `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"date-picker-field.js","names":[],"sources":["../../../../resources/js/form/components/fields/date-picker-field.tsx"],"sourcesContent":["import type { DateValue } from \"@internationalized/date\";\nimport * as datePicker from \"@zag-js/date-picker\";\nimport { normalizeProps, useMachine } from \"@zag-js/react\";\nimport { useId, useMemo } from \"react\";\nimport { Button } from \"@lattice-php/lattice/ui/button\";\nimport { Icon } from \"@lattice-php/lattice/icons\";\nimport { useLocale } from \"@lattice-php/lattice/i18n\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { Input } from \"@lattice-php/lattice/ui/input\";\nimport {\n formatDateDisplayValue,\n formatDateTimeDisplayValue,\n formatDateTimeValue,\n formatDateValue,\n formatTimeInputValue,\n parseDateDisplayValue,\n parseDateTimeDisplayValue,\n parseDateTimeValue,\n parseDateValue,\n} from \"./date-picker-value\";\nimport { TimePicker } from \"./time-picker\";\nimport { parseTimeString } from \"./time-picker-columns\";\n\nexport type DatePickerFieldProps = {\n mode: \"date\" | \"date-time\";\n label: string;\n name: string;\n testId: string;\n value: unknown;\n min?: string | null;\n max?: string | null;\n step?: number | null;\n disabled: boolean;\n readOnly: boolean;\n autoFocus?: boolean;\n tabIndex?: number | null;\n timezone?: string;\n onChange: (value: string) => void;\n onBlur?: () => void;\n};\n\nexport function DatePickerField({\n mode,\n label,\n name,\n testId,\n value,\n min,\n max,\n step,\n disabled,\n readOnly,\n autoFocus = false,\n tabIndex,\n timezone = \"UTC\",\n onChange,\n onBlur,\n}: DatePickerFieldProps) {\n const id = useId();\n const { locale } = useLocale();\n const selected = useMemo(\n () =>\n [mode === \"date\" ? parseDateValue(value) : parseDateTimeValue(value, timezone)].filter(\n Boolean,\n ) as DateValue[],\n [mode, timezone, value],\n );\n const service = useMachine(datePicker.machine, {\n id,\n name,\n value: selected.length > 0 ? selected : undefined,\n min: min ? parseDateValue(min) : undefined,\n max: max ? parseDateValue(max) : undefined,\n disabled,\n readOnly,\n locale,\n selectionMode: \"single\",\n timeZone: timezone,\n closeOnSelect: mode === \"date\",\n format(date) {\n return mode === \"date\"\n ? formatDateDisplayValue(date, locale)\n : formatDateTimeDisplayValue(date, locale, timezone);\n },\n parse(text) {\n return mode === \"date\"\n ? parseDateDisplayValue(text, locale)\n : parseDateTimeDisplayValue(text, locale, timezone);\n },\n onValueChange(details) {\n const next = details.value[0];\n\n onChange(mode === \"date\" ? formatDateValue(next) : formatDateTimeValue(next, timezone));\n },\n onOpenChange(details) {\n if (!details.open) {\n onBlur?.();\n }\n },\n });\n const api = datePicker.connect(service, normalizeProps);\n const { name: _inputName, onInput, ...inputProps } = api.getInputProps();\n const submittedValue =\n mode === \"date\" ? formatDateValue(selected[0]) : formatDateTimeValue(selected[0], timezone);\n\n return (\n <div {...api.getRootProps()} className={cn(\"relative\", api.open && \"z-lt-popover\")}>\n <input type=\"hidden\" name={name} value={submittedValue} data-test={`${testId}-value`} />\n <div {...api.getControlProps()} className=\"flex gap-2\">\n <Input\n {...inputProps}\n aria-label={label}\n autoFocus={autoFocus}\n data-test={testId}\n disabled={disabled}\n id={name}\n onInput={(event) => {\n onInput?.(event);\n\n if (mode !== \"date\") {\n return;\n }\n\n const normalized = normalizeDateInputValue(event.currentTarget.value);\n\n if (!normalized) {\n return;\n }\n\n const next = parseDateValue(normalized);\n\n if (!next) {\n return;\n }\n\n event.currentTarget.value = normalized;\n api.setValue([next]);\n event.currentTarget.value = formatDateDisplayValue(next, locale);\n onChange(formatDateValue(next));\n }}\n readOnly={readOnly}\n tabIndex={tabIndex ?? undefined}\n />\n <Button\n {...api.getTriggerProps()}\n aria-label={`Open ${label || name} calendar`}\n disabled={disabled || readOnly}\n size=\"icon\"\n type=\"button\"\n variant=\"secondary\"\n >\n <Icon name=\"calendar\" className=\"size-lt-icon-md\" aria-hidden=\"true\" />\n </Button>\n </div>\n {api.open ? (\n <div\n {...api.getPositionerProps()}\n className=\"absolute z-lt-popover mt-2 rounded-lt-sm border border-lt-border bg-lt-popover p-3 text-lt-popover-fg shadow-lt-md\"\n >\n <div {...api.getContentProps()} className=\"grid gap-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <Button {...api.getPrevTriggerProps()} emphasis=\"ghost\" size=\"icon\" type=\"button\">\n <Icon name=\"chevron-left\" className=\"size-lt-icon-md\" aria-hidden=\"true\" />\n </Button>\n <div {...api.getRangeTextProps()} className=\"text-sm font-medium text-lt-fg\" />\n <Button {...api.getNextTriggerProps()} emphasis=\"ghost\" size=\"icon\" type=\"button\">\n <Icon name=\"chevron-right\" className=\"size-lt-icon-md\" aria-hidden=\"true\" />\n </Button>\n </div>\n <table {...api.getTableProps()} className=\"w-full border-collapse text-sm\">\n <thead {...api.getTableHeadProps()}>\n <tr {...api.getTableRowProps()}>\n {api.weekDays.map((day) => (\n <th\n {...api.getTableHeaderProps()}\n aria-label={day.long}\n key={day.value.toString()}\n className=\"size-8 text-center text-xs font-medium text-lt-muted-fg\"\n >\n {day.narrow}\n </th>\n ))}\n </tr>\n </thead>\n <tbody {...api.getTableBodyProps()}>\n {api.weeks.map((week, weekIndex) => (\n <tr {...api.getTableRowProps()} key={weekIndex}>\n {week.map((day) => {\n const state = api.getDayTableCellState({ value: day });\n\n return (\n <td\n {...api.getDayTableCellProps({ value: day })}\n key={day.toString()}\n className=\"p-0 text-center\"\n >\n <button\n {...api.getDayTableCellTriggerProps({ value: day })}\n type=\"button\"\n className={cn(\n \"size-8 rounded-lt-sm text-sm text-lt-fg hover:bg-lt-muted\",\n state.selected && \"bg-lt-primary text-lt-primary-fg\",\n state.outsideRange && \"text-lt-muted-fg\",\n state.disabled && \"cursor-not-allowed opacity-40\",\n )}\n >\n {day.day}\n </button>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n {mode === \"date-time\" ? (\n <TimePicker\n value={parseTimeString(formatTimeInputValue(selected[0], timezone))}\n onChange={(next) =>\n api.setTime({ hour: next.hour, minute: next.minute, second: next.second })\n }\n step={step}\n disabled={disabled}\n readOnly={readOnly}\n testId={`${testId}-time`}\n />\n ) : null}\n </div>\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction normalizeDateInputValue(value: string): string | undefined {\n const compact = value.replace(/\\D/g, \"\");\n\n if (compact.length !== 8) {\n return undefined;\n }\n\n return `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`;\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { apiFetch } from "../../../core/api.js";
|
|
1
2
|
import { useT } from "../../../i18n/instance.js";
|
|
2
3
|
import { testIdentity } from "../../../core/test-id.js";
|
|
3
|
-
import { apiFetch } from "../../../core/api.js";
|
|
4
4
|
import { useFormContext } from "../../hooks/context.js";
|
|
5
5
|
import { useFieldScope } from "../../hooks/field-scope.js";
|
|
6
6
|
import { useFormValues, useSetFormValue } from "../../hooks/values.js";
|
|
@@ -100,7 +100,8 @@ var FileUploadComponent = ({ node }) => {
|
|
|
100
100
|
ref: componentRef,
|
|
101
101
|
body: JSON.stringify({
|
|
102
102
|
...values,
|
|
103
|
-
|
|
103
|
+
_sub: "upload",
|
|
104
|
+
_target: uploadKey,
|
|
104
105
|
filename: file.name,
|
|
105
106
|
contentType: file.type
|
|
106
107
|
}),
|