Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File uploads in v.5 #156

Open
hauptrolle opened this issue Feb 12, 2025 · 2 comments
Open

File uploads in v.5 #156

hauptrolle opened this issue Feb 12, 2025 · 2 comments

Comments

@hauptrolle
Copy link

Hey 👋 ,

is there any chance to get this fix also in v5? Seems like file uploads are broken right now.

#150

@hauptrolle
Copy link
Author

If anyone need to patch this in the old version just use this:

diff --git a/node_modules/remix-hook-form/dist/index.cjs b/node_modules/remix-hook-form/dist/index.cjs
index 58d2f94..20dcbc9 100644
--- a/node_modules/remix-hook-form/dist/index.cjs
+++ b/node_modules/remix-hook-form/dist/index.cjs
@@ -43,18 +43,23 @@ __export(src_exports, {
 module.exports = __toCommonJS(src_exports);
 
 // src/utilities/index.ts
-var tryParseJSON = (jsonString) => {
+const tryParseJSON = (value) => {
+  console.log("in tryParseJSON", value)
+  if (value instanceof File || value instanceof Blob) {
+    return value;
+  }
   try {
-    const json = JSON.parse(jsonString);
+    const json = JSON.parse(value);
+
     return json;
   } catch (e) {
-    return jsonString;
+    return value;
   }
 };
 var generateFormData = (formData, preserveStringified = false) => {
   const outputObject = {};
   for (const [key, value] of formData.entries()) {
-    const data = preserveStringified ? value : tryParseJSON(value.toString());
+    const data = preserveStringified ? value : tryParseJSON(value);
     const keyParts = key.split(".");
     let currentObject = outputObject;
     for (let i = 0; i < keyParts.length - 1; i++) {
@@ -120,7 +125,7 @@ var createFormData = (data, stringifyAll = true) => {
       }
       continue;
     }
-    if (value instanceof Array && value.length > 0 && (value[0] instanceof File || value[0] instanceof Blob)) {
+    if (value instanceof Array && value.length > 0 && value.every((item) => item instanceof File || item instanceof Blob)) {
       for (let i = 0; i < value.length; i++) {
         formData.append(key, value[i]);
       }
diff --git a/node_modules/remix-hook-form/dist/index.js b/node_modules/remix-hook-form/dist/index.js
index a4a246f..86811cf 100644
--- a/node_modules/remix-hook-form/dist/index.js
+++ b/node_modules/remix-hook-form/dist/index.js
@@ -1,16 +1,20 @@
 // src/utilities/index.ts
-var tryParseJSON = (jsonString) => {
+const tryParseJSON = (value) => {
+  if (value instanceof File || value instanceof Blob) {
+    return value;
+  }
   try {
-    const json = JSON.parse(jsonString);
+    const json = JSON.parse(value);
+
     return json;
   } catch (e) {
-    return jsonString;
+    return value;
   }
 };
 var generateFormData = (formData, preserveStringified = false) => {
   const outputObject = {};
   for (const [key, value] of formData.entries()) {
-    const data = preserveStringified ? value : tryParseJSON(value.toString());
+    const data = preserveStringified ? value : tryParseJSON(value);
     const keyParts = key.split(".");
     let currentObject = outputObject;
     for (let i = 0; i < keyParts.length - 1; i++) {
@@ -76,7 +80,7 @@ var createFormData = (data, stringifyAll = true) => {
       }
       continue;
     }
-    if (value instanceof Array && value.length > 0 && (value[0] instanceof File || value[0] instanceof Blob)) {
+    if (value instanceof Array && value.length > 0 && value.every((item) => item instanceof File || item instanceof Blob)) {
       for (let i = 0; i < value.length; i++) {
         formData.append(key, value[i]);
       }

@0zd0
Copy link

0zd0 commented Mar 1, 2025

Who else needs support under the past version of Remix I created a legacy package with the fixes https://github.com/0zd0/remix-hook-form-v5-legacy, I need it because of https://github.com/Shopify/shopify-app-template-remix. It is loaded in npmjs. Thanks for the fix you found, didn't have to waste time debugging it

Took a long time to figure out the file upload, my solution in the end:

src/pages/catalog/ui/ImportModal/ImportModal.tsx

const ImportModal = ({ open, onClose }: ImportModalProps) => {
    const fetcher = useFetcher<FormResponse>()

    const methods = useRemixForm<importSchema>({
        resolver: importResolver,
        fetcher,
        defaultValues: {
            _action: FORM_ACTIONS.IMPORT,
            file: undefined,
        },
        submitConfig: {
            method: 'POST',
            encType: 'multipart/form-data', // required
        },
    })

src/pages/catalog/api/action.server.ts

import {
    unstable_composeUploadHandlers,
    unstable_createFileUploadHandler,
    unstable_createMemoryUploadHandler,
    unstable_parseMultipartFormData,
    type ActionFunction,
} from '@remix-run/node'
import { getValidatedFormData } from 'remix-hook-form-v5-legacy'

export const action: ActionFunction = async ({ request }) => {
    const formDataRaw = await request.clone().formData()
    const formData = await unstable_parseMultipartFormData(
        request,
        unstable_composeUploadHandlers(
            unstable_createFileUploadHandler({
                directory: 'public/import',
                file: ({ filename }) => filename,
            }),
            unstable_createMemoryUploadHandler(),
        ),
    )

    const rawAction = formDataRaw.get('_action')?.toString() || ''
    const actionPath = JSON.parse(rawAction) as keyof typeof ACTION_HANDLERS
    if (!actionPath || !ACTION_HANDLERS[actionPath]) {
        return errorMessageResponse('Invalid form action')
    }

    const { resolver, handle } = ACTION_HANDLERS[actionPath]
    const result = await getValidatedFormData(formData, resolver)
}

src/shared/lib/zod/file.ts

import { z } from 'zod'

export const csvType = 'text/csv'

export const csvFileSchema = z
    .instanceof(File, { message: 'File required' })
    .refine((file) => [csvType].includes(file.type), { message: 'Invalid document file type' })

src/shared/lib/zod/file.server.ts

import { z } from 'zod'
import { csvType } from './file'
import { NodeOnDiskFile } from '@remix-run/node'

export const serverCsvFileSchema = z
    .instanceof(NodeOnDiskFile, { message: 'File required' })
    .refine((file) => [csvType].includes(file.type), { message: 'Invalid document file type' })

src/pages/catalog/model/schema/importSchema.ts

import { FORM_ACTIONS } from '../../config/formActions'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { csvFileSchema } from '@/shared/lib/zod'
import { MAX_FILE_SIZE } from '@/pages/catalog/config/constants'

export const baseImportSchema = z.object({
    _action: z.literal(FORM_ACTIONS.IMPORT),
})

export const schema = baseImportSchema.extend({
    file: csvFileSchema.refine((file) => file.size <= MAX_FILE_SIZE, {
        message: 'File size must be less than 10 MB',
    }),
})

export const importResolver = zodResolver(schema)
export type importSchema = z.infer<typeof schema>

src/pages/catalog/model/schema/importSchema.server.ts

import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { serverCsvFileSchema } from '@/shared/lib/server'
import { baseImportSchema } from './importSchema'
import { MAX_FILE_SIZE } from '@/pages/catalog/config/constants'

export const schema = baseImportSchema.extend({
    file: serverCsvFileSchema.refine((file) => file.size <= MAX_FILE_SIZE, {
        message: 'File size must be less than 10 MB',
    }),
})

export const serverImportResolver = zodResolver(schema)
export type serverImportSchema = z.infer<typeof schema>

action uses a server-side duplicated schema (serverImportResolver) because of NodeOnDiskFile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants