diff --git a/packages/views/projects/components/projects-page.tsx b/packages/views/projects/components/projects-page.tsx index 7c76d057d..1756e4900 100644 --- a/packages/views/projects/components/projects-page.tsx +++ b/packages/views/projects/components/projects-page.tsx @@ -1,10 +1,10 @@ "use client"; -import { useState, useRef } from "react"; -import { Plus, FolderKanban, ChevronRight, Maximize2, Minimize2, X as XIcon, UserMinus } from "lucide-react"; +import { useState, useRef, useCallback } from "react"; +import { Plus, FolderKanban, ChevronRight, Maximize2, Minimize2, X as XIcon, UserMinus, Check } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { projectListOptions } from "@multica/core/projects/queries"; -import { useCreateProject } from "@multica/core/projects/mutations"; +import { useCreateProject, useUpdateProject } from "@multica/core/projects/mutations"; import { PROJECT_STATUS_CONFIG, PROJECT_STATUS_ORDER, PROJECT_PRIORITY_CONFIG, PROJECT_PRIORITY_ORDER } from "@multica/core/projects/config"; import { useWorkspaceId } from "@multica/core/hooks"; import { useWorkspaceStore } from "@multica/core/workspace"; @@ -36,7 +36,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ import { ContentEditor, type ContentEditorRef } from "../../editor"; import { TitleEditor } from "../../editor"; import { EmojiPicker } from "@multica/ui/components/common/emoji-picker"; -import type { Project, ProjectStatus, ProjectPriority } from "@multica/core/types"; +import type { Project, ProjectStatus, ProjectPriority, UpdateProjectRequest } from "@multica/core/types"; import { PriorityIcon } from "../../issues/components/priority-icon"; function formatRelativeDate(date: string): string { @@ -50,32 +50,83 @@ function formatRelativeDate(date: string): string { } function ProjectRow({ project }: { project: Project }) { + const wsId = useWorkspaceId(); const statusCfg = PROJECT_STATUS_CONFIG[project.status]; const priorityCfg = PROJECT_PRIORITY_CONFIG[project.priority]; + const updateProject = useUpdateProject(); + const { data: members = [] } = useQuery(memberListOptions(wsId)); + const { data: agents = [] } = useQuery(agentListOptions(wsId)); + const { getActorName } = useActorName(); + + const [leadOpen, setLeadOpen] = useState(false); + const [leadFilter, setLeadFilter] = useState(""); + const leadQuery = leadFilter.toLowerCase(); + const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(leadQuery)); + const filteredAgents = agents.filter((a) => !a.archived_at && a.name.toLowerCase().includes(leadQuery)); + + const handleUpdate = useCallback( + (data: UpdateProjectRequest) => { + updateProject.mutate({ id: project.id, ...data }); + }, + [project.id, updateProject], + ); + return ( - - {/* Icon + Name */} - {project.icon || "📁"} - {project.title} +
+ {/* Icon + Name (navigates to detail) */} + + {project.icon || "📁"} + {project.title} + - {/* Priority */} - - - {priorityCfg.label} - + {/* Priority — dropdown */} + + + + {priorityCfg.label} + + } + /> + + {PROJECT_PRIORITY_ORDER.map((p) => ( + handleUpdate({ priority: p as ProjectPriority })}> + + {PROJECT_PRIORITY_CONFIG[p].label} + {p === project.priority && } + + ))} + + - {/* Status */} - - {statusCfg.label} - + {/* Status — dropdown */} + + + {statusCfg.label} + + } + /> + + {PROJECT_STATUS_ORDER.map((s) => ( + handleUpdate({ status: s as ProjectStatus })}> + + {PROJECT_STATUS_CONFIG[s].label} + {s === project.status && } + + ))} + + - {/* Progress */} + {/* Progress (read-only) */} {project.issue_count > 0 ? ( <> @@ -94,20 +145,85 @@ function ProjectRow({ project }: { project: Project }) { )} - {/* Lead */} - - {project.lead_type && project.lead_id ? ( - - ) : ( - - )} - + {/* Lead — popover */} + { setLeadOpen(v); if (!v) setLeadFilter(""); }}> + + {project.lead_type && project.lead_id ? ( + + } /> + {getActorName(project.lead_type, project.lead_id)} + + ) : ( + + )} + + } + /> + +
+ setLeadFilter(e.target.value)} + placeholder="Assign lead..." + className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none" + /> +
+
+ + {filteredMembers.length > 0 && ( + <> +
Members
+ {filteredMembers.map((m) => ( + + ))} + + )} + {filteredAgents.length > 0 && ( + <> +
Agents
+ {filteredAgents.map((a) => ( + + ))} + + )} + {filteredMembers.length === 0 && filteredAgents.length === 0 && leadFilter && ( +
No results
+ )} +
+
+ {/* Created */} {formatRelativeDate(project.created_at)} - +
); }