Repository navigation
Conversation
ab7762f to
bd46e34
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
There was a problem hiding this comment.
🟡 Changes recommended
Active findings identify missing pressed state on first-row activation, lost focus during nested mutation redraws, and stale aria-rowindex values when structural and keyed mutation scopes are combined.
3 open issues
What changed in this PR
This second phase of DREAM-959 builds on #26503 by routing work-package table row changes through one mutation protocol, coordinating repainting, keyboard navigation, and focus restoration.
Changes:
- Adds nested mutation handling, shared row keys, and a
committedRowssignal. - Migrates rendering, selection, editing, inline creation, totals, and drag-and-drop updates to the protocol.
- Extends regression coverage for scoped repaints, pending renders, and focus recovery.
| File | Description |
|---|---|
| frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts | Routes header updates through mutations. |
| frontend/src/app/features/work-packages/components/wp-table/wp-table.component.spec.ts | Tests header and inline-create mutations. |
| frontend/src/app/features/work-packages/components/wp-table/wp-table-sums-row/wp-table-sums-row.directive.ts | Migrates totals updates to mutations. |
| frontend/src/app/features/work-packages/components/wp-table/wp-table-sums-row/wp-table-sums-row.directive.spec.ts | Verifies totals mutation ordering. |
| frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.component.ts | Wraps inline-create row changes in mutations. |
| frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.component.spec.ts | Tests focus restoration after reset. |
| frontend/src/app/features/work-packages/components/wp-fast-table/wp-fast-table.ts | Centralizes mutation settlement and render commits. |
| frontend/src/app/features/work-packages/components/wp-fast-table/wp-fast-table.spec.ts | Tests nested mutations and commit signals. |
| frontend/src/app/features/work-packages/components/wp-fast-table/wp-fast-table-lifecycle.spec.ts | Covers pending renders and drag-handle timing. |
| frontend/src/app/features/work-packages/components/wp-fast-table/testing/table-harness.ts | Adds frame control and mutation helpers. |
| frontend/src/app/features/work-packages/components/wp-fast-table/semantics/treegrid-semantics.spec.ts | Observes semantics through mutation completion. |
| frontend/src/app/features/work-packages/components/wp-fast-table/semantics/table-row-positions.ts | Exposes position-withdrawal state. |
| frontend/src/app/features/work-packages/components/wp-fast-table/semantics/table-row-positions.spec.ts | Updates position and refresh regression tests. |
| frontend/src/app/features/work-packages/components/wp-fast-table/semantics/row-state-painter.ts | Preserves positions for occurrence-key scopes. |
| frontend/src/app/features/work-packages/components/wp-fast-table/semantics/row-classifier.ts | Uses shared edge-row keys. |
| frontend/src/app/features/work-packages/components/wp-fast-table/row-mutations.ts | Implements nested scope tracking and settlement. |
| frontend/src/app/features/work-packages/components/wp-fast-table/row-mutations.spec.ts | Tests nesting, exceptions, and ownership capture. |
| frontend/src/app/features/work-packages/components/wp-fast-table/row-keys.ts | Defines shared row-key constants. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/selection-transformer.ts | Limits repaints to changed selection and focus markers. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/selection-transformer.spec.ts | Tests scoped selection and focus repaints. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/keyboard/navigable-rows.ts | Re-exports shared navigation row keys. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/keyboard/keyboard-navigation-transformer.ts | Receives synchronous mutation notifications. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/keyboard/keyboard-navigation-transformer.spec.ts | Covers Tab stops and pending-render focus recovery. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/keyboard/cell-interaction.spec.ts | Checks repaint ordering before focus returns. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/drag-and-drop-transformer.ts | Wraps staged moves and rollback in mutations. |
| frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/drag-and-drop-transformer.spec.ts | Verifies staging and rollback mutations. |
| frontend/src/app/features/work-packages/components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-render-pass.ts | Guards current passes and mutates handle replacements. |
| frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts | Mutates edited cells before returning focus. |
Review effort: Balanced
2a14f3c to
8e9b1fc
Compare
eac83f4 to
bd828db
Compare
bd828db to
bb23b26
Compare
Moves the edge-row keys (header, totals, inline-create, empty results) into one row-keys module shared by the classifier and the keyboard navigation, so later producers address rows in one vocabulary. Adds a holdFrames verb to the table harness that intercepts the table's frame scheduling and commits held frames on demand, replacing the per-spec requestAnimationFrame and uiWork.frame spies. https://community.openproject.org/wp/DREAM-959
Adds RowMutations, which tracks how deeply row changes are nested and widens the paint scope of the outermost mutation, so nested changes paint once at the outermost exit. The exit runs in a finally block, so a throwing change still paints and releases the mutation. Entry reports whether ownership should accumulate, which is the case while an outer mutation or a pending render is open. https://community.openproject.org/wp/DREAM-959
Adds WorkPackageTable.mutate(scope, change): it captures focus ownership, runs the change, paints the scope, notifies keyboard navigation synchronously and bumps a committedRows signal. Nested calls widen the outermost scope and settle once. The full render commits through the same exit with a full flag, collapse and group toggles mutate the whole table because the owner row's expansion state changes along with its hidden descendants, and refreshRows mutates the work package's occurrence keys. Keyboard navigation is told directly through rowsChanged instead of subscribing to tableRendered and rowsRefreshed$, and captureOwnership takes an accumulate flag from the mutation depth in place of the microtask latch. The sums directive wraps its refresh and clear in a mutation so ownership is captured before the row changes. Keyed scopes of ledger occurrence keys keep carried row positions; empty sets, edge-row keys and full scopes recompute positions from the DOM order, so structural changes still withdraw or renumber. The harness gains nextMutation() for specs that waited on the removed stream. https://community.openproject.org/wp/DREAM-959
Covers the full-render token with specs: only the latest of two redraws in one frame commits, a group collapse or a resource refresh while a render is pending rebuilds the draft so the commit carries current content, a destroyed table drops its token and commits nothing, and a mutation raised by a render consumer settles after the commit as its own mutation. Makes refreshRows update the indexed row object and rebuild a pending draft, and makes destroy clear the token. https://community.openproject.org/wp/DREAM-959
Converts the inline-create component and the column header render to table.mutate with their own row keys, so the create row and the header are painted and keyboard navigation is told after each change without the component calling the painter or capturing ownership. Deletes notifyRowsRefreshed and reconcileSection, whose callers all mutate now. https://community.openproject.org/wp/DREAM-959
Wraps the edit reset's cell refresh in a table mutation, so the row is repainted and keyboard navigation is told before the form returns focus to the cell. Keeps the ownership captured at the start of a full render while the render is being scheduled: the editing reset inside it now mutates, and its own capture must accumulate rather than overwrite, or focus in an editor overlay retired by the render would not come back. https://community.openproject.org/wp/DREAM-959
Wraps the drag-handle swap and the drag-and-drop staging and rollback moves in table mutations. The handle pass runs only while its render pass is current: the draft it started from may still be waiting for its frame, so the guard accepts the pending draft and rejects only a superseded one. Keeps the Tab stop where it is when a mutation settles without a recorded focus position: a staged reorder no longer moves it to the new first row. https://community.openproject.org/wp/DREAM-959
Diffs the selected work package ids and the focused work package against what was painted last, maps the difference through the ledger's occurrence keys and mutates that scope, falling back to the whole table when more than half the rows change (select all). A render resets the baseline because the render path paints every row. Measured in Chromium with the render benchmark at 1000 rows: one selection toggle drops from 8.5 ms to 5.1 ms. The remaining cost is keyboard navigation re-classifying the rows on every mutation. https://community.openproject.org/wp/DREAM-959
Adds `WorkPackageTable#mutateWorkPackages`, which resolves the rendered occurrences of the given work packages itself, and moves `keysOf` from the row-state painter to the occurrence ledger, where the occurrences live. The table edit form and the selection transformer tell the table which work packages changed instead of asking the painter for keys and handing them back. https://community.openproject.org/wp/DREAM-959
bb23b26 to
91a86da
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |

Note
This PR is stacked on #26503 (DREAM-959 phase 1). Please review/merge that PR first.
Ticket
https://community.openproject.org/wp/DREAM-959
What are you trying to accomplish?
tl;dr for humans: Every change to table rows now goes through one entry point, so screen-reader state and keyboard focus are updated in the same order every time, and small changes no longer repaint the whole table.
Second phase of DREAM-959:
table.mutate(scope, change)captures focus ownership, runs the change, paints the scope, tells keyboard navigation synchronously and bumps acommittedRowssignal. Producers state what changed; none of them paints, captures or notifies any more. No user-visible change.What approach did you choose and why?
A small
RowMutationsmodule tracks nesting: nested calls widen the outermost scope and settle once, in afinally. The full render keeps its pending-render token and commits through the same exit. The formerrowsRefreshed$stream,notifyRowsRefreshed,reconcileSectionand the microtask latch incaptureOwnershipare gone; keyboard navigation is told directly withrowsChanged.holdFrames()/nextMutation()RowMutations,table.mutate,committedRows, directrowsChangednotify; full render, collapse andrefreshRowsconvertedFocus ownership is captured on every mutation entry, not only the outermost one: a nested entry adds to the outer capture instead of replacing it, and a render being scheduled counts as pending from its first capture. So if focus enters the table while a change is already running (or an editor overlay is retired by the render that reset it), focus is still restored afterwards instead of landing on the page body. The design review asked for this; the
keeps ownership captured…,captures ownership on every entry…andreturns to the origin when a full render retires an editor…specs pin it.Keyed scopes of ledger occurrence keys keep carried row positions; an empty set, an edge-row key or
'all'recomputes positions from the DOM order, so structural changes still withdraw or renumber.Measured in Chromium with the render benchmark at 1000 rows: one selection toggle drops from 8.5 ms to 5.1 ms. The remaining per-mutation cost is keyboard navigation re-classifying the rows in
recover; coalescing that is a follow-up.Follow-ups kept out of this PR: coalescing navigation's row re-classification per mutation; naming the key vocabulary (
OccurrenceKey | EdgeRowKey) and the classifier's edge-key lookup cast, both from the TypeScript audit.Hints for dev review
Start with
wp-fast-table/row-mutations.ts, thenWorkPackageTable.mutate/settle/commitRenderinwp-fast-table.ts, thenrowsChangedandcaptureOwnership(accumulate)inkeyboard-navigation-transformer.ts. The DREAM-886/895 keyboard specs are the regression net for ordering and passed without assertion changes.AI involvement
Collaborative – AI generated a substantial part of the code; I reviewed and understand every line.
Merge checklist