Skip to content

fix: let users switch a merge to a replace after the fact; v1.7.1#24

Merged
joaner merged 4 commits into
ioai-tech:mainfrom
joaner:fix/merge-replace-toast
Jul 9, 2026
Merged

fix: let users switch a merge to a replace after the fact; v1.7.1#24
joaner merged 4 commits into
ioai-tech:mainfrom
joaner:fix/merge-replace-toast

Conversation

@joaner

@joaner joaner commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Splits the 1400+ line RosViewerImpl.tsx into focused, independently testable hooks/components (useDatasetSession, usePlayerLifecycle, useRecordingSourceActions, useDatasetHistory, useThemeLanguagePreferences, useOpenFeedback, RosViewerLandingScreen, sourceLoading, rosViewerUtils), leaving RosViewerImpl.tsx as a thin composition layer.
  • Fixes a production bug where reopening a recording via a remembered File System Access permission (?url=file://...) could self-trigger an infinite init loop (flickering sidebar/player, repeated WorkerSourceCancelledError, occasional tab crashes). Root-caused via useEffectEvent in useSpaUrlBootstrap plus new pure helpers resolveAppendGroupId/resolveActiveId in datasetSources.ts (with regression tests).
  • Adds the ability to undo a multi-source merge: opening more files while a session is active still merges them in immediately (non-blocking), but now shows a dismissible toast ("Merged X into the current session") with a "Switch to replace instead" action across every add-file entry point (drag-and-drop, Navbar Open File/Open Directory, the hidden file input). Clicking it detaches the just-added files into their own fresh session and switches to it; the original session is left intact and reachable from the sidebar Data tab.
  • Toast layout: message stacked above a full-width action button instead of sonner's default cramped side-by-side row (scoped to this toast only).
  • Bumps package.json version to 1.7.1.

Test plan

  • npm run lint
  • npm run typecheck
  • npm test (595 unit tests)
  • npm run build (SPA) and npm run build:lib
  • npm run gen:e2e:fixtures && npm run test:e2e (37 Playwright tests, incl. new merge/replace-toast coverage in multi-sources.spec.ts)

joaner added 4 commits July 9, 2026 21:06
… loop

Reopening a recording via remembered File System Access permission
(?url=file://...) could self-trigger forever: replaying history set
activeId, which changed appendFilesAsDatasets's identity, which re-ran
the SPA bootstrap effect that depends on it, which replayed history
again. Symptoms were a flickering sidebar/player and hundreds of
WorkerSourceCancelledError as the player was torn down and rebuilt in
a tight loop, occasionally crashing the tab. Direct drag-and-drop was
unaffected since it's a one-shot event handler, not an effect.

Root-caused and fixed at the design-pattern level rather than patched:
- useSpaUrlBootstrap wraps its handlers in React's useEffectEvent, so
  its effect only ever depends on (active, url) - no callback identity
  churn can retrigger it. react-hooks/exhaustive-deps now errors if
  this guarantee is ever violated, turning the bug class into a lint
  error instead of a silent regression.
- resolveAppendGroupId/resolveActiveId (datasetSources.ts) make the
  "which group is really active" logic pure and directly testable,
  fixing the underlying activeId "orphan id" bounce.
- Regression tests pin both the pure logic and the hook's Effect
  timing (useSpaUrlBootstrap.test.tsx feeds in a worse-than-production
  case: a fresh handler identity on every render).

Also splits the 1400+ line RosViewerImpl.tsx into focused, independently
testable hooks/components (useDatasetSession, usePlayerLifecycle,
useRecordingSourceActions, useDatasetHistory, useThemeLanguagePreferences,
useOpenFeedback, RosViewerLandingScreen, sourceLoading, rosViewerUtils),
leaving RosViewerImpl.tsx as a thin composition layer. Fixed a latent bug
found during the split: the hidden #rosview-inline-file/-tar inputs must
render unconditionally even once a player exists, or history replay can't
click them.
Opening more files while a session is active still merges them in
immediately and non-blocking, but now shows a dismissible toast
("Merged X into the current session") with a "Switch to replace
instead" action across every add-file entry point (drag-and-drop,
Navbar Open File/Open Directory, the hidden file input). Clicking it
detaches the just-added files into their own fresh session and
switches to it; the original session is left intact and reachable
from the sidebar Data tab rather than being destroyed.

- appendFilesAsDatasets (useDatasetSession.ts) now returns
  { groupId, merged, addedItemIds } instead of void, without touching
  how it reads activeId/datasets (still via refs, so its identity
  stays stable) or its own dependency array.
- notifyMergeWithUndo (useRecordingSourceActions.ts) reuses the
  already-available offlineIntl/setExtraDatasets/setActiveId to show
  the toast and perform the undo; wired into the four non-forced-new-
  session entry points. Tar extraction and history replay are
  unambiguous "open a new session" actions already and are untouched.
- Extracted fileBatchDisplayName (rosViewerUtils.ts) for the "first
  +N" label shared by history recording and the new toast.
sonner's default toast layout puts the action button to the right of
the message in a single row, which read as cramped once the message
wrapped to two lines. Scope a classNames override to just this toast
(message stacked above a full-width action button) via Tailwind's
important-modifier utilities, since sonner's own attribute-selector
base styles set align-items/gap/margin directly and would otherwise
win on specificity. Scoped per-toast rather than globally, so plain
toast.error()/toast.success() calls elsewhere are unaffected.
@joaner joaner merged commit ecd6324 into ioai-tech:main Jul 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant