diff --git a/packages/react/src/components/F0Form/F0Form.tsx b/packages/react/src/components/F0Form/F0Form.tsx index 666c76bd86..7da7435f39 100644 --- a/packages/react/src/components/F0Form/F0Form.tsx +++ b/packages/react/src/components/F0Form/F0Form.tsx @@ -159,6 +159,7 @@ function F0FormPerSection( initialFiles, renderCustomField, isLoading: isFormLoading, + useUpload, } = props const showSectionsSidepanel = styling?.showSectionsSidepanel ?? false @@ -223,6 +224,7 @@ function F0FormPerSection( initialFiles={initialFiles} renderCustomField={renderCustomField} isLoading={isFormLoading} + useUpload={useUpload} /> ) @@ -354,6 +356,8 @@ function F0FormFromDefinition( renderCustomField, } = props + const useUpload = "useUpload" in props ? props.useUpload : undefined + if (formDefinition.isLoading) { if (formDefinition._brand === "single") { return ( @@ -366,6 +370,7 @@ function F0FormFromDefinition( formRef={formRef} initialFiles={initialFiles} renderCustomField={renderCustomField} + useUpload={useUpload} isLoading /> ) @@ -380,6 +385,7 @@ function F0FormFromDefinition( formRef={formRef} initialFiles={initialFiles} renderCustomField={renderCustomField} + useUpload={useUpload} isLoading /> ) @@ -396,6 +402,7 @@ function F0FormFromDefinition( formRef={formRef} initialFiles={initialFiles} renderCustomField={renderCustomField} + useUpload={useUpload} /> ) } @@ -410,6 +417,7 @@ function F0FormFromDefinition( formRef={formRef} initialFiles={initialFiles} renderCustomField={renderCustomField} + useUpload={useUpload} /> ) } @@ -421,6 +429,7 @@ function F0FormFromSingleDefinition({ formRef, initialFiles, renderCustomField, + useUpload, isLoading, }: F0FormPropsWithSingleSchemaDefinition & { isLoading?: boolean }) { const def = formDefinition as F0FormDefinitionSingleSchema @@ -447,6 +456,7 @@ function F0FormFromSingleDefinition({ formRef={formRef} initialFiles={initialFiles} renderCustomField={renderCustomField} + useUpload={useUpload} isLoading={isLoading} defaultValuesParamsSchema={def.defaultValuesParamsSchema} defaultValuesFn={def.defaultValuesFn} @@ -461,6 +471,7 @@ function F0FormFromPerSectionDefinition({ formRef, initialFiles, renderCustomField, + useUpload, isLoading, }: F0FormPropsWithPerSectionDefinition & { isLoading?: boolean }) { const def = formDefinition as F0FormDefinitionPerSection @@ -506,6 +517,7 @@ function F0FormFromPerSectionDefinition({ formRef={formRef} initialFiles={initialFiles} renderCustomField={renderCustomField} + useUpload={useUpload} isLoading={isLoading} /> ) @@ -533,6 +545,8 @@ function F0FormSingleSchema( defaultValuesFn, } = props + const { useUpload } = props + // Resolve styling configuration const showSectionsSidepanel = styling?.showSectionsSidepanel ?? false @@ -873,8 +887,15 @@ function F0FormSingleSchema( initialFiles: props.initialFiles, renderCustomField: props.renderCustomField, isLoading: isFormLoading, + useUpload, }), - [name, props.initialFiles, props.renderCustomField, isFormLoading] + [ + name, + props.initialFiles, + props.renderCustomField, + isFormLoading, + useUpload, + ] ) // Form content component to avoid repetition diff --git a/packages/react/src/components/F0Form/__stories__/F0Form.stories.tsx b/packages/react/src/components/F0Form/__stories__/F0Form.stories.tsx index 1444f813f9..85214b6722 100644 --- a/packages/react/src/components/F0Form/__stories__/F0Form.stories.tsx +++ b/packages/react/src/components/F0Form/__stories__/F0Form.stories.tsx @@ -1052,7 +1052,6 @@ export const FileFields: Story = { accept: ["image/jpeg", "image/png", "image/webp"], maxSizeMB: 5, description: "Upload a JPEG, PNG, or WebP image (max 5 MB)", - useUpload: useMockUpload, }), attachments: f0FormField( z.array(z.string()).min(1, "Upload at least one file"), @@ -1062,7 +1061,6 @@ export const FileFields: Story = { multiple: true, accept: ["application/pdf", "image"], maxSizeMB: 50, - useUpload: useMockUpload, } ), notes: f0FormField(z.string().optional(), { @@ -1090,7 +1088,7 @@ export const FileFields: Story = { submitConfig: { label: "Save Document" }, }) - return + return }, } @@ -1106,7 +1104,6 @@ export const FileFieldsWithInitialFiles: Story = { label: "Contract Document", fieldType: "file", accept: ["application/pdf"], - useUpload: useMockUpload, }), attachments: f0FormField( z.array(z.string()).min(1, "Upload at least one file"), @@ -1116,7 +1113,6 @@ export const FileFieldsWithInitialFiles: Story = { multiple: true, accept: ["application/pdf", "image"], maxSizeMB: 50, - useUpload: useMockUpload, } ), }) @@ -1139,6 +1135,7 @@ export const FileFieldsWithInitialFiles: Story = { return ( { initialFiles?: import("../fields/file/types").InitialFile[] formRef?: React.MutableRefObject renderCustomField?: RenderCustomFieldFunction + /** Upload hook shared by all file fields */ + useUpload?: import("../fields/file/types").UseFileUpload /** Whether async defaultValues are still being resolved */ isLoading?: boolean } @@ -137,6 +139,7 @@ export function F0FormSection({ initialFiles, formRef, renderCustomField, + useUpload, isLoading: isFormLoading, }: F0FormSectionProps) { const i18n = useI18n() @@ -290,8 +293,9 @@ export function F0FormSection({ initialFiles, renderCustomField, isLoading: isFormLoading, + useUpload, }), - [formName, initialFiles, renderCustomField, isFormLoading] + [formName, initialFiles, renderCustomField, isFormLoading, useUpload] ) const title = sectionConfig?.title ?? sectionId diff --git a/packages/react/src/components/F0Form/context.ts b/packages/react/src/components/F0Form/context.ts index df28dec5cc..beb6cb3103 100644 --- a/packages/react/src/components/F0Form/context.ts +++ b/packages/react/src/components/F0Form/context.ts @@ -1,6 +1,6 @@ import { createContext, useContext } from "react" -import type { InitialFile } from "./fields/file/types" +import type { InitialFile, UseFileUpload } from "./fields/file/types" import type { RenderCustomFieldFunction } from "./types" interface F0FormContextValue { @@ -12,6 +12,8 @@ interface F0FormContextValue { renderCustomField?: RenderCustomFieldFunction /** Whether async defaultValues are still being resolved */ isLoading?: boolean + /** Default upload hook shared across all file fields */ + useUpload?: UseFileUpload } export const F0FormContext = createContext(null) diff --git a/packages/react/src/components/F0Form/f0Schema.ts b/packages/react/src/components/F0Form/f0Schema.ts index 1694f3038d..2e33de48cb 100644 --- a/packages/react/src/components/F0Form/f0Schema.ts +++ b/packages/react/src/components/F0Form/f0Schema.ts @@ -669,11 +669,6 @@ export function inferFieldType( return config.fieldType } - // If useUpload is provided, it's a file field - if ("useUpload" in config && config.useUpload) { - return "file" - } - // If options or source are provided, it's a select if ( ("options" in config && config.options) || diff --git a/packages/react/src/components/F0Form/fields/file/FileFieldRenderer.tsx b/packages/react/src/components/F0Form/fields/file/FileFieldRenderer.tsx index 370d736b6b..204a3ac710 100644 --- a/packages/react/src/components/F0Form/fields/file/FileFieldRenderer.tsx +++ b/packages/react/src/components/F0Form/fields/file/FileFieldRenderer.tsx @@ -1,11 +1,12 @@ import { useCallback, useId, useMemo, useRef, useState } from "react" import { ControllerRenderProps, FieldValues } from "react-hook-form" +import type { InputFieldStatusType } from "@/ui/InputField/types" + import { F0Icon } from "@/components/F0Icon" import { Upload } from "@/icons/app" import { useI18n } from "@/lib/providers/i18n/i18n-provider" import { cn, focusRing } from "@/lib/utils" -import type { InputFieldStatusType } from "@/ui/InputField/types" import type { ResolvedField } from "../types" import type { F0FileField, FileEntry, InitialFile } from "./types" @@ -441,7 +442,7 @@ export function FileFieldRenderer({ handleUploadComplete(entry.key, value) } diff --git a/packages/react/src/components/F0Form/fields/file/__tests__/FileFieldRenderer.test.tsx b/packages/react/src/components/F0Form/fields/file/__tests__/FileFieldRenderer.test.tsx index 01ffb4745a..f31f9c7ca8 100644 --- a/packages/react/src/components/F0Form/fields/file/__tests__/FileFieldRenderer.test.tsx +++ b/packages/react/src/components/F0Form/fields/file/__tests__/FileFieldRenderer.test.tsx @@ -81,7 +81,6 @@ describe("FileFieldRenderer", () => { file: f0FormField(z.string().optional(), { label: "Document", fieldType: "file", - useUpload: createMockUploadHook(), }), }) @@ -105,7 +104,6 @@ describe("FileFieldRenderer", () => { file: f0FormField(z.string().optional(), { label: "Document", fieldType: "file", - useUpload: createMockUploadHook(), }), }) @@ -132,7 +130,6 @@ describe("FileFieldRenderer", () => { label: "Document", fieldType: "file", status: { type: "warning", message: "Potential issue" }, - useUpload: createMockUploadHook(), }), }) @@ -160,7 +157,6 @@ describe("FileFieldRenderer", () => { label: "Photo", fieldType: "file", description: "Upload a photo (max 5 MB)", - useUpload: createMockUploadHook(), }), }) @@ -182,7 +178,6 @@ describe("FileFieldRenderer", () => { label: "Attachments", fieldType: "file", multiple: true, - useUpload: createMockUploadHook(), }), }) @@ -207,7 +202,6 @@ describe("FileFieldRenderer", () => { file: f0FormField(z.string().min(1), { label: "Document", fieldType: "file", - useUpload: createMockUploadHook(), }), }) @@ -253,7 +247,6 @@ describe("FileFieldRenderer", () => { label: "Document", fieldType: "file", accept: ["application/pdf", "image/jpeg", "image/png"], - useUpload: createMockUploadHook(), }), }) @@ -277,7 +270,6 @@ describe("FileFieldRenderer", () => { label: "Photo Only", fieldType: "file", accept: ["image/jpeg", "image/png"], - useUpload: createMockUploadHook(), }), }) @@ -318,7 +310,6 @@ describe("FileFieldRenderer", () => { label: "Small File", fieldType: "file", maxSizeMB: 0.001, // ~1 KB - useUpload: createMockUploadHook(), }), }) @@ -380,7 +371,6 @@ describe("FileFieldRenderer", () => { file: f0FormField(z.string().optional(), { label: "Removable", fieldType: "file", - useUpload: createMockUploadHook(), }), }) @@ -423,7 +413,6 @@ describe("FileFieldRenderer", () => { label: "Disabled File", fieldType: "file", disabled: true, - useUpload: createMockUploadHook(), }), }) @@ -447,7 +436,6 @@ describe("FileFieldRenderer", () => { file: f0FormField(z.string().min(1), { label: "Contract", fieldType: "file", - useUpload: createMockUploadHook(), }), }) @@ -483,7 +471,6 @@ describe("FileFieldRenderer", () => { file: f0FormField(z.string().optional(), { label: "Contract", fieldType: "file", - useUpload: createMockUploadHook(), }), }) @@ -527,7 +514,6 @@ describe("FileFieldRenderer", () => { label: "Attachments", fieldType: "file", multiple: true, - useUpload: createMockUploadHook(), }), }) @@ -570,7 +556,6 @@ describe("FileFieldRenderer", () => { label: "Attachments", fieldType: "file", multiple: true, - useUpload: createMockUploadHook(), }), }) @@ -629,7 +614,6 @@ describe("FileFieldRenderer", () => { file: f0FormField(z.string().min(1), { label: "Document", fieldType: "file", - useUpload: createMockUploadHook(), }), }) @@ -672,7 +656,6 @@ describe("FileFieldRenderer", () => { label: "Multi Files", fieldType: "file", multiple: true, - useUpload: createMockUploadHook(), }), }) diff --git a/packages/react/src/components/F0Form/fields/file/types.ts b/packages/react/src/components/F0Form/fields/file/types.ts index 197b84e02b..12106a6919 100644 --- a/packages/react/src/components/F0Form/fields/file/types.ts +++ b/packages/react/src/components/F0Form/fields/file/types.ts @@ -166,8 +166,6 @@ export interface F0FileConfig { multiple?: boolean /** Helper text shown in the dropzone area */ description?: string - /** Consumer-provided hook that returns upload capabilities */ - useUpload: UseFileUpload } /** @@ -183,8 +181,6 @@ export type F0FileField = F0BaseField & { multiple?: boolean /** Dropzone description text */ description?: string - /** Consumer-provided upload hook */ - useUpload: UseFileUpload /** Conditional rendering */ renderIf?: FileFieldRenderIf } diff --git a/packages/react/src/components/F0Form/types.ts b/packages/react/src/components/F0Form/types.ts index c3b19cf3ec..443e445107 100644 --- a/packages/react/src/components/F0Form/types.ts +++ b/packages/react/src/components/F0Form/types.ts @@ -3,6 +3,7 @@ import type { z, ZodRawShape, ZodEffects, ZodType } from "zod" import type { IconType } from "@/components/F0Icon" import type { CustomFieldRenderPropsBase } from "./fields/custom/types" +import type { UseFileUpload } from "./fields/file/types" import type { F0Field, F0BaseFieldRenderIfFunction, @@ -383,6 +384,11 @@ export interface F0FormPropsWithSingleSchema { * `defaultValues` against `InitialFile.value`. */ initialFiles?: InitialFile[] + /** + * Upload hook shared by all file fields in the form. + * Called once per file to obtain an independent upload instance. + */ + useUpload?: UseFileUpload /** * Callback that renders custom fields identified by `customFieldName`. * When a field has `customFieldName`, this function is called instead of the inline `render`. @@ -485,6 +491,10 @@ export interface F0FormPropsWithPerSectionSchema { * `defaultValues` against `InitialFile.value`. */ initialFiles?: InitialFile[] + /** + * Upload hook shared by all file fields in the form. + */ + useUpload?: UseFileUpload /** * Callback that renders custom fields identified by `customFieldName`. * When a field has `customFieldName`, this function is called instead of the inline `render`. @@ -511,6 +521,8 @@ export interface F0FormPropsWithSingleSchemaDefinition< styling?: F0FormStylingConfig formRef?: React.MutableRefObject initialFiles?: InitialFile[] + /** Upload hook shared by all file fields in the form. */ + useUpload?: UseFileUpload /** * Callback that renders custom fields identified by `customFieldName`. * When a field has `customFieldName`, this function is called instead of the inline `render`. @@ -531,6 +543,8 @@ export interface F0FormPropsWithPerSectionDefinition< styling?: F0FormStylingConfig formRef?: React.MutableRefObject initialFiles?: InitialFile[] + /** Upload hook shared by all file fields in the form. */ + useUpload?: UseFileUpload /** * Callback that renders custom fields identified by `customFieldName`. * When a field has `customFieldName`, this function is called instead of the inline `render`. diff --git a/packages/react/src/components/F0Form/useSchemaDefinition.ts b/packages/react/src/components/F0Form/useSchemaDefinition.ts index edfc9be47b..33a66223b9 100644 --- a/packages/react/src/components/F0Form/useSchemaDefinition.ts +++ b/packages/react/src/components/F0Form/useSchemaDefinition.ts @@ -253,7 +253,6 @@ function configToF0Field( maxSizeMB: "maxSizeMB" in config ? config.maxSizeMB : undefined, multiple: "multiple" in config ? config.multiple : undefined, description: "description" in config ? config.description : undefined, - useUpload: "useUpload" in config ? config.useUpload : undefined, renderIf: config.renderIf, } as F0Field