From 7606e482c71dfdc5bac9e50201f75ccdcc1615d9 Mon Sep 17 00:00:00 2001 From: Tobias Netskar Date: Wed, 4 Feb 2026 08:47:32 +0100 Subject: [PATCH] feat: respect signing documents order from backend after 8.9.0 --- .../SigningDocumentListComponent.test.tsx | 1 + .../SigningDocumentListComponent.tsx | 4 +- src/layout/SigningDocumentList/api.test.ts | 88 +++++++++++++++++++ src/layout/SigningDocumentList/api.ts | 16 ++-- src/utils/versioning/versions.ts | 5 ++ 5 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 src/layout/SigningDocumentList/api.test.ts diff --git a/src/layout/SigningDocumentList/SigningDocumentListComponent.test.tsx b/src/layout/SigningDocumentList/SigningDocumentListComponent.test.tsx index 9fde8a0a34..34981277a3 100644 --- a/src/layout/SigningDocumentList/SigningDocumentListComponent.test.tsx +++ b/src/layout/SigningDocumentList/SigningDocumentListComponent.test.tsx @@ -49,6 +49,7 @@ jest.mock('src/features/language/Lang', () => ({ jest.mock('src/features/applicationMetadata/ApplicationMetadataProvider', () => ({ useApplicationMetadata: jest.fn(() => ({ dataTypes: [], + altinnNugetVersion: '8.9.0.225', })), })); diff --git a/src/layout/SigningDocumentList/SigningDocumentListComponent.tsx b/src/layout/SigningDocumentList/SigningDocumentListComponent.tsx index ec39915538..eef039c17d 100644 --- a/src/layout/SigningDocumentList/SigningDocumentListComponent.tsx +++ b/src/layout/SigningDocumentList/SigningDocumentListComponent.tsx @@ -6,6 +6,7 @@ import { DownloadIcon } from '@navikt/aksel-icons'; import { AppTable } from 'src/app-components/Table/Table'; import { Caption } from 'src/components/form/caption/Caption'; +import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import classes from 'src/layout/SigneeList/SigneeListComponent.module.css'; @@ -21,8 +22,9 @@ export function SigningDocumentListComponent({ }) { const { instanceOwnerPartyId, instanceGuid } = useParams(); const { langAsString } = useLanguage(); + const { altinnNugetVersion } = useApplicationMetadata(); - const { data, isLoading, error } = useDocumentList(instanceOwnerPartyId, instanceGuid); + const { data, isLoading, error } = useDocumentList(instanceOwnerPartyId, instanceGuid, altinnNugetVersion); if (error) { return ; diff --git a/src/layout/SigningDocumentList/api.test.ts b/src/layout/SigningDocumentList/api.test.ts new file mode 100644 index 0000000000..fd9934e590 --- /dev/null +++ b/src/layout/SigningDocumentList/api.test.ts @@ -0,0 +1,88 @@ +import { jest } from '@jest/globals'; +import { useQuery } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react'; + +import { type SigningDocument, useDocumentList } from 'src/layout/SigningDocumentList/api'; +import { httpGet } from 'src/utils/network/sharedNetworking'; + +jest.mock('@tanstack/react-query', () => ({ + useQuery: jest.fn(), +})); + +jest.mock('src/utils/network/sharedNetworking', () => ({ + httpGet: jest.fn(), +})); + +describe('useDocumentList', () => { + const mockedUseQuery = jest.mocked(useQuery); + const mockedHttpGet = jest.mocked(httpGet); + + const response = { + dataElements: [ + { + id: '1', + dataType: 'attachment', + contentType: 'application/pdf', + filename: 'zeta.pdf', + size: 100, + tags: ['tag-a'], + selfLinks: { + apps: 'https://storage.example.com/zeta.pdf', + }, + }, + { + id: '2', + dataType: 'attachment', + contentType: 'application/pdf', + filename: 'alpha.pdf', + size: 200, + tags: ['tag-b'], + selfLinks: { + apps: 'https://storage.example.com/alpha.pdf', + }, + }, + ], + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockedHttpGet.mockResolvedValue(response); + }); + + function hasQueryFn(value: unknown): value is { queryFn: () => Promise } { + if (typeof value !== 'object' || value === null) { + return false; + } + + return 'queryFn' in value && typeof value.queryFn === 'function'; + } + + const runQueryFn = async (altinnNugetVersion: string | undefined): Promise => { + mockedUseQuery.mockReturnValue({} as ReturnType); + + renderHook(() => useDocumentList('party-id', 'instance-guid', altinnNugetVersion)); + + const [queryOptions] = mockedUseQuery.mock.calls.at(-1) ?? []; + + if (!hasQueryFn(queryOptions)) { + throw new Error('Expected useQuery to receive a queryFn'); + } + + return queryOptions.queryFn(); + }; + + it('returns backend order when altinnNugetVersion is at least 8.9.0.225', async () => { + const documents = await runQueryFn('8.9.0.225'); + + expect(documents.map((doc) => doc.filename)).toEqual(['zeta.pdf', 'alpha.pdf']); + }); + + it.each<[string | undefined]>([['8.9.0.224'], [undefined]])( + 'sorts by filename when altinnNugetVersion is %s', + async (altinnNugetVersion) => { + const documents = await runQueryFn(altinnNugetVersion); + + expect(documents.map((doc) => doc.filename)).toEqual(['alpha.pdf', 'zeta.pdf']); + }, + ); +}); diff --git a/src/layout/SigningDocumentList/api.ts b/src/layout/SigningDocumentList/api.ts index 38ec7dcc09..fd52e400e7 100644 --- a/src/layout/SigningDocumentList/api.ts +++ b/src/layout/SigningDocumentList/api.ts @@ -6,6 +6,7 @@ import { DataTypeReference } from 'src/utils/attachmentsUtils'; import { httpGet } from 'src/utils/network/sharedNetworking'; import { appPath } from 'src/utils/urls/appUrlHelper'; import { makeUrlRelativeIfSameDomain } from 'src/utils/urls/urlHelper'; +import { backendSupportsSigningDocumentOrdering } from 'src/utils/versioning/versions'; const signingDocumentSchema = z .object({ @@ -33,18 +34,23 @@ export type SigningDocument = z.infer; export function useDocumentList( instanceOwnerPartyId: string | undefined, instanceGuid: string | undefined, + altinnNugetVersion: string | undefined, ): UseQueryResult { + const backendHandlesOrdering = backendSupportsSigningDocumentOrdering(altinnNugetVersion); + return useQuery({ - queryKey: ['signingDocumentList', instanceOwnerPartyId, instanceGuid], + queryKey: ['signingDocumentList', instanceOwnerPartyId, instanceGuid, backendHandlesOrdering], queryFn: async () => { const url = `${appPath}/instances/${instanceOwnerPartyId}/${instanceGuid}/signing/data-elements`; const response = await httpGet(url); - return z - .object({ dataElements: z.array(signingDocumentSchema) }) - .parse(response) - .dataElements.toSorted((a, b) => (a.filename ?? '').localeCompare(b.filename ?? '')); + const dataElements = z.object({ dataElements: z.array(signingDocumentSchema) }).parse(response).dataElements; + + if (backendHandlesOrdering) { + return dataElements; + } + return dataElements.toSorted((a, b) => (a.filename ?? '').localeCompare(b.filename ?? '')); }, staleTime: 1000 * 60 * 30, // 30 minutes refetchOnMount: 'always', diff --git a/src/utils/versioning/versions.ts b/src/utils/versioning/versions.ts index 097b7b891d..bc903faebd 100644 --- a/src/utils/versioning/versions.ts +++ b/src/utils/versioning/versions.ts @@ -9,6 +9,7 @@ export const FEATURE_VERSION_MAP = { PDF_PREVIEW_BUTTON: '8.5.0.157', APP_LANGUAGES_IN_ANONYMOUS: '8.5.6.180', SET_TAGS_ENDPOINT: '8.8.0.215', + BACKEND_ORDERED_SIGNING_DOCUMENTS: '8.9.0.225', } as const; type AppFeature = keyof typeof FEATURE_VERSION_MAP; @@ -67,3 +68,7 @@ export function appSupportsIncrementalValidationFeatures(currentNugetVersion: st export function appSupportsSetTagsEndpoint(currentNugetVersion: string | undefined) { return isFeatureSupported({ feature: 'SET_TAGS_ENDPOINT', currentNugetVersion }); } + +export function backendSupportsSigningDocumentOrdering(currentNugetVersion: string | undefined) { + return isFeatureSupported({ feature: 'BACKEND_ORDERED_SIGNING_DOCUMENTS', currentNugetVersion }); +}