feat(tasks): group "Alle Aufgaben" by task list by default

Tasks without a list come first, then the lists in tab order. Single
lists keep no grouping by default and do not offer it. The open task
list, view and grouping, like the open shopping list, are now
remembered per device instead of per tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-02 13:13:14 +01:00
1 parent b04991ee23
commit 2cdfa45375
3 files changed
+110 -36

No files matched your search

+82 -32
View File
@@ -30,7 +30,7 @@ import {
} from 'lucide-react';
import { TaskList, Thought, SubTask, Team, User, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS, TrashedTask } from '../../lib/types';
import { formatDeletedAt, formatDue, isDueToday, isOverdue, localDay, splitDue } from '../../lib/due';
import { readViewSession, writeViewSession } from '../../lib/viewSession';
import { readViewMemory, writeViewMemory } from '../../lib/viewSession';
import { useKanbanTouchDrag } from './useKanbanTouchDrag';
import { api, triggerHaptic } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
@@ -91,7 +91,28 @@ const kanbanBoardStyle = (wide: boolean, columns: number): React.CSSProperties =
} as React.CSSProperties;
};
type TasksSession = { listId: string; viewMode: 'list' | 'kanban' };
type GroupBy = 'none' | 'list' | 'tag' | 'status' | 'priority' | 'due';
type TasksSession = { listId: string; viewMode: 'list' | 'kanban'; groupAll: GroupBy; groupList: GroupBy };
// "Alle Aufgaben" is grouped by task list unless the user picks something else; a single list has no
// grouping by default (grouping it by list would give one group)
const DEFAULT_GROUP_ALL: GroupBy = 'list';
const DEFAULT_GROUP_LIST: GroupBy = 'none';
const GROUP_LABELS: Record<GroupBy, string> = {
none: 'Keine',
list: 'Nach Liste',
tag: 'Nach Hashtag',
status: 'Nach Status',
priority: 'Nach Priorität',
due: 'Nach Fälligkeit',
};
const SORT_LABELS: Record<string, string> = {
custom: 'Manuell',
created: 'Erstelldatum',
due: 'Fälligkeitsdatum',
priority: 'Priorität',
alpha: 'Alphabetisch',
};
export const TasksView: React.FC<TasksViewProps> = ({
currentUser,
@@ -102,9 +123,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
onInitialTaskShown,
openTasksCount,
}) => {
// Open list and view survive switching to other tabs (like the workspace's open note)
// Open list, view and grouping are remembered on this device (new tab, restart of the installed app)
const remembered = useMemo(() => readViewMemory<TasksSession>('tasks'), []);
const [viewMode, setViewMode] = useState<'list' | 'kanban'>(() =>
readViewSession<TasksSession>('tasks').viewMode === 'kanban' ? 'kanban' : 'list'
remembered.viewMode === 'kanban' ? 'kanban' : 'list'
);
const [groupAll, setGroupAll] = useState<GroupBy>(() =>
remembered.groupAll && remembered.groupAll in GROUP_LABELS ? remembered.groupAll : DEFAULT_GROUP_ALL
);
const [groupList, setGroupList] = useState<GroupBy>(() =>
remembered.groupList && remembered.groupList in GROUP_LABELS && remembered.groupList !== 'list'
? remembered.groupList
: DEFAULT_GROUP_LIST
);
// Wide Kanban: the board takes the whole window and shows every column in one row
const [kanbanWide, setKanbanWide] = useState(() => {
@@ -128,11 +158,19 @@ export const TasksView: React.FC<TasksViewProps> = ({
const [tasks, setTasks] = useState<Thought[]>([]);
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
const [selectedListId, setSelectedListId] = useState<string>(
() => readViewSession<TasksSession>('tasks').listId || 'all'
() => remembered.listId || 'all'
);
const isAllTasks = selectedListId === 'all';
const groupBy: GroupBy = isAllTasks ? groupAll : groupList;
const setGroupBy = (g: GroupBy) => (isAllTasks ? setGroupAll(g) : setGroupList(g));
const defaultGroupBy = isAllTasks ? DEFAULT_GROUP_ALL : DEFAULT_GROUP_LIST;
// "Nach Liste" only where several lists show up
const groupOptions = (Object.keys(GROUP_LABELS) as GroupBy[])
.filter((id) => isAllTasks || id !== 'list')
.map((id) => ({ id, label: GROUP_LABELS[id] }));
useEffect(() => {
writeViewSession<TasksSession>('tasks', { listId: selectedListId, viewMode });
}, [selectedListId, viewMode]);
writeViewMemory<TasksSession>('tasks', { listId: selectedListId, viewMode, groupAll, groupList });
}, [selectedListId, viewMode, groupAll, groupList]);
const [teams, setTeams] = useState<Team[]>([]);
const [collaborators, setCollaborators] = useState<User[]>([]);
// Task trash of the selected list (or all): deleted tasks and cleared or cleaned-up completed ones
@@ -216,7 +254,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
const tagRowRef = useRef<HTMLDivElement>(null);
const [tagsExpanded, setTagsExpanded] = useState(false);
const [tagsOverflow, setTagsOverflow] = useState(false);
const [groupBy, setGroupBy] = useState<'none' | 'tag' | 'status' | 'priority' | 'due'>('none');
const [sortBy, setSortBy] = useState<'custom' | 'created' | 'due' | 'priority' | 'alpha'>('custom');
// Reloads happen after almost every action and on realtime events. Only the first load of a list shows
@@ -571,7 +608,25 @@ export const TasksView: React.FC<TasksViewProps> = ({
if (groupBy === 'none') return null;
const groups: { key: string; title: string; count: number; tasks: Thought[] }[] = [];
if (groupBy === 'tag') {
if (groupBy === 'list') {
// Tasks without a list first, then the lists in the order of the tabs
const byList = new Map<string, Thought[]>();
for (const t of filteredTasks) {
const key = t.taskListId || '';
if (!byList.has(key)) byList.set(key, []);
byList.get(key)!.push(t);
}
const without = byList.get('') || [];
if (without.length > 0) groups.push({ key: 'list:none', title: 'Ohne Liste', count: without.length, tasks: without });
for (const tl of safeTaskLists) {
const list = byList.get(tl.id);
if (!list || list.length === 0) continue;
groups.push({ key: `list:${tl.id}`, title: [tl.icon, tl.title].filter(Boolean).join(' '), count: list.length, tasks: list });
byList.delete(tl.id);
}
byList.delete('');
byList.forEach((list, id) => groups.push({ key: `list:${id}`, title: 'Andere Liste', count: list.length, tasks: list }));
} else if (groupBy === 'tag') {
const map = new Map<string, Thought[]>();
const withoutTag: Thought[] = [];
for (const t of filteredTasks) {
@@ -616,7 +671,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
if (noDue.length > 0) groups.push({ key: 'due:none', title: 'Ohne Fälligkeitsdatum', count: noDue.length, tasks: noDue });
}
return groups;
}, [filteredTasks, groupBy, boardColumns, inBoardColumn]);
}, [filteredTasks, groupBy, boardColumns, inBoardColumn, safeTaskLists]);
// --- TASK ACTIONS ---
@@ -1341,7 +1396,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
];
const filterSortActiveCount =
(selectedTag ? 1 : 0) + (groupBy !== 'none' ? 1 : 0) + (sortBy !== 'custom' ? 1 : 0);
(selectedTag ? 1 : 0) + (groupBy !== defaultGroupBy ? 1 : 0) + (sortBy !== 'custom' ? 1 : 0);
const filterItems: any[] = [
{
@@ -1482,6 +1537,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
selectedListId,
selectedTag,
groupBy,
defaultGroupBy,
sortBy,
openTasks.length,
doneTasks.length,
@@ -2051,7 +2107,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
)}
{/* Mobile Active Filter Badge Banner (minimal and clean) */}
{(selectedTag || groupBy !== 'none' || sortBy !== 'custom') && (
{(selectedTag || groupBy !== defaultGroupBy || sortBy !== 'custom') && (
<div className="flex sm:hidden items-center justify-between gap-2 p-2 rounded-btn bg-accent/10 border border-accent/20 text-xs">
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
<Filter className="w-3.5 h-3.5 text-accent flex-shrink-0" />
@@ -2067,14 +2123,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button>
</span>
)}
{groupBy !== 'none' && (
{groupBy !== defaultGroupBy && (
<span className="text-muted text-[11px]">
Gruppe: <strong className="text-main">{groupBy}</strong>
Gruppe: <strong className="text-main">{GROUP_LABELS[groupBy]}</strong>
</span>
)}
{sortBy !== 'custom' && (
<span className="text-muted text-[11px]">
Sortiert: <strong className="text-main">{sortBy}</strong>
Sortiert: <strong className="text-main">{SORT_LABELS[sortBy]}</strong>
</span>
)}
</div>
@@ -2082,7 +2138,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
type="button"
onClick={() => {
setSelectedTag(null);
setGroupBy('none');
setGroupBy(defaultGroupBy);
setSortBy('custom');
}}
className="text-accent text-[11px] font-semibold flex-shrink-0 hover:underline cursor-pointer"
@@ -2397,17 +2453,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
Gruppierung
</span>
<div className="grid grid-cols-2 gap-1.5">
{[
{ id: 'none', label: 'Keine' },
{ id: 'tag', label: 'Nach Hashtag' },
{ id: 'status', label: 'Nach Status' },
{ id: 'priority', label: 'Nach Priorität' },
{ id: 'due', label: 'Nach Fälligkeit' },
].map((g) => (
{groupOptions.map((g) => (
<button
key={g.id}
type="button"
onClick={() => setGroupBy(g.id as any)}
onClick={() => setGroupBy(g.id)}
className={`min-h-[36px] px-3 text-xs rounded-btn border text-left transition-colors cursor-pointer ${
groupBy === g.id
? 'bg-accent-dim border-accent text-accent font-semibold'
@@ -2457,7 +2507,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
type="button"
onClick={() => {
setSelectedTag(null);
setGroupBy('none');
setGroupBy(defaultGroupBy);
setSortBy('custom');
}}
className="btn-ghost h-9 px-3 text-xs text-muted hover:text-main cursor-pointer"
@@ -2918,14 +2968,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
<span className="text-[11px]">Gruppe:</span>
<select
value={groupBy}
onChange={(e) => setGroupBy(e.target.value as any)}
onChange={(e) => setGroupBy(e.target.value as GroupBy)}
className="h-8 px-2 input-field text-xs bg-panel"
>
<option value="none">Keine</option>
<option value="tag">Nach Hashtag</option>
<option value="status">Nach Status</option>
<option value="priority">Nach Priorität</option>
<option value="due">Nach Fälligkeit</option>
{groupOptions.map((g) => (
<option key={g.id} value={g.id}>
{g.label}
</option>
))}
</select>
</div>
<div className="flex items-center gap-1 text-xs text-muted">
@@ -3014,7 +3064,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div>
) : (
<div className="space-y-3">
{groupedTasks ? (
{groupedTasks && groupedTasks.length > 0 ? (
/* GROUPED LIST */
groupedTasks.map((grp) => (
<div key={grp.key} className="panel-surface overflow-hidden">