From ce6857a9fce884550ddec24c539b8607da2d1f50 Mon Sep 17 00:00:00 2001 From: Akansha Sakhre Date: Wed, 4 Sep 2024 23:44:39 +0530 Subject: [PATCH] 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'));