From 971dbc9743c9a65b78baa29bd2ff3aacc77507ac Mon Sep 17 00:00:00 2001 From: ivarne-agent Date: Wed, 21 Oct 2026 10:26:40 +0200 Subject: [PATCH 1/6] fix: explain that the service cannot be started when instantiation returns 410 The app backend responds with 410 Gone when new instances cannot be created, either because the application's validTo has passed or because disallowUserInstantiation blocks end users. Show a dedicated error page and error report message for this instead of the generic unknown error. Refs Altinn/altinn-studio#21390 --- src/components/message/ErrorReport.tsx | 8 +++++++ .../containers/InstantiateContainer.test.tsx | 24 +++++++++++++++++++ .../containers/InstantiateContainer.tsx | 5 ++++ .../containers/InstantiationClosedError.tsx | 18 ++++++++++++++ .../selection/InstanceSelection.test.tsx | 19 +++++++++++++-- src/language/texts/en.ts | 3 +++ src/language/texts/nb.ts | 3 +++ src/language/texts/nn.ts | 3 +++ src/utils/network/networking.ts | 1 + 9 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/features/instantiate/containers/InstantiateContainer.test.tsx create mode 100644 src/features/instantiate/containers/InstantiationClosedError.tsx diff --git a/src/components/message/ErrorReport.tsx b/src/components/message/ErrorReport.tsx index 98c112cc6c..a1da6983ac 100644 --- a/src/components/message/ErrorReport.tsx +++ b/src/components/message/ErrorReport.tsx @@ -148,6 +148,14 @@ export function ErrorReportList({ formErrors, taskErrors }: ErrorReportListProps export function ErrorListFromInstantiation({ error }: { error: unknown }) { const selectedParty = useSelectedParty(); + if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Gone) { + return ( + + + + ); + } + if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Forbidden) { if (isInstantiationValidationResult(error.response.data)) { return ( diff --git a/src/features/instantiate/containers/InstantiateContainer.test.tsx b/src/features/instantiate/containers/InstantiateContainer.test.tsx new file mode 100644 index 0000000000..3c69c6ec0c --- /dev/null +++ b/src/features/instantiate/containers/InstantiateContainer.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { jest } from '@jest/globals'; +import { screen, waitFor } from '@testing-library/react'; + +import { InstantiateContainer } from 'src/features/instantiate/containers/InstantiateContainer'; +import { renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders'; + +describe('InstantiateContainer', () => { + it('should show that the service cannot be started when instantiation responds with 410', async () => { + jest.spyOn(window, 'logError').mockImplementation(() => {}); + const { mutations } = await renderWithoutInstanceAndLayout({ + renderer: () => , + waitUntilLoaded: false, + }); + + await waitFor(() => expect(mutations.doInstantiate.mock).toHaveBeenCalledTimes(1)); + mutations.doInstantiate.reject(Object.assign(new Error('Gone'), { isAxiosError: true, response: { status: 410 } })); + + expect(await screen.findByText('Du kan ikke starte denne tjenesten')).toBeInTheDocument(); + expect(screen.getByTestId('StatusCode')).toHaveTextContent('Feil 410'); + expect(screen.queryByText(/du mangler rettigheter/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/features/instantiate/containers/InstantiateContainer.tsx b/src/features/instantiate/containers/InstantiateContainer.tsx index 1ea2c0990e..656d3f90bd 100644 --- a/src/features/instantiate/containers/InstantiateContainer.tsx +++ b/src/features/instantiate/containers/InstantiateContainer.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { Loader } from 'src/core/loading/Loader'; import { InstantiateValidationError } from 'src/features/instantiate/containers/InstantiateValidationError'; +import { InstantiationClosedError } from 'src/features/instantiate/containers/InstantiationClosedError'; import { MissingRolesError } from 'src/features/instantiate/containers/MissingRolesError'; import { UnknownError } from 'src/features/instantiate/containers/UnknownError'; import { isInstantiationValidationResult } from 'src/features/instantiate/InstantiationValidation'; @@ -33,6 +34,10 @@ export const InstantiateContainer = () => { return ; } + if (isAxiosError(instantiation.error) && instantiation.error.response?.status === HttpStatusCodes.Gone) { + return ; + } + if (isAxiosError(instantiation.error) && instantiation.error.response?.status === HttpStatusCodes.Forbidden) { if (isInstantiationValidationResult(instantiation.error.response?.data)) { return ; diff --git a/src/features/instantiate/containers/InstantiationClosedError.tsx b/src/features/instantiate/containers/InstantiationClosedError.tsx new file mode 100644 index 0000000000..1e629d4b37 --- /dev/null +++ b/src/features/instantiate/containers/InstantiationClosedError.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { InstantiationErrorPage } from 'src/features/instantiate/containers/InstantiationErrorPage'; +import { Lang } from 'src/features/language/Lang'; +import { useLanguage } from 'src/features/language/useLanguage'; + +export function InstantiationClosedError() { + const { langAsString } = useLanguage(); + + return ( + } + content={} + showContactInfo={true} + statusCode={`${langAsString('party_selection.error_caption_prefix')} 410`} + /> + ); +} diff --git a/src/features/instantiate/selection/InstanceSelection.test.tsx b/src/features/instantiate/selection/InstanceSelection.test.tsx index e928585cbc..568c431afb 100644 --- a/src/features/instantiate/selection/InstanceSelection.test.tsx +++ b/src/features/instantiate/selection/InstanceSelection.test.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { screen, within } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import { InstanceSelectionWrapper } from 'src/features/instantiate/selection/InstanceSelection'; import { mockMediaQuery } from 'src/test/mockMediaQuery'; -import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders'; +import { renderWithInstanceAndLayout, renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders'; import type { ISimpleInstance } from 'src/types'; const mockActiveInstances: ISimpleInstance[] = [ @@ -75,6 +75,21 @@ describe('InstanceSelection', () => { expect(mutations.doInstantiate.mock).toHaveBeenCalledTimes(1); }); + it('should explain that new instances cannot be started when instantiation responds with 410', async () => { + jest.spyOn(window, 'logError').mockImplementation(() => {}); + const { mutations } = await renderWithoutInstanceAndLayout({ + renderer: () => , + queries: { fetchActiveInstances: () => Promise.resolve(mockActiveInstances) }, + }); + await userEvent.click(screen.getByText(/start på nytt/i)); + mutations.doInstantiate.reject(Object.assign(new Error('Gone'), { isAxiosError: true, response: { status: 410 } })); + + await waitFor(() => + expect(screen.getByText(/det er ikke mulig å starte nye skjema i denne tjenesten/i)).toBeInTheDocument(), + ); + expect(screen.queryByText(/ikke har rettigheter til å starte denne tjenesten/i)).not.toBeInTheDocument(); + }); + it('should trigger openInstance on editButton click', async () => { const { mutations, routerRef } = await render(); const row = screen.getByRole('row', { diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index 4f5863f306..f86fd36ea2 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -243,6 +243,9 @@ export function en() { 'instantiate.unknown_error_status': 'Unknown error', 'instantiate.unknown_error_customer_support': 'If the problem persists, contact us at customer service at {0}.', 'instantiate.unknown_error_show_details': 'Show error details', + 'instantiate.closed_error_title': 'You cannot start this service', + 'instantiate.closed_error_text': + 'It is not possible to start new forms in this service. You can still complete forms you have already started from your inbox.', 'instantiate.forbidden_action_error_title': 'You do not have permission to perform this action.', 'instantiate.forbidden_action_error_text': 'It looks like you do not have permission to perform this action.', 'instantiate.forbidden_action_error_status': '403 - Forbidden', diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index 077184fe62..ee5d21167c 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -244,6 +244,9 @@ export function nb() { 'instantiate.unknown_error_status': 'Ukjent feil', 'instantiate.unknown_error_customer_support': 'Om problemet vedvarer, ta kontakt med oss på brukerservice {0}.', 'instantiate.unknown_error_show_details': 'Vis detaljer om feilen', + 'instantiate.closed_error_title': 'Du kan ikke starte denne tjenesten', + 'instantiate.closed_error_text': + 'Det er ikke mulig å starte nye skjema i denne tjenesten. Skjema du allerede har startet, kan du fortsatt fullføre fra innboksen.', 'instantiate.forbidden_action_error_title': 'Du mangler rettigheter til å utføre denne handlingen', 'instantiate.forbidden_action_error_text': 'Det ser ut til at du mangler rettigheter til å utføre denne handlingen.', diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index 1035e2e2b4..ed2c77d0bb 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -244,6 +244,9 @@ export function nn() { 'instantiate.unknown_error_status': 'Ukjent feil', 'instantiate.unknown_error_customer_support': 'Om problemet hald fram, ta kontakt med oss på brukarservice {0}.', 'instantiate.unknown_error_show_details': 'Vis detaljar om feilen', + 'instantiate.closed_error_title': 'Du kan ikkje starte denne tenesta', + 'instantiate.closed_error_text': + 'Det er ikkje mogleg å starte nye skjema i denne tenesta. Skjema du allereie har starta, kan du framleis fullføre frå innboksen.', 'instantiate.forbidden_action_error_title': 'Du manglar rett til å utføre denne handlinga', 'instantiate.forbidden_action_error_text': 'Det ser ut til at du ikkje har rett til å utføre denne handlinga.', 'instantiate.forbidden_action_error_status': '403 - Forbidden', diff --git a/src/utils/network/networking.ts b/src/utils/network/networking.ts index 3aa0a9fd06..eae2e9c411 100644 --- a/src/utils/network/networking.ts +++ b/src/utils/network/networking.ts @@ -5,6 +5,7 @@ export enum HttpStatusCodes { Ok = 200, BadRequest = 400, Forbidden = 403, + Gone = 410, } export async function httpGet(url: string, options?: AxiosRequestConfig) { From 3e3520d55a7e3cca0c595bc07a07710f9975c5fd Mon Sep 17 00:00:00 2001 From: ivarne-agent Date: Wed, 21 Oct 2026 11:28:36 +0200 Subject: [PATCH 2/6] fix: check validTo and disallowUserInstantiation before instantiating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read validTo and disallowUserInstantiation from the application metadata and do not attempt to instantiate when the backend would reject it: - the instantiation page shows the closed page directly, - instance selection still lists active instances, but replaces the 'Start på nytt' button with an explanation, - InstantiationButton shows the explanation instead of sending a request. disallowUserInstantiation only applies on the production host, matching the app backend. The 410 handling stays as a fallback. --- src/components/message/ErrorReport.tsx | 14 +++-- src/features/applicationMetadata/types.ts | 2 + .../containers/InstantiateContainer.test.tsx | 15 +++++ .../containers/InstantiateContainer.tsx | 10 ++- .../instantiate/instantiationClosed.test.ts | 32 ++++++++++ .../instantiate/instantiationClosed.ts | 35 +++++++++++ .../selection/InstanceSelection.test.tsx | 14 +++++ .../selection/InstanceSelection.tsx | 62 +++++++++++-------- .../InstantiationButton.test.tsx | 12 +++- .../InstantiationButton.tsx | 31 +++++++--- src/utils/urls/urlHelper.ts | 5 ++ 11 files changed, 190 insertions(+), 42 deletions(-) create mode 100644 src/features/instantiate/instantiationClosed.test.ts create mode 100644 src/features/instantiate/instantiationClosed.ts diff --git a/src/components/message/ErrorReport.tsx b/src/components/message/ErrorReport.tsx index a1da6983ac..d73b115489 100644 --- a/src/components/message/ErrorReport.tsx +++ b/src/components/message/ErrorReport.tsx @@ -145,15 +145,19 @@ export function ErrorReportList({ formErrors, taskErrors }: ErrorReportListProps /** * @see InstantiateContainer Contains somewhat similar logic, but for a full-screen error page. */ +export function ErrorListForClosedInstantiation() { + return ( + + + + ); +} + export function ErrorListFromInstantiation({ error }: { error: unknown }) { const selectedParty = useSelectedParty(); if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Gone) { - return ( - - - - ); + return ; } if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Forbidden) { diff --git a/src/features/applicationMetadata/types.ts b/src/features/applicationMetadata/types.ts index 61ffb5143b..47fa42673f 100644 --- a/src/features/applicationMetadata/types.ts +++ b/src/features/applicationMetadata/types.ts @@ -22,6 +22,8 @@ export interface IncomingApplicationMetadata { altinnNugetVersion?: string; logo?: ILogoOptions; messageBoxConfig?: MessageBoxConfig; + validTo?: string | null; + disallowUserInstantiation?: boolean; } type HideAlwaysSettings = { diff --git a/src/features/instantiate/containers/InstantiateContainer.test.tsx b/src/features/instantiate/containers/InstantiateContainer.test.tsx index 3c69c6ec0c..fe6d782867 100644 --- a/src/features/instantiate/containers/InstantiateContainer.test.tsx +++ b/src/features/instantiate/containers/InstantiateContainer.test.tsx @@ -3,7 +3,9 @@ import React from 'react'; import { jest } from '@jest/globals'; import { screen, waitFor } from '@testing-library/react'; +import { getIncomingApplicationMetadataMock } from 'src/__mocks__/getApplicationMetadataMock'; import { InstantiateContainer } from 'src/features/instantiate/containers/InstantiateContainer'; +import { fetchApplicationMetadata } from 'src/queries/queries'; import { renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders'; describe('InstantiateContainer', () => { @@ -21,4 +23,17 @@ describe('InstantiateContainer', () => { expect(screen.getByTestId('StatusCode')).toHaveTextContent('Feil 410'); expect(screen.queryByText(/du mangler rettigheter/i)).not.toBeInTheDocument(); }); + + it('should not try to instantiate when validTo has passed', async () => { + jest + .mocked(fetchApplicationMetadata) + .mockImplementationOnce(async () => getIncomingApplicationMetadataMock({ validTo: '2020-01-01T00:00:00Z' })); + const { mutations } = await renderWithoutInstanceAndLayout({ + renderer: () => , + waitUntilLoaded: false, + }); + + expect(await screen.findByText('Du kan ikke starte denne tjenesten')).toBeInTheDocument(); + expect(mutations.doInstantiate.mock).not.toHaveBeenCalled(); + }); }); diff --git a/src/features/instantiate/containers/InstantiateContainer.tsx b/src/features/instantiate/containers/InstantiateContainer.tsx index 656d3f90bd..b4612c1004 100644 --- a/src/features/instantiate/containers/InstantiateContainer.tsx +++ b/src/features/instantiate/containers/InstantiateContainer.tsx @@ -5,6 +5,7 @@ import { InstantiateValidationError } from 'src/features/instantiate/containers/ import { InstantiationClosedError } from 'src/features/instantiate/containers/InstantiationClosedError'; import { MissingRolesError } from 'src/features/instantiate/containers/MissingRolesError'; import { UnknownError } from 'src/features/instantiate/containers/UnknownError'; +import { useIsInstantiationClosed } from 'src/features/instantiate/instantiationClosed'; import { isInstantiationValidationResult } from 'src/features/instantiate/InstantiationValidation'; import { useInstantiation } from 'src/features/instantiate/useInstantiation'; import { useSelectedParty } from 'src/features/party/PartiesProvider'; @@ -18,13 +19,18 @@ export const InstantiateContainer = () => { changeBodyBackground(AltinnPalette.greyLight); const party = useSelectedParty(); const instantiation = useInstantiation(); + const isClosed = useIsInstantiationClosed(); useEffect(() => { - const shouldCreateInstance = !!party; + const shouldCreateInstance = !!party && !isClosed; if (shouldCreateInstance) { instantiation.instantiate(party.partyId); } - }, [instantiation, party]); + }, [instantiation, party, isClosed]); + + if (isClosed) { + return ; + } // A 403 with RequiredAuthenticationLevel triggers a step-up redirect (see useInstantiation.onError). Block further // rendering with a loader until the browser navigates away, so we never flash the "missing roles" error page. After a diff --git a/src/features/instantiate/instantiationClosed.test.ts b/src/features/instantiate/instantiationClosed.test.ts new file mode 100644 index 0000000000..a7159a8938 --- /dev/null +++ b/src/features/instantiate/instantiationClosed.test.ts @@ -0,0 +1,32 @@ +import { isInstantiationClosed } from 'src/features/instantiate/instantiationClosed'; + +describe('isInstantiationClosed', () => { + const now = new Date('2026-10-21T12:00:00Z'); + const tt02Host = 'ttd.apps.tt02.altinn.no'; + const prodHost = 'ttd.apps.altinn.no'; + + it.each([ + [undefined, false], + [null, false], + ['2026-10-21T11:59:59Z', true], + ['2026-10-21T12:00:01Z', false], + ['2026-10-21T11:59:59.1504337Z', true], + ['2026-10-21T13:59:59+02:00', true], + ['2026-10-21T14:00:01+02:00', false], + ['2026-10-21T11:59:59', true], + ['2026-10-21T12:00:01', false], + ['2026-10-21', true], + ['2026-10-22', false], + ])('validTo %s gives %s', (validTo, expected) => { + expect(isInstantiationClosed({ validTo }, now, tt02Host)).toBe(expected); + }); + + it.each([ + [prodHost, true, true], + [prodHost, false, false], + [tt02Host, true, false], + ['local.altinn.cloud:8000', true, false], + ])('on %s with disallowUserInstantiation %s gives %s', (host, disallowUserInstantiation, expected) => { + expect(isInstantiationClosed({ disallowUserInstantiation }, now, host)).toBe(expected); + }); +}); diff --git a/src/features/instantiate/instantiationClosed.ts b/src/features/instantiate/instantiationClosed.ts new file mode 100644 index 0000000000..acca8aa351 --- /dev/null +++ b/src/features/instantiate/instantiationClosed.ts @@ -0,0 +1,35 @@ +import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; +import { isProductionHost } from 'src/utils/urls/urlHelper'; +import type { IncomingApplicationMetadata } from 'src/features/applicationMetadata/types'; + +type InstantiationPreconditions = Pick; + +// The app backend reads a validTo without an offset as UTC, while JavaScript reads a date-time without an offset as +// local time. +const hasTimeWithoutOffset = /T[^Z+-]*$/i; + +function parseValidTo(validTo: string): Date { + return new Date(hasTimeWithoutOffset.test(validTo) ? `${validTo}Z` : validTo); +} + +/** + * Mirrors the checks the app backend does before creating an instance, so that we can tell the user that the service + * cannot be started instead of sending a request we know will be rejected. The backend responds with 410 Gone in + * both cases. Service owners are exempt from disallowUserInstantiation, but they do not use the app frontend. + */ +export function isInstantiationClosed( + { validTo, disallowUserInstantiation }: InstantiationPreconditions, + now: Date, + host: string, +): boolean { + if (validTo && parseValidTo(validTo) < now) { + return true; + } + + return !!disallowUserInstantiation && isProductionHost(host); +} + +export function useIsInstantiationClosed(): boolean { + const applicationMetadata = useApplicationMetadata(); + return isInstantiationClosed(applicationMetadata, new Date(), window.location.host); +} diff --git a/src/features/instantiate/selection/InstanceSelection.test.tsx b/src/features/instantiate/selection/InstanceSelection.test.tsx index 568c431afb..2dc67f51e5 100644 --- a/src/features/instantiate/selection/InstanceSelection.test.tsx +++ b/src/features/instantiate/selection/InstanceSelection.test.tsx @@ -3,7 +3,9 @@ import React from 'react'; import { screen, waitFor, within } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; +import { getIncomingApplicationMetadataMock } from 'src/__mocks__/getApplicationMetadataMock'; import { InstanceSelectionWrapper } from 'src/features/instantiate/selection/InstanceSelection'; +import { fetchApplicationMetadata } from 'src/queries/queries'; import { mockMediaQuery } from 'src/test/mockMediaQuery'; import { renderWithInstanceAndLayout, renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders'; import type { ISimpleInstance } from 'src/types'; @@ -90,6 +92,18 @@ describe('InstanceSelection', () => { expect(screen.queryByText(/ikke har rettigheter til å starte denne tjenesten/i)).not.toBeInTheDocument(); }); + it('should list active instances but not offer to start a new one when validTo has passed', async () => { + jest + .mocked(fetchApplicationMetadata) + .mockImplementationOnce(async () => getIncomingApplicationMetadataMock({ validTo: '2020-01-01T00:00:00Z' })); + const { mutations } = await render(); + + expect(await screen.findByText(mockActiveInstances[0].lastChangedBy)).toBeInTheDocument(); + expect(screen.getByText(/det er ikke mulig å starte nye skjema i denne tjenesten/i)).toBeInTheDocument(); + expect(screen.queryByText(/start på nytt/i)).not.toBeInTheDocument(); + expect(mutations.doInstantiate.mock).not.toHaveBeenCalled(); + }); + it('should trigger openInstance on editButton click', async () => { const { mutations, routerRef } = await render(); const row = screen.getByRole('row', { diff --git a/src/features/instantiate/selection/InstanceSelection.tsx b/src/features/instantiate/selection/InstanceSelection.tsx index 90770ba8b9..d253bf81d2 100644 --- a/src/features/instantiate/selection/InstanceSelection.tsx +++ b/src/features/instantiate/selection/InstanceSelection.tsx @@ -12,6 +12,7 @@ import { ReadyForPrint } from 'src/components/ReadyForPrint'; import { useIsProcessing } from 'src/core/contexts/processingContext'; import { useAppName, useAppOwner } from 'src/core/texts/appTexts'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; +import { useIsInstantiationClosed } from 'src/features/instantiate/instantiationClosed'; import { ActiveInstancesProvider, useActiveInstances, @@ -58,6 +59,7 @@ function InstanceSelection() { const mobileView = useIsMobileOrTablet(); const rowsPerPageOptions = instanceSelectionOptions?.rowsPerPageOptions ?? [10, 25, 50]; const instantiation = useInstantiation(); + const isInstantiationClosed = useIsInstantiationClosed(); const selectedParty = useSelectedParty(); const setNavigationEffect = useSetNavigationEffect(); const { performProcess, isAnyProcessing, isThisProcessing: isLoading } = useIsProcessing(); @@ -251,34 +253,40 @@ function InstanceSelection() { {mobileView && renderMobileTable()} {!mobileView && renderTable()}
- : undefined} - > - - + + + )}
diff --git a/src/layout/InstantiationButton/InstantiationButton.test.tsx b/src/layout/InstantiationButton/InstantiationButton.test.tsx index d30fa21dbc..c61c9bb30d 100644 --- a/src/layout/InstantiationButton/InstantiationButton.test.tsx +++ b/src/layout/InstantiationButton/InstantiationButton.test.tsx @@ -12,12 +12,13 @@ import { InstantiationButtonComponent } from 'src/layout/InstantiationButton/Ins import { fetchApplicationMetadata } from 'src/queries/queries'; import { renderGenericComponentTest } from 'src/test/renderWithProviders'; -const render = async () => { +const render = async (validTo?: string) => { jest.mocked(fetchApplicationMetadata).mockImplementationOnce(async () => getIncomingApplicationMetadataMock({ onEntry: { show: 'stateless', }, + validTo, }), ); return await renderGenericComponentTest({ @@ -80,4 +81,13 @@ describe('InstantiationButton', () => { expect(screen.getByText('You are now looking at the instance')).toBeInTheDocument(); }); }); + + it('should explain that the service cannot be started, without instantiating, when validTo has passed', async () => { + const { mutations } = await render('2020-01-01T00:00:00Z'); + + await userEvent.click(screen.getByRole('button')); + + expect(await screen.findByText(/det er ikke mulig å starte nye skjema i denne tjenesten/i)).toBeInTheDocument(); + expect(mutations.doInstantiateWithPrefill.mock).not.toHaveBeenCalled(); + }); }); diff --git a/src/layout/InstantiationButton/InstantiationButton.tsx b/src/layout/InstantiationButton/InstantiationButton.tsx index 7d9397c865..fef48d518e 100644 --- a/src/layout/InstantiationButton/InstantiationButton.tsx +++ b/src/layout/InstantiationButton/InstantiationButton.tsx @@ -1,10 +1,15 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Button } from 'src/app-components/Button/Button'; -import { ErrorListFromInstantiation, ErrorReport } from 'src/components/message/ErrorReport'; +import { + ErrorListForClosedInstantiation, + ErrorListFromInstantiation, + ErrorReport, +} from 'src/components/message/ErrorReport'; import { useIsProcessing } from 'src/core/contexts/processingContext'; import { DataModels } from 'src/features/datamodel/DataModelsProvider'; import { FD } from 'src/features/formData/FormDataWrite'; +import { useIsInstantiationClosed } from 'src/features/instantiate/instantiationClosed'; import { useInstantiation } from 'src/features/instantiate/useInstantiation'; import { useSetNavigationEffect } from 'src/features/navigation/NavigationEffectContext'; import { useSelectedParty } from 'src/features/party/PartiesProvider'; @@ -21,15 +26,27 @@ export const InstantiationButton = ({ children, ...props }: Props) => { const prefill = FD.useMapping(props.mapping, DataModels.useDefaultDataType()); const party = useSelectedParty(); const setNavigationEffect = useSetNavigationEffect(); + const isInstantiationClosed = useIsInstantiationClosed(); + const [triedWhileClosed, setTriedWhileClosed] = useState(false); return ( : undefined} + show={triedWhileClosed || instantiation.error !== undefined} + errors={ + triedWhileClosed ? ( + + ) : instantiation.error ? ( + + ) : undefined + } >