Files
multi-seal-mail-webui/src/features/files/FilesPage.tsx

278 lines
10 KiB
TypeScript

import { useMemo, useState } from "react";
import Button from "../../components/Button";
import Card from "../../components/Card";
import PageTitle from "../../components/PageTitle";
import StatusBadge from "../../components/StatusBadge";
import ModuleSubnav, { type ModuleSubnavGroup } from "../../layout/ModuleSubnav";
type StorageRecord = {
id: string;
name: string;
description: string;
type: string;
scope: string;
status: string;
files: number;
used: string;
retention: string;
updatedAt: string;
};
type StorageSection = "browse" | "upload" | "settings" | "retention" | "bulk" | "activity";
const storages: StorageRecord[] = [
{
id: "campaign-files",
name: "Campaign files",
description: "Files uploaded or referenced for campaign attachments.",
type: "Local path / planned object storage",
scope: "Campaigns",
status: "available",
files: 124,
used: "2.4 GB",
retention: "Keep sent evidence",
updatedAt: "2026-06-08 15:36"
},
{
id: "template-assets",
name: "Template assets",
description: "Images and reusable assets for message templates.",
type: "Planned object storage",
scope: "Templates",
status: "planned",
files: 8,
used: "34 MB",
retention: "Manual cleanup",
updatedAt: "2026-06-06 10:12"
},
{
id: "shared-library",
name: "Shared library",
description: "Tenant or group-wide files available to multiple campaigns.",
type: "Planned object storage",
scope: "Tenant / groups",
status: "planned",
files: 0,
used: "0 MB",
retention: "Policy pending",
updatedAt: "Not connected"
}
];
const storageSubnav = (onBack: () => void): ModuleSubnavGroup<StorageSection>[] => [
{
items: [{ actionId: "file-storages", label: "← File storages", primary: true, onClick: onBack }]
},
{
title: "STORAGE",
items: [
{ id: "browse", label: "Browse" },
{ id: "upload", label: "Upload" },
{ id: "settings", label: "Settings" },
{ id: "retention", label: "Retention" },
{ id: "bulk", label: "Bulk actions" },
{ id: "activity", label: "Activity" }
]
}
];
const demoFiles = [
{ name: "statement_1001.pdf", path: "/2026/05/statement_1001.pdf", size: "142 KB", updatedAt: "2026-06-08 15:36", status: "ready" },
{ name: "statement_1002.pdf", path: "/2026/05/statement_1002.pdf", size: "148 KB", updatedAt: "2026-06-08 15:36", status: "ready" },
{ name: "global_notice.pdf", path: "/shared/global_notice.pdf", size: "81 KB", updatedAt: "2026-06-06 09:44", status: "ready" }
];
export default function FilesPage() {
const [selectedStorageId, setSelectedStorageId] = useState<string | null>(null);
const [active, setActive] = useState<StorageSection>("browse");
const selectedStorage = useMemo(
() => storages.find((storage) => storage.id === selectedStorageId) ?? null,
[selectedStorageId]
);
function openStorage(storageId: string) {
setSelectedStorageId(storageId);
setActive("browse");
}
if (selectedStorage) {
return (
<div className="workspace module-workspace">
<ModuleSubnav active={active} groups={storageSubnav(() => setSelectedStorageId(null))} onSelect={setActive} />
<section className="workspace-content">
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle>{selectedStorage.name}</PageTitle>
<p>{selectedStorage.description}</p>
</div>
<div className="button-row compact-actions">
<Button disabled>Upload</Button>
<Button disabled>Download</Button>
<Button variant="danger" disabled>Delete</Button>
</div>
</div>
{active === "browse" && <StorageBrowse storage={selectedStorage} />}
{active === "upload" && <StorageUpload storage={selectedStorage} />}
{active === "settings" && <StorageSettings storage={selectedStorage} />}
{active === "retention" && <StorageRetention storage={selectedStorage} />}
{active === "bulk" && <StorageBulkActions />}
{active === "activity" && <StorageActivity />}
</div>
</section>
</div>
);
}
return (
<div className="content-pad workspace-data-page module-entry-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle>Files</PageTitle>
<p>Manage file storages first. Open a storage to browse content, upload files and configure retention.</p>
</div>
<div className="button-row compact-actions">
<Button disabled>Refresh</Button>
<Button variant="primary" disabled>Add storage</Button>
</div>
</div>
<Card
title={
<div className="module-card-heading">
<h2>File storages</h2>
<span>Storage endpoints are placeholders until the backend model is added</span>
</div>
}
>
<div className="app-table-wrap compact-table-wrap module-table-wrap">
<table className="app-table module-table module-entry-table">
<thead>
<tr>
<th>Storage</th>
<th>Type</th>
<th>Scope</th>
<th>Files</th>
<th>Used</th>
<th>Retention</th>
<th>Updated</th>
<th aria-label="Actions" />
</tr>
</thead>
<tbody>
{storages.map((storage) => (
<tr key={storage.id}>
<td>
<div className="module-title-cell">
<strong>{storage.name}</strong>
<span>{storage.description}</span>
</div>
</td>
<td>{storage.type}</td>
<td>{storage.scope}</td>
<td>{storage.files}</td>
<td>{storage.used}</td>
<td>{storage.retention}</td>
<td><span className="muted small-text">{storage.updatedAt}</span></td>
<td className="table-action-cell"><Button onClick={() => openStorage(storage.id)}>Open</Button></td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
);
}
function StorageBrowse({ storage }: { storage: StorageRecord }) {
return (
<Card
title="Browse content"
actions={<div className="button-row compact-actions"><Button disabled>Upload</Button><Button disabled>Download selected</Button><Button variant="danger" disabled>Delete selected</Button></div>}
>
<div className="app-table-wrap compact-table-wrap module-table-wrap">
<table className="app-table module-table">
<thead><tr><th>Name</th><th>Path</th><th>Size</th><th>Updated</th><th>Status</th><th></th></tr></thead>
<tbody>
{(storage.id === "campaign-files" ? demoFiles : []).map((file) => (
<tr key={file.path}>
<td>{file.name}</td>
<td><code>{file.path}</code></td>
<td>{file.size}</td>
<td><span className="muted small-text">{file.updatedAt}</span></td>
<td><StatusBadge status={file.status} /></td>
<td className="table-action-cell"><Button disabled>Download</Button></td>
</tr>
))}
{storage.id !== "campaign-files" && <tr><td colSpan={6} className="muted">Files will appear here when this storage is connected.</td></tr>}
</tbody>
</table>
</div>
</Card>
);
}
function StorageUpload({ storage }: { storage: StorageRecord }) {
return (
<Card title="Upload files" actions={<Button variant="primary" disabled>Select files</Button>}>
<p className="muted">Upload will target <strong>{storage.name}</strong>. The backend will later provide chunked upload, duplicate handling and progress state.</p>
<div className="placeholder-stack">
<span>Drag and drop upload area</span>
<span>Duplicate handling: ask, replace, keep both</span>
<span>Optional file tagging after upload</span>
</div>
</Card>
);
}
function StorageSettings({ storage }: { storage: StorageRecord }) {
return (
<div className="dashboard-grid settings-dashboard-grid">
<Card title="Storage settings">
<dl className="detail-list compact-detail-list">
<div><dt>Type</dt><dd>{storage.type}</dd></div>
<div><dt>Scope</dt><dd>{storage.scope}</dd></div>
<div><dt>Status</dt><dd><StatusBadge status={storage.status} /></dd></div>
</dl>
</Card>
<Card title="Backend requirements">
<p className="muted">This view is prepared for local path, Garage/S3 and tenant/group/user storage settings.</p>
<div className="placeholder-stack"><span>Storage backend</span><span>Access policy</span><span>Quota</span><span>Encryption / lifecycle</span></div>
</Card>
</div>
);
}
function StorageRetention({ storage }: { storage: StorageRecord }) {
return (
<Card title="Retention policy" actions={<Button disabled>Save policy</Button>}>
<p className="muted">Current policy: {storage.retention}. Retention must respect audit-safe campaigns and sent attachments.</p>
<div className="placeholder-stack">
<span>Keep files for sent campaigns</span>
<span>Prune unused draft uploads after a configurable period</span>
<span>Export manifest before destructive cleanup</span>
</div>
</Card>
);
}
function StorageBulkActions() {
return (
<Card title="Bulk actions">
<p className="muted">Bulk download and delete should be available from the Browse view as well as from a dedicated filtered action view.</p>
<div className="button-row page-bottom-actions"><Button disabled>Download filtered files</Button><Button variant="danger" disabled>Delete filtered files</Button></div>
</Card>
);
}
function StorageActivity() {
return (
<Card title="Activity">
<p className="muted">Storage activity will show uploads, downloads, deletions and retention cleanup runs once backend audit events are available.</p>
<div className="placeholder-stack"><span>Last upload</span><span>Last bulk delete</span><span>Retention cleanup result</span></div>
</Card>
);
}