From d53906750bfb4db1813ed4e58470f5f7c9076184 Mon Sep 17 00:00:00 2001 From: medchedli Date: Sun, 8 Jun 2025 23:27:39 +0100 Subject: [PATCH] feat: integrate useFieldArray for dynamic pattern management in TriggersForm --- .../visual-editor/form/TriggersForm.tsx | 22 +- .../form/inputs/triggers/PatternInput.tsx | 219 +++++++++--------- .../form/inputs/triggers/PatternsInput.tsx | 75 +++--- 3 files changed, 150 insertions(+), 166 deletions(-) diff --git a/frontend/src/components/visual-editor/form/TriggersForm.tsx b/frontend/src/components/visual-editor/form/TriggersForm.tsx index de6cc446..cfec27dc 100644 --- a/frontend/src/components/visual-editor/form/TriggersForm.tsx +++ b/frontend/src/components/visual-editor/form/TriggersForm.tsx @@ -7,7 +7,7 @@ */ import { Divider } from "@mui/material"; -import { Controller, useFormContext } from "react-hook-form"; +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ContentContainer, ContentItem } from "@/app-components/dialogs"; import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; @@ -24,21 +24,21 @@ export const TriggersForm = () => { const block = useBlock(); const { t } = useTranslate(); const { control } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: "patterns", + keyName: "fieldId", + }); return ( - ( - - )} + name="patterns" + fields={fields} + append={append} + remove={remove} /> diff --git a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx index 8d0b6b3f..14c3230e 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx @@ -6,17 +6,15 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Box, TextFieldProps } from "@mui/material"; -import { FC, useEffect, useState } from "react"; -import { RegisterOptions, useFormContext } from "react-hook-form"; +import { Box } from "@mui/material"; +import { FC } from "react"; +import { Control, Controller } from "react-hook-form"; import { Input } from "@/app-components/inputs/Input"; import NlpPatternSelect from "@/app-components/inputs/NlpPatternSelect"; import { RegexInput } from "@/app-components/inputs/RegexInput"; import { useTranslate } from "@/hooks/useTranslate"; import { - IBlockAttributes, - IBlockFull, NlpPattern, Pattern, PatternType, @@ -33,119 +31,122 @@ import { OutcomeInput } from "./OutcomeInput"; import { PostbackInput } from "./PostbackInput"; const getPatternType = (pattern: Pattern): PatternType => { - if (isRegexString(pattern)) { - return "regex"; - } else if (Array.isArray(pattern)) { - return "nlp"; - } else if (typeof pattern === "object") { - if (pattern?.type === "menu") { - return "menu"; - } else if (pattern?.type === "content") { - return "content"; - } else if (pattern?.type === "outcome") { - return "outcome"; - } else { - return "payload"; - } - } else { - return "text"; + if (typeof pattern === "string") { + return isRegexString(pattern) ? "regex" : "text"; } + + if (Array.isArray(pattern)) { + return "nlp"; + } + + if (pattern && typeof pattern === "object") { + switch (pattern.type) { + case "menu": + return "menu"; + case "content": + return "content"; + case "outcome": + return "outcome"; + default: + return "payload"; + } + } + + return "text"; }; type PatternInputProps = { - value: Pattern; - onChange: (pattern: Pattern) => void; - block?: IBlockFull; - idx: number; - getInputProps?: (index: number) => TextFieldProps; + control: Control; + basePath: string; }; -const PatternInput: FC = ({ - value, - onChange, - idx, - getInputProps, -}) => { +const PatternInput: FC = ({ control, basePath }) => { const { t } = useTranslate(); - const { - register, - formState: { errors }, - } = useFormContext(); - const [pattern, setPattern] = useState(value); - const patternType = getPatternType(value); - const registerInput = ( - errorMessage: string, - idx: number, - additionalOptions?: RegisterOptions, - ) => { - return { - ...register(`patterns.${idx}`, { - required: errorMessage, - ...additionalOptions, - }), - helperText: errors.patterns?.[idx] - ? errors.patterns[idx].message - : undefined, - error: !!errors.patterns?.[idx], - }; - }; - - useEffect(() => { - if (pattern || pattern === "") { - onChange(pattern); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pattern]); return ( - - {patternType === "nlp" && ( - - )} - {["payload", "content", "menu"].includes(patternType) ? ( - { - payload && setPattern(payload); - }} - defaultValue={pattern as PayloadPattern} - /> - ) : null} - {patternType === "outcome" ? ( - { - payload && setPattern(payload); - }} - defaultValue={pattern as PayloadPattern} - /> - ) : null} - {typeof value === "string" && patternType === "regex" ? ( - { - return isRegex(extractRegexBody(pattern)) - ? true - : t("message.regex_is_invalid"); - }, - setValueAs: (v) => (isRegexString(v) ? v : formatWithSlashes(v)), - })} - value={extractRegexBody(value)} - label={t("label.regex")} - onChange={(e) => onChange(formatWithSlashes(e.target.value))} - required - /> - ) : null} - {typeof value === "string" && patternType === "text" ? ( - onChange(e.target.value)} - /> - ) : null} - + { + const type = getPatternType(currentPatternValue); + + if (type === "regex") { + const regexString = currentPatternValue as string; + + if (!regexString || extractRegexBody(regexString).trim() === "") { + return t("message.regex_is_empty"); + } + if (!isRegex(extractRegexBody(regexString))) { + return t("message.regex_is_invalid"); + } + } else if (type === "text") { + const textString = currentPatternValue as string; + + if (!textString || textString.trim() === "") { + return t("message.text_is_required"); + } + } + + return true; + }, + }} + render={({ field, fieldState }) => { + const patternForPath = field.value as Pattern; + const currentPatternType = getPatternType(patternForPath); + + return ( + + {currentPatternType === "nlp" && ( + + )} + {["payload", "content", "menu"].includes(currentPatternType) ? ( + { + payload && field.onChange(payload); + }} + defaultValue={patternForPath as PayloadPattern} + /> + ) : null} + {currentPatternType === "outcome" ? ( + { + payload && field.onChange(payload); + }} + defaultValue={patternForPath as PayloadPattern} + /> + ) : null} + {typeof patternForPath === "string" && + currentPatternType === "regex" ? ( + + field.onChange(formatWithSlashes(e.target.value)) + } + required + error={fieldState.invalid} + helperText={fieldState.error?.message} + /> + ) : null} + {typeof patternForPath === "string" && + currentPatternType === "text" ? ( + field.onChange(e.target.value)} + error={fieldState.invalid} + helperText={fieldState.error?.message} + required + /> + ) : null} + + ); + }} + /> ); }; diff --git a/frontend/src/components/visual-editor/form/inputs/triggers/PatternsInput.tsx b/frontend/src/components/visual-editor/form/inputs/triggers/PatternsInput.tsx index 6119fac3..e4e61843 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PatternsInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PatternsInput.tsx @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. @@ -14,8 +14,13 @@ import PsychologyAltIcon from "@mui/icons-material/PsychologyAlt"; import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; import SpellcheckIcon from "@mui/icons-material/Spellcheck"; import { Box, Chip, IconButton, styled, useTheme } from "@mui/material"; -import { FC, useEffect, useMemo, useState } from "react"; -import { useFormContext } from "react-hook-form"; +import { FC, useMemo } from "react"; +import { + Control, + FieldArrayWithId, + UseFieldArrayAppend, + UseFieldArrayRemove, +} from "react-hook-form"; import DropdownButton, { DropdownButtonAction, @@ -24,9 +29,6 @@ import { useTranslate } from "@/hooks/useTranslate"; import { Pattern } from "@/types/block.types"; import { PayloadType } from "@/types/message.types"; import { SXStyleOptions } from "@/utils/SXStyleOptions"; -import { createValueWithId, ValueWithId } from "@/utils/valueWithId"; - -import { getInputControls } from "../../utils/inputControls"; import PatternInput from "./PatternInput"; @@ -41,41 +43,28 @@ const StyledNoPatternsDiv = styled("div")( ); type PatternsInputProps = { - value: Pattern[]; - onChange: (patterns: Pattern[]) => void; - minInput: number; + control: Control; + name: string; + fields: FieldArrayWithId[]; + append: UseFieldArrayAppend; + remove: UseFieldArrayRemove; }; -const PatternsInput: FC = ({ value, onChange }) => { +const PatternsInput: FC = ({ + control, + name, + fields, + append, + remove, +}) => { const { t } = useTranslate(); const theme = useTheme(); - const [patterns, setPatterns] = useState[]>( - value.map((pattern) => createValueWithId(pattern)), - ); - const { - register, - formState: { errors }, - } = useFormContext(); const addInput = (defaultValue: Pattern) => { - setPatterns([...patterns, createValueWithId(defaultValue)]); + append(defaultValue); }; const removeInput = (index: number) => { - const updatedPatterns = [...patterns]; - - updatedPatterns.splice(index, 1); - - setPatterns(updatedPatterns); + remove(index); }; - const updateInput = (index: number) => (p: Pattern) => { - patterns[index].value = p; - setPatterns([...patterns]); - }; - - useEffect(() => { - onChange(patterns.map(({ value }) => value)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [patterns]); - const actions: DropdownButtonAction[] = useMemo( () => [ { @@ -87,7 +76,7 @@ const PatternsInput: FC = ({ value, onChange }) => { { icon: , name: t("label.intent_match"), - defaultValue: [], + defaultValue: [[]], }, { icon: , @@ -117,11 +106,11 @@ const PatternsInput: FC = ({ value, onChange }) => { return ( - {patterns.length == 0 ? ( + {fields.length === 0 ? ( {t("label.no_patterns")} ) : ( - patterns.map(({ value, id }, idx) => ( - + fields.map((field, idx) => ( + {idx > 0 && ( = ({ value, onChange }) => { /> )}