Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
assignment-dispatch-button.tsxassignment-status-badge.tsxassignments-panel.tsxcreate-mission-form.tsxgovernance-panels.tsxmission-dispatch.tsxmission-edit.tsxmission-state-actions.tsxmission-status-badge.tsxobjective-edit.tsxobjective-status-badge.tsxobjective-status-select.tsxobjectives-panel.tsxreport-list.tsxwork-orders-panel.tsx
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/components/files/folder-manager.tsx
13.3 KB
"use client";
import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import {
FolderPlus,
Upload,
Trash2,
Folder,
FileText,
Loader2,
X,
AlertTriangle,
CornerLeftUp,
CheckCircle2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
interface DirEntry {
name: string;
type: "file" | "dir";
path: string;
ext?: string;
size?: number;
files?: number;
folders?: number;
}
interface ManagedDir {
path: string;
isRoot: boolean;
parent: string | null;
entries: DirEntry[];
}
type Confirm =
| { kind: "folder-self"; path: string; label: string; files: number; folders: number }
| { kind: "folder"; path: string; label: string; files: number; folders: number }
| { kind: "file"; path: string; label: string };
function formatBytes(bytes?: number): string {
if (bytes == null) return "";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
/**
* Document file-manager for a managed folder: create sub-folders, upload
* documents into this folder, and delete folders/documents. All actions hit the
* sandboxed /api/files endpoints and then refresh the server-rendered view.
*/
export function FolderManager({
folder,
accept,
}: {
folder: ManagedDir;
accept: string;
}) {
const router = useRouter();
const fileRef = useRef<HTMLInputElement>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [newOpen, setNewOpen] = useState(false);
const [newName, setNewName] = useState("");
const [confirm, setConfirm] = useState<Confirm | null>(null);
function reset() {
setError(null);
setSuccess(null);
}
async function call(url: string, init: RequestInit): Promise<any> {
const res = await fetch(url, init);
const data = await res.json().catch(() => ({}));
if (!res.ok || data.ok === false) {
throw new Error(data.error ?? `Request failed (HTTP ${res.status}).`);
}
return data;
}
async function createFolder(e: React.FormEvent) {
e.preventDefault();
if (!newName.trim()) return;
setBusy(true);
reset();
try {
await call("/api/files/folder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parent: folder.path, name: newName }),
});
setNewName("");
setNewOpen(false);
setSuccess("Folder created.");
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Could not create folder.");
} finally {
setBusy(false);
}
}
async function uploadHere() {
const file = fileRef.current?.files?.[0];
if (!file) return;
setBusy(true);
reset();
try {
const body = new FormData();
body.append("dir", folder.path);
body.append("file", file);
const data = await call("/api/files/upload", { method: "POST", body });
if (fileRef.current) fileRef.current.value = "";
setSuccess(`Uploaded ${data.result.name}.`);
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Upload failed.");
} finally {
setBusy(false);
}
}
async function runConfirm() {
if (!confirm) return;
setBusy(true);
reset();
try {
if (confirm.kind === "file") {
await call("/api/files/document", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: confirm.path }),
});
setSuccess(`Deleted ${confirm.label}.`);
setConfirm(null);
router.refresh();
} else {
await call("/api/files/folder", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: confirm.path }),
});
const isSelf = confirm.kind === "folder-self";
setConfirm(null);
setSuccess(`Deleted ${confirm.label}.`);
if (isSelf && folder.parent) {
router.push(`/artifacts?dir=${encodeURIComponent(folder.parent)}`);
} else {
router.refresh();
}
}
} catch (err) {
setError(err instanceof Error ? err.message : "Delete failed.");
} finally {
setBusy(false);
}
}
return (
<div className="rounded-lg border border-border bg-card">
{/* Header */}
<div className="flex items-center justify-between gap-3 border-b border-border bg-secondary/40 px-5 py-3">
<div className="flex min-w-0 items-center gap-2">
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate font-mono text-[12px] font-medium text-foreground">
{folder.path}
</span>
</div>
{!folder.isRoot && folder.parent && (
<Link
href={`/artifacts?dir=${encodeURIComponent(folder.parent)}`}
className="inline-flex shrink-0 items-center gap-1 text-[12px] text-muted-foreground hover:text-foreground"
>
<CornerLeftUp className="h-3.5 w-3.5" /> Up
</Link>
)}
</div>
{/* Toolbar */}
<div className="flex flex-wrap items-center gap-2 border-b border-border px-5 py-3">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => {
setNewOpen((v) => !v);
reset();
}}
disabled={busy}
>
<FolderPlus className="h-3.5 w-3.5" /> New folder
</Button>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => {
reset();
fileRef.current?.click();
}}
disabled={busy}
>
<Upload className="h-3.5 w-3.5" /> Upload here
</Button>
<input
ref={fileRef}
type="file"
accept={accept}
className="hidden"
onChange={uploadHere}
/>
{!folder.isRoot && (
<Button
type="button"
size="sm"
variant="outline"
className="ml-auto text-destructive hover:bg-destructive/10"
disabled={busy}
onClick={() =>
setConfirm({
kind: "folder-self",
path: folder.path,
label: folder.path.split("/").pop() ?? folder.path,
files: folder.entries.filter((e) => e.type === "file").length,
folders: folder.entries.filter((e) => e.type === "dir").length,
})
}
>
<Trash2 className="h-3.5 w-3.5" /> Delete folder
</Button>
)}
</div>
{newOpen && (
<form
onSubmit={createFolder}
className="flex items-center gap-2 border-b border-border bg-secondary/20 px-5 py-3"
>
<Input
autoFocus
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="New folder name"
className="h-9 max-w-xs"
/>
<Button type="submit" size="sm" disabled={busy || !newName.trim()}>
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Create"}
</Button>
</form>
)}
{(error || success) && (
<div
className={cn(
"flex items-center gap-2 px-5 py-2.5 text-[13px]",
error ? "bg-destructive/10 text-destructive" : "bg-emerald-50 text-emerald-700",
)}
>
{error ? (
<AlertTriangle className="h-4 w-4 shrink-0" />
) : (
<CheckCircle2 className="h-4 w-4 shrink-0" />
)}
{error ?? success}
</div>
)}
{/* Contents */}
{folder.entries.length === 0 ? (
<div className="px-5 py-12 text-center text-sm text-muted-foreground">
This folder is empty. Use <span className="font-medium">New folder</span>{" "}
or <span className="font-medium">Upload here</span> to add to it.
</div>
) : (
<ul className="divide-y divide-border">
{folder.entries.map((entry) => (
<li
key={entry.path}
className="group flex items-center gap-3 px-5 py-2.5 hover:bg-secondary/40"
>
{entry.type === "dir" ? (
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
)}
<Link
href={`/artifacts?${entry.type === "dir" ? "dir" : "file"}=${encodeURIComponent(entry.path)}`}
className="min-w-0 flex-1 truncate text-[14px] text-foreground hover:underline"
>
{entry.name}
</Link>
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground">
{entry.type === "dir"
? `${entry.files ?? 0} file${(entry.files ?? 0) === 1 ? "" : "s"}`
: formatBytes(entry.size)}
</span>
<button
type="button"
disabled={busy}
onClick={() =>
setConfirm(
entry.type === "dir"
? {
kind: "folder",
path: entry.path,
label: entry.name,
files: entry.files ?? 0,
folders: entry.folders ?? 0,
}
: { kind: "file", path: entry.path, label: entry.name },
)
}
className="shrink-0 rounded-md p-1.5 text-muted-foreground/50 opacity-0 transition-colors hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
title={`Delete ${entry.name}`}
>
<Trash2 className="h-4 w-4" />
</button>
</li>
))}
</ul>
)}
{confirm && (
<ConfirmDialog
confirm={confirm}
busy={busy}
onCancel={() => setConfirm(null)}
onConfirm={runConfirm}
/>
)}
</div>
);
}
function ConfirmDialog({
confirm,
busy,
onCancel,
onConfirm,
}: {
confirm: Confirm;
busy: boolean;
onCancel: () => void;
onConfirm: () => void;
}) {
const isFolder = confirm.kind !== "file";
const total = isFolder ? confirm.files + confirm.folders : 0;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-sm rounded-lg border border-border bg-card p-5 shadow-lg">
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-destructive/10">
<AlertTriangle className="h-4 w-4 text-destructive" />
</div>
<div className="min-w-0">
<h3 className="text-sm font-semibold text-foreground">
Delete {isFolder ? "folder" : "document"}?
</h3>
<p className="mt-1 text-[13px] leading-relaxed text-muted-foreground">
<span className="font-medium text-foreground">{confirm.label}</span>{" "}
{isFolder ? (
total === 0 ? (
"is empty and will be permanently removed."
) : (
<>
and everything inside it —{" "}
<span className="font-medium text-foreground">
{confirm.files} file{confirm.files === 1 ? "" : "s"}
</span>
{confirm.folders > 0 && (
<>
{" "}and{" "}
<span className="font-medium text-foreground">
{confirm.folders} sub-folder{confirm.folders === 1 ? "" : "s"}
</span>
</>
)}{" "}
— will be permanently removed.
</>
)
) : (
"will be permanently removed."
)}{" "}
This cannot be undone.
</p>
</div>
<button
type="button"
onClick={onCancel}
className="shrink-0 rounded-md p-1 text-muted-foreground hover:bg-secondary"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="mt-5 flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" onClick={onCancel} disabled={busy}>
Cancel
</Button>
<Button
type="button"
size="sm"
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={onConfirm}
disabled={busy}
>
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Trash2 className="h-3.5 w-3.5" />}
Delete
</Button>
</div>
</div>
</div>
);
}
root · /srv/aaf