Skip to content

Commit bae86c3

Browse files
authored
[C-2685 C-2686] Implement collection upload form (#3870)
1 parent 1c7ec6f commit bae86c3

16 files changed

+488
-48
lines changed

packages/web/src/components/Icon/Icon.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type IconProps = {
3232
*/
3333
export const Icon = (props: IconProps) => {
3434
const {
35+
className,
3536
color,
3637
icon: IconComponent,
3738
size = 'small',
@@ -48,7 +49,7 @@ export const Icon = (props: IconProps) => {
4849

4950
return (
5051
<IconComponent
51-
className={cn(styles.icon, styles[size])}
52+
className={cn(styles.icon, styles[size], className)}
5253
style={style}
5354
{...iconProps}
5455
/>

packages/web/src/components/form-fields/DropdownField.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import DropdownInput, {
55
DropdownInputProps
66
} from 'components/data-entry/DropdownInput'
77

8-
type DropdownFieldProps = SetRequired<
8+
export type DropdownFieldProps = SetRequired<
99
Partial<DropdownInputProps>,
1010
'placeholder' | 'menu'
1111
> & {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Switch, SwitchProps } from '@audius/stems'
2+
import { useField } from 'formik'
3+
4+
type SwitchFieldProps = SwitchProps & {
5+
name: string
6+
}
7+
8+
export const SwitchField = (props: SwitchFieldProps) => {
9+
const { name, ...other } = props
10+
const [field] = useField({ name, type: 'checkbox' })
11+
12+
return <Switch {...field} {...other} />
13+
}

packages/web/src/components/form-fields/TextField.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
InputV2Variant
77
} from 'components/data-entry/InputV2'
88

9-
type TextFieldProps = InputV2Props & {
9+
export type TextFieldProps = InputV2Props & {
1010
name: string
1111
}
1212

packages/web/src/components/upload/UploadArtwork.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type UploadArtworkProps = {
2+
className?: string
23
artworkUrl?: string
34
onDropArtwork: (selectedFiles: File[], source: string) => Promise<void>
45
onRemoveArtwork?: () => void

packages/web/src/components/upload/UploadArtwork.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ const UploadArtwork = (props) => {
3838

3939
return (
4040
<div
41-
className={cn(styles.uploadArtwork, {
42-
[styles.error]: props.error
43-
})}
41+
className={cn(
42+
styles.uploadArtwork,
43+
{
44+
[styles.error]: props.error
45+
},
46+
props.className
47+
)}
4448
ref={imageSelectionAnchorRef}
4549
>
4650
<div
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { GENRES } from '@audius/common'
2+
3+
import { DropdownField, DropdownFieldProps } from 'components/form-fields'
4+
5+
const messages = {
6+
genre: 'Pick a Genre'
7+
}
8+
9+
type SelectGenreFieldProps = Partial<DropdownFieldProps> & {
10+
name: string
11+
}
12+
13+
const menu = { items: GENRES }
14+
15+
export const SelectGenreField = (props: SelectGenreFieldProps) => {
16+
return (
17+
<DropdownField
18+
aria-label={messages.genre}
19+
placeholder={messages.genre}
20+
mount='parent'
21+
// TODO: Use correct value for Genres based on label (see `convertGenreLabelToValue`)
22+
menu={menu}
23+
size='large'
24+
{...props}
25+
/>
26+
)
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { DropdownField, DropdownFieldProps } from 'components/form-fields'
2+
import { moodMap } from 'utils/Moods'
3+
4+
const MOODS = Object.keys(moodMap).map((k) => ({
5+
text: k,
6+
el: moodMap[k]
7+
}))
8+
9+
const menu = { items: MOODS }
10+
11+
const messages = {
12+
mood: 'Pick a Mood'
13+
}
14+
15+
type SelectMoodFieldProps = Partial<DropdownFieldProps> & {
16+
name: string
17+
}
18+
19+
export const SelectMoodField = (props: SelectMoodFieldProps) => {
20+
return (
21+
<DropdownField
22+
aria-label={messages.mood}
23+
placeholder={messages.mood}
24+
mount='parent'
25+
menu={menu}
26+
size='large'
27+
{...props}
28+
/>
29+
)
30+
}

packages/web/src/pages/upload-page/fields/TrackMetadataFields.tsx

+7-40
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
1-
import { GENRES } from '@audius/common'
21
import { useField } from 'formik'
32

4-
import {
5-
ArtworkField,
6-
DropdownField,
7-
TagField,
8-
TextAreaField,
9-
TextField
10-
} from 'components/form-fields'
11-
import { moodMap } from 'utils/Moods'
3+
import { ArtworkField, TagField, TextAreaField } from 'components/form-fields'
124

135
import { getTrackFieldName } from '../hooks'
146

7+
import { SelectGenreField } from './SelectGenreField'
8+
import { SelectMoodField } from './SelectMoodField'
159
import styles from './TrackMetadataFields.module.css'
16-
17-
const MOODS = Object.keys(moodMap).map((k) => ({
18-
text: k,
19-
el: moodMap[k]
20-
}))
10+
import { TrackNameField } from './TrackNameField'
2111

2212
const messages = {
23-
trackName: 'Track Name',
24-
genre: 'Pick a Genre',
25-
mood: 'Pick a Mood',
2613
description: 'Description'
2714
}
2815

@@ -36,31 +23,11 @@ export const TrackMetadataFields = () => {
3623
</div>
3724
<div className={styles.fields}>
3825
<div className={styles.trackName}>
39-
<TextField
40-
name={getTrackFieldName(index, 'title')}
41-
label={messages.trackName}
42-
maxLength={64}
43-
required
44-
/>
26+
<TrackNameField name={getTrackFieldName(index, 'title')} />
4527
</div>
4628
<div className={styles.categorization}>
47-
<DropdownField
48-
name={getTrackFieldName(index, 'genre')}
49-
aria-label={messages.genre}
50-
placeholder={messages.genre}
51-
mount='parent'
52-
// TODO: Use correct value for Genres based on label (see `convertGenreLabelToValue`)
53-
menu={{ items: GENRES }}
54-
size='large'
55-
/>
56-
<DropdownField
57-
name={getTrackFieldName(index, 'mood')}
58-
aria-label={messages.mood}
59-
placeholder={messages.mood}
60-
mount='parent'
61-
menu={{ items: MOODS }}
62-
size='large'
63-
/>
29+
<SelectGenreField name={getTrackFieldName(index, 'genre')} />
30+
<SelectMoodField name={getTrackFieldName(index, 'mood')} />
6431
</div>
6532
<div className={styles.tags}>
6633
<TagField name={getTrackFieldName(index, 'tags')} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { TextField, TextFieldProps } from 'components/form-fields'
2+
3+
const messages = {
4+
trackName: 'Track Name'
5+
}
6+
7+
type TrackNameFieldProps = Partial<TextFieldProps> & {
8+
name: string
9+
}
10+
11+
export const TrackNameField = (props: TrackNameFieldProps) => {
12+
return (
13+
<TextField label={messages.trackName} maxLength={64} required {...props} />
14+
)
15+
}

packages/web/src/pages/upload-page/hooks.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
import { Maybe } from '@audius/common'
12
import { useField } from 'formik'
23

34
const getFieldName = (base: string, index: number, path: string) =>
45
`${base}.${index}.${path}`
56

67
export const useIndexedField = <T>(
78
base: string,
8-
index: number,
9+
index: Maybe<number>,
910
path: string
1011
) => {
11-
return useField<T>(getFieldName(base, index, path))
12+
const fieldName = index === undefined ? path : getFieldName(base, index, path)
13+
return useField<T>(fieldName)
1214
}
1315

1416
export const getTrackFieldName = (index: number, path: string) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { useCallback, useEffect } from 'react'
2+
3+
import {
4+
HarmonyButton,
5+
HarmonyButtonSize,
6+
HarmonyButtonType,
7+
IconDrag,
8+
IconPlay,
9+
IconTrash
10+
} from '@audius/stems'
11+
import { useField } from 'formik'
12+
13+
import { Icon } from 'components/Icon'
14+
import { TagField } from 'components/form-fields'
15+
import { SwitchField } from 'components/form-fields/SwitchField'
16+
import { Tile } from 'components/tile'
17+
import { Text } from 'components/typography'
18+
19+
import { SelectGenreField } from '../fields/SelectGenreField'
20+
import { SelectMoodField } from '../fields/SelectMoodField'
21+
import { TrackNameField } from '../fields/TrackNameField'
22+
import { CollectionTrackForUpload } from '../types'
23+
24+
import styles from './UploadCollectionForm.module.css'
25+
26+
const messages = {
27+
overrideLabel: 'Override details for this track',
28+
preview: 'Preview',
29+
delete: 'Delete'
30+
}
31+
32+
type CollectionTrackFieldProps = {
33+
index: number
34+
remove: (index: number) => void
35+
}
36+
37+
export const CollectionTrackField = (props: CollectionTrackFieldProps) => {
38+
const { index, remove } = props
39+
const [{ value: track }] = useField<CollectionTrackForUpload>(
40+
`tracks.${index}`
41+
)
42+
43+
const [{ value: metadata }, , { setValue }] = useField<
44+
CollectionTrackForUpload['metadata']
45+
>(`tracks.${index}.metadata`)
46+
47+
const [{ value }] = useField('trackDetails')
48+
49+
const { override } = track
50+
51+
useEffect(() => {
52+
if (override) {
53+
setValue({ ...metadata, ...value })
54+
} else {
55+
setValue({ ...metadata, genre: '', mood: null, tags: null })
56+
}
57+
// eslint-disable-next-line react-hooks/exhaustive-deps
58+
}, [override])
59+
60+
const handleRemove = useCallback(() => {
61+
remove(index)
62+
}, [remove, index])
63+
64+
return (
65+
<Tile
66+
className={styles.trackField}
67+
key={track.metadata.track_id}
68+
elevation='mid'
69+
>
70+
<div className={styles.trackNameRow}>
71+
<span className={styles.iconDrag}>
72+
<Icon icon={IconDrag} size='large' />
73+
</span>
74+
<Text size='small' className={styles.trackindex}>
75+
{index}
76+
</Text>
77+
<TrackNameField name={`tracks.${index}.metadata.title`} />
78+
</div>
79+
{override ? (
80+
<div className={styles.trackInformation}>
81+
<div className={styles.genreMood}>
82+
<SelectGenreField name={`tracks.${index}.metadata.genre`} />
83+
<SelectMoodField name={`tracks.${index}.metadata.mood`} />
84+
</div>
85+
<TagField name={`tracks.${index}.metadata.tags`} />
86+
</div>
87+
) : null}
88+
<div className={styles.overrideRow}>
89+
<div className={styles.overrideSwitch}>
90+
<SwitchField name={`tracks.${index}.override`} />
91+
<Text>{messages.overrideLabel}</Text>
92+
</div>
93+
<div className={styles.actions}>
94+
<HarmonyButton
95+
variant={HarmonyButtonType.GHOST}
96+
size={HarmonyButtonSize.SMALL}
97+
text={messages.preview}
98+
iconLeft={IconPlay}
99+
/>
100+
<HarmonyButton
101+
variant={HarmonyButtonType.GHOST}
102+
size={HarmonyButtonSize.SMALL}
103+
text={messages.delete}
104+
iconLeft={IconTrash}
105+
onClick={handleRemove}
106+
/>
107+
</div>
108+
</div>
109+
</Tile>
110+
)
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { FieldArray, useField } from 'formik'
2+
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
3+
4+
import { CollectionTrackForUpload } from '../types'
5+
6+
import { CollectionTrackField } from './CollectionTrackField'
7+
8+
export const CollectionTrackFieldArray = () => {
9+
const [{ value: tracks }] = useField<CollectionTrackForUpload[]>('tracks')
10+
11+
return (
12+
<FieldArray name='tracks'>
13+
{({ move, remove }) => (
14+
<DragDropContext
15+
onDragEnd={(result) => {
16+
if (!result.destination) {
17+
return
18+
}
19+
move(result.source.index, result.destination.index)
20+
}}
21+
>
22+
<Droppable droppableId='tracks'>
23+
{(provided, snapshot) => (
24+
<div {...provided.droppableProps} ref={provided.innerRef}>
25+
{tracks.map((track, index) => (
26+
<Draggable
27+
key={track.metadata.title}
28+
draggableId={track.metadata.title}
29+
index={index}
30+
>
31+
{(provided, snapshot) => (
32+
<div
33+
ref={provided.innerRef}
34+
{...provided.draggableProps}
35+
{...provided.dragHandleProps}
36+
>
37+
<CollectionTrackField index={index} remove={remove} />
38+
</div>
39+
)}
40+
</Draggable>
41+
))}
42+
</div>
43+
)}
44+
</Droppable>
45+
</DragDropContext>
46+
)}
47+
</FieldArray>
48+
)
49+
}

0 commit comments

Comments
 (0)