Show top level dir (#4165)

* Reload FileTree and File when changed externally

* Added tests

* Show project root in files pane

* Cut off titles that are too long

* Fix tests
This commit is contained in:
49fl
2024-10-28 14:29:47 -04:00
committed by GitHub
parent 4a62862ca0
commit 05610bb0f3
11 changed files with 81 additions and 29 deletions

View File

@ -425,7 +425,9 @@ test(
const restartConfirmationButton = page.getByRole('button', { const restartConfirmationButton = page.getByRole('button', {
name: 'Make a new project', name: 'Make a new project',
}) })
const tutorialProjectIndicator = page.getByText('Tutorial Project 00') const tutorialProjectIndicator = page
.getByTestId('project-sidebar-toggle')
.filter({ hasText: 'Tutorial Project 00' })
const tutorialModalText = page.getByText('Welcome to Modeling App!') const tutorialModalText = page.getByText('Welcome to Modeling App!')
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' }) const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
const userMenuButton = page.getByTestId('user-sidebar-toggle') const userMenuButton = page.getByTestId('user-sidebar-toggle')

View File

@ -507,17 +507,18 @@ test(
'File in the file pane should open with a single click', 'File in the file pane should open with a single click',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectName = 'router-template-slate'
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl` `${dir}/${projectName}/main.kcl`
) )
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', 'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/router-template-slate/otherThingToClickOn.kcl` `${dir}/${projectName}/otherThingToClickOn.kcl`
) )
}, },
}) })
@ -526,7 +527,7 @@ test(
page.on('console', console.log) page.on('console', console.log)
await page.getByText('router-template-slate').click() await page.getByText(projectName).click()
await expect(page.getByTestId('loading')).toBeAttached() await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({ await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000, timeout: 20_000,

View File

@ -710,7 +710,9 @@ test(
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
// Locators // Locators
const projectMenuButton = page.getByRole('button', { name: projectName }) const projectMenuButton = page
.getByTestId('project-sidebar-toggle')
.filter({ hasText: projectName })
const textToCadFileButton = page.getByRole('listitem').filter({ const textToCadFileButton = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: textToCadFileName }), has: page.getByRole('button', { name: textToCadFileName }),
}) })

View File

@ -538,3 +538,19 @@ export const FileTreeInner = ({
</div> </div>
) )
} }
export const FileTreeRoot = () => {
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project } = loaderData
// project.path should never be empty here but I guess during initial loading
// it can be.
return (
<div
className="max-w-xs text-ellipsis overflow-hidden cursor-pointer"
title={project?.path ?? ''}
>
{project?.name ?? ''}
</div>
)
}

View File

@ -4,7 +4,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { createAndOpenNewProject } from 'lib/desktopFS' import { createAndOpenNewTutorialProject } from 'lib/desktopFS'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
@ -116,9 +116,10 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) { if (isInProject) {
navigate(filePath + PATHS.ONBOARDING.INDEX) navigate(filePath + PATHS.ONBOARDING.INDEX)
} else { } else {
createAndOpenNewProject({ onProjectOpen, navigate }).catch( createAndOpenNewTutorialProject({
reportRejection onProjectOpen,
) navigate,
}).catch(reportRejection)
} }
}} }}
> >

View File

@ -1,3 +1,4 @@
import { ReactNode } from 'react'
import styles from './ModelingPane.module.css' import styles from './ModelingPane.module.css'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
@ -6,22 +7,24 @@ import { CustomIconName } from 'components/CustomIcon'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from 'components/ActionIcon' import { ActionIcon } from 'components/ActionIcon'
export interface ModelingPaneProps export interface ModelingPaneProps {
extends React.PropsWithChildren, id: string
React.HTMLAttributes<HTMLDivElement> { children: ReactNode | ReactNode[]
className?: string
icon?: CustomIconName | IconDefinition icon?: CustomIconName | IconDefinition
title: string title: ReactNode
Menu?: React.ReactNode | React.FC Menu?: React.ReactNode | React.FC
detailsTestId?: string detailsTestId?: string
onClose: () => void onClose: () => void
} }
export const ModelingPaneHeader = ({ export const ModelingPaneHeader = ({
id,
icon, icon,
title, title,
Menu, Menu,
onClose, onClose,
}: Pick<ModelingPaneProps, 'icon' | 'title' | 'Menu' | 'onClose'>) => { }: Pick<ModelingPaneProps, 'id' | 'icon' | 'title' | 'Menu' | 'onClose'>) => {
return ( return (
<div className={styles.header}> <div className={styles.header}>
<div className="flex gap-2 items-center flex-1"> <div className="flex gap-2 items-center flex-1">
@ -34,7 +37,7 @@ export const ModelingPaneHeader = ({
bgClassName="!bg-transparent" bgClassName="!bg-transparent"
/> />
)} )}
<span>{title}</span> <span data-testid={id + '-header'}>{title}</span>
</div> </div>
{Menu instanceof Function ? <Menu /> : Menu} {Menu instanceof Function ? <Menu /> : Menu}
<ActionButton <ActionButton
@ -86,6 +89,7 @@ export const ModelingPane = ({
} }
> >
<ModelingPaneHeader <ModelingPaneHeader
id={id}
icon={icon} icon={icon}
title={title} title={title}
Menu={Menu} Menu={Menu}

View File

@ -6,7 +6,7 @@ import { MouseEventHandler, ReactNode } from 'react'
import { MemoryPane, MemoryPaneMenu } from './MemoryPane' import { MemoryPane, MemoryPaneMenu } from './MemoryPane'
import { LogsPane } from './LoggingPanes' import { LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane' import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree' import { FileTreeInner, FileTreeMenu, FileTreeRoot } 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 { ContextFrom } from 'xstate'
@ -38,7 +38,8 @@ interface PaneCallbackProps {
export type SidebarPane = { export type SidebarPane = {
id: SidebarType id: SidebarType
title: string title: ReactNode
sidebarName?: string
icon: CustomIconName | IconDefinition icon: CustomIconName | IconDefinition
keybinding: string keybinding: string
Content: ReactNode | React.FC Content: ReactNode | React.FC
@ -49,7 +50,7 @@ export type SidebarPane = {
export type SidebarAction = { export type SidebarAction = {
id: string id: string
title: string title: ReactNode
icon: CustomIconName icon: CustomIconName
iconClassName?: string // Just until we get rid of FontAwesome icons iconClassName?: string // Just until we get rid of FontAwesome icons
keybinding: string keybinding: string
@ -78,7 +79,8 @@ export const sidebarPanes: SidebarPane[] = [
}, },
{ {
id: 'files', id: 'files',
title: 'Project Files', title: <FileTreeRoot />,
sidebarName: 'Project Files',
icon: 'folder', icon: 'folder',
Content: FileTreeInner, Content: FileTreeInner,
keybinding: 'Shift + F', keybinding: 'Shift + F',

View File

@ -5,6 +5,7 @@ import {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
ReactNode,
useContext, useContext,
} from 'react' } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -270,7 +271,8 @@ interface ModelingPaneButtonProps
extends React.HTMLAttributes<HTMLButtonElement> { extends React.HTMLAttributes<HTMLButtonElement> {
paneConfig: { paneConfig: {
id: string id: string
title: string title: ReactNode
sidebarName?: string
icon: CustomIconName | IconDefinition icon: CustomIconName | IconDefinition
keybinding: string keybinding: string
iconClassName?: string iconClassName?: string
@ -299,7 +301,10 @@ function ModelingPaneButton({
<button <button
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary" className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick} onClick={onClick}
name={paneConfig.title} name={
paneConfig.sidebarName ??
(typeof paneConfig.title === 'string' ? paneConfig.title : '')
}
data-testid={paneConfig.id + '-pane-button'} data-testid={paneConfig.id + '-pane-button'}
disabled={disabledText !== undefined} disabled={disabledText !== undefined}
aria-disabled={disabledText !== undefined} aria-disabled={disabledText !== undefined}
@ -315,7 +320,7 @@ function ModelingPaneButton({
} }
/> />
<span className="sr-only"> <span className="sr-only">
{paneConfig.title} {paneConfig.sidebarName ?? paneConfig.title}
{paneIsOpen !== undefined ? ` pane` : ''} {paneIsOpen !== undefined ? ` pane` : ''}
</span> </span>
<Tooltip <Tooltip
@ -324,7 +329,7 @@ function ModelingPaneButton({
hoverOnly hoverOnly
> >
<span className="flex-1"> <span className="flex-1">
{paneConfig.title} {paneConfig.sidebarName ?? paneConfig.title}
{disabledText !== undefined ? ` (${disabledText})` : ''} {disabledText !== undefined ? ` (${disabledText})` : ''}
{paneIsOpen !== undefined ? ` pane` : ''} {paneIsOpen !== undefined ? ` pane` : ''}
</span> </span>

View File

@ -15,7 +15,10 @@ import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { APP_VERSION } from 'routes/Settings' import { APP_VERSION } from 'routes/Settings'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS' import {
createAndOpenNewTutorialProject,
getSettingsFolderPaths,
} from 'lib/desktopFS'
import { useDotDotSlash } from 'hooks/useDotDotSlash' import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { ForwardedRef, forwardRef, useEffect } from 'react' import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
@ -79,7 +82,7 @@ export const AllSettingsFields = forwardRef(
} else { } else {
// If we're in the global settings, create a new project and navigate // If we're in the global settings, create a new project and navigate
// to the onboarding start in that project // to the onboarding start in that project
await createAndOpenNewProject({ onProjectOpen, navigate }) await createAndOpenNewTutorialProject({ onProjectOpen, navigate })
} }
} }
} }

View File

@ -120,7 +120,7 @@ export async function getSettingsFolderPaths(projectPath?: string) {
} }
} }
export async function createAndOpenNewProject({ export async function createAndOpenNewTutorialProject({
onProjectOpen, onProjectOpen,
navigate, navigate,
}: { }: {
@ -144,6 +144,22 @@ export async function createAndOpenNewProject({
ONBOARDING_PROJECT_NAME, ONBOARDING_PROJECT_NAME,
nextIndex nextIndex
) )
// Delete the tutorial project if it already exists.
if (isDesktop()) {
if (configuration.settings?.project?.directory === undefined) {
return Promise.reject(new Error('configuration settings are undefined'))
}
const fullPath = window.electron.join(
configuration.settings.project.directory,
name
)
if (window.electron.exists(fullPath)) {
await window.electron.rm(fullPath)
}
}
const newProject = await createNewProjectDirectory( const newProject = await createNewProjectDirectory(
name, name,
bracket, bracket,

View File

@ -3,7 +3,7 @@ import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { createAndOpenNewProject } from 'lib/desktopFS' import { createAndOpenNewTutorialProject } from 'lib/desktopFS'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
@ -63,7 +63,7 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
fileContext.project.path || null, fileContext.project.path || null,
false false
) )
await createAndOpenNewProject({ onProjectOpen, navigate }) await createAndOpenNewTutorialProject({ onProjectOpen, navigate })
props.setShouldShowWarning(false) props.setShouldShowWarning(false)
} }