1
- import React , { useContext , useEffect , useState } from 'react' ;
1
+ import React , { useCallback , useContext , useEffect , useState } from 'react' ;
2
2
import { UPDATE_TOAST } from '../context/actions' ;
3
3
import { AppContext } from '../context/AppContext' ;
4
4
import { Button , Typography } from '@mui/material' ;
@@ -29,177 +29,120 @@ const componentMapping = {
29
29
STRING : SettingsString
30
30
} ;
31
31
32
+ const constructFileHandler = setting => {
33
+ return ( file ) => {
34
+ console . log ( `Pretend we saved that file for ${ setting . name } somewhere (we didn't)...` ) ;
35
+ } ;
36
+ } ;
37
+
32
38
const SettingsPage = ( ) => {
33
39
const fileRef = React . useRef ( null ) ;
34
40
const { state, dispatch } = useContext ( AppContext ) ;
35
41
const csrf = selectCsrfToken ( state ) ;
36
- const [ settingsControls , setSettingsControls ] = useState ( [ ] ) ;
37
- const [ update , setState ] = useState ( ) ;
42
+ const [ settings , setSettings ] = useState ( [ ] ) ;
43
+ const [ handlers , setHandlers ] = useState ( { } ) ;
44
+ const [ update , setUpdate ] = useState ( true ) ;
45
+ const canView = selectHasViewSettingsPermission ( state ) ||
46
+ selectHasAdministerSettingsPermission ( state ) ;
38
47
39
48
useEffect ( ( ) => {
40
49
const fetchData = async ( ) => {
41
50
// Get the options from the server
42
- const allOptions =
43
- selectHasViewSettingsPermission ( state ) ||
44
- selectHasAdministerSettingsPermission ( state )
51
+ const allOptions = canView
45
52
? ( await getAllOptions ( csrf ) ) . payload . data
46
53
: [ ] ;
47
54
48
- if ( allOptions ) {
49
- // Sort the options by category, store them, and upate the state.
50
- setSettingsControls (
51
- allOptions . sort ( ( l , r ) => {
52
- if ( l . category === r . category ) {
53
- return l . name . localeCompare ( r . name ) ;
54
- } else {
55
- return l . category . localeCompare ( r . category ) ;
56
- }
57
- } )
58
- ) ;
55
+ if ( allOptions ?. length !== 0 ) {
56
+ // Sort the options by category, store them, and update the state.
57
+ const sorted = allOptions . sort ( ( l , r ) => {
58
+ if ( l . category === r . category ) {
59
+ return l . name . localeCompare ( r . name ) ;
60
+ } else {
61
+ return l . category . localeCompare ( r . category ) ;
62
+ }
63
+ } ) ;
64
+
65
+ setSettings ( sorted ) ;
66
+ setUpdate ( false ) ;
59
67
}
60
68
} ;
61
- if ( csrf ) {
69
+ if ( csrf && update ) {
62
70
fetchData ( ) ;
63
71
}
64
- } , [ state , csrf ] ) ;
65
-
66
- const handleLogoUrl = file => {
67
- if ( csrf ) {
68
- // TODO: Need to upload the file to a storage bucket...
69
- dispatch ( {
70
- type : UPDATE_TOAST ,
71
- payload : {
72
- severity : 'warning' ,
73
- toast : 'The Logo URL setting has yet to be implemented.'
74
- }
75
- } ) ;
76
- }
77
- } ;
72
+ } , [ csrf , canView , update , setUpdate , setSettings ] ) ;
78
73
79
- const keyedHandler = ( key , event ) => {
80
- if ( handlers [ key ] ) {
81
- handlers [ key ] . setting . value = event . target . value ;
82
- setState ( { update : true } ) ;
83
- }
84
- } ;
85
-
86
- const handlePulseEmailFrequency = event => {
87
- keyedHandler ( 'PULSE_EMAIL_FREQUENCY' , event ) ;
88
- } ;
89
-
90
- const handlers = {
91
- // File handlers do not modify settings values and, therefore, do not
92
- // need to keep a reference to the setting object. However, they do need
93
- // a file reference object.
94
- LOGO_URL : {
95
- onChange : handleLogoUrl ,
96
- setting : fileRef
97
- } ,
98
-
99
- // All others need to provide an `onChange` method and a `setting` object.
100
- PULSE_EMAIL_FREQUENCY : {
101
- onChange : handlePulseEmailFrequency ,
102
- setting : undefined
103
- }
104
- } ;
105
-
106
- const addHandlersToSettings = settings => {
107
- return settings
108
- ? settings . map ( setting => {
109
- const handler = handlers [ setting . name . toUpperCase ( ) ] ;
110
- if ( handler ) {
111
- if ( setting . type . toUpperCase ( ) === 'FILE' ) {
112
- return {
113
- ...setting ,
114
- handleFunction : handler . onChange ,
115
- fileRef : handler . setting
116
- } ;
117
- }
118
-
119
- handler . setting = setting ;
120
- return { ...setting , handleChange : handler . onChange } ;
121
- }
122
-
123
- console . warn ( `WARNING: No handler for ${ setting . name } ` ) ;
124
- return setting ;
125
- } )
126
- : [ ] ;
127
- } ;
128
-
129
- const save = async ( ) => {
130
- let errors ;
131
- let saved = 0 ;
132
- for ( let key of Object . keys ( handlers ) ) {
133
- const setting = handlers [ key ] . setting ;
134
- // The settings controller does not allow blank values.
135
- if ( setting ?. name && `${ setting . value } ` != '' ) {
136
- let res ;
137
- if ( setting . id ) {
138
- res = await putOption (
139
- { name : setting . name , value : setting . value } ,
140
- csrf
141
- ) ;
74
+ useEffect ( ( ) => {
75
+ if ( settings ?. length !== 0 ) {
76
+ setHandlers ( settings . reduce ( ( acc , curr ) => {
77
+ if ( curr . type . toUpperCase ( ) === "FILE" ) {
78
+ acc [ curr . name ] = constructFileHandler ( curr ) ;
142
79
} else {
143
- res = await postOption (
144
- { name : setting . name , value : setting . value } ,
145
- csrf
146
- ) ;
147
- if ( res ?. payload ?. data ) {
148
- setting . id = res . payload . data . id ;
149
- }
150
- }
151
- if ( res ?. error ) {
152
- const error = res ?. error ?. message ;
153
- if ( errors ) {
154
- errors += '\n' + error ;
155
- } else {
156
- errors = error ;
80
+ acc [ curr . name ] = ( event ) => {
81
+ const newSettings = [ ...settings ] ;
82
+ const setting = newSettings . find ( test => test . name === curr . name ) ;
83
+ setting . value = event . target . value ;
84
+ setSettings ( newSettings ) ;
157
85
}
158
86
}
159
- if ( res ?. payload ?. data ) {
160
- saved ++ ;
161
- }
162
- } else {
163
- console . warn ( `WARNING: ${ setting . name } not sent to the server` ) ;
164
- }
87
+ return acc ;
88
+ } , { } ) ) ;
165
89
}
90
+ } , [ setHandlers , setSettings , settings ] )
166
91
167
- if ( errors ) {
168
- dispatch ( {
169
- type : UPDATE_TOAST ,
170
- payload : {
171
- severity : 'error' ,
172
- toast : errors
173
- }
92
+ const save = useCallback ( async ( ) => {
93
+ if ( settings && settings . length > 0 ) {
94
+ let errors ;
95
+ let promises = settings . filter ( setting => setting . type . toUpperCase ( ) !== "FILE" ) . map ( setting => {
96
+ let promise = setting . id ?
97
+ putOption ( { name : setting . name , value : setting . value } , csrf ) :
98
+ postOption ( { name : setting . name , value : setting . value } , csrf ) ;
99
+
100
+ return promise ;
174
101
} ) ;
175
- } else if ( saved > 0 ) {
176
- dispatch ( {
177
- type : UPDATE_TOAST ,
178
- payload : {
179
- severity : 'success' ,
180
- toast : 'Settings have been saved'
102
+
103
+ Promise . all ( promises ) . then ( ( results ) => {
104
+ results . forEach ( ( res ) => {
105
+ if ( res ?. error ) {
106
+ const error = res ?. error ?. message ;
107
+ if ( errors ) {
108
+ errors += '\n' + error ;
109
+ } else {
110
+ errors = error ;
111
+ }
112
+ }
113
+ } ) ;
114
+
115
+ // Trigger load of updated settings values and display errors or success.
116
+ setUpdate ( true ) ;
117
+ if ( errors ) {
118
+ dispatch ( {
119
+ type : UPDATE_TOAST ,
120
+ payload : {
121
+ severity : 'error' ,
122
+ toast : errors
123
+ }
124
+ } ) ;
125
+ } else {
126
+ dispatch ( {
127
+ type : UPDATE_TOAST ,
128
+ payload : {
129
+ severity : 'success' ,
130
+ toast : 'Settings have been saved'
131
+ }
132
+ } ) ;
181
133
}
182
134
} ) ;
183
135
}
184
- } ;
185
-
186
- /**
187
- * @typedef {Object } Controls
188
- * @property {ComponentName } componentName - The name of the component.
189
- *
190
- * @typedef {('SettingsBoolean'|'SettingsColor'|'SettingsFile'|'SettingsNumber'|'SettingsString') } ComponentName
191
- */
136
+ } , [ settings ] ) ;
192
137
193
- /** @type {Controls[] } */
194
- const updatedSettingsControls = addHandlersToSettings ( settingsControls ) ;
195
138
const categories = { } ;
196
139
197
- return selectHasViewSettingsPermission ( state ) ||
198
- selectHasAdministerSettingsPermission ( state ) ? (
140
+ return canView ? (
199
141
< div className = "settings-page" >
200
- { updatedSettingsControls . map ( ( componentInfo , index ) => {
201
- const Component = componentMapping [ componentInfo . type . toUpperCase ( ) ] ;
202
- const info = { ...componentInfo , name : titleCase ( componentInfo . name ) } ;
142
+ { settings . map ( ( setting , index ) => {
143
+ const Component = componentMapping [ setting . type . toUpperCase ( ) ] ;
144
+ const info = { ...setting , name : titleCase ( setting . name ) } ;
145
+ setting . type === 'FILE' ? info . handleFunction = handlers [ setting . name ] : info . handleChange = handlers [ setting . name ] ;
203
146
if ( categories [ info . category ] ) {
204
147
return < Component key = { index } { ...info } /> ;
205
148
} else {
@@ -222,8 +165,8 @@ const SettingsPage = () => {
222
165
{
223
166
// Check length against an explicit value. If length is zero, it will
224
167
// be displayed instead of evaluated to false.
225
- settingsControls &&
226
- settingsControls . length > 0 &&
168
+ settings &&
169
+ settings . length > 0 &&
227
170
selectHasAdministerSettingsPermission ( state ) && (
228
171
< div className = "buttons" >
229
172
< Button disableRipple color = "primary" onClick = { save } >
0 commit comments