Skip to content

Commit a6afbfc

Browse files
committed
feat: add error handling and search redirect for software not found
1 parent b82abdc commit a6afbfc

File tree

7 files changed

+142
-17
lines changed

7 files changed

+142
-17
lines changed

api/src/rpc/translations/en_default.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,13 @@
238238
"stop being referent": "Stop being referent",
239239
"become referent": "Become referent",
240240
"please provide a reason for unreferencing this software": "Please provide a reason why you think this software should not be in the $t(common.appAccronym) anymore",
241-
"unreference software": "Dereference the software"
241+
"unreference software": "Dereference the software",
242+
"error": {
243+
"title": "Software not found",
244+
"notFound": "The requested software could not be found in the catalog. It may have been removed or the name might be incorrect.",
245+
"generic": "An unexpected error occurred while loading the software details.",
246+
"searchInCatalog": "Look for it in the catalog"
247+
}
242248
},
243249
"headerDetailCard": {
244250
"authors": "Authors : ",

api/src/rpc/translations/fr_default.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,13 @@
241241
"stop being referent": "Ne plus être référent",
242242
"become referent": "Devenir référent",
243243
"please provide a reason for unreferencing this software": "Merci de préciser la raison pour laquelle vous estimez que ce logiciel ne devrait plus être référencé dans le $t(common.appAccronym)",
244-
"unreference software": "Dé-référencer le logiciel"
244+
"unreference software": "Dé-référencer le logiciel",
245+
"error": {
246+
"title": "Logiciel introuvable",
247+
"notFound": "Le logiciel demandé n'a pas pu être trouvé dans le catalogue. Il a peut-être été supprimé ou le nom pourrait être incorrect.",
248+
"generic": "Une erreur inattendue s'est produite lors du chargement des détails du logiciel.",
249+
"searchInCatalog": "Le chercher dans le catalogue"
250+
}
245251
},
246252
"headerDetailCard": {
247253
"authors": "Auteurs : ",

api/src/rpc/translations/schema.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,26 @@
972972
},
973973
"unreference software": {
974974
"type": "string"
975+
},
976+
"error": {
977+
"type": "object",
978+
"properties": {
979+
"title": {
980+
"type": "string",
981+
"description": "Title text"
982+
},
983+
"notFound": {
984+
"type": "string"
985+
},
986+
"generic": {
987+
"type": "string"
988+
},
989+
"searchInCatalog": {
990+
"type": "string"
991+
}
992+
},
993+
"additionalProperties": false,
994+
"required": ["title", "notFound", "generic", "searchInCatalog"]
975995
}
976996
},
977997
"additionalProperties": false,
@@ -1005,7 +1025,8 @@
10051025
"stop being referent",
10061026
"become referent",
10071027
"please provide a reason for unreferencing this software",
1008-
"unreference software"
1028+
"unreference software",
1029+
"error"
10091030
]
10101031
},
10111032
"headerDetailCard": {

web/src/core/usecases/softwareDetails/selectors.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,20 @@ const readyState = (rootState: RootState) => {
1717
return state;
1818
};
1919

20+
const errorState = (rootState: RootState) => {
21+
const state = rootState[name];
22+
23+
if (state.stateDescription !== "error") {
24+
return undefined;
25+
}
26+
27+
return state;
28+
};
29+
2030
const isReady = createSelector(readyState, state => state !== undefined);
2131

32+
const error = createSelector(errorState, state => state?.error);
33+
2234
const software = createSelector(readyState, readyState => readyState?.software);
2335

2436
const userDeclaration = createSelector(readyState, state => state?.userDeclaration);
@@ -33,7 +45,15 @@ const main = createSelector(
3345
software,
3446
userDeclaration,
3547
isUnreferencingOngoing,
36-
(isReady, software, userDeclaration, isUnreferencingOngoing) => {
48+
error,
49+
(isReady, software, userDeclaration, isUnreferencingOngoing, error) => {
50+
if (error) {
51+
return {
52+
isReady: false as const,
53+
error
54+
};
55+
}
56+
3757
if (!isReady) {
3858
return {
3959
isReady: false as const

web/src/core/usecases/softwareDetails/state.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { ApiTypes } from "api";
1010

1111
export const name = "softwareDetails";
1212

13-
export type State = State.NotReady | State.Ready;
13+
export type State = State.NotReady | State.Ready | State.Error;
1414

1515
export namespace State {
1616
export type SimilarSoftwareNotRegistered =
@@ -21,6 +21,11 @@ export namespace State {
2121
isInitializing: boolean;
2222
};
2323

24+
export type Error = {
25+
stateDescription: "error";
26+
error: globalThis.Error;
27+
};
28+
2429
export type Ready = {
2530
stateDescription: "ready";
2631
software: Software;
@@ -134,6 +139,13 @@ export const { reducer, actions } = createUsecaseActions({
134139
stateDescription: "not ready" as const,
135140
isInitializing: false
136141
}),
142+
initializationFailed: (
143+
_state,
144+
{ payload }: { payload: { error: globalThis.Error } }
145+
) => ({
146+
stateDescription: "error" as const,
147+
error: payload.error
148+
}),
137149
unreferencingStarted: state => {
138150
assert(state.stateDescription === "ready");
139151
state.isUnreferencingOngoing = true;

web/src/core/usecases/softwareDetails/thunks.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,19 @@ export const thunks = {
6464
sillApi.getInstances()
6565
]);
6666

67-
const software = apiSoftwareToSoftware({
68-
apiSoftwares,
69-
apiInstances,
70-
softwareName,
71-
mainSource
72-
});
67+
let software: State.Software;
68+
69+
try {
70+
software = apiSoftwareToSoftware({
71+
apiSoftwares,
72+
apiInstances,
73+
softwareName,
74+
mainSource
75+
});
76+
} catch (error) {
77+
dispatch(actions.initializationFailed({ error: error as Error }));
78+
return;
79+
}
7380

7481
const userDeclaration: { isReferent: boolean; isUser: boolean } | undefined =
7582
await (async () => {
@@ -169,7 +176,9 @@ function apiSoftwareToSoftware(params: {
169176
apiSoftware => apiSoftware.softwareName === softwareName
170177
);
171178

172-
assert(apiSoftware !== undefined);
179+
if (!apiSoftware) {
180+
throw new Error(`Software "${softwareName}" not found`);
181+
}
173182

174183
const {
175184
softwareId,

web/src/ui/pages/softwareDetails/SoftwareDetails.tsx

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SimilarSoftwareTab } from "ui/pages/softwareDetails/AlikeSoftwareTab";
1616
import { PublicationTab } from "./PublicationTab";
1717
import { ActionsFooter } from "ui/shared/ActionsFooter";
1818
import { DetailUsersAndReferents } from "ui/shared/DetailUsersAndReferents";
19+
import { Alert } from "@codegouvfr/react-dsfr/Alert";
1920
import { Button } from "@codegouvfr/react-dsfr/Button";
2021
import type { PageRoute } from "./route";
2122
import softwareLogoPlaceholder from "ui/assets/software_logo_placeholder.png";
@@ -44,10 +45,8 @@ export default function SoftwareDetails(props: Props) {
4445

4546
const { t } = useTranslation();
4647

47-
const { isReady, software, userDeclaration, isUnreferencingOngoing } = useCoreState(
48-
"softwareDetails",
49-
"main"
50-
);
48+
const { isReady, error, software, userDeclaration, isUnreferencingOngoing } =
49+
useCoreState("softwareDetails", "main");
5150

5251
useEffect(() => {
5352
softwareDetails.initialize({
@@ -57,12 +56,21 @@ export default function SoftwareDetails(props: Props) {
5756
return () => softwareDetails.clear();
5857
}, [route.params.name]);
5958

59+
if (error) {
60+
return (
61+
<SoftwareDetailsErrorFallback
62+
error={error}
63+
softwareName={route.params.name}
64+
/>
65+
);
66+
}
67+
6068
if (!isReady) {
6169
return <LoadingFallback />;
6270
}
6371

6472
const getLogoUrl = (): string | undefined => {
65-
if (software.logoUrl) return software.logoUrl;
73+
if (software?.logoUrl) return software.logoUrl;
6674
if (uiConfig?.softwareDetails.defaultLogo) return softwareLogoPlaceholder;
6775
};
6876

@@ -438,6 +446,49 @@ const ServiceProviderRow = ({
438446
</li>
439447
);
440448
};
449+
450+
function SoftwareDetailsErrorFallback({
451+
error,
452+
softwareName
453+
}: {
454+
error: Error;
455+
softwareName: string;
456+
}) {
457+
const { t } = useTranslation();
458+
459+
return (
460+
<div
461+
style={{
462+
padding: fr.spacing("4v"),
463+
display: "flex",
464+
flexDirection: "column",
465+
alignItems: "center",
466+
gap: fr.spacing("4v")
467+
}}
468+
>
469+
<Alert
470+
severity="error"
471+
title={t("softwareDetails.error.title")}
472+
description={
473+
error.message.includes("not found")
474+
? t("softwareDetails.error.notFound")
475+
: t("softwareDetails.error.generic")
476+
}
477+
/>
478+
<div style={{ display: "flex", gap: fr.spacing("2v") }}>
479+
<Button
480+
priority="primary"
481+
onClick={() => {
482+
routes.softwareCatalog({ search: softwareName }).push();
483+
}}
484+
>
485+
{t("softwareDetails.error.searchInCatalog")}
486+
</Button>
487+
</div>
488+
</div>
489+
);
490+
}
491+
441492
const useStyles = tss.withName({ SoftwareDetails }).create({
442493
breadcrumb: {
443494
marginBottom: fr.spacing("4v")

0 commit comments

Comments
 (0)