mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-29 05:04:31 +08:00
Replace session-storage based globals handling with url param
This commit is contained in:
parent
ac522bbc83
commit
7dc469ce2e
@ -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);
|
||||
});
|
||||
|
@ -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' },
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -39,6 +39,7 @@ export interface StoreSelectionSpecifier {
|
||||
viewMode: ViewMode;
|
||||
singleStory?: boolean;
|
||||
args?: Args;
|
||||
globalArgs?: Args;
|
||||
}
|
||||
|
||||
export interface StoreSelection {
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user