diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index a72f098a4..7b04d3200 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -8094,3 +8094,34 @@ test('Sketch on face', async ({ page }) => { const sketch002 = extrude(${[5, 5]} + 7, sketch002)` await expect(page.locator('.cm-content')).toHaveText(result2.regExp) }) + +test('Typing KCL errors induces a badge on the error logs pane button', async ({ + page, +}) => { + const u = await getUtils(page) + + // Load the app with the working starter code + await page.addInitScript((code) => { + localStorage.setItem('persistCode', code) + }, bracket) + + await page.setViewportSize({ width: 1200, height: 500 }) + await u.waitForAuthSkipAppStart() + + // wait for execution done + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + // Ensure no badge is present + const errorLogsButton = page.getByRole('button', { name: 'KCL Errors pane' }) + await expect(errorLogsButton).not.toContainText('notification') + + // Delete a character to break the KCL + await u.openKclCodePanel() + await page.getByText('extrude(').click() + await page.keyboard.press('Backspace') + + // Ensure that a badge appears on the button + await expect(errorLogsButton).toContainText('notification') +}) diff --git a/src/components/ModelingSidebar/ModelingPanes/index.ts b/src/components/ModelingSidebar/ModelingPanes/index.ts index 1f1e7f2f4..a3e373fb7 100644 --- a/src/components/ModelingSidebar/ModelingPanes/index.ts +++ b/src/components/ModelingSidebar/ModelingPanes/index.ts @@ -14,6 +14,7 @@ import { MemoryPane, MemoryPaneMenu } from './MemoryPane' import { KclErrorsPane, LogsPane } from './LoggingPanes' import { DebugPane } from './DebugPane' import { FileTreeInner, FileTreeMenu } from 'components/FileTree' +import { useKclContext } from 'lang/KclProvider' export type SidebarType = | 'code' @@ -25,6 +26,14 @@ export type SidebarType = | 'lspMessages' | 'variables' +/** + * This interface can be extended as more context is needed for the panes + * to determine if they should show their badges or not. + */ +interface PaneCallbackProps { + kclContext: ReturnType +} + export type SidebarPane = { id: SidebarType title: string @@ -33,6 +42,7 @@ export type SidebarPane = { Content: ReactNode | React.FC Menu?: ReactNode | React.FC hideOnPlatform?: 'desktop' | 'web' + showBadge?: (props: PaneCallbackProps) => boolean | number } export const sidebarPanes: SidebarPane[] = [ @@ -74,6 +84,7 @@ export const sidebarPanes: SidebarPane[] = [ icon: faExclamationCircle, Content: KclErrorsPane, keybinding: 'Shift + E', + showBadge: ({ kclContext }) => kclContext.errors.length, }, { id: 'debug', diff --git a/src/components/ModelingSidebar/ModelingSidebar.tsx b/src/components/ModelingSidebar/ModelingSidebar.tsx index c432f8dde..abc58ba71 100644 --- a/src/components/ModelingSidebar/ModelingSidebar.tsx +++ b/src/components/ModelingSidebar/ModelingSidebar.tsx @@ -12,6 +12,7 @@ import { useModelingContext } from 'hooks/useModelingContext' import { CustomIconName } from 'components/CustomIcon' import { useCommandsContext } from 'hooks/useCommandsContext' import { IconDefinition } from '@fortawesome/free-solid-svg-icons' +import { useKclContext } from 'lang/KclProvider' interface ModelingSidebarProps { paneOpacity: '' | 'opacity-20' | 'opacity-40' @@ -19,6 +20,7 @@ interface ModelingSidebarProps { export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { const { commandBarSend } = useCommandsContext() + const kclContext = useKclContext() const { settings } = useSettingsAuthContext() const onboardingStatus = settings.context.app.onboardingStatus const { send, context } = useModelingContext() @@ -62,6 +64,15 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { [sidebarPanes, showDebugPanel.current] ) + const paneBadgeMap: Record = useMemo(() => { + return filteredPanes.reduce((acc, pane) => { + if (pane.showBadge) { + acc[pane.id] = pane.showBadge({ kclContext }) + } + return acc + }, {} as Record) + }, [kclContext.errors]) + const togglePane = useCallback( (newPane: SidebarType) => { send({ @@ -120,6 +131,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { paneIsOpen={context.store?.openPanes.includes(pane.id)} onClick={() => togglePane(pane.id)} aria-pressed={context.store?.openPanes.includes(pane.id)} + showBadge={paneBadgeMap[pane.id]} /> ))} @@ -186,12 +198,14 @@ interface ModelingPaneButtonProps } onClick: () => void paneIsOpen?: boolean + showBadge?: boolean | number } function ModelingPaneButton({ paneConfig, onClick, paneIsOpen, + showBadge, ...props }: ModelingPaneButtonProps) { useHotkeys(paneConfig.keybinding, onClick, { @@ -223,6 +237,23 @@ function ModelingPaneButton({ {paneConfig.title} {paneIsOpen !== undefined ? ` pane` : ''} + {!!showBadge && ( +

+  has  + {typeof showBadge === 'number' ? ( + {showBadge} + ) : ( + a + )} + +  notification{Number(showBadge) > 1 ? 's' : ''} + +

+ )}