feat: enhance fieldset

This commit is contained in:
Mohamed Marrouchi 2024-11-27 11:16:00 +01:00
parent eba7308e16
commit 1da3a3d9ba
6 changed files with 186 additions and 121 deletions

View File

@ -232,7 +232,7 @@
"capture_context_vars": "Capture context variables?", "capture_context_vars": "Capture context variables?",
"block_event_type": "Type of event", "block_event_type": "Type of event",
"patterns": "Patterns", "patterns": "Patterns",
"no_patterns": "- No patterns -", "no_patterns": "- No triggers -",
"text_patterns": " Text Patterns", "text_patterns": " Text Patterns",
"triggers": "Triggers", "triggers": "Triggers",
"payloads": "Payloads", "payloads": "Payloads",
@ -558,7 +558,7 @@
"media_library": "Media Library", "media_library": "Media Library",
"manage_roles": "Manage Roles", "manage_roles": "Manage Roles",
"connect_with_sso": "Connect with SSO", "connect_with_sso": "Connect with SSO",
"add_pattern": "Add pattern", "add_pattern": "New Trigger",
"mark_as_default": "Mark as Default", "mark_as_default": "Mark as Default",
"toggle": "Toggle button" "toggle": "Toggle button"
}, },

View File

@ -559,7 +559,7 @@
"media_library": "Bibliothéque Media", "media_library": "Bibliothéque Media",
"manage_roles": "Gérer les rôles", "manage_roles": "Gérer les rôles",
"connect_with_sso": "Se connecter avec SSO", "connect_with_sso": "Se connecter avec SSO",
"add_pattern": "Ajouter un motif", "add_pattern": "Ajouter un déclencheur",
"mark_as_default": "Par Défaut", "mark_as_default": "Par Défaut",
"toggle": "Bouton de bascule" "toggle": "Bouton de bascule"
}, },

View File

@ -0,0 +1,96 @@
/*
* Copyright © 2024 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 { ArrowDropDown } from "@mui/icons-material";
import {
Box,
Button,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Popover,
SxProps,
Theme,
} from "@mui/material";
import React, { useState } from "react";
export interface DropdownButtonAction {
icon: React.ReactNode;
name: string;
defaultValue: any;
}
interface AddPatternProps {
actions: DropdownButtonAction[];
onClick: (action: DropdownButtonAction) => void;
label?: string;
icon?: React.ReactNode;
sx?: SxProps<Theme> | undefined;
}
const DropdownButton: React.FC<AddPatternProps> = ({
actions,
onClick,
label = "Add",
icon,
sx,
}) => {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleAddFieldset = (action: DropdownButtonAction) => {
onClick(action);
handleClose();
};
const open = Boolean(anchorEl);
return (
<Box sx={sx}>
<Button
variant="contained"
onClick={handleOpen}
startIcon={icon}
endIcon={<ArrowDropDown />}
>
{label}
</Button>
<Popover
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<List>
{actions.map((action, index) => (
<ListItemButton
key={index}
onClick={() => handleAddFieldset(action)}
>
<ListItemIcon>{action.icon}</ListItemIcon>
<ListItemText primary={action.name} />
</ListItemButton>
))}
</List>
</Popover>
</Box>
);
};
export default DropdownButton;

View File

@ -117,6 +117,7 @@ const NlpPatternSelect = (
<Autocomplete <Autocomplete
ref={ref} ref={ref}
size="medium" size="medium"
fullWidth={true}
disabled={options.length === 0} disabled={options.length === 0}
value={defaultValue} value={defaultValue}
multiple={true} multiple={true}

View File

@ -6,7 +6,7 @@
* 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). * 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 { Grid, MenuItem, TextFieldProps } from "@mui/material"; import { Box, TextFieldProps } from "@mui/material";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { RegisterOptions, useFormContext } from "react-hook-form"; import { RegisterOptions, useFormContext } from "react-hook-form";
@ -25,6 +25,7 @@ import {
import { PostbackInput } from "./PostbackInput"; import { PostbackInput } from "./PostbackInput";
const isRegex = (str: Pattern) => { const isRegex = (str: Pattern) => {
return typeof str === "string" && str.startsWith("/") && str.endsWith("/"); return typeof str === "string" && str.startsWith("/") && str.endsWith("/");
}; };
@ -65,16 +66,9 @@ const PatternInput: FC<PatternInputProps> = ({
register, register,
formState: { errors }, formState: { errors },
} = useFormContext<IBlockAttributes>(); } = useFormContext<IBlockAttributes>();
// const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY);
const [pattern, setPattern] = useState<Pattern>(value); const [pattern, setPattern] = useState<Pattern>(value);
const [patternType, setPatternType] = useState<PatternType>(getType(value)); const patternType = getType(value);
const isPostbackType = ["payload", "content", "menu"].includes(patternType); const isPostbackType = ["payload", "content", "menu"].includes(patternType);
const types = [
{ value: "text", label: t("label.match_sound") },
{ value: "regex", label: t("label.regex") },
{ value: "payload", label: t("label.postback") },
{ value: "nlp", label: t("label.nlp") },
];
const registerInput = ( const registerInput = (
errorMessage: string, errorMessage: string,
idx: number, idx: number,
@ -100,53 +94,15 @@ const PatternInput: FC<PatternInputProps> = ({
}, [pattern]); }, [pattern]);
return ( return (
<> <Box display="flex" flexGrow={1}>
<Grid item xs={2}> {patternType === "nlp" && (
<Input <NlpPatternSelect
select patterns={pattern as NlpPattern[]}
label={t("label.type")} onChange={setPattern}
value={isPostbackType ? "payload" : patternType} />
onChange={(e) => { )}
const selected = e.target.value as PatternType;
switch (selected) { {isPostbackType ? (
case "regex": {
setPattern("//");
break;
}
case "nlp": {
setPattern([]);
break;
}
case "menu":
case "content":
case "payload": {
setPattern(null);
break;
}
default: {
setPattern("");
}
}
setPatternType(selected);
}}
>
{types.map((item) => (
<MenuItem key={item.value} value={item.value}>
{item.label}
</MenuItem>
))}
</Input>
</Grid>
<Grid item xs={9}>
{patternType === "nlp" && (
<NlpPatternSelect
patterns={pattern as NlpPattern[]}
onChange={setPattern}
/>
)}
{isPostbackType ? (
<PostbackInput <PostbackInput
onChange={(payload) => { onChange={(payload) => {
payload && setPattern(payload); payload && setPattern(payload);
@ -154,40 +110,39 @@ const PatternInput: FC<PatternInputProps> = ({
defaultValue={pattern as PayloadPattern} defaultValue={pattern as PayloadPattern}
/> />
) : null} ) : null}
{typeof value === "string" && patternType === "regex" ? ( {typeof value === "string" && patternType === "regex" ? (
<RegexInput <RegexInput
{...registerInput(t("message.regex_is_empty"), idx, { {...registerInput(t("message.regex_is_empty"), idx, {
validate: (pattern) => { validate: (pattern) => {
try { try {
const parsedPattern = new RegExp(pattern.slice(1, -1)); const parsedPattern = new RegExp(pattern.slice(1, -1));
if (String(parsedPattern) !== pattern) { if (String(parsedPattern) !== pattern) {
throw t("message.regex_is_invalid"); throw t("message.regex_is_invalid");
}
return true;
} catch (_e) {
return t("message.regex_is_invalid");
} }
},
setValueAs: (v) => (isRegex(v) ? v : `/${v}/`), return true;
})} } catch (_e) {
label={t("label.regex")} return t("message.regex_is_invalid");
value={value.slice(1, -1)} }
onChange={(v) => onChange(v)} },
required setValueAs: (v) => (isRegex(v) ? v : `/${v}/`),
/> })}
) : null} label={t("label.regex")}
{typeof value === "string" && patternType === "text" ? ( value={value.slice(1, -1)}
<Input onChange={(v) => onChange(v)}
{...(getInputProps ? getInputProps(idx) : null)} required
label={t("label.text")} />
value={value} ) : null}
onChange={(e) => onChange(e.target.value)} {typeof value === "string" && patternType === "text" ? (
/> <Input
) : null} {...(getInputProps ? getInputProps(idx) : null)}
</Grid> label={t("label.text")}
</> value={value}
onChange={(e) => onChange(e.target.value)}
/>
) : null}
</Box>
); );
}; };

View File

@ -6,12 +6,21 @@
* 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). * 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 AddIcon from "@mui/icons-material/Add"; import {
import DeleteIcon from "@mui/icons-material/Delete"; Abc,
import { Box, Button, Grid, IconButton, styled } from "@mui/material"; Add,
import { FC, Fragment, useEffect, useState } from "react"; Mouse,
PsychologyAlt,
RemoveCircleOutline,
Spellcheck,
} from "@mui/icons-material";
import { Box, IconButton, styled } from "@mui/material";
import { FC, useEffect, useState } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import DropdownButton, {
DropdownButtonAction,
} from "@/app-components/buttons/DropdownButton";
import { useTranslate } from "@/hooks/useTranslate"; import { useTranslate } from "@/hooks/useTranslate";
import { Pattern } from "@/types/block.types"; import { Pattern } from "@/types/block.types";
import { SXStyleOptions } from "@/utils/SXStyleOptions"; import { SXStyleOptions } from "@/utils/SXStyleOptions";
@ -21,11 +30,6 @@ import { getInputControls } from "../../utils/inputControls";
import PatternInput from "./PatternInput"; import PatternInput from "./PatternInput";
type PatternsInputProps = {
value: Pattern[];
onChange: (patterns: Pattern[]) => void;
minInput: number;
};
const StyledNoPatternsDiv = styled("div")( const StyledNoPatternsDiv = styled("div")(
SXStyleOptions({ SXStyleOptions({
color: "grey.500", color: "grey.500",
@ -35,6 +39,19 @@ const StyledNoPatternsDiv = styled("div")(
width: "100%", width: "100%",
}), }),
); );
const actions: DropdownButtonAction[] = [
{ icon: <Spellcheck />, name: "Exact Match", defaultValue: "" },
{ icon: <Abc />, name: "Pattern Match", defaultValue: "//" },
{ icon: <PsychologyAlt />, name: "Intent Match", defaultValue: [] },
{ icon: <Mouse />, name: "Interaction", defaultValue: {} },
];
type PatternsInputProps = {
value: Pattern[];
onChange: (patterns: Pattern[]) => void;
minInput: number;
};
const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => { const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
const { t } = useTranslate(); const { t } = useTranslate();
const [patterns, setPatterns] = useState<ValueWithId<Pattern>[]>( const [patterns, setPatterns] = useState<ValueWithId<Pattern>[]>(
@ -44,8 +61,8 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
register, register,
formState: { errors }, formState: { errors },
} = useFormContext<any>(); } = useFormContext<any>();
const addInput = () => { const addInput = (defaultValue: Pattern) => {
setPatterns([...patterns, createValueWithId<Pattern>("")]); setPatterns([...patterns, createValueWithId<Pattern>(defaultValue)]);
}; };
const removeInput = (index: number) => { const removeInput = (index: number) => {
const updatedPatterns = [...patterns]; const updatedPatterns = [...patterns];
@ -64,18 +81,13 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
}, [patterns]); }, [patterns]);
return ( return (
<Box> <Box display="flex" flexDirection="column">
<Grid container spacing={2}> <Box display="flex" flexDirection="column">
{patterns.length == 0 ? ( {patterns.length == 0 ? (
<StyledNoPatternsDiv>{t("label.no_patterns")}</StyledNoPatternsDiv> <StyledNoPatternsDiv>{t("label.no_patterns")}</StyledNoPatternsDiv>
) : ( ) : (
patterns.map(({ value, id }, idx) => ( patterns.map(({ value, id }, idx) => (
<Fragment key={id}> <Box display="flex" mt={2} key={id}>
<Grid item xs={1}>
<IconButton onClick={() => removeInput(idx)}>
<DeleteIcon />
</IconButton>
</Grid>
<PatternInput <PatternInput
idx={idx} idx={idx}
value={value} value={value}
@ -87,19 +99,20 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
t("message.text_is_required"), t("message.text_is_required"),
)} )}
/> />
</Fragment> <IconButton size="small" color="error" onClick={() => removeInput(idx)}>
<RemoveCircleOutline />
</IconButton>
</Box>
)) ))
)} )}
</Grid> </Box>
<Button <DropdownButton
variant="contained" sx={{ alignSelf: "end", marginTop: 2 }}
color="primary" label={t("button.add_pattern")}
onClick={addInput} actions={actions}
startIcon={<AddIcon />} onClick={(action) => addInput(action.defaultValue as Pattern)}
sx={{ marginTop: 2, float: "right" }} icon={<Add />}
> />
{t("button.add_pattern")}
</Button>
</Box> </Box>
); );
}; };