Files
multica/packages
Naiyuan Qing 40da795f6c perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#5349)
* perf(inbox): virtualize notification list (MUL-4474)

The inbox notification list rendered every item at once. Each row mounts an
avatar + hover card, so a long inbox inflates the tab-switch commit — the
same render-amplifier class this issue targets.

Extract an InboxList component that virtualizes the rows via react-virtuoso
(customScrollParent over the existing overflow-y-auto element, same pattern
as the issue-detail timeline). Only the visible window plus a small overscan
is mounted; everything else — selection, hover, archive, scroll semantics,
the row component and callbacks — is unchanged. Virtualization changes
exactly one thing: whether an off-screen row is in the DOM.

Slice 2a of MUL-4474 (inbox is the no-DnD surface, done first to prove the
Virtuoso + scroll + keyboard harness before the drag surfaces). Draft: must
pass the manual zero-functional-change pass on a real Desktop build before
merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* perf(board): virtualize board columns (MUL-4474)

Each board column rendered every card at once; cards carry pickers, avatars,
and a per-issue activity indicator, so a tall column inflates the tab-switch
commit. Virtualize the cards within each column via react-virtuoso, using the
column's own scroll container as customScrollParent.

The dnd-kit droppable stays on the always-mounted column scroll container
(merged callback ref feeds both dnd-kit and Virtuoso), and SortableContext
still wraps the full id list. So cross-column drops (status/assignee change)
and reorder among on-screen cards are unchanged; reordering to an off-screen
target relies on drag auto-scroll to mount it — the documented virtualization
tradeoff, to be confirmed in the manual pass. The infinite-scroll sentinel
rides Virtuoso's Footer slot so loadMore still fires at the bottom, and a
per-item pt-2 reproduces the previous space-y-2 gap with padding inside the
measured item box.

issues-page.test.tsx: mock react-virtuoso to render items inline (jsdom has no
layout), and make the useDroppable mock's setNodeRef referentially stable to
match real dnd-kit — the board's merged customScrollParent ref would otherwise
loop on a fresh ref each render.

Slice 2b of MUL-4474 on the shared inbox/list/board/swimlane branch. Draft:
requires the manual zero-functional-change pass on a real Desktop build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* perf(list): virtualize issue list rows (MUL-4474)

The status-grouped list rendered every row in every expanded section at once;
each row carries a sortable, context menu, tooltip, and activity indicator, so
a long list inflates the tab-switch commit. Virtualize each expanded section's
rows with react-virtuoso, all instances sharing the page's single scroll
container as customScrollParent.

Everything structural is preserved by construction: the Base UI accordion,
sticky status headers, collapse, the per-section useDroppable, the per-section
SortableContext, and the load-more sentinel (now Virtuoso's Footer). The
Virtuoso only mounts for an expanded section (a collapsed/hidden panel has no
viewport to measure). Virtualization changes exactly one thing: whether an
off-screen row is in the DOM.

issue-surface.test.tsx: mock react-virtuoso inline (jsdom has no layout) so the
surface-level loading-semantics assertions still observe the list's rows.

Slice 2c of MUL-4474 on the shared branch. Draft: requires the manual
zero-functional-change pass on a real Desktop build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* perf(swimlane): virtualize lanes (MUL-4474)

The swimlane rendered every lane (each a full row of status cells) at once.
Virtualize the vertical lane axis with react-virtuoso over the board's outer
scroll box (customScrollParent), so only on-screen lanes stay mounted.

Behavior is preserved: pinned lanes keep their leading position, the
SortableContext still wraps the lane set for grip-drag reorder (its items are
only the non-pinned lane ids), per-cell droppables and per-cell card
SortableContexts are unchanged (cells live on mounted lanes), the sticky status
header stays above the list, and the per-status load-more sentinels ride
Virtuoso's Footer. pt-4 per lane reproduces the previous gap-4.

swimlane-view.test.tsx: mock react-virtuoso inline so the ~47 lane/cell/DnD
assertions still see the lanes the virtualized list renders.

Slice 2d of MUL-4474 on the shared branch — this completes the four surfaces
(inbox/board/list/swimlane). Draft: requires the full manual
zero-functional-change pass on a real Desktop build before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): don't pass undefined to Virtuoso `components` (MUL-4474)

react-virtuoso seeds its `components` prop with an internal `{}` default;
passing `components={undefined}` (which the list and board did when there was
no Footer — hasMore false / no column footer) overwrites that default with
undefined, so Virtuoso's startup destructure of `EmptyPlaceholder`/`Footer`
throws and the surface crashes. jsdom tests mock react-virtuoso so this only
surfaced on a real Desktop build (found in manual perf testing).

Return a stable module-level empty object instead of undefined. Inbox (omits
the prop entirely) and swimlane (always supplies a Footer) never hit this and
are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-14 16:34:30 +08:00
..