From ce6857a9fce884550ddec24c539b8607da2d1f50 Mon Sep 17 00:00:00 2001 From: Akansha Sakhre Date: Wed, 4 Sep 2024 23:44:39 +0530 Subject: [PATCH 01/27] separated hsm and speed sends --- src/components/UI/ImgFallback/ImgFallback.tsx | 2 +- src/containers/Form/FormLayout.tsx | 8 +- src/containers/Template/HSM/HSM.module.css | 102 ++ src/containers/Template/HSM/HSM.test.tsx | 319 +++++++ src/containers/Template/HSM/HSM.tsx | 889 ++++++++++++++++++ .../Template/SpeedSends/SpeedSend.module.css | 95 ++ .../Template/SpeedSends/SpeedSend.test.tsx | 133 +++ .../Template/SpeedSends/SpeedSends.tsx | 528 +++++++++++ .../Template/Template.test.helper.ts | 41 +- .../AuthenticatedRoute/AuthenticatedRoute.tsx | 4 +- 10 files changed, 2085 insertions(+), 36 deletions(-) create mode 100644 src/containers/Template/HSM/HSM.module.css create mode 100644 src/containers/Template/HSM/HSM.test.tsx create mode 100644 src/containers/Template/HSM/HSM.tsx create mode 100644 src/containers/Template/SpeedSends/SpeedSend.module.css create mode 100644 src/containers/Template/SpeedSends/SpeedSend.test.tsx create mode 100644 src/containers/Template/SpeedSends/SpeedSends.tsx diff --git a/src/components/UI/ImgFallback/ImgFallback.tsx b/src/components/UI/ImgFallback/ImgFallback.tsx index db4dcbe39..1669f6565 100644 --- a/src/components/UI/ImgFallback/ImgFallback.tsx +++ b/src/components/UI/ImgFallback/ImgFallback.tsx @@ -13,7 +13,7 @@ const ImgFallback = ({ src, alt, ...rest }: ImgProps) => { const imgRef: any = useRef(); useEffect(() => { - setImgSrc(src); + setImgSrc(src || FallbackImage); }, [src]); return ( diff --git a/src/containers/Form/FormLayout.tsx b/src/containers/Form/FormLayout.tsx index bb3b2769a..47c903b5c 100644 --- a/src/containers/Form/FormLayout.tsx +++ b/src/containers/Form/FormLayout.tsx @@ -433,7 +433,7 @@ export const FormLayout = ({ } }); // for template create media for attachment - if (isAttachment && payload.type !== 'TEXT' && payload.type) { + if (isAttachment && payload.type && payload.type !== 'TEXT') { getMediaId(payload) .then((data: any) => { if (data) { @@ -571,8 +571,10 @@ export const FormLayout = ({ variant="contained" color="primary" onClick={() => { - onSaveButtonClick(formik.errors); - formik.submitForm(); + formik.validateForm().then((errors) => { + onSaveButtonClick(errors); + formik.submitForm(); + }); }} className={styles.Button} data-testid="submitActionButton" diff --git a/src/containers/Template/HSM/HSM.module.css b/src/containers/Template/HSM/HSM.module.css new file mode 100644 index 000000000..2872755ec --- /dev/null +++ b/src/containers/Template/HSM/HSM.module.css @@ -0,0 +1,102 @@ +.Template { + margin: 20px auto; + width: 80%; + text-align: center; + box-shadow: 0 2px 3px #cccccc; + border: 1px solid #eeeeee; + padding: 10px; + box-sizing: border-box; +} + +@media (min-width: 600px) { + .Template { + width: 500px; + } +} + +.DeleteIcon { + margin-right: 9px !important; +} + +.DialogText { + margin-top: 0px; + text-align: center; + color: #073f24; + font-weight: 400; +} + +.DeleteButton { + margin-left: auto !important; +} + +.Title { + margin-left: 24px !important; + margin-top: 16px !important; + vertical-align: middle; + font-weight: 500 !important; + color: #073f24; +} + +.Input { + display: flex; + padding: 8px; +} + +.Label { + width: 50%; + align-self: center; + font-weight: bold; +} + +.TemplateAdd { + width: fit-content; +} + +.Form { + padding: 16px 16px; + width: 470px; +} + +.Buttons { + margin-top: 24px; + margin-left: 8px; + display: flex; + justify-content: flex-start; +} + +.Icon { + background-color: #eaedec !important; + margin-right: 10px !important; +} + +.ButtonsCenter { + justify-content: center !important; +} + +.Button { + margin-right: 24px !important; +} + +.Warning { + color: #ff0000; + margin-left: -43px; +} + +.IsActive { + color: #555555; + font-weight: 400; + line-height: 18px; + font-size: 16px; +} + +.TemplateIcon { + width: 29px; + height: 29px; +} + +.Checkbox { + color: #555555; + font-weight: 400; + line-height: 18px; + font-size: 16px; +} diff --git a/src/containers/Template/HSM/HSM.test.tsx b/src/containers/Template/HSM/HSM.test.tsx new file mode 100644 index 000000000..ad4ab7669 --- /dev/null +++ b/src/containers/Template/HSM/HSM.test.tsx @@ -0,0 +1,319 @@ +import { render, waitFor, within, fireEvent, screen } from '@testing-library/react'; +import { MockedProvider } from '@apollo/client/testing'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { HSM } from './HSM'; +import { + TEMPLATE_MOCKS, + getHSMTemplateTypeMedia, + getHSMTemplateTypeText, +} from 'containers/Template/Template.test.helper'; + +const mocks = TEMPLATE_MOCKS; + +beforeEach(() => { + vi.restoreAllMocks(); +}); + +vi.mock('lexical-beautiful-mentions', async (importOriginal) => { + const actual = (await importOriginal()) as typeof import('lexical-beautiful-mentions'); + return { + ...actual, + BeautifulMentionsPlugin: ({ children }: any) =>
{children}
, + BeautifulMentionsMenuProps: {}, + BeautifulMentionsMenuItemProps: {}, + }; +}); + +describe('Edit mode', () => { + test('HSM form is loaded correctly in edit mode', async () => { + const MOCKS = [...mocks, getHSMTemplateTypeText, getHSMTemplateTypeText]; + const { getByText, getAllByRole } = render( + + + + } /> + + + + ); + await waitFor(() => { + expect(getByText('Edit HSM Template')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(getAllByRole('textbox')[0]).toHaveValue('account_balance'); + }); + }); + + test('HSM templates with media', async () => { + const MOCKS = [...mocks, getHSMTemplateTypeMedia, getHSMTemplateTypeMedia]; + const { getByText, getAllByRole } = render( + + + + } /> + + + + ); + + await waitFor(() => { + expect(getByText('Edit HSM Template')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(getAllByRole('textbox')[0]).toHaveValue('account_update'); + }); + + await waitFor(() => { + expect(screen.getAllByRole('combobox')[1]).toHaveValue('IMAGE'); + }); + }); +}); + +describe('Add mode', () => { + const template = ( + + + + + + ); + const user = userEvent.setup(); + + test('check for validations for the HSM form', async () => { + const { getByText, container } = render(template); + await waitFor(() => { + expect(getByText('Add a new HSM Template')).toBeInTheDocument(); + }); + + const { queryByText } = within(container.querySelector('form') as HTMLElement); + fireEvent.click(screen.getByTestId('submitActionButton')); + + // we should have 1 errors + await waitFor(() => { + expect(queryByText('Title is required.')).toBeInTheDocument(); + }); + + fireEvent.change(container.querySelector('input[name="label"]') as HTMLInputElement, { + target: { + value: + 'We are not allowing a really long title, and we should trigger validation for this.', + }, + }); + + fireEvent.click(screen.getByTestId('submitActionButton')); + + // we should still have 1 errors + await waitFor(() => { + expect(queryByText('Title length is too long.')).toBeInTheDocument(); + }); + }); + + test('it should create a template message', async () => { + render(template); + + await waitFor(() => { + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); + }); + + const inputs = screen.getAllByRole('textbox'); + + fireEvent.change(inputs[0], { target: { value: 'element_name' } }); + fireEvent.change(inputs[1], { target: { value: 'element_name' } }); + const lexicalEditor = inputs[2]; + + await user.click(lexicalEditor); + await user.tab(); + fireEvent.input(lexicalEditor, { data: 'Hi, How are you' }); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); + + fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); + + fireEvent.click(screen.getByText('Allow meta to re-categorize template?')); + + await waitFor(() => { + expect(screen.getByText('Hi, How are you')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Add Variable')); + + await waitFor(() => { + expect(screen.getByText('Hi, How are you {{1}}')).toBeInTheDocument(); + }); + fireEvent.change(inputs[1], { target: { value: 'element_name' } }); + + fireEvent.change(screen.getByPlaceholderText('Define value'), { target: { value: 'User' } }); + + fireEvent.click(screen.getByTestId('submitActionButton')); + }); + + test('it should add and remove variables', async () => { + render(template); + + await waitFor(() => { + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); + }); + + const inputs = screen.getAllByRole('textbox'); + const lexicalEditor = inputs[2]; + + await user.click(lexicalEditor); + await user.tab(); + fireEvent.input(lexicalEditor, { data: 'Hi' }); + + await waitFor(() => { + expect(screen.getByText('Hi')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Add Variable')); + + await waitFor(() => { + expect(screen.getByText('Hi {{1}}')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('delete-variable')[0]); + }); + + test('it adds quick reply buttons', async () => { + render(template); + + await waitFor(() => { + const language = screen.getAllByTestId('AutocompleteInput')[0].querySelector('input'); + expect(language).toHaveValue('English'); + }); + + const inputs = screen.getAllByRole('textbox'); + + const elementName = inputs[0]; + const title = inputs[1]; + + await user.type(title, 'Hello'); + await user.type(elementName, 'welcome'); + + const lexicalEditor = inputs[2]; + + await user.click(lexicalEditor); + await user.tab(); + fireEvent.input(lexicalEditor, { data: 'Hi' }); + + await waitFor(() => { + expect(screen.getByText('Hi')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Add buttons')); + fireEvent.click(screen.getByText('Quick replies')); + + await user.click(screen.getByTestId('addButton')); + + fireEvent.change(screen.getByPlaceholderText('Quick reply 1 title'), { + target: { value: 'Yes' }, + }); + + fireEvent.change(screen.getByPlaceholderText('Quick reply 2 title'), { + target: { value: 'No' }, + }); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); + + fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); + + fireEvent.click(screen.getByTestId('submitActionButton')); + fireEvent.click(screen.getByTestId('submitActionButton')); + }); + + test('it adds call to action buttons', async () => { + render(template); + + await waitFor(() => { + const language = screen.getAllByTestId('AutocompleteInput')[0].querySelector('input'); + expect(language).toHaveValue('English'); + }); + + const inputs = screen.getAllByRole('textbox'); + + const elementName = inputs[0]; + const title = inputs[1]; + + await user.type(title, 'Hello'); + await user.type(elementName, 'welcome'); + + const lexicalEditor = inputs[2]; + + await user.click(lexicalEditor); + await user.tab(); + fireEvent.input(lexicalEditor, { data: 'Hi' }); + + await waitFor(() => { + expect(screen.getByText('Hi')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Add buttons')); + fireEvent.click(screen.getByText('Call to actions')); + fireEvent.click(screen.getByText('Phone number')); + + fireEvent.change(screen.getByPlaceholderText('Button Title'), { target: { value: 'Call me' } }); + fireEvent.change(screen.getByPlaceholderText('Button Value'), { + target: { value: '9876543210' }, + }); + + fireEvent.click(screen.getByText('Add Call to action')); + fireEvent.click(screen.getAllByTestId('delete-icon')[1]); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); + + fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); + + fireEvent.click(screen.getByTestId('submitActionButton')); + fireEvent.click(screen.getByTestId('submitActionButton')); + }); + + test('adding attachments', async () => { + render(template); + + await waitFor(() => { + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); + }); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + const inputs = screen.getAllByRole('textbox'); + + autocompletes[2].focus(); + fireEvent.keyDown(autocompletes[2], { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('IMAGE'), { key: 'Enter' }); + + fireEvent.change(inputs[3], { target: { value: 'https://example.com/image.jpg' } }); + + await waitFor(() => { + expect(inputs[3]).toHaveValue('https://example.com/image.jpg'); + }); + }); + + test('it creates a translation of hsm template', async () => { + render(template); + + await waitFor(() => { + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Translate existing HSM?')); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); + + fireEvent.click(screen.getByText('account_balance'), { key: 'Enter' }); + + await waitFor(() => { + expect(screen.getAllByRole('combobox')[1]).toHaveValue('account_balance'); + }); + }); +}); diff --git a/src/containers/Template/HSM/HSM.tsx b/src/containers/Template/HSM/HSM.tsx new file mode 100644 index 000000000..7f4f7a828 --- /dev/null +++ b/src/containers/Template/HSM/HSM.tsx @@ -0,0 +1,889 @@ +import { CALL_TO_ACTION, MEDIA_MESSAGE_TYPES, QUICK_REPLY } from 'common/constants'; +import { FormLayout } from 'containers/Form/FormLayout'; +import { CREATE_TEMPLATE, DELETE_TEMPLATE, UPDATE_TEMPLATE } from 'graphql/mutations/Template'; +import { GET_HSM_CATEGORIES, GET_SHORTCODES, GET_TEMPLATE } from 'graphql/queries/Template'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate, useParams } from 'react-router'; +import * as Yup from 'yup'; +import TemplateIcon from 'assets/images/icons/Template/UnselectedDark.svg?react'; +import { useLazyQuery, useMutation, useQuery } from '@apollo/client'; +import { CREATE_MEDIA_MESSAGE } from 'graphql/mutations/Chat'; +import styles from './HSM.module.css'; +import { AutoComplete } from 'components/UI/Form/AutoComplete/AutoComplete'; +import { Checkbox } from 'components/UI/Form/Checkbox/Checkbox'; +import { Typography } from '@mui/material'; +import { Input } from 'components/UI/Form/Input/Input'; +import { EmojiInput } from 'components/UI/Form/EmojiInput/EmojiInput'; +import { TemplateVariables } from '../TemplateVariables/TemplateVariables'; +import { TemplateOptions } from 'containers/TemplateOptions/TemplateOptions'; +import { CreateAutoComplete } from 'components/UI/Form/CreateAutoComplete/CreateAutoComplete'; +import { GET_TAGS } from 'graphql/queries/Tags'; +import { USER_LANGUAGES } from 'graphql/queries/Organization'; +import { Loading } from 'components/UI/Layout/Loading/Loading'; +import { Simulator } from 'components/simulator/Simulator'; +import { validateMedia } from 'common/utils'; + +const queries = { + getItemQuery: GET_TEMPLATE, + createItemQuery: CREATE_TEMPLATE, + updateItemQuery: UPDATE_TEMPLATE, + deleteItemQuery: DELETE_TEMPLATE, +}; + +interface CallToActionTemplate { + type: string; + title: string; + value: string; +} + +interface QuickReplyTemplate { + value: string; +} + +const redirectionLink = 'template'; + +const regexForShortcode = /^[a-z0-9_]+$/g; +const dialogMessage = 'It will stop showing when you are drafting a customized message.'; +const mediaTypes = MEDIA_MESSAGE_TYPES.map((option: string) => ({ + id: option, + label: option, +})).filter(({ label }) => label !== 'AUDIO' && label !== 'STICKER'); +const templateIcon = ; + +const convertButtonsToTemplate = (templateButtons: Array, templateType: string | null) => + templateButtons.reduce((result: any, temp: any) => { + const { title, value } = temp; + if (templateType === CALL_TO_ACTION && value && title) { + result.push(`[${title}, ${value}]`); + } + if (templateType === QUICK_REPLY && value) { + result.push(`[${value}]`); + } + return result; + }, []); + +const getTemplateAndButtons = (templateType: string, message: string, buttons: string) => { + const templateButtons = JSON.parse(buttons); + let result: any; + if (templateType === CALL_TO_ACTION) { + result = templateButtons.map((button: any) => { + const { phone_number: phoneNo, url, type, text } = button; + return { type, value: url || phoneNo, title: text }; + }); + } + + if (templateType === QUICK_REPLY) { + result = templateButtons.map((button: any) => { + const { text, type } = button; + return { type, value: text }; + }); + } + + // Getting in template format of gupshup + const templateFormat = convertButtonsToTemplate(result, templateType); + // Pre-pending message with buttons + const template = `${message} | ${templateFormat.join(' | ')}`; + return { buttons: result, template }; +}; + +const getExampleFromBody = (body: string, variables: Array) => { + return body.replace(/{{(\d+)}}/g, (match, number) => { + let index = parseInt(number) - 1; + + return variables[index]?.text + ? variables[index] + ? `[${variables[index]?.text}]` + : match + : `{{${number}}}`; + }); +}; + +const getVariables = (message: string, variables: any) => { + const regex = /{{\d+}}/g; + const matches = message.match(regex); + + if (!matches) { + return []; + } + + return matches.map((match, index) => + variables[index]?.text ? variables[index] : { text: '', id: index + 1 } + ); +}; + +const getExampleValue = (example: string) => { + const regex = /\[([^\]]+)\]/g; + let match; + const variables = []; + let id = 1; + + while ((match = regex.exec(example)) !== null) { + variables.push({ text: match[1], id }); + id++; + } + + return variables; +}; + +export const HSM = () => { + const [sampleMessages, setSampleMessages] = useState({ + type: 'TEXT', + location: null, + media: {}, + body: '', + }); + const [tagId, setTagId] = useState(null); + const [label, setLabel] = useState(''); + const [body, setBody] = useState(''); + const [language, setLanguageId] = useState(null); + const [type, setType] = useState(null); + const [translations, setTranslations] = useState(); + const [attachmentURL, setAttachmentURL] = useState(''); + const [languageOptions, setLanguageOptions] = useState([]); + const [isActive, setIsActive] = useState(true); + const [validatingURL, setValidatingURL] = useState(false); + const [warning, setWarning] = useState(); + const [isUrlValid, setIsUrlValid] = useState(); + const [templateType, setTemplateType] = useState(null); + const [templateButtons, setTemplateButtons] = useState< + Array + >([]); + const [isAddButtonChecked, setIsAddButtonChecked] = useState(false); + const [nextLanguage, setNextLanguage] = useState(''); + const [variables, setVariables] = useState([]); + const [editorState, setEditorState] = useState(''); + + const [exisitingShortCode, setExistingShortcode] = useState(''); + const [newShortcode, setNewShortcode] = useState(''); + const [category, setCategory] = useState(undefined); + const [languageVariant, setLanguageVariant] = useState(false); + const [allowTemplateCategoryChange, setAllowTemplateCategoryChange] = useState(true); + + const { t } = useTranslation(); + const navigate = useNavigate(); + const location: any = useLocation(); + const params = useParams(); + + const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE); + const { data: categoryList, loading } = useQuery(GET_HSM_CATEGORIES); + const { data: shortCodes } = useQuery(GET_SHORTCODES, { + variables: { + filter: { + isHsm: true, + }, + }, + }); + + const { data: tag, loading: tagLoading } = useQuery(GET_TAGS, { + variables: {}, + fetchPolicy: 'network-only', + }); + + const { data: languages, loading: languageLoading } = useQuery(USER_LANGUAGES, { + variables: { opts: { order: 'ASC' } }, + }); + + const [getSessionTemplate, { data: template, loading: templateLoading }] = + useLazyQuery(GET_TEMPLATE); + + let isEditing = false; + let mode; + let isCopyState; + + // disable fields in edit mode for hsm template + if (params.id && !isCopyState) { + isEditing = true; + } + + useEffect(() => { + if (languages) { + const lang = languages.currentUser.user.organization.activeLanguages.slice(); + // sort languages by thaeir name + lang.sort((first: any, second: any) => (first.label > second.label ? 1 : -1)); + + setLanguageOptions(lang); + if (!isEditing) setLanguageId(lang[0]); + } + }, [languages]); + + useEffect(() => { + if (params.id) { + getSessionTemplate({ variables: { id: params.id } }); + } + }, []); + + useEffect(() => { + setVariables(getVariables(body, variables)); + }, [body]); + + useEffect(() => { + if (!isEditing) { + setSimulatorMessage(getExampleFromBody(body, variables)); + } + }, [body, variables]); + + useEffect(() => { + if (!isEditing) { + const { message }: any = getTemplateAndButton(getExampleFromBody(body, variables)); + setSimulatorMessage(message || ''); + } + }, [isAddButtonChecked]); + + useEffect(() => { + if (templateType && !isEditing) { + addTemplateButtons(false); + } + }, [templateType]); + + useEffect(() => { + if (templateButtons.length > 0 && !isEditing) { + const parse = convertButtonsToTemplate(templateButtons, templateType); + + const parsedText = parse.length ? `| ${parse.join(' | ')}` : null; + + const { message }: any = getTemplateAndButton(getExampleFromBody(body, variables)); + + const sampleText: any = parsedText && message + parsedText; + if (sampleText) { + setSimulatorMessage(sampleText); + } + } + }, [templateButtons]); + + useEffect(() => { + setSimulatorMessage(getExampleFromBody(body, variables)); + + if ((type === '' || type) && attachmentURL) { + validateURL(attachmentURL); + } + }, [type, attachmentURL]); + + const states = { + language, + label, + body, + type, + attachmentURL, + category, + tagId, + isActive, + templateButtons, + isAddButtonChecked, + languageVariant, + variables, + newShortcode, + exisitingShortCode, + allowTemplateCategoryChange, + }; + + const categoryOpn: any = []; + if (categoryList) { + categoryList.whatsappHsmCategories.forEach((categories: any, index: number) => { + categoryOpn.push({ label: categories, id: index }); + }); + } + + const shortCodeOptions: any = []; + if (shortCodes) { + shortCodes.sessionTemplates.forEach((value: any, index: number) => { + shortCodeOptions.push({ label: value?.shortcode, id: index }); + }); + } + + const setStates = ({ + isActive: isActiveValue, + language: languageIdValue, + label: labelValue, + body: bodyValue, + example: exampleValue, + type: typeValue, + translations: translationsValue, + MessageMedia: MessageMediaValue, + shortcode: shortcodeValue, + category: categoryValue, + tag: tagIdValue, + buttonType: templateButtonType, + buttons, + hasButtons, + allowTemplateCategoryChange: allowCategoryChangeValue, + }: any) => { + if (languageOptions.length > 0 && languageIdValue) { + if (location.state && location.state !== 'copy') { + const selectedLangauge = languageOptions.find( + (lang: any) => lang.label === location.state.language + ); + navigate(location.pathname); + setLanguageId(selectedLangauge); + } else if (!language?.id) { + const selectedLangauge = languageOptions.find( + (lang: any) => lang.id === languageIdValue.id + ); + setLanguageId(selectedLangauge); + } else { + setLanguageId(language); + } + } + + setLabel(labelValue); + setIsActive(isActiveValue); + let variables: any = []; + if (typeof bodyValue === 'string') { + setBody(bodyValue || ''); + setEditorState(bodyValue || ''); + } + + if (exampleValue) { + variables = getExampleValue(exampleValue); + setVariables(variables); + + if (hasButtons) { + const { buttons: buttonsVal } = getTemplateAndButtons( + templateButtonType, + exampleValue, + buttons + ); + setTemplateButtons(buttonsVal); + setTemplateType(templateButtonType); + setIsAddButtonChecked(hasButtons); + const parse = convertButtonsToTemplate(buttonsVal, templateButtonType); + const parsedText = parse.length ? `| ${parse.join(' | ')}` : null; + const { message }: any = getTemplateAndButton(getExampleFromBody(bodyValue, variables)); + const sampleText: any = parsedText && message + parsedText; + setSimulatorMessage(sampleText); + } else { + setSimulatorMessage(getExampleFromBody(bodyValue, variables)); + } + } + + if (shortcodeValue) { + setNewShortcode(shortcodeValue); + } + + if (typeValue && typeValue !== 'TEXT') { + setType({ id: typeValue, label: typeValue }); + } else { + setType(null); + } + if (translationsValue) { + const translationsCopy = JSON.parse(translationsValue); + const currentLanguage = language?.id || languageIdValue.id; + if ( + Object.keys(translationsCopy).length > 0 && + translationsCopy[currentLanguage] && + !location.state + ) { + const content = translationsCopy[currentLanguage]; + setLabel(content.label); + setBody(content.body || ''); + setEditorState(content.body || ''); + } + setTranslations(translationsValue); + } + if (MessageMediaValue) { + setAttachmentURL(MessageMediaValue.sourceUrl); + } else { + setAttachmentURL(''); + } + if (categoryValue) { + setCategory(categoryValue); + } + if (tagIdValue) { + setTagId(tagIdValue); + } + if (setAllowTemplateCategoryChange) { + setAllowTemplateCategoryChange(allowCategoryChangeValue); + } + }; + + const getButtonTemplatePayload = () => { + const buttons = templateButtons.reduce((result: any, button) => { + const { type: buttonType, value, title }: any = button; + if (templateType === CALL_TO_ACTION) { + const typeObj: any = { + phone_number: 'PHONE_NUMBER', + url: 'URL', + }; + const obj: any = { type: typeObj[buttonType], text: title, [buttonType]: value }; + result.push(obj); + } + + if (templateType === QUICK_REPLY) { + const obj: any = { type: QUICK_REPLY, text: value }; + result.push(obj); + } + return result; + }, []); + + // get template body + const templateBody = getTemplateAndButton(body); + const templateExample = getTemplateAndButton(getExampleFromBody(body, variables)); + + return { + hasButtons: true, + buttons: JSON.stringify(buttons), + buttonType: templateType, + body: templateBody.message, + example: templateExample.message, + }; + }; + + const setPayload = (payload: any) => { + let payloadCopy = payload; + let translationsCopy: any = {}; + + // Create template + payloadCopy.languageId = payload.language.id; + if (payloadCopy.type) { + payloadCopy.type = payloadCopy.type.id; + // STICKER is a type of IMAGE + if (payloadCopy.type.id === 'STICKER') { + payloadCopy.type = 'IMAGE'; + } + } else { + payloadCopy.type = 'TEXT'; + } + payloadCopy.category = category.label; + if (payloadCopy.body) { + payloadCopy.example = getExampleFromBody(payloadCopy.body, variables); + } + if (languageVariant) { + payloadCopy.shortcode = exisitingShortCode.label; + } else { + payloadCopy.shortcode = newShortcode; + } + if (isAddButtonChecked && templateType) { + const templateButtonData = getButtonTemplatePayload(); + Object.assign(payloadCopy, { ...templateButtonData }); + } + + if (payloadCopy.type === 'TEXT') { + delete payloadCopy.attachmentURL; + } + payloadCopy.translations = JSON.stringify(translationsCopy); + + delete payloadCopy.isAddButtonChecked; + delete payloadCopy.templateButtons; + delete payloadCopy.language; + delete payloadCopy.languageVariant; + delete payloadCopy.variables; + delete payloadCopy.exisitingShortCode; + delete payloadCopy.newShortcode; + + return payloadCopy; + }; + + const getLanguageId = (value: any) => { + let result = value; + + // create translations only while updating + if (result && isEditing) { + // updateTranslation(result); + } + if (result) setLanguageId(result); + }; + + const getMediaId = async (payload: any) => { + const data = await createMediaMessage({ + variables: { + input: { + caption: payload.body, + sourceUrl: payload.attachmentURL, + url: payload.attachmentURL, + }, + }, + }); + return data; + }; + + const addTemplateButtons = (addFromTemplate: boolean = true) => { + let buttons: any = []; + const buttonType: any = { + QUICK_REPLY: { value: '' }, + CALL_TO_ACTION: { type: '', title: '', value: '' }, + }; + + if (templateType) { + buttons = addFromTemplate + ? [...templateButtons, buttonType[templateType]] + : [buttonType[templateType]]; + } + + setTemplateButtons(buttons); + }; + + const removeTemplateButtons = (index: number) => { + const result = templateButtons.filter((val, idx) => idx !== index); + setTemplateButtons(result); + }; + + const getTemplateAndButton = (text: string) => { + const exp = /(\|\s\[)|(\|\[)/; + const areButtonsPresent = text.search(exp); + + let message: any = text; + let buttons: any = null; + + if (areButtonsPresent !== -1) { + buttons = text.substr(areButtonsPresent); + message = text.substr(0, areButtonsPresent); + } + + return { message, buttons }; + }; + + const handeInputChange = (event: any, row: any, index: any, eventType: any) => { + const { value } = event.target; + const obj = { ...row }; + obj[eventType] = value; + + const result = templateButtons.map((val: any, idx: number) => { + if (idx === index) return obj; + return val; + }); + + setTemplateButtons(result); + }; + + const getTemplate = (text: string) => { + const { body } = sampleMessages; + /** + * Regular expression to check if message contains given pattern + * If pattern is present search will return first index of given pattern + * otherwise it will return -1 + */ + const exp = /(\|\s\[)|(\|\[)/; + + const areButtonsPresent = body.search(exp); + if (areButtonsPresent > -1) { + const buttons = body.substr(areButtonsPresent); + return text + buttons; + } + return text; + }; + + const validateURL = (value: string) => { + if (value && type) { + setValidatingURL(true); + validateMedia(value, type.id, false).then((response: any) => { + if (!response.data.is_valid) { + setIsUrlValid(response.data.message); + } else { + setIsUrlValid(''); + } + setValidatingURL(false); + }); + } + }; + + const removeFirstLineBreak = (text: any) => + text?.length === 1 ? text.slice(0, 1).replace(/(\r\n|\n|\r)/, '') : text; + + const setSimulatorMessage = (messages: any) => { + const message = removeFirstLineBreak(messages); + const mediaBody: any = { ...sampleMessages.media }; + let typeValue = 'TEXT'; + let text = messages; + + if (isAddButtonChecked) { + text = getTemplate(message); + } + + mediaBody.caption = getExampleFromBody(body, variables); + mediaBody.url = attachmentURL; + typeValue = type?.id || 'TEXT'; + + setSampleMessages({ ...sampleMessages, body: text, media: mediaBody, type: typeValue }); + }; + + const FormSchema = Yup.object().shape( + { + category: Yup.object().nullable().required(t('Category is required.')), + variables: Yup.array().of( + Yup.object().shape({ + text: Yup.string().required('Variable is required').min(1, 'Text cannot be empty'), + }) + ), + newShortcode: Yup.string().when('languageVariant', { + is: (val: any) => val === true, + then: (schema) => schema.nullable(), + otherwise: (schema) => + schema + .required(t('Element name is required.')) + .matches( + regexForShortcode, + 'Only lowercase alphanumeric characters and underscores are allowed.' + ), + }), + exisitingShortCode: Yup.object().when('languageVariant', { + is: (val: any) => val === true, + then: (schema) => schema.nullable().required(t('Element name is required.')), + otherwise: (schema) => schema.nullable(), + }), + language: Yup.object().nullable().required('Language is required.'), + label: Yup.string().required(t('Title is required.')).max(50, t('Title length is too long.')), + type: Yup.object() + .nullable() + .when('attachmentURL', { + is: (val: string) => val && val !== '', + then: (schema) => schema.nullable().required(t('Type is required.')), + }), + attachmentURL: Yup.string() + .nullable() + .when('type', { + is: (val: any) => val && val.id, + then: (schema) => schema.required(t('Attachment URL is required.')), + }), + body: Yup.string() + .required(t('Message is required.')) + .max(1024, 'Maximum 1024 characters are allowed'), + }, + [['type', 'attachmentURL']] + ); + + const templateRadioOptions = [ + { + component: Checkbox, + title: ( + + Add buttons + + ), + name: 'isAddButtonChecked', + disabled: !!(params.id && !isCopyState), + handleChange: (value: boolean) => setIsAddButtonChecked(value), + }, + { + component: TemplateOptions, + isAddButtonChecked, + templateType, + inputFields: templateButtons, + disabled: isEditing, + onAddClick: addTemplateButtons, + onRemoveClick: removeTemplateButtons, + onInputChange: handeInputChange, + onTemplateTypeChange: (value: string) => setTemplateType(value), + }, + ]; + + const attachmentField = [ + { + component: AutoComplete, + name: 'type', + options: mediaTypes, + optionLabel: 'label', + multiple: false, + label: t('Attachment Type'), + disabled: isEditing, + helperText: warning, + onChange: (event: any) => { + const val = event; + if (!event) { + setIsUrlValid(val); + } + setType(val); + }, + }, + { + component: Input, + name: 'attachmentURL', + type: 'text', + label: t('Attachment URL'), + validate: () => isUrlValid, + disabled: isEditing, + helperText: t( + 'Please provide a sample attachment for approval purpose. You may send a similar but different attachment when sending the HSM to users.' + ), + inputProp: { + onBlur: (event: any) => { + setAttachmentURL(event.target.value); + }, + onChange: (event: any) => { + setAttachmentURL(event.target.value); + }, + }, + }, + ]; + + const formFields = [ + { + component: AutoComplete, + name: 'language', + options: languageOptions, + optionLabel: 'label', + multiple: false, + label: `${t('Language')}*`, + disabled: isEditing, + onChange: getLanguageId, + }, + { + component: Checkbox, + name: 'languageVariant', + title: ( + + Translate existing HSM? + + ), + handleChange: (value: any) => setLanguageVariant(value), + skip: isEditing, + }, + { + component: Input, + name: 'newShortcode', + placeholder: `${t('Element name')}*`, + label: `${t('Element name')}*`, + disabled: isEditing, + skip: languageVariant ? true : false, + onChange: (value: any) => { + setNewShortcode(value); + }, + }, + { + component: AutoComplete, + name: 'exisitingShortCode', + options: shortCodeOptions, + optionLabel: 'label', + multiple: false, + label: `${t('Element name')}*`, + placeholder: `${t('Element name')}*`, + disabled: isEditing, + onChange: (event: any) => { + setExistingShortcode(event); + }, + skip: languageVariant ? false : true, + }, + { + component: Input, + name: 'label', + label: t('Title'), + disabled: isEditing, + helperText: t( + 'Define what use case does this template serve eg. OTP, optin, activity preference' + ), + inputProp: { + onBlur: (event: any) => setLabel(event.target.value), + }, + }, + { + component: Checkbox, + name: 'isActive', + title: ( + + Active? + + ), + darkCheckbox: true, + }, + { + component: EmojiInput, + name: 'body', + label: t('Message'), + rows: 5, + convertToWhatsApp: true, + textArea: true, + disabled: isEditing, + helperText: + 'You can provide variable values in your HSM templates to personalize the message. To add: click on the variable button and provide an example value for the variable in the field provided below', + handleChange: (value: any) => { + setBody(value); + }, + defaultValue: isEditing && editorState, + }, + { + component: TemplateVariables, + message: body, + variables: variables, + setVariables: setVariables, + isEditing: isEditing, + }, + ...templateRadioOptions, + { + component: AutoComplete, + name: 'category', + options: categoryOpn, + optionLabel: 'label', + multiple: false, + label: `${t('Category')}*`, + placeholder: `${t('Category')}*`, + disabled: isEditing, + helperText: t('Select the most relevant category'), + onChange: (event: any) => { + setCategory(event); + }, + skip: isEditing, + }, + { + component: Input, + name: 'category', + type: 'text', + label: `${t('Category')}*`, + placeholder: `${t('Category')}*`, + disabled: isEditing, + helperText: t('Select the most relevant category'), + skip: !isEditing, + }, + { + component: Checkbox, + name: 'allowTemplateCategoryChange', + title: ( + + Allow meta to re-categorize template? + + ), + darkCheckbox: true, + disabled: isEditing, + handleChange: (value: boolean) => setAllowTemplateCategoryChange(value), + }, + ...attachmentField, + { + component: CreateAutoComplete, + name: 'tagId', + options: tag ? tag.tags : [], + optionLabel: 'label', + disabled: isEditing, + hasCreateOption: true, + multiple: false, + onChange: (value: any) => { + setTagId(value); + }, + label: t('Tag'), + helperText: t('Use this to categorize your templates.'), + }, + ]; + + if (languageLoading || templateLoading || tagLoading) { + return ; + } + + return ( + <> + + + + ); +}; + +export default HSM; diff --git a/src/containers/Template/SpeedSends/SpeedSend.module.css b/src/containers/Template/SpeedSends/SpeedSend.module.css new file mode 100644 index 000000000..44bb09d3a --- /dev/null +++ b/src/containers/Template/SpeedSends/SpeedSend.module.css @@ -0,0 +1,95 @@ +.SpeedSendIcon { + width: 29px; + height: 29px; +} + +.Template { + margin: 20px auto; + width: 80%; + text-align: center; + box-shadow: 0 2px 3px #cccccc; + border: 1px solid #eeeeee; + padding: 10px; + box-sizing: border-box; +} + +@media (min-width: 600px) { + .Template { + width: 500px; + } +} + +.DeleteIcon { + margin-right: 9px !important; +} + +.DialogText { + margin-top: 0px; + text-align: center; + color: #073f24; + font-weight: 400; +} + +.DeleteButton { + margin-left: auto !important; +} + +.Title { + margin-left: 24px !important; + margin-top: 16px !important; + vertical-align: middle; + font-weight: 500 !important; + color: #073f24; +} + +.Input { + display: flex; + padding: 8px; +} + +.Label { + width: 50%; + align-self: center; + font-weight: bold; +} + +.TemplateAdd { + width: fit-content; +} + +.Form { + padding: 16px 16px; + width: 470px; +} + +.Buttons { + margin-top: 24px; + margin-left: 8px; + display: flex; + justify-content: flex-start; +} + +.Icon { + background-color: #eaedec !important; + margin-right: 10px !important; +} + +.ButtonsCenter { + justify-content: center !important; +} + +.Button { + margin-right: 24px !important; +} + +.Warning { + color: #ff0000; + margin-left: -43px; +} + +.IsActive { + color: #555555; + font-weight: 400; + line-height: 18px; + font-size: 16px; +} diff --git a/src/containers/Template/SpeedSends/SpeedSend.test.tsx b/src/containers/Template/SpeedSends/SpeedSend.test.tsx new file mode 100644 index 000000000..0e222f9ff --- /dev/null +++ b/src/containers/Template/SpeedSends/SpeedSend.test.tsx @@ -0,0 +1,133 @@ +import { render, within, fireEvent, cleanup, waitFor, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import { Routes, Route } from 'react-router-dom'; +import { SpeedSendList } from 'containers/Template/List/SpeedSendList/SpeedSendList'; +import { SPEED_SENDS_MOCKS } from 'containers/Template/Template.test.helper'; +import { setUserSession } from 'services/AuthService'; +import { SpeedSends } from './SpeedSends'; +import * as Notification from 'common/notification'; +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { BeautifulMentionNode } from 'lexical-beautiful-mentions'; + +setUserSession(JSON.stringify({ roles: ['Admin'] })); + +const mocks = SPEED_SENDS_MOCKS; + +const mockIntersectionObserver = class { + constructor() {} + observe() {} + unobserve() {} + disconnect() {} +}; + +(window as any).IntersectionObserver = mockIntersectionObserver; + +afterEach(() => { + cleanup(); +}); + +vi.mock('lexical-beautiful-mentions', async (importOriginal) => { + const actual = (await importOriginal()) as typeof import('lexical-beautiful-mentions'); + return { + ...actual, + BeautifulMentionsPlugin: ({ children }: any) =>
{children}
, + BeautifulMentionsMenuProps: {}, + BeautifulMentionsMenuItemProps: {}, + }; +}); + +describe('SpeedSend', () => { + test('cancel button should redirect to SpeedSendlist page', async () => { + const { container, getByText } = render( + + + + } /> + } /> + + + + ); + + await waitFor(() => { + const { queryByText } = within(container.querySelector('form') as HTMLElement); + const button = queryByText('Cancel') as HTMLButtonElement; + fireEvent.click(button); + }); + + await waitFor(() => { + expect(getByText('Speed sends')).toBeInTheDocument(); + }); + }); + + test('should have correct validations ', async () => { + const { container } = render( + + + console.log(error), + nodes: [BeautifulMentionNode], + }} + > + + } /> + } /> + + + + + ); + + await waitFor(() => { + fireEvent.change(container.querySelector('input[name="label"]') as HTMLInputElement, { + target: { value: 'new Template' }, + }); + }); + + const { queryByText } = within(container.querySelector('form') as HTMLElement); + await waitFor(() => { + const button = queryByText('Save') as HTMLButtonElement; + fireEvent.click(button); + }); + + await waitFor(() => { + expect(queryByText('Title is required.')).toBeInTheDocument(); + }); + }); + + test.skip('should test translations', async () => { + const notificationSpy = vi.spyOn(Notification, 'setNotification'); + render( + + + + } /> + + + + ); + + await waitFor(() => { + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('Marathi')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Marathi')); + + await waitFor(() => { + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('English')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('English')); + + fireEvent.click(screen.getByTestId('submitActionButton')); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/containers/Template/SpeedSends/SpeedSends.tsx b/src/containers/Template/SpeedSends/SpeedSends.tsx new file mode 100644 index 000000000..5a54cd07e --- /dev/null +++ b/src/containers/Template/SpeedSends/SpeedSends.tsx @@ -0,0 +1,528 @@ +import { MEDIA_MESSAGE_TYPES } from 'common/constants'; +import { FormLayout } from 'containers/Form/FormLayout'; +import { CREATE_TEMPLATE, DELETE_TEMPLATE, UPDATE_TEMPLATE } from 'graphql/mutations/Template'; +import { GET_TEMPLATE } from 'graphql/queries/Template'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate, useParams } from 'react-router'; +import * as Yup from 'yup'; +import SpeedSendIcon from 'assets/images/icons/SpeedSend/Selected.svg?react'; +import { useLazyQuery, useMutation, useQuery } from '@apollo/client'; +import { CREATE_MEDIA_MESSAGE } from 'graphql/mutations/Chat'; +import styles from './SpeedSend.module.css'; +import { AutoComplete } from 'components/UI/Form/AutoComplete/AutoComplete'; +import { Checkbox } from 'components/UI/Form/Checkbox/Checkbox'; +import { Typography } from '@mui/material'; +import { Input } from 'components/UI/Form/Input/Input'; +import { EmojiInput } from 'components/UI/Form/EmojiInput/EmojiInput'; +import { CreateAutoComplete } from 'components/UI/Form/CreateAutoComplete/CreateAutoComplete'; +import { GET_TAGS } from 'graphql/queries/Tags'; +import { USER_LANGUAGES } from 'graphql/queries/Organization'; +import { Loading } from 'components/UI/Layout/Loading/Loading'; +import { validateMedia } from 'common/utils'; +import { LanguageBar } from 'components/UI/LanguageBar/LanguageBar'; + +const queries = { + getItemQuery: GET_TEMPLATE, + createItemQuery: CREATE_TEMPLATE, + updateItemQuery: UPDATE_TEMPLATE, + deleteItemQuery: DELETE_TEMPLATE, +}; + +const redirectionLink = 'speed-send'; + +const dialogMessage = 'It will stop showing when you are drafting a customized message.'; +const mediaTypes = MEDIA_MESSAGE_TYPES.map((option: string) => ({ + id: option, + label: option, +})); +const speedSendIcon = ; + +export const SpeedSends = () => { + const [tagId, setTagId] = useState(null); + const [label, setLabel] = useState(''); + const [body, setBody] = useState(''); + const [language, setLanguageId] = useState(null); + const [type, setType] = useState(null); + const [translations, setTranslations] = useState(); + const [attachmentURL, setAttachmentURL] = useState(''); + const [languageOptions, setLanguageOptions] = useState([]); + const [isActive, setIsActive] = useState(true); + const [validatingURL, setValidatingURL] = useState(false); + const [warning, setWarning] = useState(); + const [isUrlValid, setIsUrlValid] = useState(); + const [nextLanguage, setNextLanguage] = useState(''); + const [editorState, setEditorState] = useState(''); + + const { t } = useTranslation(); + const navigate = useNavigate(); + const location: any = useLocation(); + const params = useParams(); + + const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE); + const { data: tag, loading: tagLoading } = useQuery(GET_TAGS, { + variables: {}, + fetchPolicy: 'network-only', + }); + + const { data: languages, loading: languageLoading } = useQuery(USER_LANGUAGES, { + variables: { opts: { order: 'ASC' } }, + }); + + const [getSessionTemplate, { data: template, loading: templateLoading }] = + useLazyQuery(GET_TEMPLATE); + + let isEditing = false; + let mode; + let isCopyState; + + // disable fields in edit mode for hsm template + if (params.id && !isCopyState) { + isEditing = true; + } + + useEffect(() => { + if (languages) { + const lang = languages.currentUser.user.organization.activeLanguages.slice(); + // sort languages by thaeir name + lang.sort((first: any, second: any) => (first.label > second.label ? 1 : -1)); + + setLanguageOptions(lang); + if (!isEditing) setLanguageId(lang[0]); + } + }, [languages]); + + useEffect(() => { + if (params.id) { + getSessionTemplate({ variables: { id: params.id } }); + } + }, []); + + useEffect(() => { + if ((type === '' || type) && attachmentURL) { + validateURL(attachmentURL); + } + }, [type, attachmentURL]); + + const states = { + language, + label, + body, + type, + attachmentURL, + tagId, + isActive, + }; + + const setStates = ({ + isActive: isActiveValue, + language: languageIdValue, + label: labelValue, + body: bodyValue, + type: typeValue, + translations: translationsValue, + MessageMedia: MessageMediaValue, + tag: tagIdValue, + }: any) => { + if (languageOptions.length > 0 && languageIdValue) { + if (location.state && location.state !== 'copy') { + const selectedLangauge = languageOptions.find( + (lang: any) => lang.label === location.state.language + ); + navigate(location.pathname); + console.log(selectedLangauge); + + setLanguageId(selectedLangauge); + } else if (!language?.id) { + const selectedLangauge = languageOptions.find( + (lang: any) => lang.id === languageIdValue.id + ); + setLanguageId(selectedLangauge); + } else { + setLanguageId(language); + } + } + + setLabel(labelValue); + setIsActive(isActiveValue); + let variables: any = []; + if (typeof bodyValue === 'string') { + setBody(bodyValue || ''); + setEditorState(bodyValue || ''); + } + + if (typeValue && typeValue !== 'TEXT') { + setType({ id: typeValue, label: typeValue }); + } else { + setType(null); + } + if (translationsValue) { + const translationsCopy = JSON.parse(translationsValue); + const currentLanguage = language?.id || languageIdValue.id; + if ( + Object.keys(translationsCopy).length > 0 && + translationsCopy[currentLanguage] && + !location.state + ) { + const content = translationsCopy[currentLanguage]; + setLabel(content.label); + setBody(content.body || ''); + setEditorState(content.body || ''); + } + setTranslations(translationsValue); + } + if (MessageMediaValue) { + setAttachmentURL(MessageMediaValue.sourceUrl); + } else { + setAttachmentURL(''); + } + + if (tagIdValue) { + setTagId(tagIdValue); + } + }; + + const setPayload = (payload: any) => { + let payloadCopy = payload; + let translationsCopy: any = {}; + + if (template) { + payloadCopy.languageId = language?.id; + if (payloadCopy.type) { + payloadCopy.type = payloadCopy.type.id; + // STICKER is a type of IMAGE + if (payloadCopy.type.id === 'STICKER') { + payloadCopy.type = 'IMAGE'; + } + } else { + payloadCopy.type = 'TEXT'; + } + + delete payloadCopy.language; + + if (payloadCopy.type === 'TEXT') { + delete payloadCopy.attachmentURL; + } + + // Removing unnecessary fields + delete payloadCopy.isAddButtonChecked; + delete payloadCopy.templateButtons; + + let messageMedia = null; + if (payloadCopy.type && payloadCopy.attachmentURL) { + messageMedia = { + type: payloadCopy.type.id, + sourceUrl: payloadCopy.attachmentURL, + }; + } + + // Update template translation + if (translations) { + translationsCopy = JSON.parse(translations); + translationsCopy[language?.id] = { + status: 'approved', + languageId: language, + label: payloadCopy.label, + body: payloadCopy.body, + MessageMedia: messageMedia, + }; + } + payloadCopy = { + translations: JSON.stringify(translationsCopy), + }; + } + + // Create template + payloadCopy.languageId = payload.language.id; + if (payloadCopy.type) { + payloadCopy.type = payloadCopy.type.id; + // STICKER is a type of IMAGE + if (payloadCopy.type.id === 'STICKER') { + payloadCopy.type = 'IMAGE'; + } + } else { + payloadCopy.type = 'TEXT'; + } + if (payloadCopy.body) { + } + + if (payloadCopy.type === 'TEXT') { + delete payloadCopy.attachmentURL; + } + payloadCopy.translations = JSON.stringify(translationsCopy); + + delete payloadCopy.isAddButtonChecked; + delete payloadCopy.templateButtons; + delete payloadCopy.language; + delete payloadCopy.languageVariant; + delete payloadCopy.variables; + delete payloadCopy.exisitingShortCode; + delete payloadCopy.newShortcode; + + return payloadCopy; + }; + + const getLanguageId = (value: any) => { + let result = value; + + // create translations only while updating + if (result && isEditing) { + // updateTranslation(result); + } + if (result) setLanguageId(result); + }; + + const getMediaId = async (payload: any) => { + const data = await createMediaMessage({ + variables: { + input: { + caption: payload.body, + sourceUrl: payload.attachmentURL, + url: payload.attachmentURL, + }, + }, + }); + return data; + }; + + const validateURL = (value: string) => { + if (value && type) { + setValidatingURL(true); + validateMedia(value, type.id, false).then((response: any) => { + if (!response.data.is_valid) { + setIsUrlValid(response.data.message); + } else { + setIsUrlValid(''); + } + setValidatingURL(false); + }); + } + }; + + const updateStates = ({ + language: languageIdValue, + label: labelValue, + body: bodyValue, + type: typeValue, + MessageMedia: MessageMediaValue, + }: any) => { + if (languageIdValue) { + setLanguageId(languageIdValue); + } + + setLabel(labelValue); + + if (typeof bodyValue === 'string') { + setBody(bodyValue || ''); + setEditorState(bodyValue || ''); + } + + if (typeValue && typeValue !== 'TEXT') { + setType({ id: typeValue, label: typeValue }); + } else { + setType(null); + } + + if (MessageMediaValue) { + setAttachmentURL(MessageMediaValue.sourceUrl); + } else { + setAttachmentURL(''); + } + }; + + const updateTranslation = (value: any) => { + const translationId = value.id; + // restore if selected language is same as template + if (template && template.sessionTemplate.sessionTemplate.language.id === value.id) { + updateStates({ + language: value, + label: template.sessionTemplate.sessionTemplate.label, + body: template.sessionTemplate.sessionTemplate.body, + type: template.sessionTemplate.sessionTemplate.type, + MessageMedia: template.sessionTemplate.sessionTemplate.MessageMedia, + }); + } else if (translations) { + const translationsCopy = JSON.parse(translations); + // restore if translations present for selected language + if (translationsCopy[translationId]) { + updateStates({ + language: value, + label: translationsCopy[translationId].label, + body: translationsCopy[translationId].body, + type: translationsCopy[translationId].MessageMedia + ? translationsCopy[translationId].MessageMedia.type + : null, + MessageMedia: translationsCopy[translationId].MessageMedia, + }); + } else { + updateStates({ + language: value, + label: '', + body: '', + type: null, + MessageMedia: null, + }); + } + } + }; + + const handleLanguageChange = (value: any) => { + const selected = languageOptions.find( + ({ label: languageLabel }: any) => languageLabel === value + ); + if (selected && isEditing) { + updateTranslation(selected); + } else if (selected) { + setLanguageId(selected); + } + }; + + const onLanguageChange = (option: string, form: any) => { + setNextLanguage(option); + const { values } = form; + if (values.label || values.body) { + return; + } + handleLanguageChange(option); + }; + + const FormSchema = Yup.object().shape( + { + language: Yup.object().nullable().required('Language is required.'), + label: Yup.string().required(t('Title is required.')).max(50, t('Title length is too long.')), + type: Yup.object() + .nullable() + .when('attachmentURL', { + is: (val: string) => val && val !== '', + then: (schema) => schema.nullable().required(t('Type is required.')), + }), + attachmentURL: Yup.string() + .nullable() + .when('type', { + is: (val: any) => val && val.id, + then: (schema) => schema.required(t('Attachment URL is required.')), + }), + body: Yup.string() + .required(t('Message is required.')) + .max(1024, 'Maximum 1024 characters are allowed'), + }, + [['type', 'attachmentURL']] + ); + + const attachmentField = [ + { + component: AutoComplete, + name: 'type', + options: mediaTypes, + optionLabel: 'label', + multiple: false, + label: t('Attachment Type'), + disabled: isEditing, + helperText: warning, + onChange: (event: any) => { + const val = event; + if (!event) { + setIsUrlValid(val); + } + setType(val); + }, + }, + { + component: Input, + name: 'attachmentURL', + type: 'text', + label: t('Attachment URL'), + validate: () => isUrlValid, + disabled: isEditing, + helperText: t( + 'Please provide a sample attachment for approval purpose. You may send a similar but different attachment when sending the HSM to users.' + ), + inputProp: { + onBlur: (event: any) => { + setAttachmentURL(event.target.value); + }, + onChange: (event: any) => { + setAttachmentURL(event.target.value); + }, + }, + }, + ]; + + const langOptions = languageOptions && languageOptions.map((val: any) => val.label); + + const formFields = [ + { + component: LanguageBar, + options: langOptions || [], + selectedLangauge: language && language.label, + onLanguageChange, + }, + { + component: Input, + name: 'label', + label: t('Title'), + disabled: isEditing, + inputProp: { + onBlur: (event: any) => setLabel(event.target.value), + }, + }, + { + component: Checkbox, + name: 'isActive', + title: ( + + Active? + + ), + darkCheckbox: true, + }, + { + component: EmojiInput, + name: 'body', + label: t('Message'), + rows: 5, + convertToWhatsApp: true, + textArea: true, + disabled: isEditing, + handleChange: (value: any) => { + setBody(value); + }, + defaultValue: isEditing && editorState, + }, + ...attachmentField, + ]; + + if (languageLoading || templateLoading || tagLoading) { + return ; + } + + return ( + + ); +}; + +export default SpeedSends; diff --git a/src/containers/Template/Template.test.helper.ts b/src/containers/Template/Template.test.helper.ts index 633b7ca70..7543f517e 100644 --- a/src/containers/Template/Template.test.helper.ts +++ b/src/containers/Template/Template.test.helper.ts @@ -317,21 +317,19 @@ const createHsmWithButtontemplate = { variables: { input: { label: 'Hello', - body: 'Hi {{1}}, How are you', + body: 'Hi', type: 'TEXT', - shortcode: 'welcome', - example: 'Hi [Glific], How are you', category: 'ACCOUNT_UPDATE', tagId: null, isActive: true, - isHsm: true, + allowTemplateCategoryChange: true, languageId: '1', + example: 'Hi', + shortcode: 'welcome', hasButtons: true, - buttons: - '[{"type":"QUICK_REPLY","text":"Quick reply 1"},{"type":"QUICK_REPLY","text":"Quick reply 2"}]', + buttons: '[{"type":"QUICK_REPLY","text":""},{"type":"QUICK_REPLY","text":""}]', buttonType: 'QUICK_REPLY', translations: '{}', - allowTemplateCategoryChange: false, }, }, }, @@ -371,20 +369,19 @@ const createHsmWithPhonetemplate = { variables: { input: { label: 'Hello', - body: 'Hi {{1}}, How are you', + body: 'Hi', type: 'TEXT', - shortcode: 'welcome', - example: 'Hi [[Glific], How are you', category: 'ACCOUNT_UPDATE', tagId: null, isActive: true, - isHsm: true, + allowTemplateCategoryChange: true, languageId: '1', + example: 'Hi', + shortcode: 'welcome', hasButtons: true, - buttons: '[{"type":"PHONE_NUMBER","text":"Call me","phone_number":"9876543210"}]', + buttons: '[{"type":"PHONE_NUMBER","text":"","phone_number":""}]', buttonType: 'CALL_TO_ACTION', translations: '{}', - allowTemplateCategoryChange: false, }, }, }, @@ -420,23 +417,6 @@ const createHsmWithPhonetemplate = { const createHSMtemplate = { request: { query: CREATE_TEMPLATE, - variables: { - input: { - label: 'Hello', - body: 'Hi {{1}}, How are you', - type: 'IMAGE', - shortcode: 'welcome', - example: 'Hi [Glific], How are you', - category: 'ACCOUNT_UPDATE', - tagId: null, - isActive: true, - isHsm: true, - languageId: '1', - translations: '{}', - messageMediaId: 5, - allowTemplateCategoryChange: true, - }, - }, }, result: { data: { @@ -465,6 +445,7 @@ const createHSMtemplate = { }, }, }, + variableMatcher: (variables: any) => true, }; const createMediaMessage = { diff --git a/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx b/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx index cb51d5b93..33ffd366a 100644 --- a/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx +++ b/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx @@ -22,7 +22,7 @@ import { GroupCollectionList } from 'containers/WaGroups/GroupCollections/GroupC const Chat = lazy(() => import('containers/Chat/Chat')); const Layout = lazy(() => import('components/UI/Layout/Layout')); const SpeedSendList = lazy(() => import('containers/Template/List/SpeedSendList/SpeedSendList')); -const SpeedSend = lazy(() => import('containers/Template/Form/SpeedSend/SpeedSend')); +const SpeedSend = lazy(() => import('containers/Template/SpeedSends/SpeedSends')); const FlowList = lazy(() => import('containers/Flow/FlowList/FlowList')); const Flow = lazy(() => import('containers/Flow/Flow')); const SheetIntegrationList = lazy( @@ -45,7 +45,7 @@ const StaffManagement = lazy(() => import('containers/StaffManagement/StaffManag const ContactProfile = lazy(() => import('containers/Profile/Contact/ContactProfile')); const MyAccount = lazy(() => import('containers/MyAccount/MyAccount')); const HSMList = lazy(() => import('containers/Template/List/HSMList/HSMList')); -const HSM = lazy(() => import('containers/Template/Form/HSM/HSM')); +const HSM = lazy(() => import('containers/Template/HSM/HSM')); const TicketList = lazy(() => import('containers/Ticket/TicketList/TicketList')); const SettingList = lazy(() => import('containers/SettingList/SettingList')); From 494216bec6eb5948db75b572e74cd6450dd04bf8 Mon Sep 17 00:00:00 2001 From: Akansha Sakhre Date: Fri, 20 Sep 2024 23:18:02 +0530 Subject: [PATCH 02/27] fixed errors --- .../TemplateButtons/TemplateButtons.tsx | 1 + src/containers/Template/HSM/HSM.tsx | 36 +++-- src/containers/Template/List/Template.tsx | 2 +- .../Template/SpeedSends/SpeedSend.test.tsx | 133 --------------- .../Template/SpeedSends/SpeedSends.tsx | 152 +++++++++++------- .../Template/Template.test.helper.ts | 3 + 6 files changed, 126 insertions(+), 201 deletions(-) diff --git a/src/containers/Chat/ChatMessages/TemplateButtons/TemplateButtons.tsx b/src/containers/Chat/ChatMessages/TemplateButtons/TemplateButtons.tsx index 0f96bf11f..49a165569 100644 --- a/src/containers/Chat/ChatMessages/TemplateButtons/TemplateButtons.tsx +++ b/src/containers/Chat/ChatMessages/TemplateButtons/TemplateButtons.tsx @@ -31,6 +31,7 @@ export const TemplateButtons = ({ onClick={() => handleButtonClick(type, value)} startIcon={icon} disabled={!isSimulator} + data-testid="templateButton" > {title} diff --git a/src/containers/Template/HSM/HSM.tsx b/src/containers/Template/HSM/HSM.tsx index 7f4f7a828..a91069a4a 100644 --- a/src/containers/Template/HSM/HSM.tsx +++ b/src/containers/Template/HSM/HSM.tsx @@ -188,8 +188,15 @@ export const HSM = () => { useLazyQuery(GET_TEMPLATE); let isEditing = false; + const isCopyState = location.state === 'copy'; let mode; - let isCopyState; + + if (isCopyState) { + queries.updateItemQuery = CREATE_TEMPLATE; + mode = 'copy'; + } else { + queries.updateItemQuery = UPDATE_TEMPLATE; + } // disable fields in edit mode for hsm template if (params.id && !isCopyState) { @@ -325,17 +332,23 @@ export const HSM = () => { } } - setLabel(labelValue); setIsActive(isActiveValue); let variables: any = []; - if (typeof bodyValue === 'string') { + + variables = getExampleValue(exampleValue); + setVariables(variables); + + if (isCopyState) { + setIsAddButtonChecked(hasButtons); + setTemplateType(templateButtonType); + setSimulatorMessage(''); + setEditorState(''); + setBody(''); + setLabel(''); + } else { setBody(bodyValue || ''); setEditorState(bodyValue || ''); - } - - if (exampleValue) { - variables = getExampleValue(exampleValue); - setVariables(variables); + setLabel(labelValue); if (hasButtons) { const { buttons: buttonsVal } = getTemplateAndButtons( @@ -431,6 +444,7 @@ export const HSM = () => { const setPayload = (payload: any) => { let payloadCopy = payload; let translationsCopy: any = {}; + payloadCopy.isHsm = true; // Create template payloadCopy.languageId = payload.language.id; @@ -443,7 +457,7 @@ export const HSM = () => { } else { payloadCopy.type = 'TEXT'; } - payloadCopy.category = category.label; + payloadCopy.category = category.label || category; if (payloadCopy.body) { payloadCopy.example = getExampleFromBody(payloadCopy.body, variables); } @@ -462,6 +476,10 @@ export const HSM = () => { } payloadCopy.translations = JSON.stringify(translationsCopy); + if (tagId) { + payloadCopy.tagId = payload.tagId.id; + } + delete payloadCopy.isAddButtonChecked; delete payloadCopy.templateButtons; delete payloadCopy.language; diff --git a/src/containers/Template/List/Template.tsx b/src/containers/Template/List/Template.tsx index e93445d2e..1af698233 100644 --- a/src/containers/Template/List/Template.tsx +++ b/src/containers/Template/List/Template.tsx @@ -287,7 +287,7 @@ export const Template = ({ const copyAction = { label: t('Copy'), - icon: , + icon: , parameter: 'id', dialog: setCopyDialog, insideMore: true, diff --git a/src/containers/Template/SpeedSends/SpeedSend.test.tsx b/src/containers/Template/SpeedSends/SpeedSend.test.tsx index 0e222f9ff..e69de29bb 100644 --- a/src/containers/Template/SpeedSends/SpeedSend.test.tsx +++ b/src/containers/Template/SpeedSends/SpeedSend.test.tsx @@ -1,133 +0,0 @@ -import { render, within, fireEvent, cleanup, waitFor, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import { MockedProvider } from '@apollo/client/testing'; -import { Routes, Route } from 'react-router-dom'; -import { SpeedSendList } from 'containers/Template/List/SpeedSendList/SpeedSendList'; -import { SPEED_SENDS_MOCKS } from 'containers/Template/Template.test.helper'; -import { setUserSession } from 'services/AuthService'; -import { SpeedSends } from './SpeedSends'; -import * as Notification from 'common/notification'; -import { LexicalComposer } from '@lexical/react/LexicalComposer'; -import { BeautifulMentionNode } from 'lexical-beautiful-mentions'; - -setUserSession(JSON.stringify({ roles: ['Admin'] })); - -const mocks = SPEED_SENDS_MOCKS; - -const mockIntersectionObserver = class { - constructor() {} - observe() {} - unobserve() {} - disconnect() {} -}; - -(window as any).IntersectionObserver = mockIntersectionObserver; - -afterEach(() => { - cleanup(); -}); - -vi.mock('lexical-beautiful-mentions', async (importOriginal) => { - const actual = (await importOriginal()) as typeof import('lexical-beautiful-mentions'); - return { - ...actual, - BeautifulMentionsPlugin: ({ children }: any) =>
{children}
, - BeautifulMentionsMenuProps: {}, - BeautifulMentionsMenuItemProps: {}, - }; -}); - -describe('SpeedSend', () => { - test('cancel button should redirect to SpeedSendlist page', async () => { - const { container, getByText } = render( - - - - } /> - } /> - - - - ); - - await waitFor(() => { - const { queryByText } = within(container.querySelector('form') as HTMLElement); - const button = queryByText('Cancel') as HTMLButtonElement; - fireEvent.click(button); - }); - - await waitFor(() => { - expect(getByText('Speed sends')).toBeInTheDocument(); - }); - }); - - test('should have correct validations ', async () => { - const { container } = render( - - - console.log(error), - nodes: [BeautifulMentionNode], - }} - > - - } /> - } /> - - - - - ); - - await waitFor(() => { - fireEvent.change(container.querySelector('input[name="label"]') as HTMLInputElement, { - target: { value: 'new Template' }, - }); - }); - - const { queryByText } = within(container.querySelector('form') as HTMLElement); - await waitFor(() => { - const button = queryByText('Save') as HTMLButtonElement; - fireEvent.click(button); - }); - - await waitFor(() => { - expect(queryByText('Title is required.')).toBeInTheDocument(); - }); - }); - - test.skip('should test translations', async () => { - const notificationSpy = vi.spyOn(Notification, 'setNotification'); - render( - - - - } /> - - - - ); - - await waitFor(() => { - expect(screen.getByText('Title')).toBeInTheDocument(); - expect(screen.getByText('Marathi')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('Marathi')); - - await waitFor(() => { - expect(screen.getByText('Title')).toBeInTheDocument(); - expect(screen.getByText('English')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('English')); - - fireEvent.click(screen.getByTestId('submitActionButton')); - - await waitFor(() => { - expect(notificationSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/containers/Template/SpeedSends/SpeedSends.tsx b/src/containers/Template/SpeedSends/SpeedSends.tsx index 5a54cd07e..29de386e1 100644 --- a/src/containers/Template/SpeedSends/SpeedSends.tsx +++ b/src/containers/Template/SpeedSends/SpeedSends.tsx @@ -15,12 +15,12 @@ import { Checkbox } from 'components/UI/Form/Checkbox/Checkbox'; import { Typography } from '@mui/material'; import { Input } from 'components/UI/Form/Input/Input'; import { EmojiInput } from 'components/UI/Form/EmojiInput/EmojiInput'; -import { CreateAutoComplete } from 'components/UI/Form/CreateAutoComplete/CreateAutoComplete'; import { GET_TAGS } from 'graphql/queries/Tags'; import { USER_LANGUAGES } from 'graphql/queries/Organization'; import { Loading } from 'components/UI/Layout/Loading/Loading'; import { validateMedia } from 'common/utils'; import { LanguageBar } from 'components/UI/LanguageBar/LanguageBar'; +import { setNotification } from 'common/notification'; const queries = { getItemQuery: GET_TEMPLATE, @@ -38,13 +38,29 @@ const mediaTypes = MEDIA_MESSAGE_TYPES.map((option: string) => ({ })); const speedSendIcon = ; +export const getTranslation = (attribute: any, translations: any, defaultLanguage: any) => { + const defaultTemplate = JSON.parse(translations)[defaultLanguage.id]; + + if (!defaultTemplate) { + return null; + } + + if (attribute === 'title') { + return defaultTemplate?.label; + } else if (attribute === 'body') { + return defaultTemplate?.body; + } + + return null; +}; + export const SpeedSends = () => { const [tagId, setTagId] = useState(null); const [label, setLabel] = useState(''); const [body, setBody] = useState(''); const [language, setLanguageId] = useState(null); const [type, setType] = useState(null); - const [translations, setTranslations] = useState(); + const [translations, setTranslations] = useState('{}'); const [attachmentURL, setAttachmentURL] = useState(''); const [languageOptions, setLanguageOptions] = useState([]); const [isActive, setIsActive] = useState(true); @@ -53,12 +69,15 @@ export const SpeedSends = () => { const [isUrlValid, setIsUrlValid] = useState(); const [nextLanguage, setNextLanguage] = useState(''); const [editorState, setEditorState] = useState(''); + const [defaultLanguage, setDefaultLanguage] = useState({}); const { t } = useTranslation(); const navigate = useNavigate(); const location: any = useLocation(); const params = useParams(); + const hasTranslations = params?.id && defaultLanguage?.id !== language?.id; + const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE); const { data: tag, loading: tagLoading } = useQuery(GET_TAGS, { variables: {}, @@ -104,6 +123,10 @@ export const SpeedSends = () => { } }, [type, attachmentURL]); + useEffect(() => { + displayWarning(); + }, [type]); + const states = { language, label, @@ -130,7 +153,6 @@ export const SpeedSends = () => { (lang: any) => lang.label === location.state.language ); navigate(location.pathname); - console.log(selectedLangauge); setLanguageId(selectedLangauge); } else if (!language?.id) { @@ -145,7 +167,6 @@ export const SpeedSends = () => { setLabel(labelValue); setIsActive(isActiveValue); - let variables: any = []; if (typeof bodyValue === 'string') { setBody(bodyValue || ''); setEditorState(bodyValue || ''); @@ -169,6 +190,7 @@ export const SpeedSends = () => { setBody(content.body || ''); setEditorState(content.body || ''); } + setTranslations(translationsValue); } if (MessageMediaValue) { @@ -180,58 +202,14 @@ export const SpeedSends = () => { if (tagIdValue) { setTagId(tagIdValue); } + + setDefaultLanguage(languageIdValue); }; const setPayload = (payload: any) => { let payloadCopy = payload; let translationsCopy: any = {}; - if (template) { - payloadCopy.languageId = language?.id; - if (payloadCopy.type) { - payloadCopy.type = payloadCopy.type.id; - // STICKER is a type of IMAGE - if (payloadCopy.type.id === 'STICKER') { - payloadCopy.type = 'IMAGE'; - } - } else { - payloadCopy.type = 'TEXT'; - } - - delete payloadCopy.language; - - if (payloadCopy.type === 'TEXT') { - delete payloadCopy.attachmentURL; - } - - // Removing unnecessary fields - delete payloadCopy.isAddButtonChecked; - delete payloadCopy.templateButtons; - - let messageMedia = null; - if (payloadCopy.type && payloadCopy.attachmentURL) { - messageMedia = { - type: payloadCopy.type.id, - sourceUrl: payloadCopy.attachmentURL, - }; - } - - // Update template translation - if (translations) { - translationsCopy = JSON.parse(translations); - translationsCopy[language?.id] = { - status: 'approved', - languageId: language, - label: payloadCopy.label, - body: payloadCopy.body, - MessageMedia: messageMedia, - }; - } - payloadCopy = { - translations: JSON.stringify(translationsCopy), - }; - } - // Create template payloadCopy.languageId = payload.language.id; if (payloadCopy.type) { @@ -249,6 +227,14 @@ export const SpeedSends = () => { if (payloadCopy.type === 'TEXT') { delete payloadCopy.attachmentURL; } + + // Update template translation + if (translations) { + translationsCopy = JSON.parse(translations); + translationsCopy[language?.id] = payloadCopy; + setTranslations(JSON.stringify(translationsCopy)); + } + payloadCopy.translations = JSON.stringify(translationsCopy); delete payloadCopy.isAddButtonChecked; @@ -257,7 +243,9 @@ export const SpeedSends = () => { delete payloadCopy.languageVariant; delete payloadCopy.variables; delete payloadCopy.exisitingShortCode; + delete payloadCopy.newShortcode; + console.log(payloadCopy); return payloadCopy; }; @@ -379,11 +367,46 @@ export const SpeedSends = () => { const onLanguageChange = (option: string, form: any) => { setNextLanguage(option); - const { values } = form; - if (values.label || values.body) { - return; + const { values, errors } = form; + if (values.type?.label === 'TEXT') { + if (values.title || values.body) { + if (errors) { + setNotification(t('Please check the errors'), 'warning'); + } + } else { + handleLanguageChange(option); + } + } + if (values.body) { + if (Object.keys(errors).length !== 0) { + setNotification(t('Please check the errors'), 'warning'); + } + } else { + handleLanguageChange(option); + } + }; + + const displayWarning = () => { + if (type && type.id === 'STICKER') { + setWarning( +
+
    +
  1. {t('Animated stickers are not supported.')}
  2. +
  3. {t('Captions along with stickers are not supported.')}
  4. +
+
+ ); + } else if (type && type.id === 'AUDIO') { + setWarning( +
+
    +
  1. {t('Captions along with audio are not supported.')}
  2. +
+
+ ); + } else { + setWarning(null); } - handleLanguageChange(option); }; const FormSchema = Yup.object().shape( @@ -417,7 +440,6 @@ export const SpeedSends = () => { optionLabel: 'label', multiple: false, label: t('Attachment Type'), - disabled: isEditing, helperText: warning, onChange: (event: any) => { const val = event; @@ -433,7 +455,6 @@ export const SpeedSends = () => { type: 'text', label: t('Attachment URL'), validate: () => isUrlValid, - disabled: isEditing, helperText: t( 'Please provide a sample attachment for approval purpose. You may send a similar but different attachment when sending the HSM to users.' ), @@ -452,6 +473,7 @@ export const SpeedSends = () => { const formFields = [ { + field: 'languageBar', component: LanguageBar, options: langOptions || [], selectedLangauge: language && language.label, @@ -461,10 +483,10 @@ export const SpeedSends = () => { component: Input, name: 'label', label: t('Title'), - disabled: isEditing, inputProp: { onBlur: (event: any) => setLabel(event.target.value), }, + translation: hasTranslations && getTranslation('title', translations, defaultLanguage), }, { component: Checkbox, @@ -483,15 +505,28 @@ export const SpeedSends = () => { rows: 5, convertToWhatsApp: true, textArea: true, - disabled: isEditing, handleChange: (value: any) => { setBody(value); }, defaultValue: isEditing && editorState, + translation: hasTranslations && getTranslation('body', translations, defaultLanguage), }, ...attachmentField, ]; + const afterSave = (data: any, saveClick: boolean) => { + if (!saveClick) { + if (params.id) { + handleLanguageChange(nextLanguage); + } else { + const { sessionTemplate } = data.createSessionTemplate; + navigate(`/speed-send/${sessionTemplate.id}/edit`, { + state: { language: nextLanguage }, + }); + } + } + }; + if (languageLoading || templateLoading || tagLoading) { return ; } @@ -521,6 +556,7 @@ export const SpeedSends = () => { copyNotification={t('Copy of the template has been created!')} backLinkButton={`/${redirectionLink}`} customStyles={styles.AttachmentFields} + afterSave={afterSave} /> ); }; diff --git a/src/containers/Template/Template.test.helper.ts b/src/containers/Template/Template.test.helper.ts index 7543f517e..261060f36 100644 --- a/src/containers/Template/Template.test.helper.ts +++ b/src/containers/Template/Template.test.helper.ts @@ -330,6 +330,7 @@ const createHsmWithButtontemplate = { buttons: '[{"type":"QUICK_REPLY","text":""},{"type":"QUICK_REPLY","text":""}]', buttonType: 'QUICK_REPLY', translations: '{}', + isHsm: true, }, }, }, @@ -382,6 +383,7 @@ const createHsmWithPhonetemplate = { buttons: '[{"type":"PHONE_NUMBER","text":"","phone_number":""}]', buttonType: 'CALL_TO_ACTION', translations: '{}', + isHsm: true, }, }, }, @@ -440,6 +442,7 @@ const createHSMtemplate = { buttons: null, buttonType: null, allowTemplateCategoryChange: true, + isHsm: true, }, errors: null, }, From a259362513de85cf3064be1f55483b55c04d2ad7 Mon Sep 17 00:00:00 2001 From: Akansha Sakhre Date: Fri, 20 Sep 2024 23:48:30 +0530 Subject: [PATCH 03/27] added speed sends --- .github/workflows/cypress-testing.yml | 2 +- .../Template/SpeedSends/SpeedSend.test.tsx | 143 ++++++++++++++++++ .../Template/Template.test.helper.ts | 107 +++++++------ 3 files changed, 207 insertions(+), 45 deletions(-) diff --git a/.github/workflows/cypress-testing.yml b/.github/workflows/cypress-testing.yml index b85dce6a0..33f8c0eff 100644 --- a/.github/workflows/cypress-testing.yml +++ b/.github/workflows/cypress-testing.yml @@ -96,7 +96,7 @@ jobs: git clone https://github.com/glific/cypress-testing.git echo done. go to dir. cd cypress-testing - git checkout main + git checkout templates cd .. cp -r cypress-testing/cypress cypress yarn add cypress@13.6.2 diff --git a/src/containers/Template/SpeedSends/SpeedSend.test.tsx b/src/containers/Template/SpeedSends/SpeedSend.test.tsx index e69de29bb..6e4f1661f 100644 --- a/src/containers/Template/SpeedSends/SpeedSend.test.tsx +++ b/src/containers/Template/SpeedSends/SpeedSend.test.tsx @@ -0,0 +1,143 @@ +import { render, fireEvent, cleanup, waitFor, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import { Routes, Route } from 'react-router-dom'; +import { SpeedSendList } from 'containers/Template/List/SpeedSendList/SpeedSendList'; +import { SPEED_SENDS_MOCKS } from 'containers/Template/Template.test.helper'; +import { setUserSession } from 'services/AuthService'; +import { SpeedSends } from './SpeedSends'; +import * as Notification from 'common/notification'; +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { BeautifulMentionNode } from 'lexical-beautiful-mentions'; +import userEvent from '@testing-library/user-event'; + +setUserSession(JSON.stringify({ roles: ['Admin'] })); + +const mocks = SPEED_SENDS_MOCKS; + +const mockIntersectionObserver = class { + constructor() {} + observe() {} + unobserve() {} + disconnect() {} +}; + +(window as any).IntersectionObserver = mockIntersectionObserver; + +afterEach(() => { + cleanup(); +}); + +vi.mock('lexical-beautiful-mentions', async (importOriginal) => { + const actual = (await importOriginal()) as typeof import('lexical-beautiful-mentions'); + return { + ...actual, + BeautifulMentionsPlugin: ({ children }: any) =>
{children}
, + BeautifulMentionsMenuProps: {}, + BeautifulMentionsMenuItemProps: {}, + }; +}); + +const user = userEvent.setup(); + +describe('SpeedSend', () => { + test('cancel button should redirect to SpeedSendlist page', async () => { + const { getByText } = render( + + + + } /> + } /> + + + + ); + + await waitFor(() => { + fireEvent.click(getByText('Cancel')); + }); + + await waitFor(() => { + expect(getByText('Speed sends')).toBeInTheDocument(); + }); + }); + + test('should have correct validations ', async () => { + render( + + + console.log(error), + nodes: [BeautifulMentionNode], + }} + > + + } /> + } /> + + + + + ); + + await waitFor(() => { + expect(screen.getByText('Add a new Speed send')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Save')); + + await waitFor(() => { + expect(screen.getByText('Title is required.')).toBeInTheDocument(); + }); + }); + + test('should test translations', async () => { + const notificationSpy = vi.spyOn(Notification, 'setNotification'); + render( + + + + } /> + } /> + } /> + + + + ); + + await waitFor(() => { + expect(screen.getByText('Add a new Speed send')).toBeInTheDocument(); + }); + + const inputs = screen.getAllByRole('textbox'); + const lexicalEditor = inputs[1]; + + await user.click(lexicalEditor); + await user.tab(); + fireEvent.input(lexicalEditor, { data: 'Hi, How are you' }); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[0].focus(); + fireEvent.keyDown(autocompletes[0], { key: 'ArrowDown' }); + + fireEvent.click(screen.getByText('IMAGE'), { key: 'Enter' }); + + fireEvent.change(inputs[2], { + target: { value: 'https://www.buildquickbots.com/whatsapp/media/sample/jpg/sample02.jpg' }, + }); + + await waitFor(() => { + expect(screen.getByText('Hi, How are you')).toBeInTheDocument(); + }); + + fireEvent.change(inputs[0], { target: { value: 'Template' } }); + + fireEvent.click(screen.getByText('Marathi')); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/containers/Template/Template.test.helper.ts b/src/containers/Template/Template.test.helper.ts index f8b922a01..bc3150e57 100644 --- a/src/containers/Template/Template.test.helper.ts +++ b/src/containers/Template/Template.test.helper.ts @@ -451,12 +451,12 @@ const createHSMtemplate = { variableMatcher: (variables: any) => true, }; -const createMediaMessage = { +const createMediaMessage = (caption: string) => ({ request: { query: CREATE_MEDIA_MESSAGE, variables: { input: { - caption: 'Hi {{1}}, How are you', + caption, sourceUrl: 'https://www.buildquickbots.com/whatsapp/media/sample/jpg/sample02.jpg', url: 'https://www.buildquickbots.com/whatsapp/media/sample/jpg/sample02.jpg', }, @@ -473,7 +473,7 @@ const createMediaMessage = { }, }, }, -}; +}); const getShortCodeQuery = { request: { @@ -649,32 +649,9 @@ export const TEMPLATE_MOCKS = [ getFilterTagQuery, createHsmWithButtontemplate, createHsmWithPhonetemplate, - createMediaMessage, + createMediaMessage('Hi {{1}}, How are you'), getShortCodeQuery, - { - request: { - query: CREATE_TEMPLATE, - variables: { - input: { - body: 'new Template body', - label: 'new Template', - languageId: 1, - type: 'TEXT', - }, - }, - }, - result: { - data: { - createSessionTemplate: { - sessionTemplate: { - body: 'new Template body', - id: '121', - label: 'new Template', - }, - }, - }, - }, - }, + { request: { query: CREATE_MEDIA_MESSAGE, @@ -879,25 +856,67 @@ const getTemplatesCount = { }, }; +const createSessionTemplate = { + request: { + query: CREATE_TEMPLATE, + variables: { + input: { + label: 'Template', + body: 'Hi, How are you', + type: 'IMAGE', + tagId: null, + isActive: true, + languageId: '1', + translations: + '{"1":{"language":{"id":"1","label":"English","localized":true,"locale":"en"},"label":"Template","body":"Hi, How are you","type":"IMAGE","attachmentURL":"https://www.buildquickbots.com/whatsapp/media/sample/jpg/sample02.jpg","tagId":null,"isActive":true,"languageId":"1"}}', + messageMediaId: 5, + }, + }, + }, + result: { + data: { + createSessionTemplate: { + __typename: 'SessionTemplateResult', + errors: null, + sessionTemplate: { + MessageMedia: null, + __typename: 'SessionTemplate', + allowTemplateCategoryChange: true, + body: 'Hi, How are you', + buttonType: null, + buttons: '[]', + category: null, + example: null, + hasButtons: false, + id: '1', + isActive: true, + label: 'Template', + language: { + __typename: 'Language', + id: '1', + label: 'English', + }, + shortcode: null, + translations: + '{"1":{"type":"TEXT","tagId":null,"languageId":"1","language":{"localized":true,"locale":"en","label":"English","id":"1","__typename":"Language"},"label":"Template","isActive":true,"body":"Hi, How are you"}}', + type: 'TEXT', + }, + }, + }, + }, +}; + export const SPEED_SENDS_MOCKS = [ + getFilterTagQuery, + getFilterTagQuery, + getOrganizationLanguagesQueryByOrder, getSpendSendTemplate, - getSpendSendTemplate, - getSpendSendTemplate, - speedSendOrderWith, - speedSendOrderWith, - speedSend, speedSend, - filterTemplateQuery, getTemplatesCount, getTemplatesCount, - updateSessiontemplate, - getOrganizationLanguagesQuery, - getOrganizationLanguagesQueryByOrder, - getOrganizationLanguagesQueryByOrder, - getOrganizationLanguagesQueryByOrder, - getFilterTagQuery, - getFilterTagQuery, - getFilterTagQuery, - updateSessiontemplate, - updateSessiontemplate, + speedSend, + speedSend, + createSessionTemplate, + createSessionTemplate, + createMediaMessage('Hi, How are you'), ]; From f7f0614db5a7d876fb6121318bae399a2c672d03 Mon Sep 17 00:00:00 2001 From: Akansha Sakhre Date: Sat, 21 Sep 2024 09:51:39 +0530 Subject: [PATCH 04/27] updated test cases --- .../Template/Form/HSM/HSM.module.css | 11 - src/containers/Template/Form/HSM/HSM.test.tsx | 319 ------------------ src/containers/Template/Form/HSM/HSM.tsx | 216 ------------ .../Form/SpeedSend/SpeedSend.module.css | 4 - .../Form/SpeedSend/SpeedSend.test.tsx | 148 -------- .../Template/Form/SpeedSend/SpeedSend.tsx | 17 - 6 files changed, 715 deletions(-) delete mode 100644 src/containers/Template/Form/HSM/HSM.module.css delete mode 100644 src/containers/Template/Form/HSM/HSM.test.tsx delete mode 100644 src/containers/Template/Form/HSM/HSM.tsx delete mode 100644 src/containers/Template/Form/SpeedSend/SpeedSend.module.css delete mode 100644 src/containers/Template/Form/SpeedSend/SpeedSend.test.tsx delete mode 100644 src/containers/Template/Form/SpeedSend/SpeedSend.tsx diff --git a/src/containers/Template/Form/HSM/HSM.module.css b/src/containers/Template/Form/HSM/HSM.module.css deleted file mode 100644 index caf736fa7..000000000 --- a/src/containers/Template/Form/HSM/HSM.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.TemplateIcon { - width: 29px; - height: 29px; -} - -.Checkbox { - color: #555555; - font-weight: 400; - line-height: 18px; - font-size: 16px; -} diff --git a/src/containers/Template/Form/HSM/HSM.test.tsx b/src/containers/Template/Form/HSM/HSM.test.tsx deleted file mode 100644 index ad4ab7669..000000000 --- a/src/containers/Template/Form/HSM/HSM.test.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { render, waitFor, within, fireEvent, screen } from '@testing-library/react'; -import { MockedProvider } from '@apollo/client/testing'; -import userEvent from '@testing-library/user-event'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import { HSM } from './HSM'; -import { - TEMPLATE_MOCKS, - getHSMTemplateTypeMedia, - getHSMTemplateTypeText, -} from 'containers/Template/Template.test.helper'; - -const mocks = TEMPLATE_MOCKS; - -beforeEach(() => { - vi.restoreAllMocks(); -}); - -vi.mock('lexical-beautiful-mentions', async (importOriginal) => { - const actual = (await importOriginal()) as typeof import('lexical-beautiful-mentions'); - return { - ...actual, - BeautifulMentionsPlugin: ({ children }: any) =>
{children}
, - BeautifulMentionsMenuProps: {}, - BeautifulMentionsMenuItemProps: {}, - }; -}); - -describe('Edit mode', () => { - test('HSM form is loaded correctly in edit mode', async () => { - const MOCKS = [...mocks, getHSMTemplateTypeText, getHSMTemplateTypeText]; - const { getByText, getAllByRole } = render( - - - - } /> - - - - ); - await waitFor(() => { - expect(getByText('Edit HSM Template')).toBeInTheDocument(); - }); - - await waitFor(() => { - expect(getAllByRole('textbox')[0]).toHaveValue('account_balance'); - }); - }); - - test('HSM templates with media', async () => { - const MOCKS = [...mocks, getHSMTemplateTypeMedia, getHSMTemplateTypeMedia]; - const { getByText, getAllByRole } = render( - - - - } /> - - - - ); - - await waitFor(() => { - expect(getByText('Edit HSM Template')).toBeInTheDocument(); - }); - - await waitFor(() => { - expect(getAllByRole('textbox')[0]).toHaveValue('account_update'); - }); - - await waitFor(() => { - expect(screen.getAllByRole('combobox')[1]).toHaveValue('IMAGE'); - }); - }); -}); - -describe('Add mode', () => { - const template = ( - - - - - - ); - const user = userEvent.setup(); - - test('check for validations for the HSM form', async () => { - const { getByText, container } = render(template); - await waitFor(() => { - expect(getByText('Add a new HSM Template')).toBeInTheDocument(); - }); - - const { queryByText } = within(container.querySelector('form') as HTMLElement); - fireEvent.click(screen.getByTestId('submitActionButton')); - - // we should have 1 errors - await waitFor(() => { - expect(queryByText('Title is required.')).toBeInTheDocument(); - }); - - fireEvent.change(container.querySelector('input[name="label"]') as HTMLInputElement, { - target: { - value: - 'We are not allowing a really long title, and we should trigger validation for this.', - }, - }); - - fireEvent.click(screen.getByTestId('submitActionButton')); - - // we should still have 1 errors - await waitFor(() => { - expect(queryByText('Title length is too long.')).toBeInTheDocument(); - }); - }); - - test('it should create a template message', async () => { - render(template); - - await waitFor(() => { - expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); - }); - - const inputs = screen.getAllByRole('textbox'); - - fireEvent.change(inputs[0], { target: { value: 'element_name' } }); - fireEvent.change(inputs[1], { target: { value: 'element_name' } }); - const lexicalEditor = inputs[2]; - - await user.click(lexicalEditor); - await user.tab(); - fireEvent.input(lexicalEditor, { data: 'Hi, How are you' }); - - const autocompletes = screen.getAllByTestId('autocomplete-element'); - autocompletes[1].focus(); - fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); - - fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); - - fireEvent.click(screen.getByText('Allow meta to re-categorize template?')); - - await waitFor(() => { - expect(screen.getByText('Hi, How are you')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('Add Variable')); - - await waitFor(() => { - expect(screen.getByText('Hi, How are you {{1}}')).toBeInTheDocument(); - }); - fireEvent.change(inputs[1], { target: { value: 'element_name' } }); - - fireEvent.change(screen.getByPlaceholderText('Define value'), { target: { value: 'User' } }); - - fireEvent.click(screen.getByTestId('submitActionButton')); - }); - - test('it should add and remove variables', async () => { - render(template); - - await waitFor(() => { - expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); - }); - - const inputs = screen.getAllByRole('textbox'); - const lexicalEditor = inputs[2]; - - await user.click(lexicalEditor); - await user.tab(); - fireEvent.input(lexicalEditor, { data: 'Hi' }); - - await waitFor(() => { - expect(screen.getByText('Hi')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('Add Variable')); - - await waitFor(() => { - expect(screen.getByText('Hi {{1}}')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getAllByTestId('delete-variable')[0]); - }); - - test('it adds quick reply buttons', async () => { - render(template); - - await waitFor(() => { - const language = screen.getAllByTestId('AutocompleteInput')[0].querySelector('input'); - expect(language).toHaveValue('English'); - }); - - const inputs = screen.getAllByRole('textbox'); - - const elementName = inputs[0]; - const title = inputs[1]; - - await user.type(title, 'Hello'); - await user.type(elementName, 'welcome'); - - const lexicalEditor = inputs[2]; - - await user.click(lexicalEditor); - await user.tab(); - fireEvent.input(lexicalEditor, { data: 'Hi' }); - - await waitFor(() => { - expect(screen.getByText('Hi')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('Add buttons')); - fireEvent.click(screen.getByText('Quick replies')); - - await user.click(screen.getByTestId('addButton')); - - fireEvent.change(screen.getByPlaceholderText('Quick reply 1 title'), { - target: { value: 'Yes' }, - }); - - fireEvent.change(screen.getByPlaceholderText('Quick reply 2 title'), { - target: { value: 'No' }, - }); - - const autocompletes = screen.getAllByTestId('autocomplete-element'); - autocompletes[1].focus(); - fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); - - fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); - - fireEvent.click(screen.getByTestId('submitActionButton')); - fireEvent.click(screen.getByTestId('submitActionButton')); - }); - - test('it adds call to action buttons', async () => { - render(template); - - await waitFor(() => { - const language = screen.getAllByTestId('AutocompleteInput')[0].querySelector('input'); - expect(language).toHaveValue('English'); - }); - - const inputs = screen.getAllByRole('textbox'); - - const elementName = inputs[0]; - const title = inputs[1]; - - await user.type(title, 'Hello'); - await user.type(elementName, 'welcome'); - - const lexicalEditor = inputs[2]; - - await user.click(lexicalEditor); - await user.tab(); - fireEvent.input(lexicalEditor, { data: 'Hi' }); - - await waitFor(() => { - expect(screen.getByText('Hi')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('Add buttons')); - fireEvent.click(screen.getByText('Call to actions')); - fireEvent.click(screen.getByText('Phone number')); - - fireEvent.change(screen.getByPlaceholderText('Button Title'), { target: { value: 'Call me' } }); - fireEvent.change(screen.getByPlaceholderText('Button Value'), { - target: { value: '9876543210' }, - }); - - fireEvent.click(screen.getByText('Add Call to action')); - fireEvent.click(screen.getAllByTestId('delete-icon')[1]); - - const autocompletes = screen.getAllByTestId('autocomplete-element'); - autocompletes[1].focus(); - fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); - - fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); - - fireEvent.click(screen.getByTestId('submitActionButton')); - fireEvent.click(screen.getByTestId('submitActionButton')); - }); - - test('adding attachments', async () => { - render(template); - - await waitFor(() => { - expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); - }); - - const autocompletes = screen.getAllByTestId('autocomplete-element'); - const inputs = screen.getAllByRole('textbox'); - - autocompletes[2].focus(); - fireEvent.keyDown(autocompletes[2], { key: 'ArrowDown' }); - fireEvent.click(screen.getByText('IMAGE'), { key: 'Enter' }); - - fireEvent.change(inputs[3], { target: { value: 'https://example.com/image.jpg' } }); - - await waitFor(() => { - expect(inputs[3]).toHaveValue('https://example.com/image.jpg'); - }); - }); - - test('it creates a translation of hsm template', async () => { - render(template); - - await waitFor(() => { - expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('Translate existing HSM?')); - - const autocompletes = screen.getAllByTestId('autocomplete-element'); - autocompletes[1].focus(); - fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); - - fireEvent.click(screen.getByText('account_balance'), { key: 'Enter' }); - - await waitFor(() => { - expect(screen.getAllByRole('combobox')[1]).toHaveValue('account_balance'); - }); - }); -}); diff --git a/src/containers/Template/Form/HSM/HSM.tsx b/src/containers/Template/Form/HSM/HSM.tsx deleted file mode 100644 index 5a00834a7..000000000 --- a/src/containers/Template/Form/HSM/HSM.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { useState } from 'react'; -import { useQuery } from '@apollo/client'; -import { useTranslation } from 'react-i18next'; -import { useParams, useLocation } from 'react-router-dom'; - -import TemplateIcon from 'assets/images/icons/Template/UnselectedDark.svg?react'; -import { GET_HSM_CATEGORIES, GET_SHORTCODES } from 'graphql/queries/Template'; -import { AutoComplete } from 'components/UI/Form/AutoComplete/AutoComplete'; -import { Input } from 'components/UI/Form/Input/Input'; -import { Loading } from 'components/UI/Layout/Loading/Loading'; -import { Simulator } from 'components/simulator/Simulator'; -import Template from '../Template'; -import styles from './HSM.module.css'; -import { Checkbox } from 'components/UI/Form/Checkbox/Checkbox'; -import { Typography } from '@mui/material'; - -const defaultAttribute = { - isHsm: true, -}; - -const templateIcon = ; - -export const HSM = () => { - const [sampleMessages, setSampleMessages] = useState({ - type: 'TEXT', - location: null, - media: {}, - body: '', - }); - - const [exisitingShortCode, setExistingShortcode] = useState(''); - const [newShortcode, setNewShortcode] = useState(''); - const [category, setCategory] = useState(undefined); - const [languageVariant, setLanguageVariant] = useState(false); - const [allowTemplateCategoryChange, setAllowTemplateCategoryChange] = useState(true); - - const { t } = useTranslation(); - const params = useParams(); - const location: any = useLocation(); - - const { data: categoryList, loading } = useQuery(GET_HSM_CATEGORIES); - const { data: shortCodes } = useQuery(GET_SHORTCODES, { - variables: { - filter: { - isHsm: true, - }, - }, - }); - - if (loading) { - return ; - } - - const categoryOpn: any = []; - if (categoryList) { - categoryList.whatsappHsmCategories.forEach((categories: any, index: number) => { - categoryOpn.push({ label: categories, id: index }); - }); - } - - const shortCodeOptions: any = []; - if (shortCodes) { - shortCodes.sessionTemplates.forEach((value: any, index: number) => { - shortCodeOptions.push({ label: value?.shortcode, id: index }); - }); - } - - const removeFirstLineBreak = (text: any) => - text?.length === 1 ? text.slice(0, 1).replace(/(\r\n|\n|\r)/, '') : text; - - const getTemplate = (text: string) => { - const { body } = sampleMessages; - /** - * Regular expression to check if message contains given pattern - * If pattern is present search will return first index of given pattern - * otherwise it will return -1 - */ - const exp = /(\|\s\[)|(\|\[)/; - - const areButtonsPresent = body.search(exp); - if (areButtonsPresent > -1) { - const buttons = body.substr(areButtonsPresent); - return text + buttons; - } - return text; - }; - - const getSimulatorMessage = (messages: any) => { - const message = removeFirstLineBreak(messages); - const media: any = { ...sampleMessages.media }; - const text = getTemplate(message); - media.caption = text; - setSampleMessages((val) => ({ ...val, body: text, media })); - }; - - const getAttachmentUrl = (type: any, media: any) => { - const mediaBody = { ...media }; - const mediaObj: any = sampleMessages.media; - mediaBody.caption = mediaObj.caption; - setSampleMessages((val) => ({ ...val, type, media: mediaBody })); - }; - - const addButtonsToSampleMessage = (buttonTemplate: string) => { - const message: any = { ...sampleMessages }; - message.body = buttonTemplate; - setSampleMessages(message); - }; - - const isCopyState = location.state === 'copy'; - let isEditing = false; - if (params.id && !isCopyState) { - isEditing = true; - } - - const formFields = [ - { - component: Checkbox, - name: 'languageVariant', - title: ( - - Translate existing HSM? - - ), - handleChange: (value: any) => setLanguageVariant(value), - skip: isEditing, - }, - { - component: Input, - name: 'newShortCode', - placeholder: `${t('Element name')}*`, - label: `${t('Element name')}*`, - disabled: isEditing, - skip: languageVariant ? true : false, - onChange: (value: any) => { - setNewShortcode(value); - }, - }, - { - component: AutoComplete, - name: 'existingShortCode', - options: shortCodeOptions, - optionLabel: 'label', - multiple: false, - label: `${t('Element name')}*`, - placeholder: `${t('Element name')}*`, - disabled: isEditing, - onChange: (event: any) => { - setExistingShortcode(event); - }, - skip: languageVariant ? false : true, - }, - { - component: AutoComplete, - name: 'category', - options: categoryOpn, - optionLabel: 'label', - multiple: false, - label: `${t('Category')}*`, - placeholder: `${t('Category')}*`, - disabled: isEditing, - helperText: t('Select the most relevant category'), - onChange: (event: any) => { - setCategory(event); - }, - skip: isEditing, - }, - { - component: Input, - name: 'category', - type: 'text', - label: `${t('Category')}*`, - placeholder: `${t('Category')}*`, - disabled: isEditing, - helperText: t('Select the most relevant category'), - skip: !isEditing, - }, - { - component: Checkbox, - name: 'allowTemplateCategoryChange', - title: ( - - Allow meta to re-categorize template? - - ), - darkCheckbox: true, - disabled: isEditing, - handleChange: (value: boolean) => setAllowTemplateCategoryChange(value), - }, - ]; - - return ( -
-