diff --git a/PCFControl.FileImport/FileImport/ControlManifest.Input.xml b/PCFControl.FileImport/FileImport/ControlManifest.Input.xml index c66a7fb..201e9fa 100644 --- a/PCFControl.FileImport/FileImport/ControlManifest.Input.xml +++ b/PCFControl.FileImport/FileImport/ControlManifest.Input.xml @@ -1,6 +1,6 @@ - + + + diff --git a/PCFControl.FileImport/FileImport/ImportFile.tsx b/PCFControl.FileImport/FileImport/ImportFile.tsx index 4b51172..ff412fd 100644 --- a/PCFControl.FileImport/FileImport/ImportFile.tsx +++ b/PCFControl.FileImport/FileImport/ImportFile.tsx @@ -1,20 +1,28 @@ -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 = (props: IImportProps) => { const [imported, setImported] = useState(false); const importFileRef = createRef(); + useEffect(() => { + if (props.reset) { + setImported(false); + } + }, [props.reset]); + const readFile = (file: File) => { return new Promise((resolve, reject) => { //create file reader @@ -22,11 +30,11 @@ export const ImportFile: React.FC = (props: IImportProps) => { reader.onerror = () => { console.log(`Something Went wrong while file reading : ${reject}`); - } + }; reader.onloadend = () => { resolve(reader.result); - } + }; //read file reader.readAsDataURL(file); @@ -34,20 +42,22 @@ export const ImportFile: React.FC = (props: IImportProps) => { }; 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); }; @@ -56,9 +66,23 @@ export const ImportFile: React.FC = (props: IImportProps) => {
importFileRef.current?.click()} - iconProps={imported ? uploaded : upload}>{ - imported ? "File Imported" : (props.buttonLabel ? props.buttonLabel : "Import File") - } - -
); -} + iconProps={imported ? uploaded : upload} + > + {imported + ? props.importedLabel + ? props.importedLabel + : "File Imported" + : props.buttonLabel + ? props.buttonLabel + : "Import File"} + + + + ); +}; diff --git a/PCFControl.FileImport/FileImport/index.ts b/PCFControl.FileImport/FileImport/index.ts index 3e40b20..dc4c05c 100644 --- a/PCFControl.FileImport/FileImport/index.ts +++ b/PCFControl.FileImport/FileImport/index.ts @@ -4,66 +4,76 @@ import { IImportProps, ImportFile } from "./ImportFile"; import { IControlEvent } from "./IControlEvent"; import { FileSchema } from "./FileSchema"; -export class FileImport implements ComponentFramework.ReactControl { - private notifyOutputChanged: () => void; +export class FileImport + implements ComponentFramework.ReactControl +{ + 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, 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, + 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): 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 + ): 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): Promise { - console.log(context); - return Promise.resolve({ - file: FileSchema - }); - } + public async getOutputSchema( + context: ComponentFramework.Context + ): Promise { + 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(); + } } diff --git a/README.md b/README.md index 0d8f737..f4adbd2 100644 --- a/README.md +++ b/README.md @@ -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", @@ -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
image
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. -