storybook-multilevel-sort
Advanced tools
Comparing version 1.1.2 to 1.2.0
@@ -6,6 +6,15 @@ export interface Order { | ||
// eslint-disable-next-line no-explicit-any | ||
type Story = any | ||
export type Story = any | ||
declare function sort(order: Order, story1: Story, story2: Story): 0 | -1 | 1 | ||
export type Result = 0 | -1 | 1 | ||
export default sort | ||
export interface Context { | ||
path1: string[], | ||
path2: string[] | ||
} | ||
export interface Options { | ||
compareNames?: (name1: string, name2: string, context: Context) => Result | ||
} | ||
export default function sort(order: Order, story1: Story, story2: Story, options?: Options): Result |
@@ -7,33 +7,50 @@ // See https://github.com/storybookjs/storybook/issues/548#issuecomment-1099949201 | ||
: (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) | ||
const compareAlphabetical = (a, b) => a.localeCompare(b, { numeric: true }); | ||
const compareAlphabetical = (name1, name2) => name1.localeCompare(name2, { numeric: true }) | ||
const compareStoryPaths = (order, path1, path2) => { | ||
const compareStoryPaths = (order, path1, path2, context) => { | ||
/* c8 ignore next 9 */ | ||
if (path1.length === 0 && path2.length === 0) { | ||
return 0; | ||
return 0 | ||
} else if (path1.length === 0 && path2.length > 0) { | ||
// Path1 must be an ancestor of path2 | ||
return -1; | ||
return -1 | ||
} else if (path1.length > 0 && path2.length === 0) { | ||
// Path2 must be an ancestor of path1 | ||
return 1; | ||
return 1 | ||
} | ||
const [path1Head, ...path1Tail] = path1; | ||
const [path2Head, ...path2Tail] = path2; | ||
const [path1Head, ...path1Tail] = path1 | ||
const [path2Head, ...path2Tail] = path2 | ||
if (!order) { | ||
// No reference order, so just sort alphabetically | ||
const comp = compareAlphabetical(path1Head, path2Head); | ||
if (comp === 0) { | ||
return compareStoryPaths(null, path1Tail, path2Tail); | ||
const result = context.compareNames(path1Head, path2Head, { path1, path2 }) | ||
if (result === 0) { | ||
return compareStoryPaths(null, path1Tail, path2Tail, context) | ||
} else { | ||
return comp; | ||
return result | ||
} | ||
} | ||
let currentOrder | ||
const updatetOrderAndCompare = (newOrder, path1, path2) => { | ||
// Propagate the nested wildcard to the following call to compareStoryPaths | ||
const nestedOrder = order['**'] | ||
if (nestedOrder) context.order = nestedOrder | ||
currentOrder = newOrder | ||
// If the same paths are going to be compared, do not use the nested wildcard | ||
// any more; it'd enter this clause once more and end up with a stack overflow | ||
if (!currentOrder && context.path1 !== path1 && context.path2 !== path2) { | ||
currentOrder = context.order | ||
} | ||
// Remember the current paths for future calls to compareStoryPaths | ||
context.path1 = path1 | ||
context.path2 = path2 | ||
return compareStoryPaths(currentOrder, path1, path2, context) | ||
} | ||
if (path1Head === path2Head) { | ||
// The two paths share the same head; try either the key for the head, or the | ||
// wildcard key, otherwise pass `undefined` to sort without an explicit order | ||
return compareStoryPaths(order[path1Head] || order['*'], path1Tail, path2Tail); | ||
// wildcard keys, otherwise pass `undefined` to sort without an explicit order | ||
return updatetOrderAndCompare(order[path1Head] || order['*'], path1Tail, path2Tail) | ||
} | ||
@@ -43,21 +60,21 @@ | ||
// If both heads are in the reference order, use the ordering of the keys in the reference order | ||
const orderKeys = Object.keys(order); | ||
const orderKeys = Object.keys(order) | ||
return orderKeys.indexOf(path1Head) < orderKeys.indexOf(path2Head) ? -1 : 1; | ||
return orderKeys.indexOf(path1Head) < orderKeys.indexOf(path2Head) ? -1 : 1 | ||
} else if (hasKey(order, path1Head) && !hasKey(order, path2Head)) { | ||
return -1; // Give preference to path1, since it is included in the reference order | ||
return -1 // Give preference to path1, since it is included in the reference order | ||
} else if (!hasKey(order, path1Head) && hasKey(order, path2Head)) { | ||
return 1; // Give preference to path2, since it is included in the reference order | ||
return 1 // Give preference to path2, since it is included in the reference order | ||
} else { | ||
// No explicit order for the path heads was found, try the wildcard key, | ||
// otherwise pass `undefined` to sort without an explicit order | ||
return compareStoryPaths(order['*'], path1, path2); | ||
return updatetOrderAndCompare(order['*'], path1, path2) | ||
} | ||
}; | ||
} | ||
export default (order, [, story1], [, story2]) => { | ||
const story1Path = [...story1.kind.split('/'), story1.name].map(key => key.toLowerCase()); | ||
const story2Path = [...story2.kind.split('/'), story2.name].map(key => key.toLowerCase()); | ||
export default (order, [, story1], [, story2], { compareNames = compareAlphabetical } = {}) => { | ||
const story1Path = [...story1.kind.split('/'), story1.name].map(key => key.toLowerCase()) | ||
const story2Path = [...story2.kind.split('/'), story2.name].map(key => key.toLowerCase()) | ||
return compareStoryPaths(order, story1Path, story2Path); | ||
}; | ||
return compareStoryPaths(order, story1Path, story2Path, { compareNames }) | ||
} |
{ | ||
"name": "storybook-multilevel-sort", | ||
"version": "1.1.2", | ||
"version": "1.2.0", | ||
"description": "Applies specific sort order to more than two levels of chapters and stories in a storybook.", | ||
@@ -40,6 +40,6 @@ "author": "Ferdinand Prantl <prantlf@gmail.com>", | ||
"prepare": "rollup -c", | ||
"lint": "denolint", | ||
"lint": "denolint && tsc --noEmit test/types.ts", | ||
"check": "teru-esm test/index.js && teru-cjs test/index.cjs", | ||
"cover": "c8 tehanu-esm test/index.js", | ||
"test": "denolint && teru-cjs test/index.cjs && c8 teru-esm test/index.js", | ||
"test": "denolint && tsc --noEmit test/types.ts && teru-cjs test/index.cjs && c8 teru-esm test/index.js", | ||
"ci": "teru-cjs test/index.cjs && c8 teru-esm test/index.js" | ||
@@ -75,5 +75,7 @@ }, | ||
"rollup": "^3.4.0", | ||
"storybook-multilevel-sort": "link:", | ||
"tehanu": "^1.0.1", | ||
"tehanu-repo-coco": "^1.0.0", | ||
"tehanu-teru": "^1.0.0" | ||
"tehanu-teru": "^1.0.0", | ||
"typescript": "^4.9.3" | ||
}, | ||
@@ -80,0 +82,0 @@ "keywords": [ |
140
README.md
@@ -20,6 +20,11 @@ # Multi-level Sorting for Storybook | ||
├── Components | ||
│ └── Header | ||
│ ├── Collapsed.mdx Components/Header/Collapsed | ||
│ ├── Default.mdx Components/Header/Default | ||
│ └── Expanded.mdx Components/Header/Expanded | ||
│ ├── 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 | ||
@@ -56,4 +61,9 @@ ├── Button | ||
Default | ||
With Search | ||
Collapsed | ||
Expanded | ||
List | ||
Default | ||
Collapsed | ||
Expanded | ||
``` | ||
@@ -72,2 +82,6 @@ | ||
components: { | ||
header: { | ||
default: null, | ||
'with search': null | ||
}, | ||
'*': { default: null } | ||
@@ -84,5 +98,21 @@ } | ||
A simplification using nested wildcards: | ||
```js | ||
const order = { | ||
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 14 or newer. | ||
This module can be installed in your project using [NPM], [PNPM] or [Yarn]. Make sure, that you use [Node.js] version 14.8 or newer. | ||
@@ -103,3 +133,3 @@ ```sh | ||
The function expects an object with the sorting configuration and two stories to compare, just like storybook passed them to the `storySort` method: | ||
The function expects an object with the sorting configuration, two stories to compare, just like storybook passed them to the `storySort` method, and optionally sorting options: | ||
@@ -120,2 +150,27 @@ ```js | ||
### 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 using the optional `options` parameter: | ||
```js | ||
const options = { | ||
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 }) | ||
} | ||
} | ||
... | ||
storySort: (story1, story2) => sort(order, story1, story2, options) | ||
``` | ||
Mind that the strings with names of groups and stories are converted to lower-case, before they are passed to the comparator. | ||
## Configuration | ||
@@ -138,2 +193,34 @@ | ||
### 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: | ||
```js | ||
// The name will be "With Search" | ||
export const WithSearch = Template.bind({}) | ||
``` | ||
They can be also assigned the displayable name using the `storyName` property: | ||
```js | ||
// 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: | ||
```js | ||
const order = { | ||
'*': { | ||
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: | ||
@@ -149,2 +236,42 @@ | ||
### Nested Wildcards | ||
If you want to enable implicit sorting at multiple levels, you would have to repeat the `*` selector on each level: | ||
```js | ||
{ | ||
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: | ||
```js | ||
{ | ||
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 | ||
## Motivation | ||
@@ -233,2 +360,3 @@ | ||
[sorting configuration supported by Storybook]: https://storybook.js.org/docs/react/writing-stories/naming-components-and-hierarchy#sorting-stories | ||
[algorithm how Storybook generates the names of stories]: https://storybook.js.org/docs/react/api/csf#named-story-exports | ||
[Node.js]: http://nodejs.org/ | ||
@@ -235,0 +363,0 @@ [NPM]: https://www.npmjs.com/ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
26683
153
357
10