Skip to content

Commit c565d7b

Browse files
authored
storybook: correct sidebar links (#104522)
Fix story links so that they are compatible with the double URL structure
1 parent 08318d4 commit c565d7b

File tree

15 files changed

+277
-175
lines changed

15 files changed

+277
-175
lines changed

eslint.config.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,17 @@ const restrictedImportPaths = [
129129
{
130130
name: 'sentry/views/insights/common/components/insightsTimeSeriesWidget',
131131
message:
132-
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/?name=app%2Fviews%2Fdashboards%2Fwidgets%2FtimeSeriesWidget%2FtimeSeriesWidgetVisualization.stories.tsx&query=timeseries#deeplinking for more information',
132+
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/shared/views/dashboards/widgets/timeserieswidget/timeserieswidgetvisualization#deeplinking for more information',
133133
},
134134
{
135135
name: 'sentry/views/insights/common/components/insightsLineChartWidget',
136136
message:
137-
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/?name=app%2Fviews%2Fdashboards%2Fwidgets%2FtimeSeriesWidget%2FtimeSeriesWidgetVisualization.stories.tsx&query=timeseries#deeplinking for more information',
137+
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/shared/views/dashboards/widgets/timeserieswidget/timeserieswidgetvisualization#deeplinking for more information',
138138
},
139139
{
140140
name: 'sentry/views/insights/common/components/insightsAreaChartWidget',
141141
message:
142-
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/?name=app%2Fviews%2Fdashboards%2Fwidgets%2FtimeSeriesWidget%2FtimeSeriesWidgetVisualization.stories.tsx&query=timeseries#deeplinking for more information',
142+
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/shared/views/dashboards/widgets/timeserieswidget/timeserieswidgetvisualization#deeplinking for more information',
143143
},
144144
];
145145

static/app/router/routes.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,9 @@ function buildRoutes(): RouteObject[] {
318318
],
319319
},
320320
{
321-
path: '/stories/:storyType?/:storySlug?/',
322-
component: make(() => import('sentry/stories/view/index')),
321+
path: '/stories/*',
323322
withOrgPath: true,
323+
component: make(() => import('sentry/stories/view/index')),
324324
},
325325
{
326326
path: '/debug/notifications/:notificationSource?/',

static/app/stories/type-loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function prodTypeloader(this: LoaderContext<any>, _source: string) {
120120

121121
function noopTypeLoader(this: LoaderContext<any>, _source: string) {
122122
const callback = this.async();
123-
return callback(null, 'export default {}');
123+
return callback(null, 'export default {props: {},exports: {}}');
124124
}
125125

126126
export default function typeLoader(this: LoaderContext<any>, _source: string) {

static/app/stories/view/index.tsx

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import styled from '@emotion/styled';
44

55
import {Alert} from 'sentry/components/core/alert';
66
import LoadingIndicator from 'sentry/components/loadingIndicator';
7-
import {StorySidebar} from 'sentry/stories/view/storySidebar';
8-
import {useStoryRedirect} from 'sentry/stories/view/useStoryRedirect';
7+
import {
8+
StorySidebar,
9+
useStoryBookFilesByCategory,
10+
} from 'sentry/stories/view/storySidebar';
11+
import {StoryTreeNode, type StoryCategory} from 'sentry/stories/view/storyTree';
912
import {useLocation} from 'sentry/utils/useLocation';
1013
import OrganizationContainer from 'sentry/views/organizationContainer';
1114
import RouteAnalyticsContextProvider from 'sentry/views/routeAnalyticsContextProvider';
@@ -16,13 +19,24 @@ import {StoryHeader} from './storyHeader';
1619
import {useStoryDarkModeTheme} from './useStoriesDarkMode';
1720
import {useStoriesLoader} from './useStoriesLoader';
1821

19-
export default function Stories() {
22+
export function useStoryParams(): {storyCategory?: StoryCategory; storySlug?: string} {
2023
const location = useLocation();
21-
return isLandingPage(location) ? <StoriesLanding /> : <StoryDetail />;
24+
// Match: /stories/:category/(one/optional/or/more/path/segments)
25+
// Handles both /stories/... and /organizations/{org}/stories/...
26+
const match = location.pathname.match(/\/stories\/([^/]+)\/(.+)/);
27+
return {
28+
storyCategory: match?.[1] as StoryCategory | undefined,
29+
storySlug: match?.[2] ?? undefined,
30+
};
2231
}
2332

24-
function isLandingPage(location: ReturnType<typeof useLocation>) {
25-
return /\/stories\/?$/.test(location.pathname) && !location.query.name;
33+
export default function Stories() {
34+
const location = useLocation();
35+
return isLandingPage(location) && !location.query.name ? (
36+
<StoriesLanding />
37+
) : (
38+
<StoryDetail />
39+
);
2640
}
2741

2842
function StoriesLanding() {
@@ -36,11 +50,37 @@ function StoriesLanding() {
3650
}
3751

3852
function StoryDetail() {
39-
useStoryRedirect();
53+
const location = useLocation();
54+
const {storyCategory, storySlug} = useStoryParams();
55+
const stories = useStoryBookFilesByCategory();
56+
57+
let storyNode = getStoryFromParams(stories, {
58+
category: storyCategory,
59+
slug: storySlug,
60+
});
61+
62+
// If we don't have a story node, try to find it by the filesystem path
63+
if (!storyNode && location.query.name) {
64+
const nodes = Object.values(stories).flat();
65+
const queue = [...nodes];
66+
67+
while (queue.length > 0) {
68+
const node = queue.pop();
69+
if (!node) break;
70+
71+
if (node.filesystemPath === location.query.name) {
72+
storyNode = node;
73+
break;
74+
}
75+
76+
for (const key in node.children) {
77+
queue.push(node.children[key]!);
78+
}
79+
}
80+
}
4081

41-
const location = useLocation<{name: string; query?: string}>();
4282
const story = useStoriesLoader({
43-
files: [location.state?.storyPath ?? location.query.name],
83+
files: storyNode ? [storyNode.filesystemPath] : [],
4484
});
4585

4686
return (
@@ -91,6 +131,39 @@ function StoriesLayout(props: PropsWithChildren) {
91131
);
92132
}
93133

134+
function isLandingPage(location: ReturnType<typeof useLocation>) {
135+
// Handles both /stories and /organizations/{org}/stories
136+
return /\/stories\/?$/.test(location.pathname);
137+
}
138+
139+
function getStoryFromParams(
140+
stories: ReturnType<typeof useStoryBookFilesByCategory>,
141+
context: {category?: StoryCategory; slug?: string}
142+
): StoryTreeNode | undefined {
143+
const nodes = stories[context.category as keyof typeof stories] ?? [];
144+
145+
if (!nodes || nodes.length === 0) {
146+
return undefined;
147+
}
148+
149+
const queue = [...nodes];
150+
151+
while (queue.length > 0) {
152+
const node = queue.pop();
153+
if (!node) break;
154+
155+
if (node.slug === context.slug) {
156+
return node;
157+
}
158+
159+
for (const key in node.children) {
160+
queue.push(node.children[key]!);
161+
}
162+
}
163+
164+
return undefined;
165+
}
166+
94167
function GlobalStoryStyles() {
95168
const theme = useTheme();
96169
const darkTheme = useStoryDarkModeTheme();

static/app/stories/view/landing/index.tsx

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {PropsWithChildren} from 'react';
22
import {Fragment} from 'react';
33
import {useTheme} from '@emotion/react';
44
import styled from '@emotion/styled';
5+
import type {LocationDescriptor} from 'history';
56

67
import performanceWaitingForSpan from 'sentry-images/spot/performance-waiting-for-span.svg';
78
import heroImg from 'sentry-images/stories/landing/robopigeon.png';
@@ -13,6 +14,8 @@ import {Link} from 'sentry/components/core/link';
1314
import {IconOpen} from 'sentry/icons';
1415
import {Acronym} from 'sentry/stories/view/landing/acronym';
1516
import {StoryDarkModeProvider} from 'sentry/stories/view/useStoriesDarkMode';
17+
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
18+
import useOrganization from 'sentry/utils/useOrganization';
1619

1720
import {Colors, Icons, Typography} from './figures';
1821

@@ -29,7 +32,7 @@ const frontmatter = {
2932
actions: [
3033
{
3134
children: 'Get Started',
32-
to: '/stories?name=app/styles/colors.mdx',
35+
to: '/stories/foundations/colors',
3336
priority: 'primary',
3437
},
3538
{
@@ -43,6 +46,8 @@ const frontmatter = {
4346
};
4447

4548
export function StoryLanding() {
49+
const organization = useOrganization();
50+
4651
return (
4752
<Fragment>
4853
<StoryDarkModeProvider>
@@ -57,9 +62,14 @@ export function StoryLanding() {
5762
<p>{frontmatter.hero.tagline}</p>
5863
</Flex>
5964
<Flex gap="md">
60-
{frontmatter.hero.actions.map(props => (
61-
<LinkButton {...props} key={props.to} />
62-
))}
65+
{frontmatter.hero.actions.map(props => {
66+
// Normalize internal paths with organization context
67+
const to =
68+
typeof props.to === 'string' && !props.external
69+
? normalizeUrl(`/organizations/${organization.slug}${props.to}`)
70+
: props.to;
71+
return <LinkButton {...props} to={to} key={props.to} />;
72+
})}
6373
</Flex>
6474
</Flex>
6575
<img
@@ -86,25 +96,50 @@ export function StoryLanding() {
8696
</p>
8797
</Flex>
8898
<CardGrid>
89-
<Card href="/stories?name=app/styles/colors.mdx" title="Color">
99+
<Card
100+
to={{
101+
pathname: normalizeUrl(
102+
`/organizations/${organization.slug}/stories/foundations/colors`
103+
),
104+
}}
105+
title="Color"
106+
>
90107
<CardFigure>
91108
<Colors />
92109
</CardFigure>
93110
</Card>
94-
<Card href="/stories/?name=app%2Ficons%2Ficons.stories.tsx" title="Icons">
111+
<Card
112+
to={{
113+
pathname: normalizeUrl(
114+
`/organizations/${organization.slug}/stories/foundations/icons`
115+
),
116+
}}
117+
title="Icons"
118+
>
95119
<CardFigure>
96120
<Icons />
97121
</CardFigure>
98122
</Card>
99123
<Card
100-
href="/stories/?name=app%2Fstyles%2Ftypography.stories.tsx"
124+
to={{
125+
pathname: normalizeUrl(
126+
`/organizations/${organization.slug}/stories/foundations/typography`
127+
),
128+
}}
101129
title="Typography"
102130
>
103131
<CardFigure>
104132
<Typography />
105133
</CardFigure>
106134
</Card>
107-
<Card href="/stories/?name=app%2Fstyles%2Fimages.stories.tsx" title="Images">
135+
<Card
136+
to={{
137+
pathname: normalizeUrl(
138+
`/organizations/${organization.slug}/stories/foundations/images`
139+
),
140+
}}
141+
title="Images"
142+
>
108143
<CardFigure>
109144
<img src={performanceWaitingForSpan} />
110145
</CardFigure>
@@ -192,12 +227,13 @@ const CardGrid = styled('div')`
192227

193228
interface CardProps {
194229
children: React.ReactNode;
195-
href: string;
196230
title: string;
231+
to: LocationDescriptor;
197232
}
233+
198234
function Card(props: CardProps) {
199235
return (
200-
<CardLink to={props.href}>
236+
<CardLink to={props.to}>
201237
{props.children}
202238
<CardTitle>{props.title}</CardTitle>
203239
</CardLink>

static/app/stories/view/storyExports.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, {Fragment, useEffect} from 'react';
22
import {useTheme} from '@emotion/react';
33
import styled from '@emotion/styled';
44
import {ErrorBoundary} from '@sentry/react';
5+
import {parseAsString, useQueryState} from 'nuqs';
56

67
import {Alert} from 'sentry/components/core/alert';
78
import {Tag} from 'sentry/components/core/badge/tag';
@@ -37,8 +38,13 @@ export function StoryExports(props: {story: StoryDescriptor}) {
3738

3839
function StoryLayout() {
3940
const {story} = useStory();
41+
const [tab, setTab] = useQueryState(
42+
'tab',
43+
parseAsString.withOptions({history: 'push'}).withDefault('usage')
44+
);
45+
4046
return (
41-
<Tabs>
47+
<Tabs value={tab} onChange={setTab}>
4248
{isMDXStory(story) ? <MDXStoryTitle story={story} /> : null}
4349
<StoryGrid>
4450
<StoryContainer>
@@ -123,6 +129,7 @@ function MDXStoryTitle(props: {story: MDXStoryDescriptor}) {
123129

124130
function StoryTabList() {
125131
const {story} = useStory();
132+
126133
if (!isMDXStory(story)) return null;
127134
if (story.exports.frontmatter?.layout === 'document') return null;
128135

0 commit comments

Comments
 (0)