Create project collaboration surface

This commit is contained in:
ZacharyZhang-NY committed 2026-06-07 13:41:53 -04:00
1 parent 3f34d1cf65
commit d5a7665893
5 files changed
+108 -1

No files matched your search

@@ -17,6 +17,7 @@ import {
} from "../../../shared/domain";
import { NewTaskDialog } from "./NewTaskDialog";
import { TaskDetailDialog } from "./work/TaskDetailDialog";
import { ProjectOverview } from "./work/ProjectOverview";
import { canRun, latestTaskRun, nextStatus, prettyRunState, prettyStatus, providerFor, runTone } from "./work/task-helpers";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
@@ -64,6 +65,7 @@ export function WorkPage({ snapshot, actions }: PageProps) {
const workspaces = useMemo(() => snapshot.workspaces.filter((workspace) => workspace.orgId === orgId), [snapshot.workspaces, orgId]);
const providers = useMemo(() => snapshot.providers.filter((provider) => provider.orgId === orgId), [snapshot.providers, orgId]);
const taskRuns = useMemo(() => snapshot.taskRuns.filter((run) => run.orgId === orgId), [snapshot.taskRuns, orgId]);
const chatSpaces = useMemo(() => snapshot.chatSpaces.filter((space) => space.orgId === orgId), [snapshot.chatSpaces, orgId]);
const advance = (task: TaskRecord) => {
const next = nextStatus(task.status);
@@ -93,6 +95,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
{tasks.length} {tasks.length === 1 ? "task" : "tasks"} · {assigneeCount} {assigneeCount === 1 ? "assignee" : "assignees"}
</p>
<ProjectOverview projects={projects} tasks={allTasks} agents={agents} workspaces={workspaces} chatSpaces={chatSpaces} />
{view === "board" && (
<BoardView
tasks={tasks}
@@ -0,0 +1,72 @@
import { Avatar, Pill } from "../../shared/ui";
import { TASK_STATUS, type AgentRecord, type ChatSpaceRecord, type ProjectRecord, type TaskRecord, type WorkspaceRecord } from "../../../../shared/domain";
export function ProjectOverview({
projects,
tasks,
agents,
workspaces,
chatSpaces,
}: {
projects: ProjectRecord[];
tasks: TaskRecord[];
agents: AgentRecord[];
workspaces: WorkspaceRecord[];
chatSpaces: ChatSpaceRecord[];
}) {
if (projects.length === 0) return null;
return (
<section style={{ marginBottom: 18 }}>
<div className="section-label" style={{ marginBottom: 10 }}>Projects</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: 10 }}>
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
tasks={tasks.filter((task) => task.projectId === project.id)}
lead={agents.find((agent) => agent.id === project.leadAgentId) ?? null}
workspace={workspaces.find((item) => item.projectId === project.id) ?? null}
room={chatSpaces.find((space) => space.spaceType === "project" && space.topic === project.name) ?? null}
/>
))}
</div>
</section>
);
}
function ProjectCard({
project,
tasks,
lead,
workspace,
room,
}: {
project: ProjectRecord;
tasks: TaskRecord[];
lead: AgentRecord | null;
workspace: WorkspaceRecord | null;
room: ChatSpaceRecord | null;
}) {
const done = tasks.filter((task) => task.status === TASK_STATUS.DONE).length;
return (
<article className="card" style={{ padding: 14, minWidth: 0 }}>
<div className="row" style={{ alignItems: "flex-start", gap: 10 }}>
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{ fontWeight: 560, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{project.name}</div>
<div className="row" style={{ marginTop: 8, gap: 6, flexWrap: "wrap" }}>
<Pill>{project.status.replace(/_/g, " ")}</Pill>
<Pill>{done} / {tasks.length} tasks</Pill>
{workspace && <Pill>{workspace.name}</Pill>}
{room && <Pill>#{room.name}</Pill>}
</div>
</div>
{lead && (
<div className="row" style={{ gap: 6, flexShrink: 0 }}>
<Avatar name={lead.name} size="sm" bot />
<span style={{ fontSize: 12, color: "var(--text-2)" }}>{lead.name}</span>
</div>
)}
</div>
</article>
);
}