perf(renderer): skip unchanged project task groups

This commit is contained in:
777genius 2026-05-31 02:36:13 +03:00
parent fd3137237d
commit b6139d8558

View file

@ -401,6 +401,15 @@ const TaskRows = memo(function TaskRows({
);
});
function areTaskReferencesEqual(prev: readonly GlobalTask[], next: readonly GlobalTask[]): boolean {
if (prev === next) return true;
if (prev.length !== next.length) return false;
for (let i = 0; i < prev.length; i += 1) {
if (prev[i] !== next[i]) return false;
}
return true;
}
interface ProjectTaskGroupProps {
group: ProjectTaskGroupData;
isCollapsed: boolean;
@ -427,131 +436,159 @@ interface ProjectTaskGroupProps {
getOwnerColorName: TaskOwnerColorResolver;
}
const ProjectTaskGroup = memo(function ProjectTaskGroup({
group,
isCollapsed,
visibleCount,
noProjectGroupColor,
showMoreLabel,
showLessLabel,
isPinned,
isArchived,
isNewTask,
isTeamOffline,
renamingKey,
formatTeamHeader,
onToggleGroup,
onVisibleCountChange,
onTogglePin,
onToggleArchive,
onMarkUnread,
onRename,
onDelete,
onRenameComplete,
onRenameCancel,
getDisplaySubject,
getOwnerColorName,
}: ProjectTaskGroupProps): React.JSX.Element | null {
if (group.tasks.length === 0) return null;
const ProjectTaskGroup = memo(
function ProjectTaskGroup({
group,
isCollapsed,
visibleCount,
noProjectGroupColor,
showMoreLabel,
showLessLabel,
isPinned,
isArchived,
isNewTask,
isTeamOffline,
renamingKey,
formatTeamHeader,
onToggleGroup,
onVisibleCountChange,
onTogglePin,
onToggleArchive,
onMarkUnread,
onRename,
onDelete,
onRenameComplete,
onRenameCancel,
getDisplaySubject,
getOwnerColorName,
}: ProjectTaskGroupProps): React.JSX.Element | null {
if (group.tasks.length === 0) return null;
const isNoProjectGroup = group.projectKey === NO_PROJECT_KEY;
const groupColor = isNoProjectGroup ? noProjectGroupColor : projectColor(group.projectLabel);
const showMoreVisible = canProjectGroupShowMore(visibleCount, group.tasks.length);
const showLessVisible = canProjectGroupShowLess(visibleCount, group.tasks.length);
const isNoProjectGroup = group.projectKey === NO_PROJECT_KEY;
const groupColor = isNoProjectGroup ? noProjectGroupColor : projectColor(group.projectLabel);
const showMoreVisible = canProjectGroupShowMore(visibleCount, group.tasks.length);
const showLessVisible = canProjectGroupShowLess(visibleCount, group.tasks.length);
return (
<div>
<button
type="button"
onClick={() => onToggleGroup(group.projectKey)}
className="hover:bg-surface-raised/40 sticky top-0 z-10 flex w-full cursor-pointer items-center gap-1.5 p-2 transition-colors"
style={{
backgroundColor: 'var(--color-surface-sidebar)',
backgroundImage: isNoProjectGroup
? undefined
: `linear-gradient(90deg, ${groupColor.glow} 0%, transparent 80%)`,
boxShadow: `inset 2px 0 0 ${groupColor.border}, inset 0 -1px 0 var(--color-border)`,
}}
>
{isCollapsed ? (
<ChevronRight className="size-3 shrink-0 text-text-muted" />
) : (
<ChevronDown className="size-3 shrink-0 text-text-muted" />
)}
<Folder
className="size-3.5 shrink-0"
style={{ color: groupColor.icon }}
aria-hidden="true"
/>
<span
className="truncate text-[11px] font-bold leading-none"
style={{ color: groupColor.icon }}
return (
<div>
<button
type="button"
onClick={() => onToggleGroup(group.projectKey)}
className="hover:bg-surface-raised/40 sticky top-0 z-10 flex w-full cursor-pointer items-center gap-1.5 p-2 transition-colors"
style={{
backgroundColor: 'var(--color-surface-sidebar)',
backgroundImage: isNoProjectGroup
? undefined
: `linear-gradient(90deg, ${groupColor.glow} 0%, transparent 80%)`,
boxShadow: `inset 2px 0 0 ${groupColor.border}, inset 0 -1px 0 var(--color-border)`,
}}
>
{group.projectLabel}
</span>
<span className="ml-auto shrink-0 text-[10px] font-normal text-text-muted">
{group.tasks.length}
</span>
</button>
{!isCollapsed && (
<TaskRows
tasks={group.tasks}
visibleCount={visibleCount}
isPinned={isPinned}
isArchived={isArchived}
isNewTask={isNewTask}
isTeamOffline={isTeamOffline}
hideTeamName
hideProjectName
showTeamHeader
formatTeamHeader={formatTeamHeader}
renamingKey={renamingKey}
onTogglePin={onTogglePin}
onToggleArchive={onToggleArchive}
onMarkUnread={onMarkUnread}
onRename={onRename}
onDelete={onDelete}
onRenameComplete={onRenameComplete}
onRenameCancel={onRenameCancel}
getDisplaySubject={getDisplaySubject}
getOwnerColorName={getOwnerColorName}
/>
)}
{!isCollapsed && (showMoreVisible || showLessVisible) && (
<div className="flex items-center gap-2 px-3 pb-2 pt-1">
{showMoreVisible && (
<button
type="button"
className="text-[11px] font-medium text-text-muted transition-colors hover:text-text"
onClick={() =>
onVisibleCountChange(
group.projectKey,
getNextProjectGroupVisibleCount(visibleCount, group.tasks.length)
)
}
>
{showMoreLabel}
</button>
{isCollapsed ? (
<ChevronRight className="size-3 shrink-0 text-text-muted" />
) : (
<ChevronDown className="size-3 shrink-0 text-text-muted" />
)}
{showLessVisible && (
<button
type="button"
className="text-[11px] font-medium text-text-muted transition-colors hover:text-text"
onClick={() =>
onVisibleCountChange(
group.projectKey,
getPreviousProjectGroupVisibleCount(visibleCount, group.tasks.length)
)
}
>
{showLessLabel}
</button>
)}
</div>
)}
</div>
);
});
<Folder
className="size-3.5 shrink-0"
style={{ color: groupColor.icon }}
aria-hidden="true"
/>
<span
className="truncate text-[11px] font-bold leading-none"
style={{ color: groupColor.icon }}
>
{group.projectLabel}
</span>
<span className="ml-auto shrink-0 text-[10px] font-normal text-text-muted">
{group.tasks.length}
</span>
</button>
{!isCollapsed && (
<TaskRows
tasks={group.tasks}
visibleCount={visibleCount}
isPinned={isPinned}
isArchived={isArchived}
isNewTask={isNewTask}
isTeamOffline={isTeamOffline}
hideTeamName
hideProjectName
showTeamHeader
formatTeamHeader={formatTeamHeader}
renamingKey={renamingKey}
onTogglePin={onTogglePin}
onToggleArchive={onToggleArchive}
onMarkUnread={onMarkUnread}
onRename={onRename}
onDelete={onDelete}
onRenameComplete={onRenameComplete}
onRenameCancel={onRenameCancel}
getDisplaySubject={getDisplaySubject}
getOwnerColorName={getOwnerColorName}
/>
)}
{!isCollapsed && (showMoreVisible || showLessVisible) && (
<div className="flex items-center gap-2 px-3 pb-2 pt-1">
{showMoreVisible && (
<button
type="button"
className="text-[11px] font-medium text-text-muted transition-colors hover:text-text"
onClick={() =>
onVisibleCountChange(
group.projectKey,
getNextProjectGroupVisibleCount(visibleCount, group.tasks.length)
)
}
>
{showMoreLabel}
</button>
)}
{showLessVisible && (
<button
type="button"
className="text-[11px] font-medium text-text-muted transition-colors hover:text-text"
onClick={() =>
onVisibleCountChange(
group.projectKey,
getPreviousProjectGroupVisibleCount(visibleCount, group.tasks.length)
)
}
>
{showLessLabel}
</button>
)}
</div>
)}
</div>
);
},
(prev, next) =>
prev.group.projectKey === next.group.projectKey &&
prev.group.projectLabel === next.group.projectLabel &&
areTaskReferencesEqual(prev.group.tasks, next.group.tasks) &&
prev.isCollapsed === next.isCollapsed &&
prev.visibleCount === next.visibleCount &&
prev.noProjectGroupColor === next.noProjectGroupColor &&
prev.showMoreLabel === next.showMoreLabel &&
prev.showLessLabel === next.showLessLabel &&
prev.isPinned === next.isPinned &&
prev.isArchived === next.isArchived &&
prev.isNewTask === next.isNewTask &&
prev.isTeamOffline === next.isTeamOffline &&
prev.renamingKey === next.renamingKey &&
prev.formatTeamHeader === next.formatTeamHeader &&
prev.onToggleGroup === next.onToggleGroup &&
prev.onVisibleCountChange === next.onVisibleCountChange &&
prev.onTogglePin === next.onTogglePin &&
prev.onToggleArchive === next.onToggleArchive &&
prev.onMarkUnread === next.onMarkUnread &&
prev.onRename === next.onRename &&
prev.onDelete === next.onDelete &&
prev.onRenameComplete === next.onRenameComplete &&
prev.onRenameCancel === next.onRenameCancel &&
prev.getDisplaySubject === next.getDisplaySubject &&
prev.getOwnerColorName === next.getOwnerColorName
);
export const GlobalTaskList = memo(function GlobalTaskList({
hideHeader = false,