multica/apps/web/features/issues/components/pickers/assignee-picker.tsx
Naiyuan Qing 2cf088ddf6 feat: resizable sidebar, issue detail rewrite, package consolidation
- Add drag-to-resize sidebar with localStorage persistence
- Rewrite issue detail page with Tiptap rich text editor, due date picker, acceptance criteria
- Redesign create-issue modal with pill-based property toolbar and expand/collapse
- Consolidate @multica/sdk and @multica/types into apps/web/shared/
- Simplify auth: remove verification codes, PATs, email service (dev-only login)
- Add 401 unauthorized handler to redirect expired sessions to login
- Fix due date format to send full RFC3339 timestamps
- Increase description editor debounce to 1500ms
- Remove arbitrary Tailwind values in create-issue modal
- Renumber migrations (inbox_actor 012→009), remove unused migrations
- UI polish across agents, settings, inbox, knowledge-base pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:47:04 +08:00

144 lines
4.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { Bot, UserMinus } from "lucide-react";
import type { IssueAssigneeType, UpdateIssueRequest } from "@/shared/types";
import { useWorkspaceStore, useActorName } from "@/features/workspace";
import {
PropertyPicker,
PickerItem,
PickerSection,
PickerEmpty,
} from "./property-picker";
export function AssigneePicker({
assigneeType,
assigneeId,
onUpdate,
}: {
assigneeType: IssueAssigneeType | null;
assigneeId: string | null;
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
}) {
const [open, setOpen] = useState(false);
const [filter, setFilter] = useState("");
const members = useWorkspaceStore((s) => s.members);
const agents = useWorkspaceStore((s) => s.agents);
const { getActorName, getActorInitials } = useActorName();
const query = filter.toLowerCase();
const filteredMembers = members.filter((m) =>
m.name.toLowerCase().includes(query),
);
const filteredAgents = agents.filter((a) =>
a.name.toLowerCase().includes(query),
);
const isSelected = (type: string, id: string) =>
assigneeType === type && assigneeId === id;
const triggerLabel =
assigneeType && assigneeId
? getActorName(assigneeType, assigneeId)
: "Unassigned";
return (
<PropertyPicker
open={open}
onOpenChange={(v: boolean) => {
setOpen(v);
if (!v) setFilter("");
}}
width="w-52"
searchable
searchPlaceholder="Assign to..."
onSearchChange={setFilter}
trigger={
assigneeType && assigneeId ? (
<>
<div
className={`inline-flex shrink-0 items-center justify-center rounded-full font-medium text-[8px] size-4.5 ${
assigneeType === "agent"
? "bg-info/10 text-info"
: "bg-muted text-muted-foreground"
}`}
>
{assigneeType === "agent" ? (
<Bot className="size-2.5" />
) : (
getActorInitials(assigneeType, assigneeId)
)}
</div>
<span className="truncate">{triggerLabel}</span>
</>
) : (
<span className="text-muted-foreground">Unassigned</span>
)
}
>
{/* Unassigned option */}
<PickerItem
selected={!assigneeType && !assigneeId}
onClick={() => {
onUpdate({ assignee_type: null, assignee_id: null });
setOpen(false);
}}
>
<UserMinus className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-muted-foreground">Unassigned</span>
</PickerItem>
{/* Members */}
{filteredMembers.length > 0 && (
<PickerSection label="Members">
{filteredMembers.map((m) => (
<PickerItem
key={m.user_id}
selected={isSelected("member", m.user_id)}
onClick={() => {
onUpdate({
assignee_type: "member",
assignee_id: m.user_id,
});
setOpen(false);
}}
>
<div className="inline-flex size-4.5 shrink-0 items-center justify-center rounded-full bg-muted text-[8px] font-medium text-muted-foreground">
{getActorInitials("member", m.user_id)}
</div>
<span>{m.name}</span>
</PickerItem>
))}
</PickerSection>
)}
{/* Agents */}
{filteredAgents.length > 0 && (
<PickerSection label="Agents">
{filteredAgents.map((a) => (
<PickerItem
key={a.id}
selected={isSelected("agent", a.id)}
onClick={() => {
onUpdate({
assignee_type: "agent",
assignee_id: a.id,
});
setOpen(false);
}}
>
<div className="inline-flex size-4.5 shrink-0 items-center justify-center rounded-full bg-info/10 text-info">
<Bot className="size-2.5" />
</div>
<span>{a.name}</span>
</PickerItem>
))}
</PickerSection>
)}
{filteredMembers.length === 0 &&
filteredAgents.length === 0 &&
filter && <PickerEmpty />}
</PropertyPicker>
);
}