Upload and process files
Use the UiSdlFileUpload component to let users upload one or more files and validate the input securely. You can configure file type restrictions, size limits, and visual messages. Once the user selects a file, you can process the content using a TypeScript epic registered through metadata.
Create the upload UI configuration
Define the UI component using UiSdlFileUpload. Set allowableFileExtensions and fileSizeLimit to ensure file-level validation before upload. Use effectTriggers to call a custom upload epic after the user queues files.
The JSON example configuration below tells the platform how to collect files and trigger an epic when the user uploads them. The effectTriggers field defines when the platform runs your file upload logic.
// {package}/ui/c3/meta/ExamplePackage.SimpleFileUpload.json
{
"type": "UiSdlConnected<UiSdlFileUpload>", // Connects the file upload component to Redux
"component": {
"bodyMessage": {
"subText": "Select a CSV file to upload" // Optional UI hint below the file input
},
"multiSelect": false, // Only allow the user to select one file at a time
"fileSizeLimit": 1000000000, // Set file size limit to 1 GB
"allowableFileExtensions": [
".csv" // Only allow .csv files to be uploaded
]
},
"effectTriggers": [
{
"trigger": "FILE_QUEUED", // Trigger event when a file is added
"effectType": "EpicUploadFiles", // Connect to your TypeScript file logic (EpicUploadFiles.ts)
"payloadStrategy": "MERGE", // Merge payload into dispatched actions
"payload": {
"componentId": "ExamplePackage.SimpleFileUpload" // Identify which component dispatched the action
}
}
]
}When the user selects a .csv file, the platform queues the file and dispatches a FILE_QUEUED event. This event triggers your custom epic (EpicUploadFiles) and hands it the file content to upload.
Register the epic
Create a .c3typ file to declare the epic Type. This file declares the Type for the epic. It links your TypeScript file to the platform so the system can trigger your upload logic when files are queued. The platform uses this to register the epic and link it to the frontend logic.
// {package}/src/epics/EpicUploadFiles.c3typ
/**
* Epic to read and upload multiple files based on the input of a `UiSdlFileUpload` component.
*/
@typeScript
type EpicUploadFiles mixes UiSdlEpic {
/**
* Asynchronously reads the content of a file and transforms it into the right format.
* This function reads a file as a binary ArrayBuffer and wraps it in a typed Uint8Array.
*
* @param file The binary file selected in the browser (a `File` object)
* @param filename The original file name
*/
readFile: private inline function(file: File, filename: string) ts-client
/**
* Sends a POST request to upload the file content to the server.
*
* @param inputData
* - filename: The file name to upload
* - contentType: The MIME type of the file
* - event: The binary file content, passed after reading
*/
uploadFile: private inline function(inputData: any) ts-client
}![NOTE] Without this
.c3typfile, the platform cannot register the associated TypeScript file as a valid epic. Think of it as a contract that says “a real function lives here.”
Implement the file upload logic
This logic below handles the entire upload lifecycle in one place. It responds to a file selection event, reads the file content, sends it to the server, updates the UI with success or error status, and populates hidden form fields. All logic runs inside an epic that listens for Redux actions triggered by UiSdlFileUpload.
The upload logic includes three key functions:
readFile: Reads file content as binaryuploadFile: Sends the file to the backendepic: Orchestrates reading, uploading, and updating the UI
When a user selects files:
- The code reads each file’s content in the browser (without sending it to the server yet).
- It uploads each file using an HTTP POST request.
- It waits for all uploads to complete.
- It updates hidden form fields with the file names and their download URLs.
- It updates the file status in the UI so the user sees
UiSdlFileUploadFileStatus.SUCCESSFULorUiSdlFileUploadFileStatus.ERROR. - If a file extension is not allowed, it skips that file and removes it from the list.
Load platform tools and libraries
This import section loads everything the epic needs to read files, make HTTP requests, interact with the Redux state, and trigger UI updates.
// {package}/src/epics/EpicUploadFiles.ts
// Import lodash function to safely access deeply nested object properties
import get from "lodash/get";
// Import RxJS core tools for building and combining observables
import {
EMPTY, // Emits no items and immediately completes — used when no work should be done
concat, // Runs multiple observables in order
of, // Creates an observable from static data
from, // Converts a Promise to an observable
combineLatest, // Waits for all observables to emit, then emits the latest values together
ObservableInput, // Type definition for what an observable can consume
Observable, // Observable class type
} from "rxjs";
// Import RxJS operators to transform observable data
import { mergeMap, catchError } from "rxjs/operators";
// RxJS function for making AJAX HTTP requests
import { ajax as rxAjax } from "rxjs/ajax";
// Redux utility for reading form or component state from Redux
import { ImmutableReduxState, getConfigFromState } from "@c3/ui/UiSdlConnected";
// Action to update a form field value (usually for hidden fields)
import { inputChangeAction } from "@c3/ui/UiSdlFormBase";
// Actions for updating and removing uploaded files
import {
updateFileUploadStatusAction,
removeFilesAction,
} from "@c3/ui/UiSdlFileUpload";
import UiSdlFileUploadFileStatus from '@c3/ui/UiSdlFileUploadFileStatus';
// Form actions to change the button state or read current field values
import {
changeActionButtonStateAction,
getFormFieldValuesFromState,
} from "@c3/ui/UiSdlForm";
// Epic type definition from redux-observable
import { Epic } from "redux-observable";
// Basic Redux action type
import { AnyAction } from "redux";
// Utility for resolving the base application URL
import appUrl from "@c3/app/ui/src/appUrl";Read the file in binary format
The readFile function uses the browser’s FileReader API to read file content as binary. The epic calls this before uploading each file.
/**
* Reads a binary file and converts it into a format (Uint8Array) that can be uploaded.
* @param file The File or Blob object selected by the user
* @param filename The original name of the file
* @returns A Promise that resolves with the filename and the file content in binary
*/
export function readFile(file: Blob, filename: any) {
return new Promise(function (resolve, reject) {
const fileReader = new FileReader(); // Create a browser FileReader instance
fileReader.readAsArrayBuffer(file); // Load the file as raw binary data
fileReader.onload = () => {
resolve({
filename: filename, // Keep the original filename
content: new Uint8Array(fileReader.result), // Wrap the binary data in a typed array
});
};
fileReader.onerror = reject; // If the read fails, reject the promise
});
}Upload the file to the backend
The uploadFile function uses rxjs/ajax to send a POST request. It constructs the upload URL using the platform’s appUrl and sends the binary content from readFile.
/**
* Uploads file content to the server using a POST request.
* @param inputData Object containing the file name, MIME type, binary content, and related metadata
* @param entity Optional entity name used for building the URL
* @returns Observable that emits either a success or failure response
*/
export function uploadFile(
inputData: {
action?: AnyAction;
state?: ImmutableReduxState;
filename: any;
contentType: any;
componentId?: any;
event: any;
},
entity: undefined
) {
const { filename, contentType, event } = inputData;
// Build the upload URL based on the current app path and optional entity
const currentAppUrl = appUrl
? `${appUrl.replace(location.origin + "/", "/")}`
: "";
const apiUrl = `${currentAppUrl}/file/${entity || ""}`; // Base upload endpoint
const fileUrl = `Attachment/${filename.replace(/\s/g, "")}`; // Strip spaces from filename
// Send the file content via POST using RxJS AJAX
return rxAjax({
url: apiUrl + fileUrl,
method: "POST",
headers: {
"Content-Type": contentType, // Set the correct MIME type
},
body: event.content, // Attach the file content as the body
withCredentials: true, // Include session cookies if needed
}).pipe(
mergeMap((response) => {
// Return structured success metadata
return of({
status: response.status,
apiUrl,
fileUrl,
filename,
});
}),
catchError(function (error) {
// Return error metadata
return of({
status: error.status,
apiUrl,
fileUrl,
filename,
});
})
);
}Orchestrate the full upload flow
The epic listens for the FILE_QUEUED action and starts the upload process. It skips invalid files, reads and uploads each one, and updates the form and file status.
/**
* Epic that:
* - Reads each file the user selects
* - Uploads each file
* - Updates the UI with upload status and form fields
*/
export const epic: Epic<AnyAction, any, ImmutableReduxState> = (
actionStream,
stateStream
) => {
return actionStream.pipe(
// Handle each action one at a time
mergeMap(function (action) {
const payload = action.payload;
// Safely extract the uploaded files from the action payload
const files = get(action, "payload.files");
// Exit early if there are no files to process
if (!files || files.length === 0) {
return EMPTY;
}
// Get the current Redux state snapshot
const state = stateStream.value;
// Get the ID of the file upload component that dispatched the action
const componentId = get(payload, "componentId");
// Extract the form ID to update form field values later
const formId = payload?.formId;
// Prepare an array to collect all file upload observables
const actionFiles: ObservableInput<any>[] = [];
// Read from Redux the list of files with invalid extensions (if any)
const invalidExtensionFiles =
getConfigFromState(componentId, state, ["invalidExtensionFiles"])
?.toJS()
.map((val: { name: any }) => val.name) || [];
// Loop through each file submitted by the user
files.forEach((file: { name: any; type: any }) => {
const filename = file.name;
// Skip the file if its name appears in the invalid extensions list
if (invalidExtensionFiles.includes(filename)) return;
const contentType = file.type;
// Step 1: Read the file into memory as binary
// Step 2: Pass the binary content to the upload function
actionFiles.push(
from(readFile(file, filename)).pipe(
mergeMap(function (event) {
const inputData = {
action,
state,
filename,
contentType,
componentId,
event,
};
return uploadFile(inputData); // Returns an observable with status and metadata
})
)
);
});
// Wait for all file uploads to complete and gather results
return combineLatest(actionFiles).pipe(
mergeMap(function (response) {
// Read existing form field values (if any)
const currentState = getFormFieldValuesFromState(formId, state) || {};
const filenamePayloads = currentState.filename ? [currentState.filename] : [];
const urlPayloads = currentState.fileUrl ? [currentState.fileUrl] : [];
// These payloads will be written into hidden form fields
const filenamePayload = { field: "filename" };
const urlPayload = { field: "fileUrl" };
// Prepare a list of Redux actions to dispatch
const actions: Observable<any>[] = [];
// Iterate through each uploaded file response
response.forEach((file) => {
const filename = file.filename;
// Add the filename to the running list
filenamePayloads.push(filename);
// Compute the final URL for the file (with or without full path)
let currentUrl = file.fileUrl;
if (payload?.includeFullUrl) {
currentUrl = file.apiUrl + file.fileUrl;
}
// Add the URL to the list
urlPayloads.push(currentUrl);
// Create an action that updates the status in the UI (SUCCESS or ERROR)
actions.push(
of(
updateFileUploadStatusAction(
componentId,
[filename],
file.status === 200
? UiSdlFileUploadFileStatus.SUCCESSFUL
: UiSdlFileUploadFileStatus.ERROR
)
)
);
});
// Set the joined filenames and URLs as semicolon-delimited strings
filenamePayload.value = filenamePayloads.join(";");
urlPayload.value = urlPayloads.join(";");
return concat(
// Dispatch all status update actions
...actions,
// Update hidden form fields with the list of uploaded filenames and URLs
of(inputChangeAction(formId, null, filenamePayload)),
of(inputChangeAction(formId, null, urlPayload)),
// Reset the primary form button to its active state
of(changeActionButtonStateAction(formId, "PRIMARY", null, false)),
// If any files were invalid, remove them from the UI
invalidExtensionFiles.length
? of(removeFilesAction(componentId, invalidExtensionFiles))
: EMPTY
);
}),
catchError(function () {
// If upload fails, re-enable the primary button so the user can try again
return of(changeActionButtonStateAction(formId, "PRIMARY", false, false));
})
);
}),
catchError(function (error) {
// Rethrow unexpected errors for logging or higher-level handling
throw error;
})
);
};To check the file using a URL, you can use the following format:
https://<cluster-url>/<myenv>/<myapp>/file/<your file name>.csvReplace <cluster-url>, <myenv>, and <myapp> with your actual environment details.