Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion PCFControl.FileImport/FileImport/ControlManifest.Input.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="fi" constructor="FileImport" version="0.0.1" display-name-key="FileImport" description-key="FileImport_OutputFileContent" control-type="virtual" >
<control namespace="fi" constructor="FileImport" version="0.0.2" display-name-key="FileImport" description-key="FileImport_OutputFileContent" 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 All @@ -13,6 +13,8 @@
-->
<!-- property node identifies a specific, configurable piece of data that the control expects from CDS -->
<property name="ImportLabel" display-name-key="ImportButton_Display" description-key="ImportButton_Desc" of-type="SingleLine.Text" usage="input" required="false" />
<property name="ImportedLabel" display-name-key="ImportedButton_Display" description-key="ImportedButton_Desc" of-type="SingleLine.Text" usage="input" required="false" />
<property name="Reset" display-name-key="Reset_Display" description-key="Reset_Desc" of-type="TwoOptions" usage="input" required="false" />
<property name="errorMessage" display-name-key="ErrorMessage_Display" description-key="ErrorMessage_Desc" of-type="Multiple" usage="output" required="false" />

<!-- A hidden property used by Canvas to get the output object schema -->
Expand Down
66 changes: 45 additions & 21 deletions PCFControl.FileImport/FileImport/ImportFile.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,63 @@
import * as React from 'react';
import { DefaultButton, IIconProps } from '@fluentui/react';
import { IControlEvent } from './IControlEvent';
import { useState, createRef } from 'react';
import * as React from "react";
import { DefaultButton, IIconProps } from "@fluentui/react";
import { IControlEvent } from "./IControlEvent";
import { useState, createRef, useEffect } from "react";

export interface IImportProps {
buttonLabel: string | null;
importedLabel: string | null;
reset: boolean;
onEvent: (event: IControlEvent) => void;
}

const upload: IIconProps = { iconName: 'Upload' };
const uploaded: IIconProps = { iconName: 'Accept' };
const upload: IIconProps = { iconName: "Upload" };
const uploaded: IIconProps = { iconName: "Accept" };

export const ImportFile: React.FC<IImportProps> = (props: IImportProps) => {
const [imported, setImported] = useState<boolean>(false);
const importFileRef = createRef<HTMLInputElement>();

useEffect(() => {
if (props.reset) {
setImported(false);
}
}, [props.reset]);

const readFile = (file: File) => {
return new Promise((resolve, reject) => {
//create file reader
let reader = new FileReader();

reader.onerror = () => {
console.log(`Something Went wrong while file reading : ${reject}`);
}
};

reader.onloadend = () => {
resolve(reader.result);
}
};

//read file
reader.readAsDataURL(file);
});
};

const getAsByteArray = async (file: File) => {
let fileContent: string | null = (await readFile(file) as string | null);
return fileContent?.split(',')?.[1];
}
let fileContent: string | null = (await readFile(file)) as string | null;
return fileContent?.split(",")?.[1];
};

const onFileChange = async (event: any) => {
let fileSelected: File = event.target.files[0];
let fileContent = await getAsByteArray(fileSelected);

props.onEvent({
event: "ImportedFile", errorMessage: "", file: {
event: "ImportedFile",
errorMessage: "",
file: {
contentBytes: fileContent ?? "",
name: fileSelected?.name ?? ""
}
})
name: fileSelected?.name ?? "",
},
});

setImported(true);
};
Expand All @@ -56,9 +66,23 @@ export const ImportFile: React.FC<IImportProps> = (props: IImportProps) => {
<div>
<DefaultButton
onClick={() => importFileRef.current?.click()}
iconProps={imported ? uploaded : upload}>{
imported ? "File Imported" : (props.buttonLabel ? props.buttonLabel : "Import File")
}</DefaultButton>
<input ref={importFileRef} type="file" onChange={onFileChange} key={Math.random().toString(16)} style={{ display: 'none' }} />
</div>);
}
iconProps={imported ? uploaded : upload}
>
{imported
? props.importedLabel
? props.importedLabel
: "File Imported"
: props.buttonLabel
? props.buttonLabel
: "Import File"}
</DefaultButton>
<input
ref={importFileRef}
type="file"
onChange={onFileChange}
key={Math.random().toString(16)}
style={{ display: "none" }}
/>
</div>
);
};
118 changes: 64 additions & 54 deletions PCFControl.FileImport/FileImport/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,76 @@ import { IImportProps, ImportFile } from "./ImportFile";
import { IControlEvent } from "./IControlEvent";
import { FileSchema } from "./FileSchema";

export class FileImport implements ComponentFramework.ReactControl<IInputs, IOutputs> {
private notifyOutputChanged: () => void;
export class FileImport
implements ComponentFramework.ReactControl<IInputs, IOutputs>
{
private notifyOutputChanged: () => void;

private controlEvent: IControlEvent = { event: "None", errorMessage: "" }
private controlEvent: IControlEvent = { event: "None", errorMessage: "" };

constructor() {
this.onEvent = this.onEvent.bind(this);
}
constructor() {
this.onEvent = this.onEvent.bind(this);
}

/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @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.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @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.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary): void {
this.notifyOutputChanged = notifyOutputChanged;
}
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @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.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @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.
*/
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary
): void {
this.notifyOutputChanged = notifyOutputChanged;
}

/**
* 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.
* @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
* @returns ReactElement root react element for the control
*/
public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
const props: IImportProps = {
buttonLabel: context.parameters.ImportLabel.raw,
onEvent: this.onEvent
};
return React.createElement(
ImportFile, props
);
}
/**
* 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.
* @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
* @returns ReactElement root react element for the control
*/
public updateView(
context: ComponentFramework.Context<IInputs>
): React.ReactElement {
const props: IImportProps = {
buttonLabel: context.parameters.ImportLabel.raw,
importedLabel: context.parameters.ImportedLabel.raw,
reset: context.parameters.Reset.raw ?? false,
onEvent: this.onEvent,
};
return React.createElement(ImportFile, props);
}

/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs {
return this.controlEvent;
}
/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs {
return this.controlEvent;
}

public async getOutputSchema(context: ComponentFramework.Context<IInputs>): Promise<any> {
console.log(context);
return Promise.resolve({
file: FileSchema
});
}
public async getOutputSchema(
context: ComponentFramework.Context<IInputs>
): Promise<any> {
console.log(context);
return Promise.resolve({
file: FileSchema,
});
}

/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void {
// Add code to cleanup control if necessary
}
/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void {
// Add code to cleanup control if necessary
}

private onEvent(event: IControlEvent): void {
this.controlEvent = event;
this.notifyOutputChanged();
}
private onEvent(event: IControlEvent): void {
this.controlEvent = event;
this.notifyOutputChanged();
}
}
45 changes: 25 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ This makes it simple to capture files and pass them to Power Automate or Dataver

⚡ Properties
Input Properties
Property Name Type Usage Description
ImportLabel SingleLine.Text Input Label for the button (e.g., "Upload File"). Defaults to Import File if not set.
FileSchema SingleLine.Text Input Hidden schema definition used internally for Canvas/Power Apps integration.
Property Name Type Usage Description
ImportLabel SingleLine.Text Input Label for the button (e.g., "Upload File"). Defaults to Import File if not set.
ImportedLabel SingleLine.Text Input Label for the button after upload (e.g., "File Uploaded"). Defaults to File Imported File if not set.
Reset Boolean Input Resets the control to the initial state to show Import File again.

Output Properties
Property Name Type Usage Description
errorMessage Multiple Output Any error messages related to file reading.
file Object Output Returns uploaded file details (see schema below).
Property Name Type Usage Description
errorMessage Multiple Output Any error messages related to file reading.
file Object Output Returns uploaded file details (see schema below).
FileSchema SingleLine.Text Output Hidden schema definition used internally for Canvas/Power Apps integration.

📜 File Schema

The control returns a file object in the following schema (same as Power Automate file input):

```
{
"description": "Please select file or image",
Expand All @@ -45,36 +49,37 @@ The control returns a file object in the following schema (same as Power Automat
"x-ms-dynamically-added": true
}
```

Example returned object:

```
{
"contentBytes": "JVBERi0xLjQKJcTl8uXr... (Base64)",
"name": "Invoice.pdf"
}
```

📸 Demo <br/>
<img width="305" height="100" alt="image" src="https://github.com/user-attachments/assets/5f11cbf2-0322-41e7-a465-e67138c9becf" /> <br/>

1. UI Example:
* Button before upload: "Import File"
* Button after upload: "File Imported ✅"

- Button before upload: "Import File"
- Button after upload: "File Imported ✅"
2. Power Automate Example:
Use file.contentBytes and file.name directly in flow actions such as:
* Create file in SharePoint
* Upload file in Azure Blob
* Send an email with attachment
* Store in Dataverse File column
Use file.contentBytes and file.name directly in flow actions such as:
_ Create file in SharePoint
_ Upload file in Azure Blob
_ Send an email with attachment
_ Store in Dataverse File column

🎮 Usage

1. Add the control to a Canvas App / Custom Page
- Insert the PCF control from the list of components.
- Set the ImportLabel property (optional).
- Insert the PCF control from the list of components.
- Set the ImportLabel and ImportedLabel properties (optional).
2. Capture the file object
When a user uploads a file, the file output property will contain:
- name → File name (e.g., invoice.pdf)
- contentBytes → File content as Base64
When a user uploads a file, the file output property will contain: - name → File name (e.g., invoice.pdf) - contentBytes → File content as Base64
3. Send to Power Automate You can pass the file object directly to a Power Automate flow that expects a file input.
4. Optionally set Reset property to true and then false to show the original button message again (though the button is ready to upload another file without this)

🔥 With this control, uploading files into Canvas Apps / Custom Pages becomes seamless and Power Automate-ready.