From be67ba7c34392cb76df467a296e1784b9892e3ca Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 12 Apr 2026 00:06:17 +0800 Subject: [PATCH] feat(views): support inline property editing on project list page Allow users to modify project priority, status, and lead directly from the project list without navigating to the detail page. Only the project name/icon column navigates to the detail view now. --- .../projects/components/projects-page.tsx | 182 ++++++++++++++---- 1 file changed, 149 insertions(+), 33 deletions(-) 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)} - +
); }