From 96ac9b3a40e96ee7cf38857e95101aac570fa909 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 2 Jun 2023 13:54:27 +0200 Subject: [PATCH 1/5] add a status api, and add tests --- code/lib/manager-api/src/modules/stories.ts | 10 ++ .../lib/manager-api/src/tests/stories.test.ts | 92 +++++++++++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index 0d8b7b87659c..5a0ab0530adc 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -50,6 +50,7 @@ import { } from '../lib/stories'; import type { ComposedRef, ModuleFn } from '../index'; +import { merge } from '../index'; const { FEATURES, fetch } = global; const STORY_INDEX_PATH = './index.json'; @@ -61,11 +62,14 @@ type ViewMode = 'story' | 'info' | 'settings' | string | undefined; type StoryUpdate = Partial< Pick >; +type Status = Record>; + type DocsUpdate = Partial>; export interface SubState extends API_LoadedRefData { storyId: StoryId; viewMode: ViewMode; + status: Status; } export interface SubAPI { @@ -100,6 +104,7 @@ export interface SubAPI { ): StoryId; fetchIndex: () => Promise; updateStory: (storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef) => Promise; + updateStatus: (update: Status) => Promise; updateDocs: (storyId: StoryId, update: DocsUpdate, ref?: API_ComposedRef) => Promise; setPreviewInitialized: (ref?: ComposedRef) => Promise; } @@ -378,6 +383,10 @@ export const init: ModuleFn = ({ await fullAPI.updateRef(refId, { index }); } }, + updateStatus: async (update) => { + const { status } = store.getState(); + await store.setState({ status: merge(status, update) }, { persistence: 'session' }); + }, updateDocs: async ( docsId: StoryId, update: DocsUpdate, @@ -575,6 +584,7 @@ export const init: ModuleFn = ({ viewMode: initialViewMode, hasCalledSetOptions: false, previewInitialized: false, + status: {}, }, init: initModule, }; diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index 4e6e7081db24..c4a47451ef58 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -21,6 +21,7 @@ import { Channel } from '@storybook/channels'; import type { API_StoryEntry, StoryIndex, API_PreparedStoryIndex } from '@storybook/types'; import { getEventMetadata } from '../lib/events'; +import type { SubAPI } from '../modules/stories'; import { init as initStories } from '../modules/stories'; import type Store from '../store'; import type { ModuleArgs } from '..'; @@ -125,12 +126,14 @@ describe('stories API', () => { viewMode: 'story', } as ModuleArgs); - expect(state).toEqual({ - previewInitialized: false, - storyId: 'id', - viewMode: 'story', - hasCalledSetOptions: false, - }); + expect(state).toEqual( + expect.objectContaining({ + previewInitialized: false, + storyId: 'id', + viewMode: 'story', + hasCalledSetOptions: false, + }) + ); }); describe('setIndex', () => { @@ -1629,4 +1632,81 @@ describe('stories API', () => { ); }); }); + + describe('status', () => { + it('is included in the initial state', () => { + const { state } = initStoriesAndSetState({ + storyId: 'id', + viewMode: 'story', + } as ModuleArgs); + + expect(state).toEqual( + expect.objectContaining({ + status: {}, + }) + ); + }); + + it('updates a story', async () => { + const fullAPI = Object.assign(new EventEmitter()); + const navigate = jest.fn(); + const store = createMockStore(); + + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + + const API: SubAPI = Object.assign(fullAPI, api, { + setIndex: jest.fn(), + findRef: jest.fn(), + setRef: jest.fn(), + }); + + await init(); + + await expect( + API.updateStatus({ 'a-story-id': { 'a-addon-id': 'loading' } }) + ).resolves.not.toThrow(); + + expect(store.getState().status).toMatchInlineSnapshot(` + Object { + "a-story-id": Object { + "a-addon-id": "loading", + }, + } + `); + }); + + it('updates multiple stories', async () => { + const fullAPI = Object.assign(new EventEmitter()); + const navigate = jest.fn(); + const store = createMockStore(); + + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + + const API: SubAPI = Object.assign(fullAPI, api, { + setIndex: jest.fn(), + findRef: jest.fn(), + setRef: jest.fn(), + }); + + await init(); + + await expect( + API.updateStatus({ + 'a-story-id': { 'a-addon-id': 'loading' }, + 'another-story-id': { 'a-addon-id': 'ready' }, + }) + ).resolves.not.toThrow(); + + expect(store.getState().status).toMatchInlineSnapshot(` + Object { + "a-story-id": Object { + "a-addon-id": "loading", + }, + "another-story-id": Object { + "a-addon-id": "ready", + }, + } + `); + }); + }); }); From dbfb5677dfc022a539a6979990ccd6375da54cdf Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 5 Jun 2023 10:20:51 +0200 Subject: [PATCH 2/5] refine API to include presentation data and also custom data, do not repeat the addon-id --- code/lib/manager-api/src/modules/stories.ts | 24 ++++++++++--- .../lib/manager-api/src/tests/stories.test.ts | 36 +++++++++++++++---- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index 5a0ab0530adc..dfadf05419fc 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -62,14 +62,22 @@ type ViewMode = 'story' | 'info' | 'settings' | string | undefined; type StoryUpdate = Partial< Pick >; -type Status = Record>; +interface StatusObject { + status: 'pending' | 'success' | 'error' | 'warn' | 'unknown'; + title: string; + description: string; + data?: any; +} + +type StatusState = Record>; +type StatusUpdate = Record; type DocsUpdate = Partial>; export interface SubState extends API_LoadedRefData { storyId: StoryId; viewMode: ViewMode; - status: Status; + status: StatusState; } export interface SubAPI { @@ -104,7 +112,7 @@ export interface SubAPI { ): StoryId; fetchIndex: () => Promise; updateStory: (storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef) => Promise; - updateStatus: (update: Status) => Promise; + updateStatus: (id: string, update: StatusUpdate) => Promise; updateDocs: (storyId: StoryId, update: DocsUpdate, ref?: API_ComposedRef) => Promise; setPreviewInitialized: (ref?: ComposedRef) => Promise; } @@ -383,9 +391,15 @@ export const init: ModuleFn = ({ await fullAPI.updateRef(refId, { index }); } }, - updateStatus: async (update) => { + updateStatus: async (id, update) => { const { status } = store.getState(); - await store.setState({ status: merge(status, update) }, { persistence: 'session' }); + const addition = Object.entries(update).reduce((acc, [storyId, value]) => { + acc[storyId] = acc[storyId] || {}; + acc[storyId][id] = value; + + return acc; + }, {}); + await store.setState({ status: merge(status, addition) }, { persistence: 'session' }); }, updateDocs: async ( docsId: StoryId, diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index c4a47451ef58..398f433c1d9b 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -1663,13 +1663,23 @@ describe('stories API', () => { await init(); await expect( - API.updateStatus({ 'a-story-id': { 'a-addon-id': 'loading' } }) + API.updateStatus('a-addon-id', { + 'a-story-id': { + status: 'pending', + title: 'an addon title', + description: 'an addon description', + }, + }) ).resolves.not.toThrow(); expect(store.getState().status).toMatchInlineSnapshot(` Object { "a-story-id": Object { - "a-addon-id": "loading", + "a-addon-id": Object { + "description": "an addon description", + "status": "pending", + "title": "an addon title", + }, }, } `); @@ -1691,19 +1701,31 @@ describe('stories API', () => { await init(); await expect( - API.updateStatus({ - 'a-story-id': { 'a-addon-id': 'loading' }, - 'another-story-id': { 'a-addon-id': 'ready' }, + API.updateStatus('a-addon-id', { + 'a-story-id': { + status: 'pending', + title: 'an addon title', + description: 'an addon description', + }, + 'another-story-id': { status: 'success', title: 'a addon title', description: '' }, }) ).resolves.not.toThrow(); expect(store.getState().status).toMatchInlineSnapshot(` Object { "a-story-id": Object { - "a-addon-id": "loading", + "a-addon-id": Object { + "description": "an addon description", + "status": "pending", + "title": "an addon title", + }, }, "another-story-id": Object { - "a-addon-id": "ready", + "a-addon-id": Object { + "description": "", + "status": "success", + "title": "a addon title", + }, }, } `); From ca416afb4c6ed0eeb24ec6226b2396ea285f38d0 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 5 Jun 2023 10:30:27 +0200 Subject: [PATCH 3/5] ignore dep cycle --- code/lib/manager-api/src/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx index ef5107c01ef6..db27d916d75f 100644 --- a/code/lib/manager-api/src/index.tsx +++ b/code/lib/manager-api/src/index.tsx @@ -57,6 +57,7 @@ import * as releaseNotes from './modules/release-notes'; // eslint-disable-next-line import/no-cycle import * as stories from './modules/stories'; +// eslint-disable-next-line import/no-cycle import * as refs from './modules/refs'; import * as layout from './modules/layout'; import * as shortcuts from './modules/shortcuts'; From 81f341a76243db8c49edb8ec6ff2c67b799fc96e Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 6 Jun 2023 14:36:40 +0200 Subject: [PATCH 4/5] mark as experimental --- code/lib/manager-api/src/modules/stories.ts | 24 ++++++++++--------- .../lib/manager-api/src/tests/stories.test.ts | 6 ++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index dfadf05419fc..cb84f58d7bdb 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -112,9 +112,9 @@ export interface SubAPI { ): StoryId; fetchIndex: () => Promise; updateStory: (storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef) => Promise; - updateStatus: (id: string, update: StatusUpdate) => Promise; updateDocs: (storyId: StoryId, update: DocsUpdate, ref?: API_ComposedRef) => Promise; setPreviewInitialized: (ref?: ComposedRef) => Promise; + experimental_updateStatus: (id: string, update: StatusUpdate) => Promise; } const removedOptions = ['enableShortcuts', 'theme', 'showRoots']; @@ -391,16 +391,6 @@ export const init: ModuleFn = ({ await fullAPI.updateRef(refId, { index }); } }, - updateStatus: async (id, update) => { - const { status } = store.getState(); - const addition = Object.entries(update).reduce((acc, [storyId, value]) => { - acc[storyId] = acc[storyId] || {}; - acc[storyId][id] = value; - - return acc; - }, {}); - await store.setState({ status: merge(status, addition) }, { persistence: 'session' }); - }, updateDocs: async ( docsId: StoryId, update: DocsUpdate, @@ -429,6 +419,18 @@ export const init: ModuleFn = ({ fullAPI.updateRef(ref.id, { previewInitialized: true }); } }, + + /* EXPERIMENTAL APIs */ + experimental_updateStatus: async (id, update) => { + const { status } = store.getState(); + const addition = Object.entries(update).reduce((acc, [storyId, value]) => { + acc[storyId] = acc[storyId] || {}; + acc[storyId][id] = value; + + return acc; + }, {}); + await store.setState({ status: merge(status, addition) }, { persistence: 'session' }); + }, }; const initModule = async () => { diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index 398f433c1d9b..399d68009f10 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -1633,7 +1633,7 @@ describe('stories API', () => { }); }); - describe('status', () => { + describe('experimental_updateStatus', () => { it('is included in the initial state', () => { const { state } = initStoriesAndSetState({ storyId: 'id', @@ -1663,7 +1663,7 @@ describe('stories API', () => { await init(); await expect( - API.updateStatus('a-addon-id', { + API.experimental_updateStatus('a-addon-id', { 'a-story-id': { status: 'pending', title: 'an addon title', @@ -1701,7 +1701,7 @@ describe('stories API', () => { await init(); await expect( - API.updateStatus('a-addon-id', { + API.experimental_updateStatus('a-addon-id', { 'a-story-id': { status: 'pending', title: 'an addon title', From a5cb092b274ab4684cd67fd4b902d414ae649cab Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 7 Jun 2023 10:25:25 +0200 Subject: [PATCH 5/5] add jsdoc --- code/lib/manager-api/src/modules/stories.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index cb84f58d7bdb..af1bb21faafb 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -114,7 +114,14 @@ export interface SubAPI { updateStory: (storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef) => Promise; updateDocs: (storyId: StoryId, update: DocsUpdate, ref?: API_ComposedRef) => Promise; setPreviewInitialized: (ref?: ComposedRef) => Promise; - experimental_updateStatus: (id: string, update: StatusUpdate) => Promise; + /** + * Updates the status of a collection of stories. + * + * @param {string} addonId - The ID of the addon to update. + * @param {StatusUpdate} update - An object containing the updated status information. + * @returns {Promise} A promise that resolves when the status has been updated. + */ + experimental_updateStatus: (addonId: string, update: StatusUpdate) => Promise; } const removedOptions = ['enableShortcuts', 'theme', 'showRoots'];