Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/react/src/components/F0Form/F0Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ function F0FormPerSection<T extends F0PerSectionSchema>(
initialFiles,
renderCustomField,
isLoading: isFormLoading,
useUpload,
} = props

const showSectionsSidepanel = styling?.showSectionsSidepanel ?? false
Expand Down Expand Up @@ -223,6 +224,7 @@ function F0FormPerSection<T extends F0PerSectionSchema>(
initialFiles={initialFiles}
renderCustomField={renderCustomField}
isLoading={isFormLoading}
useUpload={useUpload}
/>
</div>
)
Expand Down Expand Up @@ -354,6 +356,8 @@ function F0FormFromDefinition(
renderCustomField,
} = props

const useUpload = "useUpload" in props ? props.useUpload : undefined

if (formDefinition.isLoading) {
if (formDefinition._brand === "single") {
return (
Expand All @@ -366,6 +370,7 @@ function F0FormFromDefinition(
formRef={formRef}
initialFiles={initialFiles}
renderCustomField={renderCustomField}
useUpload={useUpload}
isLoading
/>
)
Expand All @@ -380,6 +385,7 @@ function F0FormFromDefinition(
formRef={formRef}
initialFiles={initialFiles}
renderCustomField={renderCustomField}
useUpload={useUpload}
isLoading
/>
)
Expand All @@ -396,6 +402,7 @@ function F0FormFromDefinition(
formRef={formRef}
initialFiles={initialFiles}
renderCustomField={renderCustomField}
useUpload={useUpload}
/>
)
}
Expand All @@ -410,6 +417,7 @@ function F0FormFromDefinition(
formRef={formRef}
initialFiles={initialFiles}
renderCustomField={renderCustomField}
useUpload={useUpload}
/>
)
}
Expand All @@ -421,6 +429,7 @@ function F0FormFromSingleDefinition<TSchema extends F0FormSchema>({
formRef,
initialFiles,
renderCustomField,
useUpload,
isLoading,
}: F0FormPropsWithSingleSchemaDefinition<TSchema> & { isLoading?: boolean }) {
const def = formDefinition as F0FormDefinitionSingleSchema<TSchema>
Expand All @@ -447,6 +456,7 @@ function F0FormFromSingleDefinition<TSchema extends F0FormSchema>({
formRef={formRef}
initialFiles={initialFiles}
renderCustomField={renderCustomField}
useUpload={useUpload}
isLoading={isLoading}
defaultValuesParamsSchema={def.defaultValuesParamsSchema}
defaultValuesFn={def.defaultValuesFn}
Expand All @@ -461,6 +471,7 @@ function F0FormFromPerSectionDefinition<T extends F0PerSectionSchema>({
formRef,
initialFiles,
renderCustomField,
useUpload,
isLoading,
}: F0FormPropsWithPerSectionDefinition<T> & { isLoading?: boolean }) {
const def = formDefinition as F0FormDefinitionPerSection<T>
Expand Down Expand Up @@ -506,6 +517,7 @@ function F0FormFromPerSectionDefinition<T extends F0PerSectionSchema>({
formRef={formRef}
initialFiles={initialFiles}
renderCustomField={renderCustomField}
useUpload={useUpload}
isLoading={isLoading}
/>
)
Expand Down Expand Up @@ -533,6 +545,8 @@ function F0FormSingleSchema<TSchema extends F0FormSchema>(
defaultValuesFn,
} = props

const { useUpload } = props

// Resolve styling configuration
const showSectionsSidepanel = styling?.showSectionsSidepanel ?? false

Expand Down Expand Up @@ -873,8 +887,15 @@ function F0FormSingleSchema<TSchema extends F0FormSchema>(
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -1062,7 +1061,6 @@ export const FileFields: Story = {
multiple: true,
accept: ["application/pdf", "image"],
maxSizeMB: 50,
useUpload: useMockUpload,
}
),
notes: f0FormField(z.string().optional(), {
Expand Down Expand Up @@ -1090,7 +1088,7 @@ export const FileFields: Story = {
submitConfig: { label: "Save Document" },
})

return <F0Form formDefinition={formDefinition} />
return <F0Form formDefinition={formDefinition} useUpload={useMockUpload} />
},
}

Expand All @@ -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"),
Expand All @@ -1116,7 +1113,6 @@ export const FileFieldsWithInitialFiles: Story = {
multiple: true,
accept: ["application/pdf", "image"],
maxSizeMB: 50,
useUpload: useMockUpload,
}
),
})
Expand All @@ -1139,6 +1135,7 @@ export const FileFieldsWithInitialFiles: Story = {
return (
<F0Form
formDefinition={formDefinition}
useUpload={useMockUpload}
initialFiles={[
{
value: "signed_contract_2024.pdf",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ interface F0FormSectionProps<TSchema extends F0FormSchema> {
initialFiles?: import("../fields/file/types").InitialFile[]
formRef?: React.MutableRefObject<F0FormRef | null>
renderCustomField?: RenderCustomFieldFunction
/** Upload hook shared by all file fields */
useUpload?: import("../fields/file/types").UseFileUpload
/** Whether async defaultValues are still being resolved */
isLoading?: boolean
}
Expand All @@ -137,6 +139,7 @@ export function F0FormSection<TSchema extends F0FormSchema>({
initialFiles,
formRef,
renderCustomField,
useUpload,
isLoading: isFormLoading,
}: F0FormSectionProps<TSchema>) {
const i18n = useI18n()
Expand Down Expand Up @@ -290,8 +293,9 @@ export function F0FormSection<TSchema extends F0FormSchema>({
initialFiles,
renderCustomField,
isLoading: isFormLoading,
useUpload,
}),
[formName, initialFiles, renderCustomField, isFormLoading]
[formName, initialFiles, renderCustomField, isFormLoading, useUpload]
)

const title = sectionConfig?.title ?? sectionId
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/components/F0Form/context.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<F0FormContextValue | null>(null)
Expand Down
5 changes: 0 additions & 5 deletions packages/react/src/components/F0Form/f0Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -441,7 +442,7 @@ export function FileFieldRenderer({
<FileUploadItem
key={entry.key}
entry={entry}
useUpload={entry.file ? field.useUpload : undefined}
useUpload={entry.file ? context?.useUpload : undefined}
onUploadComplete={(value) =>
Comment on lines 442 to 446
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File uploads are now sourced from context?.useUpload, but when useUpload isn't provided the UI still lets users pick/drop files and then uploads never start (because FileUploadItem no-ops without an upload hook). Please add an explicit guard (e.g. hide/disable the dropzone when no useUpload is configured, or surface a clear error/throw when a new file is added) so misconfiguration doesn't fail silently.

Copilot uses AI. Check for mistakes.
handleUploadComplete(entry.key, value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ describe("FileFieldRenderer", () => {
file: f0FormField(z.string().optional(), {
label: "Document",
fieldType: "file",
useUpload: createMockUploadHook(),
}),
})

Expand All @@ -105,7 +104,6 @@ describe("FileFieldRenderer", () => {
file: f0FormField(z.string().optional(), {
label: "Document",
fieldType: "file",
useUpload: createMockUploadHook(),
}),
})

Expand All @@ -132,7 +130,6 @@ describe("FileFieldRenderer", () => {
label: "Document",
fieldType: "file",
status: { type: "warning", message: "Potential issue" },
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -160,7 +157,6 @@ describe("FileFieldRenderer", () => {
label: "Photo",
fieldType: "file",
description: "Upload a photo (max 5 MB)",
useUpload: createMockUploadHook(),
}),
})

Expand All @@ -182,7 +178,6 @@ describe("FileFieldRenderer", () => {
label: "Attachments",
fieldType: "file",
multiple: true,
useUpload: createMockUploadHook(),
}),
})

Expand All @@ -207,7 +202,6 @@ describe("FileFieldRenderer", () => {
file: f0FormField(z.string().min(1), {
label: "Document",
fieldType: "file",
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -253,7 +247,6 @@ describe("FileFieldRenderer", () => {
label: "Document",
fieldType: "file",
accept: ["application/pdf", "image/jpeg", "image/png"],
useUpload: createMockUploadHook(),
}),
})

Expand All @@ -277,7 +270,6 @@ describe("FileFieldRenderer", () => {
label: "Photo Only",
fieldType: "file",
accept: ["image/jpeg", "image/png"],
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -318,7 +310,6 @@ describe("FileFieldRenderer", () => {
label: "Small File",
fieldType: "file",
maxSizeMB: 0.001, // ~1 KB
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -380,7 +371,6 @@ describe("FileFieldRenderer", () => {
file: f0FormField(z.string().optional(), {
label: "Removable",
fieldType: "file",
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -423,7 +413,6 @@ describe("FileFieldRenderer", () => {
label: "Disabled File",
fieldType: "file",
disabled: true,
useUpload: createMockUploadHook(),
}),
})

Expand All @@ -447,7 +436,6 @@ describe("FileFieldRenderer", () => {
file: f0FormField(z.string().min(1), {
label: "Contract",
fieldType: "file",
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -483,7 +471,6 @@ describe("FileFieldRenderer", () => {
file: f0FormField(z.string().optional(), {
label: "Contract",
fieldType: "file",
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -527,7 +514,6 @@ describe("FileFieldRenderer", () => {
label: "Attachments",
fieldType: "file",
multiple: true,
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -570,7 +556,6 @@ describe("FileFieldRenderer", () => {
label: "Attachments",
fieldType: "file",
multiple: true,
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -629,7 +614,6 @@ describe("FileFieldRenderer", () => {
file: f0FormField(z.string().min(1), {
label: "Document",
fieldType: "file",
useUpload: createMockUploadHook(),
}),
})

Expand Down Expand Up @@ -672,7 +656,6 @@ describe("FileFieldRenderer", () => {
label: "Multi Files",
fieldType: "file",
multiple: true,
useUpload: createMockUploadHook(),
}),
})

Expand Down
4 changes: 0 additions & 4 deletions packages/react/src/components/F0Form/fields/file/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -183,8 +181,6 @@ export type F0FileField = F0BaseField & {
multiple?: boolean
/** Dropzone description text */
description?: string
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useUpload was removed from F0FileField, which makes standalone file fields (e.g. F0FormField where there is no F0FormContext.Provider) unable to upload at all. With the current FileFieldRenderer implementation relying on context, consumers have no way to supply an upload hook for standalone rendering. Consider either keeping useUpload on the runtime F0FileField for non-form usage, or adding a useUpload prop to F0FormField (and/or a way to inject it into context) so file uploads still work outside <F0Form>.

Suggested change
description?: string
description?: string
/**
* Consumer-provided upload hook.
*
* This is primarily used for standalone file fields rendered outside of
* `<F0Form>` / `F0FormContext.Provider`, where the upload hook cannot be
* supplied via context.
*/
useUpload?: UseFileUpload

Copilot uses AI. Check for mistakes.
/** Consumer-provided upload hook */
useUpload: UseFileUpload
/** Conditional rendering */
renderIf?: FileFieldRenderIf
}
Expand Down
Loading
Loading