Skip to content

Commit 0fd1d2e

Browse files
committed
feat: Enable move in unit sidebar
1 parent db4e71a commit 0fd1d2e

12 files changed

Lines changed: 145 additions & 79 deletions

File tree

src/CourseAuthoringContext.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { CourseDetailsData } from './data/api';
1313
import { useCourseDetails, useWaffleFlags } from './data/apiHooks';
1414
import { RequestStatusType } from './data/constants';
1515
import { arrayMove } from '@dnd-kit/sortable';
16-
import { fetchCourseOutlineIndexQuery, setSectionOrderListQuery, setSubsectionOrderListQuery } from './course-outline/data/thunk';
16+
import { fetchCourseOutlineIndexQuery, setSectionOrderListQuery, setSubsectionOrderListQuery, setUnitOrderListQuery } from './course-outline/data/thunk';
1717

1818
type ModalState = {
1919
value?: XBlock | UnitXBlock;
@@ -51,8 +51,10 @@ export type CourseAuthoringContextData = {
5151
handleDuplicateUnitSubmit: () => void;
5252
handleSectionDragAndDrop: (sectionListIds: string[]) => void;
5353
handleSubsectionDragAndDrop: (sectionId: string, prevSectionId: string, subsectionListIds: string[]) => void;
54+
handleUnitDragAndDrop: (sectionId: string, prevSectionId: string, subsectionId: string, unitListIds: string[]) => void;
5455
updateSectionOrderByIndex: (currentIndex: number, newIndex: number) => void;
5556
updateSubsectionOrderByIndex: (section: XBlock, moveDetails: any) => void;
57+
updateUnitOrderByIndex: (section: XBlock, moveDetails: any) => void;
5658
};
5759

5860
/**
@@ -202,6 +204,41 @@ export const CourseAuthoringProvider = ({
202204
));
203205
};
204206

207+
const handleUnitDragAndDrop = (
208+
sectionId: string,
209+
prevSectionId: string,
210+
subsectionId: string,
211+
unitListIds: string[],
212+
) => {
213+
dispatch(setUnitOrderListQuery(
214+
sectionId,
215+
subsectionId,
216+
prevSectionId,
217+
unitListIds,
218+
restoreSectionList,
219+
));
220+
};
221+
222+
/**
223+
* Uses details from move information and moves unit
224+
*/
225+
const updateUnitOrderByIndex = (section: XBlock, moveDetails) => {
226+
const { fn, args, sectionId, subsectionId } = moveDetails;
227+
if (!args) {
228+
return;
229+
}
230+
const [sectionsCopy, newUnits] = fn(...args);
231+
if (newUnits && subsectionId) {
232+
setSections(sectionsCopy);
233+
handleUnitDragAndDrop(
234+
sectionId,
235+
section.id,
236+
subsectionId,
237+
newUnits.map((unit) => unit.id),
238+
);
239+
}
240+
};
241+
205242
/**
206243
* Move section to new index
207244
*/
@@ -264,8 +301,10 @@ export const CourseAuthoringProvider = ({
264301
handleDuplicateUnitSubmit,
265302
handleSectionDragAndDrop,
266303
handleSubsectionDragAndDrop,
304+
handleUnitDragAndDrop,
267305
updateSectionOrderByIndex,
268306
updateSubsectionOrderByIndex,
307+
updateUnitOrderByIndex,
269308
}), [
270309
courseId,
271310
courseUsageKey,
@@ -294,8 +333,10 @@ export const CourseAuthoringProvider = ({
294333
handleDuplicateSubsectionSubmit,
295334
handleSectionDragAndDrop,
296335
handleSubsectionDragAndDrop,
336+
handleUnitDragAndDrop,
297337
updateSectionOrderByIndex,
298338
updateSubsectionOrderByIndex,
339+
updateUnitOrderByIndex,
299340
]);
300341

301342
return (

src/course-outline/CourseOutline.tsx

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const CourseOutline = () => {
8080
setSections,
8181
updateSectionOrderByIndex,
8282
updateSubsectionOrderByIndex,
83+
updateUnitOrderByIndex,
8384
} = useCourseAuthoringContext();
8485

8586
const {
@@ -172,29 +173,6 @@ const CourseOutline = () => {
172173
const enableProctoredExams = useSelector(getProctoredExamsFlag);
173174
const enableTimedExams = useSelector(getTimedExamsFlag);
174175

175-
/**
176-
* Uses details from move information and moves unit
177-
*/
178-
const updateUnitOrderByIndex = (section: XBlock, moveDetails) => {
179-
const {
180-
fn, args, sectionId, subsectionId,
181-
} = moveDetails;
182-
if (!args) {
183-
return;
184-
}
185-
const [sectionsCopy, newUnits] = fn(...args);
186-
if (newUnits && sectionId && subsectionId) {
187-
setSections(sectionsCopy);
188-
handleUnitDragAndDrop(
189-
sectionId,
190-
section.id,
191-
subsectionId,
192-
newUnits.map(unit => unit.id),
193-
restoreSectionList,
194-
);
195-
}
196-
};
197-
198176
if (isLoading) {
199177
// eslint-disable-next-line react/jsx-no-useless-fragment
200178
return (
@@ -386,6 +364,7 @@ const CourseOutline = () => {
386364
unit={unit}
387365
subsection={subsection}
388366
section={section}
367+
sectionIndex={sectionIndex}
389368
isSelfPaced={statusBarData.isSelfPaced}
390369
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
391370
index={unitIndex}

src/course-outline/hooks.jsx

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import {
4848
setSectionOrderListQuery,
4949
setVideoSharingOptionQuery,
5050
setSubsectionOrderListQuery,
51-
setUnitOrderListQuery,
5251
dismissNotificationQuery,
5352
syncDiscussionsTopics,
5453
} from './data/thunk';
@@ -67,6 +66,7 @@ const useCourseOutline = ({ courseId }) => {
6766
handleDuplicateUnitSubmit,
6867
handleSectionDragAndDrop,
6968
handleSubsectionDragAndDrop,
69+
handleUnitDragAndDrop,
7070
} = useCourseAuthoringContext();
7171
const { selectedContainerState, clearSelection } = useOutlineSidebarContext();
7272

@@ -309,22 +309,6 @@ const useCourseOutline = ({ courseId }) => {
309309
dispatch(dismissNotificationQuery(`${getConfig().STUDIO_BASE_URL}${notificationDismissUrl}`));
310310
};
311311

312-
const handleUnitDragAndDrop = (
313-
sectionId,
314-
prevSectionId,
315-
subsectionId,
316-
unitListIds,
317-
restoreSectionList,
318-
) => {
319-
dispatch(setUnitOrderListQuery(
320-
sectionId,
321-
subsectionId,
322-
prevSectionId,
323-
unitListIds,
324-
restoreSectionList,
325-
));
326-
};
327-
328312
useEffect(() => {
329313
dispatch(fetchCourseOutlineIndexQuery(courseId));
330314
dispatch(fetchCourseBestPracticesQuery({ courseId }));

src/course-outline/outline-sidebar/info-sidebar/InfoSidebar.tsx

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,13 @@ export const InfoSidebar = () => {
1717
switch (itemType) {
1818
case ContainerType.Chapter:
1919
case ContainerType.Section:
20-
return (
21-
<SectionSidebar
22-
sectionId={selectedContainerState.currentId}
23-
index={selectedContainerState.index}
24-
/>
25-
);
20+
return <SectionSidebar />;
2621
case ContainerType.Sequential:
2722
case ContainerType.Subsection:
28-
return (
29-
<SubsectionSidebar
30-
subsectionId={selectedContainerState.currentId}
31-
index={selectedContainerState.index}
32-
sectionIndex={selectedContainerState.sectionIndex}
33-
/>
34-
);
23+
return <SubsectionSidebar />;
3524
case ContainerType.Vertical:
3625
case ContainerType.Unit:
37-
return (
38-
<UnitSidebar
39-
unitId={selectedContainerState.currentId}
40-
index={selectedContainerState.index}
41-
/>
42-
);
26+
return <UnitSidebar />;
4327
default:
4428
return <CourseInfoSidebar />;
4529
}

src/course-outline/outline-sidebar/info-sidebar/SectionInfoSidebar.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,18 @@ import messages from '../messages';
1515
import { PublishButon } from './PublishButon';
1616
import { canMoveSection } from '@src/course-outline/drag-helper/utils';
1717

18-
interface Props {
19-
sectionId: string;
20-
index?: number;
21-
}
22-
23-
export const SectionSidebar = ({ sectionId, index }: Props) => {
18+
export const SectionSidebar = () => {
2419
const intl = useIntl();
2520
const [tab, setTab] = useState<'info' | 'settings'>('info');
21+
const { clearSelection, selectedContainerState, setSelectedContainerState } = useOutlineSidebarContext();
22+
const { sectionId = '', index } = selectedContainerState ?? {};
2623
const { data: sectionData, isLoading } = useCourseItemData(sectionId);
2724
const {
2825
openPublishModal,
2926
handleDuplicateSectionSubmit,
3027
sections,
3128
updateSectionOrderByIndex,
3229
} = useCourseAuthoringContext();
33-
const { clearSelection, selectedContainerState, setSelectedContainerState } = useOutlineSidebarContext();
3430

3531
const handlePublish = () => {
3632
if (sectionData?.hasChanges) {

src/course-outline/outline-sidebar/info-sidebar/SubsectionInfoSidebar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,18 @@ interface Props {
2222
sectionIndex?: number;
2323
}
2424

25-
export const SubsectionSidebar = ({ subsectionId, index, sectionIndex }: Props) => {
25+
export const SubsectionSidebar = () => {
2626
const intl = useIntl();
2727
const [tab, setTab] = useState<'info' | 'settings'>('info');
28+
const { clearSelection, selectedContainerState, setSelectedContainerState } = useOutlineSidebarContext();
29+
const { subsectionId = '', index, sectionIndex } = selectedContainerState ?? {};
2830
const { data: subsectionData, isLoading } = useCourseItemData(subsectionId);
2931
const {
3032
openPublishModal,
3133
handleDuplicateSubsectionSubmit,
3234
sections,
3335
updateSubsectionOrderByIndex,
3436
} = useCourseAuthoringContext();
35-
const { clearSelection, selectedContainerState, setSelectedContainerState } = useOutlineSidebarContext();
3637

3738
const handlePublish = () => {
3839
if (selectedContainerState?.sectionId && subsectionData?.hasChanges) {

src/course-outline/outline-sidebar/info-sidebar/UnitInfoSidebar.tsx

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { useState } from 'react';
2+
import { isEmpty } from 'lodash';
3+
24
import { useIntl } from '@edx/frontend-platform/i18n';
35
import {
46
Button, Stack, Tab, Tabs,
@@ -17,26 +19,28 @@ import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
1719
import XBlockContainerIframe from '@src/course-unit/xblock-container-iframe';
1820
import { IframeProvider } from '@src/generic/hooks/context/iFrameContext';
1921
import { Link } from 'react-router-dom';
22+
import { possibleUnitMoves } from '@src/course-outline/drag-helper/utils';
2023
import { useOutlineSidebarContext } from '../OutlineSidebarContext';
2124
import { PublishButon } from './PublishButon';
2225
import messages from '../messages';
2326
import { InfoSection } from './InfoSection';
2427

25-
interface Props {
26-
unitId: string;
27-
index?: number
28-
}
29-
30-
export const UnitSidebar = ({ unitId, index }: Props) => {
28+
export const UnitSidebar = () => {
3129
const intl = useIntl();
3230
const [tab, setTab] = useState<'preview' | 'info' | 'settings'>('info');
31+
const { selectedContainerState, clearSelection, setSelectedContainerState } = useOutlineSidebarContext();
32+
const {
33+
currentId: unitId = '',
34+
index,
35+
} = selectedContainerState ?? {};
3336
const { data: unitData, isLoading } = useCourseItemData(unitId);
34-
const { selectedContainerState, clearSelection } = useOutlineSidebarContext();
3537
const {
3638
openPublishModal,
3739
getUnitUrl,
3840
courseId,
3941
handleDuplicateUnitSubmit,
42+
sections,
43+
updateUnitOrderByIndex,
4044
} = useCourseAuthoringContext();
4145

4246
const handlePublish = () => {
@@ -53,6 +57,63 @@ export const UnitSidebar = ({ unitId, index }: Props) => {
5357
return <Loading />;
5458
}
5559

60+
// Resolve section and subsection from selectedContainerState indices
61+
const sectionIndex = selectedContainerState?.sectionIndex;
62+
const section = sectionIndex !== undefined ? sections[sectionIndex] : undefined;
63+
const subsectionIndex = section && selectedContainerState?.subsectionId
64+
? section.childInfo.children.findIndex((s) => s.id === selectedContainerState.subsectionId)
65+
: -1;
66+
const subsection = subsectionIndex !== -1 ? section?.childInfo.children[subsectionIndex] : undefined;
67+
68+
// Build move calculator only when all ancestor context is available
69+
const getPossibleMoves = (section && subsection && subsectionIndex !== -1)
70+
? possibleUnitMoves(
71+
[...sections],
72+
sectionIndex ?? -1,
73+
subsectionIndex,
74+
section,
75+
subsection,
76+
subsection.childInfo.children,
77+
)
78+
: undefined;
79+
80+
const canMoveUnit = (oldIndex: number, step: number) => {
81+
if (getPossibleMoves) {
82+
const moveDetails = getPossibleMoves(oldIndex, step);
83+
return !isEmpty(moveDetails);
84+
}
85+
return false;
86+
};
87+
88+
const handleMove = (step: number) => {
89+
if (section && subsection && getPossibleMoves && index !== undefined && sectionIndex !== undefined) {
90+
const moveDetails = getPossibleMoves(index, step);
91+
// section is the current parent section (used as prevSection in cross-section moves)
92+
updateUnitOrderByIndex(section, moveDetails);
93+
if (!isEmpty(moveDetails)) {
94+
const newSectionId = moveDetails.sectionId;
95+
const newSubsectionId = moveDetails.subsectionId;
96+
// Cross-subsection move: unit goes to end of previous or start of next subsection
97+
const isCrossSubsection = newSubsectionId !== subsection.id;
98+
const newSectionIndex = newSectionId !== section.id
99+
? sections.findIndex((s) => s.id === newSectionId)
100+
: sectionIndex;
101+
const newIndex = isCrossSubsection
102+
? (step === -1
103+
? sections[newSectionIndex].childInfo.children.find((s) => s.id === newSubsectionId)?.childInfo.children.length ?? 0
104+
: 0)
105+
: index + step;
106+
setSelectedContainerState(selectedContainerState ? {
107+
...selectedContainerState,
108+
sectionId: newSectionId,
109+
sectionIndex: newSectionIndex,
110+
subsectionId: newSubsectionId,
111+
index: newIndex,
112+
} : undefined);
113+
}
114+
}
115+
};
116+
56117
return (
57118
<>
58119
<SidebarTitle
@@ -62,9 +123,10 @@ export const UnitSidebar = ({ unitId, index }: Props) => {
62123
menuProps={{
63124
itemId: unitId,
64125
index: index ?? -1,
126+
canMoveItem: canMoveUnit,
65127
onClickDuplicate: handleDuplicateUnitSubmit,
66-
onClickMoveUp: () => {},
67-
onClickMoveDown: () => {},
128+
onClickMoveUp: () => handleMove(-1),
129+
onClickMoveDown: () => handleMove(1),
68130
onClickUnlink: () => {},
69131
onClickDelete: () => {},
70132
onClickViewLibrary: () => {},

src/course-outline/section-card/SectionCard.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,10 +367,12 @@ describe('<SectionCard />', () => {
367367
expect(setCurrentSelection).toHaveBeenCalledWith({
368368
currentId: section.id,
369369
sectionId: section.id,
370+
index: 1,
370371
});
371372
expect(mockSetSelectedContainerState).toHaveBeenCalledWith({
372373
currentId: section.id,
373374
sectionId: section.id,
375+
index: 1,
374376
});
375377
});
376378
});

0 commit comments

Comments
 (0)