diff --git a/src/components/ContentHighlights/ContentHighlightCardItem.jsx b/src/components/ContentHighlights/ContentHighlightCardItem.jsx index 776129e352..0743db5a1b 100644 --- a/src/components/ContentHighlights/ContentHighlightCardItem.jsx +++ b/src/components/ContentHighlights/ContentHighlightCardItem.jsx @@ -2,35 +2,42 @@ import React from 'react'; import { Card, Hyperlink } from '@edx/paragon'; import Truncate from 'react-truncate'; import PropTypes from 'prop-types'; -import { getContentHighlightCardFooter } from './data/constants'; +import { getContentHighlightCardFooter } from './data/utils'; +import SkeletonContentCard from './SkeletonContentCard'; const ContentHighlightCardItem = ({ isLoading, title, - hyperlink, + href, contentType, partners, cardImageUrl, price, }) => { + if (isLoading) { + return ( + + ); + } const cardInfo = { - cardTitle: ({title}), - cardLogoAlt: partners?.length === 1 ? `${partners[0].name}'s logo` : undefined, + cardImgSrc: cardImageUrl, cardLogoSrc: partners?.length === 1 ? partners[0].logoImageUrl : undefined, + cardLogoAlt: partners?.length === 1 ? `${partners[0].name}'s logo` : undefined, + cardTitle: {title}, cardSubtitle: partners?.map(p => p.name).join(', '), - cardFooter: getContentHighlightCardFooter(price, contentType), + cardFooter: getContentHighlightCardFooter({ price, contentType }), }; - if (hyperlink) { + if (href) { cardInfo.cardTitle = ( - + {title} ); } return ( - + { const { highlightSetUUID } = useParams(); const { highlightSet, isLoading } = useHighlightSet(highlightSetUUID); return ( - + diff --git a/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx b/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx index 13f5570ed9..17782ccd60 100644 --- a/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx +++ b/src/components/ContentHighlights/ContentHighlightsCardItemsContainer.jsx @@ -1,37 +1,46 @@ import React from 'react'; -import { CardGrid } from '@edx/paragon'; +import { CardGrid, Alert } from '@edx/paragon'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ContentHighlightCardItem from './ContentHighlightCardItem'; -import { generateAboutPageUrl } from './data/constants'; +import { + DEFAULT_ERROR_MESSAGE, HIGHLIGHTS_CARD_GRID_COLUMN_SIZES, MAX_CONTENT_ITEMS_PER_HIGHLIGHT_SET, +} from './data/constants'; +import SkeletonContentCardContainer from './SkeletonContentCardContainer'; +import { generateAboutPageUrl } from './data/utils'; const ContentHighlightsCardItemsContainer = ({ enterpriseSlug, isLoading, highlightedContent }) => { + if (isLoading) { + return ( + + ); + } if (!highlightedContent || highlightedContent?.length === 0) { - return
; + return ( + + {DEFAULT_ERROR_MESSAGE.EMPTY_HIGHLIGHT_SET} + + ); } return ( - + {highlightedContent.map(({ uuid, title, contentType, authoringOrganizations, contentKey, }) => ( - ))} - {isLoading &&
} ); }; diff --git a/src/components/ContentHighlights/ContentHighlightsDashboard.jsx b/src/components/ContentHighlights/ContentHighlightsDashboard.jsx index 9b136c4ecd..7baf4630e7 100644 --- a/src/components/ContentHighlights/ContentHighlightsDashboard.jsx +++ b/src/components/ContentHighlights/ContentHighlightsDashboard.jsx @@ -30,7 +30,7 @@ const ContentHighlightsDashboardBase = ({ children }) => { }, [history, location, locationState]); return ( - + {children} { if (isLoading) { return ( -
+ +

+ -
+ ); } return ( diff --git a/src/components/ContentHighlights/HighlightSetSection.jsx b/src/components/ContentHighlights/HighlightSetSection.jsx index a0c58776f4..fbc11fb1cc 100644 --- a/src/components/ContentHighlights/HighlightSetSection.jsx +++ b/src/components/ContentHighlights/HighlightSetSection.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { CardGrid } from '@edx/paragon'; import ContentHighlightSetCard from './ContentHighlightSetCard'; +import { HIGHLIGHTS_CARD_GRID_COLUMN_SIZES } from './data/constants'; const HighlightSetSection = ({ title: sectionTitle, @@ -15,13 +16,7 @@ const HighlightSetSection = ({ return (

{sectionTitle}

- + {highlightSets.map(({ title, uuid, diff --git a/src/components/ContentHighlights/HighlightStepper/ContentConfirmContentCard.jsx b/src/components/ContentHighlights/HighlightStepper/ContentConfirmContentCard.jsx index 00beb3d779..fc3e0e4718 100644 --- a/src/components/ContentHighlights/HighlightStepper/ContentConfirmContentCard.jsx +++ b/src/components/ContentHighlights/HighlightStepper/ContentConfirmContentCard.jsx @@ -5,7 +5,7 @@ import { IconButton, Icon } from '@edx/paragon'; import { connect } from 'react-redux'; import ContentHighlightCardItem from '../ContentHighlightCardItem'; import { useContentHighlightsContext } from '../data/hooks'; -import { generateAboutPageUrl } from '../data/constants'; +import { generateAboutPageUrl } from '../data/utils'; const ContentConfirmContentCard = ({ enterpriseSlug, original }) => { const { deleteSelectedRowId } = useContentHighlightsContext(); @@ -38,7 +38,11 @@ const ContentConfirmContentCard = ({ enterpriseSlug, original }) => { { /> setDeleteKey(aggregationKey)} className="ml-1 flex-shrink-0" /> diff --git a/src/components/ContentHighlights/HighlightStepper/ContentSearchResultCard.jsx b/src/components/ContentHighlights/HighlightStepper/ContentSearchResultCard.jsx index 19d92f7a20..968176da30 100644 --- a/src/components/ContentHighlights/HighlightStepper/ContentSearchResultCard.jsx +++ b/src/components/ContentHighlights/HighlightStepper/ContentSearchResultCard.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ContentHighlightCardItem from '../ContentHighlightCardItem'; -import { generateAboutPageUrl } from '../data/constants'; +import { generateAboutPageUrl } from '../data/utils'; const ContentSearchResultCard = ({ enterpriseSlug, original }) => { const { @@ -17,7 +17,11 @@ const ContentSearchResultCard = ({ enterpriseSlug, original }) => { return ( { if (isSearchStalled) { return ( -
- - {/* eslint-disable */} - {[...new Array(8)].map((element, index) => - )} - {/* eslint-enable */} - -
+ ); } if (!searchResults) { @@ -50,7 +40,6 @@ export const BaseReviewContentSelections = ({ } const { hits } = camelCaseObject(searchResults); - // eslint-disable-next-line react-hooks/rules-of-hooks const transformedCardData = hits.map((highlightedContent) => { const { aggregationKey, title, cardImageUrl, contentType, partners, originalImageUrl, firstEnrollablePaidSeatPrice, @@ -67,14 +56,7 @@ export const BaseReviewContentSelections = ({ return original; }); return ( - + {transformedCardData.map((original) => ( ))} @@ -130,11 +112,14 @@ export const SelectedContent = ({ enterpriseId }) => { filterString += ')'; } return filterString; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentSelectedRowIds]); + }, [currentSelectedRowIds, enterpriseId]); if (currentSelectedRowIds.length === 0) { - return (
); + return ( + + {DEFAULT_ERROR_MESSAGE.EMPTY_SELECTEDROWIDS} + + ); } return ( diff --git a/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx b/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx index 4c90440299..122b0e718f 100644 --- a/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx +++ b/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx @@ -8,6 +8,7 @@ import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; import HighlightStepperConfirmContent, { BaseReviewContentSelections, SelectedContent } from '../HighlightStepperConfirmContent'; import { + DEFAULT_ERROR_MESSAGE, testCourseAggregation, } from '../../data/constants'; import { ContentHighlightsContext } from '../../ContentHighlightsContext'; @@ -67,7 +68,7 @@ describe('BaseReviewContentSelections', () => { , ); - expect(screen.getByTestId('skeleton-container')).toBeInTheDocument(); + expect(screen.getAllByTestId('card-item-skeleton')).toBeTruthy(); }); it('should render selected card content', () => { renderWithRouter( @@ -87,5 +88,6 @@ describe('SelectedContent', () => { , ); expect(screen.getByTestId('selected-content-no-results')).toBeInTheDocument(); + expect(screen.getByText(DEFAULT_ERROR_MESSAGE.EMPTY_SELECTEDROWIDS)).toBeInTheDocument(); }); }); diff --git a/src/components/ContentHighlights/SkeletonContentCard.jsx b/src/components/ContentHighlights/SkeletonContentCard.jsx index 913555428c..e35a595d75 100644 --- a/src/components/ContentHighlights/SkeletonContentCard.jsx +++ b/src/components/ContentHighlights/SkeletonContentCard.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Card } from '@edx/paragon'; const SkeletonContentCard = () => ( - + diff --git a/src/components/ContentHighlights/SkeletonContentCardContainer.jsx b/src/components/ContentHighlights/SkeletonContentCardContainer.jsx new file mode 100644 index 0000000000..6ed5505c04 --- /dev/null +++ b/src/components/ContentHighlights/SkeletonContentCardContainer.jsx @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import { CardGrid } from '@edx/paragon'; +import SkeletonContentCard from './SkeletonContentCard'; +import { HIGHLIGHTS_CARD_GRID_COLUMN_SIZES } from './data/constants'; + +const SkeletonContentCardContainer = ({ length, columnSizes }) => ( + + {[ + ...new Array(length), + // eslint-disable-next-line react/no-array-index-key + ].map((_, index) => )}; + +); + +SkeletonContentCardContainer.propTypes = { + length: PropTypes.number.isRequired, + columnSizes: PropTypes.shape({ + xs: PropTypes.number, + md: PropTypes.number, + lg: PropTypes.number, + xl: PropTypes.number, + }), +}; + +SkeletonContentCardContainer.defaultProps = { + columnSizes: HIGHLIGHTS_CARD_GRID_COLUMN_SIZES, +}; + +export default SkeletonContentCardContainer; diff --git a/src/components/ContentHighlights/data/constants.js b/src/components/ContentHighlights/data/constants.js index d118cd5de8..1d621105b6 100644 --- a/src/components/ContentHighlights/data/constants.js +++ b/src/components/ContentHighlights/data/constants.js @@ -1,18 +1,22 @@ /* eslint-disable import/no-extraneous-dependencies */ import { faker } from '@faker-js/faker'; -import { configuration } from '../../../config'; /* eslint-enable import/no-extraneous-dependencies */ -// Generate URLs for about pages from the enterprise learner portal -export const generateAboutPageUrl = (enterpriseSlug, contentType, contentKey) => { - const { ENTERPRISE_LEARNER_PORTAL_URL } = configuration; - const aboutPageBase = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}`; - if (contentType === 'learnerpathway') { - return `${aboutPageBase}/search/${contentKey}`; - } - return `${aboutPageBase}/${contentType}/${contentKey}`; +// Default Card Grid columnSizes +export const HIGHLIGHTS_CARD_GRID_COLUMN_SIZES = { + xs: 12, + md: 6, + lg: 4, + xl: 3, }; - +// Empty Content and Error Messages +export const DEFAULT_ERROR_MESSAGE = { + EMPTY_HIGHLIGHT_SET: 'There is no highlighted content for this highlight collection.', + // eslint-disable-next-line quotes + EMPTY_SELECTEDROWIDS: `You don't have any highlighted content selected. Go back to the previous step to select content.`, +}; +// Max highlight sets per enteprise curation +export const MAX_HIGHLIGHT_SETS_PER_ENTERPRISE_CURATION = 8; // Max number of content items per highlight set export const MAX_CONTENT_ITEMS_PER_HIGHLIGHT_SET = 12; // Max length of highlight title in stepper @@ -40,15 +44,6 @@ export const FOOTER_TEXT_BY_CONTENT_TYPE = { learnerpathway: 'Pathway', }; -// High Card logic for footer text -export const getContentHighlightCardFooter = (price, contentType) => { - const formattedContentType = FOOTER_TEXT_BY_CONTENT_TYPE[contentType?.toLowerCase()]; - if (!price) { - return formattedContentType; - } - return `$${price} · ${formattedContentType}`; -}; - // Test Data for Content Highlights TODO: Remove when API is available || Remove when feature completed // Test entepriseId for Content Highlights to display card selections and confirmation export const testEnterpriseId = 'e783bb19-277f-479e-9c41-8b0ed31b4060'; diff --git a/src/components/ContentHighlights/data/hooks.js b/src/components/ContentHighlights/data/hooks.js index 67970d3aa2..4131aa9b50 100644 --- a/src/components/ContentHighlights/data/hooks.js +++ b/src/components/ContentHighlights/data/hooks.js @@ -101,13 +101,13 @@ export function useContentHighlightsContext() { }, [setState]); const deleteSelectedRowId = useCallback((rowId) => { - const x = getState.stepperModal.currentSelectedRowIds; - delete x[rowId]; + const currentRowIds = getState.stepperModal.currentSelectedRowIds; + delete currentRowIds[rowId]; setState(s => ({ ...s, stepperModal: { ...s.stepperModal, - currentSelectedRowIds: x, + currentSelectedRowIds: currentRowIds, }, })); }, [setState, getState]); diff --git a/src/components/ContentHighlights/data/utils.js b/src/components/ContentHighlights/data/utils.js new file mode 100644 index 0000000000..5313002c0d --- /dev/null +++ b/src/components/ContentHighlights/data/utils.js @@ -0,0 +1,20 @@ +import { configuration } from '../../../config'; +import { FOOTER_TEXT_BY_CONTENT_TYPE } from './constants'; + +// High Card logic for footer text +export const getContentHighlightCardFooter = ({ price, contentType }) => { + const formattedContentType = FOOTER_TEXT_BY_CONTENT_TYPE[contentType?.toLowerCase()]; + if (!price) { + return formattedContentType; + } + return `$${price} · ${formattedContentType}`; +}; +// Generate URLs for about pages from the enterprise learner portal +export function generateAboutPageUrl({ enterpriseSlug, contentType, contentKey }) { + const { ENTERPRISE_LEARNER_PORTAL_URL } = configuration; + const aboutPageBase = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}`; + if (contentType === 'learnerpathway') { + return `${aboutPageBase}/search/${contentKey}`; + } + return `${aboutPageBase}/${contentType}/${contentKey}`; +} diff --git a/src/components/ContentHighlights/tests/ContentHighlightsCardItemsContainer.test.jsx b/src/components/ContentHighlights/tests/ContentHighlightsCardItemsContainer.test.jsx index f10b3a8e4e..ae98a1f7b8 100644 --- a/src/components/ContentHighlights/tests/ContentHighlightsCardItemsContainer.test.jsx +++ b/src/components/ContentHighlights/tests/ContentHighlightsCardItemsContainer.test.jsx @@ -8,7 +8,7 @@ import { camelCaseObject } from '@edx/frontend-platform'; import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import ContentHighlightsCardItemsContainer from '../ContentHighlightsCardItemsContainer'; -import { TEST_COURSE_HIGHLIGHTS_DATA } from '../data/constants'; +import { DEFAULT_ERROR_MESSAGE, TEST_COURSE_HIGHLIGHTS_DATA } from '../data/constants'; const mockStore = configureMockStore([thunk]); @@ -66,12 +66,13 @@ describe('', () => { highlightedContent={[]} />); expect(screen.getByTestId('empty-highlighted-content')).toBeInTheDocument(); + expect(screen.getByText(DEFAULT_ERROR_MESSAGE.EMPTY_HIGHLIGHT_SET)).toBeInTheDocument(); }); it('Displays Skeleton on load', () => { renderWithRouter(); - expect(screen.getByTestId('card-item-skeleton')).toBeInTheDocument(); + expect(screen.getAllByTestId('card-item-skeleton')).toBeTruthy(); }); });