Open files and replace the file row menu with real actions
This commit is contained in:
1 parent
d2695202b5
commit
69ae525cb7
10 files changed
+81
-10
No files matched your search
@@ -196,6 +196,15 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.openFile, async (_event, input) => {
|
||||
try {
|
||||
await files.openFile(input);
|
||||
return ok(null);
|
||||
} catch (error) {
|
||||
return fail("file_open_failed", error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.pickDirectory, async () => {
|
||||
try {
|
||||
const result = await dialog.showOpenDialog(deps.mainWindow, { properties: ["openDirectory", "createDirectory"] });
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createHash } from "node:crypto";
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, extname, join } from "node:path";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { dialog } from "electron";
|
||||
import type { ImportFileInput } from "../../shared/commands";
|
||||
import { dialog, shell } from "electron";
|
||||
import type { ImportFileInput, OpenFileInput } from "../../shared/commands";
|
||||
import { FILE_SCAN_STATE, type FileScanState } from "../../shared/domain";
|
||||
import { createId } from "../../shared/ids";
|
||||
import { AuditService } from "./audit-service";
|
||||
@@ -117,6 +117,16 @@ export class FileService {
|
||||
return id;
|
||||
}
|
||||
|
||||
async openFile(input: OpenFileInput): Promise<void> {
|
||||
const row = this.db.prepare(
|
||||
"select storage_path as storagePath, scan_state as scanState from file_objects where id = ? and org_id = ?",
|
||||
).get(input.fileId, input.orgId) as { storagePath: string; scanState: string } | undefined;
|
||||
if (!row) throw new Error("File was not found.");
|
||||
if (row.scanState !== FILE_SCAN_STATE.APPROVED) throw new Error("Blocked files cannot be opened.");
|
||||
const failure = await shell.openPath(row.storagePath);
|
||||
if (failure) throw new Error(failure);
|
||||
}
|
||||
|
||||
setPinned(orgId: string, fileId: string, pinned: boolean): void {
|
||||
const result = this.db.prepare(
|
||||
"update file_objects set pinned = ?, updated_at = ? where id = ? and org_id = ?",
|
||||
|
||||
@@ -43,6 +43,7 @@ const api: DesktopApi = {
|
||||
createScheduledPost: (input) => invoke(IPC_CHANNELS.createScheduledPost, input),
|
||||
setScheduledPostState: (input) => invoke(IPC_CHANNELS.setScheduledPostState, input),
|
||||
importFile: (input) => invoke(IPC_CHANNELS.importFile, input),
|
||||
openFile: (input) => invoke(IPC_CHANNELS.openFile, input),
|
||||
pickDirectory: () => invoke(IPC_CHANNELS.pickDirectory),
|
||||
setFilePinned: (input) => invoke(IPC_CHANNELS.setFilePinned, input),
|
||||
createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Button, Icon, Segmented } from "../shared/ui";
|
||||
import { TopbarActions } from "../shared/TopbarActions";
|
||||
import type { FileObjectRecord } from "../../../shared/domain";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId, formatBytes } from "./types";
|
||||
import { FilesTree } from "./files/FilesTree";
|
||||
@@ -40,6 +41,13 @@ export function FilesPage({ snapshot, actions }: PageProps) {
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
});
|
||||
};
|
||||
const openFile = (file: FileObjectRecord) => {
|
||||
void window.agentCompany.openFile({ orgId, fileId: file.id }).then((result) => {
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
});
|
||||
};
|
||||
const togglePin = (file: FileObjectRecord) =>
|
||||
void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }));
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gridTemplateColumns: "minmax(160px, 200px) minmax(0, 1fr)", gap: 24, height: "100%", minHeight: 0, padding: "28px 32px 0" }}>
|
||||
@@ -83,7 +91,8 @@ export function FilesPage({ snapshot, actions }: PageProps) {
|
||||
file={file}
|
||||
ownerName={resolveOwnerName(file, employees, agents)}
|
||||
grantCount={fileGrantCount(file.id, fileAccessGrants)}
|
||||
onUnpin={() => void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: false }))}
|
||||
onOpen={() => openFile(file)}
|
||||
onUnpin={() => togglePin(file)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -101,7 +110,8 @@ export function FilesPage({ snapshot, actions }: PageProps) {
|
||||
grants={fileAccessGrants}
|
||||
employees={employees}
|
||||
agents={agents}
|
||||
onTogglePin={(file) => void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))}
|
||||
onOpen={openFile}
|
||||
onTogglePin={togglePin}
|
||||
/>
|
||||
) : (
|
||||
<FilesGrid
|
||||
@@ -109,7 +119,8 @@ export function FilesPage({ snapshot, actions }: PageProps) {
|
||||
grants={fileAccessGrants}
|
||||
employees={employees}
|
||||
agents={agents}
|
||||
onTogglePin={(file) => void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))}
|
||||
onOpen={openFile}
|
||||
onTogglePin={togglePin}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,14 @@ export function FilesGrid({
|
||||
grants,
|
||||
employees,
|
||||
agents,
|
||||
onOpen,
|
||||
onTogglePin,
|
||||
}: {
|
||||
files: FileObjectRecord[];
|
||||
grants: FileAccessGrantRecord[];
|
||||
employees: EmployeeRecord[];
|
||||
agents: AgentRecord[];
|
||||
onOpen: (file: FileObjectRecord) => void;
|
||||
onTogglePin: (file: FileObjectRecord) => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -30,6 +32,7 @@ export function FilesGrid({
|
||||
file={file}
|
||||
ownerName={resolveOwnerName(file, employees, agents)}
|
||||
grantCount={fileGrantCount(file.id, grants)}
|
||||
onOpen={() => onOpen(file)}
|
||||
onUnpin={() => onTogglePin(file)}
|
||||
/>
|
||||
))}
|
||||
@@ -41,15 +44,23 @@ export function PinnedCard({
|
||||
file,
|
||||
ownerName,
|
||||
grantCount,
|
||||
onOpen,
|
||||
onUnpin,
|
||||
}: {
|
||||
file: FileObjectRecord;
|
||||
ownerName: string;
|
||||
grantCount: number;
|
||||
onOpen: () => void;
|
||||
onUnpin: () => void;
|
||||
}) {
|
||||
const canOpen = file.scanState === "approved";
|
||||
return (
|
||||
<div
|
||||
className={canOpen ? "clickable-card" : ""}
|
||||
role={canOpen ? "button" : undefined}
|
||||
tabIndex={canOpen ? 0 : undefined}
|
||||
onClick={canOpen ? onOpen : undefined}
|
||||
onKeyDown={canOpen ? (event) => { if (event.key === "Enter") onOpen(); } : undefined}
|
||||
style={{
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 10,
|
||||
@@ -59,6 +70,7 @@ export function PinnedCard({
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
minWidth: 0,
|
||||
cursor: canOpen ? "pointer" : "default",
|
||||
}}
|
||||
>
|
||||
<div className="row" style={{ gap: 10 }}>
|
||||
@@ -66,7 +78,7 @@ export function PinnedCard({
|
||||
<button
|
||||
type="button"
|
||||
aria-label={file.pinned ? "Unpin" : "Pin"}
|
||||
onClick={onUnpin}
|
||||
onClick={(event) => { event.stopPropagation(); onUnpin(); }}
|
||||
style={{ marginLeft: "auto", padding: 4, border: 0, background: "transparent", color: file.pinned ? "var(--accent)" : "var(--text-4)", cursor: "pointer" }}
|
||||
>
|
||||
<Icon name="pin" size={14} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Avatar, IconButton, Pill } from "../../shared/ui";
|
||||
import { MenuRow, Popover } from "../../shared/Popover";
|
||||
import type {
|
||||
AgentRecord,
|
||||
EmployeeRecord,
|
||||
@@ -16,12 +17,14 @@ export function FilesTable({
|
||||
grants,
|
||||
employees,
|
||||
agents,
|
||||
onOpen,
|
||||
onTogglePin,
|
||||
}: {
|
||||
files: FileObjectRecord[];
|
||||
grants: FileAccessGrantRecord[];
|
||||
employees: EmployeeRecord[];
|
||||
agents: AgentRecord[];
|
||||
onOpen: (file: FileObjectRecord) => void;
|
||||
onTogglePin: (file: FileObjectRecord) => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -36,6 +39,7 @@ export function FilesTable({
|
||||
owner={resolveOwnerName(file, employees, agents)}
|
||||
grantCount={fileGrantCount(file.id, grants)}
|
||||
last={index === files.length - 1}
|
||||
onOpen={() => onOpen(file)}
|
||||
onTogglePin={() => onTogglePin(file)}
|
||||
/>
|
||||
))}
|
||||
@@ -48,17 +52,24 @@ function FileRow({
|
||||
owner,
|
||||
grantCount,
|
||||
last,
|
||||
onOpen,
|
||||
onTogglePin,
|
||||
}: {
|
||||
file: FileObjectRecord;
|
||||
owner: string;
|
||||
grantCount: number;
|
||||
last: boolean;
|
||||
onOpen: () => void;
|
||||
onTogglePin: () => void;
|
||||
}) {
|
||||
const canOpen = file.scanState === "approved";
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
className={`table-row ${canOpen ? "clickable" : ""}`.trim()}
|
||||
role={canOpen ? "button" : undefined}
|
||||
tabIndex={canOpen ? 0 : undefined}
|
||||
onClick={canOpen ? onOpen : undefined}
|
||||
onKeyDown={canOpen ? (event) => { if (event.key === "Enter") onOpen(); } : undefined}
|
||||
style={{ gridTemplateColumns: ROW_COLUMNS, borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
|
||||
>
|
||||
<div className="row" style={{ gap: 10, minWidth: 0 }}>
|
||||
@@ -77,12 +88,19 @@ function FileRow({
|
||||
<span style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{owner}</span>
|
||||
</div>
|
||||
<div className="row" style={{ gap: 6, minWidth: 0 }}>
|
||||
<Pill tone={file.scanState === "approved" ? "green" : "red"}>{file.scanState === "approved" ? "Clean" : "Blocked"}</Pill>
|
||||
<Pill tone={canOpen ? "green" : "red"}>{canOpen ? "Clean" : "Blocked"}</Pill>
|
||||
<span className="mono" style={{ color: "var(--text-3)", fontSize: 11 }}>{grantCount}</span>
|
||||
</div>
|
||||
<div style={{ color: "var(--text-3)", fontSize: 12 }}>{formatRelative(file.updatedAt ?? file.createdAt)}</div>
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<IconButton icon={file.pinned ? "pin" : "more"} label={file.pinned ? "Unpin" : "More"} onClick={onTogglePin} />
|
||||
<div style={{ textAlign: "right" }} onClick={(event) => event.stopPropagation()}>
|
||||
<Popover trigger={({ toggle }) => <IconButton icon="more" label={`Actions for ${file.name}`} onClick={toggle} />}>
|
||||
{(close) => (
|
||||
<>
|
||||
{canOpen && <MenuRow onClick={() => { onOpen(); close(); }}>Open</MenuRow>}
|
||||
<MenuRow onClick={() => { onTogglePin(); close(); }}>{file.pinned ? "Unpin" : "Pin"}</MenuRow>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -519,6 +519,8 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
.table-row:last-child { border-bottom: 0; }
|
||||
.table-row.clickable { cursor: pointer; }
|
||||
.table-row.clickable:hover { background: var(--hover); }
|
||||
.clickable-card { transition: border-color 0.1s ease; }
|
||||
.clickable-card:hover { border-color: var(--border-strong); }
|
||||
|
||||
.link-button {
|
||||
border: 0;
|
||||
|
||||
@@ -248,6 +248,11 @@ export interface ImportFileInput {
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export interface OpenFileInput {
|
||||
orgId: string;
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
export interface CreateMigrationReportInput {
|
||||
orgId: string;
|
||||
sourceDatabasePath: string;
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
DeleteOrganizationInput,
|
||||
EditMessageInput,
|
||||
ImportFileInput,
|
||||
OpenFileInput,
|
||||
ImportSkillDirectoryInput,
|
||||
GrantRoleInput,
|
||||
LaunchProviderSignInInput,
|
||||
@@ -97,6 +98,7 @@ export interface DesktopApi {
|
||||
createScheduledPost(input: CreateScheduledPostInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
setScheduledPostState(input: SetScheduledPostStateInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
importFile(input: ImportFileInput): Promise<DesktopResult<string | null>>;
|
||||
openFile(input: OpenFileInput): Promise<DesktopResult<null>>;
|
||||
pickDirectory(): Promise<DesktopResult<string | null>>;
|
||||
setFilePinned(input: SetFilePinnedInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
createMigrationReport(input: CreateMigrationReportInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
|
||||
@@ -35,6 +35,7 @@ export const IPC_CHANNELS = {
|
||||
createScheduledPost: "agentcompany:create-scheduled-post",
|
||||
setScheduledPostState: "agentcompany:set-scheduled-post-state",
|
||||
importFile: "agentcompany:import-file",
|
||||
openFile: "agentcompany:open-file",
|
||||
pickDirectory: "agentcompany:pick-directory",
|
||||
setFilePinned: "agentcompany:set-file-pinned",
|
||||
createMigrationReport: "agentcompany:create-migration-report",
|
||||
|
||||
Reference in new issue
Block a user