perf(renderer): memoize global task rows
This commit is contained in:
parent
64d2bae98d
commit
9376416b50
1 changed files with 210 additions and 112 deletions
|
|
@ -194,6 +194,10 @@ function buildTaskTeamSummary(task: GlobalTask): TeamSummary {
|
||||||
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 TaskDisplaySubjectResolver = (task: GlobalTask) => string | undefined;
|
||||||
|
type TaskBooleanResolver = (teamName: string, taskId: string) => boolean;
|
||||||
|
type TeamBooleanResolver = (teamName: string) => boolean;
|
||||||
|
type TaskOwnerColorResolver = (task: GlobalTask) => string | null | undefined;
|
||||||
|
type TeamHeaderFormatter = (teamDisplayName: string) => string;
|
||||||
|
|
||||||
interface GlobalTaskRowProps {
|
interface GlobalTaskRowProps {
|
||||||
task: GlobalTask;
|
task: GlobalTask;
|
||||||
|
|
@ -288,6 +292,112 @@ const GlobalTaskRow = memo(function GlobalTaskRow({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface TaskRowsProps {
|
||||||
|
tasks: GlobalTask[];
|
||||||
|
visibleCount?: number;
|
||||||
|
keyPrefix?: string;
|
||||||
|
isPinned: TaskBooleanResolver;
|
||||||
|
isArchived: TaskBooleanResolver;
|
||||||
|
isNewTask: (task: GlobalTask) => boolean;
|
||||||
|
isTeamOffline: TeamBooleanResolver;
|
||||||
|
renamingKey: string | null;
|
||||||
|
hideTeamName?: boolean;
|
||||||
|
hideProjectName?: boolean;
|
||||||
|
showTeamName?: boolean;
|
||||||
|
showTeamHeader?: boolean;
|
||||||
|
pinnedOverride?: boolean;
|
||||||
|
archivedOverride?: boolean;
|
||||||
|
formatTeamHeader?: TeamHeaderFormatter;
|
||||||
|
onTogglePin: TaskRowAction;
|
||||||
|
onToggleArchive: TaskRowAction;
|
||||||
|
onMarkUnread: TaskRowAction;
|
||||||
|
onRename: TaskRowAction;
|
||||||
|
onDelete: TaskRowDeleteAction;
|
||||||
|
onRenameComplete: (teamName: string, taskId: string, newSubject: string) => void;
|
||||||
|
onRenameCancel: () => void;
|
||||||
|
getDisplaySubject: TaskDisplaySubjectResolver;
|
||||||
|
getOwnerColorName: TaskOwnerColorResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskRows = memo(function TaskRows({
|
||||||
|
tasks,
|
||||||
|
visibleCount,
|
||||||
|
keyPrefix = '',
|
||||||
|
isPinned,
|
||||||
|
isArchived,
|
||||||
|
isNewTask,
|
||||||
|
isTeamOffline,
|
||||||
|
renamingKey,
|
||||||
|
hideTeamName,
|
||||||
|
hideProjectName,
|
||||||
|
showTeamName,
|
||||||
|
showTeamHeader,
|
||||||
|
pinnedOverride,
|
||||||
|
archivedOverride,
|
||||||
|
formatTeamHeader,
|
||||||
|
onTogglePin,
|
||||||
|
onToggleArchive,
|
||||||
|
onMarkUnread,
|
||||||
|
onRename,
|
||||||
|
onDelete,
|
||||||
|
onRenameComplete,
|
||||||
|
onRenameCancel,
|
||||||
|
getDisplaySubject,
|
||||||
|
getOwnerColorName,
|
||||||
|
}: TaskRowsProps): React.JSX.Element {
|
||||||
|
let lastTeam: string | null = null;
|
||||||
|
const visibleTasks = typeof visibleCount === 'number' ? tasks.slice(0, visibleCount) : tasks;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{visibleTasks.map((task) => {
|
||||||
|
const taskKey = `${keyPrefix}${task.teamName}-${task.id}`;
|
||||||
|
const row = (
|
||||||
|
<GlobalTaskRow
|
||||||
|
key={taskKey}
|
||||||
|
task={task}
|
||||||
|
isPinned={pinnedOverride ?? isPinned(task.teamName, task.id)}
|
||||||
|
isArchived={archivedOverride ?? isArchived(task.teamName, task.id)}
|
||||||
|
isNew={isNewTask(task)}
|
||||||
|
hideTeamName={hideTeamName}
|
||||||
|
hideProjectName={hideProjectName}
|
||||||
|
showTeamName={showTeamName}
|
||||||
|
teamOffline={isTeamOffline(task.teamName)}
|
||||||
|
ownerColorName={getOwnerColorName(task)}
|
||||||
|
renamingKey={renamingKey}
|
||||||
|
onTogglePin={onTogglePin}
|
||||||
|
onToggleArchive={onToggleArchive}
|
||||||
|
onMarkUnread={onMarkUnread}
|
||||||
|
onRename={onRename}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onRenameComplete={onRenameComplete}
|
||||||
|
onRenameCancel={onRenameCancel}
|
||||||
|
getDisplaySubject={getDisplaySubject}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showTeamHeader || !formatTeamHeader) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShowTeamHeader = task.teamName !== lastTeam;
|
||||||
|
lastTeam = task.teamName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={taskKey}>
|
||||||
|
{shouldShowTeamHeader && (
|
||||||
|
<div className="px-3 pb-0.5 pt-1.5 text-[10px] font-medium text-text-muted">
|
||||||
|
{formatTeamHeader(task.teamDisplayName)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{row}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const GlobalTaskList = memo(function GlobalTaskList({
|
export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
hideHeader = false,
|
hideHeader = false,
|
||||||
filters: externalFilters,
|
filters: externalFilters,
|
||||||
|
|
@ -515,6 +625,14 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
},
|
},
|
||||||
[memberColorByTeam]
|
[memberColorByTeam]
|
||||||
);
|
);
|
||||||
|
const isTeamOffline = useCallback(
|
||||||
|
(teamName: string): boolean => offlineTeamNames.has(teamName),
|
||||||
|
[offlineTeamNames]
|
||||||
|
);
|
||||||
|
const formatTeamHeader = useCallback(
|
||||||
|
(teamDisplayName: string): string => t('tasksPanel.teamLabel', { team: teamDisplayName }),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
const setGroupingMode = (mode: TaskGroupingMode): void => {
|
const setGroupingMode = (mode: TaskGroupingMode): void => {
|
||||||
setGroupingModeState(mode);
|
setGroupingModeState(mode);
|
||||||
|
|
@ -708,6 +826,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
() => filtered.filter((t) => !taskLocalState.isPinned(t.teamName, t.id)),
|
() => filtered.filter((t) => !taskLocalState.isPinned(t.teamName, t.id)),
|
||||||
[filtered, taskLocalState]
|
[filtered, taskLocalState]
|
||||||
);
|
);
|
||||||
|
const sortedPinnedTasks = useMemo(() => sortTasksByFreshness(pinnedTasks), [pinnedTasks]);
|
||||||
|
|
||||||
const sortedFlat = useMemo(
|
const sortedFlat = useMemo(
|
||||||
() => applySortMode(normalTasks, sortMode, readState),
|
() => applySortMode(normalTasks, sortMode, readState),
|
||||||
|
|
@ -736,6 +855,10 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
syncProjectGroupVisibleCountByKey(projectRequestedVisibleCountByKey, projectGroupVisibility),
|
syncProjectGroupVisibleCountByKey(projectRequestedVisibleCountByKey, projectGroupVisibility),
|
||||||
[projectRequestedVisibleCountByKey, projectGroupVisibility]
|
[projectRequestedVisibleCountByKey, projectGroupVisibility]
|
||||||
);
|
);
|
||||||
|
const taskFilterTeams = useMemo(
|
||||||
|
() => teams.map((team) => ({ teamName: team.teamName, displayName: team.displayName })),
|
||||||
|
[teams]
|
||||||
|
);
|
||||||
|
|
||||||
const projectCollapsed = useCollapsedGroups('project', projectGroupKeys);
|
const projectCollapsed = useCollapsedGroups('project', projectGroupKeys);
|
||||||
const timeCollapsed = useCollapsedGroups('time', timeGroupKeys);
|
const timeCollapsed = useCollapsedGroups('time', timeGroupKeys);
|
||||||
|
|
@ -838,7 +961,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
<TaskFiltersPopover
|
<TaskFiltersPopover
|
||||||
open={filtersPopoverOpen}
|
open={filtersPopoverOpen}
|
||||||
onOpenChange={setFiltersPopoverOpen}
|
onOpenChange={setFiltersPopoverOpen}
|
||||||
teams={teams.map((t) => ({ teamName: t.teamName, displayName: t.displayName }))}
|
teams={taskFilterTeams}
|
||||||
projectOptions={projectFilterOptions}
|
projectOptions={projectFilterOptions}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onFiltersChange={setFilters}
|
onFiltersChange={setFilters}
|
||||||
|
|
@ -853,27 +976,27 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
<Pin className="size-3 text-text-muted" />
|
<Pin className="size-3 text-text-muted" />
|
||||||
<span className="text-[11px] text-text-muted">{t('tasksPanel.pinned')}</span>
|
<span className="text-[11px] text-text-muted">{t('tasksPanel.pinned')}</span>
|
||||||
</div>
|
</div>
|
||||||
{sortTasksByFreshness(pinnedTasks).map((task) => (
|
<TaskRows
|
||||||
<GlobalTaskRow
|
tasks={sortedPinnedTasks}
|
||||||
key={`pinned-${task.teamName}-${task.id}`}
|
keyPrefix="pinned-"
|
||||||
task={task}
|
isPinned={taskLocalState.isPinned}
|
||||||
isPinned={true}
|
isArchived={taskLocalState.isArchived}
|
||||||
isArchived={false}
|
isNewTask={isNewTask}
|
||||||
isNew={isNewTask(task)}
|
isTeamOffline={isTeamOffline}
|
||||||
showTeamName
|
pinnedOverride={true}
|
||||||
teamOffline={offlineTeamNames.has(task.teamName)}
|
archivedOverride={false}
|
||||||
ownerColorName={getOwnerColorName(task)}
|
showTeamName
|
||||||
renamingKey={renamingTaskKey}
|
renamingKey={renamingTaskKey}
|
||||||
onTogglePin={handleToggleTaskPin}
|
onTogglePin={handleToggleTaskPin}
|
||||||
onToggleArchive={handleToggleTaskArchive}
|
onToggleArchive={handleToggleTaskArchive}
|
||||||
onMarkUnread={handleMarkTaskUnread}
|
onMarkUnread={handleMarkTaskUnread}
|
||||||
onRename={handleStartTaskRename}
|
onRename={handleStartTaskRename}
|
||||||
onDelete={handleDeleteTask}
|
onDelete={handleDeleteTask}
|
||||||
onRenameComplete={handleRenameComplete}
|
onRenameComplete={handleRenameComplete}
|
||||||
onRenameCancel={handleRenameCancel}
|
onRenameCancel={handleRenameCancel}
|
||||||
getDisplaySubject={getTaskDisplaySubject}
|
getDisplaySubject={getTaskDisplaySubject}
|
||||||
/>
|
getOwnerColorName={getOwnerColorName}
|
||||||
))}
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -956,28 +1079,26 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{groupingMode === 'none' &&
|
{groupingMode === 'none' && (
|
||||||
sortedFlat.map((task) => (
|
<TaskRows
|
||||||
<GlobalTaskRow
|
tasks={sortedFlat}
|
||||||
key={`${task.teamName}-${task.id}`}
|
isPinned={taskLocalState.isPinned}
|
||||||
task={task}
|
isArchived={taskLocalState.isArchived}
|
||||||
isPinned={taskLocalState.isPinned(task.teamName, task.id)}
|
isNewTask={isNewTask}
|
||||||
isArchived={taskLocalState.isArchived(task.teamName, task.id)}
|
isTeamOffline={isTeamOffline}
|
||||||
isNew={isNewTask(task)}
|
showTeamName
|
||||||
showTeamName
|
renamingKey={renamingTaskKey}
|
||||||
teamOffline={offlineTeamNames.has(task.teamName)}
|
onTogglePin={handleToggleTaskPin}
|
||||||
ownerColorName={getOwnerColorName(task)}
|
onToggleArchive={handleToggleTaskArchive}
|
||||||
renamingKey={renamingTaskKey}
|
onMarkUnread={handleMarkTaskUnread}
|
||||||
onTogglePin={handleToggleTaskPin}
|
onRename={handleStartTaskRename}
|
||||||
onToggleArchive={handleToggleTaskArchive}
|
onDelete={handleDeleteTask}
|
||||||
onMarkUnread={handleMarkTaskUnread}
|
onRenameComplete={handleRenameComplete}
|
||||||
onRename={handleStartTaskRename}
|
onRenameCancel={handleRenameCancel}
|
||||||
onDelete={handleDeleteTask}
|
getDisplaySubject={getTaskDisplaySubject}
|
||||||
onRenameComplete={handleRenameComplete}
|
getOwnerColorName={getOwnerColorName}
|
||||||
onRenameCancel={handleRenameCancel}
|
/>
|
||||||
getDisplaySubject={getTaskDisplaySubject}
|
)}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{groupingMode === 'project' &&
|
{groupingMode === 'project' &&
|
||||||
projectGroups.map((group) => {
|
projectGroups.map((group) => {
|
||||||
|
|
@ -991,10 +1112,8 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
projectVisibleCountByKey[group.projectKey],
|
projectVisibleCountByKey[group.projectKey],
|
||||||
group.tasks.length
|
group.tasks.length
|
||||||
);
|
);
|
||||||
const visibleTasks = group.tasks.slice(0, visibleCount);
|
|
||||||
const showMoreVisible = canProjectGroupShowMore(visibleCount, group.tasks.length);
|
const showMoreVisible = canProjectGroupShowMore(visibleCount, group.tasks.length);
|
||||||
const showLessVisible = canProjectGroupShowLess(visibleCount, group.tasks.length);
|
const showLessVisible = canProjectGroupShowLess(visibleCount, group.tasks.length);
|
||||||
let lastTeam: string | null = null;
|
|
||||||
return (
|
return (
|
||||||
<div key={group.projectKey}>
|
<div key={group.projectKey}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -1029,39 +1148,30 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
{group.tasks.length}
|
{group.tasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{!isGroupCollapsed &&
|
{!isGroupCollapsed && (
|
||||||
visibleTasks.map((task) => {
|
<TaskRows
|
||||||
const showTeamHeader = task.teamName !== lastTeam;
|
tasks={group.tasks}
|
||||||
lastTeam = task.teamName;
|
visibleCount={visibleCount}
|
||||||
return (
|
isPinned={taskLocalState.isPinned}
|
||||||
<div key={`${task.teamName}-${task.id}`}>
|
isArchived={taskLocalState.isArchived}
|
||||||
{showTeamHeader && (
|
isNewTask={isNewTask}
|
||||||
<div className="px-3 pb-0.5 pt-1.5 text-[10px] font-medium text-text-muted">
|
isTeamOffline={isTeamOffline}
|
||||||
{t('tasksPanel.teamLabel', { team: task.teamDisplayName })}
|
hideTeamName
|
||||||
</div>
|
hideProjectName
|
||||||
)}
|
showTeamHeader
|
||||||
<GlobalTaskRow
|
formatTeamHeader={formatTeamHeader}
|
||||||
task={task}
|
renamingKey={renamingTaskKey}
|
||||||
isPinned={taskLocalState.isPinned(task.teamName, task.id)}
|
onTogglePin={handleToggleTaskPin}
|
||||||
isArchived={taskLocalState.isArchived(task.teamName, task.id)}
|
onToggleArchive={handleToggleTaskArchive}
|
||||||
isNew={isNewTask(task)}
|
onMarkUnread={handleMarkTaskUnread}
|
||||||
hideTeamName
|
onRename={handleStartTaskRename}
|
||||||
hideProjectName
|
onDelete={handleDeleteTask}
|
||||||
teamOffline={offlineTeamNames.has(task.teamName)}
|
onRenameComplete={handleRenameComplete}
|
||||||
ownerColorName={getOwnerColorName(task)}
|
onRenameCancel={handleRenameCancel}
|
||||||
renamingKey={renamingTaskKey}
|
getDisplaySubject={getTaskDisplaySubject}
|
||||||
onTogglePin={handleToggleTaskPin}
|
getOwnerColorName={getOwnerColorName}
|
||||||
onToggleArchive={handleToggleTaskArchive}
|
/>
|
||||||
onMarkUnread={handleMarkTaskUnread}
|
)}
|
||||||
onRename={handleStartTaskRename}
|
|
||||||
onDelete={handleDeleteTask}
|
|
||||||
onRenameComplete={handleRenameComplete}
|
|
||||||
onRenameCancel={handleRenameCancel}
|
|
||||||
getDisplaySubject={getTaskDisplaySubject}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!isGroupCollapsed && (showMoreVisible || showLessVisible) && (
|
{!isGroupCollapsed && (showMoreVisible || showLessVisible) && (
|
||||||
<div className="flex items-center gap-2 px-3 pb-2 pt-1">
|
<div className="flex items-center gap-2 px-3 pb-2 pt-1">
|
||||||
{showMoreVisible && (
|
{showMoreVisible && (
|
||||||
|
|
@ -1108,7 +1218,6 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
categories.map((category) => {
|
categories.map((category) => {
|
||||||
const tasks = grouped[category];
|
const tasks = grouped[category];
|
||||||
const isGroupCollapsed = timeCollapsed.isCollapsed(category);
|
const isGroupCollapsed = timeCollapsed.isCollapsed(category);
|
||||||
let lastTeam: string | null = null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
|
|
@ -1129,38 +1238,27 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{!isGroupCollapsed &&
|
{!isGroupCollapsed && (
|
||||||
tasks.map((task) => {
|
<TaskRows
|
||||||
const showTeamHeader = task.teamName !== lastTeam;
|
tasks={tasks}
|
||||||
lastTeam = task.teamName;
|
isPinned={taskLocalState.isPinned}
|
||||||
|
isArchived={taskLocalState.isArchived}
|
||||||
return (
|
isNewTask={isNewTask}
|
||||||
<div key={`${task.teamName}-${task.id}`}>
|
isTeamOffline={isTeamOffline}
|
||||||
{showTeamHeader && (
|
showTeamHeader
|
||||||
<div className="px-3 pb-0.5 pt-1.5 text-[10px] font-medium text-text-muted">
|
formatTeamHeader={formatTeamHeader}
|
||||||
{t('tasksPanel.teamLabel', { team: task.teamDisplayName })}
|
renamingKey={renamingTaskKey}
|
||||||
</div>
|
onTogglePin={handleToggleTaskPin}
|
||||||
)}
|
onToggleArchive={handleToggleTaskArchive}
|
||||||
<GlobalTaskRow
|
onMarkUnread={handleMarkTaskUnread}
|
||||||
task={task}
|
onRename={handleStartTaskRename}
|
||||||
isPinned={taskLocalState.isPinned(task.teamName, task.id)}
|
onDelete={handleDeleteTask}
|
||||||
isArchived={taskLocalState.isArchived(task.teamName, task.id)}
|
onRenameComplete={handleRenameComplete}
|
||||||
isNew={isNewTask(task)}
|
onRenameCancel={handleRenameCancel}
|
||||||
teamOffline={offlineTeamNames.has(task.teamName)}
|
getDisplaySubject={getTaskDisplaySubject}
|
||||||
ownerColorName={getOwnerColorName(task)}
|
getOwnerColorName={getOwnerColorName}
|
||||||
renamingKey={renamingTaskKey}
|
/>
|
||||||
onTogglePin={handleToggleTaskPin}
|
)}
|
||||||
onToggleArchive={handleToggleTaskArchive}
|
|
||||||
onMarkUnread={handleMarkTaskUnread}
|
|
||||||
onRename={handleStartTaskRename}
|
|
||||||
onDelete={handleDeleteTask}
|
|
||||||
onRenameComplete={handleRenameComplete}
|
|
||||||
onRenameCancel={handleRenameCancel}
|
|
||||||
getDisplaySubject={getTaskDisplaySubject}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue