From 448ea95151fad3560560940db9e8223ba26f546c Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Tue, 3 Sep 2024 17:01:40 -0400 Subject: [PATCH 01/23] feat: assign by course run implementation --- .../AssignmentModalContent.jsx | 13 ++- .../AssignmentModalmportantDates.jsx | 91 +++++++++++++++++++ .../NewAssignmentModalButton.jsx | 15 ++- .../NewAssignmentModalDropdown.jsx | 36 ++++++++ .../cards/BaseCourseCard.jsx | 23 ++++- .../cards/CourseCardFooterActions.jsx | 1 - ...dMetadata.js => useCourseCardMetadata.jsx} | 10 +- .../data/constants.js | 2 + .../learner-credit-management/data/utils.js | 2 + .../search/CatalogSearchResults.jsx | 1 - src/index.scss | 7 ++ src/utils.js | 7 ++ 12 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx create mode 100644 src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx rename src/components/learner-credit-management/cards/data/{useCourseCardMetadata.js => useCourseCardMetadata.jsx} (80%) diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx index cd18c5d5f4..7de1f6d8eb 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx @@ -17,7 +17,9 @@ import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isAssignEmailAddressesInput import AssignmentAllocationHelpCollapsibles from './AssignmentAllocationHelpCollapsibles'; import EVENT_NAMES from '../../../eventTracking'; -const AssignmentModalContent = ({ enterpriseId, course, onEmailAddressesChange }) => { +const AssignmentModalContent = ({ + enterpriseId, course, courseRun, onEmailAddressesChange, +}) => { const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const spendAvailable = subsidyAccessPolicy.aggregates.spendAvailableUsd; @@ -25,13 +27,12 @@ const AssignmentModalContent = ({ enterpriseId, course, onEmailAddressesChange } const [emailAddressesInputValue, setEmailAddressesInputValue] = useState(''); const [assignmentAllocationMetadata, setAssignmentAllocationMetadata] = useState({}); const intl = useIntl(); + // TODO: as part of fixed price, this would need to extract the contentPrice from courseRun const { contentPrice } = course.normalizedMetadata; - const handleEmailAddressInputChange = (e) => { const inputValue = e.target.value; setEmailAddressesInputValue(inputValue); }; - const handleEmailAddressesChanged = useCallback((value) => { if (!value) { setLearnerEmails([]); @@ -85,7 +86,7 @@ const AssignmentModalContent = ({ enterpriseId, course, onEmailAddressesChange } description="Header for the section to assign a course to learners using learner credit." /> - + @@ -203,6 +204,10 @@ const AssignmentModalContent = ({ enterpriseId, course, onEmailAddressesChange } AssignmentModalContent.propTypes = { enterpriseId: PropTypes.string.isRequired, course: PropTypes.shape().isRequired, // Pass-thru prop to `BaseCourseCard` + courseRun: PropTypes.shape({ + enrollBy: PropTypes.number, + start: PropTypes.string, + }).isRequired, onEmailAddressesChange: PropTypes.func.isRequired, }; diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx new file mode 100644 index 0000000000..879b5bd127 --- /dev/null +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -0,0 +1,91 @@ +import { + Col, Icon, Row, Stack, +} from '@openedx/paragon'; +import { Calendar } from '@openedx/paragon/icons'; +import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; +import PropTypes from 'prop-types'; + +import dayjs from 'dayjs'; +import { hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; + +const messages = defineMessages({ + importantDates: { + id: 'enterprise.course.about.page.important-dates.title', + defaultMessage: 'Important dates', + description: 'Title for the important dates section on the assignment modal', + }, + enrollByDate: { + id: 'enterprise.course.about.page.important-dates.enroll-by-date', + defaultMessage: 'Enroll-by date', + description: 'Enroll-by date for the important dates section on the assignment modal', + }, + courseStarts: { + id: 'enterprise.course.about.page.important-dates.course-starts', + defaultMessage: 'Course starts', + description: 'Course starts for the important dates section on the assignment modal in future tense', + }, + courseStarted: { + id: 'enterprise.course.about.page.important-dates.course-started', + defaultMessage: 'Course started', + description: 'Course started the important dates section on the assignment modal in past tense', + }, +}); + +const AssignmentModalImportantDate = ({ + label, + children, +}) => ( + + + + + {label} + + + + {children} + + +); + +AssignmentModalImportantDate.propTypes = { + label: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +const AssignmentModalImportantDates = ({ courseRun }) => { + const intl = useIntl(); + const enrollByDate = dayjs(courseRun.enrollBy * 1000).format(SHORT_MONTH_DATE_FORMAT) ?? null; + const courseStartDate = dayjs(courseRun.start).format(SHORT_MONTH_DATE_FORMAT) ?? null; + const courseHasStartedLabel = hasCourseStarted(courseStartDate) + ? intl.formatMessage(messages.courseStarted) + : intl.formatMessage(messages.courseStarts); + + if (!enrollByDate && !courseStartDate) { + return null; + } + + return ( +
+ {enrollByDate && ( + + {enrollByDate} + + )} + {courseStartDate && ( + + {courseStartDate} + + )} +
+ ); +}; + +AssignmentModalImportantDates.propTypes = { + courseRun: PropTypes.shape({ + enrollBy: PropTypes.number, + start: PropTypes.string, + }).isRequired, +}; + +export default AssignmentModalImportantDates; diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx index 65d9880e9f..c6984de91f 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx @@ -23,6 +23,7 @@ import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; import EVENT_NAMES from '../../../eventTracking'; import { LEARNER_CREDIT_ROUTE } from '../constants'; +import NewAssignmentModalDropdown from './NewAssignmentModalDropdown'; const useAllocateContentAssignments = () => useMutation({ mutationFn: async ({ @@ -45,6 +46,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const [canAllocateAssignments, setCanAllocateAssignments] = useState(false); const [assignButtonState, setAssignButtonState] = useState('default'); const [createAssignmentsErrorReason, setCreateAssignmentsErrorReason] = useState(); + const [assignmentRun, setAssignmentRun] = useState(); const { successfulAssignmentToast: { displayToastForAssignmentAllocation }, } = useContext(BudgetDetailPageContext); @@ -70,7 +72,12 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { enterpriseSlug, enterpriseAppPage, budgetId: subsidyAccessPolicyId, activeTabKey: 'activity', }); - const handleOpenAssignmentModal = () => { + const handleOpenAssignmentModal = (e) => { + // Based on the user selection, we will extract the course run metadata from the key + const selectedCourseRun = course.courseRuns.find(({ key }) => key === e.target.closest('[id]').id); + // If the selected course run is not found, we default to the advertised course run + const courseRunMetadata = selectedCourseRun ?? course.advertisedCourseRun; + setAssignmentRun(courseRunMetadata); open(); sendEnterpriseTrackEvent( enterpriseId, @@ -182,10 +189,11 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { }, }); }; - return ( <> - + + {children} + { > diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx new file mode 100644 index 0000000000..7805e6d157 --- /dev/null +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx @@ -0,0 +1,36 @@ +import { Dropdown, Stack } from '@openedx/paragon'; +import dayjs from 'dayjs'; +import PropTypes from 'prop-types'; +import { SHORT_MONTH_DATE_FORMAT } from '../data'; + +const NewAssignmentModalDropdown = ({ + id: courseKey, onClick: openAssignmentModal, courseRuns, children, +}) => ( + + + {children} + + + + By date + + {courseRuns.map(courseRun => ( + + + Enroll by {dayjs(courseRun.enrollBy * 1000).format(SHORT_MONTH_DATE_FORMAT)} + Starts {dayjs(courseRun.starts).format(SHORT_MONTH_DATE_FORMAT)} + + + ))} + + +); + +NewAssignmentModalDropdown.propTypes = { + id: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + courseRuns: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + children: PropTypes.node.isRequired, +}; + +export default NewAssignmentModalDropdown; diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx index 7aae47da29..66a7c130ea 100644 --- a/src/components/learner-credit-management/cards/BaseCourseCard.jsx +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -18,12 +18,16 @@ const BaseCourseCard = ({ footerActions: CardFooterActions, enterpriseSlug, cardClassName, + courseRun, + displayImportantDates, }) => { const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); const courseCardMetadata = useCourseCardMetadata({ course: camelCaseObject(original), + courseRun, enterpriseSlug, + displayImportantDates, }); const { imageSrc, @@ -34,10 +38,8 @@ const BaseCourseCard = ({ subtitle, formattedPrice, isExecEdCourseType, - courseEnrollmentInfo, - execEdEnrollmentInfo, + dateFooterText, } = courseCardMetadata; - return ( {CardFooterActions && } @@ -113,8 +115,21 @@ BaseCourseCard.propTypes = { ), title: PropTypes.string, }).isRequired, + courseRun: PropTypes.shape({ + enrollBy: PropTypes.number, + start: PropTypes.string, + }), footerActions: PropTypes.elementType, cardClassName: PropTypes.string, + displayImportantDates: PropTypes.bool, +}; + +BaseCourseCard.defaultProps = { + courseRun: { + enrollBy: null, + start: null, + }, + displayImportantDates: false, }; export default connect(mapStateToProps)(BaseCourseCard); diff --git a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx index 8717b48cf4..1951b5e40c 100644 --- a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx +++ b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx @@ -18,7 +18,6 @@ const CourseCardFooterActions = ({ enterpriseId, course }) => { const catalogGroupView = subsidyAccessPolicy?.groupAssociations?.length > 0 && !data.appliesToAllContexts; - const { linkToCourse, uuid } = course; const handleViewCourse = () => { sendEnterpriseTrackEvent( diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx similarity index 80% rename from src/components/learner-credit-management/cards/data/useCourseCardMetadata.js rename to src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index b49ece6cf4..419083fabd 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -9,12 +9,16 @@ import { formatPrice, getEnrollmentDeadline, } from '../../data'; +import { pluralText } from '../../../../utils'; +import AssignmentModalImportantDates from '../../assignment-modal/AssignmentModalmportantDates'; const { ENROLLMENT } = CARD_TEXT; const useCourseCardMetadata = ({ course, enterpriseSlug, + courseRun, + displayImportantDates, }) => { const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext); const { @@ -25,6 +29,7 @@ const useCourseCardMetadata = ({ normalizedMetadata, partners, title, + courseRuns, } = course; const formattedPrice = (normalizedMetadata.contentPrice || normalizedMetadata.contentPrice === 0) ? formatPrice(normalizedMetadata.contentPrice) : 'N/A'; const imageSrc = cardImageUrl || cardFallbackImg; @@ -53,7 +58,9 @@ const useCourseCardMetadata = ({ if (isExecEdCourseType) { linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`; } - + const assignmentImportantDates = ; + const availableCourseRuns = `(${courseRuns.length}) available ${pluralText('date', courseRuns.length)}`; + const dateFooterText = displayImportantDates ? assignmentImportantDates : availableCourseRuns; return { ...course, subtitle: partners.map(partner => partner.name).join(', '), @@ -67,6 +74,7 @@ const useCourseCardMetadata = ({ execEdEnrollmentInfo, linkToCourse, isExecEdCourseType, + dateFooterText, }; }; diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index e7d6b7756f..db58f6a5c5 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -32,6 +32,8 @@ export const NO_BALANCE_REMAINING_DOLLAR_THRESHOLD = 100; export const DATE_FORMAT = 'MMMM DD, YYYY'; +export const SHORT_MONTH_DATE_FORMAT = 'MMM DD, YYYY'; + export const EXEC_ED_OFFER_TYPE = 'learner_credit'; // Budget Detail Page Tabs diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index a737f751a4..df337300c0 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -551,3 +551,5 @@ export const isLmsBudget = ( activeIntegrationsLength, isUniversalGroup, ) => activeIntegrationsLength > 0 && isUniversalGroup; + +export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx index 45d1e60bab..b4769e93c7 100644 --- a/src/components/learner-credit-management/search/CatalogSearchResults.jsx +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -65,7 +65,6 @@ export const BaseCatalogSearchResults = ({ useEffect(() => { setNoContent(searchResults === null || searchResults?.nbHits === 0); }, [searchResults, setNoContent]); - if (error) { return ( diff --git a/src/index.scss b/src/index.scss index 2b587d05aa..685520a095 100644 --- a/src/index.scss +++ b/src/index.scss @@ -84,3 +84,10 @@ form { .font-size-base { font-size: $font-size-base !important; } + +.assignments-important-dates { + width: 250px; + + .course-important-date { + } +} diff --git a/src/utils.js b/src/utils.js index 9c30c76f2b..1e002ab845 100644 --- a/src/utils.js +++ b/src/utils.js @@ -546,6 +546,12 @@ function makePlural(num, string) { return `${num} ${string}`; } +const pluralText = ( + textToPlural, + pluralBenchmark, + punctuation = '', +) => (pluralBenchmark > 1 ? `${textToPlural}s${punctuation}` : `${textToPlural}${punctuation}`); + /** * Helper function to determine if a content is archived. * @@ -631,6 +637,7 @@ export { getActiveTableColumnFilters, queryCacheOnErrorHandler, makePlural, + pluralText, isArchivedContent, i18nFormatTimestamp, i18nFormatPassedTimestamp, From 6ebabafbdd87a09c1e2e1e004292d1c6aae1034d Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Wed, 4 Sep 2024 10:34:27 -0400 Subject: [PATCH 02/23] chore: optimize and cleanup --- package.json | 5 ++- .../tabs/Leaderboard.test.jsx | 1 + src/components/forms/data/reducer.ts | 6 ++- .../AssignmentModalmportantDates.jsx | 2 +- .../NewAssignmentModalButton.jsx | 22 +++++++--- .../NewAssignmentModalDropdown.jsx | 44 ++++++++++--------- .../cards/BaseCourseCard.jsx | 1 + .../cards/data/useCourseCardMetadata.jsx | 15 +++++-- .../data/constants.js | 3 ++ .../learner-credit-management/data/utils.js | 11 +++++ .../search/CatalogSearchResults.jsx | 6 ++- .../SettingsSSOTab/SSOFormWorkflowConfig.tsx | 14 +++--- .../steps/NewSSOConfigAuthorizeStep.tsx | 9 ++-- 13 files changed, 91 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 0c5fb38a52..398475adeb 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "i18n_extract": "fedx-scripts formatjs extract --throws", "build:with-theme": "THEME=npm:@edx/brand-edx.org@latest npm run install-theme && fedx-scripts webpack", "check-types": "tsc --noemit", - "lint": "fedx-scripts eslint --ext .js --ext .jsx .; npm run check-types", - "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .tsx --ext .ts .", + "eslint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .", + "lint": "npm run eslint && npm run check-types", + "lint:fix": "npm run eslint -- --fix", "precommit": "npm run lint", "prepublishOnly": "npm run build", "postinstall": "patch-package", diff --git a/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.test.jsx b/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.test.jsx index 1c09351b0a..93520b6031 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.test.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.test.jsx @@ -5,6 +5,7 @@ import '@testing-library/jest-dom'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { QueryClientProvider } from '@tanstack/react-query'; import MockAdapter from 'axios-mock-adapter'; +// eslint-disable-next-line import/no-extraneous-dependencies import axios from 'axios'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { BrowserRouter as Router } from 'react-router-dom'; diff --git a/src/components/forms/data/reducer.ts b/src/components/forms/data/reducer.ts index 273fb2310e..8cb92fdd17 100644 --- a/src/components/forms/data/reducer.ts +++ b/src/components/forms/data/reducer.ts @@ -2,7 +2,11 @@ import groupBy from 'lodash/groupBy'; import isEmpty from 'lodash/isEmpty'; import keys from 'lodash/keys'; import { - SetShowErrorsArguments, SET_FORM_FIELD, SET_SHOW_ERRORS, SET_STEP, SET_WORKFLOW_STATE, UPDATE_FORM_FIELDS, RESET_EDIT_STATE, + SetShowErrorsArguments, + SET_FORM_FIELD, SET_SHOW_ERRORS, + SET_STEP, SET_WORKFLOW_STATE, + UPDATE_FORM_FIELDS, + RESET_EDIT_STATE, } from './actions'; import type { FormActionArguments, SetFormFieldArguments, SetStepArguments, SetWorkflowStateArguments, UpdateFormFieldArguments, diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index 879b5bd127..f0bd7b78d6 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -55,7 +55,7 @@ AssignmentModalImportantDate.propTypes = { const AssignmentModalImportantDates = ({ courseRun }) => { const intl = useIntl(); - const enrollByDate = dayjs(courseRun.enrollBy * 1000).format(SHORT_MONTH_DATE_FORMAT) ?? null; + const enrollByDate = dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT) ?? null; const courseStartDate = dayjs(courseRun.start).format(SHORT_MONTH_DATE_FORMAT) ?? null; const courseHasStartedLabel = hasCourseStarted(courseStartDate) ? intl.formatMessage(messages.courseStarted) diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx index c6984de91f..10700a1af0 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx @@ -18,7 +18,13 @@ import { getConfig } from '@edx/frontend-platform/config'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; -import { learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy } from '../data'; +import { + enrollableCourseRuns, + learnerCreditManagementQueryKeys, + STALE_ENROLLMENT_DROPOFF_DAYS, + useBudgetId, + useSubsidyAccessPolicy, +} from '../data'; import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals'; import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; import EVENT_NAMES from '../../../eventTracking'; @@ -66,7 +72,11 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { courseUuid: course.uuid, assignmentConfiguration, }; - + const availableCourseRuns = enrollableCourseRuns({ + courseRuns: course.courseRuns, + subsidyExpirationDatetime: subsidyAccessPolicy.subsidyExpirationDatetime, + staleEnrollmentDropOffTime: STALE_ENROLLMENT_DROPOFF_DAYS, + }); const { mutate } = useAllocateContentAssignments(); const pathToActivityTab = generatePath(LEARNER_CREDIT_ROUTE, { enterpriseSlug, enterpriseAppPage, budgetId: subsidyAccessPolicyId, activeTabKey: 'activity', @@ -74,7 +84,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const handleOpenAssignmentModal = (e) => { // Based on the user selection, we will extract the course run metadata from the key - const selectedCourseRun = course.courseRuns.find(({ key }) => key === e.target.closest('[id]').id); + const selectedCourseRun = availableCourseRuns.find(({ key }) => key === e.target.closest('[id]').id); // If the selected course run is not found, we default to the advertised course run const courseRunMetadata = selectedCourseRun ?? course.advertisedCourseRun; setAssignmentRun(courseRunMetadata); @@ -121,11 +131,11 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { trackEventMetadata, ); }; - const handleAllocateContentAssignments = () => { + // If no assignmentRun key exist, fall back to the top level course key const payload = snakeCaseObject({ contentPriceCents: course.normalizedMetadata.contentPrice * 100, // Convert to USD cents - contentKey: course.key, + contentKey: assignmentRun.key, learnerEmails, }); const mutationArgs = { @@ -191,7 +201,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { }; return ( <> - + {children} ( - - - {children} - - - - By date - - {courseRuns.map(courseRun => ( - - - Enroll by {dayjs(courseRun.enrollBy * 1000).format(SHORT_MONTH_DATE_FORMAT)} - Starts {dayjs(courseRun.starts).format(SHORT_MONTH_DATE_FORMAT)} - - - ))} - - -); +}) => { + const [emptyState] = useState(courseRuns.length === 0); + return ( + + + {children} + + + + {emptyState ? 'No available dates' : 'By date'} + + {!emptyState && courseRuns.sort((a, b) => a.enrollBy - b.enrollBy).map(courseRun => ( + + + Enroll by {dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT)} + Starts {dayjs(courseRun.starts).format(SHORT_MONTH_DATE_FORMAT)} + + + ))} + + + ); +}; NewAssignmentModalDropdown.propTypes = { id: PropTypes.string.isRequired, diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx index 66a7c130ea..96440dd27d 100644 --- a/src/components/learner-credit-management/cards/BaseCourseCard.jsx +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -23,6 +23,7 @@ const BaseCourseCard = ({ }) => { const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); + const courseCardMetadata = useCourseCardMetadata({ course: camelCaseObject(original), courseRun, diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index 419083fabd..cd032530eb 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -4,10 +4,11 @@ import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.pn import CARD_TEXT from '../../constants'; import { + enrollableCourseRuns, EXEC_ED_COURSE_TYPE, formatDate, formatPrice, - getEnrollmentDeadline, + getEnrollmentDeadline, STALE_ENROLLMENT_DROPOFF_DAYS, useBudgetId, useSubsidyAccessPolicy, } from '../../data'; import { pluralText } from '../../../../utils'; import AssignmentModalImportantDates from '../../assignment-modal/AssignmentModalmportantDates'; @@ -21,6 +22,8 @@ const useCourseCardMetadata = ({ displayImportantDates, }) => { const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext); + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const { availability, cardImageUrl, @@ -58,9 +61,15 @@ const useCourseCardMetadata = ({ if (isExecEdCourseType) { linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`; } + + const availableCourseRuns = enrollableCourseRuns({ + courseRuns, + subsidyExpirationDatetime: subsidyAccessPolicy.subsidyExpirationDatetime, + staleEnrollmentDropOffTime: STALE_ENROLLMENT_DROPOFF_DAYS, + }); const assignmentImportantDates = ; - const availableCourseRuns = `(${courseRuns.length}) available ${pluralText('date', courseRuns.length)}`; - const dateFooterText = displayImportantDates ? assignmentImportantDates : availableCourseRuns; + const availableCourseRunsCount = `(${availableCourseRuns.length}) available ${pluralText('date', availableCourseRuns.length)}`; + const dateFooterText = displayImportantDates ? assignmentImportantDates : availableCourseRunsCount; return { ...course, subtitle: partners.map(partner => partner.name).join(', '), diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index db58f6a5c5..208fef68a0 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -73,6 +73,9 @@ export const MEMBERS_TABLE_PAGE_SIZE = 10; // Enroll-by date warning message threshold by days export const ENROLL_BY_DATE_DAYS_THRESHOLD = 10; +// Stale enrollment dropoff time +export const STALE_ENROLLMENT_DROPOFF_DAYS = 90; + // Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`. // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const learnerCreditManagementQueryKeys = { diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index df337300c0..a57972e0d5 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -553,3 +553,14 @@ export const isLmsBudget = ( ) => activeIntegrationsLength > 0 && isUniversalGroup; export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); + +export const enrollableCourseRuns = ({ courseRuns, subsidyExpirationDatetime, staleEnrollmentDropOffTime }) => { + const clonedCourseRuns = courseRuns.map(a => ({ ...a, enrollBy: a.enrollBy * 1000 })); + const sortedCourseRuns = clonedCourseRuns.sort((a, b) => a.enrollBy - b.enrollBy); + const filteredCourseRuns = sortedCourseRuns.filter( + ({ enrollBy }) => dayjs(enrollBy).isBefore( + dayjs(subsidyExpirationDatetime).add(staleEnrollmentDropOffTime, 'days'), + ), + ); + return filteredCourseRuns; +}; diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx index b4769e93c7..a8d366faf2 100644 --- a/src/components/learner-credit-management/search/CatalogSearchResults.jsx +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -9,7 +9,10 @@ import { } from '@openedx/paragon'; import CourseCard from '../cards/CourseCard'; -import { DEFAULT_PAGE, SEARCH_RESULT_PAGE_SIZE } from '../data'; +import { + DEFAULT_PAGE, + SEARCH_RESULT_PAGE_SIZE, +} from '../data'; export const ERROR_MESSAGE = 'An error occurred while retrieving data'; @@ -54,7 +57,6 @@ export const BaseCatalogSearchResults = ({ ], [], ); - const tableData = useMemo( () => searchResults?.hits || [], [searchResults?.hits], diff --git a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx index 63c907edc1..9d1ebe64da 100644 --- a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx +++ b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx @@ -1,5 +1,6 @@ import omit from 'lodash/omit'; +// eslint-disable-next-line import/no-extraneous-dependencies,@typescript-eslint/no-unused-vars import { AxiosError } from 'axios'; import type { FormWorkflowHandlerArgs, FormWorkflowStep } from '../../forms/FormWorkflow'; import SSOConfigConnectStep, { getValidations as getSSOConfigConnectStepValidations } from './steps/NewSSOConfigConnectStep'; @@ -104,15 +105,12 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError, intl }) formFieldsChanged, dispatch, }: FormWorkflowHandlerArgs) => { - let err = null; - // Accurately detect if form fields have changed or there's and error in existing record let isErrored; if (formFields?.uuid) { - isErrored = - formFields.erroredAt && - formFields.submittedAt && - formFields.submittedAt < formFields.erroredAt; + isErrored = formFields.erroredAt + && formFields.submittedAt + && formFields.submittedAt < formFields.erroredAt; } if (!isErrored && !formFieldsChanged) { return formFields; @@ -131,7 +129,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError, intl }) updatedFormFields = updateResponse.data; dispatch?.(resetFormEditState()); } catch (error: AxiosError | any) { - err = handleErrors(error); + handleErrors(error); if (error.message?.includes('Must provide valid IDP metadata url')) { errHandler?.(INVALID_IDP_METADATA_ERROR); } else if (error.message?.includes('Record has already been submitted for configuration.')) { @@ -146,7 +144,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError, intl }) updatedFormFields.uuid = createResponse.data.record; updatedFormFields.spMetadataUrl = createResponse.data.sp_metadata_url; } catch (error: AxiosError | any) { - err = handleErrors(error); + handleErrors(error); if (error.message?.includes('Must provide valid IDP metadata url')) { errHandler?.(INVALID_IDP_METADATA_ERROR); } else { diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx index 6f8f300cc9..34aef3e42a 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx @@ -58,15 +58,14 @@ const SSOConfigAuthorizeStep = () => { const { testLink } = createSAMLURLs({ configuration, idpSlug, enterpriseSlug, learnerPortalEnabled, }); - + /** - * Contains link to download service metadata XML which looks like: + * Contains link to download service metadata XML which looks like: * https://access.edx.org/samlp/metadata?connection=rwth-aachen-7cef5c13-7d67-460e-9962-3ec31f91ff20 * Built using enterprise slug and uuid */ - const linkToDownloadMetadataXML = - formFields?.spMetadataUrl || - `${configuration.EDX_ACCESS_URL}/samlp/metadata?connection=${enterpriseSlug}-${formFields?.uuid}`; + const linkToDownloadMetadataXML = formFields?.spMetadataUrl + || `${configuration.EDX_ACCESS_URL}/samlp/metadata?connection=${enterpriseSlug}-${formFields?.uuid}`; return ( <> From 0e51dec14f6bb3e1790e8ae602144bef8084f045 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Wed, 4 Sep 2024 17:09:38 -0400 Subject: [PATCH 03/23] chore: Normalize failing tests --- __mocks__/react-instantsearch-dom.jsx | 111 ++++++++++++++++-- .../cards/tests/CourseCard.test.jsx | 21 +++- .../tests/InviteMemberModal.test.jsx | 1 + .../tests/CatalogSearchResults.test.jsx | 26 ++++ 4 files changed, 149 insertions(+), 10 deletions(-) diff --git a/__mocks__/react-instantsearch-dom.jsx b/__mocks__/react-instantsearch-dom.jsx index dbb7fbaba4..f251a7ba25 100644 --- a/__mocks__/react-instantsearch-dom.jsx +++ b/__mocks__/react-instantsearch-dom.jsx @@ -2,26 +2,119 @@ /* eslint-disable react/prop-types */ const React = require('react'); +const dayjs = require('dayjs'); const MockReactInstantSearch = jest.genMockFromModule( 'react-instantsearch-dom', ); -// eslint-disable-next-line @typescript-eslint/naming-convention -const advertised_course_run = { - start: '2020-09-09T04:00:00Z', - key: 'course-v1:edX+Bee101+3T2020', -}; const mockNormalizedData = { start_date: '2020-09-09T04:00:00Z', - end_date: '2021-09-09T04:00:00Z', - enroll_by_date: '2020-09-15T04:00:00Z', + end_date: dayjs('2020-09-09T04:00:00Z').add(1, 'year').toISOString(), + enroll_by_date: dayjs('2020-09-09T04:00:00Z').add(3, 'months').toISOString(), }; +const mockCurrentStartDate = dayjs().add(3, 'months').toISOString(); + /* eslint-disable camelcase */ const fakeHits = [ - { objectID: '1', aggregation_key: 'course:Bees101', title: 'bla', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Bees101', normalized_metadata: mockNormalizedData }, - { objectID: '2', aggregation_key: 'course:Wasps200', title: 'blp', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Wasps200', normalized_metadata: mockNormalizedData }, + { + objectID: '1', + aggregation_key: 'course:Bees101', + title: 'bla', + partners: [ + { name: 'edX' }, + { name: 'another_unused' }, + ], + advertised_course_run: { + key: 'course-v1:edx+Bees101+1010', + start: mockCurrentStartDate, + end: dayjs(mockCurrentStartDate).add(1, 'year').toISOString(), + enroll_by: dayjs(mockCurrentStartDate).unix(), + isActive: true, + max_effort: 5, + min_effort: 1, + pacing_type: 'self_paced', + weeks_to_complete: 8, + upgrade_deadline: dayjs(mockCurrentStartDate).add(3, 'months').unix(), + }, + key: 'Bees101', + normalized_metadata: mockNormalizedData, + courseRuns: [ + { + key: 'course-v1:edx+Bees101+1010', + start: mockCurrentStartDate, + end: dayjs(mockCurrentStartDate).add(1, 'year').toISOString(), + enroll_by: dayjs(mockCurrentStartDate).unix(), + isActive: true, + max_effort: 5, + min_effort: 1, + pacing_type: 'self_paced', + weeks_to_complete: 8, + upgrade_deadline: dayjs(mockCurrentStartDate).add(3, 'months').unix(), + }, + { + key: 'course-v1:edX+Bee101+3T2020', + start: '2020-09-09T04:00:00Z', + end: dayjs('2020-09-09T04:00:00Z').add(1, 'year').toISOString(), + enroll_by: dayjs('2020-09-09T04:00:00Z').unix(), + isActive: true, + max_effort: 5, + min_effort: 1, + pacing_type: 'self_paced', + weeks_to_complete: 8, + upgrade_deadline: dayjs('2020-09-09T04:00:00Z').add(3, 'months').unix(), + }, + ], + }, + { objectID: '2', + aggregation_key: 'course:Wasps200', + title: 'blp', + partners: [ + { name: 'edX' }, + { name: 'another_unused' }, + ], + advertised_course_run: { + key: 'course-v1:edx+Wasps200+1010T2024', + start: '2022-10-09T04:00:00Z', + end: dayjs('2022-10-09T04:00:00Z').add(1, 'year').toISOString(), + enroll_by: dayjs('2022-10-09T04:00:00Z').unix(), + isActive: true, + max_effort: 5, + min_effort: 1, + pacing_type: 'self_paced', + weeks_to_complete: 8, + upgrade_deadline: dayjs('2022-10-09T04:00:00Z').add(3, 'months').unix(), + }, + key: 'Wasps200', + normalized_metadata: mockNormalizedData, + courseRuns: [ + { + key: 'course-v1:edx+Wasps200+1010T2024', + start: '2022-10-09T04:00:00Z', + end: dayjs('2022-10-09T04:00:00Z').add(1, 'year').toISOString(), + enroll_by: dayjs('2022-10-09T04:00:00Z').unix(), + isActive: true, + max_effort: 5, + min_effort: 1, + pacing_type: 'self_paced', + weeks_to_complete: 8, + upgrade_deadline: dayjs('2022-10-09T04:00:00Z').add(3, 'months').unix(), + }, + { + key: 'course-v1:edX+Wasps200+3T2020', + start: '2020-09-09T04:00:00Z', + end: dayjs('2020-09-09T04:00:00Z').add(1, 'year').toISOString(), + enroll_by: dayjs('2020-09-09T04:00:00Z').unix(), + isActive: true, + max_effort: 5, + min_effort: 1, + pacing_type: 'self_paced', + weeks_to_complete: 8, + upgrade_deadline: dayjs('2020-09-09T04:00:00Z').add(3, 'months').unix(), + }, + ], + }, ]; /* eslint-enable camelcase */ diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 7088eca53f..b42374330e 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -9,6 +9,7 @@ import { QueryClientProvider, useQueryClient } from '@tanstack/react-query'; import { AppContext } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import dayjs from 'dayjs'; import CourseCard from '../CourseCard'; import { formatPrice, @@ -53,13 +54,29 @@ const originalData = { course_type: 'course', key: 'course-123x', normalized_metadata: { - enroll_by_date: '2016-02-18T04:00:00Z', + enroll_by_date: dayjs(1892678399 * 1000).toISOString(), start_date: '2016-04-18T04:00:00Z', content_price: 100, }, original_image_url: '', partners: [{ logo_image_url: '', name: 'Course Provider' }], title: 'Course Title', + courseRuns: [ + { + key: 'course-v1:edX+course-123x+3T2020', + start: '2016-04-18T04:00:00Z', + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + enroll_by: 1892678399, + }, + ], + advertised_course_run: { + key: 'course-v1:edX+course-123x+3T2020', + start: '2016-04-18T04:00:00Z', + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + enroll_by: 1892678399, + }, }; const imageAltText = `${originalData.title} course image`; @@ -109,6 +126,7 @@ const mockSubsidyAccessPolicy = { aggregates: { spendAvailableUsd: 50000, }, + subsidyExpirationDatetime: '2100-02-18T04:00:00Z', }; const mockLearnerEmails = ['hello@example.com', 'world@example.com', 'dinesh@example.com']; @@ -194,6 +212,7 @@ describe('Course card works as expected', () => { expect(screen.getByText(defaultProps.original.title)).toBeInTheDocument(); expect(screen.getByText(defaultProps.original.partners[0].name)).toBeInTheDocument(); expect(screen.getByText('$100')).toBeInTheDocument(); + userEvent.click(screen.getByText('Assign')); expect(screen.getByText('Per learner price')).toBeInTheDocument(); expect(screen.getByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); expect(screen.getByText('Course')).toBeInTheDocument(); diff --git a/src/components/learner-credit-management/invite-modal/tests/InviteMemberModal.test.jsx b/src/components/learner-credit-management/invite-modal/tests/InviteMemberModal.test.jsx index efb3c305ee..3da52a3062 100644 --- a/src/components/learner-credit-management/invite-modal/tests/InviteMemberModal.test.jsx +++ b/src/components/learner-credit-management/invite-modal/tests/InviteMemberModal.test.jsx @@ -61,6 +61,7 @@ const mockSubsidyAccessPolicy = { spendAvailableUsd: 50000, }, groupAssociations: ['test-group-uuid'], + policyType: 'AssignedLearnerCreditAccessPolicy', }; const mockDisplaySuccessfulInvitationToast = jest.fn(); diff --git a/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx index 09ca3596d6..48ab49e26f 100644 --- a/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx @@ -104,6 +104,7 @@ const searchResults = { nbPages: 6, hits: [ { + key: 'Bees101', title: TEST_COURSE_NAME, partners: [{ name: TEST_PARTNER, logo_image_url: '' }], enterprise_catalog_query_titles: TEST_CATALOGS, @@ -113,10 +114,12 @@ const searchResults = { availability: ['Available Now'], content_type: CONTENT_TYPE_COURSE, advertised_course_run: { + key: 'course-v1:edx+Bees101+1010', start: '2020-01-24T05:00:00Z', end: '2080-01-01T17:00:00Z', upgrade_deadline: 1892678399, pacing_type: 'self_paced', + enroll_by: 1892678399, }, normalized_metadata: { start_date: '2020-09-09T04:00:00Z', @@ -124,8 +127,19 @@ const searchResults = { enroll_by_date: '2020-09-15T04:00:00Z', content_price: 199, }, + courseRuns: [ + { + key: 'course-v1:edx+Bees101+1010', + start: '2020-01-24T05:00:00Z', + end: '2080-01-01T17:00:00Z', + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + enroll_by: 1892678399, + }, + ], }, { + key: 'Wasps200', title: TEST_COURSE_NAME_2, partners: [{ name: TEST_PARTNER_2, logo_image_url: '' }], enterprise_catalog_query_titles: TEST_CATALOGS_2, @@ -135,10 +149,12 @@ const searchResults = { availability: ['Available Now'], content_type: CONTENT_TYPE_COURSE, advertised_course_run: { + key: 'course-v1:edX+Wasps200+3T2020', start: '2020-01-24T05:00:00Z', end: '2080-01-01T17:00:00Z', upgrade_deadline: 1892678399, pacing_type: 'self_paced', + enroll_by: 1892678399, }, normalized_metadata: { start_date: '2020-09-09T04:00:00Z', @@ -146,6 +162,16 @@ const searchResults = { enroll_by_date: '2020-09-15T04:00:00Z', content_price: 199, }, + courseRuns: [ + { + key: 'course-v1:edX+Wasps200+3T2020', + start: '2020-01-24T05:00:00Z', + end: '2080-01-01T17:00:00Z', + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + enroll_by: 1892678399, + }, + ], }, ], page: 1, From 499af785161820fbc3591300d1ba55996cc8e988 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 5 Sep 2024 10:37:35 -0400 Subject: [PATCH 04/23] chore: fix tests --- .../AssignmentModalmportantDates.jsx | 8 +- .../cards/tests/CourseCard.test.jsx | 129 +++++++++++++++--- .../SettingsSSOTab/SSOFormWorkflowConfig.tsx | 2 + .../steps/NewSSOConfigConfigureStep.tsx | 2 + 4 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index f0bd7b78d6..e6abe4d246 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -10,22 +10,22 @@ import { hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; const messages = defineMessages({ importantDates: { - id: 'enterprise.course.about.page.important-dates.title', + id: 'lcm.budget.detail.page.catalog.search.allocation.modal.important-dates.title', defaultMessage: 'Important dates', description: 'Title for the important dates section on the assignment modal', }, enrollByDate: { - id: 'enterprise.course.about.page.important-dates.enroll-by-date', + id: 'lcm.budget.detail.page.catalog.search.allocation.modal.important-dates.enroll-by-date', defaultMessage: 'Enroll-by date', description: 'Enroll-by date for the important dates section on the assignment modal', }, courseStarts: { - id: 'enterprise.course.about.page.important-dates.course-starts', + id: 'lcm.budget.detail.page.catalog.search.allocation.modal.important-dates.course-starts', defaultMessage: 'Course starts', description: 'Course starts for the important dates section on the assignment modal in future tense', }, courseStarted: { - id: 'enterprise.course.about.page.important-dates.course-started', + id: 'lcm.budget.detail.page.catalog.search.allocation.modal.important-dates.course-started', defaultMessage: 'Course started', description: 'Course started the important dates section on the assignment modal in past tense', }, diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index b42374330e..8109643756 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -13,7 +13,7 @@ import dayjs from 'dayjs'; import CourseCard from '../CourseCard'; import { formatPrice, - learnerCreditManagementQueryKeys, + learnerCreditManagementQueryKeys, SHORT_MONTH_DATE_FORMAT, useBudgetId, useSubsidyAccessPolicy, } from '../../data'; @@ -48,6 +48,9 @@ jest.mock('../../data', () => ({ })); jest.mock('../../../../data/services/EnterpriseAccessApiService'); +const futureStartDate = dayjs().add(5, 'days').toISOString(); +const pastStartDate = dayjs().subtract(5, 'days').toISOString(); + const originalData = { availability: ['Upcoming'], card_image_url: undefined, @@ -55,7 +58,7 @@ const originalData = { key: 'course-123x', normalized_metadata: { enroll_by_date: dayjs(1892678399 * 1000).toISOString(), - start_date: '2016-04-18T04:00:00Z', + start_date: futureStartDate, content_price: 100, }, original_image_url: '', @@ -64,7 +67,7 @@ const originalData = { courseRuns: [ { key: 'course-v1:edX+course-123x+3T2020', - start: '2016-04-18T04:00:00Z', + start: futureStartDate, upgrade_deadline: 1892678399, pacing_type: 'self_paced', enroll_by: 1892678399, @@ -72,7 +75,7 @@ const originalData = { ], advertised_course_run: { key: 'course-v1:edX+course-123x+3T2020', - start: '2016-04-18T04:00:00Z', + start: futureStartDate, upgrade_deadline: 1892678399, pacing_type: 'self_paced', enroll_by: 1892678399, @@ -93,10 +96,26 @@ const execEdData = { key: 'exec-ed-course-123x', entitlements: [{ price: '999.00' }], normalized_metadata: { - enroll_by_date: '2016-02-18T04:00:00Z', - start_date: '2016-04-18T04:00:00Z', + enroll_by_date: dayjs(1892678399 * 1000).toISOString(), + start_date: futureStartDate, content_price: 999, }, + courseRuns: [ + { + key: 'course-v1:edX+course-123x+3T2020', + start: futureStartDate, + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + enroll_by: 1892678399, + }, + ], + advertised_course_run: { + key: 'course-v1:edX+course-123x+3T2020', + start: futureStartDate, + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + enroll_by: 1892678399, + }, original_image_url: '', partners: [{ logo_image_url: '', name: 'Course Provider' }], title: 'Exec Ed Title', @@ -214,7 +233,7 @@ describe('Course card works as expected', () => { expect(screen.getByText('$100')).toBeInTheDocument(); userEvent.click(screen.getByText('Assign')); expect(screen.getByText('Per learner price')).toBeInTheDocument(); - expect(screen.getByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); + expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); expect(screen.getByText('Course')).toBeInTheDocument(); // Has card image defined even though the course metadata does not contain an image URL const cardImage = screen.getByAltText(imageAltText); @@ -247,7 +266,8 @@ describe('Course card works as expected', () => { test('executive education card renders', () => { renderWithRouter(); expect(screen.queryByText('$999')).toBeInTheDocument(); - expect(screen.queryByText('Starts Apr 18, 2016 • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); + userEvent.click(screen.getByText('Assign')); + expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); expect(screen.queryByText('Executive Education')).toBeInTheDocument(); const viewCourseCTA = screen.getByText('View course', { selector: 'a' }); expect(viewCourseCTA.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/executive-education-2u/course/exec-ed-course-123x'); @@ -266,6 +286,8 @@ describe('Course card works as expected', () => { const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); + expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); + userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const assignmentModal = within(screen.getByRole('dialog')); @@ -283,6 +305,8 @@ describe('Course card works as expected', () => { const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); + expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); + userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const helpCenterButton = screen.getByText('Help Center: Course Assignments'); @@ -298,50 +322,93 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: 'content_not_in_catalog', shouldRetryAllocationAfterException: false, // no ability to retry after this error + courseTense: { + courseStartDate: futureStartDate, + expected: 'Course starts', + }, }, { shouldSubmitAssignments: true, hasAllocationException: true, allocationExceptionReason: 'not_enough_value_in_subsidy', shouldRetryAllocationAfterException: false, + courseTense: { + courseStartDate: pastStartDate, + expected: 'Course started', + }, }, { shouldSubmitAssignments: true, hasAllocationException: true, allocationExceptionReason: 'not_enough_value_in_subsidy', shouldRetryAllocationAfterException: true, + courseTense: { + courseStartDate: futureStartDate, + expected: 'Course starts', + }, }, { shouldSubmitAssignments: true, hasAllocationException: true, allocationExceptionReason: 'policy_spend_limit_reached', shouldRetryAllocationAfterException: false, + courseTense: { + courseStartDate: pastStartDate, + expected: 'Course started', + }, }, { shouldSubmitAssignments: true, hasAllocationException: true, allocationExceptionReason: 'policy_spend_limit_reached', shouldRetryAllocationAfterException: true, + courseTense: { + courseStartDate: futureStartDate, + expected: 'Course starts', + }, }, { shouldSubmitAssignments: true, hasAllocationException: true, allocationExceptionReason: null, shouldRetryAllocationAfterException: false, + courseTense: { + courseStartDate: pastStartDate, + expected: 'Course started', + }, }, { shouldSubmitAssignments: true, hasAllocationException: true, allocationExceptionReason: null, shouldRetryAllocationAfterException: true, + courseTense: { + courseStartDate: futureStartDate, + expected: 'Course starts', + }, + }, + { + shouldSubmitAssignments: true, + hasAllocationException: false, + courseTense: { + courseStartDate: pastStartDate, + expected: 'Course started', + }, + }, + { + shouldSubmitAssignments: false, + hasAllocationException: false, + courseTense: { + courseStartDate: futureStartDate, + expected: 'Course starts', + }, }, - { shouldSubmitAssignments: true, hasAllocationException: false }, - { shouldSubmitAssignments: false, hasAllocationException: false }, ])('opens assignment modal, fills out information, and submits assignments accordingly - with success or with an exception (%s)', async ({ shouldSubmitAssignments, hasAllocationException, allocationExceptionReason, shouldRetryAllocationAfterException, + courseTense, }) => { const mockUpdatedLearnerAssignments = [mockLearnerEmails[0]]; const mockNoChangeLearnerAssignments = [mockLearnerEmails[1]]; @@ -380,11 +447,33 @@ describe('Course card works as expected', () => { useQueryClient.mockReturnValue({ invalidateQueries: mockInvalidateQueries, }); - renderWithRouter(); + const props = { + original: { + ...defaultProps.original, + normalized_metadata: { + ...defaultProps.original.normalized_metadata, + start_date: courseTense.courseStartDate, + }, + courseRuns: [{ + ...defaultProps.original.courseRuns[0], + start: courseTense.courseStartDate, + }, + ], + advertised_course_run: { + ...defaultProps.original.advertised_course_run, + start: courseTense.courseStartDate, + }, + }, + }; + renderWithRouter(); const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); + userEvent.click(assignCourseCTA); + expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); + userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); + const assignmentModal = within(screen.getByRole('dialog')); expect(assignmentModal.getByText('Assign this course')).toBeInTheDocument(); @@ -396,8 +485,7 @@ describe('Course card works as expected', () => { expect(modalCourseCard.getByText(defaultProps.original.partners[0].name)).toBeInTheDocument(); expect(modalCourseCard.getByText('$100')).toBeInTheDocument(); expect(modalCourseCard.getByText('Per learner price')).toBeInTheDocument(); - expect(modalCourseCard.getByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); - expect(modalCourseCard.getByText('Course')).toBeInTheDocument(); + expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); const cardImage = modalCourseCard.getByAltText(imageAltText); expect(cardImage).toBeInTheDocument(); expect(cardImage.src).toBeDefined(); @@ -420,7 +508,15 @@ describe('Course card works as expected', () => { const expectedAvailableBalance = formatPrice(mockSubsidyAccessPolicy.aggregates.spendAvailableUsd); expect(assignmentModal.getByText(expectedAvailableBalance)).toBeInTheDocument(); - // Verify collapsibles + // Verify important dates + expect(assignmentModal.getByText('Enroll-by date')).toBeInTheDocument(); + expect(assignmentModal.getByText('Dec 22, 2029')).toBeInTheDocument(); + expect(assignmentModal.getByText(courseTense.expected)).toBeInTheDocument(); + expect(assignmentModal.getByText( + dayjs(courseTense.courseStartDate).format(SHORT_MONTH_DATE_FORMAT), + )).toBeInTheDocument(); + + // Verify collapsible expect(assignmentModal.getByText('How assigning this course works')).toBeInTheDocument(); expect(assignmentModal.getByText('Next steps for assigned learners')).toBeInTheDocument(); expect(assignmentModal.getByText('Learners will be notified of this course assignment by email.')).toBeInTheDocument(); @@ -478,7 +574,7 @@ describe('Course card works as expected', () => { mockSubsidyAccessPolicy.uuid, expect.objectContaining({ content_price_cents: 10000, - content_key: 'course-123x', + content_key: 'course-v1:edX+course-123x+3T2020', learner_emails: mockLearnerEmails, }), ); @@ -598,7 +694,8 @@ describe('Course card works as expected', () => { const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); - + expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); + userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); const assignmentModal = within(screen.getByRole('dialog')); // Verify "Assign" CTA is disabled diff --git a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx index 9d1ebe64da..d5418ff3ce 100644 --- a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx +++ b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx @@ -4,6 +4,8 @@ import omit from 'lodash/omit'; import { AxiosError } from 'axios'; import type { FormWorkflowHandlerArgs, FormWorkflowStep } from '../../forms/FormWorkflow'; import SSOConfigConnectStep, { getValidations as getSSOConfigConnectStepValidations } from './steps/NewSSOConfigConnectStep'; +// TODO: Resolve dependency issue +// eslint-disable-next-line import/no-cycle import SSOConfigConfigureStep, { getValidations as getSSOConfigConfigureStepValidations } from './steps/NewSSOConfigConfigureStep'; import SSOConfigAuthorizeStep, { getValidations as getSSOConfigAuthorizeStepValidations } from './steps/NewSSOConfigAuthorizeStep'; import SSOConfigConfirmStep from './steps/NewSSOConfigConfirmStep'; diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx index 4e1806a9ac..905aa57085 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx @@ -11,6 +11,8 @@ import { urlValidation } from '../../../../utils'; import { FormWorkflowStep } from '../../../forms/FormWorkflow'; import { FORM_ERROR_MESSAGE, setStepAction } from '../../../forms/data/actions'; import { INVALID_IDP_METADATA_ERROR, RECORD_UNDER_CONFIGURATIONS_ERROR } from '../../data/constants'; +// TODO: Resolve dependency issue +// eslint-disable-next-line import/no-cycle import { SSOConfigCamelCase } from '../SSOFormWorkflowConfig'; const messages = defineMessages({ From fddd9ad2dd23e0cf6d25bd7379b3006f4eda161c Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Fri, 6 Sep 2024 10:03:02 -0400 Subject: [PATCH 05/23] chore: fix tests 2 --- __mocks__/react-instantsearch-dom.jsx | 10 +-- .../charts/ChartWrapper.jsx | 4 +- .../tabs/Completions.test.jsx | 1 + .../tabs/Engagements.test.jsx | 1 + .../tabs/Enrollments.test.jsx | 1 + .../AssignmentModalmportantDates.jsx | 10 ++- .../cards/tests/CourseCard.test.jsx | 88 ++++++++++--------- src/index.scss | 9 +- src/utils.js | 10 ++- 9 files changed, 80 insertions(+), 54 deletions(-) diff --git a/__mocks__/react-instantsearch-dom.jsx b/__mocks__/react-instantsearch-dom.jsx index f251a7ba25..46f962eea1 100644 --- a/__mocks__/react-instantsearch-dom.jsx +++ b/__mocks__/react-instantsearch-dom.jsx @@ -8,14 +8,14 @@ const MockReactInstantSearch = jest.genMockFromModule( 'react-instantsearch-dom', ); +const mockCurrentStartDate = dayjs().add(3, 'months').toISOString(); + const mockNormalizedData = { - start_date: '2020-09-09T04:00:00Z', - end_date: dayjs('2020-09-09T04:00:00Z').add(1, 'year').toISOString(), - enroll_by_date: dayjs('2020-09-09T04:00:00Z').add(3, 'months').toISOString(), + start_date: mockCurrentStartDate, + end_date: dayjs(mockCurrentStartDate).add(1, 'year').toISOString(), + enroll_by_date: dayjs(mockCurrentStartDate).add(3, 'months').toISOString(), }; -const mockCurrentStartDate = dayjs().add(3, 'months').toISOString(); - /* eslint-disable camelcase */ const fakeHits = [ { diff --git a/src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx b/src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx index ea753ed2a4..4979239a15 100644 --- a/src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx +++ b/src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx @@ -1,9 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { - Spinner, -} from '@openedx/paragon'; +import { Spinner } from '@openedx/paragon'; import ScatterChart from './ScatterChart'; import LineChart from './LineChart'; import BarChart from './BarChart'; diff --git a/src/components/AdvanceAnalyticsV2/tabs/Completions.test.jsx b/src/components/AdvanceAnalyticsV2/tabs/Completions.test.jsx index e920b824a8..8cbf6037e7 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Completions.test.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Completions.test.jsx @@ -5,6 +5,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import '@testing-library/jest-dom'; import MockAdapter from 'axios-mock-adapter'; +// eslint-disable-next-line import/no-extraneous-dependencies import axios from 'axios'; import { BrowserRouter as Router } from 'react-router-dom'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx b/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx index 9b4b196892..42327c1e38 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx @@ -5,6 +5,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import '@testing-library/jest-dom'; import MockAdapter from 'axios-mock-adapter'; +// eslint-disable-next-line import/no-extraneous-dependencies import axios from 'axios'; import { BrowserRouter as Router } from 'react-router-dom'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/components/AdvanceAnalyticsV2/tabs/Enrollments.test.jsx b/src/components/AdvanceAnalyticsV2/tabs/Enrollments.test.jsx index b129721370..b37aecce61 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Enrollments.test.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Enrollments.test.jsx @@ -5,6 +5,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import '@testing-library/jest-dom'; import MockAdapter from 'axios-mock-adapter'; +// eslint-disable-next-line import/no-extraneous-dependencies import axios from 'axios'; import { BrowserRouter as Router } from 'react-router-dom'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index e6abe4d246..319eda8596 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -6,6 +6,7 @@ import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; +import { logInfo } from '@edx/frontend-platform/logging'; import { hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; const messages = defineMessages({ @@ -55,13 +56,18 @@ AssignmentModalImportantDate.propTypes = { const AssignmentModalImportantDates = ({ courseRun }) => { const intl = useIntl(); - const enrollByDate = dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT) ?? null; - const courseStartDate = dayjs(courseRun.start).format(SHORT_MONTH_DATE_FORMAT) ?? null; + const enrollByDate = courseRun.enrollBy ? dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT) : null; + const courseStartDate = courseRun.start ? dayjs(courseRun.start).format(SHORT_MONTH_DATE_FORMAT) : null; const courseHasStartedLabel = hasCourseStarted(courseStartDate) ? intl.formatMessage(messages.courseStarted) : intl.formatMessage(messages.courseStarts); + // This is an edge case that the user should never enter but covered nonetheless if (!enrollByDate && !courseStartDate) { + logInfo(`[frontend-app-admin-portal][AssignmentModalImportantDates] + Component did not render, no courseRun enrollBy date or courseStart date provided + courseRun: ${courseRun} + `); return null; } diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 8109643756..714bea5e66 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -50,6 +50,8 @@ jest.mock('../../../../data/services/EnterpriseAccessApiService'); const futureStartDate = dayjs().add(5, 'days').toISOString(); const pastStartDate = dayjs().subtract(5, 'days').toISOString(); +const enrollByTimestamp = dayjs().subtract(10, 'days').unix(); +const enrollByDropdownText = `Enroll by ${dayjs(enrollByTimestamp * 1000).format(SHORT_MONTH_DATE_FORMAT)}`; const originalData = { availability: ['Upcoming'], @@ -70,7 +72,7 @@ const originalData = { start: futureStartDate, upgrade_deadline: 1892678399, pacing_type: 'self_paced', - enroll_by: 1892678399, + enroll_by: enrollByTimestamp, }, ], advertised_course_run: { @@ -78,7 +80,7 @@ const originalData = { start: futureStartDate, upgrade_deadline: 1892678399, pacing_type: 'self_paced', - enroll_by: 1892678399, + enroll_by: enrollByTimestamp, }, }; const imageAltText = `${originalData.title} course image`; @@ -233,7 +235,7 @@ describe('Course card works as expected', () => { expect(screen.getByText('$100')).toBeInTheDocument(); userEvent.click(screen.getByText('Assign')); expect(screen.getByText('Per learner price')).toBeInTheDocument(); - expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); expect(screen.getByText('Course')).toBeInTheDocument(); // Has card image defined even though the course metadata does not contain an image URL const cardImage = screen.getByAltText(imageAltText); @@ -286,8 +288,8 @@ describe('Course card works as expected', () => { const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); - expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); - userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); + userEvent.click(screen.getByText(enrollByDropdownText)); expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const assignmentModal = within(screen.getByRole('dialog')); @@ -305,8 +307,8 @@ describe('Course card works as expected', () => { const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); - expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); - userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); + userEvent.click(screen.getByText(enrollByDropdownText)); expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const helpCenterButton = screen.getByText('Help Center: Course Assignments'); @@ -322,9 +324,9 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: 'content_not_in_catalog', shouldRetryAllocationAfterException: false, // no ability to retry after this error - courseTense: { + courseImportantDates: { courseStartDate: futureStartDate, - expected: 'Course starts', + expectedCourseStartText: 'Course starts', }, }, { @@ -332,9 +334,9 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: 'not_enough_value_in_subsidy', shouldRetryAllocationAfterException: false, - courseTense: { + courseImportantDates: { courseStartDate: pastStartDate, - expected: 'Course started', + expectedCourseStartText: 'Course started', }, }, { @@ -342,9 +344,9 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: 'not_enough_value_in_subsidy', shouldRetryAllocationAfterException: true, - courseTense: { + courseImportantDates: { courseStartDate: futureStartDate, - expected: 'Course starts', + expectedCourseStartText: 'Course starts', }, }, { @@ -352,9 +354,9 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: 'policy_spend_limit_reached', shouldRetryAllocationAfterException: false, - courseTense: { + courseImportantDates: { courseStartDate: pastStartDate, - expected: 'Course started', + expectedCourseStartText: 'Course started', }, }, { @@ -362,9 +364,9 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: 'policy_spend_limit_reached', shouldRetryAllocationAfterException: true, - courseTense: { + courseImportantDates: { courseStartDate: futureStartDate, - expected: 'Course starts', + expectedCourseStartText: 'Course starts', }, }, { @@ -372,9 +374,9 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: null, shouldRetryAllocationAfterException: false, - courseTense: { + courseImportantDates: { courseStartDate: pastStartDate, - expected: 'Course started', + expectedCourseStartText: 'Course started', }, }, { @@ -382,25 +384,25 @@ describe('Course card works as expected', () => { hasAllocationException: true, allocationExceptionReason: null, shouldRetryAllocationAfterException: true, - courseTense: { + courseImportantDates: { courseStartDate: futureStartDate, - expected: 'Course starts', + expectedCourseStartText: 'Course starts', }, }, { shouldSubmitAssignments: true, hasAllocationException: false, - courseTense: { - courseStartDate: pastStartDate, - expected: 'Course started', + courseImportantDates: { + courseStartDate: null, + expectedCourseStartText: '', }, }, { shouldSubmitAssignments: false, hasAllocationException: false, - courseTense: { - courseStartDate: futureStartDate, - expected: 'Course starts', + courseImportantDates: { + courseStartDate: null, + expectedCourseStartText: '', }, }, ])('opens assignment modal, fills out information, and submits assignments accordingly - with success or with an exception (%s)', async ({ @@ -408,7 +410,7 @@ describe('Course card works as expected', () => { hasAllocationException, allocationExceptionReason, shouldRetryAllocationAfterException, - courseTense, + courseImportantDates, }) => { const mockUpdatedLearnerAssignments = [mockLearnerEmails[0]]; const mockNoChangeLearnerAssignments = [mockLearnerEmails[1]]; @@ -447,21 +449,24 @@ describe('Course card works as expected', () => { useQueryClient.mockReturnValue({ invalidateQueries: mockInvalidateQueries, }); + const { + courseStartDate, expectedCourseStartText, + } = courseImportantDates; const props = { original: { ...defaultProps.original, normalized_metadata: { ...defaultProps.original.normalized_metadata, - start_date: courseTense.courseStartDate, + start_date: courseStartDate, }, courseRuns: [{ ...defaultProps.original.courseRuns[0], - start: courseTense.courseStartDate, + start: courseStartDate, }, ], advertised_course_run: { ...defaultProps.original.advertised_course_run, - start: courseTense.courseStartDate, + start: courseStartDate, }, }, }; @@ -470,9 +475,8 @@ describe('Course card works as expected', () => { expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); - - expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); - userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); + userEvent.click(screen.getByText(enrollByDropdownText)); const assignmentModal = within(screen.getByRole('dialog')); @@ -485,7 +489,7 @@ describe('Course card works as expected', () => { expect(modalCourseCard.getByText(defaultProps.original.partners[0].name)).toBeInTheDocument(); expect(modalCourseCard.getByText('$100')).toBeInTheDocument(); expect(modalCourseCard.getByText('Per learner price')).toBeInTheDocument(); - expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); const cardImage = modalCourseCard.getByAltText(imageAltText); expect(cardImage).toBeInTheDocument(); expect(cardImage.src).toBeDefined(); @@ -510,11 +514,15 @@ describe('Course card works as expected', () => { // Verify important dates expect(assignmentModal.getByText('Enroll-by date')).toBeInTheDocument(); - expect(assignmentModal.getByText('Dec 22, 2029')).toBeInTheDocument(); - expect(assignmentModal.getByText(courseTense.expected)).toBeInTheDocument(); expect(assignmentModal.getByText( - dayjs(courseTense.courseStartDate).format(SHORT_MONTH_DATE_FORMAT), + dayjs(enrollByTimestamp * 1000).format(SHORT_MONTH_DATE_FORMAT), )).toBeInTheDocument(); + if (courseStartDate) { + expect(assignmentModal.getByText(expectedCourseStartText)).toBeInTheDocument(); + expect(assignmentModal.getByText( + dayjs(courseStartDate).format(SHORT_MONTH_DATE_FORMAT), + )).toBeInTheDocument(); + } // Verify collapsible expect(assignmentModal.getByText('How assigning this course works')).toBeInTheDocument(); @@ -694,8 +702,8 @@ describe('Course card works as expected', () => { const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); - expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); - userEvent.click(screen.getByText('Enroll by Dec 22, 2029')); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); + userEvent.click(screen.getByText(enrollByDropdownText)); const assignmentModal = within(screen.getByRole('dialog')); // Verify "Assign" CTA is disabled diff --git a/src/index.scss b/src/index.scss index 685520a095..2dcbcbb144 100644 --- a/src/index.scss +++ b/src/index.scss @@ -86,8 +86,11 @@ form { } .assignments-important-dates { - width: 250px; + width: 300px; +} - .course-important-date { - } +// TODO: Class override should be a contribution to Paragon if not already +.pgn__card-logo-cap { + object-fit: scale-down !important; + object-position: center center !important; } diff --git a/src/utils.js b/src/utils.js index 1e002ab845..e2f712da3e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -546,11 +546,19 @@ function makePlural(num, string) { return `${num} ${string}`; } +/** + * Pluralizes a word that typically ends with s based on the benchmark passed + * + * @param textToPlural + * @param pluralBenchmark + * @param punctuation + * @returns {string} + */ const pluralText = ( textToPlural, pluralBenchmark, punctuation = '', -) => (pluralBenchmark > 1 ? `${textToPlural}s${punctuation}` : `${textToPlural}${punctuation}`); +) => (pluralBenchmark > 1 || pluralBenchmark === 0 ? `${textToPlural}s${punctuation}` : `${textToPlural}${punctuation}`); /** * Helper function to determine if a content is archived. From c8646704b508f0eb13c32ec3f86f1ca23e5e8fd1 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Tue, 10 Sep 2024 10:11:08 -0400 Subject: [PATCH 06/23] feat: default stale course start dates to today --- .../AssignmentModalmportantDates.jsx | 12 +++-- .../NewAssignmentModalButton.jsx | 15 ++++-- .../NewAssignmentModalDropdown.jsx | 11 ++-- .../cards/data/useCourseCardMetadata.jsx | 4 +- .../data/constants.js | 3 ++ .../learner-credit-management/data/utils.js | 53 +++++++++++++++++-- 6 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index 319eda8596..612be3ca64 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import dayjs from 'dayjs'; import { logInfo } from '@edx/frontend-platform/logging'; -import { hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; +import { setStaleCourseStartDates, hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; const messages = defineMessages({ importantDates: { @@ -56,8 +56,12 @@ AssignmentModalImportantDate.propTypes = { const AssignmentModalImportantDates = ({ courseRun }) => { const intl = useIntl(); - const enrollByDate = courseRun.enrollBy ? dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT) : null; - const courseStartDate = courseRun.start ? dayjs(courseRun.start).format(SHORT_MONTH_DATE_FORMAT) : null; + const enrollByDate = courseRun.enrollBy + ? dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT) + : null; + const courseStartDate = courseRun.start + ? setStaleCourseStartDates({ start: courseRun.start }) + : null; const courseHasStartedLabel = hasCourseStarted(courseStartDate) ? intl.formatMessage(messages.courseStarted) : intl.formatMessage(messages.courseStarts); @@ -80,7 +84,7 @@ const AssignmentModalImportantDates = ({ courseRun }) => { )} {courseStartDate && ( - {courseStartDate} + {dayjs(courseStartDate).format(SHORT_MONTH_DATE_FORMAT)} )} diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx index 10700a1af0..a973042395 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx @@ -19,9 +19,8 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { - enrollableCourseRuns, + assignableCourseRuns, learnerCreditManagementQueryKeys, - STALE_ENROLLMENT_DROPOFF_DAYS, useBudgetId, useSubsidyAccessPolicy, } from '../data'; @@ -68,14 +67,14 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { isAssignable, aggregates, contentPriceCents: course.normalizedMetadata.contentPrice * 100, + parentContentKey: null, contentKey: course.key, courseUuid: course.uuid, assignmentConfiguration, }; - const availableCourseRuns = enrollableCourseRuns({ + const availableCourseRuns = assignableCourseRuns({ courseRuns: course.courseRuns, subsidyExpirationDatetime: subsidyAccessPolicy.subsidyExpirationDatetime, - staleEnrollmentDropOffTime: STALE_ENROLLMENT_DROPOFF_DAYS, }); const { mutate } = useAllocateContentAssignments(); const pathToActivityTab = generatePath(LEARNER_CREDIT_ROUTE, { @@ -94,6 +93,8 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGN_COURSE, { ...sharedEnterpriseTrackEventMetadata, + parentContentKey: course.key, + contentKey: courseRunMetadata.key, isOpen: !isOpen, }, ); @@ -121,6 +122,8 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { }) => { const trackEventMetadata = { ...sharedEnterpriseTrackEventMetadata, + parentContentKey: course.key, + contentKey: assignmentRun.key, totalLearnersAllocated, totalLearnersAlreadyAllocated, response, @@ -190,6 +193,8 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_ALLOCATION_ERROR, { ...sharedEnterpriseTrackEventMetadata, + contentKey: assignmentRun.key, + parentContentKey: course.key, totalAllocatedLearners: learnerEmails.length, errorStatus: httpErrorStatus, errorReason, @@ -219,6 +224,8 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_EXIT, { ...sharedEnterpriseTrackEventMetadata, + contentKey: assignmentRun.key, + parentContentKey: course.key, assignButtonState, }, ); diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx index 09b7aada88..1af0b87dd6 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx @@ -2,7 +2,10 @@ import { Dropdown, Stack } from '@openedx/paragon'; import dayjs from 'dayjs'; import PropTypes from 'prop-types'; import { useState } from 'react'; -import { SHORT_MONTH_DATE_FORMAT } from '../data'; +import { + setStaleCourseStartDates, + SHORT_MONTH_DATE_FORMAT, +} from '../data'; const NewAssignmentModalDropdown = ({ id: courseKey, onClick: openAssignmentModal, courseRuns, children, @@ -17,11 +20,13 @@ const NewAssignmentModalDropdown = ({ {emptyState ? 'No available dates' : 'By date'} - {!emptyState && courseRuns.sort((a, b) => a.enrollBy - b.enrollBy).map(courseRun => ( + {!emptyState && courseRuns.map(courseRun => ( Enroll by {dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT)} - Starts {dayjs(courseRun.starts).format(SHORT_MONTH_DATE_FORMAT)} + + Starts {dayjs(setStaleCourseStartDates({ start: courseRun.start })).format(SHORT_MONTH_DATE_FORMAT)} + ))} diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index cd032530eb..a94a53a856 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -4,7 +4,7 @@ import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.pn import CARD_TEXT from '../../constants'; import { - enrollableCourseRuns, + assignableCourseRuns, EXEC_ED_COURSE_TYPE, formatDate, formatPrice, @@ -62,7 +62,7 @@ const useCourseCardMetadata = ({ linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`; } - const availableCourseRuns = enrollableCourseRuns({ + const availableCourseRuns = assignableCourseRuns({ courseRuns, subsidyExpirationDatetime: subsidyAccessPolicy.subsidyExpirationDatetime, staleEnrollmentDropOffTime: STALE_ENROLLMENT_DROPOFF_DAYS, diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 208fef68a0..f557f2ff2d 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -76,6 +76,9 @@ export const ENROLL_BY_DATE_DAYS_THRESHOLD = 10; // Stale enrollment dropoff time export const STALE_ENROLLMENT_DROPOFF_DAYS = 90; +// Start date threshold to default to today days, sets start date to today if course start date is beyond this value +export const START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS = 14; + // Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`. // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const learnerCreditManagementQueryKeys = { diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index a57972e0d5..064dea1c05 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -7,6 +7,8 @@ import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, ASSIGNMENT_ENROLLMENT_DEADLINE, + STALE_ENROLLMENT_DROPOFF_DAYS, + START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, } from './constants'; import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; @@ -552,15 +554,58 @@ export const isLmsBudget = ( isUniversalGroup, ) => activeIntegrationsLength > 0 && isUniversalGroup; -export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); +/** + * Determines if the course has already started. Mostly used around text formatting for tense + * Introduces 'jitter' in the form of a 30 second offset to take into account any additional + * formatting that takes place down stream related to setting values to today's date through dayjs() + * + * This should also help with reducing 'flaky' tests. + * + * @param start + * @returns {boolean} + */ +export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs().subtract(30, 'seconds')); -export const enrollableCourseRuns = ({ courseRuns, subsidyExpirationDatetime, staleEnrollmentDropOffTime }) => { - const clonedCourseRuns = courseRuns.map(a => ({ ...a, enrollBy: a.enrollBy * 1000 })); +/** + * Returns assignable course runs within the threshold of within the subsidies expiration date + * offset by the STALE_ENROLLMENT_DROPOFF_DAYS constant. It sorts it from the soonest expiring + * enroll-by date and the enroll-by date and upgrade deadline has been normalized to ISO format. + * + * @param courseRuns + * @param subsidyExpirationDatetime + * @returns {*} + */ +export const assignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime }) => { + const clonedCourseRuns = courseRuns.map(a => ({ + ...a, + enrollBy: dayjs(a.enrollBy * 1000).toISOString(), + upgradeDeadline: dayjs(a.upgradeDeadline * 1000).toISOString(), + })); const sortedCourseRuns = clonedCourseRuns.sort((a, b) => a.enrollBy - b.enrollBy); const filteredCourseRuns = sortedCourseRuns.filter( ({ enrollBy }) => dayjs(enrollBy).isBefore( - dayjs(subsidyExpirationDatetime).add(staleEnrollmentDropOffTime, 'days'), + dayjs(subsidyExpirationDatetime).add(STALE_ENROLLMENT_DROPOFF_DAYS, 'days'), ), ); return filteredCourseRuns; }; + +/** + * If the start date of the course is before today offset by the START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS + * then return today's formatted date. Otherwise, pass-through the start date in ISO format. + * + * For cases where a start date does not exist, just return today's date. + * + * @param start + * @param format + * @returns {string} + */ +export const setStaleCourseStartDates = ({ start }) => { + if (!start) { + return dayjs().toISOString(); + } + if (dayjs(start).isBefore(dayjs().subtract(START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, 'days'))) { + return dayjs().toISOString(); + } + return dayjs(start).toISOString(); +}; From c23e647d46a936d4d4bf8f505df5981fd3cae28d Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Wed, 11 Sep 2024 16:42:51 -0400 Subject: [PATCH 07/23] chore: PR feedback 1 --- .../AssignmentModalContent.jsx | 4 +- .../AssignmentModalmportantDates.jsx | 18 ++++--- .../NewAssignmentModalButton.jsx | 8 +-- .../NewAssignmentModalDropdown.jsx | 31 +++++++++--- .../cards/BaseCourseCard.jsx | 11 ++-- .../cards/CourseCardFooterActions.jsx | 50 ++++++++++--------- .../cards/data/useCourseCardMetadata.jsx | 17 +++---- .../cards/tests/CourseCard.test.jsx | 8 +-- .../data/constants.js | 4 +- .../learner-credit-management/data/utils.js | 16 +++--- 10 files changed, 92 insertions(+), 75 deletions(-) diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx index 7de1f6d8eb..7eba828f13 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx @@ -86,7 +86,7 @@ const AssignmentModalContent = ({ description="Header for the section to assign a course to learners using learner credit." /> - +
@@ -205,7 +205,7 @@ AssignmentModalContent.propTypes = { enterpriseId: PropTypes.string.isRequired, course: PropTypes.shape().isRequired, // Pass-thru prop to `BaseCourseCard` courseRun: PropTypes.shape({ - enrollBy: PropTypes.number, + enrollBy: PropTypes.string, start: PropTypes.string, }).isRequired, onEmailAddressesChange: PropTypes.func.isRequired, diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index 612be3ca64..cdb63d8b10 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -36,9 +36,9 @@ const AssignmentModalImportantDate = ({ label, children, }) => ( - - - + + + {label} @@ -77,23 +77,25 @@ const AssignmentModalImportantDates = ({ courseRun }) => { return (
- {enrollByDate && ( + + {enrollByDate && ( {enrollByDate} - )} - {courseStartDate && ( + )} + {courseStartDate && ( {dayjs(courseStartDate).format(SHORT_MONTH_DATE_FORMAT)} - )} + )} +
); }; AssignmentModalImportantDates.propTypes = { courseRun: PropTypes.shape({ - enrollBy: PropTypes.number, + enrollBy: PropTypes.string, start: PropTypes.string, }).isRequired, }; diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx index a973042395..fe81b96fca 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx @@ -19,7 +19,7 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { - assignableCourseRuns, + getAssignableCourseRuns, learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy, @@ -72,7 +72,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { courseUuid: course.uuid, assignmentConfiguration, }; - const availableCourseRuns = assignableCourseRuns({ + const assignableCourseRuns = getAssignableCourseRuns({ courseRuns: course.courseRuns, subsidyExpirationDatetime: subsidyAccessPolicy.subsidyExpirationDatetime, }); @@ -83,7 +83,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const handleOpenAssignmentModal = (e) => { // Based on the user selection, we will extract the course run metadata from the key - const selectedCourseRun = availableCourseRuns.find(({ key }) => key === e.target.closest('[id]').id); + const selectedCourseRun = assignableCourseRuns.find(({ key }) => key === e.target.closest('[id]').id); // If the selected course run is not found, we default to the advertised course run const courseRunMetadata = selectedCourseRun ?? course.advertisedCourseRun; setAssignmentRun(courseRunMetadata); @@ -206,7 +206,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { }; return ( <> - + {children} { - const [emptyState] = useState(courseRuns.length === 0); + const intl = useIntl(); return ( {children} - - {emptyState ? 'No available dates' : 'By date'} + + {courseRuns.length > 0 ? intl.formatMessage(messages.byDate) : intl.formatMessage(messages.noAvailableDates) } - {!emptyState && courseRuns.map(courseRun => ( + {courseRuns.length > 0 && courseRuns.map(courseRun => ( Enroll by {dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT)} - + Starts {dayjs(setStaleCourseStartDates({ start: courseRun.start })).format(SHORT_MONTH_DATE_FORMAT)} @@ -38,7 +51,11 @@ const NewAssignmentModalDropdown = ({ NewAssignmentModalDropdown.propTypes = { id: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, - courseRuns: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + courseRuns: PropTypes.arrayOf(PropTypes.shape({ + key: PropTypes.string.isRequired, + enrollBy: PropTypes.string, + start: PropTypes.string, + })).isRequired, children: PropTypes.node.isRequired, }; diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx index 96440dd27d..94a8005e56 100644 --- a/src/components/learner-credit-management/cards/BaseCourseCard.jsx +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -39,7 +39,7 @@ const BaseCourseCard = ({ subtitle, formattedPrice, isExecEdCourseType, - dateFooterText, + footerText, } = courseCardMetadata; return ( @@ -87,7 +87,7 @@ const BaseCourseCard = ({ {CardFooterActions && } @@ -117,7 +117,7 @@ BaseCourseCard.propTypes = { title: PropTypes.string, }).isRequired, courseRun: PropTypes.shape({ - enrollBy: PropTypes.number, + enrollBy: PropTypes.string, start: PropTypes.string, }), footerActions: PropTypes.elementType, @@ -126,10 +126,7 @@ BaseCourseCard.propTypes = { }; BaseCourseCard.defaultProps = { - courseRun: { - enrollBy: null, - start: null, - }, + courseRun: null, displayImportantDates: false, }; diff --git a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx index 1951b5e40c..46f55f80be 100644 --- a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx +++ b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx @@ -26,32 +26,34 @@ const CourseCardFooterActions = ({ enterpriseId, course }) => { { courseUuid: uuid }, ); }; - return [ - , - (!catalogGroupView ? ( - + return ( + <> + + {!catalogGroupView && ( + + + + )} + + ); }; CourseCardFooterActions.propTypes = { diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index a94a53a856..99508b28ed 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -4,11 +4,13 @@ import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.pn import CARD_TEXT from '../../constants'; import { - assignableCourseRuns, + getAssignableCourseRuns, EXEC_ED_COURSE_TYPE, formatDate, formatPrice, - getEnrollmentDeadline, STALE_ENROLLMENT_DROPOFF_DAYS, useBudgetId, useSubsidyAccessPolicy, + getEnrollmentDeadline, + useBudgetId, + useSubsidyAccessPolicy, } from '../../data'; import { pluralText } from '../../../../utils'; import AssignmentModalImportantDates from '../../assignment-modal/AssignmentModalmportantDates'; @@ -19,7 +21,6 @@ const useCourseCardMetadata = ({ course, enterpriseSlug, courseRun, - displayImportantDates, }) => { const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext); const { subsidyAccessPolicyId } = useBudgetId(); @@ -62,14 +63,12 @@ const useCourseCardMetadata = ({ linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`; } - const availableCourseRuns = assignableCourseRuns({ + const assignableCourseRuns = getAssignableCourseRuns({ courseRuns, subsidyExpirationDatetime: subsidyAccessPolicy.subsidyExpirationDatetime, - staleEnrollmentDropOffTime: STALE_ENROLLMENT_DROPOFF_DAYS, }); - const assignmentImportantDates = ; - const availableCourseRunsCount = `(${availableCourseRuns.length}) available ${pluralText('date', availableCourseRuns.length)}`; - const dateFooterText = displayImportantDates ? assignmentImportantDates : availableCourseRunsCount; + const availableCourseRunsCount = `(${assignableCourseRuns.length}) available ${pluralText('date', assignableCourseRuns.length)}`; + const footerText = courseRun ? : availableCourseRunsCount; return { ...course, subtitle: partners.map(partner => partner.name).join(', '), @@ -83,7 +82,7 @@ const useCourseCardMetadata = ({ execEdEnrollmentInfo, linkToCourse, isExecEdCourseType, - dateFooterText, + footerText, }; }; diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 714bea5e66..facb6961fe 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -51,7 +51,7 @@ jest.mock('../../../../data/services/EnterpriseAccessApiService'); const futureStartDate = dayjs().add(5, 'days').toISOString(); const pastStartDate = dayjs().subtract(5, 'days').toISOString(); const enrollByTimestamp = dayjs().subtract(10, 'days').unix(); -const enrollByDropdownText = `Enroll by ${dayjs(enrollByTimestamp * 1000).format(SHORT_MONTH_DATE_FORMAT)}`; +const enrollByDropdownText = `Enroll by ${dayjs.unix(enrollByTimestamp).format(SHORT_MONTH_DATE_FORMAT)}`; const originalData = { availability: ['Upcoming'], @@ -59,7 +59,7 @@ const originalData = { course_type: 'course', key: 'course-123x', normalized_metadata: { - enroll_by_date: dayjs(1892678399 * 1000).toISOString(), + enroll_by_date: dayjs.unix(1892678399).toISOString(), start_date: futureStartDate, content_price: 100, }, @@ -98,7 +98,7 @@ const execEdData = { key: 'exec-ed-course-123x', entitlements: [{ price: '999.00' }], normalized_metadata: { - enroll_by_date: dayjs(1892678399 * 1000).toISOString(), + enroll_by_date: dayjs.unix(1892678399).toISOString(), start_date: futureStartDate, content_price: 999, }, @@ -515,7 +515,7 @@ describe('Course card works as expected', () => { // Verify important dates expect(assignmentModal.getByText('Enroll-by date')).toBeInTheDocument(); expect(assignmentModal.getByText( - dayjs(enrollByTimestamp * 1000).format(SHORT_MONTH_DATE_FORMAT), + dayjs.unix(enrollByTimestamp).format(SHORT_MONTH_DATE_FORMAT), )).toBeInTheDocument(); if (courseStartDate) { expect(assignmentModal.getByText(expectedCourseStartText)).toBeInTheDocument(); diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index f557f2ff2d..e6b192df81 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -73,8 +73,8 @@ export const MEMBERS_TABLE_PAGE_SIZE = 10; // Enroll-by date warning message threshold by days export const ENROLL_BY_DATE_DAYS_THRESHOLD = 10; -// Stale enrollment dropoff time -export const STALE_ENROLLMENT_DROPOFF_DAYS = 90; +// Allocation assignmenit expiration dropoff threshold +export const DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION = 90; // Start date threshold to default to today days, sets start date to today if course start date is beyond this value export const START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS = 14; diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 064dea1c05..3cfcc29bef 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -7,7 +7,7 @@ import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, ASSIGNMENT_ENROLLMENT_DEADLINE, - STALE_ENROLLMENT_DROPOFF_DAYS, + DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, } from './constants'; import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; @@ -568,26 +568,26 @@ export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs().subtrac /** * Returns assignable course runs within the threshold of within the subsidies expiration date - * offset by the STALE_ENROLLMENT_DROPOFF_DAYS constant. It sorts it from the soonest expiring + * offset by the DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION constant. It sorts it from the soonest expiring * enroll-by date and the enroll-by date and upgrade deadline has been normalized to ISO format. * * @param courseRuns * @param subsidyExpirationDatetime * @returns {*} */ -export const assignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime }) => { +export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime }) => { const clonedCourseRuns = courseRuns.map(a => ({ ...a, - enrollBy: dayjs(a.enrollBy * 1000).toISOString(), - upgradeDeadline: dayjs(a.upgradeDeadline * 1000).toISOString(), + enrollBy: dayjs.unix(a.enrollBy).toISOString(), + upgradeDeadline: dayjs.unix(a.upgradeDeadline).toISOString(), })); const sortedCourseRuns = clonedCourseRuns.sort((a, b) => a.enrollBy - b.enrollBy); - const filteredCourseRuns = sortedCourseRuns.filter( + const assignableCourseRuns = sortedCourseRuns.filter( ({ enrollBy }) => dayjs(enrollBy).isBefore( - dayjs(subsidyExpirationDatetime).add(STALE_ENROLLMENT_DROPOFF_DAYS, 'days'), + dayjs(subsidyExpirationDatetime).add(DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, 'days'), ), ); - return filteredCourseRuns; + return assignableCourseRuns; }; /** From 5b1d9b4123a3a4852bdba6b3c16a12197c052392 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 12 Sep 2024 09:07:03 -0400 Subject: [PATCH 08/23] chore: PR feedback 2 --- .../AssignmentModalmportantDates.jsx | 25 +++------- .../NewAssignmentModalDropdown.jsx | 18 ++++++- .../cards/BaseCourseCard.jsx | 48 ++++++++++--------- .../cards/data/useCourseCardMetadata.jsx | 19 ++++++-- .../cards/tests/CourseCard.test.jsx | 16 +++---- src/index.scss | 4 -- 6 files changed, 71 insertions(+), 59 deletions(-) diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index cdb63d8b10..a168ebd381 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -1,12 +1,9 @@ -import { - Col, Icon, Row, Stack, -} from '@openedx/paragon'; +import { Icon, Stack } from '@openedx/paragon'; import { Calendar } from '@openedx/paragon/icons'; import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; -import { logInfo } from '@edx/frontend-platform/logging'; import { setStaleCourseStartDates, hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; const messages = defineMessages({ @@ -36,17 +33,11 @@ const AssignmentModalImportantDate = ({ label, children, }) => ( - - - - - {label} - - - - {children} - - + + + {label}: + {children} + ); AssignmentModalImportantDate.propTypes = { @@ -68,10 +59,6 @@ const AssignmentModalImportantDates = ({ courseRun }) => { // This is an edge case that the user should never enter but covered nonetheless if (!enrollByDate && !courseStartDate) { - logInfo(`[frontend-app-admin-portal][AssignmentModalImportantDates] - Component did not render, no courseRun enrollBy date or courseStart date provided - courseRun: ${courseRun} - `); return null; } diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx index 83a528dac4..d5bc4015dd 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx @@ -2,6 +2,7 @@ import { Dropdown, Stack } from '@openedx/paragon'; import dayjs from 'dayjs'; import PropTypes from 'prop-types'; import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; +import { useState } from 'react'; import { setStaleCourseStartDates, SHORT_MONTH_DATE_FORMAT, @@ -24,6 +25,13 @@ const NewAssignmentModalDropdown = ({ id: courseKey, onClick: openAssignmentModal, courseRuns, children, }) => { const intl = useIntl(); + const [clickedDropdownItem, setClickedDropdownItem] = useState(null); + const getDropdownItemClassName = (courseRun) => { + if (clickedDropdownItem && clickedDropdownItem.key === courseRun.key) { + return null; + } + return 'text-muted'; + }; return ( @@ -34,10 +42,16 @@ const NewAssignmentModalDropdown = ({ {courseRuns.length > 0 ? intl.formatMessage(messages.byDate) : intl.formatMessage(messages.noAvailableDates) } {courseRuns.length > 0 && courseRuns.map(courseRun => ( - + setClickedDropdownItem(courseRun)} + onMouseUp={() => setClickedDropdownItem(null)} + > Enroll by {dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT)} - + Starts {dayjs(setStaleCourseStartDates({ start: courseRun.start })).format(SHORT_MONTH_DATE_FORMAT)} diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx index 94a8005e56..6aba87d669 100644 --- a/src/components/learner-credit-management/cards/BaseCourseCard.jsx +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -12,6 +12,7 @@ import { camelCaseObject } from '@edx/frontend-platform/utils'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { useCourseCardMetadata } from './data'; +import AssignmentModalImportantDates from '../assignment-modal/AssignmentModalmportantDates'; const BaseCourseCard = ({ original, @@ -19,7 +20,6 @@ const BaseCourseCard = ({ enterpriseSlug, cardClassName, courseRun, - displayImportantDates, }) => { const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); @@ -28,7 +28,6 @@ const BaseCourseCard = ({ course: camelCaseObject(original), courseRun, enterpriseSlug, - displayImportantDates, }); const { imageSrc, @@ -67,30 +66,37 @@ const BaseCourseCard = ({ )} /> - - {isExecEdCourseType - ? ( - - ) - : ( - - )} - + +
+ + {isExecEdCourseType + ? ( + + ) + : ( + + )} + +
+ {courseRun && } +
+ {CardFooterActions && ( - {CardFooterActions && } + + )} ); @@ -122,12 +128,10 @@ BaseCourseCard.propTypes = { }), footerActions: PropTypes.elementType, cardClassName: PropTypes.string, - displayImportantDates: PropTypes.bool, }; BaseCourseCard.defaultProps = { courseRun: null, - displayImportantDates: false, }; export default connect(mapStateToProps)(BaseCourseCard); diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index 99508b28ed..5234940924 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -2,6 +2,7 @@ import { useContext } from 'react'; import { AppContext } from '@edx/frontend-platform/react'; import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; +import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import CARD_TEXT from '../../constants'; import { getAssignableCourseRuns, @@ -13,15 +14,22 @@ import { useSubsidyAccessPolicy, } from '../../data'; import { pluralText } from '../../../../utils'; -import AssignmentModalImportantDates from '../../assignment-modal/AssignmentModalmportantDates'; const { ENROLLMENT } = CARD_TEXT; +const messages = defineMessages({ + courseFooterMessage: { + id: 'lcm.budget.detail.page.catalog.tab.course.card.footer-text', + defaultMessage: '({courseRuns}) available {pluralText}', + description: 'Footer text for a course card result for learner credit management', + }, +}); + const useCourseCardMetadata = ({ course, enterpriseSlug, - courseRun, }) => { + const intl = useIntl(); const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext); const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); @@ -67,8 +75,11 @@ const useCourseCardMetadata = ({ courseRuns, subsidyExpirationDatetime: subsidyAccessPolicy.subsidyExpirationDatetime, }); - const availableCourseRunsCount = `(${assignableCourseRuns.length}) available ${pluralText('date', assignableCourseRuns.length)}`; - const footerText = courseRun ? : availableCourseRunsCount; + // const footerText = `(${assignableCourseRuns.length}) available ${pluralText('date', assignableCourseRuns.length)}`; + const footerText = intl.formatMessage(messages.courseFooterMessage, { + courseRuns: assignableCourseRuns.length, + pluralText: pluralText('date', assignableCourseRuns.length), + }); return { ...course, subtitle: partners.map(partner => partner.name).join(', '), diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index facb6961fe..71a30be524 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -326,7 +326,7 @@ describe('Course card works as expected', () => { shouldRetryAllocationAfterException: false, // no ability to retry after this error courseImportantDates: { courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts', + expectedCourseStartText: 'Course starts:', }, }, { @@ -336,7 +336,7 @@ describe('Course card works as expected', () => { shouldRetryAllocationAfterException: false, courseImportantDates: { courseStartDate: pastStartDate, - expectedCourseStartText: 'Course started', + expectedCourseStartText: 'Course started:', }, }, { @@ -346,7 +346,7 @@ describe('Course card works as expected', () => { shouldRetryAllocationAfterException: true, courseImportantDates: { courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts', + expectedCourseStartText: 'Course starts:', }, }, { @@ -356,7 +356,7 @@ describe('Course card works as expected', () => { shouldRetryAllocationAfterException: false, courseImportantDates: { courseStartDate: pastStartDate, - expectedCourseStartText: 'Course started', + expectedCourseStartText: 'Course started:', }, }, { @@ -366,7 +366,7 @@ describe('Course card works as expected', () => { shouldRetryAllocationAfterException: true, courseImportantDates: { courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts', + expectedCourseStartText: 'Course starts:', }, }, { @@ -376,7 +376,7 @@ describe('Course card works as expected', () => { shouldRetryAllocationAfterException: false, courseImportantDates: { courseStartDate: pastStartDate, - expectedCourseStartText: 'Course started', + expectedCourseStartText: 'Course started:', }, }, { @@ -386,7 +386,7 @@ describe('Course card works as expected', () => { shouldRetryAllocationAfterException: true, courseImportantDates: { courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts', + expectedCourseStartText: 'Course starts:', }, }, { @@ -513,7 +513,7 @@ describe('Course card works as expected', () => { expect(assignmentModal.getByText(expectedAvailableBalance)).toBeInTheDocument(); // Verify important dates - expect(assignmentModal.getByText('Enroll-by date')).toBeInTheDocument(); + expect(assignmentModal.getByText('Enroll-by date:')).toBeInTheDocument(); expect(assignmentModal.getByText( dayjs.unix(enrollByTimestamp).format(SHORT_MONTH_DATE_FORMAT), )).toBeInTheDocument(); diff --git a/src/index.scss b/src/index.scss index 2dcbcbb144..cfac0d620d 100644 --- a/src/index.scss +++ b/src/index.scss @@ -85,10 +85,6 @@ form { font-size: $font-size-base !important; } -.assignments-important-dates { - width: 300px; -} - // TODO: Class override should be a contribution to Paragon if not already .pgn__card-logo-cap { object-fit: scale-down !important; From 986e726c8c898e8ce61f52c6b1fd6584a2374fbb Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 12 Sep 2024 09:57:30 -0400 Subject: [PATCH 09/23] refactor: Updates logic for start date threshold --- .../BudgetDetailPageOverviewAvailability.jsx | 3 +- .../BudgetDetailPageOverviewUtilization.jsx | 3 +- .../AssignmentModalmportantDates.jsx | 13 +++--- .../NewAssignmentModalButton.jsx | 3 +- .../NewAssignmentModalDropdown.jsx | 8 ++-- .../cards/data/useCourseCardMetadata.jsx | 2 +- .../learner-credit-management/constants.js | 20 --------- .../data/constants.js | 27 +++++++++++ .../data/hooks/usePathToCatalogTab.js | 3 +- .../learner-credit-management/data/utils.js | 45 +++++++++++++++---- 10 files changed, 81 insertions(+), 46 deletions(-) delete mode 100644 src/components/learner-credit-management/constants.js diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index c66b5856af..d55d29be5f 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -18,10 +18,9 @@ import { useSubsidyAccessPolicy, useEnterpriseCustomer, useEnterpriseGroup, - isLmsBudget, + isLmsBudget, LEARNER_CREDIT_ROUTE, } from './data'; import EVENT_NAMES from '../../eventTracking'; -import { LEARNER_CREDIT_ROUTE } from './constants'; import { BUDGET_STATUSES } from '../EnterpriseApp/data/constants'; import BudgetDetail from './BudgetDetail'; import { useEnterpriseBudgets } from '../EnterpriseSubsidiesContext/data/hooks'; diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx index bd996e0696..a1c5f511f7 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx @@ -9,9 +9,8 @@ import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { generatePath, useParams, Link } from 'react-router-dom'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { formatPrice } from './data'; +import { formatPrice, LEARNER_CREDIT_ROUTE } from './data'; import EVENT_NAMES from '../../eventTracking'; -import { LEARNER_CREDIT_ROUTE } from './constants'; const BudgetDetailPageOverviewUtilization = ({ budgetId, diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index a168ebd381..bf09625a51 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -4,7 +4,9 @@ import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; -import { setStaleCourseStartDates, hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; +import { + getNormalizedEnrollByDate, getNormalizedStartDate, hasCourseStarted, SHORT_MONTH_DATE_FORMAT, +} from '../data'; const messages = defineMessages({ importantDates: { @@ -47,12 +49,11 @@ AssignmentModalImportantDate.propTypes = { const AssignmentModalImportantDates = ({ courseRun }) => { const intl = useIntl(); - const enrollByDate = courseRun.enrollBy - ? dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT) - : null; - const courseStartDate = courseRun.start - ? setStaleCourseStartDates({ start: courseRun.start }) + const normalizedEnrollByDate = getNormalizedEnrollByDate(courseRun); + const enrollByDate = normalizedEnrollByDate + ? dayjs(normalizedEnrollByDate).format(SHORT_MONTH_DATE_FORMAT) : null; + const courseStartDate = getNormalizedStartDate(courseRun); const courseHasStartedLabel = hasCourseStarted(courseStartDate) ? intl.formatMessage(messages.courseStarted) : intl.formatMessage(messages.courseStarts); diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx index fe81b96fca..6904e9ea6e 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx @@ -19,7 +19,7 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { - getAssignableCourseRuns, + getAssignableCourseRuns, LEARNER_CREDIT_ROUTE, learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy, @@ -27,7 +27,6 @@ import { import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals'; import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; import EVENT_NAMES from '../../../eventTracking'; -import { LEARNER_CREDIT_ROUTE } from '../constants'; import NewAssignmentModalDropdown from './NewAssignmentModalDropdown'; const useAllocateContentAssignments = () => useMutation({ diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx index d5bc4015dd..eeaf485433 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx @@ -4,7 +4,8 @@ import PropTypes from 'prop-types'; import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import { useState } from 'react'; import { - setStaleCourseStartDates, + getNormalizedEnrollByDate, + getNormalizedStartDate, SHORT_MONTH_DATE_FORMAT, } from '../data'; @@ -32,6 +33,7 @@ const NewAssignmentModalDropdown = ({ } return 'text-muted'; }; + const startLabel = ({ start }) => (dayjs(start).isBefore(dayjs()) ? 'Started' : 'Starts'); return ( @@ -50,9 +52,9 @@ const NewAssignmentModalDropdown = ({ onMouseUp={() => setClickedDropdownItem(null)} > - Enroll by {dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT)} + Enroll by {dayjs(getNormalizedEnrollByDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT)} - Starts {dayjs(setStaleCourseStartDates({ start: courseRun.start })).format(SHORT_MONTH_DATE_FORMAT)} + {startLabel(courseRun)} {dayjs(getNormalizedStartDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT)}
diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index 5234940924..96781822f1 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -3,8 +3,8 @@ import { AppContext } from '@edx/frontend-platform/react'; import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; -import CARD_TEXT from '../../constants'; import { + CARD_TEXT, getAssignableCourseRuns, EXEC_ED_COURSE_TYPE, formatDate, diff --git a/src/components/learner-credit-management/constants.js b/src/components/learner-credit-management/constants.js deleted file mode 100644 index a344b21fff..0000000000 --- a/src/components/learner-credit-management/constants.js +++ /dev/null @@ -1,20 +0,0 @@ -const CARD_TEXT = { - BADGE: { - course: 'Course', - execEd: 'Executive Education', - }, - BUTTON_ACTION: { - viewCourse: 'View course', - assign: 'Assign', - }, - ENROLLMENT: { - text: 'Learner must enroll by', - }, - PRICE: { - subText: 'Per learner price', - }, -}; - -export const LEARNER_CREDIT_ROUTE = '/:enterpriseSlug/admin/:enterpriseAppPage/:budgetId/:activeTabKey'; - -export default CARD_TEXT; diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index e6b192df81..fcd51b3b65 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -22,6 +22,12 @@ export const API_FIELDS_BY_TABLE_COLUMN_ACCESSOR = { courseListPrice: 'course_list_price', }; +// Course pace text +export const COURSE_PACING_MAP = { + SELF_PACED: 'self_paced', + INSTRUCTOR_PACED: 'instructor_paced', +}; + // Percentage where messaging (e.g., Alert) on low remaining balance will begin appearing export const LOW_REMAINING_BALANCE_PERCENT_THRESHOLD = 0.75; @@ -46,6 +52,24 @@ export const BUDGET_DETAIL_TAB_LABELS = { [BUDGET_DETAIL_MEMBERS_TAB]: 'Members', }; +// Card text for used in useCourseCardMetadata +export const CARD_TEXT = { + BADGE: { + course: 'Course', + execEd: 'Executive Education', + }, + BUTTON_ACTION: { + viewCourse: 'View course', + assign: 'Assign', + }, + ENROLLMENT: { + text: 'Learner must enroll by', + }, + PRICE: { + subText: 'Per learner price', + }, +}; + // Facet filters export const LEARNING_TYPE_REFINEMENT = 'learning_type'; export const LANGUAGE_REFINEMENT = 'language'; @@ -93,3 +117,6 @@ export const learnerCreditManagementQueryKeys = { budgetGroupLearners: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'group learners'], enterpriseCustomer: (enterpriseId) => [...learnerCreditManagementQueryKeys.all, 'enterpriseCustomer', enterpriseId], }; + +// Route to learner credit +export const LEARNER_CREDIT_ROUTE = '/:enterpriseSlug/admin/:enterpriseAppPage/:budgetId/:activeTabKey'; diff --git a/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js b/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js index cc98165e99..c9a709729b 100644 --- a/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js +++ b/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js @@ -1,7 +1,8 @@ import { useParams, generatePath } from 'react-router-dom'; import useBudgetId from './useBudgetId'; -import { LEARNER_CREDIT_ROUTE } from '../../constants'; + +import { LEARNER_CREDIT_ROUTE } from '../constants'; const usePathToCatalogTab = () => { const { budgetId } = useBudgetId(); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 3cfcc29bef..4ae83076dd 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -8,7 +8,7 @@ import { NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, ASSIGNMENT_ENROLLMENT_DEADLINE, DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, - START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, + START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, COURSE_PACING_MAP, } from './constants'; import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; @@ -556,15 +556,13 @@ export const isLmsBudget = ( /** * Determines if the course has already started. Mostly used around text formatting for tense - * Introduces 'jitter' in the form of a 30 second offset to take into account any additional - * formatting that takes place down stream related to setting values to today's date through dayjs() * * This should also help with reducing 'flaky' tests. * * @param start * @returns {boolean} */ -export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs().subtract(30, 'seconds')); +export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); /** * Returns assignable course runs within the threshold of within the subsidies expiration date @@ -590,6 +588,16 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime return assignableCourseRuns; }; +export const isCourseSelfPaced = ({ pacingType }) => pacingType === COURSE_PACING_MAP.SELF_PACED; + +export const hasTimeToComplete = ({ end, weeksToComplete }) => { + const today = dayjs(); + const differenceInWeeks = dayjs(end).diff(today, 'week'); + return weeksToComplete <= differenceInWeeks; +}; + +const isWithinMinimumStartDateThreshold = ({ start }) => dayjs(start).isBefore(dayjs().subtract(START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, 'days')); + /** * If the start date of the course is before today offset by the START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS * then return today's formatted date. Otherwise, pass-through the start date in ISO format. @@ -600,12 +608,31 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime * @param format * @returns {string} */ -export const setStaleCourseStartDates = ({ start }) => { +export const getNormalizedStartDate = ({ + start, pacingType, end, weeksToComplete, +}) => { + const todayToIso = dayjs().toISOString(); if (!start) { - return dayjs().toISOString(); + return todayToIso; + } + const startDateIso = dayjs(start).toISOString(); + if (isCourseSelfPaced({ pacingType })) { + if (hasTimeToComplete({ end, weeksToComplete }) || isWithinMinimumStartDateThreshold({ start })) { + // always today's date (incentives enrollment) + return todayToIso; + } + return startDateIso; + } + return startDateIso; +}; + +export const getNormalizedEnrollByDate = ({ enrollBy }) => { + if (!enrollBy) { + return null; } - if (dayjs(start).isBefore(dayjs().subtract(START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, 'days'))) { - return dayjs().toISOString(); + const ninetyDayAllocationOffset = dayjs().add(DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, 'days'); + if (dayjs(enrollBy).isAfter(ninetyDayAllocationOffset)) { + return ninetyDayAllocationOffset.toISOString(); } - return dayjs(start).toISOString(); + return enrollBy; }; From 622e2fe1d862f27ab4e8d8efaa55616e946b15bf Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 12 Sep 2024 12:34:24 -0400 Subject: [PATCH 10/23] chore: PR feedback 3 --- .../AssignmentModalmportantDates.jsx | 8 ++++++-- .../cards/tests/CourseCard.test.jsx | 12 ++++++++---- .../learner-credit-management/data/constants.js | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index bf09625a51..c7ff32add8 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -5,7 +5,11 @@ import PropTypes from 'prop-types'; import dayjs from 'dayjs'; import { - getNormalizedEnrollByDate, getNormalizedStartDate, hasCourseStarted, SHORT_MONTH_DATE_FORMAT, + DATETIME_FORMAT, + getNormalizedEnrollByDate, + getNormalizedStartDate, + hasCourseStarted, + SHORT_MONTH_DATE_FORMAT, } from '../data'; const messages = defineMessages({ @@ -51,7 +55,7 @@ const AssignmentModalImportantDates = ({ courseRun }) => { const intl = useIntl(); const normalizedEnrollByDate = getNormalizedEnrollByDate(courseRun); const enrollByDate = normalizedEnrollByDate - ? dayjs(normalizedEnrollByDate).format(SHORT_MONTH_DATE_FORMAT) + ? dayjs(normalizedEnrollByDate).format(DATETIME_FORMAT) : null; const courseStartDate = getNormalizedStartDate(courseRun); const courseHasStartedLabel = hasCourseStarted(courseStartDate) diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 71a30be524..e720fd45e3 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -13,9 +13,11 @@ import dayjs from 'dayjs'; import CourseCard from '../CourseCard'; import { formatPrice, - learnerCreditManagementQueryKeys, SHORT_MONTH_DATE_FORMAT, + DATETIME_FORMAT, + learnerCreditManagementQueryKeys, + SHORT_MONTH_DATE_FORMAT, useBudgetId, - useSubsidyAccessPolicy, + useSubsidyAccessPolicy, getNormalizedEnrollByDate, } from '../../data'; import { getButtonElement, queryClient } from '../../../test/testUtils'; @@ -266,10 +268,12 @@ describe('Course card works as expected', () => { }); test('executive education card renders', () => { + const enrollByDate = getNormalizedEnrollByDate({ enrollBy: dayjs('Dec 22, 2029') }); + const formattedEnrollBy = dayjs(enrollByDate).format(SHORT_MONTH_DATE_FORMAT); renderWithRouter(); expect(screen.queryByText('$999')).toBeInTheDocument(); userEvent.click(screen.getByText('Assign')); - expect(screen.getByText('Enroll by Dec 22, 2029')).toBeInTheDocument(); + expect(screen.getByText(`Enroll by ${formattedEnrollBy}`)).toBeInTheDocument(); expect(screen.queryByText('Executive Education')).toBeInTheDocument(); const viewCourseCTA = screen.getByText('View course', { selector: 'a' }); expect(viewCourseCTA.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/executive-education-2u/course/exec-ed-course-123x'); @@ -515,7 +519,7 @@ describe('Course card works as expected', () => { // Verify important dates expect(assignmentModal.getByText('Enroll-by date:')).toBeInTheDocument(); expect(assignmentModal.getByText( - dayjs.unix(enrollByTimestamp).format(SHORT_MONTH_DATE_FORMAT), + dayjs.unix(enrollByTimestamp).format(DATETIME_FORMAT), )).toBeInTheDocument(); if (courseStartDate) { expect(assignmentModal.getByText(expectedCourseStartText)).toBeInTheDocument(); diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index fcd51b3b65..20739fcf2f 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -39,6 +39,7 @@ export const NO_BALANCE_REMAINING_DOLLAR_THRESHOLD = 100; export const DATE_FORMAT = 'MMMM DD, YYYY'; export const SHORT_MONTH_DATE_FORMAT = 'MMM DD, YYYY'; +export const DATETIME_FORMAT = 'MMM D, YYYY h:mma'; export const EXEC_ED_OFFER_TYPE = 'learner_credit'; From fe76d73fd55572bf2c24646564bbd48ddf7926b0 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 12 Sep 2024 18:19:13 -0400 Subject: [PATCH 11/23] chore: PR feedback 4 --- .../AssignmentDetailsTableCell.jsx | 5 ++- .../BudgetDetailPageOverviewAvailability.jsx | 3 +- .../AssignmentModalmportantDates.jsx | 12 +++---- .../NewAssignmentModalButton.jsx | 25 +++++++++------ .../NewAssignmentModalDropdown.jsx | 32 +++++++++++++++---- .../cards/CourseCardFooterActions.jsx | 1 - .../cards/data/useCourseCardMetadata.jsx | 1 - .../cards/tests/CourseCard.test.jsx | 3 +- .../learner-credit-management/data/utils.js | 3 +- src/index.scss | 2 +- 10 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx index 0e9da02c8f..782ca73730 100644 --- a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx @@ -21,6 +21,7 @@ const AssignmentDetailsTableCell = ({ row, enterpriseSlug, enterpriseId }) => { state: row.original.state, }, ); + const contentKey = row.original.isAssignedCourseRun ? row.original.parentContentKey : row.original.contentKey; return ( {
{
{enrollByDate && ( - - {enrollByDate} - + + {enrollByDate} + )} {courseStartDate && ( - - {dayjs(courseStartDate).format(SHORT_MONTH_DATE_FORMAT)} - + + {dayjs(courseStartDate).format(SHORT_MONTH_DATE_FORMAT)} + )}
diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx index 6904e9ea6e..a868bd0451 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx @@ -16,10 +16,12 @@ import { connect } from 'react-redux'; import { getConfig } from '@edx/frontend-platform/config'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; +import { logError } from '@edx/frontend-platform/logging'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { - getAssignableCourseRuns, LEARNER_CREDIT_ROUTE, + getAssignableCourseRuns, + LEARNER_CREDIT_ROUTE, learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy, @@ -80,12 +82,16 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { enterpriseSlug, enterpriseAppPage, budgetId: subsidyAccessPolicyId, activeTabKey: 'activity', }); - const handleOpenAssignmentModal = (e) => { - // Based on the user selection, we will extract the course run metadata from the key - const selectedCourseRun = assignableCourseRuns.find(({ key }) => key === e.target.closest('[id]').id); - // If the selected course run is not found, we default to the advertised course run - const courseRunMetadata = selectedCourseRun ?? course.advertisedCourseRun; - setAssignmentRun(courseRunMetadata); + const handleOpenAssignmentModal = (selectedCourseRun) => { + setAssignmentRun(selectedCourseRun); + if (!selectedCourseRun) { + logError(`[handleOpenAssignmentModal]: Unable to open learner credit management allocation modal, + selectedCourseRun: ${selectedCourseRun}, + parentContentKey: ${course.key}, + contentKey: ${selectedCourseRun.key}, + enterpriseUuid: ${enterpriseId}, + policyUuid: ${subsidyAccessPolicyId}`); + } open(); sendEnterpriseTrackEvent( enterpriseId, @@ -93,7 +99,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { { ...sharedEnterpriseTrackEventMetadata, parentContentKey: course.key, - contentKey: courseRunMetadata.key, + contentKey: selectedCourseRun.key, isOpen: !isOpen, }, ); @@ -134,7 +140,6 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { ); }; const handleAllocateContentAssignments = () => { - // If no assignmentRun key exist, fall back to the top level course key const payload = snakeCaseObject({ contentPriceCents: course.normalizedMetadata.contentPrice * 100, // Convert to USD cents contentKey: assignmentRun.key, @@ -196,7 +201,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { parentContentKey: course.key, totalAllocatedLearners: learnerEmails.length, errorStatus: httpErrorStatus, - errorReason, + response: err, }, ); diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx index eeaf485433..87f7921efb 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx @@ -20,6 +20,16 @@ const messages = defineMessages({ defaultMessage: 'No available dates', description: 'Dropdown header for the catalog search results section on the lcm budget detail page with no available dates', }, + enrollBy: { + id: 'lcm.budget.detail.page.catalog.search.results.assign.dropdown.enroll-by-date-item', + defaultMessage: 'Enroll by {enrollByDate}', + description: 'Dropdown item for the catalog search results section on the lcm budget detail enroll-by date', + }, + startDate: { + id: 'lcm.budget.detail.page.catalog.search.results.assign.dropdown.starts-date-item', + defaultMessage: '{startLabel} {startDate}', + description: 'Dropdown item for the catalog search results section on the lcm budget detail start date', + }, }); const NewAssignmentModalDropdown = ({ @@ -35,8 +45,14 @@ const NewAssignmentModalDropdown = ({ }; const startLabel = ({ start }) => (dayjs(start).isBefore(dayjs()) ? 'Started' : 'Starts'); return ( - - + { + const courseRunKey = event.target.closest('[data-courserunkey]').getAttribute('data-courserunkey'); + const selectedCourseRun = courseRuns.find(({ key }) => key === courseRunKey); + openAssignmentModal(selectedCourseRun); + }} + > + {children} @@ -46,15 +62,19 @@ const NewAssignmentModalDropdown = ({ {courseRuns.length > 0 && courseRuns.map(courseRun => ( setClickedDropdownItem(courseRun)} onMouseUp={() => setClickedDropdownItem(null)} > - Enroll by {dayjs(getNormalizedEnrollByDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT)} + {intl.formatMessage(messages.enrollBy, { + enrollByDate: dayjs(getNormalizedEnrollByDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT), + })} - {startLabel(courseRun)} {dayjs(getNormalizedStartDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT)} + {intl.formatMessage(messages.startDate, { + startLabel: startLabel(courseRun), + startDate: dayjs(getNormalizedStartDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT), + })} diff --git a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx index 46f55f80be..9250ed6e22 100644 --- a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx +++ b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx @@ -29,7 +29,6 @@ const CourseCardFooterActions = ({ enterpriseId, course }) => { return ( <>