refactor(frontend): align request person chips

This commit is contained in:
lizerk 2026-04-13 15:53:35 +03:00
parent 4127e65c4f
commit 192140c95d
4 changed files with 34 additions and 17 deletions

View File

@ -19,6 +19,7 @@ import {
type Dispatch,
type SetStateAction,
} from "react";
import { RequestUserBadge } from "./RequestUserBadge";
type RequestComposeModalProps = {
actionError: string | null;
@ -70,7 +71,7 @@ export function RequestComposeModal({
const selectableEmployees = useMemo(
() => employees
.filter((employee) => !currentUserId || employee.id !== currentUserId)
.map((employee) => ({ id: employee.id, name: displayUserName(employee) })),
.map((employee) => ({ id: employee.id, name: displayUserName(employee), user: employee })),
[currentUserId, employees],
);
@ -312,6 +313,13 @@ export function RequestComposeModal({
items={selectableEmployees}
selectedIds={form.recipient_ids}
onToggle={(id) => toggleSelection("recipient_ids", id)}
renderSelectedItem={(item) => (
<RequestUserBadge
person={item.user}
currentUserId={currentUserId}
disableLink
/>
)}
/>
{showCcField && (
<SearchableSelectMulti
@ -321,6 +329,13 @@ export function RequestComposeModal({
items={selectableEmployees}
selectedIds={form.cc_user_ids}
onToggle={(id) => toggleSelection("cc_user_ids", id)}
renderSelectedItem={(item) => (
<RequestUserBadge
person={item.user}
currentUserId={currentUserId}
disableLink
/>
)}
/>
)}
</div>

View File

@ -53,16 +53,7 @@ type RequestListItemProps = {
function renderDecisionMaker(decisionMaker: User | undefined, currentUserId?: number | null) {
if (!decisionMaker) return <span className="app-text-muted">—</span>;
const decisionMakerLink = userProfileLink(decisionMaker, currentUserId);
const decisionMakerName = displayUserName(decisionMaker);
return decisionMakerLink ? (
<Link href={decisionMakerLink} className="app-link-accent font-medium">
{decisionMakerName}
</Link>
) : (
<span className="font-medium text-[var(--foreground)]">{decisionMakerName}</span>
);
return <RequestUserBadge person={decisionMaker} currentUserId={currentUserId} />;
}
export function RequestListItem({

View File

@ -5,12 +5,14 @@ import { RequestAvatar } from "./RequestAvatar";
type RequestUserBadgeProps = {
currentUserId?: number | null;
disableLink?: boolean;
large?: boolean;
person: User;
};
export function RequestUserBadge({
currentUserId,
disableLink = false,
large = false,
person,
}: RequestUserBadgeProps) {
@ -31,7 +33,7 @@ export function RequestUserBadge({
const className = `app-badge inline-flex max-w-full items-center gap-2 rounded-full ${large ? "px-3 py-1.5 text-sm" : "px-2.5 py-1 text-xs"} font-medium`;
return personLink
return personLink && !disableLink
? <Link href={personLink} className={`${className} hover:bg-[var(--surface-tertiary)]`}>{chip}</Link>
: <span className={className}>{chip}</span>;
}

View File

@ -1,6 +1,7 @@
"use client";
import { ChevronDown } from "lucide-react";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
/* ── Single-select dropdown with search ── */
@ -91,7 +92,7 @@ export function SearchableSelectSingle<T extends string | number>({
}
/* ── Multi-select dropdown with search ── */
export function SearchableSelectMulti({
export function SearchableSelectMulti<T extends { id: number; name: string }>({
label,
items,
selectedIds,
@ -99,14 +100,16 @@ export function SearchableSelectMulti({
placeholder,
layout = "stacked",
className = "",
renderSelectedItem,
}: {
label: string;
items: { id: number; name: string }[];
items: T[];
selectedIds: number[];
onToggle: (id: number) => void;
placeholder?: string;
layout?: "stacked" | "inline";
className?: string;
renderSelectedItem?: (item: T) => ReactNode;
}) {
const [open, setOpen] = useState(false);
const [q, setQ] = useState("");
@ -140,9 +143,15 @@ export function SearchableSelectMulti({
<span className="app-text-muted w-14 shrink-0 pt-0.5 text-sm font-medium">{label}</span>
<span className="flex min-w-0 flex-1 flex-wrap gap-1.5">
{selectedItems.length > 0 ? selectedItems.map((item) => (
<span key={item.id} className="app-badge inline-flex max-w-full items-center rounded-full px-2.5 py-1 text-xs font-medium">
<span className="truncate">{item.name}</span>
</span>
renderSelectedItem ? (
<span key={item.id} className="min-w-0 max-w-full">
{renderSelectedItem(item)}
</span>
) : (
<span key={item.id} className="app-badge inline-flex max-w-full items-center rounded-full px-2.5 py-1 text-xs font-medium">
<span className="truncate">{item.name}</span>
</span>
)
)) : <span className="app-text-muted py-0.5">{placeholder || "Выбрать..."}</span>}
</span>
</>