Skip to content

Commit

Permalink
Implemented file upload handling to dynamics entity
Browse files Browse the repository at this point in the history
  • Loading branch information
Florian Kroenert committed May 3, 2023
1 parent de915e0 commit 5ec41c8
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="oss" constructor="HTMLWYSIWYGEDITOR" version="0.0.46" display-name-key="HTMLWYSIWYGEDITOR" description-key="HTMLWYSIWYGEDITOR description" control-type="virtual" >
<control namespace="oss" constructor="HTMLWYSIWYGEDITOR" version="0.0.57" display-name-key="HTMLWYSIWYGEDITOR" description-key="HTMLWYSIWYGEDITOR description" control-type="virtual" >
<!--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.
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.
Example1:
Expand Down Expand Up @@ -34,6 +34,10 @@
<property name="customScriptOnReadyFunc" display-name-key="Custom Script OnReady Func" description-key="Namespace of the custom onReady func for the unlayer editor" of-type-group="text" usage="input" />
<property name="projectId" display-name-key="Project Id" description-key="Your unlayer editor project Id" of-type="Whole.None" usage="input" />
<property name="displayMode" display-name-key="Display Mode" description-key="Which display mode to initialize in Unlayer editor. Email or Web, defaults to web" of-type-group="text" usage="input" />
<property name="imageUploadEntity" display-name-key="Image Upload Entity" description-key="Logical Name of entity where images should be stored" of-type-group="text" usage="input" />
<property name="imageUploadEntityFileNameField" display-name-key="Image Upload Entity File Name Field" description-key="Logical Name of field where file name should be stored as record name" of-type-group="text" usage="input" />
<property name="imageUploadEntityBodyField" display-name-key="Image Upload Entity Body Field" description-key="Logical Name of field where file content should be stored. Can be of type file or image." of-type-group="text" usage="input" />
<property name="imageUploadEntityParentLookupName" display-name-key="Image Upload Entity Parent Lookup Name" description-key="SCHEMA NAME of field in the image upload entity which points to the current templating record" of-type-group="text" usage="input" />
<!--
Property node's of-type attribute can be of-type-group attribute.
Example:
Expand Down
54 changes: 46 additions & 8 deletions src/web/Xrm.Oss.HtmlTemplating/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getExternalScript } from "../domain/ScriptCaller";
import { EditorWrapper } from "./EditorWrapper";
import { DesignState, DesignStateActionEnum, designStateReducer } from "../domain/DesignState";
import { debounce, localHost } from "../domain/Utils";
import { registerFileUploader } from "../domain/FileUploader";

export interface AppProps {
pcfContext: ComponentFramework.Context<IInputs>;
Expand All @@ -21,6 +22,24 @@ export interface AppProps {
updatedProperties: string[];
}

export interface ImageUploadSettings {
uploadEntity: string, // "msdyn_knowledgearticleimage"
uploadEntityFileNameField: string, // "msdyn_filename"
uploadEntityBodyField: string, // "msdyn_blobfile"
parentLookupName?: string | null
}

export interface FormContext {
entityId: string,
entity: string
}

export interface FunctionContext {
editorRef: EditorRef,
formContext: FormContext,
webApiClient: typeof WebApiClient
}

const asciiArmorRegex = /xtl_ascii_armor__(.*)?(?=__xtl_ascii_armor)__xtl_ascii_armor/gm;
const _defaultDesign: any = { "counters": { "u_column": 1, "u_row": 1 }, "body": { "rows": [{ "cells": [1], "columns": [{ "contents": [], "values": { "_meta": { "htmlID": "u_column_1", "htmlClassNames": "u_column" } } }], "values": { "backgroundColor": "", "backgroundImage": { "url": "", "fullWidth": true, "repeat": false, "center": true, "cover": false }, "padding": "10px", "columnsBackgroundColor": "", "_meta": { "htmlID": "u_row_1", "htmlClassNames": "u_row" }, "selectable": true, "draggable": true, "deletable": true } }], "values": { "backgroundColor": "#e7e7e7", "backgroundImage": { "url": "", "fullWidth": true, "repeat": false, "center": true, "cover": false }, "contentWidth": "800px", "fontFamily": { "label": "Arial", "value": "arial,helvetica,sans-serif" }, "_meta": { "htmlID": "u_body", "htmlClassNames": "u_body" } } } };

Expand Down Expand Up @@ -67,6 +86,11 @@ export const App: React.FC<AppProps> = React.memo((props) => {
const [designContext, dispatchDesign] = React.useReducer(designStateReducer, { design: { json: "", html: "" }, isLocked: false } as DesignState);
const [isFullScreen, setIsFullScreen] = React.useState(false);

const formContext: FormContext = {
entityId: (props.pcfContext.mode as any).contextInfo.entityId,
entity: (props.pcfContext.mode as any).contextInfo.entityTypeName
};

// Init once initially and every time fullscreen activates / deactivates
React.useEffect(() => { init(); }, [ isFullScreen ]);

Expand Down Expand Up @@ -141,21 +165,35 @@ export const App: React.FC<AppProps> = React.memo((props) => {
editorReadyFired = true;
setEditorReady(true);

// "Unregister" onEditorReady event, MS event middleware causes it to execute more often than necessary
setEditorProps({ ...editorProps, onReady: undefined });

editorRef.current!.addEventListener("design:updated", onEditorUpdate);

const functionContext: FunctionContext = {
editorRef: editorRef.current!,
formContext: formContext,
webApiClient: WebApiClient
};

if (window.location.hostname !== localHost && props.pcfContext.parameters.customScriptOnReadyFunc.raw) {
try {
const funcRef = getExternalScript(props.pcfContext.parameters.customScriptOnReadyFunc.raw);

await funcRef({ editorRef: editorRef.current, webApiClient: WebApiClient });
await funcRef(functionContext);
}
catch(ex: any) {
alert(`Error in your custom onReady func. Error message: ${ex.message || ex}`);
}
}

if (props.pcfContext.parameters.imageUploadEntity.raw && props.pcfContext.parameters.imageUploadEntityFileNameField.raw && props.pcfContext.parameters.imageUploadEntityBodyField.raw) {
const imageUploadSettings: ImageUploadSettings = {
uploadEntity: props.pcfContext.parameters.imageUploadEntity.raw,
uploadEntityFileNameField: props.pcfContext.parameters.imageUploadEntityFileNameField.raw,
uploadEntityBodyField: props.pcfContext.parameters.imageUploadEntityBodyField.raw,
parentLookupName: props.pcfContext.parameters.imageUploadEntityParentLookupName.raw
};

registerFileUploader(imageUploadSettings, functionContext);
}
}
};

Expand Down Expand Up @@ -186,11 +224,13 @@ export const App: React.FC<AppProps> = React.memo((props) => {

let propertiesToSet = properties;
let defaultDesign = _defaultDesign;
let appSettings = {};

if (window.location.hostname !== localHost && props.pcfContext.parameters.customScriptInitFunc.raw) {
try {
const funcRef = getExternalScript(props.pcfContext.parameters.customScriptInitFunc.raw);
const funcResult = await funcRef({ editorProps: properties, webApiClient: WebApiClient });

const funcResult = await funcRef({ editorProps: properties, formContext: formContext, webApiClient: WebApiClient });

if (funcResult && funcResult.editorProps) {
propertiesToSet = funcResult.editorProps;
Expand All @@ -205,8 +245,6 @@ export const App: React.FC<AppProps> = React.memo((props) => {
}
}

propertiesToSet.onReady = onEditorReady;

setEditorProps(propertiesToSet);
setDefaultDesign(defaultDesign);
};
Expand Down Expand Up @@ -268,7 +306,7 @@ export const App: React.FC<AppProps> = React.memo((props) => {
<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` }}>
{ !isFullScreen && <IconButton iconProps={{ iconName: "MiniExpand" }} title="Maximize / Minimize" styles={{ root: { position: "absolute", backgroundColor: "#efefef", borderRadius: "5px", right: "10px", bottom: "10px" }}} onClick={onMaximize} /> }
{ editorProps &&
<EditorWrapper editorProps={editorProps} refCallBack={refCallBack} />
<EditorWrapper editorProps={{...editorProps, onReady: onEditorReady}} refCallBack={refCallBack} />
}
</div>
);
Expand Down
43 changes: 43 additions & 0 deletions src/web/Xrm.Oss.HtmlTemplating/domain/FileUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as WebApiClient from "xrm-webapi-client";
import { EditorRef } from "react-email-editor";
import { FormContext, FunctionContext, ImageUploadSettings } from "../components/App";


// https://learn.microsoft.com/en-us/power-apps/developer/data-platform/file-column-data?tabs=webapi#upload-files
export const registerFileUploader = ({uploadEntity, uploadEntityFileNameField, uploadEntityBodyField, parentLookupName}: ImageUploadSettings, { editorRef, formContext, webApiClient }: FunctionContext) => {
editorRef.registerCallback('image', async function (file, done) {
var img = file.attachments[0];

var fileRequest: WebApiClient.CreateParameters = {
entityName: uploadEntity,
entity: {
[uploadEntityFileNameField]: img.name
}
};

if (parentLookupName && formContext?.entity && formContext?.entityId) {
(fileRequest as any).entity[`${parentLookupName}@odata.bind`] = `${webApiClient.GetSetName(formContext.entity)}(${formContext.entityId})`;
}

try {
const response = await webApiClient.Create(fileRequest);
const fileId = response.substr(response.length - 37, 36);

done({ progress: 50 });

var headers = [
{ key: "x-ms-file-name", value: img.name },
{ key: "Content-Type", value: "application/octet-stream" }
];

var url = (webApiClient as any).GetApiUrl({ apiVersion: "9.2" }) + `${webApiClient.GetSetName(uploadEntity)}(${fileId})/${uploadEntityBodyField}`;

await webApiClient.SendRequest("PATCH", url, img, { headers: headers, apiVersion: "9.2" });
done({ progress: 100, url: `${url}/$value` });
}
catch (error) {
console.log(error);
throw error;
}
});
};
14 changes: 7 additions & 7 deletions src/web/Xrm.Oss.HtmlTemplating/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/web/Xrm.Oss.HtmlTemplating/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"react": "16.8.6",
"react-dom": "16.8.6",
"react-email-editor": "^1.7.4",
"xrm-webapi-client": "^4.1.2"
"xrm-webapi-client": "^4.1.6"
},
"devDependencies": {
"@microsoft/eslint-plugin-power-apps": "^0.2.6",
Expand Down

0 comments on commit 5ec41c8

Please sign in to comment.