Giter Club home page Giter Club logo

storybook-multilevel-sort's Introduction

Multi-level Sorting for Storybook

Latest version Dependency status Test Coverage

Applies specific sort order to more than two levels of chapters and stories in a storybook.

Attention: Versions 2.x of this package will support Storybook 7 only. If you use Storybook 6 or older, look for the versions 1.x of this package. If you upgrade Storybook to the version 7, you will need a version 2.x of this package too. See the documentation about how to migrate from a version 1.x to a version 2.x of this package.

See also an example of a Storybook project using this package.

Synopsis

The following directory structure:

.
├── Articles
│   ├── Getting Started.mdx   Articles/Getting Started
│   └── Versioning.mdx        Articles/Versioning
├── Components
│   ├── Header
│   │   ├── Collapsed.mdx     Components/Header/Collapsed
│   │   ├── Default.mdx       Components/Header/Default
│   │   ├── Expanded.mdx      Components/Header/Expanded
│   │   └── WithSearch.mdx    Components/Header/With Search
│   └── List
│       ├── Collapsed.mdx     Components/List/Collapsed
│       ├── Default.mdx       Components/List/Default
│       └── Expanded.mdx      Components/List/Expanded
└── Elements
    ├── Button
    │   ├── Active.mdx        Elements/Button/Active
    │   └── Default.mdx       Elements/Button/Default
    └── Link
        ├── Active.mdx        Elements/Link/Active
        └── Default.mdx       Elements/Link/Default

Can be sorted according to this request:

  1. Elements before Components
  2. Default stories before the others
  3. With Search right after Default and before the others
  4. Otherwise alphabetically

Resulting in a TOC like this. The "Docs" chapters are inserted by Storybook 7 instead of the "Docs" tab. If you want to change their order, see Type Sort Order and Grouping below:

Articles
  Getting Started
  Versioning
Elements
  Button
    Docs
    Default
    Active
  Link
    Docs
    Default
    Active
Components
  Header
    Docs
    Default
    With Search
    Collapsed
    Expanded
  List
    Docs
    Default
    Collapsed
    Expanded

When using the following code in .storybook/main.js:

import { configureSort } from 'storybook-multilevel-sort'

configureSort({
  storyOrder: {
    articles: null,
    elements: {
      '*': { default: null }
    },
    components: {
      navigation: {
        header: {
          default: null,
          'with search': null
        }
      }
    },
    '**': { default: null }
  }
})

And the following code in .storybook/preview.js:

export default {
  parameters: {
    options: {
      storySort: (story1, story2) =>
        globalThis['storybook-multilevel-sort:storySort'](story1, story2)
    }
  }
}

A simpler configuration using nested wildcards:

{
  articles: null,
  elements: null,
  components: {
    header: {
      default: null,
      'with search': null
    },
  },
  '**': { default: null }
}

Installation

This module can be installed in your project using NPM, PNPM or Yarn. Make sure, that you use Node.js version 16 or newer.

npm i -D storybook-multilevel-sort
pnpm i -D storybook-multilevel-sort
yarn add storybook-multilevel-sort

API

This package exports a function to configure the custom sorting:

import { configureSort } from 'storybook-multilevel-sort'

The function is supposed to be executed in .storybook/main.js and expects an object with the sorting configuration:

configureSort({
  typeOrder: ...
  storyOrder: ...
  compareNames: ...
})

It prepares a global function, which will be called in the storySort callback with the two stories to compare, implemented in.storybook/preview.js:

export default {
  parameters: {
    options: {
      storySort: (story1, story2) =>
        globalThis['storybook-multilevel-sort:storySort'](story1, story2)
    }
  }
}

This package can be imported to CJS projects too:

const { configureSort } = require('storybook-multilevel-sort')

Configuration

The object expected by the configureSort function may include the following properties:

  • storyOrder: configuration of the sort order based on names of groups and stories (object, optional)
  • compareNames: custom name comparison function (function, optional)
  • typeOrder: configuration of the page grouping and sort order based on types of the pages (array, optional)

Name Sort Order

The sorting configuration is an object set by the sortOrder property. Keys are titles of groups and stories. Values are objects with the next level of groups or stories. Nesting of the objects follows the slash-delimited story paths set to the title attribute:

configureSort({
  storyOrder: {
    elements: {
      link: null,    // Elements/Link/...
      button: null   // Elements/Button/...
    },
    components: null // Components/Card/...
                    // Components/Header/...
  }
})

Keys in the sorting objects have to be lower-case. If a value is null or an empty object, that level will be sorted alphabetically. Names of groups or stories missing among the object keys will be sorted alphabetically, behind the names that are listed as keys.

Whitespace

Names of groups and stories may include spaces. They are usually declared using pascal-case or camel-case and Storybook will separate the words by spaces:

// The name will be "With Search"
export const WithSearch = Template.bind({})

They can be also assigned the displayable name using the storyName property:

// The name will be "With Search" too
export const story1 = Template.bind({})
story1.storyName = 'With Search'

When you refer to such groups or stories on the ordering configuration, use the displayable name (with spaces) lower-case, for example:

{
  storyOrder: {
    '*': {
      default: null,
      'with search': null
    }
  }
}

Generally, names of groups and stories are expected in the ordering configuration as Storybook displays them. Not as the exported variables are named. You need to be aware of the algorithm how Storybook generates the names of stories.

Wildcards

If you want to skip explicit sorting at one level and specify the next level, use * instead of names, for which you want to specify the next level. The * matches any name, which is not listed explicitly at the same level:

{
  storyOrder: {
    '*': {
      default: null // Link/Default
    }               // Link/Active
  }                 // Link/Visited
}

Nested Wildcards

If you want to enable implicit sorting at multiple levels, you would have to repeat the * selector on each level:

{
  storyOrder: {
    elements: {
      '*': {
        default: null // Link/Default
      }               // Link/Active
    },                // Link/Visited
    components: {
      '*': {
        default: null // Header/Default
      }               // Header/Collapsed
    }                 // Header/Expanded
  }
}

you can use a nested wildcard ** to specify default for the current and deeper levels. The ** matches any name, which is not listed explicitly at the same level and if there is no * wildcard selector at that level:

{
  storyOrder: {
    elements: null,
    components: null,
    '**': {
      default: null // Link/Default
    }               // Link/Active
  }                 // Link/Visited
                    // Header/Default
                    // Header/Collapsed
                    // Header/Expanded
}

The precedence of the selectors at a particular level:

  1. A concrete name of a group or story
  2. The wildcard * matching any name of a group or story
  3. The nested wildcard ** frm the same or from an outer level matching any name of a group or story

Custom Comparisons

Names of groups and stories on one level are compared alphabetically according to the current locale by default. If you need a different comparison, you can specify it by using the optional compareNames parameter:

{
  storyOrder: ...

  compareNames: (name1, name2, context) {
    // name1 - the string with the name on the left side of the comparison
    // name2 - the string with the name on the right side of the comparison
    // context - additional information
    // context.path1 - an array of strings with the path of groups
    //                 down to the left compared group or story name (name1)
    // context.path2 - an array of strings with the path of groups
    //                 down to the right compared group or story name (name2)
    return name1.localeCompare(name2, { numeric: true })
  }
}

Mind that the strings with names of groups and stories are converted to lower-case, before they are passed to the comparator.

Type Sort Order and Grouping

Storybook 7 introduced a new type of pages, which can appear among the stories - docs. The documentation page earlier accessible on the "Docs" tab was moved to the tree of groups and stories. It means that there is a new type of the node in the navigation tree, which you may want to sort independently of the pages of the previous type - story.

Storybook inserts the links to the "Docs" pages before the first story of a particular component. This custom sorting will retain it by default, because the "Docs" page usually contains an overview of the component's usage. But you can change it by the typeOrder property. This is the default value, which groups all pages of the docs type before all pages of the story type:

{
  storyOrder: ...

  typeOrder: ['docs', 'story']
}

The order of types in he array will be the order of the page groups. If you specify just one type, ['docs'] or ['story'], pages of this type will be grouped toghether at the beginning and all other pages will follow behind them, regardless of their type, sorted only by their names.

If you want to handle the docs pages like any other stories and sort all the pages only by their names, you can pass an empty array to typeOrder to disable the gouping by type:

{
  storyOrder: ...

  typeOrder: []
}

Motivation

Unfortunately, the sorting configuration supported by Storybook works only for two-level storybooks:

The order array can accept a nested array in order to sort 2nd-level story kinds.

If you group your components by one more level, the stories will move to the third level and you won't be able to sort them. For example:

.
├── Articles
│   ├── Getting Started.mdx
│   └── Versioning.mdx
└── Elements
    ├── Button
    │   ├── Active.mdx
    │   └── Default.mdx
    └── Link
        ├── Active.mdx
        └── Default.mdx

Let's say, that you want to sort the stories alphabetically, but put the Default story before the other stories. It's impossible using the declarative configuration, because stories are on the third level. The following configuration:

storySort: {
  order: ['Articles', '*', ['*', ['Default', '*']]]
}

Will generate the following TOC:

Articles
  Getting Started
  Versioning
Elements
  Button
    Active
    Default
  Link
    Active
    Default

This package will help generating the proper TOC:

Articles
  Getting Started
  Versioning
Elements
  Button
    Default
    Active
  Link
    Default
    Active

Using the following order configuration:

const order = {
  articles: null,
  elements: {
    '*': { default: null }
  }
}

Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Lint and test your code.

License

Copyright (c) 2022-2023 Ferdinand Prantl
Licensed under the MIT license.

Sort icons created by Freepik - Flaticon
Licensed under the Icon Free License (with attribution)

storybook-multilevel-sort's People

Contributors

prantlf avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

storybook-multilevel-sort's Issues

Multilevel Sort fails on Safari 14 - Object.hasOwn is not a function

When running Storybook with storybook-multilevel-sort on a Safari v14 browser, the error at the bottom of this post is shown.

The issue is simple - Safari v14 doesn't have an object.hasOwn() method.

It's pretty easy to workaround by adding the following polyfill before loading storybook-multilevel-sort

('hasOwn' in Object) || (Object.hasOwn = Object.call.bind(Object.hasOwnProperty));

The solution is to use Object.hasOwnProperty() instead of Object.hasOwn() in the following code

const hasKey = (obj, key) => Object.hasOwn(obj, key);

I'd be happy to have the code authors make this change, or I could issue a pull request. The fix doesn't appear to cause any backward compatibility issues.

Here's the full text of the error in case someone lands on this issue from a search. Also attached a screen shot of how this looks on a Safari v14 browser.

Error sorting stories with sort parameter function storySort(story1, story2) {
      return Object(storybook_multilevel_sort__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"])(order, story1, story2);
    }:

> Object.hasOwn is not a function. (In 'Object.hasOwn(obj, key)', 'Object.hasOwn' is undefined)

Are you using a V7-style sort function in V6 compatibility mode?

More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#v7-style-story-sort

Screen Shot 2022-10-27 at 10 34 36 AM

Issue in combination with Storybook and Cypress

Hello, I'm using this library in a Storybook project. The library is imported as described in the README and it works as expected.

At the same time, there are Cypress tests running against the Storybook website. Since the storybook-multilevel-sort library is used Cypress tests are failing.

Cypress dashboard opens regularly, once a test is selected to be run, effect is that No tests found. Cypress could not detect tests in this file. and the output in the Cypress window is the following

SyntaxError: 'import' and 'export' may appear only with 'sourceType: module'
    at EventEmitter.handler (/Users/xxxxxxxxx/Library/Caches/Cypress/9.5.3/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/util.js:69:27)
    at EventEmitter.emit (node:events:394:28)
    at ChildProcess.<anonymous> (/Users/xxxxxxxxx/Library/Caches/Cypress/9.5.3/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/util.js:19:22)
    at ChildProcess.emit (node:events:394:28)
    at emit (node:internal/child_process:920:12)
    at processTicksAndRejections (node:internal/process/task_queues:84:21)

I was thinking to use babel in cypress plugin configuration, but this makes no sense since the code with the import lives in Storybook files and not in Cypress files or tests files.
It is not super clear what to try to babelify in the process, since Cypress tests are independent by Storybook, or maybe I'm not seeing something obvious?

Thanks for the support

Use the new Framework API?

Hey @prantlf !
I'm one of the Storybook maintainers. I focus primarily on documentation and community outreach. I'm opening up this issue and letting you know that the Storybook API for building addons is currently being updated to factor in the changes introduced by the upcoming 7.0 release. If you're interested in making the required changes to help the community from benefiting from using your addon, we've prepared an abridged guide for the required changes here, and if you have any questions or issues, please reach out to us in the #prerelease channel in our Discord Server.

Hope you have a great day!

Stay safe

Incompatibility with `storyStoreV7`

Hi!

Thanks for the great plugin! 🙏

FYI, I'm seeing an error when when storybook is configured to use config.options.storyStoreV7:

Are you using a V6-style sort function in V7 mode?

When I remove the storyStoreV7 storybook builds fine.

const config = {
  framework: '@storybook/react',

  core: {
    builder: 'webpack5',
  },

  features: {
    /** Configures Storybook to load stories on demand, rather than during boot up */
    storyStoreV7: true,
  },
};

export default config;
> start-storybook -p 6006 --no-open

info @storybook/react v6.5.14
info
info => Loading presets
info => Serving static files from ././.storybook/assets at /
info => Loading custom manager config
info Addon-docs: using MDX1
info => Using implicit CSS loaders
info => Using default Webpack5 setup
info => Loading custom manager config
/my-project/node_modules/@storybook/store/dist/cjs/sortStories.js:52
    throw new Error((0, _tsDedent.default)(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n    Error sorting stories with sort parameter ", ":\n\n    > ", "\n    \n    Are you using a V6-style sort function in V7 mode?\n\n    More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#v7-style-story-sort\n  "])), storySortParameter, err.message));
          ^

Error: Error sorting stories with sort parameter (story1, story2) => sort(order, story1, story2):

> sort is not defined

Are you using a V6-style sort function in V7 mode?

More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#v7-style-story-sort
    at sortStoriesV7 (/my-project/node_modules/@storybook/store/dist/cjs/sortStories.js:52:11)
    at StoryIndexGenerator.sortStories (/my-project/node_modules/@storybook/core-server/dist/cjs/utils/StoryIndexGenerator.js:164:32)
    at StoryIndexGenerator.getIndex (/my-project/node_modules/@storybook/core-server/dist/cjs/utils/StoryIndexGenerator.js:178:18)
    at /my-project/node_modules/@storybook/core-server/dist/cjs/dev-server.js:117:24

Node.js v18.12.1
npm ERR! Lifecycle script `storybook` failed with error:
npm ERR! Error: command failed
npm ERR!   in workspace: @[email protected]
npm ERR!   at location: /my-project/apps/storybook

Wild selector level idea

This saved so much time for me! 🚀

I was thinking that it would be nice for the wild selector to go to any level. Let's say I want Playground story first on any level.

Right now I have to do this:

'*': {
    playground: null,
    '*': {
      playground: null,
    },
  },

but once I introduce a new (deeper) level, this sorter won't work for it anymore

Fallback sorting method configuration

@prantlf Excellent package, thank you for this. It works well, I just had one question regarding to the "fallback sort":

Can be sorted according to this request:

  1. Elements before Components
  2. Default stories before the others
  3. Otherwise alphabetically

Is it possible to configure the fallback sort to be the order of stories defined in the file, matching the Storybook default, instead of alphabetically?

Sorting does not work correctly when autodocs is set to 'tags'

When autodocs uses the default value ('tags'), groups with stories that have tags: ['autodocs'] are sorted above other groups, ignoring the order specified in storyOrder.

To reproduce:
Make the following changes:
Header.stories.js:
image

Main.js
image

Expected result:
Articles
Elements
Components

Actual result:
Articles
Components
Elements

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.