From aec7673f5e934a62eece93cd6eea974353d6a6f5 Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Tue, 19 Apr 2022 19:06:14 +0200 Subject: [PATCH] feat(tree): groups --- .../src/components/Introduction.story.vue | 2 +- examples/vue3/vite.config.ts | 17 +++++ packages/histoire/src/client/app/App.vue | 2 +- .../client/app/components/app/Breadcrumb.vue | 2 +- .../client/app/components/tree/StoryGroup.vue | 75 ++++++++++++++++++ .../components/{story => tree}/StoryList.vue | 10 ++- .../{story => tree}/StoryListFolder.vue | 10 ++- .../{story => tree}/StoryListItem.vue | 6 +- .../histoire/src/client/app/stores/story.ts | 22 +++--- packages/histoire/src/client/app/types.ts | 10 ++- .../src/client/server/Story.server.vue | 6 ++ packages/histoire/src/node/collect/index.ts | 6 +- packages/histoire/src/node/config.ts | 11 ++- packages/histoire/src/node/plugin.ts | 2 +- packages/histoire/src/node/tree.ts | 76 ++++++++++++++++--- packages/histoire/src/node/types.ts | 7 ++ 16 files changed, 226 insertions(+), 38 deletions(-) create mode 100644 packages/histoire/src/client/app/components/tree/StoryGroup.vue rename packages/histoire/src/client/app/components/{story => tree}/StoryList.vue (64%) rename packages/histoire/src/client/app/components/{story => tree}/StoryListFolder.vue (86%) rename packages/histoire/src/client/app/components/{story => tree}/StoryListItem.vue (91%) diff --git a/examples/vue3/src/components/Introduction.story.vue b/examples/vue3/src/components/Introduction.story.vue index 0e8bf44e..e3386a62 100644 --- a/examples/vue3/src/components/Introduction.story.vue +++ b/examples/vue3/src/components/Introduction.story.vue @@ -1,5 +1,5 @@ diff --git a/examples/vue3/vite.config.ts b/examples/vue3/vite.config.ts index e54c7c00..3821d024 100644 --- a/examples/vue3/vite.config.ts +++ b/examples/vue3/vite.config.ts @@ -15,5 +15,22 @@ export default defineConfig({ theme: { logoHref: 'http://histoire.dev', }, + + tree: { + groups: [ + { + id: 'top', + title: '', + }, + { + title: 'My Group', + include: file => /Code gen|Controls|Docs/.test(file.title), + }, + { + title: 'Components', + include: file => !file.title.includes('Serialize'), + }, + ], + }, }, }) diff --git a/packages/histoire/src/client/app/App.vue b/packages/histoire/src/client/app/App.vue index 41655de3..17d120ca 100644 --- a/packages/histoire/src/client/app/App.vue +++ b/packages/histoire/src/client/app/App.vue @@ -1,7 +1,7 @@ + + diff --git a/packages/histoire/src/client/app/components/story/StoryList.vue b/packages/histoire/src/client/app/components/tree/StoryList.vue similarity index 64% rename from packages/histoire/src/client/app/components/story/StoryList.vue rename to packages/histoire/src/client/app/components/tree/StoryList.vue index be9de731..f1e9a6ce 100644 --- a/packages/histoire/src/client/app/components/story/StoryList.vue +++ b/packages/histoire/src/client/app/components/tree/StoryList.vue @@ -1,7 +1,8 @@ @@ -34,13 +34,13 @@ const folderPadding = computed(() => {
- + { {{ folder.title }}
+ +
@@ -66,7 +68,7 @@ const folderPadding = computed(() => { diff --git a/packages/histoire/src/client/app/components/story/StoryListItem.vue b/packages/histoire/src/client/app/components/tree/StoryListItem.vue similarity index 91% rename from packages/histoire/src/client/app/components/story/StoryListItem.vue rename to packages/histoire/src/client/app/components/tree/StoryListItem.vue index beb63de2..f22fc791 100644 --- a/packages/histoire/src/client/app/components/story/StoryListItem.vue +++ b/packages/histoire/src/client/app/components/tree/StoryListItem.vue @@ -14,7 +14,7 @@ const props = withDefaults(defineProps<{ }) const filePadding = computed(() => { - return (props.depth * 16) + 'px' + return (props.depth * 12) + 'px' }) const route = useRoute() @@ -36,9 +36,9 @@ useScrollOnActive(active, el) storyId: story.id, }, }" - class="htw-pl-0.5 htw-pr-2 htw-py-2 md:htw-py-1.5 htw-m-1 htw-rounded-sm" + class="htw-pl-0.5 htw-pr-2 htw-py-2 md:htw-py-1.5 htw-mx-1 htw-rounded-sm" > - + { return path.join('␜') } - function toggleFolder (path: Array, force?: boolean) { + function toggleFolder (path: Array, defaultToggleValue = true) { const stringPath = getStringPath(path) - if (force === undefined) { - force = !openedFolders.value.get(stringPath) - } + const currentValue = openedFolders.value.get(stringPath) - if (force) { - openedFolders.value.set(stringPath, true) + if (currentValue == null) { + openedFolders.value.set(stringPath, defaultToggleValue) + } else if (currentValue) { + openedFolders.value.set(stringPath, false) } else { - openedFolders.value.delete(stringPath) + openedFolders.value.set(stringPath, true) } } - function isFolderOpened (path: Array) { - return openedFolders.value.get(getStringPath(path)) + function isFolderOpened (path: Array, defaultValue = false) { + const value = openedFolders.value.get(getStringPath(path)) + if (value == null) { + return defaultValue + } + return value } function openFileFolders (path: Array) { diff --git a/packages/histoire/src/client/app/types.ts b/packages/histoire/src/client/app/types.ts index e37431b1..0518172a 100644 --- a/packages/histoire/src/client/app/types.ts +++ b/packages/histoire/src/client/app/types.ts @@ -11,6 +11,7 @@ export interface StoryFile { export interface Story { id: string title: string + group?: string variants: Variant[] layout?: { type: 'single' @@ -48,7 +49,14 @@ export type TreeFolder = { children: (TreeFolder | TreeLeaf)[] } -export type Tree = (TreeFolder | TreeLeaf)[] +export interface TreeGroup { + group: true + id: string + title: string + children: (TreeFolder | TreeLeaf)[] +} + +export type Tree = (TreeGroup | TreeFolder | TreeLeaf)[] export interface SearchResult { kind: 'story' | 'variant' diff --git a/packages/histoire/src/client/server/Story.server.vue b/packages/histoire/src/client/server/Story.server.vue index fb71bf5a..75859a16 100644 --- a/packages/histoire/src/client/server/Story.server.vue +++ b/packages/histoire/src/client/server/Story.server.vue @@ -11,6 +11,11 @@ export default defineComponent({ default: null, }, + group: { + type: String, + default: null, + }, + layout: { type: Object as PropType, default: () => ({ type: 'single' }), @@ -37,6 +42,7 @@ export default defineComponent({ const story: Story = { id: attrs.data.id, title: props.title ?? attrs.data.fileName, + group: props.group, layout: props.layout, icon: props.icon, iconColor: props.iconColor, diff --git a/packages/histoire/src/node/collect/index.ts b/packages/histoire/src/node/collect/index.ts index b928bdd6..ead59231 100644 --- a/packages/histoire/src/node/collect/index.ts +++ b/packages/histoire/src/node/collect/index.ts @@ -7,7 +7,7 @@ import pc from 'picocolors' import Tinypool from 'tinypool' import { createBirpc } from 'birpc' import type { StoryFile } from '../types.js' -import { createPath, TreeFile } from '../tree.js' +import { createPath } from '../tree.js' import type { Context } from '../context.js' import type { Payload, ReturnData } from './worker.js' @@ -79,11 +79,11 @@ export function useCollectStories (options: UseCollectStoriesOptions, ctx: Conte console.warn(pc.yellow(`⚠️ Multiple stories not supported: ${storyFile.path}`)) } storyFile.story = storyData[0] - const file: TreeFile = { + storyFile.treeFile = { title: storyData[0].title, path: relative(server.config.root, storyFile.path), } - storyFile.treePath = createPath(ctx.config, file) + storyFile.treePath = createPath(ctx.config, storyFile.treeFile) storyFile.story.title = storyFile.treePath[storyFile.treePath.length - 1] } catch (e) { console.error(pc.red(`Error while collecting story ${storyFile.path}:\n${e.frame ? `${pc.bold(e.message)}\n${e.frame}` : e.stack}`)) diff --git a/packages/histoire/src/node/config.ts b/packages/histoire/src/node/config.ts index 77cbb6fe..4c6f9a7d 100644 --- a/packages/histoire/src/node/config.ts +++ b/packages/histoire/src/node/config.ts @@ -13,17 +13,23 @@ type CustomizableColors = 'primary' | 'gray' type ColorKeys = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' type GrayColorKeys = ColorKeys | '750' | '850' | '950' -interface ResponsivePreset { +export interface ResponsivePreset { label: string width: number height: number } -interface BackgroundPreset { +export interface BackgroundPreset { label: string color: string } +export interface TreeGroupConfig { + title: string + id?: string + include?: (file: TreeFile) => boolean +} + export interface HistoireConfig { /** * Output directory. @@ -48,6 +54,7 @@ export interface HistoireConfig { */ file?: 'title' | 'path' | ((file: TreeFile) => string[]) order?: 'asc' | ((a: string, b: string) => number) + groups?: TreeGroupConfig[] } /** * Customize the look of the histoire book. diff --git a/packages/histoire/src/node/plugin.ts b/packages/histoire/src/node/plugin.ts index 0115ede6..0cf6cbc7 100644 --- a/packages/histoire/src/node/plugin.ts +++ b/packages/histoire/src/node/plugin.ts @@ -99,7 +99,7 @@ export async function createVitePlugins (ctx: Context): Promise { return `import { defineAsyncComponent } from 'vue' ${resolvedStories.map((file, index) => `const Comp${index} = defineAsyncComponent(() => import('${file.path}'))`).join('\n')} export let files = [${files.map((file) => `{${JSON.stringify(file).slice(1, -1)}, component: Comp${file.index}}`).join(',\n')}] -export let tree = ${JSON.stringify(makeTree(ctx.config, files))} +export let tree = ${JSON.stringify(makeTree(ctx.config, resolvedStories))} const handlers = [] export function onUpdate (cb) { handlers.push(cb) diff --git a/packages/histoire/src/node/tree.ts b/packages/histoire/src/node/tree.ts index e674a2f2..4c2f3d31 100644 --- a/packages/histoire/src/node/tree.ts +++ b/packages/histoire/src/node/tree.ts @@ -1,21 +1,32 @@ -import { HistoireConfig } from './config.js' -export type TreeFile = { +import { build } from 'esbuild' +import pc from 'picocolors' +import type { HistoireConfig, TreeGroupConfig } from './config.js' +import type { StoryFile } from './types.js' + +export interface TreeFile { title: string path: string } -export type TreeLeaf = { +export interface TreeLeaf { title: string index: number } -export type TreeFolder = { +export interface TreeFolder { + title: string + children: (TreeFolder | TreeLeaf)[] +} + +export interface TreeGroup { + group: true + id: string title: string children: (TreeFolder | TreeLeaf)[] } -export type Tree = (TreeFolder | TreeLeaf)[] +export type Tree = (TreeGroup | TreeFolder | TreeLeaf)[] export function createPath (config: HistoireConfig, file: TreeFile) { if (config.tree.file === 'title') { @@ -32,16 +43,29 @@ export function createPath (config: HistoireConfig, file: TreeFile) { return config.tree.file(file) } -export function makeTree (config: HistoireConfig, files: { path: string[], index: number }[]) { +export function makeTree (config: HistoireConfig, files: StoryFile[]) { interface ITreeObject { [index: string]: number | ITreeObject } - const treeObject: ITreeObject = {} + interface ITreeGroup { + groupConfig?: TreeGroupConfig + treeObject: ITreeObject + } - for (const file of files) { - setPath(file.path, file.index, treeObject) + const groups: ITreeGroup[] = config.tree?.groups.map(g => ({ + groupConfig: g, + treeObject: {}, + })) || [] + const defaultGroup = { + treeObject: {}, } + groups.push(defaultGroup) + + files.forEach((file, index) => { + const group = getGroup(file) + setPath(file.treePath, index, group.treeObject) + }) let sortingFunction = (a: string, b: string) => a.localeCompare(b) @@ -49,7 +73,39 @@ export function makeTree (config: HistoireConfig, files: { path: string[], index sortingFunction = config.tree.order } - return buildTree(treeObject) + const result: Tree = [] + + for (const group of groups) { + if (group === defaultGroup) { + result.push(...buildTree(group.treeObject)) + } else { + result.push({ + group: true, + id: group.groupConfig.id, + title: group.groupConfig.title, + children: buildTree(group.treeObject), + }) + } + } + + return result + + function getGroup (file: StoryFile): ITreeGroup { + if (file.story?.group) { + const group = groups.find(g => g.groupConfig?.id === file.story.group) + if (group) { + return group + } else { + console.error(pc.red(`Group ${file.story.group} not found for story ${file.path}`)) + } + } + for (const group of groups) { + if (group.groupConfig?.include && group.groupConfig.include(file.treeFile)) { + return group + } + } + return defaultGroup + } function setPath (path: string[], value: unknown, tree: unknown) { path.reduce((subtree, key, i) => { diff --git a/packages/histoire/src/node/types.ts b/packages/histoire/src/node/types.ts index 9d51add1..b4560d22 100644 --- a/packages/histoire/src/node/types.ts +++ b/packages/histoire/src/node/types.ts @@ -1,3 +1,5 @@ +import type { TreeFile } from './tree.js' + export interface StoryFile { id: string /** @@ -20,11 +22,16 @@ export interface StoryFile { * Resolved story data from story file execution */ story?: Story + /** + * Data sent to user tree config functions + */ + treeFile?: TreeFile } export interface Story { id: string title: string + group?: string variants: Variant[] layout?: { type: 'single'