Skip to content

Commit 9a2b04d

Browse files
authored
Merge pull request #2 from tasbi03/validation
Validation
2 parents 4aeb57d + 7773d34 commit 9a2b04d

File tree

6 files changed

+209
-71
lines changed

6 files changed

+209
-71
lines changed

package-lock.json

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

+53-28
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,61 @@ import ChatBot, { Flow } from "react-chatbotify";
22

33
import RcbPlugin from "./factory/RcbPluginFactory";
44
import { InputValidatorBlock } from "./types/InputValidatorBlock";
5+
import { validateFile } from "./utils/validateFile";
56

67
const App = () => {
7-
// initialize example plugin
8-
const plugins = [RcbPlugin()];
8+
// Initialize the plugin
9+
const plugins = [RcbPlugin()];
910

10-
// example flow for testing
11-
const flow: Flow = {
12-
start: {
13-
message: "Hey there, please enter your age!",
14-
path: "try_again",
15-
validateInput: (userInput: string) => {
16-
if (typeof userInput === "string" && !Number.isNaN(Number(userInput))) {
17-
return {success: true};
18-
}
19-
return {success: false, promptContent: "Age must be a number!", promptDuration: 3000, promptType: "error", highlightTextArea: true};
20-
}
21-
} as InputValidatorBlock,
22-
try_again : {
23-
message: "Nice, you passed the input validation!",
24-
path: "start",
25-
}
26-
}
11+
// Example flow for testing
12+
const flow: Flow = {
13+
start: {
14+
message: "Hey there! Please enter your age.",
15+
path: "age_validation",
16+
validateTextInput: (userInput?: string) => {
17+
if (userInput && !Number.isNaN(Number(userInput))) {
18+
return { success: true };
19+
}
20+
return {
21+
success: false,
22+
promptContent: "Age must be a number!",
23+
promptDuration: 3000,
24+
promptType: "error",
25+
highlightTextArea: true,
26+
};
27+
},
28+
} as InputValidatorBlock,
2729

28-
return (
29-
<ChatBot
30-
id="chatbot-id"
31-
plugins={plugins}
32-
flow={flow}
33-
></ChatBot>
34-
);
35-
}
30+
age_validation: {
31+
message:
32+
"Great! Now please upload a profile picture (JPEG or PNG) or provide a URL.",
33+
path: "file_upload_validation",
34+
chatDisabled: true, // Text input is disabled
35+
validateFileInput: (file?: File) => {
36+
return validateFile(file); // Validation is handled here
37+
},
38+
file: async ({ files }) => {
39+
console.log("Files received:", files);
40+
41+
if (files && files[0]) {
42+
console.log("File uploaded successfully:", files[0]);
43+
} else {
44+
console.error("No file provided.");
45+
}
46+
},
47+
} as InputValidatorBlock,
48+
3649

37-
export default App;
50+
file_upload_validation: {
51+
message:
52+
"Thank you! Your input has been received. You passed the validation!",
53+
path: "start",
54+
},
55+
};
56+
57+
return (
58+
<ChatBot id="chatbot-id" plugins={plugins} flow={flow}></ChatBot>
59+
);
60+
};
61+
62+
export default App;

src/core/useRcbPlugin.ts

+72-21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
22
import {
33
useBotId,
44
RcbUserSubmitTextEvent,
5+
RcbUserUploadFileEvent,
56
useToasts,
67
useFlow,
78
useStyles,
@@ -31,67 +32,116 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => {
3132

3233
useEffect(() => {
3334
/**
34-
* Handles the user submitting input event.
35+
* Handles the user submitting text input event.
3536
*
36-
* @param event event emitted when user submits input
37+
* @param event Event emitted when user submits text input.
3738
*/
38-
const handleUserSubmitText = (event: RcbUserSubmitTextEvent): void => {
39-
// gets validator and if no validator, return
40-
const validator = getValidator(event, getBotId(), getFlow());
39+
const handleUserSubmitText = (event: Event): void => {
40+
const rcbEvent = event as RcbUserSubmitTextEvent;
41+
42+
// Get validator and if no validator, return
43+
const validator = getValidator<string>(
44+
rcbEvent,
45+
getBotId(),
46+
getFlow(),
47+
"validateTextInput"
48+
);
4149
if (!validator) {
4250
return;
4351
}
4452

45-
// gets and checks validation result
53+
// Get and check validation result
4654
const validationResult = validator(
47-
event.data.inputText
55+
rcbEvent.data.inputText
4856
) as ValidationResult;
4957
if (!validationResult?.success) {
5058
event.preventDefault();
5159
}
5260

53-
// if nothing to prompt, return
61+
// If nothing to prompt, return
5462
if (!validationResult.promptContent) {
5563
return;
5664
}
5765

58-
// if this is the first plugin toast, preserve original styles for restoration later
66+
// Preserve original styles if this is the first plugin toast
5967
if (numPluginToasts === 0) {
60-
originalStyles.current = structuredClone(styles)
68+
originalStyles.current = structuredClone(styles);
6169
}
6270
const promptStyles = getPromptStyles(
6371
validationResult,
6472
mergedPluginConfig
6573
);
6674

67-
// update toast with prompt styles
75+
// Update styles with prompt styles
6876
updateStyles(promptStyles);
6977

70-
// shows prompt toast to user
78+
// Show prompt toast to user
7179
showToast(
7280
validationResult.promptContent,
7381
validationResult.promptDuration ?? 3000
7482
);
7583

76-
// increases number of plugin toasts by 1
84+
// Increase number of plugin toasts by 1
7785
setNumPluginToasts((prev) => prev + 1);
7886
};
7987

88+
const handleUserUploadFile = (event: Event): void => {
89+
const rcbEvent = event as RcbUserUploadFileEvent;
90+
const file: File | undefined = rcbEvent.data?.files?.[0];
91+
92+
if (!file) {
93+
console.error("No file uploaded.");
94+
event.preventDefault();
95+
return;
96+
}
97+
98+
const validator = getValidator<File>(
99+
rcbEvent,
100+
getBotId(),
101+
getFlow(),
102+
"validateFileInput"
103+
);
104+
105+
if (!validator) {
106+
console.error("Validator not found for file input.");
107+
return;
108+
}
109+
110+
const validationResult = validator(file);
111+
112+
if (!validationResult.success) {
113+
console.error("Validation failed:", validationResult);
114+
if (validationResult.promptContent) {
115+
showToast(
116+
validationResult.promptContent,
117+
validationResult.promptDuration ?? 3000
118+
);
119+
}
120+
event.preventDefault();
121+
return;
122+
}
123+
124+
console.log("Validation successful:", validationResult);
125+
};
126+
80127
/**
81128
* Handles the dismiss toast event.
82129
*
83-
* @param event event emitted when toast is dismissed
130+
* @param event Event emitted when toast is dismissed.
84131
*/
85132
const handleDismissToast = (): void => {
86133
setNumPluginToasts((prev) => prev - 1);
87134
};
88135

89-
// adds required events
136+
// Add required event listeners
90137
window.addEventListener("rcb-user-submit-text", handleUserSubmitText);
138+
window.addEventListener("rcb-user-upload-file", handleUserUploadFile);
91139
window.addEventListener("rcb-dismiss-toast", handleDismissToast);
92140

93141
return () => {
142+
// Remove event listeners
94143
window.removeEventListener("rcb-user-submit-text", handleUserSubmitText);
144+
window.removeEventListener("rcb-user-upload-file", handleUserUploadFile);
95145
window.removeEventListener("rcb-dismiss-toast", handleDismissToast);
96146
};
97147
}, [
@@ -101,28 +151,29 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => {
101151
updateStyles,
102152
styles,
103153
mergedPluginConfig,
104-
numPluginToasts
154+
numPluginToasts,
105155
]);
106156

107-
// restores original styles when plugin toasts are all dismissed
157+
// Restore original styles when all plugin toasts are dismissed
108158
useEffect(() => {
109159
if (numPluginToasts === 0) {
110160
setTimeout(() => {
111161
replaceStyles(originalStyles.current);
112162
});
113163
}
114-
}, [numPluginToasts, replaceStyles, originalStyles]);
164+
}, [numPluginToasts, replaceStyles]);
115165

116-
// initializes plugin metadata with plugin name
166+
// Initialize plugin metadata with plugin name
117167
const pluginMetaData: ReturnType<Plugin> = {
118-
name: "@rcb-plugins/input-validator"
168+
name: "@rcb-plugins/input-validator",
119169
};
120170

121-
// adds required events in settings if auto config is true
171+
// Add required events in settings if autoConfig is true
122172
if (mergedPluginConfig.autoConfig) {
123173
pluginMetaData.settings = {
124174
event: {
125175
rcbUserSubmitText: true,
176+
rcbUserUploadFile: true,
126177
rcbDismissToast: true,
127178
},
128179
};

src/types/InputValidatorBlock.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { Block } from "react-chatbotify";
22
import { ValidationResult } from "./ValidationResult";
33

44
/**
5-
* Extends the Block from React ChatBotify to support inputValidator attribute.
5+
* Extends the Block from React ChatBotify to support inputValidator attributes.
66
*/
77
export type InputValidatorBlock = Block & {
8-
validateInput: (userInput?: string) => ValidationResult;
9-
};
8+
validateTextInput?: (userInput?: string) => ValidationResult;
9+
validateFileInput?: (files?: FileList) => ValidationResult; // Accepts multiple files
10+
};

src/utils/getValidator.ts

+23-17
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
import { Flow, RcbUserSubmitTextEvent } from "react-chatbotify";
1+
import { Flow, RcbUserSubmitTextEvent, RcbUserUploadFileEvent } from "react-chatbotify";
22
import { InputValidatorBlock } from "../types/InputValidatorBlock";
3+
import { ValidationResult } from "../types/ValidationResult";
34

45
/**
5-
* Retrieves the validator function and returns null if not applicable.
6+
* Union type for user events that can be validated.
67
*/
7-
export const getValidator = (event: RcbUserSubmitTextEvent, currBotId: string | null, currFlow: Flow) => {
8-
if (currBotId !== event.detail.botId) {
9-
return;
10-
}
11-
12-
if (!event.detail.currPath) {
8+
type RcbUserEvent = RcbUserSubmitTextEvent | RcbUserUploadFileEvent;
9+
10+
/**
11+
* Retrieves the validator function from the current flow block.
12+
*
13+
* @param event The event emitted by the user action (text submission or file upload).
14+
* @param currBotId The current bot ID.
15+
* @param currFlow The current flow object.
16+
* @returns The validator function if it exists, otherwise undefined.
17+
*/
18+
export const getValidator = <T = string | File>(
19+
event: RcbUserEvent,
20+
currBotId: string | null,
21+
currFlow: Flow,
22+
validatorType: "validateTextInput" | "validateFileInput" = "validateTextInput"
23+
): ((input: T) => ValidationResult) | undefined => {
24+
if (!event.detail?.currPath || currBotId !== event.detail.botId) {
1325
return;
1426
}
1527

@@ -18,12 +30,6 @@ export const getValidator = (event: RcbUserSubmitTextEvent, currBotId: string |
1830
return;
1931
}
2032

21-
const validator = currBlock.validateInput;
22-
const isValidatorFunction =
23-
validator && typeof validator === "function";
24-
if (!isValidatorFunction) {
25-
return;
26-
}
27-
28-
return validator;
29-
}
33+
const validator = currBlock[validatorType] as ((input: T) => ValidationResult) | undefined;
34+
return typeof validator === "function" ? validator : undefined;
35+
};

0 commit comments

Comments
 (0)