Skip to content

Commit 0ecc7b5

Browse files
committed
use new update tags endpoint and update validations from result
1 parent a0bb9b4 commit 0ecc7b5

File tree

17 files changed

+207
-84
lines changed

17 files changed

+207
-84
lines changed

src/features/attachments/AttachmentsStorePlugin.tsx

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isAttachmentUploaded, isDataPostError } from 'src/features/attachments/
1313
import { sortAttachmentsByName } from 'src/features/attachments/sortAttachments';
1414
import { attachmentSelector } from 'src/features/attachments/tools';
1515
import { FileScanResults } from 'src/features/attachments/types';
16+
import { DataModels } from 'src/features/datamodel/DataModelsProvider';
1617
import { FD } from 'src/features/formData/FormDataWrite';
1718
import { dataModelPairsToObject } from 'src/features/formData/types';
1819
import {
@@ -24,11 +25,18 @@ import {
2425
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
2526
import { useLanguage } from 'src/features/language/useLanguage';
2627
import { backendValidationIssueGroupListToObject } from 'src/features/validation';
28+
import {
29+
mapBackendIssuesToTaskValidations,
30+
mapBackendValidationsToValidatorGroups,
31+
mapValidatorGroupsToDataModelValidations,
32+
} from 'src/features/validation/backendValidation/backendValidationUtils';
33+
import { Validation } from 'src/features/validation/validationContext';
2734
import { useWaitForState } from 'src/hooks/useWaitForState';
35+
import { doUpdateAttachmentTags } from 'src/queries/queries';
2836
import { nodesProduce } from 'src/utils/layout/NodesContext';
2937
import { NodeDataPlugin } from 'src/utils/layout/plugins/NodeDataPlugin';
3038
import { splitDashedKey } from 'src/utils/splitDashedKey';
31-
import { appSupportsNewAttachmentAPI } from 'src/utils/versioning/versions';
39+
import { appSupportsNewAttachmentAPI, appSupportsSetTagsEndpoint } from 'src/utils/versioning/versions';
3240
import type {
3341
DataPostResponse,
3442
IAttachment,
@@ -40,10 +48,12 @@ import type {
4048
import type { AttachmentsSelector } from 'src/features/attachments/tools';
4149
import type { AttachmentStateInfo } from 'src/features/attachments/types';
4250
import type { FDActionResult } from 'src/features/formData/FormDataWriteStateMachine';
51+
import type { BackendFieldValidatorGroups } from 'src/features/validation';
4352
import type { DSPropsForSimpleSelector } from 'src/hooks/delayedSelectors';
4453
import type { IDataModelBindingsList, IDataModelBindingsSimple } from 'src/layout/common.generated';
4554
import type { RejectedFileError } from 'src/layout/FileUpload/RejectedFileError';
4655
import type { CompWithBehavior } from 'src/layout/layout';
56+
import type { SetTagsRequest } from 'src/queries/queries';
4757
import type { IData } from 'src/types/shared';
4858
import type { NodesContext, NodesStoreFull } from 'src/utils/layout/NodesContext';
4959
import type { NodeDataPluginSetState } from 'src/utils/layout/plugins/NodeDataPlugin';
@@ -384,47 +394,66 @@ export class AttachmentsStorePlugin extends NodeDataPlugin<AttachmentsStorePlugi
384394
useAttachmentsUpdate() {
385395
const { mutateAsync: removeTag } = useAttachmentsRemoveTagMutation();
386396
const { mutateAsync: addTag } = useAttachmentsAddTagMutation();
387-
const mutateDataElement = useOptimisticallyUpdateDataElement();
397+
const { mutateAsync: updateTags } = useAttachmentUpdateTagsMutation();
398+
const optimisticallyUpdateDataElement = useOptimisticallyUpdateDataElement();
388399
const { lang } = useLanguage();
389400
const update = store.useSelector((state) => state.attachmentUpdate);
390401
const fulfill = store.useSelector((state) => state.attachmentUpdateFulfilled);
391402
const reject = store.useSelector((state) => state.attachmentUpdateRejected);
403+
const backendVersion = useApplicationMetadata().altinnNugetVersion ?? '';
392404

393405
return useCallback(
394406
async (action: AttachmentActionUpdate) => {
395407
const { tags, attachment } = action;
396-
const tagToAdd = tags.filter((t) => !attachment.data.tags?.includes(t));
397-
const tagToRemove = attachment.data.tags?.filter((t) => !tags.includes(t)) || [];
398-
const areEqual = tagToAdd.length && tagToRemove.length && tagToAdd[0] === tagToRemove[0];
408+
const tagsToAdd = tags.filter((t) => !attachment.data.tags?.includes(t));
409+
const tagsToRemove = attachment.data.tags?.filter((t) => !tags.includes(t)) ?? [];
410+
const areEqual = tagsToAdd.length && tagsToRemove.length && tagsToAdd[0] === tagsToRemove[0];
411+
const dataGuid = attachment.data.id;
399412

400413
// If there are no tags to add or remove, or if the tags are the same, do nothing.
401-
if ((!tagToAdd.length && !tagToRemove.length) || areEqual) {
414+
if ((!tagsToAdd.length && !tagsToRemove.length) || areEqual) {
402415
return;
403416
}
404417

405418
update(action);
406419
try {
407-
if (tagToAdd.length) {
408-
await Promise.all(tagToAdd.map((tag) => addTag({ dataGuid: attachment.data.id, tagToAdd: tag })));
409-
}
410-
if (tagToRemove.length) {
420+
if (appSupportsSetTagsEndpoint(backendVersion)) {
421+
await updateTags({
422+
dataGuid,
423+
setTagsRequest: {
424+
tags,
425+
},
426+
});
427+
} else {
428+
await Promise.all(tagsToAdd.map((tag) => addTag({ dataGuid: attachment.data.id, tagToAdd: tag })));
411429
await Promise.all(
412-
tagToRemove.map((tag) => removeTag({ dataGuid: attachment.data.id, tagToRemove: tag })),
430+
tagsToRemove.map((tag) => removeTag({ dataGuid: attachment.data.id, tagToRemove: tag })),
413431
);
414432
}
415433
fulfill(action);
416-
mutateDataElement(attachment.data.id, (dataElement) => ({ ...dataElement, tags }));
434+
optimisticallyUpdateDataElement(dataGuid, (dataElement) => ({ ...dataElement, tags }));
435+
436+
return;
417437
} catch (error) {
418438
reject(action, error);
419439
toast(lang('form_filler.file_uploader_validation_error_update'), { type: 'error' });
420440
}
421441
},
422-
[addTag, mutateDataElement, fulfill, lang, reject, removeTag, update],
442+
[
443+
update,
444+
backendVersion,
445+
fulfill,
446+
optimisticallyUpdateDataElement,
447+
updateTags,
448+
addTag,
449+
removeTag,
450+
reject,
451+
lang,
452+
],
423453
);
424454
},
425455
useAttachmentsRemove() {
426456
const { mutateAsync: removeAttachment } = useAttachmentsRemoveMutation();
427-
const removeDataElement = useOptimisticallyRemoveDataElement();
428457
const { lang } = useLanguage();
429458
const remove = store.useSelector((state) => state.attachmentRemove);
430459
const fulfill = store.useSelector((state) => state.attachmentRemoveFulfilled);
@@ -450,7 +479,6 @@ export class AttachmentsStorePlugin extends NodeDataPlugin<AttachmentsStorePlugi
450479
}
451480

452481
fulfill(action);
453-
removeDataElement(action.attachment.data.id);
454482

455483
return true;
456484
} catch (error) {
@@ -459,7 +487,7 @@ export class AttachmentsStorePlugin extends NodeDataPlugin<AttachmentsStorePlugi
459487
return false;
460488
}
461489
},
462-
[removeDataElement, fulfill, lang, reject, remove, removeAttachment, removeValueFromList, setLeafValue],
490+
[remove, removeAttachment, fulfill, removeValueFromList, setLeafValue, reject, lang],
463491
);
464492
},
465493
useAttachments(nodeId) {
@@ -682,7 +710,7 @@ function useAttachmentsUploadMutationOld() {
682710
return useMutation(options);
683711
}
684712

685-
export function useAttachmentsUploadMutation() {
713+
function useAttachmentsUploadMutation() {
686714
const { doAttachmentUpload } = useAppMutations();
687715
const instanceId = useLaxInstanceId();
688716
const language = useCurrentLanguage();
@@ -705,6 +733,30 @@ export function useAttachmentsUploadMutation() {
705733
return useMutation(options);
706734
}
707735

736+
function useAttachmentsRemoveMutation() {
737+
const { doAttachmentRemove } = useAppMutations();
738+
const instanceId = useLaxInstanceId();
739+
const language = useCurrentLanguage();
740+
const optimisticallyRemoveDataElement = useOptimisticallyRemoveDataElement();
741+
742+
return useMutation({
743+
mutationFn: (dataGuid: string) => {
744+
if (!instanceId) {
745+
throw new Error('Missing instanceId, cannot remove attachment');
746+
}
747+
748+
return doAttachmentRemove(instanceId, dataGuid, language);
749+
},
750+
onError: (error: AxiosError) => {
751+
window.logError('Failed to delete attachment:\n', error);
752+
},
753+
onSuccess: (_data, dataGuid) => {
754+
optimisticallyRemoveDataElement(dataGuid);
755+
},
756+
});
757+
}
758+
759+
// FIXME: remove this in future release, when all backends support updateTags endpoint
708760
function useAttachmentsAddTagMutation() {
709761
const { doAttachmentAddTag } = useAppMutations();
710762
const instanceId = useLaxInstanceId();
@@ -723,6 +775,7 @@ function useAttachmentsAddTagMutation() {
723775
});
724776
}
725777

778+
// FIXME: remove this in future release, when all backends support updateTags endpoint
726779
function useAttachmentsRemoveTagMutation() {
727780
const { doAttachmentRemoveTag } = useAppMutations();
728781
const instanceId = useLaxInstanceId();
@@ -741,21 +794,36 @@ function useAttachmentsRemoveTagMutation() {
741794
});
742795
}
743796

744-
function useAttachmentsRemoveMutation() {
745-
const { doAttachmentRemove } = useAppMutations();
797+
function useAttachmentUpdateTagsMutation() {
746798
const instanceId = useLaxInstanceId();
747-
const language = useCurrentLanguage();
799+
const defaultDataElementId = DataModels.useDefaultDataElementId();
800+
const updateBackendValidations = Validation.useUpdateBackendValidations();
748801

749802
return useMutation({
750-
mutationFn: (dataGuid: string) => {
803+
mutationFn: ({ dataGuid, setTagsRequest }: { dataGuid: string; setTagsRequest: SetTagsRequest }) => {
751804
if (!instanceId) {
752-
throw new Error('Missing instanceId, cannot remove attachment');
805+
throw new Error('Missing instanceId, cannot add attachment');
753806
}
754807

755-
return doAttachmentRemove(instanceId, dataGuid, language);
808+
return doUpdateAttachmentTags({ instanceId, dataGuid, setTagsRequest });
756809
},
757810
onError: (error: AxiosError) => {
758-
window.logError('Failed to delete attachment:\n', error);
811+
window.logError('Failed to add tag to attachment:\n', error);
812+
},
813+
onSuccess: (data, variables) => {
814+
const backendValidations = data.validationIssues.reduce((prev, curr) => [...prev, ...curr.issues], []);
815+
const initialTaskValidations = mapBackendIssuesToTaskValidations(backendValidations);
816+
817+
const initialValidatorGroups: BackendFieldValidatorGroups = mapBackendValidationsToValidatorGroups(
818+
backendValidations,
819+
defaultDataElementId,
820+
);
821+
822+
const dataModelValidations = mapValidatorGroupsToDataModelValidations(initialValidatorGroups, [
823+
variables.dataGuid,
824+
]);
825+
826+
updateBackendValidations(dataModelValidations, { initial: backendValidations }, initialTaskValidations);
759827
},
760828
});
761829
}

src/features/attachments/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AxiosError } from 'axios';
22

33
import type { IDataModelPairResponse } from 'src/features/formData/types';
4-
import type { BackendValidationIssue, BackendValidationIssueGroupListItem } from 'src/features/validation';
4+
import type { BackendValidationIssue, BackendValidationIssuesWithSource } from 'src/features/validation';
55
import type { IData, IInstance, ProblemDetails } from 'src/types/shared';
66

77
interface IAttachmentTemporary {
@@ -33,7 +33,7 @@ export function isAttachmentUploaded(attachment: IAttachment): attachment is Upl
3333
export type DataPostResponse = {
3434
newDataElementId: string;
3535
instance: IInstance;
36-
validationIssues: BackendValidationIssueGroupListItem[];
36+
validationIssues: BackendValidationIssuesWithSource[];
3737
newDataModels: IDataModelPairResponse[];
3838
};
3939

src/features/formData/FormDataWrite.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,13 @@ function useFormDataSaveMutation() {
264264
return;
265265
}
266266

267-
const { newDataModels, validationIssues, instance } = await doPatchMultipleFormData(
268-
getUrlWithLanguage(multiPatchUrl, currentLanguage.current),
269-
{
267+
const { newDataModels, validationIssues, instance } = (
268+
await doPatchMultipleFormData(getUrlWithLanguage(multiPatchUrl, currentLanguage.current), {
270269
patches,
271270
// Ignore validations that require layout parsing in the backend which will slow down requests significantly
272271
ignoredValidators: IgnoredValidators,
273-
},
274-
);
272+
})
273+
).data;
275274

276275
const dataModelChanges: UpdatedDataModel[] = [];
277276
for (const { dataElementId, data } of newDataModels) {
@@ -308,11 +307,14 @@ function useFormDataSaveMutation() {
308307
if (!url) {
309308
throw new Error(`Cannot patch data, url for dataType '${dataType}' could not be determined`);
310309
}
311-
const { newDataModel, validationIssues, instance } = await doPatchFormData(url, {
312-
patch,
313-
// Ignore validations that require layout parsing in the backend which will slow down requests significantly
314-
ignoredValidators: IgnoredValidators,
315-
});
310+
const { newDataModel, validationIssues, instance } = (
311+
await doPatchFormData(url, {
312+
patch,
313+
// Ignore validations that require layout parsing in the backend which will slow down requests significantly
314+
ignoredValidators: IgnoredValidators,
315+
})
316+
).data;
317+
316318
return {
317319
newDataModels: [{ dataType, data: newDataModel, dataElementId }],
318320
validationIssues,

src/features/formData/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { JsonPatch } from 'src/features/formData/jsonPatch/types';
22
import type {
3-
BackendValidationIssueGroupListItem,
43
BackendValidationIssueGroups,
4+
BackendValidationIssuesWithSource,
55
BuiltInValidationIssueSources,
66
} from 'src/features/validation';
77
import type { IInstance } from 'src/types/shared';
@@ -60,7 +60,7 @@ export interface IPatchListItem {
6060
}
6161

6262
export interface IDataModelMultiPatchResponse {
63-
validationIssues: BackendValidationIssueGroupListItem[];
63+
validationIssues: BackendValidationIssuesWithSource[];
6464
newDataModels: IDataModelPairResponse[];
6565
instance: IInstance;
6666
}

src/features/instance/useProcessNext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function useProcessNext({ action }: ProcessNextProps = {}) {
7272
}
7373

7474
return doProcessNext(instanceId, language, action)
75-
.then((process) => [process, null] as const)
75+
.then(({ data: process }) => [process, null] as const)
7676
.catch((error) => {
7777
if (error.response?.status === 409 && error.response?.data?.['validationIssues']?.length) {
7878
// If process next failed due to validation, return validationIssues instead of throwing

src/features/processEnd/confirm/containers/ConfirmPage.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import { jest } from '@jest/globals';
44
import { screen, waitFor } from '@testing-library/react';
55
import userEvent from '@testing-library/user-event';
6+
import { AxiosResponse } from 'axios';
67

78
import { getApplicationMetadataMock } from 'src/__mocks__/getApplicationMetadataMock';
89
import { getInstanceDataMock } from 'src/__mocks__/getInstanceDataMock';
@@ -82,7 +83,7 @@ describe('ConfirmPage', () => {
8283

8384
it('should show loading when clicking submit and process/next has not resolved', async () => {
8485
const user = userEvent.setup();
85-
jest.mocked(doProcessNext).mockImplementation(() => new Promise(() => {}) as Promise<IProcess>);
86+
jest.mocked(doProcessNext).mockImplementation(async () => ({}) as AxiosResponse<IProcess>);
8687

8788
await renderWithInstanceAndLayout({
8889
renderer: () => <ConfirmPage {...props} />,

src/features/validation/backendValidation/BackendValidation.tsx

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,34 @@
1-
import { useEffect, useMemo, useRef } from 'react';
1+
import { useEffect, useRef } from 'react';
22

33
import deepEqual from 'fast-deep-equal';
44

55
import { DataModels } from 'src/features/datamodel/DataModelsProvider';
66
import { FD } from 'src/features/formData/FormDataWrite';
7-
import { type BackendFieldValidatorGroups } from 'src/features/validation';
87
import { useBackendValidationQuery } from 'src/features/validation/backendValidation/backendValidationQuery';
98
import {
109
mapBackendIssuesToFieldValidations,
1110
mapBackendIssuesToTaskValidations,
11+
mapBackendValidationsToValidatorGroups,
1212
mapValidatorGroupsToDataModelValidations,
1313
useShouldValidateInitial,
1414
} from 'src/features/validation/backendValidation/backendValidationUtils';
1515
import { Validation } from 'src/features/validation/validationContext';
16-
17-
const emptyObject = {};
18-
const emptyArray = [];
16+
import type { BackendFieldValidatorGroups } from 'src/features/validation';
1917

2018
export function BackendValidation({ dataTypes }: { dataTypes: string[] }) {
2119
const updateBackendValidations = Validation.useUpdateBackendValidations();
2220
const defaultDataElementId = DataModels.useDefaultDataElementId();
2321
const lastSaveValidations = FD.useLastSaveValidationIssues();
2422
const validatorGroups = useRef<BackendFieldValidatorGroups>({});
25-
26-
// Map initial validations
2723
const enabled = useShouldValidateInitial();
2824
const { data: initialValidations, isFetching } = useBackendValidationQuery({ enabled });
29-
const initialValidatorGroups: BackendFieldValidatorGroups = useMemo(() => {
30-
if (!initialValidations) {
31-
return emptyObject;
32-
}
33-
// Note that we completely ignore task validations (validations not related to form data) on initial validations,
34-
// this is because validations like minimum number of attachments in application metadata is not really useful to show initially
35-
const fieldValidations = mapBackendIssuesToFieldValidations(initialValidations, defaultDataElementId);
36-
const validatorGroups: BackendFieldValidatorGroups = {};
37-
for (const validation of fieldValidations) {
38-
if (!validatorGroups[validation.source]) {
39-
validatorGroups[validation.source] = [];
40-
}
41-
validatorGroups[validation.source].push(validation);
42-
}
43-
return validatorGroups;
44-
}, [defaultDataElementId, initialValidations]);
25+
const initialValidatorGroups: BackendFieldValidatorGroups = mapBackendValidationsToValidatorGroups(
26+
initialValidations,
27+
defaultDataElementId,
28+
);
4529

4630
// Map task validations
47-
const initialTaskValidations = useMemo(() => {
48-
if (!initialValidations) {
49-
return emptyArray;
50-
}
51-
return mapBackendIssuesToTaskValidations(initialValidations);
52-
}, [initialValidations]);
31+
const initialTaskValidations = mapBackendIssuesToTaskValidations(initialValidations);
5332

5433
// Initial validation
5534
useEffect(() => {

0 commit comments

Comments
 (0)