browse for the folder instead of typing it
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 <noreply@anthropic.com>
This commit is contained in:
+37
-6
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FolderPlus, FolderTree, Loader2, Trash2 } from 'lucide-react';
|
import { FolderPlus, FolderTree, Loader2, Trash2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { DirectoryPicker } from 'officerdev';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useProjects, useSelectedProject } from './useKanban';
|
import { useProjects, useSelectedProject } from './useKanban';
|
||||||
@@ -27,6 +28,7 @@ export const ProjectsPanel = () => {
|
|||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [path, setPath] = useState('');
|
const [path, setPath] = useState('');
|
||||||
const [confirming, setConfirming] = useState<number | null>(null);
|
const [confirming, setConfirming] = useState<number | null>(null);
|
||||||
|
const [picking, setPicking] = useState(false);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Land on something rather than on nothing.
|
Land on something rather than on nothing.
|
||||||
@@ -72,14 +74,43 @@ export const ProjectsPanel = () => {
|
|||||||
|
|
||||||
{adding && (
|
{adding && (
|
||||||
<form onSubmit={submit} className="flex flex-col gap-2 border-b border-border p-3">
|
<form onSubmit={submit} className="flex flex-col gap-2 border-b border-border p-3">
|
||||||
<Input autoFocus value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" className="text-sm" />
|
|
||||||
<Input
|
<Input
|
||||||
value={path}
|
autoFocus
|
||||||
onChange={(e) => setPath(e.target.value)}
|
value={name}
|
||||||
placeholder="~/projects/thing/todo"
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="font-mono text-xs"
|
placeholder="Name"
|
||||||
spellCheck={false}
|
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.
|
||||||
|
*/}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={path}
|
||||||
|
onChange={(e) => setPath(e.target.value)}
|
||||||
|
placeholder="~/projects/thing/todo"
|
||||||
|
className="min-w-0 flex-1 font-mono text-xs"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={() => setPicking((open) => !open)}>
|
||||||
|
{picking ? 'Close' : 'Browse'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{picking && (
|
||||||
|
<DirectoryPicker
|
||||||
|
onCancel={() => 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
|
{/* 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. */}
|
not the repository root, and the two are one level apart. */}
|
||||||
<p className="text-[11px] text-muted-foreground/70">
|
<p className="text-[11px] text-muted-foreground/70">
|
||||||
|
|||||||
Reference in New Issue
Block a user