1
- // @ts -check
2
- import React , { useState , useEffect } from 'react' ;
3
- import PropTypes from 'prop-types' ;
1
+ import { useState , useEffect , useCallback } from 'react' ;
4
2
import { useIntl } from '@edx/frontend-platform/i18n' ;
5
3
import {
6
- Button ,
7
4
Container ,
8
5
Layout ,
9
6
Row ,
10
7
TransitionReplace ,
11
8
Toast ,
9
+ StandardModal ,
12
10
} from '@openedx/paragon' ;
13
11
import { Helmet } from 'react-helmet' ;
14
- import {
15
- Add as IconAdd ,
16
- CheckCircle as CheckCircleIcon ,
17
- } from '@openedx/paragon/icons' ;
12
+ import { CheckCircle as CheckCircleIcon } from '@openedx/paragon/icons' ;
18
13
import { useSelector } from 'react-redux' ;
19
14
import {
20
15
arrayMove ,
21
16
SortableContext ,
22
17
verticalListSortingStrategy ,
23
18
} from '@dnd-kit/sortable' ;
24
19
import { useLocation } from 'react-router-dom' ;
25
- import { CourseAuthoringOutlineSidebarSlot } from '.. /plugin-slots/CourseAuthoringOutlineSidebarSlot' ;
20
+ import { CourseAuthoringOutlineSidebarSlot } from '@src /plugin-slots/CourseAuthoringOutlineSidebarSlot' ;
26
21
27
- import { LoadingSpinner } from '../generic/Loading' ;
28
- import { getProcessingNotification } from '../generic/processing-notification/data/selectors' ;
29
- import { RequestStatus } from '../data/constants' ;
30
- import SubHeader from '../generic/sub-header/SubHeader' ;
31
- import ProcessingNotification from '../generic/processing-notification' ;
32
- import InternetConnectionAlert from '../generic/internet-connection-alert' ;
33
- import DeleteModal from '../generic/delete-modal/DeleteModal' ;
34
- import ConfigureModal from '../generic/configure-modal/ConfigureModal' ;
35
- import AlertMessage from '../generic/alert-message' ;
36
- import getPageHeadTitle from '../generic/utils' ;
22
+ import { LoadingSpinner } from '@src/generic/Loading' ;
23
+ import { getProcessingNotification } from '@src/generic/processing-notification/data/selectors' ;
24
+ import { RequestStatus } from '@src/data/constants' ;
25
+ import SubHeader from '@src/generic/sub-header/SubHeader' ;
26
+ import ProcessingNotification from '@src/generic/processing-notification' ;
27
+ import InternetConnectionAlert from '@src/generic/internet-connection-alert' ;
28
+ import DeleteModal from '@src/generic/delete-modal/DeleteModal' ;
29
+ import ConfigureModal from '@src/generic/configure-modal/ConfigureModal' ;
30
+ import AlertMessage from '@src/generic/alert-message' ;
31
+ import getPageHeadTitle from '@src/generic/utils' ;
32
+ import CourseOutlineHeaderActionsSlot from '@src/plugin-slots/CourseOutlineHeaderActionsSlot' ;
33
+ import { ContainerType } from '@src/generic/key-utils' ;
34
+ import { ComponentPicker , SelectedComponent } from '@src/library-authoring' ;
35
+ import { ContentType } from '@src/library-authoring/routes' ;
36
+ import { NOTIFICATION_MESSAGES } from '@src/constants' ;
37
+ import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants' ;
38
+ import { XBlock } from '@src/data/types' ;
37
39
import { getCurrentItem , getProctoredExamsFlag } from './data/selectors' ;
38
40
import { COURSE_BLOCK_NAMES } from './constants' ;
39
41
import StatusBar from './status-bar/StatusBar' ;
@@ -54,13 +56,18 @@ import {
54
56
import { useCourseOutline } from './hooks' ;
55
57
import messages from './messages' ;
56
58
import { getTagsExportFile } from './data/api' ;
57
- import CourseOutlineHeaderActionsSlot from '../plugin-slots/CourseOutlineHeaderActionsSlot' ;
59
+ import OutlineAddChildButtons from './OutlineAddChildButtons' ;
60
+
61
+ interface CourseOutlineProps {
62
+ courseId : string ,
63
+ }
58
64
59
- const CourseOutline = ( { courseId } ) => {
65
+ const CourseOutline = ( { courseId } : CourseOutlineProps ) => {
60
66
const intl = useIntl ( ) ;
61
67
const location = useLocation ( ) ;
62
68
63
69
const {
70
+ courseUsageKey,
64
71
courseName,
65
72
savingStatus,
66
73
statusBarData,
@@ -89,6 +96,9 @@ const CourseOutline = ({ courseId }) => {
89
96
headerNavigationsActions,
90
97
openEnableHighlightsModal,
91
98
closeEnableHighlightsModal,
99
+ isAddLibrarySectionModalOpen,
100
+ openAddLibrarySectionModal,
101
+ closeAddLibrarySectionModal,
92
102
handleEnableHighlightsSubmit,
93
103
handleInternetConnectionFailed,
94
104
handleOpenHighlightsModal,
@@ -104,6 +114,8 @@ const CourseOutline = ({ courseId }) => {
104
114
handleNewSubsectionSubmit,
105
115
handleNewUnitSubmit,
106
116
handleAddUnitFromLibrary,
117
+ handleAddSubsectionFromLibrary,
118
+ handleAddSectionFromLibrary,
107
119
getUnitUrl,
108
120
handleVideoSharingOptionChange,
109
121
handlePasteClipboardClick,
@@ -119,10 +131,11 @@ const CourseOutline = ({ courseId }) => {
119
131
handleSubsectionDragAndDrop,
120
132
handleUnitDragAndDrop,
121
133
errors,
134
+ resetScrollState,
122
135
} = useCourseOutline ( { courseId } ) ;
123
136
124
137
// Use `setToastMessage` to show the toast.
125
- const [ toastMessage , setToastMessage ] = useState ( /** @type { null|string } */ ( null ) ) ;
138
+ const [ toastMessage , setToastMessage ] = useState < string | null > ( null ) ;
126
139
127
140
useEffect ( ( ) => {
128
141
// Wait for the course data to load before exporting tags.
@@ -139,7 +152,7 @@ const CourseOutline = ({ courseId }) => {
139
152
}
140
153
} , [ location , courseId , courseName ] ) ;
141
154
142
- const [ sections , setSections ] = useState ( sectionsList ) ;
155
+ const [ sections , setSections ] = useState < XBlock [ ] > ( sectionsList ) ;
143
156
144
157
const restoreSectionList = ( ) => {
145
158
setSections ( ( ) => [ ...sectionsList ] ) ;
@@ -157,10 +170,8 @@ const CourseOutline = ({ courseId }) => {
157
170
158
171
/**
159
172
* Move section to new index
160
- * @param {any } currentIndex
161
- * @param {any } newIndex
162
173
*/
163
- const updateSectionOrderByIndex = ( currentIndex , newIndex ) => {
174
+ const updateSectionOrderByIndex = ( currentIndex : number , newIndex : number ) => {
164
175
if ( currentIndex === newIndex ) {
165
176
return ;
166
177
}
@@ -173,11 +184,8 @@ const CourseOutline = ({ courseId }) => {
173
184
174
185
/**
175
186
* Uses details from move information and moves subsection
176
- * @param {any } section
177
- * @param {any } moveDetails
178
- * @returns {void }
179
187
*/
180
- const updateSubsectionOrderByIndex = ( section , moveDetails ) => {
188
+ const updateSubsectionOrderByIndex = ( section : XBlock , moveDetails ) => {
181
189
const { fn, args, sectionId } = moveDetails ;
182
190
if ( ! args ) {
183
191
return ;
@@ -196,11 +204,8 @@ const CourseOutline = ({ courseId }) => {
196
204
197
205
/**
198
206
* Uses details from move information and moves unit
199
- * @param {any } section
200
- * @param {any } moveDetails
201
- * @returns {void }
202
207
*/
203
- const updateUnitOrderByIndex = ( section , moveDetails ) => {
208
+ const updateUnitOrderByIndex = ( section : XBlock , moveDetails ) => {
204
209
const {
205
210
fn, args, sectionId, subsectionId,
206
211
} = moveDetails ;
@@ -220,6 +225,16 @@ const CourseOutline = ({ courseId }) => {
220
225
}
221
226
} ;
222
227
228
+ const handleSelectLibrarySection = useCallback ( ( selectedSection : SelectedComponent ) => {
229
+ handleAddSectionFromLibrary . mutateAsync ( {
230
+ type : COMPONENT_TYPES . libraryV2 ,
231
+ category : ContainerType . Chapter ,
232
+ parentLocator : courseUsageKey ,
233
+ libraryContentKey : selectedSection . usageKey ,
234
+ } ) ;
235
+ closeAddLibrarySectionModal ( ) ;
236
+ } , [ closeAddLibrarySectionModal , handleAddSectionFromLibrary . mutateAsync , courseId , courseUsageKey ] ) ;
237
+
223
238
useEffect ( ( ) => {
224
239
setSections ( sectionsList ) ;
225
240
} , [ sectionsList ] ) ;
@@ -357,6 +372,8 @@ const CourseOutline = ({ courseId }) => {
357
372
isSectionsExpanded = { isSectionsExpanded }
358
373
onNewSubsectionSubmit = { handleNewSubsectionSubmit }
359
374
onOrderChange = { updateSectionOrderByIndex }
375
+ onAddSubsectionFromLibrary = { handleAddSubsectionFromLibrary . mutateAsync }
376
+ resetScrollState = { resetScrollState }
360
377
>
361
378
< SortableContext
362
379
id = { section . id }
@@ -385,9 +402,10 @@ const CourseOutline = ({ courseId }) => {
385
402
onDuplicateSubmit = { handleDuplicateSubsectionSubmit }
386
403
onOpenConfigureModal = { openConfigureModal }
387
404
onNewUnitSubmit = { handleNewUnitSubmit }
388
- onAddUnitFromLibrary = { handleAddUnitFromLibrary }
405
+ onAddUnitFromLibrary = { handleAddUnitFromLibrary . mutateAsync }
389
406
onOrderChange = { updateSubsectionOrderByIndex }
390
407
onPasteClick = { handlePasteClipboardClick }
408
+ resetScrollState = { resetScrollState }
391
409
>
392
410
< SortableContext
393
411
id = { subsection . id }
@@ -431,23 +449,25 @@ const CourseOutline = ({ courseId }) => {
431
449
</ SortableContext >
432
450
</ DraggableList >
433
451
{ courseActions . childAddable && (
434
- < Button
435
- data-testid = "new-section-button"
436
- className = "mt-4"
437
- variant = "outline-primary"
438
- onClick = { handleNewSectionSubmit }
439
- iconBefore = { IconAdd }
440
- block
441
- >
442
- { intl . formatMessage ( messages . newSectionButton ) }
443
- </ Button >
452
+ < OutlineAddChildButtons
453
+ handleNewButtonClick = { handleNewSectionSubmit }
454
+ handleUseFromLibraryClick = { openAddLibrarySectionModal }
455
+ childType = { ContainerType . Section }
456
+ />
444
457
) }
445
458
</ >
446
459
) : (
447
- < EmptyPlaceholder
448
- onCreateNewSection = { handleNewSectionSubmit }
449
- childAddable = { courseActions . childAddable }
450
- />
460
+ < EmptyPlaceholder >
461
+ { courseActions . childAddable && (
462
+ < OutlineAddChildButtons
463
+ handleNewButtonClick = { handleNewSectionSubmit }
464
+ handleUseFromLibraryClick = { openAddLibrarySectionModal }
465
+ childType = { ContainerType . Section }
466
+ btnVariant = "primary"
467
+ btnClasses = "mt-1"
468
+ />
469
+ ) }
470
+ </ EmptyPlaceholder >
451
471
) }
452
472
</ div >
453
473
) }
@@ -493,11 +513,33 @@ const CourseOutline = ({ courseId }) => {
493
513
close = { closeDeleteModal }
494
514
onDeleteSubmit = { handleDeleteItemSubmit }
495
515
/>
516
+ < StandardModal
517
+ title = { intl . formatMessage ( messages . sectionPickerModalTitle ) }
518
+ isOpen = { isAddLibrarySectionModalOpen }
519
+ onClose = { closeAddLibrarySectionModal }
520
+ isOverflowVisible = { false }
521
+ size = "xl"
522
+ >
523
+ < ComponentPicker
524
+ showOnlyPublished
525
+ extraFilter = { [ 'block_type = "section"' ] }
526
+ componentPickerMode = "single"
527
+ onComponentSelected = { handleSelectLibrarySection }
528
+ visibleTabs = { [ ContentType . sections ] }
529
+ />
530
+ </ StandardModal >
496
531
</ Container >
497
532
< div className = "alert-toast" >
498
533
< ProcessingNotification
499
- isShow = { isShowProcessingNotification }
500
- title = { processingNotificationTitle }
534
+ // Show processing toast if any mutation is running
535
+ isShow = {
536
+ isShowProcessingNotification
537
+ || handleAddUnitFromLibrary . isPending
538
+ || handleAddSubsectionFromLibrary . isPending
539
+ || handleAddSectionFromLibrary . isPending
540
+ }
541
+ // HACK: Use saving as default title till we have a need for better messages
542
+ title = { processingNotificationTitle || NOTIFICATION_MESSAGES . saving }
501
543
/>
502
544
< InternetConnectionAlert
503
545
isFailed = { isInternetConnectionAlertFailed }
@@ -518,8 +560,4 @@ const CourseOutline = ({ courseId }) => {
518
560
) ;
519
561
} ;
520
562
521
- CourseOutline . propTypes = {
522
- courseId : PropTypes . string . isRequired ,
523
- } ;
524
-
525
563
export default CourseOutline ;
0 commit comments