diff --git a/package-lock.json b/package-lock.json
index 77ea639..ae970c6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,13 @@
{
- "name": "rcb-plugin",
+ "name": "@rcb-plugins/input-validator",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "rcb-plugin",
+ "name": "@rcb-plugins/input-validator",
"version": "0.1.1",
+ "license": "MIT",
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/react": "^18.3.10",
diff --git a/src/App.tsx b/src/App.tsx
index 760f700..202451d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,36 +2,61 @@ import ChatBot, { Flow } from "react-chatbotify";
import RcbPlugin from "./factory/RcbPluginFactory";
import { InputValidatorBlock } from "./types/InputValidatorBlock";
+import { validateFile } from "./utils/validateFile";
const App = () => {
- // initialize example plugin
- const plugins = [RcbPlugin()];
+ // Initialize the plugin
+ const plugins = [RcbPlugin()];
- // example flow for testing
- const flow: Flow = {
- start: {
- message: "Hey there, please enter your age!",
- path: "try_again",
- validateInput: (userInput: string) => {
- if (typeof userInput === "string" && !Number.isNaN(Number(userInput))) {
- return {success: true};
- }
- return {success: false, promptContent: "Age must be a number!", promptDuration: 3000, promptType: "error", highlightTextArea: true};
- }
- } as InputValidatorBlock,
- try_again : {
- message: "Nice, you passed the input validation!",
- path: "start",
- }
- }
+ // Example flow for testing
+ const flow: Flow = {
+ start: {
+ message: "Hey there! Please enter your age.",
+ path: "age_validation",
+ validateTextInput: (userInput?: string) => {
+ if (userInput && !Number.isNaN(Number(userInput))) {
+ return { success: true };
+ }
+ return {
+ success: false,
+ promptContent: "Age must be a number!",
+ promptDuration: 3000,
+ promptType: "error",
+ highlightTextArea: true,
+ };
+ },
+ } as InputValidatorBlock,
- return (
-
- );
-}
+ age_validation: {
+ message:
+ "Great! Now please upload a profile picture (JPEG or PNG) or provide a URL.",
+ path: "file_upload_validation",
+ chatDisabled: true, // Text input is disabled
+ validateFileInput: (file?: File) => {
+ return validateFile(file); // Validation is handled here
+ },
+ file: async ({ files }) => {
+ console.log("Files received:", files);
+
+ if (files && files[0]) {
+ console.log("File uploaded successfully:", files[0]);
+ } else {
+ console.error("No file provided.");
+ }
+ },
+ } as InputValidatorBlock,
+
-export default App;
\ No newline at end of file
+ file_upload_validation: {
+ message:
+ "Thank you! Your input has been received. You passed the validation!",
+ path: "start",
+ },
+ };
+
+ return (
+
+ );
+};
+
+export default App;
diff --git a/src/core/useRcbPlugin.ts b/src/core/useRcbPlugin.ts
index b8798ac..16a3daf 100644
--- a/src/core/useRcbPlugin.ts
+++ b/src/core/useRcbPlugin.ts
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
import {
useBotId,
RcbUserSubmitTextEvent,
+ RcbUserUploadFileEvent,
useToasts,
useFlow,
useStyles,
@@ -31,67 +32,116 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => {
useEffect(() => {
/**
- * Handles the user submitting input event.
+ * Handles the user submitting text input event.
*
- * @param event event emitted when user submits input
+ * @param event Event emitted when user submits text input.
*/
- const handleUserSubmitText = (event: RcbUserSubmitTextEvent): void => {
- // gets validator and if no validator, return
- const validator = getValidator(event, getBotId(), getFlow());
+ const handleUserSubmitText = (event: Event): void => {
+ const rcbEvent = event as RcbUserSubmitTextEvent;
+
+ // Get validator and if no validator, return
+ const validator = getValidator(
+ rcbEvent,
+ getBotId(),
+ getFlow(),
+ "validateTextInput"
+ );
if (!validator) {
return;
}
- // gets and checks validation result
+ // Get and check validation result
const validationResult = validator(
- event.data.inputText
+ rcbEvent.data.inputText
) as ValidationResult;
if (!validationResult?.success) {
event.preventDefault();
}
- // if nothing to prompt, return
+ // If nothing to prompt, return
if (!validationResult.promptContent) {
return;
}
- // if this is the first plugin toast, preserve original styles for restoration later
+ // Preserve original styles if this is the first plugin toast
if (numPluginToasts === 0) {
- originalStyles.current = structuredClone(styles)
+ originalStyles.current = structuredClone(styles);
}
const promptStyles = getPromptStyles(
validationResult,
mergedPluginConfig
);
- // update toast with prompt styles
+ // Update styles with prompt styles
updateStyles(promptStyles);
- // shows prompt toast to user
+ // Show prompt toast to user
showToast(
validationResult.promptContent,
validationResult.promptDuration ?? 3000
);
- // increases number of plugin toasts by 1
+ // Increase number of plugin toasts by 1
setNumPluginToasts((prev) => prev + 1);
};
+ const handleUserUploadFile = (event: Event): void => {
+ const rcbEvent = event as RcbUserUploadFileEvent;
+ const file: File | undefined = rcbEvent.data?.files?.[0];
+
+ if (!file) {
+ console.error("No file uploaded.");
+ event.preventDefault();
+ return;
+ }
+
+ const validator = getValidator(
+ rcbEvent,
+ getBotId(),
+ getFlow(),
+ "validateFileInput"
+ );
+
+ if (!validator) {
+ console.error("Validator not found for file input.");
+ return;
+ }
+
+ const validationResult = validator(file);
+
+ if (!validationResult.success) {
+ console.error("Validation failed:", validationResult);
+ if (validationResult.promptContent) {
+ showToast(
+ validationResult.promptContent,
+ validationResult.promptDuration ?? 3000
+ );
+ }
+ event.preventDefault();
+ return;
+ }
+
+ console.log("Validation successful:", validationResult);
+ };
+
/**
* Handles the dismiss toast event.
*
- * @param event event emitted when toast is dismissed
+ * @param event Event emitted when toast is dismissed.
*/
const handleDismissToast = (): void => {
setNumPluginToasts((prev) => prev - 1);
};
- // adds required events
+ // Add required event listeners
window.addEventListener("rcb-user-submit-text", handleUserSubmitText);
+ window.addEventListener("rcb-user-upload-file", handleUserUploadFile);
window.addEventListener("rcb-dismiss-toast", handleDismissToast);
return () => {
+ // Remove event listeners
window.removeEventListener("rcb-user-submit-text", handleUserSubmitText);
+ window.removeEventListener("rcb-user-upload-file", handleUserUploadFile);
window.removeEventListener("rcb-dismiss-toast", handleDismissToast);
};
}, [
@@ -101,28 +151,29 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => {
updateStyles,
styles,
mergedPluginConfig,
- numPluginToasts
+ numPluginToasts,
]);
- // restores original styles when plugin toasts are all dismissed
+ // Restore original styles when all plugin toasts are dismissed
useEffect(() => {
if (numPluginToasts === 0) {
setTimeout(() => {
replaceStyles(originalStyles.current);
});
}
- }, [numPluginToasts, replaceStyles, originalStyles]);
+ }, [numPluginToasts, replaceStyles]);
- // initializes plugin metadata with plugin name
+ // Initialize plugin metadata with plugin name
const pluginMetaData: ReturnType = {
- name: "@rcb-plugins/input-validator"
+ name: "@rcb-plugins/input-validator",
};
- // adds required events in settings if auto config is true
+ // Add required events in settings if autoConfig is true
if (mergedPluginConfig.autoConfig) {
pluginMetaData.settings = {
event: {
rcbUserSubmitText: true,
+ rcbUserUploadFile: true,
rcbDismissToast: true,
},
};
diff --git a/src/types/InputValidatorBlock.ts b/src/types/InputValidatorBlock.ts
index ffb923d..f93fbc7 100644
--- a/src/types/InputValidatorBlock.ts
+++ b/src/types/InputValidatorBlock.ts
@@ -2,8 +2,9 @@ import { Block } from "react-chatbotify";
import { ValidationResult } from "./ValidationResult";
/**
- * Extends the Block from React ChatBotify to support inputValidator attribute.
+ * Extends the Block from React ChatBotify to support inputValidator attributes.
*/
export type InputValidatorBlock = Block & {
- validateInput: (userInput?: string) => ValidationResult;
-};
\ No newline at end of file
+ validateTextInput?: (userInput?: string) => ValidationResult;
+ validateFileInput?: (files?: FileList) => ValidationResult; // Accepts multiple files
+};
diff --git a/src/utils/getValidator.ts b/src/utils/getValidator.ts
index e61f27d..b217158 100644
--- a/src/utils/getValidator.ts
+++ b/src/utils/getValidator.ts
@@ -1,15 +1,27 @@
-import { Flow, RcbUserSubmitTextEvent } from "react-chatbotify";
+import { Flow, RcbUserSubmitTextEvent, RcbUserUploadFileEvent } from "react-chatbotify";
import { InputValidatorBlock } from "../types/InputValidatorBlock";
+import { ValidationResult } from "../types/ValidationResult";
/**
- * Retrieves the validator function and returns null if not applicable.
+ * Union type for user events that can be validated.
*/
-export const getValidator = (event: RcbUserSubmitTextEvent, currBotId: string | null, currFlow: Flow) => {
- if (currBotId !== event.detail.botId) {
- return;
- }
-
- if (!event.detail.currPath) {
+type RcbUserEvent = RcbUserSubmitTextEvent | RcbUserUploadFileEvent;
+
+/**
+ * Retrieves the validator function from the current flow block.
+ *
+ * @param event The event emitted by the user action (text submission or file upload).
+ * @param currBotId The current bot ID.
+ * @param currFlow The current flow object.
+ * @returns The validator function if it exists, otherwise undefined.
+ */
+export const getValidator = (
+ event: RcbUserEvent,
+ currBotId: string | null,
+ currFlow: Flow,
+ validatorType: "validateTextInput" | "validateFileInput" = "validateTextInput"
+): ((input: T) => ValidationResult) | undefined => {
+ if (!event.detail?.currPath || currBotId !== event.detail.botId) {
return;
}
@@ -18,12 +30,6 @@ export const getValidator = (event: RcbUserSubmitTextEvent, currBotId: string |
return;
}
- const validator = currBlock.validateInput;
- const isValidatorFunction =
- validator && typeof validator === "function";
- if (!isValidatorFunction) {
- return;
- }
-
- return validator;
-}
\ No newline at end of file
+ const validator = currBlock[validatorType] as ((input: T) => ValidationResult) | undefined;
+ return typeof validator === "function" ? validator : undefined;
+};
diff --git a/src/utils/validateFile.ts b/src/utils/validateFile.ts
new file mode 100644
index 0000000..001d801
--- /dev/null
+++ b/src/utils/validateFile.ts
@@ -0,0 +1,54 @@
+import { ValidationResult } from "../types/ValidationResult";
+
+/**
+ * Validates uploaded files.
+ * Ensures each file is of an allowed type and size, and rejects invalid inputs.
+ */
+export const validateFile = (input?: File | FileList): ValidationResult => {
+ const allowedTypes = ["image/jpeg", "image/png"];
+ const maxSizeInBytes = 5 * 1024 * 1024; // 5MB
+ const files: File[] = input instanceof FileList ? Array.from(input) : input ? [input] : [];
+
+ // Check if no files are provided
+ if (files.length === 0) {
+ return {
+ success: false,
+ promptContent: "No files uploaded.",
+ promptDuration: 3000,
+ promptType: "error",
+ };
+ }
+
+ // Validate each file
+ for (const file of files) {
+ // Check if the file is empty
+ if (file.size === 0) {
+ return {
+ success: false,
+ promptContent: `The file "${file.name}" is empty. Please upload a valid file.`,
+ promptDuration: 3000,
+ promptType: "error",
+ };
+ }
+
+ // Validate file type
+ if (!allowedTypes.includes(file.type)) {
+ return {
+ success: false,
+ promptContent: `The file "${file.name}" is not a valid type. Only JPEG or PNG files are allowed.`,
+ promptType: "error",
+ };
+ }
+
+ // Validate file size
+ if (file.size > maxSizeInBytes) {
+ return {
+ success: false,
+ promptContent: `The file "${file.name}" exceeds the 5MB size limit.`,
+ promptType: "error",
+ };
+ }
+ }
+
+ return { success: true };
+};