Skip to content
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

Add easy-to-use options to storySort #9188

Merged
merged 3 commits into from
Dec 21, 2019
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
62 changes: 60 additions & 2 deletions docs/src/pages/configurations/options-parameter/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ addons.setConfig({

### Sorting stories

Import and use `addParameters` with the `options` key in your `preview.js` file.
By default, stories are sorted in the order in which they were imported. This can be overridden by adding `storySort` to the `options` parameters in your `preview.js` file.

The most powerful method of sorting is to provide a function to `storySort`. Any custom sorting can be achieved with this method.

```js
import { addParameters, configure } from '@storybook/react';
Expand All @@ -86,9 +88,65 @@ addParameters({
storySort: (a, b) =>
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, { numeric: true }),
},
};
});
```

The `storySort` can also accept a configuration object.

```js
import { addParameters, configure } from '@storybook/react';

addParameters({
options: {
storySort: {
method: 'alphabetical', // Optional, defaults to 'configure'.
sort: ['Intro', 'Components'], // Optional, defaults to [].

Choose a reason for hiding this comment

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

I see the PR merged in next-6.0.0, but not seeing the changes reflected there. I wanted to point out the sort key here is wrong. The storySort method in this PR uses order to override the order.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this doc seems to be outdated. There's order in tests.

Copy link
Member

Choose a reason for hiding this comment

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

@hasparus would you be able to make a PR fixing the docs for this?

Copy link
Contributor

Choose a reason for hiding this comment

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

yup, no biggie

locales: 'en-US', // Optional, defaults to system locale.
},
},
});
```

To sort your stories alphabetically, set `method` to `'alphabetical'` and optionally set the `locales` string. To sort your stories using a custom list, use the `sort` array; stories that don't match an item in the `sort` list will appear after the items in the list. 2nd

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

```js
import { addParameters, configure } from '@storybook/react';

addParameters({
options: {
storySort: {
sort: [
'Intro',
'Pages',
[
'Home',
'Login',
'Admin',
],
'Components',
],
},
},
});
```

Which would result in this story ordering:

1. `Intro` and then `Intro/*` stories
2. `Pages` story
3. `Pages/Home` and `Pages/Home/*` stories
4. `Pages/Login` and `Pages/Login/*` stories
5. `Pages/Admin` and `Pages/Admin/*` stories
6. `Pages/*` stories
7. `Components` and `Components/*` stories
8. All other stories

Note that the `order` option is independent of the `method` option; stories are sorted first by the `order` array and then by either the `method: 'alphabetical'` or the default `configure()` import order.

### Theming

For more information on configuring the `theme`, see [theming](../theming/).

### Per-story options
Expand Down
11 changes: 10 additions & 1 deletion lib/addons/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,17 @@ export interface WrapperSettings {
};
}

export type Comparator<T> = ((a: T, b: T) => boolean) | ((a: T, b: T) => number);
export type StorySortMethod = 'configure' | 'alphabetical';
export interface StorySortObjectParameter {
method?: StorySortMethod;
order?: any[];
locales?: string;
}
export type StorySortParameter = Comparator<any> | StorySortObjectParameter;

export interface OptionsParameter extends Object {
storySort?: any;
storySort?: StorySortParameter;
hierarchyRootSeparator?: string;
hierarchySeparator?: RegExp;
showRoots?: boolean;
Expand Down
76 changes: 76 additions & 0 deletions lib/client-api/src/storySort.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import storySort from './storySort';

describe('preview.storySort', () => {
const fixture = {
a: ['', { kind: 'a' }],
á: ['', { kind: 'á' }],
A: ['', { kind: 'A' }],
b: ['', { kind: 'b' }],
a_a: ['', { kind: 'a/a' }],
a_b: ['', { kind: 'a/b' }],
b_a_a: ['', { kind: 'b/a/a' }],
b_b: ['', { kind: 'b/b' }],
locale1: ['', { kind: 'Б' }],
locale2: ['', { kind: 'Г' }],
};

it('uses configure order by default', () => {
const sortFn = storySort();

expect(sortFn(fixture.a, fixture.b)).toBe(0);
expect(sortFn(fixture.b, fixture.a)).toBe(0);
expect(sortFn(fixture.a, fixture.a)).toBe(0);
});

it('can sort shallow kinds alphabetically', () => {
const sortFn = storySort({ method: 'alphabetical' });

expect(sortFn(fixture.a, fixture.b)).toBeLessThan(0);
expect(sortFn(fixture.b, fixture.a)).toBeGreaterThan(0);
expect(sortFn(fixture.a, fixture.á)).toBeLessThan(0);
expect(sortFn(fixture.á, fixture.a)).toBeGreaterThan(0);
});

it('can sort deep kinds alphabetically', () => {
const sortFn = storySort({ method: 'alphabetical' });

expect(sortFn(fixture.a_a, fixture.a_b)).toBeLessThan(0);
expect(sortFn(fixture.a_b, fixture.a_a)).toBeGreaterThan(0);
expect(sortFn(fixture.a_a, fixture.b)).toBeLessThan(0);
expect(sortFn(fixture.b, fixture.a_a)).toBeGreaterThan(0);
expect(sortFn(fixture.a_a, fixture.a)).toBeGreaterThan(0);
expect(sortFn(fixture.a, fixture.a_a)).toBeLessThan(0);
expect(sortFn(fixture.b_a_a, fixture.b_b)).toBeLessThan(0);
expect(sortFn(fixture.b_b, fixture.b_a_a)).toBeGreaterThan(0);
});

it('ignores case when sorting alphabetically', () => {
const sortFn = storySort({ method: 'alphabetical' });

expect(sortFn(fixture.a, fixture.A)).toBe(0);
expect(sortFn(fixture.A, fixture.a)).toBe(0);
});

it('sorts alphabetically using the given locales', () => {
const sortFn = storySort({ method: 'alphabetical', locales: 'ru-RU' });

expect(sortFn(fixture.locale1, fixture.locale2)).toBeLessThan(0);
expect(sortFn(fixture.locale2, fixture.locale1)).toBeGreaterThan(0);
});

it('sorts according to the order array', () => {
const sortFn = storySort({ order: ['b', 'c'] });

expect(sortFn(fixture.a, fixture.b)).toBeGreaterThan(0);
expect(sortFn(fixture.b, fixture.a)).toBeLessThan(0);
expect(sortFn(fixture.b_a_a, fixture.b_b)).toBe(0);
expect(sortFn(fixture.b_b, fixture.b_a_a)).toBe(0);
});

it('sorts according to the nested order array', () => {
const sortFn = storySort({ order: ['a', ['b', 'c'], 'c'] });

expect(sortFn(fixture.a_a, fixture.a_b)).toBeGreaterThan(0);
expect(sortFn(fixture.a_b, fixture.a_a)).toBeLessThan(0);
});
});
77 changes: 77 additions & 0 deletions lib/client-api/src/storySort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { StorySortObjectParameter, Comparator } from '@storybook/addons';

const storySort = (options: StorySortObjectParameter = {}): Comparator<any> => (
a: any,
b: any
): number => {
// If the two stories have the same story kind, then use the default
// ordering, which is the order they are defined in the story file.
if (a[1].kind === b[1].kind) {
return 0;
}

// Get the StorySortParameter options.
const method = options.method || 'configure';
let order = options.order || [];

// Examine each part of the story kind in turn.
const storyKindA = a[1].kind.split('/');
const storyKindB = b[1].kind.split('/');
let depth = 0;
while (storyKindA[depth] || storyKindB[depth]) {
// Stories with a shorter depth should go first.
if (!storyKindA[depth]) {
return -1;
}
if (!storyKindB[depth]) {
return 1;
}

// Compare the next part of the story kind.
const nameA = storyKindA[depth];
const nameB = storyKindB[depth];
if (nameA !== nameB) {
// Look for the names in the given `order` array.
let indexA = order.indexOf(nameA);
let indexB = order.indexOf(nameB);

// If at least one of the names is found, sort by the `order` array.
if (indexA !== -1 || indexB !== -1) {
// If one of the names is not found in `order`, list it last.
if (indexA === -1) {
indexA = order.length;
}
if (indexB === -1) {
indexB = order.length;
}

return indexA - indexB;
}

// Use the default configure() order.
if (method === 'configure') {
return 0;
}

// Otherwise, use alphabetical order.
return nameA.localeCompare(nameB, options.locales ? options.locales : undefined, {
numeric: true,
sensitivity: 'accent',
});
}

// If a nested array is provided for a name, use it for ordering.
const index = order.indexOf(nameA);
order = index !== -1 && Array.isArray(order[index + 1]) ? order[index + 1] : [];

// We'll need to look at the next part of the name.
depth += 1;
}

// Identical story kinds. The shortcut at the start of this function prevents
// this from ever being used.
/* istanbul ignore next */
return 0;
};

export default storySort;
100 changes: 98 additions & 2 deletions lib/client-api/src/story_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ describe('preview.story_store', () => {
it('sorts stories using given function', () => {
const parameters = {
options: {
// Test function does alphabetical ordering.
// Test function does reverse alphabetical ordering.
storySort: (a: any, b: any): number =>
a[1].kind === b[1].kind
? 0
: a[1].id.localeCompare(b[1].id, undefined, { numeric: true }),
: -1 * a[1].id.localeCompare(b[1].id, undefined, { numeric: true }),
},
};
const store = new StoryStore({ channel });
Expand All @@ -76,15 +76,111 @@ describe('preview.story_store', () => {
const extracted = store.extract();

expect(Object.keys(extracted)).toEqual([
'c--1',
'b-b10--1',
'b-b9--1',
'b-b1--1',
'a-b--1',
'a-a--1',
'a-a--2',
]);
});

it('sorts stories alphabetically', () => {
const parameters = {
options: {
storySort: {
method: 'alphabetical',
},
},
};
const store = new StoryStore({ channel });
store.addStory(...make('a/b', '1', () => 0, parameters));
store.addStory(...make('a/a', '2', () => 0, parameters));
store.addStory(...make('a/a', '1', () => 0, parameters));
store.addStory(...make('c', '1', () => 0, parameters));
store.addStory(...make('b/b10', '1', () => 0, parameters));
store.addStory(...make('b/b9', '1', () => 0, parameters));
store.addStory(...make('b/b1', '1', () => 0, parameters));

const extracted = store.extract();

expect(Object.keys(extracted)).toEqual([
'a-a--2',
'a-a--1',
'a-b--1',
'b-b1--1',
'b-b9--1',
'b-b10--1',
'c--1',
]);
});

it('sorts stories in specified order or alphabetically', () => {
const parameters = {
options: {
storySort: {
method: 'alphabetical',
order: ['b', ['bc', 'ba', 'bb'], 'a', 'c'],
},
},
};
const store = new StoryStore({ channel });
store.addStory(...make('a/b', '1', () => 0, parameters));
store.addStory(...make('a', '1', () => 0, parameters));
store.addStory(...make('c', '1', () => 0, parameters));
store.addStory(...make('b/bd', '1', () => 0, parameters));
store.addStory(...make('b/bb', '1', () => 0, parameters));
store.addStory(...make('b/ba', '1', () => 0, parameters));
store.addStory(...make('b/bc', '1', () => 0, parameters));
store.addStory(...make('b', '1', () => 0, parameters));

const extracted = store.extract();

expect(Object.keys(extracted)).toEqual([
'b--1',
'b-bc--1',
'b-ba--1',
'b-bb--1',
'b-bd--1',
'a--1',
'a-b--1',
'c--1',
]);
});

it('sorts stories in specified order or by configure order', () => {
const parameters = {
options: {
storySort: {
method: 'configure',
order: ['b', 'a', 'c'],
},
},
};
const store = new StoryStore({ channel });
store.addStory(...make('a/b', '1', () => 0, parameters));
store.addStory(...make('a', '1', () => 0, parameters));
store.addStory(...make('c', '1', () => 0, parameters));
store.addStory(...make('b/bd', '1', () => 0, parameters));
store.addStory(...make('b/bb', '1', () => 0, parameters));
store.addStory(...make('b/ba', '1', () => 0, parameters));
store.addStory(...make('b/bc', '1', () => 0, parameters));
store.addStory(...make('b', '1', () => 0, parameters));

const extracted = store.extract();

expect(Object.keys(extracted)).toEqual([
'b--1',
'b-bd--1',
'b-bb--1',
'b-ba--1',
'b-bc--1',
'a--1',
'a-b--1',
'c--1',
]);
});
});

describe('emitting behaviour', () => {
Expand Down
Loading