Skip to content

Commit 440fc71

Browse files
authored
ref(stories): improve param type-safety (#95580)
- Refactors `/stories/*` routes to use `storyType` and `storySlug` params, rather than generic param names - Refactors `useStoryRedirect` for clarity - Fixes an issue where navigating to a story via the address bar would result in an infinite spinner. React Router's `useParams` hook supports this use case, but `useLocation` with `location.query` only works for client-side navigations
1 parent 8c3b7cf commit 440fc71

File tree

3 files changed

+42
-52
lines changed

3 files changed

+42
-52
lines changed

static/app/routes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ function buildRoutes() {
270270
<IndexRoute component={make(() => import('sentry/views/onboarding'))} />
271271
</Route>
272272
<Route
273-
path="/stories/:category?/:topic?"
273+
path="/stories/:storyType?/:storySlug?/"
274274
component={make(() => import('sentry/stories/view/index'))}
275275
withOrgPath
276276
/>

static/app/stories/view/useStoryRedirect.tsx

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,22 @@ import kebabCase from 'lodash/kebabCase';
33

44
import {useLocation} from 'sentry/utils/useLocation';
55
import {useNavigate} from 'sentry/utils/useNavigate';
6+
import {useParams} from 'sentry/utils/useParams';
67

78
import {useStoryBookFilesByCategory} from './storySidebar';
8-
import type {StoryCategory} from './storyTree';
9+
import type {StoryCategory, StoryTreeNode} from './storyTree';
910

1011
type LegacyStoryQuery = {
1112
name: string;
12-
category?: never;
13-
topic?: never;
1413
};
15-
type NewStoryQuery = {
16-
category: StoryCategory;
17-
topic: string;
18-
name?: never;
19-
};
20-
21-
type StoryQuery = LegacyStoryQuery | NewStoryQuery;
14+
interface StoryParams {
15+
storySlug: string;
16+
storyType: StoryCategory;
17+
}
2218

2319
export function useStoryRedirect() {
24-
const location = useLocation<StoryQuery>();
20+
const location = useLocation<LegacyStoryQuery>();
21+
const params = useParams<StoryParams>();
2522
const navigate = useNavigate();
2623
const stories = useStoryBookFilesByCategory();
2724

@@ -33,69 +30,60 @@ export function useStoryRedirect() {
3330
if (!location.pathname.startsWith('/stories')) {
3431
return;
3532
}
36-
const story = getStoryMeta(location.query, stories);
33+
const story = getStory(stories, {query: location.query, params});
3734
if (!story) {
3835
return;
3936
}
40-
if (story.category === 'shared') {
41-
navigate(
42-
{pathname: `/stories/`, search: `?name=${encodeURIComponent(story.path)}`},
43-
{replace: true, state: {storyPath: story.path}}
44-
);
45-
} else {
46-
navigate(
47-
{pathname: `/stories/${story.category}/${kebabCase(story.label)}`},
48-
{replace: true, state: {storyPath: story.path}}
49-
);
50-
}
51-
}, [location, navigate, stories]);
37+
const {state, ...to} = story.location;
38+
navigate(to, {replace: true, state});
39+
}, [location, params, navigate, stories]);
5240
}
5341

54-
interface StoryMeta {
55-
category: StoryCategory;
56-
label: string;
57-
path: string;
42+
interface StoryRouteContext {
43+
params: StoryParams;
44+
query: LegacyStoryQuery;
5845
}
5946

60-
function getStoryMeta(
61-
query: StoryQuery,
62-
stories: ReturnType<typeof useStoryBookFilesByCategory>
47+
function getStory(
48+
stories: ReturnType<typeof useStoryBookFilesByCategory>,
49+
context: StoryRouteContext
6350
) {
64-
if (query.name) {
65-
return legacyGetStoryMetaFromQuery(query, stories);
51+
if (context.params.storyType && context.params.storySlug) {
52+
return getStoryFromParams(stories, context);
6653
}
67-
if (query.category && query.topic) {
68-
return getStoryMetaFromQuery(query, stories);
54+
if (context.query.name) {
55+
return legacyGetStoryFromQuery(stories, context);
6956
}
7057
return undefined;
7158
}
7259

73-
function legacyGetStoryMetaFromQuery(
74-
query: LegacyStoryQuery,
75-
stories: ReturnType<typeof useStoryBookFilesByCategory>
76-
): StoryMeta | undefined {
60+
function legacyGetStoryFromQuery(
61+
stories: ReturnType<typeof useStoryBookFilesByCategory>,
62+
context: StoryRouteContext
63+
): StoryTreeNode | undefined {
7764
for (const category of Object.keys(stories) as StoryCategory[]) {
7865
const nodes = stories[category];
7966
for (const node of nodes) {
80-
const match = node.find(n => n.filesystemPath === query.name);
67+
const match = node.find(n => n.filesystemPath === context.query.name);
8168
if (match) {
82-
return {category, label: match.label, path: match.filesystemPath};
69+
return match;
8370
}
8471
}
8572
}
8673
return undefined;
8774
}
8875

89-
function getStoryMetaFromQuery(
90-
query: NewStoryQuery,
91-
stories: ReturnType<typeof useStoryBookFilesByCategory>
92-
): StoryMeta | undefined {
93-
const {category, topic} = query;
94-
const nodes = category in stories ? stories[category] : [];
76+
function getStoryFromParams(
77+
stories: ReturnType<typeof useStoryBookFilesByCategory>,
78+
context: StoryRouteContext
79+
): StoryTreeNode | undefined {
80+
const {storyType: category, storySlug} = context.params;
81+
const nodes =
82+
category && category in stories ? stories[category as keyof typeof stories] : [];
9583
for (const node of nodes) {
96-
const match = node.find(n => kebabCase(n.label) === topic);
84+
const match = node.find(n => kebabCase(n.label) === storySlug);
9785
if (match) {
98-
return {category, label: match.label, path: match.filesystemPath};
86+
return match;
9987
}
10088
}
10189
return undefined;

static/app/utils/useParams.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ type ParamKeys =
1717
| 'authId'
1818
| 'automationId'
1919
| 'codeId'
20-
| 'dataExportId'
2120
| 'dashboardId'
21+
| 'dataExportId'
22+
| 'detectorId'
2223
| 'detectorId'
2324
| 'docIntegrationSlug'
2425
| 'eventId'
2526
| 'fineTuneType'
2627
| 'groupId'
2728
| 'id'
2829
| 'installationId'
29-
| 'detectorId'
3030
| 'integrationSlug'
3131
| 'issueId'
3232
| 'memberId'
@@ -40,6 +40,8 @@ type ParamKeys =
4040
| 'sentryAppSlug'
4141
| 'shareId'
4242
| 'spanSlug'
43+
| 'storySlug'
44+
| 'storyType'
4345
| 'tagKey'
4446
| 'teamId'
4547
| 'traceSlug'

0 commit comments

Comments
 (0)