diff --git a/apps/core/src/__tests__/e2e/api/values/values.test.ts b/apps/core/src/__tests__/e2e/api/values/values.test.ts index 8b4990b56..2a690fc76 100644 --- a/apps/core/src/__tests__/e2e/api/values/values.test.ts +++ b/apps/core/src/__tests__/e2e/api/values/values.test.ts @@ -11,6 +11,7 @@ describe('Values', () => { const treeLibName = 'tree_library_test'; const attrSimpleName = 'values_attribute_test_simple'; + const attrSimpleNameWithFormat = 'values_attribute_test_simple_with_format'; const attrSimpleExtendedName = 'values_attribute_test_simple_extended'; const attrSimpleLinkName = 'values_attribute_test_simple_link'; const attrAdvancedName = 'values_attribute_test_adv'; @@ -50,6 +51,16 @@ describe('Values', () => { } }`); + await gqlSaveAttribute({ + id: attrSimpleNameWithFormat, + type: AttributeTypes.SIMPLE, + format: AttributeFormats.TEXT, + label: 'Test attr simple with format', + actionsList: { + getValue: [{id: 'toUppercase', name: 'toUppercase'}] + } + }); + await makeGraphQlCall(`mutation { saveAttribute( attribute: { @@ -184,6 +195,7 @@ describe('Values', () => { "created_by", "created_at", "${attrSimpleName}", + "${attrSimpleNameWithFormat}", "${attrAdvancedName}", "${attrSimpleLinkName}", "${attrAdvancedLinkName}", @@ -625,4 +637,29 @@ describe('Values', () => { expect(res.data.errors[0].extensions.fields[attrDateRangeName]).toBeDefined(); }); }); + + test('Run actions list and format on value', async () => { + const res = await makeGraphQlCall(`query { + runActionsListAndFormatOnValue( + library: "${testLibName}", + value: { + attribute: "${attrSimpleNameWithFormat}", + payload: "test", + metadata: null + }, + version: null + ) { + id_value + payload + raw_payload + } + }`); + + expect(res.status).toBe(200); + + expect(res.data.errors).toBeUndefined(); + expect(res.data.data.runActionsListAndFormatOnValue[0].id_value).toBeNull(); + expect(res.data.data.runActionsListAndFormatOnValue[0].payload).toBe('TEST'); + expect(res.data.data.runActionsListAndFormatOnValue[0].raw_payload).toBe('test'); + }); }); diff --git a/apps/core/src/_types/value.ts b/apps/core/src/_types/value.ts index 28e1f30d5..7ddba84db 100644 --- a/apps/core/src/_types/value.ts +++ b/apps/core/src/_types/value.ts @@ -6,6 +6,17 @@ import {IDbEdge} from 'infra/db/_types'; import {IRecord} from './record'; import {ITreeNode, TreePaths} from './tree'; +export type IValueFromGql = Override< + Omit, + { + value: IValue['payload']; + metadata: Array<{ + name: string; + value: string; + }>; + } +>; + export interface IValueVersion { [treeName: string]: string; } diff --git a/apps/core/src/app/core/valueApp.ts b/apps/core/src/app/core/valueApp.ts index db529721c..812d2ab15 100644 --- a/apps/core/src/app/core/valueApp.ts +++ b/apps/core/src/app/core/valueApp.ts @@ -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 {IKeyValue, objectToNameValueArray} from '@leav/utils'; +import {IKeyValue, objectToNameValueArray, Override} from '@leav/utils'; import {ConvertVersionFromGqlFormatFunc} from 'app/helpers/convertVersionFromGqlFormat'; import {IAttributeDomain} from 'domain/attribute/attributeDomain'; import {IRecordDomain} from 'domain/record/recordDomain'; @@ -10,7 +10,7 @@ import {IUtils} from 'utils/utils'; import {IAppGraphQLSchema} from '_types/graphql'; import {IQueryInfos} from '_types/queryInfos'; import {IRecord} from '_types/record'; -import {IStandardValue, ITreeValue, IValue, IValueVersion} from '_types/value'; +import {IStandardValue, ITreeValue, IValue, IValueFromGql, IValueVersion, IValueVersionFromGql} from '_types/value'; import {AttributeTypes, IAttribute} from '../../_types/attribute'; import {AttributeCondition} from '../../_types/record'; @@ -232,6 +232,10 @@ export default function ({ deleteValue(library: ID!, recordId: ID!, attribute: ID!, value: ValueInput): [GenericValue!]! } + + extend type Query { + runActionsListAndFormatOnValue(library: ID, value: ValueBatchInput, version: [ValueVersionInput]): [Value!]! + } `, resolvers: { Mutation: { @@ -300,6 +304,29 @@ export default function ({ }); } }, + Query: { + async runActionsListAndFormatOnValue( + _: never, + { + library, + value, + version + }: {library: string; value: IValueFromGql; version: IValueVersionFromGql}, + ctx: IQueryInfos + ): Promise { + const convertedValue = { + ...value, + version: convertVersionFromGqlFormat(version), + metadata: utils.nameValArrayToObj(value.metadata) + }; + + return valueDomain.runActionsListAndFormatOnValue({ + library, + value: convertedValue, + ctx + }); + } + }, GenericValue: { __resolveType: async (fieldValue, _, ctx) => { const attribute = Array.isArray(fieldValue) diff --git a/apps/core/src/domain/value/valueDomain.spec.ts b/apps/core/src/domain/value/valueDomain.spec.ts index 2a5311ecb..449196578 100644 --- a/apps/core/src/domain/value/valueDomain.spec.ts +++ b/apps/core/src/domain/value/valueDomain.spec.ts @@ -2368,4 +2368,115 @@ describe('ValueDomain', () => { await expect(getVal).rejects.toHaveProperty('fields.recordId'); }); }); + + describe('runActionsListAndFormatOnValue', () => { + test('Should return the same value if no actions list', async function () { + const payload = 'test val'; + + const mockAttrDomain: Mockify = { + getAttributeProperties: global.__mockPromise({...mockAttribute, type: AttributeTypes.SIMPLE}) + }; + + const valDomain = valueDomain({ + ...depsBase, + 'core.domain.attribute': mockAttrDomain as IAttributeDomain, + 'core.infra.record': mockRecordRepo as IRecordRepo, + 'core.domain.actionsList': mockActionsListDomain as any, + 'core.domain.helpers.validate': mockValidateHelper as IValidateHelper, + 'core.utils': mockUtilsStandardAttribute as IUtils + }); + + const resValue = await valDomain.runActionsListAndFormatOnValue({ + library: 'test_lib', + value: { + attribute: 'test_attr', + payload + }, + ctx + }); + expect(resValue).toMatchObject([{payload, raw_payload: payload, attribute: 'test_attr'}]); + }); + + test('Should return formatted value after actions list', async function () { + const payload = 'test val'; + const formattedPayload = 'TEST VAL'; + + const mockAttrDomain: Mockify = { + getAttributeProperties: global.__mockPromise({...mockAttribute, type: AttributeTypes.SIMPLE}) + }; + + const mockActionsListDomainUpperCase: Mockify = { + runActionsList: jest + .fn() + .mockImplementation((_, val) => Promise.resolve([{...val[0], payload: formattedPayload}])) + }; + + const valDomain = valueDomain({ + ...depsBase, + 'core.domain.attribute': mockAttrDomain as IAttributeDomain, + 'core.infra.record': mockRecordRepo as IRecordRepo, + 'core.domain.actionsList': mockActionsListDomainUpperCase as any, + 'core.domain.helpers.validate': mockValidateHelper as IValidateHelper, + 'core.utils': mockUtilsStandardAttribute as IUtils + }); + + const resValue = await valDomain.runActionsListAndFormatOnValue({ + library: 'test_lib', + value: { + attribute: 'test_attr', + payload + }, + ctx + }); + expect(resValue).toMatchObject([{payload: formattedPayload, raw_payload: payload, attribute: 'test_attr'}]); + }); + + test('Should return fomatted value after actions list if attribute format is date range', async function () { + const dateRangePayload = JSON.stringify({from: '1727733600', to: '1729548000'}); + const dateRangeFormattedPayload = { + from: 'Monday, Septembre 30, 2024', + to: 'Tuesday, October 1, 2024' + }; + + const mockAttrDomain: Mockify = { + getAttributeProperties: global.__mockPromise({...mockAttribute, type: AttributeTypes.SIMPLE}) + }; + + const mockActionsListDomainDateRange: Mockify = { + runActionsList: jest.fn().mockImplementation((_, val) => + Promise.resolve([ + { + ...val[0], + payload: dateRangeFormattedPayload + } + ]) + ) + }; + + const valDomain = valueDomain({ + ...depsBase, + 'core.domain.attribute': mockAttrDomain as IAttributeDomain, + 'core.infra.record': mockRecordRepo as IRecordRepo, + 'core.domain.actionsList': mockActionsListDomainDateRange as any, + 'core.domain.helpers.validate': mockValidateHelper as IValidateHelper, + 'core.utils': mockUtilsStandardAttribute as IUtils + }); + + const resValue = await valDomain.runActionsListAndFormatOnValue({ + library: 'test_lib', + value: { + attribute: 'test_attr', + payload: dateRangePayload + }, + ctx + }); + expect(resValue).toMatchObject([ + { + payload: dateRangeFormattedPayload, + raw_payload: dateRangePayload, + attribute: 'test_attr' + } + ]); + }); + }); }); diff --git a/apps/core/src/domain/value/valueDomain.ts b/apps/core/src/domain/value/valueDomain.ts index 8245490b1..7cec6ea49 100644 --- a/apps/core/src/domain/value/valueDomain.ts +++ b/apps/core/src/domain/value/valueDomain.ts @@ -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 {EventAction} from '@leav/utils'; +import {EventAction, IDateRangeValue} from '@leav/utils'; import {IEventsManagerDomain} from 'domain/eventsManager/eventsManagerDomain'; import {UpdateRecordLastModifFunc} from 'domain/helpers/updateRecordLastModif'; import {SendRecordUpdateEventHelper} from 'domain/record/helpers/sendRecordUpdateEvent'; @@ -19,7 +19,7 @@ import {IRecord} from '_types/record'; import PermissionError from '../../errors/PermissionError'; import ValidationError from '../../errors/ValidationError'; import {ActionsListEvents} from '../../_types/actionsList'; -import {AttributeTypes, IAttribute, ValueVersionMode} from '../../_types/attribute'; +import {AttributeFormats, AttributeTypes, IAttribute, ValueVersionMode} from '../../_types/attribute'; import {Errors, ErrorTypes} from '../../_types/errors'; import {RecordAttributePermissionsActions, RecordPermissionsActions} from '../../_types/permissions'; import {IQueryInfos} from '../../_types/queryInfos'; @@ -117,6 +117,16 @@ export interface IValueDomain { }): Promise; runActionsList(params: IRunActionListParams): Promise; + + runActionsListAndFormatOnValue({ + library, + value, + ctx + }: { + library: string; + value: IValue; + ctx: IQueryInfos; + }): Promise; } export interface IValueDomainDeps { @@ -216,10 +226,7 @@ const valueDomain = function ({ }): Promise => { let processedValue = {...value}; // Don't mutate given value - const isLinkAttribute = - attribute.type === AttributeTypes.SIMPLE_LINK || attribute.type === AttributeTypes.ADVANCED_LINK; - - if (isLinkAttribute && attribute.linked_library) { + if (utils.isLinkAttribute(attribute)) { const linkValue = processedValue.payload ? {...processedValue.payload, library: processedValue.payload.library ?? attribute.linked_library} : null; @@ -452,10 +459,40 @@ const valueDomain = function ({ ); } - // Apply actions list and format value before returning it + const processedValues = await _runActionsListAndFormatValue(library, attribute, savedValue, ctx, record); + + if (!areValuesIdentical) { + await eventsManager.sendDatabaseEvent( + { + action: EventAction.VALUE_SAVE, + topic: { + library, + record: { + id: record.id, + libraryId: library + }, + attribute: attribute.id + }, + before: valueBefore, + after: savedValue + }, + ctx + ); + } + + return {values: processedValues, areValuesIdentical}; + }; + + const _runActionsListAndFormatValue = async ( + library: string, + attribute: IAttribute, + value: IValue, + ctx: IQueryInfos, + record: IRecord = null + ) => { let processedValues = await _runActionsList({ listName: ActionsListEvents.GET_VALUE, - values: [savedValue], + values: [value], attribute, record, library, @@ -500,26 +537,7 @@ const valueDomain = function ({ }) ); - if (!areValuesIdentical) { - await eventsManager.sendDatabaseEvent( - { - action: EventAction.VALUE_SAVE, - topic: { - library, - record: { - id: record.id, - libraryId: library - }, - attribute: attribute.id - }, - before: valueBefore, - after: savedValue - }, - ctx - ); - } - - return {values: processedValues, areValuesIdentical}; + return processedValues; }; return { @@ -861,6 +879,22 @@ const valueDomain = function ({ await validate.validateRecord(library, recordId, ctx); return _executeDeleteValue({library, recordId, attribute, value, ctx}); }, + async runActionsListAndFormatOnValue({library, value, ctx}) { + const attributeProps = await attributeDomain.getAttributeProperties({id: value.attribute, ctx}); + + if (attributeProps.format === AttributeFormats.DATE_RANGE) { + value.payload = JSON.parse(value.payload) as IDateRangeValue; + } + + //TODO: When we will adapt Tree attribute to design-system, we will have to adapt this part (like we did for link) + if (utils.isLinkAttribute(attributeProps)) { + value.payload = { + id: value.payload + }; + } + + return _runActionsListAndFormatValue(library, attributeProps, value, ctx); + }, formatValue: _formatValue, runActionsList: _runActionsList }; diff --git a/libs/ui/src/_gqlTypes/index.ts b/libs/ui/src/_gqlTypes/index.ts index 2f0b1c1e7..692631b26 100644 --- a/libs/ui/src/_gqlTypes/index.ts +++ b/libs/ui/src/_gqlTypes/index.ts @@ -1293,6 +1293,15 @@ export type DeleteValueMutationVariables = Exact<{ export type DeleteValueMutation = { deleteValue: Array<{ id_value?: string | null, isInherited?: boolean | null, isCalculated?: boolean | null, modified_at?: number | null, created_at?: number | null, linkValue?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null, attribute: { id: string, format?: AttributeFormat | null, type: AttributeType, system: boolean }, metadata?: Array<{ name: string, value?: { id_value?: string | null, modified_at?: number | null, created_at?: number | null, payload?: any | null, raw_payload?: any | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null } | null } | null> | null } | { id_value?: string | null, isInherited?: boolean | null, isCalculated?: boolean | null, modified_at?: number | null, created_at?: number | null, treeValue?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } }, ancestors?: Array<{ record: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } }> | null } | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null, attribute: { id: string, format?: AttributeFormat | null, type: AttributeType, system: boolean }, metadata?: Array<{ name: string, value?: { id_value?: string | null, modified_at?: number | null, created_at?: number | null, payload?: any | null, raw_payload?: any | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null } | null } | null> | null } | { payload?: any | null, raw_payload?: any | null, value?: any | null, raw_value?: any | null, id_value?: string | null, isInherited?: boolean | null, isCalculated?: boolean | null, modified_at?: number | null, created_at?: number | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null, attribute: { id: string, format?: AttributeFormat | null, type: AttributeType, system: boolean }, metadata?: Array<{ name: string, value?: { id_value?: string | null, modified_at?: number | null, created_at?: number | null, payload?: any | null, raw_payload?: any | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null } | null } | null> | null }> }; +export type RunActionsListAndFormatOnValueQueryVariables = Exact<{ + library: Scalars['ID']; + value?: InputMaybe; + version?: InputMaybe> | InputMaybe>; +}>; + + +export type RunActionsListAndFormatOnValueQuery = { runActionsListAndFormatOnValue: Array<{ id_value?: string | null, isInherited?: boolean | null, isCalculated?: boolean | null, modified_at?: number | null, created_at?: number | null, payload?: any | null, raw_payload?: any | null, value?: any | null, raw_value?: any | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null, attribute: { id: string, format?: AttributeFormat | null, type: AttributeType, system: boolean }, metadata?: Array<{ name: string, value?: { id_value?: string | null, modified_at?: number | null, created_at?: number | null, payload?: any | null, raw_payload?: any | null, modified_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, created_by?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null, version?: Array<{ treeId: string, treeNode?: { id: string, record: { id: string, whoAmI: { id: string, label?: string | null, library: { id: string } } } } | null } | null> | null } | null } | null> | null }> }; + export type SaveValueBatchMutationVariables = Exact<{ library: Scalars['ID']; recordId: Scalars['ID']; @@ -3710,6 +3719,47 @@ export function useDeleteValueMutation(baseOptions?: Apollo.MutationHookOptions< export type DeleteValueMutationHookResult = ReturnType; export type DeleteValueMutationResult = Apollo.MutationResult; export type DeleteValueMutationOptions = Apollo.BaseMutationOptions; +export const RunActionsListAndFormatOnValueDocument = gql` + query RUN_ACTIONS_LIST_AND_FORMAT_ON_VALUE($library: ID!, $value: ValueBatchInput, $version: [ValueVersionInput]) { + runActionsListAndFormatOnValue( + library: $library + value: $value + version: $version + ) { + ...ValueDetails + } +} + ${ValueDetailsFragmentDoc}`; + +/** + * __useRunActionsListAndFormatOnValueQuery__ + * + * To run a query within a React component, call `useRunActionsListAndFormatOnValueQuery` and pass it any options that fit your needs. + * When your component renders, `useRunActionsListAndFormatOnValueQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useRunActionsListAndFormatOnValueQuery({ + * variables: { + * library: // value for 'library' + * value: // value for 'value' + * version: // value for 'version' + * }, + * }); + */ +export function useRunActionsListAndFormatOnValueQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(RunActionsListAndFormatOnValueDocument, options); + } +export function useRunActionsListAndFormatOnValueLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(RunActionsListAndFormatOnValueDocument, options); + } +export type RunActionsListAndFormatOnValueQueryHookResult = ReturnType; +export type RunActionsListAndFormatOnValueLazyQueryHookResult = ReturnType; +export type RunActionsListAndFormatOnValueQueryResult = Apollo.QueryResult; export const SaveValueBatchDocument = gql` mutation SAVE_VALUE_BATCH($library: ID!, $recordId: ID!, $version: [ValueVersionInput!], $values: [ValueBatchInput!]!, $deleteEmpty: Boolean) { saveValueBatch( diff --git a/libs/ui/src/_queries/values/getValueAfterActionsListAndFormatQuery.graphql b/libs/ui/src/_queries/values/getValueAfterActionsListAndFormatQuery.graphql new file mode 100644 index 000000000..b552b1f37 --- /dev/null +++ b/libs/ui/src/_queries/values/getValueAfterActionsListAndFormatQuery.graphql @@ -0,0 +1,7 @@ + +query RUN_ACTIONS_LIST_AND_FORMAT_ON_VALUE($library: ID!, $value: ValueBatchInput, $version: [ValueVersionInput]) { + runActionsListAndFormatOnValue(library: $library, value: $value, version: $version) { + ...ValueDetails + } +} + diff --git a/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx b/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx index a936e3620..136a71910 100644 --- a/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx +++ b/libs/ui/src/components/RecordEdition/EditRecord/EditRecord.tsx @@ -45,6 +45,7 @@ import {EditRecordReducerContext} from '../editRecordReducer/editRecordReducerCo import EditRecordSidebar from '../EditRecordSidebar'; import CreationErrorContext, {ICreationErrorByField} from './creationErrorContext'; import {FormInstance} from 'antd/lib/form/Form'; +import {useRunActionsListAndFormatOnValue} from '../EditRecordContent/hooks/useRunActionsListAndFormatOnValue'; interface IEditRecordProps { antdForm: FormInstance; @@ -135,8 +136,6 @@ export const EditRecord: FunctionComponent = ({ // We use a ref to store the latest state and access it in the callback const pendingValuesRef = useRef(pendingValues); - const hasPendingValues = !!Object.keys(pendingValues).length; - // Update record in reducer when it changes. Might happen on record identity change (after value save) useEffect(() => { if (record && !isEqual(record, state.record)) { @@ -165,6 +164,8 @@ export const EditRecord: FunctionComponent = ({ pendingValuesRef.current = pendingValues; }, [pendingValues]); + const {runActionsListAndFormatOnValue} = useRunActionsListAndFormatOnValue(); + const _handleValueSubmit: SubmitValueFunc = async (values, version) => { if (!isCreationMode) { // In Edition mode, submit values immediately and send result back to children @@ -199,6 +200,29 @@ export const EditRecord: FunctionComponent = ({ const newPendingValues = {...pendingValues}; const storedValues: ValueDetailsFragment[] = []; for (const value of values) { + const valueToProcess = {...value, attribute: value.attribute.id, metadata: value.metadata}; + + if (valueToProcess.value) { + switch (value.attribute.type) { + case AttributeType.advanced_link: + case AttributeType.simple_link: + valueToProcess.value = (value as ISubmittedValueLink).value.id; + break; + case AttributeType.tree: + valueToProcess.value = (value as ISubmittedValueTree).value.id; + break; + default: + valueToProcess.value = (value as ISubmittedValueStandard).value; + break; + } + } + + const valueAfterActionsListAndFormat = await runActionsListAndFormatOnValue( + library, + valueToProcess as IValueToSubmit, + version + ); + const attributeId = value.attribute.id; if (!newPendingValues[attributeId]) { newPendingValues[attributeId] = {}; @@ -270,8 +294,9 @@ export const EditRecord: FunctionComponent = ({ }; break; default: - (valueToStore as ValueDetailsValueFragment).value = value.value; - (valueToStore as ValueDetailsValueFragment).raw_value = value.value; + (valueToStore as ValueDetailsValueFragment).payload = + valueAfterActionsListAndFormat.payload ?? value.value; + (valueToStore as ValueDetailsValueFragment).raw_payload = value.value; break; } diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/hooks/useRunActionsListAndFormatOnValue.test.tsx b/libs/ui/src/components/RecordEdition/EditRecordContent/hooks/useRunActionsListAndFormatOnValue.test.tsx new file mode 100644 index 000000000..39fbbbdfe --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/hooks/useRunActionsListAndFormatOnValue.test.tsx @@ -0,0 +1,79 @@ +// Copyright LEAV Solutions 2017 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import * as apolloClient from '@apollo/client'; +import {Mockify} from '@leav/utils'; +import {RunActionsListAndFormatOnValueDocument} from '_ui/_gqlTypes'; +import {useRunActionsListAndFormatOnValue} from './useRunActionsListAndFormatOnValue'; +import {MockedProvider} from '@apollo/client/testing'; +import {act, renderHook} from '_ui/_tests/testUtils'; + +describe('useRunActionsListAndFormatOnValue', () => { + const mockApolloCache: Mockify> = {modify: jest.fn(), identify: jest.fn()}; + const mockApolloClient: Mockify> = { + cache: mockApolloCache as unknown as apolloClient.ApolloCache + }; + + jest.spyOn(apolloClient, 'useApolloClient').mockImplementation( + () => mockApolloClient as unknown as apolloClient.ApolloClient + ); + + const successMock = [ + { + request: { + query: RunActionsListAndFormatOnValueDocument, + variables: { + library: 'test_library', + value: { + attribute: 'test_attribute', + payload: 'test', + metadata: null + }, + version: null + } + }, + result: { + data: { + runActionsListAndFormatOnValue: [ + { + __typename: 'GenericValue', + id_value: null, + payload: 'TEST', + raw_payload: 'test' + } + ] + } + } + } + ]; + + test('should process given value', async () => { + const {result} = renderHook(() => useRunActionsListAndFormatOnValue(), { + wrapper: ({children}) => ( + + {children as JSX.Element} + + ) + }); + + let values; + await act(async () => { + values = await result.current.runActionsListAndFormatOnValue( + 'test_library', + { + value: 'test', + idValue: null, + attribute: 'test_attribute' + }, + null + ); + }); + + expect(values).toEqual({ + __typename: 'GenericValue', + id_value: null, + payload: 'TEST', + raw_payload: 'test' + }); + }); +}); diff --git a/libs/ui/src/components/RecordEdition/EditRecordContent/hooks/useRunActionsListAndFormatOnValue.ts b/libs/ui/src/components/RecordEdition/EditRecordContent/hooks/useRunActionsListAndFormatOnValue.ts new file mode 100644 index 000000000..c9ce7e1f6 --- /dev/null +++ b/libs/ui/src/components/RecordEdition/EditRecordContent/hooks/useRunActionsListAndFormatOnValue.ts @@ -0,0 +1,37 @@ +import {useRunActionsListAndFormatOnValueLazyQuery} from '_ui/_gqlTypes'; +import {IValueToSubmit} from '../_types'; +import {objectToNameValueArray} from '@leav/utils'; +import {IValueVersion} from '_ui/types'; + +export const useRunActionsListAndFormatOnValue = () => { + const [runActionsListAndFormatOnValueLazyQuery, {loading, error}] = useRunActionsListAndFormatOnValueLazyQuery(); + + const runActionsListAndFormatOnValue = async ( + library: string, + valueToProcess: IValueToSubmit, + version: IValueVersion + ) => { + const result = await runActionsListAndFormatOnValueLazyQuery({ + variables: { + library, + value: { + attribute: valueToProcess.attribute, + payload: valueToProcess.value !== null ? String(valueToProcess.value) : null, + metadata: valueToProcess.metadata + ? objectToNameValueArray(valueToProcess.metadata).map(({name, value}) => ({ + name, + value: String(value) + })) + : null + }, + version: version + ? objectToNameValueArray(version).map(v => ({treeId: v.name, treeNodeId: v.value.id})) + : null + } + }); + + return result.data.runActionsListAndFormatOnValue?.[0] ?? null; + }; + + return {runActionsListAndFormatOnValue, loading, error}; +};