Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/server/src/wsServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,24 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
};
}

case WS_METHODS.projectsDeleteEntry: {
const body = stripRequestTag(request.body);
const target = yield* resolveWorkspaceWritePath({
workspaceRoot: body.cwd,
relativePath: body.relativePath,
path,
});
yield* fileSystem.remove(target.absolutePath, { recursive: true }).pipe(
Effect.mapError(
(cause) =>
new RouteRequestError({
message: `Failed to delete entry: ${String(cause)}`,
}),
),
);
return {};
}

case WS_METHODS.shellOpenInEditor: {
const body = stripRequestTag(request.body);
return yield* openInEditor(body);
Expand Down
55 changes: 51 additions & 4 deletions apps/web/src/components/WorkspaceFileTree.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type ProjectDirectoryEntry, type ProjectEntry } from "@okcode/contracts";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
ChevronRightIcon,
FolderClosedIcon,
Expand Down Expand Up @@ -27,9 +27,9 @@ import { toastManager } from "./ui/toast";

const TREE_ROW_LEFT_PADDING = 8;
const TREE_ROW_DEPTH_OFFSET = 14;
type WorkspaceFileAction = "open" | "open-in-editor" | "reveal-in-finder" | "copy-path";
type WorkspaceDirectoryAction = "expand" | "collapse" | "open-in-finder" | "copy-path";
type WorkspaceSearchDirectoryAction = "reveal-in-tree" | "open-in-finder" | "copy-path";
type WorkspaceFileAction = "open" | "open-in-editor" | "reveal-in-finder" | "copy-path" | "delete";
type WorkspaceDirectoryAction = "expand" | "collapse" | "open-in-finder" | "copy-path" | "delete";
type WorkspaceSearchDirectoryAction = "reveal-in-tree" | "open-in-finder" | "copy-path" | "delete";

export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
cwd: string;
Expand Down Expand Up @@ -205,6 +205,30 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
setFiltersOpen(false);
}, []);

const queryClient = useQueryClient();
const deleteEntry = useCallback(
async (pathValue: string) => {
const api = readNativeApi();
if (!api) return;
const name = basenameOfPath(pathValue);
const confirmed = await api.dialog.confirm(`Are you sure you want to delete "${name}"?`);
if (!confirmed) return;
try {
await api.projects.deleteEntry({ cwd: props.cwd, relativePath: pathValue });
toastManager.add({ type: "success", title: `Deleted ${name}` });
void queryClient.invalidateQueries({ queryKey: ["projectListDirectory"] });
void queryClient.invalidateQueries({ queryKey: ["projectSearchEntries"] });
} catch (error) {
toastManager.add({
type: "error",
title: "Failed to delete",
description: error instanceof Error ? error.message : "An error occurred.",
});
}
},
[props.cwd, queryClient],
);

return (
<div className={cn("space-y-2", props.className)}>
<div className="space-y-1.5 px-2">
Expand Down Expand Up @@ -273,6 +297,7 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
isLoading={searchResultsQuery.isLoading}
fileManagerName={fileManagerName}
onCopyPath={copyWorkspacePath}
onDeleteEntry={deleteEntry}
onOpenDirectoryInFileManager={openDirectoryInFileManager}
onOpenFileInEditor={openFileInNativeEditor}
onOpenFile={openFile}
Expand All @@ -288,6 +313,7 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
expandedDirectories={expandedDirectories}
fileManagerName={fileManagerName}
onCopyPath={copyWorkspacePath}
onDeleteEntry={deleteEntry}
onOpenDirectoryInFileManager={openDirectoryInFileManager}
onOpenFileInEditor={openFileInNativeEditor}
onOpenFile={openFile}
Expand All @@ -310,6 +336,7 @@ const WorkspaceSearchResults = memo(function WorkspaceSearchResults(props: {
truncated: boolean;
resolvedTheme: "light" | "dark";
onCopyPath: (pathValue: string) => void;
onDeleteEntry: (pathValue: string) => void;
onOpenDirectoryInFileManager: (pathValue: string) => void;
onOpenFileInEditor: (pathValue: string) => void;
onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void;
Expand Down Expand Up @@ -344,6 +371,7 @@ const WorkspaceSearchResults = memo(function WorkspaceSearchResults(props: {
entry={entry}
fileManagerName={props.fileManagerName}
onCopyPath={props.onCopyPath}
onDeleteEntry={props.onDeleteEntry}
onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager}
onOpenFileInEditor={props.onOpenFileInEditor}
onOpenFile={props.onOpenFile}
Expand All @@ -367,6 +395,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
fileManagerName: string;
resolvedTheme: "light" | "dark";
onCopyPath: (pathValue: string) => void;
onDeleteEntry: (pathValue: string) => void;
onOpenDirectoryInFileManager: (pathValue: string) => void;
onOpenFileInEditor: (pathValue: string) => void;
onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void;
Expand All @@ -387,6 +416,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
{ id: "reveal-in-tree", label: "Reveal in tree" },
{ id: "open-in-finder", label: `Open in ${props.fileManagerName}` },
{ id: "copy-path", label: "Copy path" },
{ id: "delete", label: "Delete", destructive: true },
],
{ x: event.clientX, y: event.clientY },
);
Expand All @@ -396,6 +426,8 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
props.onOpenDirectoryInFileManager(props.entry.path);
} else if (clicked === "copy-path") {
props.onCopyPath(props.entry.path);
} else if (clicked === "delete") {
props.onDeleteEntry(props.entry.path);
}
return;
}
Expand All @@ -406,6 +438,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
{ id: "open-in-editor", label: "Open in editor" },
{ id: "reveal-in-finder", label: `Reveal in ${props.fileManagerName}` },
{ id: "copy-path", label: "Copy path" },
{ id: "delete", label: "Delete", destructive: true },
],
{ x: event.clientX, y: event.clientY },
);
Expand All @@ -418,6 +451,8 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
props.onRevealFileInFileManager(props.entry.path);
} else if (clicked === "copy-path") {
props.onCopyPath(props.entry.path);
} else if (clicked === "delete") {
props.onDeleteEntry(props.entry.path);
}
},
[isDirectory, props],
Expand Down Expand Up @@ -468,6 +503,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
fileManagerName: string;
resolvedTheme: "light" | "dark";
onCopyPath: (pathValue: string) => void;
onDeleteEntry: (pathValue: string) => void;
onOpenDirectoryInFileManager: (pathValue: string) => void;
onOpenFileInEditor: (pathValue: string) => void;
onToggleDirectory: (pathValue: string) => void;
Expand Down Expand Up @@ -521,6 +557,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
fileManagerName={props.fileManagerName}
isExpanded={isExpanded}
onCopyPath={props.onCopyPath}
onDeleteEntry={props.onDeleteEntry}
onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager}
onToggleDirectory={props.onToggleDirectory}
/>
Expand All @@ -532,6 +569,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
expandedDirectories={props.expandedDirectories}
fileManagerName={props.fileManagerName}
onCopyPath={props.onCopyPath}
onDeleteEntry={props.onDeleteEntry}
onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager}
onOpenFileInEditor={props.onOpenFileInEditor}
onOpenFile={props.onOpenFile}
Expand All @@ -551,6 +589,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
entry={entry}
fileManagerName={props.fileManagerName}
onCopyPath={props.onCopyPath}
onDeleteEntry={props.onDeleteEntry}
onOpenFileInEditor={props.onOpenFileInEditor}
onOpenFile={props.onOpenFile}
onRevealFileInFileManager={props.onRevealFileInFileManager}
Expand All @@ -573,6 +612,7 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: {
fileManagerName: string;
isExpanded: boolean;
onCopyPath: (pathValue: string) => void;
onDeleteEntry: (pathValue: string) => void;
onOpenDirectoryInFileManager: (pathValue: string) => void;
onToggleDirectory: (pathValue: string) => void;
}) {
Expand All @@ -590,6 +630,7 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: {
},
{ id: "open-in-finder", label: `Open in ${props.fileManagerName}` },
{ id: "copy-path", label: "Copy path" },
{ id: "delete", label: "Delete", destructive: true },
],
{ x: event.clientX, y: event.clientY },
);
Expand All @@ -599,6 +640,8 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: {
props.onOpenDirectoryInFileManager(props.entry.path);
} else if (clicked === "copy-path") {
props.onCopyPath(props.entry.path);
} else if (clicked === "delete") {
props.onDeleteEntry(props.entry.path);
}
},
[props],
Expand Down Expand Up @@ -649,6 +692,7 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: {
fileManagerName: string;
resolvedTheme: "light" | "dark";
onCopyPath: (pathValue: string) => void;
onDeleteEntry: (pathValue: string) => void;
onOpenFileInEditor: (pathValue: string) => void;
onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void;
onRevealFileInFileManager: (pathValue: string) => void;
Expand All @@ -665,6 +709,7 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: {
{ id: "open-in-editor", label: "Open in editor" },
{ id: "reveal-in-finder", label: `Reveal in ${props.fileManagerName}` },
{ id: "copy-path", label: "Copy path" },
{ id: "delete", label: "Delete", destructive: true },
],
{ x: event.clientX, y: event.clientY },
);
Expand All @@ -676,6 +721,8 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: {
props.onRevealFileInFileManager(props.entry.path);
} else if (clicked === "copy-path") {
props.onCopyPath(props.entry.path);
} else if (clicked === "delete") {
props.onDeleteEntry(props.entry.path);
}
},
[props],
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/wsNativeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export function createWsNativeApi(): NativeApi {
listDirectory: (input) => transport.request(WS_METHODS.projectsListDirectory, input),
writeFile: (input) => transport.request(WS_METHODS.projectsWriteFile, input),
readFile: (input) => transport.request(WS_METHODS.projectsReadFile, input),
deleteEntry: (input) => transport.request(WS_METHODS.projectsDeleteEntry, input),
},
shell: {
openInEditor: (cwd, editor) =>
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
GitStatusResult,
} from "./git";
import type {
ProjectDeleteEntryInput,
ProjectListDirectoryInput,
ProjectListDirectoryResult,
ProjectReadFileInput,
Expand Down Expand Up @@ -313,6 +314,7 @@ export interface NativeApi {
listDirectory: (input: ProjectListDirectoryInput) => Promise<ProjectListDirectoryResult>;
writeFile: (input: ProjectWriteFileInput) => Promise<ProjectWriteFileResult>;
readFile: (input: ProjectReadFileInput) => Promise<ProjectReadFileResult>;
deleteEntry: (input: ProjectDeleteEntryInput) => Promise<void>;
};
shell: {
openInEditor: (cwd: string, editor: EditorId) => Promise<void>;
Expand Down
6 changes: 6 additions & 0 deletions packages/contracts/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,9 @@ export const ProjectReadFileResult = Schema.Struct({
imageDataUrl: Schema.optional(Schema.String),
});
export type ProjectReadFileResult = typeof ProjectReadFileResult.Type;

export const ProjectDeleteEntryInput = Schema.Struct({
cwd: TrimmedNonEmptyString,
relativePath: TrimmedNonEmptyString.check(Schema.isMaxLength(PROJECT_DIRECTORY_PATH_MAX_LENGTH)),
});
export type ProjectDeleteEntryInput = typeof ProjectDeleteEntryInput.Type;
3 changes: 3 additions & 0 deletions packages/contracts/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
SaveProjectEnvironmentVariablesInput,
} from "./environment";
import {
ProjectDeleteEntryInput,
ProjectListDirectoryInput,
ProjectReadFileInput,
ProjectSearchEntriesInput,
Expand Down Expand Up @@ -93,6 +94,7 @@ export const WS_METHODS = {
projectsListDirectory: "projects.listDirectory",
projectsWriteFile: "projects.writeFile",
projectsReadFile: "projects.readFile",
projectsDeleteEntry: "projects.deleteEntry",

// Shell methods
shellOpenInEditor: "shell.openInEditor",
Expand Down Expand Up @@ -204,6 +206,7 @@ const WebSocketRequestBody = Schema.Union([
tagRequestBody(WS_METHODS.projectsListDirectory, ProjectListDirectoryInput),
tagRequestBody(WS_METHODS.projectsWriteFile, ProjectWriteFileInput),
tagRequestBody(WS_METHODS.projectsReadFile, ProjectReadFileInput),
tagRequestBody(WS_METHODS.projectsDeleteEntry, ProjectDeleteEntryInput),

// Shell methods
tagRequestBody(WS_METHODS.shellOpenInEditor, OpenInEditorInput),
Expand Down
Loading