mirror of
https://github.com/hexastack/hexabot
synced 2024-12-02 00:54:56 +00:00
feat: enhance fieldset
This commit is contained in:
parent
eba7308e16
commit
1da3a3d9ba
@ -232,7 +232,7 @@
|
||||
"capture_context_vars": "Capture context variables?",
|
||||
"block_event_type": "Type of event",
|
||||
"patterns": "Patterns",
|
||||
"no_patterns": "- No patterns -",
|
||||
"no_patterns": "- No triggers -",
|
||||
"text_patterns": " Text Patterns",
|
||||
"triggers": "Triggers",
|
||||
"payloads": "Payloads",
|
||||
@ -558,7 +558,7 @@
|
||||
"media_library": "Media Library",
|
||||
"manage_roles": "Manage Roles",
|
||||
"connect_with_sso": "Connect with SSO",
|
||||
"add_pattern": "Add pattern",
|
||||
"add_pattern": "New Trigger",
|
||||
"mark_as_default": "Mark as Default",
|
||||
"toggle": "Toggle button"
|
||||
},
|
||||
|
@ -559,7 +559,7 @@
|
||||
"media_library": "Bibliothéque Media",
|
||||
"manage_roles": "Gérer les rôles",
|
||||
"connect_with_sso": "Se connecter avec SSO",
|
||||
"add_pattern": "Ajouter un motif",
|
||||
"add_pattern": "Ajouter un déclencheur",
|
||||
"mark_as_default": "Par Défaut",
|
||||
"toggle": "Bouton de bascule"
|
||||
},
|
||||
|
96
frontend/src/app-components/buttons/DropdownButton.tsx
Normal file
96
frontend/src/app-components/buttons/DropdownButton.tsx
Normal 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;
|
@ -117,6 +117,7 @@ const NlpPatternSelect = (
|
||||
<Autocomplete
|
||||
ref={ref}
|
||||
size="medium"
|
||||
fullWidth={true}
|
||||
disabled={options.length === 0}
|
||||
value={defaultValue}
|
||||
multiple={true}
|
||||
|
@ -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).
|
||||
*/
|
||||
|
||||
import { Grid, MenuItem, TextFieldProps } from "@mui/material";
|
||||
import { Box, TextFieldProps } from "@mui/material";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { RegisterOptions, useFormContext } from "react-hook-form";
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
|
||||
import { PostbackInput } from "./PostbackInput";
|
||||
|
||||
|
||||
const isRegex = (str: Pattern) => {
|
||||
return typeof str === "string" && str.startsWith("/") && str.endsWith("/");
|
||||
};
|
||||
@ -65,16 +66,9 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<IBlockAttributes>();
|
||||
// const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY);
|
||||
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 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 = (
|
||||
errorMessage: string,
|
||||
idx: number,
|
||||
@ -100,53 +94,15 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
}, [pattern]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid item xs={2}>
|
||||
<Input
|
||||
select
|
||||
label={t("label.type")}
|
||||
value={isPostbackType ? "payload" : patternType}
|
||||
onChange={(e) => {
|
||||
const selected = e.target.value as PatternType;
|
||||
|
||||
switch (selected) {
|
||||
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 ? (
|
||||
<Box display="flex" flexGrow={1}>
|
||||
{patternType === "nlp" && (
|
||||
<NlpPatternSelect
|
||||
patterns={pattern as NlpPattern[]}
|
||||
onChange={setPattern}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPostbackType ? (
|
||||
<PostbackInput
|
||||
onChange={(payload) => {
|
||||
payload && setPattern(payload);
|
||||
@ -154,40 +110,39 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
defaultValue={pattern as PayloadPattern}
|
||||
/>
|
||||
) : null}
|
||||
{typeof value === "string" && patternType === "regex" ? (
|
||||
<RegexInput
|
||||
{...registerInput(t("message.regex_is_empty"), idx, {
|
||||
validate: (pattern) => {
|
||||
try {
|
||||
const parsedPattern = new RegExp(pattern.slice(1, -1));
|
||||
{typeof value === "string" && patternType === "regex" ? (
|
||||
<RegexInput
|
||||
{...registerInput(t("message.regex_is_empty"), idx, {
|
||||
validate: (pattern) => {
|
||||
try {
|
||||
const parsedPattern = new RegExp(pattern.slice(1, -1));
|
||||
|
||||
if (String(parsedPattern) !== pattern) {
|
||||
throw t("message.regex_is_invalid");
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_e) {
|
||||
return t("message.regex_is_invalid");
|
||||
if (String(parsedPattern) !== pattern) {
|
||||
throw t("message.regex_is_invalid");
|
||||
}
|
||||
},
|
||||
setValueAs: (v) => (isRegex(v) ? v : `/${v}/`),
|
||||
})}
|
||||
label={t("label.regex")}
|
||||
value={value.slice(1, -1)}
|
||||
onChange={(v) => onChange(v)}
|
||||
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}
|
||||
</Grid>
|
||||
</>
|
||||
|
||||
return true;
|
||||
} catch (_e) {
|
||||
return t("message.regex_is_invalid");
|
||||
}
|
||||
},
|
||||
setValueAs: (v) => (isRegex(v) ? v : `/${v}/`),
|
||||
})}
|
||||
label={t("label.regex")}
|
||||
value={value.slice(1, -1)}
|
||||
onChange={(v) => onChange(v)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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).
|
||||
*/
|
||||
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { Box, Button, Grid, IconButton, styled } from "@mui/material";
|
||||
import { FC, Fragment, useEffect, useState } from "react";
|
||||
import {
|
||||
Abc,
|
||||
Add,
|
||||
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 DropdownButton, {
|
||||
DropdownButtonAction,
|
||||
} from "@/app-components/buttons/DropdownButton";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { Pattern } from "@/types/block.types";
|
||||
import { SXStyleOptions } from "@/utils/SXStyleOptions";
|
||||
@ -21,11 +30,6 @@ import { getInputControls } from "../../utils/inputControls";
|
||||
|
||||
import PatternInput from "./PatternInput";
|
||||
|
||||
type PatternsInputProps = {
|
||||
value: Pattern[];
|
||||
onChange: (patterns: Pattern[]) => void;
|
||||
minInput: number;
|
||||
};
|
||||
const StyledNoPatternsDiv = styled("div")(
|
||||
SXStyleOptions({
|
||||
color: "grey.500",
|
||||
@ -35,6 +39,19 @@ const StyledNoPatternsDiv = styled("div")(
|
||||
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 { t } = useTranslate();
|
||||
const [patterns, setPatterns] = useState<ValueWithId<Pattern>[]>(
|
||||
@ -44,8 +61,8 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<any>();
|
||||
const addInput = () => {
|
||||
setPatterns([...patterns, createValueWithId<Pattern>("")]);
|
||||
const addInput = (defaultValue: Pattern) => {
|
||||
setPatterns([...patterns, createValueWithId<Pattern>(defaultValue)]);
|
||||
};
|
||||
const removeInput = (index: number) => {
|
||||
const updatedPatterns = [...patterns];
|
||||
@ -64,18 +81,13 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
}, [patterns]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box display="flex" flexDirection="column">
|
||||
{patterns.length == 0 ? (
|
||||
<StyledNoPatternsDiv>{t("label.no_patterns")}</StyledNoPatternsDiv>
|
||||
) : (
|
||||
patterns.map(({ value, id }, idx) => (
|
||||
<Fragment key={id}>
|
||||
<Grid item xs={1}>
|
||||
<IconButton onClick={() => removeInput(idx)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Box display="flex" mt={2} key={id}>
|
||||
<PatternInput
|
||||
idx={idx}
|
||||
value={value}
|
||||
@ -87,19 +99,20 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
t("message.text_is_required"),
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
<IconButton size="small" color="error" onClick={() => removeInput(idx)}>
|
||||
<RemoveCircleOutline />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Grid>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={addInput}
|
||||
startIcon={<AddIcon />}
|
||||
sx={{ marginTop: 2, float: "right" }}
|
||||
>
|
||||
{t("button.add_pattern")}
|
||||
</Button>
|
||||
</Box>
|
||||
<DropdownButton
|
||||
sx={{ alignSelf: "end", marginTop: 2 }}
|
||||
label={t("button.add_pattern")}
|
||||
actions={actions}
|
||||
onClick={(action) => addInput(action.defaultValue as Pattern)}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user