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 0bab40f..3e7f3ed 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx @@ -10,12 +10,10 @@ import { Grid, MenuItem, TextFieldProps } from "@mui/material"; import { FC, useEffect, useState } from "react"; import { RegisterOptions, useFormContext } from "react-hook-form"; -import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; 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 { EntityType, Format } from "@/services/types"; import { IBlockAttributes, IBlockFull, @@ -24,10 +22,8 @@ import { PatternType, PayloadPattern, } from "@/types/block.types"; -import { IMenuItem } from "@/types/menu.types"; -import { ContentPostbackInput } from "./ContentPostbackInput"; -import { PostbackInput } from "./PostbackInput"; +import { PostbackInputV2 } from "./PostbackInputV2"; const isRegex = (str: Pattern) => { return typeof str === "string" && str.startsWith("/") && str.endsWith("/"); @@ -77,8 +73,6 @@ const PatternInput: FC = ({ { value: "regex", label: t("label.regex") }, { value: "payload", label: t("label.postback") }, { value: "nlp", label: t("label.nlp") }, - { value: "menu", label: t("label.menu") }, - { value: "content", label: t("label.content") }, ]; const registerInput = ( errorMessage: string, @@ -110,7 +104,11 @@ const PatternInput: FC = ({ { const selected = e.target.value as PatternType; @@ -151,39 +149,8 @@ const PatternInput: FC = ({ onChange={setPattern} /> )} - {patternType === "menu" ? ( - - value={pattern ? (pattern as PayloadPattern).value : null} - searchFields={["title"]} - entity={EntityType.MENU} - format={Format.BASIC} - idKey="payload" - labelKey="title" - label={t("label.menu")} - multiple={false} - onChange={(_e, menuItem) => { - menuItem && - setPattern({ - label: menuItem?.title, - value: menuItem?.payload, - type: "menu", - } as PayloadPattern); - }} - preprocess={(items) => { - return items.filter((item) => "payload" in item); - }} - /> - ) : null} - {patternType === "content" ? ( - { - payload && setPattern(payload); - }} - value={pattern ? (pattern as PayloadPattern).value : null} - /> - ) : null} - {patternType === "payload" ? ( - { payload && setPattern(payload); }} diff --git a/frontend/src/components/visual-editor/form/inputs/triggers/PostbackInputV2.tsx b/frontend/src/components/visual-editor/form/inputs/triggers/PostbackInputV2.tsx new file mode 100644 index 0000000..ce692a5 --- /dev/null +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PostbackInputV2.tsx @@ -0,0 +1,234 @@ +/* + * 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 { Box, Skeleton, Typography } from "@mui/material"; +import { useMemo } from "react"; + +import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect"; +import { useFind } from "@/hooks/crud/useFind"; +import { useGetFromCache } from "@/hooks/crud/useGet"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType, Format } from "@/services/types"; +import { IBlock, PayloadPattern } from "@/types/block.types"; +import { + PostBackButton, + StdOutgoingButtonsMessage, + StdOutgoingQuickRepliesMessage, + StdQuickReply, +} from "@/types/message.types"; + +import { useBlock } from "../../BlockFormProvider"; + +type PayloadOption = { + id: string; + label: string; + group?: string; +}; + +type ContentPayloadOption = { + id: string; + label: string; + group?: string; +}; + +type PostbackInputProps = { + value?: string | null; + onChange: (pattern: PayloadPattern) => void; +}; + +export const PostbackInputV2 = ({ value, onChange }: PostbackInputProps) => { + const block = useBlock(); + const getBlockFromCache = useGetFromCache(EntityType.BLOCK); + const { data: menu } = useFind( + { entity: EntityType.MENU, format: Format.FULL }, + { hasCount: false }, + ); + const { data: contents } = useFind( + { entity: EntityType.CONTENT, format: Format.FULL }, + { + hasCount: false, + }, + ); + const { t } = useTranslate(); + // General options + const generalOptions = [ + { + id: "GET_STARTED", + label: t("label.get_started"), + group: t("label.general"), + }, + + { + id: "VIEW_MORE", + label: t("label.view_more"), + group: t("label.general"), + }, + { + id: "LOCATION", + label: t("label.location"), + group: t("label.general"), + }, + ]; + // Gather previous blocks buttons + const btnOptions = useMemo( + () => + (block?.previousBlocks || []) + .map((b) => getBlockFromCache(b)) + .filter((b) => { + return b && typeof b.message === "object" && "buttons" in b.message; + }) + .map((b) => b as IBlock) + .reduce((acc, b) => { + const postbackButtons = ( + (b.message as StdOutgoingButtonsMessage)?.buttons || [] + ) + .filter((btn) => btn.type === "postback") + .map((btn) => { + return { ...btn, group: b.name }; + }); + + return acc.concat(postbackButtons); + }, [] as (PostBackButton & { group: string })[]) + .map((btn) => { + return { + id: btn.payload, + label: btn.title, + group: btn.group, + }; + }), + [block?.previousBlocks, getBlockFromCache], + ); + // Gather previous blocks quick replies + const qrOptions = useMemo( + () => + (block?.previousBlocks || []) + .map((b) => getBlockFromCache(b)) + .filter((b) => { + return ( + b && typeof b.message === "object" && "quickReplies" in b.message + ); + }) + .map((b) => b as IBlock) + .reduce((acc, b) => { + const postbackQuickReplies = ( + (b.message as StdOutgoingQuickRepliesMessage)?.quickReplies || [] + ) + .filter((btn) => btn.content_type === "text") + .map((btn) => { + return { ...btn, group: b.name }; + }); + + return acc.concat(postbackQuickReplies); + }, [] as (StdQuickReply & { group: string })[]) + .map((btn) => { + return { + id: btn.payload as string, + label: btn.title as string, + group: btn.group, + }; + }), + [block?.previousBlocks], + ); + const menuOptions = menu + .filter((menu) => menu.payload) + .map(({ payload, title }) => ({ + id: payload as string, + label: title as string, + group: "menu", + })); + const contentOptions = useMemo( + () => + (block?.previousBlocks || []) + .map((bId) => getBlockFromCache(bId)) + .filter((b) => { + return ( + b && + b.options?.content?.entity && + b.options.content.buttons.length > 0 + ); + }) + .map((b) => b as IBlock) + .map((b) => { + const availableContents = (contents || []).filter( + ({ entity, status }) => + status && entity === b.options?.content?.entity, + ); + + return (b.options?.content?.buttons || []).reduce((payloads, btn) => { + // Return a payload for each node/button combination + payloads.push({ + id: btn.title, + label: btn.title, + group: "content", + }); + + return availableContents.reduce((acc, n) => { + acc.push({ + id: n.title, + label: n.title, + group: "content", + }); + + return acc; + }, payloads); + }, [] as ContentPayloadOption[]); + }) + .flat(), + [block?.previousBlocks, contents, getBlockFromCache], + ); + // Concat all previous blocks + const options = [ + ...generalOptions, + ...btnOptions, + ...qrOptions, + ...menuOptions, + ...contentOptions, + ]; + const existOption = options.find((e) => e.id === value); + + if (!existOption) { + return ( + + ); + } + + return ( + <> + + value={value} + options={options} + labelKey="label" + label={t("label.postback")} + multiple={false} + onChange={(_e, content) => { + if (content) { + onChange({ + label: content.label, + value: content.id, + type: ["content", "menu"].includes(content.group || "") + ? content.group + : undefined, + } as PayloadPattern); + } + }} + groupBy={(option) => { + return option.group ?? t("label.other"); + }} + getOptionLabel={({ group, label }) => `${group}:${label}`} + renderGroup={(params) => ( +
  • + + {params.group} + + {params.children} +
  • + )} + /> + + ); +};