Skip to content

Commit

Permalink
Merge pull request #21371 from storybookjs/tom/21366-fix-unattached-a…
Browse files Browse the repository at this point in the history
…rgsTable

Docs: Allow ArgTable usage unattached
  • Loading branch information
tmeasday authored Mar 5, 2023
2 parents 0ae21db + 47efd80 commit 29a33ed
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 79 deletions.
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);
};

0 comments on commit 29a33ed

Please sign in to comment.