1- import React , { useContext , useEffect , useState } from 'react' ;
1+ import React , { useCallback , useContext , useEffect , useState } from 'react' ;
22import { UPDATE_TOAST } from '../context/actions' ;
33import { AppContext } from '../context/AppContext' ;
44import { Button , Typography } from '@mui/material' ;
@@ -29,177 +29,120 @@ const componentMapping = {
2929 STRING : SettingsString
3030} ;
3131
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+
3238const SettingsPage = ( ) => {
3339 const fileRef = React . useRef ( null ) ;
3440 const { state, dispatch } = useContext ( AppContext ) ;
3541 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 ) ;
3847
3948 useEffect ( ( ) => {
4049 const fetchData = async ( ) => {
4150 // Get the options from the server
42- const allOptions =
43- selectHasViewSettingsPermission ( state ) ||
44- selectHasAdministerSettingsPermission ( state )
51+ const allOptions = canView
4552 ? ( await getAllOptions ( csrf ) ) . payload . data
4653 : [ ] ;
4754
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 ) ;
5967 }
6068 } ;
61- if ( csrf ) {
69+ if ( csrf && update ) {
6270 fetchData ( ) ;
6371 }
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 ] ) ;
7873
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 ) ;
14279 } 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 ) ;
15785 }
15886 }
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+ } , { } ) ) ;
16589 }
90+ } , [ setHandlers , setSettings , settings ] )
16691
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 ;
174101 } ) ;
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+ } ) ;
181133 }
182134 } ) ;
183135 }
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 ] ) ;
192137
193- /** @type {Controls[] } */
194- const updatedSettingsControls = addHandlersToSettings ( settingsControls ) ;
195138 const categories = { } ;
196139
197- return selectHasViewSettingsPermission ( state ) ||
198- selectHasAdministerSettingsPermission ( state ) ? (
140+ return canView ? (
199141 < 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 ] ;
203146 if ( categories [ info . category ] ) {
204147 return < Component key = { index } { ...info } /> ;
205148 } else {
@@ -222,8 +165,8 @@ const SettingsPage = () => {
222165 {
223166 // Check length against an explicit value. If length is zero, it will
224167 // be displayed instead of evaluated to false.
225- settingsControls &&
226- settingsControls . length > 0 &&
168+ settings &&
169+ settings . length > 0 &&
227170 selectHasAdministerSettingsPermission ( state ) && (
228171 < div className = "buttons" >
229172 < Button disableRipple color = "primary" onClick = { save } >
0 commit comments