Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: reworked ui elements for v2 & added tests #2930

Merged
merged 14 commits into from
Jan 6, 2025
Merged
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @ts-nocheck

import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines';
import { findDocumentDefinitionById } from '@/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName';
import { Document, UIElement, UIPage } from '@/domains/collection-flow';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @ts-nocheck

import { ARRAY_VALUE_INDEX_PLACEHOLDER } from '@/common/consts/consts';
import { DocumentFieldParams } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField';
import { UIElement, UIPage } from '@/domains/collection-flow';
Expand All @@ -12,7 +14,7 @@ export const getElementByValueDestination = (

const findByElementDefinitionByDestination = (
targetDestination: string,
elements: UIElement<AnyObject>[],
elements: Array<UIElement<AnyObject>>,
): UIElement<AnyObject> | null => {
for (const element of elements) {
if (element.valueDestination === targetDestination) return element;
Expand All @@ -22,6 +24,7 @@ export const getElementByValueDestination = (
targetDestination,
element.elements,
);

if (foundElement) return foundElement;
}
}
Expand All @@ -36,26 +39,25 @@ export const getElementByValueDestination = (
);

const element = findByElementDefinitionByDestination(originArrayDestinationPath, page.elements);

return element;
}

return findByElementDefinitionByDestination(destination, page.elements);
};

export const getDocumentElementByDocumentError = (
id: string,
page: UIPage,
): UIElement<AnyObject> | null => {
export const getDocumentElementByDocumentError = (id: string, page: any): any => {
const findElement = (
id: string,
elements: UIElement<AnyObject>[],
elements: Array<UIElement<AnyObject>>,
): UIElement<DocumentFieldParams> | null => {
for (const element of elements) {
//@ts-ignore
if (element.options?.documentData?.id === id.replace('document-error-', '')) return element;

if (element.elements) {
const foundInElements = findElement(id, element.elements);

if (foundInElements) return foundInElements;
}
}
Expand Down
6 changes: 3 additions & 3 deletions apps/kyb-app/src/domains/collection-flow/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ITheme } from '@/common/types/settings';
import { Action, Rule, UIElement } from '@/domains/collection-flow/types/ui-schema.types';
import { AnyObject } from '@ballerine/ui';
import { Action, Rule } from '@/domains/collection-flow/types/ui-schema.types';
import { AnyObject, IFormElement } from '@ballerine/ui';
import { RJSFSchema, UiSchema } from '@rjsf/utils';
import { CollectionFlowConfig } from './flow-context.types';

Expand Down Expand Up @@ -128,7 +128,7 @@ export interface UIPage {
name: string;
number: number;
stateName: string;
elements: Array<UIElement<AnyObject>>;
elements: Array<IFormElement<any, any>>;
actions: Action[];
pageValidation?: Rule[];
}
Expand Down
36 changes: 6 additions & 30 deletions apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import DOMPurify from 'dompurify';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -12,13 +11,7 @@ import {
PageError,
usePageErrors,
} from '@/components/organisms/DynamicUI/Page/hooks/usePageErrors';
import { UIRenderer } from '@/components/organisms/UIRenderer';
import { Cell } from '@/components/organisms/UIRenderer/elements/Cell';
import { Divider } from '@/components/organisms/UIRenderer/elements/Divider';
import { JSONForm } from '@/components/organisms/UIRenderer/elements/JSONForm/JSONForm';
import { StepperUI } from '@/components/organisms/UIRenderer/elements/StepperUI';
import { SubmitButton } from '@/components/organisms/UIRenderer/elements/SubmitButton';
import { Title } from '@/components/organisms/UIRenderer/elements/Title';
import { useCustomer } from '@/components/providers/CustomerProvider';
import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types';
import { prepareInitialUIState } from '@/helpers/prepareInitialUIState';
Expand All @@ -36,30 +29,10 @@ import {
setCollectionFlowStatus,
setStepCompletionState,
} from '@ballerine/common';
import { AnyObject } from '@ballerine/ui';
import { CollectionFlowUI } from './components/organisms/CollectionFlowUI';
import { FailedScreen } from './components/pages/FailedScreen';
import { useAdditionalWorkflowContext } from './hooks/useAdditionalWorkflowContext';

const elems = {
h1: Title,
h3: (props: AnyObject) => <h3 className="pt-4 text-xl font-bold">{props?.options?.text}</h3>,
h4: (props: AnyObject) => <h4 className="pb-3 text-base font-bold">{props?.options?.text}</h4>,
description: (props: AnyObject) => (
<p
className="font-inter pb-2 text-sm text-slate-500"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(props.options.descriptionRaw) as string,
}}
></p>
),
'json-form': JSONForm,
container: Cell,
mainContainer: Cell,
'submit-button': SubmitButton,
stepper: StepperUI,
divider: Divider,
};

const isCompleted = (state: string) => state === 'completed' || state === 'finish';
const isFailed = (state: string) => state === 'failed';

Expand Down Expand Up @@ -145,7 +118,7 @@ export const CollectionFlow = withSessionProtected(() => {
config={collectionFlowData?.config}
additionalContext={additionalContext}
>
{({ state, stateApi }) => {
{({ state, stateApi, payload }) => {
return (
<DynamicUI.TransitionListener
pages={elements ?? []}
Expand Down Expand Up @@ -299,7 +272,10 @@ export const CollectionFlow = withSessionProtected(() => {
<ProgressBar />
</div>
<div>
<UIRenderer elements={elems} schema={currentPage.elements} />
<CollectionFlowUI
elements={currentPage.elements}
context={payload}
/>
</div>
</div>
</AppShell.FormContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import './validator';

import { DynamicFormV2, IFormElement } from '@ballerine/ui';
import { FunctionComponent } from 'react';
import { formElementsExtends } from './ui-elemenets.extends';

interface ICollectionFlowUIProps {
elements: Array<IFormElement<any, any>>;
context: object;
}

const validationParams = {
validateOnBlur: true,
abortEarly: true,
};

export const CollectionFlowUI: FunctionComponent<ICollectionFlowUIProps> = ({
elements,
context,
}) => {
return (
<DynamicFormV2
fieldExtends={formElementsExtends}
elements={elements}
values={context}
validationParams={validationParams}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useRefValue } from '@/hooks/useRefValue';
import {
AnyObject,
FileField,
IFormEventElement,
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 DocumentField: TDynamicFormField<IDocumentFieldParams> = ({ 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<any, any>) => {
const documents: AnyObject[] = 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,
};

documents[documents.length - 1] = mergedDocument;

setValue(element.id, element.valueDestination, documents);
}
},
[valuesRef, documentTemplate, element, setValue],
);

useEventsConsumer(useListener(element as IFormEventElement<any, any>, mergeDocumentWithTemplate));

return <FileField element={formattedElement} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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;
};
Loading
Loading