@@ -2,11 +2,9 @@ import 'ag-grid-community/styles/ag-grid.css';
2
2
import 'ag-grid-community/styles/ag-theme-balham.css' ;
3
3
4
4
import { Button , Divider , H1 , Intent , Tab , Tabs } from '@blueprintjs/core' ;
5
- import { cloneDeep } from 'lodash' ;
6
- import React from 'react' ;
5
+ import React , { useCallback , useEffect , useRef , useState } from 'react' ;
7
6
import { useDispatch } from 'react-redux' ;
8
- import { Role } from 'src/commons/application/ApplicationTypes' ;
9
- import { useTypedSelector } from 'src/commons/utils/Hooks' ;
7
+ import { useSession } from 'src/commons/utils/Hooks' ;
10
8
import {
11
9
addNewStoriesUsersToCourse ,
12
10
addNewUsersToCourse
@@ -19,65 +17,48 @@ import {
19
17
fetchAssessmentConfigs ,
20
18
fetchCourseConfig ,
21
19
fetchNotificationConfigs ,
22
- setAssessmentConfigurations ,
23
20
updateAssessmentConfigs ,
24
21
updateCourseConfig ,
25
22
updateUserRole
26
23
} from '../../../commons/application/actions/SessionActions' ;
27
24
import { UpdateCourseConfiguration } from '../../../commons/application/types/SessionTypes' ;
28
- import { AssessmentConfiguration } from '../../../commons/assessment/AssessmentTypes' ;
29
25
import ContentDisplay from '../../../commons/ContentDisplay' ;
30
- import AddStoriesUserPanel , { NameUsernameRole } from './subcomponents/AddStoriesUserPanel' ;
31
- import AddUserPanel , { UsernameRoleGroup } from './subcomponents/AddUserPanel' ;
32
- import AssessmentConfigPanel from './subcomponents/assessmentConfigPanel/AssessmentConfigPanel' ;
26
+ import AddStoriesUserPanel from './subcomponents/AddStoriesUserPanel' ;
27
+ import AddUserPanel from './subcomponents/AddUserPanel' ;
28
+ import AssessmentConfigPanel , {
29
+ ImperativeAssessmentConfigPanel
30
+ } from './subcomponents/assessmentConfigPanel/AssessmentConfigPanel' ;
33
31
import CourseConfigPanel from './subcomponents/CourseConfigPanel' ;
34
32
import NotificationConfigPanel from './subcomponents/NotificationConfigPanel' ;
35
33
import UserConfigPanel from './subcomponents/userConfigPanel/UserConfigPanel' ;
36
34
37
- const AdminPanel : React . FC = ( ) => {
38
- const [ hasChangesCourseConfig , setHasChangesCourseConfig ] = React . useState ( false ) ;
39
- const [ hasChangesAssessmentConfig , setHasChangesAssessmentConfig ] = React . useState ( false ) ;
35
+ const defaultCourseConfig : UpdateCourseConfiguration = {
36
+ courseName : '' ,
37
+ courseShortName : '' ,
38
+ viewable : true ,
39
+ enableGame : true ,
40
+ enableAchievements : true ,
41
+ enableSourcecast : true ,
42
+ enableStories : false ,
43
+ moduleHelpText : ''
44
+ } ;
40
45
41
- const [ courseConfiguration , setCourseConfiguration ] = React . useState < UpdateCourseConfiguration > ( {
42
- courseName : '' ,
43
- courseShortName : '' ,
44
- viewable : true ,
45
- enableGame : true ,
46
- enableAchievements : true ,
47
- enableSourcecast : true ,
48
- enableStories : false ,
49
- moduleHelpText : ''
50
- } ) ;
46
+ const AdminPanel : React . FC = ( ) => {
47
+ const [ hasChangesCourseConfig , setHasChangesCourseConfig ] = useState ( false ) ;
48
+ const [ hasChangesAssessmentConfig , setHasChangesAssessmentConfig ] = useState ( false ) ;
49
+ const [ courseConfiguration , setCourseConfiguration ] = useState ( defaultCourseConfig ) ;
51
50
52
51
const dispatch = useDispatch ( ) ;
52
+ const session = useSession ( ) ;
53
53
54
- const session = useTypedSelector ( state => state . session ) ;
55
-
56
- /**
57
- * Mutable ref to track the assessment configuration form state instead of useState. This is
58
- * because ag-grid does not update the cellRendererParams whenever there is an update in rowData,
59
- * leading to a stale closure problem where the handlers in AssessmentConfigPanel capture the old
60
- * value of assessmentConfig.
61
- *
62
- * Also, useState causes a flicker in ag-grid during rerenders. Thus we use this mutable ref and
63
- * ag-grid's API to update cell values instead.
64
- */
65
- const assessmentConfig = React . useRef ( session . assessmentConfigurations ) ;
66
-
67
- // Tracks the assessment configurations to be deleted in the backend when the save button is clicked
68
- const [ assessmentConfigsToDelete , setAssessmentConfigsToDelete ] = React . useState <
69
- AssessmentConfiguration [ ]
70
- > ( [ ] ) ;
71
-
72
- React . useEffect ( ( ) => {
54
+ useEffect ( ( ) => {
73
55
dispatch ( fetchCourseConfig ( ) ) ;
74
56
dispatch ( fetchAssessmentConfigs ( ) ) ;
75
57
dispatch ( fetchAdminPanelCourseRegistrations ( ) ) ;
76
58
dispatch ( fetchNotificationConfigs ( ) ) ;
77
59
} , [ dispatch ] ) ;
78
60
79
- // After updated configs have been loaded from the backend, put them into local React state
80
- React . useEffect ( ( ) => {
61
+ useEffect ( ( ) => {
81
62
setCourseConfiguration ( {
82
63
courseName : session . courseName ,
83
64
courseShortName : session . courseShortName ,
@@ -88,10 +69,22 @@ const AdminPanel: React.FC = () => {
88
69
enableStories : session . enableStories ,
89
70
moduleHelpText : session . moduleHelpText
90
71
} ) ;
91
-
92
- // IMPT: To prevent mutation of props
93
- assessmentConfig . current = cloneDeep ( session . assessmentConfigurations ) ;
94
- } , [ session ] ) ;
72
+ } , [
73
+ session . assessmentConfigurations ,
74
+ session . courseName ,
75
+ session . courseShortName ,
76
+ session . enableAchievements ,
77
+ session . enableGame ,
78
+ session . enableSourcecast ,
79
+ session . enableStories ,
80
+ session . moduleHelpText ,
81
+ session . viewable
82
+ ] ) ;
83
+
84
+ const tableRef = useRef < ImperativeAssessmentConfigPanel > ( null ) ;
85
+ useEffect ( ( ) => {
86
+ tableRef . current ?. resetData ( ) ;
87
+ } , [ session . assessmentConfigurations ] ) ;
95
88
96
89
const courseConfigPanelProps = {
97
90
courseConfiguration : courseConfiguration ,
@@ -101,67 +94,31 @@ const AdminPanel: React.FC = () => {
101
94
}
102
95
} ;
103
96
104
- const assessmentConfigPanelProps = React . useMemo ( ( ) => {
105
- return {
106
- // Would have been loaded by the useEffect above
107
- assessmentConfig : assessmentConfig as React . MutableRefObject < AssessmentConfiguration [ ] > ,
108
- setAssessmentConfig : ( val : AssessmentConfiguration [ ] ) => {
109
- assessmentConfig . current = val ;
110
- setHasChangesAssessmentConfig ( true ) ;
111
- } ,
112
- setAssessmentConfigsToDelete : ( deletedElement : AssessmentConfiguration ) => {
113
- // If it is not a newly created row that is yet to be persisted in the backend
114
- if ( deletedElement . assessmentConfigId !== - 1 ) {
115
- const temp = [ ...assessmentConfigsToDelete ] ;
116
- temp . push ( deletedElement ) ;
117
- setAssessmentConfigsToDelete ( temp ) ;
118
- }
119
- } ,
120
- setHasChangesAssessmentConfig : setHasChangesAssessmentConfig
121
- } ;
122
- } , [ assessmentConfigsToDelete ] ) ;
123
-
124
- const userConfigPanelProps = {
125
- courseRegId : session . courseRegId ,
126
- userCourseRegistrations : session . userCourseRegistrations ,
127
- handleUpdateUserRole : ( courseRegId : number , role : Role ) =>
128
- dispatch ( updateUserRole ( courseRegId , role ) ) ,
129
- handleDeleteUserFromCourse : ( courseRegId : number ) =>
130
- dispatch ( deleteUserCourseRegistration ( courseRegId ) )
131
- } ;
132
-
133
- const addUserPanelProps = {
134
- handleAddNewUsersToCourse : ( users : UsernameRoleGroup [ ] , provider : string ) =>
135
- dispatch ( addNewUsersToCourse ( users , provider ) )
136
- } ;
137
-
138
- const addStoriesUserPanelProps = {
139
- handleAddNewUsersToCourse : ( users : NameUsernameRole [ ] , provider : string ) =>
140
- dispatch ( addNewStoriesUsersToCourse ( users , provider ) )
141
- } ;
142
-
143
97
// Handler to submit changes to Course Configration and Assessment Configuration to the backend.
144
98
// Changes made to users are handled separately.
145
- const submitHandler = ( ) => {
99
+ const submitHandler = useCallback ( ( ) => {
146
100
if ( hasChangesCourseConfig ) {
147
101
dispatch ( updateCourseConfig ( courseConfiguration ) ) ;
148
102
setHasChangesCourseConfig ( false ) ;
149
103
}
150
- if ( assessmentConfigsToDelete . length > 0 ) {
151
- assessmentConfigsToDelete . forEach ( assessmentConfig => {
152
- dispatch ( deleteAssessmentConfig ( assessmentConfig ) ) ;
153
- } ) ;
154
- setAssessmentConfigsToDelete ( [ ] ) ;
155
- }
104
+ const tableState = tableRef . current ?. getData ( ) ?? [ ] ;
105
+ const currentConfigs = session . assessmentConfigurations ?? [ ] ;
106
+ const currentIds = new Set ( tableState . map ( config => config . assessmentConfigId ) ) ;
107
+ const configsToDelete = currentConfigs . filter (
108
+ config => ! currentIds . has ( config . assessmentConfigId )
109
+ ) ;
110
+ configsToDelete . forEach ( config => dispatch ( deleteAssessmentConfig ( config ) ) ) ;
156
111
if ( hasChangesAssessmentConfig ) {
157
- // Reset the store first so that old props do not propagate down and cause a flicker
158
- dispatch ( setAssessmentConfigurations ( [ ] ) ) ;
159
-
160
- // assessmentConfig.current will exist after the first load
161
- dispatch ( updateAssessmentConfigs ( assessmentConfig . current ! ) ) ;
112
+ dispatch ( updateAssessmentConfigs ( tableState ) ) ;
162
113
setHasChangesAssessmentConfig ( false ) ;
163
114
}
164
- } ;
115
+ } , [
116
+ courseConfiguration ,
117
+ dispatch ,
118
+ hasChangesAssessmentConfig ,
119
+ hasChangesCourseConfig ,
120
+ session . assessmentConfigurations
121
+ ] ) ;
165
122
166
123
const data = (
167
124
< div className = "admin-panel" >
@@ -175,9 +132,14 @@ const AdminPanel: React.FC = () => {
175
132
< >
176
133
< CourseConfigPanel { ...courseConfigPanelProps } />
177
134
< Divider />
178
- < AssessmentConfigPanel { ...assessmentConfigPanelProps } />
135
+ < AssessmentConfigPanel
136
+ ref = { tableRef }
137
+ initialConfigs = { session . assessmentConfigurations ?? [ ] }
138
+ setHasChangesAssessmentConfig = { setHasChangesAssessmentConfig }
139
+ />
179
140
< Button
180
141
text = "Save"
142
+ disabled = { ! hasChangesCourseConfig && ! hasChangesAssessmentConfig }
181
143
style = { { marginTop : '15px' } }
182
144
intent = {
183
145
hasChangesCourseConfig || hasChangesAssessmentConfig
@@ -189,12 +151,43 @@ const AdminPanel: React.FC = () => {
189
151
</ >
190
152
}
191
153
/>
192
- < Tab id = "users" title = "Users" panel = { < UserConfigPanel { ...userConfigPanelProps } /> } />
193
- < Tab id = "add-users" title = "Add Users" panel = { < AddUserPanel { ...addUserPanelProps } /> } />
154
+ < Tab
155
+ id = "users"
156
+ title = "Users"
157
+ panel = {
158
+ < UserConfigPanel
159
+ courseRegId = { session . courseRegId }
160
+ userCourseRegistrations = { session . userCourseRegistrations }
161
+ handleUpdateUserRole = { ( courseRegId , role ) =>
162
+ dispatch ( updateUserRole ( courseRegId , role ) )
163
+ }
164
+ handleDeleteUserFromCourse = { ( courseRegId : number ) =>
165
+ dispatch ( deleteUserCourseRegistration ( courseRegId ) )
166
+ }
167
+ />
168
+ }
169
+ />
170
+ < Tab
171
+ id = "add-users"
172
+ title = "Add Users"
173
+ panel = {
174
+ < AddUserPanel
175
+ handleAddNewUsersToCourse = { ( users , provider ) =>
176
+ dispatch ( addNewUsersToCourse ( users , provider ) )
177
+ }
178
+ />
179
+ }
180
+ />
194
181
< Tab
195
182
id = "add-stories-users"
196
183
title = "Add Stories Users"
197
- panel = { < AddStoriesUserPanel { ...addStoriesUserPanelProps } /> }
184
+ panel = {
185
+ < AddStoriesUserPanel
186
+ handleAddNewUsersToCourse = { ( users , provider ) =>
187
+ dispatch ( addNewStoriesUsersToCourse ( users , provider ) )
188
+ }
189
+ />
190
+ }
198
191
/>
199
192
< Tab id = "notification-config" title = "Notifications" panel = { < NotificationConfigPanel /> } />
200
193
</ Tabs >
0 commit comments