Skip to content

Commit da014df

Browse files
dylanjeffersclaude
andauthored
feat(web): add duplicate playlist flow under the New nav button (#14319)
## Summary Adds a **Duplicate Playlist** secondary action to the sidebar's `+` (New) popup menu. Users paste any public Audius playlist URL, see a preview of the source (title, description, cover art), and toggle per-field switches to choose what to customize in the copy. The duplicated playlist is created as private by default. Stacks on top of #14318 (Create Playlist modal) — review/merge that first. **Scope:** - Metadata-only duplication for v1: title, description, artwork. Tracks are **not** copied in this PR — a follow-up will support full duplicate including track contents. The modal surfaces this with a helper line so users know to add tracks themselves. - When the user keeps the source artwork, we pass the source's `cover_art_sizes` CID through to the existing `createPlaylist` saga so it reuses the cover instead of treating it as a new upload. No content node re-upload required. ## Implementation - New `DuplicatePlaylistModal` Redux modal slice (via existing `createModal` helper) registered in modals types/reducers/parent state. - New `DuplicatePlaylistModal` component using Harmony `Modal` + `TextInput` + `Switch` + `TextArea` + `Artwork` + the existing `UploadArtwork`. - URL paste resolves through `getPathFromPlaylistUrl` → `useCollectionByPermalink` to load the source collection. - Wires "Duplicate Playlist" into `CreatePlaylistLibraryItemButton` between Create Playlist and Create Folder. ## Test plan - [ ] Sign in to the web app - [ ] Left nav → `+` → **Duplicate Playlist** - [ ] Paste a malformed URL → see inline "Enter a valid Audius playlist URL" - [ ] Paste a valid Audius playlist URL → preview card appears with the source title, description, and artwork - [ ] Submit with all three switches off → new playlist created with `<Source Title> (Copy)`, the source description, and the source cover art reused (no re-upload) - [ ] Toggle "Customize title" → enter a custom name → that name wins - [ ] Toggle "Customize description" → enter a custom description → that description wins - [ ] Toggle "Customize artwork" → upload a new image → custom image wins - [ ] Resulting playlist is private (Hidden) and the user is routed to its edit page - [ ] The "tracks not copied" note is visible in the modal ## Follow-ups - Copy tracks from source to the duplicated playlist (probably via a new `duplicatePlaylist` action + saga that chains `createPlaylist` then `addTrackToPlaylist` per track). - Paste-tracks-by-URL on the edit page (separate PR), which pairs naturally with this once track copy lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 87badc2 commit da014df

8 files changed

Lines changed: 395 additions & 5 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createModal } from '../createModal'
2+
3+
export type DuplicatePlaylistModalState = {
4+
isAlbum?: boolean
5+
}
6+
7+
const duplicatePlaylistModal = createModal<DuplicatePlaylistModalState>({
8+
reducerPath: 'DuplicatePlaylistModal',
9+
initialState: {
10+
isOpen: false,
11+
isAlbum: false
12+
},
13+
sliceSelector: (state) => state.ui.modals
14+
})
15+
16+
export const {
17+
hook: useDuplicatePlaylistModal,
18+
reducer: duplicatePlaylistModalReducer,
19+
actions: duplicatePlaylistModalActions
20+
} = duplicatePlaylistModal

packages/common/src/store/ui/modals/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ export * from './send-tokens-modal'
4242
export * from './coin-success-modal'
4343
export * from './fan-club-details-modal'
4444
export * from './create-playlist-modal'
45+
export * from './duplicate-playlist-modal'

packages/common/src/store/ui/modals/parentSlice.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ export const initialState: BasicModalsState = {
8585
FanClubDetailsModal: { isOpen: false },
8686
VerificationSuccess: { isOpen: false },
8787
VerificationError: { isOpen: false },
88-
CreatePlaylistModal: { isOpen: false }
88+
CreatePlaylistModal: { isOpen: false },
89+
DuplicatePlaylistModal: { isOpen: false }
8990
}
9091

9192
const slice = createSlice({

packages/common/src/store/ui/modals/reducers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { createChatModalReducer } from './create-chat-modal'
1515
import { createPlaylistModalReducer } from './create-playlist-modal'
1616
import { deleteTrackConfirmationModalReducer } from './delete-track-confirmation-modal'
1717
import { downloadTrackArchiveModalReducer } from './download-track-archive-modal'
18+
import { duplicatePlaylistModalReducer } from './duplicate-playlist-modal'
1819
import { earlyReleaseConfirmationModalReducer } from './early-release-confirmation-modal'
1920
import { editAccessConfirmationModalReducer } from './edit-access-confirmation-modal'
2021
import { externalWalletSignUpModalReducer } from './external-wallet-sign-up-modal'
@@ -94,7 +95,8 @@ const combinedReducers = combineReducers({
9495
SendTokensModal: sendTokensModalReducer,
9596
CoinSuccessModal: coinSuccessModalReducer,
9697
FanClubDetailsModal: fanClubDetailsModalReducer,
97-
CreatePlaylistModal: createPlaylistModalReducer
98+
CreatePlaylistModal: createPlaylistModalReducer,
99+
DuplicatePlaylistModal: duplicatePlaylistModalReducer
98100
})
99101

100102
/**

packages/common/src/store/ui/modals/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export type Modals =
121121
| 'VerificationSuccess'
122122
| 'VerificationError'
123123
| 'CreatePlaylistModal'
124+
| 'DuplicatePlaylistModal'
124125

125126
export type BasicModalsState = {
126127
[modal in Modals]: BaseModalState

0 commit comments

Comments
 (0)