first wokring prototype
This commit is contained in:
282
src/features/files/FilesPage.tsx
Normal file
282
src/features/files/FilesPage.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
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";
|
||||
|
||||
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 storageSections: { id: StorageSection; label: string }[] = [
|
||||
{ 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">
|
||||
<aside className="section-sidebar">
|
||||
<button className="section-link section-link-primary" onClick={() => setSelectedStorageId(null)}>
|
||||
← File storages
|
||||
</button>
|
||||
<div className="section-title section-title-lower">STORAGE</div>
|
||||
{storageSections.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
className={`section-link ${active === section.id ? "active" : ""}`}
|
||||
onClick={() => setActive(section.id)}
|
||||
>
|
||||
{section.label}
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user