feat: section list and new section button
Also refactor api and hooks fix: publish button behaviour and card header tests fix: warning in highlights and publish modal test fix: courseoutline tests test: add test for new section functionality fix(lint): lint issues refactor: remove unnecessary css in CardHeader refactor: rename emptyPlaceholder test file refactor: replace ternary operator with 'and' condition refactor: add black color to expand/collapse button refactor: display only changed subsection and units in publish modal refactor: update messages and css refactor: wrap urls in function call refactor: fix jsdoc types refactor: use helmet for document title
This commit is contained in:
committed by
Kristin Aoki
parent
59071424b3
commit
134b75568a
@@ -23,6 +23,7 @@ export const NOTIFICATION_MESSAGES = {
|
||||
saving: 'Saving',
|
||||
duplicating: 'Duplicating',
|
||||
deleting: 'Deleting',
|
||||
empty: '',
|
||||
};
|
||||
|
||||
export const DEFAULT_TIME_STAMP = '00:00';
|
||||
|
||||
@@ -2,11 +2,14 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Layout,
|
||||
TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import {
|
||||
Add as IconAdd,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
@@ -66,10 +69,9 @@ const CourseOutline = ({ courseId }) => {
|
||||
handleEditSectionSubmit,
|
||||
handleDeleteSectionSubmit,
|
||||
handleDuplicateSectionSubmit,
|
||||
handleNewSectionSubmit,
|
||||
} = useCourseOutline({ courseId });
|
||||
|
||||
document.title = getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
const {
|
||||
isShow: isShowProcessingNotification,
|
||||
title: processingNotificationTitle,
|
||||
@@ -82,6 +84,9 @@ const CourseOutline = ({ courseId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle))}</title>
|
||||
</Helmet>
|
||||
<Container size="xl" className="px-4">
|
||||
<section className="course-outline-container mb-4 mt-5">
|
||||
<TransitionReplace>
|
||||
@@ -131,20 +136,34 @@ const CourseOutline = ({ courseId }) => {
|
||||
openEnableHighlightsModal={openEnableHighlightsModal}
|
||||
/>
|
||||
<div className="pt-4">
|
||||
{/* TODO add create new section handler in EmptyPlaceholder */}
|
||||
{sectionsList.length ? sectionsList.map((section) => (
|
||||
<SectionCard
|
||||
section={section}
|
||||
savingStatus={savingStatus}
|
||||
onOpenHighlightsModal={handleOpenHighlightsModal}
|
||||
onOpenPublishModal={openPublishModal}
|
||||
onOpenDeleteModal={openDeleteModal}
|
||||
onEditSectionSubmit={handleEditSectionSubmit}
|
||||
onDuplicateSubmit={handleDuplicateSectionSubmit}
|
||||
isSectionsExpanded={isSectionsExpanded}
|
||||
/>
|
||||
)) : (
|
||||
<EmptyPlaceholder onCreateNewSection={() => ({})} />
|
||||
{sectionsList.length ? (
|
||||
<>
|
||||
{sectionsList.map((section) => (
|
||||
<SectionCard
|
||||
key={section.id}
|
||||
section={section}
|
||||
savingStatus={savingStatus}
|
||||
onOpenHighlightsModal={handleOpenHighlightsModal}
|
||||
onOpenPublishModal={openPublishModal}
|
||||
onOpenDeleteModal={openDeleteModal}
|
||||
onEditSectionSubmit={handleEditSectionSubmit}
|
||||
onDuplicateSubmit={handleDuplicateSectionSubmit}
|
||||
isSectionsExpanded={isSectionsExpanded}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
data-testid="new-section-button"
|
||||
className="mt-4"
|
||||
variant="outline-primary"
|
||||
onClick={handleNewSectionSubmit}
|
||||
iconBefore={IconAdd}
|
||||
block
|
||||
>
|
||||
{intl.formatMessage(messages.newSectionButton)}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<EmptyPlaceholder onCreateNewSection={handleNewSectionSubmit} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||
import {
|
||||
render, waitFor, cleanup, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -11,13 +13,13 @@ import {
|
||||
getCourseLaunchApiUrl,
|
||||
getCourseOutlineIndexApiUrl,
|
||||
getCourseReindexApiUrl,
|
||||
getCourseReindexApiUrl,
|
||||
getCourseSectionApiUrl,
|
||||
getCourseSectionDuplicateApiUrl,
|
||||
getXBlockApiUrl,
|
||||
getEnableHighlightsEmailsApiUrl,
|
||||
getUpdateCourseSectionApiUrl,
|
||||
getXBlockBaseApiUrl,
|
||||
} from './data/api';
|
||||
import {
|
||||
addNewCourseSectionQuery,
|
||||
deleteCourseSectionQuery,
|
||||
duplicateCourseSectionQuery,
|
||||
editCourseSectionQuery,
|
||||
@@ -36,10 +38,12 @@ import {
|
||||
courseOutlineIndexWithoutSections,
|
||||
courseBestPracticesMock,
|
||||
courseLaunchMock,
|
||||
courseSectionMock,
|
||||
} from './__mocks__';
|
||||
import { executeThunk } from '../utils';
|
||||
import CourseOutline from './CourseOutline';
|
||||
import messages from './messages';
|
||||
import headerMessages from './header-navigations/messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
@@ -53,6 +57,15 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../help-urls/hooks', () => ({
|
||||
useHelpUrls: () => ({
|
||||
contentHighlights: 'some',
|
||||
visibility: 'some',
|
||||
grading: 'some',
|
||||
outline: 'some',
|
||||
}),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
@@ -100,6 +113,25 @@ describe('<CourseOutline />', () => {
|
||||
expect(getByText(messages.alertSuccessDescription.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adds new section correctly', async () => {
|
||||
const { findAllByTestId } = render(<RootWrapper />);
|
||||
let element = await findAllByTestId('section-card');
|
||||
expect(element.length).toBe(4);
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl())
|
||||
.reply(200, {
|
||||
locator: courseSectionMock.id,
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(courseSectionMock.id))
|
||||
.reply(200, courseSectionMock);
|
||||
await executeThunk(addNewCourseSectionQuery(courseId), store.dispatch);
|
||||
|
||||
element = await findAllByTestId('section-card');
|
||||
expect(element.length).toBe(5);
|
||||
});
|
||||
|
||||
it('render error alert after failed reindex correctly', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
@@ -163,11 +195,11 @@ describe('<CourseOutline />', () => {
|
||||
const { queryAllByTestId, getByText } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const collapseBtn = getByText(messages.collapseAllButton.defaultMessage);
|
||||
const collapseBtn = getByText(headerMessages.collapseAllButton.defaultMessage);
|
||||
expect(collapseBtn).toBeInTheDocument();
|
||||
fireEvent.click(collapseBtn);
|
||||
|
||||
const expendBtn = getByText(messages.expandAllButton.defaultMessage);
|
||||
const expendBtn = getByText(headerMessages.expandAllButton.defaultMessage);
|
||||
expect(expendBtn).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(expendBtn);
|
||||
@@ -210,7 +242,7 @@ describe('<CourseOutline />', () => {
|
||||
await executeThunk(editCourseSectionQuery(section.id, newDisplayName), store.dispatch);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionApiUrl(section.id))
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200);
|
||||
await executeThunk(fetchCourseSectionQuery(section.id), store.dispatch);
|
||||
|
||||
@@ -237,7 +269,7 @@ describe('<CourseOutline />', () => {
|
||||
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseSectionDuplicateApiUrl())
|
||||
.onPost(getXBlockBaseApiUrl())
|
||||
.reply(200, {
|
||||
duplicate_source_locator: section.id,
|
||||
parent_locator: courseBlockId,
|
||||
@@ -279,7 +311,7 @@ describe('<CourseOutline />', () => {
|
||||
await executeThunk(publishCourseSectionQuery(section.id), store.dispatch);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionApiUrl(section.id))
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, {
|
||||
...section,
|
||||
published: true,
|
||||
@@ -316,7 +348,7 @@ describe('<CourseOutline />', () => {
|
||||
await executeThunk(updateCourseSectionHighlightsQuery(section.id, highlights), store.dispatch);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionApiUrl(section.id))
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, {
|
||||
...section,
|
||||
highlights,
|
||||
|
||||
93
src/course-outline/__mocks__/courseSection.js
Normal file
93
src/course-outline/__mocks__/courseSection.js
Normal file
@@ -0,0 +1,93 @@
|
||||
module.exports = {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d0e78d363a424da6be5c22704c34f7a7',
|
||||
display_name: 'Section',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
edited_on: 'Nov 22, 2023 at 07:45 UTC',
|
||||
published: true,
|
||||
published_on: 'Nov 22, 2023 at 07:45 UTC',
|
||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40d0e78d363a424da6be5c22704c34f7a7',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
highlights: [],
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: [],
|
||||
},
|
||||
ancestor_has_staff_lock: false,
|
||||
staff_only_message: false,
|
||||
enable_copy_paste_units: false,
|
||||
has_partition_group_components: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
};
|
||||
@@ -2,3 +2,4 @@ export { default as courseOutlineIndexMock } from './courseOutlineIndex';
|
||||
export { default as courseOutlineIndexWithoutSections } from './courseOutlineIndexWithoutSections';
|
||||
export { default as courseBestPracticesMock } from './courseBestPractices';
|
||||
export { default as courseLaunchMock } from './courseLaunch';
|
||||
export { default as courseSectionMock } from './courseSection';
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
ArrowDropDown as ArrowDownIcon,
|
||||
ArrowDropUp as ArrowUpIcon,
|
||||
MoreVert as MoveVertIcon,
|
||||
EditOutline as EditIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
@@ -26,6 +27,7 @@ import messages from './messages';
|
||||
const CardHeader = ({
|
||||
title,
|
||||
sectionStatus,
|
||||
hasChanges,
|
||||
isExpanded,
|
||||
onClickPublish,
|
||||
onClickMenuButton,
|
||||
@@ -42,8 +44,8 @@ const CardHeader = ({
|
||||
const [titleValue, setTitleValue] = useState(title);
|
||||
|
||||
const { badgeTitle, badgeIcon } = getSectionStatusBadgeContent(sectionStatus, messages, intl);
|
||||
const isDisabledPublish = sectionStatus === SECTION_BADGE_STATUTES.live
|
||||
|| sectionStatus === SECTION_BADGE_STATUTES.publishedNotLive;
|
||||
const isDisabledPublish = (sectionStatus === SECTION_BADGE_STATUTES.live
|
||||
|| sectionStatus === SECTION_BADGE_STATUTES.publishedNotLive) && !hasChanges;
|
||||
|
||||
useEscapeClick({
|
||||
onEscape: () => {
|
||||
@@ -86,12 +88,10 @@ const CardHeader = ({
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
iconBefore={ArrowDownIcon}
|
||||
iconBefore={isExpanded ? ArrowUpIcon : ArrowDownIcon}
|
||||
variant="tertiary"
|
||||
data-testid="section-card-header__expanded-btn"
|
||||
className={classNames('section-card-header__expanded-btn', {
|
||||
collapsed: !isExpanded,
|
||||
})}
|
||||
className="section-card-header__expanded-btn"
|
||||
onClick={() => onExpand((prevState) => !prevState)}
|
||||
>
|
||||
<Truncate lines={1} className="h3 mb-0">{title}</Truncate>
|
||||
@@ -149,6 +149,7 @@ const CardHeader = ({
|
||||
CardHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
sectionStatus: PropTypes.string.isRequired,
|
||||
hasChanges: PropTypes.bool.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onClickPublish: PropTypes.func.isRequired,
|
||||
|
||||
@@ -10,23 +10,7 @@
|
||||
height: 1.5rem;
|
||||
margin-right: .25rem;
|
||||
background: transparent;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
&.collapsed > .pgn__icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
& span:first-child {
|
||||
color: $black;
|
||||
}
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.section-card-header__badge-status {
|
||||
@@ -44,11 +28,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.section-card-header__menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pgn__form-group {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { SECTION_BADGE_STATUTES } from '../constants';
|
||||
@@ -17,6 +17,7 @@ const closeFormMock = jest.fn();
|
||||
const cardHeaderProps = {
|
||||
title: 'Some title',
|
||||
sectionStatus: SECTION_BADGE_STATUTES.live,
|
||||
hasChanges: false,
|
||||
isExpanded: true,
|
||||
onExpand: onExpandMock,
|
||||
onClickMenuButton: onClickMenuButtonMock,
|
||||
@@ -40,137 +41,163 @@ const renderComponent = (props) => render(
|
||||
);
|
||||
|
||||
describe('<CardHeader />', () => {
|
||||
it('render CardHeader component correctly', () => {
|
||||
const { getByText, getByTestId, queryByTestId } = renderComponent();
|
||||
it('render CardHeader component correctly', async () => {
|
||||
const { findByText, findByTestId, queryByTestId } = renderComponent();
|
||||
|
||||
expect(getByText(cardHeaderProps.title)).toBeInTheDocument();
|
||||
expect(getByTestId('section-card-header__expanded-btn')).toBeInTheDocument();
|
||||
expect(getByTestId('section-card-header__badge-status')).toBeInTheDocument();
|
||||
expect(getByTestId('section-card-header__menu')).toBeInTheDocument();
|
||||
expect(queryByTestId('edit field')).not.toBeInTheDocument();
|
||||
expect(await findByText(cardHeaderProps.title)).toBeInTheDocument();
|
||||
expect(await findByTestId('section-card-header__expanded-btn')).toBeInTheDocument();
|
||||
expect(await findByTestId('section-card-header__badge-status')).toBeInTheDocument();
|
||||
expect(await findByTestId('section-card-header__menu')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('edit field')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('render status badge as live', () => {
|
||||
const { getByText } = renderComponent();
|
||||
expect(getByText(messages.statusBadgeLive.defaultMessage)).toBeInTheDocument();
|
||||
it('render status badge as live', async () => {
|
||||
const { findByText } = renderComponent();
|
||||
expect(await findByText(messages.statusBadgeLive.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render status badge as published_not_live', () => {
|
||||
const { getByText } = renderComponent({
|
||||
it('render status badge as published_not_live', async () => {
|
||||
const { findByText } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
sectionStatus: SECTION_BADGE_STATUTES.publishedNotLive,
|
||||
});
|
||||
|
||||
expect(getByText(messages.statusBadgePublishedNotLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(await findByText(messages.statusBadgePublishedNotLive.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render status badge as staff_only', () => {
|
||||
const { getByText } = renderComponent({
|
||||
it('render status badge as staff_only', async () => {
|
||||
const { findByText } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
sectionStatus: SECTION_BADGE_STATUTES.staffOnly,
|
||||
});
|
||||
|
||||
expect(getByText(messages.statusBadgeStuffOnly.defaultMessage)).toBeInTheDocument();
|
||||
expect(await findByText(messages.statusBadgeStaffOnly.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render status badge as draft', () => {
|
||||
const { getByText } = renderComponent({
|
||||
it('render status badge as draft', async () => {
|
||||
const { findByText } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
sectionStatus: SECTION_BADGE_STATUTES.draft,
|
||||
});
|
||||
|
||||
expect(getByText(messages.statusBadgeDraft.defaultMessage)).toBeInTheDocument();
|
||||
expect(await findByText(messages.statusBadgeDraft.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('check publish menu item is disabled when section status is live or published not live', async () => {
|
||||
const { getByText, getByTestId } = renderComponent({
|
||||
it('check publish menu item is disabled when section status is live or published not live and it has no changes', async () => {
|
||||
const { findByText, findByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
sectionStatus: SECTION_BADGE_STATUTES.publishedNotLive,
|
||||
});
|
||||
|
||||
const menuButton = getByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
expect(getByText(messages.menuPublish.defaultMessage)).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(await findByText(messages.menuPublish.defaultMessage)).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('calls handleExpanded when button is clicked', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
it('check publish menu item is enabled when section status is live or published not live and it has changes', async () => {
|
||||
const { findByText, findByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
sectionStatus: SECTION_BADGE_STATUTES.publishedNotLive,
|
||||
hasChanges: true,
|
||||
});
|
||||
|
||||
const expandButton = getByTestId('section-card-header__expanded-btn');
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
expect(await findByText(messages.menuPublish.defaultMessage)).not.toHaveAttribute('aria-disabled');
|
||||
});
|
||||
|
||||
it('calls handleExpanded when button is clicked', async () => {
|
||||
const { findByTestId } = renderComponent();
|
||||
|
||||
const expandButton = await findByTestId('section-card-header__expanded-btn');
|
||||
fireEvent.click(expandButton);
|
||||
expect(onExpandMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClickMenuButton when menu is clicked', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
it('calls onClickMenuButton when menu is clicked', async () => {
|
||||
const { findByTestId } = renderComponent();
|
||||
|
||||
const menuButton = getByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
expect(onClickMenuButtonMock).toHaveBeenCalled();
|
||||
waitFor(() => {
|
||||
expect(onClickMenuButtonMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onClickPublish when item is clicked', () => {
|
||||
const { getByText, getByTestId } = renderComponent({
|
||||
it('calls onClickPublish when item is clicked', async () => {
|
||||
const { findByText, findByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
sectionStatus: SECTION_BADGE_STATUTES.draft,
|
||||
});
|
||||
|
||||
const menuButton = getByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const publishMenuItem = getByText(messages.menuPublish.defaultMessage);
|
||||
const publishMenuItem = await findByText(messages.menuPublish.defaultMessage);
|
||||
fireEvent.click(publishMenuItem);
|
||||
expect(onClickPublishMock).toHaveBeenCalled();
|
||||
waitFor(() => {
|
||||
expect(onClickPublishMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onClickEdit when the button is clicked', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
it('calls onClickEdit when the button is clicked', async () => {
|
||||
const { findByTestId } = renderComponent();
|
||||
|
||||
const editButton = getByTestId('edit-button');
|
||||
const editButton = await findByTestId('edit-button');
|
||||
fireEvent.click(editButton);
|
||||
expect(onClickEditMock).toHaveBeenCalled();
|
||||
waitFor(() => {
|
||||
expect(onClickEditMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('check is field visible when isFormOpen is true', () => {
|
||||
const { getByTestId, queryByTestId } = renderComponent({
|
||||
it('check is field visible when isFormOpen is true', async () => {
|
||||
const { findByTestId, queryByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
isFormOpen: true,
|
||||
});
|
||||
|
||||
expect(getByTestId('edit field')).toBeInTheDocument();
|
||||
expect(queryByTestId('section-card-header__expanded-btn')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('edit-button')).not.toBeInTheDocument();
|
||||
expect(await findByTestId('edit field')).toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
expect(queryByTestId('section-card-header__expanded-btn')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('edit-button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('check is field disabled when isDisabledEditField is true', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
it('check is field disabled when isDisabledEditField is true', async () => {
|
||||
const { findByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
isFormOpen: true,
|
||||
isDisabledEditField: true,
|
||||
});
|
||||
|
||||
expect(getByTestId('edit field')).toBeDisabled();
|
||||
expect(await findByTestId('edit field')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('calls onClickDelete when item is clicked', () => {
|
||||
const { getByText, getByTestId } = renderComponent();
|
||||
it('calls onClickDelete when item is clicked', async () => {
|
||||
const { findByText, findByTestId } = renderComponent();
|
||||
|
||||
const menuButton = getByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const deleteMenuItem = getByText(messages.menuDelete.defaultMessage);
|
||||
const deleteMenuItem = await findByText(messages.menuDelete.defaultMessage);
|
||||
fireEvent.click(deleteMenuItem);
|
||||
expect(onClickDeleteMock).toHaveBeenCalledTimes(1);
|
||||
waitFor(() => {
|
||||
expect(onClickDeleteMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onClickDuplicate when item is clicked', () => {
|
||||
const { getByText, getByTestId } = renderComponent();
|
||||
it('calls onClickDuplicate when item is clicked', async () => {
|
||||
const { findByText, findByTestId } = renderComponent();
|
||||
|
||||
const menuButton = getByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const duplicateMenuItem = getByText(messages.menuDuplicate.defaultMessage);
|
||||
const duplicateMenuItem = await findByText(messages.menuDuplicate.defaultMessage);
|
||||
fireEvent.click(duplicateMenuItem);
|
||||
expect(onClickDuplicateMock).toHaveBeenCalled();
|
||||
waitFor(() => {
|
||||
expect(onClickDuplicateMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.section.status-badge.published-not-live',
|
||||
defaultMessage: 'Published not live',
|
||||
},
|
||||
statusBadgeStuffOnly: {
|
||||
statusBadgeStaffOnly: {
|
||||
id: 'course-authoring.course-outline.section.status-badge.staff-only',
|
||||
defaultMessage: 'Staff only',
|
||||
},
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
export const SECTION_BADGE_STATUTES = {
|
||||
export const SECTION_BADGE_STATUTES = /** @type {const} */ ({
|
||||
live: 'live',
|
||||
publishedNotLive: 'published_not_live',
|
||||
staffOnly: 'staff_only',
|
||||
draft: 'draft',
|
||||
};
|
||||
});
|
||||
|
||||
export const STAFF_ONLY = 'staff_only';
|
||||
|
||||
export const HIGHLIGHTS_FIELD_MAX_LENGTH = 250;
|
||||
|
||||
export const CHECKLIST_FILTERS = {
|
||||
export const CHECKLIST_FILTERS = /** @type {const} */ ({
|
||||
ALL: 'ALL',
|
||||
SELF_PACED: 'SELF_PACED',
|
||||
INSTRUCTOR_PACED: 'INSTRUCTOR_PACED',
|
||||
};
|
||||
});
|
||||
|
||||
export const LAUNCH_CHECKLIST = {
|
||||
export const DEFAULT_NEW_DISPLAY_NAMES = /** @type {const} */ ({
|
||||
chapter: 'Section',
|
||||
subsection: 'Subsection',
|
||||
});
|
||||
|
||||
export const LAUNCH_CHECKLIST = /** @type {const} */ ({
|
||||
data: [
|
||||
{
|
||||
id: 'welcomeMessage',
|
||||
@@ -42,9 +47,9 @@ export const LAUNCH_CHECKLIST = {
|
||||
pacingTypeFilter: CHECKLIST_FILTERS.ALL,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export const BEST_PRACTICES_CHECKLIST = {
|
||||
export const BEST_PRACTICES_CHECKLIST = /** @type {const} */ ({
|
||||
data: [
|
||||
{
|
||||
id: 'videoDuration',
|
||||
@@ -67,4 +72,4 @@ export const BEST_PRACTICES_CHECKLIST = {
|
||||
pacingTypeFilter: CHECKLIST_FILTERS.ALL,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
@@ -24,14 +25,32 @@ export const getEnableHighlightsEmailsApiUrl = (courseId) => {
|
||||
};
|
||||
|
||||
export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${reindexLink}`;
|
||||
export const getUpdateCourseSectionApiUrl = (sectionId) => `${getApiBaseUrl()}/xblock/${sectionId}`;
|
||||
export const getCourseSectionApiUrl = (sectionId) => `${getApiBaseUrl()}/xblock/outline/${sectionId}`;
|
||||
export const getCourseSectionDuplicateApiUrl = () => `${getApiBaseUrl()}/xblock/`;
|
||||
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
|
||||
export const getUpdateCourseSectionApiUrl = (sectionId) => `${getXBlockBaseApiUrl()}${sectionId}`;
|
||||
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
|
||||
|
||||
/**
|
||||
* @typedef {Object} courseOutline
|
||||
* @property {string} courseReleaseDate
|
||||
* @property {Object} courseStructure
|
||||
* @property {Object} deprecatedBlocksInfo
|
||||
* @property {string} discussionsIncontextFeedbackUrl
|
||||
* @property {string} discussionsIncontextLearnmoreUrl
|
||||
* @property {Object} initialState
|
||||
* @property {Object} initialUserClipboard
|
||||
* @property {string} languageCode
|
||||
* @property {string} lmsLink
|
||||
* @property {string} mfeProctoredExamSettingsUrl
|
||||
* @property {string} notificationDismissUrl
|
||||
* @property {string[]} proctoringErrors
|
||||
* @property {string} reindexLink
|
||||
* @property {null} rerunNotificationId
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get course outline index.
|
||||
* @param {string} courseId
|
||||
* @returns {Promise<Object>}
|
||||
* @returns {Promise<courseOutline>}
|
||||
*/
|
||||
export async function getCourseOutlineIndex(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
@@ -42,10 +61,8 @@ export async function getCourseOutlineIndex(courseId) {
|
||||
|
||||
/**
|
||||
* Get course best practices.
|
||||
* @param {string} courseId
|
||||
* @param {boolean} excludeGraded
|
||||
* @param {boolean} all
|
||||
* @returns {Promise<Object>}
|
||||
* @param {{courseId: string, excludeGraded: boolean, all: boolean}} options
|
||||
* @returns {Promise<{isSelfPaced: boolean, sections: any, subsection: any, units: any, videos: any }>}
|
||||
*/
|
||||
export async function getCourseBestPractices({
|
||||
courseId,
|
||||
@@ -58,13 +75,21 @@ export async function getCourseBestPractices({
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/** @typedef {object} courseLaunchData
|
||||
* @property {boolean} isSelfPaced
|
||||
* @property {object} dates
|
||||
* @property {object} assignments
|
||||
* @property {object} grades
|
||||
* @property {number} grades.sum_of_weights
|
||||
* @property {object} certificates
|
||||
* @property {object} updates
|
||||
* @property {object} proctoring
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get course launch.
|
||||
* @param {string} courseId
|
||||
* @param {boolean} gradedOnly
|
||||
* @param {boolean} validateOras
|
||||
* @param {boolean} all
|
||||
* @returns {Promise<Object>}
|
||||
* @param {{courseId: string, gradedOnly: boolean, validateOras: boolean, all: boolean}} options
|
||||
* @returns {Promise<courseLaunchData>}
|
||||
*/
|
||||
export async function getCourseLaunch({
|
||||
courseId,
|
||||
@@ -109,14 +134,52 @@ export async function restartIndexingOnCourse(reindexLink) {
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} section
|
||||
* @property {string} id
|
||||
* @property {string} displayName
|
||||
* @property {string} category
|
||||
* @property {boolean} hasChildren
|
||||
* @property {string} editedOn
|
||||
* @property {boolean} published
|
||||
* @property {string} publishedOn
|
||||
* @property {string} studioUrl
|
||||
* @property {boolean} releasedToStudents
|
||||
* @property {string} releaseDate
|
||||
* @property {string} visibilityState
|
||||
* @property {boolean} hasExplicitStaffLock
|
||||
* @property {string} start
|
||||
* @property {boolean} graded
|
||||
* @property {string} dueDate
|
||||
* @property {null} due
|
||||
* @property {null} relativeWeeksDue
|
||||
* @property {null} format
|
||||
* @property {string[]} courseGraders
|
||||
* @property {boolean} hasChanges
|
||||
* @property {object} actions
|
||||
* @property {null} explanatoryMessage
|
||||
* @property {object[]} userPartitions
|
||||
* @property {string} showCorrectness
|
||||
* @property {string[]} highlights
|
||||
* @property {boolean} highlightsEnabled
|
||||
* @property {boolean} highlightsPreviewOnly
|
||||
* @property {string} highlightsDocUrl
|
||||
* @property {object} childInfo
|
||||
* @property {boolean} ancestorHasStaffLock
|
||||
* @property {boolean} staffOnlyMessage
|
||||
* @property {boolean} hasPartitionGroupComponents
|
||||
* @property {object} userPartitionInfo
|
||||
* @property {boolean} enableCopyPasteUnits
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get course section
|
||||
* @param {string} sectionId
|
||||
* @returns {Promise<Object>}
|
||||
* @returns {Promise<section>}
|
||||
*/
|
||||
export async function getCourseSection(sectionId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseSectionApiUrl(sectionId));
|
||||
.get(getXBlockApiUrl(sectionId));
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
@@ -189,10 +252,26 @@ export async function deleteCourseSection(sectionId) {
|
||||
*/
|
||||
export async function duplicateCourseSection(sectionId, courseBlockId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getCourseSectionDuplicateApiUrl(), {
|
||||
.post(getXBlockBaseApiUrl(), {
|
||||
duplicate_source_locator: sectionId,
|
||||
parent_locator: courseBlockId,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new course item like section, subsection or unit.
|
||||
* @param {string} courseBlockId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function addNewCourseItem(courseBlockId, category, displayName) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getXBlockBaseApiUrl(), {
|
||||
parent_locator: courseBlockId,
|
||||
category,
|
||||
display_name: displayName,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,12 @@ const slice = createSlice({
|
||||
setCurrentSection: (state, { payload }) => {
|
||||
state.currentSection = payload;
|
||||
},
|
||||
addSection: (state, { payload }) => {
|
||||
state.sectionsList = [
|
||||
...state.sectionsList,
|
||||
payload,
|
||||
];
|
||||
},
|
||||
deleteSection: (state, { payload }) => {
|
||||
state.sectionsList = state.sectionsList.filter(({ id }) => id !== payload);
|
||||
},
|
||||
@@ -89,6 +95,7 @@ const slice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
addSection,
|
||||
fetchOutlineIndexSuccess,
|
||||
updateOutlineIndexLoadingStatus,
|
||||
updateReindexLoadingStatus,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { NOTIFICATION_MESSAGES } from '../../constants';
|
||||
import { DEFAULT_NEW_DISPLAY_NAMES } from '../constants';
|
||||
import {
|
||||
hideProcessingNotification,
|
||||
showProcessingNotification,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
getCourseLaunchChecklist,
|
||||
} from '../utils/getChecklistForStatusBar';
|
||||
import {
|
||||
addNewCourseItem,
|
||||
deleteCourseSection,
|
||||
duplicateCourseSection,
|
||||
editCourseSection,
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
updateCourseSectionHighlights,
|
||||
} from './api';
|
||||
import {
|
||||
addSection,
|
||||
fetchOutlineIndexSuccess,
|
||||
updateOutlineIndexLoadingStatus,
|
||||
updateReindexLoadingStatus,
|
||||
@@ -231,3 +234,28 @@ export function duplicateCourseSectionQuery(sectionId, courseBlockId) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function addNewCourseSectionQuery(courseBlockId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
||||
|
||||
try {
|
||||
await addNewCourseItem(
|
||||
courseBlockId,
|
||||
'chapter',
|
||||
DEFAULT_NEW_DISPLAY_NAMES.chapter,
|
||||
).then(async (result) => {
|
||||
if (result) {
|
||||
const data = await getCourseSection(result.locator);
|
||||
dispatch(addSection(data));
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(hideProcessingNotification());
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ const DeleteModal = ({ isOpen, close, onDeleteSubmit }) => {
|
||||
return (
|
||||
<AlertModal
|
||||
title={intl.formatMessage(messages.title)}
|
||||
variant="warning"
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
footerNode={(
|
||||
|
||||
@@ -11,7 +11,7 @@ const messages = defineMessages({
|
||||
},
|
||||
deleteButton: {
|
||||
id: 'course-authoring.course-outline.delete-modal.button.delete',
|
||||
defaultMessage: 'Yes, delete this section',
|
||||
defaultMessage: 'Delete',
|
||||
},
|
||||
cancelButton: {
|
||||
id: 'course-authoring.course-outline.delete-modal.button.cancel',
|
||||
|
||||
@@ -21,7 +21,8 @@ const EmptyPlaceholder = ({ onCreateNewSection }) => {
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
iconBefore={IconAdd}
|
||||
onClick={onCreateNewSection}
|
||||
>
|
||||
|
||||
@@ -22,6 +22,12 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../help-urls/hooks', () => ({
|
||||
useHelpUrls: () => ({
|
||||
contentHighlights: 'some',
|
||||
}),
|
||||
}));
|
||||
|
||||
const onEnableHighlightsSubmitMock = jest.fn();
|
||||
const closeMock = jest.fn();
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ const HeaderNavigations = ({
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{hasSections ? (
|
||||
{hasSections && (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
|
||||
@@ -67,7 +67,7 @@ const HeaderNavigations = ({
|
||||
? intl.formatMessage(messages.collapseAllButton)
|
||||
: intl.formatMessage(messages.expandAllButton)}
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
overlay={(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
render, fireEvent, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../help-urls/hooks', () => ({
|
||||
useHelpUrls: () => ({
|
||||
contentHighlights: 'some',
|
||||
}),
|
||||
}));
|
||||
|
||||
const currentSectionMock = {
|
||||
highlights: ['Highlight 1', 'Highlight 2'],
|
||||
displayName: 'Test Section',
|
||||
|
||||
@@ -23,7 +23,7 @@ const messages = defineMessages({
|
||||
},
|
||||
saveButton: {
|
||||
id: 'course-authoring.course-outline.highlights-modal.button.save',
|
||||
defaultMessage: 'Save highlights',
|
||||
defaultMessage: 'Save',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getCurrentSection,
|
||||
} from './data/selectors';
|
||||
import {
|
||||
addNewCourseSectionQuery,
|
||||
deleteCourseSectionQuery,
|
||||
editCourseSectionQuery,
|
||||
duplicateCourseSectionQuery,
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
const useCourseOutline = ({ courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { reindexLink, lmsLink } = useSelector(getOutlineIndexData);
|
||||
const { reindexLink, courseStructure, lmsLink } = useSelector(getOutlineIndexData);
|
||||
const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus);
|
||||
const statusBarData = useSelector(getStatusBarData);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
@@ -47,10 +48,12 @@ const useCourseOutline = ({ courseId }) => {
|
||||
const [isPublishModalOpen, openPublishModal, closePublishModal] = useToggle(false);
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||
|
||||
const handleNewSectionSubmit = () => {
|
||||
dispatch(addNewCourseSectionQuery(courseStructure.id));
|
||||
};
|
||||
|
||||
const headerNavigationsActions = {
|
||||
handleNewSection: () => {
|
||||
// TODO add handler
|
||||
},
|
||||
handleNewSection: handleNewSectionSubmit,
|
||||
handleReIndex: () => {
|
||||
setDisableReindexButton(true);
|
||||
setShowSuccessAlert(false);
|
||||
@@ -154,6 +157,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
openDeleteModal,
|
||||
handleDeleteSectionSubmit,
|
||||
handleDuplicateSectionSubmit,
|
||||
handleNewSectionSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -29,13 +29,9 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.reindex.alert.error.title',
|
||||
defaultMessage: 'There were errors reindexing course.',
|
||||
},
|
||||
expandAllButton: {
|
||||
id: 'course-authoring.course-outline.header-navigations.button.expand-all',
|
||||
defaultMessage: 'Expand all',
|
||||
},
|
||||
collapseAllButton: {
|
||||
id: 'course-authoring.course-outline.header-navigations.button.collapse-all',
|
||||
defaultMessage: 'Collapse all',
|
||||
newSectionButton: {
|
||||
id: 'course-authoring.course-outline.section-list.button.new-section',
|
||||
defaultMessage: 'New section',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ const PublishModal = ({
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<p className="small">{intl.formatMessage(messages.description)}</p>
|
||||
{subSections.length ? subSections.map((subSection) => {
|
||||
const units = subSection.childInfo.children;
|
||||
{subSections.filter(subSection => subSection.hasChanges).map((subSection) => {
|
||||
const units = subSection.childInfo.children.filter(unit => unit.hasChanges);
|
||||
|
||||
return units.length ? (
|
||||
<React.Fragment key={subSection.id}>
|
||||
@@ -52,7 +52,7 @@ const PublishModal = ({
|
||||
))}
|
||||
</React.Fragment>
|
||||
) : null;
|
||||
}) : null}
|
||||
})}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="pt-1">
|
||||
<ActionRow>
|
||||
|
||||
@@ -35,28 +35,37 @@ const currentSectionMock = {
|
||||
children: [
|
||||
{
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
hasChanges: true,
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
hasChanges: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 2',
|
||||
id: 2,
|
||||
hasChanges: true,
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
displayName: 'Subsection_2 Unit 1',
|
||||
hasChanges: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 3',
|
||||
id: 3,
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
|
||||
@@ -34,9 +34,10 @@ const SectionCard = ({
|
||||
const {
|
||||
id,
|
||||
displayName,
|
||||
hasChanges,
|
||||
published,
|
||||
releasedToStudents,
|
||||
visibleToStaffOnly,
|
||||
visibleToStaffOnly = false,
|
||||
visibilityState,
|
||||
staffOnlyMessage,
|
||||
highlights,
|
||||
@@ -83,6 +84,7 @@ const SectionCard = ({
|
||||
sectionId={id}
|
||||
title={displayName}
|
||||
sectionStatus={sectionStatus}
|
||||
hasChanges={hasChanges}
|
||||
isExpanded={isExpanded}
|
||||
onExpand={handleExpandContent}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
@@ -104,7 +106,7 @@ const SectionCard = ({
|
||||
onClick={handleOpenHighlightsModal}
|
||||
>
|
||||
<Badge className="highlights-badge">{highlights.length}</Badge>
|
||||
<p className="m-0 text-black">Section highlights</p>
|
||||
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,8 +139,9 @@ SectionCard.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
published: PropTypes.bool.isRequired,
|
||||
hasChanges: PropTypes.bool.isRequired,
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
visibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
visibleToStaffOnly: PropTypes.bool,
|
||||
visibilityState: PropTypes.string.isRequired,
|
||||
staffOnlyMessage: PropTypes.bool.isRequired,
|
||||
highlights: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
padding: $spacer 1.5rem 1.5rem;
|
||||
cursor: move;
|
||||
margin-bottom: 1.5rem;
|
||||
background: $light-100;
|
||||
|
||||
.section-card__content {
|
||||
margin-top: $spacer;
|
||||
|
||||
@@ -21,6 +21,7 @@ const section = {
|
||||
visibleToStaffOnly: false,
|
||||
visibilityState: 'visible',
|
||||
staffOnlyMessage: false,
|
||||
hasChanges: false,
|
||||
highlights: ['highlight 1', 'highlight 2'],
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.section.button.new-subsection',
|
||||
defaultMessage: 'New subsection',
|
||||
},
|
||||
sectionHighlightsBadge: {
|
||||
id: 'course-authoring.course-outline.section.badge.section-highlights',
|
||||
defaultMessage: 'Section highlights',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -31,8 +31,8 @@ const StatusBar = ({
|
||||
} = checklist;
|
||||
|
||||
const checkListTitle = `${completedCourseLaunchChecks + completedCourseBestPracticesChecks}/${totalCourseLaunchChecks + totalCourseBestPracticesChecks}`;
|
||||
const checklistDestination = new URL(`checklists/${courseId}`, config.STUDIO_BASE_URL).href;
|
||||
const scheduleDestination = new URL(`settings/details/${courseId}#schedule`, config.STUDIO_BASE_URL).href;
|
||||
const checklistDestination = () => new URL(`checklists/${courseId}`, config.STUDIO_BASE_URL).href;
|
||||
const scheduleDestination = () => new URL(`settings/details/${courseId}#schedule`, config.STUDIO_BASE_URL).href;
|
||||
|
||||
const {
|
||||
contentHighlights: contentHighlightsUrl,
|
||||
@@ -49,7 +49,7 @@ const StatusBar = ({
|
||||
<h5>{intl.formatMessage(messages.startDateTitle)}</h5>
|
||||
<Hyperlink
|
||||
className="small"
|
||||
destination={scheduleDestination}
|
||||
destination={scheduleDestination()}
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{courseReleaseDate}
|
||||
@@ -67,7 +67,7 @@ const StatusBar = ({
|
||||
<h5>{intl.formatMessage(messages.checklistTitle)}</h5>
|
||||
<Hyperlink
|
||||
className="small"
|
||||
destination={checklistDestination}
|
||||
destination={checklistDestination()}
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{checkListTitle} {intl.formatMessage(messages.checklistCompleted)}
|
||||
|
||||
@@ -21,6 +21,12 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../help-urls/hooks', () => ({
|
||||
useHelpUrls: () => ({
|
||||
contentHighlights: 'some',
|
||||
}),
|
||||
}));
|
||||
|
||||
const statusBarData = {
|
||||
courseReleaseDate: 'Feb 05, 2013 at 05:00 UTC',
|
||||
isSelfPaced: true,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { SECTION_BADGE_STATUTES, STAFF_ONLY } from './constants';
|
||||
* @param {bool} visibleToStaffOnly - value from section info
|
||||
* @param {string} visibilityState - value from section info
|
||||
* @param {bool} staffOnlyMessage - value from section info
|
||||
* @returns {typeof SECTION_BADGE_STATUTES}
|
||||
* @returns {SECTION_BADGE_STATUTES[keyof SECTION_BADGE_STATUTES]}
|
||||
*/
|
||||
const getSectionStatus = ({
|
||||
published,
|
||||
@@ -58,7 +58,7 @@ const getSectionStatusBadgeContent = (status, messages, intl) => {
|
||||
};
|
||||
case SECTION_BADGE_STATUTES.staffOnly:
|
||||
return {
|
||||
badgeTitle: intl.formatMessage(messages.statusBadgeStuffOnly),
|
||||
badgeTitle: intl.formatMessage(messages.statusBadgeStaffOnly),
|
||||
badgeIcon: LockIcon,
|
||||
};
|
||||
case SECTION_BADGE_STATUTES.draft:
|
||||
|
||||
@@ -31,8 +31,8 @@ const CourseUploadImage = ({
|
||||
}) => {
|
||||
const { courseId } = useParams();
|
||||
const intl = useIntl();
|
||||
const imageAbsolutePath = new URL(assetImagePath, getConfig().LMS_BASE_URL);
|
||||
const assetsUrl = new URL(`/assets/${courseId}`, getConfig().STUDIO_BASE_URL);
|
||||
const imageAbsolutePath = () => new URL(assetImagePath, getConfig().LMS_BASE_URL);
|
||||
const assetsUrl = () => new URL(`/assets/${courseId}`, getConfig().STUDIO_BASE_URL);
|
||||
|
||||
const handleChangeImageAsset = (path) => {
|
||||
const assetPath = _.last(path.split('/'));
|
||||
@@ -59,7 +59,7 @@ const CourseUploadImage = ({
|
||||
const inputComponent = assetImagePath ? (
|
||||
<div className="image-preview">
|
||||
<Image
|
||||
src={imageAbsolutePath.href}
|
||||
src={imageAbsolutePath().href}
|
||||
alt={intl.formatMessage(messages.uploadImageDropzoneAlt)}
|
||||
fluid
|
||||
/>
|
||||
@@ -88,7 +88,7 @@ const CourseUploadImage = ({
|
||||
values={{
|
||||
hyperlink: (
|
||||
<Hyperlink
|
||||
destination={assetsUrl.href}
|
||||
destination={assetsUrl().href}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { NOTIFICATION_MESSAGES } from '../../../constants';
|
||||
|
||||
const initialState = {
|
||||
isShow: false,
|
||||
title: '',
|
||||
title: NOTIFICATION_MESSAGES.empty,
|
||||
};
|
||||
|
||||
const slice = createSlice({
|
||||
|
||||
@@ -17,7 +17,7 @@ const CardItem = ({
|
||||
courseCreatorStatus,
|
||||
rerunCreatorStatus,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const courseUrl = new URL(url, getConfig().STUDIO_BASE_URL);
|
||||
const courseUrl = () => new URL(url, getConfig().STUDIO_BASE_URL);
|
||||
const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`;
|
||||
const readOnlyItem = !(lmsLink || rerunLink || url);
|
||||
const showActions = !(readOnlyItem || isLibraries);
|
||||
@@ -32,7 +32,7 @@ const CardItem = ({
|
||||
title={!readOnlyItem ? (
|
||||
<Hyperlink
|
||||
className="card-item-title"
|
||||
destination={courseUrl.toString()}
|
||||
destination={courseUrl().toString()}
|
||||
>
|
||||
{displayName}
|
||||
</Hyperlink>
|
||||
|
||||
Reference in New Issue
Block a user