diff --git a/src/assets/images/icons/Duplicate.svg b/src/assets/images/icons/Duplicate.svg new file mode 100644 index 000000000..9337e4c2e --- /dev/null +++ b/src/assets/images/icons/Duplicate.svg @@ -0,0 +1,25 @@ + + + + Make a copy + Create a copy using this action. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icons/Flow/Duplicate.svg b/src/assets/images/icons/Flow/Duplicate.svg deleted file mode 100644 index 6d55607bf..000000000 --- a/src/assets/images/icons/Flow/Duplicate.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - Copy Flow - Created with Sketch. - - - - - - - - - - - \ No newline at end of file diff --git a/src/containers/Flow/FlowList/FlowList.tsx b/src/containers/Flow/FlowList/FlowList.tsx index cf01c3983..8531a227a 100644 --- a/src/containers/Flow/FlowList/FlowList.tsx +++ b/src/containers/Flow/FlowList/FlowList.tsx @@ -6,7 +6,7 @@ import { useLazyQuery, useMutation, useQuery } from '@apollo/client'; import { FormControl, MenuItem, Select } from '@mui/material'; import FlowIcon from 'assets/images/icons/Flow/Dark.svg?react'; -import DuplicateIcon from 'assets/images/icons/Flow/Duplicate.svg?react'; +import DuplicateIcon from 'assets/images/icons/Duplicate.svg?react'; import ExportIcon from 'assets/images/icons/Flow/Export.svg?react'; import ConfigureIcon from 'assets/images/icons/Configure/UnselectedDark.svg?react'; import PinIcon from 'assets/images/icons/Pin/Active.svg?react'; diff --git a/src/containers/InteractiveMessage/InteractiveMessageList/InteractiveMessageList.tsx b/src/containers/InteractiveMessage/InteractiveMessageList/InteractiveMessageList.tsx index a0e3472cd..9283f8742 100644 --- a/src/containers/InteractiveMessage/InteractiveMessageList/InteractiveMessageList.tsx +++ b/src/containers/InteractiveMessage/InteractiveMessageList/InteractiveMessageList.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import InteractiveMessageIcon from 'assets/images/icons/InteractiveMessage/Dark.svg?react'; import DownArrow from 'assets/images/icons/DownArrow.svg?react'; -import DuplicateIcon from 'assets/images/icons/Flow/Duplicate.svg?react'; +import DuplicateIcon from 'assets/images/icons/Duplicate.svg?react'; import { List } from 'containers/List/List'; import { FILTER_INTERACTIVE_MESSAGES, diff --git a/src/containers/Template/Form/HSM/HSM.test.tsx b/src/containers/Template/Form/HSM/HSM.test.tsx index 8bb04964d..083ea5a70 100644 --- a/src/containers/Template/Form/HSM/HSM.test.tsx +++ b/src/containers/Template/Form/HSM/HSM.test.tsx @@ -48,21 +48,26 @@ describe('Add mode', () => { const { queryByText } = within(container.querySelector('form') as HTMLElement); const button: any = queryByText('Submit for Approval'); await user.click(button); + + // we should have 2 errors await waitFor(() => { expect(queryByText('Title is required.')).toBeInTheDocument(); expect(queryByText('Message is required.')).toBeInTheDocument(); }); - // we should have 2 errors - 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.', }, }); + + await user.click(button); + // we should still have 2 errors - expect(queryByText('Title is required.')).toBeInTheDocument(); - expect(queryByText('Message is required.')).toBeInTheDocument(); + await waitFor(() => { + expect(queryByText('Title length is too long.')).toBeInTheDocument(); + expect(queryByText('Message is required.')).toBeInTheDocument(); + }); }); }); diff --git a/src/containers/Template/Form/HSM/HSM.tsx b/src/containers/Template/Form/HSM/HSM.tsx index b756355ef..67a2f5825 100644 --- a/src/containers/Template/Form/HSM/HSM.tsx +++ b/src/containers/Template/Form/HSM/HSM.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useQuery } from '@apollo/client'; import { EditorState } from 'draft-js'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useParams, useLocation } from 'react-router-dom'; import Loading from 'components/UI/Layout/Loading/Loading'; import TemplateIcon from 'assets/images/icons/Template/UnselectedDark.svg?react'; import { GET_HSM_CATEGORIES } from 'graphql/queries/Template'; @@ -32,6 +32,7 @@ export const HSM = () => { const [category, setCategory] = useState({ label: '', id: '' }); const { t } = useTranslation(); const params = useParams(); + const location: any = useLocation(); const { data: categoryList, loading } = useQuery(GET_HSM_CATEGORIES); @@ -109,8 +110,9 @@ export const HSM = () => { setSampleMessages(message); }; + const isCopyState = location.state === 'copy'; let disabled = false; - if (params.id) { + if (params.id && !isCopyState) { disabled = true; } diff --git a/src/containers/Template/Form/Template.tsx b/src/containers/Template/Form/Template.tsx index 8d1057006..695be6525 100644 --- a/src/containers/Template/Form/Template.tsx +++ b/src/containers/Template/Form/Template.tsx @@ -13,7 +13,7 @@ import { EmojiInput } from 'components/UI/Form/EmojiInput/EmojiInput'; import { AutoComplete } from 'components/UI/Form/AutoComplete/AutoComplete'; import { Checkbox } from 'components/UI/Form/Checkbox/Checkbox'; import { LanguageBar } from 'components/UI/LanguageBar/LanguageBar'; -import { GET_TEMPLATE, FILTER_TEMPLATES } from 'graphql/queries/Template'; +import { GET_TEMPLATE } from 'graphql/queries/Template'; import { CREATE_MEDIA_MESSAGE } from 'graphql/mutations/Chat'; import { USER_LANGUAGES } from 'graphql/queries/Organization'; import { GET_TAGS } from 'graphql/queries/Tags'; @@ -182,12 +182,11 @@ const Template = ({ const [label, setLabel] = useState(''); const [body, setBody] = useState(EditorState.createEmpty()); const [example, setExample] = useState(EditorState.createEmpty()); - const [filterLabel, setFilterLabel] = useState(''); const [shortcode, setShortcode] = useState(''); const [language, setLanguageId] = useState({}); const [type, setType] = useState(null); const [translations, setTranslations] = useState(); - const [attachmentURL, setAttachmentURL] = useState(); + const [attachmentURL, setAttachmentURL] = useState(''); const [languageOptions, setLanguageOptions] = useState([]); const [isActive, setIsActive] = useState(true); const [validatingURL, setValidatingURL] = useState(false); @@ -203,13 +202,37 @@ const Template = ({ const navigate = useNavigate(); const location: any = useLocation(); const params = useParams(); - const isEditForm = !!params.id; - const { data: tag } = useQuery(GET_TAGS, { + let isEditing = false; + let mode; + + const isCopyState = location.state === 'copy'; + if (isCopyState) { + queries.updateItemQuery = CREATE_TEMPLATE; + mode = 'copy'; + } else { + queries.updateItemQuery = UPDATE_TEMPLATE; + } + + if (params.id && !isCopyState) { + isEditing = 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); + + // create media for attachment + const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE); + const states = { language, label, @@ -242,7 +265,7 @@ const Template = ({ hasButtons, }: any) => { if (languageOptions.length > 0 && languageIdValue) { - if (location.state) { + if (location.state && location.state !== 'copy') { const selectedLangauge = languageOptions.find( (lang: any) => lang.label === location.state.language ); @@ -353,26 +376,78 @@ const Template = ({ } }; - const { data: languages } = useQuery(USER_LANGUAGES, { - variables: { opts: { order: 'ASC' } }, - }); + 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); + } + }; - const [getSessionTemplates, { data: sessionTemplates }] = useLazyQuery(FILTER_TEMPLATES, { - variables: { - filter: { languageId: language ? parseInt(language.id, 10) : null }, - opts: { - order: 'ASC', - limit: null, - offset: 0, - }, - }, - }); + 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 [getSessionTemplate, { data: template, loading: templateLoading }] = - useLazyQuery(GET_TEMPLATE); + const addTemplateButtons = (addFromTemplate: boolean = true) => { + let buttons: any = []; + const buttonType: any = { + QUICK_REPLY: { value: '' }, + CALL_TO_ACTION: { type: '', title: '', value: '' }, + }; - // create media for attachment - const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE); + 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 }; + }; useEffect(() => { if (params.id) { @@ -387,16 +462,10 @@ const Template = ({ lang.sort((first: any, second: any) => (first.label > second.label ? 1 : -1)); setLanguageOptions(lang); - if (!isEditForm) setLanguageId(lang[0]); + if (!isEditing) setLanguageId(lang[0]); } }, [languages]); - useEffect(() => { - if (filterLabel && language && language.id) { - getSessionTemplates(); - } - }, [filterLabel, language, getSessionTemplates]); - useEffect(() => { setShortcode(getShortcode); }, [getShortcode]); @@ -407,27 +476,53 @@ const Template = ({ } }, [getExample]); - const validateTitle = (value: any) => { - let error; - if (value) { - setFilterLabel(value); - let found = []; - if (sessionTemplates) { - if (getSessionTemplatesCallBack) { - getSessionTemplatesCallBack(sessionTemplates); - } - // need to check exact title - found = sessionTemplates.sessionTemplates.filter((search: any) => search.label === value); - if (params.id && found.length > 0) { - found = found.filter((search: any) => search.id !== params.id); - } + useEffect(() => { + if ((type === '' || type) && attachmentURL) { + validateURL(attachmentURL); + if (getUrlAttachmentAndType) { + getUrlAttachmentAndType(type.id || 'TEXT', { url: attachmentURL }); } - if (found.length > 0) { - error = t('Title already exists.'); + } + }, [type, attachmentURL]); + + useEffect(() => { + displayWarning(); + }, [type]); + + useEffect(() => { + if (templateType) { + addTemplateButtons(false); + } + }, [templateType]); + + // Removing buttons when checkbox is checked or unchecked + useEffect(() => { + if (getExample) { + const { message }: any = getTemplateAndButton(getPlainTextFromEditor(getExample)); + onExampleChange(message || ''); + } + }, [isAddButtonChecked]); + + // Converting buttons to template and vice-versa to show realtime update on simulator + useEffect(() => { + if (templateButtons.length > 0) { + const parse = convertButtonsToTemplate(templateButtons, templateType); + + const parsedText = parse.length ? `| ${parse.join(' | ')}` : null; + + const { message }: any = getTemplateAndButton(getPlainTextFromEditor(example)); + + const sampleText: any = parsedText && message + parsedText; + + if (sampleText) { + onExampleChange(sampleText); } } - return error; - }; + }, [templateButtons]); + + if (languageLoading || templateLoading || tagLoading) { + return ; + } const updateTranslation = (value: any) => { const translationId = value.id; @@ -469,7 +564,7 @@ const Template = ({ const selected = languageOptions.find( ({ label: languageLabel }: any) => languageLabel === value ); - if (selected && isEditForm) { + if (selected && isEditing) { updateTranslation(selected); } else if (selected) { setLanguageId(selected); @@ -484,62 +579,12 @@ const Template = ({ } // create translations only while updating - if (result && isEditForm) { + if (result && isEditing) { updateTranslation(result); } if (result) setLanguageId(result); }; - 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); - }); - } - }; - - useEffect(() => { - if ((type === '' || type) && attachmentURL) { - validateURL(attachmentURL); - if (getUrlAttachmentAndType) { - getUrlAttachmentAndType(type.id || 'TEXT', { url: attachmentURL }); - } - } - }, [type, attachmentURL]); - - 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); - } - }; - - useEffect(() => { - displayWarning(); - }, [type]); - let timer: any = null; const attachmentField = [ { @@ -552,7 +597,7 @@ const Template = ({ variant: 'outlined', label: t('Attachment Type'), }, - disabled: !!(defaultAttribute.isHsm && params.id), + disabled: isEditing, helperText: warning, onChange: (event: any) => { const val = event || ''; @@ -568,7 +613,7 @@ const Template = ({ type: 'text', placeholder: t('Attachment URL'), validate: () => isUrlValid, - disabled: !!(defaultAttribute.isHsm && params.id), + 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.' ), @@ -607,7 +652,7 @@ const Template = ({ variant: 'outlined', label: `${t('Language')}*`, }, - disabled: !!(defaultAttribute.isHsm && params.id), + disabled: isEditing, onChange: getLanguageId, } : { @@ -623,8 +668,7 @@ const Template = ({ component: Input, name: 'label', placeholder: `${t('Title')}*`, - validate: validateTitle, - disabled: !!(defaultAttribute.isHsm && params.id), + disabled: isEditing, helperText: defaultAttribute.isHsm ? t('Define what use case does this template serve eg. OTP, optin, activity preference') : null, @@ -639,7 +683,7 @@ const Template = ({ rows: 5, convertToWhatsApp: true, textArea: true, - disabled: !!(defaultAttribute.isHsm && params.id), + disabled: isEditing, helperText: defaultAttribute.isHsm ? 'You can also use variable and interactive actions. Variable format: {{1}}, Button format: [Button text,Value] Value can be a URL or a phone number.' : null, @@ -649,73 +693,6 @@ const Template = ({ }, ]; - 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); - }; - - useEffect(() => { - if (templateType) { - addTemplateButtons(false); - } - }, [templateType]); - - 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 }; - }; - - // Removing buttons when checkbox is checked or unchecked - useEffect(() => { - if (getExample) { - const { message }: any = getTemplateAndButton(getPlainTextFromEditor(getExample)); - onExampleChange(message || ''); - } - }, [isAddButtonChecked]); - - // Converting buttons to template and vice-versa to show realtime update on simulator - useEffect(() => { - if (templateButtons.length > 0) { - const parse = convertButtonsToTemplate(templateButtons, templateType); - - const parsedText = parse.length ? `| ${parse.join(' | ')}` : null; - - const { message }: any = getTemplateAndButton(getPlainTextFromEditor(example)); - - const sampleText: any = parsedText && message + parsedText; - - if (sampleText) { - onExampleChange(sampleText); - } - } - }, [templateButtons]); - const handeInputChange = (event: any, row: any, index: any, eventType: any) => { const { value } = event.target; const obj = { ...row }; @@ -734,7 +711,7 @@ const Template = ({ component: Checkbox, title: Add buttons, name: 'isAddButtonChecked', - disabled: !!(defaultAttribute.isHsm && params.id), + disabled: !!(defaultAttribute.isHsm && params.id && !isCopyState), handleChange: (value: boolean) => setIsAddButtonChecked(value), }, { @@ -742,7 +719,7 @@ const Template = ({ isAddButtonChecked, templateType, inputFields: templateButtons, - disabled: !!params.id, + disabled: isEditing, onAddClick: addTemplateButtons, onRemoveClick: removeTemplateButtons, onInputChange: handeInputChange, @@ -755,7 +732,7 @@ const Template = ({ name: 'tagId', options: tag ? tag.tags : [], optionLabel: 'label', - disabled: false, + disabled: isEditing, hasCreateOption: true, multiple: false, onChange: (value: any) => { @@ -1001,8 +978,9 @@ const Template = ({ } }; - if (languageOptions.length < 1 || templateLoading) { - return ; + let copyMessage = t('Copy of the speed send has been created!'); + if (defaultAttribute.isHsm) { + copyMessage = t('Copy of the template has been created!'); } return ( @@ -1011,7 +989,7 @@ const Template = ({ states={states} setStates={setStates} setPayload={setPayload} - validationSchema={isEditForm ? Yup.object() : FormSchema} + validationSchema={isEditing ? Yup.object() : FormSchema} listItemName={listItemName} dialogMessage={dialogMessage} formFields={fields} @@ -1029,6 +1007,8 @@ const Template = ({ customStyles={customStyle} saveOnPageChange={false} afterSave={!defaultAttribute.isHsm ? afterSave : undefined} + type={mode} + copyNotification={copyMessage} /> ); }; diff --git a/src/containers/Template/List/Template.tsx b/src/containers/Template/List/Template.tsx index 81aa73601..cf3df953c 100644 --- a/src/containers/Template/List/Template.tsx +++ b/src/containers/Template/List/Template.tsx @@ -1,10 +1,11 @@ import { useContext, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import moment from 'moment'; import { useTranslation } from 'react-i18next'; import { Checkbox, FormControlLabel } from '@mui/material'; +import { useMutation, useQuery } from '@apollo/client'; import { List } from 'containers/List/List'; -import { useMutation, useQuery } from '@apollo/client'; import { WhatsAppToJsx } from 'common/RichEditor'; import { DATE_TIME_FORMAT, GUPSHUP_ENTERPRISE_SHORTCODE } from 'common/constants'; import { @@ -22,6 +23,7 @@ import DownArrow from 'assets/images/icons/DownArrow.svg?react'; import ApprovedIcon from 'assets/images/icons/Template/Approved.svg?react'; import RejectedIcon from 'assets/images/icons/Template/Rejected.svg?react'; import PendingIcon from 'assets/images/icons/Template/Pending.svg?react'; +import DuplicateIcon from 'assets/images/icons/Duplicate.svg?react'; import { ProviderContext } from 'context/session'; import { copyToClipboardMethod, exportCsvFile, getFileExtension } from 'common/utils'; import Loading from 'components/UI/Layout/Loading/Loading'; @@ -80,6 +82,7 @@ export const Template = ({ const [open, setOpen] = useState(false); const [Id, setId] = useState(''); const { t } = useTranslation(); + const navigate = useNavigate(); const { provider } = useContext(ProviderContext); const [selectedTag, setSelectedTag] = useState(null); @@ -223,6 +226,14 @@ export const Template = ({ } }; + const setCopyDialog = (id: any) => { + let redirectPath = 'speed-send'; + if (isHSM) { + redirectPath = 'template'; + } + navigate(`/${redirectPath}/${id}/edit`, { state: 'copy' }); + }; + const setDialog = (id: string) => { if (Id !== id) { setId(id); @@ -232,6 +243,13 @@ export const Template = ({ } }; + const copyAction = { + label: t('Make a copy'), + icon: , + parameter: 'id', + dialog: setCopyDialog, + }; + let additionalAction: any = () => [ { label: t('Show all languages'), @@ -239,6 +257,7 @@ export const Template = ({ parameter: 'id', dialog: setDialog, }, + copyAction, ]; let defaultSortBy; @@ -292,6 +311,7 @@ export const Template = ({ parameter: 'id', dialog: copyUuid, }, + copyAction, ]; defaultSortBy = 'STATUS'; appliedFilters = { ...templateFilters, status: filterValue }; diff --git a/src/containers/Trigger/TriggerList/TriggerList.tsx b/src/containers/Trigger/TriggerList/TriggerList.tsx index a72055949..67d77b739 100644 --- a/src/containers/Trigger/TriggerList/TriggerList.tsx +++ b/src/containers/Trigger/TriggerList/TriggerList.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import TriggerIcon from 'assets/images/icons/Trigger/Union.svg?react'; import ClockIcon from 'assets/images/icons/Trigger/Clock.svg?react'; import ClockInactiveIcon from 'assets/images/icons/Trigger/Inactive.svg?react'; -import DuplicateIcon from 'assets/images/icons/Flow/Duplicate.svg?react'; +import DuplicateIcon from 'assets/images/icons/Duplicate.svg?react'; import { TRIGGER_LIST_QUERY, TRIGGER_QUERY_COUNT } from 'graphql/queries/Trigger'; import { DELETE_TRIGGER } from 'graphql/mutations/Trigger'; import { FULL_DATE_FORMAT, dayList } from 'common/constants'; diff --git a/src/i18n/en/en.json b/src/i18n/en/en.json index 359f15221..610e51e6d 100644 --- a/src/i18n/en/en.json +++ b/src/i18n/en/en.json @@ -373,6 +373,8 @@ "Message is required.": "Message is required.", "Please enter valid phone number.": "Please enter valid phone number.", "Please enter valid url.": "Please enter valid url.", + "Copy of the speed send has been created!": "", + "Copy of the template has been created!": "", "Submit for Approval": "Submit for Approval", "Create": "Create", "Create Speed Send": "Create Speed Send", @@ -393,6 +395,7 @@ "Change assignee": "Change assignee", "Remarks": "Remarks", "Update ticket": "Update ticket", + "ID": "ID", "Created at": "Created at", "Issue": "Issue", "Opened by": "Opened by", @@ -433,6 +436,5 @@ "Request header": "Request header", "Request JSON": "Request JSON", "Response JSON": "Response JSON", - "Webhook Logs": "Webhook Logs", - "ID": "ID" + "Webhook Logs": "Webhook Logs" }