Upload faster and smarter documents with PCF Control

Upload faster and smarter documents in a Model driven app with the Drag and drop File uploader PCF control

Do you remember the standard out of the box file uploader in a model driven app?

Yes, that one. Very time consuming when uploading 20 documents. Please read this blog when you want to learn how to create a drag and drop file uploader in a PCF control that can be used in a model driven app.

Summary of this blog:

  1. Dropzone from react-dropzone
  2. Multiple files listed
  3. Checking the list
  4. Files to upload list
  5. Create record(s) in Dataverse

Create annotation (note on timeline):

Create an activitymimeattachment (file to attachmentsGrid):

Why a drag and dropzone?

Using a drag and drop component in a PCF control (Power Apps Component Framework) can help with user experience inside a model driven app. The FileUploader PCF accepts drag and dropped files or can open files in the file explorer on click. It is a feature that users will directly identify and work with to quickly collect the needed files that can be accessed from Dataverse eventually.

For this PCF we will be using react so we can use the Microsoft FluentUI component library.

Set up your PCF, make sure React is installed and set up the ControlManifest:

Add two fields, one bound and the other as input. The bound field will be the one connected with the field in the model app. The other one will be an input for the name of the field we need to refresh after the upload. This will probably be: attachmentsGrid or Timeline.

Step 1: The Dropzone from React-dropzone

A Dropzone component can be created by using the react-dropzone npm package (npm install react-dropzone). You can add multiple files and get them listed below the dropzone component. This way you can easily see which files you have dragged to the dropzone and gives you the opportunity to adjust before pressing the upload button. When the list is considered complete, the upload event is fired off by pressing the upload button.

Create a new component and return this:

            <Dropzone onDropAccepted={acceptedFiles => handleDrop(acceptedFiles)}>
                {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => (
                    <Container {...getRootProps({ isDragActive, isDragAccept, isDragReject })}>
                        <input {...getInputProps()} />
                        <p>Select files...</p>
                    </Container>
                )}
            </Dropzone>

Step 2: Listing the items

When files have been added to the dropzone, they will be added to the React Hooks Context by the handleDrop function. This is created to keep track of the current files.

    const handleDrop = (acceptedFiles:File[]): void => {
        addAttachment(acceptedFiles);
    }

The addAttachment function is located within the FilesContext.

    const addAttachment = async (files: File[]) => {
        try {
            dispatch({ type: ADD_ATTACHMENT, payload: files });
        } catch (error) {
            console.error(error);
        }
    };

This will dispatch all the files to the reducer and merge them with the files already in the list.

When the context contains files, the files will be displayed in a FluentUI Detailslist below the dropzone.

{files && files.length > 0 &&
                <>
                    <Text variant={"xLarge"} block={true}>Files ready to upload</Text>
                    <DetailsList
                        items={files}
                        columns={columns}
                        compact={true}
                        constrainMode={0}
                        selectionMode={2}
                        checkboxVisibility={2}

                    />
                </>
            }

Step 3: Check if your list is complete and correct

You can delete items from the list if you’re not yet satisfied with the list of files. By pressing the delete icon behind the file the file will be removed from the context.

Step 4: Get your Files to the Upload list

A button should be added to the dropzone component which should handle the upload function. By pressing the “Upload” button, the handleUpload function will be fired to start the upload event. The upload event means that it will create records in Dataverse. It will create an “activitymimeattachment” based on whether the EntityTypeName of the current record is “email” or “appointment”. If it is not one of those two it will create an “annotation” record.

Step 5: Dataverse record creation

When the upload button is pressed the createAnnotation function will be fired. The function loops over all the files that are in the context. This function will create the correct objects for it to properly create Dataverse records. To create a record the file must be converted to a Base64 string so it can populate the “body” property correctly. For that, FileReader is used. FileReader lets us read, asynchronously, the contents of the files we have added.

        const toBase64 = async (file: any) => new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = () => resolve(reader.result);
            reader.onabort = () => reject();
            reader.onerror = error => reject(error);
        });

Then we can add that string to our object that will be used in the createRecord function of the WebApi together with some other properties.

        const createAnnotationRecord = async (file: File): Promise<ComponentFramework.WebApi.Entity | undefined> => {
            const base64Data: string | ArrayBuffer | null = await toBase64(file);

            if (typeof base64Data === 'string') {
                const base64: string = base64Data.replace(/^data:.+;base64,/, '');
                const entitySetName: string = await getMeta();

                const attachmentRecord: ComponentFramework.WebApi.Entity = {
                    filename: file.name,
                    objecttypecode: entity
                }

                if (isActivityMimeAttachment) {
                    attachmentRecord["objectid_activitypointer@odata.bind"] = `/activitypointers(${entityId})`
                    attachmentRecord["body"] = base64;
                } else {
                    attachmentRecord[`objectid_${entity}@odata.bind`] = `/${entitySetName}(${entityId})`;
                    attachmentRecord["documentbody"] = base64;
                }

                if (file.type && file.type !== "") {
                    attachmentRecord["mimetype"] = file.type;
                }

                return attachmentRecord
            }
        }

At the top, the entitySetName is retrieved since it is necessary for an “annotation”. It can be retrieved from the context, the following function will return it.

const getMeta = async () => context.utils.getEntityMetadata(entity).then((response) => {
            return response.EntitySetName;
        })

Now all the needed information has been collected and we can send the object to create the record.

        let successfulUploads: number = 0;
        let unsuccessfulUploads: number = 0;
        let finished = false;

        files.forEach(async (file: File, i: number) => {
            const annotation: ComponentFramework.WebApi.Entity | undefined = await createAnnotationRecord(file);

            if (annotation) {
                await webapi.createRecord(attachmentEntity, annotation).then((res) => {
                    if (res.id) {
                        dispatch({ type: REMOVE_ATTACHMENT, payload: file });
                        successfulUploads++;
                    }
                }).catch((err) => {
                    unsuccessfulUploads++;
                    console.error(err);
                }).finally(() => {
                    if (unsuccessfulUploads === 0 && successfulUploads === files.length) {
                        finished = true;
                    }
                    if(unsuccessfulUploads !== 0) {
                        console.error("There were unsuccessful uploads")
                    }
                })
            }

            if (finished) {
                const control = Xrm.Page.getControl(ioConnector.controlToRefresh);
                // @ts-ignore
                control.refresh();
                setMessage(successfulUploads);
            }
        })

After the record is created the context is cleared of the files.

We can refresh the control to which the files have been added.

const control = Xrm.Page.getControl(ioConnector.controlToRefresh);
control.refresh();

That is the process of creating attachment records in Dataverse. The files will be visible on the notes timeline or the attachmentsGrid of the current record.

Check the code on github for:

  • Styling the dropzone
  • Create an AppFunctionsContext
  • Create a FilesContext
  • Typing
  • Displaying a message when upload is done

If you want to use this File Uploader make sure to get if from the PCFGallery

I’d like to end this with a thanks to ramarao9 for the annotation/activitymimeattachment creation.

MORE RELEVANT NEWS

How to maximize the Power Platform

How to maximize the Power Platform

Integrate Power BI and Apps in your daily applications In the (Microsoft) Power Platform it’s becoming easier to combine different tools to get the most out of your data. That is also since we have more options for data...

read more
Increase productivity with the right integration

Increase productivity with the right integration

PowerApps Portal Part 2: Sharepoint Integration Part 1 of the PowerApps Portal series covered the collaboration with partners and customers. I briefly touched on the Sharepoint Integration and I will go into more detail in...

read more

Get in Touch

Accelerate your innovation capacity