Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
6da1011481
commit
b821dd6256
@@ -18,6 +18,7 @@ import {
|
||||
ExternalLink,
|
||||
Pencil,
|
||||
Share2,
|
||||
FolderInput,
|
||||
} from "lucide-react";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { toast } from "sonner";
|
||||
@@ -29,6 +30,14 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { ShareDocumentDialog } from "./share-document-dialog";
|
||||
import JSZip from "jszip";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
const MAX_BYTES = 150 * 1024 * 1024;
|
||||
|
||||
@@ -46,6 +55,10 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [shareDoc, setShareDoc] = useState<any | null>(null);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedFolders, setSelectedFolders] = useState<Set<string>>(new Set());
|
||||
const [selectedDocs, setSelectedDocs] = useState<Set<string>>(new Set());
|
||||
const [moveOpen, setMoveOpen] = useState(false);
|
||||
const [moveDest, setMoveDest] = useState<string>("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const [{ data: ds, error: de }, { data: fs }] = await Promise.all([
|
||||
@@ -90,6 +103,12 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
// Clear selection when folder changes
|
||||
useEffect(() => {
|
||||
setSelectedFolders(new Set());
|
||||
setSelectedDocs(new Set());
|
||||
}, [path]);
|
||||
|
||||
// Visible items: subfolders directly under `path` and files whose folder === path
|
||||
const subfolders = folders
|
||||
.filter((f) => {
|
||||
@@ -340,6 +359,84 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
load();
|
||||
};
|
||||
|
||||
const toggleFolderSel = (full: string) => {
|
||||
setSelectedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(full)) next.delete(full); else next.add(full);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const toggleDocSel = (id: string) => {
|
||||
setSelectedDocs((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id); else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const selectionCount = selectedFolders.size + selectedDocs.size;
|
||||
|
||||
// Destinations: Root + all folders that are NOT a selected folder or descendant of one
|
||||
const moveDestinations = (() => {
|
||||
const blocked = (p: string) => {
|
||||
for (const sel of selectedFolders) {
|
||||
if (p === sel || p.startsWith(`${sel}/`)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return ["", ...folders.filter((f) => !blocked(f))].filter(
|
||||
(dest) => dest !== path, // moving to current folder is a no-op
|
||||
);
|
||||
})();
|
||||
|
||||
const performMove = async () => {
|
||||
const dest = moveDest; // "" = root
|
||||
// Move folders
|
||||
for (const full of selectedFolders) {
|
||||
const name = full.split("/").pop() || full;
|
||||
const newFull = dest ? `${dest}/${name}` : name;
|
||||
if (newFull === full) continue;
|
||||
if (folders.includes(newFull)) {
|
||||
toast.error(`A folder named "${name}" already exists at the destination`);
|
||||
return;
|
||||
}
|
||||
// Update documents in this folder + descendants
|
||||
const affectedDocs = docs.filter(
|
||||
(d) => (d.folder || "") === full || (d.folder || "").startsWith(`${full}/`),
|
||||
);
|
||||
for (const d of affectedDocs) {
|
||||
const next = d.folder === full ? newFull : `${newFull}${d.folder.slice(full.length)}`;
|
||||
const { error } = await supabase.from("documents").update({ folder: next }).eq("id", d.id);
|
||||
if (error) { toast.error("Move failed", { description: error.message }); return; }
|
||||
}
|
||||
// Update folder rows
|
||||
const affectedFolders = folders.filter((f) => f === full || f.startsWith(`${full}/`));
|
||||
for (const f of affectedFolders) {
|
||||
const next = f === full ? newFull : `${newFull}${f.slice(full.length)}`;
|
||||
const { error } = await supabase
|
||||
.from("document_folders")
|
||||
.update({ path: next })
|
||||
.eq("case_id", caseId)
|
||||
.eq("path", f);
|
||||
if (error) {
|
||||
await supabase.from("document_folders").delete().eq("case_id", caseId).eq("path", f);
|
||||
await ensureFolder(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move loose files
|
||||
for (const id of selectedDocs) {
|
||||
const { error } = await supabase.from("documents").update({ folder: dest }).eq("id", id);
|
||||
if (error) { toast.error("Move failed", { description: error.message }); return; }
|
||||
}
|
||||
toast.success(`Moved ${selectionCount} item${selectionCount === 1 ? "" : "s"}`);
|
||||
setMoveOpen(false);
|
||||
setMoveDest("");
|
||||
setSelectedFolders(new Set());
|
||||
setSelectedDocs(new Set());
|
||||
load();
|
||||
};
|
||||
|
||||
const crumbs = path ? path.split("/") : [];
|
||||
|
||||
return (
|
||||
@@ -372,6 +469,15 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
<Button variant="outline" size="sm" onClick={() => setCreatingFolder((v) => !v)}>
|
||||
<FolderPlus className="h-4 w-4 mr-1.5" /> New folder
|
||||
</Button>
|
||||
{selectionCount > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { setMoveDest(""); setMoveOpen(true); }}
|
||||
>
|
||||
<FolderInput className="h-4 w-4 mr-1.5" /> Move ({selectionCount})
|
||||
</Button>
|
||||
)}
|
||||
<input ref={fileRef} type="file" multiple className="hidden" onChange={onSelectFiles} />
|
||||
<Button size="sm" onClick={() => fileRef.current?.click()} disabled={uploading}>
|
||||
{uploading ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <Upload className="h-4 w-4 mr-1.5" />}
|
||||
@@ -409,6 +515,7 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50 text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<tr>
|
||||
<th className="w-8 px-2 py-3"></th>
|
||||
<th className="text-left px-4 py-3 font-medium">Name</th>
|
||||
<th className="text-left px-4 py-3 font-medium">Uploaded by</th>
|
||||
<th className="text-left px-4 py-3 font-medium">Date</th>
|
||||
@@ -419,7 +526,7 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
<tbody>
|
||||
{subfolders.length === 0 && visibleDocs.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-16 text-muted-foreground">
|
||||
<td colSpan={6} className="text-center py-16 text-muted-foreground">
|
||||
<Upload className="h-6 w-6 mx-auto mb-2 opacity-60" />
|
||||
Drag & drop files or folders here, or click <span className="font-medium">Upload</span>.
|
||||
</td>
|
||||
@@ -427,6 +534,13 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
)}
|
||||
{subfolders.map((f) => (
|
||||
<tr key={`d-${f.full}`} className="border-t hover:bg-muted/30">
|
||||
<td className="px-2 py-3 text-center">
|
||||
<Checkbox
|
||||
checked={selectedFolders.has(f.full)}
|
||||
onCheckedChange={() => toggleFolderSel(f.full)}
|
||||
aria-label={`Select folder ${f.name}`}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => setPath(f.full)} className="flex items-center gap-2 font-medium hover:text-primary">
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -446,6 +560,13 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
))}
|
||||
{visibleDocs.map((d) => (
|
||||
<tr key={d.id} className="border-t hover:bg-muted/30">
|
||||
<td className="px-2 py-3 text-center">
|
||||
<Checkbox
|
||||
checked={selectedDocs.has(d.id)}
|
||||
onCheckedChange={() => toggleDocSel(d.id)}
|
||||
aria-label={`Select ${d.name}`}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -504,6 +625,42 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
|
||||
caseId={caseId}
|
||||
doc={shareDoc}
|
||||
/>
|
||||
|
||||
<Dialog open={moveOpen} onOpenChange={setMoveOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Move {selectionCount} item{selectionCount === 1 ? "" : "s"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-muted-foreground">Choose a destination folder:</div>
|
||||
<Select value={moveDest} onValueChange={setMoveDest}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select destination…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{moveDestinations.map((dest) => (
|
||||
<SelectItem key={dest || "__root__"} value={dest || "__root__"}>
|
||||
{dest ? dest : "Root"}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="ghost" onClick={() => setMoveOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// SelectItem cannot have empty value; map sentinel back to ""
|
||||
if (moveDest === "__root__") setMoveDest("");
|
||||
performMove();
|
||||
}}
|
||||
disabled={!moveDest}
|
||||
>
|
||||
Move
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user