feat: move up & down menu action for sections, subsections & units

test: add tests for move options

refactor: disable move option instead of hiding

fix: incorrect variable name in tests

feat: move up & down menu action for units

test: add tests for unit move options
This commit is contained in:
Navin Karkera
2024-01-16 15:40:04 +05:30
committed by Kristin Aoki
parent d2f63b8b16
commit 53118a4e0b
13 changed files with 511 additions and 34 deletions

46
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0",
"license": "AGPL-3.0",
"dependencies": {
"@dnd-kit/sortable": "^8.0.0",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-ai-translations-edx": "^1.4.2",
"@edx/frontend-component-footer": "^12.3.0",
@@ -2337,9 +2338,9 @@
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
"integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==",
"dependencies": {
"tslib": "^2.0.0"
},
@@ -2348,12 +2349,12 @@
}
},
"node_modules/@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz",
"integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"@dnd-kit/accessibility": "^3.1.0",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
@@ -2362,22 +2363,22 @@
}
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"@dnd-kit/core": "^6.1.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"dependencies": {
"tslib": "^2.0.0"
},
@@ -2811,6 +2812,19 @@
"react-dom": "^16.14.0 || ^17.0.0"
}
},
"node_modules/@edx/frontend-lib-content-components/node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"react": ">=16.8.0"
}
},
"node_modules/@edx/frontend-lib-content-components/node_modules/react-responsive": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz",

View File

@@ -36,6 +36,7 @@
"url": "https://github.com/openedx/frontend-app-course-authoring/issues"
},
"dependencies": {
"@dnd-kit/sortable": "^8.0.0",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-ai-translations-edx": "^1.4.2",
"@edx/frontend-component-footer": "^12.3.0",

View File

@@ -21,6 +21,7 @@ import {
DraggableList,
ErrorAlert,
} from '@edx/frontend-lib-content-components';
import { arrayMove } from '@dnd-kit/sortable';
import { LoadingSpinner } from '../generic/Loading';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
@@ -148,6 +149,86 @@ const CourseOutline = ({ courseId }) => {
});
};
/**
* Check if item can be moved by given step.
* Inner function returns false if the new index after moving by given step
* is out of bounds of item length.
* If it is within bounds, returns draggable flag of the item in the new index.
* This helps us avoid moving the item to a position of unmovable item.
* @param {Array} items
* @returns {(id, step) => bool}
*/
const canMoveItem = (items) => (id, step) => {
const newId = id + step;
const indexCheck = newId >= 0 && newId < items.length;
if (!indexCheck) {
return false;
}
const newItem = items[newId];
return newItem.actions.draggable;
};
/**
* Move section to new index
* @param {any} currentIndex
* @param {any} newIndex
*/
const updateSectionOrderByIndex = (currentIndex, newIndex) => {
if (currentIndex === newIndex) {
return;
}
setSections((prevSections) => {
const newSections = arrayMove(prevSections, currentIndex, newIndex);
finalizeSectionOrder()(newSections);
return newSections;
});
};
/**
* Returns a function for given section which can move a subsection inside it
* to a new position
* @param {any} sectionIndex
* @param {any} section
* @param {any} subsections
* @returns {(currentIndex, newIndex) => void}
*/
const updateSubsectionOrderByIndex = (sectionIndex, section, subsections) => (currentIndex, newIndex) => {
if (currentIndex === newIndex) {
return;
}
setSubsection(sectionIndex)(() => {
const newSubsections = arrayMove(subsections, currentIndex, newIndex);
finalizeSubsectionOrder(section)()(newSubsections);
return newSubsections;
});
};
/**
* Returns a function for given section & subsection which can move a unit
* inside it to a new position
* @param {any} sectionIndex
* @param {any} section
* @param {any} subsection
* @param {any} units
* @returns {(currentIndex, newIndex) => void}
*/
const updateUnitOrderByIndex = (
sectionIndex,
subsectionIndex,
section,
subsection,
units,
) => (currentIndex, newIndex) => {
if (currentIndex === newIndex) {
return;
}
setUnit(sectionIndex, subsectionIndex)(() => {
const newUnits = arrayMove(units, currentIndex, newIndex);
finalizeUnitOrder(section, subsection)()(newUnits);
return newUnits;
});
};
useEffect(() => {
setSections(sectionsList);
}, [sectionsList]);
@@ -228,6 +309,8 @@ const CourseOutline = ({ courseId }) => {
id={section.id}
key={section.id}
section={section}
index={sectionIndex}
canMoveItem={canMoveItem(sections)}
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
@@ -237,6 +320,7 @@ const CourseOutline = ({ courseId }) => {
onDuplicateSubmit={handleDuplicateSectionSubmit}
isSectionsExpanded={isSectionsExpanded}
onNewSubsectionSubmit={handleNewSubsectionSubmit}
onOrderChange={updateSectionOrderByIndex}
>
<DraggableList
itemList={section.childInfo.children}
@@ -248,30 +332,46 @@ const CourseOutline = ({ courseId }) => {
key={subsection.id}
section={section}
subsection={subsection}
index={subsectionIndex}
canMoveItem={canMoveItem(section.childInfo.children)}
savingStatus={savingStatus}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
onNewUnitSubmit={handleNewUnitSubmit}
onOrderChange={updateSubsectionOrderByIndex(
sectionIndex,
section,
section.childInfo.children,
)}
>
<DraggableList
itemList={subsection.childInfo.children}
setState={setUnit(sectionIndex, subsectionIndex)}
updateOrder={finalizeUnitOrder(section, subsection)}
>
{subsection.childInfo.children.map((unit) => (
{subsection.childInfo.children.map((unit, unitIndex) => (
<UnitCard
key={unit.id}
unit={unit}
subsection={subsection}
section={section}
index={unitIndex}
canMoveItem={canMoveItem(subsection.childInfo.children)}
savingStatus={savingStatus}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateUnitSubmit}
getTitleLink={getUnitUrl}
onOrderChange={updateUnitOrderByIndex(
sectionIndex,
subsectionIndex,
section,
subsection,
subsection.childInfo.children,
)}
/>
))}
</DraggableList>

View File

@@ -686,6 +686,241 @@ describe('<CourseOutline />', () => {
});
});
it('check whether section move up and down options work correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section element
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
const [, secondSection] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
// mock api call
axiosMock
.onPut(getCourseBlockApiUrl(courseBlockId))
.reply(200, { dummy: 'value' });
// find menu button and click on it to open menu
const menu = await within(sectionElement).findByTestId('section-card-header__menu-button');
fireEvent.click(menu);
// move second section to first position to test move up option
const moveUpButton = await within(sectionElement).findByTestId('section-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstSectionId = store.getState().courseOutline.sectionsList[0].id;
expect(secondSection.id).toBe(firstSectionId);
// move first section back to second position to test move down option
const moveDownButton = await within(sectionElement).findByTestId('section-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const newSecondSectionId = store.getState().courseOutline.sectionsList[1].id;
expect(secondSection.id).toBe(newSecondSectionId);
});
it('check whether section move up & down option is rendered correctly based on index', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get first, second and last section element
const {
0: firstSection, 1: secondSection, length, [length - 1]: lastSection,
} = await findAllByTestId('section-card');
// find menu button and click on it to open menu in first section
const firstMenu = await within(firstSection).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(firstMenu));
// move down option should be enabled in first element
expect(
await within(firstSection).findByTestId('section-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
// move up option should not be enabled in first element
expect(
await within(firstSection).findByTestId('section-card-header__menu-move-up-button'),
).toHaveAttribute('aria-disabled', 'true');
// find menu button and click on it to open menu in second section
const secondMenu = await within(secondSection).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(secondMenu));
// both move down & up option should be enabled in second element
expect(
await within(secondSection).findByTestId('section-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
expect(
await within(secondSection).findByTestId('section-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
// find menu button and click on it to open menu in last section
const lastMenu = await within(lastSection).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(lastMenu));
// move down option should not be enabled in last element
expect(
await within(lastSection).findByTestId('section-card-header__menu-move-down-button'),
).toHaveAttribute('aria-disabled', 'true');
// move up option should be enabled in last element
expect(
await within(lastSection).findByTestId('section-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
});
it('check whether subsection move up and down options work correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section element
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const [, secondSubsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(store.getState().courseOutline.sectionsList[0].id))
.reply(200, { dummy: 'value' });
// find menu button and click on it to open menu
const menu = await within(subsectionElement).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move second subsection to first position to test move up option
const moveUpButton = await within(subsectionElement).findByTestId('subsection-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstSubsectionId = store.getState().courseOutline.sectionsList[0].childInfo.children[0].id;
expect(secondSubsection.id).toBe(firstSubsectionId);
// move first section back to second position to test move down option
const moveDownButton = await within(subsectionElement).findByTestId('subsection-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const secondSubsectionId = store.getState().courseOutline.sectionsList[0].childInfo.children[1].id;
expect(secondSubsection.id).toBe(secondSubsectionId);
});
it('check whether subsection move up & down option is rendered correctly based on index', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// using second section as second section in mock has 3 subsections
const [, sectionElement] = await findAllByTestId('section-card');
// get first, second and last subsection element
const {
0: firstSubsection,
1: secondSubsection,
length,
[length - 1]: lastSubsection,
} = await within(sectionElement).findAllByTestId('subsection-card');
// find menu button and click on it to open menu in first section
const firstMenu = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(firstMenu));
// move down option should be enabled in first element
expect(
await within(firstSubsection).findByTestId('subsection-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
// move up option should not be enabled in first element
expect(
await within(firstSubsection).findByTestId('subsection-card-header__menu-move-up-button'),
).toHaveAttribute('aria-disabled', 'true');
// find menu button and click on it to open menu in second section
const secondMenu = await within(secondSubsection).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(secondMenu));
// both move down & up option should be enabled in second element
expect(
await within(secondSubsection).findByTestId('subsection-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
expect(
await within(secondSubsection).findByTestId('subsection-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
// find menu button and click on it to open menu in last section
const lastMenu = await within(lastSubsection).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(lastMenu));
// move down option should not be enabled in last element
expect(
await within(lastSubsection).findByTestId('subsection-card-header__menu-move-down-button'),
).toHaveAttribute('aria-disabled', 'true');
// move up option should be enabled in last element
expect(
await within(lastSubsection).findByTestId('subsection-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
});
it('check whether unit move up and down options work correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section -> second subsection -> second unit element
const [, section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
const [, subsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [, secondUnit] = subsection.childInfo.children;
const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(store.getState().courseOutline.sectionsList[1].childInfo.children[1].id))
.reply(200, { dummy: 'value' });
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move second unit to first position to test move up option
const moveUpButton = await within(unitElement).findByTestId('unit-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstUnitId = store.getState().courseOutline.sectionsList[1].childInfo.children[1].childInfo.children[0].id;
expect(secondUnit.id).toBe(firstUnitId);
// move first unit back to second position to test move down option
const moveDownButton = await within(subsectionElement).findByTestId('unit-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const secondUnitId = store.getState().courseOutline.sectionsList[1].childInfo.children[1].childInfo.children[1].id;
expect(secondUnit.id).toBe(secondUnitId);
});
it('check whether unit move up & down option is rendered correctly based on index', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// using second section -> second subsection as it has 5 units in mock.
const [, sectionElement] = await findAllByTestId('section-card');
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
// get first, second and last unit element
const {
0: firstUnit,
1: secondUnit,
length,
[length - 1]: lastUnit,
} = await within(subsectionElement).findAllByTestId('unit-card');
// find menu button and click on it to open menu in first section
const firstMenu = await within(firstUnit).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(firstMenu));
// move down option should be enabled in first element
expect(
await within(firstUnit).findByTestId('unit-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
// move up option should not be enabled in first element
expect(
await within(firstUnit).findByTestId('unit-card-header__menu-move-up-button'),
).toHaveAttribute('aria-disabled', 'true');
// find menu button and click on it to open menu in second section
const secondMenu = await within(secondUnit).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(secondMenu));
// both move down & up option should be enabled in second element
expect(
await within(secondUnit).findByTestId('unit-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
expect(
await within(secondUnit).findByTestId('unit-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
// find menu button and click on it to open menu in last section
const lastMenu = await within(lastUnit).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(lastMenu));
// move down option should not be enabled in last element
expect(
await within(lastUnit).findByTestId('unit-card-header__menu-move-down-button'),
).toHaveAttribute('aria-disabled', 'true');
// move up option should be enabled in last element
expect(
await within(lastUnit).findByTestId('unit-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
});
it('check that new section list is saved when dragged', async () => {
const { findAllByRole } = render(<RootWrapper />);
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
@@ -865,10 +1100,12 @@ describe('<CourseOutline />', () => {
},
},
});
const { queryByTestId } = render(<RootWrapper />);
const { findAllByTestId } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
const [sectionElement] = await findAllByTestId('conditional-sortable-element--no-drag-handle')
await waitFor(() => {
expect(queryByTestId('conditional-sortable-element--no-drag-handle')).toBeInTheDocument();
expect(within(sectionElement).queryByText(section.displayName)).toBeInTheDocument();
});
});
});

View File

@@ -30,6 +30,8 @@ const CardHeader = ({
isDisabledEditField,
onClickDelete,
onClickDuplicate,
onClickMoveUp,
onClickMoveDown,
titleComponent,
namePrefix,
actions,
@@ -112,8 +114,27 @@ const CardHeader = ({
{intl.formatMessage(messages.menuDuplicate)}
</Dropdown.Item>
)}
{actions.draggable && (
<>
<Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-move-up-button`}
onClick={onClickMoveUp}
disabled={!actions.allowMoveUp}
>
{intl.formatMessage(messages.menuMoveUp)}
</Dropdown.Item>
<Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-move-down-button`}
onClick={onClickMoveDown}
disabled={!actions.allowMoveDown}
>
{intl.formatMessage(messages.menuMoveDown)}
</Dropdown.Item>
</>
)}
{actions.deletable && (
<Dropdown.Item
className="border-top border-light"
data-testid={`${namePrefix}-card-header__menu-delete-button`}
onClick={onClickDelete}
>
@@ -141,6 +162,8 @@ CardHeader.propTypes = {
isDisabledEditField: PropTypes.bool.isRequired,
onClickDelete: PropTypes.func.isRequired,
onClickDuplicate: PropTypes.func.isRequired,
onClickMoveUp: PropTypes.func.isRequired,
onClickMoveDown: PropTypes.func.isRequired,
titleComponent: PropTypes.node.isRequired,
namePrefix: PropTypes.string.isRequired,
actions: PropTypes.shape({
@@ -148,6 +171,8 @@ CardHeader.propTypes = {
draggable: PropTypes.bool.isRequired,
childAddable: PropTypes.bool.isRequired,
duplicable: PropTypes.bool.isRequired,
allowMoveUp: PropTypes.bool,
allowMoveDown: PropTypes.bool,
}).isRequired,
};

View File

@@ -41,6 +41,14 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.card.menu.duplicate',
defaultMessage: 'Duplicate',
},
menuMoveUp: {
id: 'course-authoring.course-outline.card.menu.moveup',
defaultMessage: 'Move up',
},
menuMoveDown: {
id: 'course-authoring.course-outline.card.menu.movedown',
defaultMessage: 'Move down',
},
menuDelete: {
id: 'course-authoring.course-outline.card.menu.delete',
defaultMessage: 'Delete',

View File

@@ -10,6 +10,7 @@ import {
import { useSelector } from 'react-redux';
import { getCurrentItem } from '../data/selectors';
import { COURSE_BLOCK_NAMES } from '../constants';
import messages from './messages';
const PublishModal = ({
@@ -19,6 +20,7 @@ const PublishModal = ({
}) => {
const intl = useIntl();
const { displayName, childInfo, category } = useSelector(getCurrentItem);
const categoryName = COURSE_BLOCK_NAMES[category]?.name.toLowerCase();
const children = childInfo?.children || [];
return (
@@ -35,7 +37,9 @@ const PublishModal = ({
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<p className="small">{intl.formatMessage(messages.description, { category })}</p>
<p className="small">
{intl.formatMessage(messages.description, { category: categoryName })}
</p>
{children.filter(child => child.hasChanges).map((child) => {
let grandChildren = child.childInfo?.children || [];
grandChildren = grandChildren.filter(grandChild => grandChild.hasChanges);

View File

@@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Badge, Button, useToggle } from '@edx/paragon';
import { Add as IconAdd } from '@edx/paragon/icons';
import classNames from 'classnames';
import { setCurrentItem, setCurrentSection } from '../data/slice';
import { RequestStatus } from '../../data/constants';
@@ -19,6 +20,8 @@ import messages from './messages';
const SectionCard = ({
section,
children,
index,
canMoveItem,
onOpenHighlightsModal,
onOpenPublishModal,
onOpenConfigureModal,
@@ -28,6 +31,7 @@ const SectionCard = ({
onDuplicateSubmit,
isSectionsExpanded,
onNewSubsectionSubmit,
onOrderChange,
}) => {
const currentRef = useRef(null);
const intl = useIntl();
@@ -54,11 +58,17 @@ const SectionCard = ({
published,
visibilityState,
highlights,
actions,
actions: sectionActions,
isHeaderVisible = true,
explanatoryMessage = '',
} = section;
// re-create actions object for customizations
const actions = { ...sectionActions };
// add actions to control display of move up & down menu buton.
actions.allowMoveUp = canMoveItem(index, -1);
actions.allowMoveDown = canMoveItem(index, 1);
const sectionStatus = getItemStatus({
published,
visibilityState,
@@ -95,6 +105,14 @@ const SectionCard = ({
onNewSubsectionSubmit(id);
};
const handleSectionMoveUp = () => {
onOrderChange(index, index - 1);
};
const handleSectionMoveDown = () => {
onOrderChange(index, index + 1);
};
useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
closeForm();
@@ -115,10 +133,12 @@ const SectionCard = ({
</TitleButton>
);
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
return (
<ConditionalSortableElement
id={id}
draggable={actions.draggable}
draggable={isDraggable}
componentStyle={{
padding: '1.75rem',
...borderStyle,
@@ -141,6 +161,8 @@ const SectionCard = ({
onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
onClickMoveUp={handleSectionMoveUp}
onClickMoveDown={handleSectionMoveDown}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
@@ -166,7 +188,10 @@ const SectionCard = ({
</div>
</div>
{isExpanded && (
<div data-testid="section-card__subsections" className="item-children section-card__subsections">
<div
data-testid="section-card__subsections"
className={classNames('section-card__subsections', { 'item-children': isDraggable })}
>
{children}
{actions.childAddable && (
<Button
@@ -220,6 +245,9 @@ SectionCard.propTypes = {
onDuplicateSubmit: PropTypes.func.isRequired,
isSectionsExpanded: PropTypes.bool.isRequired,
onNewSubsectionSubmit: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
canMoveItem: PropTypes.func.isRequired,
onOrderChange: PropTypes.func.isRequired,
};
export default SectionCard;

View File

@@ -38,6 +38,9 @@ const renderComponent = (props) => render(
<IntlProvider locale="en">
<SectionCard
section={section}
index="1"
canMoveItem={jest.fn()}
onOrderChange={jest.fn()}
onOpenPublishModal={jest.fn()}
onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()}

View File

@@ -4,6 +4,7 @@ import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, useToggle } from '@edx/paragon';
import { Add as IconAdd } from '@edx/paragon/icons';
import classNames from 'classnames';
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
import { RequestStatus } from '../../data/constants';
@@ -18,12 +19,15 @@ const SubsectionCard = ({
section,
subsection,
children,
index,
canMoveItem,
onOpenPublishModal,
onEditSubmit,
savingStatus,
onOpenDeleteModal,
onDuplicateSubmit,
onNewUnitSubmit,
onOrderChange,
}) => {
const currentRef = useRef(null);
const intl = useIntl();
@@ -37,10 +41,16 @@ const SubsectionCard = ({
hasChanges,
published,
visibilityState,
actions,
actions: subsectionActions,
isHeaderVisible = true,
} = subsection;
// re-create actions object for customizations
const actions = { ...subsectionActions };
// add actions to control display of move up & down menu buton.
actions.allowMoveUp = canMoveItem(index, -1);
actions.allowMoveDown = canMoveItem(index, 1);
const [isExpanded, setIsExpanded] = useState(!isHeaderVisible);
const subsectionStatus = getItemStatus({
published,
@@ -68,6 +78,14 @@ const SubsectionCard = ({
closeForm();
};
const handleSubsectionMoveUp = () => {
onOrderChange(index, index - 1);
};
const handleSubsectionMoveDown = () => {
onOrderChange(index, index + 1);
};
const handleNewButtonClick = () => onNewUnitSubmit(id);
const titleComponent = (
@@ -99,13 +117,17 @@ const SubsectionCard = ({
}
}, [savingStatus]);
const isDraggable = (
actions.draggable
&& (actions.allowMoveUp || actions.allowMoveDown)
&& !(isHeaderVisible === false)
);
return (
<ConditionalSortableElement
id={id}
key={id}
draggable={
actions.draggable && !(isHeaderVisible === false)
}
draggable={isDraggable}
componentStyle={{
background: '#f8f7f6',
...borderStyle,
@@ -121,6 +143,8 @@ const SubsectionCard = ({
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
onClickMoveUp={handleSubsectionMoveUp}
onClickMoveDown={handleSubsectionMoveDown}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
@@ -132,7 +156,10 @@ const SubsectionCard = ({
/>
)}
{isExpanded && (
<div data-testid="subsection-card__units" className="item-children subsection-card__units">
<div
data-testid="subsection-card__units"
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
>
{children}
{actions.childAddable && (
<Button
@@ -188,6 +215,9 @@ SubsectionCard.propTypes = {
onOpenDeleteModal: PropTypes.func.isRequired,
onDuplicateSubmit: PropTypes.func.isRequired,
onNewUnitSubmit: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
canMoveItem: PropTypes.func.isRequired,
onOrderChange: PropTypes.func.isRequired,
};
export default SubsectionCard;

View File

@@ -48,6 +48,9 @@ const renderComponent = (props) => render(
<SubsectionCard
section={section}
subsection={subsection}
index="1"
canMoveItem={jest.fn()}
onOrderChange={jest.fn()}
onOpenPublishModal={jest.fn()}
onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()}

View File

@@ -15,12 +15,15 @@ const UnitCard = ({
unit,
subsection,
section,
index,
canMoveItem,
onOpenPublishModal,
onEditSubmit,
savingStatus,
onOpenDeleteModal,
onDuplicateSubmit,
getTitleLink,
onOrderChange,
}) => {
const currentRef = useRef(null);
const dispatch = useDispatch();
@@ -33,10 +36,16 @@ const UnitCard = ({
hasChanges,
published,
visibilityState,
actions,
actions: unitActions,
isHeaderVisible = true,
} = unit;
// re-create actions object for customizations
const actions = { ...unitActions };
// add actions to control display of move up & down menu buton.
actions.allowMoveUp = canMoveItem(index, -1);
actions.allowMoveDown = canMoveItem(index, 1);
const unitStatus = getItemStatus({
published,
visibilityState,
@@ -59,6 +68,14 @@ const UnitCard = ({
closeForm();
};
const handleUnitMoveUp = () => {
onOrderChange(index, index - 1);
};
const handleUnitMoveDown = () => {
onOrderChange(index, index + 1);
};
const titleComponent = (
<TitleLink
titleLink={getTitleLink(id)}
@@ -88,17 +105,16 @@ const UnitCard = ({
}, [savingStatus]);
if (!isHeaderVisible) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
return null;
}
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
return (
<ConditionalSortableElement
id={id}
key={id}
draggable={
actions.draggable && !(isHeaderVisible === false)
}
draggable={isDraggable}
componentStyle={{
background: '#fdfdfd',
...borderStyle,
@@ -117,6 +133,8 @@ const UnitCard = ({
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
onClickMoveUp={handleUnitMoveUp}
onClickMoveDown={handleUnitMoveDown}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
@@ -169,6 +187,9 @@ UnitCard.propTypes = {
onOpenDeleteModal: PropTypes.func.isRequired,
onDuplicateSubmit: PropTypes.func.isRequired,
getTitleLink: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
canMoveItem: PropTypes.func.isRequired,
onOrderChange: PropTypes.func.isRequired,
};
export default UnitCard;

View File

@@ -54,6 +54,9 @@ const renderComponent = (props) => render(
section={section}
subsection={subsection}
unit={unit}
index="1"
canMoveItem={jest.fn()}
onOrderChange={jest.fn()}
onOpenPublishModal={jest.fn()}
onOpenDeleteModal={jest.fn()}
savingStatus=""