diff --git a/src/components/ContentHighlights/ContentHighlightArchivedAlert.jsx b/src/components/ContentHighlights/ContentHighlightArchivedAlert.jsx index 4f899e08b2..bd126a9002 100644 --- a/src/components/ContentHighlights/ContentHighlightArchivedAlert.jsx +++ b/src/components/ContentHighlights/ContentHighlightArchivedAlert.jsx @@ -3,7 +3,8 @@ import { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { NEW_ARCHIVED_CONTENT_ALERT_DISMISSED_COOKIE_NAME } from './data/constants'; import { EnterpriseAppContext } from '../EnterpriseApp/EnterpriseAppContextProvider'; -import { enterpriseCurationActions, isArchivedContent } from '../EnterpriseApp/data/enterpriseCurationReducer'; +import { enterpriseCurationActions } from '../EnterpriseApp/data/enterpriseCurationReducer'; +import { isArchivedContent } from '../../utils'; const ContentHighlightArchivedAlert = ({ open, onClose }) => { const [archivedContentLocalStorage, setArchivedContentLocalStorage] = useState({}); diff --git a/src/components/ContentHighlights/ContentHighlightSetCard.jsx b/src/components/ContentHighlights/ContentHighlightSetCard.jsx index a0357e8085..1a731ae0b2 100644 --- a/src/components/ContentHighlights/ContentHighlightSetCard.jsx +++ b/src/components/ContentHighlights/ContentHighlightSetCard.jsx @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom'; import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; import { useContentHighlightsContext } from './data/hooks'; +import { makePlural } from '../../utils'; const ContentHighlightSetCard = ({ imageCapSrc, @@ -14,6 +15,7 @@ const ContentHighlightSetCard = ({ isPublished, enterpriseSlug, itemCount, + archivedItemCount, onClick, }) => { const navigate = useNavigate(); @@ -30,6 +32,15 @@ const ContentHighlightSetCard = ({ openStepperModal(); }; + const cardItemText = () => { + let returnString = ''; + returnString += makePlural(itemCount, 'item'); + if (archivedItemCount > 0) { + returnString += ` : ${makePlural(archivedItemCount, 'archived item')}`; + } + return returnString; + }; + return ( - {itemCount} items + {cardItemText()} ); @@ -51,6 +62,7 @@ ContentHighlightSetCard.propTypes = { enterpriseSlug: PropTypes.string.isRequired, isPublished: PropTypes.bool.isRequired, itemCount: PropTypes.number.isRequired, + archivedItemCount: PropTypes.number.isRequired, imageCapSrc: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, }; diff --git a/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx b/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx index 9e72f98322..802809e67c 100644 --- a/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx +++ b/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx @@ -7,7 +7,6 @@ import { connect } from 'react-redux'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import ContentHighlightCardItem from './ContentHighlightCardItem'; import { - COURSE_RUN_STATUSES, DEFAULT_ERROR_MESSAGE, HIGHLIGHTS_CARD_GRID_COLUMN_SIZES, MAX_CONTENT_ITEMS_PER_HIGHLIGHT_SET, @@ -17,6 +16,7 @@ import { generateAboutPageUrl } from './data/utils'; import EVENT_NAMES from '../../eventTracking'; import { features } from '../../config'; import DeleteArchivedHighlightsDialogs from './DeleteArchivedHighlightsDialogs'; +import { isArchivedContent } from '../../utils'; const ContentHighlightsCardItemsContainer = ({ enterpriseId, enterpriseSlug, isLoading, highlightedContent, updateHighlightSet, @@ -44,16 +44,8 @@ const ContentHighlightsCardItemsContainer = ({ if (FEATURE_HIGHLIGHTS_ARCHIVE_MESSAGING) { for (let i = 0; i < highlightedContent.length; i++) { - const { - courseRunStatuses, - } = highlightedContent[i]; - if (courseRunStatuses) { - // a course is only archived if all of its runs are archived - if (courseRunStatuses?.every(status => status === COURSE_RUN_STATUSES.archived)) { - archivedContent.push(highlightedContent[i]); - } else { - activeContent.push(highlightedContent[i]); - } + if (isArchivedContent(highlightedContent[i])) { + archivedContent.push(highlightedContent[i]); } else { activeContent.push(highlightedContent[i]); } diff --git a/src/components/ContentHighlights/HighlightSetSection.jsx b/src/components/ContentHighlights/HighlightSetSection.jsx index c9cb79128e..1fa04b5b60 100644 --- a/src/components/ContentHighlights/HighlightSetSection.jsx +++ b/src/components/ContentHighlights/HighlightSetSection.jsx @@ -40,6 +40,7 @@ const HighlightSetSection = ({ isPublished, highlightedContentUuids, cardImageUrl, + archivedContentCount, }) => ( trackClickEvent({ uuid, title, isPublished, highlightedContentUuids, diff --git a/src/components/EnterpriseApp/data/enterpriseCurationReducer.js b/src/components/EnterpriseApp/data/enterpriseCurationReducer.js index 08d2a45ebc..6e1a0eee20 100644 --- a/src/components/EnterpriseApp/data/enterpriseCurationReducer.js +++ b/src/components/EnterpriseApp/data/enterpriseCurationReducer.js @@ -1,5 +1,6 @@ import { logError } from '@edx/frontend-platform/logging'; -import { NEW_ARCHIVED_CONTENT_ALERT_DISMISSED_COOKIE_NAME, COURSE_RUN_STATUSES } from '../../ContentHighlights/data/constants'; +import { NEW_ARCHIVED_CONTENT_ALERT_DISMISSED_COOKIE_NAME } from '../../ContentHighlights/data/constants'; +import { isArchivedContent } from '../../../utils'; export const initialReducerState = { isLoading: true, @@ -71,23 +72,6 @@ function getHighlightSetsFromState(state) { return state.enterpriseCuration?.highlightSets || []; } -/** - * Helper function to determine if a content is archived. - * - * @param {Object} content - * @returns {Boolean} - */ -export function isArchivedContent(content) { - const { courseRunStatuses } = content; - - if (!courseRunStatuses) { - return false; - } - - const ARCHIVABLE_STATUSES = [COURSE_RUN_STATUSES.archived, COURSE_RUN_STATUSES.unpublished]; - return courseRunStatuses.every(status => ARCHIVABLE_STATUSES.includes(status)); -} - /** * Helper function to determine if there is a new archived content in a highlight set * diff --git a/src/components/settings/SettingsLMSTab/ExistingCard.jsx b/src/components/settings/SettingsLMSTab/ExistingCard.jsx index 1f8a73f720..28b78d26de 100644 --- a/src/components/settings/SettingsLMSTab/ExistingCard.jsx +++ b/src/components/settings/SettingsLMSTab/ExistingCard.jsx @@ -32,7 +32,7 @@ const ExistingCard = ({ getStatus, }) => { const location = useLocation(); - const pathname = location.pathname; + const { pathname } = location; const redirectPath = pathname.endsWith('/') ? pathname : `${pathname}/`; const [showDeleteModal, setShowDeleteModal] = useState(false); const isEdxStaff = getAuthenticatedUser().administrator; diff --git a/src/data/services/EnterpriseDataApiService.js b/src/data/services/EnterpriseDataApiService.js index b047d6329a..e64e860a80 100644 --- a/src/data/services/EnterpriseDataApiService.js +++ b/src/data/services/EnterpriseDataApiService.js @@ -2,7 +2,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { snakeCaseObject } from '@edx/frontend-platform/utils'; import store from '../store'; -import { configuration, features } from '../../config'; +import { configuration } from '../../config'; class EnterpriseDataApiService { // TODO: This should access the data-api through the gateway instead of direct diff --git a/src/utils.js b/src/utils.js index 9678faffcc..f5dc8840ab 100644 --- a/src/utils.js +++ b/src/utils.js @@ -24,6 +24,7 @@ import CornerstoneIcon from './icons/CSOD.png'; import DegreedIcon from './icons/Degreed.png'; import MoodleIcon from './icons/Moodle.png'; import SAPIcon from './icons/SAP.svg'; +import { COURSE_RUN_STATUSES } from './components/ContentHighlights/data/constants'; import LmsApiService from './data/services/LmsApiService'; @@ -447,6 +448,37 @@ function getActiveTableColumnFilters(columns) { })).filter(filter => !!filter.filterValue); } +/** + * Helper to transform a string into a plural form based on a number. + * + * @returns A string with the number and the plural form of the string. + */ +function makePlural(num, string) { + const stringEndings = ['s', 'x', 'z']; + if (num > 1 || num === 0) { + if (stringEndings.includes(string.charAt(string.length - 1))) { + return `${num} ${string}es`; + } + return `${num} ${string}s`; + } + return `${num} ${string}`; +} + +/** + * Helper function to determine if a content is archived. + * + * @param {Object} content (can be program, course, or pathway) + * @returns {Boolean} + */ +function isArchivedContent(content) { + const { courseRunStatuses } = content; + if (!courseRunStatuses) { + return false; + } + const ARCHIVABLE_STATUSES = [COURSE_RUN_STATUSES.archived, COURSE_RUN_STATUSES.unpublished]; + return courseRunStatuses.every(status => ARCHIVABLE_STATUSES.includes(status)); +} + export { camelCaseDict, camelCaseDictArray, @@ -484,4 +516,6 @@ export { isAssignableSubsidyAccessPolicyType, getActiveTableColumnFilters, queryCacheOnErrorHandler, + makePlural, + isArchivedContent, };