diff --git a/core/config/test/__snapshots__/stories.test.ts.snap b/core/config/test/__snapshots__/stories.test.ts.snap index 9824bc08f..63322c46e 100644 --- a/core/config/test/__snapshots__/stories.test.ts.snap +++ b/core/config/test/__snapshots__/stories.test.ts.snap @@ -65,6 +65,7 @@ Array [ "/Users/atanasster/component-controls/ui/blocks/src/Title/Title.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/controls-editors-starter.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/controls-editors.stories.jsx", + "/Users/atanasster/component-controls/examples/stories/src/stories/dynamic-stories.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/inherit-from-doc.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/smart-prop-type.stories.js", "/Users/atanasster/component-controls/examples/stories/src/stories/smart-typescript.stories.js", @@ -137,6 +138,7 @@ Array [ "/Users/atanasster/component-controls/ui/blocks/src/Title/Title.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/controls-editors-starter.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/controls-editors.stories.jsx", + "/Users/atanasster/component-controls/examples/stories/src/stories/dynamic-stories.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/inherit-from-doc.stories.tsx", "/Users/atanasster/component-controls/examples/stories/src/stories/smart-prop-type.stories.js", "/Users/atanasster/component-controls/examples/stories/src/stories/smart-typescript.stories.js", diff --git a/core/core/README.md b/core/core/README.md index 63addcab6..10bc6d58b 100644 --- a/core/core/README.md +++ b/core/core/README.md @@ -19,6 +19,7 @@ - [Stories](#stories) - [Story](#story) - [StoryArguments](#storyarguments) + - [StoryFactoryFn](#storyfactoryfn) - [CURRENT_STORY](#current_story) - [defDocType](#defdoctype) - [dateToLocalString](#datetolocalstring) @@ -139,7 +140,7 @@ $ npm install @component-controls/core --save-dev ## DefaultStore -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L340)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L356)_ @@ -203,7 +204,7 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr store of stories information in memory after the loader is applied -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L301)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L317)_ @@ -260,7 +261,7 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr list of components used in stories -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L276)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L292)_ Record<string, @@ -274,7 +275,7 @@ A documentation file's metadata. For MDX files, fromtmatter is used to declare the document properties. For ESM (ES Modules) documentation files, the default export is used. -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L165)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L181)_ ### properties @@ -305,7 +306,7 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr list of story files, or groups -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L281)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L297)_ Record<string, @@ -317,7 +318,7 @@ Record<string, list of repositories -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L293)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L309)_ Record<string, @@ -327,13 +328,13 @@ Record<string, ## Pages -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L283)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L299)_ [Document](#document)\[] ## StoreObserver -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L295)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L311)_ **function** (`story`: [Story](#story)): void; @@ -348,7 +349,7 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr list of stories -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L288)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L304)_ Record<string, @@ -364,18 +365,20 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr ### properties -| Name | Type | Description | -| ------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- | -| `arguments` | [StoryArguments](#storyarguments) | arguments passed to the story function. eg \`export const story = props => <Story {...props} />;\` | -| `description` | string | story extended description. can use markdown. | -| `doc` | string | title of the file/group of stories | -| `id` | string | id of the story | -| `loc` | [CodeLocation](#codelocation) | location in the source file of the story definition | -| `name*` | string | name of the Story. | -| `renderFn` | [StoryRenderFn](#storyrenderfn) | render function for the story | -| `source` | string | the source code of the story, extracted by the AST instrumenting loaders | -| `subtitle` | string | optional story subtitle property | -| `StoryProps` | [StoryProps](#storyprops) | | +| Name | Type | Description | +| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `arguments` | [StoryArguments](#storyarguments) | arguments passed to the story function. eg \`export const story = props => <Story {...props} />;\` | +| `description` | string | story extended description. can use markdown. | +| `doc` | string | title of the file/group of stories | +| `factory` | boolean | if set to true, the function is a stories factory, returns a list of Story objects | +| `factoryId` | string | if the story was created by a dynacmi storiers factory, this is the original 'parent' factory id. it is set internally and will be used to create a story URL | +| `id` | string | id of the story | +| `loc` | [CodeLocation](#codelocation) | location in the source file of the story definition | +| `name*` | string | name of the Story. | +| `renderFn` | [StoryRenderFn](#storyrenderfn) | render function for the story | +| `source` | string | the source code of the story, extracted by the AST instrumenting loaders | +| `subtitle` | string | optional story subtitle property | +| `StoryProps` | [StoryProps](#storyprops) | | ## StoryArguments @@ -386,21 +389,37 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr [StoryArgument](#storyargument)\[] +## StoryFactoryFn + +dynamic story factory function type. +returns an array of dynamically loaded stories + +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L173)_ + +**function** (`doc`\*: [Document](#document)): [Story](#story)\[]; + +### parameters + +| Name | Type | Description | +| --------- | --------------------- | ----------- | +| `doc*` | [Document](#document) | | +| `returns` | [Story](#story)\[] | | + ## CURRENT_STORY -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L297)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L313)_ ## defDocType -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L159)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L175)_ ## dateToLocalString -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L266)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L282)_ **function** dateToLocalString(`date`: [Date](#date)): string; @@ -413,7 +432,7 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr ## getDefaultStore -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L366)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L382)_ **function** getDefaultStore(): [Store](#store); @@ -1327,7 +1346,7 @@ _defined in [@component-controls/core/src/configuration.ts](https://github.com/c ## StoreObserver -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L295)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L311)_ **function** (`story`: [Story](#story)): void; @@ -1346,24 +1365,26 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr ### properties -| Name | Type | Description | -| ------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- | -| `arguments` | [StoryArguments](#storyarguments) | arguments passed to the story function. eg \`export const story = props => <Story {...props} />;\` | -| `description` | string | story extended description. can use markdown. | -| `doc` | string | title of the file/group of stories | -| `id` | string | id of the story | -| `loc` | [CodeLocation](#codelocation) | location in the source file of the story definition | -| `name*` | string | name of the Story. | -| `renderFn` | [StoryRenderFn](#storyrenderfn) | render function for the story | -| `source` | string | the source code of the story, extracted by the AST instrumenting loaders | -| `subtitle` | string | optional story subtitle property | -| `StoryProps` | [StoryProps](#storyprops) | | +| Name | Type | Description | +| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `arguments` | [StoryArguments](#storyarguments) | arguments passed to the story function. eg \`export const story = props => <Story {...props} />;\` | +| `description` | string | story extended description. can use markdown. | +| `doc` | string | title of the file/group of stories | +| `factory` | boolean | if set to true, the function is a stories factory, returns a list of Story objects | +| `factoryId` | string | if the story was created by a dynacmi storiers factory, this is the original 'parent' factory id. it is set internally and will be used to create a story URL | +| `id` | string | id of the story | +| `loc` | [CodeLocation](#codelocation) | location in the source file of the story definition | +| `name*` | string | name of the Story. | +| `renderFn` | [StoryRenderFn](#storyrenderfn) | render function for the story | +| `source` | string | the source code of the story, extracted by the AST instrumenting loaders | +| `subtitle` | string | optional story subtitle property | +| `StoryProps` | [StoryProps](#storyprops) | | ## Components list of components used in stories -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L276)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L292)_ Record<string, @@ -1386,7 +1407,7 @@ _defined in [@component-controls/core/src/configuration.ts](https://github.com/c list of story files, or groups -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L281)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L297)_ Record<string, @@ -1398,7 +1419,7 @@ Record<string, list of repositories -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L293)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L309)_ Record<string, @@ -1410,7 +1431,7 @@ Record<string, list of stories -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L288)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L304)_ Record<string, @@ -1455,7 +1476,7 @@ A documentation file's metadata. For MDX files, fromtmatter is used to declare the document properties. For ESM (ES Modules) documentation files, the default export is used. -_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L165)_ +_defined in [@component-controls/core/src/document.ts](https://github.com/ccontrols/component-controls/tree/master/core/core/src/document.ts#L181)_ ### properties @@ -1629,18 +1650,20 @@ _defined in [@component-controls/core/src/document.ts](https://github.com/ccontr ### properties -| Name | Type | Description | -| ------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- | -| `arguments` | [StoryArguments](#storyarguments) | arguments passed to the story function. eg \`export const story = props => <Story {...props} />;\` | -| `description` | string | story extended description. can use markdown. | -| `doc` | string | title of the file/group of stories | -| `id` | string | id of the story | -| `loc` | [CodeLocation](#codelocation) | location in the source file of the story definition | -| `name*` | string | name of the Story. | -| `renderFn` | [StoryRenderFn](#storyrenderfn) | render function for the story | -| `source` | string | the source code of the story, extracted by the AST instrumenting loaders | -| `subtitle` | string | optional story subtitle property | -| `StoryProps` | [StoryProps](#storyprops) | | +| Name | Type | Description | +| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `arguments` | [StoryArguments](#storyarguments) | arguments passed to the story function. eg \`export const story = props => <Story {...props} />;\` | +| `description` | string | story extended description. can use markdown. | +| `doc` | string | title of the file/group of stories | +| `factory` | boolean | if set to true, the function is a stories factory, returns a list of Story objects | +| `factoryId` | string | if the story was created by a dynacmi storiers factory, this is the original 'parent' factory id. it is set internally and will be used to create a story URL | +| `id` | string | id of the story | +| `loc` | [CodeLocation](#codelocation) | location in the source file of the story definition | +| `name*` | string | name of the Story. | +| `renderFn` | [StoryRenderFn](#storyrenderfn) | render function for the story | +| `source` | string | the source code of the story, extracted by the AST instrumenting loaders | +| `subtitle` | string | optional story subtitle property | +| `StoryProps` | [StoryProps](#storyprops) | | ## DocType diff --git a/core/core/src/document-utils.ts b/core/core/src/document-utils.ts index 4ea53aadb..be1ff93a4 100644 --- a/core/core/src/document-utils.ts +++ b/core/core/src/document-utils.ts @@ -3,7 +3,7 @@ import { storyNameFromExport as csfStoryNameFromExport, } from '@storybook/csf'; import { PagesOnlyRoutes, DocType, PageConfiguration } from './configuration'; -import { Document, defDocType } from './document'; +import { Document, Story, defDocType, Store } from './document'; export const storyNameFromExport = csfStoryNameFromExport; export const strToId = (str: string) => str.replace(/\W/g, '-').toLowerCase(); @@ -20,27 +20,30 @@ export const removeTrailingSlash = (route: string) => export const getDocPath = ( docType: DocType, doc?: Document, - pagesConfig?: PagesOnlyRoutes, + store?: Store, name: string = '', tab?: string, ): string => { + const pagesConfig: PagesOnlyRoutes | undefined = store + ? (store.config.pages as PagesOnlyRoutes) + : undefined; const { basePath = '', sideNav = {} } = pagesConfig?.[docType] || {}; const { storyPaths } = sideNav; const activeTab = doc?.MDXPage ? undefined : tab; - if (storyPaths && doc && doc.stories && doc.stories.length > 0) { - return getStoryPath(doc.stories[0], doc, pagesConfig, activeTab); + if (storyPaths && doc && doc.stories && doc.stories.length > 0 && store) { + return getStoryPath(doc.stories[0], doc, store, activeTab); } const route = doc ? doc.route || `${ensureStartingSlash( ensureTrailingSlash(basePath), )}${ensureTrailingSlash(strToId(doc.title))}${ - activeTab ? `${ensureTrailingSlash(activeTab)}` : '' + activeTab ? ensureTrailingSlash(activeTab) : '' }` : `${ensureStartingSlash( ensureTrailingSlash(basePath), )}${ensureTrailingSlash(strToId(name))}${ - activeTab ? `${ensureTrailingSlash(activeTab)}` : '' + activeTab ? ensureTrailingSlash(activeTab) : '' }`; return removeTrailingSlash(route); }; @@ -48,25 +51,31 @@ export const getDocPath = ( export const getStoryPath = ( storyId?: string, doc?: Document, - pagesConfig?: PagesOnlyRoutes, + store?: Store, tab?: string, ): string => { + const pagesConfig: PagesOnlyRoutes | undefined = store + ? (store.config.pages as PagesOnlyRoutes) + : undefined; + const docType = doc?.type || defDocType; const activeTab = doc?.MDXPage ? undefined : tab; if (!storyId) { - return getDocPath(docType, doc, pagesConfig, undefined, activeTab); + return getDocPath(docType, doc, store, undefined, activeTab); } - const { basePath = '' } = pagesConfig?.[docType] || {}; const docRoute = `${ doc?.route ? ensureStartingSlash(ensureTrailingSlash(doc?.route)) - : `${ensureStartingSlash(ensureTrailingSlash(basePath))}` - }`; - const route = `${docRoute}${storyId ? ensureTrailingSlash(storyId) : ''}${ - activeTab ? `${ensureTrailingSlash(activeTab)}` : '' + : ensureStartingSlash(ensureTrailingSlash(basePath)) }`; - return removeTrailingSlash(route); + const story = store?.stories[storyId]; + const { factoryId, name } = story || {}; + const id = factoryId || storyId; + const route = `${docRoute}${id ? ensureTrailingSlash(id) : ''}${ + activeTab ? ensureTrailingSlash(activeTab) : '' + }${factoryId ? `?story=${name}` : ''}`; + return encodeURI(removeTrailingSlash(route)); }; export const getDocTypePath = (type: PageConfiguration) => @@ -76,3 +85,22 @@ export const getDocTypePath = (type: PageConfiguration) => export const docStoryToId = (docId: string, storyId: string) => toId(docId, storyNameFromExport(storyId)); + +/** + * maps an exported story to an array of stories. Used for dynamically created stories. + */ +export const mapDynamicStories = (story: Story, doc: Document): Story[] => { + if (story.factory && typeof story.renderFn === 'function') { + const stories = story.renderFn(doc); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, name, ...storyProps } = story; + return Array.isArray(stories) + ? stories.map(s => ({ + ...storyProps, + factoryId: docStoryToId(doc.title, id || name), + ...s, + })) + : [story]; + } + return [story]; +}; diff --git a/core/core/src/document.ts b/core/core/src/document.ts index 12adebf48..6625aa032 100644 --- a/core/core/src/document.ts +++ b/core/core/src/document.ts @@ -154,8 +154,24 @@ export type Story = { * optional story subtitle property */ subtitle?: string; + /** + * if set to true, the function is a stories factory, returns a list of Story objects + */ + factory?: boolean; + + /** + * if the story was created by a dynacmi storiers factory, this is the original 'parent' factory id. + * it is set internally and will be used to create a story URL + */ + factoryId?: string; } & StoryProps; +/** + * dynamic story factory function type. + * returns an array of dynamically loaded stories + */ +export type StoryFactoryFn = (doc: Document) => Story[]; + export const defDocType: DocType = 'story'; /** * A documentation file's metadata. diff --git a/core/store/src/create-pages/pages-paths.ts b/core/store/src/create-pages/pages-paths.ts index 8ab5e6e67..bb92d57b3 100644 --- a/core/store/src/create-pages/pages-paths.ts +++ b/core/store/src/create-pages/pages-paths.ts @@ -153,7 +153,7 @@ export const getDocPages = (store: Store): DocPagesPath[] => { ? doc.stories : [undefined]; stories.forEach((storyId?: string) => { - const path = getStoryPath(storyId, doc, pages, route); + const path = getStoryPath(storyId, doc, store, route); docPaths.push({ path, type: docType, @@ -172,7 +172,7 @@ export const getDocPages = (store: Store): DocPagesPath[] => { const path = getDocPath( type as DocType, { title: tag, componentsLookup: {} }, - pages, + store, ); docPaths.push({ path, diff --git a/core/store/src/serialization/load-store.ts b/core/store/src/serialization/load-store.ts index c1f385178..ebfdcae9a 100644 --- a/core/store/src/serialization/load-store.ts +++ b/core/store/src/serialization/load-store.ts @@ -14,6 +14,7 @@ import { PageConfiguration, Pages, PageLayoutProps, + mapDynamicStories, } from '@component-controls/core'; import { LoadingStore } from '@component-controls/loader'; import { render as reactRender } from '@component-controls/render/react'; @@ -70,22 +71,26 @@ export const loadStore = (store: LoadingStore): Store => { }; globalStore.docs[doc.title] = doc; Object.keys(storeStories).forEach((storyName: string) => { - const story: Story = storeStories[storyName]; - Object.assign(story, deepMerge(docStoryProps, story)); - story.controls = transformControls(story, doc, loadedComponents); - if (doc.title && story.id) { - const id = docStoryToId(doc.title, story.id); - if (!doc.stories) { - doc.stories = []; + const exportedStory: Story = storeStories[storyName]; + let stories: Story[] = mapDynamicStories(exportedStory, doc); + stories.forEach(story => { + story.id = story.id || story.name; + Object.assign(story, deepMerge(docStoryProps, story)); + story.controls = transformControls(story, doc, loadedComponents); + if (doc.title && story.id) { + const id = docStoryToId(doc.title, story.id); + if (!doc.stories) { + doc.stories = []; + } + doc.stories.push(id); + globalStore.stories[id] = { + ...story, + name: storyNameFromExport(story.name), + id, + doc: doc.title, + }; } - doc.stories.push(id); - globalStore.stories[id] = { - ...story, - name: storyNameFromExport(story.name), - id, - doc: doc.title, - }; - } + }); }); } }); diff --git a/core/store/src/state/context/document.tsx b/core/store/src/state/context/document.tsx index 48a36c91f..7ab8e8542 100644 --- a/core/store/src/state/context/document.tsx +++ b/core/store/src/state/context/document.tsx @@ -9,7 +9,7 @@ import { getDocPath, getComponentName, } from '@component-controls/core'; -import { useStore, useConfig, useActiveTab } from './store'; +import { useStore, useActiveTab } from './store'; const DocumentContext = createContext(undefined); @@ -190,7 +190,7 @@ const useNavigationLinks = (doc: Document): NavigationResult => { const docId = doc.title; const type = doc.type || defDocType; const docs = useDocByType(type); - const config = useConfig(); + const store = useStore(); const activeTab = useActiveTab(); const result: NavigationResult = {}; //next page @@ -202,7 +202,7 @@ const useNavigationLinks = (doc: Document): NavigationResult => { link: getDocPath( nextDoc.type || defDocType, nextDoc, - config.pages, + store, nextDoc.title, activeTab, ), @@ -217,7 +217,7 @@ const useNavigationLinks = (doc: Document): NavigationResult => { link: getDocPath( prevDoc.type || defDocType, prevDoc, - config.pages, + store, prevDoc.title, activeTab, ), @@ -252,16 +252,15 @@ export const useDocumentPath: UseGetDocumentPath = ( activeTab, ) => { const doc = useDocument(docId); - const config = useConfig(); - return getDocPath(type, doc, config.pages, docId, activeTab); + const store = useStore(); + return getDocPath(type, doc, store, docId, activeTab); }; export const useGetDocumentPath = (): UseGetDocumentPath => { const store = useStore(); - const config = useConfig(); return (type = defDocType, docId, activeTab) => { const doc = store.docs[docId]; - return getDocPath(type, doc, config.pages, docId, activeTab); + return getDocPath(type, doc, store, docId, activeTab); }; }; diff --git a/core/store/src/state/context/story.tsx b/core/store/src/state/context/story.tsx index c2b32e9e1..4e4f01dad 100644 --- a/core/store/src/state/context/story.tsx +++ b/core/store/src/state/context/story.tsx @@ -13,7 +13,7 @@ import { getComponentName, Component, } from '@component-controls/core'; -import { useStore, StoreContext, useActiveTab, useConfig } from './store'; +import { useStore, StoreContext, useActiveTab } from './store'; interface StoryContextProps { story?: Story; @@ -160,21 +160,19 @@ export const useStoryComponent = ( export const useStoryPath = (storyId: string): string => { const store = useStore(); const activeTab = useActiveTab(); - const config = useConfig(); const story = store.stories[storyId]; if (!story) { return ''; } const doc = store.docs[story?.doc || '']; - return getStoryPath(story.id, doc, config.pages, activeTab); + return getStoryPath(story.id, doc, store, activeTab); }; export const useGetStoryPath = () => { const store = useStore(); - const config = useConfig(); return (storyId: string, activeTab?: string): string => { const story = store.stories[storyId]; const doc = story && story.doc ? store.docs[story.doc] : undefined; - return getStoryPath(storyId, doc, config.pages, activeTab); + return getStoryPath(storyId, doc, store, activeTab); }; }; diff --git a/core/store/src/state/recoil/document.ts b/core/store/src/state/recoil/document.ts index 6756e8297..9bc5981fb 100644 --- a/core/store/src/state/recoil/document.ts +++ b/core/store/src/state/recoil/document.ts @@ -16,13 +16,7 @@ import { getDocPath, getComponentName, } from '@component-controls/core'; -import { - storeState, - useStore, - useConfig, - configState, - activeTabState, -} from './store'; +import { storeState, useStore, activeTabState } from './store'; export const documentIdState = atom({ key: 'document_id', @@ -213,7 +207,7 @@ const navigationState = selector({ key: 'navigation_selector', get: ({ get }) => { const doc = get(currentDocumentState); - const config = get(configState); + const store = get(storeState); const activeTab = get(activeTabState); const result: NavigationResult = {}; @@ -230,7 +224,7 @@ const navigationState = selector({ link: getDocPath( nextDoc.type || defDocType, nextDoc, - config.pages, + store, nextDoc.title, activeTab, ), @@ -246,7 +240,7 @@ const navigationState = selector({ link: getDocPath( prevDoc.type || defDocType, prevDoc, - config.pages, + store, prevDoc.title, activeTab, ), @@ -280,16 +274,16 @@ export const useDocumentPath: UseGetDocumentPath = ( activeTab, ) => { const doc = useDocument(docId); - const config = useConfig(); - return getDocPath(type, doc, config.pages, name, activeTab); + const store = useStore(); + return getDocPath(type, doc, store, name, activeTab); }; export const useGetDocumentPath = (): UseGetDocumentPath => { const getDoc = useGetDocument(); - const config = useConfig(); + const store = useStore(); return (type = defDocType, docId, activeTab) => { const doc = getDoc(docId); - return getDocPath(type, doc, config.pages, docId, activeTab); + return getDocPath(type, doc, store, docId, activeTab); }; }; diff --git a/core/store/src/state/recoil/story.ts b/core/store/src/state/recoil/story.ts index 4dc40f9e7..b956341b4 100644 --- a/core/store/src/state/recoil/story.ts +++ b/core/store/src/state/recoil/story.ts @@ -126,21 +126,19 @@ export const useStoryComponent = ( export const useStoryPath = (storyId: string): string => { const store = useStore(); const activeTab = useActiveTab(); - const config = useConfig(); const story = store.stories[storyId]; if (!story) { return ''; } const doc = store.docs[story?.doc || '']; - return getStoryPath(story.id, doc, config.pages, activeTab); + return getStoryPath(story.id, doc, store, activeTab); }; export const useGetStoryPath = () => { const store = useStore(); - const config = useConfig(); return (storyId: string, activeTab?: string): string => { const story = store.stories[storyId]; const doc = story && story.doc ? store.docs[story.doc] : undefined; - return getStoryPath(storyId, doc, config.pages, activeTab); + return getStoryPath(storyId, doc, store, activeTab); }; }; diff --git a/core/webpack-compile/tests/__snapshots__/example-stories.test.ts.snap b/core/webpack-compile/tests/__snapshots__/example-stories.test.ts.snap index 73efe48a8..43d6017c0 100644 --- a/core/webpack-compile/tests/__snapshots__/example-stories.test.ts.snap +++ b/core/webpack-compile/tests/__snapshots__/example-stories.test.ts.snap @@ -478,6 +478,16 @@ Button.propTypes = { }, "version": "1.27.0", }, + "2ee4e194b81724461f4c45baab611609": Object { + "fileHash": "2ee4e194b81724461f4c45baab611609", + "name": "component-controls-stories", + "repository": Object { + "browse": "https://github.com/ccontrols/component-controls/tree/master/examples/stories/src/stories/dynamic-stories.stories.tsx", + "docs": "https://github.com/ccontrols/component-controls/tree/master#readme", + "issues": "https://github.com/ccontrols/component-controls/issues", + }, + "version": "1.27.0", + }, "af3ea9029e5eedf5278b612cda3d978c": Object { "fileHash": "af3ea9029e5eedf5278b612cda3d978c", "name": "component-controls-stories", @@ -1972,6 +1982,54 @@ any *markdown* is allowed }, }, }, + Object { + "doc": Object { + "author": "atanasster", + "components": Object {}, + "componentsLookup": Object {}, + "date": "2020-10-08T17:00:16.577Z", + "dateModified": "2020-10-09T01:08:05.017Z", + "description": "ESM story file to demostrate creating 'dynamic' stories at run-time. Creates a story iterating through each theme color", + "fileName": "/Users/atanasster/component-controls/examples/stories/src/stories/dynamic-stories.stories.tsx", + "package": "2ee4e194b81724461f4c45baab611609", + "title": "Introduction/Dynamic stories", + }, + "filePath": "/Users/atanasster/component-controls/examples/stories/src/stories/dynamic-stories.stories.tsx", + "stories": Object { + "buttonColors": Object { + "arguments": Array [], + "factory": true, + "id": "buttonColors", + "loc": Object { + "end": Object { + "column": 1, + "line": 27, + }, + "start": Object { + "column": 28, + "line": 13, + }, + }, + "name": "buttonColors", + "renderFn": [Function], + "source": "() => { + return Object.keys(theme.colors) + .filter(color => typeof theme.colors[color] === 'string') + .map(color => { + return { + name: color, + source: \`\`, + renderFn: () => ( + + ), + }; + }); +}", + }, + }, + }, Object { "doc": Object { "author": "atanasster", diff --git a/examples/stories/src/stories/dynamic-stories.stories.tsx b/examples/stories/src/stories/dynamic-stories.stories.tsx new file mode 100644 index 000000000..0b9fc3e91 --- /dev/null +++ b/examples/stories/src/stories/dynamic-stories.stories.tsx @@ -0,0 +1,29 @@ +/* eslint-disable react/display-name */ +/** @jsx jsx */ +import { jsx, Button } from 'theme-ui'; +import { theme } from '@component-controls/components'; + +export default { + title: 'Introduction/Dynamic stories', + author: 'atanasster', + description: + "ESM story file to demostrate creating 'dynamic' stories at run-time. Creates a story iterating through each theme color", +}; + +export const buttonColors = () => { + return Object.keys(theme.colors) + .filter(color => typeof theme.colors[color] === 'string') + .map(color => { + return { + name: color, + source: ``, + renderFn: () => ( + + ), + }; + }); +}; + +buttonColors.factory = true; diff --git a/examples/stories/src/tutorial/reference/story.mdx b/examples/stories/src/tutorial/reference/story.mdx index 1134c45d4..2c6d097ad 100644 --- a/examples/stories/src/tutorial/reference/story.mdx +++ b/examples/stories/src/tutorial/reference/story.mdx @@ -426,3 +426,36 @@ overview.decorators = [ story decorators + +### factory + +type `boolean` + +If this flag is set to true, the story is considered a `factory` for creating dynamic stories at run-time + +[ESM](/tutorial/esmodules-stories) +``` +import React from 'react'; +export default { + title: 'Library/Components/Button', +}; + +export const buttonColors = () => { + return Object.keys(theme.colors) + .filter(color => typeof theme.colors[color] === 'string') + .map(color => { + return { + name: color, + source: ``, + renderFn: () => ( + + ), + }; + }); +}; + +buttonColors.factory = true; +``` + diff --git a/examples/stories/src/tutorial/write-documentation/esmodules-stories.mdx b/examples/stories/src/tutorial/write-documentation/esmodules-stories.mdx index 8f1180ea1..4174052f6 100644 --- a/examples/stories/src/tutorial/write-documentation/esmodules-stories.mdx +++ b/examples/stories/src/tutorial/write-documentation/esmodules-stories.mdx @@ -15,7 +15,7 @@ You can create interactive component examples (aka stories) in pure Javascript o To do so, you need to create a [default export](#metadata) with at least a title attribute and at least one [named export](#stories), which is considered a story. -Our ESM stories format is loosely compatible with storybook's [CSF](https://storybook.js.org/docs/formats/component-story-format/) with a few properties in common (ie title, component, decorators). However, our ESM stories format is fully extensible beyond just using a select few hard-coded props (ie parameters). +Our ESM story format is loosely compatible with Storybook's [CSF](https://storybook.js.org/docs/formats/component-story-format/) with a few properties in common (ie title, component, decorators). However, our ESM story format is fully extensible beyond just using a select few hard-coded props (ie parameters). ## Advantages @@ -36,7 +36,7 @@ By default, your ESM story files should end with `.(story|stories).(js|jsx|ts|ts ## Metadata -All the document metadata is exported as the default module export (there can be only one default export per es module). For example the document(page) title, component etc. are defined as properties: +All the document metadata is exported as the default module export (there can be only one default export per es module). For example the document(page) title, component, etc. are defined as properties: ``` export default { @@ -76,3 +76,38 @@ myStory.story = { }; ``` + +### Dynamic stories + +You can create multiple stories dynamically (at run time) from a function To do this, all you need is to add `factory = true` flag to the exported story: + +``` +import React from 'react'; +export default { + title: Components/Button', +}; + +export const buttonColors = () => { + return Object.keys(theme.colors) + .filter(color => typeof theme.colors[color] === 'string') + .map(color => { + return { + name: color, + source: ``, + renderFn: () => ( + + ), + }; + }); +}; + +buttonColors.factory = true; +``` +[live example](/api/introduction-dynamic-stories--button-colors/?story=Background) + +**Important**: The dynamic stories are created at run-time (when the site loads in the browser), while the pages static routes are assembled during the static site building process. This makes it so all the newly created dynamic stories will reside under the URL of the parent *factory* story, with a `story` parameter: + +`https://xxx/api/components-button--button-colors/?story=Background` + diff --git a/ui/app/package.json b/ui/app/package.json index 933ac902f..055f03c12 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -35,6 +35,7 @@ "@component-controls/core": "^1.27.3", "@component-controls/pages": "^1.27.3", "@component-controls/store": "^1.27.3", + "query-string": "^6.13.5", "react": "^16.13.1", "react-dom": "^16.13.1", "react-helmet": "^6.0.0", @@ -43,6 +44,7 @@ "devDependencies": { "@component-controls/jest-snapshots": "^1.27.3", "@component-controls/ts-markdown-docs": "^1.21.0", + "@types/query-string": "^6.3.0", "@types/react-helmet": "^6.0.0", "cross-env": "^5.2.1", "eslint": "^6.5.1" diff --git a/ui/app/src/AppContext/AppContext.tsx b/ui/app/src/AppContext/AppContext.tsx index b9435e23d..fbe707128 100644 --- a/ui/app/src/AppContext/AppContext.tsx +++ b/ui/app/src/AppContext/AppContext.tsx @@ -1,7 +1,8 @@ /** @jsx jsx */ import { FC } from 'react'; import { jsx } from 'theme-ui'; -import { Store } from '@component-controls/core'; +import queryString from 'query-string'; +import { Store, docStoryToId } from '@component-controls/core'; import { SidebarContextProvider, LinkContextProvider, @@ -27,9 +28,17 @@ export const AppContext: FC = ({ linkClass, activeTab, }) => { + const query = queryString.parse(location.search); + const dynStoryId = + docId && + storyId && + !store.stories[storyId] && + typeof query.story === 'string' + ? docStoryToId(docId, query.story) + : storyId; return ( { const doc = useCurrentDocument(); const { author, tags } = doc || {}; - const config = useConfig(); + const store = useStore(); return ( { + {author} } diff --git a/ui/app/src/Sidebar/Sidebar.tsx b/ui/app/src/Sidebar/Sidebar.tsx index eb804238b..62c144e5b 100644 --- a/ui/app/src/Sidebar/Sidebar.tsx +++ b/ui/app/src/Sidebar/Sidebar.tsx @@ -8,6 +8,7 @@ import { useDocByType, useConfig, useActiveTab, + useCurrentStory, } from '@component-controls/store'; import { Sidebar as AppSidebar, @@ -25,7 +26,6 @@ import { DocType, Pages, defDocType, - RunConfiguration, Store, getStoryPath, getDocPath, @@ -47,7 +47,6 @@ export interface SidebarProps { const createMenuItem = ( store: Store, - config: RunConfiguration, doc: Document, type: DocType, levels: string[], @@ -66,7 +65,7 @@ const createMenuItem = ( return ''; } const doc = story.doc ? store.docs[story.doc] : undefined; - return getStoryPath(story.id, doc, config.pages, activeTab); + return getStoryPath(story.id, doc, store, activeTab); }; const documentPath = ( @@ -75,7 +74,7 @@ const createMenuItem = ( activeTab?: string, ): string => { const doc = store.docs[name]; - return getDocPath(type, doc, config.pages, name, activeTab); + return getDocPath(type, doc, store, name, activeTab); }; const newItem: MenuItem = { id: levels[0], @@ -120,7 +119,6 @@ const createMenuItem = ( } return createMenuItem( store, - config, doc, type, levels.slice(1), @@ -170,7 +168,8 @@ export const Sidebar: FC = ({ const store = useStore(); const activeTab = useActiveTab(); const { title: docId } = useCurrentDocument() || {}; - + const story = useCurrentStory(); + const activeId = story ? story.id : docId; const config = useConfig() || {}; const { pages, menu, sidebar = [] } = config; const page: PageConfiguration = useMemo(() => pages?.[type] || {}, [ @@ -192,7 +191,6 @@ export const Sidebar: FC = ({ } createMenuItem( store, - config, doc, type, [title], @@ -204,13 +202,13 @@ export const Sidebar: FC = ({ } } const levels = title.split('/'); - createMenuItem(store, config, doc, type, levels, page, activeTab, acc); + createMenuItem(store, doc, type, levels, page, activeTab, acc); return acc; }, staticMenus); return menuItems; } return staticMenus; - }, [type, activeTab, store, config, page, docs, menu]); + }, [type, activeTab, store, page, docs, menu]); const [search, setSearch] = useState(undefined); const actions: ActionItems = [...sidebar]; @@ -237,7 +235,7 @@ export const Sidebar: FC = ({ ), id: 'filter', }); - + console.log(docId); return ( {responsive && ( @@ -248,7 +246,11 @@ export const Sidebar: FC = ({ )} - + ); diff --git a/ui/app/tests/__snapshots__/stories.test.js.snap b/ui/app/tests/__snapshots__/stories.test.js.snap index 9f5ca6953..f0290c596 100644 --- a/ui/app/tests/__snapshots__/stories.test.js.snap +++ b/ui/app/tests/__snapshots__/stories.test.js.snap @@ -1790,7 +1790,7 @@ exports[`Application/Sidebar Overview 1`] = ` class="css-j6yaho" > + + + +
= ({ doc, link }) => { const { type = defDocType, tags = [], date, author } = doc; - const config = useConfig(); + const store = useStore(); const dateNode = date ? ( {date ? ( @@ -43,7 +43,7 @@ export const DocumentItem: FC = ({ doc, link }) => { {date && ,} by - + {author} diff --git a/ui/blocks/src/TagsList/TagsList.tsx b/ui/blocks/src/TagsList/TagsList.tsx index aab7436f8..9c656af14 100644 --- a/ui/blocks/src/TagsList/TagsList.tsx +++ b/ui/blocks/src/TagsList/TagsList.tsx @@ -3,7 +3,7 @@ import { FC, useState, useEffect } from 'react'; import { jsx, Box } from 'theme-ui'; import { getDocPath } from '@component-controls/core'; import { Tag, Link, getPaletteColor } from '@component-controls/components'; -import { useConfig, useDocPropCount } from '@component-controls/store'; +import { useStore, useDocPropCount } from '@component-controls/store'; export interface TagsListProps { /** @@ -18,7 +18,7 @@ export interface TagsListProps { export const TagsList: FC = ({ tags }) => { const [tagColors, setTagColors] = useState<{ [tag: string]: string }>({}); const tagCounts = useDocPropCount('tags'); - const config = useConfig(); + const store = useStore(); useEffect(() => { setTagColors( Object.keys(tagCounts).reduce( @@ -34,7 +34,7 @@ export const TagsList: FC = ({ tags }) => { return tags ? ( {tags.map(tag => ( - + {tag} diff --git a/yarn.lock b/yarn.lock index 7b8a4cac3..24fca17a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5127,6 +5127,13 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== +"@types/query-string@^6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.3.0.tgz#b6fa172a01405abcaedac681118e78429d62ea39" + integrity sha512-yuIv/WRffRzL7cBW+sla4HwBZrEXRNf1MKQ5SklPEadth+BKbDxiVG8A3iISN5B3yC4EeSCzMZP8llHTcUhOzQ== + dependencies: + query-string "*" + "@types/reach__router@^1.2.3", "@types/reach__router@^1.3.3", "@types/reach__router@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.5.tgz#14e1e981cccd3a5e50dc9e969a72de0b9d472f6d" @@ -19406,6 +19413,15 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@*, query-string@^6.13.5: + version "6.13.5" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.5.tgz#99e95e2fb7021db90a6f373f990c0c814b3812d8" + integrity sha512-svk3xg9qHR39P3JlHuD7g3nRnyay5mHbrPctEBDUxUkHRifPHXJDhBUycdCC0NBjXoDf44Gb+IsOZL1Uwn8M/Q== + dependencies: + decode-uri-component "^0.2.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"