Skip to content
Merged
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
155 changes: 133 additions & 22 deletions storybook/preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) => {
Copy link
Contributor

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?

Copy link
Member Author

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.

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' ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the docs-related strings be extracted to a constant, since they are a Storybook convention?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 );
},
},
};
Expand Down
Loading