diff --git a/apps/kyb-app/src/domains/collection-flow/types/index.ts b/apps/kyb-app/src/domains/collection-flow/types/index.ts index d60735da60..e9d94e7b03 100644 --- a/apps/kyb-app/src/domains/collection-flow/types/index.ts +++ b/apps/kyb-app/src/domains/collection-flow/types/index.ts @@ -164,6 +164,9 @@ export interface UISchema { }; uiOptions?: UIOptions; version: number; + metadata: { + businessId: string; + } } export * from './ui-schema.types'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/versions/v2/CollectionFlowV2.tsx b/apps/kyb-app/src/pages/CollectionFlow/versions/v2/CollectionFlowV2.tsx index 19be0004ef..2bd767a89e 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/versions/v2/CollectionFlowV2.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/versions/v2/CollectionFlowV2.tsx @@ -80,23 +80,29 @@ export const CollectionFlowV2 = withSessionProtected(() => { const [isLogoLoaded, setLogoLoaded] = useState(customer?.logoImageUri ? false : true); useEffect(() => { - if (!customer?.logoImageUri) return; + if (!customer?.logoImageUri) { + return; + } // Resseting loaded state in case of logo change setLogoLoaded(false); }, [customer?.logoImageUri]); - if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.approved) + if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.approved) { return ; + } - if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.rejected) + if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.rejected) { return ; + } - if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.completed) + if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.completed) { return ; + } - if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.failed) + if (getCollectionFlowState(initialContext)?.status === CollectionFlowStatusesEnum.failed) { return ; + } return definition && collectionFlowData ? ( @@ -119,11 +125,17 @@ export const CollectionFlowV2 = withSessionProtected(() => { > {() => { // Temp state, has to be resolved to success or failure by plugins - if (state === 'done') return ; + if (state === 'done') { + return ; + } - if (isCompleted(state)) return ; + if (isCompleted(state)) { + return ; + } - if (isFailed(state)) return ; + if (isFailed(state)) { + return ; + } return ( { } context={payload} isRevision={isRevision} + metadata={schema?.metadata} /> diff --git a/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx b/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx index d86aaa1857..5d40755373 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx @@ -2,6 +2,7 @@ import './validator'; import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider/hooks/useStateManagerContext'; +import { UISchema } from '@/domains/collection-flow'; import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; import { DynamicFormV2, IDynamicFormValidationParams, IFormElement, IFormRef } from '@ballerine/ui'; import { FunctionComponent, useCallback, useMemo, useRef } from 'react'; @@ -18,6 +19,7 @@ interface ICollectionFlowUIProps { elements: Array>; context: TValues; isRevision?: boolean; + metadata: UISchema['metadata']; } const validationParams: IDynamicFormValidationParams = { @@ -32,6 +34,7 @@ export const CollectionFlowUI: FunctionComponent = ({ elements, context, isRevision, + metadata: _uiSchemaMetadata, }) => { const { stateApi } = useStateManagerContext(); const { helpers } = useDynamicUIContext(); @@ -60,8 +63,9 @@ export const CollectionFlowUI: FunctionComponent = ({ _appState: { isSyncing, }, + ..._uiSchemaMetadata, }), - [appMetadata, pluginStatuses, isSyncing], + [appMetadata, pluginStatuses, isSyncing, _uiSchemaMetadata], ); const handleChange = useCallback( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx index 38a7aba0a8..0cca8e765f 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx @@ -1,10 +1,11 @@ import { AnyObject, ctw } from '@/common'; -import { IHttpParams } from '@/common/hooks/useHttp'; +import { IHttpParams, useHttp } from '@/common/hooks/useHttp'; import { Button } from '@/components/atoms'; import { Input } from '@/components/atoms/Input'; import { createTestId } from '@/components/organisms/Renderer/utils/create-test-id'; import { Upload, XCircle } from 'lucide-react'; import { useCallback, useMemo, useRef } from 'react'; +import { useDynamicForm } from '../../context'; import { useElement, useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; @@ -48,8 +49,16 @@ export interface IDocumentFieldParams extends IFileFieldParams { export const DOCUMENT_FIELD_TYPE = 'documentfield'; export const DocumentField: TDynamicFormField = ({ element }) => { + const { metadata } = useDynamicForm(); + useMountEvent(element); useUnmountEvent(element); + + const { run: deleteDocument, isLoading: isDeletingDocument } = useHttp( + (element.params?.httpParams?.deleteDocument || {}) as IHttpParams, + metadata, + ); + const { handleChange, isUploading: disabledWhileUploading } = useDocumentUpload( element as IFormElement<'documentfield', IDocumentFieldParams>, element.params || ({} as IDocumentFieldParams), @@ -95,7 +104,7 @@ export const DocumentField: TDynamicFormField = ({ element return undefined; }, [value]); - const clearFileAndInput = useCallback(() => { + const clearFileAndInput = useCallback(async () => { if (!element.params?.template?.id) { console.warn('Template id is migging in element', element); @@ -107,20 +116,29 @@ export const DocumentField: TDynamicFormField = ({ element element.params?.template?.id as string, ); + const documentId = value; + + if (typeof documentId === 'string') { + await deleteDocument({ ids: [documentId] }); + } + onChange(updatedDocuments); removeTask(id); if (inputRef.current) { inputRef.current.value = ''; } - }, [documentsList, element, onChange, id, removeTask]); + }, [documentsList, element, onChange, id, removeTask, value, deleteDocument]); return (
= ({ element variant="ghost" size="icon" className="h-[28px] w-[28px] rounded-full" - onClick={e => { + onClick={async e => { e.stopPropagation(); - clearFileAndInput(); + await clearFileAndInput(); }} >
diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts index bf30dd130e..0c8c6ef023 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts @@ -1,17 +1,16 @@ import { AnyObject } from '@/common'; +import { IHttpParams, useHttp } from '@/common/hooks/useHttp'; import get from 'lodash/get'; import set from 'lodash/set'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useDynamicForm } from '../../../../context'; -import { uploadFile } from '../../../../helpers/upload-file'; import { useElement, useField } from '../../../../hooks/external'; import { useTaskRunner } from '../../../../providers/TaskRunner/hooks/useTaskRunner'; import { ITask } from '../../../../providers/TaskRunner/types'; import { IFormElement } from '../../../../types'; -import { formatHeaders } from '../../../../utils/format-headers'; -import { formatString } from '../../../../utils/format-string'; import { useStack } from '../../../FieldList/providers/StackProvider'; import { IDocumentFieldParams } from '../../DocumentField'; +import { buildDocumentFormData } from '../../helpers/build-document-form-data'; import { createOrUpdateFileIdOrFileInDocuments } from './helpers/create-or-update-fileid-or-file-in-documents'; export const useDocumentUpload = ( @@ -22,8 +21,11 @@ export const useDocumentUpload = ( const { stack } = useStack(); const { id } = useElement(element, stack); const { addTask, removeTask } = useTaskRunner(); - const [isUploading, setIsUploading] = useState(false); const { metadata, values } = useDynamicForm(); + const { run: uploadDocument, isLoading: isUploading } = useHttp( + (element.params?.httpParams?.createDocument || {}) as IHttpParams, + metadata, + ); const { onChange } = useField(element, stack); @@ -37,29 +39,29 @@ export const useDocumentUpload = ( async (e: React.ChangeEvent) => { removeTask(id); - const { uploadSettings } = params; + const { createDocument } = params?.httpParams || {}; - if (!uploadSettings) { + if (!createDocument) { console.warn('Upload settings are missing on element', element, 'Upload will be skipped.'); return; } - const uploadParams = { - ...uploadSettings, - method: uploadSettings?.method || 'POST', - headers: formatHeaders(uploadSettings?.headers || {}, metadata), - url: formatString(uploadSettings?.url || '', metadata), - }; + if (!metadata.entityId) { + console.warn('Entity ID is missing on element', element, 'Upload will be skipped.'); + + return; + } + + const documentUploadPayload = buildDocumentFormData( + element, + { businessId: metadata.businessId as string }, + e.target?.files?.[0] as File, + ); if (uploadOn === 'change') { try { - setIsUploading(true); - - const result = await uploadFile( - e.target?.files?.[0] as File, - uploadParams as IDocumentFieldParams['uploadSettings'], - ); + const result = await uploadDocument(documentUploadPayload); const documents = get(valuesRef.current, element.valueDestination); const updatedDocuments = createOrUpdateFileIdOrFileInDocuments( @@ -70,8 +72,6 @@ export const useDocumentUpload = ( onChange(updatedDocuments); } catch (error) { console.error('Failed to upload file.', error); - } finally { - setIsUploading(false); } } @@ -89,11 +89,7 @@ export const useDocumentUpload = ( try { const documents = get(context, element.valueDestination); - setIsUploading(true); - const result = await uploadFile( - e.target?.files?.[0] as File, - uploadParams as IDocumentFieldParams['uploadSettings'], - ); + const result = await uploadDocument(documentUploadPayload); const updatedDocuments = createOrUpdateFileIdOrFileInDocuments( documents, @@ -108,8 +104,6 @@ export const useDocumentUpload = ( console.error('Failed to upload file.', error, element); return context; - } finally { - setIsUploading(false); } }; @@ -121,7 +115,18 @@ export const useDocumentUpload = ( addTask(task); } }, - [uploadOn, params, metadata, addTask, removeTask, onChange, id, element, valuesRef], + [ + uploadOn, + params, + metadata, + addTask, + removeTask, + onChange, + uploadDocument, + id, + element, + valuesRef, + ], ); return { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/EntityFieldGroup.stories.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/EntityFieldGroup.stories.tsx index 8cf4bda97b..7698f64797 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/EntityFieldGroup.stories.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/EntityFieldGroup.stories.tsx @@ -215,7 +215,7 @@ const ubosSchema: Array> = [ resultPath: 'id', }, deleteDocument: { - url: '{apiUrl}collection-flow/files/{documentId}', + url: '{apiUrl}collection-flow/files', method: 'DELETE', headers: { Authorization: 'Bearer {token}', @@ -490,7 +490,6 @@ const initialUbosContext = { const metadata = { apiUrl: 'http://localhost:3000/api/v1/', token: 'e3a69aa3-c1ad-42f3-87ac-5105cff81a94', - workflowId: 'cm6ufmpme0004tl7sqoxwlah4', }; export const UbosFieldGroup = () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/components/EntityFields/EntityFields.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/components/EntityFields/EntityFields.tsx index 5ad99e5e5b..a63260c64a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/components/EntityFields/EntityFields.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/components/EntityFields/EntityFields.tsx @@ -1,3 +1,4 @@ +import { AnyObject } from '@/common'; import { useHttp } from '@/common/hooks/useHttp'; import { Button } from '@/components/atoms'; import { formatValueDestination, TDeepthLevelStack } from '@/components/organisms/Form/Validator'; @@ -67,7 +68,16 @@ export const EntityFields: FunctionComponent = ({ const entitiesDestination = formatValueDestination(element.valueDestination, stack); - const createEntityPayload = await buildEntityCreationPayload(element, entity, context); + let createEntityPayload: AnyObject; + + try { + createEntityPayload = await buildEntityCreationPayload(element, entity, context); + } catch (error) { + console.error(error); + toast.error('Failed to build entity creation payload.'); + setIsCreatingEntity(false); + throw error; + } let createdEntityId: string; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/hooks/useEntityFieldGroupList/useEntityFieldGroupList.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/hooks/useEntityFieldGroupList/useEntityFieldGroupList.ts index 8bfd9bc15e..fa971e4f7f 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/hooks/useEntityFieldGroupList/useEntityFieldGroupList.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/EntityFieldGroup/hooks/useEntityFieldGroupList/useEntityFieldGroupList.ts @@ -1,4 +1,5 @@ import { useHttp } from '@/common/hooks/useHttp'; +import jsonata from 'jsonata'; import { useCallback } from 'react'; import { toast } from 'sonner'; import { useDynamicForm } from '../../../../context'; @@ -15,7 +16,7 @@ export interface IUseFieldListProps { export const useEntityFieldGroupList = ({ element }: IUseFieldListProps) => { const { stack } = useStack(); const { onChange, value } = useField(element, stack); - const { metadata } = useDynamicForm(); + const { metadata, values } = useDynamicForm(); const { run: deleteEntity, isLoading } = useHttp( element.params!.httpParams?.deleteEntity, @@ -23,11 +24,27 @@ export const useEntityFieldGroupList = ({ element }: IUseFieldListProps) => { ); const addItem = useCallback(async () => { - const initialEntity = { + let initialValue = { __id: crypto.randomUUID(), }; - onChange([...(value || []), initialEntity]); - }, [value, onChange]); + const expression = element.params?.defaultValue; + + if (!expression) { + console.log('Default value is missing for', element.id); + onChange([...(value || []), initialValue]); + + return; + } + + const result = await jsonata(expression).evaluate(values); + + initialValue = { + ...initialValue, + ...result, + }; + + onChange([...(value || []), initialValue]); + }, [value, values, onChange, element.params?.defaultValue, element.id]); const removeItem = useCallback( async (id: string) => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx index fae4d9fee6..bdf270adef 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx @@ -1,9 +1,11 @@ import { ctw } from '@/common'; +import { IHttpParams, useHttp } from '@/common/hooks/useHttp'; import { Button } from '@/components/atoms'; import { Input } from '@/components/atoms/Input'; import { createTestId } from '@/components/organisms/Renderer'; import { Upload, XCircle } from 'lucide-react'; import { useCallback, useMemo, useRef } from 'react'; +import { useDynamicForm } from '../../context'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; @@ -17,24 +19,27 @@ import { useFileUpload } from './hooks/useFileUpload'; export interface IFileFieldParams extends ICommonFieldParams { uploadOn?: 'change' | 'submit'; - uploadSettings: { - url: string; - resultPath: string; - headers?: Record; - method?: 'POST' | 'PUT'; - }; acceptFileFormats?: string; + httpParams: { + createDocument: IHttpParams; + deleteDocument: IHttpParams; + }; } export const FileField: TDynamicFormField = ({ element }) => { useMountEvent(element); useUnmountEvent(element); + const { metadata } = useDynamicForm(); const { placeholder = 'Choose file', acceptFileFormats = undefined } = element.params || {}; const { handleChange, isUploading: disabledWhileUploading } = useFileUpload( element, element.params!, ); + const { run: deleteDocument, isLoading: isDeletingDocument } = useHttp( + element.params!.httpParams!.deleteDocument || {}, + metadata, + ); const { stack } = useStack(); const { value, disabled, onChange, onBlur, onFocus } = useField( @@ -59,20 +64,29 @@ export const FileField: TDynamicFormField = ({ element }) => { return undefined; }, [value]); - const clearFileAndInput = useCallback(() => { + const clearFileAndInput = useCallback(async () => { onChange(undefined); + const fileId = value; + + if (typeof fileId === 'string') { + await deleteDocument({ ids: [fileId] }); + } + if (inputRef.current) { inputRef.current.value = ''; } - }, [onChange]); + }, [onChange, value, deleteDocument]); return (
= ({ element }) => { className="h-[28px] w-[28px] rounded-full" onClick={e => { e.stopPropagation(); - clearFileAndInput(); + void clearFileAndInput(); }} >
diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts index 6523d8eb2f..99046fc8b9 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts @@ -20,7 +20,7 @@ export const useFileUpload = ( const { addTask, removeTask } = useTaskRunner(); const { metadata } = useDynamicForm(); - const { run, isLoading } = useHttp(element.params!.uploadSettings, metadata); + const { run, isLoading } = useHttp(element.params!.httpParams!.createDocument || {}, metadata); const { onChange } = useField(element); @@ -28,9 +28,9 @@ export const useFileUpload = ( async (e: React.ChangeEvent) => { removeTask(id); - const { uploadSettings } = params; + const { createDocument } = params?.httpParams || {}; - if (!uploadSettings) { + if (!createDocument) { onChange(e.target?.files?.[0] as File); console.log('Failed to upload, no upload settings provided'); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts deleted file mode 100644 index 64671e7cc1..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { useHttp } from '@/common/hooks/useHttp'; -import { renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useDynamicForm } from '../../../../context'; -import { useElement, useField } from '../../../../hooks/external'; -import { useTaskRunner } from '../../../../providers/TaskRunner/hooks/useTaskRunner'; -import { useStack } from '../../../FieldList/providers/StackProvider'; -import { useFileUpload } from './useFileUpload'; - -vi.mock('@/common/hooks/useHttp'); -vi.mock('../../../../hooks/external'); -vi.mock('../../../../providers/TaskRunner/hooks/useTaskRunner'); -vi.mock('../../../../context'); -vi.mock('../../../FieldList/providers/StackProvider'); - -describe('useFileUpload', () => { - const mockElement = { - id: 'test-id', - element: 'filefield', - params: { - uploadSettings: { - url: 'test-url', - resultPath: 'test.path', - }, - }, - valueDestination: 'test.destination', - }; - - const mockParams = { - uploadSettings: { - url: 'test-url', - resultPath: 'test.path', - }, - }; - - const mockFile = new File(['test'], 'test.txt'); - const mockEvent = { - target: { - files: [mockFile], - }, - } as unknown as React.ChangeEvent; - - const mockOnChange = vi.fn(); - const mockAddTask = vi.fn(); - const mockRemoveTask = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - - vi.mocked(useStack).mockReturnValue({ stack: [] }); - vi.mocked(useElement).mockReturnValue({ - id: 'test-id', - originId: 'test-origin-id', - hidden: false, - }); - vi.mocked(useTaskRunner).mockReturnValue({ - addTask: mockAddTask, - removeTask: mockRemoveTask, - tasks: [], - isRunning: false, - runTasks: vi.fn(), - }); - vi.mocked(useDynamicForm).mockReturnValue({ - metadata: {}, - values: {}, - touched: {}, - elementsMap: {}, - fieldHelpers: {}, - } as ReturnType); - vi.mocked(useHttp).mockReturnValue({ - run: vi.fn().mockResolvedValue('uploaded-file-url'), - isLoading: false, - error: null, - }); - vi.mocked(useField).mockReturnValue({ - value: undefined, - touched: false, - disabled: false, - onChange: mockOnChange, - onBlur: vi.fn(), - onFocus: vi.fn(), - }); - }); - - it('should handle file upload on change', async () => { - vi.mocked(useHttp).mockReturnValue({ - run: vi.fn().mockResolvedValue('uploaded-file-url'), - isLoading: false, - error: null, - }); - - const { result } = renderHook(() => useFileUpload(mockElement, mockParams)); - - await result.current.handleChange(mockEvent); - - expect(useHttp).toHaveBeenCalledWith(mockElement.params.uploadSettings, {}); - expect(mockOnChange).toHaveBeenCalledWith('uploaded-file-url'); - }); - - it('should handle file upload on submit', async () => { - const mockParamsWithSubmit = { - uploadSettings: { - url: 'test-url', - resultPath: 'test.path', - }, - uploadOn: 'submit' as const, - }; - - const { result } = renderHook(() => useFileUpload(mockElement, mockParamsWithSubmit)); - - await result.current.handleChange(mockEvent); - - expect(mockOnChange).toHaveBeenCalledWith(mockFile); - expect(mockAddTask).toHaveBeenCalled(); - }); - - it('should handle missing upload settings', async () => { - const mockElementWithoutSettings = { - ...mockElement, - params: { - uploadSettings: undefined as any, - }, - }; - const mockParamsWithoutSettings = { - uploadSettings: undefined, - } as any; - - const consoleSpy = vi.spyOn(console, 'log'); - - const { result } = renderHook(() => - useFileUpload(mockElementWithoutSettings, mockParamsWithoutSettings), - ); - - await result.current.handleChange(mockEvent); - - expect(mockOnChange).toHaveBeenCalledWith(mockFile); - expect(consoleSpy).toHaveBeenCalledWith('Failed to upload, no upload settings provided'); - }); - - it('should handle upload error on change', async () => { - vi.mocked(useHttp).mockReturnValue({ - run: vi.fn().mockRejectedValue(new Error('Upload failed')), - isLoading: false, - error: null, - }); - - const consoleSpy = vi.spyOn(console, 'error'); - - const { result } = renderHook(() => useFileUpload(mockElement, mockParams)); - - await result.current.handleChange(mockEvent); - - expect(consoleSpy).toHaveBeenCalledWith('Failed to upload file.', expect.any(Error)); - }); - - it('should handle upload error on submit', async () => { - const mockParamsWithSubmit = { - uploadSettings: { - url: 'test-url', - resultPath: 'test.path', - }, - uploadOn: 'submit' as const, - }; - - vi.mocked(useHttp).mockReturnValue({ - run: vi.fn().mockRejectedValue(new Error('Upload failed')), - isLoading: false, - error: null, - }); - - const consoleSpy = vi.spyOn(console, 'error'); - - const { result } = renderHook(() => useFileUpload(mockElement, mockParamsWithSubmit)); - - await result.current.handleChange(mockEvent); - - const addedTask = mockAddTask.mock.calls[0][0]; - const context = {}; - await addedTask.run(context); - - expect(consoleSpy).toHaveBeenCalledWith('Failed to upload file.', expect.any(Error)); - }); - - it('should remove existing task before handling change', async () => { - const { result } = renderHook(() => useFileUpload(mockElement, mockParams)); - - await result.current.handleChange(mockEvent); - - expect(mockRemoveTask).toHaveBeenCalledWith('test-id'); - }); - - it('should return correct loading state', () => { - vi.mocked(useHttp).mockReturnValue({ - run: vi.fn(), - isLoading: true, - error: null, - }); - - const { result } = renderHook(() => useFileUpload(mockElement, mockParams)); - - expect(result.current.isUploading).toBe(true); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/index.ts deleted file mode 100644 index c3aceca325..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './upload-file'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.ts deleted file mode 100644 index c03fd12e53..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.ts +++ /dev/null @@ -1,27 +0,0 @@ -import axios from 'axios'; -import get from 'lodash/get'; -import { IFileFieldParams } from '../../fields'; -import { IDocumentFieldParams } from '../../fields/DocumentField'; - -export const uploadFile = async ( - file: File, - params: IDocumentFieldParams['uploadSettings'] | IFileFieldParams['uploadSettings'], -) => { - if (!params) { - throw new Error('Upload settings are required to upload a file'); - } - - const { url, method = 'POST', headers = {} } = params; - - const formData = new FormData(); - formData.append('file', file); - - const response = await axios({ - method, - url, - headers, - data: formData, - }); - - return get(response.data, params.resultPath); -}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.unit.test.ts deleted file mode 100644 index 30a4260822..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.unit.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import axios from 'axios'; -import { IDocumentFieldParams } from '../../fields'; -import { uploadFile } from './upload-file'; - -vi.mock('axios'); -const mockedAxios = vi.mocked(axios); - -describe('uploadFile', () => { - const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' }); - const mockParams = { - url: 'http://test.com/upload', - method: 'POST' as const, - headers: { 'Content-Type': 'multipart/form-data' }, - resultPath: 'fileUrl', - }; - - it('should throw error if no params provided', async () => { - mockedAxios.mockRejectedValueOnce(new Error('Upload settings are required to upload a file')); - - await expect( - uploadFile(mockFile, {} as IDocumentFieldParams['uploadSettings']), - ).rejects.toThrow('Upload settings are required to upload a file'); - }); - - it('should upload file successfully and return result from specified path', async () => { - const mockResponse = { - data: { - fileUrl: 'http://test.com/files/test.txt', - }, - }; - - mockedAxios.mockResolvedValueOnce(mockResponse); - - const result = await uploadFile(mockFile, mockParams); - - expect(mockedAxios).toHaveBeenCalledWith({ - method: 'POST', - url: mockParams.url, - headers: mockParams.headers, - data: expect.any(FormData), - }); - expect(result).toBe(mockResponse.data.fileUrl); - }); - - it('should use POST as default method if not specified', async () => { - const paramsWithoutMethod = { - url: 'http://test.com/upload', - headers: {}, - resultPath: 'data.fileUrl', - }; - - mockedAxios.mockResolvedValueOnce({ data: { fileUrl: 'test' } }); - - await uploadFile(mockFile, paramsWithoutMethod); - - expect(mockedAxios).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'POST', - }), - ); - }); - - it('should use empty object as default headers if not specified', async () => { - const paramsWithoutHeaders = { - url: 'http://test.com/upload', - method: 'POST' as const, - resultPath: 'data.fileUrl', - }; - - mockedAxios.mockResolvedValueOnce({ data: { fileUrl: 'test' } }); - - await uploadFile(mockFile, paramsWithoutHeaders); - - expect(mockedAxios).toHaveBeenCalledWith( - expect.objectContaining({ - headers: {}, - }), - ); - }); -}); diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index cd3b6508d5..53c44c78db 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit cd3b6508d535d90fdc94000b3d805279bdbd114d +Subproject commit 53c44c78db829372607349502276233db4836dd8 diff --git a/services/workflows-service/src/collection-flow/collection-flow.service.ts b/services/workflows-service/src/collection-flow/collection-flow.service.ts index f3a1131593..74847b22fc 100644 --- a/services/workflows-service/src/collection-flow/collection-flow.service.ts +++ b/services/workflows-service/src/collection-flow/collection-flow.service.ts @@ -44,6 +44,7 @@ export class CollectionFlowService { context: WorkflowRuntimeData['context'], language: string, projectIds: TProjectIds, + tokenScope: ITokenScope, args?: Prisma.UiDefinitionFindFirstOrThrowArgs, ): Promise { const workflowDefinition = await this.workflowService.getWorkflowDefinitionById( @@ -59,6 +60,12 @@ export class CollectionFlowService { args, ); + const workflowRuntimeData = await this.workflowRuntimeDataRepository.findById( + tokenScope.workflowRuntimeDataId, + {}, + projectIds, + ); + const translationService = new TranslationService( this.uiDefinitionService.getTranslationServiceResources(uiDefinition), ); @@ -84,6 +91,10 @@ export class CollectionFlowService { ? (uiDefinition.definition as unknown as UiDefDefinition) : undefined, version: uiDefinition.version, + metadata: { + businessId: workflowRuntimeData.businessId, + entityId: tokenScope.endUserId, + }, }; } diff --git a/services/workflows-service/src/collection-flow/controllers/collection-flow.controller.ts b/services/workflows-service/src/collection-flow/controllers/collection-flow.controller.ts index f85d7299e6..63c453b0da 100644 --- a/services/workflows-service/src/collection-flow/controllers/collection-flow.controller.ts +++ b/services/workflows-service/src/collection-flow/controllers/collection-flow.controller.ts @@ -95,6 +95,7 @@ export class CollectionFlowController { workflow.context, params.language, [tokenScope.projectId], + tokenScope, workflow.uiDefinitionId ? { where: { id: workflow.uiDefinitionId } } : {}, ); } diff --git a/services/workflows-service/src/collection-flow/controllers/collection-flow.no-user.controller.ts b/services/workflows-service/src/collection-flow/controllers/collection-flow.no-user.controller.ts index 6a6b8770f0..84218c2b25 100644 --- a/services/workflows-service/src/collection-flow/controllers/collection-flow.no-user.controller.ts +++ b/services/workflows-service/src/collection-flow/controllers/collection-flow.no-user.controller.ts @@ -45,6 +45,7 @@ export class CollectionFlowNoUserController { workflow.context, params.language, [tokenScope.projectId], + tokenScope, workflow.uiDefinitionId ? { where: { id: workflow.uiDefinitionId } } : {}, ); }