From 1ff2a913cff2bded5b5f3568a091eded84a581a7 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 15 Aug 2025 14:11:23 -0400 Subject: [PATCH] feat(upload, form-upload): allow headers to be passed as a function --- packages/form-upload/src/Upload.js | 5 +- packages/form-upload/tests/Upload.test.tsx | 88 +++++++++++++++++++++- packages/upload/src/Upload.js | 6 +- packages/upload/tests/Upload.test.js | 71 +++++++++++++++++ 4 files changed, 165 insertions(+), 5 deletions(-) diff --git a/packages/form-upload/src/Upload.js b/packages/form-upload/src/Upload.js index 5dff7b7dd..e138000d3 100644 --- a/packages/form-upload/src/Upload.js +++ b/packages/form-upload/src/Upload.js @@ -198,7 +198,6 @@ const Upload = ({ fileTypes: allowedFileTypes, maxSize, allowedFileNameCharacters, - customHeaders, }; if (isCloud) options.endpoint = CLOUD_URL; @@ -207,6 +206,8 @@ const Upload = ({ let newTotalSize = 0; for await (const file of selectedFiles) { + options.headers = typeof customHeaders === 'function' ? customHeaders(file) : customHeaders; + const upload = new UploadCore(file, options); await upload.generateId(); @@ -385,7 +386,7 @@ Upload.propTypes = { /** Set as true to show a drag and drop file upload option instead of a button (file explorer still available on click). */ showFileDrop: PropTypes.bool, /** Set custom headers on the upload request */ - customHeaders: PropTypes.object, + customHeaders: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), }; export default Upload; diff --git a/packages/form-upload/tests/Upload.test.tsx b/packages/form-upload/tests/Upload.test.tsx index 59318ddcc..c1cf1842c 100644 --- a/packages/form-upload/tests/Upload.test.tsx +++ b/packages/form-upload/tests/Upload.test.tsx @@ -344,7 +344,7 @@ describe('Upload', () => { initialValues={initialValues} onSubmit={(values) => { - mockFn(values.upload?.[0].options.customHeaders); + mockFn(values.upload?.[0].options.headers); }} > @@ -375,6 +375,92 @@ describe('Upload', () => { }); }); + test('passes customHeaders function result to UploadCore options', async () => { + const customHeadersFunction = jest.fn((file: File) => ({ + 'X-File-Name': file.name, + 'x-metadata-content-type': `application/${file.name.split('.').pop()?.toLowerCase()}`, + })); + const mockFn = jest.fn(); + const onFileUploadMock = jest.fn(); + + render( + + initialValues={initialValues} + onSubmit={(values) => { + mockFn(values.upload?.[0].options.headers); + }} + > + + + + ); + + const file: UploadFile = Buffer.from('hello world'); + file.name = 'fileName.png'; + const fileEvent = { target: { files: [file] } }; + + const inputNode = screen.getByTestId('file-picker') as HTMLInputElement; + + act(() => { + fireEvent.change(inputNode, fileEvent); + }); + + await waitFor(() => { + expect(onFileUploadMock).toHaveBeenCalled(); + expect(customHeadersFunction).toHaveBeenCalledWith(file); + }); + + act(() => { + fireEvent.click(screen.getByText('click')); + }); + + await waitFor(() => { + expect(mockFn).toHaveBeenCalledWith({ + 'X-File-Name': 'fileName.png', + 'x-metadata-content-type': 'application/png', + }); + }); + }); + + test('handles customHeaders as undefined', async () => { + const mockFn = jest.fn(); + const onFileUploadMock = jest.fn(); + + render( + + initialValues={initialValues} + onSubmit={(values) => { + mockFn(values.upload?.[0].options.headers); + }} + > + + + + ); + + const file: UploadFile = Buffer.from('hello world'); + file.name = 'fileName.png'; + const fileEvent = { target: { files: [file] } }; + + const inputNode = screen.getByTestId('file-picker') as HTMLInputElement; + + act(() => { + fireEvent.change(inputNode, fileEvent); + }); + + await waitFor(() => { + expect(onFileUploadMock).toHaveBeenCalled(); + }); + + act(() => { + fireEvent.click(screen.getByText('click')); + }); + + await waitFor(() => { + expect(mockFn).toHaveBeenCalledWith(undefined); + }); + }); + describe('dropzone', () => { // start msw server beforeAll(() => server.listen()); diff --git a/packages/upload/src/Upload.js b/packages/upload/src/Upload.js index bfb728cfd..0685bd08d 100644 --- a/packages/upload/src/Upload.js +++ b/packages/upload/src/Upload.js @@ -54,6 +54,8 @@ class Upload extends Component { // eslint-disable-next-line unicorn/prefer-spread this.files.concat( selectedFiles.map(async (file) => { + const headers = + typeof this.props.customHeaders === 'function' ? this.props.customHeaders(file) : this.props.customHeaders; const options = { bucketId: this.props.bucketId, customerId: this.props.customerId, @@ -62,7 +64,7 @@ class Upload extends Component { maxSize: this.props.maxSize, onPreStart: this.props.onFilePreUpload || [], allowedFileNameCharacters: this.props.allowedFileNameCharacters, - headers: this.props.customHeaders, + headers, }; if (this.props.endpoint) options.endpoint = this.props.endpoint; @@ -290,7 +292,7 @@ Upload.propTypes = { /** Override the endpoint used for uploading the file(s) */ endpoint: PropTypes.string, /** Set custom headers on the upload request */ - customHeaders: PropTypes.object, + customHeaders: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), }; Upload.defaultProps = { diff --git a/packages/upload/tests/Upload.test.js b/packages/upload/tests/Upload.test.js index 3d56b6287..5d8b9b668 100644 --- a/packages/upload/tests/Upload.test.js +++ b/packages/upload/tests/Upload.test.js @@ -331,4 +331,75 @@ describe('Upload', () => { expect(mockFn).toHaveBeenCalledWith(customHeaders); }); }); + + test('passes customHeaders function result to UploadCore options', async () => { + const customHeadersFunction = jest.fn((file) => ({ + 'X-File-Name': file.name, + 'x-metadata-content-type': `application/${file.name.split('.').pop()?.toLowerCase()}`, + })); + const mockFn = jest.fn(); + + render( + { + mockFn(file.options.headers); + }, + ]} + /> + ); + + const file = Buffer.from('hello world'); + file.name = 'fileName.png'; + const fileEvent = { target: { files: [file] } }; + + const inputNode = screen.getByTestId('file-picker'); + + fireEvent.change(inputNode, fileEvent); + + expect(inputNode.files.length).toBe(1); + + await waitFor(() => { + expect(customHeadersFunction).toHaveBeenCalledWith(file); + expect(mockFn).toHaveBeenCalledWith({ + 'X-File-Name': 'fileName.png', + 'x-metadata-content-type': 'application/png', + }); + }); + }); + + test('handles customHeaders as undefined', async () => { + const mockFn = jest.fn(); + + render( + { + mockFn(file.options.headers); + }, + ]} + /> + ); + + const file = Buffer.from('hello world'); + file.name = 'fileName.png'; + const fileEvent = { target: { files: [file] } }; + + const inputNode = screen.getByTestId('file-picker'); + + fireEvent.change(inputNode, fileEvent); + + expect(inputNode.files.length).toBe(1); + + await waitFor(() => { + expect(mockFn).toHaveBeenCalledWith(undefined); + }); + }); });