Skip to content
Merged
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
6 changes: 1 addition & 5 deletions static/app/stories/view/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {useMemo} from 'react';
import styled from '@emotion/styled';

import {Alert} from 'sentry/components/core/alert';
Expand Down Expand Up @@ -44,10 +43,7 @@ function StoriesLanding() {
function StoryDetail() {
useStoryRedirect();
const location = useLocation<{name: string; query?: string}>();
const files = useMemo(
() => [location.state?.storyPath ?? location.query.name],
[location.state?.storyPath, location.query.name]
);
const files = [location.state?.storyPath ?? location.query.name];
const story = useStoriesLoader({files});

return (
Expand Down
50 changes: 35 additions & 15 deletions static/app/stories/view/storySearch.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {Key} from 'react';
import {useCallback, useMemo, useRef, useState} from 'react';
import {useMemo, useRef, useState} from 'react';
import styled from '@emotion/styled';
import {type AriaComboBoxProps} from '@react-aria/combobox';
import {Item, Section} from '@react-stately/collections';
Expand Down Expand Up @@ -135,10 +135,10 @@ function SearchInput(

type SearchComboBoxItem<T extends StoryTreeNode> = T | StorySection;

interface SearchComboBoxProps<T extends StoryTreeNode>
extends Omit<AriaComboBoxProps<SearchComboBoxItem<T>>, 'children'> {
children: CollectionChildren<SearchComboBoxItem<T>>;
defaultItems: Array<SearchComboBoxItem<T>>;
interface SearchComboBoxProps
extends Omit<AriaComboBoxProps<SearchComboBoxItem<StoryTreeNode>>, 'children'> {
children: CollectionChildren<SearchComboBoxItem<StoryTreeNode>>;
defaultItems: Array<SearchComboBoxItem<StoryTreeNode>>;
inputRef: React.RefObject<HTMLInputElement | null>;
description?: string | null;
label?: string;
Expand All @@ -149,20 +149,23 @@ function filter(textValue: string, inputValue: string): boolean {
return match.score > 0;
}

function SearchComboBox<T extends StoryTreeNode>(props: SearchComboBoxProps<T>) {
function SearchComboBox(props: SearchComboBoxProps) {
const [inputValue, setInputValue] = useState('');
const {inputRef} = props;
const listBoxRef = useRef<HTMLUListElement | null>(null);
const popoverRef = useRef<HTMLDivElement | null>(null);
const navigate = useNavigate();
const handleSelectionChange = useCallback(
(key: Key | null) => {
if (key) {
navigate(`/stories?name=${key}`, {replace: true});
}
},
[navigate]
);
const handleSelectionChange = (key: Key | null) => {
if (!key) {
return;
}
const node = getStoryTreeNodeFromKey(key, props);
if (!node) {
return;
}
const {state, ...to} = node.location;
navigate(to, {replace: true, state});
};

const state = useComboBoxState({
...props,
Expand All @@ -175,7 +178,7 @@ function SearchComboBox<T extends StoryTreeNode>(props: SearchComboBoxProps<T>)
});

const {inputProps, listBoxProps, labelProps} = useSearchTokenCombobox<
SearchComboBoxItem<T>
SearchComboBoxItem<StoryTreeNode>
>(
{
...props,
Expand Down Expand Up @@ -242,3 +245,20 @@ const SectionTitle = styled('span')`
font-weight: 600;
text-transform: uppercase;
`;

function getStoryTreeNodeFromKey(
key: Key,
props: SearchComboBoxProps
): StoryTreeNode | undefined {
for (const category of props.defaultItems) {
if (isStorySection(category)) {
for (const node of category.options) {
const match = node.find(item => item.filesystemPath === key);
if (match) {
return match;
}
}
}
}
return undefined;
}
61 changes: 38 additions & 23 deletions static/app/stories/view/storyTree.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useMemo, useRef, useState} from 'react';
import styled from '@emotion/styled';
import * as qs from 'query-string';
import type {LocationDescriptorObject} from 'history';
import kebabCase from 'lodash/kebabCase';

import {Flex} from 'sentry/components/core/layout';
import {Link} from 'sentry/components/core/link';
Expand All @@ -14,6 +15,8 @@ export class StoryTreeNode {
public label: string;
public path: string;
public filesystemPath: string;
public category: StoryCategory;
public location: LocationDescriptorObject;

public visible = true;
public expanded = false;
Expand All @@ -26,6 +29,19 @@ export class StoryTreeNode {
this.label = normalizeFilename(name);
this.path = path;
this.filesystemPath = filesystemPath;
this.category = inferFileCategory(filesystemPath);
this.location = this.getLocation();
}

private getLocation(): LocationDescriptorObject {
const state = {storyPath: this.filesystemPath};
if (this.category === 'shared') {
return {pathname: '/stories/', query: {name: this.filesystemPath}, state};
}
return {
pathname: `/stories/${this.category}/${kebabCase(this.label)}`,
state,
};
}

find(predicate: (node: StoryTreeNode) => boolean): StoryTreeNode | undefined {
Expand Down Expand Up @@ -111,10 +127,10 @@ function folderOrSearchScoreFirst(
return a[0].localeCompare(b[0]);
}

const order: FileCategory[] = ['components', 'hooks', 'views', 'assets', 'styles'];
const order: StoryCategory[] = ['foundations', 'core', 'shared'];
function rootCategorySort(
a: [FileCategory | string, StoryTreeNode],
b: [FileCategory | string, StoryTreeNode]
a: [StoryCategory | string, StoryTreeNode],
b: [StoryCategory | string, StoryTreeNode]
) {
if (isFolderNode(a[1]) && isFolderNode(b[1])) {
return a[0].localeCompare(b[0]);
Expand All @@ -128,8 +144,8 @@ function rootCategorySort(
return -1;
}

if (order.includes(a[0] as FileCategory) && order.includes(b[0] as FileCategory)) {
return order.indexOf(a[0] as FileCategory) - order.indexOf(b[0] as FileCategory);
if (order.includes(a[0] as StoryCategory) && order.includes(b[0] as StoryCategory)) {
return order.indexOf(a[0] as StoryCategory) - order.indexOf(b[0] as StoryCategory);
}

return a[0].localeCompare(b[0]);
Expand All @@ -148,28 +164,26 @@ function normalizeFilename(filename: string) {
);
}

type FileCategory = 'hooks' | 'components' | 'views' | 'styles' | 'assets';
export type StoryCategory = 'foundations' | 'core' | 'shared';

function inferFileCategory(path: string): FileCategory {
const parts = path.split('/');
const filename = parts.at(-1);
if (filename?.startsWith('use')) {
return 'hooks';
function inferFileCategory(path: string): StoryCategory {
if (isCoreFile(path)) {
return 'core';
}

if (parts[1]?.startsWith('icons') || path.endsWith('images.stories.tsx')) {
return 'assets';
if (isFoundationFile(path)) {
return 'foundations';
}

if (parts[1]?.startsWith('views')) {
return 'views';
}
return 'shared';
}

if (parts[1]?.startsWith('styles')) {
return 'styles';
}
function isCoreFile(file: string) {
return file.includes('components/core');
}

return 'components';
function isFoundationFile(file: string) {
return file.includes('app/styles') || file.includes('app/icons');
}

function inferComponentName(path: string): string {
Expand Down Expand Up @@ -467,12 +481,13 @@ function Folder(props: {node: StoryTreeNode}) {

function File(props: {node: StoryTreeNode}) {
const location = useLocation();
const query = qs.stringify({...location.query, name: props.node.filesystemPath});
const {state, ...to} = props.node.location;

return (
<li>
<FolderLink
to={`/stories/?${query}`}
to={to}
state={state}
active={
props.node.filesystemPath === (location.state?.storyPath ?? location.query.name)
}
Expand Down
11 changes: 6 additions & 5 deletions static/app/stories/view/useStoryRedirect.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {useEffect} from 'react';
import {useLayoutEffect} from 'react';
import kebabCase from 'lodash/kebabCase';

import {useStoryBookFilesByCategory} from 'sentry/stories/view/storySidebar';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';

import {useStoryBookFilesByCategory} from './storySidebar';
import type {StoryCategory} from './storyTree';

type LegacyStoryQuery = {
name: string;
category?: never;
Expand All @@ -23,9 +25,9 @@ export function useStoryRedirect() {
const navigate = useNavigate();
const stories = useStoryBookFilesByCategory();

useEffect(() => {
useLayoutEffect(() => {
// If we already have a `storyPath` in state, bail out
if (location.state?.storyPath ?? location.query.name) {
if (location.state?.storyPath) {
return;
}
if (!location.pathname.startsWith('/stories')) {
Expand All @@ -49,7 +51,6 @@ export function useStoryRedirect() {
}, [location, navigate, stories]);
}

type StoryCategory = keyof ReturnType<typeof useStoryBookFilesByCategory>;
interface StoryMeta {
category: StoryCategory;
label: string;
Expand Down
Loading