From 62165696ed87e2b3b99f4d0ff31b8c5d72dc7ca7 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Thu, 25 Jan 2024 12:48:35 +0100 Subject: [PATCH 01/21] un-memoize getTabs to always get up-to-date tabs from addons registry --- code/ui/manager/src/components/preview/Preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/ui/manager/src/components/preview/Preview.tsx b/code/ui/manager/src/components/preview/Preview.tsx index 3130cccb3c19..724a56ef32ea 100644 --- a/code/ui/manager/src/components/preview/Preview.tsx +++ b/code/ui/manager/src/components/preview/Preview.tsx @@ -45,7 +45,7 @@ const createCanvasTab = (): Addon_BaseType => ({ const useTabs = (getElements: API['getElements'], entry: PreviewProps['entry']) => { const canvasTab = useMemo(() => createCanvasTab(), []); - const tabsFromConfig = useMemo(() => getTabs(getElements), [getElements]); + const tabsFromConfig = getTabs(getElements); return useMemo(() => { if (entry?.type === 'story' && entry.parameters) { From d6d958330be6f5294db384d3d19fedf202807216 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 26 Jan 2024 14:59:32 +0100 Subject: [PATCH 02/21] add tab= query parameter --- code/lib/manager-api/src/modules/url.ts | 12 ++++ .../src/components/preview/Preview.tsx | 58 ++++++++++++------- .../src/components/preview/Toolbar.tsx | 31 +++++----- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/code/lib/manager-api/src/modules/url.ts b/code/lib/manager-api/src/modules/url.ts index e9431f04a1f5..8896ab272899 100644 --- a/code/lib/manager-api/src/modules/url.ts +++ b/code/lib/manager-api/src/modules/url.ts @@ -144,6 +144,12 @@ export interface SubAPI { * @returns {void} */ setQueryParams: (input: QueryParams) => void; + /** + * Set the query parameters for the current URL & navigates. + * @param {QueryParams} input - An object containing the query parameters to set. + * @returns {void} + */ + applyQueryParams: (input: QueryParams) => void; } export const init: ModuleFn = (moduleArgs) => { @@ -188,6 +194,12 @@ export const init: ModuleFn = (moduleArgs) => { provider.channel?.emit(UPDATE_QUERY_PARAMS, update); } }, + applyQueryParams(input) { + const { path, queryParams } = api.getUrlState(); + + navigateTo(path, { ...queryParams, ...input } as any); + api.setQueryParams(input); + }, navigateUrl(url, options) { navigate(url, { plain: true, ...options }); }, diff --git a/code/ui/manager/src/components/preview/Preview.tsx b/code/ui/manager/src/components/preview/Preview.tsx index 724a56ef32ea..365d85674c45 100644 --- a/code/ui/manager/src/components/preview/Preview.tsx +++ b/code/ui/manager/src/components/preview/Preview.tsx @@ -8,7 +8,7 @@ import type { Addon_BaseType } from '@storybook/types'; import { PREVIEW_BUILDER_PROGRESS, SET_CURRENT_STORY } from '@storybook/core-events'; import { Loader } from '@storybook/components'; -import { Location } from '@storybook/router'; +import { Location, Route } from '@storybook/router'; import * as S from './utils/components'; import { ZoomProvider, ZoomConsumer } from './tools/zoom'; @@ -81,20 +81,22 @@ const Preview = React.memo(function Preview(props) { useEffect(() => { if (entry && viewMode) { // Don't emit the event on first ("real") render, only when entry changes - if (storyId !== previousStoryId.current) { - previousStoryId.current = storyId; + if (storyId === previousStoryId.current) { + return; + } - if (viewMode.match(/docs|story/)) { - const { refId, id } = entry; - api.emit(SET_CURRENT_STORY, { - storyId: id, - viewMode, - options: { target: refId }, - }); - } + previousStoryId.current = storyId; + + if (viewMode.match(/docs|story/)) { + const { refId, id } = entry; + api.emit(SET_CURRENT_STORY, { + storyId: id, + viewMode, + options: { target: refId }, + }); } } - }, [entry, viewMode]); + }, [entry, viewMode, storyId, api]); return ( @@ -113,16 +115,28 @@ const Preview = React.memo(function Preview(props) { tabs={visibleTabsInToolbar} /> - - {tabs.map(({ render: Render, match, ...t }, i) => { - // @ts-expect-error (Converted from ts-ignore) - const key = t.id || t.key || i; - return ( - - {(lp) => } - - ); - })} + ({ + customQueryParams: api.getQueryParam('tab'), + })} + > + {({ customQueryParams: x }) => { + console.log(api.getQueryParam('tab')); + console.log(x); + const tabId = api.getQueryParam('tab'); + const tabContent = tabs.find((tab) => tab.id === tabId)?.render; + + console.log('LOG: tabbs', { tabContent, tabs, tabId }); + return ( + <> + + {tabContent && tabContent({ active: true })} + + ); + }} + diff --git a/code/ui/manager/src/components/preview/Toolbar.tsx b/code/ui/manager/src/components/preview/Toolbar.tsx index b09d8f3e2a57..34e297b207c5 100644 --- a/code/ui/manager/src/components/preview/Toolbar.tsx +++ b/code/ui/manager/src/components/preview/Toolbar.tsx @@ -73,12 +73,10 @@ export const fullScreenTool: Addon_BaseType = { }, }; -const tabsMapper = ({ state }: Combo) => ({ - viewMode: state.docsOnly, - storyId: state.storyId, +const tabsMapper = ({ api, state }: Combo) => ({ + navigate: api.navigate, path: state.path, - location: state.location, - refId: state.refId, + applyQueryParams: api.applyQueryParams, }); export const createTabsTool = (tabs: Addon_BaseType[]): Addon_BaseType => ({ @@ -91,16 +89,21 @@ export const createTabsTool = (tabs: Addon_BaseType[]): Addon_BaseType => ({ {tabs - .filter((p) => !p.hidden) - .map((t, index) => { - const to = t.route(rp); - const isActive = rp.path === to; + .filter(({ hidden }) => !hidden) + .map((tab, index) => { + const tabIdToApply = tab.id === 'canvas' ? undefined : tab.id; + const isActive = rp.path.includes(`tab=${tab.id}`); return ( - - - {t.title as any} - - + { + rp.applyQueryParams({ tab: tabIdToApply }); + }} + key={tab.id || `tab-${index}`} + > + {tab.title as any} + ); })} From 1d84cd9b4b8a189ea06d30c9b9cab5bcae09339a Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Sat, 27 Jan 2024 00:05:18 +0100 Subject: [PATCH 03/21] make tabs work with a queryParam --- code/ui/manager/src/App.tsx | 4 +- .../manager/src/components/layout/Layout.tsx | 15 +- .../src/components/preview/Preview.tsx | 90 ++++------- .../src/components/preview/Toolbar.tsx | 148 +++++++----------- .../src/components/preview/Wrappers.tsx | 6 +- .../src/components/preview/utils/types.tsx | 9 +- code/ui/manager/src/container/Preview.tsx | 74 ++++++++- code/ui/manager/src/container/Sidebar.tsx | 8 +- code/ui/manager/src/index.tsx | 7 +- 9 files changed, 186 insertions(+), 175 deletions(-) diff --git a/code/ui/manager/src/App.tsx b/code/ui/manager/src/App.tsx index 394016ff5708..22f3a9b072ee 100644 --- a/code/ui/manager/src/App.tsx +++ b/code/ui/manager/src/App.tsx @@ -16,15 +16,17 @@ type Props = { managerLayoutState: ComponentProps['managerLayoutState']; setManagerLayoutState: ComponentProps['setManagerLayoutState']; pages: Addon_PageType[]; + hasTab: boolean; }; -export const App = ({ managerLayoutState, setManagerLayoutState, pages }: Props) => { +export const App = ({ managerLayoutState, setManagerLayoutState, pages, hasTab }: Props) => { const { setMobileAboutOpen } = useLayout(); return ( <> { viewMode: API_ViewMode; + showPanel: boolean; } export type LayoutState = InternalLayoutState & ManagerLayoutState; @@ -25,6 +26,7 @@ interface Props { slotSidebar?: React.ReactNode; slotPanel?: React.ReactNode; slotPages?: React.ReactNode; + hasTab: boolean; } const MINIMUM_CONTENT_WIDTH_PX = 100; @@ -44,10 +46,12 @@ const useLayoutSyncingState = ({ managerLayoutState, setManagerLayoutState, isDesktop, + hasTab, }: { managerLayoutState: Props['managerLayoutState']; setManagerLayoutState: Props['setManagerLayoutState']; isDesktop: boolean; + hasTab: boolean; }) => { // ref to keep track of previous managerLayoutState, to check if the props change const prevManagerLayoutStateRef = React.useRef(managerLayoutState); @@ -95,7 +99,7 @@ const useLayoutSyncingState = ({ const isPagesShown = managerLayoutState.viewMode !== 'story' && managerLayoutState.viewMode !== 'docs'; - const isPanelShown = managerLayoutState.viewMode === 'story'; + const isPanelShown = managerLayoutState.viewMode === 'story' && !hasTab; const { panelResizerRef, sidebarResizerRef } = useDragging({ setState: setInternalDraggingSizeState, @@ -119,7 +123,7 @@ const useLayoutSyncingState = ({ }; }; -export const Layout = ({ managerLayoutState, setManagerLayoutState, ...slots }: Props) => { +export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...slots }: Props) => { const { isDesktop, isMobile } = useLayout(); const { @@ -132,7 +136,7 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, ...slots }: showPages, showPanel, isDragging, - } = useLayoutSyncingState({ managerLayoutState, setManagerLayoutState, isDesktop }); + } = useLayoutSyncingState({ managerLayoutState, setManagerLayoutState, isDesktop, hasTab }); return ( {showPages && {slots.slotPages}} @@ -172,7 +177,7 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, ...slots }: }; const LayoutContainer = styled.div( - ({ navSize, rightPanelWidth, bottomPanelHeight, viewMode, panelPosition }) => { + ({ navSize, rightPanelWidth, bottomPanelHeight, viewMode, panelPosition, showPanel }) => { return { width: '100%', height: ['100vh', '100dvh'], // This array is a special Emotion syntax to set a fallback if 100dvh is not supported @@ -186,7 +191,7 @@ const LayoutContainer = styled.div( gridTemplateColumns: `minmax(0, ${navSize}px) minmax(${MINIMUM_CONTENT_WIDTH_PX}px, 1fr) minmax(0, ${rightPanelWidth}px)`, gridTemplateRows: `1fr minmax(0, ${bottomPanelHeight}px)`, gridTemplateAreas: (() => { - if (viewMode === 'docs') { + if (viewMode === 'docs' || !showPanel) { // remove panel in docs viewMode return `"sidebar content content" "sidebar content content"`; diff --git a/code/ui/manager/src/components/preview/Preview.tsx b/code/ui/manager/src/components/preview/Preview.tsx index 365d85674c45..dfb5b2277747 100644 --- a/code/ui/manager/src/components/preview/Preview.tsx +++ b/code/ui/manager/src/components/preview/Preview.tsx @@ -3,24 +3,20 @@ import React, { Fragment, useMemo, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { global } from '@storybook/global'; -import { type API, Consumer, type Combo, merge, addons, types } from '@storybook/manager-api'; -import type { Addon_BaseType } from '@storybook/types'; +import { Consumer, type Combo, merge, addons, types } from '@storybook/manager-api'; +import type { Addon_BaseType, Addon_WrapperType } from '@storybook/types'; import { PREVIEW_BUILDER_PROGRESS, SET_CURRENT_STORY } from '@storybook/core-events'; import { Loader } from '@storybook/components'; -import { Location, Route } from '@storybook/router'; import * as S from './utils/components'; import { ZoomProvider, ZoomConsumer } from './tools/zoom'; -import { defaultWrappers, ApplyWrappers } from './Wrappers'; +import { ApplyWrappers } from './Wrappers'; import { ToolbarComp } from './Toolbar'; import { FramesRenderer } from './FramesRenderer'; import type { PreviewProps } from './utils/types'; -const getWrappers = (getFn: API['getElements']) => Object.values(getFn(types.PREVIEW)); -const getTabs = (getFn: API['getElements']) => Object.values(getFn(types.TAB)); - const canvasMapper = ({ state, api }: Combo) => ({ storyId: state.storyId, refId: state.refId, @@ -31,10 +27,9 @@ const canvasMapper = ({ state, api }: Combo) => ({ entry: api.getData(state.storyId, state.refId), previewInitialized: state.previewInitialized, refs: state.refs, - active: !!(state.viewMode && state.viewMode.match(/^(story|docs)$/)), }); -const createCanvasTab = (): Addon_BaseType => ({ +export const createCanvasTab = (): Addon_BaseType => ({ id: 'canvas', type: types.TAB, title: 'Canvas', @@ -43,19 +38,6 @@ const createCanvasTab = (): Addon_BaseType => ({ render: () => null, }); -const useTabs = (getElements: API['getElements'], entry: PreviewProps['entry']) => { - const canvasTab = useMemo(() => createCanvasTab(), []); - const tabsFromConfig = getTabs(getElements); - - return useMemo(() => { - if (entry?.type === 'story' && entry.parameters) { - return filterTabs([canvasTab, ...tabsFromConfig], entry.parameters); - } - - return [canvasTab, ...tabsFromConfig]; - }, [entry, ...tabsFromConfig]); -}; - const Preview = React.memo(function Preview(props) { const { api, @@ -67,14 +49,17 @@ const Preview = React.memo(function Preview(props) { description, baseUrl, withLoader = true, + tools, + toolsExtra, + tabs, + wrappers, + tabId, } = props; - const { getElements } = api; - const tabs = useTabs(getElements, entry); + const tabContent = tabs.find((tab) => tab.id === tabId)?.render; const shouldScale = viewMode === 'story'; - const { showToolbar, showTabs = true } = options; - const visibleTabsInToolbar = showTabs ? tabs : []; + const { showToolbar } = options; const previousStoryId = useRef(storyId); @@ -109,34 +94,16 @@ const Preview = React.memo(function Preview(props) { - ({ - customQueryParams: api.getQueryParam('tab'), - })} - > - {({ customQueryParams: x }) => { - console.log(api.getQueryParam('tab')); - console.log(x); - const tabId = api.getQueryParam('tab'); - const tabContent = tabs.find((tab) => tab.id === tabId)?.render; - - console.log('LOG: tabbs', { tabContent, tabs, tabId }); - return ( - <> - - {tabContent && tabContent({ active: true })} - - ); - }} - + {tabContent && {tabContent({ active: true })}} + @@ -146,10 +113,13 @@ const Preview = React.memo(function Preview(props) { export { Preview }; -const Canvas: FC<{ withLoader: boolean; baseUrl: string; children?: never }> = ({ - baseUrl, - withLoader, -}) => { +const Canvas: FC<{ + withLoader: boolean; + baseUrl: string; + children?: never; + active: boolean; + wrappers: Addon_WrapperType[]; +}> = ({ baseUrl, withLoader, active, wrappers }) => { return ( {({ @@ -160,15 +130,9 @@ const Canvas: FC<{ withLoader: boolean; baseUrl: string; children?: never }> = ( refId, viewMode, queryParams, - getElements, previewInitialized, - active, }) => { const id = 'canvas'; - const wrappers = useMemo( - () => [...defaultWrappers, ...getWrappers(getElements)], - [getElements, ...defaultWrappers] - ); const [progress, setProgress] = useState(undefined); useEffect(() => { @@ -196,7 +160,7 @@ const Canvas: FC<{ withLoader: boolean; baseUrl: string; children?: never }> = ( {({ value: scale }) => { return ( <> - {withLoader && isLoading && ( + {active && withLoader && isLoading && ( @@ -233,7 +197,7 @@ const Canvas: FC<{ withLoader: boolean; baseUrl: string; children?: never }> = ( ); }; -function filterTabs(panels: Addon_BaseType[], parameters: Record) { +export function filterTabs(panels: Addon_BaseType[], parameters: Record) { const { previewTabs } = addons.getConfig(); const parametersTabs = parameters ? parameters.previewTabs : undefined; diff --git a/code/ui/manager/src/components/preview/Toolbar.tsx b/code/ui/manager/src/components/preview/Toolbar.tsx index 34e297b207c5..934087058525 100644 --- a/code/ui/manager/src/components/preview/Toolbar.tsx +++ b/code/ui/manager/src/components/preview/Toolbar.tsx @@ -122,76 +122,61 @@ export const defaultToolsExtra: Addon_BaseType[] = [ copyTool, ]; -const useTools = ( - getElements: API['getElements'], - tabs: Addon_BaseType[], - viewMode: PreviewProps['viewMode'], - entry: PreviewProps['entry'], - location: PreviewProps['location'], - path: PreviewProps['path'] -) => { - const toolsFromConfig = useMemo( - () => getTools(getElements), - [getElements, getTools(getElements).length] - ); - const toolsExtraFromConfig = useMemo(() => getToolsExtra(getElements), [getElements]); - - const tools = useMemo( - () => [...defaultTools, ...toolsFromConfig], - [defaultTools, toolsFromConfig] - ); - const toolsExtra = useMemo( - () => [...defaultToolsExtra, ...toolsExtraFromConfig], - [defaultToolsExtra, toolsExtraFromConfig] - ); - - return useMemo(() => { - return ['story', 'docs'].includes(entry?.type) - ? filterTools(tools, toolsExtra, tabs, { - viewMode, - entry, - location, - path, - }) - : { left: tools, right: toolsExtra }; - }, [viewMode, entry, location, path, tools, toolsExtra, tabs]); -}; - export interface ToolData { isShown: boolean; tabs: Addon_BaseType[]; + tools: Addon_BaseType[]; + tabId: string; + toolsExtra: Addon_BaseType[]; api: API; - entry: LeafEntry; } -export const ToolRes: FunctionComponent = React.memo( - function ToolRes({ api, entry, tabs, isShown, location, path, viewMode }) { - const { left, right } = useTools(api.getElements, tabs, viewMode, entry, location, path); - - return left || right ? ( - - - - - - - - - - - ) : null; - } -); - -export const ToolbarComp = React.memo(function ToolbarComp(props) { - return ( - - {({ location, path, viewMode }) => } - - ); +export const ToolbarComp = React.memo(function ToolbarComp({ + isShown, + tools, + toolsExtra, + tabs, + tabId, + api, +}) { + console.log({ tabs, tools, toolsExtra }); + return tabs || tools || toolsExtra ? ( + + + + {tabs.length > 1 ? ( + + + {tabs.map((tab, index) => { + return ( + { + api.applyQueryParams({ tab: tab.id === 'canvas' ? undefined : tab.id }); + }} + key={tab.id || `tab-${index}`} + > + {tab.title as any} + + ); + })} + + + + ) : null} + + + + + + + + ) : null; }); export const Tools = React.memo<{ list: Addon_BaseType[] }>(function Tools({ list }) { + console.log({ list }); return ( <> {list.filter(Boolean).map(({ render: Render, id, ...t }, index) => ( @@ -202,55 +187,36 @@ export const Tools = React.memo<{ list: Addon_BaseType[] }>(function Tools({ lis ); }); -function toolbarItemHasBeenExcluded(item: Partial, entry: LeafEntry) { - const parameters = entry.type === 'story' && entry.prepared ? entry.parameters : {}; +function toolbarItemHasBeenExcluded(item: Partial, entry: LeafEntry | undefined) { + const parameters = entry?.type === 'story' && entry?.prepared ? entry?.parameters : {}; const toolbarItemsFromStoryParameters = 'toolbar' in parameters ? parameters.toolbar : undefined; const { toolbar: toolbarItemsFromAddonsConfig } = addons.getConfig(); const toolbarItems = merge(toolbarItemsFromAddonsConfig, toolbarItemsFromStoryParameters); - return toolbarItems ? !!toolbarItems[item.id]?.hidden : false; + return toolbarItems ? !!toolbarItems[item?.id]?.hidden : false; } -export function filterTools( +export function filterToolsSide( tools: Addon_BaseType[], - toolsExtra: Addon_BaseType[], - tabs: Addon_BaseType[], - { - viewMode, - entry, - location, - path, - }: { - viewMode: State['viewMode']; - entry: PreviewProps['entry']; - location: State['location']; - path: State['path']; - } + entry: PreviewProps['entry'], + viewMode: State['viewMode'], + location: State['location'], + path: State['path'] ) { - const toolsLeft = [ - menuTool, - tabs.filter((p) => !p.hidden).length > 1 && createTabsTool(tabs), - ...tools, - ]; - const toolsRight = [...toolsExtra]; - const filter = (item: Partial) => item && (!item.match || item.match({ - storyId: entry.id, - refId: entry.refId, + storyId: entry?.id, + refId: entry?.refId, viewMode, location, path, })) && !toolbarItemHasBeenExcluded(item, entry); - const left = toolsLeft.filter(filter); - const right = toolsRight.filter(filter); - - return { left, right }; + return tools.filter(filter); } const Toolbar = styled.div<{ shown: boolean }>(({ theme, shown }) => ({ diff --git a/code/ui/manager/src/components/preview/Wrappers.tsx b/code/ui/manager/src/components/preview/Wrappers.tsx index 80640a9875fa..b5c1319c753b 100644 --- a/code/ui/manager/src/components/preview/Wrappers.tsx +++ b/code/ui/manager/src/components/preview/Wrappers.tsx @@ -29,7 +29,11 @@ export const defaultWrappers: Addon_WrapperType[] = [ id: 'iframe-wrapper', type: Addon_TypesEnum.PREVIEW, render: (p) => ( -