perf(renderer): precompute sidebar task local state
This commit is contained in:
parent
effe9b522f
commit
94c09727f1
3 changed files with 139 additions and 86 deletions
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue