diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts index 0c0ae874f5..e23c43eff7 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts @@ -43,6 +43,8 @@ export const usePageErrors = (context: CollectionFlowContext, pages: UIPage[]): return pageErrorBase; }); + console.log('contex documents', context.documents); + pagesWithErrors.forEach(pageError => { pageError.errors = [ ...((context.documents as Document[]) || []), diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/DocumentField.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/DocumentField.tsx deleted file mode 100644 index 9e8328311d..0000000000 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/DocumentField.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { useEventEmitterLogic } from '@/components/organisms/DynamicUI/StateManager/components/ActionsHandler'; -import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; -import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; -import { useUIElementToolsLogic } from '@/components/organisms/DynamicUI/hooks/useUIStateLogic/hooks/useUIElementsStateLogic/hooks/useUIElementToolsLogic'; -import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines'; -import { DocumentValueDestinationParser } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser'; -import { serializeDocumentId } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id'; -import { FileUploaderField } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField'; -import { useFileRepository } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField/hooks/useFileRepository'; -import { UploadFileFn } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField/hooks/useFileUploading/types'; -import { useUIElementErrors } from '@/components/organisms/UIRenderer/hooks/useUIElementErrors/useUIElementErrors'; -import { useUIElementState } from '@/components/organisms/UIRenderer/hooks/useUIElementState'; -import { Document, UIElement } from '@/domains/collection-flow'; -import { fetchFile, uploadFile } from '@/domains/storage/storage.api'; -import { collectionFlowFileStorage } from '@/pages/CollectionFlow/collection-flow.file-storage'; -import { findDocumentSchemaByTypeAndCategory } from '@ballerine/common'; -import { AnyObject, ErrorsList, RJSFInputProps } from '@ballerine/ui'; -import { HTTPError } from 'ky'; -import get from 'lodash/get'; -import set from 'lodash/set'; -import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; - -export interface DocumentFieldParams { - documentData: Partial; - acceptFileFormats?: string; -} - -export const DocumentField = ( - props: RJSFInputProps & { definition: UIElement } & { - inputIndex: number | null; - }, -) => { - const { state } = useDynamicUIContext(); - //@ts-ignore - const { definition, formData, inputIndex, onBlur, ...restProps } = props; - const { stateApi } = useStateManagerContext(); - const { payload } = useStateManagerContext(); - const [fieldError, setFieldError] = useState(null); - const { options } = definition; - - const { toggleElementLoading } = useUIElementToolsLogic(definition.name); - const { state: elementState } = useUIElementState(definition); - - const documentDefinition = useMemo( - () => ({ - ...definition, - valueDestination: `document-error-${serializeDocumentId( - //@ts-ignore - definition.options.documentData.id, - inputIndex, - )}`, - }), - [definition, inputIndex], - ); - - const sendEvent = useEventEmitterLogic(definition); - - const getErrorKey = useCallback( - () => - inputIndex === null - ? (documentDefinition?.options?.documentData.id as string) - : (documentDefinition.valueDestination as string), - [documentDefinition], - ); - const { validationErrors, warnings } = useUIElementErrors(documentDefinition, getErrorKey); - const warningsRef = useRef(warnings); - - const { isTouched } = elementState; - - const fileId = useMemo(() => { - if (!Array.isArray(payload.documents)) return null; - - //@ts-ignore - const parser = new DocumentValueDestinationParser(definition.valueDestination); - const documentsPath = parser.extractRootPath(); - const documentPagePath = parser.extractPagePath(); - //@ts-ignore - const documents = (get(payload, documentsPath) as Document[]) || []; - - const document = documents.find((document: Document) => { - //@ts-ignore - return document?.id === serializeDocumentId(definition.options.documentData.id, inputIndex); - }); - - //@ts-ignore - const documentPage = get(document, documentPagePath) as Document['pages'][number]; - const fileIdPath = parser.extractFileIdPath(); - - //@ts-ignore - const fileId = get(documentPage, fileIdPath) as string | null; - - return fileId; - }, [payload, definition, inputIndex]); - - //@ts-ignore - useFileRepository(collectionFlowFileStorage, fileId); - - useLayoutEffect(() => { - if (!fileId) return; - - const persistedFile = collectionFlowFileStorage.getFileById(fileId); - - if (persistedFile) return; - - void fetchFile(fileId).then(file => { - const createdFile = new File([''], file.fileNameInBucket || file.fileNameOnDisk || '', { - type: 'text/plain', - }); - - collectionFlowFileStorage.registerFile(fileId, createdFile); - }); - }, [fileId, toggleElementLoading]); - - //@ts-ignore - const fileUploader: UploadFileFn = useCallback( - async (file: File) => { - //@ts-ignore - const parser = new DocumentValueDestinationParser(definition.valueDestination); - - const context = stateApi.getContext(); - //@ts-ignore - const documents = (get(context, parser.extractRootPath()) as Document[]) || []; - const document = documents.find( - document => - //@ts-ignore - document && document.id === serializeDocumentId(options.documentData.id, inputIndex), - ); - - try { - toggleElementLoading(); - //@ts-ignore - const uploadResult = await uploadFile({ file }); - setFieldError(null); - - return { fileId: uploadResult.id }; - } catch (error) { - if (error instanceof HTTPError) { - const response = (await error.response.json()) as AnyObject; - setFieldError({ - // @ts-ignore - fieldId: document.id, - message: response.message as string, - type: 'warning', - }); - - return; - } - - console.error('Unexpected exception', error); - setFieldError({ - //@ts-ignore - fieldId: document?.id, - message: 'Failed to upload file.', - type: 'error', - }); - - throw error; - } finally { - toggleElementLoading(); - } - }, - [stateApi, options, inputIndex, definition.valueDestination, toggleElementLoading], - ); - - const handleChange = useCallback( - (fileId: string, clear?: boolean) => { - //@ts-ignore - const destinationParser = new DocumentValueDestinationParser(definition.valueDestination); - const pathToDocumentsList = destinationParser.extractRootPath(); - const pathToPage = destinationParser.extractPagePath(); - const pathToFileId = destinationParser.extractFileIdPath(); - const file = collectionFlowFileStorage.getFileById(fileId); - - const context = stateApi.getContext(); - //@ts-ignore - const documents = (get(context, pathToDocumentsList) as Document[]) || []; - - let document = documents.find( - document => - document?.id === - serializeDocumentId(definition.options?.documentData?.id || '', Number(inputIndex)), - ); - - if (!document) { - document = { - ...options.documentData, - //@ts-ignore - id: serializeDocumentId(options.documentData.id, inputIndex), - propertiesSchema: - findDocumentSchemaByTypeAndCategory( - //@ts-ignore - options.documentData.type, - options.documentData.category, - )?.propertiesSchema || undefined, - } as Document; - documents.push(document); - //@ts-ignore - set(context, pathToDocumentsList, documents); - } - - //@ts-ignore - const documentPage = - //@ts-ignore - (get(document, pathToPage) as Document['pages'][number]) || - //@ts-ignore - ({} as Document['pages'][number]); - - // Assigning file properties - if (clear) { - set(documentPage, pathToFileId!, undefined); - } else { - set(documentPage, pathToFileId!, fileId); - } - - //@ts-ignore - set(documentPage, 'fileName', file?.name); - set(documentPage, 'type', file?.type); - - //@ts-ignore - set(document, pathToPage, documentPage); - set(document, 'decision', {}); - - stateApi.setContext(context); - - sendEvent('onChange'); - }, - [stateApi, options, definition, inputIndex, sendEvent], - ); - - return ( -
- void} - testId={definition.name} - onChange={handleChange} - acceptFileFormats={definition.options.acceptFileFormats} - /> - {!!warnings.length && err.message)} />} - {isTouched && !!validationErrors.length && ( - error.message)} /> - )} - {fieldError && } -
- ); -}; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/deserialize-document-id.unit.test.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/deserialize-document-id.unit.test.ts deleted file mode 100644 index 96efe654fd..0000000000 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/deserialize-document-id.unit.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { deserializeDocumentId } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id'; - -describe('deserializeDocumentId', () => { - it('will return origin documentId template', () => { - expect(deserializeDocumentId('some-document[index:1]')).toBe('some-document[{INDEX}]'); - }); - - it('will return same string when format is not valid', () => { - expect(deserializeDocumentId('some-document[NOTVALID:1]')).toBe('some-document[NOTVALID:1]'); - }); -}); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser.ts deleted file mode 100644 index 97d5d4538f..0000000000 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser.ts +++ /dev/null @@ -1,30 +0,0 @@ -export class DocumentValueDestinationParser { - constructor(private readonly valueDestination: string) {} - - extractRootPath(): string | null { - const rootPathRegexp = /(documents|.+documents).+/g; - const match = rootPathRegexp.exec(this.valueDestination); - - if (!match) return null; - - return match[1] || null; - } - - extractPagePath(): string | null { - const pagePathRegexp = /documents\[\d+\]\.(.*?pages\[\d+\])/g; - const match = pagePathRegexp.exec(this.valueDestination); - - if (!match) return null; - - return match[1] || null; - } - - extractFileIdPath(): string | null { - const fileIdRegex = /pages(?:\[\d+\])?(?:\.(.+)|(.+))/g; - const match = fileIdRegex.exec(this.valueDestination); - - if (!match || !match[1]) return null; - - return match[1]; - } -} diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser.unit.test.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser.unit.test.ts deleted file mode 100644 index ad2be11a48..0000000000 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser.unit.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { DocumentValueDestinationParser } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/document-value-destination-parser'; - -describe('DocumentValueDestinationParser', () => { - describe('.extractRootPath', () => { - describe('when path is valid', () => { - describe.each([ - ['some.root.path.documents[0].pages[0]', 'some.root.path.documents'], - ['documents[0].pages[0]', 'documents'], - ])('will extract root path from %s', (input, expected) => { - test(`returns ${expected}`, () => { - const parser = new DocumentValueDestinationParser(input); - - expect(parser.extractRootPath()).toBe(expected); - }); - }); - }); - - describe('otherwise', () => { - it('will be null', () => { - const parser = new DocumentValueDestinationParser( - 'some.random.broken.path[3433434].test.99.1221', - ); - - expect(parser.extractRootPath()).toBe(null); - }); - }); - }); - - describe('.extractPagePath', () => { - describe('when path is valid', () => { - it('will extract path to document page', () => { - const parser = new DocumentValueDestinationParser( - 'some.path.to.documents[0].page.nested.pages[1].file.id', - ); - - expect(parser.extractPagePath()).toBe('page.nested.pages[1]'); - }); - }); - - describe('otherwise', () => { - it('will be null', () => { - const parser = new DocumentValueDestinationParser('brokenpath'); - - expect(parser.extractPagePath()).toBe(null); - }); - }); - }); - - describe('.extractFileIdPath', () => { - describe('when path is valid', () => { - it('will extract path to fileId', () => { - const parser = new DocumentValueDestinationParser( - 'context.documents[1].additionalInfo.pages[0].path.to.file.id', - ); - - expect(parser.extractFileIdPath()).toBe('path.to.file.id'); - }); - }); - - describe('otherwise', () => { - it('will be null', () => { - const parser = new DocumentValueDestinationParser('some-broken.path-pages[1121]'); - - expect(parser.extractFileIdPath()).toBe(null); - }); - }); - }); -}); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id.ts deleted file mode 100644 index a967f70944..0000000000 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const serializeDocumentId = (baseId: string, index: number): string => { - return baseId.replace('[{INDEX}]', `[index:${String(index)}]`); -}; - -export const deserializeDocumentId = (id: string): string => { - const result = id.replace(/\[index:\d+\]/g, '[{INDEX}]'); - return result; -}; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id.unit.test.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id.unit.test.ts deleted file mode 100644 index a2d17a19ab..0000000000 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id.unit.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { serializeDocumentId } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id'; - -describe('serializeDocumentId', () => { - it('will populate INDEX placeholder with index', () => { - expect(serializeDocumentId('some-id-with-[{INDEX}]-of-document', 1)).toBe( - 'some-id-with-[index:1]-of-document', - ); - }); - - it('will not modify string if index template isnt presented', () => { - expect(serializeDocumentId('some-id', 69)).toBe('some-id'); - }); -}); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/index.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/index.ts deleted file mode 100644 index 4aa75d4e29..0000000000 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DocumentField'; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts index 66fb3bdc57..45f75391cd 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts @@ -1,7 +1,10 @@ -import { deserializeDocumentId } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id'; import { UIElement } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; +const deserializeDocumentId = (id: string) => { + return id; +}; + export const findDefinitionByName = ( name: string, elements: Array>, diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/DocumentField.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/DocumentField.tsx deleted file mode 100644 index 214e34b511..0000000000 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/DocumentField.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useRefValue } from '@/hooks/useRefValue'; -import { TDocument } from '@ballerine/common'; -import { - FileField, - IFormElement, - IFormEventElement, - TBaseFields, - TDynamicFormField, - TElementEvent, - useDynamicForm, - useEventsConsumer, -} from '@ballerine/ui'; -import get from 'lodash/get'; -import { useCallback, useMemo } from 'react'; -import { buildPathToDocumentFileId, formatFileFieldElement, getDocumentIndex } from './helpers'; -import { useListener } from './hooks/useListener'; -import { IDocumentTemplate } from './types'; -// Main logic behind this component is to merge the document with the template when the document is changed -// After File input is changed, we need to merge the document with the template -// This is done by using the useListener hook to listen to the onChange event -// When the onChange event is triggered, we merge the document with the template -// If the document is being removed by the input, we remove the document from the array -// If the document is being selected by the input, we merge the document with the template - -// TODO: Tests -export const DOCUMENT_FIELD_TYPE = 'documentfield'; - -export interface IDocumentFieldParams { - documentTemplate: IDocumentTemplate; - page?: number; - pageProperty?: string; -} - -export const isDocumentFieldDefinition = ( - element: IFormElement, -): element is IFormElement => { - return element.element === DOCUMENT_FIELD_TYPE; -}; - -export const DocumentField: TDynamicFormField = ({ element }) => { - const { documentTemplate, page = 0, pageProperty = 'ballerineFileId' } = element.params || {}; - - if (!documentTemplate) { - console.error('Document template is required'); - throw new Error('Document template is required'); - } - - const { values, fieldHelpers } = useDynamicForm(); - const { setValue } = fieldHelpers; - - const documentIndex = useMemo(() => { - return getDocumentIndex(element.valueDestination, values, documentTemplate.id); - }, [element.valueDestination, values, documentTemplate.id]); - - const formattedElement = useMemo(() => { - return formatFileFieldElement(element, { - path: buildPathToDocumentFileId({ - rootPath: element.valueDestination, - documentIndex, - page, - pageProperty, - }), - }); - }, [element, documentIndex, page, pageProperty]); - - const valuesRef = useRefValue(values); - - const mergeDocumentWithTemplate = useCallback( - (_: TElementEvent, eventElement: IFormEventElement) => { - const documents: TDocument[] = get(valuesRef.current, element.valueDestination, []); - - // Document is being removed by input - if (get(valuesRef.current, eventElement.valueDestination) === undefined) { - const filteredDocuments = documents.filter(document => document.id !== documentTemplate.id); - - setValue(element.id, element.valueDestination, filteredDocuments); - } - // Document selection - else { - if (!documents.length) return; - - const latestDocument = documents[documents.length - 1]; - - if (!latestDocument) return; - - const mergedDocument = { - ...((latestDocument as unknown as object) || {}), - ...documentTemplate, - decision: {}, - } as unknown as TDocument; - - documents[documents.length - 1] = mergedDocument; - - setValue(element.id, element.valueDestination, documents); - } - }, - [valuesRef, documentTemplate, element, setValue], - ); - - useEventsConsumer(useListener(element as IFormEventElement, mergeDocumentWithTemplate)); - - return ; -}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.ts deleted file mode 100644 index 74567e7a46..0000000000 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { AnyObject, IFormElement } from '@ballerine/ui'; -import get from 'lodash/get'; - -export interface IBuildPathToDocumentFileIdParams { - rootPath: string; - documentIndex: number; - page: number; - pageProperty: string; -} - -export const buildPathToDocumentFileId = ({ - rootPath, - documentIndex, - page, - pageProperty, -}: IBuildPathToDocumentFileIdParams) => { - return `${rootPath}[${documentIndex}].pages[${page}].${pageProperty}`; -}; - -export interface IFormatFileFieldElementParams { - path: string; -} - -export const formatFileFieldElement = ( - element: IFormElement, - { path }: IFormatFileFieldElementParams, -) => { - const elementClone = structuredClone(element); - - elementClone.valueDestination = path; - - return elementClone; -}; - -export const getDocumentIndex = (path: string, context: AnyObject, documentId: string) => { - const documents = get(context, path, []); - - if (!documents.length) return 0; - - const documentIndex = documents.findIndex( - (document: { id: string }) => document.id === documentId, - ); - - return documentIndex === -1 ? documents.length : documentIndex; -}; - -export const getDocumentIndexByDocumentId = ( - path: string, - context: AnyObject, - documentId: string, -) => { - const documents = get(context, path, []); - - if (!documents.length) return 0; - - const documentIndex = documents.findIndex( - (document: { id: string }) => document.id === documentId, - ); - - return documentIndex === -1 ? 0 : documentIndex; -}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.unit.test.ts deleted file mode 100644 index b713b7bf1b..0000000000 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.unit.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { IFormElement } from '@ballerine/ui'; -import { buildPathToDocumentFileId, formatFileFieldElement, getDocumentIndex } from './helpers'; - -describe('buildPathToDocumentFileId', () => { - it('should build correct path with given params', () => { - const params = { - rootPath: 'documents', - documentIndex: 0, - page: 1, - pageProperty: 'fileId', - }; - - const result = buildPathToDocumentFileId(params); - - expect(result).toBe('documents[0].pages[1].fileId'); - }); -}); - -describe('formatFileFieldElement', () => { - it('should format element with new value destination', () => { - const element = { - id: 'test', - valueDestination: 'old.path', - } as IFormElement; - - const result = formatFileFieldElement(element, { path: 'new.path' }); - - expect(result).toEqual({ - id: 'test', - valueDestination: 'new.path', - }); - // Verify original wasn't mutated - expect(element.valueDestination).toBe('old.path'); - }); -}); - -describe('getDocumentIndex', () => { - it('should return 0 when documents array is empty', () => { - const result = getDocumentIndex('documents', { documents: [] }, 'doc1'); - - expect(result).toBe(0); - }); - - it('should return index when document id exists', () => { - const context = { - documents: [{ id: 'doc1' }, { id: 'doc2' }], - }; - - const result = getDocumentIndex('documents', context, 'doc2'); - - expect(result).toBe(1); - }); - - it('should return array length when document id not found', () => { - const context = { - documents: [{ id: 'doc1' }, { id: 'doc2' }], - }; - - const result = getDocumentIndex('documents', context, 'doc3'); - - expect(result).toBe(2); - }); -}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/index.ts deleted file mode 100644 index 4ef462d5d9..0000000000 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useListener'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/useListener.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/useListener.ts deleted file mode 100644 index 3ff4cd9e82..0000000000 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/useListener.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IEventsListener } from '@ballerine/ui'; -import { - IFormEventElement, - TElementEvent, -} from '@ballerine/ui/dist/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types'; -import { useMemo } from 'react'; - -export const useListener = ( - element: IFormEventElement, - callback: (eventName: TElementEvent, eventElement: IFormEventElement) => void, -): IEventsListener => { - const listener: IEventsListener = useMemo(() => { - return { - id: element.id, - eventName: 'onChange', - callback, - }; - }, [element.id, callback]); - - return listener; -}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/index.ts deleted file mode 100644 index 4aa75d4e29..0000000000 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DocumentField'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/types.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/types.ts deleted file mode 100644 index 00c1c2b118..0000000000 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AnyObject } from '@ballerine/ui'; - -export interface IDocumentTemplate { - id: string; - category: string; - type: string; - issuer: { - country: string; - }; - version: string; - issuingVersion: number; - properties: AnyObject; -} diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.ts index f801dea90d..2995b72bd9 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.ts +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.ts @@ -6,14 +6,11 @@ import { getFieldDefinitionsFromSchema, IFormElement, IPriorityField, + isDocumentFieldDefinition, TBaseFields, TDeepthLevelStack, } from '@ballerine/ui'; import get from 'lodash/get'; -import { - DOCUMENT_FIELD_TYPE, - isDocumentFieldDefinition, -} from '../../../components/form/DocumentField'; export const generatePriorityFields = ( elements: Array>, @@ -22,21 +19,16 @@ export const generatePriorityFields = ( const fieldElements = getFieldDefinitionsFromSchema(elements); const priorityFields: IPriorityField[] = []; - const run = ( - elements: Array>, - stack: TDeepthLevelStack = [], - ) => { + const run = (elements: Array>, stack: TDeepthLevelStack = []) => { for (const element of elements) { // Extracting revision reason fro documents isnt common so we handling it explicitly - if (element.element === DOCUMENT_FIELD_TYPE) { + if (element.element === 'documentfield') { const documentDefinition = isDocumentFieldDefinition(element); if (!documentDefinition) continue; const documents = get(context, formatValueDestination(element.valueDestination, stack)); - const document = documents.find( - (doc: TDocument) => doc.id === element.params?.documentTemplate.id, - ); + const document = documents.find((doc: TDocument) => doc.id === element.params?.template.id); if (!document) continue; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.unit.test.ts index 566af04d9f..e10fb7f4fa 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.unit.test.ts +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePriorityFields/utils/generate-priority-fields.unit.test.ts @@ -1,5 +1,4 @@ import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; -import { DOCUMENT_FIELD_TYPE } from '../../../components/form/DocumentField'; import { generatePriorityFields } from './generate-priority-fields'; describe('generatePriorityFields', () => { @@ -16,7 +15,7 @@ describe('generatePriorityFields', () => { const mockElements = [ { - element: DOCUMENT_FIELD_TYPE, + element: 'documentfield', id: 'document-1', valueDestination: 'documents', params: { @@ -67,7 +66,7 @@ describe('generatePriorityFields', () => { children: [ { id: 'document', - element: DOCUMENT_FIELD_TYPE, + element: 'documentfield', valueDestination: 'entries[$0].documents', params: { documentTemplate: { diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts index 66308d5990..24a10b3a3c 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts @@ -1,7 +1,5 @@ import { TBaseFields } from '@ballerine/ui'; import { COUNTRY_PICKER_FIELD_TYPE, CountryPickerField } from './components/form/CountryPicker'; -import { DOCUMENT_FIELD_TYPE } from './components/form/DocumentField'; -import { DocumentField } from './components/form/DocumentField/DocumentField'; import { INDUSTRIES_PICKER_FIELD_TYPE, IndustriesPickerField, @@ -31,7 +29,6 @@ const fields = { [MCC_PICKER_FIELD_TYPE]: MCCPickerField, [NATIONALITY_PICKER_FIELD_TYPE]: NationalityPickerField, [STATE_PICKER_FIELD_TYPE]: StatePickerField, - [DOCUMENT_FIELD_TYPE]: DocumentField, }; const uiElements = { diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts index 9ec5fe7bf3..d2edd6a512 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts @@ -11,7 +11,7 @@ export const documentValidator: TValidator< const { id, pageNumber = 0, pageProperty = 'ballerineFileId' } = params.value; if (!Array.isArray(value) || !value.length) { - throw new Error('Document is required'); + throw new Error(message); } const document = value.find(doc => doc.id === id); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts index b790fe332c..91736e815f 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts @@ -16,12 +16,12 @@ describe('documentValidator', () => { it('should throw error when value is not an array', () => { expect(() => documentValidator(null as unknown as TDocument[], mockParams)).toThrow( - 'Document is required', + 'Test message', ); }); it('should throw error when array is empty', () => { - expect(() => documentValidator([], mockParams)).toThrow('Document is required'); + expect(() => documentValidator([], mockParams)).toThrow('Test message'); }); it('should throw error when document with specified id is not found', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx index 17c25df649..96caf01727 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx @@ -36,8 +36,8 @@ const schema: Array> = [ }, { id: 'FileField:SubmitUpload', - element: 'filefield', - valueDestination: 'file-submit-upload', + element: 'documentfield', + valueDestination: 'documents', params: { label: 'Upload on Submit', placeholder: 'Select File', @@ -46,6 +46,28 @@ const schema: Array> = [ url: 'http://localhost:3000/upload', resultPath: 'filename', }, + template: { + id: 'document-1', + pages: [], + }, + }, + }, + { + id: 'FileField:SubmitUpload-2', + element: 'documentfield', + valueDestination: 'documents', + params: { + label: 'Upload on Submit-2', + placeholder: 'Select File', + uploadOn: 'submit', + uploadSettings: { + url: 'http://localhost:3000/upload', + resultPath: 'filename', + }, + template: { + id: 'document-2', + pages: [], + }, }, }, { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx index 88c034e861..71385c9351 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx @@ -134,6 +134,49 @@ const schema: Array> = [ description: 'Upload a file from your device', }, }, + { + id: 'DocumentField-1', + element: 'documentfield', + valueDestination: 'documents', + params: { + label: 'Document Field', + placeholder: 'Select File', + description: 'Upload a file from your device', + pageIndex: 0, + pageProperty: 'ballerineFileId', + template: { + id: 'document-1', + pages: [], + }, + uploadSettings: { + url: 'http://localhost:3000/upload', + method: 'POST', + resultPath: 'filename', + }, + }, + }, + { + id: 'DocumentField-2', + element: 'documentfield', + valueDestination: 'documents', + params: { + label: 'Document Field', + placeholder: 'Select File', + description: 'Upload a file from your device', + pageIndex: 0, + pageProperty: 'ballerineFileId', + template: { + id: 'document-2', + pages: [], + }, + uploadOn: 'submit', + uploadSettings: { + url: 'http://localhost:3000/upload', + method: 'POST', + resultPath: 'filename', + }, + }, + }, { id: 'FieldList', element: 'fieldlist', 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 new file mode 100644 index 0000000000..1fc76696d9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx @@ -0,0 +1,141 @@ +import { ctw } from '@/common'; +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 { useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; +import { FieldErrors } from '../../layouts/FieldErrors'; +import { FieldLayout } from '../../layouts/FieldLayout'; +import { FieldPriorityReason } from '../../layouts/FieldPriorityReason'; +import { IFormElement, TDynamicFormField } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; +import { IFileFieldParams } from '../FileField'; +import { useDocumentUpload } from './hooks/useDocumentUpload'; +import { getFileOrFileIdFromDocumentsList } from './hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list'; +import { removeDocumentFromListByTemplateId } from './hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id'; + +export interface IDocumentFieldParams< + TTemplate extends { id: string; pages: Array<{ [key: string]: string }> } = { + id: string; + pages: []; + }, +> extends IFileFieldParams { + template: TTemplate; + pageIndex?: number; + pageProperty?: string; +} + +export const DocumentField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { handleChange, isUploading: disabledWhileUploading } = useDocumentUpload( + element as IFormElement<'documentfield', IDocumentFieldParams>, + element.params || ({} as IDocumentFieldParams), + ); + + const { params } = element; + const { placeholder = 'Choose file', acceptFileFormats = undefined } = params || {}; + + const { stack } = useStack(); + const { + value: documentsList, + disabled, + onChange, + onBlur, + onFocus, + } = useField | undefined>(element, stack); + const value = useMemo( + () => + getFileOrFileIdFromDocumentsList( + documentsList, + element as IFormElement<'documentfield', IDocumentFieldParams>, + ), + [documentsList, element], + ); + + const inputRef = useRef(null); + + const focusInputOnContainerClick = useCallback(() => { + inputRef.current?.click(); + }, [inputRef]); + + const file = useMemo(() => { + if (value instanceof File) return value; + + if (typeof value === 'string') return new File([], value); + + return undefined; + }, [value]); + + const clearFileAndInput = useCallback(() => { + if (!element.params?.template?.id) { + console.warn('Template id is migging in element', element); + + return; + } + + const updatedDocuments = removeDocumentFromListByTemplateId( + documentsList, + element.params?.template?.id as string, + ); + + onChange(updatedDocuments); + + if (inputRef.current) { + inputRef.current.value = ''; + } + }, [documentsList, element, onChange]); + + return ( + +
+
+ + {placeholder} +
+ {file ? file.name : 'No File Choosen'} + {file && ( + + )} + +
+ + + +
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/index.ts new file mode 100644 index 0000000000..03364799cd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/index.ts @@ -0,0 +1 @@ +export * from './is-document-field-definition'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/is-document-field-definition.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/is-document-field-definition.ts new file mode 100644 index 0000000000..f28d408857 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/is-document-field-definition.ts @@ -0,0 +1,8 @@ +import { IDocumentFieldParams } from '../../..'; +import { IFormElement, TBaseFields } from '../../../../types'; + +export const isDocumentFieldDefinition = ( + element: IFormElement, +): element is IFormElement => { + return element.element === 'documentfield'; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/is-document-field-definition.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/is-document-field-definition.unit.test.ts new file mode 100644 index 0000000000..60a74f6414 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/helpers/is-document-field-definition/is-document-field-definition.unit.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { IFormElement } from '../../../../types'; +import { isDocumentFieldDefinition } from './is-document-field-definition'; + +describe('isDocumentFieldDefinition', () => { + it('should return true for document field elements', () => { + const element: IFormElement = { + id: 'test', + element: 'documentfield', + valueDestination: 'test', + params: { + label: 'Test Document', + }, + }; + + expect(isDocumentFieldDefinition(element)).toBe(true); + }); + + it('should return false for non-document field elements', () => { + const element: IFormElement = { + id: 'test', + element: 'textfield', + valueDestination: 'test', + params: { + label: 'Test Field', + }, + }; + + expect(isDocumentFieldDefinition(element)).toBe(false); + }); + + it('should return false for elements without element property', () => { + const element = { + id: 'test', + valueDestination: 'test', + params: {}, + } as IFormElement; + + expect(isDocumentFieldDefinition(element)).toBe(false); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/compose-path-to-file-id.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/compose-path-to-file-id.ts new file mode 100644 index 0000000000..f19d54fe31 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/compose-path-to-file-id.ts @@ -0,0 +1,5 @@ +export const composePathToFileId = ( + documentIndex: number, + pageProperty: string, + pageIndex: number, +) => `[${documentIndex}].pages[${pageIndex}].${pageProperty}`; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/compose-path-to-file-id.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/compose-path-to-file-id.unit.test.ts new file mode 100644 index 0000000000..96f146d03f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/compose-path-to-file-id.unit.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { composePathToFileId } from './compose-path-to-file-id'; + +describe('composePathToFileId', () => { + it('should compose path with given document index, page property and page index', () => { + const result = composePathToFileId(0, 'ballerineFileId', 1); + expect(result).toBe('[0].pages[1].ballerineFileId'); + }); + + it('should handle different document indices', () => { + const result = composePathToFileId(2, 'ballerineFileId', 0); + expect(result).toBe('[2].pages[0].ballerineFileId'); + }); + + it('should handle different page properties', () => { + const result = composePathToFileId(0, 'customFileId', 0); + expect(result).toBe('[0].pages[0].customFileId'); + }); + + it('should handle different page indices', () => { + const result = composePathToFileId(0, 'ballerineFileId', 3); + expect(result).toBe('[0].pages[3].ballerineFileId'); + }); + + it('should handle all parameters being zero', () => { + const result = composePathToFileId(0, 'ballerineFileId', 0); + expect(result).toBe('[0].pages[0].ballerineFileId'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/index.ts new file mode 100644 index 0000000000..af132c6c1d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/compose-path-to-file-id/index.ts @@ -0,0 +1 @@ +export * from './compose-path-to-file-id'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/create-or-update-fileid-or-file-in-documents.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/create-or-update-fileid-or-file-in-documents.ts new file mode 100644 index 0000000000..8e88507482 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/create-or-update-fileid-or-file-in-documents.ts @@ -0,0 +1,37 @@ +import set from 'lodash/set'; +import { IDocumentFieldParams } from '../../../..'; +import { IFormElement } from '../../../../../../..'; +import { composePathToFileId } from '../compose-path-to-file-id'; + +export const createOrUpdateFileIdOrFileInDocuments = ( + _documents: Array = [], + element: IFormElement<'documentfield', IDocumentFieldParams>, + fileIdOrFile: File | string, +) => { + const documents = structuredClone(_documents || []); + + const { pageIndex = 0, pageProperty = 'ballerineFileId', template } = element.params || {}; + + if (!template) { + console.error('Document template is missing on element', element); + + return _documents; + } + + const documentInListIndex = documents?.findIndex(document => document.id === template?.id); + + if (documentInListIndex === -1) { + documents.push(structuredClone(template)); + const pathToFileId = composePathToFileId(documents.length - 1, pageProperty, pageIndex); + set(documents, pathToFileId, fileIdOrFile); + + return documents; + } else { + const existingDocumentIndex = documents.findIndex(document => document.id === template?.id); + const pathToFileId = composePathToFileId(existingDocumentIndex, pageProperty, pageIndex); + + set(documents, pathToFileId, fileIdOrFile); + } + + return documents; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/create-or-update-fileid-or-file-in-documents.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/create-or-update-fileid-or-file-in-documents.unit.test.ts new file mode 100644 index 0000000000..c5d869e5d9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/create-or-update-fileid-or-file-in-documents.unit.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'vitest'; +import { IDocumentFieldParams } from '../../../..'; +import { IFormElement } from '../../../../../../..'; +import { createOrUpdateFileIdOrFileInDocuments } from './create-or-update-fileid-or-file-in-documents'; + +describe('createOrUpdateFileIdOrFileInDocuments', () => { + const mockTemplate = { + id: 'test-doc', + type: 'id_card', + pages: [{ ballerineFileId: null }], + } as unknown as IDocumentFieldParams['template']; + + const mockElement: IFormElement<'documentfield', IDocumentFieldParams> = { + id: 'test-field', + element: 'documentfield', + valueDestination: 'documents', + params: { + template: mockTemplate, + pageIndex: 0, + pageProperty: 'ballerineFileId', + }, + }; + + it('should create new document when documents array is empty', () => { + const result = createOrUpdateFileIdOrFileInDocuments([], mockElement, 'test-file-id'); + + expect(result).toHaveLength(1); + expect(result[0]?.id).toBe('test-doc'); + + //@ts-ignore + expect(result[0]?.pages[0].ballerineFileId).toBe('test-file-id'); + }); + + it('should create new document when document with template id does not exist', () => { + const existingDocs = [ + { + id: 'different-doc', + type: 'passport', + pages: [{ ballerineFileId: 'existing-file' }], + }, + ] as unknown as Array; + + const result = createOrUpdateFileIdOrFileInDocuments(existingDocs, mockElement, 'test-file-id'); + + expect(result).toHaveLength(2); + expect(result[1]?.id).toBe('test-doc'); + //@ts-ignore + expect(result[1]?.pages[0].ballerineFileId).toBe('test-file-id'); + }); + + it('should update existing document when document with template id exists', () => { + const existingDocs = [ + { + id: 'test-doc', + type: 'id_card', + pages: [{ ballerineFileId: 'old-file-id' }], + }, + ] as unknown as Array; + + const result = createOrUpdateFileIdOrFileInDocuments(existingDocs, mockElement, 'new-file-id'); + + expect(result).toHaveLength(1); + expect(result[0]?.id).toBe('test-doc'); + //@ts-ignore + expect(result[0]?.pages[0].ballerineFileId).toBe('new-file-id'); + }); + + it('should handle File object as fileIdOrFile parameter', () => { + const mockFile = new File([''], 'test.jpg', { type: 'image/jpeg' }); + + const result = createOrUpdateFileIdOrFileInDocuments([], mockElement, mockFile); + + expect(result).toHaveLength(1); + //@ts-ignore + expect(result[0]?.pages[0].ballerineFileId).toBe(mockFile); + }); + + it('should return original documents when template is missing', () => { + const elementWithoutTemplate = { + ...mockElement, + params: {}, + } as unknown as IFormElement<'documentfield', IDocumentFieldParams>; + + const existingDocs = [ + { + id: 'test-doc', + type: 'id_card', + pages: [{ ballerineFileId: 'existing-file' }], + }, + ] as unknown as Array; + + const result = createOrUpdateFileIdOrFileInDocuments( + existingDocs, + elementWithoutTemplate, + 'new-file-id', + ); + + expect(result).toBe(existingDocs); + }); + + it('should use default values for pageIndex and pageProperty when not provided', () => { + const elementWithoutPageParams = { + ...mockElement, + params: { + template: mockTemplate, + }, + }; + + const result = createOrUpdateFileIdOrFileInDocuments( + [], + elementWithoutPageParams, + 'test-file-id', + ); + + expect(result).toHaveLength(1); + //@ts-ignore + expect(result[0]?.pages[0].ballerineFileId).toBe('test-file-id'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/index.ts new file mode 100644 index 0000000000..380e698c58 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/create-or-update-fileid-or-file-in-documents/index.ts @@ -0,0 +1 @@ +export * from './create-or-update-fileid-or-file-in-documents'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/get-file-or-fileid-from-documents-list.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/get-file-or-fileid-from-documents-list.ts new file mode 100644 index 0000000000..d9a9c9f24d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/get-file-or-fileid-from-documents-list.ts @@ -0,0 +1,21 @@ +import { IFormElement } from '@/components/organisms/Form/DynamicForm/types'; +import get from 'lodash/get'; +import { IDocumentFieldParams } from '../../../../DocumentField'; +import { composePathToFileId } from '../compose-path-to-file-id'; + +export const getFileOrFileIdFromDocumentsList = ( + documentsList: Array = [], + element: IFormElement<'documentfield', IDocumentFieldParams>, +): File | string | undefined => { + const { pageIndex = 0, pageProperty = 'ballerineFileId', template } = element.params || {}; + + console.log('documentsList', documentsList); + const documentIndex = documentsList?.findIndex(document => document.id === template?.id); + + if (documentIndex === -1) return undefined; + + const filePath = composePathToFileId(documentIndex, pageProperty, pageIndex); + const fileOrFileId = get(documentsList, filePath, undefined); + + return fileOrFileId; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/get-file-or-fileid-from-documents-list.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/get-file-or-fileid-from-documents-list.unit.test.ts new file mode 100644 index 0000000000..dc12c111c2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/get-file-or-fileid-from-documents-list.unit.test.ts @@ -0,0 +1,98 @@ +import { IFormElement } from '@/components/organisms/Form/DynamicForm/types'; +import { describe, expect, it } from 'vitest'; +import { IDocumentFieldParams } from '../../../../DocumentField'; +import { getFileOrFileIdFromDocumentsList } from './get-file-or-fileid-from-documents-list'; + +describe('getFileOrFileIdFromDocumentsList', () => { + const mockElement: IFormElement<'documentfield', IDocumentFieldParams> = { + id: 'test-doc', + element: 'documentfield', + valueDestination: 'documents', + params: { + template: { + id: 'doc-1', + pages: [], + }, + pageIndex: 0, + pageProperty: 'ballerineFileId', + }, + }; + + it('should return undefined when documentsList is empty', () => { + const result = getFileOrFileIdFromDocumentsList([], mockElement); + expect(result).toBeUndefined(); + }); + + it('should return undefined when document with matching template id is not found', () => { + const documentsList = [ + { + id: 'different-doc', + pages: [], + }, + ] as Array; + const result = getFileOrFileIdFromDocumentsList(documentsList, mockElement); + expect(result).toBeUndefined(); + }); + + it('should return file id when matching document is found', () => { + const documentsList = [ + { + id: 'doc-1', + pages: [ + { + ballerineFileId: 'file-123', + }, + ], + }, + ] as unknown as Array; + const result = getFileOrFileIdFromDocumentsList(documentsList, mockElement); + expect(result).toBe('file-123'); + }); + + it('should use default values when params are not provided', () => { + const elementWithoutParams: IFormElement<'documentfield', IDocumentFieldParams> = { + id: 'test-doc', + element: 'documentfield', + valueDestination: 'documents', + }; + + const documentsList = [ + { + id: undefined, + pages: [ + { + ballerineFileId: 'file-123', + }, + ], + }, + ] as unknown as Array; + + const result = getFileOrFileIdFromDocumentsList(documentsList, elementWithoutParams); + expect(result).toBe('file-123'); + }); + + it('should handle custom pageProperty and pageIndex', () => { + const customElement = { + ...mockElement, + params: { + ...mockElement.params, + pageProperty: 'customFileId', + pageIndex: 1, + template: { + id: 'doc-1', + pages: [{ customFileId: 'file-1' }, { customFileId: 'file-2' }], + }, + }, + } as unknown as IFormElement<'documentfield', IDocumentFieldParams>; + + const documentsList = [ + { + id: 'doc-1', + pages: [{ customFileId: 'file-1' }, { customFileId: 'file-2' }], + }, + ] as unknown as Array; + + const result = getFileOrFileIdFromDocumentsList(documentsList, customElement); + expect(result).toBe('file-2'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/index.ts new file mode 100644 index 0000000000..1a1dffb38b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/get-file-or-fileid-from-documents-list/index.ts @@ -0,0 +1 @@ +export * from './get-file-or-fileid-from-documents-list'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/index.ts new file mode 100644 index 0000000000..5f3e26c906 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/index.ts @@ -0,0 +1 @@ +export * from './remove-document-from-list-by-template-id'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/remove-document-from-list-by-template-id.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/remove-document-from-list-by-template-id.ts new file mode 100644 index 0000000000..e3147e9b9f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/remove-document-from-list-by-template-id.ts @@ -0,0 +1,14 @@ +import { IDocumentFieldParams } from '../../../..'; + +export const removeDocumentFromListByTemplateId = ( + documents: Array = [], + templateId: string, +) => { + const isDocumentInList = documents.some(document => document.id === templateId); + + if (!isDocumentInList) { + return documents; + } + + return documents.filter(document => document.id !== templateId); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/remove-document-from-list-by-template-id.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/remove-document-from-list-by-template-id.unit.test.ts new file mode 100644 index 0000000000..590de6eb32 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/helpers/remove-document-from-list-by-template-id/remove-document-from-list-by-template-id.unit.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { IDocumentFieldParams } from '../../../..'; +import { removeDocumentFromListByTemplateId } from './remove-document-from-list-by-template-id'; + +describe('removeDocumentFromListByTemplateId', () => { + it('should remove document with matching template id from list', () => { + const documents = [ + { id: 'doc1', pages: [] }, + { id: 'doc2', pages: [] }, + ] as Array; + + const result = removeDocumentFromListByTemplateId(documents, 'doc1'); + + expect(result).toHaveLength(1); + expect(result?.[0]?.id).toBe('doc2'); + }); + + it('should return original list if template id not found', () => { + const documents = [ + { id: 'doc1', pages: [] }, + { id: 'doc2', pages: [] }, + ] as Array; + + const result = removeDocumentFromListByTemplateId(documents, 'doc3'); + + expect(result).toHaveLength(2); + expect(result).toBe(documents); + }); + + it('should handle empty documents array', () => { + const result = removeDocumentFromListByTemplateId([], 'doc1'); + + expect(result).toHaveLength(0); + }); + + it('should handle undefined documents array', () => { + const result = removeDocumentFromListByTemplateId(undefined, 'doc1'); + + expect(result).toHaveLength(0); + }); + + it('should remove only matching document when multiple documents exist', () => { + const documents = [ + { id: 'doc1', pages: [] }, + { id: 'doc2', pages: [] }, + { id: 'doc3', pages: [] }, + ] as Array; + + const result = removeDocumentFromListByTemplateId(documents, 'doc2'); + + expect(result).toHaveLength(2); + expect(result?.[0]?.id).toBe('doc1'); + expect(result?.[1]?.id).toBe('doc3'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/index.ts new file mode 100644 index 0000000000..b067342cd0 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/index.ts @@ -0,0 +1 @@ +export * from './useDocumentUpload'; 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 new file mode 100644 index 0000000000..7fef570a26 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts @@ -0,0 +1,122 @@ +import get from 'lodash/get'; +import { useCallback, useEffect, useRef, useState } 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 { createOrUpdateFileIdOrFileInDocuments } from './helpers/create-or-update-fileid-or-file-in-documents'; + +export const useDocumentUpload = ( + element: IFormElement<'documentfield', IDocumentFieldParams>, + params: IDocumentFieldParams, +) => { + const { uploadOn = 'change' } = params; + const { stack } = useStack(); + const { id } = useElement(element, stack); + const { addTask, removeTask } = useTaskRunner(); + const [isUploading, setIsUploading] = useState(false); + const { metadata, values } = useDynamicForm(); + + const { onChange } = useField(element); + + const valuesRef = useRef(values); + + useEffect(() => { + valuesRef.current = values; + }, [values]); + + const handleChange = useCallback( + async (e: React.ChangeEvent) => { + removeTask(id); + + const { uploadSettings } = params; + + if (!uploadSettings) { + 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 (uploadOn === 'change') { + try { + setIsUploading(true); + + const result = await uploadFile( + e.target?.files?.[0] as File, + uploadParams as IDocumentFieldParams['uploadSettings'], + ); + + const documents = get(valuesRef.current, element.valueDestination); + const updatedDocuments = createOrUpdateFileIdOrFileInDocuments( + documents, + element, + result, + ); + onChange(updatedDocuments); + } catch (error) { + console.error('Failed to upload file.', error); + } finally { + setIsUploading(false); + } + } + + if (uploadOn === 'submit') { + const documents = get(valuesRef.current, element.valueDestination); + const updatedDocuments = createOrUpdateFileIdOrFileInDocuments( + documents, + element, + e.target?.files?.[0] as File, + ); + onChange(updatedDocuments); + + const taskRun = async () => { + try { + setIsUploading(true); + const result = await uploadFile( + e.target?.files?.[0] as File, + uploadParams as IDocumentFieldParams['uploadSettings'], + ); + + const documents = get(valuesRef.current, element.valueDestination); + const updatedDocuments = createOrUpdateFileIdOrFileInDocuments( + documents, + element, + result, + ); + onChange(updatedDocuments); + } catch (error) { + console.error('Failed to upload file.', error); + } finally { + setIsUploading(false); + } + }; + + const task: ITask = { + id, + element, + run: taskRun, + }; + addTask(task); + } + }, + [uploadOn, params, metadata, addTask, removeTask, onChange, id, element, valuesRef], + ); + + return { + isUploading, + handleChange, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/index.ts new file mode 100644 index 0000000000..b7b4c4d9ea --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/index.ts @@ -0,0 +1,2 @@ +export * from './DocumentField'; +export * from './helpers/is-document-field-definition'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts index f18d1beb98..6dbe9e83a8 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts @@ -2,6 +2,7 @@ export * from './AutocompleteField'; export * from './CheckboxField'; export * from './CheckboxList'; export * from './DateField'; +export * from './DocumentField'; export * from './FieldList'; export * from './FileField'; export * from './MultiselectField'; 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 new file mode 100644 index 0000000000..c3aceca325 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/index.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000..c03fd12e53 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000000..0170bfdd7f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/upload-file/upload-file.unit.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it, vi } from 'vitest'; + +import axios from 'axios'; +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 () => { + await expect(uploadFile(mockFile, undefined)).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/packages/ui/src/components/organisms/Form/DynamicForm/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts index df0f350bd2..af9a905058 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts @@ -5,4 +5,5 @@ export * from './helpers/get-field-definitions-from-schema'; export * from './hooks/external'; export * from './providers/EventsProvider'; export * from './types'; +export * from './utils/format-headers'; export * from './utils/format-string'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts index 09bb216015..9177358332 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts @@ -3,6 +3,7 @@ import { AutocompleteField } from '../fields/AutocompleteField'; import { CheckboxField } from '../fields/CheckboxField'; import { CheckboxListField } from '../fields/CheckboxList'; import { DateField } from '../fields/DateField'; +import { DocumentField } from '../fields/DocumentField'; import { FieldList } from '../fields/FieldList'; import { FileField } from '../fields/FileField'; import { MultiselectField } from '../fields/MultiselectField'; @@ -18,6 +19,7 @@ export const baseFields = { checkboxfield: CheckboxField, checkboxlistfield: CheckboxListField, datefield: DateField, + documentfield: DocumentField, multiselectfield: MultiselectField, textfield: TextField, fieldlist: FieldList, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-headers.ts b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-headers.ts new file mode 100644 index 0000000000..77dcf8d9b9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-headers.ts @@ -0,0 +1,15 @@ +import { formatString } from './format-string'; + +export const formatHeaders = ( + headers: Record, + metadata: Record = {}, +) => { + const formattedHeaders: Record = {}; + + Object.entries(headers).forEach(([key, value]) => { + const formattedValue = formatString(value, metadata); + formattedHeaders[key] = formattedValue; + }); + + return formattedHeaders; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-headers.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-headers.unit.test.ts new file mode 100644 index 0000000000..f6ae912f80 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-headers.unit.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it, vi } from 'vitest'; +import { formatHeaders } from './format-headers'; +import { formatString } from './format-string'; + +vi.mock('./format-string', () => ({ + formatString: vi.fn(), +})); + +const mockedFormatString = vi.mocked(formatString); + +describe('formatHeaders', () => { + it('should format headers with metadata', () => { + const headers = { + Authorization: 'Bearer {token}', + 'Content-Type': 'application/json', + }; + + const metadata = { + token: 'abc123', + }; + + mockedFormatString.mockReturnValueOnce('Bearer abc123').mockReturnValueOnce('application/json'); + + const result = formatHeaders(headers, metadata); + + expect(result).toEqual({ + Authorization: 'Bearer abc123', + 'Content-Type': 'application/json', + }); + + expect(mockedFormatString).toHaveBeenCalledTimes(2); + expect(mockedFormatString).toHaveBeenCalledWith('Bearer {token}', metadata); + expect(mockedFormatString).toHaveBeenCalledWith('application/json', metadata); + }); + + it('should handle empty headers', () => { + const result = formatHeaders({}); + + expect(result).toEqual({}); + expect(mockedFormatString).not.toHaveBeenCalled(); + }); + + it('should use empty metadata object if not provided', () => { + const headers = { + 'X-Custom': 'test', + }; + + mockedFormatString.mockReturnValueOnce('test'); + + const result = formatHeaders(headers); + + expect(result).toEqual({ + 'X-Custom': 'test', + }); + + expect(mockedFormatString).toHaveBeenCalledWith('test', {}); + }); +});