diff --git a/addons/storysource/src/StoryPanel.tsx b/addons/storysource/src/StoryPanel.tsx index 00435f113517..6f726e8c88b7 100644 --- a/addons/storysource/src/StoryPanel.tsx +++ b/addons/storysource/src/StoryPanel.tsx @@ -10,6 +10,7 @@ import { } from '@storybook/components'; import { SourceBlock, LocationsMap } from '@storybook/source-loader'; +import { Story } from '@storybook/api/dist/lib/stories'; const StyledStoryLink = styled(Link)<{ to: string; key: string }>(({ theme }) => ({ display: 'block', @@ -45,21 +46,13 @@ interface SourceParams { source: string; locationsMap: LocationsMap; } -export interface StoryData { - id: string; - kind?: string; - parameters?: { - storySource?: SourceParams; - mdxSource?: string; - }; -} export const StoryPanel: React.FC = ({ api }) => { const [state, setState] = React.useState({ source: 'loading source...', locationsMap: {}, }); - const story: StoryData | undefined = api.getCurrentStoryData(); + const story: Story | undefined = api.getCurrentStoryData() as Story; const selectedStoryRef = React.useRef(null); React.useEffect(() => { if (story) { diff --git a/app/server/package.json b/app/server/package.json index 37d3ba79d0f4..b64c94c08984 100644 --- a/app/server/package.json +++ b/app/server/package.json @@ -41,7 +41,7 @@ "global": "^4.3.2", "regenerator-runtime": "^0.13.3", "safe-identifier": "^0.3.1", - "ts-dedent": "^1.1.0" + "ts-dedent": "^1.1.1" }, "devDependencies": { "fs-extra": "^8.0.1" diff --git a/examples/angular-cli/addon-jest.testresults.json b/examples/angular-cli/addon-jest.testresults.json new file mode 100644 index 000000000000..f77f57bce8fc --- /dev/null +++ b/examples/angular-cli/addon-jest.testresults.json @@ -0,0 +1 @@ +{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":1,"numPassedTests":3,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":3,"openHandles":[],"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0},"startTime":1581465367102,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should create the app","location":null,"status":"passed","title":"should create the app"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should have as title 'app'","location":null,"status":"passed","title":"should have as title 'app'"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should render title in a h1 tag","location":null,"status":"passed","title":"should render title in a h1 tag"}],"endTime":1581465368794,"message":"","name":"/Users/dev/Projects/GitHub/storybook/core/examples/angular-cli/src/app/app.component.spec.ts","startTime":1581465367711,"status":"passed","summary":""}],"wasInterrupted":false} \ No newline at end of file diff --git a/lib/api/package.json b/lib/api/package.json index c476d80c47d9..42ee3a1d2919 100644 --- a/lib/api/package.json +++ b/lib/api/package.json @@ -45,6 +45,7 @@ "shallow-equal": "^1.1.0", "store2": "^2.7.1", "telejson": "^3.2.0", + "ts-dedent": "^1.1.1", "util-deprecate": "^1.0.2" }, "devDependencies": { diff --git a/lib/api/src/index.tsx b/lib/api/src/index.tsx index 2ad5214f5e04..0ff0f611655d 100644 --- a/lib/api/src/index.tsx +++ b/lib/api/src/index.tsx @@ -1,12 +1,4 @@ -import React, { - ReactElement, - Component, - useContext, - useEffect, - useMemo, - useRef, - ReactNode, -} from 'react'; +import React, { ReactElement, Component, useContext, useEffect, useMemo, ReactNode } from 'react'; import memoize from 'memoizerific'; // @ts-ignore shallow-equal is not in DefinitelyTyped import shallowEqualObjects from 'shallow-equal/objects'; @@ -34,11 +26,17 @@ import initNotifications, { SubState as NotificationState, SubAPI as NotificationAPI, } from './modules/notifications'; -import initStories, { - SubState as StoriesSubState, - SubAPI as StoriesAPI, +import initStories, { SubState as StoriesSubState, SubAPI as StoriesAPI } from './modules/stories'; +import { StoriesRaw, -} from './modules/stories'; + StoriesHash, + Story, + Root, + Group, + isGroup, + isRoot, + isStory, +} from './lib/stories'; import initLayout, { ActiveTabs, SubState as LayoutSubState, @@ -318,7 +316,17 @@ export function useStorybookApi(): API { return api; } -export { ManagerConsumer as Consumer, ManagerProvider as Provider }; +export { + ManagerConsumer as Consumer, + ManagerProvider as Provider, + StoriesHash, + Story, + Root, + Group, + isGroup, + isRoot, + isStory, +}; export interface EventMap { [eventId: string]: Listener; diff --git a/lib/api/src/init-provider-api.ts b/lib/api/src/init-provider-api.ts index b97d4e639b4f..c55f4163d9fb 100644 --- a/lib/api/src/init-provider-api.ts +++ b/lib/api/src/init-provider-api.ts @@ -17,6 +17,7 @@ export interface Provider { channel?: Channel; renderPreview?: IframeRenderer; handleAPI(api: API): void; + getConfig(): Record; [key: string]: any; } diff --git a/lib/api/src/lib/stories.ts b/lib/api/src/lib/stories.ts new file mode 100644 index 000000000000..e458327e15bb --- /dev/null +++ b/lib/api/src/lib/stories.ts @@ -0,0 +1,262 @@ +import deprecate from 'util-deprecate'; +import dedent from 'ts-dedent'; +import { sanitize, parseKind } from '@storybook/csf'; +import merge from './merge'; + +export type StoryId = string; + +export interface Root { + id: StoryId; + depth: 0; + name: string; + children: StoryId[]; + isComponent: false; + isRoot: true; + isLeaf: false; + // MDX stories are "Group" type + parameters?: any; +} + +export interface Group { + id: StoryId; + depth: number; + name: string; + children: StoryId[]; + parent?: StoryId; + isComponent: boolean; + isRoot: false; + isLeaf: false; + // MDX stories are "Group" type + parameters?: any; +} + +export interface Story { + id: StoryId; + depth: number; + parent: StoryId; + name: string; + kind: string; + children?: StoryId[]; + isComponent: boolean; + isRoot: false; + isLeaf: true; + parameters?: { + filename: string; + options: { + hierarchyRootSeparator?: RegExp; + hierarchySeparator?: RegExp; + showRoots?: boolean; + [k: string]: any; + }; + [k: string]: any; + }; +} + +export interface StoryInput { + id: StoryId; + name: string; + kind: string; + children: string[]; + parameters: { + filename: string; + options: { + hierarchyRootSeparator: RegExp; + hierarchySeparator: RegExp; + showRoots?: boolean; + [key: string]: any; + }; + [parameterName: string]: any; + }; + isLeaf: boolean; +} + +export interface StoriesHash { + [id: string]: Root | Group | Story; +} + +export type StoriesList = (Group | Story)[]; + +export type GroupsList = (Root | Group)[]; + +export interface StoriesRaw { + [id: string]: StoryInput; +} + +const warnUsingHierarchySeparatorsAndShowRoots = deprecate( + () => {}, + dedent` + You cannot use both the hierarchySeparator/hierarchyRootSeparator and showRoots options. + ` +); + +const warnRemovingHierarchySeparators = deprecate( + () => {}, + dedent` + hierarchySeparator and hierarchyRootSeparator are deprecated and will be removed in Storybook 6.0. + Read more about it in the migration guide: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md + ` +); + +const warnChangingDefaultHierarchySeparators = deprecate( + () => {}, + dedent` + The default hierarchy separators are changing in Storybook 6.0. + '|' and '.' will no longer create a hierarchy, but codemods are available. + Read more about it in the migration guide: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md + ` +); + +const toKey = (input: string) => + input.replace(/[^a-z0-9]+([a-z0-9])/gi, (...params) => params[1].toUpperCase()); + +const toGroup = (name: string) => ({ + name, + id: toKey(name), +}); + +export const transformStoriesRawToStoriesHash = ( + input: StoriesRaw, + base: StoriesHash +): StoriesHash => { + const anyKindMatchesOldHierarchySeparators = Object.values(input).some(({ kind }) => + kind.match(/\.|\|/) + ); + + const storiesHashOutOfOrder = Object.values(input).reduce((acc, item) => { + const { kind, parameters } = item; + const { + hierarchyRootSeparator: rootSeparator = undefined, + hierarchySeparator: groupSeparator = undefined, + showRoots = undefined, + } = (parameters && parameters.options) || {}; + + const usingShowRoots = typeof showRoots !== 'undefined'; + + // Kind splitting behavior as per https://github.com/storybookjs/storybook/issues/8793 + let root = ''; + let groups: string[]; + // 1. If the user has passed separators, use the old behavior but warn them + if (typeof rootSeparator !== 'undefined' || typeof groupSeparator !== 'undefined') { + warnRemovingHierarchySeparators(); + if (usingShowRoots) warnUsingHierarchySeparatorsAndShowRoots(); + ({ root, groups } = parseKind(kind, { + rootSeparator: rootSeparator || '|', + groupSeparator: groupSeparator || /\/|\./, + })); + + // 2. If the user hasn't passed separators, but is using | or . in kinds, use the old behaviour but warn + } else if (anyKindMatchesOldHierarchySeparators && !usingShowRoots) { + warnChangingDefaultHierarchySeparators(); + ({ root, groups } = parseKind(kind, { rootSeparator: '|', groupSeparator: /\/|\./ })); + + // 3. If the user passes showRoots, or doesn't match above, do a simpler splitting. + } else { + const parts: string[] = kind.split('/'); + if (showRoots && parts.length > 1) { + [root, ...groups] = parts; + } else { + groups = parts; + } + } + + const rootAndGroups = [] + .concat(root || []) + .concat(groups) + .map(toGroup) + // Map a bunch of extra fields onto the groups, collecting the path as we go (thus the reduce) + .reduce((soFar, group, index, original) => { + const { name } = group; + const parent = index > 0 && soFar[index - 1].id; + const id = sanitize(parent ? `${parent}-${name}` : name); + if (parent === id) { + throw new Error( + dedent` + Invalid part '${name}', leading to id === parentId ('${id}'), inside kind '${kind}' + + Did you create a path that uses the separator char accidentally, such as 'Vue ' where '/' is a separator char? See https://github.com/storybookjs/storybook/issues/6128 + ` + ); + } + + if (!!root && index === 0) { + const result: Root = { + ...group, + id, + depth: index, + children: [], + isComponent: false, + isLeaf: false, + isRoot: true, + parameters, + }; + return soFar.concat([result]); + } + const result: Group = { + ...group, + id, + parent, + depth: index, + children: [], + isComponent: false, + isLeaf: false, + isRoot: false, + parameters, + }; + return soFar.concat([result]); + }, [] as GroupsList); + + const paths = [...rootAndGroups.map(g => g.id), item.id]; + + // Ok, now let's add everything to the store + rootAndGroups.forEach((group, index) => { + const child = paths[index + 1]; + const { id } = group; + acc[id] = merge(acc[id] || {}, { + ...group, + ...(child && { children: [child] }), + }); + }); + + const story = { ...item, parent: rootAndGroups[rootAndGroups.length - 1].id, isLeaf: true }; + acc[item.id] = story as Story; + + return acc; + }, {} as StoriesHash); + + function addItem(acc: StoriesHash, item: Story | Group) { + if (!acc[item.id]) { + // If we were already inserted as part of a group, that's great. + acc[item.id] = item; + const { children } = item; + if (children) { + const childNodes = children.map(id => storiesHashOutOfOrder[id]) as (Story | Group)[]; + acc[item.id].isComponent = childNodes.every(childNode => childNode.isLeaf); + childNodes.forEach(childNode => addItem(acc, childNode)); + } + } + return acc; + } + + return Object.values(storiesHashOutOfOrder).reduce(addItem, base); +}; + +export type Item = StoriesHash[keyof StoriesHash]; + +export function isRoot(item: Item): item is Root { + if (item as Root) { + return item.isRoot; + } + return false; +} +export function isGroup(item: Item): item is Group { + if (item as Group) { + return !item.isRoot && !item.isLeaf; + } + return false; +} +export function isStory(item: Item): item is Story { + if (item as Story) { + return item.isLeaf; + } + return false; +} diff --git a/lib/api/src/modules/addons.ts b/lib/api/src/modules/addons.ts index 6a822157bfba..1e5a45040001 100644 --- a/lib/api/src/modules/addons.ts +++ b/lib/api/src/modules/addons.ts @@ -44,13 +44,11 @@ export interface Addon { disabled?: boolean; hidden?: boolean; } -export interface Collection { - [key: string]: Addon; +export interface Collection { + [key: string]: T; } -interface Panels { - [id: string]: Addon; -} +type Panels = Collection; type StateMerger = (input: S) => S; @@ -61,9 +59,9 @@ interface StoryInput { } export interface SubAPI { - getElements: (type: Types) => Collection; - getPanels: () => Collection; - getStoryPanels: () => Collection; + getElements: (type: Types) => Collection; + getPanels: () => Panels; + getStoryPanels: () => Panels; getSelectedPanel: () => string; setSelectedPanel: (panelName: string) => void; setAddonState( diff --git a/lib/api/src/modules/layout.ts b/lib/api/src/modules/layout.ts index dd4b30322545..fa4557589871 100644 --- a/lib/api/src/modules/layout.ts +++ b/lib/api/src/modules/layout.ts @@ -105,7 +105,7 @@ const applyDeprecatedThemeOptions = deprecate(({ name, url, theme }: Options): P }; }, deprecationMessage(deprecatedThemeOptions)); -const applyDeprecatedLayoutOptions = deprecate((options: Options): PartialLayout => { +const applyDeprecatedLayoutOptions = deprecate((options: Partial): PartialLayout => { const layoutUpdate: PartialLayout = {}; ['goFullScreen', 'showStoriesPanel', 'showAddonPanel'].forEach( @@ -130,7 +130,7 @@ const checkDeprecatedThemeOptions = (options: Options) => { return {}; }; -const checkDeprecatedLayoutOptions = (options: Options) => { +const checkDeprecatedLayoutOptions = (options: Partial) => { if (Object.keys(deprecatedLayoutOptions).find(v => v in options)) { return applyDeprecatedLayoutOptions(options); } diff --git a/lib/api/src/modules/stories.ts b/lib/api/src/modules/stories.ts index c332b4a5061d..15d129a96ae8 100644 --- a/lib/api/src/modules/stories.ts +++ b/lib/api/src/modules/stories.ts @@ -1,15 +1,22 @@ import { DOCS_MODE } from 'global'; -import { toId, sanitize, parseKind } from '@storybook/csf'; -import deprecate from 'util-deprecate'; +import { toId, sanitize } from '@storybook/csf'; + +import { + transformStoriesRawToStoriesHash, + StoriesHash, + Story, + Group, + StoriesRaw, + StoryId, + isStory, +} from '../lib/stories'; import { Module } from '../index'; -import merge from '../lib/merge'; type Direction = -1 | 1; -type StoryId = string; type ParameterName = string; -type ViewMode = 'story' | 'info' | 'settings' | undefined | string; +type ViewMode = 'story' | 'info' | 'settings' | string | undefined; export interface SubState { storiesHash: StoriesHash; @@ -30,64 +37,7 @@ export interface SubAPI { getCurrentParameter(parameterName?: ParameterName): S; } -interface Group { - id: StoryId; - name: string; - children: StoryId[]; - parent: StoryId; - depth: number; - isComponent: boolean; - isRoot: boolean; - isLeaf: boolean; - // MDX stories are "Group" type - parameters?: any; -} - -interface StoryInput { - id: StoryId; - name: string; - kind: string; - children: string[]; - parameters: { - filename: string; - options: { - hierarchyRootSeparator: RegExp; - hierarchySeparator: RegExp; - showRoots?: boolean; - [key: string]: any; - }; - [parameterName: string]: any; - }; - isLeaf: boolean; -} - -type Story = StoryInput & Group; - -export interface StoriesHash { - [id: string]: Group | Story; -} -export type StoriesList = (Group | Story)[]; -export type GroupsList = Group[]; - -export interface StoriesRaw { - [id: string]: StoryInput; -} - -const warnUsingHierarchySeparatorsAndShowRoots = deprecate(() => {}, -`You cannot use both the hierarchySeparator/hierarchyRootSeparator and showRoots options.`); - -const warnRemovingHierarchySeparators = deprecate( - () => {}, - `hierarchySeparator and hierarchyRootSeparator are deprecated and will be removed in Storybook 6.0. -Read more about it in the migration guide: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md` -); - -const warnChangingDefaultHierarchySeparators = deprecate( - () => {}, - `The default hierarchy separators are changing in Storybook 6.0. -'|' and '.' will no longer create a hierarchy, but codemods are available. -Read more about it in the migration guide: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md` -); +// When adding a group, also add all of its children, depth first const initStoriesApi = ({ store, @@ -95,15 +45,14 @@ const initStoriesApi = ({ storyId: initialStoryId, viewMode: initialViewMode, }: Module) => { - const isStory = (obj: Group | Story): boolean => { - const story = obj as Story; - return !!(story && story.parameters); - }; - const getData = (storyId: StoryId) => { const { storiesHash } = store.getState(); - return storiesHash[storyId]; + if (storiesHash[storyId]) { + return storiesHash[storyId]; + } + + return undefined; }; const getCurrentStoryData = () => { const { storyId } = store.getState(); @@ -114,7 +63,7 @@ const initStoriesApi = ({ const data = getData(storyId); if (isStory(data)) { - const { parameters } = data as Story; + const { parameters } = data; return parameterName ? parameters[parameterName] : parameters; } @@ -196,14 +145,6 @@ const initStoriesApi = ({ } }; - const toKey = (input: string) => - input.replace(/[^a-z0-9]+([a-z0-9])/gi, (...params) => params[1].toUpperCase()); - - const toGroup = (name: string) => ({ - name, - id: toKey(name), - }); - // Recursively traverse storiesHash from the initial storyId until finding // the leaf story. const findLeafStoryId = (storiesHash: StoriesHash, storyId: string): string => { @@ -216,118 +157,11 @@ const initStoriesApi = ({ }; const setStories = (input: StoriesRaw) => { - const hash: StoriesHash = {}; - - const anyKindMatchesOldHierarchySeparators = Object.values(input).some(({ kind }) => - kind.match(/\.|\|/) - ); - - const storiesHashOutOfOrder = Object.values(input).reduce((acc, item) => { - const { kind, parameters } = item; - // FIXME: figure out why parameters is missing when used with react-native-server - const { - hierarchyRootSeparator: rootSeparator = undefined, - hierarchySeparator: groupSeparator = undefined, - showRoots = undefined, - } = (parameters && parameters.options) || {}; - - const usingShowRoots = typeof showRoots !== 'undefined'; - - // Kind splitting behaviour as per https://github.com/storybookjs/storybook/issues/8793 - let root = ''; - let groups: string[]; - // 1. If the user has passed separators, use the old behaviour but warn them - if (typeof rootSeparator !== 'undefined' || typeof groupSeparator !== 'undefined') { - warnRemovingHierarchySeparators(); - if (usingShowRoots) warnUsingHierarchySeparatorsAndShowRoots(); - ({ root, groups } = parseKind(kind, { - rootSeparator: rootSeparator || '|', - groupSeparator: groupSeparator || /\/|\./, - })); - - // 2. If the user hasn't passed separators, but is using | or . in kinds, use the old behaviour but warn - } else if (anyKindMatchesOldHierarchySeparators && !usingShowRoots) { - warnChangingDefaultHierarchySeparators(); - ({ root, groups } = parseKind(kind, { rootSeparator: '|', groupSeparator: /\/|\./ })); - - // 3. If the user passes showRoots, or doesn't match above, do a simpler splitting. - } else { - const parts: string[] = kind.split('/'); - if (showRoots && parts.length > 1) { - [root, ...groups] = parts; - } else { - groups = parts; - } - } - - const rootAndGroups = [] - .concat(root || []) - .concat(groups) - .map(toGroup) - // Map a bunch of extra fields onto the groups, collecting the path as we go (thus the reduce) - .reduce((soFar, group, index, original) => { - const { name } = group; - const parent = index > 0 && soFar[index - 1].id; - const id = sanitize(parent ? `${parent}-${name}` : name); - if (parent === id) { - throw new Error( - ` -Invalid part '${name}', leading to id === parentId ('${id}'), inside kind '${kind}' - -Did you create a path that uses the separator char accidentally, such as 'Vue ' where '/' is a separator char? See https://github.com/storybookjs/storybook/issues/6128 - `.trim() - ); - } - - const result: Group = { - ...group, - id, - parent, - depth: index, - children: [], - isComponent: false, - isLeaf: false, - isRoot: !!root && index === 0, - parameters, - }; - return soFar.concat([result]); - }, [] as GroupsList); - - const paths = [...rootAndGroups.map(g => g.id), item.id]; - - // Ok, now let's add everything to the store - rootAndGroups.forEach((group, index) => { - const child = paths[index + 1]; - const { id } = group; - acc[id] = merge(acc[id] || {}, { - ...group, - ...(child && { children: [child] }), - }); - }); - - const story = { ...item, parent: rootAndGroups[rootAndGroups.length - 1].id, isLeaf: true }; - acc[item.id] = story as Story; - - return acc; - }, hash); - - // When adding a group, also add all of its children, depth first - function addItem(acc: StoriesHash, item: Story | Group) { - if (!acc[item.id]) { - // If we were already inserted as part of a group, that's great. - acc[item.id] = item; - const { children } = item; - if (children) { - const childNodes = children.map(id => storiesHashOutOfOrder[id]); - acc[item.id].isComponent = childNodes.every(childNode => childNode.isLeaf); - childNodes.forEach(childNode => addItem(acc, childNode)); - } - } - return acc; - } - // Now create storiesHash by reordering the above by group - const storiesHash: StoriesHash = Object.values(storiesHashOutOfOrder).reduce(addItem, {}); + const storiesHash: StoriesHash = transformStoriesRawToStoriesHash( + input, + (store.getState().storiesHash || {}) as StoriesHash + ); const settingsPageList = ['about', 'shortcuts']; const { storyId, viewMode } = store.getState(); @@ -365,26 +199,32 @@ Did you create a path that uses the separator char accidentally, such as 'Vue { + const selectStory = (kindOrId: string, story: string = undefined) => { const { viewMode = 'story', storyId, storiesHash } = store.getState(); + + const hash = storiesHash; + if (!story) { - const s = storiesHash[sanitize(kindOrId)]; + const real = sanitize(kindOrId); + const s = hash[real]; // eslint-disable-next-line no-nested-ternary const id = s ? (s.children ? s.children[0] : s.id) : kindOrId; navigate(`/${viewMode}/${id}`); } else if (!kindOrId) { // This is a slugified version of the kind, but that's OK, our toId function is idempotent const kind = storyId.split('--', 2)[0]; - selectStory(toId(kind, story)); + const id = toId(kind, story); + + selectStory(id); } else { const id = toId(kindOrId, story); - if (storiesHash[id]) { + if (hash[id]) { selectStory(id); } else { // Support legacy API with component permalinks, where kind is `x/y` but permalink is 'z' - const k = storiesHash[sanitize(kindOrId)]; + const k = hash[sanitize(kindOrId)]; if (k && k.children) { - const foundId = k.children.find(childId => storiesHash[childId].name === story); + const foundId = k.children.find(childId => hash[childId].name === story); if (foundId) { selectStory(foundId); } diff --git a/lib/ui/src/FakeProvider.tsx b/lib/ui/src/FakeProvider.tsx new file mode 100644 index 000000000000..d32621e831f3 --- /dev/null +++ b/lib/ui/src/FakeProvider.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { styled } from '@storybook/theming'; +import { addons } from '@storybook/addons'; +import Provider from './provider'; + +export class FakeProvider extends Provider { + constructor() { + super(); + + // @ts-ignore + this.addons = addons; + // @ts-ignore + this.channel = { + on: () => {}, + off: () => {}, + emit: () => {}, + addPeerListener: () => {}, + }; + } + + // @ts-ignore + getElements(type) { + return addons.getElements(type); + } + + renderPreview() { + return
This is from a 'renderPreview' call from FakeProvider
; + } + + // @ts-ignore + handleAPI(api) { + addons.loadAddons(api); + } + + // @ts-ignore + getConfig() { + return {}; + } +} + +export const Centered = styled.div({ + width: '100vw', + height: '100vh', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', +}); + +export class PrettyFakeProvider extends FakeProvider { + renderPreview(...args: any[]) { + return ( + + This is from a 'renderPreview' call from FakeProvider +
+ 'renderPreview' was called with: +
{JSON.stringify(args, null, 2)}
+
+ ); + } +} diff --git a/lib/ui/src/app.stories.js b/lib/ui/src/app.stories.js deleted file mode 100644 index d4c5b163d891..000000000000 --- a/lib/ui/src/app.stories.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { styled } from '@storybook/theming'; -import { storiesOf } from '@storybook/react'; -import addons from '@storybook/addons'; - -import { Root as App, Provider } from './index'; - -const CustomApp = styled.div({ - '#preview-loader': { - display: 'none', - }, -}); - -class FakeProvider extends Provider { - constructor() { - super(); - - this.addons = addons; - this.channel = { - on: () => {}, - off: () => {}, - emit: () => {}, - addPeerListener: () => {}, - }; - } - - getElements(type) { - return addons.getElements(type); - } - - renderPreview() { - return
Hello world
; - } - - handleAPI(api) { - addons.loadAddons(api); - } - - getConfig() { - return {}; - } -} - -class FakeLoadingProvider extends FakeProvider { - renderPreview() { - return

Switch between Desktop and Mobile viewport to see how the loading state behaves.

; - } -} - -storiesOf('UI/Layout/App', module) - .addParameters({ - component: App, - }) - .add('default', () => ( - - - - )) - .add('loading state', () => ); diff --git a/lib/ui/src/app.stories.tsx b/lib/ui/src/app.stories.tsx new file mode 100644 index 000000000000..78578b95e0a4 --- /dev/null +++ b/lib/ui/src/app.stories.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Root as App } from './index'; +import { PrettyFakeProvider, FakeProvider } from './FakeProvider'; +import Provider from './provider'; + +export default { + title: 'UI/App', + component: App, +}; + +export const Default = () => ; +export const LoadingState = () => ( + +); diff --git a/lib/ui/src/app.tsx b/lib/ui/src/app.tsx index 1c4651509129..0d49de3ec8b7 100644 --- a/lib/ui/src/app.tsx +++ b/lib/ui/src/app.tsx @@ -8,7 +8,7 @@ import { Route } from '@storybook/router'; import { State } from '@storybook/api'; import { Mobile } from './components/layout/mobile'; import { Desktop } from './components/layout/desktop'; -import Nav from './containers/nav'; +import Sidebar from './containers/sidebar'; import Preview from './containers/preview'; import Panel from './containers/panel'; import Notifications from './containers/notifications'; @@ -16,7 +16,7 @@ import Notifications from './containers/notifications'; import SettingsPages from './settings'; const createProps = memoize(1)(() => ({ - Nav, + Sidebar, Preview, Panel, Notifications, @@ -40,7 +40,7 @@ const View = styled.div({ width: '100vw', }); -const App = React.memo<{ +export interface AppProps { viewMode: State['viewMode']; docsOnly: boolean; layout: State['layout']; @@ -49,39 +49,43 @@ const App = React.memo<{ width: number; height: number; }; -}>(({ viewMode, docsOnly, layout, panelCount, size: { width, height } }) => { - const props = createProps(); +} - let content; +const App = React.memo( + ({ viewMode, docsOnly, layout, panelCount, size: { width, height } }) => { + const props = createProps(); - if (!width || !height) { - content = ( -
- {width} x {height} -
- ); - } else if (width < 600) { - content = ; - } else { - content = ( - + let content; + + if (!width || !height) { + content = ( +
+ {width} x {height} +
+ ); + } else if (width < 600) { + content = ; + } else { + content = ( + + ); + } + + return ( + + + {content} + ); } - - return ( - - - {content} - - ); -}); +); const SizedApp = sizeMe({ monitorHeight: true })(App); diff --git a/lib/ui/src/components/layout/app.mockdata.tsx b/lib/ui/src/components/layout/app.mockdata.tsx new file mode 100644 index 000000000000..f878a5fe4a27 --- /dev/null +++ b/lib/ui/src/components/layout/app.mockdata.tsx @@ -0,0 +1,166 @@ +import { setInterval } from 'global'; +import React, { Component, FunctionComponent } from 'react'; +import { styled } from '@storybook/theming'; +import { Collection } from '@storybook/addons'; +import Sidebar, { SidebarProps } from '../sidebar/Sidebar'; +import Panel from '../panel/panel'; +import { Preview } from '../preview/preview'; + +import { previewProps } from '../preview/preview.mockdata'; +import { mockDataset } from '../sidebar/treeview/treeview.mockdata'; +import { DesktopProps } from './desktop'; + +export const panels: Collection = { + test1: { + title: 'Test 1', + render: ({ active, key }) => + active ? ( +
+ TEST 1 +
+ ) : null, + }, + test2: { + title: 'Test 2', + render: ({ active, key }) => + active ? ( +
+ TEST 2 +
+ ) : null, + }, +}; + +const realSidebarProps: SidebarProps = { + stories: mockDataset.withRoot, + menu: [], +}; + +const PlaceholderBlock = styled.div(({ color }) => ({ + background: color || 'hotpink', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', +})); + +class PlaceholderClock extends Component<{ color: string }, { count: number }> { + state = { + count: 1, + }; + + interval: ReturnType; + + componentDidMount() { + this.interval = setInterval(() => { + const { count } = this.state; + this.setState({ count: count + 1 }); + }, 1000); + } + + componentWillUnmount() { + const { interval } = this; + clearInterval(interval); + } + + render() { + const { children, color } = this.props; + const { count } = this.state; + return ( + +

+ {count} +

+ {children} +
+ ); + } +} + +const MockSidebar: FunctionComponent = props => ( + +
{JSON.stringify(props, null, 2)}
+
+); +const MockPreview: FunctionComponent = props => ( + +
{JSON.stringify(props, null, 2)}
+
+); +const MockPanel: FunctionComponent = props => ( + +
{JSON.stringify(props, null, 2)}
+
+); +export const MockPage: FunctionComponent = props => ( + +
{JSON.stringify(props, null, 2)}
+
+); + +export const mockProps: DesktopProps = { + Sidebar: MockSidebar, + Preview: MockPreview, + Panel: MockPanel, + Notifications: () => null, + pages: [], + options: { + isFullscreen: false, + showNav: true, + showPanel: true, + panelPosition: 'right', + isToolshown: true, + initialActive: 'canvas', + }, + viewMode: 'story', + panelCount: 2, + width: 900, + height: 600, + docsOnly: false, +}; + +export const realProps: DesktopProps = { + Sidebar: () => , + Preview: () => , + Notifications: () => null, + Panel: () => ( + {}, toggleVisibility: () => {}, togglePosition: () => {} }} + selectedPanel="test2" + panelPosition="bottom" + absolute={false} + /> + ), + pages: [], + options: { + isFullscreen: false, + showNav: true, + showPanel: true, + panelPosition: 'right', + isToolshown: true, + initialActive: 'canvas', + }, + viewMode: 'story', + panelCount: 2, + width: 900, + height: 600, + docsOnly: false, +}; diff --git a/lib/ui/src/components/layout/container.tsx b/lib/ui/src/components/layout/container.tsx index 4e4207a6b314..ae1b7cc7b627 100644 --- a/lib/ui/src/components/layout/container.tsx +++ b/lib/ui/src/components/layout/container.tsx @@ -95,7 +95,7 @@ const Paper = styled.div<{ isFullscreen: boolean }>( } ); -export const Nav: FunctionComponent<{ hidden: boolean; position: CSSProperties }> = ({ +export const Sidebar: FunctionComponent<{ hidden: boolean; position: CSSProperties }> = ({ hidden = false, children, position = undefined, diff --git a/lib/ui/src/components/layout/desktop.stories.tsx b/lib/ui/src/components/layout/desktop.stories.tsx new file mode 100644 index 000000000000..e8b7e46d6c8a --- /dev/null +++ b/lib/ui/src/components/layout/desktop.stories.tsx @@ -0,0 +1,73 @@ +/* eslint-disable react/destructuring-assignment */ +import React, { Fragment } from 'react'; +import { withKnobs, boolean, number } from '@storybook/addon-knobs'; +import { DecoratorFn } from '@storybook/react'; + +import { isChromatic } from 'storybook-chromatic/isChromatic'; + +import { Desktop, DesktopProps } from './desktop'; + +import { store } from './persist'; +import { mockProps, realProps, MockPage } from './app.mockdata'; + +export default { + title: 'UI/Layout/Desktop', + component: Desktop, + decorators: [ + withKnobs, + ((StoryFn, c) => { + const mocked = boolean('mock', true); + const height = number('height', 900); + const width = number('width', 1200); + + if (isChromatic) { + store.local.set(`storybook-layout`, {}); + } + + const props = { + height, + width, + ...(mocked ? mockProps : realProps), + }; + + return ( +
+ ; +
+ ); + }) as DecoratorFn, + ], +}; + +export const Default = ({ props }: { props: DesktopProps }) => ; +export const NoAddons = ({ props }: { props: DesktopProps }) => ( + +); +export const NoSidebar = ({ props }: { props: DesktopProps }) => ( + +); +export const NoPanel = ({ props }: { props: DesktopProps }) => ( + +); +export const BottomPanel = ({ props }: { props: DesktopProps }) => ( + +); +export const Fullscreen = ({ props }: { props: DesktopProps }) => ( + +); +export const NoPanelNoSidebar = ({ props }: { props: DesktopProps }) => ( + +); +export const Page = ({ props }: { props: DesktopProps }) => ( + {children}, + render: () => , + }, + ]} + viewMode="settings" + /> +); diff --git a/lib/ui/src/components/layout/desktop.tsx b/lib/ui/src/components/layout/desktop.tsx index 4c120dc6a238..18549493a306 100644 --- a/lib/ui/src/components/layout/desktop.tsx +++ b/lib/ui/src/components/layout/desktop.tsx @@ -3,11 +3,11 @@ import React, { Fragment, ComponentType, FunctionComponent } from 'react'; import { State } from '@storybook/api'; import * as S from './container'; -interface DesktopProps { +export interface DesktopProps { width: number; panelCount: number; height: number; - Nav: ComponentType; + Sidebar: ComponentType; Preview: ComponentType; Panel: ComponentType; Notifications: ComponentType; @@ -24,7 +24,7 @@ interface DesktopProps { const Desktop = React.memo( ({ Panel, - Nav, + Sidebar, Preview, Notifications, pages, @@ -53,9 +53,9 @@ const Desktop = React.memo( > {({ navProps, mainProps, panelProps, previewProps }) => ( - -