Skip to content

Commit

Permalink
xstream-452/506 adapter le design d'un attribut liaison mono-valuée (#…
Browse files Browse the repository at this point in the history
…445)

* feat(@leav/ui): design system Select to mono value link field
  • Loading branch information
fatb38 authored Apr 17, 2024
1 parent 6b3e727 commit de2184b
Show file tree
Hide file tree
Showing 21 changed files with 1,088 additions and 518 deletions.
4 changes: 2 additions & 2 deletions apps/data-studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
"@leav/ui": "workspace:libs/ui",
"@leav/utils": "workspace:libs/utils",
"@reduxjs/toolkit": "1.9.2",
"antd": "5.14.0",
"antd": "5.15.3",
"apollo-cache-inmemory": "1.6.6",
"apollo-upload-client": "14.1.3",
"aristid-ds": "4.0.0-3ef6e97",
"aristid-ds": "4.0.0-8683c72",
"dayjs": "1.11.10",
"graphql": "15.0.0",
"graphql-tag": "2.12.6",
Expand Down
4 changes: 2 additions & 2 deletions apps/login/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"dependencies": {
"@ant-design/icons": "5.2.6",
"@leav/ui": "workspace:libs/ui",
"antd": "5.14.0",
"aristid-ds": "4.0.0-3ef6e97",
"antd": "5.15.3",
"aristid-ds": "4.0.0-8683c72",
"i18next": "22.5.0",
"i18next-browser-languagedetector": "7.0.2",
"i18next-http-backend": "2.1.1",
Expand Down
4 changes: 2 additions & 2 deletions apps/portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"@apollo/client": "3.8.1",
"@leav/ui": "workspace:libs/ui",
"@leav/utils": "workspace:libs/utils",
"antd": "5.14.0",
"aristid-ds": "4.0.0-3ef6e97",
"antd": "5.15.3",
"aristid-ds": "4.0.0-8683c72",
"cross-fetch": "3.1.5",
"graphql-ws": "5.12.0",
"i18next": "22.5.0",
Expand Down
4 changes: 2 additions & 2 deletions libs/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"peerDependencies": {
"@ant-design/icons": ">=5.2",
"@apollo/client": ">=3.8.1",
"antd": "5.9.1",
"antd": "5.15.3",
"i18next": "22.5",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand All @@ -62,7 +62,7 @@
"@ckeditor/ckeditor5-build-inline": "39.0.1",
"@ckeditor/ckeditor5-react": "6.1.0",
"@leav/utils": "0.0.1",
"aristid-ds": "4.0.0-3ef6e97",
"aristid-ds": "4.0.0-8683c72",
"dayjs": "1.11.10",
"dompurify": "3.0.5",
"html-react-parser": "4.2.2",
Expand Down
24 changes: 23 additions & 1 deletion libs/ui/src/_tests/testUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,29 @@
// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt
import {InMemoryCache, InMemoryCacheConfig} from '@apollo/client';
import {MockedProvider, MockedResponse} from '@apollo/client/testing';
import {render, RenderOptions, RenderResult} from '@testing-library/react';
import {Queries, render, renderHook, RenderHookOptions, RenderOptions, RenderResult} from '@testing-library/react';
import {KitApp} from 'aristid-ds';
import {PropsWithChildren, ReactElement} from 'react';
import {MemoryRouter, MemoryRouterProps} from 'react-router-dom';
import {gqlPossibleTypes} from '_ui/gqlPossibleTypes';
import MockedUserContextProvider from '_ui/testing/MockedUserContextProvider';
import MockedLangContextProvider from '../testing/MockedLangContextProvider';
import {queries} from '@testing-library/dom';

export interface ICustomRenderOptions extends RenderOptions {
mocks?: readonly MockedResponse[];
[key: string]: any;
}

export interface ICustomRenderHookOptions<
Props,
Q extends Queries = typeof queries,
Container extends Element | DocumentFragment = HTMLElement,
BaseElement extends Element | DocumentFragment = Container
> extends RenderHookOptions<Props, Q, Container, BaseElement> {
mocks?: readonly MockedResponse[];
}

interface IProvidersProps {
mocks?: readonly MockedResponse[];
cacheSettings?: InMemoryCacheConfig;
Expand Down Expand Up @@ -44,7 +54,19 @@ const renderWithProviders = (ui: ReactElement, options?: ICustomRenderOptions):
return render(ui, {wrapper: props => <Providers {...props} {...options} />, ...options});
};

const renderHookWithProviders = <
Result,
Props,
Q extends Queries = typeof queries,
Container extends Element | DocumentFragment = HTMLElement,
BaseElement extends Element | DocumentFragment = Container
>(
hook: (initialProps: Props) => Result,
options?: ICustomRenderHookOptions<Props, Q, Container, BaseElement>
) => renderHook(hook, {wrapper: props => <Providers {...props} {...options} />, ...options});

// Re-export everything from testing-library to improve DX. You can everything you need from this file when you use this
// custom render
export * from '@testing-library/react';
export {renderWithProviders as render};
export {renderHookWithProviders as renderHook};
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@ import {mockRecord} from '_ui/__mocks__/common/record';
import {render, screen} from '../../../_tests/testUtils';
import EditRecordContent from './EditRecordContent';
import {Form} from 'antd';
import {ComponentProps, FunctionComponent} from 'react';

jest.mock('./uiElements/StandardField', () => {
return function StandardField() {
return <div>StandardField</div>;
};
});
jest.mock('./uiElements/StandardField', () => () => <div>StandardField</div>);

const EditRecordContentWithForm = props => {
const EditRecordContentWithForm: FunctionComponent<
Omit<ComponentProps<typeof EditRecordContent>, 'antdForm'>
> = props => {
const [form] = Form.useForm();

return <EditRecordContent form={form} {...props} />;
return <EditRecordContent antdForm={form} {...props} />;
};

describe('EditRecordContent', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Copyright LEAV Solutions 2017
// This file is released under LGPL V3
// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt
import {FormUIElementTypes, FORM_ROOT_CONTAINER_ID, simpleStringHash, IDateRangeValue} from '@leav/utils';
import {useEffect, useMemo} from 'react';
import {FORM_ROOT_CONTAINER_ID, FormUIElementTypes, simpleStringHash} from '@leav/utils';
import {FunctionComponent, useEffect, useMemo} from 'react';
import {ErrorDisplay} from '_ui/components';
import useGetRecordForm, {RecordFormElementsValueStandardValue} from '_ui/hooks/useGetRecordForm';
import useGetRecordForm from '_ui/hooks/useGetRecordForm';
import {useGetRecordUpdatesSubscription} from '_ui/hooks/useGetRecordUpdatesSubscription';
import useRecordsConsultationHistory from '_ui/hooks/useRecordsConsultationHistory';
import {useSharedTranslation} from '_ui/hooks/useSharedTranslation';
import {IRecordIdentityWhoAmI} from '_ui/types/records';
import {AttributeFormat, FormElementTypes} from '_ui/_gqlTypes';
import {FormElementTypes} from '_ui/_gqlTypes';
import {EditRecordReducerActionsTypes} from '../editRecordReducer/editRecordReducer';
import {useEditRecordReducer} from '../editRecordReducer/useEditRecordReducer';
import EditRecordSkeleton from './EditRecordSkeleton';
Expand All @@ -18,9 +18,8 @@ import {RecordEditionContext} from './hooks/useRecordEditionContext';
import {formComponents} from './uiElements';
import {DeleteMultipleValuesFunc, DeleteValueFunc, FormElement, SubmitValueFunc} from './_types';
import {Form, FormInstance} from 'antd';
import {Store} from 'antd/lib/form/interface';
import dayjs from 'dayjs';
import {EDIT_OR_CREATE_RECORD_FORM_ID} from './formConstants';
import {getAntdFormInitialValues} from '_ui/components/RecordEdition/EditRecordContent/antdUtils';

interface IEditRecordContentProps {
antdForm: FormInstance;
Expand All @@ -33,7 +32,7 @@ interface IEditRecordContentProps {
readonly: boolean;
}

function EditRecordContent({
const EditRecordContent: FunctionComponent<IEditRecordContentProps> = ({
antdForm,
record,
library,
Expand All @@ -42,15 +41,13 @@ function EditRecordContent({
onValueDelete,
onDeleteMultipleValues,
readonly
}: IEditRecordContentProps): JSX.Element {
}) => {
const formId = record ? 'edition' : 'creation';
const {t} = useSharedTranslation();
const {state, dispatch} = useEditRecordReducer();

useRecordsConsultationHistory(record?.library?.id ?? null, record?.id ?? null);

const forminstance = Form.useFormInstance();

const {data: recordUpdateData} = useGetRecordUpdatesSubscription(
{records: [record?.id], ignoreOwnEvents: true},
!record?.id
Expand Down Expand Up @@ -133,43 +130,8 @@ function EditRecordContent({
uiElement: formComponents[FormUIElementTypes.FIELDS_CONTAINER]
};

const hasDateRangeValues = (dateRange: unknown): dateRange is IDateRangeValue =>
(dateRange as IDateRangeValue).from !== undefined && (dateRange as IDateRangeValue).to !== undefined;

const antdFormInitialValues = recordForm.elements.reduce<Store>((acc, {attribute, values}) => {
if (!attribute) {
return acc;
}

const fieldValue = values[0] as RecordFormElementsValueStandardValue;

if (attribute.format === AttributeFormat.text) {
acc[attribute.id] = fieldValue?.raw_value ?? '';
}

if (attribute.format === AttributeFormat.date_range) {
if (!fieldValue?.raw_value) {
return acc;
}

if (hasDateRangeValues(fieldValue.raw_value)) {
acc[attribute.id] = [
dayjs.unix(Number(fieldValue.raw_value.from)),
dayjs.unix(Number(fieldValue.raw_value.to))
];
} else if (typeof fieldValue.raw_value === 'string') {
const convertedFieldValue = JSON.parse(fieldValue.raw_value);
acc[attribute.id] = [
dayjs.unix(Number(convertedFieldValue.from)),
dayjs.unix(Number(convertedFieldValue.to))
];
}
}
const antdFormInitialValues = getAntdFormInitialValues(recordForm);

return acc;
}, {});

// Use a hash of record form as a key to force a full re-render when the form changes
return (
<Form
id={EDIT_OR_CREATE_RECORD_FORM_ID}
Expand All @@ -179,6 +141,7 @@ function EditRecordContent({
>
<RecordEditionContext.Provider value={{elements: elementsByContainer, readOnly: readonly, record}}>
<rootElement.uiElement
// Use a hash of record form as a key to force a full re-render when the form changes
key={recordFormHash}
element={rootElement}
onValueSubmit={_handleValueSubmit}
Expand All @@ -188,6 +151,6 @@ function EditRecordContent({
</RecordEditionContext.Provider>
</Form>
);
}
};

export default EditRecordContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {getAntdFormInitialValues} from '_ui/components/RecordEdition/EditRecordContent/antdUtils';
import {AttributeFormat, AttributeType} from '_ui/_gqlTypes';

jest.mock('dayjs', () => ({
unix: jest.fn(t => t)
}));

describe('getAntdFormInitialValues', () => {
test('Should return empty object on empty elements', async () => {
const recordForm = {elements: []};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({});
});

test('Should skip if attribute is undefined', async () => {
const elementWithoutAttribute = {values: []};
const recordForm = {elements: [elementWithoutAttribute]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({});
});

describe.each([{type: AttributeType.simple_link}, {type: AttributeType.advanced_link, multiple_values: false}])(
'links',
attributeProperties => {
test('Should initialize antd form with given value for links (advanced and simple)', async () => {
const elementFormId = 'elementFormId';
const linkAttributeId = 'linkAttributeId';
const linkElement = {
attribute: {...attributeProperties, id: linkAttributeId},
values: [{linkValue: {id: elementFormId}}]
};
const recordForm = {elements: [linkElement]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({
[linkAttributeId]: 'elementFormId'
});
});

test('Should initialize antd form with undefined for links (advanced and simple) when linkValue is not set', async () => {
const linkAttributeId = 'linkAttributeId';
const linkElement = {
attribute: {...attributeProperties, id: linkAttributeId},
values: [{}]
};
const recordForm = {elements: [linkElement]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({
[linkAttributeId]: undefined
});
});
}
);

describe('AttributeFormat.text', () => {
test('Should initialize antd form with given value for text attribute', async () => {
const rawValue = 'rawValue';
const textAttributeId = 'textAttributeId';
const textElement = {
attribute: {format: AttributeFormat.text, id: textAttributeId},
values: [{raw_value: rawValue}]
};
const recordForm = {elements: [textElement]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({
[textAttributeId]: rawValue
});
});

test('Should initialize antd form with given empty string for text when raw_value is not set', async () => {
const textAttributeId = 'textAttributeId';
const textElement = {
attribute: {format: AttributeFormat.text, id: textAttributeId},
values: [{}]
};
const recordForm = {elements: [textElement]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({
[textAttributeId]: ''
});
});
});

describe('AttributeFormat.date_range', () => {
test('Should skip when raw_value is not set', async () => {
const dateRangeElementWithoutRawValue = {
attribute: {format: AttributeFormat.date_range},
values: [{}]
};
const recordForm = {elements: [dateRangeElementWithoutRawValue]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({});
});

test('Should initialize antd form with dayjs formatted value for date_range attribute', async () => {
const from = '1000';
const to = '2000';
const dateRangeAttributeId = 'dateRangeAttributeId';
const strcturedDateRangeElement = {
attribute: {format: AttributeFormat.date_range, id: dateRangeAttributeId},
values: [{raw_value: {from, to}}]
};
const recordForm = {elements: [strcturedDateRangeElement]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({
[dateRangeAttributeId]: [Number(from), Number(to)]
});
});

test('Should initialize antd form with dayjs formatted value for stringified date_range attribute', async () => {
const from = '1000';
const to = '2000';
const dateRangeAttributeId = 'dateRangeAttributeId';
const strcturedDateRangeElement = {
attribute: {format: AttributeFormat.date_range, id: dateRangeAttributeId},
values: [{raw_value: JSON.stringify({from, to})}]
};
const recordForm = {elements: [strcturedDateRangeElement]};

const antdFormInitialValues = getAntdFormInitialValues(recordForm as any);

expect(antdFormInitialValues).toEqual({
[dateRangeAttributeId]: [Number(from), Number(to)]
});
});
});
});
Loading

0 comments on commit de2184b

Please sign in to comment.