From 5c7398d6301c13403e1bd1d47b760f225e144274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 28 May 2026 20:04:46 -0400 Subject: [PATCH] Split FilesPage into a files module FilesPage had grown past 500 lines by carrying the tree, table, grid, card visuals and all pure helpers inline. Extract file-helpers.ts, FileVisuals, FilesTree, FilesTable and FilesGrid under pages/files/, leaving FilesPage as the orchestrator. Behaviour is unchanged; the pinned-card radius is corrected to 10 to match the design. --- .../src/renderer/src/pages/FilesPage.tsx | 499 ++---------------- .../renderer/src/pages/files/FileVisuals.tsx | 47 ++ .../renderer/src/pages/files/FilesGrid.tsx | 94 ++++ .../renderer/src/pages/files/FilesTable.tsx | 89 ++++ .../renderer/src/pages/files/FilesTree.tsx | 99 ++++ .../renderer/src/pages/files/file-helpers.ts | 106 ++++ 6 files changed, 485 insertions(+), 449 deletions(-) create mode 100644 AgentCompany-New/src/renderer/src/pages/files/FileVisuals.tsx create mode 100644 AgentCompany-New/src/renderer/src/pages/files/FilesGrid.tsx create mode 100644 AgentCompany-New/src/renderer/src/pages/files/FilesTable.tsx create mode 100644 AgentCompany-New/src/renderer/src/pages/files/FilesTree.tsx create mode 100644 AgentCompany-New/src/renderer/src/pages/files/file-helpers.ts diff --git a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx index 0efef480..3c2ded6c 100644 --- a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx @@ -1,31 +1,21 @@ import { useMemo, useState } from "react"; -import { Avatar, Button, Icon, IconButton, Pill, Segmented } from "../shared/ui"; +import { Button, Icon, Segmented } from "../shared/ui"; import { TopbarActions } from "../shared/TopbarActions"; -import type { - AgentRecord, - EmployeeRecord, - FileAccessGrantRecord, - FileObjectRecord, -} from "../../../shared/domain"; import type { PageProps } from "./types"; -import { activeOrgId, formatBytes, formatRelative } from "./types"; - -type ViewMode = "grid" | "list"; -type Section = - | { kind: "all" } - | { kind: "recent" } - | { kind: "pinned" } - | { kind: "department"; value: string } - | { kind: "agent"; value: string }; - -const FILE_COLOR_MAP: Record = { - doc: { color: "#5e6ad2", label: "DOC" }, - sheet: { color: "#2f9e58", label: "XLS" }, - md: { color: "#52525b", label: "MD" }, - pdf: { color: "#d23b3b", label: "PDF" }, - img: { color: "#c87d12", label: "IMG" }, - folder: { color: "#a1a1aa", label: "" }, -}; +import { activeOrgId, formatBytes } from "./types"; +import { FilesTree } from "./files/FilesTree"; +import { FilesTable } from "./files/FilesTable"; +import { FilesGrid, PinnedCard } from "./files/FilesGrid"; +import { + collectAgentFiles, + collectDepartments, + fileGrantCount, + filterFiles, + labelFor, + resolveOwnerName, + type Section, + type ViewMode, +} from "./files/file-helpers"; export function FilesPage({ snapshot, actions }: PageProps) { const orgId = activeOrgId(snapshot); @@ -77,436 +67,47 @@ export function FilesPage({ snapshot, actions }: PageProps) {

- {pinnedFiles.length > 0 && section.kind === "all" && ( - <> -
Pinned
-
- {pinnedFiles.slice(0, 8).map((file) => ( - void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: false }))} - /> - ))} -
- - )} + {pinnedFiles.length > 0 && section.kind === "all" && ( + <> +
Pinned
+
+ {pinnedFiles.slice(0, 8).map((file) => ( + void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: false }))} + /> + ))} +
+ + )} -
-
{labelFor(section)}
-
Modified ↓
-
+
+
{labelFor(section)}
+
Modified ↓
+
- {view === "list" ? ( - void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))} - /> - ) : ( - void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))} - /> - )} -
- - - ); -} - -function FilesTree({ - files, - pinnedFiles, - section, - departments, - filesByAgent, - onChange, -}: { - files: FileObjectRecord[]; - pinnedFiles: FileObjectRecord[]; - section: Section; - departments: Array<{ name: string; count: number }>; - filesByAgent: Array<{ agent: AgentRecord; count: number }>; - onChange: (next: Section) => void; -}) { - const recent = files.filter((file) => isRecent(file.createdAt)); - return ( - - ); -} - -function TreeItem({ - icon, - label, - count, - active, - onClick, -}: { - icon: string; - label: string; - count: number; - active: boolean; - onClick: () => void; -}) { - return ( - - ); -} - -function FilesTable({ - files, - grants, - employees, - agents, - onTogglePin, -}: { - files: FileObjectRecord[]; - grants: FileAccessGrantRecord[]; - employees: EmployeeRecord[]; - agents: AgentRecord[]; - onTogglePin: (file: FileObjectRecord) => void; -}) { - return ( -
-
-
Name
Size
Owner
Access
Modified
-
- {files.map((file, index) => ( - onTogglePin(file)} - /> - ))} -
- ); -} - -function FilesGrid({ - files, - grants, - employees, - agents, - onTogglePin, -}: { - files: FileObjectRecord[]; - grants: FileAccessGrantRecord[]; - employees: EmployeeRecord[]; - agents: AgentRecord[]; - onTogglePin: (file: FileObjectRecord) => void; -}) { - return ( -
- {files.map((file) => ( - onTogglePin(file)} - /> - ))} -
- ); -} - -function FileRow({ - file, - owner, - grantCount, - last, - onTogglePin, -}: { - file: FileObjectRecord; - owner: string; - grantCount: number; - last: boolean; - onTogglePin: () => void; -}) { - return ( -
-
- -
- {file.name} - - sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)} - + )}
- {file.pinned && Pinned} -
-
{formatBytes(file.sizeBytes)}
-
- - {owner} -
-
- {file.scanState === "approved" ? "Clean" : "Blocked"} - {grantCount} -
-
{formatRelative(file.updatedAt ?? file.createdAt)}
-
-
); } - -function PinnedCard({ - file, - ownerName, - grantCount, - onUnpin, -}: { - file: FileObjectRecord; - ownerName: string; - grantCount: number; - onUnpin: () => void; -}) { - return ( -
-
- - -
-
- {file.name} -
-
- sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)} -
-
- {file.scanState === "approved" ? "Clean" : "Blocked"} - {grantCount} -
- -
- - - {formatRelative(file.updatedAt ?? file.createdAt)} · {ownerName.split(" ")[0]} - -
-
- ); -} - -function ScanLabelStrip({ file }: { file: FileObjectRecord }) { - const labels = parseScanLabels(file).slice(0, 2); - if (labels.length === 0) return null; - return ( -
- {labels.map((label) => ( - {label} - ))} -
- ); -} - -function FileIcon({ kind, big = false }: { kind: string; big?: boolean }) { - const map = FILE_COLOR_MAP[kind] ?? FILE_COLOR_MAP.doc; - if (!map) throw new Error("File color map default missing."); - if (kind === "folder") { - return ( -
- -
- ); - } - const label = map.label || kind.slice(0, 3).toUpperCase(); - return ( - - {label} - - ); -} - -function collectDepartments(agents: AgentRecord[]): Array<{ name: string; count: number }> { - const seen = new Map(); - for (const agent of agents) { - if (!agent.department) continue; - seen.set(agent.department, (seen.get(agent.department) ?? 0) + 1); - } - return [...seen.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => a.name.localeCompare(b.name)); -} - -function collectAgentFiles(files: FileObjectRecord[], agents: AgentRecord[]): Array<{ agent: AgentRecord; count: number }> { - const out: Array<{ agent: AgentRecord; count: number }> = []; - for (const agent of agents) { - const count = files.filter((file) => file.ownerType === "agent" && file.ownerId === agent.id).length; - if (count > 0) out.push({ agent, count }); - } - return out.sort((a, b) => b.count - a.count); -} - -function filterFiles(files: FileObjectRecord[], section: Section, agents: AgentRecord[]): FileObjectRecord[] { - if (section.kind === "all") return files; - if (section.kind === "pinned") return files.filter((file) => file.pinned); - if (section.kind === "recent") return files.filter((file) => isRecent(file.createdAt)); - if (section.kind === "agent") return files.filter((file) => file.ownerType === "agent" && file.ownerId === section.value); - if (section.kind === "department") { - const departmentAgents = new Set(agents.filter((agent) => agent.department === section.value).map((agent) => agent.id)); - return files.filter((file) => file.ownerType === "agent" && file.ownerId !== null && departmentAgents.has(file.ownerId)); - } - return files; -} - -function fileGrantCount(fileId: string, grants: FileAccessGrantRecord[]): number { - return grants.filter((grant) => grant.fileId === fileId).length; -} - -function parseScanLabels(file: FileObjectRecord): string[] { - const parsed: unknown = JSON.parse(file.scanLabelsJson); - if (!Array.isArray(parsed) || parsed.some((label) => typeof label !== "string")) { - throw new Error("File scan labels are invalid."); - } - return parsed; -} - -function shortChecksum(value: string): string { - if (!/^[a-f0-9]{8,}$/i.test(value)) throw new Error("File checksum is invalid."); - return value.slice(0, 8); -} - -function shortObjectRef(value: string): string { - const parts = value.split(/[\\/]/).filter(Boolean); - if (parts.length === 0) throw new Error("File object reference is invalid."); - return parts.slice(-2).join("/"); -} - -function isRecent(iso: string): boolean { - const sevenDays = 7 * 24 * 60 * 60 * 1000; - return Date.now() - new Date(iso).getTime() <= sevenDays; -} - -function isAgentOwner(file: FileObjectRecord): boolean { - return file.ownerType === "agent"; -} - -function resolveOwnerName(file: FileObjectRecord, employees: EmployeeRecord[], agents: AgentRecord[]): string { - if (file.ownerType === "employee" && file.ownerId) { - const employee = employees.find((item) => item.id === file.ownerId); - if (employee) return employee.displayName; - } - if (file.ownerType === "agent" && file.ownerId) { - const agent = agents.find((item) => item.id === file.ownerId); - if (agent) return agent.name; - } - return "System"; -} - -function labelFor(section: Section): string { - if (section.kind === "all") return "All files"; - if (section.kind === "recent") return "Recent"; - if (section.kind === "pinned") return "Pinned"; - if (section.kind === "agent") return "By agent"; - return section.value; -} diff --git a/AgentCompany-New/src/renderer/src/pages/files/FileVisuals.tsx b/AgentCompany-New/src/renderer/src/pages/files/FileVisuals.tsx new file mode 100644 index 00000000..5f0cfb9b --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/files/FileVisuals.tsx @@ -0,0 +1,47 @@ +import { Icon, Pill } from "../../shared/ui"; +import type { FileObjectRecord } from "../../../../shared/domain"; +import { FILE_COLOR_MAP, parseScanLabels } from "./file-helpers"; + +export function FileIcon({ kind, big = false }: { kind: string; big?: boolean }) { + const map = FILE_COLOR_MAP[kind] ?? FILE_COLOR_MAP.doc; + if (!map) throw new Error("File color map default missing."); + if (kind === "folder") { + return ( +
+ +
+ ); + } + const label = map.label || kind.slice(0, 3).toUpperCase(); + return ( + + {label} + + ); +} + +export function ScanLabelStrip({ file }: { file: FileObjectRecord }) { + const labels = parseScanLabels(file).slice(0, 2); + if (labels.length === 0) return null; + return ( +
+ {labels.map((label) => ( + {label} + ))} +
+ ); +} diff --git a/AgentCompany-New/src/renderer/src/pages/files/FilesGrid.tsx b/AgentCompany-New/src/renderer/src/pages/files/FilesGrid.tsx new file mode 100644 index 00000000..27de60cf --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/files/FilesGrid.tsx @@ -0,0 +1,94 @@ +import { Avatar, Icon, Pill } from "../../shared/ui"; +import type { + AgentRecord, + EmployeeRecord, + FileAccessGrantRecord, + FileObjectRecord, +} from "../../../../shared/domain"; +import { formatRelative } from "../types"; +import { FileIcon, ScanLabelStrip } from "./FileVisuals"; +import { fileGrantCount, isAgentOwner, resolveOwnerName, shortChecksum, shortObjectRef } from "./file-helpers"; + +export function FilesGrid({ + files, + grants, + employees, + agents, + onTogglePin, +}: { + files: FileObjectRecord[]; + grants: FileAccessGrantRecord[]; + employees: EmployeeRecord[]; + agents: AgentRecord[]; + onTogglePin: (file: FileObjectRecord) => void; +}) { + return ( +
+ {files.map((file) => ( + onTogglePin(file)} + /> + ))} +
+ ); +} + +export function PinnedCard({ + file, + ownerName, + grantCount, + onUnpin, +}: { + file: FileObjectRecord; + ownerName: string; + grantCount: number; + onUnpin: () => void; +}) { + return ( +
+
+ + +
+
+ {file.name} +
+
+ sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)} +
+
+ {file.scanState === "approved" ? "Clean" : "Blocked"} + {grantCount} +
+ +
+ + + {formatRelative(file.updatedAt ?? file.createdAt)} · {ownerName.split(" ")[0]} + +
+
+ ); +} diff --git a/AgentCompany-New/src/renderer/src/pages/files/FilesTable.tsx b/AgentCompany-New/src/renderer/src/pages/files/FilesTable.tsx new file mode 100644 index 00000000..b418eb6b --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/files/FilesTable.tsx @@ -0,0 +1,89 @@ +import { Avatar, IconButton, Pill } from "../../shared/ui"; +import type { + AgentRecord, + EmployeeRecord, + FileAccessGrantRecord, + FileObjectRecord, +} from "../../../../shared/domain"; +import { formatBytes, formatRelative } from "../types"; +import { FileIcon } from "./FileVisuals"; +import { fileGrantCount, isAgentOwner, resolveOwnerName, shortChecksum, shortObjectRef } from "./file-helpers"; + +const ROW_COLUMNS = "minmax(0, 2.7fr) minmax(70px, 0.7fr) minmax(0, 1.2fr) minmax(94px, 0.8fr) minmax(88px, 0.8fr) 56px"; + +export function FilesTable({ + files, + grants, + employees, + agents, + onTogglePin, +}: { + files: FileObjectRecord[]; + grants: FileAccessGrantRecord[]; + employees: EmployeeRecord[]; + agents: AgentRecord[]; + onTogglePin: (file: FileObjectRecord) => void; +}) { + return ( +
+
+
Name
Size
Owner
Access
Modified
+
+ {files.map((file, index) => ( + onTogglePin(file)} + /> + ))} +
+ ); +} + +function FileRow({ + file, + owner, + grantCount, + last, + onTogglePin, +}: { + file: FileObjectRecord; + owner: string; + grantCount: number; + last: boolean; + onTogglePin: () => void; +}) { + return ( +
+
+ +
+ {file.name} + + sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)} + +
+ {file.pinned && Pinned} +
+
{formatBytes(file.sizeBytes)}
+
+ + {owner} +
+
+ {file.scanState === "approved" ? "Clean" : "Blocked"} + {grantCount} +
+
{formatRelative(file.updatedAt ?? file.createdAt)}
+
+ +
+
+ ); +} diff --git a/AgentCompany-New/src/renderer/src/pages/files/FilesTree.tsx b/AgentCompany-New/src/renderer/src/pages/files/FilesTree.tsx new file mode 100644 index 00000000..529f6406 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/files/FilesTree.tsx @@ -0,0 +1,99 @@ +import { Icon } from "../../shared/ui"; +import type { AgentRecord, FileObjectRecord } from "../../../../shared/domain"; +import { isRecent, type Section } from "./file-helpers"; + +export function FilesTree({ + files, + pinnedFiles, + section, + departments, + filesByAgent, + onChange, +}: { + files: FileObjectRecord[]; + pinnedFiles: FileObjectRecord[]; + section: Section; + departments: Array<{ name: string; count: number }>; + filesByAgent: Array<{ agent: AgentRecord; count: number }>; + onChange: (next: Section) => void; +}) { + const recent = files.filter((file) => isRecent(file.createdAt)); + return ( + + ); +} + +function TreeItem({ + icon, + label, + count, + active, + onClick, +}: { + icon: string; + label: string; + count: number; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/AgentCompany-New/src/renderer/src/pages/files/file-helpers.ts b/AgentCompany-New/src/renderer/src/pages/files/file-helpers.ts new file mode 100644 index 00000000..44849ba0 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/files/file-helpers.ts @@ -0,0 +1,106 @@ +import type { + AgentRecord, + EmployeeRecord, + FileAccessGrantRecord, + FileObjectRecord, +} from "../../../../shared/domain"; + +export type ViewMode = "grid" | "list"; + +export type Section = + | { kind: "all" } + | { kind: "recent" } + | { kind: "pinned" } + | { kind: "department"; value: string } + | { kind: "agent"; value: string }; + +export const FILE_COLOR_MAP: Record = { + doc: { color: "#5e6ad2", label: "DOC" }, + sheet: { color: "#2f9e58", label: "XLS" }, + md: { color: "#52525b", label: "MD" }, + pdf: { color: "#d23b3b", label: "PDF" }, + img: { color: "#c87d12", label: "IMG" }, + folder: { color: "#a1a1aa", label: "" }, +}; + +export function collectDepartments(agents: AgentRecord[]): Array<{ name: string; count: number }> { + const seen = new Map(); + for (const agent of agents) { + if (!agent.department) continue; + seen.set(agent.department, (seen.get(agent.department) ?? 0) + 1); + } + return [...seen.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => a.name.localeCompare(b.name)); +} + +export function collectAgentFiles(files: FileObjectRecord[], agents: AgentRecord[]): Array<{ agent: AgentRecord; count: number }> { + const out: Array<{ agent: AgentRecord; count: number }> = []; + for (const agent of agents) { + const count = files.filter((file) => file.ownerType === "agent" && file.ownerId === agent.id).length; + if (count > 0) out.push({ agent, count }); + } + return out.sort((a, b) => b.count - a.count); +} + +export function filterFiles(files: FileObjectRecord[], section: Section, agents: AgentRecord[]): FileObjectRecord[] { + if (section.kind === "all") return files; + if (section.kind === "pinned") return files.filter((file) => file.pinned); + if (section.kind === "recent") return files.filter((file) => isRecent(file.createdAt)); + if (section.kind === "agent") return files.filter((file) => file.ownerType === "agent" && file.ownerId === section.value); + if (section.kind === "department") { + const departmentAgents = new Set(agents.filter((agent) => agent.department === section.value).map((agent) => agent.id)); + return files.filter((file) => file.ownerType === "agent" && file.ownerId !== null && departmentAgents.has(file.ownerId)); + } + return files; +} + +export function fileGrantCount(fileId: string, grants: FileAccessGrantRecord[]): number { + return grants.filter((grant) => grant.fileId === fileId).length; +} + +export function parseScanLabels(file: FileObjectRecord): string[] { + const parsed: unknown = JSON.parse(file.scanLabelsJson); + if (!Array.isArray(parsed) || parsed.some((label) => typeof label !== "string")) { + throw new Error("File scan labels are invalid."); + } + return parsed; +} + +export function shortChecksum(value: string): string { + if (!/^[a-f0-9]{8,}$/i.test(value)) throw new Error("File checksum is invalid."); + return value.slice(0, 8); +} + +export function shortObjectRef(value: string): string { + const parts = value.split(/[\\/]/).filter(Boolean); + if (parts.length === 0) throw new Error("File object reference is invalid."); + return parts.slice(-2).join("/"); +} + +export function isRecent(iso: string): boolean { + const sevenDays = 7 * 24 * 60 * 60 * 1000; + return Date.now() - new Date(iso).getTime() <= sevenDays; +} + +export function isAgentOwner(file: FileObjectRecord): boolean { + return file.ownerType === "agent"; +} + +export function resolveOwnerName(file: FileObjectRecord, employees: EmployeeRecord[], agents: AgentRecord[]): string { + if (file.ownerType === "employee" && file.ownerId) { + const employee = employees.find((item) => item.id === file.ownerId); + if (employee) return employee.displayName; + } + if (file.ownerType === "agent" && file.ownerId) { + const agent = agents.find((item) => item.id === file.ownerId); + if (agent) return agent.name; + } + return "System"; +} + +export function labelFor(section: Section): string { + if (section.kind === "all") return "All files"; + if (section.kind === "recent") return "Recent"; + if (section.kind === "pinned") return "Pinned"; + if (section.kind === "agent") return "By agent"; + return section.value; +}