diff --git a/api/src/chat/schemas/types/message.ts b/api/src/chat/schemas/types/message.ts index 5c8d9be..2c6f2fa 100644 --- a/api/src/chat/schemas/types/message.ts +++ b/api/src/chat/schemas/types/message.ts @@ -70,6 +70,8 @@ export enum FileType { export enum PayloadType { location = 'location', attachments = 'attachments', + quick_reply = 'quick_reply', + button = 'button', } export type StdOutgoingTextMessage = { text: string }; diff --git a/api/src/chat/schemas/types/quick-reply.ts b/api/src/chat/schemas/types/quick-reply.ts index 700cbb7..df47e1a 100644 --- a/api/src/chat/schemas/types/quick-reply.ts +++ b/api/src/chat/schemas/types/quick-reply.ts @@ -7,11 +7,7 @@ */ import { IncomingAttachmentPayload } from './attachment'; - -export enum PayloadType { - location = 'location', - attachments = 'attachments', -} +import { PayloadType } from './message'; export type Payload = | { diff --git a/api/src/utils/test/mocks/block.ts b/api/src/utils/test/mocks/block.ts index f7322e2..1c8d3c5 100644 --- a/api/src/utils/test/mocks/block.ts +++ b/api/src/utils/test/mocks/block.ts @@ -13,10 +13,13 @@ import { import { BlockFull } from '@/chat/schemas/block.schema'; import { FileType } from '@/chat/schemas/types/attachment'; import { ButtonType } from '@/chat/schemas/types/button'; -import { OutgoingMessageFormat } from '@/chat/schemas/types/message'; +import { + OutgoingMessageFormat, + PayloadType, +} from '@/chat/schemas/types/message'; import { BlockOptions, ContentOptions } from '@/chat/schemas/types/options'; import { Pattern } from '@/chat/schemas/types/pattern'; -import { PayloadType, QuickReplyType } from '@/chat/schemas/types/quick-reply'; +import { QuickReplyType } from '@/chat/schemas/types/quick-reply'; import { CaptureVar } from '@/chat/validation-rules/is-valid-capture'; import { modelInstance } from './misc'; 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 2e9f4a6..729fea4 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx @@ -151,7 +151,7 @@ const PatternInput: FC = ({ onChange={(payload) => { payload && setPattern(payload); }} - value={pattern ? (pattern as PayloadPattern).value : null} + defaultValue={pattern as PayloadPattern} /> ) : null} {typeof value === "string" && patternType === "regex" ? ( diff --git a/frontend/src/components/visual-editor/form/inputs/triggers/PostbackInput.tsx b/frontend/src/components/visual-editor/form/inputs/triggers/PostbackInput.tsx index dd25d24..cbaf8b5 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PostbackInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PostbackInput.tsx @@ -6,10 +6,17 @@ * 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 { + Autocomplete, + Box, + CircularProgress, + InputAdornment, + Skeleton, + Typography, +} from "@mui/material"; +import { useMemo, useState } from "react"; -import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect"; +import { Input } from "@/app-components/inputs/Input"; import { useFind } from "@/hooks/crud/useFind"; import { useGetFromCache } from "@/hooks/crud/useGet"; import { useTranslate } from "@/hooks/useTranslate"; @@ -26,25 +33,31 @@ import { import { useBlock } from "../../BlockFormProvider"; -type PayloadOption = { - id: string; - label: string; - group?: string; +type PayloadOption = PayloadPattern & { + group: string; +}; + +const isSamePostback = (a: T, b: T) => { + return a.label === b.label && a.value === b.value; }; type PostbackInputProps = { - value?: string | null; - onChange: (pattern: PayloadPattern) => void; + defaultValue?: PayloadPattern; + onChange: (pattern: PayloadPattern | null) => void; }; -export const PostbackInput = ({ value, onChange }: PostbackInputProps) => { +export const PostbackInput = ({ + defaultValue, + onChange, +}: PostbackInputProps) => { const block = useBlock(); + const [selectedValue, setSelectedValue] = useState(defaultValue || null); const getBlockFromCache = useGetFromCache(EntityType.BLOCK); - const { data: menu } = useFind( + const { data: menu, isLoading: isLoadingMenu } = useFind( { entity: EntityType.MENU, format: Format.FULL }, { hasCount: false }, ); - const { data: contents } = useFind( + const { data: contents, isLoading: isLoadingContent } = useFind( { entity: EntityType.CONTENT, format: Format.FULL }, { hasCount: false, @@ -54,20 +67,21 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => { // General options const generalOptions = [ { - id: "GET_STARTED", label: t("label.get_started"), - group: t("label.general"), + value: "GET_STARTED", + group: "general", }, { - id: "VIEW_MORE", label: t("label.view_more"), - group: t("label.general"), + value: "VIEW_MORE", + group: "general", }, { - id: "LOCATION", label: t("label.location"), - group: t("label.general"), + value: "LOCATION", + type: PayloadType.location, + group: "general", }, ]; // Gather previous blocks buttons @@ -92,8 +106,8 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => { }, [] as (PostBackButton & { group: string })[]) .map((btn) => { return { - id: btn.payload, label: btn.title, + value: btn.payload, group: btn.group, }; }), @@ -125,6 +139,8 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => { return { id: btn.payload as string, label: btn.title as string, + value: btn.payload as string, + type: PayloadType.menu, group: btn.group, }; }), @@ -132,9 +148,11 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => { ); const menuOptions = menu .filter(({ payload }) => payload) - .map(({ title }) => ({ + .map(({ title, payload }) => ({ id: title, label: title, + value: payload as string, + type: PayloadType.menu, group: "menu", })); const contentOptions = useMemo( @@ -158,15 +176,17 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => { return (b.options?.content?.buttons || []).reduce((payloads, btn) => { // Return a payload for each node/button combination payloads.push({ - id: btn.title, label: btn.title, + value: btn.title, + type: PayloadType.content, group: "content", }); return availableContents.reduce((acc, n) => { acc.push({ - id: n.title, label: n.title, + value: n.title, + type: PayloadType.content, group: "content", }); @@ -178,52 +198,76 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => { [block?.previousBlocks, contents, getBlockFromCache], ); // Concat all previous blocks - const options = [ + const options: PayloadOption[] = [ ...generalOptions, ...btnOptions, ...qrOptions, ...menuOptions, ...contentOptions, ]; - const isOptionsReady = !value || options.find((e) => e.id === value); + const isOptionsReady = + !defaultValue || options.find((o) => isSamePostback(o, defaultValue)); if (!isOptionsReady) { return ( ); } + const selected = defaultValue + ? options.find((o) => { + return isSamePostback(o, defaultValue); + }) + : undefined; return ( - <> - - value={value} - options={options} - labelKey="label" - label={t("label.postback")} - multiple={false} - onChange={(_e, content) => { - content && - onChange({ - label: content.label, - value: content.id, - type: ["content", "menu"].includes(content.group || "") - ? PayloadType[content?.group || ""] - : undefined, - }); - }} - groupBy={(option) => { - return option.group ?? t("label.other"); - }} - getOptionLabel={({ group, label }) => `${group}:${label}`} - renderGroup={(params) => ( -
  • - - {params.group} - - {params.children} -
  • - )} - /> - + + size="small" + defaultValue={selected} + options={options} + // label={t("label.postback")} + multiple={false} + onChange={(_e, value) => { + setSelectedValue(value); + if (value) { + const { group: _g, ...payloadPattern } = value; + + onChange(payloadPattern); + } else { + onChange(null); + } + }} + groupBy={(option) => { + return option.group ?? t("label.other"); + }} + getOptionLabel={({ label }) => label} + renderGroup={(params) => ( +
  • + + {t(`label.${params.group}`)} + + {params.children} +
  • + )} + renderInput={(props) => { + return ( + + {selectedValue?.type || t("label.postback")} + + ), + endAdornment: + isLoadingMenu || isLoadingContent ? ( + + ) : null, + }} + /> + ); + }} + /> ); }; diff --git a/frontend/src/types/block.types.ts b/frontend/src/types/block.types.ts index 5eb686d..c8825a3 100644 --- a/frontend/src/types/block.types.ts +++ b/frontend/src/types/block.types.ts @@ -66,6 +66,8 @@ export interface PayloadPattern { label: string; value: string; // @todo : rename 'attachment' to 'attachments' + // @todo: If undefined, that means the payload could be either quick_reply or button + // We will move soon so that it will be a required attribute type?: PayloadType; } diff --git a/frontend/src/types/message.types.ts b/frontend/src/types/message.types.ts index a1b9dea..50b5fb7 100644 --- a/frontend/src/types/message.types.ts +++ b/frontend/src/types/message.types.ts @@ -28,6 +28,8 @@ export enum PayloadType { attachments = "attachments", menu = "menu", content = "content", + quick_reply = "quick_reply", + button = "button", } export enum FileType {