Skip to content

Fixing missing instantiation error(s) #3259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@ export const App = () => (
path='/instance-selection/*'
element={<InstanceSelectionWrapper />}
/>

<Route
path='/party-selection/*'
element={<PartySelection />}
/>

<Route
path='/instance/:instanceOwnerPartyId/:instanceGuid/*'
element={
Expand Down
38 changes: 25 additions & 13 deletions src/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet-async';
import { Flex } from 'src/app-components/Flex/Flex';
import classes from 'src/components/form/Form.module.css';
import { MessageBanner } from 'src/components/form/MessageBanner';
import { ErrorReport } from 'src/components/message/ErrorReport';
import { ErrorReport, ErrorReportList } from 'src/components/message/ErrorReport';
import { ReadyForPrint } from 'src/components/ReadyForPrint';
import { Loader } from 'src/core/loading/Loader';
import { useAppName, useAppOwner } from 'src/core/texts/appTexts';
Expand Down Expand Up @@ -114,18 +114,30 @@ export function FormPage({ currentPageId }: { currentPageId: string | undefined
id={id}
/>
))}
<Flex
item={true}
size={{ xs: 12 }}
aria-live='polite'
className={classes.errorReport}
>
<ErrorReport
renderIds={errorReportIds}
formErrors={formErrors}
taskErrors={taskErrors}
/>
</Flex>
{formErrors.length > 0 || taskErrors.length > 0 ? (
<Flex
item={true}
size={{ xs: 12 }}
aria-live='polite'
className={classes.errorReport}
>
<ErrorReport
errors={
<ErrorReportList
formErrors={formErrors}
taskErrors={taskErrors}
/>
}
>
{errorReportIds.map((id) => (
<GenericComponentById
key={id}
id={id}
/>
))}
</ErrorReport>
</Flex>
) : null}
</Flex>
<ReadyForPrint type='load' />
<HandleNavigationFocusComponent />
Expand Down
4 changes: 4 additions & 0 deletions src/components/message/ErrorReport.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.errorReport {
width: 100%;
}

.errorList {
list-style-position: outside;
margin: 0 0 0 24px;
Expand Down
169 changes: 114 additions & 55 deletions src/components/message/ErrorReport.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react';
import React, { createContext, useContext } from 'react';
import { Link } from 'react-router-dom';
import type { PropsWithChildren } from 'react';

import { Flex } from 'src/app-components/Flex/Flex';
import { PANEL_VARIANT } from 'src/app-components/Panel/constants';
Expand All @@ -7,81 +9,138 @@ import { FullWidthWrapper } from 'src/components/form/FullWidthWrapper';
import classes from 'src/components/message/ErrorReport.module.css';
import { useNavigateToNode } from 'src/features/form/layout/NavigateToNode';
import { Lang } from 'src/features/language/Lang';
import { GenericComponentById } from 'src/layout/GenericComponent';
import { useCurrentParty } from 'src/features/party/PartiesProvider';
import { isAxiosError } from 'src/utils/isAxiosError';
import { Hidden, useNode } from 'src/utils/layout/NodesContext';
import { HttpStatusCodes } from 'src/utils/network/networking';
import { useGetUniqueKeyFromObject } from 'src/utils/useGetKeyFromObject';
import type { AnyValidation, BaseValidation, NodeRefValidation } from 'src/features/validation';

export interface IErrorReportProps {
renderIds: string[];
formErrors: NodeRefValidation<AnyValidation<'error'>>[];
taskErrors: BaseValidation<'error'>[];
export interface IErrorReportProps extends PropsWithChildren {
errors: React.ReactNode | undefined;
}

const ArrowForwardSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" style="position: relative; top: 2px">
<path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"></path>
</svg>`;
const listStyleImg = `url("data:image/svg+xml,${encodeURIComponent(ArrowForwardSvg)}")`;

export const ErrorReport = ({ renderIds, formErrors, taskErrors }: IErrorReportProps) => {
const hasErrors = Boolean(formErrors.length) || Boolean(taskErrors.length);
const getUniqueKeyFromObject = useGetUniqueKeyFromObject();
// It is possible to render multiple error reports inside each other. If that happens, we should detect it and only
// render the outermost one. This may be a case in stateless apps, where you can have both validation errors and
// instantiation errors at the same time.
const ErrorReportContext = createContext(false);

if (!hasErrors) {
return null;
export const ErrorReport = ({ children, errors }: IErrorReportProps) => {
const hasErrorReport = useContext(ErrorReportContext);
if (errors === undefined || hasErrorReport) {
return children;
}

return (
<div data-testid='ErrorReport'>
<FullWidthWrapper isOnBottom={true}>
<Panel
title={<Lang id='form_filler.error_report_header' />}
variant={PANEL_VARIANT.Error}
>
<Flex
container
item
spacing={6}
alignItems='flex-start'
<ErrorReportContext.Provider value={true}>
<div
data-testid='ErrorReport'
className={classes.errorReport}
>
<FullWidthWrapper isOnBottom={true}>
<Panel
title={<Lang id='form_filler.error_report_header' />}
variant={PANEL_VARIANT.Error}
>
<Flex
container
item
size={{ xs: 12 }}
spacing={6}
alignItems='flex-start'
>
<ul className={classes.errorList}>
{taskErrors.map((error) => (
<li
key={getUniqueKeyFromObject(error)}
style={{ listStyleImage: listStyleImg }}
>
<Lang
id={error.message.key}
params={error.message.params}
/>
</li>
))}
{formErrors.map((error) => (
<Error
key={getUniqueKeyFromObject(error)}
error={error}
/>
))}
</ul>
<Flex
item
size={{ xs: 12 }}
>
<ul className={classes.errorList}>{errors}</ul>
</Flex>
{children}
</Flex>
{renderIds.map((id) => (
<GenericComponentById
key={id}
id={id}
/>
))}
</Flex>
</Panel>
</FullWidthWrapper>
</div>
</Panel>
</FullWidthWrapper>
</div>
</ErrorReportContext.Provider>
);
};

function Error({ error }: { error: NodeRefValidation }) {
function ErrorReportListItem({ children }: PropsWithChildren) {
return <li style={{ listStyleImage: listStyleImg }}>{children}</li>;
}

interface ErrorReportListProps {
formErrors: NodeRefValidation<AnyValidation<'error'>>[];
taskErrors: BaseValidation<'error'>[];
}

export function ErrorReportList({ formErrors, taskErrors }: ErrorReportListProps) {
const getUniqueKeyFromObject = useGetUniqueKeyFromObject();

return (
<>
{taskErrors.map((error) => (
<ErrorReportListItem key={getUniqueKeyFromObject(error)}>
<Lang
id={error.message.key}
params={error.message.params}
/>
</ErrorReportListItem>
))}
{formErrors.map((error) => (
<ErrorWithLink
key={getUniqueKeyFromObject(error)}
error={error}
/>
))}
</>
);
}

/**
* @see InstantiateContainer Contains somewhat similar logic, but for a full-screen error page.
*/
export function ErrorListFromInstantiation({ error }: { error: unknown }) {
const selectedParty = useCurrentParty();

if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Forbidden) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = (error.response?.data as any)?.message;
if (message) {
return (
<ErrorReportListItem>
<Lang id={message} />
</ErrorReportListItem>
);
}
return (
<ErrorReportListItem>
<span>
<Lang
id='instantiate.authorization_error_rights'
params={[selectedParty?.name]}
/>{' '}
(
<Link to='/party-selection/'>
<Lang id='party_selection.change_party' />
</Link>
).
</span>
</ErrorReportListItem>
);
}

return (
<ErrorReportListItem>
<Lang id='instantiate.unknown_error_text' />
</ErrorReportListItem>
);
}

function ErrorWithLink({ error }: { error: NodeRefValidation }) {
const node = useNode(error.nodeId);
const navigateTo = useNavigateToNode();
const isHidden = Hidden.useIsHidden(node);
Expand All @@ -100,7 +159,7 @@ function Error({ error }: { error: NodeRefValidation }) {
};

return (
<li style={{ listStyleImage: listStyleImg }}>
<ErrorReportListItem>
<button
className={classes.buttonAsInvisibleLink}
onClick={handleErrorClick}
Expand All @@ -112,6 +171,6 @@ function Error({ error }: { error: NodeRefValidation }) {
node={node}
/>
</button>
</li>
</ErrorReportListItem>
);
}
26 changes: 25 additions & 1 deletion src/features/instantiate/InstantiationContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import type { MutableRefObject } from 'react';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { QueryClient } from '@tanstack/react-query';
Expand Down Expand Up @@ -32,6 +33,8 @@ interface InstantiationContext {
error: AxiosError | undefined | null;
lastResult: IInstance | undefined;
clear: () => void;
clearTimeout: MutableRefObject<ReturnType<typeof setTimeout> | undefined>;
cancelClearTimeout: () => void;
}

const { Provider, useCtx } = createContext<InstantiationContext>({ name: 'InstantiationContext', required: true });
Expand Down Expand Up @@ -78,6 +81,7 @@ export function InstantiationProvider({ children }: React.PropsWithChildren) {
const queryClient = useQueryClient();
const instantiate = useInstantiateMutation();
const instantiateWithPrefill = useInstantiateWithPrefillMutation();
const clearRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

return (
<Provider
Expand All @@ -93,8 +97,21 @@ export function InstantiationProvider({ children }: React.PropsWithChildren) {
}
},
clear: () => {
removeMutations(queryClient);
if (clearRef.current) {
clearTimeout(clearRef.current);
}
clearRef.current = setTimeout(() => {
removeMutations(queryClient);
instantiate.reset();
instantiateWithPrefill.reset();
}, TIMEOUT);
},
cancelClearTimeout: () => {
if (clearRef.current) {
clearTimeout(clearRef.current);
}
},
clearTimeout: clearRef,

error: instantiate.error || instantiateWithPrefill.error,
lastResult: instantiate.data ?? instantiateWithPrefill.data,
Expand All @@ -116,3 +133,10 @@ function removeMutations(queryClient: QueryClient) {
const mutations = queryClient.getMutationCache().findAll({ mutationKey: ['instantiate'] });
mutations.forEach((mutation) => queryClient.getMutationCache().remove(mutation));
}

/* When this component is unmounted, we clear the instantiation to allow users to start a new instance later. This is
* needed for (for example) navigating back to party selection or instance selection, and then creating a new instance
* from there. However, React may decide to unmount this component and then mount it again quickly, so in those
* cases we want to avoid clearing the instantiation too soon (and cause a bug we had for a while where two instances
* would be created in quick succession). */
const TIMEOUT = 500;
25 changes: 25 additions & 0 deletions src/features/instantiate/InstantiationError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { useParams } from 'react-router-dom';

import { InstantiateValidationError } from 'src/features/instantiate/containers/InstantiateValidationError';
import { MissingRolesError } from 'src/features/instantiate/containers/MissingRolesError';
import { UnknownError } from 'src/features/instantiate/containers/UnknownError';
import { useInstantiation } from 'src/features/instantiate/InstantiationContext';
import { isAxiosError } from 'src/utils/isAxiosError';

export function InstantiationError() {
const error = useParams()?.error;
const exception = useInstantiation().error;

if (error === 'forbidden') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = isAxiosError(exception) ? (exception.response?.data as any)?.message : undefined;
if (message) {
return <InstantiateValidationError message={message} />;
}

return <MissingRolesError />;
}

return <UnknownError />;
}
Loading