# Add "New Playlist…" to the Add-to-Playlist menu **Date:** 2026-05-30 **Status:** Approved design ## Goal In a track's right-click "Add to Playlist" submenu, let the user create a brand-new regular playlist on the fly: pick **New Playlist…**, enter a name, and on save the playlist is created and the track is added to it. ## Background The "Add to Playlist" submenu lives in `TrackContextMenuModifier.swift` and is driven by the data-only `TrackContextMenuConfig` struct (a `playlists` array plus action closures), built in `ContentView.trackContextMenuConfig` (`ContentView.swift:415`). The app already creates playlists from the sidebar via an `.alert` + `TextField` (`ContentView.swift:273`), calling `PlaylistViewModel.createPlaylist(name:)` → `DatabaseService.createPlaylist(name:) -> Playlist`. Adding a track is `PlaylistViewModel.addTrack(_:to:)`, which also records the playlist as last-used. Regular and smart playlists are separate tables; this feature only creates **regular** playlists (`Playlist`). ## Approach Route the name prompt up to `ContentView`, which already owns the new-playlist alert. SwiftUI alerts do not present reliably when attached *inside* a context menu's content (the menu dismisses, and the per-row modifier is re-instantiated). So the menu item only signals intent — "create a new playlist for this track" — and `ContentView` owns the prompt and the orchestration. This reuses the existing alert pattern rather than duplicating it inside the modifier (the rejected alternative, which would also need the modifier to reach into `PlaylistViewModel`). ## Behavior decisions - Adds **only the clicked track** (matches the current single-track "Add to Playlist"). - **No navigation** — after create+add the sidebar selection is unchanged. - Empty / whitespace-only name → no-op (matches the existing create flow). - When the user has **no** existing playlists, the submenu still appears showing just "New Playlist…" (today the whole submenu is hidden when `playlists` is empty). - Remote mode → wired the same way as the existing "Add to Playlist" action (unconditionally), keeping parity with current behavior. ## Changes ### 1. `PlaylistViewModel` (`Music/ViewModels/PlaylistViewModel.swift`) Add one orchestration method — the unit under test: ```swift @discardableResult func createPlaylistAndAddTrack(name: String, track: Track) throws -> Playlist { let playlist = try db.createPlaylist(name: name) try addTrack(track, to: playlist) return playlist } ``` `db.createPlaylist` returns a `Playlist` with its assigned `id`; `addTrack` adds the track and sets `lastUsedPlaylistId` to the new playlist (so the "Add to " item updates too). ### 2. `TrackContextMenuConfig` (`Music/Models/TrackContextMenuConfig.swift`) Add a new optional closure, defaulting to `nil` (so all existing call sites and tests compile unchanged): ```swift let onAddToNewPlaylist: ((Track) -> Void)? ``` Add it to the explicit `init` with a `= nil` default, alongside the other optionals. ### 3. `TrackContextMenuModifier` (`Music/Views/TrackContextMenuModifier.swift`) Inside the "Add to Playlist" submenu: - Add a **New Playlist…** button at the top (ellipsis = opens a prompt) when `onAddToNewPlaylist != nil`, followed by a `Divider` before the list of existing playlists. - Relax the visibility guard so the submenu shows when `!config.playlists.isEmpty || config.onAddToNewPlaylist != nil` (previously hidden whenever `playlists` was empty). ### 4. `ContentView` (`Music/ContentView.swift`) - Add state: `@State private var newPlaylistTrack: Track?` and a name field (reuse/parallel the existing `playlistNameInput` style; a dedicated field is fine). - In `trackContextMenuConfig`, wire `onAddToNewPlaylist: { track in newPlaylistTrack = track }`. - Present an alert (mirroring the existing New Playlist alert) gated on `newPlaylistTrack != nil`. **Create** trims whitespace, and if non-empty calls `playlist.createPlaylistAndAddTrack(name:track:)` with the pending track; **Cancel** and completion both clear `newPlaylistTrack` and the name field. ## Testing (TDD) Unit-test `PlaylistViewModel.createPlaylistAndAddTrack(name:track:)` against an in-memory database: 1. Seed a track in the DB. 2. Call `createPlaylistAndAddTrack(name:track:)`. 3. Assert a regular playlist with that name now exists. 4. Assert the playlist's tracks contain the seeded track. 5. Assert `lastUsedPlaylistId` equals the new playlist's id. The SwiftUI menu/alert rendering is not unit-tested, consistent with the rest of the codebase. `TrackContextMenuConfig`'s new optional defaults to `nil`, so existing `TrackContextMenuConfigTests` are unaffected.