Commit graph

6 commits

Author SHA1 Message Date
Johannes Millan
eff41c041d
feat(project): project completion experience (#8036)
* feat(project): add project completion with celebration + trophy view

Completing a project marks it done and archives it, with a celebration
dialog (confetti + live stats) and a prompt to resolve unfinished tasks
(move to Inbox / mark done). Completed projects show a trophy badge and
a Reopen action on the archived-projects page.

isDone stays distinct from isArchived; selectArchivedProjects is left
intact so completed projects' tasks stay filtered out of Today/Overdue.
Append/merge deferred to #8032. Plan: docs/plans/2026-06-05-project-completion.md

* refactor(project): drop Archive menu item; Complete is the retire path

Archive and Complete produced near-identical end states; collapse to one
user-facing action. Removes the 'Archive project' menu item and handler so
Complete is the single way to retire a project. The archiveProject action,
reducer and ProjectService.archive() stay (needed for op-log replay of
historical archive ops and the legacy unarchive/restore path). Wiki updated;
menu specs cover the Complete flow.

* fix(project): keep completion dialog open for the active project

MatDialog closeOnNavigation (default true) dismissed the celebration the
moment completing the currently-active project navigated to '/'. Navigate
first, then open the dialog.

* test(project): e2e for completion flow (complete, celebrate, reopen)

Covers the resolve-unfinished-tasks prompt, the celebration dialog with
stats, the trophy badge on the archived page, and reopening. Adds an
openProjectContextMenu page-object helper.

* feat(project): confirm project completion

* feat(project): show completion celebration fullscreen

* fix(project): harden completion celebration flow

* style(project): use spacing tokens in completion dialog

* fix(project): align completion screen with project context

* style(project): soften completion screen coloring

* style(project): reuse completion screen surfaces

* fix: refine project completion dialog actions

* fix: complete projects with atomic task resolution

* fix: restore archive path in project completion UI

* refactor(project): remove dead completion code

The project-level completeProject action (OpType.Update) was never
dispatched — completion goes exclusively through the atomic
TaskSharedActions.completeProject (OpType.Batch) meta-reducer. Drop the
dead action, its reducer case, the PROJECT_COMPLETE enum member and the
immutable 'PCO' op-log code (which would otherwise be permanently
reserved for an op that can never be produced); enum count 147->146.

Also remove the unused selectCompletedProjects / selectPlainArchivedProjects
selectors (no consumers) and the misspelled, unused 'angel' confetti
field, keeping the regression tests that guard selectArchivedProjects.

* fix(project): make project completion non-reversible

Completion resolves a project's open tasks (move-to-inbox / mark-done),
which reopen cannot truly restore, so the "Reopen"/"Undo" affordances on
the completion path were misleading. Drop the celebration dialog's Reopen
button and the post-complete undo snack; the fullscreen celebration is
the feedback and deliberate reactivation still lives on the
archived-projects page (Project.reopen kept for it).

Also restore the project title param on the archive confirm dialog (it
was rendering a raw {{title}} placeholder), remove the now-unused
moveTasksToInbox / markTasksDone resolution helpers (the meta-reducer
resolves tasks atomically), and drop the orphaned UNDO / S.COMPLETED
i18n keys. Updates the completion e2e to the close flow and asserts the
resolution props are forwarded to the atomic action.

* fix(tasks): cancel native reminders for project-completed tasks

Completing a project marks its unfinished tasks done inside the
meta-reducer (no per-task updateTask), so unscheduleDoneTask$'s
native-reminder cancellation is bypassed and an OS-scheduled Android
notification could still fire for a now-done task. Add a local-only
effect that cancels native reminders for the force-completed task ids.

Local-only by design: it dispatches no actions (the persistent
dismissReminderOnly/clearDeadlineReminder would each be an extra synced
op), and done tasks are already filtered from reminders$ on all
platforms — only the native Android notification needs explicit removal.

* test(project): drop TaskService spies orphaned by helper removal

getByIdWithSubTaskData$/moveToProject/setDone/setUnDone were only used
by the removed moveTasksToInbox/markTasksDone helpers and their deleted
tests.

* feat(sync): affectedEntities multi-entity conflict detection for atomic completion

WIP checkpoint of the atomic completeProject approach. The Batch op declares
every touched entity (PROJECT, INBOX, TASKs, TODAY_TAG) via a new
affectedEntities field threaded through op-log capture, conflict detection,
the sync server (+Prisma migration) and shared-schema. Per-effect
completeProject handlers (issue two-way-sync, time-block, repeat-cfg)
re-derive the task changes the atomic op bypasses.

* revert(sync): remove affectedEntities multi-entity conflict detection

Reverts 0893a86162. The affectedEntities feature existed solely to make the
atomic completeProject Batch op sync-correct (its only producer). Decoupling
project completion into normal per-task ops (next commit) makes the existing
per-entity conflict detection and effects fire naturally, so this entire
layer — sync-core, super-sync-server, shared-schema, op-log plumbing, the
Prisma migration, and the per-effect completeProject listeners — is no longer
needed. Preserved in history via the checkpoint commit.

* refactor(project): decouple completion from task resolution (Option C)

Completion was an atomic multi-entity Batch op (completeProject) that marked
tasks done / moved them to Inbox inside the project-shared meta-reducer.
Because it bypassed the normal per-task actions, every downstream consumer had
to be taught about it separately — conflict detection (the affectedEntities
feature, reverted in the previous commit), native-reminder cancellation, issue
two-way-sync, time-block and repeat-cfg effects.

Decouple instead: completion is now a plain single-entity PROJECT flag flip
(completeProject = OpType.Update, mirroring archiveProject). Unfinished-task
resolution runs first as the normal per-task actions (moveToOtherProject /
updateTask isDone) from the completion flow, so the existing effects and
per-entity conflict detection fire naturally — no special-casing anywhere.

- project.actions/reducer: restore plain completeProject action + on() handler
- project.service: complete() is a flag dispatch; restore moveTasksToInbox /
  markTasksDone (normal per-task dispatch + Rule #6 flush)
- work-context-menu: resolve unfinished work before the flag flip
- drop the completeProject meta-reducer block, the Batch action, the
  TASK_SHARED_COMPLETE_PROJECT op code, and the reminder-cancel effect
  (unscheduleDoneTask$ already cancels native reminders on the normal path);
  current-task clearing is covered by the existing task-internal effect

Net: ~190 LOC removed here on top of ~1565 (affectedEntities + a Prisma
migration) in the revert. Completion's task resolution is not undone either
way, so the atomic bundle never bought a clean reversal.

* docs(project): record decoupled-completion decision (ADR #5)

Document why project completion uses decoupled per-task resolution + a plain
single-entity flag flip instead of an atomic multi-entity op: the atomic op
forced a cross-stack affectedEntities conflict-detection feature and per-effect
listeners, for an undo guarantee it never delivered. Adds ARCHITECTURE-DECISIONS
#5 and a revision note + corrected undo/bulk-mechanic notes in the plan doc.

* test(project): pin completion ordering + resolution edge cases

Address multi-agent review of the decoupled-completion refactor:
- assert resolution (moveTasksToInbox / markTasksDone) runs BEFORE the
  completeProject flag flip (toHaveBeenCalledBefore) — the core invariant of
  the decoupled design that was previously not pinned
- cover the not-done branch of moveTasksToInbox (no setUnDone) and assert
  markTasksDone dispatches exactly the passed set
- add explicit PCO encode/decode round-trip assertions
- document the inbox-path current-task carry-forward nuance in ADR #5

Composition is covered end-to-end by e2e/tests/project/project-completion.spec.ts.

* refactor(project): tighten completion flow per review

- collapse 3x getCompletionInfo() to <=2: gate the resolve prompt once,
  recompute only after a resolution; each call now wrapped with an error
  snack so a failed archive load no longer aborts silently
- drop the dead post-confirm re-prompt branch (unreachable between two
  sequential single-user modals)
- reuse getDiffInDays + dateStrToUtcDate in completion-stats util instead
  of hand-rolled local-midnight/duration helpers
- use a Set for the top-level-task membership check (was O(n*m))
- drop redundant inline dialog sizing; the panelClass owns fullscreen
- remove dead --project-complete-accent test assertion

* refactor(project): finish review follow-ups for completion flow

- W3: reset the celebration confetti instance on dialog destroy so its
  rAF loop + window resize listener are torn down when the dialog closes
  before the animation ends (ConfettiService now returns the handle and
  fires without awaiting completion)
- S2: extract resolveBgImageToDataUrl() shared by app.component and the
  celebration dialog (was duplicated file://->data-url resolution)
- S3: split completeProject() into _getCompletionInfoOrNotify (dedupes the
  error handling), _promptResolveUnfinishedTasks and _confirmCompletion
- W2: keep prefers-reduced-motion gating app-wide (a11y) + document intent

* fix(project): close confetti teardown race on early dialog close

If the celebration dialog is dismissed while canvas-confetti is still
loading, the instance was assigned after ngOnDestroy ran, so reset() never
fired and the rAF loop + resize listener leaked. Guard with an _isDestroyed
flag and reset the instance immediately if it arrives post-destroy.

Also drop the now-dead CanvasConfetti type alias (superseded by
ConfettiInstance, zero references).

* docs(project): drop non-existent undo-snack from completion wiki

* refactor(project): extract completion task-tree and dialog helpers

* refactor(project): hide Archive menu item; Complete is the retire path

* refactor(project): drop dead archive(), reuse resolve-choice type

Multi-review follow-ups on the completion feature:
- Remove orphaned ProjectService.archive() (+ unused import, spec) — the
  menu collapsed Archive into Complete, leaving no caller. The
  archiveProject action/reducer stay for op-log decode of historical ops.
- Reuse the exported ResolveUnfinishedTasksChoice type instead of
  re-spelling the union three times in work-context-menu.
- Fix misleading moveTasksToInbox comment (setUnDone re-opens, not move).
- Note the as-shipped deviations (no extra selectors, no celebration
  effect) in the design plan so they aren't hunted for later.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-08 13:43:38 +02:00
Johannes Millan
315c900391
fix(boards): show tasks from sidebar-hidden projects (#8021)
* fix(boards): show tasks from sidebar-hidden projects

* docs(projects): clarify sidebar hidden wording

* fix(nav): restore project visibility affordance
2026-06-05 12:10:14 +02:00
Johannes Millan
34af7b1036 feat(projects): polish archive flow per #7748
- Confirm dialog for archive (replaces snack+UNDO); navigation in
  work-context-menu now awaits confirmation.
- Archived-projects rows link to /project/<id>/tasks; project view
  shows an inline restore notice when the active project is archived.
- Unarchive snack split: when the project is still hidden from the
  menu, offer a "Show in menu" action; otherwise keep UNDO.
- Terminology: snack now reads "Project restored" to match the button.
- Tests: spec for archived-projects-page and project-task-page;
  defensive archive-filter coverage on selectAllTasksWithReminder /
  selectAllTasksWithDeadlineReminder / selectOverdueTasks; service
  spec covers confirm/cancel paths and the hidden-from-menu branch.
2026-05-23 16:36:04 +02:00
Alberto Avon
b945091b83
Archive project - Continuation of #7162 (#7639)
* feat(project): add archive project UI

Allow users to archive completed projects via the project context menu.
Archived projects are hidden from the sidebar and visibility menu, but
retained with all their tasks. They can be unarchived at any time from
the visibility menu (eye icon), where they appear below a divider with
an unarchive icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(project): add unit tests for archive/unarchive project feature

- projectReducer: verify archiveProject sets isArchived=true, unarchiveProject
  sets it back to false, and that other projects are not affected
- WorkContextMenuComponent: verify archiveProject() calls the service, shows
  a snack, navigates away when the archived project is currently active, and
  does not navigate otherwise

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(project): document archive/unarchive project feature

Add "Archiving Projects" section to the Project View wiki page explaining
how to archive a completed project and how to restore it via the visibility menu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(project): new archived projects page

* feat(project): improve archive UX with undo snack and hidden-menu warning
- Replace plain archive notification with icon + undo action so accidental archiving is recoverable without a confirmation dialog
- Add snack on unarchive; warn when project is still hidden from menu (isHiddenFromMenu=true) so users know why it won't appear in sidebar

* feat(reminder): suppress reminders for archived project tasks

* feat(tasks): hide archived project tasks from tags, today, and boards

* feat(tasks): hide archived project tasks from overdue tasks list

* fix(repeatTasks): exclude TaskRepeatCfg of archived projects when generating the next instance

* fix(tasks): hide Task and TaskRepeatCfg of archived project from the /scheduled-list page

This effectively hides the "Overlaps with another scheduled task" message if the other task belongs to an archived project.

* feat(docs): Add documentation for archived projects

* fix: revert changes to the it.json file

* feat: hide all task-repeat-cfg from planners and introduce a new task-repeat-cfg selector selectActiveTaskRepeatCfgs

* feat(tasks): add base selector for filtering tasks from active projects

* feat(ui): add count of archived project in project visibility menu and hide the archived projects page link when none exist

* feat(ui): improve accessibility for archived projects page by adding aria-labels and aria-hidden attributes

* perf(projects): use a sorted selector in archived projects page to avoid sorting during user's search

* refactor(tasks): derive selectAllUndoneTasksWithDueDay from the common shared selector

* refactor(tasks): introduced shared selectors to centralize active projects filtering

* fix: add mock selector for selectUndoneOverdueDeadlineTasks in plan view tests

* refactor(planner): use selectAllTasksInActiveProjects to exclude archived project tasks

Replaces direct task state access with the shared selectAllTasksInActiveProjects selector, which already filters archived project tasks. Fixes archived project tasks appearing in planner view.

Test mock stores updated to include tasks+projects slices (required by new selector chain). Removed "missing entity references" test: that guard now lives upstream in selectAllTasks.

* refactor(work-context): exclude archived project tasks from TAG/TODAY and SCHEDULE

selectActiveWorkContext and selectTodayTaskIds were using raw
taskState.entities, causing archived-project tasks to appear in tag boards and Today.

Fix by filtering entities in the selectors using selectArchivedProjectIds before calling computeOrderedTaskIdsForToday / computeOrderedTaskIdsForTag.

Apply the same logic to selectTimelineTasks and remove the
combineLatest workaround added in 9d6fe5e.

* fix(work-context): exclude archived project's tasks when selecting tasks in schedule page

* fix(tasks): exclude archived project tasks from unplanned deadline banner

* fix: fix tests after rebase onto master

* fix: stabilize selectArchivedProjectIds Set instance

Derive selectArchivedProjectIds from a base selectArrayOfArchivedProjectIds selector to avoid recreating the Set whenever unrelated project properties change

* perf(selectors): avoid unnecessary recomputations from archived project selectors

selectArchivedProjectIds was creating a new Set on every project-state update, causing unnecessary recomputation across downstream selectors even when archived projects did not change.

- Add selectArrayOfArchivedProjectIds as a stable intermediate selector
- Rebuild selectArchivedProjectIds only when archived IDs change
- Memoize active-project task entities instead of filtering inline
- Remove unused selectTaskFeatureState dependency from selectActiveWorkContext

* fix(project): prevent inbox project to be archived

* fix(task): add filters for non-archived projects and comment explaining why filter was not added in selectors

* fix: always call archive service before navigateByUrl when triggering archive action

* feat(ui): add undo action on project restored from archive snack

* fix: add guards against empty projectId in tasks

* fix(tasks): keep orphaned subtasks scheduled in selectLaterTodayTasksWithSubTasks

Restored selector to its original form, but reading tasks from selectAllTasksInActiveProjects instead of selectTaskFeatureState.

While categorizing tasks, group subtasks by their parentId in a single pass to avoid calling mapSubTasksToTask that requires TaskState.

* refactor(tasks): compose selectAllTasksWithoutHiddenProjects from selectAllTasksInActiveProjects + new selector for hidden projects

* test(tasks): restore coverage gaps from selector refactors

- selectAllUndoneTasksWithDueDay: add "should include subtasks with dueDay" (dropped when selectAllTasksWithDueDay was removed in 450855b)
- selectAllTasksDueToday: add "should handle missing entity references in planner days with empty task state gracefully" (replacing the taskState.ids variant removed in 9280b85 since upstream guard now owns that invariant)

* feat(tasks): extracted a new selector selectActiveTaskMap that returns a memoized map of active tasks indexed by id

* fix(tasks): removed unused selector

* refactor(projects): move archive/unarchive snacks inside project service and drop the UNARCHIVED_BUT_HIDDEN message

* refactor(projects): use only filtered sources in selectTimelineTasks

* refactor(tasks): rename selectActiveTaskMap into selectMapOfAllTasksInActiveProjects to better align with current naming conventions

* feat(a11y): add aria label to project archive elements

* chore: add comment to selectAllTasksInActiveProjects selector's fast path

* test: add expectation on operation order in archive project action

Navigation must happen after the ProjectService.archive() call

---------

Co-authored-by: Symon <peterbaikov12@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 13:24:14 +02:00
Corey Newton
5f1dd7ef7f docs(wiki): normalize tables and rework ToC 2026-02-04 14:45:00 +01:00
Corey Newton
8118f7cada
docs/wiki content v0.3 (#6231)
* docs(wiki): add "views"

* docs(wiki): fix MD compliance (inline HTML)

* docs(wiki): local style linting
2026-01-28 16:11:22 +01:00