diff --git a/src/renderer/components/sidebar/GlobalTaskList.tsx b/src/renderer/components/sidebar/GlobalTaskList.tsx index 9427817e..37fdc2cb 100644 --- a/src/renderer/components/sidebar/GlobalTaskList.tsx +++ b/src/renderer/components/sidebar/GlobalTaskList.tsx @@ -162,6 +162,8 @@ const dateCategoryLabels: Record = { Older: 'Earlier', }; +type ProjectTaskGroupData = ReturnType[number]; + function applySearch(tasks: GlobalTask[], query: string): GlobalTask[] { if (!query.trim()) return tasks; const q = query.toLowerCase(); @@ -198,6 +200,7 @@ type TaskBooleanResolver = (teamName: string, taskId: string) => boolean; type TeamBooleanResolver = (teamName: string) => boolean; type TaskOwnerColorResolver = (task: GlobalTask) => string | null | undefined; type TeamHeaderFormatter = (teamDisplayName: string) => string; +type ProjectGroupVisibleCountChange = (projectKey: string, visibleCount: number) => void; interface GlobalTaskRowProps { task: GlobalTask; @@ -398,6 +401,158 @@ const TaskRows = memo(function TaskRows({ ); }); +interface ProjectTaskGroupProps { + group: ProjectTaskGroupData; + isCollapsed: boolean; + visibleCount: number; + noProjectGroupColor: ReturnType; + showMoreLabel: string; + showLessLabel: string; + isPinned: TaskBooleanResolver; + isArchived: TaskBooleanResolver; + isNewTask: (task: GlobalTask) => boolean; + isTeamOffline: TeamBooleanResolver; + renamingKey: string | null; + formatTeamHeader: TeamHeaderFormatter; + onToggleGroup: (projectKey: string) => void; + onVisibleCountChange: ProjectGroupVisibleCountChange; + 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 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); + + return ( +
+ + {!isCollapsed && ( + + )} + {!isCollapsed && (showMoreVisible || showLessVisible) && ( +
+ {showMoreVisible && ( + + )} + {showLessVisible && ( + + )} +
+ )} +
+ ); +}); + export const GlobalTaskList = memo(function GlobalTaskList({ hideHeader = false, filters: externalFilters, @@ -633,6 +788,15 @@ export const GlobalTaskList = memo(function GlobalTaskList({ (teamDisplayName: string): string => t('tasksPanel.teamLabel', { team: teamDisplayName }), [t] ); + const handleProjectGroupVisibleCountChange = useCallback( + (projectKey: string, visibleCount: number): void => { + setProjectRequestedVisibleCountByKey((prev) => ({ + ...prev, + [projectKey]: visibleCount, + })); + }, + [] + ); const setGroupingMode = (mode: TaskGroupingMode): void => { setGroupingModeState(mode); @@ -860,8 +1024,20 @@ export const GlobalTaskList = memo(function GlobalTaskList({ [teams] ); - const projectCollapsed = useCollapsedGroups('project', projectGroupKeys); - const timeCollapsed = useCollapsedGroups('time', timeGroupKeys); + const { isCollapsed: isProjectGroupCollapsed, toggle: toggleProjectGroup } = useCollapsedGroups( + 'project', + projectGroupKeys + ); + const { isCollapsed: isTimeGroupCollapsed, toggle: toggleTimeGroup } = useCollapsedGroups( + 'time', + timeGroupKeys + ); + const handleToggleProjectGroup = useCallback( + (projectKey: string): void => { + toggleProjectGroup(projectKey); + }, + [toggleProjectGroup] + ); const hasContent = pinnedTasks.length > 0 || @@ -1102,128 +1278,50 @@ export const GlobalTaskList = memo(function GlobalTaskList({ {groupingMode === 'project' && projectGroups.map((group) => { - if (group.tasks.length === 0) return null; - const isGroupCollapsed = projectCollapsed.isCollapsed(group.projectKey); - const isNoProjectGroup = group.projectKey === NO_PROJECT_KEY; - const groupColor = isNoProjectGroup - ? noProjectGroupColor - : projectColor(group.projectLabel); const visibleCount = getProjectGroupVisibleCount( projectVisibleCountByKey[group.projectKey], group.tasks.length ); - const showMoreVisible = canProjectGroupShowMore(visibleCount, group.tasks.length); - const showLessVisible = canProjectGroupShowLess(visibleCount, group.tasks.length); return ( -
- - {!isGroupCollapsed && ( - - )} - {!isGroupCollapsed && (showMoreVisible || showLessVisible) && ( -
- {showMoreVisible && ( - - )} - {showLessVisible && ( - - )} -
- )} -
+ ); })} {groupingMode === 'time' && categories.map((category) => { const tasks = grouped[category]; - const isGroupCollapsed = timeCollapsed.isCollapsed(category); + const isGroupCollapsed = isTimeGroupCollapsed(category); return (