Add goal, project, and workspace CRUD
Full-stack: IPC channels, command types, OrganizationService methods, IPC handlers, preload bridge, desktop-api types, and UI dialogs on the Work page. Users can now create goals and projects directly instead of relying on the onboarding bootstrap.
This commit is contained in:
1 parent
2ef721774f
commit
5f92cd0d75
9 files changed
+218
-1
No files matched your search
@@ -16,6 +16,8 @@ import {
|
||||
type WorkspaceRecord,
|
||||
} from "../../../shared/domain";
|
||||
import { NewTaskDialog } from "./NewTaskDialog";
|
||||
import { GoalDialog } from "./work/GoalDialog";
|
||||
import { ProjectDialog } from "./work/ProjectDialog";
|
||||
import { TaskDetailDialog } from "./work/TaskDetailDialog";
|
||||
import { ProjectOverview } from "./work/ProjectOverview";
|
||||
import { canRun, latestTaskRun, nextStatus, prettyRunState, prettyStatus, providerFor, runTone } from "./work/task-helpers";
|
||||
@@ -41,6 +43,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
|
||||
const orgId = activeOrgId(snapshot);
|
||||
const [view, setView] = useState<View>("board");
|
||||
const [newTaskOpen, setNewTaskOpen] = useState(false);
|
||||
const [newGoalOpen, setNewGoalOpen] = useState(false);
|
||||
const [newProjectOpen, setNewProjectOpen] = useState(false);
|
||||
const [priorityFilter, setPriorityFilter] = useState<TaskPriority | "all">("all");
|
||||
const [assigneeFilter, setAssigneeFilter] = useState<string>("all");
|
||||
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
|
||||
@@ -88,6 +92,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
|
||||
onPriority={setPriorityFilter}
|
||||
onAssignee={setAssigneeFilter}
|
||||
/>
|
||||
<Button tone="ghost" onClick={() => setNewGoalOpen(true)}><Icon name="plus" size={13} /> Goal</Button>
|
||||
<Button tone="ghost" onClick={() => setNewProjectOpen(true)}><Icon name="plus" size={13} /> Project</Button>
|
||||
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={13} /> New task</Button>
|
||||
</TopbarActions>
|
||||
<h1 className="page-h1">Work</h1>
|
||||
@@ -124,6 +130,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
|
||||
)}
|
||||
{view === "timeline" && <TimelineView tasks={tasks} goals={goals} />}
|
||||
|
||||
<GoalDialog open={newGoalOpen} orgId={orgId} agents={agents} actions={actions} onClose={() => setNewGoalOpen(false)} />
|
||||
<ProjectDialog open={newProjectOpen} orgId={orgId} goals={goals} agents={agents} actions={actions} onClose={() => setNewProjectOpen(false)} />
|
||||
<NewTaskDialog open={newTaskOpen} orgId={orgId} snapshot={snapshot} actions={actions} onClose={() => setNewTaskOpen(false)} />
|
||||
<TaskDetailDialog
|
||||
task={detailTask}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { type FormEvent } from "react";
|
||||
import { Field } from "../../shared/ui";
|
||||
import { Dialog } from "../../shared/Dialog";
|
||||
import type { AgentRecord, AppSnapshot } from "../../../../shared/domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
|
||||
function text(form: FormData, key: string): string {
|
||||
return String(form.get(key) ?? "").trim();
|
||||
}
|
||||
|
||||
function optional(form: FormData, key: string): string | null {
|
||||
return text(form, key) || null;
|
||||
}
|
||||
|
||||
export function GoalDialog({
|
||||
open,
|
||||
orgId,
|
||||
agents,
|
||||
actions,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
orgId: string;
|
||||
agents: AgentRecord[];
|
||||
actions: SnapshotActions;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
void actions.run(() => window.agentCompany.createGoal({
|
||||
orgId,
|
||||
title: text(form, "title"),
|
||||
description: text(form, "description"),
|
||||
ownerAgentId: optional(form, "ownerAgentId"),
|
||||
})).then((result) => { if (result) onClose(); });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog title="New goal" open={open} submitLabel="Create" onClose={onClose} onSubmit={submit}>
|
||||
<Field label="Title"><input className="input" name="title" required /></Field>
|
||||
<Field label="Description"><textarea className="input" name="description" required /></Field>
|
||||
<Field label="Owner agent">
|
||||
<select className="input" name="ownerAgentId" defaultValue="">
|
||||
<option value="">Unassigned</option>
|
||||
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name} · {agent.role}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { type FormEvent } from "react";
|
||||
import { Field } from "../../shared/ui";
|
||||
import { Dialog } from "../../shared/Dialog";
|
||||
import type { AgentRecord, GoalRecord } from "../../../../shared/domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
|
||||
function text(form: FormData, key: string): string {
|
||||
return String(form.get(key) ?? "").trim();
|
||||
}
|
||||
|
||||
function optional(form: FormData, key: string): string | null {
|
||||
return text(form, key) || null;
|
||||
}
|
||||
|
||||
export function ProjectDialog({
|
||||
open,
|
||||
orgId,
|
||||
goals,
|
||||
agents,
|
||||
actions,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
orgId: string;
|
||||
goals: GoalRecord[];
|
||||
agents: AgentRecord[];
|
||||
actions: SnapshotActions;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
void actions.run(() => window.agentCompany.createProject({
|
||||
orgId,
|
||||
goalId: optional(form, "goalId"),
|
||||
name: text(form, "name"),
|
||||
description: text(form, "description"),
|
||||
leadAgentId: optional(form, "leadAgentId"),
|
||||
})).then((result) => { if (result) onClose(); });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog title="New project" open={open} submitLabel="Create" onClose={onClose} onSubmit={submit}>
|
||||
<Field label="Name"><input className="input" name="name" required /></Field>
|
||||
<Field label="Description"><textarea className="input" name="description" required /></Field>
|
||||
<Field label="Goal">
|
||||
<select className="input" name="goalId" defaultValue="">
|
||||
<option value="">None</option>
|
||||
{goals.map((goal) => <option key={goal.id} value={goal.id}>{goal.title}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Lead agent">
|
||||
<select className="input" name="leadAgentId" defaultValue="">
|
||||
<option value="">Unassigned</option>
|
||||
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name} · {agent.role}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user