Make FileTree a pane (desktop only) (#2232)
This commit is contained in:
@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import Tooltip from './Tooltip'
|
||||
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
@ -133,18 +133,13 @@ const FileTreeItem = ({
|
||||
project,
|
||||
currentFile,
|
||||
fileOrDir,
|
||||
closePanel,
|
||||
onDoubleClick,
|
||||
level = 0,
|
||||
}: {
|
||||
project?: IndexLoaderData['project']
|
||||
currentFile?: IndexLoaderData['file']
|
||||
fileOrDir: FileEntry
|
||||
closePanel: (
|
||||
focusableElement?:
|
||||
| HTMLElement
|
||||
| React.MutableRefObject<HTMLElement | null>
|
||||
| undefined
|
||||
) => void
|
||||
onDoubleClick?: () => void
|
||||
level?: number
|
||||
}) => {
|
||||
const { send, context } = useFileContext()
|
||||
@ -186,7 +181,7 @@ const FileTreeItem = ({
|
||||
// Open kcl files
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
}
|
||||
closePanel()
|
||||
onDoubleClick?.()
|
||||
}
|
||||
|
||||
return (
|
||||
@ -194,8 +189,10 @@ const FileTreeItem = ({
|
||||
{fileOrDir.children === undefined ? (
|
||||
<li
|
||||
className={
|
||||
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' +
|
||||
(isCurrentFile ? '!bg-primary/10 !text-primary' : '')
|
||||
'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 dark:!bg-primary/20 dark:!text-inherit'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
{!isRenaming ? (
|
||||
@ -227,9 +224,9 @@ const FileTreeItem = ({
|
||||
{!isRenaming ? (
|
||||
<Disclosure.Button
|
||||
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)
|
||||
? ' ui-open:text-primary'
|
||||
? ' ui-open:bg-primary/10'
|
||||
: '')
|
||||
}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
@ -293,7 +290,7 @@ const FileTreeItem = ({
|
||||
fileOrDir={child}
|
||||
project={project}
|
||||
currentFile={currentFile}
|
||||
closePanel={closePanel}
|
||||
onDoubleClick={onDoubleClick}
|
||||
level={level + 1}
|
||||
key={level + '-' + child.path}
|
||||
/>
|
||||
@ -325,20 +322,8 @@ interface FileTreeProps {
|
||||
) => void
|
||||
}
|
||||
|
||||
export const FileTree = ({
|
||||
className = '',
|
||||
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])
|
||||
export const FileTreeMenu = () => {
|
||||
const { send } = useFileContext()
|
||||
|
||||
async function createFile() {
|
||||
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
||||
@ -348,58 +333,88 @@ export const FileTree = ({
|
||||
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
||||
}
|
||||
|
||||
useHotkeys('meta + n', createFile)
|
||||
useHotkeys('meta + shift + n', createFolder)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'filePlus',
|
||||
iconClassName: '!text-current',
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={createFile}
|
||||
>
|
||||
<Tooltip position="bottom-right" delay={750}>
|
||||
Create file
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'folderPlus',
|
||||
iconClassName: '!text-current',
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={createFolder}
|
||||
>
|
||||
<Tooltip position="bottom-right" delay={750}>
|
||||
Create folder
|
||||
</Tooltip>
|
||||
</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>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'filePlus',
|
||||
iconClassName: '!text-current',
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={createFile}
|
||||
>
|
||||
<Tooltip position="bottom-right" delay={750}>
|
||||
Create file
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'folderPlus',
|
||||
iconClassName: '!text-current',
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={createFolder}
|
||||
>
|
||||
<Tooltip position="bottom-right" delay={750}>
|
||||
Create folder
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-full pb-12">
|
||||
<ul
|
||||
className="m-0 p-0 text-sm"
|
||||
onClickCapture={(e) => {
|
||||
send({ type: 'Set selected directory', data: context.project })
|
||||
}}
|
||||
>
|
||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||
<FileTreeItem
|
||||
project={context.project}
|
||||
currentFile={file}
|
||||
fileOrDir={fileOrDir}
|
||||
closePanel={closePanel}
|
||||
key={fileOrDir.path}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<FileTreeMenu />
|
||||
</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">
|
||||
<ul
|
||||
className="m-0 p-0 text-sm"
|
||||
onClickCapture={(e) => {
|
||||
send({ type: 'Set selected directory', data: context.project })
|
||||
}}
|
||||
>
|
||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||
<FileTreeItem
|
||||
project={context.project}
|
||||
currentFile={loaderData?.file}
|
||||
fileOrDir={fileOrDir}
|
||||
onDoubleClick={onDoubleClick}
|
||||
key={fileOrDir.path}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -10,21 +10,32 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
||||
import { ReactNode } from 'react'
|
||||
import type { PaneType } from 'useStore'
|
||||
import { MemoryPane } from './MemoryPane'
|
||||
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
||||
import { DebugPane } from './DebugPane'
|
||||
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
|
||||
|
||||
export type Pane = {
|
||||
id: PaneType
|
||||
export type SidebarType =
|
||||
| 'code'
|
||||
| 'debug'
|
||||
| 'export'
|
||||
| 'files'
|
||||
| 'kclErrors'
|
||||
| 'logs'
|
||||
| 'lspMessages'
|
||||
| 'variables'
|
||||
|
||||
export type SidebarPane = {
|
||||
id: SidebarType
|
||||
title: string
|
||||
icon: CustomIconName | IconDefinition
|
||||
keybinding: string
|
||||
Content: ReactNode | React.FC
|
||||
Menu?: ReactNode | React.FC
|
||||
keybinding: string
|
||||
hideOnPlatform?: 'desktop' | 'web'
|
||||
}
|
||||
|
||||
export const topPanes: Pane[] = [
|
||||
export const topPanes: SidebarPane[] = [
|
||||
{
|
||||
id: 'code',
|
||||
title: 'KCL Code',
|
||||
@ -33,9 +44,18 @@ export const topPanes: Pane[] = [
|
||||
keybinding: 'shift + c',
|
||||
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',
|
||||
title: 'Variables',
|
||||
|
@ -2,13 +2,19 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Resizable } from 're-resizable'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { PaneType, useStore } from 'useStore'
|
||||
import { useStore } from 'useStore'
|
||||
import { Tab } from '@headlessui/react'
|
||||
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
|
||||
import {
|
||||
SidebarPane,
|
||||
SidebarType,
|
||||
bottomPanes,
|
||||
topPanes,
|
||||
} from './ModelingPanes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import styles from './ModelingSidebar.module.css'
|
||||
import { ModelingPane } from './ModelingPane'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
@ -52,7 +58,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
}
|
||||
|
||||
interface ModelingSidebarSectionProps {
|
||||
panes: Pane[]
|
||||
panes: SidebarPane[]
|
||||
alignButtons?: 'start' | 'end'
|
||||
}
|
||||
|
||||
@ -69,11 +75,11 @@ function ModelingSidebarSection({
|
||||
}))
|
||||
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
||||
const [currentPane, setCurrentPane] = useState(
|
||||
foundOpenPane || ('none' as PaneType | 'none')
|
||||
foundOpenPane || ('none' as SidebarType | 'none')
|
||||
)
|
||||
|
||||
const togglePane = useCallback(
|
||||
(newPane: PaneType | 'none') => {
|
||||
(newPane: SidebarType | 'none') => {
|
||||
if (newPane === 'none') {
|
||||
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
||||
setCurrentPane('none')
|
||||
@ -90,9 +96,15 @@ function ModelingSidebarSection({
|
||||
|
||||
// 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
|
||||
const filteredPanes = showDebugPanel.current
|
||||
? panes
|
||||
: panes.filter((pane) => pane.id !== 'debug')
|
||||
const filteredPanes = (
|
||||
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
|
||||
).filter(
|
||||
(pane) =>
|
||||
!pane.hideOnPlatform ||
|
||||
(isTauri()
|
||||
? pane.hideOnPlatform === 'web'
|
||||
: pane.hideOnPlatform === 'desktop')
|
||||
)
|
||||
useEffect(() => {
|
||||
if (
|
||||
!showDebugPanel.current &&
|
||||
@ -168,8 +180,8 @@ function ModelingSidebarSection({
|
||||
}
|
||||
|
||||
interface ModelingPaneButtonProps {
|
||||
paneConfig: Pane
|
||||
currentPane: PaneType | 'none'
|
||||
paneConfig: SidebarPane
|
||||
currentPane: SidebarType | 'none'
|
||||
togglePane: () => void
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import { enginelessExecutor } from './lib/testHelpers'
|
||||
import { EngineCommandManager } from './lang/std/engineConnection'
|
||||
import { KCLError } from './lang/errors'
|
||||
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
|
||||
|
||||
export type ToolTip =
|
||||
| 'lineTo'
|
||||
@ -44,14 +45,6 @@ export const toolTips = [
|
||||
'tangentialArcTo',
|
||||
] as any as ToolTip[]
|
||||
|
||||
export type PaneType =
|
||||
| 'code'
|
||||
| 'variables'
|
||||
| 'debug'
|
||||
| 'kclErrors'
|
||||
| 'logs'
|
||||
| 'lspMessages'
|
||||
|
||||
export interface StoreState {
|
||||
mediaStream?: MediaStream
|
||||
setMediaStream: (mediaStream: MediaStream) => void
|
||||
@ -77,8 +70,8 @@ export interface StoreState {
|
||||
|
||||
showHomeMenu: boolean
|
||||
setHomeShowMenu: (showMenu: boolean) => void
|
||||
openPanes: PaneType[]
|
||||
setOpenPanes: (panes: PaneType[]) => void
|
||||
openPanes: SidebarType[]
|
||||
setOpenPanes: (panes: SidebarType[]) => void
|
||||
homeMenuItems: {
|
||||
name: string
|
||||
path: string
|
||||
|
Reference in New Issue
Block a user