Skip to content

Commit d06c6df

Browse files
authored
fix: accept both UUID an short UUID in routes params (#777)
* refactor: rename UUID and SUUID wrappers functions * fix: support both UUID and shortUUID formats in URL parameter * feat: change list ID in link for consistency * refactor: add parseIdParamSafe utility function Use it in cases and lists pages
1 parent b4b47fc commit d06c6df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+308
-250
lines changed

packages/app-builder/src/components/Cases/CaseDecisions.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { type ScenarioIterationRule } from '@app-builder/models/scenario-iterati
1212
import { ReviewDecisionModal } from '@app-builder/routes/ressources+/cases+/review-decision';
1313
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
1414
import { getRoute } from '@app-builder/utils/routes';
15-
import { fromUUID } from '@app-builder/utils/short-uuid';
15+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
1616
import * as Ariakit from '@ariakit/react';
1717
import { Await, Link } from '@remix-run/react';
1818
import * as React from 'react';
@@ -116,7 +116,7 @@ export function CaseDecisions({
116116
<Tooltip.Default content={row.scenario.name}>
117117
<Link
118118
to={getRoute('/scenarios/:scenarioId', {
119-
scenarioId: fromUUID(row.scenario.id),
119+
scenarioId: fromUUIDtoSUUID(row.scenario.id),
120120
})}
121121
className="hover:text-purple-60 focus:text-purple-60 text-purple-65 relative line-clamp-2 font-semibold hover:underline focus:underline"
122122
>
@@ -243,7 +243,7 @@ function DecisionActions({
243243
render={
244244
<Link
245245
to={getRoute('/decisions/:decisionId', {
246-
decisionId: fromUUID(decision.id),
246+
decisionId: fromUUIDtoSUUID(decision.id),
247247
})}
248248
/>
249249
}
@@ -357,8 +357,8 @@ function DecisionDetail({
357357
<SanctionState sanctionCheck={sanctionCheck} />
358358
<Link
359359
to={getRoute('/cases/:caseId/sanctions/:decisionId', {
360-
caseId: fromUUID(caseId),
361-
decisionId: fromUUID(decision.id),
360+
caseId: fromUUIDtoSUUID(caseId),
361+
decisionId: fromUUIDtoSUUID(decision.id),
362362
})}
363363
>
364364
<Button>

packages/app-builder/src/components/Cases/CaseHistory/CaseEvents.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useOrganizationUsers } from '@app-builder/services/organization/organiz
1616
import { getFullName } from '@app-builder/services/user';
1717
import { formatDateRelative, formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
1818
import { getRoute } from '@app-builder/utils/routes';
19-
import { fromUUID } from '@app-builder/utils/short-uuid';
19+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
2020
import * as Ariakit from '@ariakit/react';
2121
import { Link } from '@remix-run/react';
2222
import { cx } from 'class-variance-authority';
@@ -340,7 +340,7 @@ function DecisionReviewedEventDetail({ event }: { event: DecisionReviewedEvent }
340340
<Link
341341
className="text-s hover:text-purple-60 focus:text-purple-60 text-purple-65 relative font-normal hover:underline focus:underline"
342342
to={getRoute('/decisions/:decisionId', {
343-
decisionId: fromUUID(event.decisionId),
343+
decisionId: fromUUIDtoSUUID(event.decisionId),
344344
})}
345345
>
346346
{t('cases:case_detail.history.event_detail.rule_snooze_created.decision_detail')}
@@ -458,7 +458,7 @@ function RuleSnoozeDetail({ ruleSnoozeId }: { ruleSnoozeId: string }) {
458458
<Link
459459
className="hover:text-purple-60 focus:text-purple-60 text-purple-65 relative font-normal hover:underline focus:underline"
460460
to={getRoute('/decisions/:decisionId', {
461-
decisionId: fromUUID(data.ruleSnoozeDetail.createdFromDecisionId),
461+
decisionId: fromUUIDtoSUUID(data.ruleSnoozeDetail.createdFromDecisionId),
462462
})}
463463
>
464464
{t('cases:case_detail.history.event_detail.rule_snooze_created.decision_detail')}
@@ -477,9 +477,11 @@ function RuleSnoozeDetail({ ruleSnoozeId }: { ruleSnoozeId: string }) {
477477
<Link
478478
className="hover:text-purple-60 focus:text-purple-60 text-purple-65 relative font-normal hover:underline focus:underline"
479479
to={getRoute('/scenarios/:scenarioId/i/:iterationId/rules/:ruleId', {
480-
scenarioId: fromUUID(data.ruleSnoozeDetail.createdFromRule.scenarioId),
481-
iterationId: fromUUID(data.ruleSnoozeDetail.createdFromRule.scenarioIterationId),
482-
ruleId: fromUUID(data.ruleSnoozeDetail.createdFromRule.ruleId),
480+
scenarioId: fromUUIDtoSUUID(data.ruleSnoozeDetail.createdFromRule.scenarioId),
481+
iterationId: fromUUIDtoSUUID(
482+
data.ruleSnoozeDetail.createdFromRule.scenarioIterationId,
483+
),
484+
ruleId: fromUUIDtoSUUID(data.ruleSnoozeDetail.createdFromRule.ruleId),
483485
})}
484486
>
485487
{data.ruleSnoozeDetail.createdFromRule.ruleName ??

packages/app-builder/src/components/Cases/CasesList.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type Case } from '@app-builder/models/cases';
22
import { useOrganizationTags } from '@app-builder/services/organization/organization-tags';
33
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
44
import { getRoute } from '@app-builder/utils/routes';
5-
import { fromUUID } from '@app-builder/utils/short-uuid';
5+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
66
import { Link } from '@remix-run/react';
77
import { createColumnHelper, getCoreRowModel, type SortingState } from '@tanstack/react-table';
88
import clsx from 'clsx';
@@ -56,7 +56,7 @@ export function CasesList({
5656
<Tooltip.Default content={caseName}>
5757
<Link
5858
className="text-purple-65 text-s line-clamp-2 w-fit font-normal underline"
59-
to={getRoute('/cases/:caseId', { caseId: fromUUID(row.id) })}
59+
to={getRoute('/cases/:caseId', { caseId: fromUUIDtoSUUID(row.id) })}
6060
>
6161
{caseName}
6262
</Link>
@@ -116,7 +116,7 @@ export function CasesList({
116116
state: { sorting },
117117
manualSorting: true,
118118
onSortingChange: setSorting,
119-
rowLink: ({ id }) => <Link to={getRoute('/cases/:caseId', { caseId: fromUUID(id) })} />,
119+
rowLink: ({ id }) => <Link to={getRoute('/cases/:caseId', { caseId: fromUUIDtoSUUID(id) })} />,
120120
});
121121

122122
return (

packages/app-builder/src/components/Decisions/DecisionDetail.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CaseStatus, decisionsI18n } from '@app-builder/components';
22
import { type DecisionDetail } from '@app-builder/models/decision';
33
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
44
import { getRoute } from '@app-builder/utils/routes';
5-
import { fromUUID } from '@app-builder/utils/short-uuid';
5+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
66
import { Link } from '@remix-run/react';
77
import { useTranslation } from 'react-i18next';
88
import { Collapsible } from 'ui-design-system';
@@ -24,7 +24,7 @@ export function DecisionDetail({ decision }: { decision: DecisionDetail }) {
2424
<DetailLabel>{t('decisions:scenario.name')}</DetailLabel>
2525
<Link
2626
to={getRoute('/scenarios/:scenarioId', {
27-
scenarioId: fromUUID(scenario.id),
27+
scenarioId: fromUUIDtoSUUID(scenario.id),
2828
})}
2929
className="hover:text-purple-60 focus:text-purple-60 text-purple-65 font-semibold hover:underline focus:underline"
3030
>
@@ -34,8 +34,8 @@ export function DecisionDetail({ decision }: { decision: DecisionDetail }) {
3434
<DetailLabel>{t('decisions:scenario.version')}</DetailLabel>
3535
<Link
3636
to={getRoute('/scenarios/:scenarioId/i/:iterationId', {
37-
scenarioId: fromUUID(scenario.id),
38-
iterationId: fromUUID(scenario.scenarioIterationId),
37+
scenarioId: fromUUIDtoSUUID(scenario.id),
38+
iterationId: fromUUIDtoSUUID(scenario.scenarioIterationId),
3939
})}
4040
className="hover:text-purple-60 focus:text-purple-60 text-purple-65 font-semibold hover:underline focus:underline"
4141
>
@@ -51,7 +51,7 @@ export function DecisionDetail({ decision }: { decision: DecisionDetail }) {
5151
<CaseStatus size="small" type="first-letter" status={caseDetail.status} />
5252
<Link
5353
to={getRoute('/cases/:caseId', {
54-
caseId: fromUUID(caseDetail.id),
54+
caseId: fromUUIDtoSUUID(caseDetail.id),
5555
})}
5656
className="hover:text-purple-60 focus:text-purple-60 text-purple-65 font-semibold hover:underline focus:underline"
5757
>

packages/app-builder/src/components/Decisions/DecisionsList.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type ReviewStatus } from '@app-builder/models/decision';
44
import { type Outcome } from '@app-builder/models/outcome';
55
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
66
import { getRoute } from '@app-builder/utils/routes';
7-
import { fromUUID } from '@app-builder/utils/short-uuid';
7+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
88
import { Link } from '@remix-run/react';
99
import { createColumnHelper, getCoreRowModel } from '@tanstack/react-table';
1010
import clsx from 'clsx';
@@ -248,7 +248,7 @@ export function DecisionsList({
248248
rowLink: (decision) => (
249249
<Link
250250
to={getRoute('/decisions/:decisionId', {
251-
decisionId: fromUUID(decision.id),
251+
decisionId: fromUUIDtoSUUID(decision.id),
252252
})}
253253
/>
254254
),

packages/app-builder/src/components/Scenario/TestRun/TestRunSelector.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type ScenarioIterationWithType } from '@app-builder/models/scenario-ite
33
import { type TestRun } from '@app-builder/models/testrun';
44
import { useCurrentScenario } from '@app-builder/routes/_builder+/scenarios+/$scenarioId+/_layout';
55
import { getRoute } from '@app-builder/utils/routes';
6-
import { fromUUID } from '@app-builder/utils/short-uuid';
6+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
77
import { Link } from '@remix-run/react';
88
import clsx from 'clsx';
99
import { Avatar } from 'ui-design-system';
@@ -31,8 +31,8 @@ export const TestRunSelector = ({
3131
return (
3232
<Link
3333
to={getRoute('/scenarios/:scenarioId/test-run/:testRunId', {
34-
scenarioId: fromUUID(currentScenario.id),
35-
testRunId: fromUUID(id),
34+
scenarioId: fromUUIDtoSUUID(currentScenario.id),
35+
testRunId: fromUUIDtoSUUID(id),
3636
})}
3737
className={clsx(
3838
'grid cursor-pointer grid-cols-[30%_30%_8%_auto] items-center rounded-lg border py-4 transition-colors',

packages/app-builder/src/components/Transfers/TransfersList.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Transfer } from '@app-builder/models/transfer';
22
import { formatCurrency, useFormatLanguage } from '@app-builder/utils/format';
33
import { getRoute } from '@app-builder/utils/routes';
4-
import { fromUUID } from '@app-builder/utils/short-uuid';
4+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
55
import { Link } from '@remix-run/react';
66
import { createColumnHelper, getCoreRowModel } from '@tanstack/react-table';
77
import clsx from 'clsx';
@@ -59,7 +59,7 @@ export function TransfersList({ className, transfers }: TransfersListProps) {
5959
rowLink: (transfer) => (
6060
<Link
6161
to={getRoute('/transfercheck/transfers/:transferId', {
62-
transferId: fromUUID(transfer.id),
62+
transferId: fromUUIDtoSUUID(transfer.id),
6363
})}
6464
/>
6565
),

packages/app-builder/src/queries/builder-options.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type BuilderOptionsResource } from '@app-builder/routes/ressources+/scenarios+/$scenarioId+/builder-options';
22
import { getRoute } from '@app-builder/utils/routes';
3-
import { fromUUID } from '@app-builder/utils/short-uuid';
3+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
44
import { useQuery } from '@tanstack/react-query';
55

66
const endpoint = (scenarioId: string) =>
@@ -13,7 +13,7 @@ type UseBuilderOptionsQueryParams = {
1313
initialData?: BuilderOptionsResource;
1414
};
1515
export function useBuilderOptionsQuery(params: UseBuilderOptionsQueryParams) {
16-
const queryKey = ['resources', 'builder-options', fromUUID(params.scenarioId)] as const;
16+
const queryKey = ['resources', 'builder-options', fromUUIDtoSUUID(params.scenarioId)] as const;
1717

1818
return useQuery({
1919
queryKey,

packages/app-builder/src/queries/validate-ast.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
type AstValidationReturnType,
44
} from '@app-builder/routes/ressources+/scenarios+/$scenarioId+/validate-ast';
55
import { getRoute } from '@app-builder/utils/routes';
6-
import { fromUUID } from '@app-builder/utils/short-uuid';
6+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
77
import { useMutation } from '@tanstack/react-query';
88

99
const endpoint = (scenarioId: string) =>
@@ -15,7 +15,7 @@ type UseValidateAstMutationParams = {
1515
scenarioId: string;
1616
};
1717
export function useValidateAstMutation(params: UseValidateAstMutationParams) {
18-
const scenarioNanoId = fromUUID(params.scenarioId);
18+
const scenarioNanoId = fromUUIDtoSUUID(params.scenarioId);
1919

2020
return useMutation({
2121
mutationFn: async (payload: AstValidationPayload & { ac: AbortController }) => {

packages/app-builder/src/repositories/TestRunRepository.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
type TestRunStatus,
1313
testRunStatuses,
1414
} from '@app-builder/models/testrun';
15-
import { toUUID } from '@app-builder/utils/short-uuid';
15+
import { fromSUUIDtoUUID } from '@app-builder/utils/short-uuid';
1616
import { addDays } from 'date-fns';
1717
import { sleep } from 'radash';
1818
import { randomInteger } from 'remeda';
@@ -182,7 +182,7 @@ export const makeGetTestRunRepository2 = () => {
182182
},
183183
launchTestRun: (args: TestRunCreateInput) => {
184184
const testRun: TestRun = {
185-
id: toUUID(short.generate()),
185+
id: fromSUUIDtoUUID(short.generate()),
186186
refIterationId: '6f6fe0d8-9a1a-4d5a-bdd7-fa7fcda1b4e3',
187187
scenarioId: args.scenarioId,
188188
testIterationId: args.testIterationId,
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { initServerServices } from '@app-builder/services/init.server';
2+
import { parseIdParamSafe } from '@app-builder/utils/input-validation';
23
import { getRoute } from '@app-builder/utils/routes';
3-
import { fromParams, fromUUID } from '@app-builder/utils/short-uuid';
4+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
45
import { type LoaderFunctionArgs } from '@remix-run/node';
56

67
export async function loader({ request, params }: LoaderFunctionArgs) {
78
const { authService } = initServerServices(request);
89

9-
const caseId = fromParams(params, 'caseId');
10-
if (!caseId) {
11-
return {
12-
redirect: getRoute('/cases/inboxes'),
13-
};
10+
const parsedParams = await parseIdParamSafe(params, 'caseId');
11+
12+
if (!parsedParams.success) {
13+
return { redirect: getRoute('/cases/inboxes') };
1414
}
1515

1616
await authService.isAuthenticated(request, {
1717
failureRedirect: getRoute('/sign-in'),
1818
successRedirect: getRoute('/cases/:caseId/decisions', {
19-
caseId: fromUUID(caseId),
19+
caseId: fromUUIDtoSUUID(parsedParams.data.caseId),
2020
}),
2121
});
2222
}

packages/app-builder/src/routes/_builder+/cases+/$caseId._layout.tsx

+21-12
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ import {
3131
import { initServerServices } from '@app-builder/services/init.server';
3232
import { getCaseFileUploadEndpoint } from '@app-builder/utils/files';
3333
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
34+
import { parseIdParamSafe } from '@app-builder/utils/input-validation';
3435
import { getRoute, type RouteID } from '@app-builder/utils/routes';
35-
import { fromParams, fromUUID } from '@app-builder/utils/short-uuid';
36+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
3637
import { defer, type LoaderFunctionArgs, type SerializeFrom } from '@remix-run/node';
3738
import {
3839
isRouteErrorResponse,
3940
Outlet,
41+
redirect,
4042
useLoaderData,
4143
useNavigate,
4244
useRouteError,
@@ -62,12 +64,12 @@ export const handle = {
6264
);
6365
},
6466
({ isLast }: BreadCrumbProps) => {
65-
const { inbox } = useLoaderData<typeof loader>();
67+
const { inbox } = useLoaderData<typeof loader>(); // Ensure inbox is part of the loader's return type
6668

6769
return (
6870
<BreadCrumbLink
6971
to={getRoute('/cases/inboxes/:inboxId', {
70-
inboxId: fromUUID(inbox.id),
72+
inboxId: fromUUIDtoSUUID(inbox.id),
7173
})}
7274
isLast={isLast}
7375
>
@@ -76,12 +78,13 @@ export const handle = {
7678
);
7779
},
7880
({ isLast }: BreadCrumbProps) => {
79-
const { caseDetail } = useLoaderData<typeof loader>();
81+
const data = useLoaderData<typeof loader>();
82+
const caseDetail = data.caseDetail; // Safely access caseDetail from the loader data
8083

8184
return (
8285
<div className="flex items-center gap-4">
8386
<BreadCrumbLink
84-
to={getRoute('/cases/:caseId', { caseId: fromUUID(caseDetail.id) })}
87+
to={getRoute('/cases/:caseId', { caseId: fromUUIDtoSUUID(caseDetail.id) })}
8588
isLast={isLast}
8689
>
8790
<span className="line-clamp-2 text-start">{caseDetail.name}</span>
@@ -114,9 +117,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
114117
failureRedirect: getRoute('/sign-in'),
115118
});
116119

117-
const caseId = fromParams(params, 'caseId');
120+
const parsedParams = await parseIdParamSafe(params, 'caseId');
121+
122+
if (!parsedParams.success) return redirect(getRoute('/cases/inboxes'));
123+
118124
try {
119-
const caseDetail = await cases.getCase({ caseId });
125+
const caseDetail = await cases.getCase({ caseId: parsedParams.data.caseId });
120126
const currentInbox = await inbox.getInbox(caseDetail.inboxId);
121127

122128
const dataModelPromise = dataModelRepository.getDataModel();
@@ -159,9 +165,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
159165
};
160166
}),
161167
);
162-
163168
return defer({
164-
caseDetail,
169+
caseDetail, // Ensure caseDetail is explicitly included in the returned data
165170
inbox: currentInbox,
166171
user,
167172
entitlements,
@@ -182,10 +187,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
182187
}
183188
}
184189

185-
export function useCurrentCase() {
186-
return useRouteLoaderData(
190+
export function useCurrentCase(): SerializeFrom<typeof loader> {
191+
const data = useRouteLoaderData<typeof loader>(
187192
'routes/_builder+/cases+/$caseId._layout' satisfies RouteID,
188-
) as SerializeFrom<typeof loader>;
193+
);
194+
if (!data || typeof data !== 'object') {
195+
throw new Error('Loader data is undefined or invalid');
196+
}
197+
return data as SerializeFrom<typeof loader>;
189198
}
190199

191200
export default function CasePage() {

0 commit comments

Comments
 (0)