perf(renderer): memoize member card row controls
This commit is contained in:
parent
a5444f3240
commit
64d2bae98d
1 changed files with 120 additions and 68 deletions
|
|
@ -567,6 +567,118 @@ const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface MemberActionButtonProps {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberActionButton = memo(function MemberActionButton({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}: MemberActionButtonProps): React.JSX.Element {
|
||||||
|
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip open={tooltipOpen} onOpenChange={setTooltipOpen}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{tooltipOpen ? <TooltipContent side="bottom">{label}</TooltipContent> : null}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface MemberQuickActionsProps {
|
||||||
|
onSendMessage?: () => void;
|
||||||
|
onAssignTask?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberQuickActions = memo(function MemberQuickActions({
|
||||||
|
onSendMessage,
|
||||||
|
onAssignTask,
|
||||||
|
}: MemberQuickActionsProps): React.JSX.Element {
|
||||||
|
const { t } = useAppTranslation('team');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex shrink-0 items-center gap-0.5">
|
||||||
|
<MemberActionButton label={t('members.actions.sendMessage')} onClick={onSendMessage}>
|
||||||
|
<MessageSquare size={13} />
|
||||||
|
</MemberActionButton>
|
||||||
|
<MemberActionButton label={t('members.actions.assignTask')} onClick={onAssignTask}>
|
||||||
|
<Plus size={13} />
|
||||||
|
</MemberActionButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface MemberTaskProgressBadgeProps {
|
||||||
|
showStartingSkeleton: boolean;
|
||||||
|
memberTaskCount: number;
|
||||||
|
completed: number;
|
||||||
|
totalTasks: number;
|
||||||
|
progressPercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberTaskProgressBadge = memo(function MemberTaskProgressBadge({
|
||||||
|
showStartingSkeleton,
|
||||||
|
memberTaskCount,
|
||||||
|
completed,
|
||||||
|
totalTasks,
|
||||||
|
progressPercent,
|
||||||
|
}: MemberTaskProgressBadgeProps): React.JSX.Element {
|
||||||
|
if (showStartingSkeleton) {
|
||||||
|
return (
|
||||||
|
<div className="shrink-0" aria-hidden="true">
|
||||||
|
<div
|
||||||
|
className="skeleton-shimmer h-[18px] w-[62px] rounded-full border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--skeleton-base-dim)',
|
||||||
|
borderColor: 'var(--color-border)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="skeleton-shimmer mx-1 mt-1 h-[2px] w-10 rounded-full"
|
||||||
|
style={{ backgroundColor: 'var(--skeleton-base)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="shrink-0"
|
||||||
|
title={totalTasks > 0 ? `${completed}/${totalTasks} completed` : undefined}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none"
|
||||||
|
>
|
||||||
|
{memberTaskCount} {memberTaskCount === 1 ? 'task' : 'tasks'}
|
||||||
|
</Badge>
|
||||||
|
{totalTasks > 0 && (
|
||||||
|
<div className="mx-0.5 mt-0.5 h-[2px] rounded-full bg-[var(--color-border)]">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-emerald-500 transition-all duration-500"
|
||||||
|
style={{ width: `${progressPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* NOTE: lead context bar disabled - usage formula is inaccurate */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const MemberCard = memo(function MemberCard({
|
export const MemberCard = memo(function MemberCard({
|
||||||
teamName,
|
teamName,
|
||||||
member,
|
member,
|
||||||
|
|
@ -1416,75 +1528,15 @@ export const MemberCard = memo(function MemberCard({
|
||||||
{isRemoved ? 'removed' : displayPresenceLabel}
|
{isRemoved ? 'removed' : displayPresenceLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{showStartingSkeleton ? (
|
<MemberTaskProgressBadge
|
||||||
<div className="shrink-0" aria-hidden="true">
|
showStartingSkeleton={showStartingSkeleton}
|
||||||
<div
|
memberTaskCount={member.taskCount}
|
||||||
className="skeleton-shimmer h-[18px] w-[62px] rounded-full border"
|
completed={completed}
|
||||||
style={{
|
totalTasks={totalTasks}
|
||||||
backgroundColor: 'var(--skeleton-base-dim)',
|
progressPercent={progressPercent}
|
||||||
borderColor: 'var(--color-border)',
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="skeleton-shimmer mx-1 mt-1 h-[2px] w-10 rounded-full"
|
|
||||||
style={{ backgroundColor: 'var(--skeleton-base)' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="shrink-0"
|
|
||||||
title={totalTasks > 0 ? `${completed}/${totalTasks} completed` : undefined}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none"
|
|
||||||
>
|
|
||||||
{member.taskCount} {member.taskCount === 1 ? 'task' : 'tasks'}
|
|
||||||
</Badge>
|
|
||||||
{totalTasks > 0 && (
|
|
||||||
<div className="mx-0.5 mt-0.5 h-[2px] rounded-full bg-[var(--color-border)]">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full bg-emerald-500 transition-all duration-500"
|
|
||||||
style={{ width: `${progressPercent}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* NOTE: lead context bar disabled — usage formula is inaccurate */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isRemoved && (
|
{!isRemoved && (
|
||||||
<div className="flex shrink-0 items-center gap-0.5">
|
<MemberQuickActions onSendMessage={onSendMessage} onAssignTask={onAssignTask} />
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSendMessage?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MessageSquare size={13} />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">{t('members.actions.sendMessage')}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onAssignTask?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus size={13} />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">{t('members.actions.assignTask')}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{canRestoreMember ? (
|
{canRestoreMember ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue