Skip to content

Commit 69f03ca

Browse files
committed
feat: Enable menus in unit info sidebar in the unit page
1 parent df90eec commit 69f03ca

4 files changed

Lines changed: 120 additions & 10 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useContext, useState } from 'react';
22
import { isEmpty } from 'lodash';
33

44
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -26,6 +26,7 @@ import { PublishButon } from './PublishButon';
2626
import messages from '../messages';
2727
import { InfoSection } from './InfoSection';
2828
import { useClipboard } from '@src/generic/clipboard';
29+
import { ToastContext } from '@src/generic/toast-context';
2930

3031
export const UnitSidebar = () => {
3132
const intl = useIntl();
@@ -48,6 +49,7 @@ export const UnitSidebar = () => {
4849
openUnlinkModal,
4950
} = useCourseAuthoringContext();
5051
const { copyToClipboard } = useClipboard();
52+
const { showToast } = useContext(ToastContext);
5153

5254
const handlePublish = () => {
5355
if (unitData?.hasChanges) {
@@ -124,7 +126,9 @@ export const UnitSidebar = () => {
124126
// Extract the location ID: the part after "block@" at the end of the usage key
125127
// e.g. "block-v1:org+course+run+type@vertical+block@abc123" → "abc123"
126128
const locationId = unitId.match(/block@(.+)$/)?.[1];
127-
if (!locationId) { return; }
129+
if (!locationId) {
130+
return;
131+
}
128132

129133
if (navigator.clipboard) {
130134
// Modern approach: requires HTTPS (secure context)
@@ -139,6 +143,7 @@ export const UnitSidebar = () => {
139143
document.execCommand('copy'); // eslint-disable-line deprecation/deprecation
140144
document.body.removeChild(textarea);
141145
}
146+
showToast(intl.formatMessage(messages.locationCopiedText));
142147
};
143148

144149
return (

src/course-outline/outline-sidebar/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ const messages = defineMessages({
175175
defaultMessage: '{name} is a library {category}. Content cannot be added to Library referenced {category}s.',
176176
description: 'Alert displayed in sidebar when author tries to add content in library referenced blocks',
177177
},
178+
locationCopiedText: {
179+
id: 'course-authoring.course-outline.sidebar.unit.copied-location',
180+
defaultMessage: 'Location ID saved in the Clipboard',
181+
description: 'Toast messages when the user copied an unit location ID',
182+
},
178183
});
179184

180185
export default messages;

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

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
11
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
2-
import { useParams } from 'react-router-dom';
2+
import { useNavigate, useParams } from 'react-router-dom';
33
import { ComponentCountSnippet, getItemIcon } from '@src/generic/block-type-utils';
44
import { SidebarContent, SidebarSection, SidebarTitle } from '@src/generic/sidebar';
5-
import { useEffect, useMemo } from 'react';
5+
import { useContext, useEffect, useMemo } from 'react';
66
import { Tag } from '@openedx/paragon/icons';
77
import { ContentTagsSnippet } from '@src/content-tags-drawer';
88
import configureMessages from '@src/generic/configure-modal/messages';
99
import {
1010
Button, ButtonGroup, Tab, Tabs,
1111
} from '@openedx/paragon';
12+
import { useToggle } from '@openedx/paragon';
1213
import { useDispatch, useSelector } from 'react-redux';
1314
import { useIframe } from '@src/generic/hooks/context/hooks';
1415
import { AccessEditComponent, DiscussionEditComponent } from '@src/generic/configure-modal/UnitTab';
1516
import { Form, Formik } from 'formik';
1617
import { getCourseUnitData, getCourseVerticalChildren } from '@src/course-unit/data/selectors';
1718
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from '@src/course-unit/constants';
18-
import { editCourseUnitVisibilityAndData } from '@src/course-unit/data/thunk';
19+
import { editCourseUnitVisibilityAndData, fetchCourseSectionVerticalData, fetchCourseVerticalChildrenData } from '@src/course-unit/data/thunk';
1920
import PublishControls from './PublishControls';
2021
import { useUnitSidebarContext } from '../UnitSidebarContext';
2122
import messages from './messages';
23+
import { getLibraryId } from '@src/generic/key-utils';
24+
import { useClipboard } from '@src/generic/clipboard';
25+
import { ToastContext } from '@src/generic/toast-context';
26+
import { UnlinkModal } from '@src/generic/unlink-modal';
27+
import { useUnlinkDownstream } from '@src/generic/unlink-modal/data/apiHooks';
28+
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
29+
import { useQueryClient } from '@tanstack/react-query';
30+
import { courseOutlineQueryKeys, useDeleteCourseItem } from '@src/course-outline/data/apiHooks';
31+
import DeleteModal from '@src/generic/delete-modal/DeleteModal';
2232

2333
/**
2434
* Component to show unit details: Publish status, Component counts and Content Tags.
@@ -193,17 +203,83 @@ const UnitInfoSettings = () => {
193203
*/
194204
export const UnitInfoSidebar = () => {
195205
const intl = useIntl();
206+
const navigate = useNavigate();
207+
const dispatch = useDispatch();
208+
const { copyToClipboard } = useClipboard();
196209
const currentItemData = useSelector(getCourseUnitData);
197210
const {
198211
currentTabKey,
199212
setCurrentTabKey,
200213
} = useUnitSidebarContext();
214+
const { showToast } = useContext(ToastContext);
215+
const { courseId } = useCourseAuthoringContext();
216+
217+
const [isUnlinkModalOpen, openUnlinkModal, closeUnlinkModal] = useToggle(false);
218+
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
219+
const { mutateAsync: unlinkDownstream } = useUnlinkDownstream();
220+
const { mutateAsync: deleteCourseItem } = useDeleteCourseItem();
221+
const queryClient = useQueryClient();
222+
223+
const sequenceId = currentItemData?.ancestorInfo?.ancestors?.[0]?.id;
224+
const sectionId = currentItemData?.ancestorInfo?.ancestors?.[1]?.id;
225+
226+
const handleDeleteSubmit = async () => {
227+
await deleteCourseItem({
228+
itemId: currentItemData.id,
229+
subsectionId: sequenceId,
230+
sectionId,
231+
}, {
232+
onSuccess: () => {
233+
closeDeleteModal();
234+
navigate(`/course/${courseId}`);
235+
},
236+
});
237+
};
238+
239+
const handleUnlinkSubmit = async () => {
240+
await unlinkDownstream({
241+
downstreamBlockId: currentItemData.id,
242+
subsectionId: sequenceId,
243+
sectionId,
244+
}, {
245+
onSuccess: () => {
246+
closeUnlinkModal();
247+
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(currentItemData.id) });
248+
dispatch(fetchCourseSectionVerticalData(currentItemData.id, sequenceId));
249+
dispatch(fetchCourseVerticalChildrenData(currentItemData.id, false));
250+
},
251+
});
252+
};
201253

202254
useEffect(() => {
203255
// Set default Tab key
204256
setCurrentTabKey('details');
205257
}, []);
206258

259+
const handleCopyLocation = () => {
260+
// Extract the location ID: the part after "block@" at the end of the usage key
261+
// e.g. "block-v1:org+course+run+type@vertical+block@abc123" → "abc123"
262+
const locationId = currentItemData.id.match(/block@(.+)$/)?.[1];
263+
if (!locationId) {
264+
return;
265+
}
266+
267+
if (navigator.clipboard) {
268+
// Modern approach: requires HTTPS (secure context)
269+
void navigator.clipboard.writeText(locationId);
270+
} else {
271+
// Fallback for HTTP (non-secure) dev environments
272+
// Note: execCommand is deprecated but still widely supported as fallback
273+
const textarea = document.createElement('textarea');
274+
textarea.value = locationId;
275+
document.body.appendChild(textarea);
276+
textarea.select();
277+
document.execCommand('copy'); // eslint-disable-line deprecation/deprecation
278+
document.body.removeChild(textarea);
279+
}
280+
showToast(intl.formatMessage(messages.locationCopiedText));
281+
};
282+
207283
return (
208284
<>
209285
<SidebarTitle
@@ -212,11 +288,17 @@ export const UnitInfoSidebar = () => {
212288
menuProps={{
213289
itemId: currentItemData.id,
214290
index: -1,
215-
onClickUnlink: () => {},
216-
onClickDelete: () => {},
217-
onClickViewLibrary: () => {},
218-
onClickCopy: () => {},
219-
onClickCopyLocation: () => {},
291+
onClickUnlink: openUnlinkModal,
292+
onClickDelete: openDeleteModal,
293+
onClickViewLibrary: () => {
294+
const upstreamRef = currentItemData?.upstreamInfo?.upstreamRef;
295+
if (upstreamRef) {
296+
const libId = getLibraryId(upstreamRef);
297+
navigate(`/library/${libId}/unit/${upstreamRef}`);
298+
}
299+
},
300+
onClickCopy: () => copyToClipboard(currentItemData.id),
301+
onClickCopyLocation: handleCopyLocation,
220302
}}
221303
/>
222304
<Tabs
@@ -242,6 +324,19 @@ export const UnitInfoSidebar = () => {
242324
</div>
243325
</Tab>
244326
</Tabs>
327+
<DeleteModal
328+
isOpen={isDeleteModalOpen}
329+
close={closeDeleteModal}
330+
onDeleteSubmit={handleDeleteSubmit}
331+
category="unit"
332+
/>
333+
<UnlinkModal
334+
isOpen={isUnlinkModalOpen}
335+
close={closeUnlinkModal}
336+
onUnlinkSubmit={handleUnlinkSubmit}
337+
displayName={currentItemData.displayName}
338+
category="vertical"
339+
/>
245340
</>
246341
);
247342
};

src/course-unit/unit-sidebar/unit-info/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ const messages = defineMessages({
134134
defaultMessage: 'Settings',
135135
description: 'Label for the settings tab of the unit info sidebar',
136136
},
137+
locationCopiedText: {
138+
id: 'course-authoring.unit-page.sidebar.info.copied-location',
139+
defaultMessage: 'Location ID saved in the Clipboard',
140+
description: 'Toast messages when the user copied an unit location ID',
141+
},
137142
});
138143

139144
export default messages;

0 commit comments

Comments
 (0)