From 24354049f46f706916b613a8171318a87d08158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 07:09:49 +0100 Subject: [PATCH] browse for the folder instead of typing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the project form asked for a path and gave a text field, while fofos-admin — built hours earlier — had a directory picker for exactly the same question. that picker is now shared from the officerdev barrel, so this uses it. browse OR type: the picker fills the field and the FIELD is what saves, so the path stays visible and editable before it is stored. a folder outside home is still reachable by typing, and the server refuses it with a reason rather than the picker silently making it unreachable. picking a folder offers its last two segments as the name when the name is empty — `thing/todo` — because that is what somebody would have typed. left editable. Co-Authored-By: Claude Opus 5 --- web/ProjectsPanel.tsx | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/web/ProjectsPanel.tsx b/web/ProjectsPanel.tsx index 55c8724..326634a 100644 --- a/web/ProjectsPanel.tsx +++ b/web/ProjectsPanel.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { FolderPlus, FolderTree, Loader2, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; +import { DirectoryPicker } from 'officerdev'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useProjects, useSelectedProject } from './useKanban'; @@ -27,6 +28,7 @@ export const ProjectsPanel = () => { const [name, setName] = useState(''); const [path, setPath] = useState(''); const [confirming, setConfirming] = useState(null); + const [picking, setPicking] = useState(false); /* Land on something rather than on nothing. @@ -72,14 +74,43 @@ export const ProjectsPanel = () => { {adding && (
- setName(e.target.value)} placeholder="Name" className="text-sm" /> setPath(e.target.value)} - placeholder="~/projects/thing/todo" - className="font-mono text-xs" - spellCheck={false} + autoFocus + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="Name" + className="text-sm" /> + {/* + Browse OR type. The picker fills the field and the field is what saves, so the path stays + visible and editable before it is stored — and a folder outside home, which the picker + cannot reach, can still be typed and will be refused by the server with a reason. + */} +
+ setPath(e.target.value)} + placeholder="~/projects/thing/todo" + className="min-w-0 flex-1 font-mono text-xs" + spellCheck={false} + /> + +
+ + {picking && ( + setPicking(false)} + onPick={(absolutePath) => { + setPath(absolutePath); + setPicking(false); + // A folder is usually named after itself — offer that as the name rather than an empty + // field, and leave it editable. + if (!name.trim()) setName(absolutePath.split('/').filter(Boolean).slice(-2).join('/')); + }} + /> + )} {/* Said before they press it, not after it fails: the folder holding the theme directories is not the repository root, and the two are one level apart. */}