Skip to content

Commit 3e13210

Browse files
committed
add workspace directory feature
Allows setting a default directory for workspaces. New shells opened in the workspace will start in this directory. Includes validation, error handling, and debounced updates in the workspace editor.
1 parent 31a8714 commit 3e13210

File tree

18 files changed

+844
-613
lines changed

18 files changed

+844
-613
lines changed

emain/emain-ipc.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,21 @@ export function initIpcHandlers() {
258258
event.returnValue = getWaveVersion() as AboutModalDetails;
259259
});
260260

261+
electron.ipcMain.handle("show-open-folder-dialog", async () => {
262+
const ww = focusedWaveWindow;
263+
if (ww == null) {
264+
return null;
265+
}
266+
const result = await electron.dialog.showOpenDialog(ww, {
267+
title: "Select Workspace Directory",
268+
properties: ["openDirectory", "createDirectory"],
269+
});
270+
if (result.canceled || result.filePaths.length === 0) {
271+
return null;
272+
}
273+
return result.filePaths[0];
274+
});
275+
261276
electron.ipcMain.on("get-zoom-factor", (event) => {
262277
event.returnValue = event.sender.getZoomFactor();
263278
});

emain/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld("api", {
6868
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
6969
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
7070
doRefresh: () => ipcRenderer.send("do-refresh"),
71+
showOpenFolderDialog: () => ipcRenderer.invoke("show-open-folder-dialog"),
7172
});
7273

7374
// Custom event for "new-window"

frontend/app/store/services.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ class WorkspaceServiceType {
182182
}
183183

184184
// @returns object updates
185-
UpdateWorkspace(workspaceId: string, name: string, icon: string, color: string, applyDefaults: boolean): Promise<void> {
185+
UpdateWorkspace(workspaceId: string, name: string, icon: string, color: string, directory: string, applyDefaults: boolean): Promise<void> {
186186
return WOS.callBackendService("workspace", "UpdateWorkspace", Array.from(arguments))
187187
}
188188
}

frontend/app/tab/workspaceeditor.scss

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,33 @@
6161
}
6262
}
6363

64+
.directory-selector {
65+
margin-top: 15px;
66+
padding-top: 15px;
67+
border-top: 1px solid var(--modal-border-color);
68+
69+
.directory-label {
70+
display: block;
71+
font-size: 12px;
72+
color: var(--secondary-text-color);
73+
margin-bottom: 5px;
74+
}
75+
76+
.directory-input-row {
77+
display: flex;
78+
gap: 8px;
79+
align-items: center;
80+
81+
.directory-input {
82+
flex: 1;
83+
}
84+
85+
.browse-btn {
86+
flex-shrink: 0;
87+
}
88+
}
89+
}
90+
6491
.delete-ws-btn-wrapper {
6592
display: flex;
6693
align-items: center;

frontend/app/tab/workspaceeditor.tsx

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getApi } from "@/app/store/global";
12
import { fireAndForget, makeIconClass } from "@/util/util";
23
import clsx from "clsx";
34
import { memo, useEffect, useRef, useState } from "react";
@@ -13,19 +14,20 @@ interface ColorSelectorProps {
1314
className?: string;
1415
}
1516

16-
const ColorSelector = memo(({ colors, selectedColor, onSelect, className }: ColorSelectorProps) => {
17-
const handleColorClick = (color: string) => {
18-
onSelect(color);
19-
};
20-
17+
const ColorSelector = memo(function ColorSelector({
18+
colors,
19+
selectedColor,
20+
onSelect,
21+
className,
22+
}: ColorSelectorProps) {
2123
return (
2224
<div className={clsx("color-selector", className)}>
2325
{colors.map((color) => (
2426
<div
2527
key={color}
2628
className={clsx("color-circle", { selected: selectedColor === color })}
2729
style={{ backgroundColor: color }}
28-
onClick={() => handleColorClick(color)}
30+
onClick={() => onSelect(color)}
2931
/>
3032
))}
3133
</div>
@@ -39,11 +41,12 @@ interface IconSelectorProps {
3941
className?: string;
4042
}
4143

42-
const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSelectorProps) => {
43-
const handleIconClick = (icon: string) => {
44-
onSelect(icon);
45-
};
46-
44+
const IconSelector = memo(function IconSelector({
45+
icons,
46+
selectedIcon,
47+
onSelect,
48+
className,
49+
}: IconSelectorProps) {
4750
return (
4851
<div className={clsx("icon-selector", className)}>
4952
{icons.map((icon) => {
@@ -52,7 +55,7 @@ const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSel
5255
<i
5356
key={icon}
5457
className={clsx(iconClass, "icon-item", { selected: selectedIcon === icon })}
55-
onClick={() => handleIconClick(icon)}
58+
onClick={() => onSelect(icon)}
5659
/>
5760
);
5861
})}
@@ -64,33 +67,37 @@ interface WorkspaceEditorProps {
6467
title: string;
6568
icon: string;
6669
color: string;
70+
directory: string;
6771
focusInput: boolean;
6872
onTitleChange: (newTitle: string) => void;
6973
onColorChange: (newColor: string) => void;
7074
onIconChange: (newIcon: string) => void;
75+
onDirectoryChange: (newDirectory: string) => void;
7176
onDeleteWorkspace: () => void;
7277
}
73-
const WorkspaceEditorComponent = ({
78+
export const WorkspaceEditor = memo(function WorkspaceEditor({
7479
title,
7580
icon,
7681
color,
82+
directory,
7783
focusInput,
7884
onTitleChange,
7985
onColorChange,
8086
onIconChange,
87+
onDirectoryChange,
8188
onDeleteWorkspace,
82-
}: WorkspaceEditorProps) => {
89+
}: WorkspaceEditorProps) {
8390
const inputRef = useRef<HTMLInputElement>(null);
8491

8592
const [colors, setColors] = useState<string[]>([]);
8693
const [icons, setIcons] = useState<string[]>([]);
8794

8895
useEffect(() => {
8996
fireAndForget(async () => {
90-
const colors = await WorkspaceService.GetColors();
91-
const icons = await WorkspaceService.GetIcons();
92-
setColors(colors);
93-
setIcons(icons);
97+
const fetchedColors = await WorkspaceService.GetColors();
98+
const fetchedIcons = await WorkspaceService.GetIcons();
99+
setColors(fetchedColors);
100+
setIcons(fetchedIcons);
94101
});
95102
}, []);
96103

@@ -113,13 +120,37 @@ const WorkspaceEditorComponent = ({
113120
/>
114121
<ColorSelector selectedColor={color} colors={colors} onSelect={onColorChange} />
115122
<IconSelector selectedIcon={icon} icons={icons} onSelect={onIconChange} />
123+
<div className="directory-selector">
124+
<label className="directory-label">Directory</label>
125+
<div className="directory-input-row">
126+
<Input
127+
value={directory}
128+
onChange={onDirectoryChange}
129+
placeholder="~/projects/myworkspace"
130+
className="directory-input"
131+
/>
132+
<Button
133+
className="ghost browse-btn"
134+
onClick={async () => {
135+
try {
136+
const path = await getApi().showOpenFolderDialog();
137+
if (path) {
138+
onDirectoryChange(path);
139+
}
140+
} catch (e) {
141+
console.error("error opening folder dialog:", e);
142+
}
143+
}}
144+
>
145+
Browse
146+
</Button>
147+
</div>
148+
</div>
116149
<div className="delete-ws-btn-wrapper">
117150
<Button className="ghost red text-[12px] bold" onClick={onDeleteWorkspace}>
118151
Delete workspace
119152
</Button>
120153
</div>
121154
</div>
122155
);
123-
};
124-
125-
export const WorkspaceEditor = memo(WorkspaceEditorComponent) as typeof WorkspaceEditorComponent;
156+
});

0 commit comments

Comments
 (0)