Q166: workspaces can be edited, made primary and removed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
20c37362f3
commit
e8722cd815
12 files changed
+247
-9
No files matched your search
@@ -55,6 +55,10 @@ export const zh: Record<string, string> = {
|
||||
"Show older runs": "显示更早的运行",
|
||||
"Target date": "目标日期",
|
||||
"Edit company": "编辑公司",
|
||||
"Make primary": "设为主工作区",
|
||||
"Remove this workspace?": "移除这个工作区?",
|
||||
"Remove this workspace? Its folder stays on disk.": "移除这个工作区?文件夹仍保留在磁盘上。",
|
||||
"Edit workspace": "编辑工作区",
|
||||
"Who runs the work": "由谁推进工作",
|
||||
"AI CEO: runs tasks without asking": "AI CEO:无需询问直接执行任务",
|
||||
"AI Co-Founder: asks before risky tasks": "AI 联合创始人:高风险任务先征求同意",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WorkspaceRow } from "./WorkspaceRow";
|
||||
import { CompanyDialog } from "./CompanyDialog";
|
||||
import { ScopeNote } from "./ScopeNote";
|
||||
import { useState, type FormEvent } from "react";
|
||||
@@ -124,15 +125,7 @@ export function WorkspacePanel({
|
||||
</div>
|
||||
<ScopeNote scope="company_folders" company={organization.name} />
|
||||
{workspaces.map((workspace) => (
|
||||
<div key={workspace.id} className="row" aria-current={workspace.id === focusedWorkspaceId ? "true" : undefined} style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)", background: workspace.id === focusedWorkspaceId ? "var(--accent-soft)" : undefined }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540 }}>{workspace.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: "auto" }}>
|
||||
{workspace.isPrimary && <Pill tone="violet">{t("Primary")}</Pill>}
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceRow key={workspace.id} workspace={workspace} projects={projects} focused={workspace.id === focusedWorkspaceId} actions={actions} />
|
||||
))}
|
||||
|
||||
<Dialog title={t("Add workspace")} open={createOpen} submitLabel={t("Create")} onClose={() => setCreateOpen(false)} onSubmit={submit}>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import type { ProjectRecord, WorkspaceRecord } from "../../../../shared/domain";
|
||||
import { Button, Field, Icon, IconButton, Pill } from "../../shared/ui";
|
||||
import { Dialog } from "../../shared/Dialog";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
/** One workspace with its upkeep: edit its name, folder and project, make it primary, or remove it once nothing open uses it. */
|
||||
export function WorkspaceRow({ workspace, projects, focused, actions }: { workspace: WorkspaceRecord; projects: ProjectRecord[]; focused: boolean; actions: SnapshotActions }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [confirmRemove, setConfirmRemove] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const act = (operation: () => Promise<unknown>, after?: () => void) => {
|
||||
setError(null);
|
||||
operation().then(after, (reason: unknown) => setError(reason instanceof Error ? reason.message : String(reason)));
|
||||
};
|
||||
const orgId = workspace.orgId;
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
const value = (key: string) => String(form.get(key) ?? "");
|
||||
act(() => actions.run(() => window.agentCompany.updateWorkspace({
|
||||
orgId, workspaceId: workspace.id, name: value("name"), localPath: value("localPath"), projectId: value("projectId") || null,
|
||||
})), () => setEditing(false));
|
||||
};
|
||||
const browse = () => {
|
||||
void window.agentCompany.pickDirectory().then((result) => {
|
||||
const input = document.querySelector<HTMLInputElement>(`input[data-workspace-path="${workspace.id}"]`);
|
||||
if (result.ok && result.data && input) input.value = result.data;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div role="group" aria-label={workspace.name} aria-current={focused ? "true" : undefined} style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)", background: focused ? "var(--accent-soft)" : undefined }}>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ fontWeight: 540 }}>{workspace.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
|
||||
</div>
|
||||
{workspace.isPrimary
|
||||
? <Pill tone="violet">{t("Primary")}</Pill>
|
||||
: <Button size="small" tone="ghost" onClick={() => act(() => actions.run(() => window.agentCompany.setPrimaryWorkspace({ orgId, workspaceId: workspace.id })))}>{t("Make primary")}</Button>}
|
||||
<IconButton icon="edit" label={t("Edit {name}", { name: workspace.name })} onClick={() => setEditing(true)} />
|
||||
<Button size="small" tone="ghost" onClick={() => setConfirmRemove(true)}>{t("Remove")}</Button>
|
||||
</div>
|
||||
{confirmRemove && (
|
||||
<div className="row" role="alertdialog" aria-label={t("Remove this workspace?")} style={{ gap: 8, marginTop: 6 }}>
|
||||
<span style={{ fontSize: 13 }}>{t("Remove this workspace? Its folder stays on disk.")}</span>
|
||||
<Button size="small" tone="primary" onClick={() => act(() => actions.run(() => window.agentCompany.deleteWorkspace({ orgId, workspaceId: workspace.id })))}>{t("Remove")}</Button>
|
||||
<Button size="small" tone="ghost" onClick={() => { setConfirmRemove(false); setError(null); }}>{t("Cancel")}</Button>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="form-error" role="alert">{error}</div>}
|
||||
<Dialog title={t("Edit workspace")} open={editing} submitLabel={t("Save")} onClose={() => setEditing(false)} onSubmit={submit}>
|
||||
<Field label={t("Name")}><input className="input" name="name" defaultValue={workspace.name} required /></Field>
|
||||
<Field label={t("Local path")}>
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<input className="input" name="localPath" data-workspace-path={workspace.id} defaultValue={workspace.localPath} required style={{ flex: 1 }} />
|
||||
<Button tone="ghost" onClick={browse}><Icon name="folder" size={16} /> {t("Browse")}</Button>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label={t("Project")}>
|
||||
<select className="input" name="projectId" defaultValue={workspace.projectId ?? ""}>
|
||||
<option value="">{t("None")}</option>
|
||||
{projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
{error && <div className="form-error" role="alert">{error}</div>}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user