perf(renderer): precompute sidebar task local state

This commit is contained in:
777genius 2026-05-31 07:43:41 +03:00
parent effe9b522f
commit 94c09727f1
3 changed files with 139 additions and 86 deletions

View file

@ -203,15 +203,57 @@ function buildTaskTeamSummary(task: GlobalTask): TeamSummary {
}; };
} }
function buildTaskLocalPresentationKey(task: GlobalTask): string {
return `${task.teamName}:${task.id}`;
}
function buildTaskLocalPresentationState(
task: GlobalTask,
pinnedIds: ReadonlySet<string>,
archivedIds: ReadonlySet<string>,
renamedSubjects: ReadonlyMap<string, string>
): TaskLocalPresentationState {
const key = buildTaskLocalPresentationKey(task);
return {
key,
pinned: pinnedIds.has(key),
archived: archivedIds.has(key),
renamedSubject: renamedSubjects.get(key),
};
}
function buildTaskLocalPresentationByTask(
tasks: readonly GlobalTask[],
pinnedIds: ReadonlySet<string>,
archivedIds: ReadonlySet<string>,
renamedSubjects: ReadonlyMap<string, string>
): WeakMap<GlobalTask, TaskLocalPresentationState> {
const presentationByTask = new WeakMap<GlobalTask, TaskLocalPresentationState>();
for (const task of tasks) {
presentationByTask.set(
task,
buildTaskLocalPresentationState(task, pinnedIds, archivedIds, renamedSubjects)
);
}
return presentationByTask;
}
type TaskRowAction = (teamName: string, taskId: string) => void; type TaskRowAction = (teamName: string, taskId: string) => void;
type TaskRowDeleteAction = (teamName: string, taskId: string) => void | Promise<void>; type TaskRowDeleteAction = (teamName: string, taskId: string) => void | Promise<void>;
type TaskDisplaySubjectResolver = (task: GlobalTask) => string | undefined;
type TaskBooleanResolver = (teamName: string, taskId: string) => boolean;
type TeamBooleanResolver = (teamName: string) => boolean; type TeamBooleanResolver = (teamName: string) => boolean;
type TaskOwnerColorResolver = (task: GlobalTask) => string | null | undefined; type TaskOwnerColorResolver = (task: GlobalTask) => string | null | undefined;
type TeamHeaderFormatter = (teamDisplayName: string) => string; type TeamHeaderFormatter = (teamDisplayName: string) => string;
type ProjectGroupVisibleCountChange = (projectKey: string, visibleCount: number) => void; type ProjectGroupVisibleCountChange = (projectKey: string, visibleCount: number) => void;
type TeamMemberColorInput = Parameters<typeof buildMemberColorMap>[0][number]; type TeamMemberColorInput = Parameters<typeof buildMemberColorMap>[0][number];
interface TaskLocalPresentationState {
key: string;
pinned: boolean;
archived: boolean;
renamedSubject: string | undefined;
}
type TaskLocalPresentationResolver = (task: GlobalTask) => TaskLocalPresentationState;
interface SidebarTeamsDerived { interface SidebarTeamsDerived {
identityKey: string; identityKey: string;
filterTeams: { teamName: string; displayName: string }[]; filterTeams: { teamName: string; displayName: string }[];
@ -340,6 +382,7 @@ function selectSidebarTeamsDerived(teams: readonly TeamSummary[]): SidebarTeamsD
interface GlobalTaskRowProps { interface GlobalTaskRowProps {
task: GlobalTask; task: GlobalTask;
taskLocalKey: string;
isPinned: boolean; isPinned: boolean;
isArchived: boolean; isArchived: boolean;
isNew: boolean; isNew: boolean;
@ -356,7 +399,7 @@ interface GlobalTaskRowProps {
onDelete: TaskRowDeleteAction; onDelete: TaskRowDeleteAction;
onRenameComplete: (teamName: string, taskId: string, newSubject: string) => void; onRenameComplete: (teamName: string, taskId: string, newSubject: string) => void;
onRenameCancel: () => void; onRenameCancel: () => void;
getDisplaySubject: TaskDisplaySubjectResolver; displaySubjectOverride?: string;
ownerColorName?: string | null; ownerColorName?: string | null;
} }
@ -395,14 +438,14 @@ function taskSidebarFieldsEqual(prev: GlobalTask, next: GlobalTask): boolean {
); );
} }
function effectiveRenamingKey(task: GlobalTask, renamingKey: string | null): string | null { function effectiveRenamingKey(taskLocalKey: string, renamingKey: string | null): string | null {
const taskRenamingKey = `${task.teamName}:${task.id}`; return renamingKey === taskLocalKey ? renamingKey : null;
return renamingKey === taskRenamingKey ? renamingKey : null;
} }
const GlobalTaskRow = memo( const GlobalTaskRow = memo(
function GlobalTaskRow({ function GlobalTaskRow({
task, task,
taskLocalKey,
isPinned, isPinned,
isArchived, isArchived,
isNew, isNew,
@ -419,11 +462,10 @@ const GlobalTaskRow = memo(
onDelete, onDelete,
onRenameComplete, onRenameComplete,
onRenameCancel, onRenameCancel,
getDisplaySubject, displaySubjectOverride,
ownerColorName, ownerColorName,
}: GlobalTaskRowProps): React.JSX.Element { }: GlobalTaskRowProps): React.JSX.Element {
const taskRenamingKey = `${task.teamName}:${task.id}`; const rowRenamingKey = effectiveRenamingKey(taskLocalKey, renamingKey);
const effectiveRenamingKey = renamingKey === taskRenamingKey ? renamingKey : null;
const handleTogglePin = useCallback(() => { const handleTogglePin = useCallback(() => {
onTogglePin(task.teamName, task.id); onTogglePin(task.teamName, task.id);
@ -464,10 +506,10 @@ const GlobalTaskRow = memo(
showTeamName={showTeamName} showTeamName={showTeamName}
isLight={isLight} isLight={isLight}
teamOffline={teamOffline} teamOffline={teamOffline}
renamingKey={effectiveRenamingKey} renamingKey={rowRenamingKey}
onRenameComplete={onRenameComplete} onRenameComplete={onRenameComplete}
onRenameCancel={onRenameCancel} onRenameCancel={onRenameCancel}
getDisplaySubject={getDisplaySubject} displaySubjectOverride={displaySubjectOverride}
ownerColorName={ownerColorName} ownerColorName={ownerColorName}
/> />
</AnimatedHeightReveal> </AnimatedHeightReveal>
@ -476,12 +518,13 @@ const GlobalTaskRow = memo(
}, },
(prev, next) => (prev, next) =>
taskSidebarFieldsEqual(prev.task, next.task) && taskSidebarFieldsEqual(prev.task, next.task) &&
prev.taskLocalKey === next.taskLocalKey &&
prev.isPinned === next.isPinned && prev.isPinned === next.isPinned &&
prev.isArchived === next.isArchived && prev.isArchived === next.isArchived &&
prev.isNew === next.isNew && prev.isNew === next.isNew &&
prev.teamOffline === next.teamOffline && prev.teamOffline === next.teamOffline &&
effectiveRenamingKey(prev.task, prev.renamingKey) === effectiveRenamingKey(prev.taskLocalKey, prev.renamingKey) ===
effectiveRenamingKey(next.task, next.renamingKey) && effectiveRenamingKey(next.taskLocalKey, next.renamingKey) &&
prev.hideTeamName === next.hideTeamName && prev.hideTeamName === next.hideTeamName &&
prev.hideProjectName === next.hideProjectName && prev.hideProjectName === next.hideProjectName &&
prev.showTeamName === next.showTeamName && prev.showTeamName === next.showTeamName &&
@ -493,7 +536,7 @@ const GlobalTaskRow = memo(
prev.onDelete === next.onDelete && prev.onDelete === next.onDelete &&
prev.onRenameComplete === next.onRenameComplete && prev.onRenameComplete === next.onRenameComplete &&
prev.onRenameCancel === next.onRenameCancel && prev.onRenameCancel === next.onRenameCancel &&
prev.getDisplaySubject === next.getDisplaySubject && prev.displaySubjectOverride === next.displaySubjectOverride &&
prev.ownerColorName === next.ownerColorName prev.ownerColorName === next.ownerColorName
); );
@ -501,8 +544,7 @@ interface TaskRowsProps {
tasks: GlobalTask[]; tasks: GlobalTask[];
visibleCount?: number; visibleCount?: number;
keyPrefix?: string; keyPrefix?: string;
isPinned: TaskBooleanResolver; getTaskLocalPresentation: TaskLocalPresentationResolver;
isArchived: TaskBooleanResolver;
isNewTask: (task: GlobalTask) => boolean; isNewTask: (task: GlobalTask) => boolean;
isTeamOffline: TeamBooleanResolver; isTeamOffline: TeamBooleanResolver;
renamingKey: string | null; renamingKey: string | null;
@ -521,7 +563,6 @@ interface TaskRowsProps {
onDelete: TaskRowDeleteAction; onDelete: TaskRowDeleteAction;
onRenameComplete: (teamName: string, taskId: string, newSubject: string) => void; onRenameComplete: (teamName: string, taskId: string, newSubject: string) => void;
onRenameCancel: () => void; onRenameCancel: () => void;
getDisplaySubject: TaskDisplaySubjectResolver;
getOwnerColorName: TaskOwnerColorResolver; getOwnerColorName: TaskOwnerColorResolver;
} }
@ -529,13 +570,11 @@ type TaskRowsDerivedProps = Pick<
TaskRowsProps, TaskRowsProps,
| 'tasks' | 'tasks'
| 'visibleCount' | 'visibleCount'
| 'isPinned' | 'getTaskLocalPresentation'
| 'isArchived'
| 'isNewTask' | 'isNewTask'
| 'isTeamOffline' | 'isTeamOffline'
| 'pinnedOverride' | 'pinnedOverride'
| 'archivedOverride' | 'archivedOverride'
| 'getDisplaySubject'
| 'getOwnerColorName' | 'getOwnerColorName'
>; >;
@ -547,14 +586,6 @@ function getTaskRowsVisibleTasks(
: props.tasks; : props.tasks;
} }
function resolveTaskBooleanState(
override: boolean | undefined,
resolver: TaskBooleanResolver,
task: GlobalTask
): boolean {
return override ?? resolver(task.teamName, task.id);
}
function areTaskRowsDerivedValuesEqual( function areTaskRowsDerivedValuesEqual(
prev: TaskRowsDerivedProps, prev: TaskRowsDerivedProps,
next: TaskRowsDerivedProps next: TaskRowsDerivedProps
@ -571,14 +602,16 @@ function areTaskRowsDerivedValuesEqual(
if (!prevTask || !nextTask) { if (!prevTask || !nextTask) {
return false; return false;
} }
const prevLocalPresentation = prev.getTaskLocalPresentation(prevTask);
const nextLocalPresentation = next.getTaskLocalPresentation(nextTask);
if ( if (
resolveTaskBooleanState(prev.pinnedOverride, prev.isPinned, prevTask) !== (prev.pinnedOverride ?? prevLocalPresentation.pinned) !==
resolveTaskBooleanState(next.pinnedOverride, next.isPinned, nextTask) || (next.pinnedOverride ?? nextLocalPresentation.pinned) ||
resolveTaskBooleanState(prev.archivedOverride, prev.isArchived, prevTask) !== (prev.archivedOverride ?? prevLocalPresentation.archived) !==
resolveTaskBooleanState(next.archivedOverride, next.isArchived, nextTask) || (next.archivedOverride ?? nextLocalPresentation.archived) ||
prev.isNewTask(prevTask) !== next.isNewTask(nextTask) || prev.isNewTask(prevTask) !== next.isNewTask(nextTask) ||
prev.isTeamOffline(prevTask.teamName) !== next.isTeamOffline(nextTask.teamName) || prev.isTeamOffline(prevTask.teamName) !== next.isTeamOffline(nextTask.teamName) ||
prev.getDisplaySubject(prevTask) !== next.getDisplaySubject(nextTask) || prevLocalPresentation.renamedSubject !== nextLocalPresentation.renamedSubject ||
prev.getOwnerColorName(prevTask) !== next.getOwnerColorName(nextTask) prev.getOwnerColorName(prevTask) !== next.getOwnerColorName(nextTask)
) { ) {
return false; return false;
@ -592,8 +625,7 @@ const TaskRows = memo(function TaskRows({
tasks, tasks,
visibleCount, visibleCount,
keyPrefix = '', keyPrefix = '',
isPinned, getTaskLocalPresentation,
isArchived,
isNewTask, isNewTask,
isTeamOffline, isTeamOffline,
renamingKey, renamingKey,
@ -612,7 +644,6 @@ const TaskRows = memo(function TaskRows({
onDelete, onDelete,
onRenameComplete, onRenameComplete,
onRenameCancel, onRenameCancel,
getDisplaySubject,
getOwnerColorName, getOwnerColorName,
}: TaskRowsProps): React.JSX.Element { }: TaskRowsProps): React.JSX.Element {
let lastTeam: string | null = null; let lastTeam: string | null = null;
@ -621,13 +652,15 @@ const TaskRows = memo(function TaskRows({
return ( return (
<> <>
{visibleTasks.map((task) => { {visibleTasks.map((task) => {
const taskLocalPresentation = getTaskLocalPresentation(task);
const taskKey = `${keyPrefix}${task.teamName}-${task.id}`; const taskKey = `${keyPrefix}${task.teamName}-${task.id}`;
const row = ( const row = (
<GlobalTaskRow <GlobalTaskRow
key={taskKey} key={taskKey}
task={task} task={task}
isPinned={pinnedOverride ?? isPinned(task.teamName, task.id)} taskLocalKey={taskLocalPresentation.key}
isArchived={archivedOverride ?? isArchived(task.teamName, task.id)} isPinned={pinnedOverride ?? taskLocalPresentation.pinned}
isArchived={archivedOverride ?? taskLocalPresentation.archived}
isNew={isNewTask(task)} isNew={isNewTask(task)}
hideTeamName={hideTeamName} hideTeamName={hideTeamName}
hideProjectName={hideProjectName} hideProjectName={hideProjectName}
@ -643,7 +676,7 @@ const TaskRows = memo(function TaskRows({
onDelete={onDelete} onDelete={onDelete}
onRenameComplete={onRenameComplete} onRenameComplete={onRenameComplete}
onRenameCancel={onRenameCancel} onRenameCancel={onRenameCancel}
getDisplaySubject={getDisplaySubject} displaySubjectOverride={taskLocalPresentation.renamedSubject}
/> />
); );
@ -714,8 +747,7 @@ interface ProjectTaskGroupProps {
noProjectGroupColor: ReturnType<typeof projectColor>; noProjectGroupColor: ReturnType<typeof projectColor>;
showMoreLabel: string; showMoreLabel: string;
showLessLabel: string; showLessLabel: string;
isPinned: TaskBooleanResolver; getTaskLocalPresentation: TaskLocalPresentationResolver;
isArchived: TaskBooleanResolver;
isNewTask: (task: GlobalTask) => boolean; isNewTask: (task: GlobalTask) => boolean;
isTeamOffline: TeamBooleanResolver; isTeamOffline: TeamBooleanResolver;
renamingKey: string | null; renamingKey: string | null;
@ -730,7 +762,6 @@ interface ProjectTaskGroupProps {
onDelete: TaskRowDeleteAction; onDelete: TaskRowDeleteAction;
onRenameComplete: (teamName: string, taskId: string, newSubject: string) => void; onRenameComplete: (teamName: string, taskId: string, newSubject: string) => void;
onRenameCancel: () => void; onRenameCancel: () => void;
getDisplaySubject: TaskDisplaySubjectResolver;
getOwnerColorName: TaskOwnerColorResolver; getOwnerColorName: TaskOwnerColorResolver;
} }
@ -742,8 +773,7 @@ const ProjectTaskGroup = memo(
noProjectGroupColor, noProjectGroupColor,
showMoreLabel, showMoreLabel,
showLessLabel, showLessLabel,
isPinned, getTaskLocalPresentation,
isArchived,
isNewTask, isNewTask,
isTeamOffline, isTeamOffline,
renamingKey, renamingKey,
@ -758,7 +788,6 @@ const ProjectTaskGroup = memo(
onDelete, onDelete,
onRenameComplete, onRenameComplete,
onRenameCancel, onRenameCancel,
getDisplaySubject,
getOwnerColorName, getOwnerColorName,
}: ProjectTaskGroupProps): React.JSX.Element | null { }: ProjectTaskGroupProps): React.JSX.Element | null {
if (group.tasks.length === 0) return null; if (group.tasks.length === 0) return null;
@ -806,8 +835,7 @@ const ProjectTaskGroup = memo(
<TaskRows <TaskRows
tasks={group.tasks} tasks={group.tasks}
visibleCount={visibleCount} visibleCount={visibleCount}
isPinned={isPinned} getTaskLocalPresentation={getTaskLocalPresentation}
isArchived={isArchived}
isNewTask={isNewTask} isNewTask={isNewTask}
isTeamOffline={isTeamOffline} isTeamOffline={isTeamOffline}
isLight={isLight} isLight={isLight}
@ -823,7 +851,6 @@ const ProjectTaskGroup = memo(
onDelete={onDelete} onDelete={onDelete}
onRenameComplete={onRenameComplete} onRenameComplete={onRenameComplete}
onRenameCancel={onRenameCancel} onRenameCancel={onRenameCancel}
getDisplaySubject={getDisplaySubject}
getOwnerColorName={getOwnerColorName} getOwnerColorName={getOwnerColorName}
/> />
)} )}
@ -887,21 +914,17 @@ const ProjectTaskGroup = memo(
{ {
tasks: prev.group.tasks, tasks: prev.group.tasks,
visibleCount: prev.visibleCount, visibleCount: prev.visibleCount,
isPinned: prev.isPinned, getTaskLocalPresentation: prev.getTaskLocalPresentation,
isArchived: prev.isArchived,
isNewTask: prev.isNewTask, isNewTask: prev.isNewTask,
isTeamOffline: prev.isTeamOffline, isTeamOffline: prev.isTeamOffline,
getDisplaySubject: prev.getDisplaySubject,
getOwnerColorName: prev.getOwnerColorName, getOwnerColorName: prev.getOwnerColorName,
}, },
{ {
tasks: next.group.tasks, tasks: next.group.tasks,
visibleCount: next.visibleCount, visibleCount: next.visibleCount,
isPinned: next.isPinned, getTaskLocalPresentation: next.getTaskLocalPresentation,
isArchived: next.isArchived,
isNewTask: next.isNewTask, isNewTask: next.isNewTask,
isTeamOffline: next.isTeamOffline, isTeamOffline: next.isTeamOffline,
getDisplaySubject: next.getDisplaySubject,
getOwnerColorName: next.getOwnerColorName, getOwnerColorName: next.getOwnerColorName,
} }
) )
@ -984,6 +1007,39 @@ export const GlobalTaskList = memo(function GlobalTaskList({
const taskLocalState = useTaskLocalState(); const taskLocalState = useTaskLocalState();
const electronMode = isElectronMode(); const electronMode = isElectronMode();
const taskLocalPresentationByTask = useMemo(
() =>
buildTaskLocalPresentationByTask(
globalTasks,
taskLocalState.pinnedIds,
taskLocalState.archivedIds,
taskLocalState.renamedSubjects
),
[
globalTasks,
taskLocalState.pinnedIds,
taskLocalState.archivedIds,
taskLocalState.renamedSubjects,
]
);
const getTaskLocalPresentation = useCallback(
(task: GlobalTask): TaskLocalPresentationState =>
taskLocalPresentationByTask.get(task) ??
buildTaskLocalPresentationState(
task,
taskLocalState.pinnedIds,
taskLocalState.archivedIds,
taskLocalState.renamedSubjects
),
[
taskLocalPresentationByTask,
taskLocalState.pinnedIds,
taskLocalState.archivedIds,
taskLocalState.renamedSubjects,
]
);
const provisioningState = useMemo( const provisioningState = useMemo(
() => ({ currentProvisioningRunIdByTeam, provisioningRuns }), () => ({ currentProvisioningRunIdByTeam, provisioningRuns }),
[currentProvisioningRunIdByTeam, provisioningRuns] [currentProvisioningRunIdByTeam, provisioningRuns]
@ -1012,14 +1068,14 @@ export const GlobalTaskList = memo(function GlobalTaskList({
isInitialTaskLoadRef.current = false; isInitialTaskLoadRef.current = false;
for (const t of globalTasks) { for (const t of globalTasks) {
// eslint-disable-next-line react-hooks/refs -- Synchronous diff is required so new rows mount with animate=true. // eslint-disable-next-line react-hooks/refs -- Synchronous diff is required so new rows mount with animate=true.
knownTaskIdsRef.current.add(`${t.teamName}:${t.id}`); knownTaskIdsRef.current.add(buildTaskLocalPresentationKey(t));
} }
return new Set<string>(); return new Set<string>();
} }
const newIds = new Set<string>(); const newIds = new Set<string>();
for (const t of globalTasks) { for (const t of globalTasks) {
const key = `${t.teamName}:${t.id}`; const key = buildTaskLocalPresentationKey(t);
// eslint-disable-next-line react-hooks/refs -- Synchronous diff is required so new rows mount with animate=true. // eslint-disable-next-line react-hooks/refs -- Synchronous diff is required so new rows mount with animate=true.
if (!knownTaskIdsRef.current.has(key)) { if (!knownTaskIdsRef.current.has(key)) {
newIds.add(key); newIds.add(key);
@ -1031,7 +1087,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
}, [globalTasks, globalTasksInitialized]); }, [globalTasks, globalTasksInitialized]);
const isNewTask = useCallback( const isNewTask = useCallback(
(task: GlobalTask): boolean => newTaskIds.has(`${task.teamName}:${task.id}`), (task: GlobalTask): boolean => newTaskIds.has(buildTaskLocalPresentationKey(task)),
[newTaskIds] [newTaskIds]
); );
@ -1189,12 +1245,6 @@ export const GlobalTaskList = memo(function GlobalTaskList({
setRenamingTaskKey(`${teamName}:${taskId}`); setRenamingTaskKey(`${teamName}:${taskId}`);
}, []); }, []);
const getTaskDisplaySubject = useCallback(
(task: GlobalTask): string | undefined =>
taskLocalState.getRenamedSubject(task.teamName, task.id),
[taskLocalState]
);
const handleDeleteTask = useCallback( const handleDeleteTask = useCallback(
async (teamName: string, taskId: string): Promise<void> => { async (teamName: string, taskId: string): Promise<void> => {
const confirmed = await confirm({ const confirmed = await confirm({
@ -1287,8 +1337,8 @@ export const GlobalTaskList = memo(function GlobalTaskList({
// Resolve project filter from filters state // Resolve project filter from filters state
const selectedProjectPath = filters.projectPath; const selectedProjectPath = filters.projectPath;
const hasArchivedTasks = useMemo( const hasArchivedTasks = useMemo(
() => globalTasks.some((t) => taskLocalState.isArchived(t.teamName, t.id)), () => globalTasks.some((t) => getTaskLocalPresentation(t).archived),
[globalTasks, taskLocalState] [globalTasks, getTaskLocalPresentation]
); );
const effectiveShowArchived = showArchived && hasArchivedTasks; const effectiveShowArchived = showArchived && hasArchivedTasks;
@ -1311,9 +1361,9 @@ export const GlobalTaskList = memo(function GlobalTaskList({
result = applySearch(result, searchQuery); result = applySearch(result, searchQuery);
// Archive filtering // Archive filtering
if (effectiveShowArchived) { if (effectiveShowArchived) {
result = result.filter((t) => taskLocalState.isArchived(t.teamName, t.id)); result = result.filter((t) => getTaskLocalPresentation(t).archived);
} else { } else {
result = result.filter((t) => !taskLocalState.isArchived(t.teamName, t.id)); result = result.filter((t) => !getTaskLocalPresentation(t).archived);
} }
return result; return result;
}, [ }, [
@ -1325,17 +1375,17 @@ export const GlobalTaskList = memo(function GlobalTaskList({
searchQuery, searchQuery,
readState, readState,
effectiveShowArchived, effectiveShowArchived,
taskLocalState, getTaskLocalPresentation,
]); ]);
// Split into pinned and normal (non-pinned) tasks // Split into pinned and normal (non-pinned) tasks
const pinnedTasks = useMemo( const pinnedTasks = useMemo(
() => filtered.filter((t) => taskLocalState.isPinned(t.teamName, t.id)), () => filtered.filter((t) => getTaskLocalPresentation(t).pinned),
[filtered, taskLocalState] [filtered, getTaskLocalPresentation]
); );
const normalTasks = useMemo( const normalTasks = useMemo(
() => filtered.filter((t) => !taskLocalState.isPinned(t.teamName, t.id)), () => filtered.filter((t) => !getTaskLocalPresentation(t).pinned),
[filtered, taskLocalState] [filtered, getTaskLocalPresentation]
); );
const sortedPinnedTasks = useMemo(() => sortTasksByFreshness(pinnedTasks), [pinnedTasks]); const sortedPinnedTasks = useMemo(() => sortTasksByFreshness(pinnedTasks), [pinnedTasks]);
@ -1508,8 +1558,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
<TaskRows <TaskRows
tasks={sortedPinnedTasks} tasks={sortedPinnedTasks}
keyPrefix="pinned-" keyPrefix="pinned-"
isPinned={taskLocalState.isPinned} getTaskLocalPresentation={getTaskLocalPresentation}
isArchived={taskLocalState.isArchived}
isNewTask={isNewTask} isNewTask={isNewTask}
isTeamOffline={isTeamOffline} isTeamOffline={isTeamOffline}
isLight={isLight} isLight={isLight}
@ -1524,7 +1573,6 @@ export const GlobalTaskList = memo(function GlobalTaskList({
onDelete={handleDeleteTask} onDelete={handleDeleteTask}
onRenameComplete={handleRenameComplete} onRenameComplete={handleRenameComplete}
onRenameCancel={handleRenameCancel} onRenameCancel={handleRenameCancel}
getDisplaySubject={getTaskDisplaySubject}
getOwnerColorName={getOwnerColorName} getOwnerColorName={getOwnerColorName}
/> />
</div> </div>
@ -1612,8 +1660,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
{groupingMode === 'none' && ( {groupingMode === 'none' && (
<TaskRows <TaskRows
tasks={sortedFlat} tasks={sortedFlat}
isPinned={taskLocalState.isPinned} getTaskLocalPresentation={getTaskLocalPresentation}
isArchived={taskLocalState.isArchived}
isNewTask={isNewTask} isNewTask={isNewTask}
isTeamOffline={isTeamOffline} isTeamOffline={isTeamOffline}
isLight={isLight} isLight={isLight}
@ -1626,7 +1673,6 @@ export const GlobalTaskList = memo(function GlobalTaskList({
onDelete={handleDeleteTask} onDelete={handleDeleteTask}
onRenameComplete={handleRenameComplete} onRenameComplete={handleRenameComplete}
onRenameCancel={handleRenameCancel} onRenameCancel={handleRenameCancel}
getDisplaySubject={getTaskDisplaySubject}
getOwnerColorName={getOwnerColorName} getOwnerColorName={getOwnerColorName}
/> />
)} )}
@ -1646,8 +1692,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
noProjectGroupColor={noProjectGroupColor} noProjectGroupColor={noProjectGroupColor}
showMoreLabel={t('tasksPanel.showMore')} showMoreLabel={t('tasksPanel.showMore')}
showLessLabel={t('tasksPanel.showLess')} showLessLabel={t('tasksPanel.showLess')}
isPinned={taskLocalState.isPinned} getTaskLocalPresentation={getTaskLocalPresentation}
isArchived={taskLocalState.isArchived}
isNewTask={isNewTask} isNewTask={isNewTask}
isTeamOffline={isTeamOffline} isTeamOffline={isTeamOffline}
isLight={isLight} isLight={isLight}
@ -1662,7 +1707,6 @@ export const GlobalTaskList = memo(function GlobalTaskList({
onDelete={handleDeleteTask} onDelete={handleDeleteTask}
onRenameComplete={handleRenameComplete} onRenameComplete={handleRenameComplete}
onRenameCancel={handleRenameCancel} onRenameCancel={handleRenameCancel}
getDisplaySubject={getTaskDisplaySubject}
getOwnerColorName={getOwnerColorName} getOwnerColorName={getOwnerColorName}
/> />
); );
@ -1695,8 +1739,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
{!isGroupCollapsed && ( {!isGroupCollapsed && (
<TaskRows <TaskRows
tasks={tasks} tasks={tasks}
isPinned={taskLocalState.isPinned} getTaskLocalPresentation={getTaskLocalPresentation}
isArchived={taskLocalState.isArchived}
isNewTask={isNewTask} isNewTask={isNewTask}
isTeamOffline={isTeamOffline} isTeamOffline={isTeamOffline}
isLight={isLight} isLight={isLight}
@ -1710,7 +1753,6 @@ export const GlobalTaskList = memo(function GlobalTaskList({
onDelete={handleDeleteTask} onDelete={handleDeleteTask}
onRenameComplete={handleRenameComplete} onRenameComplete={handleRenameComplete}
onRenameCancel={handleRenameCancel} onRenameCancel={handleRenameCancel}
getDisplaySubject={getTaskDisplaySubject}
getOwnerColorName={getOwnerColorName} getOwnerColorName={getOwnerColorName}
/> />
)} )}

View file

@ -81,6 +81,8 @@ interface SidebarTaskItemProps {
onRenameCancel?: () => void; onRenameCancel?: () => void;
/** Returns a custom display subject if the task was renamed locally */ /** Returns a custom display subject if the task was renamed locally */
getDisplaySubject?: (task: GlobalTask) => string | undefined; getDisplaySubject?: (task: GlobalTask) => string | undefined;
/** Precomputed custom display subject from list parents. */
displaySubjectOverride?: string;
ownerColorName?: string | null; ownerColorName?: string | null;
} }
@ -95,6 +97,7 @@ const SidebarTaskItemContent = ({
onRenameComplete, onRenameComplete,
onRenameCancel, onRenameCancel,
getDisplaySubject, getDisplaySubject,
displaySubjectOverride,
ownerColorName, ownerColorName,
}: SidebarTaskItemProps & { isLight: boolean }): React.JSX.Element => { }: SidebarTaskItemProps & { isLight: boolean }): React.JSX.Element => {
const { t } = useAppTranslation('team'); const { t } = useAppTranslation('team');
@ -109,7 +112,7 @@ const SidebarTaskItemContent = ({
const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments); const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments);
const isRenaming = renamingKey === `${task.teamName}:${task.id}`; const isRenaming = renamingKey === `${task.teamName}:${task.id}`;
const displaySubject = getDisplaySubject?.(task) ?? task.subject; const displaySubject = displaySubjectOverride ?? getDisplaySubject?.(task) ?? task.subject;
const [editValue, setEditValue] = useState(displaySubject); const [editValue, setEditValue] = useState(displaySubject);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
// Focus input when rename starts // Focus input when rename starts

View file

@ -37,6 +37,9 @@ const storeState = {} as StoreState;
const toggleCollapsedGroup = vi.fn(); const toggleCollapsedGroup = vi.fn();
const sidebarTaskItemRenderSpy = vi.hoisted(() => vi.fn()); const sidebarTaskItemRenderSpy = vi.hoisted(() => vi.fn());
const taskLocalState = { const taskLocalState = {
pinnedIds: new Set<string>(),
archivedIds: new Set<string>(),
renamedSubjects: new Map<string, string>(),
isPinned: vi.fn(() => false), isPinned: vi.fn(() => false),
isArchived: vi.fn(() => false), isArchived: vi.fn(() => false),
getRenamedSubject: vi.fn(() => undefined), getRenamedSubject: vi.fn(() => undefined),
@ -100,10 +103,12 @@ vi.mock('../../../../src/renderer/components/sidebar/SidebarTaskItem', () => ({
task, task,
hideProjectName, hideProjectName,
teamOffline, teamOffline,
displaySubjectOverride,
}: { }: {
task: GlobalTask; task: GlobalTask;
hideProjectName?: boolean; hideProjectName?: boolean;
teamOffline?: boolean; teamOffline?: boolean;
displaySubjectOverride?: string;
}) => { }) => {
sidebarTaskItemRenderSpy(task.id); sidebarTaskItemRenderSpy(task.id);
return React.createElement( return React.createElement(
@ -113,7 +118,7 @@ vi.mock('../../../../src/renderer/components/sidebar/SidebarTaskItem', () => ({
'data-hide-project-name': hideProjectName ? 'true' : 'false', 'data-hide-project-name': hideProjectName ? 'true' : 'false',
'data-team-offline': teamOffline ? 'true' : 'false', 'data-team-offline': teamOffline ? 'true' : 'false',
}, },
task.subject displaySubjectOverride ?? task.subject
); );
}, },
})); }));
@ -233,6 +238,9 @@ describe('GlobalTaskList project grouping', () => {
storeState.currentProvisioningRunIdByTeam = {}; storeState.currentProvisioningRunIdByTeam = {};
storeState.leadActivityByTeam = {}; storeState.leadActivityByTeam = {};
toggleCollapsedGroup.mockReset(); toggleCollapsedGroup.mockReset();
taskLocalState.pinnedIds.clear();
taskLocalState.archivedIds.clear();
taskLocalState.renamedSubjects.clear();
taskLocalState.isPinned.mockClear(); taskLocalState.isPinned.mockClear();
taskLocalState.isArchived.mockClear(); taskLocalState.isArchived.mockClear();
taskLocalState.getRenamedSubject.mockClear(); taskLocalState.getRenamedSubject.mockClear();