- 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>
144 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|