refactor: Add ComponentTextModal and Prompt modal for displaying and editing text areas and components (#3346)

* add separated component for prompt modal

* refactor: rename promptAreaModal to promptModal and update type import

* refactor: update promptComponent to use PromptModal instead of GenericModal

* refactor: update textarea-primary class in applies.css

* refactor: add ComponentTextModal for displaying and editing text areas

Add a new component, ComponentTextModal, for displaying and editing text areas. This component includes a textarea input, a title, and an icon. It also supports password visibility toggling. The ComponentTextModal is used within a BaseModal component and includes a save button for finishing the editing process. This commit refactors the code to add the ComponentTextModal and its related functionality.

* refactor: Add ComponentTextModal for displaying and editing text areas

* delete genericModal component and their references

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
anovazzi1 2024-08-14 15:08:59 -03:00 • committed by GitHub
commit e5ee0ba946
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 186 additions and 140 deletions

View file

@ -1,7 +1,5 @@
import PromptModal from "@/modals/promptModal";
import { useEffect } from "react";
import { TypeModal } from "../../constants/enums";
import GenericModal from "../../modals/genericModal";
import { PromptAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
@ -25,14 +23,11 @@ export default function PromptAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full" : "w-full"}>
<GenericModal
<PromptModal
id={id}
field_name={field_name}
readonly={readonly}
type={TypeModal.PROMPT}
value={value}
buttonText="Check & Save"
modalTitle="Edit Prompt"
setValue={onChange}
nodeClass={nodeClass}
setNodeClass={setNodeClass}
@ -63,7 +58,7 @@ export default function PromptAreaComponent({
)}
</div>
</Button>
</GenericModal>
</PromptModal>
</div>
);
}

View file

@ -1,8 +1,6 @@
import ComponentTextModal from "@/modals/textAreaModal";
import { classNames } from "@/utils/utils";
import { useEffect } from "react";
import { EDIT_TEXT_MODAL_TITLE } from "../../constants/constants";
import { TypeModal } from "../../constants/enums";
import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
@ -47,11 +45,8 @@ export default function TextAreaComponent({
onChange(event.target.value);
}}
/>
<GenericModal
<ComponentTextModal
changeVisibility={updateVisibility}
type={TypeModal.TEXT}
buttonText="Finish Editing"
modalTitle={EDIT_TEXT_MODAL_TITLE}
value={value}
setValue={(value: string) => {
onChange(value);
@ -76,7 +71,7 @@ export default function TextAreaComponent({
/>
</Button>
</div>
</GenericModal>
</ComponentTextModal>
{password !== undefined && (
<Button
unstyled

View file

@ -0,0 +1,291 @@
import { useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import SanitizedHTMLWrapper from "../../components/sanitizedHTMLWrapper";
import ShadTooltip from "../../components/shadTooltipComponent";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { Textarea } from "../../components/ui/textarea";
import {
BUG_ALERT,
PROMPT_ERROR_ALERT,
PROMPT_SUCCESS_ALERT,
TEMP_NOTICE_ALERT,
} from "../../constants/alerts_constants";
import {
EDIT_TEXT_PLACEHOLDER,
INVALID_CHARACTERS,
MAX_WORDS_HIGHLIGHT,
PROMPT_DIALOG_SUBTITLE,
regexHighlight,
} from "../../constants/constants";
import { postValidatePrompt } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import { PromptModalType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
import varHighlightHTML from "./utils/var-highlight-html";
export default function PromptModal({
field_name = "",
value,
setValue,
nodeClass,
setNodeClass,
children,
disabled,
id = "",
readonly = false,
}: PromptModalType): JSX.Element {
const [modalOpen, setModalOpen] = useState(false);
const [inputValue, setInputValue] = useState(value);
const [isEdit, setIsEdit] = useState(true);
const [wordsHighlight, setWordsHighlight] = useState<Set<string>>(new Set());
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
const divRef = useRef(null);
const divRefPrompt = useRef(null);
function checkVariables(valueToCheck: string): void {
const regex = /\{([^{}]+)\}/g;
const matches: string[] = [];
let match;
while ((match = regex.exec(valueToCheck))) {
matches.push(`{${match[1]}}`);
}
let invalid_chars: string[] = [];
let fixed_variables: string[] = [];
let input_variables = matches;
for (let variable of input_variables) {
let new_var = variable;
for (let char of INVALID_CHARACTERS) {
if (variable.includes(char)) {
invalid_chars.push(new_var);
}
}
fixed_variables.push(new_var);
if (new_var !== variable) {
const index = input_variables.indexOf(variable);
if (index !== -1) {
input_variables.splice(index, 1, new_var);
}
}
}
const filteredWordsHighlight = new Set(
matches.filter((word) => !invalid_chars.includes(word)),
);
setWordsHighlight(filteredWordsHighlight);
}
const coloredContent = (typeof inputValue === "string" ? inputValue : "")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(regexHighlight, (match, p1, p2) => {
// Decide which group was matched. If p1 is not undefined, do nothing
// we don't want to change the text. If p2 is not undefined, then we
// have a variable, so we should highlight it.
// ! This will not work with multiline or indented json yet
if (p1 !== undefined) {
return match;
} else if (p2 !== undefined) {
return varHighlightHTML({ name: p2 });
}
return match;
})
.replace(/\n/g, "<br />");
useEffect(() => {
if (inputValue && inputValue != "") {
checkVariables(inputValue);
}
}, [inputValue]);
useEffect(() => {
if (typeof value === "string") setInputValue(value);
}, [value, modalOpen]);
function getClassByNumberLength(): string {
let sumOfCaracteres: number = 0;
wordsHighlight.forEach((element) => {
sumOfCaracteres = sumOfCaracteres + element.replace(/[{}]/g, "").length;
});
return sumOfCaracteres > MAX_WORDS_HIGHLIGHT
? "code-highlight"
: "code-nohighlight";
}
// Function need some review, working for now
function validatePrompt(closeModal: boolean): void {
//nodeClass is always null on tweaks
postValidatePrompt(field_name, inputValue, nodeClass!)
.then((apiReturn) => {
// if field_name is an empty string, then we need to set it
// to the first key of the custom_fields object
if (field_name === "") {
field_name = Array.isArray(
apiReturn.data?.frontend_node?.custom_fields?.[""],
)
? apiReturn.data?.frontend_node?.custom_fields?.[""][0] ?? ""
: apiReturn.data?.frontend_node?.custom_fields?.[""] ?? "";
}
if (apiReturn.data) {
let inputVariables = apiReturn.data.input_variables ?? [];
if (
JSON.stringify(apiReturn.data?.frontend_node) !== JSON.stringify({})
) {
setValue(inputValue);
apiReturn.data.frontend_node.template.template.value = inputValue;
if (setNodeClass) setNodeClass(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setIsEdit(false);
}
if (!inputVariables || inputVariables.length === 0) {
setNoticeData({
title: TEMP_NOTICE_ALERT,
});
} else {
setSuccessData({
title: PROMPT_SUCCESS_ALERT,
});
}
} else {
setIsEdit(true);
setErrorData({
title: BUG_ALERT,
});
}
})
.catch((error) => {
setIsEdit(true);
return setErrorData({
title: PROMPT_ERROR_ALERT,
list: [error.response.data.detail ?? ""],
});
});
}
return (
<BaseModal
onChangeOpenModal={(open) => {}}
open={modalOpen}
setOpen={setModalOpen}
>
<BaseModal.Trigger disable={disabled} asChild>
{children}
</BaseModal.Trigger>
<BaseModal.Header description={PROMPT_DIALOG_SUBTITLE}>
<div className="flex w-full items-start gap-3">
<div className="flex">
<span className="pr-2" data-testid="modal-title">
Edit Prompt
</span>
<IconComponent
name="TerminalSquare"
className="h-6 w-6 pl-1 text-primary"
aria-hidden="true"
/>
</div>
</div>
</BaseModal.Header>
<BaseModal.Content overflowHidden>
<div className={classNames("flex h-full w-full rounded-lg border")}>
{isEdit && !readonly ? (
<Textarea
id={"modal-" + id}
data-testid={"modal-" + id}
ref={divRefPrompt}
className="form-input h-full w-full resize-none rounded-lg custom-scroll focus-visible:ring-1"
value={inputValue}
onBlur={() => {
setIsEdit(false);
}}
autoFocus
onChange={(event) => {
setInputValue(event.target.value);
checkVariables(event.target.value);
}}
placeholder={EDIT_TEXT_PLACEHOLDER}
onKeyDown={(e) => {
handleKeyDown(e, inputValue, "");
}}
/>
) : (
<SanitizedHTMLWrapper
className={getClassByNumberLength() + " bg-muted"}
content={coloredContent}
onClick={() => {
if (!readonly) setIsEdit(true);
}}
suppressWarning={true}
/>
)}
</div>
</BaseModal.Content>
<BaseModal.Footer>
<div className="flex w-full shrink-0 items-end justify-between">
<div className="mb-auto flex-1">
<div className="mr-2">
<div
ref={divRef}
className="max-h-20 overflow-y-auto custom-scroll"
>
<div className="flex flex-wrap items-center gap-2">
<IconComponent
name="Braces"
className="flex h-4 w-4 text-primary"
/>
<span className="text-md font-semibold text-primary">
Prompt Variables:
</span>
{Array.from(wordsHighlight).map((word, index) => (
<ShadTooltip
key={index}
content={word.replace(/[{}]/g, "")}
asChild={false}
>
<Badge
key={index}
variant="gray"
size="md"
className="max-w-[40vw] cursor-default truncate p-1 text-sm"
>
<div className="relative bottom-[1px]">
<span id={"badge" + index.toString()}>
{word.replace(/[{}]/g, "").length > 59
? word.replace(/[{}]/g, "").slice(0, 56) + "..."
: word.replace(/[{}]/g, "")}
</span>
</div>
</Badge>
</ShadTooltip>
))}
</div>
</div>
<span className="mt-2 text-xs text-muted-foreground">
Prompt variables can be created with any chosen name inside
curly brackets, e.g. {"{variable_name}"}
</span>
</div>
</div>
<Button
data-testid="genericModalBtnSave"
id="genericModalBtnSave"
disabled={readonly}
onClick={() => {
validatePrompt(false);
}}
type="submit"
>
Check & Save
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
);
}

View file

@ -0,0 +1,6 @@
import { IVarHighlightType } from "../../../types/components";
export default function varHighlightHTML({ name }: IVarHighlightType): string {
const html = `<span class="font-semibold chat-message-highlight">{${name}}</span>`;
return html;
}

View file

@ -0,0 +1,106 @@
import { useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Textarea } from "../../components/ui/textarea";
import {
EDIT_TEXT_PLACEHOLDER,
TEXT_DIALOG_SUBTITLE,
} from "../../constants/constants";
import { textModalPropsType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
export default function ComponentTextModal({
value,
setValue,
children,
disabled,
readonly = false,
password,
changeVisibility,
}: textModalPropsType): JSX.Element {
const [modalOpen, setModalOpen] = useState(false);
const [inputValue, setInputValue] = useState(value);
const textRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (typeof value === "string") setInputValue(value);
}, [value, modalOpen]);
return (
<BaseModal
onChangeOpenModal={(open) => {}}
open={modalOpen}
setOpen={setModalOpen}
>
<BaseModal.Trigger disable={disabled} asChild>
{children}
</BaseModal.Trigger>
<BaseModal.Header description={TEXT_DIALOG_SUBTITLE}>
<div className="flex w-full items-start gap-3">
<div className="flex">
<span className="pr-2" data-testid="modal-title">
{TEXT_DIALOG_SUBTITLE}
</span>
<IconComponent
name={"FileText"}
className="h-6 w-6 pl-1 text-primary"
aria-hidden="true"
/>
</div>
{password !== undefined && (
<div>
<button
onClick={() => {
if (changeVisibility) changeVisibility();
}}
>
<IconComponent
name={password ? "Eye" : "EyeOff"}
className="h-6 w-6 cursor-pointer text-primary"
/>
</button>
</div>
)}
</div>
</BaseModal.Header>
<BaseModal.Content overflowHidden>
<div className={classNames("flex h-full w-full rounded-lg border")}>
<Textarea
password={password}
ref={textRef}
className="form-input h-full w-full resize-none overflow-auto rounded-lg focus-visible:ring-1"
value={inputValue}
onChange={(event) => {
setInputValue(event.target.value);
}}
placeholder={EDIT_TEXT_PLACEHOLDER}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
}}
readOnly={readonly}
id={"text-area-modal"}
data-testid={"text-area-modal"}
/>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<div className="flex w-full shrink-0 items-end justify-end">
<Button
data-testid="genericModalBtnSave"
id="genericModalBtnSave"
disabled={readonly}
onClick={() => {
setValue(inputValue);
setModalOpen(false);
}}
type="submit"
>
Finish Editing
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
);
}

View file

@ -196,7 +196,7 @@
/* The same as primary-input but no-truncate */
.textarea-primary {
@apply form-input block w-full rounded-md border-border bg-muted px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
@apply form-input block w-full rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
}
.input-edit-node {

View file

@ -700,6 +700,28 @@ export type genericModalPropsType = {
changeVisibility?: () => void;
};
export type PromptModalType = {
field_name?: string;
setValue: (value: string) => void;
value: string;
disabled?: boolean;
nodeClass?: APIClassType;
setNodeClass?: (Class: APIClassType, type?: string) => void;
children: ReactNode;
id?: string;
readonly?: boolean;
};
export type textModalPropsType = {
setValue: (value: string) => void;
value: string;
disabled?: boolean;
children: ReactNode;
readonly?: boolean;
password?: boolean;
changeVisibility?: () => void;
};
export type newFlowModalPropsType = {
open: boolean;
setOpen: (open: boolean) => void;