From c88f1f42a026a777e33c31f5ec0a630c6aeee10d Mon Sep 17 00:00:00 2001 From: Camilla Marie Dalan Date: Wed, 27 Aug 2025 13:59:07 +0200 Subject: [PATCH 1/2] cleans up and collocates versions and features --- .../ApplicationMetadataProvider.tsx | 10 +-- .../VersionErrorOrChildren.tsx | 3 +- .../attachments/AttachmentsStorePlugin.tsx | 5 +- src/features/attachments/tools.ts | 6 -- src/features/instance/useProcessNext.tsx | 22 +++---- src/features/language/LanguageProvider.tsx | 10 +-- .../backendValidationQuery.ts | 6 +- .../backendValidationUtils.ts | 19 ------ .../PDFPreviewButtonComponent.tsx | 14 ++--- .../{ => versioning}/versionCompare.test.ts | 2 +- src/utils/{ => versioning}/versionCompare.ts | 0 src/utils/versioning/versions.ts | 63 +++++++++++++++++++ 12 files changed, 92 insertions(+), 68 deletions(-) rename src/utils/{ => versioning}/versionCompare.test.ts (94%) rename src/utils/{ => versioning}/versionCompare.ts (100%) create mode 100644 src/utils/versioning/versions.ts diff --git a/src/features/applicationMetadata/ApplicationMetadataProvider.tsx b/src/features/applicationMetadata/ApplicationMetadataProvider.tsx index c5dae60c42..743f9baeb1 100644 --- a/src/features/applicationMetadata/ApplicationMetadataProvider.tsx +++ b/src/features/applicationMetadata/ApplicationMetadataProvider.tsx @@ -7,11 +7,10 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { delayedContext } from 'src/core/contexts/delayedContext'; import { createQueryContext } from 'src/core/contexts/queryContext'; import { onEntryValuesThatHaveState } from 'src/features/applicationMetadata/appMetadataUtils'; -import { MINIMUM_APPLICATION_VERSION } from 'src/features/applicationMetadata/minVersion'; import { VersionErrorOrChildren } from 'src/features/applicationMetadata/VersionErrorOrChildren'; import { useNavigationParam } from 'src/hooks/navigation'; import { fetchApplicationMetadata } from 'src/queries/queries'; -import { isAtLeastVersion } from 'src/utils/versionCompare'; +import { isMinimumApplicationVersion } from 'src/utils/versioning/versions'; import type { ApplicationMetadata, IncomingApplicationMetadata } from 'src/features/applicationMetadata/types'; // Also used for prefetching @see appPrefetcher.ts @@ -24,12 +23,7 @@ export function getApplicationMetadataQueryDef(instanceGuid: string | undefined) return { ...data, - isValidVersion: - !!data.altinnNugetVersion && - isAtLeastVersion({ - actualVersion: data.altinnNugetVersion, - minimumVersion: MINIMUM_APPLICATION_VERSION.build, - }), + isValidVersion: isMinimumApplicationVersion(data.altinnNugetVersion), onEntry, isStatelessApp: isStatelessApp(!!instanceGuid, onEntry.show), logoOptions: data.logo, diff --git a/src/features/applicationMetadata/VersionErrorOrChildren.tsx b/src/features/applicationMetadata/VersionErrorOrChildren.tsx index 72ffd316c6..56c4422f91 100644 --- a/src/features/applicationMetadata/VersionErrorOrChildren.tsx +++ b/src/features/applicationMetadata/VersionErrorOrChildren.tsx @@ -2,7 +2,6 @@ import React from 'react'; import type { PropsWithChildren } from 'react'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; -import { MINIMUM_APPLICATION_VERSION } from 'src/features/applicationMetadata/minVersion'; import { InstantiationErrorPage } from 'src/features/instantiate/containers/InstantiationErrorPage'; import { Lang } from 'src/features/language/Lang'; @@ -21,7 +20,7 @@ export function VersionErrorOrChildren({ children }: PropsWithChildren) {
} diff --git a/src/features/attachments/AttachmentsStorePlugin.tsx b/src/features/attachments/AttachmentsStorePlugin.tsx index 0646e19a77..dfd0c927e5 100644 --- a/src/features/attachments/AttachmentsStorePlugin.tsx +++ b/src/features/attachments/AttachmentsStorePlugin.tsx @@ -11,7 +11,7 @@ import { ContextNotProvided } from 'src/core/contexts/context'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; import { isAttachmentUploaded, isDataPostError } from 'src/features/attachments/index'; import { sortAttachmentsByName } from 'src/features/attachments/sortAttachments'; -import { appSupportsNewAttachmentAPI, attachmentSelector } from 'src/features/attachments/tools'; +import { attachmentSelector } from 'src/features/attachments/tools'; import { FileScanResults } from 'src/features/attachments/types'; import { FD } from 'src/features/formData/FormDataWrite'; import { dataModelPairsToObject } from 'src/features/formData/types'; @@ -28,6 +28,7 @@ import { useWaitForState } from 'src/hooks/useWaitForState'; import { nodesProduce } from 'src/utils/layout/NodesContext'; import { NodeDataPlugin } from 'src/utils/layout/plugins/NodeDataPlugin'; import { splitDashedKey } from 'src/utils/splitDashedKey'; +import { appSupportsNewAttachmentAPI } from 'src/utils/versioning/versions'; import type { DataPostResponse, IAttachment, @@ -296,7 +297,7 @@ export class AttachmentsStorePlugin extends NodeDataPlugin (state: NodesContext) => { } return emptyArray; }; - -export function appSupportsNewAttachmentAPI({ altinnNugetVersion }: ApplicationMetadata) { - return !altinnNugetVersion || isAtLeastVersion({ actualVersion: altinnNugetVersion, minimumVersion: '8.5.0.153' }); -} diff --git a/src/features/instance/useProcessNext.tsx b/src/features/instance/useProcessNext.tsx index 13f9a0e315..e8f3a40c71 100644 --- a/src/features/instance/useProcessNext.tsx +++ b/src/features/instance/useProcessNext.tsx @@ -12,13 +12,14 @@ import { useOptimisticallyUpdateProcess, useProcessQuery } from 'src/features/in import { Lang } from 'src/features/language/Lang'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; import { useUpdateInitialValidations } from 'src/features/validation/backendValidation/backendValidationQuery'; -import { appSupportsIncrementalValidationFeatures } from 'src/features/validation/backendValidation/backendValidationUtils'; import { useOnFormSubmitValidation } from 'src/features/validation/callbacks/onFormSubmitValidation'; import { Validation } from 'src/features/validation/validationContext'; import { TaskKeys, useNavigateToTask } from 'src/hooks/useNavigatePage'; import { doProcessNext } from 'src/queries/queries'; -import { isAtLeastVersion } from 'src/utils/versionCompare'; -import type { ApplicationMetadata } from 'src/features/applicationMetadata/types'; +import { + appSupportsIncrementalValidationFeatures, + appSupportsUnlockingOnProcessNextFailure, +} from 'src/utils/versioning/versions'; import type { BackendValidationIssue } from 'src/features/validation'; import type { IActionType, IProcess, ProblemDetails } from 'src/types/shared'; import type { HttpClientError } from 'src/utils/network/sharedNetworking'; @@ -44,12 +45,15 @@ export function useProcessNext({ action }: ProcessNextProps = {}) { const updateInitialValidations = useUpdateInitialValidations(); const setShowAllBackendErrors = Validation.useSetShowAllBackendErrors(); const onSubmitFormValidation = useOnFormSubmitValidation(); - const applicationMetadata = useApplicationMetadata(); const queryClient = useQueryClient(); const displayError = useDisplayError(); const hasPendingScans = useHasPendingScans(); const optimisticallyUpdateProcess = useOptimisticallyUpdateProcess(); + const altinnNugetVersion = useApplicationMetadata().altinnNugetVersion; + const isUnlockingOnProcessNextSupported = appSupportsUnlockingOnProcessNextFailure(altinnNugetVersion); + const isIncrementalValidationSupported = appSupportsIncrementalValidationFeatures(altinnNugetVersion); + return useMutation({ scope: { id: 'process/next' }, mutationKey: getProcessNextMutationKey(action), @@ -76,7 +80,7 @@ export function useProcessNext({ action }: ProcessNextProps = {}) { } else if ( error.response?.status === 500 && error.response?.data?.['detail'] === 'Pdf generation failed' && - appSupportsUnlockingOnProcessNextFailure(applicationMetadata) + isUnlockingOnProcessNextSupported ) { // If process next fails due to the PDF generator failing, don't show unknown error if the app unlocks data elements toast(, { type: 'error', autoClose: false }); @@ -100,7 +104,7 @@ export function useProcessNext({ action }: ProcessNextProps = {}) { navigateToTask(task); } else if (validationIssues) { // Set initial validation to validation issues from process/next and make all errors visible - updateInitialValidations(validationIssues, !appSupportsIncrementalValidationFeatures(applicationMetadata)); + updateInitialValidations(validationIssues, !isIncrementalValidationSupported); const hasValidationErrors = await onSubmitFormValidation(true); if (!hasValidationErrors) { @@ -111,7 +115,7 @@ export function useProcessNext({ action }: ProcessNextProps = {}) { onError: async (error: HttpClientError) => { window.logError('Process next failed:\n', error); - if (!appSupportsUnlockingOnProcessNextFailure(applicationMetadata)) { + if (!isUnlockingOnProcessNextSupported) { displayError(error); return; } @@ -132,10 +136,6 @@ export function useProcessNext({ action }: ProcessNextProps = {}) { }); } -function appSupportsUnlockingOnProcessNextFailure({ altinnNugetVersion }: ApplicationMetadata) { - return !altinnNugetVersion || isAtLeastVersion({ actualVersion: altinnNugetVersion, minimumVersion: '8.1.0.115' }); -} - export function getTargetTaskFromProcess(processData: IProcess | undefined) { if (!processData) { return undefined; diff --git a/src/features/language/LanguageProvider.tsx b/src/features/language/LanguageProvider.tsx index 42f465cfeb..bf1542e664 100644 --- a/src/features/language/LanguageProvider.tsx +++ b/src/features/language/LanguageProvider.tsx @@ -7,7 +7,7 @@ import { useGetAppLanguageQuery } from 'src/features/language/textResources/useG import { useProfileQuery } from 'src/features/profile/ProfileProvider'; import { useIsAllowAnonymous } from 'src/features/stateless/getAllowAnonymous'; import { useLocalStorageState } from 'src/hooks/useLocalStorageState'; -import { isAtLeastVersion } from 'src/utils/versionCompare'; +import { appSupportsFetchAppLanguagesInAnonymous } from 'src/utils/versioning/versions'; interface LanguageCtx { current: string; @@ -91,15 +91,9 @@ export const SetShouldFetchAppLanguages = () => { // We make the same assumption as in ProfileProvider that the user is logged in when the app does not allow anonymous. const userIsAuthenticated = useIsAllowAnonymous(false); const { altinnNugetVersion } = useApplicationMetadata(); - const appSupportsFetchAppLanguagesInAnonymous = - altinnNugetVersion && - isAtLeastVersion({ - actualVersion: altinnNugetVersion, - minimumVersion: '8.5.6.180', - }); const setShouldFetchAppLanguages = useCtx().setShouldFetchAppLanguages; - const shouldFetchAppLanguages = appSupportsFetchAppLanguagesInAnonymous || userIsAuthenticated; + const shouldFetchAppLanguages = appSupportsFetchAppLanguagesInAnonymous(altinnNugetVersion) || userIsAuthenticated; useEffect(() => { setShouldFetchAppLanguages(shouldFetchAppLanguages); }, [shouldFetchAppLanguages, setShouldFetchAppLanguages]); diff --git a/src/features/validation/backendValidation/backendValidationQuery.ts b/src/features/validation/backendValidation/backendValidationQuery.ts index 15ec877d49..8422457600 100644 --- a/src/features/validation/backendValidation/backendValidationQuery.ts +++ b/src/features/validation/backendValidation/backendValidationQuery.ts @@ -11,9 +11,9 @@ import { useCurrentDataModelGuid } from 'src/features/datamodel/useBindingSchema import { useLaxInstanceId } from 'src/features/instance/InstanceContext'; import { useProcessQuery } from 'src/features/instance/useProcessQuery'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; -import { appSupportsIncrementalValidationFeatures } from 'src/features/validation/backendValidation/backendValidationUtils'; import { useAsRef } from 'src/hooks/useAsRef'; import { fetchBackendValidationsForDataElement } from 'src/queries/queries'; +import { appSupportsIncrementalValidationFeatures } from 'src/utils/versioning/versions'; import type { fetchBackendValidations } from 'src/queries/queries'; /** @@ -132,7 +132,9 @@ export function useBackendValidationQuery( ) { const queryKey = useBackendValidationQueryKey(); const { fetchBackendValidations, fetchBackendValidationsForDataElement } = useAppQueries(); - const hasIncrementalValidationFeatures = appSupportsIncrementalValidationFeatures(useApplicationMetadata()); + const hasIncrementalValidationFeatures = appSupportsIncrementalValidationFeatures( + useApplicationMetadata().altinnNugetVersion, + ); const currentDataElementID = useCurrentDataModelGuid(); const instanceId = useLaxInstanceId(); const currentLanguage = useAsRef(useCurrentLanguage()).current; diff --git a/src/features/validation/backendValidation/backendValidationUtils.ts b/src/features/validation/backendValidation/backendValidationUtils.ts index abd7d69a0b..8ac950e244 100644 --- a/src/features/validation/backendValidation/backendValidationUtils.ts +++ b/src/features/validation/backendValidation/backendValidationUtils.ts @@ -7,8 +7,6 @@ import { BackendValidationSeverity, BuiltInValidationIssueSources, ValidationMas import { validationTexts } from 'src/features/validation/backendValidation/validationTexts'; import { useIsPdf } from 'src/hooks/useIsPdf'; import { TaskKeys } from 'src/hooks/useNavigatePage'; -import { isAtLeastVersion } from 'src/utils/versionCompare'; -import type { ApplicationMetadata } from 'src/features/applicationMetadata/types'; import type { TextReference } from 'src/features/language/useLanguage'; import type { BackendFieldValidatorGroups, @@ -179,20 +177,3 @@ export function mapValidatorGroupsToDataModelValidations( return backendValidations; } - -/** - * TODO(Subform): Make sure we reference the correct version here, and in applicationMetadataMock - * - * Prior to app-lib version 8.5.0 there was no way of identifying validation messages that were not run incrementally (ITaskValidator), - * this led to an edge case where if an ITaskValidator returned a validation message with a field, we could not - * distinguish this from a regular custom backend validation which does runs incrementally. The problem is that we block - * submit when we have custom backend validation errors until they are fixed, but since ITaskValidator is not run - * incrementally it would never get fixed until the user refreshed the page. This issue was somewhat mitigated - * by the old dataElement validation API which did not run ITaskValidators. - * - * Therefore, if this function returns false, this means that the app does not make this distinction, but - * has the old API available, so this needs to be used for backwards compatibility. - */ -export function appSupportsIncrementalValidationFeatures({ altinnNugetVersion }: ApplicationMetadata) { - return !altinnNugetVersion || isAtLeastVersion({ actualVersion: altinnNugetVersion, minimumVersion: '8.5.0.141' }); -} diff --git a/src/layout/PDFPreviewButton/PDFPreviewButtonComponent.tsx b/src/layout/PDFPreviewButton/PDFPreviewButtonComponent.tsx index 5e684a1a11..dbab3539a3 100644 --- a/src/layout/PDFPreviewButton/PDFPreviewButtonComponent.tsx +++ b/src/layout/PDFPreviewButton/PDFPreviewButtonComponent.tsx @@ -7,22 +7,18 @@ import { useApplicationMetadata } from 'src/features/applicationMetadata/Applica import { useStrictInstanceId } from 'src/features/instance/InstanceContext'; import { NodesInternal } from 'src/utils/layout/NodesContext'; import { useItemWhenType } from 'src/utils/layout/useNodeItem'; -import { isAtLeastVersion } from 'src/utils/versionCompare'; +import { appSupportsPdfPreviewButton, FEATURE_VERSION_MAP } from 'src/utils/versioning/versions'; import type { NodeValidationProps } from 'src/layout/layout'; export function PDFPreviewButtonRenderLayoutValidator({ intermediateItem }: NodeValidationProps<'PDFPreviewButton'>) { const instanceId = useStrictInstanceId(); const addError = NodesInternal.useAddError(); const applicationMetadata = useApplicationMetadata(); - const minimumBackendVersion = '8.5.0.157'; - const backendVersionOK = isAtLeastVersion({ - actualVersion: applicationMetadata.altinnNugetVersion ?? '', - minimumVersion: minimumBackendVersion, - }); + const isPdfPreviewButtonSupported = appSupportsPdfPreviewButton(applicationMetadata.altinnNugetVersion); useEffect(() => { - if (!backendVersionOK) { - const error = `Need to be on at least backend version: ${minimumBackendVersion} to user this component`; + if (!isPdfPreviewButtonSupported) { + const error = `Need to be on at least backend version: ${FEATURE_VERSION_MAP.PDF_PREVIEW_BUTTON} to use this component`; addError(error, intermediateItem.id, 'node'); window.logErrorOnce(`Validation error for '${intermediateItem.id}': ${error}`); } @@ -32,7 +28,7 @@ export function PDFPreviewButtonRenderLayoutValidator({ intermediateItem }: Node addError(error, intermediateItem.id, 'node'); window.logErrorOnce(`Validation error for '${intermediateItem.id}': ${error}`); } - }, [addError, backendVersionOK, instanceId, intermediateItem.id]); + }, [addError, isPdfPreviewButtonSupported, instanceId, intermediateItem.id]); return null; } diff --git a/src/utils/versionCompare.test.ts b/src/utils/versioning/versionCompare.test.ts similarity index 94% rename from src/utils/versionCompare.test.ts rename to src/utils/versioning/versionCompare.test.ts index f1368faf2e..6c597c650e 100644 --- a/src/utils/versionCompare.test.ts +++ b/src/utils/versioning/versionCompare.test.ts @@ -1,4 +1,4 @@ -import { isAtLeastVersion } from 'src/utils/versionCompare'; +import { isAtLeastVersion } from 'src/utils/versioning/versionCompare'; describe('versionCompare', () => { interface TestCase { diff --git a/src/utils/versionCompare.ts b/src/utils/versioning/versionCompare.ts similarity index 100% rename from src/utils/versionCompare.ts rename to src/utils/versioning/versionCompare.ts diff --git a/src/utils/versioning/versions.ts b/src/utils/versioning/versions.ts new file mode 100644 index 0000000000..8715c906b7 --- /dev/null +++ b/src/utils/versioning/versions.ts @@ -0,0 +1,63 @@ +import { isAtLeastVersion } from 'src/utils/versioning/versionCompare'; + +export const FEATURE_VERSION_MAP = { + MINIMUM_APPLICATION_VERSION: '8.0.0.108', + UNLOCKING_ON_PROCESS_NEXT_FAILURE: '8.1.0.115', + INCREMENTAL_VALIDATION: '8.5.0.141', + NEW_ATTACHMENTS_API: '8.5.0.153', + PDF_PREVIEW_BUTTON: '8.5.0.157', + APP_LANGUAGES_IN_ANONYMOUS: '8.5.6.180', +} as const; + +type AppFeature = keyof typeof FEATURE_VERSION_MAP; + +function isFeatureSupported({ + feature, + currentNugetVersion, +}: { + feature: AppFeature; + currentNugetVersion: string | undefined; +}) { + if (!currentNugetVersion) { + return false; + } + + return isAtLeastVersion({ actualVersion: currentNugetVersion, minimumVersion: FEATURE_VERSION_MAP[feature] }); +} + +export function isMinimumApplicationVersion(currentNugetVersion: string | undefined) { + return isFeatureSupported({ feature: 'MINIMUM_APPLICATION_VERSION', currentNugetVersion }); +} + +export function appSupportsPdfPreviewButton(currentNugetVersion: string | undefined) { + return isFeatureSupported({ feature: 'PDF_PREVIEW_BUTTON', currentNugetVersion }); +} + +export function appSupportsFetchAppLanguagesInAnonymous(currentNugetVersion: string | undefined) { + return isFeatureSupported({ feature: 'APP_LANGUAGES_IN_ANONYMOUS', currentNugetVersion }); +} + +export function appSupportsUnlockingOnProcessNextFailure(currentNugetVersion: string | undefined) { + return isFeatureSupported({ feature: 'UNLOCKING_ON_PROCESS_NEXT_FAILURE', currentNugetVersion }); +} + +export function appSupportsNewAttachmentAPI(currentNugetVersion: string | undefined) { + return isFeatureSupported({ feature: 'NEW_ATTACHMENTS_API', currentNugetVersion }); +} + +/** + * TODO(Subform): Make sure we reference the correct version here, and in applicationMetadataMock + * + * Prior to app-lib version 8.5.0 there was no way of identifying validation messages that were not run incrementally (ITaskValidator), + * this led to an edge case where if an ITaskValidator returned a validation message with a field, we could not + * distinguish this from a regular custom backend validation which does runs incrementally. The problem is that we block + * submit when we have custom backend validation errors until they are fixed, but since ITaskValidator is not run + * incrementally it would never get fixed until the user refreshed the page. This issue was somewhat mitigated + * by the old dataElement validation API which did not run ITaskValidators. + * + * Therefore, if this function returns false, this means that the app does not make this distinction, but + * has the old API available, so this needs to be used for backwards compatibility. + */ +export function appSupportsIncrementalValidationFeatures(currentNugetVersion: string | undefined) { + return isFeatureSupported({ feature: 'INCREMENTAL_VALIDATION', currentNugetVersion }); +} From 105403b6f41682d4e1935886228f54a5fb1cc4ed Mon Sep 17 00:00:00 2001 From: Camilla Marie Dalan Date: Wed, 27 Aug 2025 17:03:25 +0200 Subject: [PATCH 2/2] adds qa suggestions --- src/features/applicationMetadata/VersionErrorOrChildren.tsx | 3 ++- src/features/applicationMetadata/minVersion.ts | 4 ---- src/utils/versioning/versions.ts | 1 + 3 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 src/features/applicationMetadata/minVersion.ts diff --git a/src/features/applicationMetadata/VersionErrorOrChildren.tsx b/src/features/applicationMetadata/VersionErrorOrChildren.tsx index 56c4422f91..01b187b231 100644 --- a/src/features/applicationMetadata/VersionErrorOrChildren.tsx +++ b/src/features/applicationMetadata/VersionErrorOrChildren.tsx @@ -4,6 +4,7 @@ import type { PropsWithChildren } from 'react'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; import { InstantiationErrorPage } from 'src/features/instantiate/containers/InstantiationErrorPage'; import { Lang } from 'src/features/language/Lang'; +import { MINIMUM_APPLICATION_VERSION_NAME } from 'src/utils/versioning/versions'; export function VersionErrorOrChildren({ children }: PropsWithChildren) { const { isValidVersion } = useApplicationMetadata(); @@ -20,7 +21,7 @@ export function VersionErrorOrChildren({ children }: PropsWithChildren) {
} diff --git a/src/features/applicationMetadata/minVersion.ts b/src/features/applicationMetadata/minVersion.ts deleted file mode 100644 index d36f6c123d..0000000000 --- a/src/features/applicationMetadata/minVersion.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const MINIMUM_APPLICATION_VERSION = { - build: '8.0.0.108', - name: 'v8.0.0', -}; diff --git a/src/utils/versioning/versions.ts b/src/utils/versioning/versions.ts index 8715c906b7..071ac2ad54 100644 --- a/src/utils/versioning/versions.ts +++ b/src/utils/versioning/versions.ts @@ -1,5 +1,6 @@ import { isAtLeastVersion } from 'src/utils/versioning/versionCompare'; +export const MINIMUM_APPLICATION_VERSION_NAME = 'v8.0.0'; export const FEATURE_VERSION_MAP = { MINIMUM_APPLICATION_VERSION: '8.0.0.108', UNLOCKING_ON_PROCESS_NEXT_FAILURE: '8.1.0.115',