@@ -12,10 +12,50 @@ import PropTypes from 'prop-types';
1212import messages from './messages' ;
1313import SortableHeader from './customSortableHeader' ;
1414import TableActions from './customTableActions' ;
15+ import { getChangedRows } from './utils' ;
16+ import ChangeConfirmationModal from './changeConfirmationModal' ;
17+ import { updateUserRolesInCourses } from './data/api' ;
1518
16- export default function CoursesTable ( { username, userCourses } ) {
19+ export default function CoursesTable ( {
20+ username, email, userCourses, setCourseUpdateErrors,
21+ } ) {
1722 const intl = useIntl ( ) ;
23+ const saveButtonRef = useRef ( ) ;
24+ const [ showModal , setShowModal ] = useState ( false ) ;
25+ const [ submitButtonState , setSubmitButtonState ] = useState ( 'default' ) ;
26+ const handleCancel = ( ) => {
27+ setShowModal ( false ) ;
28+ } ;
29+
1830 let userCoursesData = userCourses ;
31+ const [ originalRowRoles ] = useState ( ( ) => userCoursesData . reduce ( ( acc , row ) => {
32+ acc [ row . run ] = row . role == null ? 'null' : row . role ;
33+ return acc ;
34+ } , { } ) ) ;
35+
36+ const [ originalCheckedRows ] = useState ( ( ) => {
37+ const initial = { } ;
38+ userCoursesData . forEach ( ( row ) => {
39+ if ( row . role === 'staff' || row . role === 'instructor' ) {
40+ initial [ row . run ] = true ;
41+ }
42+ } ) ;
43+ return initial ;
44+ } ) ;
45+
46+ const unsavedChangesRef = useRef ( { newlyCheckedWithRole : [ ] , uncheckedWithRole : [ ] , roleChangedRows : [ ] } ) ;
47+ const hasUnsavedChangesRef = useRef ( false ) ;
48+ useEffect ( ( ) => {
49+ if ( submitButtonState === 'pending' ) {
50+ updateUserRolesInCourses ( { userEmail : email , changedCourses : unsavedChangesRef . current } ) . then ( ( data ) => {
51+ setSubmitButtonState ( 'complete' ) ;
52+ setTimeout ( ( ) => {
53+ setCourseUpdateErrors ( { success : true , errors : data } ) ;
54+ setShowModal ( false ) ;
55+ } , 2000 ) ;
56+ } ) ;
57+ }
58+ } , [ submitButtonState ] ) ;
1959
2060 // Change role null to 'null' for sorting to working correctly
2161 // As we are considering 'null' to appear as staff with disabled dropdown
@@ -93,11 +133,20 @@ export default function CoursesTable({ username, userCourses }) {
93133 const numChecked = allRowIds . filter ( ( id ) => checkedRows [ id ] ) . length ;
94134 const allChecked = numChecked === allRowIds . length && allRowIds . length > 0 ;
95135 const someChecked = numChecked > 0 && numChecked < allRowIds . length ;
136+ const [ isSaveBtnEnabled , setIsSaveBtnEnabled ] = useState ( false ) ;
96137 useEffect ( ( ) => {
97138 if ( headerCheckboxRef . current ) {
98139 headerCheckboxRef . current . indeterminate = someChecked && ! allChecked ;
99140 }
100- } , [ someChecked , allChecked , numChecked , sortBy ] ) ;
141+ } , [ someChecked , allChecked , numChecked , sortBy , rowRoles , isSaveBtnEnabled , showModal , submitButtonState ] ) ;
142+
143+ useEffect ( ( ) => {
144+ const changes = getChangedRows ( checkedRows , originalCheckedRows , rowRoles , originalRowRoles , userCoursesData ) ;
145+ hasUnsavedChangesRef . current = Object . values ( changes ) . some ( arr => arr . length > 0 ) ;
146+ setIsSaveBtnEnabled ( hasUnsavedChangesRef . current ) ;
147+ sessionStorage . setItem ( `${ username } hasUnsavedChanges` , hasUnsavedChangesRef . current ) ;
148+ unsavedChangesRef . current = changes ;
149+ } , [ checkedRows , rowRoles ] ) ;
101150
102151 const handleHeaderCheckboxChange = ( ) => {
103152 if ( allChecked ) {
@@ -246,6 +295,17 @@ export default function CoursesTable({ username, userCourses }) {
246295 setSortBy = { setSortBy }
247296 />
248297 ) ;
298+ useEffect ( ( ) => {
299+ const handleBeforeUnload = ( e ) => {
300+ if ( ! hasUnsavedChangesRef . current ) { return ; }
301+
302+ e . preventDefault ( ) ;
303+ e . returnValue = '' ;
304+ } ;
305+
306+ window . addEventListener ( 'beforeunload' , handleBeforeUnload ) ;
307+ return ( ) => window . removeEventListener ( 'beforeunload' , handleBeforeUnload ) ;
308+ } , [ ] ) ;
249309
250310 return (
251311 < div className = "course-team-management-courses-table" >
@@ -322,17 +382,38 @@ export default function CoursesTable({ username, userCourses }) {
322382 { sortedAndFilteredData . length > 0 && < DataTable . TableFooter /> }
323383 </ DataTable >
324384 < div className = "py-4 my-2 d-flex justify-content-end align-items-center" >
325- < Button variant = "brand" style = { { width : '8%' } } >
385+ < Button onClick = { ( ) => setShowModal ( true ) } disabled = { ! isSaveBtnEnabled } variant = "brand" style = { { width : '8%' } } >
326386 { intl . formatMessage ( messages . saveButtonLabel ) }
327387 </ Button >
328388 </ div >
389+ < ChangeConfirmationModal
390+ changedData = { unsavedChangesRef . current }
391+ isOpen = { showModal }
392+ onConfirm = { setSubmitButtonState }
393+ submitButtonState = { submitButtonState }
394+ onCancel = { handleCancel }
395+ username = { username }
396+ email = { email }
397+ positionRef = { saveButtonRef }
398+ />
329399 </ div >
330400 ) ;
331401}
332402
333403CoursesTable . propTypes = {
334- username : PropTypes . string . isRequired ,
335- userCourses : PropTypes . string . isRequired ,
404+ username : PropTypes . string ,
405+ email : PropTypes . string ,
406+ userCourses : PropTypes . shape ( {
407+ course_id : PropTypes . string ,
408+ course_name : PropTypes . string ,
409+ course_url : PropTypes . string ,
410+ role : PropTypes . string ,
411+ status : PropTypes . string ,
412+ org : PropTypes . string ,
413+ number : PropTypes . string ,
414+ run : PropTypes . string ,
415+ } ) . isRequired ,
416+ setCourseUpdateErrors : PropTypes . func . isRequired ,
336417 row : PropTypes . shape ( {
337418 run : PropTypes . string . isRequired ,
338419 original : PropTypes . shape ( {
@@ -341,3 +422,8 @@ CoursesTable.propTypes = {
341422 } ) . isRequired ,
342423 } ) . isRequired ,
343424} ;
425+
426+ CoursesTable . defaultProps = {
427+ username : '' ,
428+ email : '' ,
429+ } ;
0 commit comments