Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[C-2879] Add validation to single track upload flow (#3855)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyle-Shanks authored Aug 8, 2023
1 parent 6f4fc89 commit 7f79830
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ const selectSearchResults = createSelector(getSearchResults, (results) => {
return { items }
})

type AiAttributionDropdownProps = SelectProps
type AiAttributionDropdownProps = SelectProps & {
error?: string | false
helperText?: string | false
}

export const AiAttributionDropdown = (props: AiAttributionDropdownProps) => {
const dispatch = useDispatch()
Expand All @@ -69,6 +72,7 @@ export const AiAttributionDropdown = (props: AiAttributionDropdownProps) => {
size='large'
input={searchInput}
onSearch={handleSearch}
layout='vertical'
{...props}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cn from 'classnames'
import PropTypes from 'prop-types'

import { ReactComponent as IconCaretDown } from 'assets/img/iconCaretDown.svg'
import { HelperText } from 'components/data-entry/HelperText'

import styles from './DropdownInput.module.css'

Expand Down Expand Up @@ -37,6 +38,7 @@ class DropdownInput extends Component {
labelStyle,
dropdownStyle,
dropdownInputStyle,
helperText,
layout,
size,
variant,
Expand Down Expand Up @@ -155,6 +157,9 @@ class DropdownInput extends Component {
</Select>
<IconCaretDown className={styles.arrow} />
</div>
{helperText ? (
<HelperText error={error}>{helperText}</HelperText>
) : null}
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/components/data-entry/ContextualMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,9 @@ export const ContextualMenu = <
const handleSubmit = useCallback(
(values: FormValues, helpers: FormikHelpers<FormValues>) => {
onSubmit(values, helpers)
toggleMenu()
if (!error) toggleMenu()
},
[onSubmit, toggleMenu]
[error, onSubmit, toggleMenu]
)

return (
Expand Down
1 change: 0 additions & 1 deletion packages/web/src/components/data-entry/HelperText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export const HelperText = (props: HelperTextProps) => {
return (
<div className={styles.root}>
<Text
variant='body'
size='xSmall'
strength='default'
// @ts-expect-error
Expand Down
128 changes: 115 additions & 13 deletions packages/web/src/pages/upload-page/components/EditPageNew.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { useCallback, useMemo } from 'react'

import {
Genre,
HashId,
Mood,
PremiumConditionsFollowUserId,
PremiumConditionsNFTCollection,
PremiumConditionsTipUserId
} from '@audius/sdk'
import {
HarmonyButton,
HarmonyButtonType,
Expand All @@ -9,7 +17,8 @@ import {
import cn from 'classnames'
import { Form, Formik, FormikProps, useField } from 'formik'
import moment from 'moment'
import * as Yup from 'yup'
import { z } from 'zod'
import { toFormikValidationSchema } from 'zod-formik-adapter'

import { ReactComponent as IconCaretLeft } from 'assets/img/iconCaretLeft.svg'
import layoutStyles from 'components/layout/layout.module.css'
Expand All @@ -32,7 +41,11 @@ const messages = {
multiTrackCount: (index: number, total: number) =>
`TRACK ${index} of ${total}`,
prev: 'Prev',
next: 'Next Track'
next: 'Next Track',
titleRequiredError: 'Your track must have a name',
artworkRequiredError: 'Artwork is required',
genreRequiredError: 'Genre is required',
invalidReleaseDateError: 'Release date should no be in the future'
}

type EditPageProps = {
Expand All @@ -41,27 +54,115 @@ type EditPageProps = {
onContinue: () => void
}

const EditTrackSchema = Yup.object().shape({
title: Yup.string().required(messages.titleError),
artwork: Yup.object({
url: Yup.string()
}).required(messages.artworkError),
trackArtwork: Yup.string().nullable(),
genre: Yup.string().required(messages.genreError),
description: Yup.string().max(1000).nullable()
})
// TODO: KJ - Need to update the schema in sdk and then import here
const createUploadTrackMetadataSchema = () =>
z.object({
aiAttributionUserId: z.optional(HashId),
description: z.optional(z.string().max(1000)),
download: z.optional(
z
.object({
cid: z.string(),
isDownloadable: z.boolean(),
requiresFollow: z.boolean()
})
.strict()
.nullable()
),
fieldVisibility: z.optional(
z.object({
mood: z.optional(z.boolean()),
tags: z.optional(z.boolean()),
genre: z.optional(z.boolean()),
share: z.optional(z.boolean()),
playCount: z.optional(z.boolean()),
remixes: z.optional(z.boolean())
})
),
genre: z
.enum(Object.values(Genre) as [Genre, ...Genre[]])
.nullable()
.refine((val) => val !== null, {
message: messages.genreRequiredError
}),
isPremium: z.optional(z.boolean()),
isrc: z.optional(z.string().nullable()),
isUnlisted: z.optional(z.boolean()),
iswc: z.optional(z.string().nullable()),
license: z.optional(z.string().nullable()),
mood: z
.optional(z.enum(Object.values(Mood) as [Mood, ...Mood[]]))
.nullable(),
premiumConditions: z.optional(
z.union([
PremiumConditionsNFTCollection,
PremiumConditionsFollowUserId,
PremiumConditionsTipUserId
])
),
releaseDate: z.optional(
z.date().max(new Date(), { message: messages.invalidReleaseDateError })
),
remixOf: z.optional(
z
.object({
tracks: z
.array(
z.object({
parentTrackId: HashId
})
)
.min(1)
})
.strict()
),
tags: z.optional(z.string()),
title: z.string({
required_error: messages.titleRequiredError
}),
previewStartSeconds: z.optional(z.number()),
audioUploadId: z.optional(z.string()),
previewCid: z.optional(z.string())
})

const createTrackMetadataSchema = () => {
return createUploadTrackMetadataSchema()
.merge(
z.object({
artwork: z
.object({
url: z.string()
})
.nullable()
})
)
.refine((form) => form.artwork !== null, {
message: messages.artworkRequiredError,
path: ['artwork']
})
}

export type TrackMetadataValues = z.input<
ReturnType<typeof createTrackMetadataSchema>
>

const EditFormValidationSchema = () =>
z.object({
trackMetadatas: z.array(createTrackMetadataSchema())
})

export const EditPageNew = (props: EditPageProps) => {
const { tracks, setTracks, onContinue } = props

// @ts-ignore - Slight differences in the sdk vs common track metadata types
const initialValues: TrackEditFormValues = useMemo(
() => ({
trackMetadatasIndex: 0,
trackMetadatas: tracks.map((track) => ({
...track.metadata,
artwork: null,
description: '',
releaseDate: moment().startOf('day'),
releaseDate: new Date(moment().startOf('day').toString()),
tags: '',
field_visibility: {
...defaultHiddenFields,
Expand Down Expand Up @@ -93,7 +194,8 @@ export const EditPageNew = (props: EditPageProps) => {
<Formik<TrackEditFormValues>
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={EditTrackSchema}
// @ts-ignore - There are slight mismatches between the sdk and common track metadata types
validationSchema={toFormikValidationSchema(EditFormValidationSchema())}
>
{TrackEditForm}
</Formik>
Expand Down
17 changes: 9 additions & 8 deletions packages/web/src/pages/upload-page/fields/ModalField.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { PropsWithChildren, ReactElement, useState } from 'react'
import { PropsWithChildren, ReactElement, useState } from 'react'

import {
Button,
ButtonType,
HarmonyButton,
HarmonyButtonType,
IconCaretRight,
Modal,
ModalContent,
Expand All @@ -11,6 +11,7 @@ import {
ModalTitle
} from '@audius/stems'
import { useFormikContext } from 'formik'
import { isEmpty } from 'lodash'

import styles from './ModalField.module.css'

Expand All @@ -27,7 +28,7 @@ type ModalFieldProps = PropsWithChildren & {
export const ModalField = (props: ModalFieldProps) => {
const { children, title, icon, preview } = props
const [isModalOpen, setIsModalOpen] = useState(false)
const { submitForm, resetForm } = useFormikContext()
const { submitForm, resetForm, errors } = useFormikContext()

const open = () => setIsModalOpen(true)
const close = () => setIsModalOpen(false)
Expand All @@ -45,14 +46,14 @@ export const ModalField = (props: ModalFieldProps) => {
</ModalHeader>
<ModalContent>{children}</ModalContent>
<ModalFooter>
<Button
type={ButtonType.PRIMARY}
<HarmonyButton
variant={HarmonyButtonType.PRIMARY}
text={messages.save}
onClick={() => {
submitForm()
close()
isEmpty(errors) && close()
}}
buttonType='submit'
type='submit'
/>
</ModalFooter>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export const ReleaseDateField = () => {

const onSubmit = useCallback(
(values: ReleaseDateFormValues) => {
setValue(values[RELEASE_DATE])
const date = new Date(values[RELEASE_DATE].toString())
// @ts-ignore - There are slight differences in the sdk and common track metadata types
setValue(date)
},
[setValue]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,28 @@
flex: 1 auto;
min-width: 335px;
}

.fields > div {
margin-bottom: var(--unit-4);
}

.fields > div.description > .textArea > textarea {
min-height: 90px;
}

.fields > div.description {
margin-bottom: var(--unit-4);
}

.categorization {
display: flex;
align-items: center;
align-items: flex-start;
}

.categorization > div {
flex: 1 auto;
}

.categorization > div:first-child {
margin-right: var(--unit-2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@
.licenseIcons {
gap: 6px;
}

.textFieldContainer {
flex: 1 1 0;
align-self: flex-start;
}
Loading

0 comments on commit 7f79830

Please sign in to comment.