Skip to content

Commit 2e6c896

Browse files
olemartinorgOle Martin Handeland
andauthored
Fixing navigation issues after validating attachment tags (#3753)
* Extracting incremental validation stuff from BackendValidation.tsx and adding it as a separate hook. This should work better than updating initial validations, which broke navigation later in the app. * Adding cypress test for the tag validation functionality * Adding an extra assertion that makes sure the validation is still there before the tag is selected --------- Co-authored-by: Ole Martin Handeland <[email protected]>
1 parent ed6f5c5 commit 2e6c896

File tree

7 files changed

+156
-77
lines changed

7 files changed

+156
-77
lines changed

src/features/attachments/AttachmentsStorePlugin.tsx

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ 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';
1716
import { FD } from 'src/features/formData/FormDataWrite';
1817
import { dataModelPairsToObject } from 'src/features/formData/types';
1918
import {
@@ -25,12 +24,7 @@ import {
2524
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
2625
import { useLanguage } from 'src/features/language/useLanguage';
2726
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';
27+
import { useUpdateIncrementalValidations } from 'src/features/validation/backendValidation/useUpdateIncrementalValidations';
3428
import { useWaitForState } from 'src/hooks/useWaitForState';
3529
import { doUpdateAttachmentTags } from 'src/queries/queries';
3630
import { nodesProduce } from 'src/utils/layout/NodesContext';
@@ -48,7 +42,6 @@ import type {
4842
import type { AttachmentsSelector } from 'src/features/attachments/tools';
4943
import type { AttachmentStateInfo } from 'src/features/attachments/types';
5044
import type { FDActionResult } from 'src/features/formData/FormDataWriteStateMachine';
51-
import type { BackendFieldValidatorGroups, BackendValidationIssue } from 'src/features/validation';
5245
import type { DSPropsForSimpleSelector } from 'src/hooks/delayedSelectors';
5346
import type { IDataModelBindingsList, IDataModelBindingsSimple } from 'src/layout/common.generated';
5447
import type { RejectedFileError } from 'src/layout/FileUpload/RejectedFileError';
@@ -418,12 +411,7 @@ export class AttachmentsStorePlugin extends NodeDataPlugin<AttachmentsStorePlugi
418411
update(action);
419412
try {
420413
if (appSupportsSetTagsEndpoint(backendVersion)) {
421-
await updateTags({
422-
dataElementId,
423-
setTagsRequest: {
424-
tags,
425-
},
426-
});
414+
await updateTags({ dataElementId, setTagsRequest: { tags } });
427415
} else {
428416
await Promise.all(tagsToAdd.map((tag) => addTag({ dataElementId: attachment.data.id, tagToAdd: tag })));
429417
await Promise.all(
@@ -796,8 +784,7 @@ function useAttachmentsRemoveTagMutation() {
796784

797785
function useAttachmentUpdateTagsMutation() {
798786
const instanceId = useLaxInstanceId();
799-
const defaultDataElementId = DataModels.useDefaultDataElementId();
800-
const updateBackendValidations = Validation.useUpdateBackendValidations();
787+
const updateIncrementalValidations = useUpdateIncrementalValidations();
801788

802789
return useMutation({
803790
mutationFn: ({ dataElementId, setTagsRequest }: { dataElementId: string; setTagsRequest: SetTagsRequest }) => {
@@ -811,23 +798,9 @@ function useAttachmentUpdateTagsMutation() {
811798
window.logError('Failed to add tag to attachment:\n', error);
812799
},
813800
onSuccess: (data) => {
814-
if (!data.validationIssues) {
815-
return;
801+
if (data.validationIssues) {
802+
updateIncrementalValidations(backendValidationIssueGroupListToObject(data.validationIssues));
816803
}
817-
818-
const backendValidations: BackendValidationIssue[] = data.validationIssues.reduce(
819-
(prev, curr) => [...prev, ...curr.issues],
820-
[],
821-
);
822-
const initialTaskValidations = mapBackendIssuesToTaskValidations(backendValidations);
823-
const initialValidatorGroups: BackendFieldValidatorGroups = mapBackendValidationsToValidatorGroups(
824-
backendValidations,
825-
defaultDataElementId,
826-
);
827-
828-
const dataModelValidations = mapValidatorGroupsToDataModelValidations(initialValidatorGroups);
829-
830-
updateBackendValidations(dataModelValidations, { initial: backendValidations }, initialTaskValidations);
831804
},
832805
});
833806
}

src/features/formData/FormDataWrite.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import { getFormDataQueryKey } from 'src/features/formData/useFormDataQuery';
2222
import { useLaxInstanceId, useOptimisticallyUpdateCachedInstance } from 'src/features/instance/InstanceContext';
2323
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
2424
import { useSelectedParty } from 'src/features/party/PartiesProvider';
25-
import { type BackendValidationIssueGroups, IgnoredValidators } from 'src/features/validation';
25+
import {
26+
backendValidationIssueGroupListToObject,
27+
type BackendValidationIssueGroups,
28+
IgnoredValidators,
29+
} from 'src/features/validation';
2630
import { useIsUpdatingInitialValidations } from 'src/features/validation/backendValidation/backendValidationQuery';
2731
import { useAsRef } from 'src/hooks/useAsRef';
2832
import { useWaitForState } from 'src/hooks/useWaitForState';
@@ -174,7 +178,7 @@ function useFormDataSaveMutation() {
174178
}
175179
}
176180

177-
const mutation = useMutation({
181+
return useMutation({
178182
mutationKey: saveFormDataMutationKey,
179183
scope: { id: saveFormDataMutationKey[0] },
180184
mutationFn: async (): Promise<FDSaveFinished | undefined> => {
@@ -303,13 +307,9 @@ function useFormDataSaveMutation() {
303307
}
304308
}
305309

306-
const validationIssueGroups: BackendValidationIssueGroups = Object.fromEntries(
307-
validationIssues.map(({ source, issues }) => [source, issues]),
308-
);
309-
310310
return {
311311
newDataModels: dataModelChanges,
312-
validationIssues: validationIssueGroups,
312+
validationIssues: backendValidationIssueGroupListToObject(validationIssues),
313313
instance,
314314
savedData: next,
315315
};
@@ -358,8 +358,6 @@ function useFormDataSaveMutation() {
358358
checkForRunawaySaving();
359359
},
360360
});
361-
362-
return mutation;
363361
}
364362

365363
function useIsSavingFormData() {
Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,41 @@
1-
import { useEffect, useRef } from 'react';
2-
3-
import deepEqual from 'fast-deep-equal';
1+
import { useEffect } from 'react';
42

53
import { DataModels } from 'src/features/datamodel/DataModelsProvider';
64
import { FD } from 'src/features/formData/FormDataWrite';
75
import { useBackendValidationQuery } from 'src/features/validation/backendValidation/backendValidationQuery';
86
import {
9-
mapBackendIssuesToFieldValidations,
107
mapBackendIssuesToTaskValidations,
118
mapBackendValidationsToValidatorGroups,
129
mapValidatorGroupsToDataModelValidations,
1310
useShouldValidateInitial,
1411
} from 'src/features/validation/backendValidation/backendValidationUtils';
12+
import { useUpdateIncrementalValidations } from 'src/features/validation/backendValidation/useUpdateIncrementalValidations';
1513
import { Validation } from 'src/features/validation/validationContext';
16-
import type { BackendFieldValidatorGroups } from 'src/features/validation';
1714

1815
export function BackendValidation() {
1916
const updateBackendValidations = Validation.useUpdateBackendValidations();
2017
const defaultDataElementId = DataModels.useDefaultDataElementId();
2118
const lastSaveValidations = FD.useLastSaveValidationIssues();
22-
const validatorGroups = useRef<BackendFieldValidatorGroups>({});
2319
const enabled = useShouldValidateInitial();
24-
const { data: initialValidations, isFetching } = useBackendValidationQuery({ enabled });
25-
const initialValidatorGroups: BackendFieldValidatorGroups = mapBackendValidationsToValidatorGroups(
26-
initialValidations,
27-
defaultDataElementId,
28-
);
29-
30-
// Map task validations
31-
const initialTaskValidations = mapBackendIssuesToTaskValidations(initialValidations);
20+
const { data: initialValidations, isFetching: isFetchingInitial } = useBackendValidationQuery({ enabled });
21+
const updateIncrementalValidations = useUpdateIncrementalValidations();
3222

3323
// Initial validation
3424
useEffect(() => {
35-
if (!isFetching) {
36-
validatorGroups.current = initialValidatorGroups;
25+
if (!isFetchingInitial) {
26+
const initialTaskValidations = mapBackendIssuesToTaskValidations(initialValidations);
27+
const initialValidatorGroups = mapBackendValidationsToValidatorGroups(initialValidations, defaultDataElementId);
3728
const backendValidations = mapValidatorGroupsToDataModelValidations(initialValidatorGroups);
3829
updateBackendValidations(backendValidations, { initial: initialValidations }, initialTaskValidations);
3930
}
40-
}, [initialTaskValidations, initialValidations, initialValidatorGroups, isFetching, updateBackendValidations]);
31+
}, [defaultDataElementId, initialValidations, isFetchingInitial, updateBackendValidations]);
4132

42-
// Incremental validation: Update validators and propagate changes to validationcontext
33+
// Incremental validation: Update validators and propagate changes to validation context
4334
useEffect(() => {
4435
if (lastSaveValidations) {
45-
const newValidatorGroups = structuredClone(validatorGroups.current);
46-
47-
for (const [group, validationIssues] of Object.entries(lastSaveValidations)) {
48-
newValidatorGroups[group] = mapBackendIssuesToFieldValidations(validationIssues, defaultDataElementId);
49-
}
50-
51-
if (deepEqual(validatorGroups.current, newValidatorGroups)) {
52-
// Dont update any validations, only set last saved validations
53-
updateBackendValidations(undefined, { incremental: lastSaveValidations });
54-
return;
55-
}
56-
57-
validatorGroups.current = newValidatorGroups;
58-
const backendValidations = mapValidatorGroupsToDataModelValidations(validatorGroups.current);
59-
updateBackendValidations(backendValidations, { incremental: lastSaveValidations });
36+
updateIncrementalValidations(lastSaveValidations);
6037
}
61-
}, [defaultDataElementId, lastSaveValidations, updateBackendValidations]);
38+
}, [lastSaveValidations, updateIncrementalValidations]);
6239

6340
return null;
6441
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useCallback } from 'react';
2+
3+
import deepEqual from 'fast-deep-equal';
4+
5+
import { DataModels } from 'src/features/datamodel/DataModelsProvider';
6+
import { useGetCachedInitialValidations } from 'src/features/validation/backendValidation/backendValidationQuery';
7+
import {
8+
mapBackendIssuesToFieldValidations,
9+
mapBackendValidationsToValidatorGroups,
10+
mapValidatorGroupsToDataModelValidations,
11+
} from 'src/features/validation/backendValidation/backendValidationUtils';
12+
import { Validation } from 'src/features/validation/validationContext';
13+
import type { BackendValidationIssueGroups } from 'src/features/validation';
14+
15+
/**
16+
* Hook for updating incremental validations from various sources (usually the validations updated from last saved data)
17+
*/
18+
export function useUpdateIncrementalValidations() {
19+
const updateBackendValidations = Validation.useUpdateBackendValidations();
20+
const defaultDataElementId = DataModels.useDefaultDataElementId();
21+
const getCachedInitialValidations = useGetCachedInitialValidations();
22+
23+
return useCallback(
24+
(lastSaveValidations: BackendValidationIssueGroups) => {
25+
const { cachedInitialValidations } = getCachedInitialValidations();
26+
const initialValidatorGroups = mapBackendValidationsToValidatorGroups(
27+
cachedInitialValidations,
28+
defaultDataElementId,
29+
);
30+
31+
const newValidatorGroups = structuredClone(initialValidatorGroups);
32+
for (const [group, validationIssues] of Object.entries(lastSaveValidations)) {
33+
newValidatorGroups[group] = mapBackendIssuesToFieldValidations(validationIssues, defaultDataElementId);
34+
}
35+
36+
if (deepEqual(initialValidatorGroups, newValidatorGroups)) {
37+
// Don't update any validations, only set last saved validations
38+
updateBackendValidations(undefined, { incremental: lastSaveValidations });
39+
return;
40+
}
41+
42+
const backendValidations = mapValidatorGroupsToDataModelValidations(newValidatorGroups);
43+
updateBackendValidations(backendValidations, { incremental: lastSaveValidations });
44+
},
45+
[defaultDataElementId, getCachedInitialValidations, updateBackendValidations],
46+
);
47+
}

src/features/validation/validationContext.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ function initialCreateStore() {
144144
const {
145145
Provider,
146146
useSelector,
147+
useStaticSelector,
147148
useMemoSelector,
148149
useLaxShallowSelector,
149150
useSelectorAsRef,
@@ -359,8 +360,8 @@ export const Validation = {
359360
}, [s]);
360361
},
361362
useValidating: () => useSelector((state) => state.validating!),
362-
useUpdateDataModelValidations: () => useSelector((state) => state.updateDataModelValidations),
363-
useUpdateBackendValidations: () => useSelector((state) => state.updateBackendValidations),
363+
useUpdateDataModelValidations: () => useStaticSelector((state) => state.updateDataModelValidations),
364+
useUpdateBackendValidations: () => useStaticSelector((state) => state.updateBackendValidations),
364365

365366
useFullState: <U,>(selector: (state: ValidationContext & Internals) => U): U =>
366367
useMemoSelector((state) => selector(state)),
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { AppFrontend } from 'test/e2e/pageobjects/app-frontend';
2+
3+
const appFrontend = new AppFrontend();
4+
5+
describe('Attachment tags validation', () => {
6+
beforeEach(() => {
7+
cy.intercept('**/active', []).as('noActiveInstances');
8+
cy.startAppInstance(appFrontend.apps.expressionValidationTest);
9+
});
10+
11+
it('should update validations when saving tags', () => {
12+
cy.gotoNavPage('CV');
13+
cy.findByRole('textbox', { name: /alder/i }).type('17');
14+
15+
// Opt-in to attachment type validation
16+
cy.findByRole('radio', { name: /ja/i }).dsCheck();
17+
18+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Vitnemål'");
19+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Søknad'");
20+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Motivasjonsbrev'");
21+
22+
cy.get(appFrontend.expressionValidationTest.cvUploader).selectFile('test/e2e/fixtures/test.pdf', { force: true });
23+
24+
cy.contains('Ferdig lastet').should('be.visible');
25+
cy.dsSelect(appFrontend.expressionValidationTest.groupTag, 'Søknad');
26+
cy.findByRole('button', { name: /^lagre$/i }).click();
27+
28+
// Verify "Søknad" validation is removed, but others remain
29+
cy.get(appFrontend.errorReport).should('not.contain.text', "Du må laste opp 'Søknad'");
30+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Vitnemål'");
31+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Motivasjonsbrev'");
32+
33+
cy.findByRole('button', { name: /rediger/i }).click();
34+
cy.dsSelect(appFrontend.expressionValidationTest.groupTag, 'Vitnemål');
35+
cy.findByRole('button', { name: /^lagre$/i }).click();
36+
37+
// Verify "Vitnemål" validation is removed and "Søknad" validation is back
38+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Søknad'");
39+
cy.get(appFrontend.errorReport).should('not.contain.text', "Du må laste opp 'Vitnemål'");
40+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Motivasjonsbrev'");
41+
42+
// Upload second file and tag as "Søknad"
43+
cy.get(appFrontend.expressionValidationTest.cvUploader).selectFile('test/e2e/fixtures/test.pdf', { force: true });
44+
cy.contains('Ferdig lastet').should('be.visible');
45+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Søknad'");
46+
cy.dsSelect(appFrontend.expressionValidationTest.groupTag, 'Søknad');
47+
cy.findAllByRole('button', { name: /^lagre$/i })
48+
.last()
49+
.click();
50+
51+
// Verify "Søknad" validation is removed
52+
cy.get(appFrontend.errorReport).should('not.contain.text', "Du må laste opp 'Søknad'");
53+
cy.get(appFrontend.errorReport).should('not.contain.text', "Du må laste opp 'Vitnemål'");
54+
cy.get(appFrontend.errorReport).should('contain.text', "Du må laste opp 'Motivasjonsbrev'");
55+
56+
// Upload third file and tag as "Motivasjonsbrev"
57+
cy.get(appFrontend.expressionValidationTest.cvUploader).selectFile('test/e2e/fixtures/test.pdf', { force: true });
58+
cy.contains('Ferdig lastet').should('be.visible');
59+
cy.dsSelect(appFrontend.expressionValidationTest.groupTag, 'Motivasjonsbrev');
60+
cy.findAllByRole('button', { name: /^lagre$/i })
61+
.last()
62+
.click();
63+
64+
// Verify no errors are visible, and tags are visible in the table
65+
cy.get(appFrontend.errorReport).should('not.exist');
66+
cy.contains('td', 'Søknad').should('be.visible');
67+
cy.contains('td', 'Vitnemål').should('be.visible');
68+
cy.contains('td', 'Motivasjonsbrev').should('be.visible');
69+
70+
// Fill in remaining required fields
71+
cy.findByRole('textbox', { name: /fornavn/i }).type('Per');
72+
cy.findByRole('textbox', { name: /etternavn/i }).type('Hansen');
73+
cy.dsSelect(appFrontend.expressionValidationTest.kjønn, 'Mann');
74+
cy.findByRole('textbox', { name: /e-post/i }).type('[email protected]');
75+
cy.findByRole('textbox', { name: /telefonnummer/i }).type('98765432');
76+
cy.dsSelect(appFrontend.expressionValidationTest.bosted, 'Oslo');
77+
78+
cy.findByRole('button', { name: /neste/i }).click();
79+
cy.findByRole('button', { name: /send inn/i }).click();
80+
cy.get(appFrontend.receipt.container).should('be.visible');
81+
});
82+
});

test/e2e/pageobjects/app-frontend.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ export class AppFrontend {
350350
kjønn: '#kjonn',
351351
bosted: '#bosted',
352352
groupTag: 'input[id^=attachment-tag]',
353+
cvUploader: '#vedlegg-cv',
353354
uploaders: '[id^=Vedlegg-]',
354355
};
355356

0 commit comments

Comments
 (0)