diff --git a/code/core/src/manager-api/lib/intersect.ts b/code/core/src/manager-api/lib/intersect.ts index 84eae122322d..5918f7c8babf 100644 --- a/code/core/src/manager-api/lib/intersect.ts +++ b/code/core/src/manager-api/lib/intersect.ts @@ -1,6 +1,6 @@ export default (a: T[], b: T[]): T[] => { // no point in intersecting if one of the input is ill-defined - if (!a || !b) { + if (!Array.isArray(a) || !Array.isArray(b) || !a.length || !b.length) { return []; } diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index eed677371ef0..59b59f070a10 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -1,4 +1,5 @@ import type { + API_BaseEntry, API_ComponentEntry, API_DocsEntry, API_GroupEntry, @@ -16,6 +17,7 @@ import type { StoryId, StoryIndexV2, StoryIndexV3, + Tag, } from '@storybook/core/types'; import { sanitize } from '@storybook/csf'; @@ -248,6 +250,7 @@ export const transformStoryIndexToStoriesHash = ( type: 'root', id, name: names[idx], + tags: [], depth: idx, renderLabel, startCollapsed: collapsedRoots.includes(id), @@ -267,6 +270,7 @@ export const transformStoryIndexToStoriesHash = ( type: 'component', id, name: names[idx], + tags: [], parent: paths[idx - 1], depth: idx, renderLabel, @@ -274,14 +278,12 @@ export const transformStoryIndexToStoriesHash = ( children: [childId], }), }); - // merge computes a union of arrays but we want an intersection on this - // specific array property, so it's easier to add it after the merge. - acc[id].tags = intersect(acc[id]?.tags ?? item.tags, item.tags); } else { acc[id] = merge((acc[id] || {}) as API_GroupEntry, { type: 'group', id, name: names[idx], + tags: [], parent: paths[idx - 1], depth: idx, renderLabel, @@ -295,6 +297,7 @@ export const transformStoryIndexToStoriesHash = ( // Finally add an entry for the docs/story itself acc[item.id] = { type: 'story', + tags: [], ...item, depth: paths.length, parent: paths[paths.length - 1], @@ -313,9 +316,18 @@ export const transformStoryIndexToStoriesHash = ( } acc[item.id] = item; - // Ensure we add the children depth-first *before* inserting any other entries + // Ensure we add the children depth-first *before* inserting any other entries, + // and compute tags from the children put in the accumulator afterwards, once + // they're all known and we can compute a sound intersection. if (item.type === 'root' || item.type === 'group' || item.type === 'component') { item.children.forEach((childId: any) => addItem(acc, storiesHashOutOfOrder[childId])); + + item.tags = item.children.reduce((currentTags: Tag[] | null, childId: any): Tag[] => { + const child = acc[childId]; + + // On the first child, we have nothing to intersect against so we use it as a source of data. + return currentTags === null ? child.tags : intersect(currentTags, child.tags); + }, null); } return acc; } diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index 9d3c4433fdec..b652c87cd7a1 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -162,6 +162,7 @@ describe('stories API', () => { expect(index!['design-system']).toMatchObject({ type: 'root', name: 'Design System', // root name originates from `kind`, so it gets trimmed + tags: [], }); expect(index!['design-system-some-component']).toMatchObject({ type: 'component', @@ -186,6 +187,7 @@ describe('stories API', () => { title: 'Root/First', name: 'Story 1', importPath: './path/to/root/first.ts', + tags: [], }, ...mockEntries, }, @@ -207,6 +209,7 @@ describe('stories API', () => { type: 'root', id: 'root', children: ['root-first'], + tags: [], }); }); it('sets roots when showRoots = true', () => { @@ -222,6 +225,7 @@ describe('stories API', () => { id: 'a-b--1', title: 'a/b', name: '1', + tags: [], importPath: './a/b.ts', }, }, @@ -233,6 +237,7 @@ describe('stories API', () => { type: 'root', id: 'a', children: ['a-b'], + tags: [], }); expect(index!['a-b']).toMatchObject({ type: 'component', @@ -332,6 +337,76 @@ describe('stories API', () => { tags: ['shared', 'two-specific'], }); }); + + it('intersects story/docs tags to compute tags for root and group entries', () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + api.setIndex({ + v: 5, + entries: { + 'a-sampleone': { + type: 'story', + id: 'a-sampleone', + title: 'A/SampleOne', + name: '1', + tags: ['shared', 'one-specific'], + importPath: './a.ts', + }, + 'a-sampletwo': { + type: 'story', + id: 'a-sampletwo', + title: 'A/SampleTwo', + name: '2', + tags: ['shared', 'two-specific'], + importPath: './a.ts', + }, + 'a-embedded-othertopic': { + type: 'docs', + id: 'a-embedded-othertopic', + title: 'A/Embedded/OtherTopic', + name: '3', + tags: ['shared', 'embedded-docs-specific', 'other'], + storiesImports: [], + importPath: './embedded/other.mdx', + }, + 'a-embedded-extras': { + type: 'docs', + id: 'a-embedded-extras', + title: 'A/Embedded/Extras', + name: '3', + tags: ['shared', 'embedded-docs-specific', 'extras'], + storiesImports: [], + importPath: './embedded/extras.mdx', + }, + }, + }); + const { index } = store.getState(); + // We need exact key ordering, even if in theory JS doesn't guarantee it + expect(Object.keys(index!)).toEqual([ + 'a', + 'a-sampleone', + 'a-sampletwo', + 'a-embedded', + 'a-embedded-othertopic', + 'a-embedded-extras', + ]); + // Acts as the root, so that the next level is a group we're testing. + expect(index!.a).toMatchObject({ + type: 'root', + id: 'a', + children: ['a-sampleone', 'a-sampletwo', 'a-embedded'], + tags: ['shared'], + }); + // The object of this test. + expect(index!['a-embedded']).toMatchObject({ + type: 'group', + id: 'a-embedded', + parent: 'a', + name: 'Embedded', + tags: ['shared', 'embedded-docs-specific'], + }); + }); // Stories can get out of order for a few reasons -- see reproductions on // https://github.com/storybookjs/storybook/issues/5518 it('does the right thing for out of order stories', async () => { @@ -1515,6 +1590,7 @@ describe('stories API', () => { "parent": "a", "prepared": false, "renderLabel": undefined, + "tags": [], "title": "a", "type": "story", }, @@ -1526,6 +1602,7 @@ describe('stories API', () => { "parent": "a", "prepared": false, "renderLabel": undefined, + "tags": [], "title": "a", "type": "story", }, @@ -1581,6 +1658,7 @@ describe('stories API', () => { "parent": "a", "prepared": false, "renderLabel": undefined, + "tags": [], "title": "a", "type": "story", }, @@ -1623,6 +1701,7 @@ describe('stories API', () => { "parent": "a", "prepared": false, "renderLabel": undefined, + "tags": [], "title": "a", "type": "story", }, @@ -1634,6 +1713,7 @@ describe('stories API', () => { "parent": "a", "prepared": false, "renderLabel": undefined, + "tags": [], "title": "a", "type": "story", }, diff --git a/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx b/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx index 2e1583fdcf87..0840be5fcd71 100644 --- a/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx +++ b/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx @@ -50,6 +50,7 @@ const generateStories = ({ title, refId }: { title: string; refId?: string }): A name: root, children: [componentId], startCollapsed: false, + tags: [], }, { type: 'component', diff --git a/code/core/src/types/modules/api-stories.ts b/code/core/src/types/modules/api-stories.ts index c53d379b7192..5f7b8dfe864b 100644 --- a/code/core/src/types/modules/api-stories.ts +++ b/code/core/src/types/modules/api-stories.ts @@ -7,6 +7,7 @@ export interface API_BaseEntry { id: StoryId; depth: number; name: string; + tags: Tag[]; refId?: string; renderLabel?: (item: API_BaseEntry, api: any) => any; } @@ -27,7 +28,6 @@ export interface API_ComponentEntry extends API_BaseEntry { type: 'component'; parent?: StoryId; children: StoryId[]; - tags: Tag[]; } export interface API_DocsEntry extends API_BaseEntry { @@ -35,7 +35,6 @@ export interface API_DocsEntry extends API_BaseEntry { parent: StoryId; title: ComponentTitle; importPath: Path; - tags: Tag[]; prepared: boolean; parameters?: { [parameterName: string]: any; @@ -47,7 +46,6 @@ export interface API_StoryEntry extends API_BaseEntry { parent: StoryId; title: ComponentTitle; importPath: Path; - tags: Tag[]; prepared: boolean; parameters?: { [parameterName: string]: any;