├── .eslintrc.json ├── .gitignore ├── AttachmentDragandDrop ├── .gitignore ├── AttachmentDragandDrop.cdsproj └── Other │ ├── Customizations.xml │ ├── Relationships.xml │ └── Solution.xml ├── AttachmentUpload ├── AttachmentUpload.1030.resx ├── AttachmentUpload.1033.resx ├── AttachmentUpload.1044.resx ├── AttachmentUploader.css ├── AttachmentUploader.tsx ├── ControlManifest.Input.xml ├── index.ts └── uploadicn.png ├── AttachmentUploader.pcfproj ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── pcfconfig.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended" 8 | ], 9 | "globals": { 10 | "ComponentFramework": true 11 | }, 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 12, 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@microsoft/power-apps", 19 | "@typescript-eslint" 20 | ], 21 | "rules": { 22 | "no-unused-vars": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # generated directory 7 | **/generated 8 | 9 | # output directory 10 | /out 11 | 12 | # msbuild output directories 13 | /bin 14 | /obj 15 | .DS_Store 16 | ~/Library/Microsoft/PowerAppsCli/usersettings.json 17 | -------------------------------------------------------------------------------- /AttachmentDragandDrop/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # msbuild output directories 4 | /bin 5 | /obj -------------------------------------------------------------------------------- /AttachmentDragandDrop/AttachmentDragandDrop.cdsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps 5 | 6 | 7 | 8 | 9 | 10 | 11 | 9a3a23dd-d324-4670-a71a-5813852c4778 12 | v4.6.2 13 | 14 | net462 15 | PackageReference 16 | 17 | 18 | 19 | 20 | Both 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | PreserveNewest 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /AttachmentDragandDrop/Other/Customizations.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 1033 17 | 18 | -------------------------------------------------------------------------------- /AttachmentDragandDrop/Other/Relationships.xml: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /AttachmentDragandDrop/Other/Solution.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AttachmentDragandDrop 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 1.2.1.0 15 | 16 | 2 17 | 18 | 19 | ramakoneru 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | https://github.com/ramarao9/ 30 | 31 | ram 32 | 33 | 69998 34 | 35 | 36 |
37 | 1 38 | 1 39 | 40 | 41 | USA 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 1 55 | MN 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 | 2 66 | 1 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 1 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 |
93 |
94 | 95 | 96 |
97 |
-------------------------------------------------------------------------------- /AttachmentUpload/AttachmentUpload.1030.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Der skete en eller flere fejl ved forsøg på at uploade de(n) vedhæftede fil(er). 122 | 123 | 124 | Gem posten for at aktivere indholdet. 125 | 126 | 127 | Uploader 128 | 129 | 130 | Træk filer hertil... 131 | 132 | 133 | Træk filer hertil eller klik for at uploade. 134 | 135 | 136 | Der skete en fejl ved upload af den vedhæftede fil. 137 | 138 | -------------------------------------------------------------------------------- /AttachmentUpload/AttachmentUpload.1033.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | text/microsoft-resx 54 | 55 | 56 | 2.0 57 | 58 | 59 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 60 | 61 | 62 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 63 | 64 | 65 | 66 | An error has occurred while trying to upload the attachment. 67 | 68 | 69 | One or more errors occured when trying to upload the attachments. 70 | 71 | 72 | To enable the content create the record. 73 | 74 | 75 | Uploading 76 | 77 | 78 | Drop the files here ... 79 | 80 | 81 | Drop files here or click to upload. 82 | 83 | -------------------------------------------------------------------------------- /AttachmentUpload/AttachmentUpload.1044.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | text/microsoft-resx 54 | 55 | 56 | 2.0 57 | 58 | 59 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 60 | 61 | 62 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 63 | 64 | 65 | En feil oppstod under opplastning av vedlegg. 66 | 67 | 68 | En feil oppstod under opplastning av vedlegg. 69 | 70 | 71 | Velg Lagre for å vise innholdet. 72 | 73 | 74 | Laster opp 75 | 76 | 77 | Slipp filen(e) her... 78 | 79 | 80 | Slipp filer her eller klikk for å laste opp. 81 | 82 | -------------------------------------------------------------------------------- /AttachmentUpload/AttachmentUploader.css: -------------------------------------------------------------------------------- 1 | 2 | .defaultContentCont{ 3 | height: 150px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | 10 | .dragDropCont{ 11 | height:150px; 12 | width:99%; 13 | position: relative; 14 | } 15 | 16 | .dropZoneCont{ 17 | 18 | display:flex; 19 | border:2px dashed #D3D3D3; 20 | align-items: center; 21 | justify-content: center; 22 | flex-flow: column; 23 | padding:5px; 24 | color: rgb(68, 68, 68); 25 | font-family: 'SegoeUI-Semibold', 'Segoe UI Semibold', 'Segoe UI Regular', 'Segoe UI'; 26 | } 27 | 28 | .uploadDivs{ 29 | position: absolute; 30 | width: calc(100% - 12px); 31 | height:calc(100% - 12px); 32 | } 33 | 34 | .filesStatsCont{ 35 | display: flex; 36 | flex-flow: column; 37 | align-items: center; 38 | justify-content: center; 39 | background-color: rgba(0,0,0,0.5); 40 | z-index: 999; 41 | } 42 | 43 | 44 | 45 | .uploadStatusText{ 46 | color:white; 47 | font-weight: 500; 48 | margin:5px 0px; 49 | } 50 | 51 | .uploadImgDD{ 52 | width:64px; 53 | height:64px; 54 | } -------------------------------------------------------------------------------- /AttachmentUpload/AttachmentUploader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IInputs } from "./generated/ManifestTypes" 3 | import { useDropzone } from 'react-dropzone' 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 5 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 6 | import { faSpinner } from '@fortawesome/free-solid-svg-icons' 7 | 8 | export interface UploadProps { 9 | id: string; 10 | entityName: string; 11 | entitySetName: string; 12 | controlToRefresh: string | null; 13 | uploadIcon: string; 14 | useNoteAttachment: boolean; 15 | context: ComponentFramework.Context | undefined; 16 | defaultNoteTitle: string | null; 17 | } 18 | 19 | export interface FileInfo { 20 | name: string; 21 | type: string; 22 | body: string; 23 | } 24 | 25 | export const AttachmentUploader: React.FC = (uploadProps: UploadProps) => { 26 | 27 | const [uploadIcn, setuploadIcn] = React.useState(uploadProps.uploadIcon); 28 | const [totalFileCount, setTotalFileCount] = React.useState(0); 29 | const [currentUploadCount, setCurrentUploadCount] = React.useState(0); 30 | const translate = (name: string) => uploadProps.context?.resources.getString(name); 31 | 32 | const onDrop = React.useCallback((acceptedFiles: any) => { 33 | 34 | if (acceptedFiles && acceptedFiles.length) { 35 | setTotalFileCount(acceptedFiles.length); 36 | } 37 | 38 | 39 | const toBase64 = async (file: any) => new Promise((resolve, reject) => { 40 | const reader = new FileReader(); 41 | reader.readAsDataURL(file); 42 | reader.onload = () => resolve(reader.result); 43 | reader.onabort = () => reject(); 44 | reader.onerror = error => reject(error); 45 | }); 46 | 47 | const uploadFileToRecord = async (id: string, entity: string, entitySetName: string, 48 | fileInfo: FileInfo, context: ComponentFramework.Context, defaultNoteTitle: string|null) => { 49 | 50 | let isActivityMimeAttachment = !uploadProps.useNoteAttachment && (entity.toLowerCase() === "email" || entity.toLowerCase() === "appointment"); 51 | let attachmentRecord: ComponentFramework.WebApi.Entity = {}; 52 | if (isActivityMimeAttachment) { 53 | attachmentRecord["objectid_activitypointer@odata.bind"] = `/activitypointers(${id})`; 54 | attachmentRecord["body"] = fileInfo.body; 55 | } 56 | else { 57 | attachmentRecord[`objectid_${entity}@odata.bind`] = `/${entitySetName}(${id})`; 58 | attachmentRecord["documentbody"] = fileInfo.body; 59 | 60 | if (defaultNoteTitle != null && defaultNoteTitle !== "") { 61 | attachmentRecord["subject"] = defaultNoteTitle; 62 | } 63 | } 64 | 65 | if (fileInfo.type && fileInfo.type !== "") { 66 | attachmentRecord["mimetype"] = fileInfo.type; 67 | } 68 | 69 | attachmentRecord["filename"] = fileInfo.name; 70 | attachmentRecord["objecttypecode"] = entity; 71 | let attachmentEntity = isActivityMimeAttachment ? "activitymimeattachment" : "annotation"; 72 | await context.webAPI.createRecord(attachmentEntity, attachmentRecord) 73 | } 74 | 75 | 76 | const uploadFilesToCRM = async (files: any) => { 77 | 78 | 79 | try { 80 | for (let i = 0; i < acceptedFiles.length; i++) { 81 | setCurrentUploadCount(i); 82 | let file = acceptedFiles[i] as any; 83 | let base64Data = await toBase64(acceptedFiles[i]); 84 | let base64DataStr = base64Data as string; 85 | let base64IndexOfBase64 = base64DataStr.indexOf(';base64,') + ';base64,'.length; 86 | var base64 = base64DataStr.substring(base64IndexOfBase64); 87 | let fileInfo: FileInfo = { name: file.name, type: file.type, body: base64 }; 88 | let entityId = uploadProps.id; 89 | let entityName = uploadProps.entityName; 90 | 91 | if (entityId == null || entityId === "") {//this happens when the record is created and the user tries to upload 92 | let currentPageContext = uploadProps.context as any; 93 | currentPageContext = currentPageContext ? currentPageContext["page"] : undefined; 94 | entityId = currentPageContext.entityId; 95 | entityName = currentPageContext.entityTypeName; 96 | } 97 | 98 | await uploadFileToRecord(entityId, entityName, uploadProps.entitySetName, fileInfo, uploadProps.context!!, uploadProps.defaultNoteTitle); 99 | } 100 | } 101 | catch (e: any) { 102 | let errorMessagePrefix = (acceptedFiles.length === 1) ? translate("error_while_uploading_attachment") : translate("error_while_uploading_attachments"); 103 | let errOptions = { message: `${errorMessagePrefix} ${e.message}` }; 104 | uploadProps.context?.navigation.openErrorDialog(errOptions) 105 | } 106 | 107 | setTotalFileCount(0); 108 | let xrmObj: any = (window as any)["Xrm"]; 109 | if (xrmObj && xrmObj.Page && uploadProps.controlToRefresh) { 110 | var controlToRefresh = xrmObj.Page.getControl(uploadProps.controlToRefresh); 111 | if (controlToRefresh) { 112 | controlToRefresh.refresh(); 113 | } 114 | } 115 | } 116 | 117 | 118 | uploadFilesToCRM(acceptedFiles); 119 | 120 | 121 | 122 | 123 | }, [totalFileCount, currentUploadCount, uploadProps.defaultNoteTitle]) 124 | 125 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }) 126 | 127 | 128 | if (uploadProps.id == null || uploadProps.id === "") { 129 | return ( 130 |
131 | {translate("save_record_to_enable_content")} 132 |
133 | ); 134 | } 135 | 136 | let fileStats = null; 137 | if (totalFileCount > 0) { 138 | fileStats = ( 139 |
140 |
141 | 142 |
143 |
144 | {translate("uploading")} ({currentUploadCount}/{totalFileCount}) 145 |
146 |
147 | ); 148 | } 149 | 150 | 151 | 152 | return ( 153 |
154 |
155 | 156 |
157 | Upload 158 |
159 |
160 | { 161 | isDragActive ? 162 |

{translate("drop_files_here")}

: 163 |

{translate("drop_files_here_or_click_to_upload")}

164 | } 165 |
166 |
167 | 168 | {fileStats} 169 | 170 | 171 | 172 |
173 | ) 174 | 175 | 176 | 177 | } -------------------------------------------------------------------------------- /AttachmentUpload/ControlManifest.Input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SingleLine.Text 6 | SingleLine.Phone 7 | SingleLine.Email 8 | SingleLine.Ticker 9 | TwoOptions 10 | Whole.None 11 | Currency 12 | FP 13 | Decimal 14 | 15 | 16 | SingleLine.Text 17 | SingleLine.Phone 18 | SingleLine.Email 19 | SingleLine.Ticker 20 | SingleLine.TextArea 21 | Multiple 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 0 31 | 1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /AttachmentUpload/index.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { IInputs, IOutputs } from "./generated/ManifestTypes"; 4 | import { AttachmentUploader, UploadProps } from './AttachmentUploader'; 5 | 6 | 7 | interface EntityRef { 8 | id: string, 9 | entityName: string 10 | } 11 | 12 | export class AttachmentUpload implements ComponentFramework.StandardControl { 13 | private UploadIconName: string = "uploadicn.png"; 14 | private attachmentUploaderContainer: HTMLDivElement; 15 | private _context: ComponentFramework.Context; 16 | private uploadProps: UploadProps = { 17 | id: "", 18 | entityName: "", 19 | entitySetName: "", 20 | controlToRefresh: "", 21 | uploadIcon: "", 22 | useNoteAttachment: false, 23 | context: undefined, 24 | defaultNoteTitle: "" 25 | }; 26 | constructor() { 27 | 28 | } 29 | 30 | /** 31 | * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. 32 | * Data-set values are not initialized here, use updateView. 33 | * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions. 34 | * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. 35 | * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface. 36 | * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. 37 | */ 38 | public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) { 39 | let entityRef = this.getEntityReference(context); 40 | if (entityRef) { 41 | this.uploadProps.id = entityRef.id 42 | this.uploadProps.entityName = entityRef.entityName; 43 | this.uploadProps.context = context; 44 | this.uploadProps.controlToRefresh = context.parameters.ControlNameForRefresh.raw; 45 | this.uploadProps.uploadIcon = this.getImageBase64(); 46 | this.uploadProps.useNoteAttachment = context.parameters.UseNoteAttachment.raw === "1"; 47 | this.uploadProps.defaultNoteTitle = context.parameters.DefaultNoteTitle.raw; 48 | this.uploadProps.context = context; 49 | } 50 | this.attachmentUploaderContainer = container; 51 | } 52 | 53 | //since we want the image to also render during dev, we can use this approach until better support is provided in the future. 54 | private getImageBase64() { 55 | return "data:image/png;base64,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"; 56 | } 57 | 58 | 59 | private getEntityReference(context: ComponentFramework.Context): EntityRef | undefined { 60 | let currentPageContext = context as any; 61 | currentPageContext = currentPageContext ? currentPageContext["page"] : undefined; 62 | var entityRef: EntityRef = { id: "", entityName: "" }; 63 | if (currentPageContext) { 64 | if (currentPageContext.entityTypeName) { 65 | entityRef.entityName = currentPageContext.entityTypeName; 66 | } 67 | if (currentPageContext.entityId && currentPageContext.entityId !== "") { 68 | entityRef.id = currentPageContext.entityId; 69 | } 70 | } 71 | 72 | return entityRef; 73 | 74 | } 75 | 76 | /** 77 | * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc. 78 | * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions 79 | */ 80 | public updateView(context: ComponentFramework.Context): void { 81 | this.uploadProps.context = context; 82 | 83 | let entityRef = this.getEntityReference(context); 84 | if (entityRef && entityRef.id !== "") { 85 | this.uploadProps.id = entityRef.id; 86 | } 87 | this.uploadProps.controlToRefresh = context.parameters.ControlNameForRefresh.raw; 88 | this.uploadProps.uploadIcon = this.getImageBase64();//when initially a new record tha's transitioning to an existing record, so the UI is now being updated to enable the content 89 | 90 | this.uploadProps.defaultNoteTitle = context.parameters.DefaultNoteTitle.raw; 91 | if (this.uploadProps.entitySetName === "") { 92 | this.retrieveEntitySetNameAndRender(context, this.uploadProps.entityName); 93 | } 94 | else { 95 | this.renderComponent(); 96 | } 97 | 98 | 99 | } 100 | 101 | private retrieveEntitySetNameAndRender(context: ComponentFramework.Context, entityName: string) { 102 | var thisRef = this; 103 | context.utils.getEntityMetadata(entityName).then(function (response) { 104 | thisRef.uploadProps.entitySetName = response.EntitySetName; 105 | thisRef.renderComponent(); 106 | }, 107 | function (errorResponse: any) { 108 | console.log(`Error occurred while retrieving the entity metadata. ${errorResponse}`); 109 | }); 110 | } 111 | 112 | private renderComponent() { 113 | ReactDOM.render( 114 | React.createElement( 115 | AttachmentUploader, 116 | this.uploadProps 117 | ), 118 | this.attachmentUploaderContainer 119 | ); 120 | } 121 | /** 122 | * It is called by the framework prior to a control receiving new data. 123 | * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output” 124 | */ 125 | public getOutputs(): IOutputs { 126 | return {}; 127 | } 128 | 129 | /** 130 | * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. 131 | * i.e. cancelling any pending remote calls, removing listeners, etc. 132 | */ 133 | public destroy(): void { 134 | ReactDOM.unmountComponentAtNode(this.attachmentUploaderContainer); 135 | } 136 | } -------------------------------------------------------------------------------- /AttachmentUpload/uploadicn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramarao9/AttachmentUploader/4cafde34d8290ba580ec8f996855cd0f2bdf57e8/AttachmentUpload/uploadicn.png -------------------------------------------------------------------------------- /AttachmentUploader.pcfproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps 5 | 6 | 7 | 8 | 9 | 10 | 11 | AttachmentUploader 12 | 0a2ccb16-fcad-4da0-97e3-8a21deda08c3 13 | $(MSBuildThisFileDirectory)out\controls 14 | 15 | 16 | 17 | v4.6.2 18 | 19 | net462 20 | PackageReference 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rama Rao Koneru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Attachment Uploader 2 | A Power App code component to easily upload one or more attachments on Dynamics365 records. Works with Email and normal Notes attachments. 3 | 4 | 5 | ## Installation 6 | 7 | Download the unmanaged/managed solution from the [Releases](https://github.com/ramarao9/AttachmentUploader/releases) 8 | 9 | 10 | ### Setting up the control 11 | 12 | 13 | * Insert a section with a single column on the form 14 | 15 | * Add the field you would like to use that's will not be used on the form 16 | 17 | ![Setting control on form](https://ramarao.blob.core.windows.net/attachmentuploader/SettingControlOnForm.jpg) 18 | 19 | * Also, uncheck 'Display label on the form' for the field 20 | 21 | * Save and publish the form. 22 | 23 | * Navigate to the form and you should see the control 24 | 25 | ![ExistingRecord](https://ramarao.blob.core.windows.net/attachmentuploader/ExistingRecordState.jpg) 26 | 27 | 28 | When the record is not yet created, you would see the below 29 | 30 | ![NewRecordState](https://ramarao.blob.core.windows.net/attachmentuploader/NewRecordState.jpg) 31 | 32 | 33 | 34 | * If using to upload note attachments, you could specify the name of the Timeline control as below to refresh after the upload 35 | 36 | ![TimelineRefresh](https://ramarao.blob.core.windows.net/attachmentuploader/TimelineControlRefresh.jpg) 37 | 38 | 39 | 40 | ## Development 41 | 42 | After cloning the project, run the below commands 43 | 44 | `npm install` -- installs the required dependencies 45 | 46 | `npm run start` -- local development and testing 47 | 48 | `npm run build` -- to build for production 49 | 50 | 51 | If you are new to PCF, the [official documentation](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/implementing-controls-using-typescript) is a good place to start. 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "attachmentupload", 3 | "version": "1.0.3", 4 | "description": "Easily upload attachment for Email and Notes", 5 | "scripts": { 6 | "build": "pcf-scripts build", 7 | "clean": "pcf-scripts clean", 8 | "rebuild": "pcf-scripts rebuild", 9 | "start": "pcf-scripts start" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-svg-core": "^1.3.0", 13 | "@fortawesome/free-solid-svg-icons": "^6.0.0", 14 | "@fortawesome/react-fontawesome": "^0.1.17", 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2", 17 | "react-dropzone": "^12.1.0" 18 | }, 19 | "devDependencies": { 20 | "@microsoft/eslint-plugin-power-apps": "^0.2.6", 21 | "@types/node": "^18.8.2", 22 | "@types/powerapps-component-framework": "^1.3.4", 23 | "@types/react": "^16.14.5", 24 | "@types/react-dom": "^16.9.12", 25 | "@typescript-eslint/eslint-plugin": "^5.39.0", 26 | "@typescript-eslint/parser": "^5.39.0", 27 | "eslint": "^8.24.0", 28 | "eslint-plugin-import": "^2.26.0", 29 | "eslint-plugin-node": "^11.1.0", 30 | "eslint-plugin-promise": "^6.0.1", 31 | "pcf-scripts": "^1", 32 | "pcf-start": "^1", 33 | "typescript": "^4.9.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pcfconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "outDir": "./out/controls" 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/pcf-scripts/tsconfig_base.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "jsxFactory": "React.createElement", 6 | "typeRoots": ["node_modules/@types"] 7 | } 8 | } --------------------------------------------------------------------------------