Skip to content

feat(web): Better playlist editing UX — row-click multi-select + bulk actions#14393

Open
dylanjeffers wants to merge 9 commits into
mainfrom
claude/per-row-selection
Open

feat(web): Better playlist editing UX — row-click multi-select + bulk actions#14393
dylanjeffers wants to merge 9 commits into
mainfrom
claude/per-row-selection

Conversation

@dylanjeffers
Copy link
Copy Markdown
Contributor

Summary

Implements the Better playlist editing UX spec for the desktop playlist detail page. While in edit mode, clicking a track row toggles selection (shift+click extends the range), selected rows are visually highlighted, and a bulk-actions bar at the top of the table operates on the selection (remove, copy URLs, undo/redo). All standard editor flows (drag-reorder, single-row remove via overflow menu) keep working.

The branch stacks 8 small commits that build the whole spec:

  1. feat(web): replace quick-create playlist flow with a modal
  2. feat(web): add duplicate playlist flow under the New nav button
  3. feat(web): add tracks to a playlist by pasting Audius URLs
  4. feat(web): copy source tracks when duplicating a playlist
  5. feat(web): inline edit mode on playlist detail page with staged Apply
  6. feat(web): bulk track actions toolbar (select-all, copy URLs, remove, undo/redo)
  7. feat(web): row-click selection on playlist tracks in edit mode
  8. feat(web): highlight selected playlist rows while in edit mode

The interesting moving pieces of the row-click + highlight work:

  • EditAwareTracksTable wraps the standard TracksTable and, while the page is in edit mode, rewrites onClickRow to call selection.toggle(id, index, { shift }). Since TracksTable.onClickRow does not forward the MouseEvent, the wrapper captures shift state from a window keydown/keyup listener.
  • TrackSelectionContext owns the selected-id set and the anchor index for shift-click ranges; toggle() walks [lastIndex..index] against the page's orderedIds for range fills.
  • TracksTable gains an optional, ref-stable rowClassNameAddition(track, index) prop that composes with its existing per-row className (used internally for the locked/disabled states). EditAwareTracksTable uses it to apply a selected class (surface-2 fill + 3px accent bar) for selected rows.
  • Keyboard shortcuts (in TrackBulkActionsBar) only bind while editing this collection: ⌘/Ctrl+A select all, Esc clear, Delete/Backspace remove, ⌘/Ctrl+Z/Y undo/redo.
  • Outside of edit mode EditAwareTracksTable is a transparent pass-through and play-on-click is preserved.

Test plan

  • Open a playlist you own → enter edit mode → click a row → row highlights, bulk-actions bar appears with "1 selected"
  • Click another row → 2 selected; click a selected row again → it deselects
  • Shift+click a row further down → fills the contiguous range, including both endpoints
  • ⌘/Ctrl+A selects all; Esc clears; Delete/Backspace removes selected tracks; ⌘/Ctrl+Z restores them
  • "Copy URLs" copies one URL per line to clipboard; toast confirms count
  • Drag-reorder still works (action buttons + overflow menu don't trigger row selection)
  • Exit edit mode → row click activates playback as before; no highlight class lingers
  • Album page / other collections unaffected

dylanjeffers and others added 8 commits May 15, 2026 11:09
The "New" button in the playlist library sidebar (and the empty-state nav
link) used to immediately create a playlist named "New Playlist" and route
the user into the edit page. Replace that one-shot dispatch with a Create
Playlist modal that lets the user set a title, optional description, and
optional artwork before the playlist is created.

- New `CreatePlaylistModal` slice via `createModal` helper, wired into
  the modals reducer/state.
- New `CreatePlaylistModal` component (Harmony Modal + TextInput +
  TextArea + UploadArtwork) reusing the existing `resizeImage` pipeline.
- The sidebar "New" popup item and the empty-library nav link both open
  the modal instead of dispatching `createPlaylist` directly.
- Playlists still default to private (enforced by the existing
  `optimisticallySavePlaylist` saga) and the saga still routes the user
  to the new playlist page after creation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Duplicate Playlist" secondary action to the sidebar's New (+)
popup menu. Opens a modal where the user pastes any public Audius
playlist URL, sees a preview of the source playlist (title, description,
cover art), and toggles per-field switches to customize what should
differ in the copy. The duplicated playlist is created as private by
default (enforced by the existing optimisticallySavePlaylist saga).

Scope:
- Metadata-only duplication for now. Tracks are not copied — a follow-up
  PR will support full duplicate including track contents. The modal
  surfaces this with a helper line so users know they need to add tracks
  separately.
- Reuses the existing createPlaylist saga: when artwork is not
  customized, we pass the source playlist's cover_art_sizes CID through
  so the saga reuses the cover instead of treating it as a new upload.

Implementation:
- New `DuplicatePlaylistModal` Redux modal slice (createModal helper).
- New `DuplicatePlaylistModal` component (Harmony Modal + TextInput +
  Switch + TextArea + Artwork + UploadArtwork).
- Resolves pasted URL → permalink via `getPathFromPlaylistUrl`, then
  loads the source via `useCollectionByPermalink`.
- Wires "Duplicate Playlist" into `CreatePlaylistLibraryItemButton`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new "Add Tracks by URL" affordance to the owner action row on
playlist detail pages. Clicking it opens a modal where the user pastes
Audius track links — line, comma, or tab separated — and submits to
batch-add them to the current playlist. Albums and DDEX-imported
collections are intentionally excluded.

Implementation details:
- New `AddTracksByUrlModal` Redux modal slice (createModal helper) wired
  through types/parentSlice/reducers/index.
- New `AddTracksByUrlModal` component:
  - Parses pasted text into a deduped list of permalinks via
    `getPathFromTrackUrl`.
  - Resolves them in one round-trip via `sdk.tracks.getBulkTracks`.
  - Filters out tracks already in the playlist, enforces a 100-track
    cap, and reports invalid/unresolved/duplicate/over-limit counts in a
    single summary toast.
  - Dispatches `addTrackToPlaylist` per track with a 30 ms gap so each
    saga's optimistic update reads the previous one's state.
- `addTrackToPlaylist` now accepts `{ silent: true }` so the per-track
  "Added track to playlist" toast can be suppressed during batch adds;
  default behavior (single-track adds elsewhere) is unchanged.
- New `IconLink` button in `OwnerActionButtons` opens the modal,
  prefilled with the current collection id. Hidden for albums and
  DDEX-imported collections.

Scope notes:
- Resolution uses the existing `addTrackToPlaylist` saga path
  (sequential dispatches with small delay). A future PR could replace
  this with a dedicated `addTracksToPlaylistBatch` saga that issues a
  single SDK update for cleaner semantics on large pastes.
- Larger track-curation features from the spec (multi-select, range
  select, undo/redo, copy selected URLs, multi-row drag) are deferred
  to follow-up PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotes the duplicate-playlist flow from metadata-only to a true
duplicate that also copies every track from the source.

- New DUPLICATE_PLAYLIST action carries the source playlist id, the
  composed form fields, the full source track id list, and an isAlbum
  flag.
- New duplicatePlaylistSaga drives the full sequence: it dispatches the
  existing createPlaylist / createAlbum saga with the first source
  track as initTrackId, takes() the resulting CREATE_PLAYLIST_REQUESTED
  to learn the new playlist id, then sequentially dispatches
  addTrackToPlaylist({ silent: true }) for every remaining track with
  a small inter-dispatch delay so each saga sees the previous
  optimistic update. Closes with a single summary toast.
- DuplicatePlaylistModal now dispatches DUPLICATE_PLAYLIST and exposes
  the actual track count to the user ("All N tracks will be copied")
  instead of the previous "tracks not copied" note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings playlist detail-page editing inline per the new UX spec. The
existing /edit route still works for advanced fields (audience,
price, genre, etc.), but the common metadata flow is now handled
directly on the detail page.

- New `PlaylistEditModeProvider` context holds the edit-mode flag,
  the staged metadata draft, conflict status, and saving status. The
  matching `usePlaylistEditMode` hook safely returns a no-op shape
  when not inside the provider, so shared components stay
  backwards-compatible.
- Wrapped the desktop CollectionPage in the provider and rendered a
  sticky `PlaylistEditModeBar` at the page footer. The bar shows a
  Discard / Apply pair when there are pending changes, a slim "no
  changes yet" footer while in edit mode without changes, and a
  conflict banner with a Reload action when the playlist was changed
  remotely since edit mode started.
- The `EditButton` pencil in the owner action row now toggles inline
  edit mode instead of routing to /edit (the legacy link is kept as
  a fallback when no provider is mounted).
- CollectionHeader (desktop) renders an inline `TextInput` for the
  title, a `TextArea` for the description, and a `Switch` for
  visibility while in edit mode. Otherwise behavior is unchanged.
- Desktop Artwork supports inline upload (file picker) when edit
  mode is on; the staged image previews immediately and is sent
  through on Apply along with the metadata draft.
- Apply checks the collection's `updated_at` against the timestamp
  captured when edit mode started; if it has advanced, the bar
  flips to the conflict state and aborts the save.
- Success messages are specific ("Saved details", "Saved artwork",
  or "Saved details and artwork") based on which fields the user
  changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… undo/redo)

Adds the bulk track-curation toolbar to the playlist detail page when
in edit mode, plus keyboard shortcuts for the common operations.

- New `TrackSelectionProvider` + `useTrackSelection` hook: a Set of
  selected track IDs with toggle (including shift-range across an
  ordered id list), select-all, clear, and a "last clicked index" ref
  so range select works in chronological click order.
- New `TrackHistoryProvider` + `useTrackHistoryContext`: undo/redo
  stacks for `remove` and `add` operations. Undo of a `remove`
  dispatches `addTrackToPlaylist({ silent: true })` (note: the
  existing saga appends rather than re-inserting at the original
  index — current ordering is a known limitation surfaced as a
  follow-up).
- New `TrackBulkActionsBar` sticks to the top of the track table
  while in edit mode and shows a count of selected tracks plus the
  five bulk actions: Copy URLs (clipboard-writes
  `${origin}${permalink}` for each selected track), Remove
  (dispatches removeTrackFromPlaylist per id and pushes history
  entries so each remove is undoable), Undo, Redo, and Select all /
  Clear pair.
- Keyboard shortcuts wired through the same component while in edit
  mode: Cmd/Ctrl+A select all, Cmd/Ctrl+Z undo, Cmd/Ctrl+Shift+Z /
  Cmd/Ctrl+Y redo, Escape clears selection, Delete/Backspace removes
  selected (and skips when focus is in a text input).
- CollectionPage is wrapped in the selection + history providers
  alongside the existing edit-mode provider.

Scope notes — per-row checkbox UI and shift-range row-click select
require deeper changes to the existing TracksTable component and are
deferred to a follow-up PR; users can still select-all via the bar
or Cmd/Ctrl+A and operate on the full set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
While the playlist detail page is in edit mode, clicking a track row
toggles its selection in the bulk-actions context instead of playing
the track. Holding shift while clicking extends the selection over
the range between the previous click and the new one.

- New `EditAwareTracksTable` wrapper around the standard
  `TracksTable`. It captures the global shift-key state with a
  window listener (TracksTable's onClickRow does not pass a
  MouseEvent) and rewrites `onClickRow` to call
  `selection.toggle(id, index, { shift })` when edit mode is active.
- Outside of edit mode the wrapper is a transparent pass-through and
  the existing play-on-click behavior is preserved.
- Desktop `CollectionPage` swaps its `TracksTable` usage for the new
  edit-aware wrapper.

Combined with the bulk-actions bar from the previous PR, the user
can now: shift-click a range, Cmd/Ctrl+A to select all, Escape to
clear, Delete to remove, Cmd/Ctrl+Z/Y for undo/redo, and the bar's
Copy URLs / Remove buttons for bulk operations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds on the row-click selection from this branch to give the user
visible feedback for which tracks are currently selected.

- `TracksTable` gains an optional `rowClassNameAddition(track, index)`
  prop that's composed with the table's existing per-row className
  (used internally for the locked/disabled states). The hook is
  ref-stable so external state changes don't force a full re-render
  of the table machinery.
- `EditAwareTracksTable` passes a `rowClassNameAddition` that
  returns the new `selected` CSS class when the row's track id is
  in the selection set and the page is in edit mode. The class
  draws a surface-2 background fill and a 3px accent bar on the
  left edge so selected rows are immediately scannable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 23, 2026

⚠️ No Changeset found

Latest commit: 0402d02

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Two small correctness fixes on top of the existing row-click selection
in playlist edit mode.

- `EditAwareTracksTable` now resets its shift-key ref on `window` blur.
  Previously, holding Shift while Cmd/Alt-Tabbing away would leave the
  ref stuck true (the keyup fires in the other window), so the next
  click on return was interpreted as a shift-extend.
- `TrackSelectionContext.toggle` now keeps the anchor row stable across
  a sequence of shift-clicks. The anchor only moves on a plain click.
  This matches Finder / Gmail / Drive: click A, shift-click C selects
  A..C; a follow-up shift-click E then selects A..E (not C..E).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant