@@ -9,9 +9,10 @@ import { IconButton } from "@fluentui/react";
9
9
import { loadWebResource } from "../domain/WebResourceLoader" ;
10
10
import { getExternalScript } from "../domain/ScriptCaller" ;
11
11
import { EditorWrapper } from "./EditorWrapper" ;
12
- import { DesignState , DesignStateActionEnum , designStateReducer } from "../domain/DesignState" ;
12
+ import { DesignState , DesignStateAction , DesignStateActionEnum , designStateReducer } from "../domain/DesignState" ;
13
13
import { debounce , localHost } from "../domain/Utils" ;
14
14
import { registerFileUploader } from "../domain/FileUploader" ;
15
+ import { AppState , SetDefaultDesign , SetEditorProps , SetEditorReady , SetIsFullScreen , appStateReducer } from "../domain/AppState" ;
15
16
16
17
export interface AppProps {
17
18
pcfContext : ComponentFramework . Context < IInputs > ;
@@ -75,40 +76,37 @@ const reviveXtlExpressionJson = (object: { [key: string]: any }) => {
75
76
} , { } as { [ key : string ] : any } ) ;
76
77
} ;
77
78
78
- let editorReadyFired = false ;
79
-
80
79
export const App : React . FC < AppProps > = React . memo ( ( props ) => {
81
80
const editorRef = React . useRef < EditorRef > ( ) ;
82
- const [ editorReady , setEditorReady ] = React . useState ( false ) ;
83
- const [ editorProps , setEditorProps ] = React . useState < EmailEditorProps > ( ) ;
84
- const [ defaultDesign , setDefaultDesign ] = React . useState ( _defaultDesign ) ;
85
81
86
- const [ designContext , dispatchDesign ] = React . useReducer ( designStateReducer , { design : { json : "" , html : "" } , isLocked : false } as DesignState ) ;
87
- const [ isFullScreen , setIsFullScreen ] = React . useState ( false ) ;
82
+ const [ designContext , dispatchDesign ] = React . useReducer ( designStateReducer , { design : { json : props . jsonInput ?? "" } } as DesignState ) ;
83
+ const [ appState , dispatchAppState ] = React . useReducer ( appStateReducer , { defaultDesign : undefined , editorProps : undefined , editorReady : false , isFullScreen : false } as AppState )
88
84
89
85
const getFormContext : ( ) => FormContext = ( ) => ( {
90
86
entityId : ( props . pcfContext . mode as any ) . contextInfo . entityId ,
91
87
entity : ( props . pcfContext . mode as any ) . contextInfo . entityTypeName
92
88
} ) ;
93
89
94
90
// Init once initially and every time fullscreen activates / deactivates
95
- React . useEffect ( ( ) => { init ( ) ; } , [ isFullScreen ] ) ;
91
+ React . useEffect ( ( ) => { init ( ) ; } , [ appState . isFullScreen ] ) ;
96
92
97
- // Load design on external update
98
93
React . useEffect ( ( ) => {
99
- if ( ! editorReady ) {
100
- return ;
94
+ if ( appState . editorReady ) {
95
+ editorBootstrap ( ) ;
96
+ dispatchDesign ( {
97
+ origin : 'external' ,
98
+ payload : {
99
+ json : designContext ?. design ?. json ,
100
+ html : ""
101
+ } ,
102
+ type : DesignStateActionEnum . SET
103
+ } ) ;
101
104
}
105
+ } , [ appState . editorReady ] ) ;
102
106
103
- dispatchDesign ( {
104
- origin : 'external' ,
105
- payload : {
106
- json : props . jsonInput ?? "" ,
107
- html : ""
108
- } ,
109
- type : DesignStateActionEnum . SET
110
- } ) ;
111
- } , [ props . jsonInput , editorReady , isFullScreen ] ) ;
107
+ const delayedDesignDispatch = debounce ( ( design : DesignStateAction ) => {
108
+ dispatchDesign ( design ) ;
109
+ } , 1000 ) ;
112
110
113
111
const retrieveMergeTags = ( ) : Promise < Array < MergeTag > > => {
114
112
if ( window . location . hostname === localHost ) {
@@ -149,7 +147,7 @@ export const App: React.FC<AppProps> = React.memo((props) => {
149
147
const onEditorUpdate = React . useCallback ( async ( ) => {
150
148
const [ designOutput , htmlOutput ] = await getEditorContent ( ) ;
151
149
152
- dispatchDesign ( {
150
+ delayedDesignDispatch ( {
153
151
origin : 'internal' ,
154
152
payload : {
155
153
json : designOutput ,
@@ -159,42 +157,42 @@ export const App: React.FC<AppProps> = React.memo((props) => {
159
157
} ) ;
160
158
} , [ ] ) ;
161
159
162
- const onEditorReady = async ( ) => {
163
- // Run only once. MS wires up a middleware in UCI "windowEventListenerBootTask", which interfers with unlayer and makes it post the ready event on every change...
164
- if ( ! editorReadyFired ) {
165
- editorReadyFired = true ;
166
- setEditorReady ( true ) ;
160
+ const onEditorReady = ( ) => {
161
+ dispatchAppState ( SetEditorReady ( true ) ) ;
162
+ } ;
167
163
168
- editorRef . current ! . addEventListener ( "design:updated" , onEditorUpdate ) ;
164
+ const editorBootstrap = async ( ) => {
165
+ console . log ( "[WYSIWYG_PCF] Bootstrapping unlayer editor" ) ;
169
166
170
- const functionContext : FunctionContext = {
171
- editorRef : editorRef . current ! ,
172
- getFormContext : getFormContext ,
173
- webApiClient : WebApiClient
174
- } ;
167
+ editorRef . current ! . addEventListener ( "design:updated" , onEditorUpdate ) ;
175
168
176
- if ( window . location . hostname !== localHost && props . pcfContext . parameters . customScriptOnReadyFunc . raw ) {
177
- try {
178
- const funcRef = getExternalScript ( props . pcfContext . parameters . customScriptOnReadyFunc . raw ) ;
179
-
180
- await funcRef ( functionContext ) ;
181
- }
182
- catch ( ex : any ) {
183
- alert ( `Error in your custom onReady func. Error message: ${ ex . message || ex } ` ) ;
184
- }
185
- }
169
+ const functionContext : FunctionContext = {
170
+ editorRef : editorRef . current ! ,
171
+ getFormContext : getFormContext ,
172
+ webApiClient : WebApiClient
173
+ } ;
186
174
187
- if ( props . pcfContext . parameters . imageUploadEntity . raw && props . pcfContext . parameters . imageUploadEntityBodyField . raw ) {
188
- const imageUploadSettings : ImageUploadSettings = {
189
- uploadEntity : props . pcfContext . parameters . imageUploadEntity . raw ,
190
- uploadEntityFileNameField : props . pcfContext . parameters . imageUploadEntityFileNameField . raw ,
191
- uploadEntityBodyField : props . pcfContext . parameters . imageUploadEntityBodyField . raw ,
192
- parentLookupName : props . pcfContext . parameters . imageUploadEntityParentLookupName . raw
193
- } ;
175
+ if ( window . location . hostname !== localHost && props . pcfContext . parameters . customScriptOnReadyFunc . raw ) {
176
+ try {
177
+ const funcRef = getExternalScript ( props . pcfContext . parameters . customScriptOnReadyFunc . raw ) ;
194
178
195
- registerFileUploader ( imageUploadSettings , functionContext ) ;
179
+ await funcRef ( functionContext ) ;
180
+ }
181
+ catch ( ex : any ) {
182
+ alert ( `Error in your custom onReady func. Error message: ${ ex . message || ex } ` ) ;
196
183
}
197
184
}
185
+
186
+ if ( props . pcfContext . parameters . imageUploadEntity . raw && props . pcfContext . parameters . imageUploadEntityBodyField . raw ) {
187
+ const imageUploadSettings : ImageUploadSettings = {
188
+ uploadEntity : props . pcfContext . parameters . imageUploadEntity . raw ,
189
+ uploadEntityFileNameField : props . pcfContext . parameters . imageUploadEntityFileNameField . raw ,
190
+ uploadEntityBodyField : props . pcfContext . parameters . imageUploadEntityBodyField . raw ,
191
+ parentLookupName : props . pcfContext . parameters . imageUploadEntityParentLookupName . raw
192
+ } ;
193
+
194
+ registerFileUploader ( imageUploadSettings , functionContext ) ;
195
+ }
198
196
} ;
199
197
200
198
const refCallBack = ( editor : EditorRef ) => {
@@ -224,7 +222,6 @@ export const App: React.FC<AppProps> = React.memo((props) => {
224
222
225
223
let propertiesToSet = properties ;
226
224
let defaultDesign = _defaultDesign ;
227
- let appSettings = { } ;
228
225
229
226
if ( window . location . hostname !== localHost && props . pcfContext . parameters . customScriptInitFunc . raw ) {
230
227
try {
@@ -245,15 +242,22 @@ export const App: React.FC<AppProps> = React.memo((props) => {
245
242
}
246
243
}
247
244
248
- setEditorProps ( propertiesToSet ) ;
249
- setDefaultDesign ( defaultDesign ) ;
245
+ dispatchAppState ( SetEditorProps ( propertiesToSet ) ) ;
246
+ dispatchAppState ( SetDefaultDesign ( defaultDesign ) ) ;
250
247
} ;
251
248
252
- const unlockEditor = debounce ( ( ) => {
253
- dispatchDesign ( {
254
- type : DesignStateActionEnum . UNLOCK
255
- } ) ;
256
- } , 500 ) ;
249
+ const processExternalUpdate = ( ) => {
250
+ if ( props . jsonInput !== designContext . design . json ) {
251
+ delayedDesignDispatch ( {
252
+ origin : 'external' ,
253
+ payload : {
254
+ json : props . jsonInput ?? "" ,
255
+ html : ""
256
+ } ,
257
+ type : DesignStateActionEnum . SET
258
+ } ) ;
259
+ }
260
+ } ;
257
261
258
262
const handleDesignChange = async ( ) => {
259
263
if ( ! designContext . lastOrigin ) {
@@ -262,28 +266,26 @@ export const App: React.FC<AppProps> = React.memo((props) => {
262
266
263
267
if ( designContext . lastOrigin === 'external' ) {
264
268
const design = designContext . design ;
265
- editorRef . current ! . loadDesign ( ( design && design . json && JSON . parse ( design . json ) ) || defaultDesign ) ;
269
+ editorRef . current ! . loadDesign ( ( design && design . json && JSON . parse ( design . json ) ) || appState . defaultDesign ) ;
266
270
}
267
271
268
272
const [ json , html ] = await getEditorContent ( ) ;
269
-
270
- if ( designContext . lastOrigin === 'internal' ) {
271
- unlockEditor ( ) ;
272
- }
273
-
274
273
props . updateOutputs ( json , html ) ;
275
274
} ;
276
275
277
276
React . useEffect ( ( ) => { handleDesignChange ( ) ; } , [ designContext . design ] ) ;
278
277
279
278
React . useEffect ( ( ) => {
280
279
if ( props . updatedProperties && props . updatedProperties . includes ( "fullscreen_open" ) ) {
281
- editorReadyFired = false ;
282
- setIsFullScreen ( true ) ;
280
+ dispatchAppState ( SetIsFullScreen ( true ) ) ;
283
281
}
284
282
else if ( props . updatedProperties && props . updatedProperties . includes ( "fullscreen_close" ) ) {
285
- editorReadyFired = false ;
286
- setIsFullScreen ( false ) ;
283
+ dispatchAppState ( SetIsFullScreen ( false ) ) ;
284
+ }
285
+ else if ( props . updatedProperties && props . updatedProperties . includes ( "jsonInputField" ) ) {
286
+ if ( appState . editorReady ) {
287
+ processExternalUpdate ( ) ;
288
+ }
287
289
}
288
290
} , [ props . updatedProperties ] ) ;
289
291
@@ -298,15 +300,15 @@ export const App: React.FC<AppProps> = React.memo((props) => {
298
300
} ) ;
299
301
}
300
302
301
- const onMaximize = ( ) => {
303
+ const onMaximize = debounce ( ( ) => {
302
304
props . pcfContext . mode . setFullScreen ( true ) ;
303
- } ;
305
+ } , 1000 ) ;
304
306
305
307
return (
306
308
< div id = 'oss_htmlroot' style = { { display : "flex" , flexDirection : "column" , minWidth : "1024px" , minHeight : "500px" , position : "relative" , height : `${ props . allocatedHeight > 0 ? props . pcfContext . mode . allocatedHeight : 800 } px` , width : `${ props . allocatedWidth > 0 ? props . pcfContext . mode . allocatedWidth : 1024 } px` } } >
307
- { ! isFullScreen && < IconButton iconProps = { { iconName : "MiniExpand" } } title = "Maximize / Minimize" styles = { { root : { position : "absolute" , backgroundColor : "#efefef" , borderRadius : "5px" , right : "10px" , bottom : "10px" } } } onClick = { onMaximize } /> }
308
- { editorProps && defaultDesign &&
309
- < EditorWrapper editorProps = { { ...editorProps , onReady : onEditorReady } } refCallBack = { refCallBack } />
309
+ { ! appState . isFullScreen && < IconButton iconProps = { { iconName : "MiniExpand" } } title = "Maximize / Minimize" styles = { { root : { position : "absolute" , backgroundColor : "#efefef" , borderRadius : "5px" , right : "10px" , bottom : "10px" } } } onClick = { onMaximize } /> }
310
+ { appState . editorProps && appState . defaultDesign &&
311
+ < EditorWrapper editorProps = { { ...appState . editorProps , onReady : onEditorReady } } refCallBack = { refCallBack } />
310
312
}
311
313
</ div >
312
314
) ;
0 commit comments