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