Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions web/src/components/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export type TreeProps = {
nodeClassName?: (node: TreeNodeData, depth: number, isOpen: boolean, isPicked: boolean) => string
/** Bump to force re-fetch children for all nodes */
reloadKey?: number
/** Called when the inline add button on a directory row is clicked */
onAddInDir?: (node: TreeNodeData) => void
}

export function Tree({
Expand All @@ -78,6 +80,7 @@ export function Tree({
renderNode,
nodeClassName,
reloadKey,
onAddInDir,
}: TreeProps) {
return (
<>
Expand All @@ -95,6 +98,7 @@ export function Tree({
renderNode={renderNode}
nodeClassName={nodeClassName}
reloadKey={reloadKey}
onAddInDir={onAddInDir}
/>
))}
</>
Expand All @@ -113,6 +117,7 @@ function TreeNode({
renderNode,
nodeClassName,
reloadKey,
onAddInDir,
}: {
treeId: string
entry: TreeNodeData
Expand All @@ -125,6 +130,7 @@ function TreeNode({
renderNode?: (node: TreeNodeData, depth: number, isOpen: boolean, toggleOpen: () => void) => ReactNode
nodeClassName?: (node: TreeNodeData, depth: number, isOpen: boolean, isPicked: boolean) => string
reloadKey?: number
onAddInDir?: (node: TreeNodeData) => void
}) {
const [open, setOpen] = useState(() => getExpanded(treeId).has(entry.path))
const [children, setChildren] = useState<TreeNodeData[] | null>(null)
Expand Down Expand Up @@ -187,6 +193,7 @@ function TreeNode({
renderNode={renderNode}
nodeClassName={nodeClassName}
reloadKey={reloadKey}
onAddInDir={onAddInDir}
/>
))}
</>
Expand All @@ -206,20 +213,36 @@ function TreeNode({

return (
<>
<div onContextMenu={handleContextMenu}>
<div className="relative group/treerow" onContextMenu={handleContextMenu}>
<button
type="button"
onClick={toggleOpen}
className={nodeClassName
? nodeClassName(entry, depth, open, false)
: "w-full py-1 flex items-center gap-1.5 hover:bg-gray-50 text-left group"
}
style={{ paddingLeft, paddingRight: 8 }}
style={{ paddingLeft, paddingRight: onAddInDir ? 32 : 8 }}
>
<span className="text-gray-500 w-3">{open ? "▾" : "▸"}</span>
<span className="text-gray-500">{open ? <FolderOpen size={13} /> : <Folder size={13} />}</span>
<span className="text-[13px] text-gray-900 truncate">{entry.name}</span>
</button>
{onAddInDir && (
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onAddInDir(entry)
}}
onMouseDown={(e) => e.stopPropagation()}
className="absolute right-0.5 top-1/2 -translate-y-1/2 p-1 rounded text-gray-500 hover:text-gray-900 hover:bg-gray-200 opacity-100 [@media(hover:hover)]:opacity-0 group-hover/treerow:opacity-100 focus:opacity-100 transition-opacity"
title="new file in this directory"
aria-label="new file in this directory"
>
<Plus size={14} />
</button>
)}
</div>
{loading && (
<div className="text-[12px] text-gray-400 italic" style={{ paddingLeft: paddingLeft + 12 }}>
Expand All @@ -242,6 +265,7 @@ function TreeNode({
renderNode={renderNode}
nodeClassName={nodeClassName}
reloadKey={reloadKey}
onAddInDir={onAddInDir}
/>
))}
{children.length === 0 && (
Expand Down
40 changes: 34 additions & 6 deletions web/src/pages/ContextPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini
}
}, [pickedPath, searchParams, setSearchParams])
const [reloadKey, setReloadKey] = useState(0)
const [showNewFile, setShowNewFile] = useState(false)
const [showNewFile, setShowNewFile] = useState<{ initialPath: string } | null>(null)
const [query, setQuery] = useState("")
const [sidebarOpen, setSidebarOpen] = useState(false)
const [creating, setCreating] = useState<{ type: "file" | "folder"; path: string } | null>(null)
Expand Down Expand Up @@ -340,14 +340,18 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini
const onCreate = async (path: string) => {
const r = await vaultCreateFile(vault, path)
if (r.ok) {
setShowNewFile(false)
setShowNewFile(null)
setReloadKey((k) => k + 1)
setPickedPath(path)
} else {
alert(`failed: ${r.error}`)
}
}

const handleAddInDir = useCallback((node: TreeNodeData) => {
setShowNewFile({ initialPath: node.path ? node.path + "/" : "" })
}, [])

const handleCreate = async () => {
if (!creating || !newName.trim()) { setCreating(null); return }
const targetPath = creating.path + "/" + newName.trim()
Expand Down Expand Up @@ -448,7 +452,7 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini
className="flex-1 min-w-0 bg-transparent outline-none text-[12px] text-gray-700 placeholder:text-gray-400"
/>
<button
onClick={() => setShowNewFile(true)}
onClick={() => setShowNewFile({ initialPath: "" })}
className="text-gray-500 hover:text-gray-900 px-1.5 rounded hover:bg-gray-100 text-xs"
title="new file"
>
Expand Down Expand Up @@ -499,6 +503,7 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini
onLoadChildren={handleLoadChildren}
getContextActions={getContextActions}
onAction={handleAction}
onAddInDir={handleAddInDir}
nodeClassName={getNodeClassName}
reloadKey={reloadKey}
/>
Expand Down Expand Up @@ -561,7 +566,14 @@ function VaultPane({ vault, initialFile, initialEditing }: { vault: VaultId; ini
</div>
)}
</main>
{showNewFile && <NewFileDialog vault={vault} onClose={() => setShowNewFile(false)} onCreate={onCreate} />}
{showNewFile && (
<NewFileDialog
vault={vault}
initialPath={showNewFile.initialPath}
onClose={() => setShowNewFile(null)}
onCreate={onCreate}
/>
)}
{creating && (
<CreateItemDialog
type={creating.type}
Expand Down Expand Up @@ -1139,14 +1151,26 @@ function DocView({

function NewFileDialog({
vault,
initialPath = "",
onClose,
onCreate,
}: {
vault: VaultId
initialPath?: string
onClose: () => void
onCreate: (path: string) => void
}) {
const [path, setPath] = useState("")
const [path, setPath] = useState(initialPath)
const inputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
const input = inputRef.current
if (!input) return
input.focus()
const end = input.value.length
input.setSelectionRange(end, end)
}, [])

return (
<div className="fixed inset-0 z-50 bg-black/30 flex items-center justify-center" onClick={onClose}>
<div
Expand All @@ -1155,10 +1179,14 @@ function NewFileDialog({
>
<div className="text-base font-semibold text-gray-900 mb-3">new file in {vault}/</div>
<input
autoFocus
ref={inputRef}
type="text"
value={path}
onChange={(e) => setPath(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing && path.trim()) onCreate(path.trim())
if (e.key === "Escape") onClose()
}}
placeholder="loopat/new-doc.md"
className="w-full px-3 py-2 text-sm border border-gray-300 rounded outline-none focus:border-gray-500 font-mono"
/>
Expand Down