Skip to content

Commit

Permalink
feat: Course outline - Section duplicate (openedx#88)
Browse files Browse the repository at this point in the history
* feat: [2u-336] add tests

* feat: [2u-271] fix button

* feat: [2u-336] add component, refactor header

* feat: [2u-342] add modal

* fix: [2u-342] fix translates and indents

* feat: [2u-342] add modal

* feat: [2u-342] add api

* feat: [2u-342] add tests and translates

* feat: [2u-342] fix indents

* fix: [2u-342] fix indents, variant and utils

* feat: [2u-342] fixed slice, thunks, hooks

* feat: [2u-354] add publish modal, api and update tests

* feat: [2u-615] update section card

* feat: [2u-348] add api, handlers, tests

* feat: [2u-510] add delete api, add delete modal

* feat: [2u-360] add api

* feat: [2u-360] add slice

* feat: [2u-360] add tests

* fix: [2u-360] fixed tests

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>
  • Loading branch information
2 people authored and navinkarkera committed Nov 22, 2023
1 parent 424170a commit 278df72
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const CourseOutline = ({ courseId }) => {
handlePublishSectionSubmit,
handleEditSectionSubmit,
handleDeleteSectionSubmit,
handleDuplicateSectionSubmit,
} = useCourseOutline({ courseId });

if (isLoading) {
Expand Down Expand Up @@ -123,6 +124,7 @@ const CourseOutline = ({ courseId }) => {
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSectionSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
// TODO add handler in Add new subsection feature
onClickNewSubsection={() => ({})}
/>
Expand Down
24 changes: 24 additions & 0 deletions src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import {
getCourseLaunchApiUrl,
getCourseOutlineIndexApiUrl,
getCourseReindexApiUrl,
getCourseSectionDuplicateApiUrl,
getEnableHighlightsEmailsApiUrl,
getUpdateCourseSectionApiUrl,
} from './data/api';
import {
deleteCourseSectionQuery,
duplicateCourseSectionQuery,
editCourseSectionQuery,
enableCourseHighlightsEmailsQuery,
fetchCourseBestPracticesQuery,
Expand Down Expand Up @@ -208,4 +210,26 @@ describe('<CourseOutline />', () => {
expect(queryByText(section.displayName)).not.toBeInTheDocument();
});
});

it('check duplicate section when duplicate query is successfully', async () => {
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, courseOutlineIndexMock);

const { getAllByTestId } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
const courseBlockId = courseOutlineIndexMock.courseStructure.id;

axiosMock
.onPost(getCourseSectionDuplicateApiUrl())
.reply(200, {
duplicate_source_locator: section.id,
parent_locator: courseBlockId,
});
await executeThunk(duplicateCourseSectionQuery(section.id, courseBlockId), store.dispatch);

await waitFor(() => {
expect(getAllByTestId('section-card')).toHaveLength(4);
});
});
});
4 changes: 3 additions & 1 deletion src/course-outline/card-header/CardHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const CardHeader = ({
closeForm,
isDisabledEditField,
onClickDelete,
onClickDuplicate,
}) => {
const intl = useIntl();
const [titleValue, setTitleValue] = useState(title);
Expand Down Expand Up @@ -136,7 +137,7 @@ const CardHeader = ({
{intl.formatMessage(messages.menuPublish)}
</Dropdown.Item>
<Dropdown.Item>{intl.formatMessage(messages.menuConfigure)}</Dropdown.Item>
<Dropdown.Item>{intl.formatMessage(messages.menuDuplicate)}</Dropdown.Item>
<Dropdown.Item onClick={onClickDuplicate}>{intl.formatMessage(messages.menuDuplicate)}</Dropdown.Item>
<Dropdown.Item onClick={onClickDelete}>{intl.formatMessage(messages.menuDelete)}</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
Expand All @@ -158,6 +159,7 @@ CardHeader.propTypes = {
closeForm: PropTypes.func.isRequired,
isDisabledEditField: PropTypes.bool.isRequired,
onClickDelete: PropTypes.func.isRequired,
onClickDuplicate: PropTypes.func.isRequired,
};

export default CardHeader;
13 changes: 13 additions & 0 deletions src/course-outline/card-header/CardHeader.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const onClickMenuButtonMock = jest.fn();
const onClickPublishMock = jest.fn();
const onClickEditMock = jest.fn();
const onClickDeleteMock = jest.fn();
const onClickDuplicateMock = jest.fn();

const cardHeaderProps = {
title: 'Some title',
Expand All @@ -25,6 +26,7 @@ const cardHeaderProps = {
closeForm: jest.fn(),
isDisabledEditField: false,
onClickDelete: onClickDeleteMock,
onClickDuplicate: onClickDuplicateMock,
};

const renderComponent = (props) => render(
Expand Down Expand Up @@ -166,4 +168,15 @@ describe('<CardHeader />', () => {
fireEvent.click(deleteMenuItem);
expect(onClickDeleteMock).toHaveBeenCalledTimes(1);
});

it('calls onClickDuplicate when item is clicked', () => {
const { getByText, getByTestId } = renderComponent();

const menuButton = getByTestId('section-card-header__menu-button');
fireEvent.click(menuButton);

const duplicateMenuItem = getByText(messages.menuDuplicate.defaultMessage);
fireEvent.click(duplicateMenuItem);
expect(onClickDuplicateMock).toHaveBeenCalled();
});
});
19 changes: 19 additions & 0 deletions src/course-outline/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ 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/`;

/**
* Get course outline index.
Expand Down Expand Up @@ -148,3 +151,19 @@ export async function deleteCourseSection(sectionId) {

return data;
}

/**
* Duplicate course section
* @param {string} sectionId
* @param {string} courseBlockId
* @returns {Promise<Object>}
*/
export async function duplicateCourseSection(sectionId, courseBlockId) {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseSectionDuplicateApiUrl(), {
duplicate_source_locator: sectionId,
parent_locator: courseBlockId,
});

return data;
}
9 changes: 9 additions & 0 deletions src/course-outline/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ const slice = createSlice({
deleteSection: (state, { payload }) => {
state.sectionsList = state.sectionsList.filter(({ id }) => id !== payload);
},
duplicateSection: (state, { payload }) => {
state.sectionsList = state.sectionsList.reduce((result, currentValue) => {
if (currentValue.id === payload.id) {
return [...result, currentValue, payload.duplicatedSection];
}
return [...result, currentValue];
}, []);
},
},
});

Expand All @@ -84,6 +92,7 @@ export const {
updateSectionList,
setCurrentSection,
deleteSection,
duplicateSection,
} = slice.actions;

export const {
Expand Down
26 changes: 26 additions & 0 deletions src/course-outline/data/thunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../utils/getChecklistForStatusBar';
import {
deleteCourseSection,
duplicateCourseSection,
editCourseSection,
enableCourseHighlightsEmails,
getCourseBestPractices,
Expand All @@ -31,6 +32,7 @@ import {
updateSectionList,
updateFetchSectionLoadingStatus,
deleteSection,
duplicateSection,
} from './slice';

export function fetchCourseOutlineIndexQuery(courseId) {
Expand Down Expand Up @@ -219,3 +221,27 @@ export function deleteCourseSectionQuery(sectionId) {
}
};
}

export function duplicateCourseSectionQuery(sectionId, courseBlockId) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));

try {
await duplicateCourseSection(sectionId, courseBlockId).then(async (result) => {
if (result) {
const duplicatedSection = await getCourseSection(result.locator);
dispatch(duplicateSection({ id: sectionId, duplicatedSection }));
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
}
});

return true;
} catch (error) {
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
}
};
}
6 changes: 6 additions & 0 deletions src/course-outline/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {
// deleteCourseSectionQuery,
editCourseSectionQuery,
duplicateCourseSectionQuery,
enableCourseHighlightsEmailsQuery,
fetchCourseBestPracticesQuery,
fetchCourseLaunchQuery,
Expand Down Expand Up @@ -82,6 +83,10 @@ const useCourseOutline = ({ courseId }) => {
closeDeleteModal();
};

const handleDuplicateSectionSubmit = () => {
dispatch(duplicateCourseSectionQuery(currentSection.id, courseStructure.id));
};

useEffect(() => {
dispatch(fetchCourseOutlineIndexQuery(courseId));
dispatch(fetchCourseBestPracticesQuery({ courseId }));
Expand Down Expand Up @@ -127,6 +132,7 @@ const useCourseOutline = ({ courseId }) => {
closeDeleteModal,
openDeleteModal,
handleDeleteSectionSubmit,
handleDuplicateSectionSubmit,
};
};

Expand Down
5 changes: 4 additions & 1 deletion src/course-outline/section-card/SectionCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const SectionCard = ({
onEditSectionSubmit,
savingStatus,
onOpenDeleteModal,
onDuplicateSubmit,
}) => {
const intl = useIntl();
const [isExpanded, setIsExpanded] = useState(true);
Expand Down Expand Up @@ -63,7 +64,7 @@ const SectionCard = ({
}, [savingStatus]);

return (
<div className="section-card">
<div className="section-card" data-testid="section-card">
<CardHeader
sectionId={id}
title={displayName}
Expand All @@ -78,6 +79,7 @@ const SectionCard = ({
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
/>
<div className="section-card__content" data-testid="section-card__content">
<div className="outline-section__status">
Expand Down Expand Up @@ -126,6 +128,7 @@ SectionCard.propTypes = {
onEditSectionSubmit: PropTypes.func.isRequired,
savingStatus: PropTypes.string.isRequired,
onOpenDeleteModal: PropTypes.func.isRequired,
onDuplicateSubmit: PropTypes.func.isRequired,
};

export default SectionCard;
1 change: 1 addition & 0 deletions src/course-outline/section-card/SectionCard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const renderComponent = (props) => render(
onEditClick={jest.fn()}
savingStatus=""
onEditSectionSubmit={jest.fn()}
onDuplicateSubmit={jest.fn()}
{...props}
>
<span>children</span>
Expand Down

0 comments on commit 278df72

Please sign in to comment.