11import { FormattedMessage , useIntl } from '@edx/frontend-platform/i18n' ;
2- import { useParams } from 'react-router-dom' ;
2+ import { useNavigate , useParams } from 'react-router-dom' ;
33import { ComponentCountSnippet , getItemIcon } from '@src/generic/block-type-utils' ;
44import { SidebarContent , SidebarSection , SidebarTitle } from '@src/generic/sidebar' ;
5- import { useEffect , useMemo } from 'react' ;
5+ import { useContext , useEffect , useMemo } from 'react' ;
66import { Tag } from '@openedx/paragon/icons' ;
77import { ContentTagsSnippet } from '@src/content-tags-drawer' ;
88import configureMessages from '@src/generic/configure-modal/messages' ;
99import {
1010 Button , ButtonGroup , Tab , Tabs ,
1111} from '@openedx/paragon' ;
12+ import { useToggle } from '@openedx/paragon' ;
1213import { useDispatch , useSelector } from 'react-redux' ;
1314import { useIframe } from '@src/generic/hooks/context/hooks' ;
1415import { AccessEditComponent , DiscussionEditComponent } from '@src/generic/configure-modal/UnitTab' ;
1516import { Form , Formik } from 'formik' ;
1617import { getCourseUnitData , getCourseVerticalChildren } from '@src/course-unit/data/selectors' ;
1718import { 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' ;
1920import PublishControls from './PublishControls' ;
2021import { useUnitSidebarContext } from '../UnitSidebarContext' ;
2122import 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 */
194204export 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 ( / b l o c k @ ( .+ ) $ / ) ?. [ 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} ;
0 commit comments