Replace session-storage based globals handling with url param

This commit is contained in:
Gert Hengeveld 2021-05-26 20:00:30 +02:00
parent ac522bbc83
commit 7dc469ce2e
5 changed files with 80 additions and 53 deletions

View File

@ -1,5 +1,10 @@
import { navigate as navigateRouter, NavigateOptions } from '@reach/router';
import { NAVIGATE_URL, STORY_ARGS_UPDATED, SET_CURRENT_STORY } from '@storybook/core-events';
import {
NAVIGATE_URL,
STORY_ARGS_UPDATED,
SET_CURRENT_STORY,
GLOBALS_UPDATED,
} from '@storybook/core-events';
import { queryFromLocation, navigate as queryNavigate, buildArgsParam } from '@storybook/router';
import { toId, sanitize } from '@storybook/csf';
import deepEqual from 'fast-deep-equal';
@ -168,7 +173,7 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r
fullAPI.on(SET_CURRENT_STORY, () => updateArgsParam());
let handleOrId: any;
fullAPI.on(STORY_ARGS_UPDATED, ({ args }) => {
fullAPI.on(STORY_ARGS_UPDATED, () => {
if ('requestIdleCallback' in globalWindow) {
if (handleOrId) globalWindow.cancelIdleCallback(handleOrId);
handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 });
@ -178,6 +183,14 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r
}
});
fullAPI.on(GLOBALS_UPDATED, ({ globals, defaultGlobals, initialGlobals }) => {
const { path } = fullAPI.getUrlState();
const argsString = buildArgsParam({ ...defaultGlobals, ...initialGlobals }, globals);
const globalsParam = argsString.length ? `&globals=${argsString}` : '';
queryNavigate(`${path}${globalsParam}`, { replace: true });
api.setQueryParams({ globals: argsString });
});
fullAPI.on(NAVIGATE_URL, (url: string, options: { [k: string]: any }) => {
fullAPI.navigateUrl(url, options);
});

View File

@ -3,7 +3,6 @@ import createChannel from '@storybook/channel-postmessage';
import { toId } from '@storybook/csf';
import addons, { mockChannel } from '@storybook/addons';
import Events from '@storybook/core-events';
import store2 from 'store2';
import StoryStore from './story_store';
import { defaultDecorateStory } from './decorators';
@ -16,8 +15,6 @@ jest.mock('@storybook/node-logger', () => ({
},
}));
jest.mock('store2');
let channel;
beforeEach(() => {
channel = createChannel({ page: 'preview' });
@ -433,14 +430,6 @@ describe('preview.story_store', () => {
});
});
it('sets session storage on initialization', () => {
(store2.session.set as any).mockClear();
const store = new StoryStore({ channel });
addStoryToStore(store, 'a', '1', () => 0);
store.finishConfiguring();
expect(store2.session.set).toHaveBeenCalled();
});
it('on HMR it sensibly re-initializes with memory', () => {
const store = new StoryStore({ channel });
addons.setChannel(channel);
@ -509,16 +498,17 @@ describe('preview.story_store', () => {
});
it('sensibly re-initializes with memory based on session storage', () => {
(store2.session.get as any).mockReturnValueOnce({
globals: {
const store = new StoryStore({ channel });
store.setSelectionSpecifier({
storySpecifier: '*',
viewMode: 'story',
globalArgs: {
arg1: 'arg1',
arg2: 2,
arg3: { complex: { object: ['type'] } },
arg4: 4,
},
});
const store = new StoryStore({ channel });
addons.setChannel(channel);
addStoryToStore(store, 'a', '1', () => 0);
@ -541,6 +531,7 @@ describe('preview.story_store', () => {
expect(store.getRawStory('a', '1').globals).toEqual({
// We should keep the previous values because we cannot tell if the user changed it or not in the UI
// and we don't want to revert to the defaults every HMR
// arg1 is missing because it's not one of allowedGlobals
arg2: 2,
arg3: { complex: { object: ['type'] } },
arg4: 4,
@ -561,15 +552,6 @@ describe('preview.story_store', () => {
expect(store.getRawStory('a', '1').globals).toEqual({ foo: 'bar', baz: 'bing' });
});
it('updateGlobals sets session storage', () => {
const store = new StoryStore({ channel });
addStoryToStore(store, 'a', '1', () => 0);
(store2.session.set as any).mockClear();
store.updateGlobals({ foo: 'bar' });
expect(store2.session.set).toHaveBeenCalled();
});
it('is passed to the story in the context', () => {
const storyFn = jest.fn();
const store = new StoryStore({ channel });
@ -601,13 +583,29 @@ describe('preview.story_store', () => {
const store = new StoryStore({ channel: testChannel });
addStoryToStore(store, 'a', '1', () => 0);
store.addGlobalMetadata({
parameters: {
globalTypes: {
foo: { defaultValue: 'Foo' },
bar: { defaultValue: 'Bar' },
},
globals: { baz: 'Baz', qux: 'Qux' },
},
});
store.finishConfiguring();
store.updateGlobals({ foo: 'bar' });
expect(onGlobalsChangedChannel).toHaveBeenCalledWith({ globals: { foo: 'bar' } });
store.updateGlobals({ baz: 'bing' });
store.updateGlobals({ foo: 'FUD' });
expect(onGlobalsChangedChannel).toHaveBeenCalledWith({
globals: { foo: 'bar', baz: 'bing' },
globals: { foo: 'FUD', bar: 'Bar', baz: 'Baz', qux: 'Qux' },
defaultGlobals: { foo: 'Foo', bar: 'Bar' },
initialGlobals: { baz: 'Baz', qux: 'Qux' },
});
store.updateGlobals({ baz: 'BING' });
expect(onGlobalsChangedChannel).toHaveBeenCalledWith({
globals: { foo: 'FUD', bar: 'Bar', baz: 'BING', qux: 'Qux' },
defaultGlobals: { foo: 'Foo', bar: 'Bar' },
initialGlobals: { baz: 'Baz', qux: 'Qux' },
});
});

View File

@ -4,7 +4,6 @@ import dedent from 'ts-dedent';
import stable from 'stable';
import mapValues from 'lodash/mapValues';
import pick from 'lodash/pick';
import store from 'store2';
import deprecate from 'util-deprecate';
import { Channel } from '@storybook/channels';
@ -50,8 +49,6 @@ interface StoryOptions {
type KindMetadata = StoryMetadata & { order: number };
const STORAGE_KEY = '@storybook/preview/store';
function extractSanitizedKindNameFromStorySpecifier(storySpecifier: StorySpecifier): string {
if (typeof storySpecifier === 'string') {
return storySpecifier.split('--').shift();
@ -143,6 +140,10 @@ export default class StoryStore {
_globals: Args;
_initialGlobals: Args;
_defaultGlobals: Args;
_globalMetadata: StoryMetadata;
// Keyed on kind name
@ -162,10 +163,9 @@ export default class StoryStore {
constructor(params: { channel: Channel }) {
// Assume we are configuring until we hear otherwise
this._configuring = true;
// We store global args in session storage. Note that when we finish
// configuring below we will ensure we only use values here that make sense
this._globals = store.session.get(STORAGE_KEY)?.globals || {};
this._globals = {};
this._defaultGlobals = {};
this._initialGlobals = {};
this._globalMetadata = { parameters: {}, decorators: [], loaders: [] };
this._kinds = {};
this._stories = {};
@ -213,24 +213,23 @@ export default class StoryStore {
safePush(inferControls, this._argTypesEnhancers);
}
storeGlobals() {
// Store the global args on the session
store.session.set(STORAGE_KEY, { globals: this._globals });
}
finishConfiguring() {
this._configuring = false;
const { globals: initialGlobals = {}, globalTypes = {} } = this._globalMetadata.parameters;
const { globals = {}, globalTypes = {} } = this._globalMetadata.parameters;
const defaultGlobals: Args = Object.entries(
this._initialGlobals = globals;
this._defaultGlobals = Object.entries(
globalTypes as Record<string, { defaultValue: any }>
).reduce((acc, [arg, { defaultValue }]) => {
if (defaultValue) acc[arg] = defaultValue;
return acc;
}, {} as Args);
const allowedGlobals = new Set([...Object.keys(initialGlobals), ...Object.keys(globalTypes)]);
const allowedGlobals = new Set([
...Object.keys(this._initialGlobals),
...Object.keys(globalTypes),
]);
// To deal with HMR & persistence, we consider the previous value of global args, and:
// 1. Remove any keys that are not in the new parameter
@ -242,15 +241,27 @@ export default class StoryStore {
return acc;
},
{ ...defaultGlobals, ...initialGlobals }
{ ...this._defaultGlobals, ...this._initialGlobals }
);
this.storeGlobals();
// Set the current selection based on the current selection specifier, if selection is not yet set
const stories = this.sortedStories();
let foundStory;
if (this._selectionSpecifier && !this._selection) {
const { storySpecifier, viewMode, args: urlArgs } = this._selectionSpecifier;
const {
storySpecifier,
viewMode,
args: urlArgs,
globalArgs: urlGlobals,
} = this._selectionSpecifier;
if (urlGlobals) {
const allowedUrlGlobals = Object.entries(urlGlobals).reduce((acc, [key, value]) => {
if (allowedGlobals.has(key)) acc[key] = value;
return acc;
}, {} as Args);
this._globals = combineParameters(this._globals, allowedUrlGlobals);
}
if (storySpecifier === '*') {
// '*' means select the first story. If there is none, we have no selection.
@ -614,8 +625,11 @@ export default class StoryStore {
updateGlobals(newGlobals: Args) {
this._globals = { ...this._globals, ...newGlobals };
this.storeGlobals();
this._channel.emit(Events.GLOBALS_UPDATED, { globals: this._globals });
this._channel.emit(Events.GLOBALS_UPDATED, {
globals: this._globals,
defaultGlobals: this._defaultGlobals,
initialGlobals: this._initialGlobals,
});
}
updateStoryArgs(id: string, newArgs: Args) {

View File

@ -39,6 +39,7 @@ export interface StoreSelectionSpecifier {
viewMode: ViewMode;
singleStory?: boolean;
args?: Args;
globalArgs?: Args;
}
export interface StoreSelection {

View File

@ -68,6 +68,7 @@ See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-url-stru
export const getSelectionSpecifierFromPath: () => StoreSelectionSpecifier = () => {
const query = qs.parse(document.location.search, { ignoreQueryPrefix: true });
const args = typeof query.args === 'string' ? parseArgsParam(query.args) : undefined;
const globalArgs = typeof query.globals === 'string' ? parseArgsParam(query.globals) : undefined;
let viewMode = getFirstString(query.viewMode) as ViewMode;
if (typeof viewMode !== 'string' || !viewMode.match(/docs|story/)) {
@ -79,7 +80,7 @@ export const getSelectionSpecifierFromPath: () => StoreSelectionSpecifier = () =
const storyId = path ? pathToId(path) : getFirstString(query.id);
if (storyId) {
return { storySpecifier: storyId, args, viewMode, singleStory };
return { storySpecifier: storyId, args, globalArgs, viewMode, singleStory };
}
// Legacy URL format
@ -88,7 +89,7 @@ export const getSelectionSpecifierFromPath: () => StoreSelectionSpecifier = () =
if (kind && name) {
deprecatedLegacyQuery();
return { storySpecifier: { kind, name }, args, viewMode, singleStory };
return { storySpecifier: { kind, name }, args, globalArgs, viewMode, singleStory };
}
return null;
};