-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Storybook: Automate sidebar sort order #74672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,45 +95,156 @@ export const parameters = { | |
| ), | ||
| }, | ||
| options: { | ||
| storySort: { | ||
| order: [ | ||
| storySort: ( a, b ) => { | ||
| const SECTION_ORDER = [ | ||
| 'Docs', | ||
| 'Playground', | ||
| 'BlockEditor', | ||
| 'Components', | ||
| [ | ||
| 'Introduction', | ||
| 'Contributing Guidelines', | ||
| 'Actions', | ||
| 'Containers', | ||
| 'Feedback', | ||
| 'Layout', | ||
| 'Navigation', | ||
| 'Overlays', | ||
| 'Selection & Input', | ||
| [ | ||
| 'Color', | ||
| 'Common', | ||
| 'File Upload', | ||
| 'Time & Date', | ||
| 'Validated Form Controls', | ||
| [ 'Overview' ], | ||
| ], | ||
| 'Typography', | ||
| 'Utilities', | ||
| ], | ||
| [ | ||
| 'Actions', | ||
| 'Layout', | ||
| 'Navigation', | ||
| 'Overlays', | ||
| 'Selection & Input', | ||
| 'Typography', | ||
| ], | ||
| 'Icons', | ||
| 'Design System', | ||
| [ 'Introduction', 'Theme', 'Components', [ 'Introduction' ] ], | ||
| ], | ||
| ]; | ||
| const PRIORITIZED_MDX_DOCS = [ 'Introduction', 'Overview' ]; | ||
|
|
||
| // Get order index for a name in an order array (skipping nested arrays) | ||
| const getOrderIndex = ( orderArray, name ) => { | ||
| let index = 0; | ||
| for ( const item of orderArray ) { | ||
| if ( item === name ) { | ||
| return index; | ||
| } | ||
| if ( ! Array.isArray( item ) ) { | ||
| index++; | ||
| } | ||
| } | ||
| return index; // Not found, put at end | ||
| }; | ||
|
|
||
| // Get the nested order array that follows a name | ||
| const getNestedOrder = ( orderArray, name ) => { | ||
| for ( let i = 0; i < orderArray.length; i++ ) { | ||
| if ( | ||
| orderArray[ i ] === name && | ||
| Array.isArray( orderArray[ i + 1 ] ) | ||
| ) { | ||
| return orderArray[ i + 1 ]; | ||
| } | ||
| } | ||
| return []; | ||
| }; | ||
|
|
||
| // Check if an entry is an MDX doc page ending at a specific path depth | ||
| const isMdxAtDepth = ( entry, path, depth ) => | ||
| entry.type === 'docs' && | ||
| path.length === depth + 1 && | ||
| entry.importPath?.endsWith( '.mdx' ); | ||
|
|
||
| const aPath = a.title.split( '/' ); | ||
| const bPath = b.title.split( '/' ); | ||
| const maxDepth = Math.max( aPath.length, bPath.length ); | ||
|
|
||
| let currentOrder = SECTION_ORDER; | ||
|
|
||
| for ( let depth = 0; depth < maxDepth; depth++ ) { | ||
| const aSegment = aPath[ depth ]; | ||
| const bSegment = bPath[ depth ]; | ||
|
|
||
| // If one path is shorter, check if the longer one is an MDX doc | ||
| // that should appear before stories at the parent level | ||
| if ( aSegment === undefined && bSegment !== undefined ) { | ||
| // MDX subfolder comes before parent's stories | ||
| if ( | ||
| a.type === 'story' && | ||
| isMdxAtDepth( b, bPath, depth ) | ||
| ) { | ||
| return 1; | ||
| } | ||
| return -1; | ||
| } | ||
| if ( aSegment !== undefined && bSegment === undefined ) { | ||
| // MDX subfolder comes before parent's stories | ||
| if ( | ||
| b.type === 'story' && | ||
| isMdxAtDepth( a, aPath, depth ) | ||
| ) { | ||
| return -1; | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| if ( aSegment !== bSegment ) { | ||
| // MDX pages (importPath ends in .mdx) come before components | ||
| // (whose autodocs are generated from .story.tsx files) | ||
| const aIsMdx = isMdxAtDepth( a, aPath, depth ); | ||
| const bIsMdx = isMdxAtDepth( b, bPath, depth ); | ||
|
|
||
| if ( aIsMdx !== bIsMdx ) { | ||
| return aIsMdx ? -1 : 1; | ||
| } | ||
|
|
||
| // Among MDX pages, prioritize those in PRIORITIZED_MDX_DOCS | ||
| if ( aIsMdx && bIsMdx ) { | ||
| const aIsPrioritized = | ||
| PRIORITIZED_MDX_DOCS.includes( aSegment ); | ||
| const bIsPrioritized = | ||
| PRIORITIZED_MDX_DOCS.includes( bSegment ); | ||
| if ( aIsPrioritized !== bIsPrioritized ) { | ||
| return aIsPrioritized ? -1 : 1; | ||
| } | ||
| } | ||
|
|
||
| const aIndex = getOrderIndex( currentOrder, aSegment ); | ||
| const bIndex = getOrderIndex( currentOrder, bSegment ); | ||
| if ( aIndex !== bIndex ) { | ||
| return aIndex - bIndex; | ||
| } | ||
| // Same index (both not in order list), sort alphabetically | ||
| return aSegment.localeCompare( bSegment ); | ||
| } | ||
|
|
||
| // Same segment, descend into nested order | ||
| currentOrder = getNestedOrder( currentOrder, aSegment ); | ||
| } | ||
|
|
||
| // Same title - sort by type: | ||
| // 0: autodocs "Docs" | ||
| // 1: prioritized MDX pages (Introduction, Overview, etc.) | ||
| // 2: other MDX docs | ||
| // 3: stories (which appear before subfolders due to shorter paths) | ||
| const getPriority = ( entry ) => { | ||
| if ( entry.type === 'docs' && entry.name === 'Docs' ) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My intent here with the two constants I split out was to highlight the user-configurable values. I'd say the Storybook-specific values are unlikely to change. |
||
| return 0; | ||
| } | ||
| if ( | ||
| entry.type === 'docs' && | ||
| PRIORITIZED_MDX_DOCS.includes( entry.name ) | ||
| ) { | ||
| return 1; | ||
| } | ||
| if ( entry.type === 'docs' ) { | ||
| return 2; | ||
| } | ||
| return 3; | ||
| }; | ||
| const aPriority = getPriority( a ); | ||
| const bPriority = getPriority( b ); | ||
| if ( aPriority !== bPriority ) { | ||
| return aPriority - bPriority; | ||
| } | ||
|
|
||
| // Same priority, sort alphabetically by name | ||
| return a.name.localeCompare( b.name ); | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor, but should we move these local functions outside of the config object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather, I was thinking whether we should split the entire story sort function outside of the config file. Not a strong opinion, but decided against it because the indirection was probably not worth it given how infrequently we touch this file.