mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(tasks): optional setting to sort completed tasks by completion date (#8810)
* feat(tasks): optional setting to sort completed tasks by completion date Adds an opt-in "Sort completed tasks by completion date (newest first)" setting under Settings > Tasks. When enabled, the Done task list is ordered by each task's doneOn timestamp descending; when off (the default) behaviour is unchanged. Sorting is applied at the doneTasks$ choke point so it covers the Today list and project/tag contexts alike. * refactor(tasks): make completion-date sort the calm default, not a toggle Builds on the previous commit's opt-in setting. Ordering the Done list by completion date (newest first) is the expected default for reviewing just- finished work (issue #2936), and no manual done-order is preserved anyway, so drop the toggle and apply the sort unconditionally at doneTasks$. Removes the config field + default, the settings-form checkbox, and the T/en.json label; moves the pure helper to work-context.util.ts with its own spec (stable-sort and empty-array cases included). Also guards _move so a reorder WITHIN the now-auto-sorted Done list no longer emits a spurious moveTaskInTodayList op that would sync to other devices; dragging a task out of Done (DONE -> UNDONE) still works. Specs cover both. * fix(tasks): skip keyboard reorder of auto-sorted done tasks Complements the drag guard from the previous commit. The move up/down/top/ bottom shortcuts on a top-level done task in the main list dispatched a taskIds-reorder that the completion-date-sorted Done list ignores, emitting a spurious op that syncs to other devices. Guard _moveAndRefocus (the single choke point for all four keyboard reorders) so it no-ops for done main-list tasks. Done subtasks and backlog tasks stay reorderable — neither is auto-sorted. --------- Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
This commit is contained in:
parent
3e56836a23
commit
c330ab1d7b
6 changed files with 91 additions and 2 deletions
|
|
@ -716,6 +716,24 @@ describe('TaskListComponent', () => {
|
|||
expect(sectionServiceMock.addTaskToSection).not.toHaveBeenCalled();
|
||||
expect(sectionServiceMock.removeTaskFromSection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips a reorder WITHIN the Done list (auto-sorted) so no moveTaskInTodayList op is emitted', () => {
|
||||
callMove('task1', 'DONE', 'DONE', 'PARENT', 'PARENT');
|
||||
|
||||
const dispatched = (store.dispatch as jasmine.Spy).calls
|
||||
.allArgs()
|
||||
.map((args) => args[0]);
|
||||
expect(dispatched.some((a) => a.type === moveTaskInTodayList.type)).toBe(false);
|
||||
});
|
||||
|
||||
it('still moves a task OUT of Done (DONE -> UNDONE) via moveTaskInTodayList', () => {
|
||||
callMove('task1', 'DONE', 'UNDONE', 'PARENT', 'PARENT');
|
||||
|
||||
const dispatched = (store.dispatch as jasmine.Spy).calls
|
||||
.allArgs()
|
||||
.map((args) => args[0]);
|
||||
expect(dispatched.some((a) => a.type === moveTaskInTodayList.type)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// The public drop() handler turns a CdkDragDrop event into the right action,
|
||||
|
|
|
|||
|
|
@ -557,6 +557,15 @@ export class TaskListComponent implements OnDestroy, AfterViewInit {
|
|||
return;
|
||||
}
|
||||
|
||||
// The Done list is always ordered by completion date, so reordering within
|
||||
// it can't take effect (the view re-sorts on the next emission). Skip the
|
||||
// move to avoid emitting a spurious taskIds-reorder op that would sync to
|
||||
// other devices. Dragging a task OUT of Done (DONE -> UNDONE) still falls
|
||||
// through below.
|
||||
if (src === 'DONE' && target === 'DONE') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
srcListId === 'SUB' &&
|
||||
targetListId === 'PARENT' &&
|
||||
|
|
|
|||
|
|
@ -699,6 +699,13 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
|
|||
move: (id: string, parentId: string | undefined, isBacklog: boolean) => void,
|
||||
): void {
|
||||
const t = this.task();
|
||||
// Top-level done tasks in the main list are ordered by completion date, so
|
||||
// a manual reorder can't take effect — skip it to avoid emitting a spurious
|
||||
// taskIds-reorder op that would sync to other devices. Done subtasks and
|
||||
// backlog tasks are not auto-sorted, so they stay reorderable.
|
||||
if (t.isDone && !t.parentId && !this.isBacklog()) {
|
||||
return;
|
||||
}
|
||||
move(t.id, t.parentId, this.isBacklog());
|
||||
// timeout required to let changes take place
|
||||
setTimeout(() => this.focusSelf());
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ import { Tag } from '../tag/tag.model';
|
|||
import { DEFAULT_TAG_COLOR } from './work-context.const';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { ArchiveTask, Task, TaskWithSubTasks } from '../tasks/task.model';
|
||||
import { hasTasksToWorkOn, mapEstimateRemainingFromTasks } from './work-context.util';
|
||||
import {
|
||||
hasTasksToWorkOn,
|
||||
mapEstimateRemainingFromTasks,
|
||||
sortDoneTasksByDoneDate,
|
||||
} from './work-context.util';
|
||||
import {
|
||||
flattenTasks,
|
||||
selectAllTasks,
|
||||
|
|
@ -441,7 +445,9 @@ export class WorkContextService {
|
|||
switchMap((isToday) =>
|
||||
isToday ? this._store$.select(selectAllTasksWithSubTasks) : this.mainListTasks$,
|
||||
),
|
||||
map((tasks) => tasks.filter((task) => task && task.isDone)),
|
||||
// Show completed tasks newest-first (by completion time) so the task you
|
||||
// just finished is at the top of the Done list.
|
||||
map((tasks) => sortDoneTasksByDoneDate(tasks.filter((task) => task && task.isDone))),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
|
|
|
|||
40
src/app/features/work-context/work-context.util.spec.ts
Normal file
40
src/app/features/work-context/work-context.util.spec.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { sortDoneTasksByDoneDate } from './work-context.util';
|
||||
import { TaskWithSubTasks } from '../tasks/task.model';
|
||||
|
||||
describe('sortDoneTasksByDoneDate', () => {
|
||||
const task = (id: string, doneOn?: number): TaskWithSubTasks =>
|
||||
({ id, isDone: true, doneOn }) as TaskWithSubTasks;
|
||||
|
||||
it('orders completed tasks by completion date, newest first', () => {
|
||||
const sorted = sortDoneTasksByDoneDate([
|
||||
task('OLD', 1000),
|
||||
task('NEW', 3000),
|
||||
task('MID', 2000),
|
||||
]);
|
||||
expect(sorted.map((t) => t.id)).toEqual(['NEW', 'MID', 'OLD']);
|
||||
});
|
||||
|
||||
it('treats a missing completion timestamp as oldest', () => {
|
||||
const sorted = sortDoneTasksByDoneDate([task('NO_DATE'), task('HAS_DATE', 5000)]);
|
||||
expect(sorted.map((t) => t.id)).toEqual(['HAS_DATE', 'NO_DATE']);
|
||||
});
|
||||
|
||||
it('does not mutate the input array', () => {
|
||||
const input = [task('A', 1000), task('B', 2000)];
|
||||
sortDoneTasksByDoneDate(input);
|
||||
expect(input.map((t) => t.id)).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it('preserves input order for equal completion timestamps (stable sort)', () => {
|
||||
const sorted = sortDoneTasksByDoneDate([
|
||||
task('A', 1000),
|
||||
task('B', 1000),
|
||||
task('C', 1000),
|
||||
]);
|
||||
expect(sorted.map((t) => t.id)).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
it('returns an empty array unchanged', () => {
|
||||
expect(sortDoneTasksByDoneDate([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,14 @@
|
|||
import { TaskWithSubTasks } from '../tasks/task.model';
|
||||
|
||||
/**
|
||||
* Order completed tasks by when they were completed, newest first, so the most
|
||||
* recently finished task is at the top of the Done list. Copies the input since
|
||||
* it usually comes from a memoized selector that must not be mutated. Tasks
|
||||
* without a `doneOn` timestamp sort last.
|
||||
*/
|
||||
export const sortDoneTasksByDoneDate = (tasks: TaskWithSubTasks[]): TaskWithSubTasks[] =>
|
||||
[...tasks].sort((a, b) => (b.doneOn ?? 0) - (a.doneOn ?? 0));
|
||||
|
||||
export const mapEstimateRemainingFromTasks = (tasks: TaskWithSubTasks[]): number =>
|
||||
tasks &&
|
||||
tasks.length &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue