From 2fe80c744da2ee195d618e3483f433999dc2baba Mon Sep 17 00:00:00 2001 From: Teddy Paul Date: Wed, 16 Oct 2024 14:33:54 +0200 Subject: [PATCH] feature(@leav/ui): Handle 'read mode' on record edit form (#584) --- .../domain/actions/formatDateAction.spec.ts | 22 +-- .../src/domain/actions/formatDateAction.ts | 20 ++- .../actions/formatDateRangeAction.spec.ts | 108 +++++++++------ .../domain/actions/formatDateRangeAction.ts | 19 ++- apps/core/src/domain/record/recordDomain.ts | 3 - .../attributeSimpleLinkRepo.spec.ts | 4 +- .../attributeTypes/attributeSimpleLinkRepo.ts | 2 +- libs/ui/src/__mocks__/common/form.tsx | 2 + .../records/getRecordPropertiesQuery.ts | 46 +++++-- libs/ui/src/_utils/typeguards.ts | 3 + .../RecordEdition/EditRecordContent/_types.ts | 10 +- .../EditRecordContent/antdUtils.test.tsx | 6 +- .../EditRecordContent/antdUtils.tsx | 20 +-- .../linkFieldReducer/linkFieldReducer.test.ts | 16 +-- .../linkFieldReducer/linkFieldReducer.ts | 18 +-- .../standardFieldReducer.test.ts | 35 ++--- .../standardFieldReducer.ts | 78 ++++++----- .../standardFieldReducerContext.ts | 15 ++ .../useStandardFieldReducer.ts | 4 + .../shared/AddValueBtn/AddValueBtn.test.tsx | 4 +- .../shared/AddValueBtn/AddValueBtn.tsx | 6 +- .../ValuesVersionBtn.test.tsx | 16 ++- .../ValuesVersionBtn/ValuesVersionBtn.tsx | 27 ++-- .../ValuesVersionIndicator.tsx | 6 +- .../MonoValueSelect/MonoValueSelect.test.tsx | 12 +- .../MultiValueSelect.test.tsx | 12 +- .../StandardField/StandardField.test.tsx | 127 ++++++++++++++--- .../StandardField/StandardField.tsx | 83 ++++++------ .../StandardFieldNumeric.test.tsx | 22 +-- .../StandardFieldRichText.test.tsx | 121 ----------------- .../StandardField/StandardFieldText.test.tsx | 93 ------------- .../DSBooleanWrapper.test.tsx | 42 ++++-- .../DSDatePickerWrapper.test.tsx | 44 +++--- .../DSDatePickerWrapper.tsx | 19 ++- ...t.tsx => DSInputEncryptedWrapper.test.tsx} | 86 +++++------- ...rapper.tsx => DSInputEncryptedWrapper.tsx} | 28 +++- .../DSInputNumberWrapper.test.tsx | 50 ++++--- .../DSInputNumberWrapper.tsx | 22 ++- .../DSInputWrapper.test.tsx | 68 ++++------ .../StandardFieldValue/DSInputWrapper.tsx | 30 +++- .../DSRangePickerWrapper.test.tsx | 48 ++++--- .../DSRangePickerWrapper.tsx | 21 ++- .../DSRichTextWrapper.test.tsx | 62 ++++----- .../StandardFieldValue/DSRichTextWrapper.tsx | 17 ++- .../StandardFieldValue/StandardFieldValue.tsx | 111 ++++----------- .../StandardFieldValueDisplayHandler.tsx | 113 ++++++++++++++++ .../StandardFieldValueRead.tsx | 128 ++++++++++++++++++ .../StandardFieldValueDisplayHandler/index.ts | 1 + .../uiElements/TreeField/TreeField.tsx | 12 +- .../useRefreshFieldValues.ts | 10 +- 50 files changed, 1089 insertions(+), 783 deletions(-) create mode 100644 libs/ui/src/_utils/typeguards.ts create mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducerContext.ts create mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/useStandardFieldReducer.ts delete mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldRichText.test.tsx delete mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldText.test.tsx rename libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/{DSInputPasswordWrapper.test.tsx => DSInputEncryptedWrapper.test.tsx} (80%) rename libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/{DSInputPasswordWrapper.tsx => DSInputEncryptedWrapper.tsx} (82%) create mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueDisplayHandler.tsx create mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/StandardFieldValueRead.tsx create mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/index.ts diff --git a/apps/core/src/domain/actions/formatDateAction.spec.ts b/apps/core/src/domain/actions/formatDateAction.spec.ts index 3584e2d14..76dcf27f1 100644 --- a/apps/core/src/domain/actions/formatDateAction.spec.ts +++ b/apps/core/src/domain/actions/formatDateAction.spec.ts @@ -1,6 +1,7 @@ // 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 {IStandardValue} from '_types/value'; import {AttributeFormats, AttributeTypes, IAttribute} from '../../_types/attribute'; import formatDateAction from './formatDateAction'; @@ -10,6 +11,7 @@ describe('formatDateAction', () => { const ctx = {attribute: attrText, userId: 'test_user'}; const testingDate = 2119477320; + const testValue: IStandardValue = {payload: testingDate, raw_payload: testingDate}; describe('Localized format', () => { test('with options', async () => { @@ -23,7 +25,7 @@ describe('formatDateAction', () => { expect( ( await action( - [{payload: testingDate}], + [testValue], {localized}, { ...ctx, @@ -35,7 +37,7 @@ describe('formatDateAction', () => { expect( ( await action( - [{payload: testingDate}], + [testValue], {localized}, { ...ctx, @@ -47,7 +49,7 @@ describe('formatDateAction', () => { expect( ( await action( - [{payload: testingDate}], + [testValue], {localized}, { ...ctx, @@ -63,7 +65,7 @@ describe('formatDateAction', () => { 'auto should print default on invalid json format: `%s`', async localized => { const result = await action( - [{payload: testingDate}], + [testValue], {localized}, { ...ctx, @@ -84,7 +86,7 @@ describe('formatDateAction', () => { expect( ( await action( - [{payload: testingDate}], + [testValue], { universal: 'D/MMMM-YY HH:mm' }, @@ -96,15 +98,15 @@ describe('formatDateAction', () => { describe('edge cases', () => { test('should fallback to empty localized param when neither params provided', async () => { - const resultWithoutParams = await action([{payload: testingDate}], {}, ctx); - const resultWithEmptyLocalizedParam = await action([{payload: testingDate}], {localized: '{}'}, ctx); + const resultWithoutParams = await action([testValue], {}, ctx); + const resultWithEmptyLocalizedParam = await action([testValue], {localized: '{}'}, ctx); expect(resultWithoutParams).toEqual(resultWithEmptyLocalizedParam); }); test('should return empty string on non numerical value in DB', async () => { - expect((await action([{payload: 'aaaa'}], {}, ctx)).values[0].payload).toBe(''); + expect((await action([{payload: 'aaaa', raw_payload: 'aaa'}], {}, ctx)).values[0].payload).toBe(''); }); test('should return null on null value in DB', async () => { - expect((await action([{payload: null}], {}, ctx)).values[0].payload).toBe(null); + expect((await action([{payload: null, raw_payload: null}], {}, ctx)).values[0].payload).toBe(null); }); }); @@ -112,7 +114,7 @@ describe('formatDateAction', () => { expect( ( await action( - [{payload: testingDate}], + [testValue], { universal: 'D/MMMM/YY', localized: `{ diff --git a/apps/core/src/domain/actions/formatDateAction.ts b/apps/core/src/domain/actions/formatDateAction.ts index 2b2ee9b77..8511c02ef 100644 --- a/apps/core/src/domain/actions/formatDateAction.ts +++ b/apps/core/src/domain/actions/formatDateAction.ts @@ -4,6 +4,8 @@ import moment from 'moment'; import {ActionsListIOTypes, IActionsListFunction, IActionsListFunctionResult} from '../../_types/actionsList'; import {Errors} from '../../_types/errors'; +import cloneDeep from 'lodash/cloneDeep'; +import {TypeGuards} from '../../utils'; const defaultValueLocalizedParam = `{ "weekday": "long", @@ -40,14 +42,23 @@ export default function (): IActionsListFunction<{localized: false; universal: f action: (values, {localized, universal}, {lang}) => { const errors: IActionsListFunctionResult['errors'] = []; - const formattedValues = values.map(elementValue => { + const formattedValues = cloneDeep(values).map(elementValue => { + if (!TypeGuards.isIStandardValue(elementValue)) { + errors.push({ + errorType: Errors.INVALID_VALUES, + attributeValue: elementValue, + message: 'Non standard value received in formatDate.' + }); + return elementValue; + } + if ('raw_value' in elementValue) { elementValue.payload = elementValue.raw_value; } if (elementValue.payload === null) { return elementValue; } - const numberVal = Number(elementValue.payload); + const numberVal = Number(elementValue.raw_payload); if (isNaN(numberVal)) { elementValue.payload = ''; @@ -61,9 +72,8 @@ export default function (): IActionsListFunction<{localized: false; universal: f let options: Intl.DateTimeFormatOptions = {}; try { - options = JSON.parse(localized ?? {}); + options = JSON.parse(localized ?? '{}'); } catch (e) { - // TODO: rise error to inform user without break app errors.push({ errorType: Errors.FORMAT_ERROR, attributeValue: {payload: localized}, @@ -76,7 +86,7 @@ export default function (): IActionsListFunction<{localized: false; universal: f return elementValue; }); - return {values: formattedValues, errors: []}; + return {values: formattedValues, errors}; } }; } diff --git a/apps/core/src/domain/actions/formatDateRangeAction.spec.ts b/apps/core/src/domain/actions/formatDateRangeAction.spec.ts index 52cd8b95c..9a5dd31cd 100644 --- a/apps/core/src/domain/actions/formatDateRangeAction.spec.ts +++ b/apps/core/src/domain/actions/formatDateRangeAction.spec.ts @@ -1,6 +1,7 @@ // 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 {IStandardValue} from '_types/value'; import {AttributeFormats, AttributeTypes, IAttribute} from '../../_types/attribute'; import formatDateRangeAction from './formatDateRangeAction'; @@ -10,6 +11,7 @@ describe('formatDateRangeAction', () => { const ctx = {attribute: attrText, userId: 'test_user'}; const testingRangeDate = {from: '2119477320', to: '2119477380'}; + const testValue: IStandardValue = {payload: testingRangeDate, raw_payload: testingRangeDate}; describe('Localized format', () => { test('with options', async () => { @@ -21,37 +23,37 @@ describe('formatDateRangeAction', () => { "minute": "2-digit" }`; - expect( - ( - await action( - [{payload: testingRangeDate}], - {localized}, - { - ...ctx, - lang: 'en-GB' - } - ) - ).values[0].payload - ).toEqual({from: '28 February 37 at 23:42', to: '28 February 37 at 23:43'}); - expect( - ( - await action( - [{payload: testingRangeDate}], - {localized}, - { - ...ctx, - lang: 'ko-KR' - } - ) - ).values[0].payload - ).toEqual({from: '37년 2월 28일 오후 11:42', to: '37년 2월 28일 오후 11:43'}); + const resEnGb = await action( + [testValue], + {localized}, + { + ...ctx, + lang: 'en-GB' + } + ); + expect(resEnGb.errors).toEqual([]); + expect(resEnGb.values[0].payload).toEqual({from: '28 February 37 at 23:42', to: '28 February 37 at 23:43'}); + + const resKoKr = await action( + [testValue], + {localized}, + { + ...ctx, + lang: 'ko-KR' + } + ); + expect(resKoKr.errors).toEqual([]); + expect(resKoKr.values[0].payload).toEqual({ + from: '37년 2월 28일 오후 11:42', + to: '37년 2월 28일 오후 11:43' + }); }); test.skip.each(['{', '{withoutDoubleQuote: true}', '', '{"params1": "long", "param2": "too many coma",}'])( 'auto should print default on invalid json format: `%s`', async localized => { const result = await action( - [{payload: testingRangeDate}], + [testValue], {localized}, { ...ctx, @@ -72,7 +74,7 @@ describe('formatDateRangeAction', () => { }); test('Universal format', async () => { - const result = await action([{payload: testingRangeDate}], {universal: 'D/MMMM-YY HH:mm'}, ctx); + const result = await action([testValue], {universal: 'D/MMMM-YY HH:mm'}, ctx); const formattedRangeDate = result.values[0].payload as { from: string; to: string; @@ -83,22 +85,41 @@ describe('formatDateRangeAction', () => { describe('edge cases', () => { test('should return null value if properties are omitted', async () => { - expect((await action([{payload: 'aaaa'}], {}, ctx)).values[0].payload).toBe(null); - expect((await action([{payload: {}}], {}, ctx)).values[0].payload).toBe(null); - expect((await action([{payload: {unknownProperty: null}}], {}, ctx)).values[0].payload).toBe(null); - expect((await action([{payload: {from: '2119477320'}}], {}, ctx)).values[0].payload).toBe(null); - expect((await action([{payload: {to: '2119477320'}}], {}, ctx)).values[0].payload).toBe(null); - expect((await action([{payload: null}], {}, ctx)).values[0].payload).toBe(null); + expect((await action([{payload: 'aaaa', raw_payload: 'aaa'}], {}, ctx)).values[0].payload).toBe(null); + expect((await action([{payload: {}, raw_payload: {}}], {}, ctx)).values[0].payload).toBe(null); + expect( + (await action([{payload: {unknownProperty: null}, raw_payload: {unknownProperty: null}}], {}, ctx)) + .values[0].payload + ).toBe(null); + expect( + (await action([{payload: {from: '2119477320'}, raw_payload: {from: '2119477320'}}], {}, ctx)).values[0] + .payload + ).toBe(null); + expect( + (await action([{payload: {to: '2119477320'}, raw_payload: {to: '2119477320'}}], {}, ctx)).values[0] + .payload + ).toBe(null); + expect((await action([{payload: null, raw_payload: null}], {}, ctx)).values[0].payload).toBe(null); }); test('should return empty string couple on non numerical value in DB', async () => { - expect((await action([{payload: {from: 'aaaa', to: '2119477320'}}], {}, ctx)).values[0].payload).toEqual([ - '', - '' - ]); - expect((await action([{payload: {from: '2119477320', to: 'aaaa'}}], {}, ctx)).values[0].payload).toEqual([ - '', - '' - ]); + expect( + ( + await action( + [{payload: {from: 'aaaa', to: '2119477320'}, raw_payload: {from: 'aaaa', to: '2119477320'}}], + {}, + ctx + ) + ).values[0].payload + ).toEqual(['', '']); + expect( + ( + await action( + [{payload: {from: '2119477320', to: 'aaaa'}, raw_payload: {from: '2119477320', to: 'aaaa'}}], + {}, + ctx + ) + ).values[0].payload + ).toEqual(['', '']); }); }); @@ -106,7 +127,12 @@ describe('formatDateRangeAction', () => { expect( ( await action( - [{payload: {from: '2119477320', to: '2119477380'}}], + [ + { + payload: {from: '2119477320', to: '2119477380'}, + raw_payload: {from: '2119477320', to: '2119477380'} + } + ], { universal: 'D/MMMM/YY', localized: `{ diff --git a/apps/core/src/domain/actions/formatDateRangeAction.ts b/apps/core/src/domain/actions/formatDateRangeAction.ts index b5c138448..5220d1537 100644 --- a/apps/core/src/domain/actions/formatDateRangeAction.ts +++ b/apps/core/src/domain/actions/formatDateRangeAction.ts @@ -5,6 +5,8 @@ import moment from 'moment'; import {IDateRangeValue} from '_types/value'; import {ActionsListIOTypes, IActionsListFunction, IActionsListFunctionResult} from '../../_types/actionsList'; import {Errors} from '../../_types/errors'; +import cloneDeep from 'lodash/cloneDeep'; +import {TypeGuards} from '../../utils/typeGuards'; const defaultValueLocalizedParam = `{ "weekday": "long", @@ -41,8 +43,16 @@ export default function (): IActionsListFunction<{localized: false; universal: f action: (values, {localized, universal}, {lang}) => { const errors: IActionsListFunctionResult['errors'] = []; - const formattedValues = values.map(elementValue => { - const dateRangeValue = elementValue.payload as IDateRangeValue; + const formattedValues = cloneDeep(values).map(elementValue => { + if (!TypeGuards.isIStandardValue(elementValue)) { + errors.push({ + errorType: Errors.INVALID_VALUES, + attributeValue: elementValue, + message: 'Non standard value received in formatDateRange.' + }); + return elementValue; + } + const dateRangeValue = elementValue.raw_payload as IDateRangeValue; if (dateRangeValue === null || !dateRangeValue.from || !dateRangeValue.to) { return {...dateRangeValue, payload: null}; @@ -66,9 +76,8 @@ export default function (): IActionsListFunction<{localized: false; universal: f let options: Intl.DateTimeFormatOptions = {}; try { - options = JSON.parse(localized ?? {}); + options = JSON.parse(localized ?? '{}'); } catch (e) { - // TODO: rise error to inform user without break app errors.push({ errorType: Errors.FORMAT_ERROR, attributeValue: {payload: localized}, @@ -84,7 +93,7 @@ export default function (): IActionsListFunction<{localized: false; universal: f return elementValue; }); - return {values: formattedValues, errors: []}; + return {values: formattedValues, errors}; } }; } diff --git a/apps/core/src/domain/record/recordDomain.ts b/apps/core/src/domain/record/recordDomain.ts index 210465b39..c49b2f5f6 100644 --- a/apps/core/src/domain/record/recordDomain.ts +++ b/apps/core/src/domain/record/recordDomain.ts @@ -1189,9 +1189,6 @@ export default function ({ ]; } - const forceArray = options?.forceArray ?? false; - - //TODO: fix "[object]" on input after edit let formattedValues = await Promise.all( values.map(async v => { const formattedValue = await valueDomain.formatValue({ diff --git a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts index a57abda1e..b9940e1a0 100644 --- a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts +++ b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts @@ -193,7 +193,7 @@ describe('AttributeSimpleLinkRepo', () => { expect(values[0]).toMatchObject({ id_value: null, - value: { + payload: { id: 987654, created_at: 1521475225, modified_at: 1521475225 @@ -258,7 +258,7 @@ describe('AttributeSimpleLinkRepo', () => { expect(values[0]).toMatchObject({ id_value: null, - value: { + payload: { id: 987654, created_at: 1521475225, modified_at: 1521475225 diff --git a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts index 07c7ee774..48810c918 100644 --- a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts +++ b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts @@ -124,7 +124,7 @@ export default function ({ .map(r => ({ id_value: null, library: attribute.linked_library, - value: dbUtils.cleanup({...r, library: attribute.linked_library}), + payload: dbUtils.cleanup({...r, library: attribute.linked_library}), created_by: null, modified_by: null })); diff --git a/libs/ui/src/__mocks__/common/form.tsx b/libs/ui/src/__mocks__/common/form.tsx index adfa230fa..8ad4f3c84 100644 --- a/libs/ui/src/__mocks__/common/form.tsx +++ b/libs/ui/src/__mocks__/common/form.tsx @@ -26,6 +26,8 @@ const formElementBase = { { value: 'My value formatted', raw_value: 'my_raw_value', + payload: 'My value formatted', + raw_payload: 'my_raw_payload', created_at: 123456789, modified_at: 123456789, created_by: mockModifier, diff --git a/libs/ui/src/_queries/records/getRecordPropertiesQuery.ts b/libs/ui/src/_queries/records/getRecordPropertiesQuery.ts index e4a3d27a1..2992d2cac 100644 --- a/libs/ui/src/_queries/records/getRecordPropertiesQuery.ts +++ b/libs/ui/src/_queries/records/getRecordPropertiesQuery.ts @@ -7,14 +7,15 @@ import { AttributeType, RecordFilterCondition, RecordFormAttributeLinkAttributeFragment, + ValueDetailsFragment, ValueVersionInput } from '_ui/_gqlTypes'; import {gqlUnchecked} from '_ui/_utils'; import {recordIdentityFragment} from '../../gqlFragments'; import {IRecordIdentityWhoAmI} from '../../types/records'; import {SystemTranslation} from '../../types/scalars'; -import {IValueVersion} from '../../types/values'; import {valuesVersionDetailsFragment} from '../values/valuesVersionFragment'; +import {IValueVersion} from '_ui/types'; export interface IRecordPropertyAttribute { id: string; @@ -47,36 +48,55 @@ export interface IRecordPropertyModifier { whoAmI: IRecordIdentityWhoAmI; } -interface IRecordPropertyBase { +interface IRecordPropertyBase { + __typename?: string; id_value?: string | null; created_at?: number | null; created_by?: IRecordPropertyModifier | null; modified_at?: number | null; modified_by?: IRecordPropertyModifier | null; metadata?: Array<{name: string; value: IRecordPropertyStandard}>; - version?: IValueVersion; + version?: T extends 'clean' ? IValueVersion : ValueDetailsFragment['version']; } -export interface IRecordPropertyStandard extends IRecordPropertyBase { +type RecordPropertyParam = 'clean' | 'raw'; +type DefaultRecordPropertyParam = 'clean'; + +export interface IRecordPropertyStandard + extends IRecordPropertyBase { payload?: string | null; raw_payload?: string | null; } -export interface IRecordPropertyLink extends IRecordPropertyBase { +export interface IRecordPropertyLink + extends IRecordPropertyBase { linkValue?: ILinkValue | null; } -export interface IRecordPropertyTree extends IRecordPropertyBase { +export interface IRecordPropertyTree + extends IRecordPropertyBase { treeValue?: ITreeValue | null; } -export type RecordProperty = IRecordPropertyStandard | IRecordPropertyLink | IRecordPropertyTree; +export type RecordProperty = + | IRecordPropertyStandard + | IRecordPropertyLink + | IRecordPropertyTree; + +export type RecordPropertyWhoAmI = Pick & { + library: Pick; +}; + +export type RecordPropertyListItem = { + _id: string; + whoAmI: RecordPropertyWhoAmI; +} & { + [attributeName: string]: Array>; +}; export interface IGetRecordProperties { [libName: string]: { - list: { - [attributeName: string]: RecordProperty[]; - }; + list: RecordPropertyListItem[]; }; } @@ -156,6 +176,12 @@ export const getRecordPropertiesQuery = (fields: IRecordPropertiesField[]) => gq ) { list { _id: id + whoAmI { + id + library { + id + } + } ${fields.length ? fields.map(field => _getFieldQueryPart(field)).join('\n') : ''} } } diff --git a/libs/ui/src/_utils/typeguards.ts b/libs/ui/src/_utils/typeguards.ts new file mode 100644 index 000000000..e7f852b3c --- /dev/null +++ b/libs/ui/src/_utils/typeguards.ts @@ -0,0 +1,3 @@ +import {WithTypename} from '@leav/utils'; + +export const hasTypename = (value: any): value is WithTypename => '__typename' in value; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts index 866cdbc2c..dbbded361 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts @@ -17,7 +17,7 @@ import {IRecordIdentity, IRecordIdentityWhoAmI} from '_ui/types/records'; import {ITreeNodeWithRecord} from '_ui/types/trees'; import {IValueVersion} from '_ui/types/values'; import {RecordFormAttributeFragment, SaveValueBatchMutation, ValueDetailsFragment, ValueInput} from '_ui/_gqlTypes'; -import {IRecordPropertyAttribute, RecordProperty} from '_ui/_queries/records/getRecordPropertiesQuery'; +import {RecordProperty} from '_ui/_queries/records/getRecordPropertiesQuery'; import {RecordFormElementFragment} from '../../../_gqlTypes'; import {IStandardFieldReducerState, IStandardFieldValue} from './reducers/standardFieldReducer/standardFieldReducer'; import {FormInstance} from 'antd/lib/form/Form'; @@ -135,7 +135,7 @@ export type InputRefPossibleTypes = InputRef | typeof DatePicker | typeof Checkb export type StandardValueTypes = AnyPrimitive | IDateRangeValue; -export enum FieldScope { +export enum VersionFieldScope { INHERITED = 'INHERITED', // inherited values CURRENT = 'CURRENT' // values of "current" version, eg. the version selected in the form } @@ -143,11 +143,11 @@ export enum FieldScope { export interface ICommonFieldsReducerState { record: IRecordIdentityWhoAmI; formElement: FormElement; - attribute: IRecordPropertyAttribute; + attribute: RecordFormAttributeFragment; isReadOnly: boolean; - activeScope: FieldScope; + activeScope: VersionFieldScope; values: { - [scope in FieldScope]: { + [scope in VersionFieldScope]: { version: IValueVersion; values: ValuesType; } | null; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx index 141a5e20f..a01ce9d1b 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx @@ -102,7 +102,7 @@ describe('getAntdFormInitialValues', () => { const textAttributeId = 'textAttributeId'; const textElement = { attribute: {format: AttributeFormat.text, id: textAttributeId}, - values: [{raw_value: rawValue}] + values: [{raw_payload: rawValue}] }; const recordForm = {elements: [textElement]}; @@ -148,7 +148,7 @@ describe('getAntdFormInitialValues', () => { const dateRangeAttributeId = 'dateRangeAttributeId'; const strcturedDateRangeElement = { attribute: {format: AttributeFormat.date_range, id: dateRangeAttributeId}, - values: [{raw_value: {from, to}}] + values: [{raw_payload: {from, to}}] }; const recordForm = {elements: [strcturedDateRangeElement]}; @@ -165,7 +165,7 @@ describe('getAntdFormInitialValues', () => { const dateRangeAttributeId = 'dateRangeAttributeId'; const strcturedDateRangeElement = { attribute: {format: AttributeFormat.date_range, id: dateRangeAttributeId}, - values: [{raw_value: JSON.stringify({from, to})}] + values: [{raw_payload: JSON.stringify({from, to})}] }; const recordForm = {elements: [strcturedDateRangeElement]}; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx index e123b3e5d..eba687e64 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx @@ -16,7 +16,7 @@ const hasDateRangeValues = (dateRange: unknown): dateRange is IDateRangeValue => (dateRange as IDateRangeValue).from !== undefined && (dateRange as IDateRangeValue).to !== undefined; const getInheritedValue = values => values.find(value => value.isInherited); -const getNotInheritedOrOverrideValue = values => values.find(value => !value.isInherited && value.raw_value !== null); +const getNotInheritedOrOverrideValue = values => values.find(value => !value.isInherited && value.raw_payload !== null); const isRecordFormElementsValueLinkValue = ( value: RecordFormElementsValue, @@ -51,7 +51,7 @@ export const getAntdFormInitialValues = (recordForm: IRecordForm) => const standardValue = value as RecordFormElementsValueStandardValue; - if (!standardValue?.raw_value) { + if (!standardValue?.raw_payload) { if (attribute.format === AttributeFormat.date_range) { return acc; } @@ -64,23 +64,23 @@ export const getAntdFormInitialValues = (recordForm: IRecordForm) => case AttributeFormat.text: case AttributeFormat.rich_text: case AttributeFormat.boolean: - acc[attribute.id] = standardValue.raw_value; + acc[attribute.id] = standardValue.raw_payload; break; case AttributeFormat.numeric: - acc[attribute.id] = Number(standardValue.raw_value); + acc[attribute.id] = Number(standardValue.raw_payload); break; case AttributeFormat.date: - acc[attribute.id] = dayjs.unix(Number(standardValue.raw_value)); + acc[attribute.id] = dayjs.unix(Number(standardValue.raw_payload)); break; case AttributeFormat.date_range: - if (hasDateRangeValues(standardValue.raw_value)) { + if (hasDateRangeValues(standardValue.raw_payload)) { acc[attribute.id] = [ - dayjs.unix(Number(standardValue.raw_value.from)), - dayjs.unix(Number(standardValue.raw_value.to)) + dayjs.unix(Number(standardValue.raw_payload.from)), + dayjs.unix(Number(standardValue.raw_payload.to)) ]; break; - } else if (typeof standardValue.raw_value === 'string') { - const convertedFieldValue = JSON.parse(standardValue.raw_value); + } else if (typeof standardValue.raw_payload === 'string') { + const convertedFieldValue = JSON.parse(standardValue.raw_payload); acc[attribute.id] = [ dayjs.unix(Number(convertedFieldValue.from)), dayjs.unix(Number(convertedFieldValue.to)) diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.test.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.test.ts index 072a20e76..38f617081 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.test.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.test.ts @@ -2,7 +2,7 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import getActiveFieldValues from '_ui/components/RecordEdition/EditRecordContent/helpers/getActiveFieldValues'; -import {FieldScope} from '_ui/components/RecordEdition/EditRecordContent/_types'; +import {VersionFieldScope} from '_ui/components/RecordEdition/EditRecordContent/_types'; import {RecordFormElementsValueLinkValue} from '_ui/hooks/useGetRecordForm/useGetRecordForm'; import {mockFormElementLinkVersionable, mockLinkValue} from '_ui/__mocks__/common/form'; import linkFieldReducer, {ILinkFieldState, LinkFieldReducerActionsType, virginState} from './linkFieldReducer'; @@ -127,10 +127,10 @@ describe('linkFieldReducer', () => { test('CHANGE_ACTIVE_SCOPE', () => { const newState = linkFieldReducer(initialLinkFieldState, { type: LinkFieldReducerActionsType.CHANGE_ACTIVE_SCOPE, - scope: FieldScope.INHERITED + scope: VersionFieldScope.INHERITED }); - expect(newState.activeScope).toBe(FieldScope.INHERITED); + expect(newState.activeScope).toBe(VersionFieldScope.INHERITED); }); test('REFRESH_VALUES', async () => { @@ -164,10 +164,10 @@ describe('linkFieldReducer', () => { } ); - expect(newState.activeScope).toBe(FieldScope.INHERITED); - expect(newState.values[FieldScope.INHERITED].version).toBe(valuesVersion); - expect(newState.values[FieldScope.INHERITED].values).toHaveLength(1); - expect(newState.values[FieldScope.INHERITED].values[0].id_value).toBe('123456'); - expect(newState.values[FieldScope.CURRENT].values).toEqual([]); + expect(newState.activeScope).toBe(VersionFieldScope.INHERITED); + expect(newState.values[VersionFieldScope.INHERITED].version).toBe(valuesVersion); + expect(newState.values[VersionFieldScope.INHERITED].values).toHaveLength(1); + expect(newState.values[VersionFieldScope.INHERITED].values[0].id_value).toBe('123456'); + expect(newState.values[VersionFieldScope.CURRENT].values).toEqual([]); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.ts index 139f50c20..aa44c0668 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/linkFieldReducer/linkFieldReducer.ts @@ -4,7 +4,7 @@ import getActiveFieldValues from '_ui/components/RecordEdition/EditRecordContent/helpers/getActiveFieldValues'; import isCurrentVersion from '_ui/components/RecordEdition/EditRecordContent/helpers/isCurrentVersion'; import { - FieldScope, + VersionFieldScope, FormElement, ICommonFieldsReducerState } from '_ui/components/RecordEdition/EditRecordContent/_types'; @@ -49,7 +49,7 @@ export type LinkFieldReducerActions = | {type: LinkFieldReducerActionsType.SET_ERROR_MESSAGE; errorMessage: string | string[]} | {type: LinkFieldReducerActionsType.CLEAR_ERROR_MESSAGE} | {type: LinkFieldReducerActionsType.SET_IS_VALUES_ADD_VISIBLE; isValuesAddVisible: boolean} - | {type: LinkFieldReducerActionsType.CHANGE_ACTIVE_SCOPE; scope: FieldScope} + | {type: LinkFieldReducerActionsType.CHANGE_ACTIVE_SCOPE; scope: VersionFieldScope} | { type: LinkFieldReducerActionsType.REFRESH_VALUES; values: ValuesType[]; @@ -61,13 +61,13 @@ export const virginState: ILinkFieldState = { formElement: null, attribute: null, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: null, values: [] }, - [FieldScope.INHERITED]: null + [VersionFieldScope.INHERITED]: null }, errorMessage: '', isValuesAddVisible: false @@ -100,13 +100,13 @@ const _computeScopeAndValues = (params: { const inheritedVersion = hasInheritedValues ? values?.[0]?.version : null; return { - activeScope: hasInheritedValues ? FieldScope.INHERITED : FieldScope.CURRENT, + activeScope: hasInheritedValues ? VersionFieldScope.INHERITED : VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: currentVersion ?? null, values: hasInheritedValues ? [] : values }, - [FieldScope.INHERITED]: hasInheritedValues ? {version: inheritedVersion ?? null, values} : null + [VersionFieldScope.INHERITED]: hasInheritedValues ? {version: inheritedVersion ?? null, values} : null } }; }; @@ -198,7 +198,7 @@ const linkFieldReducer = ( ..._computeScopeAndValues({ attribute: state.formElement.attribute, values: action.values, - formVersion: state.values[FieldScope.CURRENT].version + formVersion: state.values[VersionFieldScope.CURRENT].version }) }; } diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.test.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.test.ts index 709d3d0f6..9ea5f8745 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.test.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.test.ts @@ -2,8 +2,8 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import getActiveFieldValues from '_ui/components/RecordEdition/EditRecordContent/helpers/getActiveFieldValues'; -import {FieldScope} from '_ui/components/RecordEdition/EditRecordContent/_types'; -import {AttributeFormat, AttributeType, FormElementTypes} from '_ui/_gqlTypes'; +import {VersionFieldScope} from '_ui/components/RecordEdition/EditRecordContent/_types'; +import {AttributeFormat, AttributeType} from '_ui/_gqlTypes'; import {mockFormElementInput, mockFormElementInputVersionable} from '_ui/__mocks__/common/form'; import {mockRecord} from '_ui/__mocks__/common/record'; import {mockModifier} from '_ui/__mocks__/common/value'; @@ -18,6 +18,7 @@ import { StandardFieldValueState, virginValue } from './standardFieldReducer'; +import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; describe('standardFieldReducer', () => { const mockAttribute = { @@ -55,15 +56,15 @@ describe('standardFieldReducer', () => { const initialState: IStandardFieldReducerState = { record: mockRecord, formElement: mockFormElementInput, - attribute: mockAttribute, + attribute: mockFormAttribute, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: null, values: {[idValue]: mockValue} }, - [FieldScope.INHERITED]: null + [VersionFieldScope.INHERITED]: null }, metadataEdit: false, inheritedValue: null, @@ -84,8 +85,8 @@ describe('standardFieldReducer', () => { ...initialState, values: { ...initialState.values, - [FieldScope.CURRENT]: { - ...initialState.values[FieldScope.CURRENT], + [VersionFieldScope.CURRENT]: { + ...initialState.values[VersionFieldScope.CURRENT], values: {...values} } }, @@ -515,9 +516,9 @@ describe('standardFieldReducer', () => { const newState = standardFieldValueReducer( { ...initialState, - activeScope: FieldScope.INHERITED, + activeScope: VersionFieldScope.INHERITED, values: { - [FieldScope.INHERITED]: { + [VersionFieldScope.INHERITED]: { version: inheritedVersion, values: { '123465789': { @@ -538,7 +539,7 @@ describe('standardFieldReducer', () => { } } }, - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: currentVersion, values: { [newValueId]: { @@ -550,12 +551,12 @@ describe('standardFieldReducer', () => { }, { type: StandardFieldReducerActionsTypes.CHANGE_VERSION_SCOPE, - scope: FieldScope.CURRENT + scope: VersionFieldScope.CURRENT } ); const newStateValues = getActiveFieldValues(newState); - expect(newState.activeScope).toBe(FieldScope.CURRENT); + expect(newState.activeScope).toBe(VersionFieldScope.CURRENT); expect(Object.keys(newStateValues)).toHaveLength(1); expect(newStateValues[newValueId]).toBeDefined(); }); @@ -591,10 +592,10 @@ describe('standardFieldReducer', () => { } ); - expect(newState.activeScope).toBe(FieldScope.INHERITED); - expect(newState.values[FieldScope.INHERITED].version).toBe(valuesVersion); - expect(newState.values[FieldScope.INHERITED].values['123456']).toBeDefined(); - expect(newState.values[FieldScope.CURRENT].values[newValueId]).toBeDefined(); + expect(newState.activeScope).toBe(VersionFieldScope.INHERITED); + expect(newState.values[VersionFieldScope.INHERITED].version).toBe(valuesVersion); + expect(newState.values[VersionFieldScope.INHERITED].values['123456']).toBeDefined(); + expect(newState.values[VersionFieldScope.CURRENT].values[newValueId]).toBeDefined(); }); describe('computeInitialState', () => { diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.ts index 48c398880..ce4fbea34 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.ts @@ -4,7 +4,7 @@ import {AnyPrimitive, IDateRangeValue, IKeyValue} from '@leav/utils'; import isCurrentVersion from '_ui/components/RecordEdition/EditRecordContent/helpers/isCurrentVersion'; import { - FieldScope, + VersionFieldScope, FormElement, ICommonFieldsReducerState, StandardValueTypes @@ -145,7 +145,7 @@ export type StandardFieldReducerValueActions = export type StandardFieldReducerFieldActions = | { type: StandardFieldReducerActionsTypes.CHANGE_VERSION_SCOPE; - scope: FieldScope; + scope: VersionFieldScope; } | { type: StandardFieldReducerActionsTypes.REFRESH_VALUES; @@ -189,6 +189,10 @@ const _updateValueData = ( return res; }; +const getInheritedValue = (values: RecordFormElementsValueStandardValue[]) => values.filter(value => value.isInherited); +const getNotInheritedOrOverrideValue = (values: RecordFormElementsValueStandardValue[]) => + values.filter(value => !value.isInherited && value.raw_value !== null); + /** * For given list of values, determine if there is inherited values, inherited/current version and default active scope */ @@ -199,31 +203,35 @@ const _computeScopeAndValues = (params: { }): Pick => { const {attribute, values, formVersion} = params; - const preparedValues = - Array.isArray(values) && values.length - ? values.reduce( - (allValues: IKeyValue, fieldValue, index) => ({ - ...allValues, - [fieldValue?.id_value ?? null]: { - ...virginValue, - idValue: fieldValue?.id_value ?? null, - index, - value: fieldValue ?? null, - displayValue: fieldValue?.value ?? '', - editingValue: - attribute.format === AttributeFormat.encrypted ? '' : fieldValue?.raw_value ?? '', - originRawValue: - attribute.format === AttributeFormat.encrypted ? '' : fieldValue?.raw_value ?? '' - } - }), - {} - ) - : { - [newValueId]: { + // Get override or inhertied values + let valuesToHandle = getNotInheritedOrOverrideValue(values); + if (!valuesToHandle.length) { + valuesToHandle = getInheritedValue(values); + } + + const preparedValues = valuesToHandle.length + ? valuesToHandle.reduce( + (allValues: IKeyValue, fieldValue, index) => ({ + ...allValues, + [fieldValue?.id_value ?? null]: { ...virginValue, - idValue: newValueId + idValue: fieldValue?.id_value ?? null, + index, + value: fieldValue ?? null, + displayValue: fieldValue?.value ?? '', + editingValue: attribute.format === AttributeFormat.encrypted ? '' : (fieldValue?.raw_value ?? ''), + originRawValue: + attribute.format === AttributeFormat.encrypted ? '' : (fieldValue?.raw_value ?? '') } - }; + }), + {} + ) + : { + [newValueId]: { + ...virginValue, + idValue: newValueId + } + }; const currentVersion: IValueVersion = attribute?.versions_conf?.versionable ? attribute.versions_conf.profile.trees.reduce((relevantVersion, tree) => { @@ -235,19 +243,21 @@ const _computeScopeAndValues = (params: { }, {}) : null; - const hasInheritedValues = attribute?.versions_conf?.versionable + const hasVersionInheritedValues = attribute?.versions_conf?.versionable ? !isCurrentVersion(currentVersion, values?.[0]?.version ?? currentVersion) : false; // We assume that all values have the same version - const inheritedVersion = hasInheritedValues ? values?.[0]?.version : null; + const inheritedVersion = hasVersionInheritedValues ? values?.[0]?.version : null; return { - activeScope: hasInheritedValues ? FieldScope.INHERITED : FieldScope.CURRENT, + activeScope: hasVersionInheritedValues ? VersionFieldScope.INHERITED : VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: currentVersion ?? null, - values: hasInheritedValues ? {[newValueId]: {...virginValue, idValue: newValueId}} : preparedValues + values: hasVersionInheritedValues + ? {[newValueId]: {...virginValue, idValue: newValueId}} + : preparedValues }, - [FieldScope.INHERITED]: hasInheritedValues + [VersionFieldScope.INHERITED]: hasVersionInheritedValues ? {version: inheritedVersion ?? null, values: preparedValues} : null } @@ -296,7 +306,7 @@ export const computeInitialState = (params: { const {element, record, metadataEdit, isRecordReadOnly, formVersion} = params; const attribute = element.attribute; - const fieldValues = [...(element.values as RecordFormElementsValueStandardValue[])] ?? []; + const fieldValues = [...(element.values as RecordFormElementsValueStandardValue[])]; return { attribute, @@ -387,10 +397,10 @@ const standardFieldReducer = ( const newValueData = { ...virginValue, idValue: action.newValue.id_value, - value: ({ + value: { ...(action.newValue as ValueDetailsValueFragment), version: newValueVersion - } as unknown) as IRecordPropertyStandard, + } as unknown as IRecordPropertyStandard, displayValue: (action.newValue as ValueDetailsValueFragment).value, editingValue: newRawValue, originRawValue: newRawValue, diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducerContext.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducerContext.ts new file mode 100644 index 000000000..8fee7861f --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducerContext.ts @@ -0,0 +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 React, {Dispatch} from 'react'; +import {IStandardFieldReducerState, StandardFieldReducerValueActions} from './standardFieldReducer'; + +export interface IStandardFieldReducerContext { + state: IStandardFieldReducerState; + dispatch: Dispatch; +} + +export const StandardFieldReducerContext = React.createContext({ + state: null, + dispatch: null +}); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/useStandardFieldReducer.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/useStandardFieldReducer.ts new file mode 100644 index 000000000..c2719f163 --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/useStandardFieldReducer.ts @@ -0,0 +1,4 @@ +import {useContext} from 'react'; +import {StandardFieldReducerContext} from './standardFieldReducerContext'; + +export const useStandardFieldReducer = () => useContext(StandardFieldReducerContext); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.test.tsx index b8324a29f..6863140a3 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.test.tsx @@ -3,7 +3,7 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import userEvent from '@testing-library/user-event'; import {act, render, screen, waitFor} from '_ui/_tests/testUtils'; -import {FieldScope} from '../../_types'; +import {VersionFieldScope} from '../../_types'; import AddValueBtn from './AddValueBtn'; describe('AddValueBtn', () => { @@ -11,7 +11,7 @@ describe('AddValueBtn', () => { const mockOnClick = jest.fn(); await act(async () => { - render(); + render(); }); const btn = screen.getByRole('button', {name: /add_value/}); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.tsx index 1fbcec1c9..cffcc7dc7 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/AddValueBtn/AddValueBtn.tsx @@ -6,12 +6,12 @@ import {ButtonProps} from 'antd'; import {themeVars} from '_ui/antdTheme'; import {BasicButton} from '_ui/components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {FieldScope} from '../../_types'; +import {VersionFieldScope} from '../../_types'; interface IAddValueBtnProps extends ButtonProps { bordered?: boolean; linkField?: boolean; - activeScope: FieldScope; + activeScope: VersionFieldScope; } function AddValueBtn({bordered = false, linkField = false, activeScope, ...props}: IAddValueBtnProps): JSX.Element { @@ -22,7 +22,7 @@ function AddValueBtn({bordered = false, linkField = false, activeScope, ...props {...props} style={{ color: - activeScope === FieldScope.INHERITED + activeScope === VersionFieldScope.INHERITED ? themeVars.inheritedValuesVersionColor : themeVars.defaultTextColor }} diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.test.tsx index c9152c2c8..d9ae567c8 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.test.tsx @@ -3,19 +3,19 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import userEvent from '@testing-library/user-event'; import {render, screen, waitFor} from '_ui/_tests/testUtils'; -import {FieldScope} from '../../_types'; +import {VersionFieldScope} from '../../_types'; import ValuesVersionBtn from './ValuesVersionBtn'; describe('ValuesVersionBtn', () => { test('Display available versions on click', async () => { const versions = { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { lang: { id: '1337', label: 'FR' } }, - [FieldScope.INHERITED]: { + [VersionFieldScope.INHERITED]: { lang: { id: '1337', label: 'EN' @@ -25,7 +25,11 @@ describe('ValuesVersionBtn', () => { const onScopeChange = jest.fn(); render( - + ); const valuesVersionBtn = screen.getByRole('button', {name: /values-version/}); @@ -38,12 +42,12 @@ describe('ValuesVersionBtn', () => { userEvent.click(screen.getByRole('menuitem', {name: /fr/i})); - await waitFor(() => expect(onScopeChange).toHaveBeenCalledWith(FieldScope.CURRENT)); + await waitFor(() => expect(onScopeChange).toHaveBeenCalledWith(VersionFieldScope.CURRENT)); await userEvent.click(valuesVersionBtn); userEvent.click(screen.getByRole('menuitem', {name: /en/i})); - await waitFor(() => expect(onScopeChange).toHaveBeenCalledWith(FieldScope.INHERITED)); + await waitFor(() => expect(onScopeChange).toHaveBeenCalledWith(VersionFieldScope.INHERITED)); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.tsx index 506c75dd6..f9c1cd7f4 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionBtn/ValuesVersionBtn.tsx @@ -10,12 +10,12 @@ import {BasicButton} from '_ui/components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; import {IValueVersion} from '_ui/types/values'; import {getValueVersionLabel} from '_ui/_utils'; -import {FieldScope} from '../../_types'; +import {VersionFieldScope} from '../../_types'; interface IValuesVersionBtnProps extends Omit { - versions: {[scope in FieldScope]: IValueVersion}; - activeScope: FieldScope; - onScopeChange: (scope: FieldScope) => void; + versions: {[scope in VersionFieldScope]: IValueVersion}; + activeScope: VersionFieldScope; + onScopeChange: (scope: VersionFieldScope) => void; basic?: boolean; } @@ -27,26 +27,27 @@ function ValuesVersionBtn({ ...buttonProps }: IValuesVersionBtnProps): JSX.Element { const {t} = useSharedTranslation(); - const hasInheritedVersion = !!versions[FieldScope.INHERITED]; + const hasInheritedVersion = !!versions[VersionFieldScope.INHERITED]; const _handleVersionSelect: MenuItemType['onClick'] = item => { item.domEvent.preventDefault(); item.domEvent.stopPropagation(); - onScopeChange(item.key as FieldScope); + onScopeChange(item.key as VersionFieldScope); }; - const currentVersionLabel = getValueVersionLabel(versions[FieldScope.CURRENT]); + const currentVersionLabel = getValueVersionLabel(versions[VersionFieldScope.CURRENT]); const iconProps = { size: '1.8em', style: { paddingTop: '5px' } }; - const icon = activeScope === FieldScope.CURRENT ? : ; + const icon = + activeScope === VersionFieldScope.CURRENT ? : ; const menuItems: ItemType[] = [ { - key: FieldScope.CURRENT, + key: VersionFieldScope.CURRENT, label: ( {icon} @@ -59,7 +60,7 @@ function ValuesVersionBtn({ if (hasInheritedVersion) { const inheritedVersionLabel = - getValueVersionLabel(versions[FieldScope.INHERITED]) + ` (${t('values_version.inherited_value')})`; + getValueVersionLabel(versions[VersionFieldScope.INHERITED]) + ` (${t('values_version.inherited_value')})`; const inheritedIconProps = { size: '1.8em', @@ -70,13 +71,13 @@ function ValuesVersionBtn({ }; const inheritedIcon = - activeScope === FieldScope.INHERITED ? ( + activeScope === VersionFieldScope.INHERITED ? ( ) : ( ); menuItems.unshift({ - key: FieldScope.INHERITED, + key: VersionFieldScope.INHERITED, label: ( {inheritedIcon} @@ -95,7 +96,7 @@ function ValuesVersionBtn({ ); return ( - + {button} ); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionIndicator/ValuesVersionIndicator.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionIndicator/ValuesVersionIndicator.tsx index c9e6cdcf8..7a895ecbe 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionIndicator/ValuesVersionIndicator.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/shared/ValuesVersionIndicator/ValuesVersionIndicator.tsx @@ -5,7 +5,7 @@ import {theme} from 'antd'; import {GlobalToken} from 'antd/lib/theme/interface'; import styled, {CSSObject} from 'styled-components'; import {themeVars} from '_ui/antdTheme'; -import {FieldScope} from '../../_types'; +import {VersionFieldScope} from '../../_types'; const Indicator = styled.div<{$isCurrentVersion: boolean; $style: CSSObject; $themeToken: GlobalToken}>` position: absolute; @@ -28,12 +28,12 @@ const Indicator = styled.div<{$isCurrentVersion: boolean; $style: CSSObject; $th `; interface IValuesVersionIndicatorProps { - activeScope: FieldScope; + activeScope: VersionFieldScope; style?: CSSObject; } function ValuesVersionIndicator({activeScope, style}: IValuesVersionIndicatorProps): JSX.Element { - const isCurrentVersion = activeScope === FieldScope.CURRENT; + const isCurrentVersion = activeScope === VersionFieldScope.CURRENT; const {token} = theme.useToken(); return ; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.test.tsx index f7d3aaa97..6b6e70df4 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.test.tsx @@ -5,8 +5,8 @@ import {render, screen} from '_ui/_tests/testUtils'; import {MonoValueSelect} from './MonoValueSelect'; import {mockFormElementInput, mockFormElementLink} from '_ui/__mocks__/common/form'; import {LinkFieldReducerState} from '../LinkField'; -import {mockAttributeLink} from '_ui/__mocks__/common/attribute'; -import {APICallStatus, FieldScope} from '../../../_types'; +import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; +import {APICallStatus, VersionFieldScope} from '../../../_types'; import {mockRecord} from '_ui/__mocks__/common/record'; import {AntForm} from 'aristid-ds'; import userEvent from '@testing-library/user-event'; @@ -134,12 +134,12 @@ describe('', () => { required: true } }, - attribute: mockAttributeLink, + attribute: mockFormAttribute, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: null, - [FieldScope.INHERITED]: null + [VersionFieldScope.CURRENT]: null, + [VersionFieldScope.INHERITED]: null } }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.test.tsx index 5bd6b4118..b961de3e1 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.test.tsx @@ -5,8 +5,8 @@ import {render, screen} from '_ui/_tests/testUtils'; import {MultiValueSelect} from './MultiValueSelect'; import {mockFormElementInput, mockFormElementLink} from '_ui/__mocks__/common/form'; import {LinkFieldReducerState} from '../LinkField'; -import {mockAttributeLink} from '_ui/__mocks__/common/attribute'; -import {APICallStatus, FieldScope} from '../../../_types'; +import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; +import {APICallStatus, VersionFieldScope} from '../../../_types'; import {mockRecord} from '_ui/__mocks__/common/record'; import {AntForm} from 'aristid-ds'; import userEvent from '@testing-library/user-event'; @@ -81,12 +81,12 @@ describe('', () => { required: true } }, - attribute: mockAttributeLink, + attribute: mockFormAttribute, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: null, - [FieldScope.INHERITED]: null + [VersionFieldScope.CURRENT]: null, + [VersionFieldScope.INHERITED]: null } }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.test.tsx index 37e50620f..70ef36d21 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.test.tsx @@ -2,7 +2,6 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import userEvent from '@testing-library/user-event'; -import {Suspense} from 'react'; import { EditRecordReducerActionsTypes, initialState @@ -16,7 +15,7 @@ import { ValueDetailsValueFragment } from '_ui/_gqlTypes'; import {IRecordPropertyAttribute} from '_ui/_queries/records/getRecordPropertiesQuery'; -import {render, screen, waitFor, waitForOptions} from '_ui/_tests/testUtils'; +import {render, screen, waitFor} from '_ui/_tests/testUtils'; import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; import {mockFormElementInput} from '_ui/__mocks__/common/form'; import {mockRecord} from '_ui/__mocks__/common/record'; @@ -30,12 +29,43 @@ import { SubmitValueFunc } from '../../_types'; import StandardField from './StandardField'; +import {AntForm} from 'aristid-ds'; +import {getAntdFormInitialValues} from '../../antdUtils'; jest.mock('../../hooks/useExecuteDeleteValueMutation'); jest.useRealTimers(); jest.setTimeout(15000); +const _makeRecordForm = (payloads: {payload: any; raw_payload: any}, format: AttributeFormat) => ({ + dependencyAttributes: [], + elements: [ + { + ...mockFormElementInput, + settings: [ + {key: 'label', value: 'test attribute'}, + {key: 'attribute', value: 'test_attribute'} + ], + attribute: {...mockFormAttribute, format}, + values: [ + { + created_at: 123456789, + modified_at: 123456789, + created_by: mockModifier, + modified_by: mockModifier, + id_value: null, + metadata: null, + version: null, + ...payloads + } + ] + } + ], + id: 'edition', + recordId: 'recordId', + library: {id: 'libraryId'} +}); + describe('StandardField', () => { const mockEditRecordDispatch = jest.fn(); jest.spyOn(useEditRecordReducer, 'useEditRecordReducer').mockImplementation(() => ({ @@ -108,6 +138,81 @@ describe('StandardField', () => { beforeEach(() => jest.clearAllMocks()); + describe('Display value with read/write mode', () => { + const testCases = [ + { + format: AttributeFormat.text, + payload: 'My value formatted', + rawPayload: 'Some raw value', + inputRole: 'textbox' + }, + { + format: AttributeFormat.numeric, + payload: '42,00 €', + rawPayload: '42', + inputRole: 'spinbutton' + }, + { + format: AttributeFormat.date, + payload: '08 juin 1987', + rawPayload: '550108800', + inputRole: 'textbox', + inputValue: '1987-06-08' + }, + { + format: AttributeFormat.date_range, + payload: {from: '1er janvier 2024', to: '1er janvier 2025'}, + readValue: 'record_edition.date_range_value|1er janvier 2024|1er janvier 2025', + rawPayload: { + from: 1704067200, + to: 1735689600 + }, + inputRole: 'textbox', + inputValue: '2024-01-01' + }, + { + format: AttributeFormat.encrypted, + readValue: '●●●●●●●', + payload: 'true', + rawPayload: 'true', + inputTestId: 'kit-input-password', + inputValue: '' + } + ]; + + test.each(testCases)( + 'Format $format', + async ({format, readValue, payload, rawPayload, inputRole, inputTestId, inputValue}) => { + const payloads = { + payload, + raw_payload: rawPayload + }; + const recordForm = _makeRecordForm(payloads, format); + const formElement = {...recordForm.elements[0], settings: {}}; + + const antdFormInitialValues = getAntdFormInitialValues(recordForm); + render( + + + + + + ); + + const formattedValueElem = screen.getByText(readValue ?? String(payloads.payload)); + expect(formattedValueElem).toBeVisible(); + + expect(screen.queryByRole(inputRole)).toBeNull(); + + await userEvent.click(formattedValueElem); + + const inputElem = inputTestId ? screen.getAllByTestId(inputTestId) : screen.getAllByRole(inputRole); + expect(inputElem[0]).toBeVisible(); + expect(inputElem[0]).toHaveValue(inputValue ?? String(payloads.raw_payload)); + } + ); + }); + test('Display informations about value', async () => { render(); @@ -192,24 +297,6 @@ describe('StandardField', () => { expect(screen.getByText('ERROR_MESSAGE')).toBeInTheDocument(); }); - test('Delete value', async () => { - render(); - - const inputWrapper = screen.getByTestId('input-wrapper'); - await userEvent.hover(inputWrapper); - - const deleteBtn = screen.getByRole('button', {name: /delete/, hidden: true}); - expect(deleteBtn).toBeInTheDocument(); - - await userEvent.click(deleteBtn); - - const confirmDeleteBtn = screen.getByRole('button', {name: 'delete-confirm-button'}); - - await userEvent.click(confirmDeleteBtn); - - expect(mockHandleDelete).toHaveBeenCalled(); - }); - test('On multiple-values attribute, can delete all values', async () => { render( ` margin-bottom: ${props => (props.$metadataEdit ? 0 : '1.5em')}; @@ -230,7 +231,7 @@ const StandardField: FunctionComponent }); }; - const _handleScopeChange = (scope: FieldScope) => { + const _handleScopeChange = (scope: VersionFieldScope) => { dispatch({ type: StandardFieldReducerActionsTypes.CHANGE_VERSION_SCOPE, scope @@ -284,46 +285,52 @@ const StandardField: FunctionComponent const isAttributeVersionable = attribute?.versions_conf?.versionable; const versions = { - [FieldScope.CURRENT]: state.values[FieldScope.CURRENT]?.version ?? null, - [FieldScope.INHERITED]: state.values[FieldScope.INHERITED]?.version ?? null + [VersionFieldScope.CURRENT]: state.values[VersionFieldScope.CURRENT]?.version ?? null, + [VersionFieldScope.INHERITED]: state.values[VersionFieldScope.INHERITED]?.version ?? null }; return ( - - {valuesToDisplay.map(value => ( - - ))} - {(canDeleteAllValues || canAddAnotherValue || attribute?.versions_conf?.versionable) && ( - -
- {isAttributeVersionable && ( - + + + {valuesToDisplay.map(value => ( + + ))} + {(canDeleteAllValues || canAddAnotherValue || attribute?.versions_conf?.versionable) && ( + +
+ {isAttributeVersionable && ( + + )} + {canDeleteAllValues && } +
+ {canAddAnotherValue && ( + )} - {canDeleteAllValues && } -
- {canAddAnotherValue && } -
- )} -
+ + )} + + ); }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldNumeric.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldNumeric.test.tsx index df18741ee..8e81ebdb6 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldNumeric.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldNumeric.test.tsx @@ -1,7 +1,7 @@ // 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 {render, screen, waitFor} from '_ui/_tests/testUtils'; +import {render, screen} from '_ui/_tests/testUtils'; import userEvent from '@testing-library/user-event'; import StandardField from '../StandardField'; import {mockModifier} from '_ui/__mocks__/common/value'; @@ -79,8 +79,8 @@ describe('StandardField, Numeric input', () => { const recordValuesNumeric = [ { ...mockRecordValuesCommon, - value: '123456', - raw_value: '123456' + payload: '42,00 €', + raw_payload: '42' } ]; @@ -94,7 +94,7 @@ describe('StandardField, Numeric input', () => { { ...mockFormElementInput, settings: [ - {key: 'label', value: 'test atribute'}, + {key: 'label', value: 'test attribute'}, {key: 'attribute', value: 'test_attribute'} ], attribute: {...mockFormAttribute, format: AttributeFormat.numeric}, @@ -123,11 +123,17 @@ describe('StandardField, Numeric input', () => { ); - const inputElem = screen.getByRole('spinbutton'); - expect(inputElem).toHaveValue('123456'); + const formattedValueElem = screen.getByText(recordValuesNumeric[0].payload); + expect(formattedValueElem).toBeVisible(); + + expect(screen.queryByRole('spinbutton')).toBeNull(); - await userEvent.click(inputElem); + await userEvent.click(formattedValueElem); + + const inputElem = screen.getByRole('spinbutton'); + expect(inputElem).toBeVisible(); + expect(inputElem).toHaveValue(recordValuesNumeric[0].raw_payload); - expect(screen.getByRole('spinbutton')).toBeInTheDocument(); + expect(screen.getByText(recordValuesNumeric[0].payload)).not.toBeVisible(); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldRichText.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldRichText.test.tsx deleted file mode 100644 index 549c4151c..000000000 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldRichText.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -// 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 {mockBrowserFunctionsForTiptap, render, screen, waitFor} from '_ui/_tests/testUtils'; -import userEvent from '@testing-library/user-event'; -import StandardField from '../StandardField'; -import { - APICallStatus, - DeleteMultipleValuesFunc, - DeleteValueFunc, - ISubmitMultipleResult, - SubmitValueFunc -} from '../../_types'; -import {mockModifier} from '_ui/__mocks__/common/value'; -import {AttributeFormat, AttributeType, ValueDetailsValueFragment} from '_ui/_gqlTypes'; -import {IRecordPropertyAttribute} from '_ui/_queries/records/getRecordPropertiesQuery'; -import {mockFormElementInput} from '_ui/__mocks__/common/form'; -import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; -import {Suspense} from 'react'; - -const tiptapCleanup = mockBrowserFunctionsForTiptap(); - -afterAll(() => { - tiptapCleanup(); -}); - -describe('StandardField, Rich Text input', () => { - const mockRecordValuesCommon = { - created_at: 123456789, - modified_at: 123456789, - created_by: mockModifier, - modified_by: mockModifier, - id_value: null, - metadata: null, - version: null - }; - - const mockAttribute: IRecordPropertyAttribute = { - id: 'test_attribute', - label: {en: 'Test Attribute'}, - format: AttributeFormat.color, - type: AttributeType.simple, - system: false - }; - - const mockSubmitRes: ISubmitMultipleResult = { - status: APICallStatus.SUCCESS, - values: [ - { - id_value: null, - created_at: 1234567890, - created_by: { - ...mockModifier - }, - modified_at: 1234567890, - modified_by: { - ...mockModifier - }, - value: 'new value', - raw_value: 'new raw value', - version: null, - attribute: mockAttribute as ValueDetailsValueFragment['attribute'], - metadata: null - } - ] - }; - - const mockHandleSubmit: SubmitValueFunc = jest.fn().mockReturnValue(mockSubmitRes); - const mockHandleDelete: DeleteValueFunc = jest.fn().mockReturnValue({status: APICallStatus.SUCCESS}); - const mockHandleMultipleValues: DeleteMultipleValuesFunc = jest - .fn() - .mockReturnValue({status: APICallStatus.SUCCESS}); - - const baseProps = { - onValueSubmit: mockHandleSubmit, - onValueDelete: mockHandleDelete, - onDeleteMultipleValues: mockHandleMultipleValues - }; - - global.ResizeObserver = jest.fn().mockImplementation(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn() - })); - - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - test('Render Rich Text field', async () => { - const recordValuesRichText = [ - { - ...mockRecordValuesCommon, - value: '

rich text editor test

', - raw_value: 'new rich text editor test' - } - ]; - render( - Loading}> - - - ); - - const richTextElem = screen.getByRole('textbox'); - await userEvent.click(richTextElem); - - const richTextElemOpen = richTextElem.parentElement.previousSibling as HTMLElement; - const menuBarClassNames = richTextElemOpen?.getAttribute('class'); - expect(menuBarClassNames).toContain('menu-bar'); - expect(richTextElemOpen).toBeInTheDocument(); - - await userEvent.type(richTextElem, 'new value'); - - await userEvent.click(document.body); - expect(mockHandleSubmit).toHaveBeenCalled(); - }); -}); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldText.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldText.test.tsx deleted file mode 100644 index 4e3d93115..000000000 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldText.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// 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 {render, screen, waitFor} from '_ui/_tests/testUtils'; -import StandardField from './StandardField'; -import userEvent from '@testing-library/user-event'; -import {mockFormElementInput} from '_ui/__mocks__/common/form'; -import { - APICallStatus, - DeleteMultipleValuesFunc, - DeleteValueFunc, - ISubmitMultipleResult, - SubmitValueFunc -} from '../../_types'; -import {mockModifier} from '_ui/__mocks__/common/value'; -import {AttributeFormat, AttributeType, ValueDetailsValueFragment} from '_ui/_gqlTypes'; -import {IRecordPropertyAttribute} from '_ui/_queries/records/getRecordPropertiesQuery'; - -describe('StandardField, Text input', () => { - const mockAttribute: IRecordPropertyAttribute = { - id: 'test_attribute', - label: {en: 'Test Attribute'}, - format: AttributeFormat.text, - type: AttributeType.simple, - system: false - }; - - const mockSubmitRes: ISubmitMultipleResult = { - status: APICallStatus.SUCCESS, - values: [ - { - id_value: null, - created_at: 1234567890, - created_by: { - ...mockModifier - }, - modified_at: 1234567890, - modified_by: { - ...mockModifier - }, - value: 'new value', - raw_value: 'new raw value', - version: null, - attribute: mockAttribute as ValueDetailsValueFragment['attribute'], - metadata: null - } - ] - }; - - const mockHandleSubmit: SubmitValueFunc = jest.fn().mockReturnValue(mockSubmitRes); - const mockHandleDelete: DeleteValueFunc = jest.fn().mockReturnValue({status: APICallStatus.SUCCESS}); - const mockHandleMultipleValues: DeleteMultipleValuesFunc = jest - .fn() - .mockReturnValue({status: APICallStatus.SUCCESS}); - - const baseProps = { - onValueSubmit: mockHandleSubmit, - onValueDelete: mockHandleDelete, - onDeleteMultipleValues: mockHandleMultipleValues - }; - - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - test('Render text field, type value and submit', async () => { - render(); - - const inputElem = screen.getByRole('textbox'); - expect(inputElem).toBeInTheDocument(); - expect(inputElem).toHaveValue('My value formatted'); - - await userEvent.click(inputElem); - - // When editing, input is a new component, thus we have to get it again - const editingInputElem = screen.getByRole('textbox'); - await waitFor(() => { - expect(editingInputElem).toHaveValue('my_raw_value'); - }); - - const submitBtn = screen.getByRole('button', {name: 'global.submit'}); - expect(submitBtn).toBeVisible(); - - await userEvent.clear(editingInputElem); - await userEvent.type(editingInputElem, 'value modified'); - await waitFor(() => { - expect(editingInputElem).toHaveValue('value modified'); - }); - - userEvent.click(submitBtn); - - await waitFor(() => { - expect(mockHandleSubmit).toHaveBeenCalled(); - }); - }); -}); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.test.tsx index cb9c079b9..88a54f9ea 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.test.tsx @@ -11,8 +11,8 @@ import { } from '../../../reducers/standardFieldReducer/standardFieldReducer'; import {mockRecord} from '_ui/__mocks__/common/record'; import {mockFormElementInput} from '_ui/__mocks__/common/form'; -import {mockAttributeLink} from '_ui/__mocks__/common/attribute'; -import {FieldScope} from '../../../_types'; +import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; +import {VersionFieldScope} from '../../../_types'; import userEvent from '@testing-library/user-event'; const en_label = 'label'; @@ -82,15 +82,15 @@ const getInitialState = ({ required } }, - attribute: mockAttributeLink, + attribute: mockFormAttribute, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: null, values: {[idValue]: mockValueFalse} }, - [FieldScope.INHERITED]: null + [VersionFieldScope.INHERITED]: null }, metadataEdit: false, inheritedValue: null, @@ -142,7 +142,12 @@ describe('DSBooleanWrapper', () => { render( - + ); @@ -156,7 +161,12 @@ describe('DSBooleanWrapper', () => { render( - + ); @@ -170,7 +180,12 @@ describe('DSBooleanWrapper', () => { render( - + ); @@ -187,7 +202,12 @@ describe('DSBooleanWrapper', () => { render( - + ); @@ -274,7 +294,7 @@ describe('DSBooleanWrapper', () => { const clearButton = screen.getByRole('button'); await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', 'my_attribute'); + expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.test.tsx index cfa55bfec..f7ba25d96 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.test.tsx @@ -3,18 +3,18 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {render, screen} from '_ui/_tests/testUtils'; import {DSDatePickerWrapper} from './DSDatePickerWrapper'; -import {FieldScope} from '../../../_types'; +import {VersionFieldScope} from '../../../_types'; import { IStandardFieldReducerState, StandardFieldValueState } from '../../../reducers/standardFieldReducer/standardFieldReducer'; import {mockRecord} from '_ui/__mocks__/common/record'; import {mockFormElementInput} from '_ui/__mocks__/common/form'; -import {mockAttributeLink} from '_ui/__mocks__/common/attribute'; import userEvent from '@testing-library/user-event'; import {Form} from 'antd'; import dayjs from 'dayjs'; import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; +import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; const en_label = 'label'; const fr_label = 'libellé'; @@ -56,15 +56,15 @@ const getInitialState = ({ required } }, - attribute: mockAttributeLink, + attribute: mockFormAttribute, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: null, values: {[idValue]: mockValue} }, - [FieldScope.INHERITED]: null + [VersionFieldScope.INHERITED]: null }, metadataEdit: false, inheritedValue: null, @@ -76,12 +76,14 @@ const getInitialState = ({ describe('DSDatePickerWrapper', () => { const mockOnChange = jest.fn(); const mockHandleSubmit = jest.fn(); + const mockHandleBlur = jest.fn(); let user!: ReturnType; beforeEach(() => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); + mockHandleBlur.mockReset(); }); describe('Without required field', () => { @@ -93,8 +95,9 @@ describe('DSDatePickerWrapper', () => { @@ -112,8 +115,9 @@ describe('DSDatePickerWrapper', () => { @@ -131,9 +135,10 @@ describe('DSDatePickerWrapper', () => { @@ -157,9 +162,10 @@ describe('DSDatePickerWrapper', () => { @@ -189,9 +195,10 @@ describe('DSDatePickerWrapper', () => { @@ -215,9 +222,10 @@ describe('DSDatePickerWrapper', () => { @@ -249,9 +257,10 @@ describe('DSDatePickerWrapper', () => { @@ -270,9 +279,10 @@ describe('DSDatePickerWrapper', () => { @@ -298,9 +308,10 @@ describe('DSDatePickerWrapper', () => { @@ -332,9 +343,10 @@ describe('DSDatePickerWrapper', () => { diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.tsx index 576032c78..e69c2fb55 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSDatePickerWrapper.tsx @@ -2,12 +2,12 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {KitDatePicker} from 'aristid-ds'; -import {FunctionComponent} from 'react'; +import {FunctionComponent, useEffect, useRef} from 'react'; import { IStandardFieldReducerState, IStandardFieldValue } from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form} from 'antd'; +import {Form, type GetRef} from 'antd'; import dayjs from 'dayjs'; import styled from 'styled-components'; import {IProvidedByAntFormItem, StandardValueTypes} from '../../../_types'; @@ -17,12 +17,12 @@ import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; import {useValueDetailsButton} from '_ui/components/RecordEdition/EditRecordContent/shared/ValueDetailsBtn/useValueDetailsButton'; import {useLang} from '_ui/hooks'; import {localizedTranslation} from '@leav/utils'; - interface IDSDatePickerWrapperProps extends IProvidedByAntFormItem { state: IStandardFieldReducerState; attribute: RecordFormAttributeFragment; fieldValue: IStandardFieldValue; handleSubmit: (value: StandardValueTypes, id?: string) => void; + handleBlur: () => void; shouldShowValueDetailsButton?: boolean; } @@ -33,6 +33,7 @@ const KitDatePickerStyled = styled(KitDatePicker)<{$shouldHighlightColor: boolea export const DSDatePickerWrapper: FunctionComponent = ({ value, onChange, + handleBlur, state, attribute, fieldValue, @@ -47,6 +48,14 @@ export const DSDatePickerWrapper: FunctionComponent = attribute }); + const inputRef = useRef>(null); + + useEffect(() => { + if (fieldValue.isEditing && inputRef.current) { + inputRef.current.nativeElement.click(); // To automatically open the date picker + } + }, [fieldValue.isEditing]); + const _handleDateChange: (datePickerDate: dayjs.Dayjs | null, antOnChangeParams: string | string[]) => void = ( datePickerDate, ...antOnChangeParams @@ -65,7 +74,7 @@ export const DSDatePickerWrapper: FunctionComponent = } let dateToSave = null; - if (datePickerDate !== null) { + if (!!datePickerDate) { dateToSave = String(datePickerDate.unix()); } @@ -76,6 +85,7 @@ export const DSDatePickerWrapper: FunctionComponent = return ( = }) : undefined } + onBlur={handleBlur} $shouldHighlightColor={state.isInheritedNotOverrideValue} /> ); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputPasswordWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.test.tsx similarity index 80% rename from libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputPasswordWrapper.test.tsx rename to libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.test.tsx index a6cfbaae0..7525ca919 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputPasswordWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.test.tsx @@ -2,8 +2,8 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {render, screen} from '_ui/_tests/testUtils'; -import {DSInputPasswordWrapper} from './DSInputPasswordWrapper'; -import {FieldScope} from '../../../_types'; +import {DSInputEncryptedWrapper} from './DSInputEncryptedWrapper'; +import {VersionFieldScope} from '../../../_types'; import { InheritedFlags, IStandardFieldReducerState, @@ -11,7 +11,7 @@ import { } from '../../../reducers/standardFieldReducer/standardFieldReducer'; import {mockRecord} from '_ui/__mocks__/common/record'; import {mockFormElementInput} from '_ui/__mocks__/common/form'; -import {mockAttributeLink} from '_ui/__mocks__/common/attribute'; +import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; import userEvent from '@testing-library/user-event'; import {AntForm} from 'aristid-ds'; import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; @@ -57,15 +57,15 @@ const getInitialState = ({ required } }, - attribute: mockAttributeLink, + attribute: mockFormAttribute, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: null, values: {[idValue]: mockValue} }, - [FieldScope.INHERITED]: null + [VersionFieldScope.INHERITED]: null }, metadataEdit: false, inheritedValue: null, @@ -101,15 +101,18 @@ const inheritedOverrideValue: InheritedFlags = { inheritedValue: {raw_value: inheritedValues[1].raw_value} }; -describe('DSInputPasswordWrapper', () => { +describe('DSInputEncryptedWrapper', () => { const mockHandleSubmit = jest.fn(); const mockOnChange = jest.fn(); + const mockHandleBlur = jest.fn(); + let user!: ReturnType; beforeEach(() => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); + mockHandleBlur.mockReset(); }); test('Should display input with fr label ', async () => { @@ -117,11 +120,12 @@ describe('DSInputPasswordWrapper', () => { render( - @@ -136,11 +140,12 @@ describe('DSInputPasswordWrapper', () => { render( - @@ -150,16 +155,17 @@ describe('DSInputPasswordWrapper', () => { expect(screen.getByText(en_label)).toBeVisible(); }); - test('Should submit empty value if field is not required', async () => { + test('Should not submit if field has not changed', async () => { const state = getInitialState({required: false, fallbackLang: false}); render( - @@ -171,8 +177,8 @@ describe('DSInputPasswordWrapper', () => { await user.click(input); await user.tab(); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - expect(mockOnChange).toHaveBeenCalledTimes(1); + expect(mockHandleSubmit).not.toHaveBeenCalledWith(); + expect(mockOnChange).not.toHaveBeenCalled(); }); describe('With required input and no inheritance', () => { @@ -181,11 +187,12 @@ describe('DSInputPasswordWrapper', () => { render( - @@ -201,30 +208,6 @@ describe('DSInputPasswordWrapper', () => { expect(mockHandleSubmit).toHaveBeenCalledWith(text, state.attribute.id); expect(mockOnChange).toHaveBeenCalled(); }); - - test('Should submit the default value if field is empty', async () => { - const state = getInitialState({required: true, fallbackLang: false}); - render( - - - - - - ); - - const input = screen.getByTestId('kit-input-password'); - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).toHaveBeenCalledWith(mockValue.originRawValue, state.attribute.id); - }); }); describe('With inheritance', () => { @@ -238,11 +221,12 @@ describe('DSInputPasswordWrapper', () => { render( - @@ -271,11 +255,12 @@ describe('DSInputPasswordWrapper', () => { render( - @@ -299,11 +284,12 @@ describe('DSInputPasswordWrapper', () => { render( - @@ -314,7 +300,7 @@ describe('DSInputPasswordWrapper', () => { await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', 'my_attribute'); + expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputPasswordWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.tsx similarity index 82% rename from libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputPasswordWrapper.tsx rename to libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.tsx index 83dd2c074..e1f599bc7 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputPasswordWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.tsx @@ -2,7 +2,7 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {KitInput} from 'aristid-ds'; -import {ChangeEvent, FocusEvent, FunctionComponent, ReactNode, useState} from 'react'; +import {ChangeEvent, FocusEvent, FunctionComponent, useEffect, useRef, useState} from 'react'; import { IStandardFieldReducerState, IStandardFieldValue @@ -16,11 +16,12 @@ import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; import {useLang} from '_ui/hooks'; import {localizedTranslation} from '@leav/utils'; -interface IDSInputWrapperProps extends IProvidedByAntFormItem { +interface IDSInputEncryptedWrapper extends IProvidedByAntFormItem { state: IStandardFieldReducerState; attribute: RecordFormAttributeFragment; fieldValue: IStandardFieldValue; handleSubmit: (value: string, id?: string) => void; + handleBlur: () => void; shouldShowValueDetailsButton?: boolean; } @@ -34,13 +35,14 @@ const KitInputPasswordStyled = styled(KitInput.Password)<{$shouldHighlightColor: } `; -export const DSInputPasswordWrapper: FunctionComponent = ({ +export const DSInputEncryptedWrapper: FunctionComponent = ({ value, onChange, state, attribute, fieldValue, handleSubmit, + handleBlur, shouldShowValueDetailsButton = false }) => { const {t} = useSharedTranslation(); @@ -52,6 +54,14 @@ export const DSInputPasswordWrapper: FunctionComponent = ( const [hasChanged, setHasChanged] = useState(false); const {lang: availableLang} = useLang(); + const inputRef = useRef(null); + + useEffect(() => { + if (fieldValue.isEditing && inputRef.current) { + inputRef.current.focus(); + } + }, [fieldValue.isEditing]); + const _resetToInheritedValue = () => { setHasChanged(false); onChange(state.inheritedValue.raw_value); @@ -59,14 +69,21 @@ export const DSInputPasswordWrapper: FunctionComponent = ( }; const _handleOnBlur = (event: FocusEvent) => { + if (!hasChanged) { + handleBlur(); + return; + } + const valueToSubmit = event.target.value; if (valueToSubmit === '' && state.isInheritedValue) { _resetToInheritedValue(); return; } - if (hasChanged || !state.isInheritedValue) { + + if (!state.isInheritedValue) { handleSubmit(valueToSubmit, state.attribute.id); } + onChange(event); }; @@ -84,6 +101,9 @@ export const DSInputPasswordWrapper: FunctionComponent = ( return ( { const mockHandleSubmit = jest.fn(); const mockOnChange = jest.fn(); + const mockHandleBlur = jest.fn(); + let user!: ReturnType; beforeEach(() => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); + mockHandleBlur.mockReset(); }); test('Should display inputNumber with fr label ', async () => { @@ -113,8 +118,9 @@ describe('DSInputNumberWrapper', () => { @@ -132,8 +138,9 @@ describe('DSInputNumberWrapper', () => { @@ -143,7 +150,7 @@ describe('DSInputNumberWrapper', () => { expect(screen.getByText(en_label)).toBeVisible(); }); - test('Should submit empty value if field is not required', async () => { + test('Should submit empty value on clear if field is not required', async () => { const state = getInitialState(false); render( @@ -151,20 +158,23 @@ describe('DSInputNumberWrapper', () => { ); const input = screen.getByRole('spinbutton'); + await user.clear(input); await user.tab(); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); expect(mockOnChange).toHaveBeenCalled(); + expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); describe('With required input and no inheritance', () => { @@ -176,8 +186,9 @@ describe('DSInputNumberWrapper', () => { @@ -201,8 +212,9 @@ describe('DSInputNumberWrapper', () => { @@ -232,8 +244,9 @@ describe('DSInputNumberWrapper', () => { @@ -263,8 +276,9 @@ describe('DSInputNumberWrapper', () => { @@ -293,8 +307,9 @@ describe('DSInputNumberWrapper', () => { @@ -324,8 +339,9 @@ describe('DSInputNumberWrapper', () => { diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.tsx index 75397c3f4..24d654bac 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.tsx @@ -2,12 +2,12 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {KitInputNumber} from 'aristid-ds'; -import {ComponentPropsWithRef, FocusEvent, FunctionComponent, useState} from 'react'; +import {ComponentPropsWithRef, FocusEvent, FunctionComponent, useEffect, useRef, useState} from 'react'; import { IStandardFieldReducerState, IStandardFieldValue } from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, InputNumberProps} from 'antd'; +import {Form, GetRef, InputNumberProps} from 'antd'; import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; import styled from 'styled-components'; @@ -21,6 +21,7 @@ interface IDSInputWrapperProps extends IProvidedByAntFormItem attribute: RecordFormAttributeFragment; fieldValue: IStandardFieldValue; handleSubmit: (value: string, id?: string) => void; + handleBlur: () => void; shouldShowValueDetailsButton?: boolean; } @@ -38,6 +39,7 @@ export const DSInputNumberWrapper: FunctionComponent = ({ attribute, fieldValue, handleSubmit, + handleBlur, shouldShowValueDetailsButton = false }) => { if (!onChange) { @@ -54,6 +56,14 @@ export const DSInputNumberWrapper: FunctionComponent = ({ const [hasChanged, setHasChanged] = useState(false); + const inputRef = useRef>(null); + + useEffect(() => { + if (fieldValue.isEditing && inputRef.current) { + inputRef.current.focus(); // To automatically open the date picker + } + }, [fieldValue.isEditing]); + const _resetToInheritedValue = () => { setHasChanged(false); onChange(state.inheritedValue.raw_value); @@ -61,14 +71,21 @@ export const DSInputNumberWrapper: FunctionComponent = ({ }; const _handleOnBlur = (event: FocusEvent) => { + if (!hasChanged) { + handleBlur(); + return; + } + const valueToSubmit = event.target.value; if (valueToSubmit === '' && state.isInheritedValue) { _resetToInheritedValue(); return; } + if (hasChanged || !state.isInheritedValue) { handleSubmit(valueToSubmit, state.attribute.id); } + onChange(valueToSubmit); }; @@ -81,6 +98,7 @@ export const DSInputNumberWrapper: FunctionComponent = ({ return ( { const mockHandleSubmit = jest.fn(); const mockOnChange = jest.fn(); + const mockHandleBlur = jest.fn(); + let user!: ReturnType; beforeEach(() => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); + mockHandleBlur.mockReset(); }); test('Should display input with fr label ', async () => { @@ -113,8 +116,9 @@ describe('DSInputWrapper', () => { @@ -132,8 +136,9 @@ describe('DSInputWrapper', () => { @@ -143,7 +148,7 @@ describe('DSInputWrapper', () => { expect(screen.getByText(en_label)).toBeVisible(); }); - test('Should submit empty value if field is not required', async () => { + test('Should not submit if field has not changed', async () => { const state = getInitialState(false); render( @@ -151,8 +156,9 @@ describe('DSInputWrapper', () => { @@ -163,8 +169,8 @@ describe('DSInputWrapper', () => { await user.click(input); await user.tab(); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - expect(mockOnChange).toHaveBeenCalled(); + expect(mockOnChange).not.toHaveBeenCalled(); + expect(mockHandleSubmit).not.toHaveBeenCalled(); }); describe('With required input and no inheritance', () => { @@ -176,8 +182,9 @@ describe('DSInputWrapper', () => { @@ -193,30 +200,6 @@ describe('DSInputWrapper', () => { expect(mockHandleSubmit).toHaveBeenCalledWith(text, state.attribute.id); expect(mockOnChange).toHaveBeenCalled(); }); - - test('Should submit the default value if field is empty', async () => { - const state = getInitialState(true); - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).toHaveBeenCalledWith(mockValue.originRawValue, state.attribute.id); - }); }); describe('With inheritance', () => { @@ -233,8 +216,9 @@ describe('DSInputWrapper', () => { @@ -266,8 +250,9 @@ describe('DSInputWrapper', () => { @@ -294,8 +279,9 @@ describe('DSInputWrapper', () => { @@ -306,7 +292,7 @@ describe('DSInputWrapper', () => { await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', 'my_attribute'); + expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.tsx index fee2ef5d4..bcfa19e7e 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.tsx @@ -2,12 +2,12 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {KitInput} from 'aristid-ds'; -import {ChangeEvent, FocusEvent, FunctionComponent, ReactNode, useState} from 'react'; +import {ChangeEvent, FocusEvent, FunctionComponent, useEffect, useRef, useState} from 'react'; import { IStandardFieldReducerState, IStandardFieldValue } from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, InputProps} from 'antd'; +import {Form, GetRef, InputProps} from 'antd'; import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; import styled from 'styled-components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; @@ -21,6 +21,7 @@ interface IDSInputWrapperProps extends IProvidedByAntFormItem { attribute: RecordFormAttributeFragment; fieldValue: IStandardFieldValue; handleSubmit: (value: string, id?: string) => void; + handleBlur: () => void; shouldShowValueDetailsButton?: boolean; } @@ -35,6 +36,7 @@ export const DSInputWrapper: FunctionComponent = ({ attribute, fieldValue, handleSubmit, + handleBlur, shouldShowValueDetailsButton = false }) => { const {t} = useSharedTranslation(); @@ -45,6 +47,13 @@ export const DSInputWrapper: FunctionComponent = ({ }); const [hasChanged, setHasChanged] = useState(false); const {lang: availableLang} = useLang(); + const inputRef = useRef>(null); + + useEffect(() => { + if (fieldValue.isEditing && inputRef.current) { + inputRef.current.focus(); + } + }, [fieldValue.isEditing]); const _resetToInheritedValue = () => { setHasChanged(false); @@ -53,14 +62,19 @@ export const DSInputWrapper: FunctionComponent = ({ }; const _handleOnBlur = (event: FocusEvent) => { + if (!hasChanged) { + handleBlur(); + return; + } + const valueToSubmit = event.target.value; if (valueToSubmit === '' && state.isInheritedValue) { _resetToInheritedValue(); return; } - if (hasChanged || !state.isInheritedValue) { - handleSubmit(valueToSubmit, state.attribute.id); - } + + handleSubmit(valueToSubmit, state.attribute.id); + onChange(event); }; @@ -71,6 +85,11 @@ export const DSInputWrapper: FunctionComponent = ({ _resetToInheritedValue(); return; } + + if (inputValue === '' && event.type === 'click') { + handleSubmit(inputValue, state.attribute.id); + } + onChange(event); }; @@ -78,6 +97,7 @@ export const DSInputWrapper: FunctionComponent = ({ return ( { const mockOnChange = jest.fn(); const mockHandleSubmit = jest.fn(); + const mockHandleBlur = jest.fn(); let user!: ReturnType; beforeEach(() => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); + mockHandleBlur.mockReset(); }); describe('Without required field', () => { @@ -87,8 +89,9 @@ describe('DSRangePickerWrapper', () => { @@ -106,8 +109,9 @@ describe('DSRangePickerWrapper', () => { @@ -125,9 +129,10 @@ describe('DSRangePickerWrapper', () => { @@ -161,9 +166,10 @@ describe('DSRangePickerWrapper', () => { @@ -195,9 +201,10 @@ describe('DSRangePickerWrapper', () => { @@ -229,9 +236,10 @@ describe('DSRangePickerWrapper', () => { @@ -265,9 +273,10 @@ describe('DSRangePickerWrapper', () => { @@ -286,9 +295,10 @@ describe('DSRangePickerWrapper', () => { @@ -317,9 +327,10 @@ describe('DSRangePickerWrapper', () => { @@ -355,9 +366,10 @@ describe('DSRangePickerWrapper', () => { diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.tsx index 43ccb7dd3..f68eca449 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.tsx @@ -2,7 +2,7 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {KitDatePicker} from 'aristid-ds'; -import {FunctionComponent} from 'react'; +import {FunctionComponent, useEffect, useRef} from 'react'; import { IStandardFieldReducerState, IStandardFieldValue @@ -23,6 +23,7 @@ interface IDSRangePickerWrapperProps extends IProvidedByAntFormItem void; + handleBlur: () => void; shouldShowValueDetailsButton?: boolean; } @@ -37,6 +38,7 @@ export const DSRangePickerWrapper: FunctionComponent attribute, fieldValue, handleSubmit, + handleBlur, shouldShowValueDetailsButton = false }) => { const {t} = useSharedTranslation(); @@ -47,6 +49,14 @@ export const DSRangePickerWrapper: FunctionComponent attribute }); + const inputRef = useRef(); + + useEffect(() => { + if (fieldValue.isEditing && inputRef.current) { + inputRef.current.nativeElement.click(); // To automatically open the date picker + } + }, [fieldValue.isEditing]); + const _handleDateChange: ( rangePickerDates: [from: dayjs.Dayjs, to: dayjs.Dayjs] | null, antOnChangeParams: [from: string, to: string] | null @@ -80,10 +90,18 @@ export const DSRangePickerWrapper: FunctionComponent handleSubmit(datesToSave, state.attribute.id); }; + const _handleOpenChange = (open: boolean) => { + if (!open) { + handleBlur(); + } + }; + const label = localizedTranslation(state.formElement.settings.label, availableLangs); return ( }) : undefined } + onOpenChange={_handleOpenChange} $shouldHighlightColor={state.isInheritedNotOverrideValue} /> ); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.test.tsx index effb8e2c6..f9910cedc 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.test.tsx @@ -3,7 +3,7 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {mockBrowserFunctionsForTiptap, render, screen} from '_ui/_tests/testUtils'; import {DSRichTextWrapper} from './DSRichTextWrapper'; -import {FieldScope} from '../../../_types'; +import {VersionFieldScope} from '../../../_types'; import { InheritedFlags, IStandardFieldReducerState, @@ -11,7 +11,7 @@ import { } from '../../../reducers/standardFieldReducer/standardFieldReducer'; import {mockRecord} from '_ui/__mocks__/common/record'; import {mockFormElementInput} from '_ui/__mocks__/common/form'; -import {mockAttributeLink} from '_ui/__mocks__/common/attribute'; +import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; import userEvent from '@testing-library/user-event'; import {AntForm} from 'aristid-ds'; import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; @@ -50,15 +50,15 @@ const getInitialState = (required: boolean, fallbackLang = false): IStandardFiel required } }, - attribute: mockAttributeLink, + attribute: mockFormAttribute, isReadOnly: false, - activeScope: FieldScope.CURRENT, + activeScope: VersionFieldScope.CURRENT, values: { - [FieldScope.CURRENT]: { + [VersionFieldScope.CURRENT]: { version: null, values: {[idValue]: mockValue} }, - [FieldScope.INHERITED]: null + [VersionFieldScope.INHERITED]: null }, metadataEdit: false, inheritedValue: null, @@ -102,6 +102,7 @@ describe('DSRichTextWrapper', () => { }); const mockHandleSubmit = jest.fn(); + const mockHandleBlur = jest.fn(); const mockOnChange = jest.fn(); let user!: ReturnType; @@ -109,6 +110,7 @@ describe('DSRichTextWrapper', () => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); + mockHandleBlur.mockReset(); }); test('Should display input with fr label ', async () => { @@ -119,8 +121,9 @@ describe('DSRichTextWrapper', () => { @@ -138,8 +141,9 @@ describe('DSRichTextWrapper', () => { @@ -149,7 +153,7 @@ describe('DSRichTextWrapper', () => { expect(screen.getByText(en_label)).toBeVisible(); }); - test('Should submit empty value if field is not required', async () => { + test('Should not submit if value has not changed', async () => { const state = getInitialState(false); render( @@ -157,8 +161,9 @@ describe('DSRichTextWrapper', () => { @@ -169,8 +174,8 @@ describe('DSRichTextWrapper', () => { await user.click(input); await user.click(document.body); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - expect(mockOnChange).toHaveBeenCalled(); + expect(mockHandleSubmit).not.toHaveBeenCalled(); + expect(mockOnChange).not.toHaveBeenCalled(); }); describe('With required input and no inheritance', () => { @@ -182,8 +187,9 @@ describe('DSRichTextWrapper', () => { @@ -199,30 +205,6 @@ describe('DSRichTextWrapper', () => { expect(mockHandleSubmit).toHaveBeenCalledWith(`

${text}

`, state.attribute.id); expect(mockOnChange).toHaveBeenCalled(); }); - - test('Should submit the default value if field is empty', async () => { - const state = getInitialState(true); - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - await user.click(input); - await user.click(document.body); - - expect(mockHandleSubmit).toHaveBeenCalledWith(mockValue.originRawValue, state.attribute.id); - }); }); describe('With inheritance', () => { @@ -239,8 +221,9 @@ describe('DSRichTextWrapper', () => { @@ -271,8 +254,9 @@ describe('DSRichTextWrapper', () => { diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.tsx index 8631e2799..4426013cb 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRichTextWrapper.tsx @@ -6,7 +6,7 @@ import { IStandardFieldReducerState, IStandardFieldValue } from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, InputProps} from 'antd'; +import {Form, GetRef, InputProps} from 'antd'; import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; import styled from 'styled-components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; @@ -21,6 +21,7 @@ interface IDSRichTextWrapperProps extends IProvidedByAntFormItem { attribute: RecordFormAttributeFragment; fieldValue: IStandardFieldValue; handleSubmit: (value: string, id?: string) => void; + handleBlur: () => void; shouldShowValueDetailsButton?: boolean; } @@ -37,6 +38,7 @@ export const DSRichTextWrapper: FunctionComponent = ({ attribute, fieldValue, handleSubmit, + handleBlur, shouldShowValueDetailsButton = false }) => { const {t} = useSharedTranslation(); @@ -47,6 +49,13 @@ export const DSRichTextWrapper: FunctionComponent = ({ }); const [hasChanged, setHasChanged] = useState(false); const {lang: availableLang} = useLang(); + const inputRef = useRef>(null); + + useEffect(() => { + if (fieldValue.isEditing && inputRef.current) { + (inputRef.current.children[0] as HTMLElement).focus(); + } + }, [fieldValue.isEditing]); const _resetToInheritedValue = () => { setHasChanged(false); @@ -55,6 +64,11 @@ export const DSRichTextWrapper: FunctionComponent = ({ }; const _handleOnBlur = inputValue => { + if (!hasChanged) { + handleBlur(); + return; + } + const valueToSubmit = isEmptyValue(inputValue) ? '' : inputValue; if (valueToSubmit === '' && state.isInheritedValue) { @@ -81,6 +95,7 @@ export const DSRichTextWrapper: FunctionComponent = ({ return ( void; + onSubmit: (idValue: IdValue, value: AnyPrimitive) => Promise; onDelete: (idValue: IdValue) => void; - onScopeChange: (scope: FieldScope) => void; + onScopeChange: (scope: VersionFieldScope) => void; } function StandardFieldValue({ @@ -250,13 +244,20 @@ function StandardFieldValue({ } }, [fieldValue.isEditing, fieldValue.editingValue]); + const _uneditField = () => { + dispatch({ + type: StandardFieldReducerActionsTypes.CANCEL_EDITING, + idValue: fieldValue.idValue + }); + }; + const _handleSubmit = async (valueToSave: StandardValueTypes, id?: string) => { if (valueToSave === '') { return _handleDelete(); } const convertedValue = typeof valueToSave === 'object' ? JSON.stringify(valueToSave) : valueToSave; - onSubmit(fieldValue.idValue, convertedValue); + await onSubmit(fieldValue.idValue, convertedValue); }; const _handlePressEnter = async () => { @@ -277,7 +278,8 @@ function StandardFieldValue({ return _handleCancel(); } - onDelete(fieldValue.idValue); + await onDelete(fieldValue.idValue); + _uneditField(); }; const _handleFocus = () => { @@ -522,8 +524,8 @@ function StandardFieldValue({ if (attribute?.versions_conf?.versionable) { const versions = { - [FieldScope.CURRENT]: state.values[FieldScope.CURRENT]?.version ?? null, - [FieldScope.INHERITED]: state.values[FieldScope.INHERITED]?.version ?? null + [VersionFieldScope.CURRENT]: state.values[VersionFieldScope.CURRENT]?.version ?? null, + [VersionFieldScope.INHERITED]: state.values[VersionFieldScope.INHERITED]?.version ?? null }; if (!hasMultipleValuesDisplay) { @@ -569,73 +571,12 @@ function StandardFieldValue({ return ( <> {attributeFormatsWithDS.includes(attribute.format) && ( - - {attribute.format === AttributeFormat.text && ( - - )} - {attribute.format === AttributeFormat.date && ( - - )} - {attribute.format === AttributeFormat.date_range && ( - - )} - {attribute.format === AttributeFormat.numeric && ( - - )} - {attribute.format === AttributeFormat.encrypted && ( - - )} - {attribute.format === AttributeFormat.rich_text && ( - - )} - {attribute.format === AttributeFormat.boolean && ( - - )} - + )} {attributeFormatsWithoutDS.includes(attribute.format) && ( @@ -661,8 +602,10 @@ function StandardFieldValue({ {editRecordState.externalUpdate.updatedValues[attribute?.id] && ( )} - {state.activeScope === FieldScope.INHERITED && ( - + {state.activeScope === VersionFieldScope.INHERITED && ( + )} )} diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueDisplayHandler.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueDisplayHandler.tsx new file mode 100644 index 000000000..af81f0dc8 --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueDisplayHandler.tsx @@ -0,0 +1,113 @@ +import {AttributeFormat, RecordFormAttributeFragment} from '_ui/_gqlTypes'; +import {ComponentProps, FunctionComponent} from 'react'; +import { + IStandardFieldReducerState, + IStandardFieldValue, + StandardFieldReducerActionsTypes +} from '../../../../reducers/standardFieldReducer/standardFieldReducer'; +import {DSBooleanWrapper} from '../DSBooleanWrapper'; +import {DSDatePickerWrapper} from '../DSDatePickerWrapper'; +import {DSInputNumberWrapper} from '../DSInputNumberWrapper'; +import {DSInputEncryptedWrapper} from '../DSInputEncryptedWrapper'; +import {DSInputWrapper} from '../DSInputWrapper'; +import {DSRangePickerWrapper} from '../DSRangePickerWrapper'; +import {StandardValueTypes} from '../../../../_types'; +import {useEditRecordReducer} from '_ui/components/RecordEdition/editRecordReducer/useEditRecordReducer'; +import {StandardFieldValueRead} from './StandardFieldValueRead/StandardFieldValueRead'; +import {useStandardFieldReducer} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/useStandardFieldReducer'; +import {Form} from 'antd'; +import {useTranslation} from 'react-i18next'; +import {DSRichTextWrapper} from '../DSRichTextWrapper'; + +interface IStandardFieldValueDisplayHandlerProps { + state: IStandardFieldReducerState; + attribute: RecordFormAttributeFragment; + fieldValue: IStandardFieldValue; + shouldShowValueDetailsButton?: boolean; + handleSubmit: (value: StandardValueTypes, id?: string) => void; +} + +export const StandardFieldValueDisplayHandler: FunctionComponent = ({ + attribute, + fieldValue, + handleSubmit +}) => { + const {t} = useTranslation(); + const {state: editRecordState} = useEditRecordReducer(); + const {state, dispatch} = useStandardFieldReducer(); + const mustDisplayReadValue = !fieldValue.isEditing && attribute.format !== AttributeFormat.boolean; + + const _handleClickOnReadValue: ComponentProps['onClick'] = e => { + e.stopPropagation(); + + if (state.isReadOnly) { + return; + } + + dispatch({ + type: StandardFieldReducerActionsTypes.FOCUS_FIELD, + idValue: fieldValue.idValue + }); + }; + + const _handleBlur = () => { + dispatch({ + type: StandardFieldReducerActionsTypes.CANCEL_EDITING, + idValue: fieldValue.idValue + }); + }; + + const commonProps = { + state, + handleSubmit, + handleBlur: _handleBlur, + attribute, + fieldValue, + shouldShowValueDetailsButton: editRecordState.withInfoButton + }; + + let valueContent; + switch (attribute.format) { + case AttributeFormat.text: + valueContent = ; + break; + case AttributeFormat.date: + valueContent = ; + break; + case AttributeFormat.date_range: + valueContent = ; + break; + case AttributeFormat.numeric: + valueContent = ; + break; + case AttributeFormat.encrypted: + valueContent = ; + break; + case AttributeFormat.boolean: + valueContent = ; + break; + case AttributeFormat.rich_text: + valueContent = ; + break; + } + + return ( + <> +
+ +
+ + {valueContent} + + + ); +}; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/StandardFieldValueRead.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/StandardFieldValueRead.tsx new file mode 100644 index 000000000..2a4fac85d --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/StandardFieldValueRead.tsx @@ -0,0 +1,128 @@ +import {localizedTranslation} from '@leav/utils'; +import {AttributeFormat} from '_ui/_gqlTypes'; +import {IStandardFieldValue} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer'; +import {useStandardFieldReducer} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/useStandardFieldReducer'; +import {useValueDetailsButton} from '_ui/components/RecordEdition/EditRecordContent/shared/ValueDetailsBtn/useValueDetailsButton'; +import {useEditRecordReducer} from '_ui/components/RecordEdition/editRecordReducer/useEditRecordReducer'; +import {RecordFormElementsValueStandardValue} from '_ui/hooks/useGetRecordForm'; +import useLang from '_ui/hooks/useLang'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; +import {KitInputWrapper, KitTypography} from 'aristid-ds'; +import {FunctionComponent, SyntheticEvent} from 'react'; +import styled from 'styled-components'; +import DOMPurify from 'dompurify'; + +interface IStandardFieldValueReadProps { + fieldValue: IStandardFieldValue; + onClick: (e: SyntheticEvent) => void; + className?: string; +} + +const _isDateRangeValue = (value: any): value is {from: string; to: string} => + !!value && typeof value === 'object' && 'from' in value && 'to' in value; + +const KitInputWrapperStyled = styled(KitInputWrapper)<{$width: string}>` + > .kit-input-wrapper-content { + width: ${({$width}) => $width}; + } +`; + +const ValueWrapper = styled(KitTypography.Paragraph)<{$highlighted: boolean}>` + min-height: calc(var(--general-typography-fontSize6) * var(--general-typography-lineHeight6) * 1px); + color: ${({$highlighted}) => ($highlighted ? 'var(--general-colors-primary-400)' : 'initial')}; + font-size: calc(var(--general-typography-fontSize5) * 1px); +`; + +export const StandardFieldValueRead: FunctionComponent = ({ + fieldValue, + onClick, + className +}) => { + const {state} = useStandardFieldReducer(); + const {state: editRecordState} = useEditRecordReducer(); + const {lang: availableLang} = useLang(); + const {t} = useSharedTranslation(); + + const shouldShowValueDetailsButton = editRecordState.withInfoButton; + + const {onValueDetailsButtonClick} = useValueDetailsButton({ + value: fieldValue?.value, + attribute: state.attribute + }); + + const label = localizedTranslation(state.formElement.settings.label, availableLang); + + const _getInheritedValueForHelper = (inheritedValue: RecordFormElementsValueStandardValue) => { + switch (state.attribute.format) { + case AttributeFormat.date_range: + return t('record_edition.date_range_from_to', { + from: inheritedValue.value.from, + to: inheritedValue.value.to + }); + case AttributeFormat.encrypted: + return inheritedValue.value ? '●●●●●●●' : ''; + default: + return inheritedValue.value; + } + }; + + const _handleFocus = (e: SyntheticEvent) => { + if (state.isReadOnly) { + return; + } + onClick(e); + }; + + let displayValue = String(fieldValue.value?.payload ?? ''); + let width = '100%'; + switch (state.attribute.format) { + case AttributeFormat.date_range: + if (!_isDateRangeValue(fieldValue.value?.payload)) { + displayValue = ''; + } else { + const {from, to} = fieldValue.value?.payload; + displayValue = t('record_edition.date_range_value', {from, to}); + } + break; + case AttributeFormat.encrypted: + displayValue = fieldValue.value?.payload ? '●●●●●●●' : ''; + break; + case AttributeFormat.numeric: + width = '90px'; + break; + case AttributeFormat.date: + width = '185px'; + break; + } + + const isValueHighlighted = state.isInheritedNotOverrideValue; + return ( + + + + + + ); +}; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/index.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/index.ts new file mode 100644 index 000000000..734ca8f9a --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/index.ts @@ -0,0 +1 @@ +export {StandardFieldValueDisplayHandler} from './StandardFieldValueDisplayHandler'; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/TreeField/TreeField.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/TreeField/TreeField.tsx index 706201c26..c7f475809 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/TreeField/TreeField.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/TreeField/TreeField.tsx @@ -36,7 +36,7 @@ import UpdatedFieldIcon from '../../shared/UpdatedFieldIcon'; import ValueDetailsBtn from '../../shared/ValueDetailsBtn'; import ValuesVersionBtn from '../../shared/ValuesVersionBtn'; import ValuesVersionIndicator from '../../shared/ValuesVersionIndicator'; -import {APICallStatus, FieldScope, IFormElementProps} from '../../_types'; +import {APICallStatus, VersionFieldScope, IFormElementProps} from '../../_types'; import TreeFieldValue from './TreeFieldValue'; import ValuesAdd from './ValuesAdd'; import {useLang} from '_ui/hooks'; @@ -255,7 +255,7 @@ function TreeField({ } }; - const _handleScopeChange = (scope: FieldScope) => { + const _handleScopeChange = (scope: VersionFieldScope) => { dispatch({ type: LinkFieldReducerActionsType.CHANGE_ACTIVE_SCOPE, scope @@ -263,8 +263,8 @@ function TreeField({ }; const versions = { - [FieldScope.CURRENT]: state.values[FieldScope.CURRENT]?.version ?? null, - [FieldScope.INHERITED]: state.values[FieldScope.INHERITED]?.version ?? null + [VersionFieldScope.CURRENT]: state.values[VersionFieldScope.CURRENT]?.version ?? null, + [VersionFieldScope.INHERITED]: state.values[VersionFieldScope.INHERITED]?.version ?? null }; const ListFooter = ( @@ -318,8 +318,8 @@ function TreeField({ {label} {editRecordState.externalUpdate.updatedValues[attribute?.id] && } - {state.activeScope === FieldScope.INHERITED && ( - + {state.activeScope === VersionFieldScope.INHERITED && ( + )} Promise; @@ -49,9 +50,14 @@ const useRefreshFieldValues = ( } const values = res.data.records.list[0][attributeId].map(value => { - const {__typename, ...valueData} = value; // Clear off __typename + // Clear off __typename + const data = {...value}; + if (hasTypename(data)) { + delete data.__typename; + } + return { - ...valueData, + ...data, version: arrayValueVersionToObject(value.version ?? []) }; });