Make FileTree a pane (desktop only) (#2232)

This commit is contained in:
Frank Noirot
2024-04-25 09:56:55 -04:00
committed by GitHub
parent 0a96dc6fd2
commit fab3d2b130
4 changed files with 141 additions and 101 deletions

View File

@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { Dispatch, useEffect, useRef, useState } from 'react' import { Dispatch, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react' import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons' import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
@ -133,18 +133,13 @@ const FileTreeItem = ({
project, project,
currentFile, currentFile,
fileOrDir, fileOrDir,
closePanel, onDoubleClick,
level = 0, level = 0,
}: { }: {
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file'] currentFile?: IndexLoaderData['file']
fileOrDir: FileEntry fileOrDir: FileEntry
closePanel: ( onDoubleClick?: () => void
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
level?: number level?: number
}) => { }) => {
const { send, context } = useFileContext() const { send, context } = useFileContext()
@ -186,7 +181,7 @@ const FileTreeItem = ({
// Open kcl files // Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`) navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
} }
closePanel() onDoubleClick?.()
} }
return ( return (
@ -194,8 +189,10 @@ const FileTreeItem = ({
{fileOrDir.children === undefined ? ( {fileOrDir.children === undefined ? (
<li <li
className={ className={
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' + 'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
(isCurrentFile ? '!bg-primary/10 !text-primary' : '') (isCurrentFile
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
: '')
} }
> >
{!isRenaming ? ( {!isRenaming ? (
@ -227,9 +224,9 @@ const FileTreeItem = ({
{!isRenaming ? ( {!isRenaming ? (
<Disclosure.Button <Disclosure.Button
className={ className={
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5' + ' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
(context.selectedDirectory.path.includes(fileOrDir.path) (context.selectedDirectory.path.includes(fileOrDir.path)
? ' ui-open:text-primary' ? ' ui-open:bg-primary/10'
: '') : '')
} }
style={{ paddingInlineStart: getIndentationCSS(level) }} style={{ paddingInlineStart: getIndentationCSS(level) }}
@ -293,7 +290,7 @@ const FileTreeItem = ({
fileOrDir={child} fileOrDir={child}
project={project} project={project}
currentFile={currentFile} currentFile={currentFile}
closePanel={closePanel} onDoubleClick={onDoubleClick}
level={level + 1} level={level + 1}
key={level + '-' + child.path} key={level + '-' + child.path}
/> />
@ -325,20 +322,8 @@ interface FileTreeProps {
) => void ) => void
} }
export const FileTree = ({ export const FileTreeMenu = () => {
className = '', const { send } = useFileContext()
file,
closePanel,
}: FileTreeProps) => {
const { send, context } = useFileContext()
const docuemntHasFocus = useDocumentHasFocus()
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
}, [docuemntHasFocus])
async function createFile() { async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } }) send({ type: 'Create file', data: { name: '', makeDir: false } })
@ -348,10 +333,11 @@ export const FileTree = ({
send({ type: 'Create file', data: { name: '', makeDir: true } }) send({ type: 'Create file', data: { name: '', makeDir: true } })
} }
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
return ( return (
<div className={className}> <>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ icon={{
@ -381,7 +367,37 @@ export const FileTree = ({
Create folder Create folder
</Tooltip> </Tooltip>
</ActionButton> </ActionButton>
</>
)
}
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
return (
<div className={className}>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<FileTreeMenu />
</div> </div>
<FileTreeInner onDoubleClick={closePanel} />
</div>
)
}
export const FileTreeInner = ({
onDoubleClick,
}: {
onDoubleClick?: () => void
}) => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const { send, context } = useFileContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
}, [documentHasFocus])
return (
<div className="overflow-auto max-h-full pb-12"> <div className="overflow-auto max-h-full pb-12">
<ul <ul
className="m-0 p-0 text-sm" className="m-0 p-0 text-sm"
@ -392,14 +408,13 @@ export const FileTree = ({
{sortProject(context.project.children || []).map((fileOrDir) => ( {sortProject(context.project.children || []).map((fileOrDir) => (
<FileTreeItem <FileTreeItem
project={context.project} project={context.project}
currentFile={file} currentFile={loaderData?.file}
fileOrDir={fileOrDir} fileOrDir={fileOrDir}
closePanel={closePanel} onDoubleClick={onDoubleClick}
key={fileOrDir.path} key={fileOrDir.path}
/> />
))} ))}
</ul> </ul>
</div> </div>
</div>
) )
} }

View File

@ -10,21 +10,32 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane' import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import type { PaneType } from 'useStore'
import { MemoryPane } from './MemoryPane' import { MemoryPane } from './MemoryPane'
import { KclErrorsPane, LogsPane } from './LoggingPanes' import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane' import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
export type Pane = { export type SidebarType =
id: PaneType | 'code'
| 'debug'
| 'export'
| 'files'
| 'kclErrors'
| 'logs'
| 'lspMessages'
| 'variables'
export type SidebarPane = {
id: SidebarType
title: string title: string
icon: CustomIconName | IconDefinition icon: CustomIconName | IconDefinition
keybinding: string
Content: ReactNode | React.FC Content: ReactNode | React.FC
Menu?: ReactNode | React.FC Menu?: ReactNode | React.FC
keybinding: string hideOnPlatform?: 'desktop' | 'web'
} }
export const topPanes: Pane[] = [ export const topPanes: SidebarPane[] = [
{ {
id: 'code', id: 'code',
title: 'KCL Code', title: 'KCL Code',
@ -33,9 +44,18 @@ export const topPanes: Pane[] = [
keybinding: 'shift + c', keybinding: 'shift + c',
Menu: KclEditorMenu, Menu: KclEditorMenu,
}, },
{
id: 'files',
title: 'Project Files',
icon: 'folder',
Content: FileTreeInner,
keybinding: 'shift + f',
Menu: FileTreeMenu,
hideOnPlatform: 'web',
},
] ]
export const bottomPanes: Pane[] = [ export const bottomPanes: SidebarPane[] = [
{ {
id: 'variables', id: 'variables',
title: 'Variables', title: 'Variables',

View File

@ -2,13 +2,19 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { PaneType, useStore } from 'useStore' import { useStore } from 'useStore'
import { Tab } from '@headlessui/react' import { Tab } from '@headlessui/react'
import { Pane, bottomPanes, topPanes } from './ModelingPanes' import {
SidebarPane,
SidebarType,
bottomPanes,
topPanes,
} 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'
import { ModelingPane } from './ModelingPane' import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -52,7 +58,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
} }
interface ModelingSidebarSectionProps { interface ModelingSidebarSectionProps {
panes: Pane[] panes: SidebarPane[]
alignButtons?: 'start' | 'end' alignButtons?: 'start' | 'end'
} }
@ -69,11 +75,11 @@ function ModelingSidebarSection({
})) }))
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane)) const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
const [currentPane, setCurrentPane] = useState( const [currentPane, setCurrentPane] = useState(
foundOpenPane || ('none' as PaneType | 'none') foundOpenPane || ('none' as SidebarType | 'none')
) )
const togglePane = useCallback( const togglePane = useCallback(
(newPane: PaneType | 'none') => { (newPane: SidebarType | 'none') => {
if (newPane === 'none') { if (newPane === 'none') {
setOpenPanes(openPanes.filter((p) => p !== currentPane)) setOpenPanes(openPanes.filter((p) => p !== currentPane))
setCurrentPane('none') setCurrentPane('none')
@ -90,9 +96,15 @@ function ModelingSidebarSection({
// 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
// TODO: abstract out for allowing user to configure which panes to show // TODO: abstract out for allowing user to configure which panes to show
const filteredPanes = showDebugPanel.current const filteredPanes = (
? panes showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
: panes.filter((pane) => pane.id !== 'debug') ).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
)
useEffect(() => { useEffect(() => {
if ( if (
!showDebugPanel.current && !showDebugPanel.current &&
@ -168,8 +180,8 @@ function ModelingSidebarSection({
} }
interface ModelingPaneButtonProps { interface ModelingPaneButtonProps {
paneConfig: Pane paneConfig: SidebarPane
currentPane: PaneType | 'none' currentPane: SidebarType | 'none'
togglePane: () => void togglePane: () => void
} }

View File

@ -9,6 +9,7 @@ import {
import { enginelessExecutor } from './lib/testHelpers' import { enginelessExecutor } from './lib/testHelpers'
import { EngineCommandManager } from './lang/std/engineConnection' import { EngineCommandManager } from './lang/std/engineConnection'
import { KCLError } from './lang/errors' import { KCLError } from './lang/errors'
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
export type ToolTip = export type ToolTip =
| 'lineTo' | 'lineTo'
@ -44,14 +45,6 @@ export const toolTips = [
'tangentialArcTo', 'tangentialArcTo',
] as any as ToolTip[] ] as any as ToolTip[]
export type PaneType =
| 'code'
| 'variables'
| 'debug'
| 'kclErrors'
| 'logs'
| 'lspMessages'
export interface StoreState { export interface StoreState {
mediaStream?: MediaStream mediaStream?: MediaStream
setMediaStream: (mediaStream: MediaStream) => void setMediaStream: (mediaStream: MediaStream) => void
@ -77,8 +70,8 @@ export interface StoreState {
showHomeMenu: boolean showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void setHomeShowMenu: (showMenu: boolean) => void
openPanes: PaneType[] openPanes: SidebarType[]
setOpenPanes: (panes: PaneType[]) => void setOpenPanes: (panes: SidebarType[]) => void
homeMenuItems: { homeMenuItems: {
name: string name: string
path: string path: string