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:
@ -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')
|
||||||
|
@ -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,
|
||||||
|
@ -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 }),
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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}
|
||||||
|
@ -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',
|
@ -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>
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user