diff --git a/src/data/constants.js b/src/data/constants.js index 65c330ef6d..6e97135e33 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -53,3 +53,7 @@ export const VisibilityTypes = /** @type {const} */ ({ UNSCHEDULED: 'unscheduled', NEEDS_ATTENTION: 'needs_attention', }); + +export const TOTAL_LENGTH_KEY = 'total-length'; + +export const MAX_TOTAL_LENGTH = 65; diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx index cffbf53f24..ae93d520c6 100644 --- a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx @@ -16,7 +16,7 @@ import TypeaheadDropdown from '../../editors/sharedComponents/TypeaheadDropdown' import AlertMessage from '../alert-message'; import { STATEFUL_BUTTON_STATES } from '../../constants'; -import { RequestStatus } from '../../data/constants'; +import { RequestStatus, TOTAL_LENGTH_KEY } from '../../data/constants'; import { getSavingStatus } from '../data/selectors'; import { getStudioHomeData } from '../../studio-home/data/selectors'; import { updatePostErrors } from '../data/slice'; @@ -201,6 +201,15 @@ const CreateOrRerunCourseForm = ({ return (
+ {errors[TOTAL_LENGTH_KEY] && ( + + {errors[TOTAL_LENGTH_KEY]} + + )} {showErrorBanner ? ( ', () => { expect(rerunBtn).toBeDisabled(); }); + it('shows error message when total length exceeds 65 characters', async () => { + const updatedProps = { + ...props, + initialValues: { + displayName: 'Long Title Course', + org: 'long-org', + number: 'number', + run: '2024', + }, + }; + + render(); + await mockStore(); + const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + + fireEvent.change(numberInput, { target: { value: 'long-name-which-is-longer-than-65-characters-to-check-for-errors' } }); + + waitFor(() => { + expect(screen.getByText(messages.totalLengthError)).toBeInTheDocument(); + }); + }); + it('should be disabled create button if form has error', async () => { render(); await mockStore(); diff --git a/src/generic/create-or-rerun-course/hooks.jsx b/src/generic/create-or-rerun-course/hooks.jsx index a5c5196a80..2c0a8d0b71 100644 --- a/src/generic/create-or-rerun-course/hooks.jsx +++ b/src/generic/create-or-rerun-course/hooks.jsx @@ -6,7 +6,7 @@ import * as Yup from 'yup'; import { useNavigate } from 'react-router-dom'; import { REGEX_RULES } from '../../constants'; -import { RequestStatus } from '../../data/constants'; +import { RequestStatus, MAX_TOTAL_LENGTH, TOTAL_LENGTH_KEY } from '../../data/constants'; import { getStudioHomeData } from '../../studio-home/data/selectors'; import { getRedirectUrlObj, @@ -60,6 +60,12 @@ const useCreateOrRerunCourse = (initialValues) => { intl.formatMessage(messages.disallowedCharsError), ) .matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)), + }).test(TOTAL_LENGTH_KEY, intl.formatMessage(messages.totalLengthError), function validateTotalLength() { + const { org, number, run } = this?.options.originalValue || {}; + if ((org?.length || 0) + (number?.length || 0) + (run?.length || 0) > MAX_TOTAL_LENGTH) { + return this.createError({ path: TOTAL_LENGTH_KEY, message: intl.formatMessage(messages.totalLengthError) }); + } + return true; }); const { diff --git a/src/generic/create-or-rerun-course/messages.js b/src/generic/create-or-rerun-course/messages.js index 16a07af20e..194271d544 100644 --- a/src/generic/create-or-rerun-course/messages.js +++ b/src/generic/create-or-rerun-course/messages.js @@ -1,4 +1,5 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; +import { MAX_TOTAL_LENGTH } from '../../data/constants'; const messages = defineMessages({ courseDisplayNameLabel: { @@ -117,6 +118,10 @@ const messages = defineMessages({ id: 'course-authoring.create-or-rerun-course.no-space.error', defaultMessage: 'Please do not use any spaces in this field.', }, + totalLengthError: { + id: 'course-authoring.create-or-rerun-course.total-length-error.error', + defaultMessage: `The combined length of the organization, course number and course run fields cannot be more than ${MAX_TOTAL_LENGTH} characters.`, + }, alertErrorExistsAriaLabelledBy: { id: 'course-authoring.create-or-rerun-course.error.already-exists.labelledBy', defaultMessage: 'alert-already-exists-title',