Bug fix: make "phantom side panes" get properly cleared (#3878)

* Write a failing test

* Make `openPanes` get cleared of any hidden panes

* Grrr fmt
This commit is contained in:
Frank Noirot
2024-09-13 14:49:33 -04:00
committed by GitHub
parent 8c6266e94b
commit 3cd3e1af72
3 changed files with 148 additions and 29 deletions

View File

@ -9,7 +9,11 @@ import {
executorInputPath, executorInputPath,
} from './test-utils' } from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes' import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates' import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS,
} from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
@ -637,4 +641,82 @@ const extrude001 = extrude(5, sketch001)
.toBeLessThan(15) .toBeLessThan(15)
}) })
}) })
test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
page,
}) => {
const u = await getUtils(page)
// Override beforeEach test setup
// with debug panel open
// but "show debug panel" set to false
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
localStorage.setItem(
'persistModelingContext',
'{"openPanes":["debug"]}'
)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: {
...TEST_SETTINGS,
modeling: { ...TEST_SETTINGS.modeling, showDebugPanel: false },
},
}),
}
)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · modeling · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
).toBeVisible()
}
await test.step(`Initial load with corrupted settings`, async () => {
await u.waitForAuthSkipAppStart()
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
})
}) })

View File

@ -15,6 +15,8 @@ import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree' import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { ContextFrom } from 'xstate'
import { settingsMachine } from 'machines/settingsMachine'
export type SidebarType = export type SidebarType =
| 'code' | 'code'
@ -36,6 +38,8 @@ export interface BadgeInfo {
*/ */
interface PaneCallbackProps { interface PaneCallbackProps {
kclContext: ReturnType<typeof useKclContext> kclContext: ReturnType<typeof useKclContext>
settings: ContextFrom<typeof settingsMachine>
platform: 'web' | 'desktop'
} }
export type SidebarPane = { export type SidebarPane = {
@ -45,10 +49,21 @@ export type SidebarPane = {
keybinding: string keybinding: string
Content: ReactNode | React.FC Content: ReactNode | React.FC
Menu?: ReactNode | React.FC Menu?: ReactNode | React.FC
hideOnPlatform?: 'desktop' | 'web' hide?: boolean | ((props: PaneCallbackProps) => boolean)
showBadge?: BadgeInfo showBadge?: BadgeInfo
} }
export type SidebarAction = {
id: string
title: string
icon: CustomIconName
iconClassName?: string // Just until we get rid of FontAwesome icons
keybinding: string
action: () => void
hide?: boolean | ((props: PaneCallbackProps) => boolean)
disable?: () => string | undefined
}
export const sidebarPanes: SidebarPane[] = [ export const sidebarPanes: SidebarPane[] = [
{ {
id: 'code', id: 'code',
@ -74,7 +89,7 @@ export const sidebarPanes: SidebarPane[] = [
Content: FileTreeInner, Content: FileTreeInner,
keybinding: 'Shift + F', keybinding: 'Shift + F',
Menu: FileTreeMenu, Menu: FileTreeMenu,
hideOnPlatform: 'web', hide: ({ platform }) => platform === 'web',
}, },
{ {
id: 'variables', id: 'variables',
@ -97,5 +112,6 @@ export const sidebarPanes: SidebarPane[] = [
icon: faBugSlash, icon: faBugSlash,
Content: DebugPane, Content: DebugPane,
keybinding: 'Shift + D', keybinding: 'Shift + D',
hide: ({ settings }) => !settings.modeling.showDebugPanel.current,
}, },
] ]

View File

@ -1,8 +1,8 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { MouseEventHandler, useCallback, useMemo } from 'react' import { MouseEventHandler, useCallback, useEffect, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarType, sidebarPanes } from './ModelingPanes' import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon' import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css' import styles from './ModelingSidebar.module.css'
@ -24,6 +24,10 @@ interface BadgeInfoComputed {
onClick?: MouseEventHandler<any> onClick?: MouseEventHandler<any>
} }
function getPlatformString(): 'web' | 'desktop' {
return isDesktop() ? 'desktop' : 'web'
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext() const kclContext = useKclContext()
@ -37,6 +41,15 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
: 'pointer-events-auto ' : 'pointer-events-auto '
const showDebugPanel = settings.context.modeling.showDebugPanel const showDebugPanel = settings.context.modeling.showDebugPanel
const paneCallbackProps = useMemo(
() => ({
kclContext,
settings: settings.context,
platform: getPlatformString(),
}),
[kclContext.errors, settings.context]
)
const sidebarActions: SidebarAction[] = [ const sidebarActions: SidebarAction[] = [
{ {
id: 'export', id: 'export',
@ -71,11 +84,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
] ]
const filteredActions: SidebarAction[] = sidebarActions.filter( const filteredActions: SidebarAction[] = sidebarActions.filter(
(action) => (action) =>
(!action.hide || (action.hide instanceof Function && !action.hide())) && !action.hide ||
(!action.hideOnPlatform || (action.hide instanceof Function && !action.hide(paneCallbackProps))
(isDesktop()
? action.hideOnPlatform === 'web'
: action.hideOnPlatform === 'desktop'))
) )
// // Filter out the debug panel if it's not supposed to be shown // // Filter out the debug panel if it's not supposed to be shown
@ -87,25 +97,47 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
: sidebarPanes.filter((pane) => pane.id !== 'debug') : sidebarPanes.filter((pane) => pane.id !== 'debug')
).filter( ).filter(
(pane) => (pane) =>
!pane.hideOnPlatform || !pane.hide ||
(isDesktop() (pane.hide instanceof Function && !pane.hide(paneCallbackProps))
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
), ),
[sidebarPanes, showDebugPanel.current] [sidebarPanes, paneCallbackProps]
) )
const paneBadgeMap: Record<SidebarType, BadgeInfoComputed> = useMemo(() => { const paneBadgeMap: Record<SidebarType, BadgeInfoComputed> = useMemo(() => {
return filteredPanes.reduce((acc, pane) => { return filteredPanes.reduce((acc, pane) => {
if (pane.showBadge) { if (pane.showBadge) {
acc[pane.id] = { acc[pane.id] = {
value: pane.showBadge.value({ kclContext }), value: pane.showBadge.value(paneCallbackProps),
onClick: pane.showBadge.onClick, onClick: pane.showBadge.onClick,
} }
} }
return acc return acc
}, {} as Record<SidebarType, BadgeInfoComputed>) }, {} as Record<SidebarType, BadgeInfoComputed>)
}, [kclContext.errors]) }, [paneCallbackProps])
// Clear any hidden panes from the `openPanes` array
useEffect(() => {
const panesToReset: SidebarType[] = []
sidebarPanes.forEach((pane) => {
if (
pane.hide === true ||
(pane.hide instanceof Function && pane.hide(paneCallbackProps))
) {
panesToReset.push(pane.id)
}
})
if (panesToReset.length > 0) {
send({
type: 'Set context',
data: {
openPanes: context.store?.openPanes.filter(
(pane) => !panesToReset.includes(pane)
),
},
})
}
}, [settings.context])
const togglePane = useCallback( const togglePane = useCallback(
(newPane: SidebarType) => { (newPane: SidebarType) => {
@ -130,6 +162,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
}} }}
minWidth={200} minWidth={200}
maxWidth={800} maxWidth={800}
handleWrapperClass="sidebar-resize-handles"
handleClasses={{ handleClasses={{
right: right:
(context.store?.openPanes.length === 0 ? 'hidden ' : 'block ') + (context.store?.openPanes.length === 0 ? 'hidden ' : 'block ') +
@ -324,15 +357,3 @@ function ModelingPaneButton({
</div> </div>
) )
} }
export type SidebarAction = {
id: string
title: string
icon: CustomIconName
iconClassName?: string // Just until we get rid of FontAwesome icons
keybinding: string
action: () => void
hideOnPlatform?: 'desktop' | 'web'
hide?: boolean | (() => boolean)
disable?: () => string | undefined
}