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

Docs: Allow ArgTable usage unattached #21371

Merged
merged 7 commits into from
Mar 5, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ describe('resolveOf', () => {
const { story, csfFile, storyExport, metaExport, moduleExports, component } = csfFileParts();

describe('attached', () => {
const projectAnnotations = { render: jest.fn() };
const store = {
componentStoriesFromCSFFile: () => [story],
projectAnnotations,
} as unknown as StoryStore<Renderer>;
const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]);
context.attachCSFFile(csfFile);
Expand All @@ -47,27 +49,47 @@ describe('resolveOf', () => {
});

it('works for meta exports', () => {
expect(context.resolveOf(metaExport)).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf(metaExport)).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('works for full module exports', () => {
expect(context.resolveOf(moduleExports)).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf(moduleExports)).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('works for components', () => {
expect(context.resolveOf(component)).toEqual({ type: 'component', component });
expect(context.resolveOf(component)).toEqual({
type: 'component',
component,
projectAnnotations: expect.objectContaining(projectAnnotations),
});
});

it('finds primary story', () => {
expect(context.resolveOf('story')).toEqual({ type: 'story', story });
});

it('finds attached CSF file', () => {
expect(context.resolveOf('meta')).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf('meta')).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('finds attached component', () => {
expect(context.resolveOf('component')).toEqual({ type: 'component', component });
expect(context.resolveOf('component')).toEqual({
type: 'component',
component,
projectAnnotations: expect.objectContaining(projectAnnotations),
});
});

describe('validation allowed', () => {
Expand All @@ -76,17 +98,26 @@ describe('resolveOf', () => {
});

it('works for meta exports', () => {
expect(context.resolveOf(metaExport, ['meta'])).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf(metaExport, ['meta'])).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('works for full module exports', () => {
expect(context.resolveOf(moduleExports, ['meta'])).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf(moduleExports, ['meta'])).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('works for components', () => {
expect(context.resolveOf(component, ['component'])).toEqual({
type: 'component',
component,
projectAnnotations: expect.objectContaining(projectAnnotations),
});
});

Expand All @@ -95,13 +126,18 @@ describe('resolveOf', () => {
});

it('finds attached CSF file', () => {
expect(context.resolveOf('meta', ['meta'])).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf('meta', ['meta'])).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('finds attached component', () => {
expect(context.resolveOf('component', ['component'])).toEqual({
type: 'component',
component,
projectAnnotations: expect.objectContaining(projectAnnotations),
});
});
});
Expand Down Expand Up @@ -140,8 +176,10 @@ describe('resolveOf', () => {
});

describe('unattached', () => {
const projectAnnotations = { render: jest.fn() };
const store = {
componentStoriesFromCSFFile: () => [story],
projectAnnotations,
} as unknown as StoryStore<Renderer>;
const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]);

Expand All @@ -150,15 +188,27 @@ describe('resolveOf', () => {
});

it('works for meta exports', () => {
expect(context.resolveOf(metaExport)).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf(metaExport)).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('works for full module exports', () => {
expect(context.resolveOf(moduleExports)).toEqual({ type: 'meta', csfFile });
expect(context.resolveOf(moduleExports)).toEqual({
type: 'meta',
csfFile,
preparedMeta: expect.any(Object),
});
});

it('works for components', () => {
expect(context.resolveOf(component)).toEqual({ type: 'component', component });
expect(context.resolveOf(component)).toEqual({
type: 'component',
component,
projectAnnotations: expect.objectContaining(projectAnnotations),
});
});

it('throws for primary story', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
import type { Channel } from '@storybook/channels';

import type { StoryStore } from '../../store';
import { prepareMeta } from '../../store';
import type { DocsContextProps } from './DocsContextProps';

export class DocsContext<TRenderer extends Renderer> implements DocsContextProps<TRenderer> {
Expand Down Expand Up @@ -165,7 +166,29 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
)}`
);
}
return resolved;

switch (resolved.type) {
case 'component': {
return {
...resolved,
projectAnnotations: this.projectAnnotations,
};
}
case 'meta': {
return {
...resolved,
preparedMeta: prepareMeta(
resolved.csfFile.meta,
this.projectAnnotations,
resolved.csfFile.moduleExports.default
),
};
}
case 'story':
default: {
return resolved;
}
}
}

storyIdByName = (storyName: StoryName) => {
Expand Down
22 changes: 14 additions & 8 deletions code/lib/types/src/modules/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
PreparedStory,
NormalizedProjectAnnotations,
RenderContext,
PreparedMeta,
} from './story';

export type RenderContextCallbacks<TRenderer extends Renderer> = Pick<
Expand All @@ -21,14 +22,6 @@ export type StoryRenderOptions = {

export type ResolvedModuleExportType = 'component' | 'meta' | 'story';

export type ResolvedModuleExportFromType<
TType extends ResolvedModuleExportType,
TRenderer extends Renderer = Renderer
> = TType extends 'component'
? { type: 'component'; component: TRenderer['component'] }
: TType extends 'meta'
? { type: 'meta'; csfFile: CSFFile<TRenderer> }
: { type: 'story'; story: PreparedStory<TRenderer> };
/**
* What do we know about an of={} call?
*
Expand All @@ -37,6 +30,19 @@ export type ResolvedModuleExportFromType<
* - story === `PreparedStory`
* But these shorthands capture the idea of what is being talked about
*/
export type ResolvedModuleExportFromType<
TType extends ResolvedModuleExportType,
TRenderer extends Renderer = Renderer
> = TType extends 'component'
? {
type: 'component';
component: TRenderer['component'];
projectAnnotations: NormalizedProjectAnnotations<Renderer>;
}
: TType extends 'meta'
? { type: 'meta'; csfFile: CSFFile<TRenderer>; preparedMeta: PreparedMeta }
: { type: 'story'; story: PreparedStory<TRenderer> };

export type ResolvedModuleExport<TRenderer extends Renderer = Renderer> = {
type: ResolvedModuleExportType;
} & (
Expand Down
30 changes: 18 additions & 12 deletions code/ui/blocks/src/blocks/ArgsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import mapValues from 'lodash/mapValues.js';
import type { ArgTypesExtractor } from '@storybook/docs-tools';
import type { PropDescriptor } from '@storybook/preview-api';
import { filterArgTypes } from '@storybook/preview-api';
import type { StrictArgTypes, Args, Globals } from '@storybook/types';
import type { StrictArgTypes, Args, Globals, Parameters } from '@storybook/types';
import {
STORY_ARGS_UPDATED,
UPDATE_STORY_ARGS,
Expand Down Expand Up @@ -90,11 +90,10 @@ const useGlobals = (context: DocsContextProps): [Globals] => {

export const extractComponentArgTypes = (
component: Component,
context: DocsContextProps,
parameters: Parameters,
include?: PropDescriptor,
exclude?: PropDescriptor
): StrictArgTypes => {
const { parameters } = context.storyById();
const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = parameters.docs || {};
if (!extractArgTypes) {
throw new Error(ArgsTableError.ARGS_UNSUPPORTED);
Expand All @@ -109,10 +108,9 @@ const isShortcut = (value?: string) => {
return value && [PRIMARY_STORY].includes(value);
};

export const getComponent = (props: ArgsTableProps = {}, context: DocsContextProps): Component => {
export const getComponent = (props: ArgsTableProps = {}, component: Component): Component => {
const { of } = props as OfProps;
const { story } = props as StoryProps;
const { component } = context.storyById();
if (isShortcut(of) || isShortcut(story)) {
return component || null;
}
Expand Down Expand Up @@ -220,25 +218,33 @@ export const ArgsTable: FC<ArgsTableProps> = (props) => {
Please refer to the migration guide: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#argstable-block
`);
const context = useContext(DocsContext);
const {
parameters: { controls },
subcomponents,
} = context.storyById();

let parameters: Parameters;
let component: any;
let subcomponents: Record<string, any>;
try {
({ parameters, component, subcomponents } = context.storyById());
} catch (err) {
const { of } = props as OfProps;
({
projectAnnotations: { parameters },
} = context.resolveOf(of, ['component']));
}

const { include, exclude, components, sort: sortProp } = props as ComponentsProps;
const { story: storyName } = props as StoryProps;

const sort = sortProp || controls?.sort;
const sort = sortProp || parameters.controls?.sort;

const main = getComponent(props, context);
const main = getComponent(props, component);
if (storyName) {
return <StoryTable {...(props as StoryProps)} component={main} {...{ subcomponents, sort }} />;
}

if (!components && !subcomponents) {
let mainProps;
try {
mainProps = { rows: extractComponentArgTypes(main, context, include, exclude) };
mainProps = { rows: extractComponentArgTypes(main, parameters, include, exclude) };
} catch (err) {
mainProps = { error: err.message };
}
Expand Down
4 changes: 2 additions & 2 deletions code/ui/blocks/src/blocks/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable react/destructuring-assignment */
import React, { Children, useContext } from 'react';
import type { FC, ReactElement, ReactNode } from 'react';
import type { ModuleExport, ModuleExports, Renderer } from '@storybook/types';
import type { ModuleExport, ModuleExports, PreparedStory, Renderer } from '@storybook/types';
import { deprecate } from '@storybook/client-logger';
import dedent from 'ts-dedent';
import type { Layout, PreviewProps as PurePreviewProps } from '../components';
Expand Down Expand Up @@ -157,7 +157,7 @@ export const Canvas: FC<CanvasProps & DeprecatedCanvasProps> = (props) => {
const { children, of, source } = props;
const { isLoading, previewProps } = useDeprecatedPreviewProps(props, docsContext, sourceContext);

let story;
let story: PreparedStory;
let sourceProps;
/**
* useOf and useSourceProps will throw if they can't find the story, in the scenario where
Expand Down
49 changes: 4 additions & 45 deletions code/ui/blocks/src/blocks/useOf.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,24 @@
import type {
DocsContextProps,
ModuleExport,
NormalizedProjectAnnotations,
PreparedMeta,
Renderer,
ResolvedModuleExportFromType,
ResolvedModuleExportType,
ResolvedModuleExportFromType,
} from '@storybook/types';
import { prepareMeta } from '@storybook/preview-api';
import { useContext } from 'react';
import { DocsContext } from './DocsContext';

export type Of = Parameters<DocsContextProps['resolveOf']>[0];

export type EnhancedResolvedModuleExportType<
TType extends ResolvedModuleExportType,
TRenderer extends Renderer = Renderer
> = TType extends 'component'
? ResolvedModuleExportFromType<TType, TRenderer> & {
projectAnnotations: NormalizedProjectAnnotations<Renderer>;
}
: TType extends 'meta'
? ResolvedModuleExportFromType<TType, TRenderer> & { preparedMeta: PreparedMeta }
: ResolvedModuleExportFromType<TType, TRenderer>;

/**
* A hook to resolve the `of` prop passed to a block.
* will return the resolved module
* if the resolved module is a meta it will include a preparedMeta property similar to a preparedStory
* if the resolved module is a component it will include the project annotations
*/
export const useOf = <
TType extends ResolvedModuleExportType,
TRenderer extends Renderer = Renderer
>(
export const useOf = <TType extends ResolvedModuleExportType>(
moduleExportOrType: ModuleExport | TType,
validTypes?: TType[]
): EnhancedResolvedModuleExportType<TType, TRenderer> => {
): ResolvedModuleExportFromType<TType> => {
const context = useContext(DocsContext);
const resolved = context.resolveOf(moduleExportOrType, validTypes);

switch (resolved.type) {
case 'component': {
return {
...resolved,
projectAnnotations: context.projectAnnotations,
} as EnhancedResolvedModuleExportType<TType, TRenderer>;
}
case 'meta': {
return {
...resolved,
preparedMeta: prepareMeta(
resolved.csfFile.meta,
context.projectAnnotations,
resolved.csfFile.moduleExports.default
),
} as EnhancedResolvedModuleExportType<TType, TRenderer>;
}
case 'story':
default: {
return resolved as EnhancedResolvedModuleExportType<TType, TRenderer>;
}
}
return context.resolveOf(moduleExportOrType, validTypes);
};