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

Core: Single story option in iframe view #14875

Merged
merged 11 commits into from
May 19, 2021
13 changes: 13 additions & 0 deletions lib/client-api/src/story_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,10 @@ export default class StoryStore {
this._selectionSpecifier = selectionSpecifier;
}

getSelectionSpecifier(): StoreSelectionSpecifier {
return this._selectionSpecifier;
}

setSelection(selection: StoreSelection): void {
this._selection = selection;

Expand All @@ -658,6 +662,15 @@ export default class StoryStore {
}
}

isSingleStoryMode(): boolean {
if (!this._selectionSpecifier) {
return false;
}

const { singleStory, storySpecifier } = this._selectionSpecifier;
return storySpecifier && storySpecifier !== '*' && singleStory;
}

getSelection = (): StoreSelection => this._selection;

getDataForManager = () => {
Expand Down
3 changes: 2 additions & 1 deletion lib/client-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export interface StoryMetadata {
}
export type ArgTypesEnhancer = (context: StoryContext) => ArgTypes;

type StorySpecifier = StoryId | { name: StoryName; kind: StoryKind } | '*';
export type StorySpecifier = StoryId | { name: StoryName; kind: StoryKind } | '*';

export interface StoreSelectionSpecifier {
storySpecifier: StorySpecifier;
viewMode: ViewMode;
singleStory: boolean;
args?: Args;
}

Expand Down
63 changes: 62 additions & 1 deletion lib/core-client/src/preview/loadCsf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ afterEach(() => {
doHMRDispose();
});

function makeMocks() {
function makeMocks(singleStory?:boolean, storyId?:string) {
const configApi = ({ configure: (x: Function) => x() } as unknown) as ConfigApi;
const storyStore = ({
isSingleStoryMode: jest.fn(() => !!singleStory),
getSelectionSpecifier: jest.fn(() => ({
storySpecifier: storyId || '',
singleStory: !!singleStory,
viewMode: 'story'
})),
removeStoryKind: jest.fn(),
} as unknown) as StoryStore;
const clientApi = ({
Expand Down Expand Up @@ -92,6 +98,36 @@ describe('core.preview.loadCsf', () => {
expect(bApi.add).toHaveBeenCalledWith('two', input.b[2], { __id: 'b--2', ...extras });
});

it('calls storiesOf and add correctly from CSF exports when singleStory mode is switched on', () => {
const { configure, clientApi } = makeMocks(true, 'a--1');

const input = {
a: {
default: {
title: 'a',
},
1: () => 0,
2: () => 0,
},
b: {
default: {
title: 'b',
},
1: () => 0,
2: Object.assign(() => 0, { storyName: 'two' }),
},
};
configure('react', makeRequireContext(input), mod);

const mockedStoriesOf = clientApi.storiesOf as jest.Mock;
expect(mockedStoriesOf).toHaveBeenCalledTimes(1);
expect(mockedStoriesOf).toHaveBeenCalledWith('a', true);
const aApi = mockedStoriesOf.mock.results[0].value;
const extras: any = { decorators: [], args: {}, argTypes: {}, loaders: [] };
expect(aApi.add).toHaveBeenCalledTimes(1);
expect(aApi.add).toHaveBeenCalledWith('1', input.a[1], { __id: 'a--1', ...extras });
});

it('adds stories in the right order if __namedExportsOrder is supplied', () => {
const { configure, clientApi } = makeMocks();

Expand Down Expand Up @@ -185,6 +221,31 @@ describe('core.preview.loadCsf', () => {
});
});

it('allows setting componentId when single mode is switch on', () => {
const { configure, clientApi } = makeMocks(true, 'random--x');

const input = {
a: {
default: {
title: 'a',
id: 'random',
},
x: () => 0,
},
};
configure('react', makeRequireContext(input), mod);

const mockedStoriesOf = clientApi.storiesOf as jest.Mock;
const aApi = mockedStoriesOf.mock.results[0].value;
expect(aApi.add).toHaveBeenCalledWith('X', input.a.x, {
__id: 'random--x',
decorators: [],
args: {},
argTypes: {},
loaders: [],
});
});

it('sets various parameters on components', () => {
const { configure, clientApi } = makeMocks();

Expand Down
53 changes: 49 additions & 4 deletions lib/core-client/src/preview/loadCsf.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ConfigApi, ClientApi, StoryStore } from '@storybook/client-api';
import { isExportStory, storyNameFromExport, toId } from '@storybook/csf';
import {ConfigApi, ClientApi, StoryStore, StorySpecifier} from '@storybook/client-api';
import { isExportStory, storyNameFromExport, toId, sanitize } from '@storybook/csf';
import { logger } from '@storybook/client-logger';
import dedent from 'ts-dedent';
import deprecate from 'util-deprecate';
Expand Down Expand Up @@ -27,6 +27,22 @@ const duplicateKindWarning = deprecate(
`
);

function extractSanitizedKindNameFromStorySpecifier(storySpecifier:StorySpecifier):string {
if (typeof storySpecifier === 'string') {
return storySpecifier.split('--').shift();
}

return sanitize(storySpecifier.kind);
}

function extractIdFromStorySpecifier(storySpecifier:StorySpecifier):string {
if (typeof storySpecifier === 'string') {
return storySpecifier
}

return toId(storySpecifier.kind, storySpecifier.name);
}

let previousExports = new Map<any, string>();
const loadStories = (
loadable: Loadable,
Expand Down Expand Up @@ -81,7 +97,23 @@ const loadStories = (
}
});

const added = Array.from(currentExports.keys()).filter((exp) => !previousExports.has(exp));
const added = Array.from(currentExports.keys()).filter((exp) => {
const isPreviousExport = previousExports.has(exp);
if (storyStore.isSingleStoryMode() && exp.default && exp.default.title) {
const sanitizedKindName = sanitize(exp.default.id || exp.default.title);
const selectionSpecifier = storyStore.getSelectionSpecifier();
if (
extractSanitizedKindNameFromStorySpecifier(selectionSpecifier.storySpecifier) ===
sanitizedKindName
) {
return !isPreviousExport;
}

return false;
}

return !storyStore.isSingleStoryMode() && !isPreviousExport;
});

added.forEach((fileExports) => {
// An old-style story file
Expand Down Expand Up @@ -151,7 +183,7 @@ const loadStories = (
kind.addLoader(loader);
});

const storyExports = Object.keys(exports);
let storyExports = Object.keys(exports);
if (storyExports.length === 0) {
logger.warn(
dedent`
Expand All @@ -162,6 +194,19 @@ const loadStories = (
return;
}

if (storyStore.isSingleStoryMode()) {
const selectionSpecifier = storyStore.getSelectionSpecifier();
storyExports = storyExports.filter((key) => {
const exportName = storyNameFromExport(key);
const id = toId(componentId || kindName, exportName);
return id === extractIdFromStorySpecifier(selectionSpecifier.storySpecifier);
});

if (storyExports.length === 0) {
return;
}
}

storyExports.forEach((key) => {
if (isExportStory(key, meta)) {
const storyFn = exports[key];
Expand Down
12 changes: 12 additions & 0 deletions lib/core-client/src/preview/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,29 +73,41 @@ describe('url', () => {
expect(getSelectionSpecifierFromPath()).toEqual({
storySpecifier: 'story--id',
viewMode: 'story',
singleStory: false,
});
});
it('should handle id queries with *', () => {
document.location.search = '?id=*';
expect(getSelectionSpecifierFromPath()).toEqual({
storySpecifier: '*',
viewMode: 'story',
singleStory: false,
});
});
it('should redirect legacy queries', () => {
document.location.search = '?selectedKind=kind&selectedStory=story';
expect(getSelectionSpecifierFromPath()).toEqual({
storySpecifier: { kind: 'kind', name: 'story' },
viewMode: 'story',
singleStory: false,
});
});
it('should parse args', () => {
document.location.search = '?id=story--id&args=obj.key:val';
expect(getSelectionSpecifierFromPath()).toEqual({
storySpecifier: 'story--id',
viewMode: 'story',
singleStory: false,
args: { obj: { key: 'val' } },
});
});
it('should handle singleStory param', () => {
document.location.search = '?id=abc&singleStory=true';
expect(getSelectionSpecifierFromPath()).toEqual({
storySpecifier: 'abc',
viewMode: 'story',
singleStory: true,
});
});
});
});
5 changes: 3 additions & 2 deletions lib/core-client/src/preview/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ export const getSelectionSpecifierFromPath: () => StoreSelectionSpecifier = () =
viewMode = 'story';
}

const singleStory = getFirstString(query.singleStory) === 'true';
const path = getFirstString(query.path);
const storyId = path ? pathToId(path) : getFirstString(query.id);

if (storyId) {
return { storySpecifier: storyId, args, viewMode };
return { storySpecifier: storyId, args, viewMode, singleStory };
}

// Legacy URL format
Expand All @@ -85,7 +86,7 @@ export const getSelectionSpecifierFromPath: () => StoreSelectionSpecifier = () =

if (kind && name) {
deprecatedLegacyQuery();
return { storySpecifier: { kind, name }, args, viewMode };
return { storySpecifier: { kind, name }, args, viewMode, singleStory };
}
return null;
};