feat: add colored left border to items in course outline

refactor: move common styles to conditional sortable element
This commit is contained in:
Navin Karkera
2024-01-10 19:47:33 +05:30
committed by Kristin Aoki
parent eb0c61ce6d
commit 0e829974ef
9 changed files with 260 additions and 209 deletions

View File

@@ -42,7 +42,6 @@ import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
import PublishModal from './publish-modal/PublishModal';
import ConfigureModal from './configure-modal/ConfigureModal';
import DeleteModal from './delete-modal/DeleteModal';
import ConditionalSortableElement from './drag-helper/ConditionalSortableElement';
import { useCourseOutline } from './hooks';
import messages from './messages';
@@ -203,83 +202,55 @@ const CourseOutline = ({ courseId }) => {
<>
<DraggableList itemList={sections} setState={setSections} updateOrder={finalizeSectionOrder}>
{sections.map((section, index) => (
<ConditionalSortableElement
<SectionCard
id={section.id}
key={section.id}
draggable={section.actions.draggable}
componentStyle={{
background: 'white',
padding: '1.75rem',
marginBottom: '1.5rem',
borderRadius: '0.35rem',
boxShadow: '0 0 .125rem rgba(0, 0, 0, .15), 0 0 .25rem rgba(0, 0, 0, .15)',
}}
section={section}
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
onOpenConfigureModal={openConfigureModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
isSectionsExpanded={isSectionsExpanded}
onNewSubsectionSubmit={handleNewSubsectionSubmit}
>
<SectionCard
id={section.id}
key={section.id}
section={section}
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
onOpenConfigureModal={openConfigureModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
isSectionsExpanded={isSectionsExpanded}
onNewSubsectionSubmit={handleNewSubsectionSubmit}
<DraggableList
itemList={section.childInfo.children}
setState={setSubsection(index)}
updateOrder={finalizeSubsectionOrder(section)}
>
<DraggableList
itemList={section.childInfo.children}
setState={setSubsection(index)}
updateOrder={finalizeSubsectionOrder(section)}
>
{section.childInfo.children.map((subsection) => (
<ConditionalSortableElement
id={subsection.id}
key={subsection.id}
draggable={
subsection.actions.draggable && !(subsection.isHeaderVisible === false)
}
componentStyle={{
background: '#f8f7f6',
padding: '1rem 1.5rem',
marginBottom: '1.5rem',
borderRadius: '0.35rem',
boxShadow: '0 0 .125rem rgba(0, 0, 0, .15), 0 0 .25rem rgba(0, 0, 0, .15)',
}}
>
<SubsectionCard
key={subsection.id}
section={section}
{section.childInfo.children.map((subsection) => (
<SubsectionCard
key={subsection.id}
section={section}
subsection={subsection}
savingStatus={savingStatus}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
onNewUnitSubmit={handleNewUnitSubmit}
>
{subsection.childInfo.children.map((unit) => (
<UnitCard
key={unit.id}
unit={unit}
subsection={subsection}
section={section}
savingStatus={savingStatus}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
onNewUnitSubmit={handleNewUnitSubmit}
>
{subsection.childInfo.children.map((unit) => (
<UnitCard
key={unit.id}
unit={unit}
subsection={subsection}
section={section}
savingStatus={savingStatus}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateUnitSubmit}
getTitleLink={getUnitUrl}
/>
))}
</SubsectionCard>
</ConditionalSortableElement>
))}
</DraggableList>
</SectionCard>
</ConditionalSortableElement>
onDuplicateSubmit={handleDuplicateUnitSubmit}
getTitleLink={getUnitUrl}
/>
))}
</SubsectionCard>
))}
</DraggableList>
</SectionCard>
))}
</DraggableList>
{courseActions.childAddable && (

View File

@@ -554,21 +554,19 @@ describe('<CourseOutline />', () => {
})
.reply(200, { dummy: 'value' });
let mockReturnValue = { ...section, published: true };
if (elementName === 'subsection') {
mockReturnValue = {
...section,
childInfo: {
children: [
{
...section.childInfo.children[0],
published: true,
},
...section.childInfo.children.slice(1),
],
},
};
} else if (elementName === 'unit') {
let mockReturnValue = {
...section,
childInfo: {
children: [
{
...section.childInfo.children[0],
published: true,
},
...section.childInfo.children.slice(1),
],
},
};
if (elementName === 'unit') {
mockReturnValue = {
...section,
childInfo: {
@@ -612,8 +610,7 @@ describe('<CourseOutline />', () => {
await checkPublishBtn(unit, unitElement, 'unit');
// check subsection
await checkPublishBtn(subsection, subsectionElement, 'subsection');
// check section
await checkPublishBtn(section, sectionElement, 'section');
// section doesn't display badges
});
it('check configure section when configure query is successful', async () => {

View File

@@ -144,7 +144,7 @@ module.exports = {
studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40edx_introduction',
releasedToStudents: false,
releaseDate: 'Jan 01, 1970 at 05:00 UTC',
visibilityState: 'staff_only',
visibilityState: 'needs_attention',
hasExplicitStaffLock: false,
start: '1970-01-01T05:00:00Z',
graded: false,
@@ -214,7 +214,7 @@ module.exports = {
studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
releasedToStudents: false,
releaseDate: 'Jan 01, 1970 at 05:00 UTC',
visibilityState: 'staff_only',
visibilityState: 'needs_attention',
hasExplicitStaffLock: false,
start: '1970-01-01T05:00:00Z',
graded: false,

View File

@@ -9,11 +9,20 @@ const ConditionalSortableElement = ({
children,
componentStyle,
}) => {
const style = {
background: 'white',
padding: '1rem 1.5rem',
marginBottom: '1.5rem',
borderRadius: '0.35rem',
boxShadow: '0 0 .125rem rgba(0, 0, 0, .15), 0 0 .25rem rgba(0, 0, 0, .15)',
...componentStyle,
};
if (draggable) {
return (
<SortableItem
id={id}
componentStyle={componentStyle}
componentStyle={style}
>
<div className="extend-margin">
{children}
@@ -24,7 +33,7 @@ const ConditionalSortableElement = ({
return (
<Row
data-testid="conditional-sortable-element--no-drag-handle"
style={componentStyle}
style={style}
className="mx-0"
>
{children}

View File

@@ -11,8 +11,9 @@ import { setCurrentItem, setCurrentSection } from '../data/slice';
import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader';
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
import TitleButton from '../card-header/TitleButton';
import { getItemStatus, scrollToElement } from '../utils';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import messages from './messages';
const SectionCard = ({
@@ -64,6 +65,9 @@ const SectionCard = ({
hasChanges,
});
// remove border when section is expanded
const borderStyle = getItemStatusBorder(!isExpanded ? sectionStatus : '');
const handleExpandContent = () => {
setIsExpanded((prevState) => !prevState);
};
@@ -112,66 +116,75 @@ const SectionCard = ({
);
return (
<div
className="section-card"
data-testid="section-card"
ref={currentRef}
<ConditionalSortableElement
id={id}
draggable={actions.draggable}
componentStyle={{
padding: '1.75rem',
...borderStyle,
}}
>
<div>
{isHeaderVisible && (
<CardHeader
sectionId={id}
title={displayName}
status={sectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
)}
<div className="section-card__content" data-testid="section-card__content">
{explanatoryMessage && <p className="text-secondary-400 x-small mb-1">{explanatoryMessage}</p>}
<div className="outline-section__status">
<Button
className="section-card__highlights"
data-destid="section-card-highlights-button"
variant="tertiary"
onClick={handleOpenHighlightsModal}
>
<Badge className="highlights-badge">{highlights.length}</Badge>
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
</Button>
</div>
</div>
{isExpanded && (
<div data-testid="section-card__subsections" className="item-children section-card__subsections">
{children}
{actions.childAddable && (
<div
className="section-card"
data-testid="section-card"
ref={currentRef}
>
<div>
{isHeaderVisible && (
<CardHeader
sectionId={id}
title={displayName}
status={sectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
)}
<div className="section-card__content" data-testid="section-card__content">
{explanatoryMessage && <p className="text-secondary-400 x-small mb-1">{explanatoryMessage}</p>}
<div className="outline-section__status">
<Button
data-testid="new-subsection-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewSubsectionSubmit}
className="section-card__highlights"
data-destid="section-card-highlights-button"
variant="tertiary"
onClick={handleOpenHighlightsModal}
>
{intl.formatMessage(messages.newSubsectionButton)}
<Badge className="highlights-badge">{highlights.length}</Badge>
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
</Button>
)}
</div>
</div>
)}
{isExpanded && (
<div data-testid="section-card__subsections" className="item-children section-card__subsections">
{children}
{actions.childAddable && (
<Button
data-testid="new-subsection-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewSubsectionSubmit}
>
{intl.formatMessage(messages.newSubsectionButton)}
</Button>
)}
</div>
)}
</div>
</div>
</div>
</ConditionalSortableElement>
);
};

View File

@@ -9,8 +9,9 @@ import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data
import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader';
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
import TitleButton from '../card-header/TitleButton';
import { getItemStatus, scrollToElement } from '../utils';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import messages from './messages';
const SubsectionCard = ({
@@ -46,6 +47,7 @@ const SubsectionCard = ({
visibilityState,
hasChanges,
});
const borderStyle = getItemStatusBorder(subsectionStatus);
const handleExpandContent = () => {
setIsExpanded((prevState) => !prevState);
@@ -98,44 +100,56 @@ const SubsectionCard = ({
}, [savingStatus]);
return (
<div className="subsection-card" data-testid="subsection-card" ref={currentRef}>
{isHeaderVisible && (
<CardHeader
title={displayName}
status={subsectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
)}
{isExpanded && (
<div data-testid="subsection-card__units" className="item-children subsection-card__units">
{children}
{actions.childAddable && (
<Button
data-testid="new-unit-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewButtonClick}
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
)}
</div>
)}
</div>
<ConditionalSortableElement
id={id}
key={id}
draggable={
actions.draggable && !(isHeaderVisible === false)
}
componentStyle={{
background: '#f8f7f6',
...borderStyle,
}}
>
<div className="subsection-card" data-testid="subsection-card" ref={currentRef}>
{isHeaderVisible && (
<CardHeader
title={displayName}
status={subsectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
)}
{isExpanded && (
<div data-testid="subsection-card__units" className="item-children subsection-card__units">
{children}
{actions.childAddable && (
<Button
data-testid="new-unit-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewButtonClick}
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
)}
</div>
)}
</div>
</ConditionalSortableElement>
);
};

View File

@@ -7,8 +7,9 @@ import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data
import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader';
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
import TitleLink from '../card-header/TitleLink';
import { getItemStatus, scrollToElement } from '../utils';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
const UnitCard = ({
unit,
@@ -41,6 +42,7 @@ const UnitCard = ({
visibilityState,
hasChanges,
});
const borderStyle = getItemStatusBorder(unitStatus);
const handleClickMenuButton = () => {
dispatch(setCurrentItem(unit));
@@ -91,25 +93,39 @@ const UnitCard = ({
}
return (
<div className="unit-card" data-testid="unit-card" ref={currentRef}>
<CardHeader
title={displayName}
status={unitStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
</div>
<ConditionalSortableElement
id={id}
key={id}
draggable={false} // update to {actions.draggable} when unit drag-n-drop is implemented
componentStyle={{
background: '#fdfdfd',
...borderStyle,
}}
>
<div
className="unit-card"
data-testid="unit-card"
ref={currentRef}
>
<CardHeader
title={displayName}
status={unitStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
</div>
</ConditionalSortableElement>
);
};

View File

@@ -1,9 +1,5 @@
.unit-card {
@include pgn-box-shadow(1, "centered");
padding: $spacer 2rem;
margin-bottom: 1.5rem;
background: $light-100;
flex-grow: 1;
.unit-card__content {
margin: $spacer;

View File

@@ -75,6 +75,40 @@ const getItemStatusBadgeContent = (status, messages, intl) => {
}
};
/**
* Get section border color
* @param {string} status - value from on getItemStatus util
* @returns {
* borderLeft: string,
* }
*/
const getItemStatusBorder = (status) => {
switch (status) {
case ITEM_BADGE_STATUS.live:
return {
borderLeft: '5px solid #00688D',
};
case ITEM_BADGE_STATUS.publishedNotLive:
return {
borderLeft: '5px solid #0D7D4D',
};
case ITEM_BADGE_STATUS.staffOnly:
return {
borderLeft: '5px solid #000000',
};
case ITEM_BADGE_STATUS.unpublishedChanges:
return {
borderLeft: '5px solid #F0CC00',
};
case ITEM_BADGE_STATUS.draft:
return {
borderLeft: '5px solid #F0CC00',
};
default:
return {};
}
};
/**
* Get formatted highlights form values
* @param {Array<string>} currentHighlights - section highlights
@@ -150,6 +184,7 @@ const getVideoSharingOptionText = (id, messages, intl) => {
export {
getItemStatus,
getItemStatusBadgeContent,
getItemStatusBorder,
getHighlightsFormValues,
getVideoSharingOptionText,
scrollToElement,