Skip to content

Commit 764f136

Browse files
author
Florian Kroenert
committed
Simplified and improved event reaction behavior
1 parent bee1413 commit 764f136

File tree

5 files changed

+152
-90
lines changed

5 files changed

+152
-90
lines changed

src/web/Xrm.Oss.HtmlTemplating/HTMLWYSIWYGEDITOR/ControlManifest.Input.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<manifest>
3-
<control namespace="oss" constructor="HTMLWYSIWYGEDITOR" version="0.0.61" display-name-key="HTMLWYSIWYGEDITOR" description-key="HTMLWYSIWYGEDITOR description" control-type="virtual" >
3+
<control namespace="oss" constructor="HTMLWYSIWYGEDITOR" version="0.0.70" display-name-key="HTMLWYSIWYGEDITOR" description-key="HTMLWYSIWYGEDITOR description" control-type="virtual" >
44
<!--external-service-usage node declares whether this 3rd party PCF control is using external service or not, if yes, this control will be considered as premium and please also add the external domain it is using.
55
If it is not using any external service, please set the enabled="false" and DO NOT add any domain below. The "enabled" will be false by default.
66
Example1:

src/web/Xrm.Oss.HtmlTemplating/components/App.tsx

Lines changed: 76 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { IconButton } from "@fluentui/react";
99
import { loadWebResource } from "../domain/WebResourceLoader";
1010
import { getExternalScript } from "../domain/ScriptCaller";
1111
import { EditorWrapper } from "./EditorWrapper";
12-
import { DesignState, DesignStateActionEnum, designStateReducer } from "../domain/DesignState";
12+
import { DesignState, DesignStateAction, DesignStateActionEnum, designStateReducer } from "../domain/DesignState";
1313
import { debounce, localHost } from "../domain/Utils";
1414
import { registerFileUploader } from "../domain/FileUploader";
15+
import { AppState, SetDefaultDesign, SetEditorProps, SetEditorReady, SetIsFullScreen, appStateReducer } from "../domain/AppState";
1516

1617
export interface AppProps {
1718
pcfContext: ComponentFramework.Context<IInputs>;
@@ -75,40 +76,37 @@ const reviveXtlExpressionJson = (object: { [key: string]: any }) => {
7576
}, {} as { [key: string]: any });
7677
};
7778

78-
let editorReadyFired = false;
79-
8079
export const App: React.FC<AppProps> = React.memo((props) => {
8180
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);
8581

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)
8884

8985
const getFormContext: () => FormContext = () => ({
9086
entityId: (props.pcfContext.mode as any).contextInfo.entityId,
9187
entity: (props.pcfContext.mode as any).contextInfo.entityTypeName
9288
});
9389

9490
// Init once initially and every time fullscreen activates / deactivates
95-
React.useEffect(() => { init(); }, [ isFullScreen ]);
91+
React.useEffect(() => { init(); }, [ appState.isFullScreen ]);
9692

97-
// Load design on external update
9893
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+
});
101104
}
105+
}, [ appState.editorReady ]);
102106

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);
112110

113111
const retrieveMergeTags = (): Promise<Array<MergeTag>> => {
114112
if (window.location.hostname === localHost) {
@@ -149,7 +147,7 @@ export const App: React.FC<AppProps> = React.memo((props) => {
149147
const onEditorUpdate = React.useCallback(async () => {
150148
const [designOutput, htmlOutput] = await getEditorContent();
151149

152-
dispatchDesign({
150+
delayedDesignDispatch({
153151
origin: 'internal',
154152
payload: {
155153
json: designOutput,
@@ -159,42 +157,42 @@ export const App: React.FC<AppProps> = React.memo((props) => {
159157
});
160158
}, []);
161159

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+
};
167163

168-
editorRef.current!.addEventListener("design:updated", onEditorUpdate);
164+
const editorBootstrap = async () => {
165+
console.log("[WYSIWYG_PCF] Bootstrapping unlayer editor");
169166

170-
const functionContext: FunctionContext = {
171-
editorRef: editorRef.current!,
172-
getFormContext: getFormContext,
173-
webApiClient: WebApiClient
174-
};
167+
editorRef.current!.addEventListener("design:updated", onEditorUpdate);
175168

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+
};
186174

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);
194178

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}`);
196183
}
197184
}
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+
}
198196
};
199197

200198
const refCallBack = (editor: EditorRef) => {
@@ -224,7 +222,6 @@ export const App: React.FC<AppProps> = React.memo((props) => {
224222

225223
let propertiesToSet = properties;
226224
let defaultDesign = _defaultDesign;
227-
let appSettings = {};
228225

229226
if (window.location.hostname !== localHost && props.pcfContext.parameters.customScriptInitFunc.raw) {
230227
try {
@@ -245,15 +242,22 @@ export const App: React.FC<AppProps> = React.memo((props) => {
245242
}
246243
}
247244

248-
setEditorProps(propertiesToSet);
249-
setDefaultDesign(defaultDesign);
245+
dispatchAppState(SetEditorProps(propertiesToSet));
246+
dispatchAppState(SetDefaultDesign(defaultDesign));
250247
};
251248

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+
};
257261

258262
const handleDesignChange = async () => {
259263
if (!designContext.lastOrigin) {
@@ -262,28 +266,26 @@ export const App: React.FC<AppProps> = React.memo((props) => {
262266

263267
if (designContext.lastOrigin === 'external') {
264268
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);
266270
}
267271

268272
const [json, html] = await getEditorContent();
269-
270-
if (designContext.lastOrigin === 'internal') {
271-
unlockEditor();
272-
}
273-
274273
props.updateOutputs(json, html);
275274
};
276275

277276
React.useEffect(() => { handleDesignChange(); }, [ designContext.design ]);
278277

279278
React.useEffect(() => {
280279
if (props.updatedProperties && props.updatedProperties.includes("fullscreen_open")) {
281-
editorReadyFired = false;
282-
setIsFullScreen(true);
280+
dispatchAppState(SetIsFullScreen(true));
283281
}
284282
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+
}
287289
}
288290
}, [props.updatedProperties]);
289291

@@ -298,15 +300,15 @@ export const App: React.FC<AppProps> = React.memo((props) => {
298300
});
299301
}
300302

301-
const onMaximize = () => {
303+
const onMaximize = debounce(() => {
302304
props.pcfContext.mode.setFullScreen(true);
303-
};
305+
}, 1000);
304306

305307
return (
306308
<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} />
310312
}
311313
</div>
312314
);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { EmailEditorProps } from "react-email-editor";
2+
3+
export interface AppStateAction<T, P> {
4+
type: T;
5+
payload: P;
6+
}
7+
8+
type SetEditorReadyType = "SET_EDITOR_READY";
9+
type SetEditorReadyPayload = { editorReady: boolean };
10+
type SetEditorReadyAction = AppStateAction<SetEditorReadyType, SetEditorReadyPayload>;
11+
export const SetEditorReady = (editorReady: boolean): SetEditorReadyAction => ({ type: "SET_EDITOR_READY", payload: { editorReady } });
12+
13+
type SetEditorPropsType = "SET_EDITOR_PROPS";
14+
type SetEditorPropsPayload = { props: EmailEditorProps };
15+
type SetEditorPropsAction = AppStateAction<SetEditorPropsType, SetEditorPropsPayload>;
16+
export const SetEditorProps = (props: EmailEditorProps): SetEditorPropsAction => ({ type: "SET_EDITOR_PROPS", payload: { props } });
17+
18+
type SetDefaultDesignType = "SET_DEFAULT_DESIGN";
19+
type SetDefaultDesignPayload = { defaultDesign: { [key: string]: any } };
20+
type SetDefaultDesignAction = AppStateAction<SetDefaultDesignType, SetDefaultDesignPayload>;
21+
export const SetDefaultDesign = (defaultDesign: {[key: string]: any}): SetDefaultDesignAction => ({ type: "SET_DEFAULT_DESIGN", payload: { defaultDesign } });
22+
23+
24+
type SetIsFullScreenType = "SET_IS_FULLSCREEN";
25+
type SetIsFullScreenPayload = { isFullscreen: boolean };
26+
type SetIsFullScreenAction = AppStateAction<SetIsFullScreenType, SetIsFullScreenPayload>;
27+
export const SetIsFullScreen = (isFullscreen: boolean): SetIsFullScreenAction => ({ type: "SET_IS_FULLSCREEN", payload: { isFullscreen } });
28+
29+
30+
export interface DesignDefinition {
31+
json: string;
32+
html: string;
33+
}
34+
35+
export interface AppState {
36+
editorReady: boolean;
37+
editorProps?: EmailEditorProps;
38+
defaultDesign?: {[key: string]: any};
39+
isFullScreen: boolean;
40+
}
41+
42+
export function appStateReducer(designState: AppState, action: SetEditorReadyAction | SetEditorPropsAction | SetDefaultDesignAction | SetIsFullScreenAction) {
43+
const { type, payload } = action;
44+
45+
switch (type) {
46+
case "SET_DEFAULT_DESIGN":
47+
return {
48+
...designState,
49+
defaultDesign: payload.defaultDesign
50+
} as AppState;
51+
case "SET_EDITOR_PROPS":
52+
return {
53+
...designState,
54+
editorProps: payload.props
55+
} as AppState;
56+
case "SET_EDITOR_READY":
57+
return {
58+
...designState,
59+
editorReady: payload.editorReady
60+
} as AppState;
61+
case "SET_IS_FULLSCREEN":
62+
return {
63+
...designState,
64+
isFullScreen: payload.isFullscreen,
65+
editorReady: false
66+
} as AppState;
67+
default:
68+
return designState;
69+
}
70+
}

src/web/Xrm.Oss.HtmlTemplating/domain/DesignState.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export enum DesignStateActionEnum {
2-
SET = 'SET',
3-
UNLOCK = 'UNLOCK'
2+
SET = 'SET'
43
}
54

65
export type Origin = 'internal' | 'external';
@@ -18,7 +17,6 @@ export interface DesignDefinition {
1817

1918
export interface DesignState {
2019
design: DesignDefinition;
21-
isLocked: boolean;
2220
lastOrigin?: Origin
2321
}
2422

@@ -27,20 +25,12 @@ export function designStateReducer(designState: DesignState, action: DesignState
2725

2826
switch (type) {
2927
case DesignStateActionEnum.SET:
30-
if (designState.isLocked && origin !== 'internal') {
31-
return designState;
32-
}
28+
console.log(`[WYSIWYG_PCF] Editor received event from ${origin}`);
3329

3430
return {
3531
...designState,
3632
design: payload,
37-
lastOrigin: origin,
38-
isLocked: origin === 'internal'
39-
} as DesignState;
40-
case DesignStateActionEnum.UNLOCK:
41-
return {
42-
...designState,
43-
isLocked: false
33+
lastOrigin: origin
4434
} as DesignState;
4535
default:
4636
return designState;

src/web/Xrm.Oss.HtmlTemplating/domain/FileUploader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export const registerFileUploader = ({uploadEntity, uploadEntityFileNameField, u
4444
await webApiClient.SendRequest("PATCH", url, img, { headers: headers, apiVersion: "9.2" });
4545
done({ progress: 100, url: `${url}/$value` });
4646
}
47-
catch (error) {
48-
console.log(error);
47+
catch (error: any) {
48+
console.log(`[WYSIWYG_PCF] Encountered error: ${error?.message ?? error}`);
4949
throw error;
5050
}
5151
});

0 commit comments

Comments
 (0)