perf(renderer): memoize kanban task card actions
This commit is contained in:
parent
93b53dbeb2
commit
482105a94c
1 changed files with 225 additions and 147 deletions
|
|
@ -266,6 +266,208 @@ const TaskActionIconButton = ({
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface TaskMetaActionsProps {
|
||||||
|
taskId: string;
|
||||||
|
unreadCount: number;
|
||||||
|
commentCount: number;
|
||||||
|
pulseKey: number;
|
||||||
|
canOpenChanges: boolean;
|
||||||
|
changesNeedAttention: boolean;
|
||||||
|
onViewChanges?: (taskId: string) => void;
|
||||||
|
onDeleteTask?: (taskId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskMetaActions = memo(function TaskMetaActions({
|
||||||
|
taskId,
|
||||||
|
unreadCount,
|
||||||
|
commentCount,
|
||||||
|
pulseKey,
|
||||||
|
canOpenChanges,
|
||||||
|
changesNeedAttention,
|
||||||
|
onViewChanges,
|
||||||
|
onDeleteTask,
|
||||||
|
}: TaskMetaActionsProps): React.JSX.Element {
|
||||||
|
const { t } = useAppTranslation('team');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{canOpenChanges && onViewChanges ? (
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={
|
||||||
|
changesNeedAttention
|
||||||
|
? t('kanban.taskCard.changesNeedAttention')
|
||||||
|
: t('kanban.taskCard.changes')
|
||||||
|
}
|
||||||
|
icon={<FileCode className="size-2.5" />}
|
||||||
|
variant="ghost"
|
||||||
|
className={
|
||||||
|
changesNeedAttention
|
||||||
|
? 'text-amber-400 hover:bg-amber-500/10 hover:text-amber-300'
|
||||||
|
: 'text-sky-400 hover:bg-sky-500/10 hover:text-sky-300'
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onViewChanges(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<UnreadCommentsBadge
|
||||||
|
unreadCount={unreadCount}
|
||||||
|
totalCount={commentCount}
|
||||||
|
pulseKey={pulseKey}
|
||||||
|
/>
|
||||||
|
{onDeleteTask ? (
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.deleteTask')}
|
||||||
|
icon={<Trash2 size={11} />}
|
||||||
|
variant="ghost"
|
||||||
|
className="text-red-400 hover:bg-red-500/10 hover:text-red-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteTask(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface TaskPrimaryActionsProps {
|
||||||
|
taskId: string;
|
||||||
|
columnId: KanbanColumnId;
|
||||||
|
isReviewManual: boolean;
|
||||||
|
onRequestReview: (taskId: string) => void;
|
||||||
|
onApprove: (taskId: string) => void;
|
||||||
|
onRequestChanges: (taskId: string) => void;
|
||||||
|
onMoveBackToDone: (taskId: string) => void;
|
||||||
|
onStartTask: (taskId: string) => void;
|
||||||
|
onCompleteTask: (taskId: string) => void;
|
||||||
|
onCancelTask: (taskId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskPrimaryActions = memo(function TaskPrimaryActions({
|
||||||
|
taskId,
|
||||||
|
columnId,
|
||||||
|
isReviewManual,
|
||||||
|
onRequestReview,
|
||||||
|
onApprove,
|
||||||
|
onRequestChanges,
|
||||||
|
onMoveBackToDone,
|
||||||
|
onStartTask,
|
||||||
|
onCompleteTask,
|
||||||
|
onCancelTask,
|
||||||
|
}: TaskPrimaryActionsProps): React.JSX.Element {
|
||||||
|
const { t } = useAppTranslation('team');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-0 flex-nowrap gap-2">
|
||||||
|
{columnId === 'todo' ? (
|
||||||
|
<>
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.start')}
|
||||||
|
icon={<Play size={11} />}
|
||||||
|
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStartTask(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.complete')}
|
||||||
|
icon={<CheckCircle2 size={11} />}
|
||||||
|
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCompleteTask(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{columnId === 'in_progress' ? (
|
||||||
|
<>
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.complete')}
|
||||||
|
icon={<CheckCircle2 size={11} />}
|
||||||
|
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCompleteTask(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CancelTaskButton taskId={taskId} onConfirm={onCancelTask} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{columnId === 'done' ? (
|
||||||
|
<>
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.approve')}
|
||||||
|
icon={<CheckCircle2 size={11} />}
|
||||||
|
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onApprove(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.requestReview')}
|
||||||
|
icon={<Eye size={11} />}
|
||||||
|
className="border-violet-500/40 text-violet-400 hover:bg-violet-500/10 hover:text-violet-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRequestReview(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{columnId === 'review' ? (
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||||
|
{isReviewManual ? (
|
||||||
|
<div className="whitespace-nowrap text-[11px] text-[var(--color-text-muted)]">
|
||||||
|
{t('kanban.taskCard.manualReview')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.approve')}
|
||||||
|
icon={<CheckCircle2 size={11} />}
|
||||||
|
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onApprove(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TaskActionIconButton
|
||||||
|
label={t('kanban.taskCard.requestChanges')}
|
||||||
|
icon={<FilePenLine size={11} />}
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-red-500/90 text-white hover:bg-red-500"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRequestChanges(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{columnId === 'approved' ? (
|
||||||
|
<TaskActionIconButton
|
||||||
|
label="Disapprove"
|
||||||
|
icon={<RotateCcw size={11} />}
|
||||||
|
className="border-amber-500/40 text-amber-400 hover:bg-amber-500/10 hover:text-amber-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onMoveBackToDone(taskId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const KanbanTaskCard = memo(
|
export const KanbanTaskCard = memo(
|
||||||
function KanbanTaskCard({
|
function KanbanTaskCard({
|
||||||
task,
|
task,
|
||||||
|
|
@ -326,48 +528,6 @@ export const KanbanTaskCard = memo(
|
||||||
syncCommentPulse({ taskKey: commentPulseTaskKey, comments });
|
syncCommentPulse({ taskKey: commentPulseTaskKey, comments });
|
||||||
}, [commentCount, commentPulseTaskKey, comments]);
|
}, [commentCount, commentPulseTaskKey, comments]);
|
||||||
|
|
||||||
const metaActions = (
|
|
||||||
<>
|
|
||||||
{canOpenChanges ? (
|
|
||||||
<TaskActionIconButton
|
|
||||||
label={
|
|
||||||
changesNeedAttention
|
|
||||||
? t('kanban.taskCard.changesNeedAttention')
|
|
||||||
: t('kanban.taskCard.changes')
|
|
||||||
}
|
|
||||||
icon={<FileCode className="size-2.5" />}
|
|
||||||
variant="ghost"
|
|
||||||
className={
|
|
||||||
changesNeedAttention
|
|
||||||
? 'text-amber-400 hover:bg-amber-500/10 hover:text-amber-300'
|
|
||||||
: 'text-sky-400 hover:bg-sky-500/10 hover:text-sky-300'
|
|
||||||
}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onViewChanges!(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<UnreadCommentsBadge
|
|
||||||
unreadCount={unreadCount}
|
|
||||||
totalCount={commentCount}
|
|
||||||
pulseKey={visibleCommentPulseKey}
|
|
||||||
/>
|
|
||||||
{onDeleteTask ? (
|
|
||||||
<TaskActionIconButton
|
|
||||||
label={t('kanban.taskCard.deleteTask')}
|
|
||||||
icon={<Trash2 size={11} />}
|
|
||||||
variant="ghost"
|
|
||||||
className="text-red-400 hover:bg-red-500/10 hover:text-red-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDeleteTask(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-task-id={task.id}
|
data-task-id={task.id}
|
||||||
|
|
@ -460,113 +620,31 @@ export const KanbanTaskCard = memo(
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex min-w-0 flex-nowrap gap-2">
|
<TaskPrimaryActions
|
||||||
{columnId === 'todo' ? (
|
taskId={task.id}
|
||||||
<>
|
columnId={columnId}
|
||||||
<TaskActionIconButton
|
isReviewManual={isReviewManual}
|
||||||
label={t('kanban.taskCard.start')}
|
onRequestReview={onRequestReview}
|
||||||
icon={<Play size={11} />}
|
onApprove={onApprove}
|
||||||
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
onRequestChanges={onRequestChanges}
|
||||||
onClick={(e) => {
|
onMoveBackToDone={onMoveBackToDone}
|
||||||
e.stopPropagation();
|
onStartTask={onStartTask}
|
||||||
onStartTask(task.id);
|
onCompleteTask={onCompleteTask}
|
||||||
}}
|
onCancelTask={onCancelTask}
|
||||||
/>
|
/>
|
||||||
<TaskActionIconButton
|
|
||||||
label={t('kanban.taskCard.complete')}
|
|
||||||
icon={<CheckCircle2 size={11} />}
|
|
||||||
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onCompleteTask(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{columnId === 'in_progress' ? (
|
<div className="flex shrink-0 flex-nowrap items-center gap-1.5">
|
||||||
<>
|
<TaskMetaActions
|
||||||
<TaskActionIconButton
|
taskId={task.id}
|
||||||
label={t('kanban.taskCard.complete')}
|
unreadCount={unreadCount}
|
||||||
icon={<CheckCircle2 size={11} />}
|
commentCount={commentCount}
|
||||||
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
pulseKey={visibleCommentPulseKey}
|
||||||
onClick={(e) => {
|
canOpenChanges={canOpenChanges}
|
||||||
e.stopPropagation();
|
changesNeedAttention={changesNeedAttention}
|
||||||
onCompleteTask(task.id);
|
onViewChanges={onViewChanges}
|
||||||
}}
|
onDeleteTask={onDeleteTask}
|
||||||
/>
|
/>
|
||||||
<CancelTaskButton taskId={task.id} onConfirm={onCancelTask} />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{columnId === 'done' ? (
|
|
||||||
<>
|
|
||||||
<TaskActionIconButton
|
|
||||||
label={t('kanban.taskCard.approve')}
|
|
||||||
icon={<CheckCircle2 size={11} />}
|
|
||||||
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onApprove(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TaskActionIconButton
|
|
||||||
label={t('kanban.taskCard.requestReview')}
|
|
||||||
icon={<Eye size={11} />}
|
|
||||||
className="border-violet-500/40 text-violet-400 hover:bg-violet-500/10 hover:text-violet-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRequestReview(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{columnId === 'review' ? (
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
||||||
{isReviewManual ? (
|
|
||||||
<div className="whitespace-nowrap text-[11px] text-[var(--color-text-muted)]">
|
|
||||||
{t('kanban.taskCard.manualReview')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<TaskActionIconButton
|
|
||||||
label={t('kanban.taskCard.approve')}
|
|
||||||
icon={<CheckCircle2 size={11} />}
|
|
||||||
className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onApprove(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TaskActionIconButton
|
|
||||||
label={t('kanban.taskCard.requestChanges')}
|
|
||||||
icon={<FilePenLine size={11} />}
|
|
||||||
variant="destructive"
|
|
||||||
className="bg-red-500/90 text-white hover:bg-red-500"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRequestChanges(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{columnId === 'approved' ? (
|
|
||||||
<TaskActionIconButton
|
|
||||||
label="Disapprove"
|
|
||||||
icon={<RotateCcw size={11} />}
|
|
||||||
className="border-amber-500/40 text-amber-400 hover:bg-amber-500/10 hover:text-amber-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onMoveBackToDone(task.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 flex-nowrap items-center gap-1.5">{metaActions}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue