diff --git a/packages/web/src/components/ai-attribution-modal/AiAttributionDropdown.tsx b/packages/web/src/components/ai-attribution-modal/AiAttributionDropdown.tsx index 2683e218ec..0edd03452a 100644 --- a/packages/web/src/components/ai-attribution-modal/AiAttributionDropdown.tsx +++ b/packages/web/src/components/ai-attribution-modal/AiAttributionDropdown.tsx @@ -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() @@ -69,6 +72,7 @@ export const AiAttributionDropdown = (props: AiAttributionDropdownProps) => { size='large' input={searchInput} onSearch={handleSearch} + layout='vertical' {...props} /> ) diff --git a/packages/web/src/components/ai-attribution-modal/DropdownInput.js b/packages/web/src/components/ai-attribution-modal/DropdownInput.js index 4b2bf43713..ac33176994 100644 --- a/packages/web/src/components/ai-attribution-modal/DropdownInput.js +++ b/packages/web/src/components/ai-attribution-modal/DropdownInput.js @@ -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' @@ -37,6 +38,7 @@ class DropdownInput extends Component { labelStyle, dropdownStyle, dropdownInputStyle, + helperText, layout, size, variant, @@ -155,6 +157,9 @@ class DropdownInput extends Component { + {helperText ? ( + {helperText} + ) : null} ) } diff --git a/packages/web/src/components/data-entry/ContextualMenu.tsx b/packages/web/src/components/data-entry/ContextualMenu.tsx index 3a1717f4b1..8b2f33efbd 100644 --- a/packages/web/src/components/data-entry/ContextualMenu.tsx +++ b/packages/web/src/components/data-entry/ContextualMenu.tsx @@ -164,9 +164,9 @@ export const ContextualMenu = < const handleSubmit = useCallback( (values: FormValues, helpers: FormikHelpers) => { onSubmit(values, helpers) - toggleMenu() + if (!error) toggleMenu() }, - [onSubmit, toggleMenu] + [error, onSubmit, toggleMenu] ) return ( diff --git a/packages/web/src/components/data-entry/HelperText.tsx b/packages/web/src/components/data-entry/HelperText.tsx index ba94cf7808..7416d45306 100644 --- a/packages/web/src/components/data-entry/HelperText.tsx +++ b/packages/web/src/components/data-entry/HelperText.tsx @@ -14,7 +14,6 @@ export const HelperText = (props: HelperTextProps) => { return (
`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 = { @@ -41,19 +54,107 @@ 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 +> + +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, @@ -61,7 +162,7 @@ export const EditPageNew = (props: EditPageProps) => { ...track.metadata, artwork: null, description: '', - releaseDate: moment().startOf('day'), + releaseDate: new Date(moment().startOf('day').toString()), tags: '', field_visibility: { ...defaultHiddenFields, @@ -93,7 +194,8 @@ export const EditPageNew = (props: EditPageProps) => { initialValues={initialValues} onSubmit={onSubmit} - validationSchema={EditTrackSchema} + // @ts-ignore - There are slight mismatches between the sdk and common track metadata types + validationSchema={toFormikValidationSchema(EditFormValidationSchema())} > {TrackEditForm} diff --git a/packages/web/src/pages/upload-page/fields/ModalField.tsx b/packages/web/src/pages/upload-page/fields/ModalField.tsx index a8ad15e1fd..aa65666f94 100644 --- a/packages/web/src/pages/upload-page/fields/ModalField.tsx +++ b/packages/web/src/pages/upload-page/fields/ModalField.tsx @@ -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, @@ -11,6 +11,7 @@ import { ModalTitle } from '@audius/stems' import { useFormikContext } from 'formik' +import { isEmpty } from 'lodash' import styles from './ModalField.module.css' @@ -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) @@ -45,14 +46,14 @@ export const ModalField = (props: ModalFieldProps) => { {children} -