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

Allow custom argType sorting #28565

Draft
wants to merge 3 commits into
base: next
Choose a base branch
from
Draft
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
9 changes: 7 additions & 2 deletions code/addons/controls/src/ControlsPanel.tsx
Copy link
Author

Choose a reason for hiding this comment

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

question: @shilman how is it possible to somehow access inside ControlsPanel a custom provided function? (I mean one that wont be a string like '(a,b)=>a.compare(b)', and then put that into eval(myFn), because this would be hilarious 😆)

Copy link
Author

Choose a reason for hiding this comment

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

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
useParameter,
useStorybookState,
} from 'storybook/internal/manager-api';
import { PureArgsTable as ArgsTable, type PresetColor, type SortType } from '@storybook/blocks';
import {
PureArgsTable as ArgsTable,
type PresetColor,
type SortFn,
type SortType,
} from '@storybook/blocks';
import { styled } from 'storybook/internal/theming';
import type { ArgTypes } from 'storybook/internal/types';

Expand All @@ -31,7 +36,7 @@ const AddonWrapper = styled.div({
});

interface ControlsParameters {
sort?: SortType;
sort?: SortType | SortFn;
expanded?: boolean;
presetColors?: PresetColor[];
}
Expand Down
11 changes: 11 additions & 0 deletions code/addons/controls/template/stories/sorting.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,14 @@ export const None = { parameters: { controls: { sort: 'none' } } };
export const Alpha = { parameters: { controls: { sort: 'alpha' } } };

export const RequiredFirst = { parameters: { controls: { sort: 'requiredFirst' } } };

export const CustomSort = {
parameters: {
controls: {
sort: (a, b) =>
a.table?.category?.localeCompare(b.table?.category) ||
(a.type?.required === b.type?.required ? 0 : a.type?.required ? 1 : -1) ||
0,
},
Comment on lines +37 to +42
Copy link
Author

Choose a reason for hiding this comment

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

question (blocking): @kylegach @jonniebigodes just asking you both because GitHub suggests you both
image

I tested to pass a callback function here, however const { expanded, sort, presetColors } = useParameter<ControlsParameters>(PARAM_KEY, {}); then returns undefined for sort.
I also tried to pass e.g. 'custom' as string, and then it successfully pass through as is. My assumption is that somewhere runtime-functions are not serializable and results in undefined.
Is someone knowing what that could be and point me in the right direction?

Copy link
Member

Choose a reason for hiding this comment

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

@Shinigami92 parameters are loaded into Storybook's "preview" (the inner iframe that renders the stories). They are then serialized across a channel into the "manager" (the outer iframe that's Storybook's actual UI) using a library called telejson.

Historically, telejson serialized pure functions by default. But then we started getting complaints about the use of eval, which is forbidden in some companies. Now function serialization is opt-in using channelOptions.allowFunction. However, I think we might even remove that in SB9 because the eval code still exists in telejson which triggers some security checks.

If you want a function to be available in the server, your best bet is to have users set it in .storybook/manager.js, see the renderLabel example (which strangely doesn't seem to be documented anywhere else @kylegach @jonniebigodes). The manager APIs are sorely underdeveloped (something that @ndelangen wants to do something about, but which is currently lower priority) and in your case I think you really want the function to be available in both the manager AND the preview and we really don't have a great solution for that.

I'm open to suggestions for better ways to approach this -- it's definitely an area that could use work. We're talking about some architectural changes next year that could solve this in a very different way. But nothing in the short term.

Copy link
Author

Choose a reason for hiding this comment

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

@shilman OH 😲 this makes a lot of sense!
As you said, I could try to allow to register a function in manager and then just pass the registered function-name as string. And then see if that can be used.
If not, then I might need to wait until SB-9.

Copy link
Member

Choose a reason for hiding this comment

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

@ndelangen another good use case for the annotation server

},
};
4 changes: 2 additions & 2 deletions code/lib/blocks/src/blocks/ArgTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { filterArgTypes } from 'storybook/internal/preview-api';
import type { ArgTypesExtractor } from 'storybook/internal/docs-tools';
import React from 'react';

import type { SortType } from '../components';
import type { SortFn, SortType } from '../components';
import { ArgsTable as PureArgsTable, ArgsTableError, TabbedArgsTable } from '../components';
import { useOf } from './useOf';
import { getComponentName } from './utils';

type ArgTypesParameters = {
include?: PropDescriptor;
exclude?: PropDescriptor;
sort?: SortType;
sort?: SortType | SortFn;
};

type ArgTypesProps = ArgTypesParameters & {
Expand Down
4 changes: 2 additions & 2 deletions code/lib/blocks/src/blocks/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { filterArgTypes } from 'storybook/internal/preview-api';
import type { PropDescriptor } from 'storybook/internal/preview-api';
import type { ArgTypesExtractor } from 'storybook/internal/docs-tools';

import type { SortType } from '../components';
import type { SortFn, SortType } from '../components';
import { ArgsTable as PureArgsTable, ArgsTableError, TabbedArgsTable } from '../components';
import { DocsContext } from './DocsContext';
import { useGlobals } from './useGlobals';
Expand All @@ -17,7 +17,7 @@ import { getComponentName } from './utils';
type ControlsParameters = {
include?: PropDescriptor;
exclude?: PropDescriptor;
sort?: SortType;
sort?: SortType | SortFn;
};

type ControlsProps = ControlsParameters & {
Expand Down
8 changes: 4 additions & 4 deletions code/lib/blocks/src/components/ArgsTable/ArgsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export enum ArgsTableError {
}

export type SortType = 'alpha' | 'requiredFirst' | 'none';
type SortFn = (a: ArgType, b: ArgType) => number;
export type SortFn = (a: ArgType, b: ArgType) => number;

const sortFns: Record<SortType, SortFn | null> = {
alpha: (a: ArgType, b: ArgType) => a.name.localeCompare(b.name),
Expand All @@ -197,7 +197,7 @@ export interface ArgsTableOptionProps {
inAddonPanel?: boolean;
initialExpandedArgs?: boolean;
isLoading?: boolean;
sort?: SortType;
sort?: SortType | SortFn;
}
interface ArgsTableDataProps {
rows: ArgTypes;
Expand Down Expand Up @@ -228,7 +228,7 @@ type Sections = {
sections: Record<string, Section>;
};

const groupRows = (rows: ArgType, sort: SortType) => {
const groupRows = (rows: ArgType, sort: SortType | SortFn) => {
const sections: Sections = { ungrouped: [], ungroupedSubsections: {}, sections: {} };
if (!rows) return sections;

Expand All @@ -254,7 +254,7 @@ const groupRows = (rows: ArgType, sort: SortType) => {
});

// apply sort
const sortFn = sortFns[sort];
const sortFn = typeof sort === 'function' ? sort : sortFns[sort];

const sortSubsection = (record: Record<string, Subsection>) => {
if (!sortFn) return record;
Expand Down
14 changes: 14 additions & 0 deletions code/lib/blocks/src/examples/ArgTypesParameters.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ export const Sort: Story = {
parameters: { docs: { argTypes: { sort: 'alpha' } } },
};

export const SortCustom: Story = {
...NoParameters,
parameters: {
docs: {
argTypes: {
sort: (a, b) =>
a.table?.category?.localeCompare(b.table?.category) ||
(a.type?.required === b.type?.required ? 0 : a.type?.required ? 1 : -1) ||
0,
},
},
},
};

export const Categories: Story = {
...NoParameters,
argTypes: {
Expand Down
14 changes: 14 additions & 0 deletions code/lib/blocks/src/examples/ControlsParameters.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ export const Sort: Story = {
parameters: { docs: { controls: { sort: 'alpha' } } },
};

export const SortCustom: Story = {
...NoParameters,
parameters: {
docs: {
controls: {
sort: (a, b) =>
a.table?.category?.localeCompare(b.table?.category) ||
(a.type?.required === b.type?.required ? 0 : a.type?.required ? 1 : -1) ||
0,
},
},
},
};

export const Categories: Story = {
...NoParameters,
argTypes: {
Expand Down
2 changes: 1 addition & 1 deletion code/lib/blocks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// FIXME: sort this out, maybe with package.json exports map
// https://medium.com/swlh/npm-new-package-json-exports-field-1a7d1f489ccf
export { ArgsTable as PureArgsTable } from './components';
export type { SortType } from './components';
export type { SortFn, SortType } from './components';

export * from './blocks';
export * from './controls';
3 changes: 2 additions & 1 deletion docs/api/doc-blocks/doc-block-argtypes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Specifies which story to get the arg types from. If a CSF file exports is provid

### `sort`

Type: `'none' | 'alpha' | 'requiredFirst'`
Type: `'none' | 'alpha' | 'requiredFirst' | SortFn`

Default: `parameters.docs.argTypes.sort` or `'none'`

Expand All @@ -95,3 +95,4 @@ Specifies how the arg types are sorted.
* **none**: Unsorted, displayed in the same order the arg types are processed in
* **alpha**: Sorted alphabetically, by the arg type's name
* **requiredFirst**: Same as `alpha`, with any required arg types displayed first
* **SortFn**: A callback to define your own sorting logic
3 changes: 2 additions & 1 deletion docs/api/doc-blocks/doc-block-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Specifies which story to get the controls from. If a CSF file exports is provide

### `sort`

Type: `'none' | 'alpha' | 'requiredFirst'`
Type: `'none' | 'alpha' | 'requiredFirst' | SortFn`

Default: `parameters.docs.controls.sort` or `'none'`

Expand All @@ -109,3 +109,4 @@ Specifies how the controls are sorted.
* **none**: Unsorted, displayed in the same order the controls are processed in
* **alpha**: Sorted alphabetically, by the arg type's name
* **requiredFirst**: Same as `alpha`, with any required controls displayed first
* **SortFn**: A callback to define your own sorting logic
3 changes: 2 additions & 1 deletion docs/essentials/controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ Specify preset color swatches for the color picker control. The color value may

#### `sort`

Type: `'none' | 'alpha' | 'requiredFirst'`
Type: `'none' | 'alpha' | 'requiredFirst' | SortFn`

Default: `'none'`

Expand All @@ -484,3 +484,4 @@ Specifies how the controls are sorted.
* **none**: Unsorted, displayed in the same order the arg types are processed in
* **alpha**: Sorted alphabetically, by the arg type's name
* **requiredFirst**: Same as `alpha`, with any required arg types displayed first
* **SortFn**: A callback to define your own sorting logic