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 | 
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 | 
26 |
27 |
28 | When the record is not yet created, you would see the below
29 |
30 | 
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 | 
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 | }
--------------------------------------------------------------------------------