diff --git a/code/addons/docs/src/typings.d.ts b/code/addons/docs/src/typings.d.ts index 651c8846fc0b..48d1127304af 100644 --- a/code/addons/docs/src/typings.d.ts +++ b/code/addons/docs/src/typings.d.ts @@ -10,12 +10,6 @@ declare module 'sveltedoc-parser' { export function parse(options: any): Promise; } -declare var FEATURES: - | { - storyStoreV7?: boolean; - argTypeTargetsV7?: boolean; - legacyMdx1?: boolean; - } - | undefined; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; diff --git a/code/addons/storyshots-core/src/typings.d.ts b/code/addons/storyshots-core/src/typings.d.ts index 02634df94986..2ae47a5617de 100644 --- a/code/addons/storyshots-core/src/typings.d.ts +++ b/code/addons/storyshots-core/src/typings.d.ts @@ -10,12 +10,7 @@ declare var STORYBOOK_ENV: any; declare var STORIES: any; declare var CONFIG_TYPE: 'DEVELOPMENT' | 'PRODUCTION'; -declare var FEATURES: - | { - storyStoreV7?: boolean; - argTypeTargetsV7?: boolean; - } - | undefined; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; declare var __STORYBOOK_STORY_STORE__: any; declare var __requireContext: any; diff --git a/code/builders/builder-manager/src/utils/template.ts b/code/builders/builder-manager/src/utils/template.ts index 7c9f873794f4..64fb626869b3 100644 --- a/code/builders/builder-manager/src/utils/template.ts +++ b/code/builders/builder-manager/src/utils/template.ts @@ -34,7 +34,7 @@ export const renderHTML = async ( refs: Promise>, logLevel: Promise, docsOptions: Promise, - { versionCheck, releaseNotesData, previewUrl, configType }: Options + { versionCheck, previewUrl, configType }: Options ) => { const titleRef = await title; const templateRef = await template; @@ -51,7 +51,6 @@ export const renderHTML = async ( CONFIG_TYPE: JSON.stringify(await configType, null, 2), // These two need to be double stringified because the UI expects a string VERSIONCHECK: JSON.stringify(JSON.stringify(versionCheck), null, 2), - RELEASE_NOTES_DATA: JSON.stringify(JSON.stringify(releaseNotesData), null, 2), PREVIEW_URL: JSON.stringify(previewUrl, null, 2), // global preview URL }, head: (await customHead) || '', diff --git a/code/frameworks/angular/src/typings.d.ts b/code/frameworks/angular/src/typings.d.ts index f934e349fc76..a1bde958b56d 100644 --- a/code/frameworks/angular/src/typings.d.ts +++ b/code/frameworks/angular/src/typings.d.ts @@ -12,12 +12,7 @@ declare var __STORYBOOK_STORY_STORE__: any; declare var CHANNEL_OPTIONS: any; declare var DOCS_OPTIONS: any; -declare var FEATURES: - | { - storyStoreV7?: boolean; - argTypeTargetsV7?: boolean; - } - | undefined; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; declare var IS_STORYBOOK: any; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index 0a3ea06f54c5..f12564b0d9c5 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -203,11 +203,6 @@ command('dev') .option('--loglevel ', 'Control level of logging during build') .option('--quiet', 'Suppress verbose build output') .option('--no-version-updates', 'Suppress update check', true) - .option( - '--no-release-notes', - 'Suppress automatic redirects to the release notes after upgrading', - true - ) .option('--debug-webpack', 'Display final webpack configurations for debugging purposes') .option('--webpack-stats-json [directory]', 'Write Webpack Stats JSON to disk') .option( diff --git a/code/lib/core-events/src/index.ts b/code/lib/core-events/src/index.ts index b65ebe7dbd07..64a452dc11f2 100644 --- a/code/lib/core-events/src/index.ts +++ b/code/lib/core-events/src/index.ts @@ -64,6 +64,10 @@ enum events { SHARED_STATE_SET = 'sharedStateSet', NAVIGATE_URL = 'navigateUrl', UPDATE_QUERY_PARAMS = 'updateQueryParams', + + REQUEST_WHATS_NEW_DATA = 'requestWhatsNewData', + RESULT_WHATS_NEW_DATA = 'resultWhatsNewData', + SET_WHATS_NEW_CACHE = 'setWhatsNewCache', } // Enables: `import Events from ...` @@ -111,7 +115,29 @@ export const { UPDATE_GLOBALS, UPDATE_QUERY_PARAMS, UPDATE_STORY_ARGS, + REQUEST_WHATS_NEW_DATA, + RESULT_WHATS_NEW_DATA, + SET_WHATS_NEW_CACHE, } = events; // Used to break out of the current render without showing a redbox export const IGNORED_EXCEPTION = new Error('ignoredException'); + +export interface WhatsNewCache { + lastDismissedPost?: string; + lastReadPost?: string; +} + +export type WhatsNewData = + | { + status: 'SUCCESS'; + title: string; + url: string; + publishedAt: string; + excerpt: string; + postIsRead: boolean; + showNotification: boolean; + } + | { + status: 'ERROR'; + }; diff --git a/code/lib/core-server/src/build-dev.ts b/code/lib/core-server/src/build-dev.ts index 910ac023a725..ef3d7e3cc3f8 100644 --- a/code/lib/core-server/src/build-dev.ts +++ b/code/lib/core-server/src/build-dev.ts @@ -7,7 +7,6 @@ import type { StorybookConfig, } from '@storybook/types'; import { - cache, loadAllPresets, loadMainConfig, resolveAddonName, @@ -20,7 +19,6 @@ import { telemetry } from '@storybook/telemetry'; import { join, resolve } from 'path'; import { storybookDevServer } from './dev-server'; -import { getReleaseNotesData, getReleaseNotesFailedState } from './utils/release-notes'; import { outputStats } from './utils/output-stats'; import { outputStartupInformation } from './utils/output-startup-information'; import { updateCheck } from './utils/update-check'; @@ -31,18 +29,15 @@ import { warnOnIncompatibleAddons } from './utils/warnOnIncompatibleAddons'; export async function buildDevStandalone( options: CLIOptions & LoadOptions & BuilderOptions ): Promise<{ port: number; address: string; networkAddress: string }> { - const { packageJson, versionUpdates, releaseNotes } = options; + const { packageJson, versionUpdates } = options; const { version } = packageJson; - // updateInfo and releaseNotesData are cached, so this is typically pretty fast - const [port, versionCheck, releaseNotesData] = await Promise.all([ + // updateInfo are cached, so this is typically pretty fast + const [port, versionCheck] = await Promise.all([ getServerPort(options.port), versionUpdates ? updateCheck(version) : Promise.resolve({ success: false, cached: false, data: {}, time: Date.now() }), - releaseNotes - ? getReleaseNotesData(version, cache) - : Promise.resolve(getReleaseNotesFailedState(version)), ]); if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) { @@ -58,7 +53,6 @@ export async function buildDevStandalone( /* eslint-disable no-param-reassign */ options.port = port; options.versionCheck = versionCheck; - options.releaseNotesData = releaseNotesData; options.configType = 'DEVELOPMENT'; options.configDir = resolve(options.configDir); options.outputDir = options.smokeTest diff --git a/code/lib/core-server/src/presets/common-preset.ts b/code/lib/core-server/src/presets/common-preset.ts index 225512e09cd4..79439587cb32 100644 --- a/code/lib/core-server/src/presets/common-preset.ts +++ b/code/lib/core-server/src/presets/common-preset.ts @@ -8,16 +8,24 @@ import { } from '@storybook/core-common'; import type { CLIOptions, - IndexerOptions, - StoryIndexer, CoreConfig, + IndexerOptions, Options, - StorybookConfig, PresetPropertyFn, + StorybookConfig, + StoryIndexer, } from '@storybook/types'; import { loadCsf } from '@storybook/csf-tools'; import { join } from 'path'; import { dedent } from 'ts-dedent'; +import fetch from 'node-fetch'; +import type { Channel } from '@storybook/channels'; +import type { WhatsNewCache, WhatsNewData } from '@storybook/core-events'; +import { + REQUEST_WHATS_NEW_DATA, + RESULT_WHATS_NEW_DATA, + SET_WHATS_NEW_CACHE, +} from '@storybook/core-events'; import { parseStaticDir } from '../utils/server-statics'; import { defaultStaticDirs } from '../utils/constants'; @@ -180,6 +188,7 @@ export const features = async ( storyStoreV7: true, argTypeTargetsV7: true, legacyDecoratorFileOrder: false, + whatsNewNotifications: false, }); export const storyIndexers = async (indexers?: StoryIndexer[]) => { @@ -232,3 +241,46 @@ export const managerHead = async (_: any, options: Options) => { return ''; }; + +const WHATS_NEW_CACHE = 'whats-new-cache'; +const WHATS_NEW_URL = 'https://storybook.js.org/whats-new/v1'; + +// Grabbed from the implementation: https://github.com/storybookjs/dx-functions/blob/main/netlify/functions/whats-new.ts +type WhatsNewResponse = { title: string; url: string; publishedAt: string; excerpt: string }; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const experimental_serverChannel = (channel: Channel, options: Options) => { + channel.on(SET_WHATS_NEW_CACHE, async (data: WhatsNewCache) => { + const cache: WhatsNewCache = await options.cache.get(WHATS_NEW_CACHE).catch((e) => { + logger.verbose(e); + return {}; + }); + await options.cache.set(WHATS_NEW_CACHE, { ...cache, ...data }); + }); + + channel.on(REQUEST_WHATS_NEW_DATA, async () => { + try { + const post = (await fetch(WHATS_NEW_URL).then(async (response) => { + if (response.ok) return response.json(); + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw response; + })) as WhatsNewResponse; + + const cache: WhatsNewCache = (await options.cache.get(WHATS_NEW_CACHE)) ?? {}; + const data = { + ...post, + status: 'SUCCESS', + postIsRead: post.url === cache.lastReadPost, + showNotification: post.url !== cache.lastDismissedPost && post.url !== cache.lastReadPost, + } satisfies WhatsNewData; + channel.emit(RESULT_WHATS_NEW_DATA, { data }); + } catch (e) { + logger.verbose(e); + channel.emit(RESULT_WHATS_NEW_DATA, { + data: { status: 'ERROR' } satisfies WhatsNewData, + }); + } + }); + + return channel; +}; diff --git a/code/lib/core-server/src/typings.d.ts b/code/lib/core-server/src/typings.d.ts index b9b16282161c..7ebf6a02c0b4 100644 --- a/code/lib/core-server/src/typings.d.ts +++ b/code/lib/core-server/src/typings.d.ts @@ -6,10 +6,4 @@ declare module '@aw-web-design/x-default-browser'; declare module '@discoveryjs/json-ext'; declare module 'watchpack'; -declare var FEATURES: - | { - storyStoreV7?: boolean; - argTypeTargetsV7?: boolean; - legacyMdx1?: boolean; - } - | undefined; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; diff --git a/code/lib/core-server/src/utils/__tests__/release-notes.test.ts b/code/lib/core-server/src/utils/__tests__/release-notes.test.ts deleted file mode 100644 index 71062b7a951b..000000000000 --- a/code/lib/core-server/src/utils/__tests__/release-notes.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getReleaseNotesData, RELEASE_NOTES_CACHE_KEY } from '../release-notes'; - -describe('getReleaseNotesData', () => { - it('handles errors gracefully', async () => { - const version = '4.0.0'; - // The cache is missing necessary functions. This will cause an error. - const cache = {}; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: false, - success: false, - }); - }); - - it('does not show the release notes on first build', async () => { - const version = '4.0.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve([]), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: false, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0']); - }); - - it('shows the release notes after upgrading a major version', async () => { - const version = '4.0.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['3.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: true, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['3.0.0', '4.0.0']); - }); - - it('shows the release notes after upgrading a minor version', async () => { - const version = '4.1.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['4.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: version, - showOnFirstLaunch: true, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0', '4.1.0']); - }); - - it('transforms patch versions to the closest major.minor version', async () => { - const version = '4.0.1'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['4.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: '4.0.0', - showOnFirstLaunch: false, - success: true, - }); - expect(set).not.toHaveBeenCalled(); - }); - - it('does not show release notes when downgrading', async () => { - const version = '3.0.0'; - const set = jest.fn((...args: any[]) => Promise.resolve()); - const cache = { get: () => Promise.resolve(['4.0.0']), set }; - - expect(await getReleaseNotesData(version, cache)).toEqual({ - currentVersion: '3.0.0', - showOnFirstLaunch: false, - success: true, - }); - expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0', '3.0.0']); - }); -}); diff --git a/code/lib/core-server/src/utils/release-notes.ts b/code/lib/core-server/src/utils/release-notes.ts deleted file mode 100644 index 0aa249113c58..000000000000 --- a/code/lib/core-server/src/utils/release-notes.ts +++ /dev/null @@ -1,64 +0,0 @@ -import semver from 'semver'; -import type { ReleaseNotesData } from '@storybook/types'; - -// We only expect to have release notes available for major and minor releases. -// For this reason, we convert the actual version of the build here so that -// every place that relies on this data can reference the version of the -// release notes that we expect to use. -const getReleaseNotesVersion = (version: string): string => { - const { major, minor } = semver.parse(version); - const { version: releaseNotesVersion } = semver.coerce(`${major}.${minor}`); - return releaseNotesVersion; -}; -export const getReleaseNotesFailedState = (version: string) => { - return { - success: false, - currentVersion: getReleaseNotesVersion(version), - showOnFirstLaunch: false, - }; -}; - -export const RELEASE_NOTES_CACHE_KEY = 'releaseNotesData'; - -export const getReleaseNotesData = async ( - currentVersionToParse: string, - fileSystemCache: any -): Promise => { - let result; - try { - const fromCache = (await fileSystemCache.get('releaseNotesData', []).catch(() => {})) || []; - const releaseNotesVersion = getReleaseNotesVersion(currentVersionToParse); - const versionHasNotBeenSeen = !fromCache.includes(releaseNotesVersion); - - if (versionHasNotBeenSeen) { - await fileSystemCache.set('releaseNotesData', [...fromCache, releaseNotesVersion]); - } - - const sortedHistory = semver.sort(fromCache); - const highestVersionSeenInThePast = sortedHistory.slice(-1)[0]; - - let isUpgrading = false; - let isMajorOrMinorDiff = false; - - if (highestVersionSeenInThePast) { - isUpgrading = semver.gt(releaseNotesVersion, highestVersionSeenInThePast); - const versionDiff = semver.diff(releaseNotesVersion, highestVersionSeenInThePast); - isMajorOrMinorDiff = versionDiff === 'major' || versionDiff === 'minor'; - } - - result = { - success: true, - showOnFirstLaunch: - versionHasNotBeenSeen && - // Only show the release notes if this is not the first time Storybook - // has been built. - !!highestVersionSeenInThePast && - isUpgrading && - isMajorOrMinorDiff, - currentVersion: releaseNotesVersion, - }; - } catch (e) { - result = getReleaseNotesFailedState(currentVersionToParse); - } - return result; -}; diff --git a/code/lib/instrumenter/src/typings.d.ts b/code/lib/instrumenter/src/typings.d.ts index d9ef4b0e849f..cde68861ee72 100644 --- a/code/lib/instrumenter/src/typings.d.ts +++ b/code/lib/instrumenter/src/typings.d.ts @@ -1,11 +1,6 @@ /* eslint-disable no-underscore-dangle, @typescript-eslint/naming-convention */ -declare var FEATURES: - | { - storyStoreV7?: boolean; - argTypeTargetsV7?: boolean; - } - | undefined; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; declare var __STORYBOOK_PREVIEW__: any; declare var __STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__: any; diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx index db27d916d75f..92ca071fb97f 100644 --- a/code/lib/manager-api/src/index.tsx +++ b/code/lib/manager-api/src/index.tsx @@ -53,7 +53,6 @@ import * as channel from './modules/channel'; import * as notifications from './modules/notifications'; import * as settings from './modules/settings'; -import * as releaseNotes from './modules/release-notes'; // eslint-disable-next-line import/no-cycle import * as stories from './modules/stories'; @@ -64,6 +63,7 @@ import * as shortcuts from './modules/shortcuts'; import * as url from './modules/url'; import * as version from './modules/versions'; +import * as whatsnew from './modules/whatsnew'; import * as globals from './modules/globals'; @@ -92,9 +92,9 @@ export type State = layout.SubState & version.SubState & url.SubState & shortcuts.SubState & - releaseNotes.SubState & settings.SubState & globals.SubState & + whatsnew.SubState & RouterData & API_OptionsData & DeprecatedState & @@ -109,10 +109,10 @@ export type API = addons.SubAPI & layout.SubAPI & notifications.SubAPI & shortcuts.SubAPI & - releaseNotes.SubAPI & settings.SubAPI & version.SubAPI & url.SubAPI & + whatsnew.SubAPI & Other; interface DeprecatedState { @@ -214,13 +214,13 @@ class ManagerProvider extends Component { layout, notifications, settings, - releaseNotes, shortcuts, stories, refs, globals, url, version, + whatsnew, ].map((m) => m.init({ ...routeData, ...optionsData, ...apiData, state: this.state, fullAPI: this.api }) ); diff --git a/code/lib/manager-api/src/modules/notifications.ts b/code/lib/manager-api/src/modules/notifications.ts index b75d8879317d..1f1059dc1939 100644 --- a/code/lib/manager-api/src/modules/notifications.ts +++ b/code/lib/manager-api/src/modules/notifications.ts @@ -15,8 +15,9 @@ export interface SubAPI { * @param notification - The notification to add. */ addNotification: (notification: API_Notification) => void; + /** - * Removes a notification from the list of notifications. + * Removes a notification from the list of notifications and calls the onClear callback. * @param id - The ID of the notification to remove. */ clearNotification: (id: string) => void; @@ -40,7 +41,7 @@ export const init: ModuleFn = ({ store }) => { const notification = notifications.find((n) => n.id === id); if (notification && notification.onClear) { - notification.onClear(); + notification.onClear({ dismissed: false }); } }, }; diff --git a/code/lib/manager-api/src/modules/release-notes.ts b/code/lib/manager-api/src/modules/release-notes.ts deleted file mode 100644 index 2beccdc312a0..000000000000 --- a/code/lib/manager-api/src/modules/release-notes.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { global } from '@storybook/global'; -import type { API_ReleaseNotes } from '@storybook/types'; -import memoize from 'memoizerific'; - -import type { ModuleFn } from '../index'; - -const { RELEASE_NOTES_DATA } = global; - -const getReleaseNotesData = memoize(1)((): API_ReleaseNotes => { - try { - return { ...(JSON.parse(RELEASE_NOTES_DATA) || {}) }; - } catch (e) { - return {}; - } -}); - -export interface SubAPI { - /** - * Returns the current version of the release notes. - * @returns {string} The current version of the release notes. - */ - releaseNotesVersion: () => string; - /** - * Sets the release notes as viewed. - * @returns {void} - */ - setDidViewReleaseNotes: () => void; - /** - * Determines whether to show the release notes on launch. - * @returns {boolean} Whether to show the release notes on launch. - */ - showReleaseNotesOnLaunch: () => boolean; -} - -export interface SubState { - releaseNotesViewed: string[]; -} - -export const init: ModuleFn = ({ store }) => { - const releaseNotesData = getReleaseNotesData(); - const getReleaseNotesViewed = () => { - const { releaseNotesViewed: persistedReleaseNotesViewed } = store.getState(); - return persistedReleaseNotesViewed || []; - }; - - const api: SubAPI = { - releaseNotesVersion: () => releaseNotesData.currentVersion, - setDidViewReleaseNotes: () => { - const releaseNotesViewed = getReleaseNotesViewed(); - - if (!releaseNotesViewed.includes(releaseNotesData.currentVersion)) { - store.setState( - { releaseNotesViewed: [...releaseNotesViewed, releaseNotesData.currentVersion] }, - { persistence: 'permanent' } - ); - } - }, - showReleaseNotesOnLaunch: () => { - // The currentVersion will only exist for dev builds - if (!releaseNotesData.currentVersion) return false; - const releaseNotesViewed = getReleaseNotesViewed(); - const didViewReleaseNotes = releaseNotesViewed.includes(releaseNotesData.currentVersion); - const showReleaseNotesOnLaunch = releaseNotesData.showOnFirstLaunch && !didViewReleaseNotes; - return showReleaseNotesOnLaunch; - }, - }; - - return { state: { releaseNotesViewed: [] }, api }; -}; diff --git a/code/lib/manager-api/src/modules/url.ts b/code/lib/manager-api/src/modules/url.ts index e8e5f2c479fc..b8e4dcf6ebf1 100644 --- a/code/lib/manager-api/src/modules/url.ts +++ b/code/lib/manager-api/src/modules/url.ts @@ -199,10 +199,6 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r fullAPI.on(NAVIGATE_URL, (url: string, options: NavigateOptions) => { fullAPI.navigateUrl(url, options); }); - - if (fullAPI.showReleaseNotesOnLaunch()) { - navigate('/settings/release-notes'); - } }; return { diff --git a/code/lib/manager-api/src/modules/versions.ts b/code/lib/manager-api/src/modules/versions.ts index 232fcfe9fbfb..49ff24be9b1f 100644 --- a/code/lib/manager-api/src/modules/versions.ts +++ b/code/lib/manager-api/src/modules/versions.ts @@ -44,7 +44,7 @@ export interface SubAPI { versionUpdateAvailable: () => boolean; } -export const init: ModuleFn = ({ store, mode, fullAPI }) => { +export const init: ModuleFn = ({ store }) => { const { dismissedVersionNotification } = store.getState(); const state = { @@ -112,34 +112,6 @@ export const init: ModuleFn = ({ store, mode, fullAPI }) => { await store.setState({ versions: { ...versions, latest, next }, }); - - if (api.versionUpdateAvailable()) { - const latestVersion = api.getLatestVersion().version; - const diff = semver.diff(versions.current.version, versions.latest.version); - - if ( - latestVersion !== dismissedVersionNotification && - diff !== 'patch' && - !semver.prerelease(latestVersion) && - mode !== 'production' - ) { - fullAPI.addNotification({ - id: 'update', - link: '/settings/about', - content: { - headline: `Storybook ${latestVersion} is available!`, - subHeadline: `Your current version is: ${versions.current.version}`, - }, - icon: { name: 'book' }, - onClear() { - store.setState( - { dismissedVersionNotification: latestVersion }, - { persistence: 'permanent' } - ); - }, - }); - } - } }; return { init: initModule, state, api }; diff --git a/code/lib/manager-api/src/modules/whatsnew.ts b/code/lib/manager-api/src/modules/whatsnew.ts new file mode 100644 index 000000000000..b970d0e96bac --- /dev/null +++ b/code/lib/manager-api/src/modules/whatsnew.ts @@ -0,0 +1,87 @@ +import { global } from '@storybook/global'; +import type { WhatsNewCache, WhatsNewData } from '@storybook/core-events'; +import { + REQUEST_WHATS_NEW_DATA, + RESULT_WHATS_NEW_DATA, + SET_WHATS_NEW_CACHE, +} from '@storybook/core-events'; +import type { ModuleFn } from '../index'; + +export type SubState = { + whatsNewData?: WhatsNewData; +}; + +export type SubAPI = { + isWhatsNewUnread(): boolean; + whatsNewHasBeenRead(): void; +}; + +const WHATS_NEW_NOTIFICATION_ID = 'whats-new'; + +export const init: ModuleFn = ({ fullAPI, store }) => { + const state: SubState = { + whatsNewData: undefined, + }; + + function setWhatsNewState(newState: WhatsNewData) { + store.setState({ whatsNewData: newState }); + state.whatsNewData = newState; + } + + const api: SubAPI = { + isWhatsNewUnread() { + return state.whatsNewData?.status === 'SUCCESS' && !state.whatsNewData.postIsRead; + }, + whatsNewHasBeenRead() { + if (state.whatsNewData?.status === 'SUCCESS') { + setWhatsNewCache({ lastReadPost: state.whatsNewData.url }); + setWhatsNewState({ ...state.whatsNewData, postIsRead: true }); + fullAPI.clearNotification(WHATS_NEW_NOTIFICATION_ID); + } + }, + }; + + function getLatestWhatsNewPost(): Promise { + fullAPI.emit(REQUEST_WHATS_NEW_DATA); + + return new Promise((resolve) => + fullAPI.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) => resolve(data)) + ); + } + + function setWhatsNewCache(cache: WhatsNewCache): void { + fullAPI.emit(SET_WHATS_NEW_CACHE, cache); + } + + const initModule = async () => { + // The server channel doesn't exist in production, and we don't want to show what's new in production storybooks. + if (global.CONFIG_TYPE !== 'DEVELOPMENT') return; + + const whatsNewData = await getLatestWhatsNewPost(); + setWhatsNewState(whatsNewData); + + const isNewStoryBookUser = fullAPI.getUrlState().path.includes('onboarding'); + + if ( + global.FEATURES.whatsNewNotifications && + !isNewStoryBookUser && + whatsNewData.status === 'SUCCESS' && + whatsNewData.showNotification + ) { + fullAPI.addNotification({ + id: WHATS_NEW_NOTIFICATION_ID, + link: '/settings/whats-new', + content: { + headline: whatsNewData.excerpt, + subHeadline: "Click to learn what's new in Storybook", + }, + icon: { name: 'hearthollow' }, + onClear({ dismissed }) { + if (dismissed) setWhatsNewCache({ lastDismissedPost: whatsNewData.url }); + }, + }); + } + }; + + return { init: initModule, state, api }; +}; diff --git a/code/lib/manager-api/src/tests/url.test.js b/code/lib/manager-api/src/tests/url.test.js index 6f4362d27bda..c269331b1c77 100644 --- a/code/lib/manager-api/src/tests/url.test.js +++ b/code/lib/manager-api/src/tests/url.test.js @@ -199,13 +199,4 @@ describe('initModule', () => { expect.objectContaining({ replace: true }) ); }); - - it('navigates to release notes when needed', () => { - fullAPI.showReleaseNotesOnLaunch.mockReturnValueOnce(true); - - const navigate = jest.fn(); - initURL({ store, state: { location: {} }, navigate, fullAPI }).init(); - - expect(navigate).toHaveBeenCalledWith('/settings/release-notes'); - }); }); diff --git a/code/lib/manager-api/src/tests/versions.test.js b/code/lib/manager-api/src/tests/versions.test.js index d92809ab65a1..004221a3d076 100644 --- a/code/lib/manager-api/src/tests/versions.test.js +++ b/code/lib/manager-api/src/tests/versions.test.js @@ -70,7 +70,6 @@ describe('versions API', () => { const store = createMockStore(); const { state: initialState, init } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); store.setState(initialState); store.setState.mockReset(); @@ -86,102 +85,6 @@ describe('versions API', () => { }); }); - describe('notifications', () => { - it('sets an update notification right away in the init function', async () => { - const store = createMockStore(); - const addNotification = jest.fn(); - const { init, state: initialState } = initVersions({ - store, - fullAPI: { addNotification }, - }); - store.setState(initialState); - - await init(); - expect(addNotification).toHaveBeenCalled(); - }); - - it('does not set an update notification if it has been dismissed', async () => { - const store = createMockStore(); - store.setState({ dismissedVersionNotification: '5.2.3' }); - const { - init, - api, - state: initialState, - } = initVersions({ - store, - fullAPI: { addNotification: jest.fn() }, - }); - store.setState(initialState); - - const addNotification = jest.fn(); - await init(); - expect(addNotification).not.toHaveBeenCalled(); - }); - - it('does not set an update notification if the latest version is a patch', async () => { - const store = createMockStore(); - const { - init, - api, - state: initialState, - } = initVersions({ - store, - fullAPI: { addNotification: jest.fn() }, - }); - store.setState({ - ...initialState, - versions: { ...initialState.versions, current: { version: '5.2.1' } }, - }); - - const addNotification = jest.fn(); - await init(); - expect(addNotification).not.toHaveBeenCalled(); - }); - - it('does not set an update notification in production mode', async () => { - const store = createMockStore(); - const { - init, - api, - state: initialState, - } = initVersions({ - store, - fullAPI: { addNotification: jest.fn() }, - }); - store.setState(initialState); - - const addNotification = jest.fn(); - await init(); - expect(addNotification).not.toHaveBeenCalled(); - }); - - it('persists a dismissed notification', async () => { - const store = createMockStore(); - let notification; - const addNotification = jest.fn().mockImplementation((n) => { - notification = n; - }); - - const { - init, - api, - state: initialState, - } = initVersions({ - store, - fullAPI: { addNotification }, - }); - store.setState(initialState); - - await init(); - - notification.onClear(); - expect(store.setState).toHaveBeenCalledWith( - { dismissedVersionNotification: '5.2.3' }, - { persistence: 'permanent' } - ); - }); - }); - it('getCurrentVersion works', async () => { const store = createMockStore(); const { @@ -190,7 +93,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); store.setState(initialState); @@ -209,7 +111,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); store.setState(initialState); @@ -229,7 +130,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); store.setState({ ...initialState, @@ -253,7 +153,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); store.setState({ ...initialState, @@ -277,7 +176,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); await init(); @@ -302,7 +200,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); await init(); @@ -327,7 +224,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); await init(); @@ -346,14 +242,7 @@ describe('versions API', () => { it('from older prerelease version', async () => { const store = createMockStore(); - const { - init, - api, - state: initialState, - } = initVersions({ - store, - fullAPI: { addNotification: jest.fn() }, - }); + const { init, api, state: initialState } = initVersions({ store }); await init(); @@ -377,7 +266,6 @@ describe('versions API', () => { state: initialState, } = initVersions({ store, - fullAPI: { addNotification: jest.fn() }, }); await init(); diff --git a/code/lib/manager-api/src/typings.d.ts b/code/lib/manager-api/src/typings.d.ts index 6b9d1aed3768..1d0d8d33ff0e 100644 --- a/code/lib/manager-api/src/typings.d.ts +++ b/code/lib/manager-api/src/typings.d.ts @@ -1,14 +1,8 @@ /* eslint-disable no-underscore-dangle, @typescript-eslint/naming-convention */ -declare var RELEASE_NOTES_DATA: any; declare var __STORYBOOK_ADDONS_MANAGER: any; -declare var FEATURES: - | { - storyStoreV7?: boolean; - argTypeTargetsV7?: boolean; - } - | undefined; - +declare var CONFIG_TYPE: string; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; declare var REFS: any; declare var VERSIONCHECK: any; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; diff --git a/code/lib/preview-api/src/typings.d.ts b/code/lib/preview-api/src/typings.d.ts index ff6233a2b547..fb9194834b96 100644 --- a/code/lib/preview-api/src/typings.d.ts +++ b/code/lib/preview-api/src/typings.d.ts @@ -7,15 +7,7 @@ declare module 'better-opn'; declare module 'open'; declare module '@aw-web-design/x-default-browser'; -declare var FEATURES: - | { - storyStoreV7?: boolean; - storyStoreV7MdxErrors?: boolean; - argTypeTargetsV7?: boolean; - legacyMdx1?: boolean; - legacyDecoratorFileOrder?: boolean; - } - | undefined; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; declare var STORIES: any; declare var DOCS_OPTIONS: any; diff --git a/code/lib/types/src/modules/api.ts b/code/lib/types/src/modules/api.ts index b20e3d0e120e..7a043dfc7382 100644 --- a/code/lib/types/src/modules/api.ts +++ b/code/lib/types/src/modules/api.ts @@ -123,7 +123,9 @@ export interface API_Notification { name: string; color?: string; }; - onClear?: () => void; + onClear?: (options: { + /** True when the user dismissed the notification. */ dismissed: boolean; + }) => void; } type API_Versions = Record; diff --git a/code/lib/types/src/modules/core-common.ts b/code/lib/types/src/modules/core-common.ts index 275f1422347d..ff5141f5238a 100644 --- a/code/lib/types/src/modules/core-common.ts +++ b/code/lib/types/src/modules/core-common.ts @@ -104,12 +104,6 @@ export interface VersionCheck { time: number; } -export interface ReleaseNotesData { - success: boolean; - currentVersion: string; - showOnFirstLaunch: boolean; -} - export interface Stats { toJson: () => any; } @@ -156,7 +150,6 @@ export interface CLIOptions { loglevel?: string; quiet?: boolean; versionUpdates?: boolean; - releaseNotes?: boolean; docs?: boolean; debugWebpack?: boolean; webpackStatsJson?: string | boolean; @@ -171,7 +164,6 @@ export interface BuilderOptions { docsMode?: boolean; features?: StorybookConfig['features']; versionCheck?: VersionCheck; - releaseNotesData?: ReleaseNotesData; disableWebpackDefaults?: boolean; serverChannelUrl?: string; } @@ -313,6 +305,11 @@ export interface StorybookConfig { * Apply decorators from preview.js before decorators from addons or frameworks */ legacyDecoratorFileOrder?: boolean; + + /** + * Show a notification anytime a What's new? post is published in the Storybook blog. + */ + whatsNewNotifications?: boolean; }; /** diff --git a/code/ui/manager/src/FakeProvider.tsx b/code/ui/manager/src/FakeProvider.tsx index 647e1ad81b06..e94773404356 100644 --- a/code/ui/manager/src/FakeProvider.tsx +++ b/code/ui/manager/src/FakeProvider.tsx @@ -12,6 +12,7 @@ export class FakeProvider extends Provider { // @ts-expect-error (Converted from ts-ignore) this.channel = { on: () => {}, + once: () => {}, off: () => {}, emit: () => {}, addListener: () => {}, diff --git a/code/ui/manager/src/app.stories.tsx b/code/ui/manager/src/app.stories.tsx index c213f64faabe..8709f9c60038 100644 --- a/code/ui/manager/src/app.stories.tsx +++ b/code/ui/manager/src/app.stories.tsx @@ -1,12 +1,11 @@ -import React from 'react'; +import React, { useEffect } from 'react'; -import type { API } from '@storybook/manager-api'; -import { Consumer, Provider as ManagerProvider } from '@storybook/manager-api'; +import { Provider as ManagerProvider, useStorybookApi } from '@storybook/manager-api'; import { LocationProvider } from '@storybook/router'; import { HelmetProvider } from 'react-helmet-async'; import { styled } from '@storybook/theming'; import App from './app'; -import { PrettyFakeProvider, FakeProvider } from './FakeProvider'; +import { FakeProvider, PrettyFakeProvider } from './FakeProvider'; export default { component: App, @@ -38,13 +37,17 @@ const ThemeStack = styled.div( }) ); -function setPreviewInitialized({ api }: { api: API }) { - api.setPreviewInitialized(); - return {}; +function SetPreviewInitialized(): JSX.Element { + const api = useStorybookApi(); + useEffect(() => { + api.setPreviewInitialized(); + }, [api]); + return null; } export const Default = () => { const provider = new FakeProvider(); + return ( { navigate={() => {}} docsOptions={{ docsMode: false }} > - {() => <>} + ({ position: 'relative', display: 'flex', @@ -76,26 +74,30 @@ const SubHeadline = styled.div(({ theme }) => ({ const ItemContent: FC> = ({ icon, content: { headline, subHeadline }, -}) => ( - <> - {!icon || ( - - - - )} - - - {headline} - - {subHeadline && {subHeadline}} - - -); +}) => { + const theme = useTheme(); + const defaultColor = theme.base === 'dark' ? theme.color.mediumdark : theme.color.mediumlight; + return ( + <> + {!icon || ( + + + + )} + + + {headline} + + {subHeadline && {subHeadline}} + + + ); +}; const DismissButtonWrapper = styled(IconButton)(({ theme }) => ({ alignSelf: 'center', @@ -128,7 +130,7 @@ const NotificationItem: FC<{ const dismissNotificationItem = () => { onDismissNotification(id); if (onClear) { - onClear(); + onClear({ dismissed: true }); } }; return link ? ( diff --git a/code/ui/manager/src/components/sidebar/Menu.stories.tsx b/code/ui/manager/src/components/sidebar/Menu.stories.tsx index 44caa61af1da..acf071da57ce 100644 --- a/code/ui/manager/src/components/sidebar/Menu.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Menu.stories.tsx @@ -6,6 +6,7 @@ import type { ComponentProps } from 'react'; import { TooltipLinkList } from '@storybook/components'; import { styled } from '@storybook/theming'; import { within, userEvent, screen } from '@storybook/testing-library'; +import type { State } from '@storybook/manager-api'; import { SidebarMenu, ToolbarMenu } from './Menu'; import { useMenu } from '../../containers/menu'; @@ -45,13 +46,15 @@ const DoubleThemeRenderingHack = styled.div({ export const Expanded: Story = { render: () => { + window.FEATURES.whatsNewNotifications = true; const menu = useMenu( + { whatsNewData: { status: 'SUCCESS' } } as State, { // @ts-expect-error (Converted from ts-ignore) getShortcutKeys: () => ({}), getAddonsShortcuts: () => ({}), versionUpdateAvailable: () => false, - releaseNotesVersion: () => '6.0.0', + isWhatsNewUnread: () => true, }, false, false, @@ -84,16 +87,17 @@ export const Expanded: Story = { ], }; -export const ExpandedWithoutReleaseNotes: Story = { +export const ExpandedWithoutWhatsNew: Story = { ...Expanded, render: () => { const menu = useMenu( + { whatsNewData: undefined } as State, { // @ts-expect-error (invalid) getShortcutKeys: () => ({}), getAddonsShortcuts: () => ({}), versionUpdateAvailable: () => false, - releaseNotesVersion: () => undefined, + isWhatsNewUnread: () => false, }, false, false, @@ -114,7 +118,7 @@ export const ExpandedWithoutReleaseNotes: Story = { setTimeout(res, 500); }); await Expanded.play(context); - const releaseNotes = await canvas.queryByText(/Release notes/); + const releaseNotes = await canvas.queryByText(/What's new/); await expect(releaseNotes).not.toBeInTheDocument(); }, }; diff --git a/code/ui/manager/src/containers/menu.tsx b/code/ui/manager/src/containers/menu.tsx index dba70ddb950c..6d5272ad8fd6 100644 --- a/code/ui/manager/src/containers/menu.tsx +++ b/code/ui/manager/src/containers/menu.tsx @@ -2,10 +2,10 @@ import type { FC } from 'react'; import React, { useCallback, useMemo } from 'react'; import { Badge, Icons } from '@storybook/components'; -import type { API } from '@storybook/manager-api'; -import { styled, useTheme } from '@storybook/theming'; - +import type { API, State } from '@storybook/manager-api'; import { shortcutToHumanString } from '@storybook/manager-api'; +import { styled, useTheme } from '@storybook/theming'; +import { global } from '@storybook/global'; const focusableUIElements = { storySearchField: 'storybook-explorer-searchfield', @@ -49,6 +49,7 @@ export const Shortcut: FC<{ keys: string[] }> = ({ keys }) => ( ); export const useMenu = ( + state: State, api: API, showToolbar: boolean, isFullscreen: boolean, @@ -64,18 +65,22 @@ export const useMenu = ( id: 'about', title: 'About your Storybook', onClick: () => api.navigateToSettingsPage('/settings/about'), - right: api.versionUpdateAvailable() && Update, }), [api] ); - const releaseNotes = useMemo( + const whatsNewNotificationsEnabled = global.FEATURES.whatsNewNotifications; + const isWhatsNewUnread = api.isWhatsNewUnread(); + const whatsNew = useMemo( () => ({ - id: 'release-notes', - title: 'Release notes', - onClick: () => api.navigateToSettingsPage('/settings/release-notes'), + id: 'whats-new', + title: "What's new?", + onClick: () => api.navigateToSettingsPage('/settings/whats-new'), + right: whatsNewNotificationsEnabled && isWhatsNewUnread && ( + Check it out + ), }), - [api] + [api, whatsNewNotificationsEnabled, isWhatsNewUnread] ); const shortcuts = useMemo( @@ -225,7 +230,7 @@ export const useMenu = ( return useMemo( () => [ about, - ...(api.releaseNotesVersion() ? [releaseNotes] : []), + ...(state.whatsNewData?.status === 'SUCCESS' ? [whatsNew] : []), shortcuts, sidebarToggle, toolbarToogle, @@ -242,8 +247,8 @@ export const useMenu = ( ], [ about, - api, - releaseNotes, + state, + whatsNew, shortcuts, sidebarToggle, toolbarToogle, diff --git a/code/ui/manager/src/containers/sidebar.tsx b/code/ui/manager/src/containers/sidebar.tsx index 78a05c817ff3..6a9b606e2db4 100755 --- a/code/ui/manager/src/containers/sidebar.tsx +++ b/code/ui/manager/src/containers/sidebar.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type { Combo, StoriesHash } from '@storybook/manager-api'; import { Consumer } from '@storybook/manager-api'; +import { global } from '@storybook/global'; import { Sidebar as SidebarComponent } from '../components/sidebar/Sidebar'; import { useMenu } from './menu'; @@ -22,7 +23,15 @@ const Sidebar = React.memo(function Sideber() { refs, } = state; - const menu = useMenu(api, showToolbar, isFullscreen, showPanel, showNav, enableShortcuts); + const menu = useMenu( + state, + api, + showToolbar, + isFullscreen, + showPanel, + showNav, + enableShortcuts + ); return { title: name, @@ -35,7 +44,7 @@ const Sidebar = React.memo(function Sideber() { refId, viewMode, menu, - menuHighlighted: api.versionUpdateAvailable(), + menuHighlighted: global.FEATURES.whatsNewNotifications && api.isWhatsNewUnread(), enableShortcuts, }; }; diff --git a/code/ui/manager/src/globals/exports.ts b/code/ui/manager/src/globals/exports.ts index 67c0dc8aff91..0e8fa703c97a 100644 --- a/code/ui/manager/src/globals/exports.ts +++ b/code/ui/manager/src/globals/exports.ts @@ -137,13 +137,16 @@ export default { 'PREVIEW_BUILDER_PROGRESS', 'PREVIEW_KEYDOWN', 'REGISTER_SUBSCRIPTION', + 'REQUEST_WHATS_NEW_DATA', 'RESET_STORY_ARGS', + 'RESULT_WHATS_NEW_DATA', 'SELECT_STORY', 'SET_CONFIG', 'SET_CURRENT_STORY', 'SET_GLOBALS', 'SET_INDEX', 'SET_STORIES', + 'SET_WHATS_NEW_CACHE', 'SHARED_STATE_CHANGED', 'SHARED_STATE_SET', 'STORIES_COLLAPSE_ALL', diff --git a/code/ui/manager/src/settings/about.tsx b/code/ui/manager/src/settings/about.tsx index af703e2ad108..fccd4ee15111 100644 --- a/code/ui/manager/src/settings/about.tsx +++ b/code/ui/manager/src/settings/about.tsx @@ -1,11 +1,10 @@ import type { FC } from 'react'; -import React, { Fragment } from 'react'; +import React from 'react'; import semver from 'semver'; import { styled } from '@storybook/theming'; import type { State } from '@storybook/manager-api'; -import Markdown from 'markdown-to-jsx'; -import { StorybookIcon, SyntaxHighlighter, Link, DocumentWrapper } from '@storybook/components'; +import { StorybookIcon, SyntaxHighlighter, DocumentWrapper } from '@storybook/components'; import SettingsFooter from './SettingsFooter'; @@ -24,26 +23,6 @@ const Header = styled.header(({ theme }) => ({ }, })); -const Subheading = styled.span(({ theme }) => ({ - letterSpacing: '0.35em', - textTransform: 'uppercase', - fontWeight: theme.typography.weight.bold, - fontSize: theme.typography.size.s2 - 1, - lineHeight: '24px', - color: theme.textMutedColor, -})); - -const SubheadingLink = styled(Link)(({ theme }) => ({ - fontSize: theme.typography.size.s1, -})); - -const Subheader = styled.div({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: '.75rem', -}); - const UpdateMessage = styled.div<{ status: 'positive' | 'negative' | string }>( ({ status, theme }) => { if (status === 'positive') { @@ -69,11 +48,6 @@ const UpdateMessage = styled.div<{ status: 'positive' | 'negative' | string }>( }) ); -const ErrorMessage = styled.div(({ theme }) => ({ - fontWeight: theme.typography.weight.bold, - textAlign: 'center', -})); - const Upgrade = styled.div(({ theme }) => ({ marginTop: 20, borderTop: `1px solid ${theme.appBorderColor}`, @@ -121,38 +95,6 @@ const AboutScreen: FC<{ {updateMessage} - {latest ? ( - - - {latest.version} Changelog - - Read full changelog - - - - {latest.info.plain} - - - ) : ( - - - Check Storybook's release history - - - )} - {canUpdate && ( diff --git a/code/ui/manager/src/settings/index.tsx b/code/ui/manager/src/settings/index.tsx index 773147c991aa..17afcdfed7d6 100644 --- a/code/ui/manager/src/settings/index.tsx +++ b/code/ui/manager/src/settings/index.tsx @@ -7,7 +7,7 @@ import type { FC, SyntheticEvent } from 'react'; import React, { Fragment } from 'react'; import { AboutPage } from './about_page'; -import { ReleaseNotesPage } from './release_notes_page'; +import { WhatsNewPage } from './whats_new_page'; import { ShortcutsPage } from './shortcuts_page'; import { matchesModifiers, matchesKeyCode } from '../keybinding'; @@ -61,9 +61,9 @@ const Content = styled(ScrollArea)( const Pages: FC<{ onClose: () => void; enableShortcuts?: boolean; - hasReleaseNotes?: boolean; changeTab: (tab: string) => void; -}> = ({ changeTab, onClose, enableShortcuts = true, hasReleaseNotes = false }) => { + enableWhatsNew: boolean; +}> = ({ changeTab, onClose, enableShortcuts = true, enableWhatsNew }) => { React.useEffect(() => { const handleEscape = (event: KeyboardEvent) => { if (!enableShortcuts || event.repeat) return; @@ -81,8 +81,8 @@ const Pages: FC<{ - {hasReleaseNotes && ( - + {enableWhatsNew && ( + )} @@ -100,8 +100,8 @@ const Pages: FC<{ - - + + @@ -118,7 +118,7 @@ const SettingsPages: FC = () => { return ( ( - -); - -export const DidHitMaxWaitTime = () => ( - -); diff --git a/code/ui/manager/src/settings/release_notes_page.tsx b/code/ui/manager/src/settings/release_notes_page.tsx deleted file mode 100644 index 536954bce484..000000000000 --- a/code/ui/manager/src/settings/release_notes_page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useStorybookApi } from '@storybook/manager-api'; -import type { FC } from 'react'; -import React, { useEffect } from 'react'; - -import { ReleaseNotesScreen } from './release_notes'; - -const ReleaseNotesPage: FC = () => { - const api = useStorybookApi(); - - useEffect(() => { - api.setDidViewReleaseNotes(); - }, []); - - const version = api.releaseNotesVersion(); - - return ; -}; - -export { ReleaseNotesPage }; diff --git a/code/ui/manager/src/settings/typings.d.ts b/code/ui/manager/src/settings/typings.d.ts index fc0733790e6b..95e36ac9fcf9 100644 --- a/code/ui/manager/src/settings/typings.d.ts +++ b/code/ui/manager/src/settings/typings.d.ts @@ -2,12 +2,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ declare module '@storybook/components/src/treeview/utils'; -declare var FEATURES: - | { - storyStoreV7?: boolean; - argTypeTargetsV7?: boolean; - } - | undefined; +declare var FEATURES: import('@storybook/types').StorybookConfig['features']; declare var __REACT__: any; declare var __REACTDOM__: any; diff --git a/code/ui/manager/src/settings/release_notes.tsx b/code/ui/manager/src/settings/whats_new.tsx similarity index 63% rename from code/ui/manager/src/settings/release_notes.tsx rename to code/ui/manager/src/settings/whats_new.tsx index 90f9c23fff56..a033f3b7ffe9 100644 --- a/code/ui/manager/src/settings/release_notes.tsx +++ b/code/ui/manager/src/settings/whats_new.tsx @@ -2,6 +2,7 @@ import type { FC, ComponentProps } from 'react'; import React, { useEffect, useState, Fragment } from 'react'; import { styled } from '@storybook/theming'; import { Icons, Loader } from '@storybook/components'; +import { useStorybookApi } from '@storybook/manager-api'; const Centered = styled.div({ top: '50%', @@ -50,62 +51,46 @@ const AlertIcon = styled(((props) => ) as FC< margin: '0 auto', })); -const getIframeUrl = (version: string) => { - const [major, minor] = version.split('.'); - return `https://storybook.js.org/releases/iframe/${major}.${minor}`; -}; - -const ReleaseNotesLoader: FC = () => ( +const WhatsNewLoader: FC = () => ( - Loading release notes + Loading... ); const MaxWaitTimeMessaging: FC = () => ( - - The release notes couldn't be loaded. Check your internet connection and try again. - + The page couldn't be loaded. Check your internet connection and try again. ); -export interface ReleaseNotesProps { +export interface WhatsNewProps { didHitMaxWaitTime: boolean; isLoaded: boolean; - setLoaded: (isLoaded: boolean) => void; - version: string; + onLoad: () => void; + url?: string; } -const PureReleaseNotesScreen: FC = ({ - didHitMaxWaitTime, - isLoaded, - setLoaded, - version, -}) => ( +const PureWhatsNewScreen: FC = ({ didHitMaxWaitTime, isLoaded, onLoad, url }) => ( - {!isLoaded && !didHitMaxWaitTime && } + {!isLoaded && !didHitMaxWaitTime && } {didHitMaxWaitTime ? ( ) : ( -