This commit is contained in:
Mohamed Chedli Ben Yaghlane 2025-06-11 10:18:15 +01:00 committed by GitHub
commit b38a5b4bd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 210 additions and 193 deletions

View File

@ -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<IBlockAttributes>();
const { fields, append, remove } = useFieldArray({
control,
name: "patterns",
keyName: "fieldId",
});
return (
<ContentContainer>
<ContentItem>
<Controller
name="patterns"
<PatternsInput
control={control}
defaultValue={block?.patterns || []}
render={({ field }) => (
<PatternsInput
value={field?.value || []}
onChange={field.onChange}
minInput={1}
/>
)}
name="patterns"
fields={fields}
append={append}
remove={remove}
/>
</ContentItem>
<Divider orientation="horizontal" flexItem />

View File

@ -6,146 +6,124 @@
* 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,
PayloadPattern,
} from "@/types/block.types";
import {
extractRegexBody,
formatWithSlashes,
isRegex,
isRegexString,
} from "@/utils/string";
import { NlpPattern, Pattern, PayloadPattern } from "@/types/block.types";
import { PatternType } from "@/types/pattern.types";
import { getPatternType } from "@/utils/pattern";
import { extractRegexBody, formatWithSlashes, isRegex } from "@/utils/string";
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";
}
};
type PatternInputProps = {
value: Pattern;
onChange: (pattern: Pattern) => void;
block?: IBlockFull;
idx: number;
getInputProps?: (index: number) => TextFieldProps;
control: Control<any>;
basePath: string;
};
const PatternInput: FC<PatternInputProps> = ({
value,
onChange,
idx,
getInputProps,
}) => {
const PatternInput: FC<PatternInputProps> = ({ control, basePath }) => {
const { t } = useTranslate();
const {
register,
formState: { errors },
} = useFormContext<IBlockAttributes>();
const [pattern, setPattern] = useState<Pattern>(value);
const patternType = getPatternType(value);
const registerInput = (
errorMessage: string,
idx: number,
additionalOptions?: RegisterOptions<IBlockAttributes>,
) => {
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 (
<Box display="flex" flexGrow={1}>
{patternType === "nlp" && (
<NlpPatternSelect
patterns={pattern as NlpPattern[]}
onChange={setPattern}
/>
)}
{["payload", "content", "menu"].includes(patternType) ? (
<PostbackInput
onChange={(payload) => {
payload && setPattern(payload);
}}
defaultValue={pattern as PayloadPattern}
/>
) : null}
{patternType === "outcome" ? (
<OutcomeInput
onChange={(payload) => {
payload && setPattern(payload);
}}
defaultValue={pattern as PayloadPattern}
/>
) : null}
{typeof value === "string" && patternType === "regex" ? (
<RegexInput
{...registerInput(t("message.regex_is_empty"), idx, {
validate: (pattern) => {
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" ? (
<Input
{...(getInputProps ? getInputProps(idx) : null)}
label={t("label.text")}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
) : null}
</Box>
<Controller
name={basePath}
control={control}
rules={{
validate: (currentPatternValue: Pattern) => {
const type = getPatternType(currentPatternValue);
const isEmpty = (val: string) => !val || val === "";
if (type === PatternType.REGEX || type === PatternType.TEXT) {
if (typeof currentPatternValue !== "string") {
return t("message.text_is_required");
}
const value = currentPatternValue.trim();
if (type === PatternType.REGEX) {
const regexBody = extractRegexBody(value);
if (isEmpty(regexBody)) {
return t("message.regex_is_empty");
}
if (!isRegex(regexBody)) {
return t("message.regex_is_invalid");
}
} else if (type === PatternType.TEXT) {
if (isEmpty(value)) {
return t("message.text_is_required");
}
}
}
return true;
},
}}
render={({ field, fieldState }) => {
const patternForPath = field.value as Pattern;
const currentPatternType = getPatternType(patternForPath);
return (
<Box display="flex" flexGrow={1}>
{currentPatternType === PatternType.NLP && (
<NlpPatternSelect
patterns={patternForPath as NlpPattern[]}
onChange={field.onChange}
/>
)}
{[
PatternType.PAYLOAD,
PatternType.CONTENT,
PatternType.MENU,
].includes(currentPatternType) ? (
<PostbackInput
onChange={(payload) => {
payload && field.onChange(payload);
}}
defaultValue={patternForPath as PayloadPattern}
/>
) : null}
{currentPatternType === PatternType.OUTCOME ? (
<OutcomeInput
onChange={(payload) => {
payload && field.onChange(payload);
}}
defaultValue={patternForPath as PayloadPattern}
/>
) : null}
{typeof patternForPath === "string" &&
currentPatternType === PatternType.REGEX ? (
<RegexInput
value={extractRegexBody(patternForPath as string)}
label={t("label.regex")}
onChange={(e) =>
field.onChange(formatWithSlashes(e.target.value))
}
required
error={fieldState.invalid}
helperText={fieldState.error?.message}
/>
) : null}
{typeof patternForPath === "string" &&
currentPatternType === PatternType.TEXT ? (
<Input
label={t("label.text")}
value={patternForPath as string}
onChange={(e) => field.onChange(e.target.value)}
error={fieldState.invalid}
helperText={fieldState.error?.message}
required
/>
) : null}
</Box>
);
}}
/>
);
};

View File

@ -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<any>;
name: string;
fields: FieldArrayWithId<any, string, "fieldId">[];
append: UseFieldArrayAppend<any, string>;
remove: UseFieldArrayRemove;
};
const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
const PatternsInput: FC<PatternsInputProps> = ({
control,
name,
fields,
append,
remove,
}) => {
const { t } = useTranslate();
const theme = useTheme();
const [patterns, setPatterns] = useState<ValueWithId<Pattern>[]>(
value.map((pattern) => createValueWithId(pattern)),
);
const {
register,
formState: { errors },
} = useFormContext<any>();
const addInput = (defaultValue: Pattern) => {
setPatterns([...patterns, createValueWithId<Pattern>(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<PatternsInputProps> = ({ value, onChange }) => {
{
icon: <PsychologyAltIcon />,
name: t("label.intent_match"),
defaultValue: [],
defaultValue: [[]],
},
{
icon: <MouseIcon />,
@ -117,11 +106,11 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
return (
<Box display="flex" flexDirection="column">
<Box display="flex" flexDirection="column">
{patterns.length == 0 ? (
{fields.length === 0 ? (
<StyledNoPatternsDiv>{t("label.no_patterns")}</StyledNoPatternsDiv>
) : (
patterns.map(({ value, id }, idx) => (
<Box display="flex" alignItems="center" mt={2} key={id}>
fields.map((field, idx) => (
<Box display="flex" alignItems="center" mt={2} key={field.fieldId}>
{idx > 0 && (
<Chip
sx={{ m: 1, color: theme.palette.grey[600] }}
@ -131,15 +120,9 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
/>
)}
<PatternInput
idx={idx}
value={value}
onChange={updateInput(idx)}
getInputProps={getInputControls(
"label",
errors,
register,
t("message.text_is_required"),
)}
control={control}
basePath={`${name}.${idx}`}
//idx={idx}
/>
<IconButton
size="small"

View File

@ -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.
@ -8,7 +8,6 @@
import {
Autocomplete,
Box,
Chip,
CircularProgress,
InputAdornment,
@ -230,7 +229,9 @@ export const PostbackInput = ({
<Typography component="h4" p={2} fontWeight={700} color="primary">
{t(`label.${group}`)}
</Typography>
<Box>{children}</Box>
<ul style={{ margin: 0, padding: 0, listStyleType: "none" }}>
{children}
</ul>
</li>
)}
renderInput={(props) => (

View File

@ -20,6 +20,7 @@ import {
StdOutgoingTextMessage,
StdPluginMessage,
} from "./message.types";
import { PatternType } from "./pattern.types";
import { IUser } from "./user.types";
export type Position = {
@ -76,14 +77,7 @@ export type NlpPattern = {
export type Pattern = null | string | PayloadPattern | NlpPattern[];
export type PatternType =
| "regex"
| "nlp"
| "menu"
| "content"
| "outcome"
| "payload"
| "text";
export type { PatternType };
export interface IBlockAttributes {
name: string;

View File

@ -0,0 +1,17 @@
/*
* 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.
* 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).
*/
export enum PatternType {
TEXT = "text",
REGEX = "regex",
NLP = "nlp",
PAYLOAD = "payload",
MENU = "menu",
CONTENT = "content",
OUTCOME = "outcome",
}

View File

@ -0,0 +1,44 @@
/*
* 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.
* 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 { Pattern } from "@/types/block.types";
import { PatternType } from "@/types/pattern.types";
import { isRegexString } from "./string";
/**
* Determines the type of a given pattern and returns the corresponding `PatternType`.
* Defaults to returning `PatternType.TEXT` if none of the conditions are met.
*
* @param pattern - The pattern to evaluate, which can be a string, array, or object.
* @returns The determined `PatternType` for the given pattern.
*/
export const getPatternType = (pattern: Pattern): PatternType => {
if (typeof pattern === "string") {
return isRegexString(pattern) ? PatternType.REGEX : PatternType.TEXT;
}
if (Array.isArray(pattern)) {
return PatternType.NLP;
}
if (pattern && typeof pattern === "object") {
switch (pattern.type) {
case "menu":
return PatternType.MENU;
case "content":
return PatternType.CONTENT;
case "outcome":
return PatternType.OUTCOME;
default:
return PatternType.PAYLOAD;
}
}
return PatternType.TEXT;
};