From 7338b867895cce970176ec749ea8be28c3aee6f1 Mon Sep 17 00:00:00 2001 From: renaudAmsellem Date: Thu, 28 Nov 2024 11:23:10 +0100 Subject: [PATCH] feat(@leav/ui): adapt multivalue to ds (#636) * feat(@leav/ui): Adapt multivalue to DS * feat(@leav/ui): Remove StandardFieldValueRead and use StandardField even for formated values --------- Co-authored-by: Philippe Chevieux --- apps/data-studio/package.json | 2 +- apps/login/package.json | 2 +- apps/portal/package.json | 2 +- libs/ui/package.json | 2 +- .../RecordEdition/EditRecord/EditRecord.tsx | 2 +- .../RecordEdition/EditRecordContent/_types.ts | 8 +- .../EditRecordContent/antdUtils.test.tsx | 44 ++ .../EditRecordContent/antdUtils.tsx | 90 ++-- .../standardFieldReducer.test.ts | 2 +- .../standardFieldReducer.ts | 4 +- .../MonoValueSelect/MonoValueSelect.tsx | 2 +- .../MultiValueSelect/MultiValueSelect.tsx | 2 +- .../StandardField/DeleteAllValuesButton.tsx | 31 ++ .../StandardField/StandardField.test.tsx | 256 +--------- .../StandardField/StandardField.tsx | 393 +++++++++++++--- .../StandardFieldNumeric.test.tsx | 139 ------ .../DSBooleanWrapper.test.tsx | 234 ++-------- .../StandardFieldValue/DSBooleanWrapper.tsx | 50 +- .../DSColorPickerWrapper.test.tsx | 289 +++--------- .../DSColorPickerWrapper.tsx | 80 +--- .../DSDatePickerWrapper.test.tsx | 185 ++------ .../DSDatePickerWrapper.tsx | 101 ++-- .../DSInputEncryptedWrapper.test.tsx | 341 +------------- .../DSInputEncryptedWrapper.tsx | 124 +---- .../DSInputNumberWrapper.test.tsx | 436 ++++-------------- .../DSInputNumberWrapper.tsx | 104 ++--- .../DSInputWrapper.test.tsx | 348 ++++---------- .../StandardFieldValue/DSInputWrapper.tsx | 109 ++--- .../DSRangePickerWrapper.test.tsx | 261 ++--------- .../DSRangePickerWrapper.tsx | 152 +++--- .../DSRichTextWrapper.test.tsx | 288 ++++-------- .../StandardFieldValue/DSRichTextWrapper.tsx | 112 ++--- .../StandardFieldValue/StandardFieldValue.tsx | 112 +++-- .../StandardFieldValueDisplayHandler.tsx | 127 ----- .../ColorPickerBlock.tsx | 63 --- .../StandardFieldValueRead.tsx | 196 -------- .../StandardFieldValueDisplayHandler/index.ts | 4 - .../ValuesList/MonoValueSelect.test.tsx | 195 +------- .../ValuesList/MonoValueSelect.tsx | 89 ++-- .../StandardFieldValue/ValuesList/_types.ts | 14 +- .../StandardFieldValue/_types.ts | 13 + .../useGetRecordForm/useGetRecordForm.ts | 2 + libs/ui/src/locales/en/shared.json | 14 +- libs/ui/src/locales/fr/shared.json | 14 +- yarn.lock | 142 +++--- 45 files changed, 1418 insertions(+), 3762 deletions(-) create mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/DeleteAllValuesButton.tsx delete mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldNumeric.test.tsx delete mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueDisplayHandler.tsx delete mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/ColorPickerBlock.tsx delete mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/StandardFieldValueRead.tsx delete mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/index.ts create mode 100644 libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/_types.ts diff --git a/apps/data-studio/package.json b/apps/data-studio/package.json index fee7845b3..71f76c167 100644 --- a/apps/data-studio/package.json +++ b/apps/data-studio/package.json @@ -13,7 +13,7 @@ "antd": "5.15.3", "apollo-cache-inmemory": "1.6.6", "apollo-upload-client": "14.1.3", - "aristid-ds": "10.1.0-b8e3d81", + "aristid-ds": "10.1.0-449b3e9", "dayjs": "1.11.10", "graphql": "15.0.0", "graphql-tag": "2.12.6", diff --git a/apps/login/package.json b/apps/login/package.json index 227cfbc39..fa554159c 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -7,7 +7,7 @@ "@ant-design/icons": "5.2.6", "@leav/ui": "workspace:libs/ui", "antd": "5.15.3", - "aristid-ds": "10.1.0-b8e3d81", + "aristid-ds": "10.1.0-449b3e9", "i18next": "22.5.0", "i18next-browser-languagedetector": "7.0.2", "i18next-http-backend": "2.1.1", diff --git a/apps/portal/package.json b/apps/portal/package.json index a8c2c884d..fd01bf570 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -8,7 +8,7 @@ "@leav/ui": "workspace:libs/ui", "@leav/utils": "workspace:libs/utils", "antd": "5.15.3", - "aristid-ds": "10.1.0-b8e3d81", + "aristid-ds": "10.1.0-449b3e9", "cross-fetch": "3.1.5", "graphql-ws": "5.12.0", "i18next": "22.5.0", diff --git a/libs/ui/package.json b/libs/ui/package.json index 78046c2a1..9c5fb7a42 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -51,7 +51,7 @@ "@ant-design/icons": ">=5.2", "@apollo/client": ">=3.8.1", "antd": "5.15.3", - "aristid-ds": "10.1.0-b8e3d81", + "aristid-ds": "10.1.0-449b3e9", "dayjs": "^1.11.10", "i18next": "22.5", "react": "18.2.0", diff --git a/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx b/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx index f8bf6b543..df7a42c46 100644 --- a/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx @@ -192,7 +192,7 @@ export const EditRecord: FunctionComponent = ({ return savableValue as IValueToSubmit; }), version, - true // deleteEmpty + false // deleteEmpty ); } diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts index 522caa1fb..dee47199d 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/_types.ts @@ -97,20 +97,20 @@ export interface ISubmittedValueBase { metadata?: IKeyValue; } -export interface IFormElementProps { - element: FormElement; +export interface IFormElementProps { + element: FormElement; onValueSubmit?: SubmitValueFunc; onValueDelete?: DeleteValueFunc; onDeleteMultipleValues?: DeleteMultipleValuesFunc; metadataEdit?: boolean; } -export type FormElement = Override< +export type FormElement = Override< RecordFormElementFragment, { settings: SettingsType; uiElementType: FormUIElementTypes | FormFieldTypes; - values: RecordFormElementsValue[]; + values: RecordFormElements[]; } > & { uiElement: (props: IFormElementProps & {antdForm?: FormInstance}) => JSX.Element; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx index 5fa17fb52..8ec23a38d 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.test.tsx @@ -96,6 +96,50 @@ describe('getAntdFormInitialValues', () => { }); }); + describe('Advanced standard field with multiple values', () => { + test('Should initialize antd form with given value', async () => { + const elementFormId = 'elementFormId'; + const yetAnotherElementFormId = 'yetAnotherElementFormId'; + const standardAttributeId = 'standardAttributeId'; + const standardElement = { + attribute: { + type: AttributeType.advanced, + format: AttributeFormat.text, + multiple_values: true, + id: standardAttributeId + }, + values: [{raw_payload: elementFormId}, {raw_payload: yetAnotherElementFormId}] + }; + const recordForm = {elements: [standardElement]}; + + const antdFormInitialValues = getAntdFormInitialValues(recordForm as any); + + expect(antdFormInitialValues).toEqual({ + [standardAttributeId]: [elementFormId, yetAnotherElementFormId] + }); + }); + + test('Should initialize antd form with array containing null for standard field when value is not set', async () => { + const standardAttributeId = 'standardAttributeId'; + const standardElement = { + attribute: { + type: AttributeType.advanced, + format: AttributeFormat.text, + multiple_values: true, + id: standardAttributeId + }, + values: [] + }; + const recordForm = {elements: [standardElement]}; + + const antdFormInitialValues = getAntdFormInitialValues(recordForm as any); + + expect(antdFormInitialValues).toEqual({ + [standardAttributeId]: [null] + }); + }); + }); + describe('AttributeFormat.text', () => { test('Should initialize antd form with given value for text attribute', async () => { const rawValue = 'rawValue'; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx index b3382f0ce..0d4cd79a7 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/antdUtils.tsx @@ -3,6 +3,7 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import { IRecordForm, + RecordFormElementAttribute, RecordFormElementsValue, RecordFormElementsValueLinkValue, RecordFormElementsValueStandardValue @@ -18,24 +19,60 @@ const hasDateRangeValues = (dateRange: unknown): dateRange is IDateRangeValue => const getCalculatedValue = values => values.find(value => value.isCalculated); const getInheritedValue = values => values.find(value => value.isInherited); -const getNotInheritedOrOverrideValue = values => values.find(value => !value.isInherited && value.raw_payload !== null); const getUserInputValue = values => values.find(value => !value.isInherited && !value.isCalculated && value.raw_value !== null); const isRecordFormElementsValueLinkValue = ( value: RecordFormElementsValue, - attribute: IRecordForm['elements'][0]['attribute'] + attribute: RecordFormElementAttribute ): value is RecordFormElementsValueLinkValue => attribute.type === AttributeType.simple_link || (attribute.type === AttributeType.advanced_link && attribute.multiple_values === false); const isRecordFormElementsValueLinkValues = ( values: RecordFormElementsValue[], - attribute: IRecordForm['elements'][0]['attribute'] + attribute: RecordFormElementAttribute ): values is RecordFormElementsValueLinkValue[] => attribute.type === AttributeType.advanced_link && attribute.multiple_values === true; +const isRecordFormElementsMultipleValues = (attribute: RecordFormElementAttribute) => + attribute.type === AttributeType.advanced && attribute.multiple_values === true; + +const formatStandardInitialValue = ( + standardValue: RecordFormElementsValueStandardValue, + attribute: RecordFormElementAttribute +) => { + if (!standardValue?.raw_payload) { + if (attribute.format === AttributeFormat.date_range) { + return undefined; + } + return ''; + } + + switch (attribute.format) { + case AttributeFormat.color: + case AttributeFormat.text: + case AttributeFormat.rich_text: + case AttributeFormat.boolean: + return standardValue.raw_payload; + case AttributeFormat.numeric: + return Number(standardValue.raw_payload); + case AttributeFormat.date: + return dayjs.unix(Number(standardValue.raw_payload)); + case AttributeFormat.date_range: + if (hasDateRangeValues(standardValue.raw_payload)) { + return [ + dayjs.unix(Number(standardValue.raw_payload.from)), + dayjs.unix(Number(standardValue.raw_payload.to)) + ]; + } else if (typeof standardValue.raw_payload === 'string') { + const convertedFieldValue = JSON.parse(standardValue.raw_payload) as any; + return [dayjs.unix(Number(convertedFieldValue.from)), dayjs.unix(Number(convertedFieldValue.to))]; + } + } +}; + export const getAntdFormInitialValues = (recordForm: IRecordForm) => recordForm.elements.reduce((acc, {attribute, values}) => { if (!attribute) { @@ -54,46 +91,19 @@ export const getAntdFormInitialValues = (recordForm: IRecordForm) => return acc; } - const standardValue = value as RecordFormElementsValueStandardValue; - - if (!standardValue?.raw_payload) { - if (attribute.format === AttributeFormat.date_range) { - return acc; - } - - acc[attribute.id] = ''; + if (isRecordFormElementsMultipleValues(attribute)) { + acc[attribute.id] = + values.length === 0 + ? [null] + : values + .sort((a, b) => Number(a.id_value) - Number(b.id_value)) + .map(val => formatStandardInitialValue(val, attribute)); return acc; } - switch (attribute.format) { - case AttributeFormat.color: - case AttributeFormat.text: - case AttributeFormat.rich_text: - case AttributeFormat.boolean: - acc[attribute.id] = standardValue.raw_payload; - break; - case AttributeFormat.numeric: - acc[attribute.id] = Number(standardValue.raw_payload); - break; - case AttributeFormat.date: - acc[attribute.id] = dayjs.unix(Number(standardValue.raw_payload)); - break; - case AttributeFormat.date_range: - if (hasDateRangeValues(standardValue.raw_payload)) { - acc[attribute.id] = [ - dayjs.unix(Number(standardValue.raw_payload.from)), - dayjs.unix(Number(standardValue.raw_payload.to)) - ]; - break; - } else if (typeof standardValue.raw_payload === 'string') { - const convertedFieldValue = JSON.parse(standardValue.raw_payload) as any; - acc[attribute.id] = [ - dayjs.unix(Number(convertedFieldValue.from)), - dayjs.unix(Number(convertedFieldValue.to)) - ]; - break; - } - } + const standardValue = value as RecordFormElementsValueStandardValue; + + acc[attribute.id] = formatStandardInitialValue(standardValue, attribute); return acc; }, {}); 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 584b4ac40..591b83581 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 @@ -626,7 +626,7 @@ describe('standardFieldReducer', () => { element: { values: [ {isInherited: true, value: 'testValue', raw_value: 'testRawValue'}, - {isInherited: false, value: null, raw_value: null} + {isInherited: false, value: null, raw_value: null, payload: null} ], attribute: { format: AttributeFormat.date_range 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 1feaaacee..1c9c34bef 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer.ts @@ -310,7 +310,7 @@ const _computeInheritedFlags = (fieldValues: RecordFormElementsValueStandardValu const isInheritedValue = true; - if (!overrideValue || overrideValue.value === null) { + if (!overrideValue || overrideValue.payload === null) { return { inheritedValue, isInheritedValue, @@ -342,7 +342,7 @@ const _computeCalculatedFlags = (fieldValues: RecordFormElementsValueStandardVal const isCalculatedValue = true; - if (!overrideValue || overrideValue.value === null) { + if (!overrideValue || overrideValue.payload === null) { return { calculatedValue, isCalculatedValue, diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.tsx index 5ba2072c5..3c58a3961 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MonoValueSelect/MonoValueSelect.tsx @@ -104,7 +104,7 @@ export const MonoValueSelect: FunctionComponent = ({ status={errors.length > 0 && 'error'} showSearch optionFilterProp="label" - placeholder={t('record_edition.record_select')} + placeholder={t('record_edition.placeholder.record_select')} onSelect={handleSelect} onChange={onChange} onClear={required ? undefined : handleClear} diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.tsx index ddc6f5e06..c4959b9ab 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/LinkField/MultiValueSelect/MultiValueSelect.tsx @@ -127,7 +127,7 @@ export const MultiValueSelect: FunctionComponent = ({ options={selectOptions} showSearch optionFilterProp="label" - placeholder={t('record_edition.record_select')} + placeholder={t('record_edition.placeholder.record_select')} onSelect={_handleSelect} onClear={_clearValues} onBlur={_handleBlur} diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/DeleteAllValuesButton.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/DeleteAllValuesButton.tsx new file mode 100644 index 000000000..208596f3b --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/DeleteAllValuesButton.tsx @@ -0,0 +1,31 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; +import {KitButton, KitModal} from 'aristid-ds'; +import {FunctionComponent} from 'react'; + +export const DeleteAllValuesButton: FunctionComponent<{handleDelete: () => Promise}> = ({handleDelete}) => { + const {t} = useSharedTranslation(); + + const _confirmDeleteAllValues = () => { + KitModal.confirm({ + title: t('record_edition.delete_all_values'), + content: t('record_edition.delete_all_values_confirm'), + icon: false, + showSecondaryCta: true, + showCloseIcon: false, + dangerConfirm: true, + type: 'confirm', + okText: t('global.confirm'), + cancelText: t('global.cancel'), + onOk: handleDelete + }); + }; + + return ( + + {t('record_edition.delete_all')} + + ); +}; 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 95cc3f3be..646e6e64c 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 @@ -31,6 +31,7 @@ import { import StandardField from './StandardField'; import {AntForm} from 'aristid-ds'; import {getAntdFormInitialValues} from '../../antdUtils'; +import {FormInstance} from 'antd'; jest.mock('../../hooks/useExecuteDeleteValueMutation'); @@ -138,81 +139,6 @@ 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(); @@ -296,184 +222,4 @@ describe('StandardField', () => { expect(screen.getByText('ERROR_MESSAGE')).toBeInTheDocument(); }); - - test('On multiple-values attribute, can delete all values', async () => { - render( - - ); - - const deleteAllButton = screen.getByRole('button', {name: 'delete-all-values'}); - expect(deleteAllButton).toBeInTheDocument(); - - // Click on the parent, because of the issue on Tooltip. See DeleteAllValuesBtn component file - await userEvent.click(deleteAllButton.parentElement); - - const confirmBtn = await screen.findByRole('button', {name: /confirm/}); - await userEvent.click(confirmBtn); - - await waitFor(() => { - expect(baseProps.onDeleteMultipleValues).toBeCalled(); - }); - }); - - describe('Values list', () => { - const mockFormElementWithValuesList: FormElement<{}> = { - ...mockFormElementInput, - attribute: { - ...mockFormElementInput.attribute, - values_list: { - enable: true, - allowFreeEntry: false, - allowListUpdate: false, - values: ['My value', 'Other value'] - } - } - }; - - const mockFormElementWithValuesListOpen = { - ...mockFormElementWithValuesList, - attribute: { - ...mockFormElementWithValuesList.attribute, - values_list: { - ...(mockFormElementWithValuesList.attribute as RecordFormAttributeStandardAttributeFragment) - .values_list, - allowFreeEntry: true - } - } - }; - - test('Display values list', async () => { - render(); - - await userEvent.click(screen.getByRole('textbox')); - - expect(await screen.findByText('My value')).toBeInTheDocument(); - expect(screen.getByText('Other value')).toBeInTheDocument(); - - expect(screen.queryByRole('button', {name: 'global.submit'})).not.toBeInTheDocument(); - }); - - test('Filters list when typing', async () => { - render(); - - const originInputElem = screen.getByRole('textbox'); - await userEvent.click(originInputElem); - - expect(await screen.findByText('My value')).toBeInTheDocument(); - expect(screen.getByText('Other value')).toBeInTheDocument(); - - const editingInputElem = screen.getByRole('textbox'); - await userEvent.type(editingInputElem, 'Other'); - - await waitFor(() => { - expect(screen.queryByText('My value')).not.toBeInTheDocument(); - }); - expect(screen.getByText('Other value')).toBeInTheDocument(); - }); - - test('On click on a value, save it', async () => { - render(); - - const inputElem = screen.getByRole('textbox'); - await userEvent.click(inputElem); - - const elem = await screen.findByText('My value'); - await userEvent.click(elem); - - await waitFor(() => { - expect(mockHandleSubmit).toHaveBeenCalledWith( - [{idValue: null, value: 'My value', attribute: mockFormElementWithValuesList.attribute}], - null - ); - }); - }); - - test('On Enter, first matching value is selected', async () => { - render(); - - const inputElem = screen.getByRole('textbox'); - await userEvent.click(inputElem); - - const editingInputElem = screen.getByRole('textbox'); - await userEvent.type(editingInputElem, '{enter}'); - - await waitFor(() => { - expect(mockHandleSubmit).toHaveBeenCalledWith( - [ - { - idValue: null, - value: 'My value', - attribute: mockFormElementWithValuesList.attribute - } - ], - null - ); - }); - }); - - test('If no match, display a message', async () => { - render(); - - const originInputElem = screen.getByRole('textbox'); - await userEvent.click(originInputElem); - - const editingInputElem = screen.getByRole('textbox'); - await userEvent.type(editingInputElem, 'zzz'); - expect(await screen.findByText('record_edition.no_matching_value')).toBeInTheDocument(); - }); - - test('If open values list, display submit button', async () => { - render(); - - await userEvent.click(screen.getByRole('textbox')); - - expect(screen.getByRole('button', {name: 'global.submit'})).toBeInTheDocument(); - }); - - test('If open values list, can copy a value from the list and edit it', async () => { - render(); - - await userEvent.click(screen.getByRole('textbox')); - - const copyValueBtn = await screen.findAllByRole('button', {name: 'copy'}); - expect(copyValueBtn).toHaveLength(2); - - await userEvent.click(copyValueBtn[0]); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('My value'); - }); - }); - - test('If open values list, current value appears on the list', async () => { - render(); - - await userEvent.click(screen.getByRole('textbox')); - - const editingInputElem = screen.getByRole('textbox'); - await userEvent.type(editingInputElem, 'Some new value'); - - expect(screen.getByText(/Some new value/)).toBeInTheDocument(); - }); - }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.tsx index 5dfe09928..572a63087 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardField.tsx @@ -1,8 +1,8 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {AnyPrimitive, ErrorTypes, ICommonFieldsSettings} from '@leav/utils'; -import {FunctionComponent, useContext, useEffect, useMemo, useReducer} from 'react'; +import {AnyPrimitive, ErrorTypes, ICommonFieldsSettings, localizedTranslation} from '@leav/utils'; +import {FunctionComponent, useContext, useEffect, useMemo, useReducer, useRef, useState} from 'react'; import {useTranslation} from 'react-i18next'; import styled from 'styled-components'; import {ErrorDisplay} from '_ui/components'; @@ -16,35 +16,115 @@ import {useRecordEditionContext} from '../../hooks/useRecordEditionContext'; import standardFieldReducer, { computeInitialState, IdValue, + IStandardFieldValue, newValueId, StandardFieldReducerActionsTypes } from '../../reducers/standardFieldReducer/standardFieldReducer'; -import AddValueBtn from '../../shared/AddValueBtn'; -import DeleteAllValuesBtn from '../../shared/DeleteAllValuesBtn'; import FieldFooter from '../../shared/FieldFooter'; import ValuesVersionBtn from '../../shared/ValuesVersionBtn'; -import {APICallStatus, VersionFieldScope, IFormElementProps} from '../../_types'; +import {APICallStatus, VersionFieldScope, IFormElementProps, ISubmitMultipleResult} from '../../_types'; import StandardFieldValue from './StandardFieldValue'; -import {FormInstance} from 'antd'; +import {Form, FormInstance, FormListOperation} from 'antd'; import {StandardFieldReducerContext} from '../../reducers/standardFieldReducer/standardFieldReducerContext'; +import {KitButton, KitInputWrapper, KitSpace} from 'aristid-ds'; +import {useLang} from '_ui/hooks'; +import {useValueDetailsButton} from '../../shared/ValueDetailsBtn/useValueDetailsButton'; +import {TFunction} from 'i18next'; +import {FaPlus, FaTrash} from 'react-icons/fa'; +import {DeleteAllValuesButton} from './DeleteAllValuesButton'; const Wrapper = styled.div<{$metadataEdit: boolean}>` margin-bottom: ${props => (props.$metadataEdit ? 0 : '1.5em')}; `; -const StandardField: FunctionComponent & {antdForm?: FormInstance}> = ({ - element, - antdForm, - onValueSubmit, - onValueDelete, - onDeleteMultipleValues, - metadataEdit = false -}) => { - const {t} = useTranslation(); +const KitFieldsWrapper = styled.div` + max-height: 322px; + overflow-y: scroll; +`; + +const RowValueWrapper = styled.div` + display: flex; + flex-direction: row; +`; + +const StandardFieldValueWrapper = styled.div` + flex: 1; +`; + +const KitInputWrapperStyled = styled(KitInputWrapper)` + &.bordered > .kit-input-wrapper-content { + padding: calc((var(--general-spacing-xs) - 3) * 1px); + + .kit-input-wrapper-content { + margin: 3px; + } + } +`; + +const KitDeleteValueButton = styled(KitButton)` + margin: 3px; +`; + +const KitAddValueButton = styled(KitButton)` + margin-top: calc((var(--general-spacing-xs) - 3) * 1px); + margin-bottom: 3px; +`; + +const _isDateRangeValue = (value: any): value is {from: string; to: string} => + !!value && typeof value === 'object' && 'from' in value && 'to' in value; + +const _getPresentationValue = ({ + t, + format, + value, + calculatedValue, + inheritedValue +}: { + t: TFunction; + format: AttributeFormat; + value: ValueDetailsValueFragment['payload']; + calculatedValue: RecordFormElementsValueStandardValue; + inheritedValue: RecordFormElementsValueStandardValue; +}): string => { + let presentationValue = value || calculatedValue?.payload || inheritedValue?.payload || ''; + + switch (format) { + case AttributeFormat.date_range: + if (!_isDateRangeValue(presentationValue)) { + presentationValue = ''; + } else { + const {from, to} = presentationValue; + presentationValue = t('record_edition.date_range_value', {from, to}); + } + break; + case AttributeFormat.color: + if (!presentationValue) { + presentationValue = '#00000000'; + } else { + presentationValue = '#' + presentationValue; + } + break; + } + + return presentationValue; +}; +const StandardField: FunctionComponent< + IFormElementProps & { + antdForm?: FormInstance; + } +> = ({element, antdForm, onValueSubmit, onValueDelete, onDeleteMultipleValues, metadataEdit = false}) => { + const {t} = useTranslation(); + const {lang: availableLang} = useLang(); const {readOnly: isRecordReadOnly, record} = useRecordEditionContext(); const {state: editRecordState, dispatch: editRecordDispatch} = useEditRecordReducer(); + const antdListFieldsRef = useRef<{ + add: FormListOperation['add']; + remove: FormListOperation['remove']; + indexes: number[]; + } | null>(null); + const {fetchValues} = useRefreshFieldValues( editRecordState.record?.library?.id, element.attribute?.id, @@ -70,6 +150,18 @@ const StandardField: FunctionComponent const [state, dispatch] = useReducer(standardFieldReducer, initialState); + const [presentationValues, setPresentationValues] = useState( + element.values.map(value => + _getPresentationValue({ + t, + format: attribute.format, + value: value.payload, + calculatedValue: state.calculatedValue, + inheritedValue: state.inheritedValue + }) + ) + ); + useEffect(() => { if (creationErrors[attribute.id]) { // Affect error to each invalid value to display it on form @@ -85,7 +177,23 @@ const StandardField: FunctionComponent } }, [creationErrors, attribute.id]); - const _handleSubmit = async (idValue: IdValue, valueToSave: AnyPrimitive) => { + const setAntdErrorField = (error: null | string, fieldName?: number) => { + const shouldSpecifyFieldName = attribute.multiple_values && fieldName !== undefined; + const name = shouldSpecifyFieldName ? [attribute.id, fieldName] : attribute.id; + + antdForm.setFields([ + { + name, + errors: error ? [error] : null + } + ]); + }; + + const _handleSubmit = async ( + idValue: IdValue, + valueToSave: AnyPrimitive, + fieldName?: number + ): Promise => { const isSavingNewValue = idValue === newValueId; dispatch({ type: StandardFieldReducerActionsTypes.CLEAR_ERROR, @@ -99,32 +207,63 @@ const StandardField: FunctionComponent ); if (submitRes.status === APICallStatus.SUCCESS) { + if (antdForm) { + setAntdErrorField(null, fieldName); + } + const submitResValue = submitRes.values[0] as ValueDetailsValueFragment; let resultValue: ValueDetailsValueFragment; + if (state.metadataEdit) { - const metadataValue = - (submitResValue.metadata ?? []).find(({name}) => name === element.attribute.id)?.value ?? null; resultValue = { - id_value: null, + id_value: submitResValue.id_value, created_at: null, modified_at: null, created_by: null, modified_by: null, version: null, - raw_payload: metadataValue.raw_payload ?? metadataValue.payload, - payload: metadataValue.payload, + raw_payload: '', + payload: '', metadata: null, attribute }; + + if (state.metadataEdit) { + const metadataValue = + (submitResValue.metadata ?? []).find(({name}) => name === element.attribute.id)?.value ?? null; + + resultValue.raw_payload = metadataValue.raw_payload ?? metadataValue.payload; + resultValue.payload = metadataValue.payload; + } } else { resultValue = submitResValue; } - dispatch({ - type: StandardFieldReducerActionsTypes.UPDATE_AFTER_SUBMIT, - newValue: resultValue, - idValue + if (valueToSave === null) { + dispatch({ + type: StandardFieldReducerActionsTypes.UPDATE_AFTER_DELETE, + idValue + }); + } else { + dispatch({ + type: StandardFieldReducerActionsTypes.UPDATE_AFTER_SUBMIT, + newValue: resultValue, + idValue + }); + } + + const index = fieldName ?? 0; + setPresentationValues(previousPresentationValues => { + const nextPresentationValues = [...previousPresentationValues]; + nextPresentationValues[index] = _getPresentationValue({ + t, + format: attribute.format, + value: resultValue.payload, + calculatedValue: state.calculatedValue, + inheritedValue: state.inheritedValue + }); + return nextPresentationValues; }); const newActiveValue = state.metadataEdit @@ -152,7 +291,7 @@ const StandardField: FunctionComponent value: newActiveValue }); - return; + return submitRes; } let errorMessage = submitRes.error; @@ -166,12 +305,7 @@ const StandardField: FunctionComponent : t(`errors.${attributeError.type}`); if (antdForm) { - antdForm.setFields([ - { - name: attributeError.attribute, - errors: [errorMessage] - } - ]); + setAntdErrorField(errorMessage, fieldName); } } } @@ -181,6 +315,8 @@ const StandardField: FunctionComponent idValue, error: errorMessage }); + + return submitRes; }; const _handleDelete = async (idValue: IdValue) => { @@ -217,17 +353,28 @@ const StandardField: FunctionComponent }); }; - const _handleAddValue = () => { - editRecordDispatch({ - type: EditRecordReducerActionsTypes.SET_ACTIVE_VALUE, - value: { - attribute, - value: null - } + const _handleAddValue = async (antdAdd: FormListOperation['add']) => { + dispatch({ + type: StandardFieldReducerActionsTypes.ADD_VALUE }); + antdAdd(); + }; + const _handleDeleteValue = async ( + field: IStandardFieldValue, + antdRemove: FormListOperation['remove'], + deletedFieldIndex: number + ) => { + if (field.idValue !== newValueId) { + await onValueDelete({id_value: field.idValue}, attribute.id); + } + antdRemove(deletedFieldIndex); + setPresentationValues(previousPresentationValues => + previousPresentationValues.filter((_, index) => index !== deletedFieldIndex) + ); dispatch({ - type: StandardFieldReducerActionsTypes.ADD_VALUE + type: StandardFieldReducerActionsTypes.UPDATE_AFTER_DELETE, + idValue: field.idValue }); }; @@ -246,6 +393,9 @@ const StandardField: FunctionComponent ); if (deleteRes.status === APICallStatus.SUCCESS) { + antdListFieldsRef.current.remove(antdListFieldsRef.current.indexes); + antdListFieldsRef.current.add(); + setPresentationValues(['']); dispatch({ type: StandardFieldReducerActionsTypes.UPDATE_AFTER_DELETE, allDeleted: true @@ -280,7 +430,10 @@ const StandardField: FunctionComponent ); const hasValue = valuesToDisplay[0].idValue !== newValueId && valuesToDisplay[0].idValue !== null; const canAddAnotherValue = - !state.isReadOnly && isMultipleValues && hasValue && attribute.format !== AttributeFormat.boolean; + !state.isReadOnly && + isMultipleValues && + attribute.format !== AttributeFormat.boolean && + attribute.format !== AttributeFormat.encrypted; const canDeleteAllValues = !state.isReadOnly && hasValue && valuesToDisplay.length > 1; const isAttributeVersionable = attribute?.versions_conf?.versionable; @@ -289,28 +442,150 @@ const StandardField: FunctionComponent [VersionFieldScope.INHERITED]: state.values[VersionFieldScope.INHERITED]?.version ?? null }; + const label = localizedTranslation(state.formElement.settings.label, availableLang); + + const {onValueDetailsButtonClick} = useValueDetailsButton({ + value: valuesToDisplay[0]?.value, + attribute: state.attribute + }); + + const shouldShowValueDetailsButton = editRecordState.withInfoButton; + + const _getFormattedValueForHelper = (valueToFormat: RecordFormElementsValueStandardValue) => { + switch (state.attribute.format) { + case AttributeFormat.date_range: + return t('record_edition.date_range_from_to', { + from: valueToFormat.payload.from, + to: valueToFormat.payload.to + }); + case AttributeFormat.encrypted: + return valueToFormat.payload ? '●●●●●●●' : ''; + case AttributeFormat.color: + return '#' + valueToFormat.payload; + default: + return valueToFormat.payload; + } + }; + + const _getHelper = () => { + if (attribute.multiple_values) { + return; + } + + if (state.isInheritedOverrideValue) { + return t('record_edition.inherited_input_helper', { + inheritedValue: _getFormattedValueForHelper(state.inheritedValue) + }); + } + + if (state.isCalculatedOverrideValue) { + return t('record_edition.calculated_input_helper', { + calculatedValue: _getFormattedValueForHelper(state.calculatedValue) + }); + } + + return; + }; + + let isFieldInError = false; + if (antdForm) { + const hasErrorsInFormList = valuesToDisplay.some((_, index) => { + const errors = antdForm.getFieldError([attribute.id, index]); + return errors.length > 0; + }); + + isFieldInError = antdForm.getFieldError(attribute.id).length > 0 || hasErrorsInFormList; + } + return ( - {valuesToDisplay.map(value => ( - - ))} - {(canDeleteAllValues || canAddAnotherValue || attribute?.versions_conf?.versionable) && ( + ] + : undefined + } + > + {!attribute.multiple_values && ( + + )} + {attribute.multiple_values && ( + + {(fields, {add, remove}) => { + antdListFieldsRef.current = {add, remove, indexes: fields.map((_, index) => index)}; + + return ( + <> + + {fields.map((field, index) => ( + + + + + {fields.length > 1 && ( + } + onClick={() => + _handleDeleteValue( + valuesToDisplay[index], + remove, + index + ) + } + /> + )} + + ))} + + {canAddAnotherValue && ( + } + onClick={() => _handleAddValue(add)} + disabled={valuesToDisplay.some(value => value.idValue === newValueId)} + > + {t('record_edition.add_value')} + + )} + + ); + }} + + )} + + {attribute?.versions_conf?.versionable && (
@@ -322,11 +597,7 @@ const StandardField: FunctionComponent onScopeChange={_handleScopeChange} /> )} - {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 deleted file mode 100644 index f63f6ae3c..000000000 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldNumeric.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 -// 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 userEvent from '@testing-library/user-event'; -import StandardField from '../StandardField'; -import {mockModifier} from '_ui/__mocks__/common/value'; -import {AttributeFormat, AttributeType, ValueDetailsValueFragment} from '_ui/_gqlTypes'; -import {IRecordPropertyAttribute} from '_ui/_queries/records/getRecordPropertiesQuery'; -import {mockFormElementContainer, mockFormElementInput} from '_ui/__mocks__/common/form'; -import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; -import { - APICallStatus, - DeleteMultipleValuesFunc, - DeleteValueFunc, - ISubmitMultipleResult, - SubmitValueFunc -} from '../../_types'; -import {AntForm} from 'aristid-ds'; -import {getAntdFormInitialValues} from '../../antdUtils'; -import {IRecordForm} from '_ui/hooks/useGetRecordForm'; - -describe('StandardField, Numeric 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 - }; - - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - - test('Render numeric input', async () => { - const recordValuesNumeric = [ - { - ...mockRecordValuesCommon, - payload: '42,00 €', - raw_payload: '42' - } - ]; - - const recordForm: IRecordForm = { - dependencyAttributes: [], - elements: [ - { - ...mockFormElementContainer, - settings: [{key: 'content', value: ''}] - }, - { - ...mockFormElementInput, - settings: [ - {key: 'label', value: 'test attribute'}, - {key: 'attribute', value: 'test_attribute'} - ], - attribute: {...mockFormAttribute, format: AttributeFormat.numeric}, - values: recordValuesNumeric - } - ], - id: 'edition', - recordId: 'recordId', - library: {id: 'libraryId'} - }; - - const antdFormInitialValues = getAntdFormInitialValues(recordForm); - - render( - - - - - - ); - - const formattedValueElem = screen.getByText(recordValuesNumeric[0].payload); - expect(formattedValueElem).toBeVisible(); - - expect(screen.queryByRole('spinbutton')).toBeNull(); - - await userEvent.click(formattedValueElem); - - const inputElem = screen.getByRole('spinbutton'); - expect(inputElem).toBeVisible(); - expect(inputElem).toHaveValue(recordValuesNumeric[0].raw_payload); - - expect(screen.getByText(recordValuesNumeric[0].payload)).not.toBeVisible(); - }); -}); 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 210d589f2..5bfcf35fa 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 @@ -54,13 +54,6 @@ const inheritedValues = [ } ]; -const inheritedNotOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: false, - isInheritedNotOverrideValue: true, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - const inheritedOverrideValue: InheritedFlags = { isInheritedValue: true, isInheritedOverrideValue: true, @@ -81,13 +74,6 @@ const calculatedValues = [ } ]; -const calculatedNotOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: false, - isCalculatedNotOverrideValue: true, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - const calculatedOverrideValue: CalculatedFlags = { isCalculatedValue: true, isCalculatedOverrideValue: true, @@ -142,32 +128,6 @@ describe('DSBooleanWrapper', () => { mockOnChange.mockReset(); }); - test('Should display boolean with fr label', async () => { - const state = getInitialState({required: false, fallbackLang: false}); - render( - - - - - - ); - - expect(screen.getByText(fr_label)).toBeVisible(); - }); - - test('Should display boolean with fallback label', async () => { - const state = getInitialState({required: false, fallbackLang: true}); - render( - - - - - - ); - - expect(screen.getByText(en_label)).toBeVisible(); - }); - test('Should display boolean value as yes', async () => { const state = getInitialState({required: false, fallbackLang: false}); @@ -250,163 +210,57 @@ describe('DSBooleanWrapper', () => { expect(mockHandleSubmit).toHaveBeenCalledWith('false', state.attribute.id); }); - describe('With inheritance', () => { - test('Should display inherited value', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...inheritedNotOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); - - expect(screen.getByText(/yes/)).toBeVisible(); - }); - - test('Should display override value, clear icon and inherited value under it', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); + test('Should allow to clear inherited override value', async () => { + let state = getInitialState({required: false, fallbackLang: false}); + state = { + ...state, + ...inheritedOverrideValue, + formElement: {...state.formElement, values: inheritedValues} + }; - expect(screen.getByText(/no/)).toBeVisible(); - - const helperText = screen.getByText(/inherited_input_helper/); - expect(helperText).toBeInTheDocument(); - }); - - test('Should allow to clear override value', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); + render( + + + + + + ); - const clearButton = screen.getByRole('button'); - await user.click(clearButton); + const clearButton = screen.getByRole('button'); + await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - }); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); }); - describe('With calculus', () => { - test('Should display calculated value', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...calculatedNotOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - expect(screen.getByText(/yes/)).toBeVisible(); - }); + test('Should allow to clear calculated override value', async () => { + let state = getInitialState({required: false, fallbackLang: false}); + state = { + ...state, + ...calculatedOverrideValue, + formElement: {...state.formElement, values: calculatedValues} + }; - test('Should display override value, clear icon and calculated value under it', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - expect(screen.getByText(/no/)).toBeVisible(); - - const helperText = screen.getByText(/calculated_input_helper/); - expect(helperText).toBeInTheDocument(); - }); - - test('Should allow to clear override value', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); + render( + + + + + + ); - const clearButton = screen.getByRole('button'); - await user.click(clearButton); + const clearButton = screen.getByRole('button'); + await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - }); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.tsx index 46edb3c6f..e9253bd0a 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSBooleanWrapper.tsx @@ -1,23 +1,14 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {KitInputWrapper, KitSwitch, KitTypography} from 'aristid-ds'; +import {KitSwitch, KitTypography} from 'aristid-ds'; import {FunctionComponent, MouseEvent} from 'react'; -import {IStandardFieldReducerState} from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form} from 'antd'; -import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; import styled from 'styled-components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {useLang} from '_ui/hooks'; -import {localizedTranslation} from '@leav/utils'; import {IKitSwitch} from 'aristid-ds/dist/Kit/DataEntry/Switch/types'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faCircleXmark} from '@fortawesome/free-solid-svg-icons'; - -interface IDSBooleanWrapperProps extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - handleSubmit: (value: string, id?: string) => void; -} +import {IStandFieldValueContentProps} from './_types'; const KitTypographyTextStyled = styled(KitTypography.Text)<{$shouldHighlightColor: boolean}>` vertical-align: middle; @@ -34,14 +25,17 @@ const FontAwesomeIconStyled = styled(FontAwesomeIcon)` const _getBooleanValueAsStringForTranslation = (value: boolean): string => (value ? 'global.yes' : 'global.no'); -export const DSBooleanWrapper: FunctionComponent = ({value, onChange, state, handleSubmit}) => { +export const DSBooleanWrapper: FunctionComponent> = ({ + value, + onChange, + state, + handleSubmit +}) => { if (!onChange) { throw Error('DSBooleanWrapper should be used inside a antd Form.Item'); } const {t} = useSharedTranslation(); - const {errors} = Form.Item.useStatus(); - const {lang: availableLang} = useLang(); const _resetToInheritedOrCalculatedValue = () => { if (state.isInheritedValue) { @@ -49,36 +43,16 @@ export const DSBooleanWrapper: FunctionComponent = ({val } else if (state.isCalculatedValue) { onChange(state.calculatedValue.raw_value, undefined); } - handleSubmit('', state.attribute.id); + handleSubmit(null, state.attribute.id); }; const _handleOnChange: (checked: boolean, event: MouseEvent) => void = (checked, event) => { - handleSubmit(String(checked), state.attribute.id); onChange(checked, event); + handleSubmit(String(checked), state.attribute.id); }; - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: t(_getBooleanValueAsStringForTranslation(state.inheritedValue.raw_value)) - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: t(_getBooleanValueAsStringForTranslation(state.calculatedValue.raw_value)) - }); - } - return undefined; - }; - - const label = localizedTranslation(state.formElement.settings.label, availableLang); - return ( - 0 ? 'error' : undefined} - disabled={state.isReadOnly} - > + <> + ); }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.test.tsx index f7632e6c1..31248098e 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.test.tsx @@ -1,7 +1,7 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {fireEvent, render, screen, waitFor, within} from '_ui/_tests/testUtils'; +import {render, screen} from '_ui/_tests/testUtils'; import {DSColorPickerWrapper} from './DSColorPickerWrapper'; import {VersionFieldScope} from '../../../_types'; import { @@ -148,28 +148,8 @@ describe('DSColorPickerWrapper', () => { mockHandleBlur.mockReset(); }); - test('Should display colorPicker with fr label', async () => { - const state = getInitialState({required: false, fallbackLang: false}); - - render( - - - - - - ); - - expect(screen.getByText(fr_label)).toBeVisible(); - }); - - test('Should display colorPicker with fallback label', async () => { + test('Should display the presentationValue value', async () => { + const presentationValue = pinkColor; const state = getInitialState({required: false, fallbackLang: true}); render( @@ -177,17 +157,17 @@ describe('DSColorPickerWrapper', () => { ); - expect(screen.getByText(en_label)).toBeVisible(); + expect(screen.getByText(presentationValue)).toBeVisible(); }); test('Should not submit if field has not changed', async () => { @@ -197,10 +177,9 @@ describe('DSColorPickerWrapper', () => { @@ -215,7 +194,7 @@ describe('DSColorPickerWrapper', () => { expect(mockHandleSubmit).not.toHaveBeenCalled(); }); - describe('With required colorPicker and no inheritance', () => { + describe('With no inheritance', () => { test('Should submit the value if field is not empty', async () => { const state = getInitialState({required: false, fallbackLang: true}); render( @@ -223,10 +202,9 @@ describe('DSColorPickerWrapper', () => { @@ -247,210 +225,73 @@ describe('DSColorPickerWrapper', () => { }); }); - describe('With inheritance', () => { - test("Should display the inherited value by default and not save if we don't change it", async () => { - let state = getInitialState({required: false, fallbackLang: true}); + test("Should allow to clear input when it's inherited and override", async () => { + const presentationValue = '#000000'; + let state = getInitialState({required: false, fallbackLang: true}); - state = { - ...state, - ...inheritedNotOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; + state = { + ...state, + ...inheritedOverrideValue, + formElement: {...state.formElement, values: inheritedValues} + }; - render( - - - - - - ); - - expect(screen.getByText('#' + inheritedValues[1].raw_value)).toBeVisible(); - - const colorPicker = screen.getByLabelText(en_label); - await user.click(colorPicker); - await user.click(document.body); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - expect(mockOnChange).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and inherited value under it', async () => { - let state = getInitialState({required: false, fallbackLang: true}); - - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); - - const inputText = screen.getByText('#' + inheritedValues[0].raw_value); - expect(inputText).toBeVisible(); - - const helperText = screen.getByText(new RegExp(inheritedValues[1].raw_value, 'i')); - expect(helperText).toBeInTheDocument(); - }); - - test("Should allow to clear input when it's override", async () => { - let state = getInitialState({required: false, fallbackLang: true}); - - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); + render( + + + + + + ); - const colorPicker = screen.getByLabelText(en_label); - await user.click(colorPicker); + const colorPicker = screen.getByLabelText(en_label); + await user.click(colorPicker); - const clearButton = screen.getByLabelText('clear'); - await user.click(clearButton); + const clearButton = screen.getByLabelText('clear'); + await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - expect(screen.queryByText('#00000000')).toBeVisible(); - }); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); + expect(screen.getByText(presentationValue)).toBeVisible(); }); - describe('With calculation', () => { - test("Should display the calculated value by default and not save if we don't change it", async () => { - let state = getInitialState({required: false, fallbackLang: true}); + test("Should allow to clear input when it's calculated and override", async () => { + const presentationValue = '#000000'; + let state = getInitialState({required: false, fallbackLang: true}); - state = { - ...state, - ...calculatedNotOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; + state = { + ...state, + ...calculatedOverrideValue, + formElement: {...state.formElement, values: calculatedValues} + }; - render( - - - - - - ); - - expect(screen.getByText('#' + calculatedValues[1].raw_value)).toBeVisible(); - - const colorPicker = screen.getByLabelText(en_label); - await user.click(colorPicker); - await user.click(document.body); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - expect(mockOnChange).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and calculated value under it', async () => { - let state = getInitialState({required: false, fallbackLang: true}); - - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - const inputText = screen.getByText('#' + calculatedValues[0].raw_value); - expect(inputText).toBeVisible(); - - const helperText = screen.getByText(new RegExp(calculatedValues[1].raw_value, 'i')); - expect(helperText).toBeInTheDocument(); - }); - - test("Should allow to clear input when it's override", async () => { - let state = getInitialState({required: false, fallbackLang: true}); - - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); + render( + + + + + + ); - const colorPicker = screen.getByLabelText(en_label); - await user.click(colorPicker); + const colorPicker = screen.getByLabelText(en_label); + await user.click(colorPicker); - const clearButton = screen.getByLabelText('clear'); - await user.click(clearButton); + const clearButton = screen.getByLabelText('clear'); + await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - }); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); + expect(screen.getByText(presentationValue)).toBeVisible(); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.tsx index 7c514501a..3c14a2bad 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSColorPickerWrapper.tsx @@ -2,30 +2,17 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {KitColorPicker} from 'aristid-ds'; -import {FunctionComponent, useEffect, useRef, useState} from 'react'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; +import {FunctionComponent, useState} from 'react'; import styled from 'styled-components'; -import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; import {useLang} from '_ui/hooks'; import {localizedTranslation} from '@leav/utils'; -import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; -import {useValueDetailsButton} from '../../../shared/ValueDetailsBtn/useValueDetailsButton'; -import {KitColor, KitColorPickerProps, KitColorPickerRef} from 'aristid-ds/dist/Kit/DataEntry/ColorPicker/types'; - -interface IDSColorPickerWrapperProps extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: string, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; -} +import {KitColor, KitColorPickerProps} from 'aristid-ds/dist/Kit/DataEntry/ColorPicker/types'; +import {IStandFieldValueContentProps} from './_types'; const KitColorPickerStyled = styled(KitColorPicker)<{$shouldHighlightColor: boolean}>` + width: 100%; + justify-content: start; //TODO: Remove when the component is fixed in DS + .ant-color-picker-trigger-text { color: ${({$shouldHighlightColor}) => $shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'var(--general-utilities-text-primary)'}; @@ -36,46 +23,36 @@ const KitColorPickerStyled = styled(KitColorPicker)<{$shouldHighlightColor: bool } `; -export const DSColorPickerWrapper: FunctionComponent = ({ +export const DSColorPickerWrapper: FunctionComponent> = ({ value, + presentationValue, + onChange, state, attribute, - fieldValue, - onChange, - handleSubmit, - handleBlur, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('DSColorPickerWrapper should be used inside a antd Form.Item'); } - const {t} = useSharedTranslation(); const {lang: availableLang} = useLang(); const [hasChanged, setHasChanged] = useState(false); + const [isFocused, setIsFocused] = useState(false); const [currentColor, setCurrentColor] = useState(); const [currentHex, setCurrentHex] = useState((value as string) ?? ''); - const colorPickerRef = useRef(null); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); - useEffect(() => { - if (fieldValue.isEditing) { - colorPickerRef.current.focus(); - } - }, [fieldValue.isEditing]); - - const _handleOnOpenChange = (open: boolean) => { + const _handleOnOpenChange = async (open: boolean) => { if (!open) { if (!hasChanged) { - handleBlur(); + setIsFocused(false); return; } - handleSubmit(currentColor.toHex(), state.attribute.id); onChange(currentColor, currentHex); + setIsFocused(false); + await handleSubmit(currentColor.toHex(), state.attribute.id); + } else { + setIsFocused(true); } }; @@ -89,7 +66,7 @@ export const DSColorPickerWrapper: FunctionComponent onChange(color, hex); }; - const _handleOnClear = () => { + const _handleOnClear = async () => { setHasChanged(false); if (state.isInheritedValue) { @@ -98,33 +75,20 @@ export const DSColorPickerWrapper: FunctionComponent onChange(undefined, state.calculatedValue.raw_value); } - onChange(null, ''); - handleSubmit('', state.attribute.id); - }; - - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', {inheritedValue: state.inheritedValue.raw_value}); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', {calculatedValue: state.calculatedValue.raw_value}); - } - return; + onChange(null, null); + setIsFocused(false); + await handleSubmit(null, state.attribute.id); }; const label = localizedTranslation(state.formElement.settings.label, availableLang); return ( `${presentationValue}`} aria-label={label} - label={label} - helper={_getHelper()} - required={state.formElement.settings.required} - onInfoClick={shouldShowValueDetailsButton ? onValueDetailsButtonClick : null} disabled={state.isReadOnly} disabledAlpha - showText allowClear={!state.isInheritedNotOverrideValue && !state.isCalculatedNotOverrideValue} onOpenChange={_handleOnOpenChange} onChange={_handleOnChange} 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 4c7090df7..123db7746 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 @@ -90,47 +90,30 @@ describe('DSDatePickerWrapper', () => { mockHandleBlur.mockReset(); }); - describe('Without required field', () => { - test('Should display date picker with fr label ', async () => { - const state = getInitialState({required: false, fallbackLang: false}); - render( -
- - - -
- ); - - expect(screen.getByText(fr_label)).toBeVisible(); - }); - - test('Should display date picker with fallback label ', async () => { - const state = getInitialState({required: false, fallbackLang: true}); - render( -
- - - -
- ); - - expect(screen.getByText(en_label)).toBeVisible(); - }); + test('Should display presentationValue By default', async () => { + const state = getInitialState({required: false, fallbackLang: false}); + const value = dayjs(); + const presentationValue = '12 octobre 2034'; + + render( +
+ + + +
+ ); + + expect(screen.getByRole('textbox')).toHaveValue(presentationValue); + }); + describe('Without required field', () => { test('Should call onChange / handle submit with value at noon', async () => { const state = getInitialState({required: false, fallbackLang: false}); render( @@ -138,11 +121,10 @@ describe('DSDatePickerWrapper', () => { @@ -172,11 +154,10 @@ describe('DSDatePickerWrapper', () => { @@ -205,11 +186,10 @@ describe('DSDatePickerWrapper', () => { @@ -239,11 +219,10 @@ describe('DSDatePickerWrapper', () => { @@ -265,51 +244,7 @@ describe('DSDatePickerWrapper', () => { }); describe('Inherited values', () => { - test('Should not display helper without inherited value', async () => { - const state = getInitialState({required: false, fallbackLang: false}); - state.inheritedValue = null; - state.isInheritedOverrideValue = false; - render( -
- - - -
- ); - - expect(screen.queryByText('record_edition.inherited_input_helper', {exact: false})).not.toBeInTheDocument(); - }); - - test('Should display helper with inherited value', async () => { - const state = getInitialState({required: false, fallbackLang: false}); - state.inheritedValue = mockValue.value; - state.isInheritedOverrideValue = true; - render( -
- - - -
- ); - - expect(screen.getByText('record_edition.inherited_input_helper', {exact: false})).toBeVisible(); - }); - - test('Should call onChange/handleSubmit with inherited value on clear', async () => { + test('Should call onChange/handleSubmit with empty value on clear', async () => { const raw_value = '1714138054'; const state = getInitialState({required: false, fallbackLang: false}); state.inheritedValue = {...mockValue.value, raw_value}; @@ -325,11 +260,10 @@ describe('DSDatePickerWrapper', () => { @@ -341,7 +275,7 @@ describe('DSDatePickerWrapper', () => { expect(mockOnChange).toHaveBeenCalledTimes(1); expect(mockOnChange).toHaveBeenCalledWith(expect.any(Object), raw_value); expect(mockHandleSubmit).toHaveBeenCalledTimes(1); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); }); test('Should hide clear icon when value is inherited, but not override', async () => { @@ -360,11 +294,10 @@ describe('DSDatePickerWrapper', () => { @@ -374,53 +307,7 @@ describe('DSDatePickerWrapper', () => { }); }); describe('Calculated values', () => { - test('Should not display helper without calculated value', async () => { - const state = getInitialState({required: false, fallbackLang: false}); - state.calculatedValue = null; - state.isCalculatedOverrideValue = false; - render( -
- - - -
- ); - - expect( - screen.queryByText('record_edition.calculated_input_helper', {exact: false}) - ).not.toBeInTheDocument(); - }); - - test('Should display helper with calculated value', async () => { - const state = getInitialState({required: false, fallbackLang: false}); - state.calculatedValue = mockValue.value; - state.isCalculatedOverrideValue = true; - render( -
- - - -
- ); - - expect(screen.getByText('record_edition.calculated_input_helper', {exact: false})).toBeVisible(); - }); - - test('Should call onChange/handleSubmit with calculated value on clear', async () => { + test('Should call onChange/handleSubmit with empty value on clear', async () => { const raw_value = '1714138054'; const state = getInitialState({required: false, fallbackLang: false}); state.calculatedValue = {...mockValue.value, raw_value}; @@ -436,11 +323,10 @@ describe('DSDatePickerWrapper', () => { @@ -452,7 +338,7 @@ describe('DSDatePickerWrapper', () => { expect(mockOnChange).toHaveBeenCalledTimes(1); expect(mockOnChange).toHaveBeenCalledWith(expect.any(Object), raw_value); expect(mockHandleSubmit).toHaveBeenCalledTimes(1); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); }); test('Should hide clear icon when value is calculated, but not override', async () => { @@ -471,11 +357,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 8495e0f15..545fe999a 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,79 +2,56 @@ // 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, useEffect, useRef} from 'react'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, type GetRef} from 'antd'; +import {FunctionComponent, useState} from 'react'; +import {Form} from 'antd'; import dayjs from 'dayjs'; import styled from 'styled-components'; -import {IProvidedByAntFormItem, StandardValueTypes} from '../../../_types'; -import {DatePickerProps} from 'antd/lib/date-picker'; -import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -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'; import {setDateToUTCNoon} from '_ui/_utils'; -interface IDSDatePickerWrapperProps extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: StandardValueTypes, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; -} +import {IStandFieldValueContentProps} from './_types'; +import {IKitDatePicker} from 'aristid-ds/dist/Kit/DataEntry/DatePicker/types'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; const KitDatePickerStyled = styled(KitDatePicker)<{$shouldHighlightColor: boolean}>` + width: 100%; color: ${({$shouldHighlightColor}) => ($shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial')}; `; -export const DSDatePickerWrapper: FunctionComponent = ({ +export const DSDatePickerWrapper: FunctionComponent> = ({ value, + presentationValue, onChange, - handleBlur, state, attribute, - fieldValue, - handleSubmit, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('DSDatePickerWrapper should be used inside a antd Form.Item'); } - const {t} = useSharedTranslation(); - const {lang: availableLangs} = useLang(); + const [isFocused, setIsFocused] = useState(false); const {errors} = Form.Item.useStatus(); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); - - const inputRef = useRef>(null); + const {t} = useSharedTranslation(); - useEffect(() => { - if (fieldValue.isEditing && inputRef.current) { - inputRef.current.nativeElement.click(); // To automatically open the date picker - } - }, [fieldValue.isEditing]); + const isErrors = errors.length > 0; - const _resetToInheritedOrCalculatedValue = () => { + const _resetToInheritedOrCalculatedValue = async () => { if (state.isInheritedValue) { onChange(dayjs.unix(Number(state.inheritedValue.raw_value)), state.inheritedValue.raw_value); } else if (state.isCalculatedValue) { onChange(dayjs.unix(Number(state.calculatedValue.raw_value)), state.calculatedValue.raw_value); } - handleSubmit('', state.attribute.id); + await handleSubmit(null, state.attribute.id); }; - const _handleDateChange: (datePickerDate: dayjs.Dayjs | null, antOnChangeParams: string | string[]) => void = ( - datePickerDate, - ...antOnChangeParams - ) => { + const _handleFocus = () => setIsFocused(true); + + const _handleBlur = () => setIsFocused(false); + + const _handleDateChange: ( + datePickerDate: dayjs.Dayjs | null, + antOnChangeParams: string | string[] + ) => void = async (datePickerDate, ...antOnChangeParams) => { if ((state.isInheritedValue || state.isCalculatedValue) && datePickerDate === null) { _resetToInheritedOrCalculatedValue(); return; @@ -91,43 +68,27 @@ export const DSDatePickerWrapper: FunctionComponent = return; } - let dateToSave = null; + let dateToSave = ''; if (!!datePickerDate) { dateToSave = String(datePickerDate.unix()); } - handleSubmit(dateToSave, state.attribute.id); - }; - - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: state.inheritedValue.value - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: state.calculatedValue.value - }); - } - return; + await handleSubmit(dateToSave, state.attribute.id); }; - const label = localizedTranslation(state.formElement.settings.label, availableLangs); - return ( presentationValue} disabled={state.isReadOnly} allowClear={!state.isInheritedNotOverrideValue && !state.isCalculatedNotOverrideValue} - status={errors.length > 0 ? 'error' : undefined} - onInfoClick={shouldShowValueDetailsButton ? onValueDetailsButtonClick : null} - helper={_getHelper()} - onBlur={handleBlur} + helper={isErrors ? String(errors[0]) : undefined} + status={isErrors ? 'error' : undefined} + onChange={_handleDateChange} + onFocus={_handleFocus} + onBlur={_handleBlur} $shouldHighlightColor={state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue} + placeholder={t('record_edition.placeholder.enter_a_date')} /> ); }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.test.tsx index 0192cf378..6a90fa861 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.test.tsx @@ -5,8 +5,6 @@ import {render, screen} from '_ui/_tests/testUtils'; import {DSInputEncryptedWrapper} from './DSInputEncryptedWrapper'; import {VersionFieldScope} from '../../../_types'; import { - CalculatedFlags, - InheritedFlags, IStandardFieldReducerState, StandardFieldValueState } from '../../../reducers/standardFieldReducer/standardFieldReducer'; @@ -16,10 +14,7 @@ import {mockFormAttribute} from '_ui/__mocks__/common/attribute'; import userEvent from '@testing-library/user-event'; import {AntForm} from 'aristid-ds'; import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; -import {RecordFormElementsValueStandardValue} from '_ui/hooks/useGetRecordForm'; -const en_label = 'label'; -const fr_label = 'libellé'; const idValue = '123'; const mockValue = { index: 0, @@ -43,18 +38,12 @@ const mockValue = { state: StandardFieldValueState.PRISTINE }; -const getInitialState = ({ - required, - fallbackLang -}: { - required: boolean; - fallbackLang: boolean; -}): IStandardFieldReducerState => ({ +const getInitialState = ({required}: {required: boolean}): IStandardFieldReducerState => ({ record: mockRecord, formElement: { ...mockFormElementInput, settings: { - label: fallbackLang ? {en: en_label} : {fr: fr_label, en: en_label}, + label: {}, required } }, @@ -79,64 +68,9 @@ const getInitialState = ({ isCalculatedValue: false }); -const inheritedValues: RecordFormElementsValueStandardValue[] = [ - { - isInherited: null, - value: 'override value', - raw_value: 'override value' - }, - { - isInherited: true, - value: 'inherited value', - raw_value: 'inherited value' - } -]; - -const inheritedNotOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: false, - isInheritedNotOverrideValue: true, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const inheritedOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: true, - isInheritedNotOverrideValue: false, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const calculatedValues: RecordFormElementsValueStandardValue[] = [ - { - isCalculated: null, - value: 'override value', - raw_value: 'override value' - }, - { - isCalculated: true, - value: 'calculated value', - raw_value: 'calculated value' - } -]; - -const calculatedNotOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: false, - isCalculatedNotOverrideValue: true, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - -const calculatedOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: true, - isCalculatedNotOverrideValue: false, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - describe('DSInputEncryptedWrapper', () => { const mockHandleSubmit = jest.fn(); const mockOnChange = jest.fn(); - const mockHandleBlur = jest.fn(); let user!: ReturnType; @@ -144,289 +78,52 @@ describe('DSInputEncryptedWrapper', () => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); - mockHandleBlur.mockReset(); }); - test('Should display input with fr label ', async () => { - const state = getInitialState({required: false, fallbackLang: false}); + test('Should submit the value', async () => { + const state = getInitialState({required: true}); render( ); - expect(screen.getByText(fr_label)).toBeVisible(); - }); - - test('Should display input with fallback label ', async () => { - const state = getInitialState({required: false, fallbackLang: true}); - render( - - - - - - ); + const text = 'text'; + const input = screen.getByTestId('kit-input-password'); + await user.click(input); + await user.type(input, text); + await user.tab(); - expect(screen.getByText(en_label)).toBeVisible(); + expect(mockHandleSubmit).toHaveBeenCalledWith(text, mockFormAttribute.id); + expect(mockOnChange).toHaveBeenCalled(); }); - test('Should not submit if field has not changed', async () => { - const state = getInitialState({required: false, fallbackLang: false}); + test('Should allow to clear input', async () => { + const state = getInitialState({required: false}); render( ); + const clearButton = screen.getByRole('button'); - // TODO : change testid with click on label when https://aristid.atlassian.net/browse/DS-174 is done - const input = screen.getByTestId('kit-input-password'); - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalledWith(); - expect(mockOnChange).not.toHaveBeenCalled(); - }); - - describe('With required input and no inheritance', () => { - test('Should submit the value if field is not empty', async () => { - const state = getInitialState({required: true, fallbackLang: false}); - render( - - - - - - ); - - const text = 'text'; - const input = screen.getByTestId('kit-input-password'); - await user.click(input); - await user.type(input, text); - await user.tab(); - - expect(mockHandleSubmit).toHaveBeenCalledWith(text, state.attribute.id); - expect(mockOnChange).toHaveBeenCalled(); - }); - }); - - describe('With inheritance', () => { - test("Should display the inherited value by default and not save if we don't change it", async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...inheritedNotOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - render( - - - - - - ); - const input = screen.getByTestId('kit-input-password'); - expect(input).toHaveValue(inheritedValues[1].raw_value); - - expect(screen.queryByRole('button')).toBeNull(); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and inherited value under it', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); - - const input = screen.getByTestId('kit-input-password'); - const helperText = screen.getByText(/inherited value/); - expect(input).toHaveValue(inheritedValues[0].raw_value); - expect(helperText).toBeInTheDocument(); - }); - - test("Should allow to clear input when it's override", async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - render( - - - - - - ); - const clearButton = screen.getByRole('button'); - - await user.click(clearButton); - - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - }); - }); - - describe('With calculation', () => { - test("Should display the calculated value by default and not save if we don't change it", async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...calculatedNotOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - render( - - - - - - ); - const input = screen.getByTestId('kit-input-password'); - expect(input).toHaveValue(calculatedValues[1].raw_value); - - expect(screen.queryByRole('button')).toBeNull(); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and calculated value under it', async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - const input = screen.getByTestId('kit-input-password'); - const helperText = screen.getByText(/calculated value/); - expect(input).toHaveValue(calculatedValues[0].raw_value); - expect(helperText).toBeInTheDocument(); - }); - - test("Should allow to clear input when it's override", async () => { - let state = getInitialState({required: false, fallbackLang: false}); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - render( - - - - - - ); - const clearButton = screen.getByRole('button'); - - await user.click(clearButton); + await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - }); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, mockFormAttribute.id); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.tsx index eb67db58e..e84423f09 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputEncryptedWrapper.tsx @@ -1,144 +1,56 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {ChangeEvent, FocusEvent, FunctionComponent} from 'react'; +import {Form} from 'antd'; +import {IStandFieldValueContentProps} from './_types'; +import {IKitPassword} from 'aristid-ds/dist/Kit/DataEntry/Input/types'; import {KitInput} from 'aristid-ds'; -import {ChangeEvent, FocusEvent, FunctionComponent, useEffect, useRef, useState} from 'react'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, InputProps} from 'antd'; -import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; -import styled from 'styled-components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {useValueDetailsButton} from '_ui/components/RecordEdition/EditRecordContent/shared/ValueDetailsBtn/useValueDetailsButton'; -import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; -import {useLang} from '_ui/hooks'; -import {localizedTranslation} from '@leav/utils'; -interface IDSInputEncryptedWrapper extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: string, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; -} - -const KitInputPasswordStyled = styled(KitInput.Password)<{$shouldHighlightColor: boolean}>` - color: ${({$shouldHighlightColor}) => ($shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial')}; - - .kit-input-wrapper-helper { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } -`; - -export const DSInputEncryptedWrapper: FunctionComponent = ({ +export const DSInputEncryptedWrapper: FunctionComponent> = ({ value, onChange, state, attribute, - fieldValue, - handleSubmit, - handleBlur, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('DSInputEncryptedWrapper should be used inside a antd Form.Item'); } - const {t} = useSharedTranslation(); const {errors} = Form.Item.useStatus(); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); - 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 {t} = useSharedTranslation(); - const _resetToInheritedOrCalculatedValue = () => { - setHasChanged(false); - if (state.isInheritedValue) { - onChange(state.inheritedValue.raw_value); - } else if (state.isCalculatedValue) { - onChange(state.calculatedValue.raw_value); - } - handleSubmit('', state.attribute.id); - }; + const isErrors = errors.length > 0; const _handleOnBlur = (event: FocusEvent) => { - if (!hasChanged) { - handleBlur(); - return; - } - const valueToSubmit = event.target.value; - if (valueToSubmit === '' && (state.isInheritedValue || state.isCalculatedValue)) { - _resetToInheritedOrCalculatedValue(); - return; - } - - if (!state.isInheritedValue && !state.isCalculatedValue) { - handleSubmit(valueToSubmit, state.attribute.id); - } + handleSubmit(valueToSubmit, attribute.id); onChange(event); }; - const _handleOnChange = (event: ChangeEvent) => { - setHasChanged(true); + const _handleOnChange = async (event: ChangeEvent) => { const inputValue = event.target.value; - if ((state.isInheritedValue || state.isCalculatedValue) && inputValue === '' && event.type === 'click') { - _resetToInheritedOrCalculatedValue(); - return; - } onChange(event); - }; - - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: state.inheritedValue.raw_value - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: state.calculatedValue.raw_value - }); + if (inputValue === '' && event.type === 'click') { + await handleSubmit(null, attribute.id); } - return; }; - const label = localizedTranslation(state.formElement.settings.label, availableLang); - return ( - 0 ? 'error' : undefined} - onInfoClick={shouldShowValueDetailsButton ? onValueDetailsButtonClick : null} - helper={_getHelper()} + helper={isErrors ? String(errors[0]) : undefined} + status={isErrors ? 'error' : undefined} value={value} + placeholder={t('record_edition.placeholder.enter_a_password')} disabled={state.isReadOnly} - allowClear={!state.isInheritedNotOverrideValue && !state.isCalculatedNotOverrideValue} - onBlur={_handleOnBlur} + allowClear onChange={_handleOnChange} - $shouldHighlightColor={ - !hasChanged && (state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue) - } + onBlur={_handleOnBlur} /> ); }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.test.tsx index 18540813f..c3600e660 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputNumberWrapper.test.tsx @@ -5,8 +5,6 @@ import {render, screen} from '_ui/_tests/testUtils'; import {DSInputNumberWrapper} from './DSInputNumberWrapper'; import {VersionFieldScope} from '../../../_types'; import { - CalculatedFlags, - InheritedFlags, IStandardFieldReducerState, StandardFieldValueState } from '../../../reducers/standardFieldReducer/standardFieldReducer'; @@ -74,64 +72,9 @@ const getInitialState = (required: boolean, fallbackLang = false): IStandardFiel isCalculatedValue: false }); -const inheritedValues = [ - { - isInherited: null, - value: '8', - raw_value: '8' - }, - { - isInherited: true, - value: '3.5', - raw_value: '3.5' - } -]; - -const inheritedNotOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: false, - isInheritedNotOverrideValue: true, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const inheritedOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: true, - isInheritedNotOverrideValue: false, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const calculatedValues = [ - { - isCalculated: null, - value: '8', - raw_value: '8' - }, - { - isCalculated: true, - value: '3.5', - raw_value: '3.5' - } -]; - -const calculatedNotOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: false, - isCalculatedNotOverrideValue: true, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - -const calculatedOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: true, - isCalculatedNotOverrideValue: false, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - describe('DSInputNumberWrapper', () => { const mockHandleSubmit = jest.fn(); const mockOnChange = jest.fn(); - const mockHandleBlur = jest.fn(); let user!: ReturnType; @@ -139,378 +82,157 @@ describe('DSInputNumberWrapper', () => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); - mockHandleBlur.mockReset(); }); - test('Should display inputNumber with fr label ', async () => { - const state = getInitialState(false, false); + test('Should display the presentationValue', async () => { + const presentationValue = 'presentationValue'; + const state = getInitialState(false); render( ); - expect(screen.getByText(fr_label)).toBeVisible(); + const input = screen.getByRole('spinbutton'); + expect(input).toHaveValue(presentationValue); }); - test('Should display inputNumber with fallback label ', async () => { - const state = getInitialState(false, true); + test('Should display the value on focus', async () => { + const presentationValue = 'presentationValue'; + const value = '42'; + const state = getInitialState(false); render( ); - expect(screen.getByText(en_label)).toBeVisible(); + const input = screen.getByRole('spinbutton'); + await user.click(input); + expect(input).toHaveValue(value); }); - test('Should submit empty value on clear if field is not required', async () => { + test('Should call handleSubmit on blur with new value', async () => { const state = getInitialState(false); render( ); + const newValue = '72'; const input = screen.getByRole('spinbutton'); - await user.clear(input); + await user.click(input); + await user.type(input, newValue); await user.tab(); expect(mockOnChange).toHaveBeenCalled(); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); + expect(mockHandleSubmit).toHaveBeenCalledWith(newValue, state.attribute.id); }); - describe('With required input and no inheritance', () => { - test('Should submit the value if field is not empty', async () => { - const state = getInitialState(true); + describe('Without inherited or calculated flags', () => { + test('Should submit empty value on clear', async () => { + const state = getInitialState(false); render( - - + + ); - const text = '7.4'; const input = screen.getByRole('spinbutton'); - await user.type(input, text); - await user.tab(); - - expect(mockHandleSubmit).toHaveBeenCalledWith(text, state.attribute.id); - expect(mockOnChange).toHaveBeenCalled(); - }); - test('Should submit the empty value if field is empty', async () => { - const state = getInitialState(true); - render( - - - - - - ); - - const input = screen.getByRole('spinbutton'); + await user.click(input); await user.clear(input); await user.tab(); + expect(mockOnChange).toHaveBeenCalled(); expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); }); - describe('With inheritance', () => { - test("Should display the inherited value by default and not save if we don't change it", async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedNotOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - render( - - - - - - ); - const input = screen.getByRole('spinbutton'); - expect(input).toHaveValue(inheritedValues[1].raw_value); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and inherited value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('spinbutton'); - const helperText = screen.getByText(/3.5/); - expect(input).toHaveValue(inheritedValues[0].raw_value); - expect(helperText).toBeVisible(); - }); - }); - - describe('With required and inheritance', () => { - test("Should display the inherited value by default and not save if we don't change it", async () => { - let state = getInitialState(true); - state = { - ...state, - ...inheritedNotOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - render( - - - - - - ); - const input = screen.getByRole('spinbutton'); - expect(input).toHaveValue(inheritedValues[1].raw_value); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and inherited value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('spinbutton'); - const helperText = screen.getByText(/3.5/); - expect(input).toHaveValue(inheritedValues[0].raw_value); - expect(helperText).toBeVisible(); - }); - }); - - describe('With calculation', () => { - test("Should display the calculated value by default and not save if we don't change it", async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedNotOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - render( - - - - - - ); - const input = screen.getByRole('spinbutton'); - expect(input).toHaveValue(calculatedValues[1].raw_value); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and calculated value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('spinbutton'); - const helperText = screen.getByText(/3.5/); - expect(input).toHaveValue(calculatedValues[0].raw_value); - expect(helperText).toBeVisible(); - }); - }); - - describe('With required and calculation', () => { - test("Should display the calculated value by default and not save if we don't change it", async () => { - let state = getInitialState(true); - state = { - ...state, - ...calculatedNotOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - render( - - - - - - ); - const input = screen.getByRole('spinbutton'); - expect(input).toHaveValue(calculatedValues[1].raw_value); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and calculated value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('spinbutton'); - const helperText = screen.getByText(/3.5/); - expect(input).toHaveValue(calculatedValues[0].raw_value); - expect(helperText).toBeVisible(); - }); + describe('With inherited or calculated value', () => { + it.each` + calculatedValue | inheritedValue | onChangeValue + ${'calculated'} | ${null} | ${'calculated'} + ${null} | ${'inherited'} | ${'inherited'} + ${'calculated'} | ${'inherited'} | ${'inherited'} + `( + 'Should submit empty value on clear and call onChange with inherited value', + async ({ + calculatedValue, + inheritedValue, + onChangeValue + }: { + calculatedValue: string | null; + inheritedValue: string | null; + onChangeValue: string; + }) => { + const state = getInitialState(false); + render( + + + + + + ); + + const input = screen.getByRole('spinbutton'); + + await user.clear(input); + await user.tab(); + + expect(mockOnChange).toHaveBeenCalledWith(onChangeValue); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); + } + ); }); }); 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 0756f0dff..8e228c9ee 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,95 +2,70 @@ // 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, useEffect, useRef, useState} from 'react'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, GetRef, InputNumberProps} from 'antd'; -import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; -import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; +import {ComponentPropsWithRef, FocusEvent, FunctionComponent, useRef, useState} from 'react'; +import {Form, GetRef} from 'antd'; import styled from 'styled-components'; -import {useValueDetailsButton} from '_ui/components/RecordEdition/EditRecordContent/shared/ValueDetailsBtn/useValueDetailsButton'; -import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; -import {localizedTranslation} from '@leav/utils'; -import {useLang} from '_ui/hooks'; - -interface IDSInputWrapperProps extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: string, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; -} +import {IStandFieldValueContentProps} from './_types'; +import {KitInputNumberProps} from 'aristid-ds/dist/Kit/DataEntry/InputNumber/types'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; const KitInputNumberStyled = styled(KitInputNumber)<{$shouldHighlightColor: boolean}>` + width: 100%; .ant-input-number-input-wrap .ant-input-number-input { color: ${({$shouldHighlightColor}) => $shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial'}; } `; -export const DSInputNumberWrapper: FunctionComponent = ({ +export const DSInputNumberWrapper: FunctionComponent> = ({ value, + presentationValue, onChange, state, attribute, - fieldValue, - handleSubmit, - handleBlur, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('DSInputNumberWrapper should be used inside a antd Form.Item'); } - const {t} = useSharedTranslation(); - const {lang} = useLang(); - const {errors} = Form.Item.useStatus(); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); - const [hasChanged, setHasChanged] = useState(false); - + const [isFocused, setIsFocused] = useState(false); const inputRef = useRef>(null); + const {errors} = Form.Item.useStatus(); + const {t} = useSharedTranslation(); - useEffect(() => { - if (fieldValue.isEditing && inputRef.current) { - inputRef.current.focus(); // To automatically open the date picker - } - }, [fieldValue.isEditing]); + const isErrors = errors.length > 0; - const _resetToInheritedOrCalculatedValue = () => { + const _resetToInheritedOrCalculatedValue = async () => { setHasChanged(false); if (state.isInheritedValue) { - onChange(state.inheritedValue.raw_value); + onChange(state.inheritedValue.raw_payload); } else if (state.isCalculatedValue) { - onChange(state.calculatedValue.raw_value); + onChange(state.calculatedValue.raw_payload); } - handleSubmit('', state.attribute.id); + await handleSubmit(null, state.attribute.id); }; - const _handleOnBlur = (event: FocusEvent) => { + const _handleFocus = () => setIsFocused(true); + + const _handleOnBlur = async (event: FocusEvent) => { + const valueToSubmit = event.target.value; + if (!hasChanged) { - handleBlur(); + onChange(valueToSubmit); + setIsFocused(false); return; } - const valueToSubmit = event.target.value; if (valueToSubmit === '' && (state.isInheritedValue || state.isCalculatedValue)) { _resetToInheritedOrCalculatedValue(); return; } - if (hasChanged || (!state.isInheritedValue && !state.isCalculatedValue)) { - handleSubmit(valueToSubmit, state.attribute.id); - } - onChange(valueToSubmit); + setIsFocused(false); + await handleSubmit(valueToSubmit, state.attribute.id); }; const _handleOnChange: ComponentPropsWithRef['onChange'] = inputValue => { @@ -98,36 +73,21 @@ export const DSInputNumberWrapper: FunctionComponent = ({ onChange(inputValue); }; - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: state.inheritedValue.raw_value - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: state.calculatedValue.raw_value - }); - } - return; - }; - - const label = localizedTranslation(state.formElement.settings.label, lang); - return ( 0 ? 'error' : undefined} - helper={_getHelper()} + helper={isErrors ? String(errors[0]) : undefined} + status={isErrors ? 'error' : undefined} value={value} - onChange={_handleOnChange} + formatter={v => (isFocused || isErrors || !presentationValue ? `${v}` : `${presentationValue}`)} disabled={state.isReadOnly} + onChange={_handleOnChange} + onFocus={_handleFocus} onBlur={_handleOnBlur} $shouldHighlightColor={ !hasChanged && (state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue) } + placeholder={t('record_edition.placeholder.enter_a_number')} /> ); }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.test.tsx index 9e867e559..586576670 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSInputWrapper.test.tsx @@ -5,8 +5,6 @@ import {render, screen} from '_ui/_tests/testUtils'; import {DSInputWrapper} from './DSInputWrapper'; import {VersionFieldScope} from '../../../_types'; import { - CalculatedFlags, - InheritedFlags, IStandardFieldReducerState, StandardFieldValueState } from '../../../reducers/standardFieldReducer/standardFieldReducer'; @@ -72,64 +70,9 @@ const getInitialState = (required: boolean, fallbackLang = false): IStandardFiel isCalculatedValue: false }); -const inheritedValues = [ - { - isInherited: null, - value: 'override value', - raw_value: 'override value' - }, - { - isInherited: true, - value: 'inherited value', - raw_value: 'inherited value' - } -]; - -const inheritedNotOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: false, - isInheritedNotOverrideValue: true, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const inheritedOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: true, - isInheritedNotOverrideValue: false, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const calculatedValues = [ - { - isCalculated: null, - value: 'override value', - raw_value: 'override value' - }, - { - isCalculated: true, - value: 'calculated value', - raw_value: 'calculated value' - } -]; - -const calculatedNotOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: false, - isCalculatedNotOverrideValue: true, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - -const calculatedOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: true, - isCalculatedNotOverrideValue: false, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - describe('DSInputWrapper', () => { const mockHandleSubmit = jest.fn(); const mockOnChange = jest.fn(); - const mockHandleBlur = jest.fn(); let user!: ReturnType; @@ -137,288 +80,151 @@ describe('DSInputWrapper', () => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); - mockHandleBlur.mockReset(); }); - test('Should display input with fr label ', async () => { - const state = getInitialState(false, false); + test('Should display the presentationValue', async () => { + const presentationValue = 'presentationValue'; + const state = getInitialState(false); render( ); - expect(screen.getByText(fr_label)).toBeVisible(); + const input = screen.getByRole('textbox'); + expect(input).toHaveValue(presentationValue); }); - test('Should display input with fallback label ', async () => { - const state = getInitialState(false, true); + test('Should display the value on focus', async () => { + const presentationValue = 'presentationValue'; + const value = '42'; + const state = getInitialState(false); render( ); - expect(screen.getByText(en_label)).toBeVisible(); + const input = screen.getByRole('textbox'); + await user.click(input); + expect(input).toHaveValue(value); }); - test('Should not submit if field has not changed', async () => { + test('Should call handleSubmit on blur with new value', async () => { const state = getInitialState(false); render( ); + const newValue = '72'; const input = screen.getByRole('textbox'); + await user.click(input); + await user.type(input, newValue); await user.tab(); - expect(mockOnChange).not.toHaveBeenCalled(); - expect(mockHandleSubmit).not.toHaveBeenCalled(); + expect(mockOnChange).toHaveBeenCalled(); + expect(mockHandleSubmit).toHaveBeenCalledWith(newValue, state.attribute.id); }); - describe('With required input and no inheritance', () => { - test('Should submit the value if field is not empty', async () => { - const state = getInitialState(true); + describe('Without inherited or calculated flags', () => { + test('Should submit empty value on clear', async () => { + const state = getInitialState(false); render( - - + + ); - const text = 'text'; - const input = screen.getByRole('textbox'); - await user.click(input); - await user.type(input, text); - await user.tab(); + const clearIcon = screen.getByLabelText('clear'); + await user.click(clearIcon); - expect(mockHandleSubmit).toHaveBeenCalledWith(text, state.attribute.id); expect(mockOnChange).toHaveBeenCalled(); - }); - }); - - describe('With inheritance', () => { - test("Should display the inherited value by default and not save if we don't change it", async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedNotOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - render( - - - - - - ); - const input = screen.getByRole('textbox'); - expect(input).toHaveValue(inheritedValues[1].raw_value); - - expect(screen.queryByRole('button')).toBeNull(); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and inherited value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - const helperText = screen.getByText(/inherited value/); - expect(input).toHaveValue(inheritedValues[0].raw_value); - expect(helperText).toBeInTheDocument(); - }); - - test("Should allow to clear input when it's override", async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - render( - - - - - - ); - const clearButton = screen.getByRole('button'); - - await user.click(clearButton); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); }); - describe('With calculation', () => { - test("Should display the calculated value by default and not save if we don't change it", async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedNotOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - render( - - - - - - ); - const input = screen.getByRole('textbox'); - expect(input).toHaveValue(calculatedValues[1].raw_value); - - expect(screen.queryByRole('button')).toBeNull(); - - await user.click(input); - await user.tab(); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and calculated value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - const helperText = screen.getByText(/calculated value/); - expect(input).toHaveValue(calculatedValues[0].raw_value); - expect(helperText).toBeInTheDocument(); - }); - - test("Should allow to clear input when it's override", async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - render( - - - - - - ); - const clearButton = screen.getByRole('button'); - - await user.click(clearButton); - - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - }); + describe('With inherited or calculated value', () => { + it.each` + calculatedValue | inheritedValue | onChangeValue + ${'calculated'} | ${null} | ${'calculated'} + ${null} | ${'inherited'} | ${'inherited'} + ${'calculated'} | ${'inherited'} | ${'inherited'} + `( + 'Should submit empty value on clear and call onChange with inherited value', + async ({ + calculatedValue, + inheritedValue, + onChangeValue + }: { + calculatedValue: string | null; + inheritedValue: string | null; + onChangeValue: string; + }) => { + const state = getInitialState(false); + render( + + + + + + ); + + const clearIcon = screen.getByLabelText('clear'); + await user.click(clearIcon); + + expect(mockOnChange).toHaveBeenCalledWith(onChangeValue); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, 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 f9e2c0ec0..18dfa14e4 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,76 +2,53 @@ // 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, useEffect, useRef, useState} from 'react'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, GetRef, InputProps} from 'antd'; -import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; +import {ChangeEvent, FocusEvent, FunctionComponent, useState} from 'react'; +import {Form} from 'antd'; import styled from 'styled-components'; +import {IStandFieldValueContentProps} from './_types'; +import {IKitInput} from 'aristid-ds/dist/Kit/DataEntry/Input/types'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {useValueDetailsButton} from '_ui/components/RecordEdition/EditRecordContent/shared/ValueDetailsBtn/useValueDetailsButton'; -import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; -import {useLang} from '_ui/hooks'; -import {localizedTranslation} from '@leav/utils'; - -interface IDSInputWrapperProps extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: string, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; -} const KitInputStyled = styled(KitInput)<{$shouldHighlightColor: boolean}>` color: ${({$shouldHighlightColor}) => ($shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial')}; `; -export const DSInputWrapper: FunctionComponent = ({ +export const DSInputWrapper: FunctionComponent> = ({ value, + presentationValue, onChange, state, attribute, - fieldValue, - handleSubmit, - handleBlur, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('DSInputWrapper should be used inside a antd Form.Item'); } - const {t} = useSharedTranslation(); - const {errors} = Form.Item.useStatus(); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); const [hasChanged, setHasChanged] = useState(false); - const {lang: availableLang} = useLang(); - const inputRef = useRef>(null); + const [isFocused, setIsFocused] = useState(false); + const {errors} = Form.Item.useStatus(); + const {t} = useSharedTranslation(); - useEffect(() => { - if (fieldValue.isEditing && inputRef.current) { - inputRef.current.focus(); - } - }, [fieldValue.isEditing]); + const isErrors = errors.length > 0; + const valueToDisplay = isFocused || isErrors ? value : presentationValue; - const _resetToInheritedOrCalculatedValue = () => { + const _resetToInheritedOrCalculatedValue = async () => { setHasChanged(false); if (state.isInheritedValue) { - onChange(state.inheritedValue.raw_value); + onChange(state.inheritedValue.raw_payload); } else if (state.isCalculatedValue) { - onChange(state.calculatedValue.raw_value); + onChange(state.calculatedValue.raw_payload); } - handleSubmit('', state.attribute.id); + await handleSubmit(null, state.attribute.id); }; - const _handleOnBlur = (event: FocusEvent) => { + const _handleFocus = () => setIsFocused(true); + + const _handleOnBlur = async (event: FocusEvent) => { if (!hasChanged) { - handleBlur(); + onChange(event); + setIsFocused(false); return; } @@ -80,58 +57,40 @@ export const DSInputWrapper: FunctionComponent = ({ _resetToInheritedOrCalculatedValue(); return; } - if (hasChanged || (!state.isInheritedValue && !state.isCalculatedValue)) { - handleSubmit(valueToSubmit, state.attribute.id); - } + onChange(event); + setIsFocused(false); + await handleSubmit(valueToSubmit, state.attribute.id); }; - const _handleOnChange = (event: ChangeEvent) => { + // TODO remove this function to use onClear Prop when ant is updated to 5.20+ (and other inputs too) + const _handleOnChange = async (event: ChangeEvent) => { setHasChanged(true); const inputValue = event.target.value; if ((state.isInheritedValue || state.isCalculatedValue) && inputValue === '' && event.type === 'click') { _resetToInheritedOrCalculatedValue(); return; } - - if (inputValue === '' && event.type === 'click') { - handleSubmit(inputValue, state.attribute.id); - } - onChange(event); - }; - - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: state.inheritedValue.raw_value - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: state.calculatedValue.raw_value - }); + if (inputValue === '' && event.type === 'click') { + await handleSubmit(inputValue, state.attribute.id); } - return; }; - const label = localizedTranslation(state.formElement.settings.label, availableLang); - return ( 0 ? 'error' : undefined} - helper={_getHelper()} - value={value} disabled={state.isReadOnly} + helper={isErrors ? String(errors[0]) : undefined} + status={isErrors ? 'error' : undefined} + value={valueToDisplay} allowClear={!state.isInheritedNotOverrideValue && !state.isCalculatedNotOverrideValue} - onBlur={_handleOnBlur} onChange={_handleOnChange} + onFocus={_handleFocus} + onBlur={_handleOnBlur} $shouldHighlightColor={ !hasChanged && (state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue) } + placeholder={t('record_edition.placeholder.enter_a_text')} /> ); }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.test.tsx index af81d9382..9ff0adc51 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/DSRangePickerWrapper.test.tsx @@ -74,57 +74,35 @@ const getInitialState = (required: boolean, fallbackLang = false): IStandardFiel describe('DSRangePickerWrapper', () => { 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', () => { - test('Should display range picker with fr label ', async () => { - const state = getInitialState(false, false); - render( -
- - - -
- ); - - expect(screen.getByText(fr_label)).toBeVisible(); - }); - - test('Should display range picker with fallback label ', async () => { - const state = getInitialState(false, true); - render( -
- - - -
- ); - - expect(screen.getByText(en_label)).toBeVisible(); - }); + test('Should display presentationValue by default', async () => { + const state = getInitialState(false); + const presentationValue = 'Du 20 novembre au 12 décembre'; + render( +
+ + + +
+ ); + + expect(screen.getByRole('textbox')).toHaveValue(presentationValue); + }); + describe('Without required field', () => { test('Should call onChange with value', async () => { const state = getInitialState(false); render( @@ -133,10 +111,9 @@ describe('DSRangePickerWrapper', () => { @@ -188,10 +165,9 @@ describe('DSRangePickerWrapper', () => { @@ -223,10 +199,9 @@ describe('DSRangePickerWrapper', () => { @@ -265,18 +240,18 @@ describe('DSRangePickerWrapper', () => { ); }); - test('Should not save to LEAV if field becomes empty', async () => { + test('Should display presentationValue after save', async () => { const state = getInitialState(true); + const presentationValue = 'presentationValue'; render(
@@ -284,105 +259,50 @@ describe('DSRangePickerWrapper', () => { const rangePickerInputs = screen.getAllByRole('textbox'); await user.click(rangePickerInputs[0]); - const currentDate = dayjs().format('YYYY-MM-DD'); - await user.click(screen.getAllByTitle(currentDate)[0]); - await user.click(screen.getAllByTitle(currentDate)[0]); - - expect(mockOnChange).toHaveBeenCalledTimes(1); - expect(mockHandleSubmit).toHaveBeenCalledTimes(1); - - const clearButton = screen.getByRole('button'); - await user.click(clearButton); - - expect(mockOnChange).toHaveBeenCalledTimes(2); - expect(mockHandleSubmit).toHaveBeenCalledTimes(1); - }); - }); + expect(screen.queryByText(presentationValue)).not.toBeInTheDocument(); + const startRangeDateFormatted = dayjs().format('YYYY-MM-DD'); + const endRangeDateFormatted = dayjs().add(1, 'day').format('YYYY-MM-DD'); - describe('Inherited values', () => { - test('Should not display helper without inherited value', async () => { - const state = getInitialState(false); - state.inheritedValue = null; - state.isInheritedOverrideValue = false; - render( -
- - - -
- ); + await user.click(screen.getAllByTitle(startRangeDateFormatted)[0]); + await user.click(screen.getAllByTitle(endRangeDateFormatted)[0]); - expect(screen.queryByText('record_edition.inherited_input_helper', {exact: false})).not.toBeInTheDocument(); + expect(screen.getByRole('textbox')).toHaveValue(presentationValue); }); - test('Should display helper with inherited value', async () => { - const state = getInitialState(false); - state.inheritedValue = mockValue.value; - state.isInheritedOverrideValue = true; + test.skip('Should not save to LEAV if field becomes empty', async () => { + const state = getInitialState(true); render(
); - expect(screen.getByText('record_edition.inherited_input_helper', {exact: false})).toBeVisible(); - }); + const rangePickerInputs = screen.getAllByRole('textbox'); + await user.click(rangePickerInputs[0]); + const currentDate = dayjs().format('YYYY-MM-DD'); + await user.click(screen.getAllByTitle(currentDate)[0]); + await user.click(screen.getAllByTitle(currentDate)[0]); - test('Should call onChange/handleSubmit with inherited value on clear', async () => { - const raw_value = { - from: '1714138054', - to: '1714138054' - }; - const state = getInitialState(false); - state.inheritedValue = {...mockValue.value, raw_value}; - state.isInheritedValue = true; - state.isInheritedOverrideValue = true; - state.isInheritedNotOverrideValue = false; - render( -
- - - -
- ); + expect(mockOnChange).toHaveBeenCalledTimes(1); + expect(mockHandleSubmit).toHaveBeenCalledTimes(1); - // TODO : target clear button when DS add html attribute - Ticket DS-219 const clearButton = screen.getByRole('button'); await user.click(clearButton); - expect(mockOnChange).toHaveBeenCalledTimes(1); - expect(mockOnChange).toHaveBeenCalledWith([expect.any(Object), expect.any(Object)], raw_value); + expect(mockOnChange).toHaveBeenCalledTimes(2); expect(mockHandleSubmit).toHaveBeenCalledTimes(1); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); + }); + describe('Inherited values', () => { test('Should hide clear icon when value is inherited, but not override', async () => { const raw_value = { from: '1714138054', @@ -403,10 +323,9 @@ describe('DSRangePickerWrapper', () => { @@ -418,91 +337,6 @@ describe('DSRangePickerWrapper', () => { }); describe('Calculated values', () => { - test('Should not display helper without calculated value', async () => { - const state = getInitialState(false); - state.calculatedValue = null; - state.isCalculatedOverrideValue = false; - render( -
- - - -
- ); - - expect( - screen.queryByText('record_edition.calculated_input_helper', {exact: false}) - ).not.toBeInTheDocument(); - }); - - test('Should display helper with calculated value', async () => { - const state = getInitialState(false); - state.calculatedValue = mockValue.value; - state.isCalculatedOverrideValue = true; - render( -
- - - -
- ); - - expect(screen.getByText('record_edition.calculated_input_helper', {exact: false})).toBeVisible(); - }); - - test('Should call onChange/handleSubmit with calculated value on clear', async () => { - const raw_value = { - from: '1714138054', - to: '1714138054' - }; - const state = getInitialState(false); - state.calculatedValue = {...mockValue.value, raw_value}; - state.isCalculatedValue = true; - state.isCalculatedOverrideValue = true; - state.isCalculatedNotOverrideValue = false; - render( -
- - - -
- ); - - // TODO : target clear button when DS add html attribute - Ticket DS-219 - const clearButton = screen.getByRole('button'); - await user.click(clearButton); - - expect(mockOnChange).toHaveBeenCalledTimes(1); - expect(mockOnChange).toHaveBeenCalledWith([expect.any(Object), expect.any(Object)], raw_value); - expect(mockHandleSubmit).toHaveBeenCalledTimes(1); - expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); - }); - test('Should hide clear icon when value is calculated, but not override', async () => { const raw_value = { from: '1714138054', @@ -523,9 +357,8 @@ 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 cc3450338..c4c3334a1 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 @@ -1,68 +1,45 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // 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, useEffect, useRef} from 'react'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '../../../reducers/standardFieldReducer/standardFieldReducer'; +import {KitDatePicker, KitInput} from 'aristid-ds'; +import {ChangeEvent, FunctionComponent, useState} from 'react'; import {Form} from 'antd'; import dayjs from 'dayjs'; import styled from 'styled-components'; -import {IProvidedByAntFormItem, StandardValueTypes} from '../../../_types'; -import {RangePickerProps} from 'antd/lib/date-picker'; -import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -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'; +import {StandardValueTypes} from '../../../_types'; import {setDateToUTCNoon} from '_ui/_utils'; - -interface IDSRangePickerWrapperProps extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: StandardValueTypes, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; -} +import {FaCalendar} from 'react-icons/fa'; +import {IStandFieldValueContentProps} from './_types'; +import {IKitRangePicker} from 'aristid-ds/dist/Kit/DataEntry/DatePicker/types'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; const KitDatePickerRangePickerStyled = styled(KitDatePicker.RangePicker)<{$shouldHighlightColor: boolean}>` color: ${({$shouldHighlightColor}) => ($shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial')}; `; -export const DSRangePickerWrapper: FunctionComponent = ({ +const KitInputStyled = styled(KitInput)<{$shouldHighlightColor: boolean}>` + color: ${({$shouldHighlightColor}) => ($shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial')}; +`; + +export const DSRangePickerWrapper: FunctionComponent> = ({ + presentationValue, value, onChange, state, attribute, - fieldValue, - handleSubmit, - handleBlur, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('DSRangePickerWrapper should be used inside a antd Form.Item'); } - const {t} = useSharedTranslation(); - const {lang: availableLangs} = useLang(); + const [isFocused, setIsFocused] = useState(false); const {errors} = Form.Item.useStatus(); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); - - const inputRef = useRef(); + const {t} = useSharedTranslation(); - useEffect(() => { - if (fieldValue.isEditing && inputRef.current) { - inputRef.current.nativeElement.click(); // To automatically open the date picker - } - }, [fieldValue.isEditing]); + const isErrors = errors.length > 0; - const _resetToInheritedOrCalculatedValue = () => { + const _resetToInheritedOrCalculatedValue = async () => { if (state.isInheritedValue) { onChange( [ @@ -80,13 +57,15 @@ export const DSRangePickerWrapper: FunctionComponent state.calculatedValue.raw_value ); } - handleSubmit('', state.attribute.id); + await handleSubmit(null, state.attribute.id); }; + const _handleFocus = () => setIsFocused(true); + const _handleDateChange: ( rangePickerDates: [from: dayjs.Dayjs, to: dayjs.Dayjs] | null, antOnChangeParams: [from: string, to: string] | null - ) => void = (rangePickerDates, ...antOnChangeParams) => { + ) => void = async (rangePickerDates, ...antOnChangeParams) => { if ((state.isInheritedValue || state.isCalculatedValue) && rangePickerDates === null) { _resetToInheritedOrCalculatedValue(); return; @@ -103,59 +82,68 @@ export const DSRangePickerWrapper: FunctionComponent return; } - const datesToSave = {from: null, to: null}; + let datesToSave: StandardValueTypes = ''; + if (rangePickerDates !== null) { const [dateFrom, dateTo] = rangePickerDates; - - datesToSave.from = String(dateFrom.unix()); - datesToSave.to = String(dateTo.unix()); + datesToSave = {from: String(dateFrom.unix()), to: String(dateTo.unix())}; } - handleSubmit(datesToSave, state.attribute.id); + await handleSubmit(datesToSave, state.attribute.id); }; const _handleOpenChange = (open: boolean) => { if (!open) { - handleBlur(); + setIsFocused(false); } }; - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: t('record_edition.date_range_from_to', { - from: state.inheritedValue.value.from, - to: state.inheritedValue.value.to - }) - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: t('record_edition.date_range_from_to', { - from: state.calculatedValue.value.from, - to: state.calculatedValue.value.to - }) - }); + // TOTO remove this function to use onClear Prop when ant is updated to 5.20+ + const _handleClear = async (event: ChangeEvent) => { + const inputValue = event.target.value; + + if ((state.isInheritedValue || state.isCalculatedValue) && inputValue === '' && event.type === 'click') { + _resetToInheritedOrCalculatedValue(); + return; + } + onChange(null, null); + if (inputValue === '' && event.type === 'click') { + await handleSubmit('', state.attribute.id); } - return; }; - const label = localizedTranslation(state.formElement.settings.label, availableLangs); - return ( - 0 ? 'error' : undefined} - onInfoClick={shouldShowValueDetailsButton ? onValueDetailsButtonClick : null} - onOpenChange={_handleOpenChange} - helper={_getHelper()} - $shouldHighlightColor={state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue} - /> + <> + { + !isFocused && !isErrors && ( + } + helper={isErrors ? String(errors[0]) : undefined} + status={isErrors ? 'error' : undefined} + value={presentationValue} + onFocus={_handleFocus} + onChange={_handleClear} + $shouldHighlightColor={state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue} + placeholder={t('record_edition.placeholder.enter_a_period')} + /> + ) + //TODO: Léger décalage car l'icon n'est pas exactement le même que celui du DS (MAJ React-icons ?) + } + {(isFocused || isErrors) && ( + + )} + ); }; 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 4d9336193..eea7d7519 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 @@ -5,8 +5,6 @@ import {mockBrowserFunctionsForTiptap, render, screen} from '_ui/_tests/testUtil import {DSRichTextWrapper} from './DSRichTextWrapper'; import {VersionFieldScope} from '../../../_types'; import { - CalculatedFlags, - InheritedFlags, IStandardFieldReducerState, StandardFieldValueState } from '../../../reducers/standardFieldReducer/standardFieldReducer'; @@ -72,60 +70,6 @@ const getInitialState = (required: boolean, fallbackLang = false): IStandardFiel isCalculatedValue: false }); -const inheritedValues = [ - { - isInherited: null, - value: 'override value', - raw_value: 'override value' - }, - { - isInherited: true, - value: 'inherited value', - raw_value: 'inherited value' - } -]; - -const inheritedNotOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: false, - isInheritedNotOverrideValue: true, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const inheritedOverrideValue: InheritedFlags = { - isInheritedValue: true, - isInheritedOverrideValue: true, - isInheritedNotOverrideValue: false, - inheritedValue: {raw_value: inheritedValues[1].raw_value} -}; - -const calculatedValues = [ - { - isCalculated: null, - value: 'override value', - raw_value: 'override value' - }, - { - isCalculated: true, - value: 'calculated value', - raw_value: 'calculated value' - } -]; - -const calculatedNotOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: false, - isCalculatedNotOverrideValue: true, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - -const calculatedOverrideValue: CalculatedFlags = { - isCalculatedValue: true, - isCalculatedOverrideValue: true, - isCalculatedNotOverrideValue: false, - calculatedValue: {raw_value: calculatedValues[1].raw_value} -}; - const tiptapCleanup = mockBrowserFunctionsForTiptap(); describe('DSRichTextWrapper', () => { @@ -134,7 +78,6 @@ describe('DSRichTextWrapper', () => { }); const mockHandleSubmit = jest.fn(); - const mockHandleBlur = jest.fn(); const mockOnChange = jest.fn(); let user!: ReturnType; @@ -142,228 +85,155 @@ describe('DSRichTextWrapper', () => { user = userEvent.setup({}); mockOnChange.mockReset(); mockHandleSubmit.mockReset(); - mockHandleBlur.mockReset(); }); - test('Should display input with fr label ', async () => { - const state = getInitialState(false, false); + test('Should display the presentationValue', async () => { + const presentationValue = 'presentationValue'; + const state = getInitialState(false); render( ); - expect(screen.getByText(fr_label)).toBeVisible(); + const input = screen.getByRole('textbox'); + expect(input).toContainHTML(presentationValue); }); - test('Should display input with fallback label ', async () => { - const state = getInitialState(false, true); + test('Should display the value on focus', async () => { + const presentationValue = 'presentationValue'; + const value = '42'; + const state = getInitialState(false); render( ); - expect(screen.getByText(en_label)).toBeVisible(); + const input = screen.getByRole('textbox'); + await user.click(input); + expect(input).toContainHTML(value); }); - test('Should not submit if value has not changed', async () => { + test('Should call handleSubmit on blur with new value', async () => { const state = getInitialState(false); render( ); + const newValue = '72'; const input = screen.getByRole('textbox'); + await user.click(input); + await user.type(input, newValue); await user.click(document.body); - expect(mockHandleSubmit).not.toHaveBeenCalled(); - expect(mockOnChange).not.toHaveBeenCalled(); + expect(mockOnChange).toHaveBeenCalled(); + expect(mockHandleSubmit).toHaveBeenCalledWith(`

${newValue}

`, state.attribute.id); }); - describe('With required input and no inheritance', () => { - test('Should submit the value if field is not empty', async () => { - const state = getInitialState(true); + describe('Without inherited or calculated flags', () => { + test('Should submit empty value on clear', async () => { + const state = getInitialState(false); render( - - + + ); - const text = 'text'; const input = screen.getByRole('textbox'); await user.click(input); - await user.type(input, text); + await user.clear(input); await user.click(document.body); - expect(mockHandleSubmit).toHaveBeenCalledWith(`

${text}

`, state.attribute.id); expect(mockOnChange).toHaveBeenCalled(); + expect(mockHandleSubmit).toHaveBeenCalledWith('', state.attribute.id); }); }); - describe('With inheritance', () => { - test("Should display the inherited value by default and not save if we don't change it", async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedNotOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - expect(input).toContainHTML(inheritedValues[1].raw_value); - - await user.click(input); - await user.click(document.body); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and inherited value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...inheritedOverrideValue, - formElement: {...state.formElement, values: inheritedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - const helperText = screen.getByText(/inherited value/); - expect(input).toContainHTML(inheritedValues[0].raw_value); - expect(helperText).toBeInTheDocument(); - }); - }); - - describe('With calculation', () => { - test("Should display the calculated value by default and not save if we don't change it", async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedNotOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - expect(input).toContainHTML(calculatedValues[1].raw_value); - - await user.click(input); - await user.click(document.body); - - expect(mockHandleSubmit).not.toHaveBeenCalled(); - }); - - test('Should display the override value in the input and calculated value under it', async () => { - let state = getInitialState(false); - state = { - ...state, - ...calculatedOverrideValue, - formElement: {...state.formElement, values: calculatedValues} - }; - - render( - - - - - - ); - - const input = screen.getByRole('textbox'); - const helperText = screen.getByText(/calculated value/); - expect(input).toContainHTML(calculatedValues[0].raw_value); - expect(helperText).toBeInTheDocument(); - }); + describe('With inherited or calculated value', () => { + it.each` + calculatedValue | inheritedValue | onChangeValue + ${'calculated'} | ${null} | ${'calculated'} + ${null} | ${'inherited'} | ${'inherited'} + ${'calculated'} | ${'inherited'} | ${'inherited'} + `( + 'Should submit empty value on clear and call onChange with inherited value', + async ({ + calculatedValue, + inheritedValue, + onChangeValue + }: { + calculatedValue: string | null; + inheritedValue: string | null; + onChangeValue: string; + }) => { + const state = getInitialState(false); + render( + + + + + + ); + + const input = screen.getByRole('textbox'); + await user.click(input); + await user.clear(input); + await user.click(document.body); + + expect(mockOnChange).toHaveBeenCalledWith(onChangeValue); + expect(mockHandleSubmit).toHaveBeenCalledWith(null, state.attribute.id); + } + ); }); }); 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 026e4b0b8..3ee528f99 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 @@ -1,93 +1,72 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {FunctionComponent, useEffect, useRef, useState} from 'react'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '../../../reducers/standardFieldReducer/standardFieldReducer'; -import {Form, GetRef, InputProps} from 'antd'; -import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; +import {FunctionComponent, useState} from 'react'; +import {Form} from 'antd'; import styled from 'styled-components'; -import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {useValueDetailsButton} from '_ui/components/RecordEdition/EditRecordContent/shared/ValueDetailsBtn/useValueDetailsButton'; -import {RecordFormAttributeFragment} from '_ui/_gqlTypes'; -import {useLang} from '_ui/hooks'; -import {localizedTranslation} from '@leav/utils'; import {KitRichText} from 'aristid-ds'; - -interface IDSRichTextWrapperProps extends IProvidedByAntFormItem { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: string, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; -} +import {IStandFieldValueContentProps} from './_types'; +import {KitRichTextProps} from 'aristid-ds/dist/Kit/DataEntry/RichText/types'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; const KitRichTextStyled = styled(KitRichText)<{$shouldHighlightColor: boolean}>` - color: ${({$shouldHighlightColor}) => ($shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial')}; + .tiptap.ProseMirror { + color: ${({$shouldHighlightColor}) => + $shouldHighlightColor ? 'var(--general-colors-primary-400)' : 'initial'}; + } `; const isEmptyValue = value => !value || value === '

'; -export const DSRichTextWrapper: FunctionComponent = ({ +export const DSRichTextWrapper: FunctionComponent> = ({ value, + presentationValue, onChange, state, attribute, - fieldValue, - handleSubmit, - handleBlur, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('DSRichTextWrapper should be used inside a antd Form.Item'); } - const {t} = useSharedTranslation(); - const {errors} = Form.Item.useStatus(); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); const [hasChanged, setHasChanged] = useState(false); - const {lang: availableLang} = useLang(); - const inputRef = useRef>(null); + const [isFocused, setIsFocused] = useState(false); + const {errors} = Form.Item.useStatus(); + const {t} = useSharedTranslation(); - useEffect(() => { - if (fieldValue.isEditing && inputRef.current) { - (inputRef.current.children[0] as HTMLElement).focus(); - } - }, [fieldValue.isEditing]); + const isErrors = errors.length > 0; + const valueToDisplay = isFocused || isErrors ? value : presentationValue; - const _resetToInheritedOrCalculatedValue = () => { + const _resetToInheritedOrCalculatedValue = async () => { setHasChanged(false); if (state.isInheritedValue) { - onChange(state.inheritedValue.raw_value); + onChange(state.inheritedValue.raw_payload); } else if (state.isCalculatedValue) { - onChange(state.calculatedValue.raw_value); + onChange(state.calculatedValue.raw_payload); } - handleSubmit('', state.attribute.id); + await handleSubmit(null, state.attribute.id); }; - const _handleOnBlur = inputValue => { + const _handleFocus = () => setIsFocused(true); + + const _handleOnBlur = async inputValue => { + const valueToSubmit = isEmptyValue(inputValue) ? '' : inputValue; + if (!hasChanged) { - handleBlur(); + onChange(valueToSubmit); + setIsFocused(false); return; } - const valueToSubmit = isEmptyValue(inputValue) ? '' : inputValue; - if (valueToSubmit === '' && (state.isInheritedValue || state.isCalculatedValue)) { _resetToInheritedOrCalculatedValue(); return; } - if (hasChanged || (!state.isInheritedValue && !state.isCalculatedValue)) { - handleSubmit(valueToSubmit, state.attribute.id); - } + onChange(valueToSubmit); - return; + setIsFocused(false); + await handleSubmit(valueToSubmit, state.attribute.id); }; const _handleOnChange = inputValue => { @@ -99,36 +78,19 @@ export const DSRichTextWrapper: FunctionComponent = ({ onChange(inputValue); }; - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: state.inheritedValue.raw_value - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: state.calculatedValue.raw_value - }); - } - return; - }; - - const label = localizedTranslation(state.formElement.settings.label, availableLang); - return ( 0 ? 'error' : undefined} - helper={_getHelper()} - value={value as string} + helper={isErrors ? String(errors[0]) : undefined} + status={isErrors ? 'error' : undefined} + value={valueToDisplay} disabled={state.isReadOnly} onChange={_handleOnChange} + onFocus={_handleFocus} onBlur={_handleOnBlur} $shouldHighlightColor={ - (!hasChanged && state.isInheritedNotOverrideValue) || state.isCalculatedNotOverrideValue + !hasChanged && (state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue) } + placeholder={t('record_edition.placeholder.enter_a_text')} /> ); }; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValue.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValue.tsx index 4be3972c3..82c5e3a17 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValue.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValue.tsx @@ -3,9 +3,9 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {CloseOutlined, ExclamationCircleOutlined} from '@ant-design/icons'; import {AnyPrimitive, IDateRangeValue, localizedTranslation} from '@leav/utils'; -import {Button, Input, InputRef, Popover, Space, theme} from 'antd'; +import {Button, Form, FormListFieldData, Input, InputRef, Popover, Space, theme} from 'antd'; import moment from 'moment'; -import React, {MutableRefObject, useEffect, useRef} from 'react'; +import React, {MutableRefObject, ReactNode, useEffect, useRef} from 'react'; import styled, {CSSObject} from 'styled-components'; import {themeVars} from '_ui/antdTheme'; import {FloatingMenu, FloatingMenuAction} from '_ui/components'; @@ -20,7 +20,8 @@ import { VersionFieldScope, InputRefPossibleTypes, IStandardInputProps, - StandardValueTypes + StandardValueTypes, + ISubmitMultipleResult } from '_ui/components/RecordEdition/EditRecordContent/_types'; import {EditRecordReducerActionsTypes} from '_ui/components/RecordEdition/editRecordReducer/editRecordReducer'; import {useEditRecordReducer} from '_ui/components/RecordEdition/editRecordReducer/useEditRecordReducer'; @@ -46,7 +47,15 @@ import TextInput from './Inputs/TextInput'; import ValuesList from './ValuesList'; import {IValueOfValuesList} from './ValuesList/ValuesList'; import {useLang} from '_ui/hooks'; -import {StandardFieldValueDisplayHandler} from './StandardFieldValueDisplayHandler'; +import {MonoValueSelect} from './ValuesList/MonoValueSelect'; +import {DSInputWrapper} from './DSInputWrapper'; +import {DSDatePickerWrapper} from './DSDatePickerWrapper'; +import {DSRangePickerWrapper} from './DSRangePickerWrapper'; +import {DSInputNumberWrapper} from './DSInputNumberWrapper'; +import {DSInputEncryptedWrapper} from './DSInputEncryptedWrapper'; +import {DSBooleanWrapper} from './DSBooleanWrapper'; +import {DSRichTextWrapper} from './DSRichTextWrapper'; +import {DSColorPickerWrapper} from './DSColorPickerWrapper'; const ErrorMessage = styled.div` color: ${themeVars.errorColor}; @@ -189,20 +198,28 @@ type IDateRangeValuesListConf = StandardValuesListFragmentStandardDateRangeValue interface IStandardFieldValueProps { value: IStandardFieldValue; + presentationValue: string; state: IStandardFieldReducerState; dispatch: StandardFieldDispatchFunc; - onSubmit: (idValue: IdValue, value: AnyPrimitive) => Promise; + onSubmit: ( + idValue: IdValue, + value: AnyPrimitive | null, + fieldName?: number + ) => Promise; onDelete: (idValue: IdValue) => void; onScopeChange: (scope: VersionFieldScope) => void; + listField?: FormListFieldData; } function StandardFieldValue({ value: fieldValue, + presentationValue, onSubmit, onDelete, onScopeChange, state, - dispatch + dispatch, + listField }: IStandardFieldValueProps): JSX.Element { const {t, i18n} = useSharedTranslation(); const {token} = theme.useToken(); @@ -225,16 +242,6 @@ function StandardFieldValue({ } }, [fieldValue.isEditing]); - // Cancel value editing if value details panel is closed - useEffect(() => { - if (editRecordState.activeValue === null && fieldValue.isEditing) { - dispatch({ - type: StandardFieldReducerActionsTypes.CANCEL_EDITING, - idValue: fieldValue.idValue - }); - } - }, [editRecordState.activeValue]); - useEffect(() => { if (fieldValue.isEditing) { editRecordDispatch({ @@ -244,14 +251,11 @@ function StandardFieldValue({ } }, [fieldValue.isEditing, fieldValue.editingValue]); - const _handleSubmit = async (valueToSave: StandardValueTypes, id?: string) => { - if (valueToSave === '') { - return _handleDelete(); - } - - const convertedValue = typeof valueToSave === 'object' ? JSON.stringify(valueToSave) : valueToSave; + const _handleSubmit = async (valueToSave: StandardValueTypes) => { + const convertedValue = + valueToSave === null ? null : typeof valueToSave === 'object' ? JSON.stringify(valueToSave) : valueToSave; - await onSubmit(fieldValue.idValue, convertedValue); + await onSubmit(fieldValue.idValue, convertedValue, listField?.name); }; const _handlePressEnter = async () => { @@ -562,15 +566,65 @@ function StandardFieldValue({ const attributeFormatsWithoutDS = [AttributeFormat.extended]; + const commonProps = { + state, + handleSubmit: _handleSubmit, + attribute, + presentationValue + }; + + let valueContent: ReactNode; + if (isValuesListEnabled) { + valueContent = ; + } else { + 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; + case AttributeFormat.color: + valueContent = ; + } + } + return ( <> {attributeFormatsWithDS.includes(attribute.format) && ( - + + {valueContent} + )} {attributeFormatsWithoutDS.includes(attribute.format) && ( 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 deleted file mode 100644 index 3a9d0f519..000000000 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueDisplayHandler.tsx +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 -// This file is released under LGPL V3 -// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {AttributeFormat, RecordFormAttributeStandardAttributeFragment} from '_ui/_gqlTypes'; -import {ComponentProps, FunctionComponent, ReactNode} 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'; -import {DSColorPickerWrapper} from '../DSColorPickerWrapper'; -import {MonoValueSelect} from '../ValuesList/MonoValueSelect'; - -interface IStandardFieldValueDisplayHandlerProps { - state: IStandardFieldReducerState; - attribute: RecordFormAttributeStandardAttributeFragment; - fieldValue: IStandardFieldValue; - shouldShowValueDetailsButton?: boolean; - handleSubmit: (value: StandardValueTypes, id?: string) => void; -} - -export const StandardFieldValueDisplayHandler: FunctionComponent = ({ - attribute, - fieldValue, - handleSubmit -}) => { - const {t} = useTranslation(); - const {state: editRecordState, dispatch: editRecordDispatch} = 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, - editRecordDispatch, - handleSubmit, - handleBlur: _handleBlur, - attribute, - fieldValue, - shouldShowValueDetailsButton: editRecordState.withInfoButton - }; - - const isValuesListEnabled = Boolean(attribute.values_list?.enable); - - let valueContent: ReactNode; - if (isValuesListEnabled) { - valueContent = ; - } else { - 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; - case AttributeFormat.color: - valueContent = ; - } - } - - return ( - <> -
- -
- - {valueContent} - - - ); -}; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/ColorPickerBlock.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/ColorPickerBlock.tsx deleted file mode 100644 index d6dbc53d4..000000000 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/ColorPickerBlock.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 -// This file is released under LGPL V3 -// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {FunctionComponent} from 'react'; -import styled from 'styled-components'; - -interface IColorPickerBlockProps { - isValueDefaultColor: boolean; - color: string; -} - -const ColorPickerColorBlock = styled.div` - height: 32px; - width: 32px; - border-radius: calc(var(--general-border-radius-xs) * 1px); - box-shadow: inset 0 0 1px 0 rgba(0, 0, 0, 0.25); - background-image: conic-gradient( - rgba(0, 0, 0, 0.06) 0 25%, - transparent 0 50%, - rgba(0, 0, 0, 0.06) 0 75%, - transparent 0 - ); - background-size: 50% 50%; -`; - -const ColorPickerColorBlockInner = styled.div<{$color: string}>` - width: 100%; - height: 100%; - border: 1px solid rgba(0, 0, 0, 0.06); - border-radius: calc(var(--general-border-radius-xs) * 1px); - background-color: ${({$color}) => $color}; -`; - -const ColorPickerClear = styled.div` - width: 24px; - height: 24px; - margin: calc(var(--general-spacing-xxs) * 1px) 0; - border-radius: calc(var(--general-border-radius-xs) * 1px); - border: 1px solid rgba(5, 5, 5, 0.06); - position: relative; - - &::after { - content: ''; - position: absolute; - inset-inline-end: 1px; - top: 0; - display: block; - width: 28px; - height: 2px; - transform-origin: right; - transform: rotate(-45deg); - background-color: var(--general-utilities-error-default); - } -`; - -export const ColorPickerBlock: FunctionComponent = ({isValueDefaultColor, color}) => - isValueDefaultColor ? ( - - ) : ( - - - - ); 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 deleted file mode 100644 index ba53bd2e8..000000000 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/StandardFieldValueRead/StandardFieldValueRead.tsx +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 -// This file is released under LGPL V3 -// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -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'; -import {ColorPickerBlock} from './ColorPickerBlock'; - -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; $isColorAttribute: boolean}>` - &.bordered > .kit-input-wrapper-content { - width: ${({$width}) => $width}; - - ${({$isColorAttribute}) => $isColorAttribute && 'padding: 3px;'} - } -`; - -const ValueWrapper = styled(KitTypography.Paragraph)<{$highlighted: boolean; $isColorAttribute: boolean}>` - min-height: calc(var(--general-typography-fontSize6) * var(--general-typography-lineHeight6) * 1px); - color: ${({$highlighted}) => - $highlighted ? 'var(--general-colors-primary-400)' : 'var(--general-utilities-text-primary)'}; - font-size: calc(var(--general-typography-fontSize5) * 1px); - line-height: calc(var(--general-typography-fontSize5) * 1px); - display: flex; - flex-direction: row; - align-items: center; - - ${({$isColorAttribute}) => - $isColorAttribute && - `span { - margin-inline-start: 8px; - margin-inline-end: 5px; - } - `} -`; - -const defaultColorValue = '#00000000'; - -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 ? '●●●●●●●' : ''; - case AttributeFormat.color: - return '#' + inheritedValue.value; - default: - return inheritedValue.value; - } - }; - - const _getCalculatedValueForHelper = (calculatedValue: RecordFormElementsValueStandardValue) => { - switch (state.attribute.format) { - case AttributeFormat.date_range: - return t('record_edition.date_range_from_to', { - from: calculatedValue.value.from, - to: calculatedValue.value.to - }); - case AttributeFormat.encrypted: - return calculatedValue.value ? '●●●●●●●' : ''; - case AttributeFormat.color: - return '#' + calculatedValue.value; - default: - return calculatedValue.value; - } - }; - - const _handleFocus = (e: SyntheticEvent) => { - if (state.isReadOnly) { - return; - } - onClick(e); - }; - - let displayValue = - fieldValue.value?.payload || - String(fieldValue.displayValue) || - state.calculatedValue?.payload || - state.inheritedValue?.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.color: - if (!displayValue) { - displayValue = defaultColorValue; - } else { - displayValue = '#' + displayValue; - } - width = 'fit-content'; - break; - case AttributeFormat.date: - width = '185px'; - break; - } - - const _getHelper = () => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: _getInheritedValueForHelper(state.inheritedValue) - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: _getCalculatedValueForHelper(state.calculatedValue) - }); - } - return; - }; - - const isValueHighlighted = state.isInheritedNotOverrideValue || state.isCalculatedNotOverrideValue; - const isColorAttribute = state.attribute.format === AttributeFormat.color; - const isValueDefaultColor = displayValue === defaultColorValue; - - return ( - - - {isColorAttribute && ( - - )} - - - - ); -}; 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 deleted file mode 100644 index 176d298e8..000000000 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/StandardFieldValueDisplayHandler/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 -// This file is released under LGPL V3 -// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -export {StandardFieldValueDisplayHandler} from './StandardFieldValueDisplayHandler'; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.test.tsx index e5f332696..7bd9730e2 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.test.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.test.tsx @@ -6,23 +6,20 @@ import {MonoValueSelect} from './MonoValueSelect'; import {mockFormElementInput} from '_ui/__mocks__/common/form'; import {mockAttributeSimple, mockAttributeWithDetails, mockFormAttribute} from '_ui/__mocks__/common/attribute'; import * as gqlTypes from '_ui/_gqlTypes'; +import * as useEditRecordReducer from '_ui/components/RecordEdition/editRecordReducer/useEditRecordReducer'; import {mockRecord} from '_ui/__mocks__/common/record'; import {AntForm} from 'aristid-ds'; import userEvent from '@testing-library/user-event'; import {RecordFormAttributeStandardAttributeFragment} from '_ui/_gqlTypes'; import {VersionFieldScope} from '_ui/components/RecordEdition/EditRecordContent/_types'; +import {IStandardFieldReducerState} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer'; import { - CalculatedFlags, - InheritedFlags, - IStandardFieldReducerState, - IStandardFieldValue -} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer'; -import {EditRecordReducerActionsTypes} from '_ui/components/RecordEdition/editRecordReducer/editRecordReducer'; + EditRecordReducerActionsTypes, + IEditRecordReducerState +} from '_ui/components/RecordEdition/editRecordReducer/editRecordReducer'; describe('', () => { const handleSubmitMock = jest.fn(); - const handleBlurMock = jest.fn(); - const dispatch = jest.fn(); const mockSaveAttributeMutation = jest.fn().mockReturnValue({ data: { @@ -80,21 +77,8 @@ describe('', () => { isCalculatedValue: false } satisfies IStandardFieldReducerState; - const fieldValue: IStandardFieldValue = { - idValue: '12', - index: 12, - value: null, - displayValue: '12', - editingValue: '12', - originRawValue: '12', - isEditing: false, - isErrorDisplayed: false, - state: null - }; - afterEach(() => { handleSubmitMock.mockClear(); - handleBlurMock.mockClear(); }); describe('errors', () => { @@ -104,11 +88,9 @@ describe('', () => { render( ), 'MonoValueSelect should be used inside a antd Form.Item' @@ -123,11 +105,9 @@ describe('', () => {
@@ -143,11 +123,9 @@ describe('', () => {
@@ -169,132 +147,6 @@ describe('', () => { expect(handleSubmitMock).toHaveBeenCalledWith('', attribute.id); }); - describe('calulated & inherited values', () => { - it.each` - calculatedValue | inheritedValue | displayedValue - ${'calculated'} | ${null} | ${'calculated'} - ${null} | ${'inherited'} | ${'inherited'} - ${'calculated'} | ${'inherited'} | ${'inherited'} - `( - 'should display calculated / inherited value in helper', - ({ - calculatedValue, - inheritedValue, - displayedValue - }: { - calculatedValue: string | null; - inheritedValue: string | null; - displayedValue: string; - }) => { - const stateWithInheritedValue: InheritedFlags = { - inheritedValue: {raw_value: inheritedValue}, - isInheritedValue: true, - isInheritedNotOverrideValue: false, - isInheritedOverrideValue: true - }; - const stateWithCalculatedValue: CalculatedFlags = { - calculatedValue: {raw_value: calculatedValue}, - isCalculatedValue: true, - isCalculatedNotOverrideValue: false, - isCalculatedOverrideValue: true - }; - - let newState = state; - if (inheritedValue) { - newState = {...newState, ...stateWithInheritedValue} as any; - } - - if (calculatedValue) { - newState = {...newState, ...stateWithCalculatedValue} as any; - } - - render( - - - - - - ); - - expect(screen.getByText(new RegExp(`${displayedValue}`))).toBeVisible(); - } - ); - - it.each` - calculatedValue | inheritedValue | displayedValue - ${'calculated'} | ${null} | ${'calculated'} - ${null} | ${'inherited'} | ${'inherited'} - ${'calculated'} | ${'inherited'} | ${'inherited'} - `( - 'should revert to calculated / inherited value on click on clear icon', - async ({ - calculatedValue, - inheritedValue, - displayedValue - }: { - calculatedValue: string | null; - inheritedValue: string | null; - displayedValue: string; - }) => { - const stateWithInheritedValue: InheritedFlags = { - inheritedValue: {raw_value: inheritedValue}, - isInheritedValue: true, - isInheritedNotOverrideValue: true, - isInheritedOverrideValue: false - }; - const stateWithCalculatedValue: CalculatedFlags = { - calculatedValue: {raw_value: calculatedValue}, - isCalculatedValue: true, - isCalculatedNotOverrideValue: true, - isCalculatedOverrideValue: false - }; - - let newState = state; - if (inheritedValue) { - newState = {...newState, ...stateWithInheritedValue} as any; - } - - if (calculatedValue) { - newState = {...newState, ...stateWithCalculatedValue} as any; - } - - render( - - - - - - ); - - expect(screen.getByTestId(attribute.id).innerHTML).not.toMatch(new RegExp(displayedValue)); - const select = screen.getByRole('combobox'); - await userEvent.click(select); - - const green = screen.getAllByText('green').pop(); - await userEvent.click(green); - - const clearIcon = screen.getByLabelText('clear'); - await userEvent.click(clearIcon); - - expect(screen.getByTestId(attribute.id).innerHTML).toMatch(new RegExp(displayedValue)); - } - ); - }); - describe('search with result', () => { it('should display a specific text on search with results', async () => { render( @@ -302,11 +154,9 @@ describe('', () => {
@@ -316,6 +166,8 @@ describe('', () => { await userEvent.click(select); await userEvent.type(select, 'r'); + await new Promise(resolve => setTimeout(resolve, 3000)); // 3 sec + expect(screen.getByText('record_edition.press_enter_to')).toBeVisible(); expect(screen.getByText('record_edition.select_this_value')).toBeVisible(); }); @@ -327,14 +179,7 @@ describe('', () => { render( - + ); @@ -354,10 +199,7 @@ describe('', () => { @@ -376,7 +218,13 @@ describe('', () => { }); it('should display the option and create it with list update', async () => { - const editRecordDispatch = jest.fn(); + const mockEditRecordDispatch = jest.fn(); + const editRecordState = {}; + jest.spyOn(useEditRecordReducer, 'useEditRecordReducer').mockImplementation(() => ({ + state: editRecordState as IEditRecordReducerState, + dispatch: mockEditRecordDispatch + })); + render( @@ -386,10 +234,7 @@ describe('', () => { values_list: {...valuesList, allowFreeEntry: true, allowListUpdate: true} }} state={state} - editRecordDispatch={editRecordDispatch} - fieldValue={{...fieldValue, isEditing: true}} handleSubmit={handleSubmitMock} - handleBlur={handleBlurMock} /> @@ -404,7 +249,7 @@ describe('', () => { await userEvent.click(newColorOption); expect(handleSubmitMock).toHaveBeenCalledWith(newColor, attribute.id); - expect(editRecordDispatch).toHaveBeenCalledWith({type: EditRecordReducerActionsTypes.REQUEST_REFRESH}); + expect(mockEditRecordDispatch).toHaveBeenCalledWith({type: EditRecordReducerActionsTypes.REQUEST_REFRESH}); expect(screen.getByTestId(attribute.id).innerHTML).toMatch(new RegExp(newColor)); }); }); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.tsx index fee22815c..44dd5e36f 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/MonoValueSelect.tsx @@ -1,18 +1,18 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {FunctionComponent, useEffect, useMemo, useRef, useState} from 'react'; +import {FunctionComponent, useMemo, useState} from 'react'; import {KitSelect, KitTypography} from 'aristid-ds'; import useSharedTranslation from '_ui/hooks/useSharedTranslation/useSharedTranslation'; import {AttributeFormat, useSaveAttributeMutation} from '_ui/_gqlTypes'; -import {Form, GetRef} from 'antd'; -import {useValueDetailsButton} from '_ui/components/RecordEdition/EditRecordContent/shared/ValueDetailsBtn/useValueDetailsButton'; -import {useLang} from '_ui/hooks'; -import {localizedTranslation} from '@leav/utils'; +import {Form} from 'antd'; import moment from 'moment'; import {stringifyDateRangeValue} from '_ui/_utils'; -import {IDateRangeValuesListConf, IMonoValueSelectProps, IStringValuesListConf} from './_types'; +import {IDateRangeValuesListConf, IStringValuesListConf} from './_types'; import {EditRecordReducerActionsTypes} from '_ui/components/RecordEdition/editRecordReducer/editRecordReducer'; +import {useEditRecordReducer} from '_ui/components/RecordEdition/editRecordReducer/useEditRecordReducer'; +import {IStandFieldValueContentProps} from '../_types'; +import {IKitSelect} from 'aristid-ds/dist/Kit/DataEntry/Select/types'; interface IOption { label: string; @@ -33,16 +33,13 @@ const addOption = (options: IOption[], optionToAdd: IOption) => { return newOptions; }; -export const MonoValueSelect: FunctionComponent = ({ +export const MonoValueSelect: FunctionComponent> = ({ value, + presentationValue, onChange, state, - editRecordDispatch, attribute, - fieldValue, - handleSubmit, - handleBlur, - shouldShowValueDetailsButton = false + handleSubmit }) => { if (!onChange) { throw Error('MonoValueSelect should be used inside a antd Form.Item'); @@ -54,25 +51,14 @@ export const MonoValueSelect: FunctionComponent = ({ const {t} = useSharedTranslation(); const {errors} = Form.Item.useStatus(); - const {onValueDetailsButtonClick} = useValueDetailsButton({ - value: fieldValue?.value, - attribute - }); - const [isSelectOpen, setIsSelectOpen] = useState(false); + const [isFocused, setIsFocused] = useState(false); const [searchedString, setSearchedString] = useState(''); - const {lang: availableLang} = useLang(); - const selectRef = useRef>(null); const [saveAttribute] = useSaveAttributeMutation(); + const {dispatch: editRecordDispatch} = useEditRecordReducer(); + const allowFreeEntry = attribute.values_list.allowFreeEntry; const allowListUpdate = attribute.values_list.allowListUpdate; - useEffect(() => { - if (fieldValue.isEditing && selectRef.current) { - selectRef.current.focus(); - setIsSelectOpen(true); - } - }, [fieldValue.isEditing]); - const _getFilteredValuesList = () => { let values = []; @@ -115,22 +101,16 @@ export const MonoValueSelect: FunctionComponent = ({ [searchedString] ); - const _resetToInheritedOrCalculatedValue = () => { + const _resetToInheritedOrCalculatedValue = async () => { if (state.isInheritedValue) { setTimeout(() => onChange(state.inheritedValue.raw_value, options), 0); } else if (state.isCalculatedValue) { setTimeout(() => onChange(state.calculatedValue.raw_value, options), 0); } - handleSubmit('', attribute.id); - }; - - const _handleOnBlur = () => { - handleBlur(); - setIsSelectOpen(false); + await handleSubmit(null, attribute.id); }; const _handleOnChange = async (selectedValue: string) => { - setIsSelectOpen(false); setSearchedString(''); if ((state.isInheritedValue || state.isCalculatedValue) && selectedValue === '') { _resetToInheritedOrCalculatedValue(); @@ -154,50 +134,39 @@ export const MonoValueSelect: FunctionComponent = ({ editRecordDispatch({type: EditRecordReducerActionsTypes.REQUEST_REFRESH}); } - handleSubmit(selectedValue, attribute.id); + await handleSubmit(selectedValue, attribute.id); onChange(selectedValue, options); }; - const _handleOnClear = () => { - _handleOnChange(''); - handleBlur(); + const _handleOnClear = async () => { + await _handleOnChange(''); }; - const helper = useMemo(() => { - if (state.isInheritedOverrideValue) { - return t('record_edition.inherited_input_helper', { - inheritedValue: state.inheritedValue.raw_value - }); - } else if (state.isCalculatedOverrideValue) { - return t('record_edition.calculated_input_helper', { - calculatedValue: state.calculatedValue.raw_value - }); + const _handleOnSearch = async (search: string) => { + setSearchedString(search); + if (search) { + setIsFocused(true); } - return ''; - }, [state.isInheritedOverrideValue, state.isCalculatedOverrideValue]); + }; - const label = localizedTranslation(state.formElement.settings.label, availableLang); const required = state.formElement.settings.required; + const valueToDisplay = isFocused ? value : presentationValue; + return ( 0 && 'error'} showSearch - helper={helper} + onDropdownVisibleChange={visible => setIsFocused(visible)} onSelect={_handleOnChange} onChange={onChange} onClear={_handleOnClear} - onBlur={_handleOnBlur} - onSearch={setSearchedString} - onInfoClick={shouldShowValueDetailsButton ? onValueDetailsButtonClick : null} + onSearch={_handleOnSearch} dropdownRender={menu => ( <> {searchedString !== '' && searchResultsCount > 0 && ( diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/_types.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/_types.ts index b0eb8a7d7..83e11bcf8 100644 --- a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/_types.ts +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/ValuesList/_types.ts @@ -7,22 +7,14 @@ import { StandardValuesListFragmentStandardStringValuesListConfFragment } from '_ui/_gqlTypes'; import {IProvidedByAntFormItem} from '_ui/components/RecordEdition/EditRecordContent/_types'; -import { - IStandardFieldReducerState, - IStandardFieldValue -} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer'; -import {IEditRecordReducerActions} from '_ui/components/RecordEdition/editRecordReducer/editRecordReducer'; +import {IStandardFieldReducerState} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer'; import {SelectProps} from 'antd'; -import {Dispatch} from 'react'; export interface IMonoValueSelectProps extends IProvidedByAntFormItem { + presentationValue: string; state: IStandardFieldReducerState; - editRecordDispatch: Dispatch; attribute: RecordFormAttributeStandardAttributeFragment; - fieldValue: IStandardFieldValue; - handleSubmit: (value: string, id?: string) => void; - handleBlur: () => void; - shouldShowValueDetailsButton?: boolean; + handleSubmit: (value: string, id?: string) => Promise; } export type IStringValuesListConf = StandardValuesListFragmentStandardStringValuesListConfFragment; diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/_types.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/_types.ts new file mode 100644 index 000000000..467162c9e --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/uiElements/StandardField/StandardFieldValue/_types.ts @@ -0,0 +1,13 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {RecordFormAttributeStandardAttributeFragment} from '_ui/_gqlTypes'; +import {IProvidedByAntFormItem, StandardValueTypes} from '_ui/components/RecordEdition/EditRecordContent/_types'; +import {IStandardFieldReducerState} from '_ui/components/RecordEdition/EditRecordContent/reducers/standardFieldReducer/standardFieldReducer'; + +export interface IStandFieldValueContentProps extends IProvidedByAntFormItem { + presentationValue?: string; + state: IStandardFieldReducerState; + attribute?: RecordFormAttributeStandardAttributeFragment; + handleSubmit: (value: StandardValueTypes, id?: string) => Promise; +} diff --git a/libs/ui/src/hooks/useGetRecordForm/useGetRecordForm.ts b/libs/ui/src/hooks/useGetRecordForm/useGetRecordForm.ts index 930c37222..916511f50 100644 --- a/libs/ui/src/hooks/useGetRecordForm/useGetRecordForm.ts +++ b/libs/ui/src/hooks/useGetRecordForm/useGetRecordForm.ts @@ -71,6 +71,8 @@ export type IRecordForm = Override< } >; +export type RecordFormElementAttribute = RecordFormElement['attribute']; + export interface IUseGetRecordFormHook { loading: boolean; error: ApolloError; diff --git a/libs/ui/src/locales/en/shared.json b/libs/ui/src/locales/en/shared.json index 9bfa5f87f..706b387fc 100644 --- a/libs/ui/src/locales/en/shared.json +++ b/libs/ui/src/locales/en/shared.json @@ -423,6 +423,7 @@ "metadata_section": "Value properties", "metadata_section_link": "Link properties", "value_length": "{{length}} characters", + "delete_all": "Delete all", "delete_all_values": "Delete all values", "delete_all_values_confirm": "Do you really want to delete all values?", "value_details_tooltip": "More details", @@ -453,7 +454,6 @@ "cancel_confirm_modal_title": "Confirm cancellation?", "cancel_confirm_modal_content": "You are about to cancel the creation.", "cancel_confirm_modal_question": "Are you sure?", - "record_select": "Select a record", "inherited_input_helper": "Inherited value: {{- inheritedValue}}", "calculated_input_helper": "Calculated value: {{- calculatedValue}}", "date_range_from_to": "from {{- from}} to {{- to}}", @@ -463,7 +463,17 @@ "press_enter_to": "Press enter to ", "select_this_value": "Select this value", "select_option": "Select option ", - "create_and_select_option": "Create and select option " + "create_and_select_option": "Create and select option ", + "placeholder": { + "record_select": "Select a record", + "enter_a_text": "Enter a text", + "enter_a_date": "Enter a date (e.g. 12/25/2024)", + "enter_a_period": "Enter a period", + "enter_a_number": "Enter a number", + "enter_a_password": "Enter a password", + "start_date": "Start date", + "end_date": "End date" + } }, "record_summary": { "preview_title": "Click here to visualize preview.", diff --git a/libs/ui/src/locales/fr/shared.json b/libs/ui/src/locales/fr/shared.json index 4aa7b8880..36bfedfd8 100644 --- a/libs/ui/src/locales/fr/shared.json +++ b/libs/ui/src/locales/fr/shared.json @@ -423,6 +423,7 @@ "metadata_section": "Propriétés de la valeur", "metadata_section_link": "Propriétés de la liaison", "value_length": "{{length}} caractère(s)", + "delete_all": "Tout supprimer", "delete_all_values": "Supprimer toutes les valeurs", "delete_all_values_confirm": "Êtes-vous sûr de vouloir supprimer toutes les valeurs?", "value_details_tooltip": "Plus d'informations", @@ -453,7 +454,6 @@ "cancel_confirm_modal_title": "Confirmer l'abandon?", "cancel_confirm_modal_content": "Vous êtes sur le point d'abandonner la création.", "cancel_confirm_modal_question": "Êtes-vous sûr(e) ?", - "record_select": "Sélectionnez un élément", "inherited_input_helper": "Valeur héritée : {{- inheritedValue }}", "calculated_input_helper": "Valeur calculée : {{- calculatedValue}}", "date_range_from_to": "du {{- from}} au {{- to}}", @@ -463,7 +463,17 @@ "press_enter_to": "Appuyer sur Entrer pour ", "select_this_value": "Sélectionner cette valeur", "select_option": "Sélectionner l’option ", - "create_and_select_option": "Créer et sélectionner l’option " + "create_and_select_option": "Créer et sélectionner l’option ", + "placeholder": { + "record_select": "Sélectionnez un élément", + "enter_a_text": "Saisir un texte", + "enter_a_date": "Saisir une date (ex: 25/12/2024)", + "enter_a_period": "Saisir une période", + "enter_a_number": "Saisir un nombre", + "enter_a_password": "Saisir un mot de passe", + "start_date": "Date de début", + "end_date": "Date de fin" + } }, "record_summary": { "preview_title": "Cliquez ici pour voir l’aperçu.", diff --git a/yarn.lock b/yarn.lock index c5bc5c019..77202b9fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8110,7 +8110,7 @@ __metadata: "@ant-design/icons": ">=5.2" "@apollo/client": ">=3.8.1" antd: 5.15.3 - aristid-ds: 10.1.0-b8e3d81 + aristid-ds: 10.1.0-449b3e9 dayjs: ^1.11.10 i18next: 22.5 react: 18.2.0 @@ -12451,9 +12451,9 @@ __metadata: languageName: node linkType: hard -"aristid-ds@npm:10.1.0-b8e3d81": - version: 10.1.0-b8e3d81 - resolution: "aristid-ds@npm:10.1.0-b8e3d81" +"aristid-ds@npm:10.1.0-449b3e9": + version: 10.1.0-449b3e9 + resolution: "aristid-ds@npm:10.1.0-449b3e9" dependencies: "@dnd-kit/core": "npm:^6.1.0" "@dnd-kit/modifiers": "npm:^7.0.0" @@ -12509,7 +12509,7 @@ __metadata: react-uuid: ^2.0.0 remark-gfm: ^3.0.1 styled-components: ^6.0.7 - checksum: f6e0828f6c4018145c1cd5e85079d22112effd709bfa87efba518d8b644954aeb7dc58304791b9a8d33d03b956d058cf205f05929f72a28c19e4f8f97cc21155 + checksum: cb83e8c52948ff37d2eac69ada92cd3fc2391fb5162b736881c1c2b4ecdce63f23cb8e463604faa8259a72f109bca128b741bf1334a8ebeb437e85b47673ec0a languageName: node linkType: hard @@ -15688,7 +15688,7 @@ __metadata: apollo: "npm:2.34.0" apollo-cache-inmemory: "npm:1.6.6" apollo-upload-client: "npm:14.1.3" - aristid-ds: "npm:10.1.0-b8e3d81" + aristid-ds: "npm:10.1.0-449b3e9" commander: "npm:5.1.0" dayjs: "npm:1.11.10" graphql: "npm:15.0.0" @@ -24178,7 +24178,7 @@ __metadata: "@types/react-dom": "npm:18.2.6" "@vitejs/plugin-react": "npm:3.1.0" antd: "npm:5.15.3" - aristid-ds: "npm:10.1.0-b8e3d81" + aristid-ds: "npm:10.1.0-449b3e9" i18next: "npm:22.5.0" i18next-browser-languagedetector: "npm:7.0.2" i18next-http-backend: "npm:2.1.1" @@ -24797,8 +24797,8 @@ __metadata: linkType: hard "micromark-core-commonmark@npm:^2.0.0": - version: 2.0.1 - resolution: "micromark-core-commonmark@npm:2.0.1" + version: 2.0.2 + resolution: "micromark-core-commonmark@npm:2.0.2" dependencies: decode-named-character-reference: "npm:^1.0.0" devlop: "npm:^1.0.0" @@ -24816,7 +24816,7 @@ __metadata: micromark-util-subtokenize: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 15e788b3222401572ff8f549f8ecba21fa3395c000b8005e47204e8c97200e98bb0652c2c648e357b0996f1b50a7a63cc43e849f2976e4845b4453049040f8cc + checksum: eafa6b9cd6fd9f51efa7795824af9a765e24a4519855a5b6dfcb0f619a93d90599d39a261f626bfcc1dfa64f22430f7a677a83cb6ce4bd8e4eeabc892610c016 languageName: node linkType: hard @@ -24914,195 +24914,195 @@ __metadata: linkType: hard "micromark-factory-destination@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-destination@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-factory-destination@npm:2.0.1" dependencies: micromark-util-character: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: d36e65ed1c072ff4148b016783148ba7c68a078991154625723e24bda3945160268fb91079fb28618e1613c2b6e70390a8ddc544c45410288aa27b413593071a + checksum: 9c4baa9ca2ed43c061bbf40ddd3d85154c2a0f1f485de9dea41d7dd2ad994ebb02034a003b2c1dbe228ba83a0576d591f0e90e0bf978713f84ee7d7f3aa98320 languageName: node linkType: hard "micromark-factory-label@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-label@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-factory-label@npm:2.0.1" dependencies: devlop: "npm:^1.0.0" micromark-util-character: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: c021dbd0ed367610d35f2bae21209bc804d1a6d1286ffce458fd6a717f4d7fe581a7cba7d5c2d7a63757c44eb927c80d6a571d6ea7969fae1b48ab6461d109c4 + checksum: bd03f5a75f27cdbf03b894ddc5c4480fc0763061fecf9eb927d6429233c930394f223969a99472df142d570c831236134de3dc23245d23d9f046f9d0b623b5c2 languageName: node linkType: hard "micromark-factory-space@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-space@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-factory-space@npm:2.0.1" dependencies: micromark-util-character: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 4ffdcdc2f759887bbb356500cb460b3915ecddcb5d85c3618d7df68ad05d13ed02b1153ee1845677b7d8126df8f388288b84fcf0d943bd9c92bcc71cd7222e37 + checksum: 1bd68a017c1a66f4787506660c1e1c5019169aac3b1cb075d49ac5e360e0b2065e984d4e1d6e9e52a9d44000f2fa1c98e66a743d7aae78b4b05616bf3242ed71 languageName: node linkType: hard "micromark-factory-title@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-title@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-factory-title@npm:2.0.1" dependencies: micromark-factory-space: "npm:^2.0.0" micromark-util-character: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 39e1ac23af3554e6e652e56065579bc7faf21ade7b8704b29c175871b4152b7109b790bb3cae0f7e088381139c6bac9553b8400772c3d322e4fa635f813a3578 + checksum: b4d2e4850a8ba0dff25ce54e55a3eb0d43dda88a16293f53953153288f9d84bcdfa8ca4606b2cfbb4f132ea79587bbb478a73092a349f893f5264fbcdbce2ee1 languageName: node linkType: hard "micromark-factory-whitespace@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-whitespace@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-factory-whitespace@npm:2.0.1" dependencies: micromark-factory-space: "npm:^2.0.0" micromark-util-character: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 9587c2546d1a58b4d5472b42adf05463f6212d0449455285662d63cd8eaed89c6b159ac82713fcee5f9dd88628c24307d9533cccd8971a2f3f4d48702f8f850a + checksum: 67b3944d012a42fee9e10e99178254a04d48af762b54c10a50fcab988688799993efb038daf9f5dbc04001a97b9c1b673fc6f00e6a56997877ab25449f0c8650 languageName: node linkType: hard "micromark-util-character@npm:^2.0.0": - version: 2.1.0 - resolution: "micromark-util-character@npm:2.1.0" + version: 2.1.1 + resolution: "micromark-util-character@npm:2.1.1" dependencies: micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 089fe853c2bede2a48fd73d977910fa657c3cf6649eddcd300557a975c6c7f1c73030d01724a483ff1dc69a0d3ac28b43b2ba4210f5ea6414807cdcd0c2fa63c + checksum: 85da8f8e5f7ed16046575bef5b0964ca3fca3162b87b74ae279f1e48eb7160891313eb64f04606baed81c58b514dbdb64f1a9d110a51baaaa79225d72a7b1852 languageName: node linkType: hard "micromark-util-chunked@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-chunked@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-util-chunked@npm:2.0.1" dependencies: micromark-util-symbol: "npm:^2.0.0" - checksum: 324f95cccdae061332a8241936eaba6ef0782a1e355bac5c607ad2564fd3744929be7dc81651315a2921535747a33243e6a5606bcb64b7a56d49b6d74ea1a3d4 + checksum: f8cb2a67bcefe4bd2846d838c97b777101f0043b9f1de4f69baf3e26bb1f9885948444e3c3aec66db7595cad8173bd4567a000eb933576c233d54631f6323fe4 languageName: node linkType: hard "micromark-util-classify-character@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-classify-character@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-util-classify-character@npm:2.0.1" dependencies: micromark-util-character: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 086e52904deffebb793fb1c08c94aabb8901f76958142dfc3a6282890ebaa983b285e69bd602b9d507f1b758ed38e75a994d2ad9fbbefa7de2584f67a16af405 + checksum: 4d8bbe3a6dbf69ac0fc43516866b5bab019fe3f4568edc525d4feaaaf78423fa54e6b6732b5bccbeed924455279a3758ffc9556954aafb903982598a95a02704 languageName: node linkType: hard "micromark-util-combine-extensions@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-combine-extensions@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-util-combine-extensions@npm:2.0.1" dependencies: micromark-util-chunked: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 107c47700343f365b4ed81551e18bc3458b573c500e56ac052b2490bd548adc475216e41d2271633a8867fac66fc22ba3e0a2d74a31ed79b9870ca947eb4e3ba + checksum: 5d22fb9ee37e8143adfe128a72b50fa09568c2cc553b3c76160486c96dbbb298c5802a177a10a215144a604b381796071b5d35be1f2c2b2ee17995eda92f0c8e languageName: node linkType: hard "micromark-util-decode-numeric-character-reference@npm:^2.0.0": - version: 2.0.1 - resolution: "micromark-util-decode-numeric-character-reference@npm:2.0.1" + version: 2.0.2 + resolution: "micromark-util-decode-numeric-character-reference@npm:2.0.2" dependencies: micromark-util-symbol: "npm:^2.0.0" - checksum: 9512507722efd2033a9f08715eeef787fbfe27e23edf55db21423d46d82ab46f76c89b4f960be3f5e50a2d388d89658afc0647989cf256d051e9ea01277a1adb + checksum: ee11c8bde51e250e302050474c4a2adca094bca05c69f6cdd241af12df285c48c88d19ee6e022b9728281c280be16328904adca994605680c43af56019f4b0b6 languageName: node linkType: hard "micromark-util-decode-string@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-decode-string@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-util-decode-string@npm:2.0.1" dependencies: decode-named-character-reference: "npm:^1.0.0" micromark-util-character: "npm:^2.0.0" micromark-util-decode-numeric-character-reference: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" - checksum: a75daf32a4a6b549e9f19b4d833ebfeb09a32a9a1f9ce50f35dec6b6a3e4f9f121f49024ba7f9c91c55ebe792f7c7a332fc9604795181b6a612637df0df5b959 + checksum: 2f517e4c613609445db4b9a17f8c77832f55fb341620a8fd598f083c1227027485d601c2021c2f8f9883210b8671e7b3990f0c6feeecd49a136475465808c380 languageName: node linkType: hard "micromark-util-encode@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-encode@npm:2.0.0" - checksum: 853a3f33fce72aaf4ffa60b7f2b6fcfca40b270b3466e1b96561b02185d2bd8c01dd7948bc31a24ac014f4cc854e545ca9a8e9cf7ea46262f9d24c9e88551c66 + version: 2.0.1 + resolution: "micromark-util-encode@npm:2.0.1" + checksum: be890b98e78dd0cdd953a313f4148c4692cc2fb05533e56fef5f421287d3c08feee38ca679f318e740530791fc251bfe8c80efa926fcceb4419b269c9343d226 languageName: node linkType: hard "micromark-util-html-tag-name@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-html-tag-name@npm:2.0.0" - checksum: d786d4486f93eb0ac5b628779809ca97c5dc60f3c9fc03eb565809831db181cf8cb7f05f9ac76852f3eb35461af0f89fa407b46f3a03f4f97a96754d8dc540d8 + version: 2.0.1 + resolution: "micromark-util-html-tag-name@npm:2.0.1" + checksum: dea365f5ad28ad74ff29fcb581f7b74fc1f80271c5141b3b2bc91c454cbb6dfca753f28ae03730d657874fcbd89d0494d0e3965dfdca06d9855f467c576afa9d languageName: node linkType: hard "micromark-util-normalize-identifier@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-normalize-identifier@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-util-normalize-identifier@npm:2.0.1" dependencies: micromark-util-symbol: "npm:^2.0.0" - checksum: b36da2d3fd102053dadd953ce5c558328df12a63a8ac0e5aad13d4dda8e43b6a5d4a661baafe0a1cd8a260bead4b4a8e6e0e74193dd651e8484225bd4f4e68aa + checksum: 1eb9a289d7da067323df9fdc78bfa90ca3207ad8fd893ca02f3133e973adcb3743b233393d23d95c84ccaf5d220ae7f5a28402a644f135dcd4b8cfa60a7b5f84 languageName: node linkType: hard "micromark-util-resolve-all@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-resolve-all@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-util-resolve-all@npm:2.0.1" dependencies: micromark-util-types: "npm:^2.0.0" - checksum: 31fe703b85572cb3f598ebe32750e59516925c7ff1f66cfe6afaebe0771a395a9eaa770787f2523d3c46082ea80e6c14f83643303740b3d650af7c96ebd30ccc + checksum: 9275f3ddb6c26f254dd2158e66215d050454b279707a7d9ce5a3cd0eba23201021cedcb78ae1a746c1b23227dcc418ee40dd074ade195359506797a5493550cc languageName: node linkType: hard "micromark-util-sanitize-uri@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-sanitize-uri@npm:2.0.0" + version: 2.0.1 + resolution: "micromark-util-sanitize-uri@npm:2.0.1" dependencies: micromark-util-character: "npm:^2.0.0" micromark-util-encode: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" - checksum: 7d10622f5a2bb058dda6d2e95b2735c43fdf8daa4f88a0863bc90eef6598f8e10e3df98e034341fcbc090d8021c53501308c463c49d3fe91f41eb64b5bf2766e + checksum: 064c72abfc9777864ca0521a016dde62ab3e7af5215d10fd27e820798500d5d305da638459c589275c1a093cf588f493cc2f65273deac5a5331ecefc6c9ea78a languageName: node linkType: hard "micromark-util-subtokenize@npm:^2.0.0": - version: 2.0.1 - resolution: "micromark-util-subtokenize@npm:2.0.1" + version: 2.0.2 + resolution: "micromark-util-subtokenize@npm:2.0.2" dependencies: devlop: "npm:^1.0.0" micromark-util-chunked: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: 8e1cae8859bcc3eed54c0dc896d9c2141c990299696455124205ce538e084caeaafcbe0d459a39b81cd45e761ff874d773dbf235ab6825914190701a15226789 + checksum: 11ed11173d3e59687f1bb003ab26243182ad194be3dafe5eae08213d403ccbb1bb36869abb8691198779883b325ec306546f7cde641d803e41240af4dcf0ee9c languageName: node linkType: hard "micromark-util-symbol@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-symbol@npm:2.0.0" - checksum: 8c662644c326b384f02a5269974d843d400930cf6f5d6a8e6db1743fc8933f5ecc125b4203ad4ebca25447f5d23eb7e5bf1f75af34570c3fdd925cb618752fcd + version: 2.0.1 + resolution: "micromark-util-symbol@npm:2.0.1" + checksum: 497e6d95fc21c2bb5265b78a6a60db518c376dc438739b2e7d4aee6f9f165222711724b456c63163314f32b8eea68a064687711d41e986262926eab23ddb9229 languageName: node linkType: hard "micromark-util-types@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-types@npm:2.0.0" - checksum: b88e0eefd4b7c8d86b54dbf4ed0094ef56a3b0c7774d040bd5c8146b8e4e05b1026bbf1cd9308c8fcd05ecdc0784507680c8cee9888a4d3c550e6e574f7aef62 + version: 2.0.1 + resolution: "micromark-util-types@npm:2.0.1" + checksum: 69c5e18e6ba4e12473d6fe5f1a7cc113ac1d4bfc23c7ad57b16a5e4bfd09ef48b7c17a40c39d43996f2078ad898efd3f1945007c14f395abd55f2af03d413acd languageName: node linkType: hard "micromark@npm:^4.0.0": - version: 4.0.0 - resolution: "micromark@npm:4.0.0" + version: 4.0.1 + resolution: "micromark@npm:4.0.1" dependencies: "@types/debug": "npm:^4.0.0" debug: "npm:^4.0.0" @@ -25121,7 +25121,7 @@ __metadata: micromark-util-subtokenize: "npm:^2.0.0" micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" - checksum: a697c1c0c169077f5d5def9af26985baea9d4375395dcb974a96f63761d382b455d4595a60e856c83e653b1272a732e85128d992511d6dc938d61a35bdf98c99 + checksum: b948b1b239e589826bdaf2835daa9e88873e23d4b9148cd22109a86d4af55b96345cf9fc9059b6b19ae828f64d55e66f376ca3aeb4af3d2b0241560125f5dae6 languageName: node linkType: hard @@ -26958,7 +26958,7 @@ __metadata: "@vitejs/plugin-react": "npm:3.1.0" antd: "npm:5.15.3" apollo: "npm:2.34.0" - aristid-ds: "npm:10.1.0-b8e3d81" + aristid-ds: "npm:10.1.0-449b3e9" commander: "npm:10.0.0" cross-fetch: "npm:3.1.5" graphql-ws: "npm:5.12.0"