Skip to content

feat(webapp): filter the board and keep its state in the url - #536

Merged
NathaelB merged 1 commit into
mainfrom
feature/planification-ws6
Sep 18, 2026
Merged

NathaelB merged 1 commit into
mainfrom
feature/planification-ws6

Conversation

@NathaelB

@NathaelB NathaelB commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Closes #467. Refs #461. The last of seven. A member narrows the board to one project — or one assignee, one label, one search — and the narrowed board is a URL they can bookmark and share.

Written on top of WS5's branch (#517, since merged), which is where the board itself lives. Now replayed onto main with git rebase --onto, so it carries this workstream's single commit and nothing of WS5's — the conflicts GitHub reported came entirely from the stale base, and a plain git rebase main would have tried to replay WS5 over its own squash-merge. Re-verified after the rebase: 180 files, 1864 tests, all green, and the filtered drop/rollback guard re-run on its own against main's copy of the board.

The trap this workstream had to avoid

useMoveBoardTask writes its optimistic update straight into useBoardTasks's cache entry — that is why the board has its own query instead of reusing useRootTasks, and WS5 said so in a doc comment. Add filters without putting them in the query key and the optimistic write lands on the wrong entry: cards jump or vanish on drop, and the existing rollback test does not catch it, because it runs unfiltered.

Both hooks now build their request from one boardTasksRequest(organizationId, filters), so the write names the entry the screen reads.

This is verified by mutation rather than by inspection: pointing the mutation's key at empty filters makes exactly one test fail — moves optimistically and rolls back, filters and all — and leaves the other 85 in the module green. That is the proof the new test is load-bearing and that the old one would not have covered it. Reproduced independently before merge.

Every filter is server-side

No client-side filtering of a fetched page: a filter that only hides loaded rows lies about the column counts, which is the one number a board is read for. The only .filter() over the fetched tasks groups them into their five columns by status — presentation, not filtering.

The bar: project picker first and visually dominant, then assignee, then label, then search, plus an "unscheduled only" toggle. customer_id is accepted by the endpoint but has no control, because #467 did not ask for one; it would drop straight into the schema.

The URL is the single source of truth

boardSearchSchema follows planningSearchSchema's register — every field carries .catch(...), plus an outer .catch so "never throws" holds even for a non-object search. A malformed value falls back on its own field only: ?project_id=project-1&unscheduled=peut-etre&q= validates to { project_id: 'project-1' } and the board renders.

Two of the folds are behavioural rather than cosmetic, and both come from a WS4 rule that is invisible in the client's types:

  • q= present-but-empty would match every title and widen the listing;
  • unscheduled=false would widen it for nothing.

Both fold to undefined, so unscheduled is a literal(true): the toggle is either on and in the address, or off and absent.

A cleared board leaves a clean address. EMPTY_BOARD_FILTERS carries all five keys explicitly undefined and "Effacer les filtres" navigates with that object rather than {} — a key present-and-undefined is dropped from the URL, a key merely omitted keeps its old value. Asserted as searchStr() === ''.

Search commits on submit, not per keystroke: q lives in the address, and one history entry per character would defeat the back button. The input is uncontrolled and keyed on the applied q, so back and forward still reset it — the half-typed word is DOM state, the applied filter is the URL.

The widening, stated rather than hidden

WS4's GET /tasks lists roots when no filter is present and every matching task at any depth as soon as one is. Deliberate — a project's board must show its subtasks — but it means applying a filter moves the column counts, which would read as a bug to anyone watching.

So the board says it: while any filter is on, the toolbar states that matching subtasks are shown as cards alongside root tasks. The test harness implements the widening rule independently of the app, so the behaviour is exercised rather than assumed — unfiltered, En cours holds one card; filtered on a project, two, the second a subtask of a Backlog card.

Verification

pnpm check   # 547 files, no fixes applied
pnpm test    # 1775 passed, 180 files — all green
pnpm build   # ✓ built

Baseline on WS5's branch was 1744 across 177 files: +3 files, +31 tests, nothing broken.

Note for whoever runs this on a loaded machine: main currently fails about five tests under load — planning-calendar-feature, task-sheet-feature, absences-overview-feature, team-list-feature, workflow-canvas-feature — each passing in isolation. That flakiness predates this chantier and is not this branch's.

Deviations

  • ui/board-ui.tsx gains two optional props, toolbar and emptyReason. Not in this workstream's declared file list, though not forbidden either: the bar has to live inside the page shell WS5's UI owns, and an empty filtered board needs to say why it is empty. No behaviour change when both are omitted.
  • Native <select> for the three pickers, not the shadcn combobox and not planning/ui/assignee-picker.tsx — that one is a multi-select popover built for the task form, and the board wants one value at a time. projects/ui/project-form-dialog.tsx is the precedent for a plain select, and the reason is in the component's doc comment.

Open

  • The project picker loads projects capped at 100 by useProjects. Past that the picker truncates — the same cap the projects list already documents as out of scope. A deep link to an unlisted project still filters correctly, but the picker reads "Projet introuvable".
  • The empty-with-reason message does not distinguish "project deleted" from "project belongs to another organization". Both read as introuvable, which is all the client can honestly tell from an empty listing.

@codspeed

codspeed Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Congrats! CodSpeed is installed 🎉

🆕 11 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


Open in CodSpeed

Base automatically changed from feature/planification-ws5 to main September 18, 2026 12:37
@NathaelB
NathaelB force-pushed the feature/planification-ws6 branch from ff1b29d to ffd55ba Compare September 18, 2026 12:40
@NathaelB
NathaelB merged commit 50fe683 into main Sep 18, 2026
7 checks passed
@NathaelB
NathaelB deleted the feature/planification-ws6 branch September 18, 2026 12:42
@NathaelB

Copy link
Copy Markdown
Contributor Author

🚀 Preview deployed: https://pr-536.mestier.fr

Synced revision <no value>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:frontend Frontend React (webapp) mvp Périmètre MVP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(webapp): WS6 — board filters and project focus, state in the URL

1 participant