diff --git a/core/store/src/Store/HMRStore.ts b/core/store/src/Store/HMRStore.ts index 67a1f2ba0..77fbb3a71 100644 --- a/core/store/src/Store/HMRStore.ts +++ b/core/store/src/Store/HMRStore.ts @@ -1,11 +1,11 @@ import { LoadingStore } from '@component-controls/loader'; import { Store } from './Store'; -import { loadStoryStore } from '../serialization/load-store'; +import { loadStore } from '../serialization/load-store'; import { StoryStore } from '../types'; export class HMRStore extends Store { constructor(store?: LoadingStore) { - super(store ? loadStoryStore(store) : undefined); + super(store ? loadStore(store) : undefined); } hmr = (store?: LoadingStore): StoryStore => { diff --git a/core/store/src/create-pages/index.ts b/core/store/src/create-pages/index.ts new file mode 100644 index 000000000..323f2c000 --- /dev/null +++ b/core/store/src/create-pages/index.ts @@ -0,0 +1 @@ +export * from './pages-paths'; diff --git a/core/store/src/create-pages/pages-paths.ts b/core/store/src/create-pages/pages-paths.ts new file mode 100644 index 000000000..759253859 --- /dev/null +++ b/core/store/src/create-pages/pages-paths.ts @@ -0,0 +1,167 @@ +import { + StoriesStore, + defDocType, + DocType, + getDocTypePath, + removeTrailingSlash, + ensureStartingSlash, + Pages, + TabConfiguration, + getDocPath, + getStoryPath, +} from '@component-controls/core'; + +import { HomePageInfo } from '../types'; + +export const getIndexPage = (store: StoriesStore): HomePageInfo => { + const docs = Object.keys(store.docs); + const homePageId = docs.find(key => { + const doc = store.docs[key]; + return doc.route === '/'; + }); + const homePage = homePageId + ? store.docs[homePageId] + : docs.length > 0 + ? store.docs[docs[0]] + : undefined; + return { + docId: homePage?.title, + type: homePage?.type || defDocType, + }; +}; + +export interface DocHomePagesPath { + type: DocType; + path: string; + docId?: string; +} +export const getHomePages = (store: StoriesStore): DocHomePagesPath[] => { + const { pages = {} } = store?.config || {}; + if (pages) { + const docs = Object.keys(store.docs); + const paths: DocHomePagesPath[] = Object.keys(pages).map( + (type: DocType) => { + const page = pages[type]; + const path = getDocTypePath(page) as string; + + const docId = + docs.find(key => { + const doc = store.docs[key]; + return ( + removeTrailingSlash(ensureStartingSlash(doc?.route || '')) === + path + ); + }) || docs.find(key => (store.docs[key].type || defDocType) === type); + return { + type, + path, + docId, + }; + }, + ); + return paths; + } + return []; +}; + +export const getPageList = ( + store: StoriesStore, + type: DocType = defDocType, +): Pages => { + if (store) { + return Object.keys(store.docs).reduce((acc: Pages, key: string) => { + const doc = store.docs[key]; + if (doc) { + const { type: docType = defDocType } = doc; + if (docType === type) { + return [...acc, doc]; + } + } + return acc; + }, []); + } + return []; +}; + +export const getUniquesByField = ( + store: StoriesStore, + field: string, +): { [key: string]: number } => { + return Object.keys(store.docs).reduce( + (acc: { [key: string]: number }, key) => { + const doc = store.docs[key]; + const value = (doc as any)[field]; + const values = Array.isArray(value) ? value : [value]; + values.forEach(v => { + if (v !== undefined) { + if (typeof acc[v] === 'undefined') { + acc[v] = 0; + } + acc[v] = acc[v] = 1; + } + }); + return acc; + }, + {}, + ); +}; + +export interface DocPagesPath { + type: DocType; + path: string; + docId?: string | null; + storyId?: string | null; + category?: string | null; + activeTab?: string | null; +} +export const getDocPages = (store: StoriesStore): DocPagesPath[] => { + const { pages = {}, categories = [] } = store?.config || {}; + const docPaths: DocPagesPath[] = []; + Object.keys(pages).forEach(type => { + if (!categories.includes(type as DocType)) { + const page = pages[type as DocType]; + const docType = type as DocType; + const docs: Pages = getPageList(store, docType); + const tabs: Pick[] = page.tabs || [ + { route: undefined }, + ]; + tabs.forEach((tab, tabIndex) => { + const route = tabIndex > 0 ? tab.route : undefined; + docs.forEach(doc => { + if (doc.route !== '/') { + const stories = + page.storyPaths && doc.stories?.length + ? doc.stories + : [undefined]; + stories.forEach((storyId?: string) => { + const path = getStoryPath(storyId, doc, pages, route); + docPaths.push({ + path, + type: docType, + activeTab: route, + docId: doc.title, + storyId, + }); + }); + } + }); + }); + } else { + const cats = getUniquesByField(store, type); + const catKeys = Object.keys(cats); + catKeys.forEach(tag => { + const path = getDocPath( + type as DocType, + { title: tag, componentsLookup: {} }, + pages, + ); + docPaths.push({ + path, + type, + category: tag, + }); + }); + } + }); + return docPaths; +}; diff --git a/core/store/src/hooks/document.ts b/core/store/src/hooks/document.ts index 994399c08..cbb2a275d 100644 --- a/core/store/src/hooks/document.ts +++ b/core/store/src/hooks/document.ts @@ -108,46 +108,16 @@ const docsByTypeSelector = selectorFamily({ get: type => ({ get }) => { const store = get(storeAtom); const docs = store.docs; - const { storySort } = get(configSelector) || {}; - let resultDocs = Object.keys(docs).reduce((acc: Pages, key: string) => { + return Object.keys(docs).reduce((acc: Pages, key: string) => { const doc: Document | undefined = docs[key]; if (doc) { const { type: docType = defDocType } = doc; if (docType === type) { - return [...acc, doc]; + return [...acc, { ...doc }]; } } return acc; }, []); - - if (storySort) { - resultDocs = resultDocs.sort((a: Document, b: Document) => { - //@ts-ignore - const sort = storySort(a.title, b.title); - if (sort !== 0) { - return sort; - } - return resultDocs.indexOf(a) - resultDocs.indexOf(b); - }); - } - //split documents by their common 'parent' - resultDocs = resultDocs - .map(doc => { - const levels = doc.title.split('/'); - const parent = levels.slice(0, -1).join('/'); - return { id: doc, parent }; - }) - .sort((a, b) => { - if (a.parent === b.parent) { - return ( - (store.docs[a.id.title].order || 0) - - (store.docs[b.id.title].order || 0) - ); - } - return 0; - }) - .map(item => item.id); - return resultDocs; }, }); diff --git a/core/store/src/index.ts b/core/store/src/index.ts index 72898ec10..2a9217190 100644 --- a/core/store/src/index.ts +++ b/core/store/src/index.ts @@ -3,3 +3,4 @@ export * from './Store/HMRStore'; export * from './types'; export * from './hooks'; export * from './serialization/load-store'; +export * from './create-pages'; diff --git a/core/store/src/serialization/load-store.ts b/core/store/src/serialization/load-store.ts index b1e44f0e1..f1dadb403 100644 --- a/core/store/src/serialization/load-store.ts +++ b/core/store/src/serialization/load-store.ts @@ -12,78 +12,109 @@ import { storyNameFromExport, defDocType, PageConfiguration, + Pages, } from '@component-controls/core'; import { LoadingStore } from '@component-controls/loader'; import { transformControls } from './transform-controls'; -export const loadStoryStore = ( - store: LoadingStore, -): StoriesStore | undefined => { - if (store) { - try { - const { - stores, - packages: loadedPackages, - components: loadedComponents, - config = {}, - buildConfig = {}, - } = store; +export const loadStore = (store: LoadingStore): StoriesStore => { + const globalStore: StoriesStore = { + docs: {}, + stories: {}, + components: {}, + packages: {}, + config: {}, + }; + try { + const { + stores, + packages: loadedPackages, + components: loadedComponents, + config = {}, + buildConfig = {}, + } = store; - if (stores) { - const globalStore: StoriesStore = { - docs: {}, - stories: {}, - components: {}, - packages: {}, - config: deepMergeArrays( - buildConfig, - deepMergeArrays(defaultRunConfig, config), - ), - }; - stores.forEach(s => { - const storeDoc = s.doc; - const storeStories = s.stories; - if (storeDoc && storeStories && s.stories) { - const page = globalStore.config?.pages?.[ - storeDoc.type || defDocType - ] as PageConfiguration; - const doc: Document = deepMerge({ layout: page.layout }, storeDoc); - //props shared by document and story, extract so story gets default values from doc - const docStoryProps: StoryProps = { - component: doc.component, - subcomponents: doc.subcomponents, - controls: doc.controls, - smartControls: doc.smartControls, - decorators: doc.decorators, - }; - 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 = []; - } - doc.stories.push(id); - globalStore.stories[id] = { - ...story, - name: storyNameFromExport(story.name), - id, - doc: doc.title, - }; + if (stores) { + globalStore.config = deepMergeArrays( + buildConfig, + deepMergeArrays(defaultRunConfig, config), + ); + stores.forEach(s => { + const storeDoc = s.doc; + const storeStories = s.stories; + if (storeDoc && storeStories && s.stories) { + const page = globalStore.config?.pages?.[ + storeDoc.type || defDocType + ] as PageConfiguration; + const doc: Document = deepMerge({ layout: page.layout }, storeDoc); + //props shared by document and story, extract so story gets default values from doc + const docStoryProps: StoryProps = { + component: doc.component, + subcomponents: doc.subcomponents, + controls: doc.controls, + smartControls: doc.smartControls, + decorators: doc.decorators, + }; + 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 = []; } - }); + doc.stories.push(id); + globalStore.stories[id] = { + ...story, + name: storyNameFromExport(story.name), + id, + doc: doc.title, + }; + } + }); + } + }); + globalStore.packages = loadedPackages; + globalStore.components = loadedComponents; + const { storySort } = globalStore.config || {}; + let pages: Pages = Object.keys(globalStore.docs).map( + key => globalStore.docs[key], + ); + + if (storySort) { + pages = pages.sort((a: Document, b: Document) => { + const sort = storySort(a.title, b.title); + if (sort !== 0) { + return sort; } + return pages.indexOf(a) - pages.indexOf(b); }); - globalStore.packages = loadedPackages; - globalStore.components = loadedComponents; - return globalStore; } - } catch (e) { - console.error(e); + //split documents by their common 'parent' + const sortedDocs = pages + .map(doc => { + const levels = doc.title.split('/'); + const parent = levels.slice(0, -1).join('/'); + return { id: doc, parent }; + }) + .sort((a, b) => { + if (a.parent === b.parent) { + return ( + (globalStore.docs[a.id.title].order || 0) - + (globalStore.docs[b.id.title].order || 0) + ); + } + return 0; + }); + globalStore.docs = sortedDocs.reduce((acc, d) => { + const doc = d.id; + return { ...acc, [doc.title]: doc }; + }, {}); } + } catch (e) { + console.error(e); } - return undefined; + return globalStore; }; diff --git a/core/store/src/webpack/loader.ts b/core/store/src/webpack/loader.ts index f622d9cd7..e0d500da4 100644 --- a/core/store/src/webpack/loader.ts +++ b/core/store/src/webpack/loader.ts @@ -6,14 +6,12 @@ module.exports = function(content: string) { const options = getOptions(context) || {}; const { bundleFileName } = options; content = ` - import { HMRStore } from './dist/types'; - export let store = new HMRStore( - require('${bundleFileName}'), - ); + import { loadStore } from './dist/types'; + export let store = loadStore(require('${bundleFileName}')); if (module.hot) { module.hot.accept('${bundleFileName}', () => { import('${bundleFileName}').then(updated => { - store = store.hmr(updated); + store = loadStore(updated); }); }); } diff --git a/integrations/gatsby-theme-stories/src/components/Layout.tsx b/integrations/gatsby-theme-stories/src/components/Layout.tsx index fbdedfd74..2ad42c5ea 100644 --- a/integrations/gatsby-theme-stories/src/components/Layout.tsx +++ b/integrations/gatsby-theme-stories/src/components/Layout.tsx @@ -1,7 +1,6 @@ /** @jsx jsx */ import { FC } from 'react'; import { jsx } from 'theme-ui'; -import { DocType } from '@component-controls/core'; import { AppContext } from '@component-controls/app'; import { store } from '@component-controls/store/controls-store'; @@ -10,14 +9,12 @@ import { GatsbyLink } from './GatsbyLink'; interface LayoutProps { docId?: string; storyId?: string; - type?: DocType; activeTab?: string; } export const Layout: FC = ({ docId, storyId, - type, children, activeTab, }) => { @@ -27,7 +24,6 @@ export const Layout: FC = ({ storyId={storyId} store={store} linkClass={GatsbyLink} - type={type} activeTab={activeTab} > {children} diff --git a/integrations/gatsby-theme-stories/src/gatsby-node.ts b/integrations/gatsby-theme-stories/src/gatsby-node.ts index 31a9508c9..f6f2cd0af 100644 --- a/integrations/gatsby-theme-stories/src/gatsby-node.ts +++ b/integrations/gatsby-theme-stories/src/gatsby-node.ts @@ -5,7 +5,16 @@ import { getBundleName, } from '@component-controls/webpack-compile'; import { CreatePagesArgs, CreateWebpackConfigArgs } from 'gatsby'; -import { HMRStore } from '@component-controls/store'; +import { StoriesStore } from '@component-controls/core'; +import { + getIndexPage, + getHomePages, + DocHomePagesPath, + getDocPages, + DocPagesPath, + loadStore, +} from '@component-controls/store'; + const { StorePlugin } = require('@component-controls/store/plugin'); const defaultPresets = ['react', 'react-docgen-typescript']; @@ -24,10 +33,9 @@ exports.createPages = async ( ? await watch(config) : await compile(config); if (bundleName) { - const bundle = require(bundleName); - const store = new HMRStore(bundle); + const store: StoriesStore = loadStore(require(bundleName)); //home page - const { docId = null, type = null } = store.getIndexPage() || {}; + const { docId = null, type = null } = getIndexPage(store) || {}; createPage({ path: `/`, component: require.resolve(`../src/templates/DocPage.tsx`), @@ -36,9 +44,8 @@ exports.createPages = async ( type, }, }); - const paths: string[] = store.getHomePaths(); - paths.forEach(path => { - const { type = null, docId = null } = store.getHomePage(path) || {}; + const homePages = getHomePages(store); + homePages.forEach(({ path, docId, type }: DocHomePagesPath) => { createPage({ path, component: require.resolve(`../src/templates/DocHome.tsx`), @@ -49,28 +56,29 @@ exports.createPages = async ( }); }); - const docPaths: string[] = store.getDocPaths(); - docPaths.forEach(path => { - const { - type = null, + const docPages = getDocPages(store); + docPages.forEach( + ({ + path, + type, docId = null, storyId = null, category = null, activeTab = null, - } = store.getDocPage(path) || {}; - - createPage({ - path, - component: require.resolve(`../src/templates/DocPage.tsx`), - context: { - type, - docId, - storyId, - category, - activeTab, - }, - }); - }); + }: DocPagesPath) => { + createPage({ + path, + component: require.resolve(`../src/templates/DocPage.tsx`), + context: { + type, + docId, + storyId, + category, + activeTab, + }, + }); + }, + ); } }; diff --git a/integrations/gatsby-theme-stories/src/templates/CategoryPage.tsx b/integrations/gatsby-theme-stories/src/templates/CategoryPage.tsx index 5e866573f..c66075114 100644 --- a/integrations/gatsby-theme-stories/src/templates/CategoryPage.tsx +++ b/integrations/gatsby-theme-stories/src/templates/CategoryPage.tsx @@ -15,7 +15,7 @@ interface CategoryPageProps { const CategoryPageTemplate: FC = ({ pathContext: { type, category, docId }, }) => ( - + ); diff --git a/integrations/gatsby-theme-stories/src/templates/DocHome.tsx b/integrations/gatsby-theme-stories/src/templates/DocHome.tsx index 8da7ba92b..3805a26d0 100644 --- a/integrations/gatsby-theme-stories/src/templates/DocHome.tsx +++ b/integrations/gatsby-theme-stories/src/templates/DocHome.tsx @@ -15,7 +15,7 @@ const DocHomeTemplate: FC = ({ pathContext: { type = defDocType, docId }, }) => { return ( - + ); diff --git a/integrations/gatsby-theme-stories/src/templates/DocPage.tsx b/integrations/gatsby-theme-stories/src/templates/DocPage.tsx index 97f6d0260..c5354a7e6 100644 --- a/integrations/gatsby-theme-stories/src/templates/DocPage.tsx +++ b/integrations/gatsby-theme-stories/src/templates/DocPage.tsx @@ -17,7 +17,7 @@ const DocPageTemplate: FC = ({ pathContext: { docId, storyId, type, activeTab, category }, }) => { return ( - + ); diff --git a/integrations/nextjs-plugin/src/components/Layout.tsx b/integrations/nextjs-plugin/src/components/Layout.tsx index 3de35f465..226ae8056 100644 --- a/integrations/nextjs-plugin/src/components/Layout.tsx +++ b/integrations/nextjs-plugin/src/components/Layout.tsx @@ -1,23 +1,19 @@ /** @jsx jsx */ import { FC } from 'react'; import { jsx } from 'theme-ui'; -import { DocType } from '@component-controls/core'; import { AppContext } from '@component-controls/app'; import { store } from '../store'; - import { NextLink } from './NextLink'; interface LayoutProps { docId?: string; storyId?: string; - type?: DocType; activeTab?: string; } export const Layout: FC = ({ docId, storyId, - type, children, activeTab, }) => { @@ -27,7 +23,6 @@ export const Layout: FC = ({ storyId={storyId} store={store} linkClass={NextLink} - type={type} activeTab={activeTab} > {children} diff --git a/integrations/nextjs-plugin/src/store.ts b/integrations/nextjs-plugin/src/store.ts index 392b4cf6c..3967cad1d 100644 --- a/integrations/nextjs-plugin/src/store.ts +++ b/integrations/nextjs-plugin/src/store.ts @@ -1,3 +1,4 @@ -import { StoryStore, HMRStore } from '@component-controls/store'; -const bundle = require('./component-controls'); -export const store: StoryStore = new HMRStore(bundle); +import { StoriesStore } from '@component-controls/core'; +import { loadStore } from '@component-controls/store'; + +export const store: StoriesStore = loadStore(require('./component-controls')); diff --git a/ui/app/src/AppContext/AppContext.tsx b/ui/app/src/AppContext/AppContext.tsx index f7a0deff2..459595d0d 100644 --- a/ui/app/src/AppContext/AppContext.tsx +++ b/ui/app/src/AppContext/AppContext.tsx @@ -1,7 +1,7 @@ /** @jsx jsx */ import { FC } from 'react'; import { jsx } from 'theme-ui'; -import { DocType, defDocType } from '@component-controls/core'; +import { StoriesStore } from '@component-controls/core'; import { ThemeProvider } from '@component-controls/components'; import { SidebarContextProvider, @@ -9,21 +9,18 @@ import { LinkContextProviderProps, } from '@component-controls/components'; import { BlockContextProvider } from '@component-controls/blocks'; -import { StoryStore } from '@component-controls/store'; import { App } from '../App'; import { mdxComponents } from './mdxComponents'; export interface AppContextProps { - type?: DocType; docId?: string; storyId?: string; - store: StoryStore; + store: StoriesStore; linkClass: LinkContextProviderProps['linkClass']; activeTab?: string; } export const AppContext: FC = ({ - type = defDocType, docId, storyId, children, @@ -31,18 +28,11 @@ export const AppContext: FC = ({ linkClass, activeTab, }) => { - const { pages } = store.config || {}; - const page = pages?.[type]; - const documentId = docId - ? docId - : !docId && page?.layout?.navSidebar - ? store.getFirstDocument(type) - : undefined; return ( diff --git a/ui/app/src/Sidebar/Sidebar.tsx b/ui/app/src/Sidebar/Sidebar.tsx index 214b56445..c4ab0155d 100644 --- a/ui/app/src/Sidebar/Sidebar.tsx +++ b/ui/app/src/Sidebar/Sidebar.tsx @@ -74,7 +74,6 @@ const createMenuItem = ( activeTab?: string, ): string => { const doc = store.docs[name]; - const config = useConfig(); return getDocPath(type, doc, config?.pages, name, activeTab); }; const newItem: MenuItem = { diff --git a/ui/blocks/src/context/block/BlockContext.tsx b/ui/blocks/src/context/block/BlockContext.tsx index db0024912..4287d0250 100644 --- a/ui/blocks/src/context/block/BlockContext.tsx +++ b/ui/blocks/src/context/block/BlockContext.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { deepMerge } from '@component-controls/core'; +import { deepMerge, StoriesStore } from '@component-controls/core'; import { - StoryStore, documentIdAtom, storeAtom, storyIdAtom, @@ -25,7 +24,7 @@ export interface BlockContextInputProps { /** * store object */ - store: StoryStore; + store: StoriesStore; /** * active page tab */ @@ -38,31 +37,6 @@ export interface BlockContextInputProps { options?: object; } -export interface BlockContextProps { - /** - * current story - */ - storyId?: string; - /** - * current documentation page, if no story is selected - */ - docId?: string; - - /** - * store interface - */ - storeProvider: StoryStore; - - /** - * global options passed from container - * those are global parameters as well as decorators - */ - options?: object; -} - -//@ts-ignore -export const BlockContext = React.createContext({}); - export const BlockContextProvider: React.FC = ({ children, storyId: propsStoryId, @@ -74,10 +48,10 @@ export const BlockContextProvider: React.FC = ({ let storyId = propsStoryId; let docId = propsDocId; if (storyId && !docId) { - const story = store.getStory(storyId); + const story = store.stories[storyId]; docId = story?.doc; } else if (!storyId && docId) { - const doc = store.getStoryDoc(docId); + const doc = store.docs[docId]; storyId = doc && doc.stories && doc.stories.length ? doc.stories[0] : undefined; } @@ -85,7 +59,7 @@ export const BlockContextProvider: React.FC = ({ { set(documentIdAtom, docId); - set(storeAtom, store.store); + set(storeAtom, store); set(storyIdAtom, storyId); set(activeTabAtom, activeTab); set(optionsAtom, options || {}); diff --git a/ui/blocks/src/test/MockContext.tsx b/ui/blocks/src/test/MockContext.tsx index 10f9831ce..bca7f702f 100644 --- a/ui/blocks/src/test/MockContext.tsx +++ b/ui/blocks/src/test/MockContext.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Store } from '@component-controls/store'; import { BlockContextProvider } from '../context'; import { store } from './storyStore'; @@ -9,13 +8,12 @@ export interface MockContexProps { [key: string]: any; } -const storyStore = new Store(store); export const MockContext: React.FC = ({ children, storyId = 'id-of-story', }) => { return ( - + {children} );