From 5ef2ab8b69006916f376ee3beefae13c82d39673 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 15:43:19 +0200 Subject: [PATCH 01/35] ADD api for setting the state of an addon --- lib/api/src/modules/addons.ts | 7 ++++++- lib/api/src/store.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/api/src/modules/addons.ts b/lib/api/src/modules/addons.ts index 8ed4d507faa1..36671163b8bb 100644 --- a/lib/api/src/modules/addons.ts +++ b/lib/api/src/modules/addons.ts @@ -1,5 +1,6 @@ -import { Module } from '../index'; +import { Module, State } from '../index'; import { ReactElement } from 'react'; +import { Options } from '../store'; export enum types { TAB = 'tab', @@ -43,6 +44,7 @@ export interface SubAPI { getPanels: () => Collection; getSelectedPanel: () => string; setSelectedPanel: (panelName: string) => void; + setAddonState: (addonId: string, state: any, options: Options) => Promise; } export function ensurePanel(panels: Panels, selectedPanel?: string, currentPanel?: string) { @@ -69,6 +71,9 @@ export default ({ provider, store }: Module) => { setSelectedPanel: panelName => { store.setState({ selectedPanel: panelName }, { persistence: 'session' }); }, + setAddonState: (addonId, state, options) => { + return store.setState({ addons: { [addonId]: state } }, options); + }, }; return { diff --git a/lib/api/src/store.ts b/lib/api/src/store.ts index ae4be40beeee..11fae5e333c6 100644 --- a/lib/api/src/store.ts +++ b/lib/api/src/store.ts @@ -37,7 +37,7 @@ type Patch = Partial; type InputFnPatch = (s: State) => Patch; type InputPatch = Patch | InputFnPatch; -interface Options { +export interface Options { persistence: 'none' | 'session' | string; } type CallBack = (s: State) => void; From bbc75ea00bf7a13962fdd15b22022b4f56a2101c Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 16:47:02 +0200 Subject: [PATCH 02/35] MIGRATE addon-background to use new api --- .../src/containers/BackgroundSelector.tsx | 30 ++++++++++++------- addons/backgrounds/src/register.tsx | 4 +-- lib/api/src/modules/addons.ts | 3 +- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/addons/backgrounds/src/containers/BackgroundSelector.tsx b/addons/backgrounds/src/containers/BackgroundSelector.tsx index 4e94e4fb768d..45894e755465 100644 --- a/addons/backgrounds/src/containers/BackgroundSelector.tsx +++ b/addons/backgrounds/src/containers/BackgroundSelector.tsx @@ -1,7 +1,7 @@ import React, { Component, Fragment } from 'react'; import memoize from 'memoizerific'; -import { Combo, Consumer } from '@storybook/api'; +import { Combo, Consumer, API } from '@storybook/api'; import { Global, Theme } from '@storybook/theming'; import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; @@ -63,14 +63,15 @@ const getSelectedBackgroundColor = (list: Input[], currentSelectedValue: string) return 'transparent'; }; -const mapper = ({ api, state }: Combo): { items: Input[] } => { +const mapper = ({ api, state }: Combo): { items: Input[]; selected: string | null } => { const story = state.storiesHash[state.storyId]; const list = story ? api.getParameters(story.id, PARAM_KEY) : []; + const selected = state.addons[PARAM_KEY] || null; - return { items: list || [] }; + return { items: list || [], selected }; }; -const getDisplayedItems = memoize(10)((list: Input[], selected: State['selected'], change) => { +const getDisplayedItems = memoize(10)((list: Input[], selected: string | null, change) => { let availableBackgroundSelectorItems: Item[] = []; if (selected !== 'transparent') { @@ -92,17 +93,26 @@ const getDisplayedItems = memoize(10)((list: Input[], selected: State['selected' }); interface State { - selected: string; expanded: boolean; } -export class BackgroundSelector extends Component<{}, State> { +interface Props { + api: API; +} + +export class BackgroundSelector extends Component { state: State = { - selected: null, expanded: false, }; - change = (args: State) => this.setState(args); + change = (args: State & { selected: string | null }) => { + if (typeof args.expanded === 'boolean') { + this.setState({ expanded: args.expanded }); + } + if (typeof args.selected === 'string') { + this.props.api.setAddonState(PARAM_KEY, args.selected); + } + }; onVisibilityChange = (s: boolean) => { if (this.state.expanded !== s) { @@ -111,11 +121,11 @@ export class BackgroundSelector extends Component<{}, State> { }; render() { - const { expanded, selected } = this.state; + const { expanded } = this.state; return ( - {({ items }: { items: Input[] }) => { + {({ items, selected }: ReturnType) => { const selectedBackgroundColor = getSelectedBackgroundColor(items, selected); const links = getDisplayedItems(items, selectedBackgroundColor, this.change); diff --git a/addons/backgrounds/src/register.tsx b/addons/backgrounds/src/register.tsx index 43d43dcae1c9..7dde13f9e169 100644 --- a/addons/backgrounds/src/register.tsx +++ b/addons/backgrounds/src/register.tsx @@ -4,11 +4,11 @@ import { addons, types } from '@storybook/addons'; import { ADDON_ID } from './constants'; import { BackgroundSelector } from './containers/BackgroundSelector'; -addons.register(ADDON_ID, () => { +addons.register(ADDON_ID, api => { addons.add(ADDON_ID, { title: 'Backgrounds', type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', - render: () => , + render: () => , }); }); diff --git a/lib/api/src/modules/addons.ts b/lib/api/src/modules/addons.ts index 36671163b8bb..bd2cd4fd10ee 100644 --- a/lib/api/src/modules/addons.ts +++ b/lib/api/src/modules/addons.ts @@ -44,7 +44,7 @@ export interface SubAPI { getPanels: () => Collection; getSelectedPanel: () => string; setSelectedPanel: (panelName: string) => void; - setAddonState: (addonId: string, state: any, options: Options) => Promise; + setAddonState: (addonId: string, state: any, options?: Options) => Promise; } export function ensurePanel(panels: Panels, selectedPanel?: string, currentPanel?: string) { @@ -80,6 +80,7 @@ export default ({ provider, store }: Module) => { api, state: { selectedPanel: ensurePanel(api.getPanels(), store.getState().selectedPanel), + addons: {}, }, }; }; From 6d979247ce2e8f9caf6a98de613b8527ea4feddb Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 22:35:24 +0200 Subject: [PATCH 03/35] ADD api for reading queryParams in client-api --- lib/client-api/src/index.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/client-api/src/index.js b/lib/client-api/src/index.js index 4cc48ab9503a..600e6aa32047 100644 --- a/lib/client-api/src/index.js +++ b/lib/client-api/src/index.js @@ -1,3 +1,5 @@ +import { document } from 'global'; +import qs from 'qs'; import ClientApi, { defaultDecorateStory } from './client_api'; import StoryStore, { splitPath } from './story_store'; import ConfigApi from './config_api'; @@ -13,3 +15,12 @@ export { pathToId, splitPath, }; + +export const getQueryParams = () => { + return qs.parse(document.location.search, { ignoreQueryPrefix: true }); +}; + +export const getQueryParam = key => { + const params = getQueryParams(); + return params[key]; +}; From c236cb4487414043b8a7334bb922708fae60e9b7 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 22:41:29 +0200 Subject: [PATCH 04/35] CHANGE addon-knobs so it can read value of knob from url --- addons/knobs/package.json | 1 + addons/knobs/src/KnobManager.js | 22 ++++++++++++-- addons/knobs/src/converters.js | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 addons/knobs/src/converters.js diff --git a/addons/knobs/package.json b/addons/knobs/package.json index 9ec76aba0abf..f21d8df48a97 100644 --- a/addons/knobs/package.json +++ b/addons/knobs/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@storybook/addons": "5.1.0-alpha.24", + "@storybook/client-api": "5.1.0-alpha.24", "@storybook/components": "5.1.0-alpha.24", "@storybook/core-events": "5.1.0-alpha.24", "@storybook/theming": "5.1.0-alpha.24", diff --git a/addons/knobs/src/KnobManager.js b/addons/knobs/src/KnobManager.js index 6c2c75c003b9..3313cef83aad 100644 --- a/addons/knobs/src/KnobManager.js +++ b/addons/knobs/src/KnobManager.js @@ -2,9 +2,20 @@ import deepEqual from 'fast-deep-equal'; import escape from 'escape-html'; +import { getQueryParams } from '@storybook/client-api'; + import KnobStore from './KnobStore'; import { SET } from './shared'; +import { deserializers } from './converters'; + +const knobValuesFromUrl = Object.entries(getQueryParams()).reduce((acc, [k, v]) => { + if (k.includes('knob-')) { + return { ...acc, [k.replace('knob-', '')]: v }; + } + return acc; +}, {}); + // This is used by _mayCallChannel to determine how long to wait to before triggering a panel update const PANEL_UPDATE_INTERVAL = 400; @@ -56,13 +67,20 @@ export default class KnobManager { return this.getKnobValue(existingKnob); } - const defaultValue = options.value; const knobInfo = { ...options, name, - defaultValue, }; + if (knobValuesFromUrl[name]) { + const value = deserializers[options.type](knobValuesFromUrl[name]); + + knobInfo.defaultValue = value; + knobInfo.value = value; + } else { + knobInfo.defaultValue = options.value; + } + knobStore.set(name, knobInfo); return this.getKnobValue(knobStore.get(name)); } diff --git a/addons/knobs/src/converters.js b/addons/knobs/src/converters.js new file mode 100644 index 000000000000..7e63c8de78a3 --- /dev/null +++ b/addons/knobs/src/converters.js @@ -0,0 +1,51 @@ +const unconvertable = () => undefined; + +export const converters = { + jsonParse: value => JSON.parse(value), + jsonStringify: value => JSON.stringify(value), + simple: value => value, + stringifyIfSet: value => (value === null || value === undefined ? '' : String(value)), + stringifyIfTruthy: value => (value ? String(value) : null), + toArray: value => { + if (Array.isArray(value)) { + return value; + } + + return value.split(','); + }, + toBoolean: value => value === 'true', + toDate: value => new Date(value).getTime() || new Date().getTime(), + toFloat: value => (value === '' ? null : parseFloat(value)), +}; + +export const serializers = { + array: converters.simple, + boolean: converters.stringifyIfTruthy, + button: unconvertable, + checkbox: converters.simple, + color: converters.simple, + date: converters.toDate, + files: unconvertable, + number: converters.stringifyIfSet, + object: converters.jsonStringify, + options: converters.simple, + radios: converters.simple, + select: converters.simple, + text: converters.simple, +}; + +export const deserializers = { + array: converters.toArray, + boolean: converters.toBoolean, + button: unconvertable, + checkbox: converters.simple, + color: converters.simple, + date: converters.toDate, + files: unconvertable, + number: converters.toFloat, + object: converters.jsonParse, + options: converters.simple, + radios: converters.simple, + select: converters.simple, + text: converters.simple, +}; From 00fce40b3f18f11c2286393cc857d692561dab51 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 22:53:13 +0200 Subject: [PATCH 05/35] CHANGE addon-knob so it sets queryParams to actual values instead of deleting them This DOES NOT set them to the URL, they are STILL REMOVED as always from main URL. --- addons/knobs/src/components/Panel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/knobs/src/components/Panel.js b/addons/knobs/src/components/Panel.js index fa144c9578d9..a83f097f9ecf 100644 --- a/addons/knobs/src/components/Panel.js +++ b/addons/knobs/src/components/Panel.js @@ -87,8 +87,8 @@ export default class KnobPanel extends PureComponent { } } - // set all knobsquery params to be deleted from URL - queryParams[`knob-${name}`] = null; + // set all knobsquery params to serialized value + queryParams[`knob-${name}`] = Types[knob.type].serialize(knobs[name].value); }); api.setQueryParams(queryParams); From 1b5685787717a53493a06ae536b8d3262e59fc56 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 22:58:29 +0200 Subject: [PATCH 06/35] ADD custom queryParameters to the generation of the new window URL --- lib/ui/src/components/preview/preview.js | 18 ++++++++++++++++-- lib/ui/src/containers/preview.js | 4 ++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/ui/src/components/preview/preview.js b/lib/ui/src/components/preview/preview.js index 55093a54c734..154c157f163d 100644 --- a/lib/ui/src/components/preview/preview.js +++ b/lib/ui/src/components/preview/preview.js @@ -70,7 +70,18 @@ const defaultWrappers = [ ]; const getTools = memoize(10)( - (getElements, panels, actions, options, storyId, viewMode, location, path, baseUrl) => { + ( + getElements, + getQueryParams, + panels, + actions, + options, + storyId, + viewMode, + location, + path, + baseUrl + ) => { const tools = getElementList(getElements, types.TOOL, [ panels.filter(p => p.id !== 'canvas').length ? { @@ -144,7 +155,7 @@ const getTools = memoize(10)( render: () => ( window.open(`${baseUrl}?id=${storyId}`)} + onClick={() => window.open(`${baseUrl}?id=${storyId}${getQueryParams()}`)} title="Open canvas in new tab" > @@ -204,6 +215,7 @@ class Preview extends Component { location, viewMode, storyId, + getQueryParams, getElements, actions, options, @@ -238,6 +250,7 @@ class Preview extends Component { ]); const { left, right } = getTools( getElements, + getQueryParams, panels, actions, options, @@ -287,6 +300,7 @@ Preview.propTypes = { viewMode: PropTypes.oneOf(['story', 'info']), location: PropTypes.shape({}).isRequired, getElements: PropTypes.func.isRequired, + getQueryParams: PropTypes.func.isRequired, options: PropTypes.shape({ isFullscreen: PropTypes.bool, isToolshown: PropTypes.bool, diff --git a/lib/ui/src/containers/preview.js b/lib/ui/src/containers/preview.js index d20a4574bb9d..f12b5a571d97 100644 --- a/lib/ui/src/containers/preview.js +++ b/lib/ui/src/containers/preview.js @@ -20,6 +20,10 @@ const mapper = ({ api, state: { layout, location, path, storyId, viewMode, selec : { api, getElements: api.getElements, + getQueryParams: () => + Object.entries(api.getUrlState().queryParams).reduce((acc, [k, v]) => { + return `${acc}&${k}=${v}`; + }, ''), actions: createPreviewActions(api), options: layout, location, From 5c550c555dcdeb10f88179e169068067d4125b69 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 23:16:45 +0200 Subject: [PATCH 07/35] CHANGE addon-knob to allow for setting of knob after loading from URL --- addons/knobs/src/KnobManager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/knobs/src/KnobManager.js b/addons/knobs/src/KnobManager.js index 3313cef83aad..2a8abab4ad42 100644 --- a/addons/knobs/src/KnobManager.js +++ b/addons/knobs/src/KnobManager.js @@ -77,6 +77,8 @@ export default class KnobManager { knobInfo.defaultValue = value; knobInfo.value = value; + + delete knobValuesFromUrl[name]; } else { knobInfo.defaultValue = options.value; } From 477836d31521677ee3ac1a9f93d93526000c01bd Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Apr 2019 23:17:34 +0200 Subject: [PATCH 08/35] ADD customQueryParams to initial src of iframe --- lib/ui/src/components/preview/preview.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/ui/src/components/preview/preview.js b/lib/ui/src/components/preview/preview.js index 154c157f163d..4c377b2ffedd 100644 --- a/lib/ui/src/components/preview/preview.js +++ b/lib/ui/src/components/preview/preview.js @@ -26,12 +26,12 @@ const DesktopOnly = styled.span({ }, }); -const renderIframe = (storyId, id, baseUrl, scale) => ( +const renderIframe = (storyId, id, baseUrl, scale, getQueryParams) => (