From eb8b60242640896ef6ac4539fc7e5d2612512d30 Mon Sep 17 00:00:00 2001 From: Camilla Marie Dalan Date: Thu, 30 Oct 2025 17:11:07 +0100 Subject: [PATCH] uses ValidationContext as single source of truth for incremental validations in order to fix incremental validations from fdw not matching incremental validations from tag updates --- .../attachments/AttachmentsStorePlugin.tsx | 4 +--- src/features/formData/FormDataWrite.tsx | 21 +++++++------------ .../formData/FormDataWriteStateMachine.tsx | 6 +----- .../backendValidation/BackendValidation.tsx | 11 ---------- src/features/validation/validationContext.tsx | 13 ++++++------ .../FileUploadWithTag/EditWindowComponent.tsx | 12 +++++------ 6 files changed, 22 insertions(+), 45 deletions(-) diff --git a/src/features/attachments/AttachmentsStorePlugin.tsx b/src/features/attachments/AttachmentsStorePlugin.tsx index 93b7a82caa..5b2c73e5b4 100644 --- a/src/features/attachments/AttachmentsStorePlugin.tsx +++ b/src/features/attachments/AttachmentsStorePlugin.tsx @@ -418,10 +418,8 @@ export class AttachmentsStorePlugin extends NodeDataPlugin removeTag({ dataElementId: attachment.data.id, tagToRemove: tag })), ); } - fulfill(action); optimisticallyUpdateDataElement(dataElementId, (dataElement) => ({ ...dataElement, tags })); - - return; + fulfill(action); } catch (error) { reject(action, error); toast(lang('form_filler.file_uploader_validation_error_update'), { type: 'error' }); diff --git a/src/features/formData/FormDataWrite.tsx b/src/features/formData/FormDataWrite.tsx index 024ae1e637..0abe60dc70 100644 --- a/src/features/formData/FormDataWrite.tsx +++ b/src/features/formData/FormDataWrite.tsx @@ -28,6 +28,7 @@ import { IgnoredValidators, } from 'src/features/validation'; import { useIsUpdatingInitialValidations } from 'src/features/validation/backendValidation/backendValidationQuery'; +import { Validation } from 'src/features/validation/validationContext'; import { useAsRef } from 'src/hooks/useAsRef'; import { useWaitForState } from 'src/hooks/useWaitForState'; import { doPatchMultipleFormData } from 'src/queries/queries'; @@ -103,6 +104,8 @@ function useFormDataSaveMutation() { const isStateless = useApplicationMetadata().isStatelessApp; const debounce = useSelector((s) => s.debounce); const selectedPartyId = useSelectedParty()?.partyId; + const updateBackendValidations = Validation.useLaxUpdateBackendValidations(); + const waitFor = useWaitForState< { prev: { [dataType: string]: object }; next: { [dataType: string]: object } }, FormDataContext @@ -352,6 +355,9 @@ function useFormDataSaveMutation() { if (result) { updateQueryCache(result); saveFinished(result); + if (updateBackendValidations !== ContextNotProvided) { + updateBackendValidations(undefined, { incremental: result.validationIssues }); + } } else { cancelSave(); } @@ -621,9 +627,8 @@ const useWaitForSave = () => { return Promise.resolve(undefined); } - return await waitFor((state, setReturnValue) => { + return await waitFor((state) => { if (state === ContextNotProvided) { - setReturnValue(undefined); return true; } @@ -632,12 +637,7 @@ const useWaitForSave = () => { return false; } - if (hasUnsavedChanges(state)) { - return false; - } - - setReturnValue(state.validationIssues); - return true; + return !hasUnsavedChanges(state); }); }, [requestSave, dataTypes, waitFor], @@ -1155,11 +1155,6 @@ export const FD = { */ useRemoveValueFromList: () => useStaticSelector((s) => s.removeValueFromList), - /** - * Returns the latest validation issues from the backend, from the last time the form data was saved. - */ - useLastSaveValidationIssues: () => useSelector((s) => s.validationIssues), - useRemoveIndexFromList: () => useStaticSelector((s) => s.removeIndexFromList), useGetDataTypeForElementId: () => { diff --git a/src/features/formData/FormDataWriteStateMachine.tsx b/src/features/formData/FormDataWriteStateMachine.tsx index 2b84f891a4..7b70e97909 100644 --- a/src/features/formData/FormDataWriteStateMachine.tsx +++ b/src/features/formData/FormDataWriteStateMachine.tsx @@ -85,9 +85,6 @@ type FormDataState = { // as a way to immediately save the data model to the server, for example before locking the data model. manualSaveRequested: boolean; - // This contains the validation issues we receive from the server last time we saved the data model. - validationIssues: BackendValidationIssueGroups | undefined; - // This is used to track which component is currently blocking the auto-saving feature. If this is set to a string // value, auto-saving will be disabled, even if the autoSaving flag is set to true. This is useful when you want // to temporarily disable auto-saving, for example when clicking a CustomButton and waiting for the server to @@ -260,9 +257,8 @@ function makeActions( } function processChanges(state: FormDataContext, toProcess: FDSaveFinished) { - const { validationIssues, savedData, newDataModels, instance } = toProcess; + const { savedData, newDataModels, instance } = toProcess; state.manualSaveRequested = false; - state.validationIssues = validationIssues; if (instance) { changeInstance(() => instance); diff --git a/src/features/validation/backendValidation/BackendValidation.tsx b/src/features/validation/backendValidation/BackendValidation.tsx index 3e40849c6c..56189ba29a 100644 --- a/src/features/validation/backendValidation/BackendValidation.tsx +++ b/src/features/validation/backendValidation/BackendValidation.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react'; import { DataModels } from 'src/features/datamodel/DataModelsProvider'; -import { FD } from 'src/features/formData/FormDataWrite'; import { useBackendValidationQuery } from 'src/features/validation/backendValidation/backendValidationQuery'; import { mapBackendIssuesToTaskValidations, @@ -9,16 +8,13 @@ import { mapValidatorGroupsToDataModelValidations, useShouldValidateInitial, } from 'src/features/validation/backendValidation/backendValidationUtils'; -import { useUpdateIncrementalValidations } from 'src/features/validation/backendValidation/useUpdateIncrementalValidations'; import { Validation } from 'src/features/validation/validationContext'; export function BackendValidation() { const updateBackendValidations = Validation.useUpdateBackendValidations(); const defaultDataElementId = DataModels.useDefaultDataElementId(); - const lastSaveValidations = FD.useLastSaveValidationIssues(); const enabled = useShouldValidateInitial(); const { data: initialValidations, isFetching: isFetchingInitial } = useBackendValidationQuery({ enabled }); - const updateIncrementalValidations = useUpdateIncrementalValidations(); // Initial validation useEffect(() => { @@ -30,12 +26,5 @@ export function BackendValidation() { } }, [defaultDataElementId, initialValidations, isFetchingInitial, updateBackendValidations]); - // Incremental validation: Update validators and propagate changes to validation context - useEffect(() => { - if (lastSaveValidations) { - updateIncrementalValidations(lastSaveValidations); - } - }, [lastSaveValidations, updateIncrementalValidations]); - return null; } diff --git a/src/features/validation/validationContext.tsx b/src/features/validation/validationContext.tsx index 8cceca5fca..9492ecc30f 100644 --- a/src/features/validation/validationContext.tsx +++ b/src/features/validation/validationContext.tsx @@ -199,14 +199,12 @@ function useWaitForValidation(): WaitForValidation { // Wait until we've saved changed to backend, and we've processed the backend validations we got from that save await waitForNodesReady(); - const validationsFromSave = await waitForSave(forceSave); + await waitForSave(forceSave); // If validationsFromSave is not defined, we check if initial validations are done processing await waitForState(async (state) => { const { isFetching, cachedInitialValidations } = getCachedInitialValidations(); - const incrementalMatch = deepEqual(state.processedLast.incremental, validationsFromSave); const initialMatch = deepEqual(state.processedLast.initial, cachedInitialValidations); - - const validationsReady = incrementalMatch && initialMatch && !isFetching; + const validationsReady = initialMatch && !isFetching; if (validationsReady) { await waitForNodesToValidate(state.processedLast); @@ -266,9 +264,9 @@ function UpdateShowAllErrors() { * Call /validate manually whenever a data element changes to get updated non-incremental validations. * This should happen whenever any data element changes, so we should check the lastChanged on each data element, * or if new data elements are added. Single-patch does not return updated instance data so for now we need to - * also check useLastSaveValidationIssues which will change on each patch. + * also check useGetIncrementalValidations which will change on each patch. */ - const lastSaved = FD.useLastSaveValidationIssues(); + const lastSaved = Validation.useGetIncrementalValidations(); const instanceDataChanges = useInstanceDataQuery({ select: (instance) => instance.data.map(({ id, lastChanged }) => ({ id, lastChanged })), }).data; @@ -362,7 +360,8 @@ export const Validation = { useValidating: () => useSelector((state) => state.validating!), useUpdateDataModelValidations: () => useStaticSelector((state) => state.updateDataModelValidations), useUpdateBackendValidations: () => useStaticSelector((state) => state.updateBackendValidations), - + useLaxUpdateBackendValidations: () => useLaxShallowSelector((state) => state.updateBackendValidations), + useGetIncrementalValidations: () => useSelector((state) => state.processedLast.incremental), useFullState: (selector: (state: ValidationContext & Internals) => U): U => useMemoSelector((state) => selector(state)), useGetProcessedLast: () => { diff --git a/src/layout/FileUploadWithTag/EditWindowComponent.tsx b/src/layout/FileUploadWithTag/EditWindowComponent.tsx index cb92936d9c..f8fb975ddf 100644 --- a/src/layout/FileUploadWithTag/EditWindowComponent.tsx +++ b/src/layout/FileUploadWithTag/EditWindowComponent.tsx @@ -56,16 +56,16 @@ export function EditWindowComponent({ const hasErrors = hasValidationErrors(attachmentValidations); - const formatSelectedValue = (tags: string[]): string | SuggestionItem | undefined => { + function formatSelectedValue(tags: string[]): string | SuggestionItem | undefined { const tag = tags[0]; if (!tag) { return undefined; } const option = options?.find((o) => o.value === tag); return option ? { value: option.value, label: langAsString(option.label) } : tag; - }; + } - const handleSave = async () => { + async function handleSave() { if (!uploadedAttachment) { return; } @@ -78,15 +78,15 @@ export function EditWindowComponent({ } setEditIndex(-1); await onAttachmentSave(baseComponentId, uploadedAttachment.data.id); - }; + } - const setAttachmentTag = async (tags: string[]) => { + async function setAttachmentTag(tags: string[]) { if (!isAttachmentUploaded(attachment)) { return; } await updateAttachment({ attachment, nodeId, tags }); - }; + } const isLoading = attachment.updating || !attachment.uploaded || isFetching || options?.length === 0; const uniqueId = isAttachmentUploaded(attachment) ? attachment.data.id : attachment.data.temporaryId;