diff --git a/.github/workflows/cypress-testing.yml b/.github/workflows/cypress-testing.yml index b85dce6a03..8256c7922c 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 feature/template-flows cd .. cp -r cypress-testing/cypress cypress yarn add cypress@13.6.2 diff --git a/src/assets/images/icons/ViewLight.svg b/src/assets/images/icons/ViewLight.svg new file mode 100644 index 0000000000..3a9d55b862 --- /dev/null +++ b/src/assets/images/icons/ViewLight.svg @@ -0,0 +1,15 @@ + + + View + + + + + + + + + diff --git a/src/components/floweditor/FlowEditor.helper.ts b/src/components/floweditor/FlowEditor.helper.ts index 8b59d0943e..321dd91814 100644 --- a/src/components/floweditor/FlowEditor.helper.ts +++ b/src/components/floweditor/FlowEditor.helper.ts @@ -4,14 +4,14 @@ import '@nyaruka/temba-components/dist/temba-components.js'; const glificBase = FLOW_EDITOR_API; -export const setConfig = (uuid: any) => { +export const setConfig = (uuid: any, isTemplate: boolean) => { const services = JSON.parse(localStorage.getItem('organizationServices') || '{}'); const config = { flow: uuid, flowType: 'messaging', localStorage: true, - mutable: true, + mutable: !isTemplate, showNodeLabel: false, attachmentsEnabled: false, filters: ['whatsapp', 'classifier', 'profile', 'optins', 'ticketer'], diff --git a/src/components/floweditor/FlowEditor.tsx b/src/components/floweditor/FlowEditor.tsx index 3d71a4b7ae..46f1e33cea 100644 --- a/src/components/floweditor/FlowEditor.tsx +++ b/src/components/floweditor/FlowEditor.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useMutation, useLazyQuery, useQuery } from '@apollo/client'; -import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'; import { Menu, MenuItem, Typography } from '@mui/material'; import BackIconFlow from 'assets/images/icons/BackIconFlow.svg?react'; import WarningIcon from 'assets/images/icons/Warning.svg?react'; @@ -31,11 +31,13 @@ export const FlowEditor = () => { const params = useParams(); const { uuid } = params; const navigate = useNavigate(); + const location = useLocation(); const [publishDialog, setPublishDialog] = useState(false); const [loading, setLoading] = useState(true); const [flowEditorLoaded, setFlowEditorLoaded] = useState(false); const [flowId, setFlowId] = useState(); - const config = setConfig(uuid); + const isTemplate = location?.state === 'template'; + const config = setConfig(uuid, isTemplate); const [published, setPublished] = useState(false); const [showSimulator, setShowSimulator] = useState(false); const [stayOnPublish, setStayOnPublish] = useState(false); @@ -350,7 +352,7 @@ export const FlowEditor = () => {
navigate('/flow')} + onClick={() => (isTemplate ? navigate('/flow?isTemplate=true') : navigate('/flow'))} className={styles.BackIcon} data-testid="back-button" /> @@ -402,6 +404,7 @@ export const FlowEditor = () => { handleClose(); }} disableRipple + disabled={isTemplate} > Reset flow count @@ -411,6 +414,7 @@ export const FlowEditor = () => { variant="outlined" color="primary" data-testid="previewButton" + disabled={isTemplate} onClick={() => { setShowTranslateFlowModal(true); handleClose(); @@ -433,6 +437,7 @@ export const FlowEditor = () => { variant="contained" color="primary" data-testid="button" + disabled={isTemplate} onClick={() => setPublishDialog(true)} > diff --git a/src/containers/Flow/Flow.test.tsx b/src/containers/Flow/Flow.test.tsx index e64f356cef..3d7a29e04d 100644 --- a/src/containers/Flow/Flow.test.tsx +++ b/src/containers/Flow/Flow.test.tsx @@ -12,6 +12,8 @@ import { createFlowQuery, createTagQuery, updateFlowQueryWithError, + getFlowCountQuery, + releaseFlow, } from 'mocks/Flow'; import { Flow } from './Flow'; import { setOrganizationServices } from 'services/AuthService'; @@ -19,6 +21,7 @@ import { getFilterTagQuery } from 'mocks/Tag'; import { getRoleNameQuery } from 'mocks/Role'; import userEvent from '@testing-library/user-event'; import { setErrorMessage, setNotification } from 'common/notification'; +import FlowList from './FlowList/FlowList'; setOrganizationServices('{"__typename":"OrganizationServicesResult","rolesAndPermission":true}'); @@ -26,13 +29,17 @@ const mocks = [ ...getOrganizationQuery, getFlowQuery({ id: 1 }), getFlowQuery({ id: '1' }), - filterFlowQuery, + filterFlowQuery({ isActive: true, isTemplate: false }), getFilterTagQuery, getRoleNameQuery, getOrganizationLanguagesQuery, - copyFlowQuery, + copyFlowQuery(), + copyFlowQuery(true), createFlowQuery, createTagQuery, + getFlowCountQuery({ isActive: true, isTemplate: false }), + releaseFlow, + getFilterTagQuery, ]; const mockUseLocationValue: any = { @@ -41,11 +48,14 @@ const mockUseLocationValue: any = { hash: '', state: null, }; +const mockedUsedNavigate = vi.fn(); + vi.mock('react-router-dom', async () => ({ ...((await vi.importActual('react-router-dom')) as {}), useLocation: () => { return mockUseLocationValue; }, + useNavigate: () => mockedUsedNavigate, })); vi.mock('common/notification', async (importOriginal) => { @@ -155,6 +165,7 @@ it('should edit the flow', async () => { + } /> } /> @@ -180,6 +191,32 @@ it('should edit the flow', async () => { }); }); +it('should configure the flow', async () => { + const editFlow = () => ( + + + + } /> + } /> + + + + ); + const { getByText } = render(editFlow()); + + expect(getByText('Loading...')).toBeInTheDocument(); + + await waitFor(() => { + expect(getByText('Edit flow')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Configure')); + + await waitFor(() => { + expect(setNotification).toHaveBeenCalled(); + }); +}); + it('should edit the flow and show error if exists', async () => { const editFlow = () => ( @@ -236,3 +273,56 @@ it('should create copy of flow', async () => { fireEvent.click(button); await waitFor(() => {}); }); + +it('buttons should be disabled in template state', async () => { + mockUseLocationValue.state = 'template'; + + render( + + + + } /> + + + + ); + + await waitFor(() => { + expect(screen.getByText('Template Flow')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByTestId('submitActionButton')).toBeDisabled(); + expect(screen.getByTestId('remove-icon')).toBeDisabled(); + }); + + fireEvent.click(screen.getByText('View')); + + await waitFor(() => { + expect(mockedUsedNavigate).toHaveBeenCalled(); + }); +}); + +it('should create copy of a template flow', async () => { + mockUseLocationValue.state = 'copyTemplate'; + + const copyFlow = () => ( + + + + } /> + + + + ); + + const { container, getByTestId } = render(copyFlow()); + await waitFor(() => { + const inputElement = container.querySelector('input[name="name"]') as HTMLInputElement; + expect(inputElement?.value).toBe('Copy of Help'); + }); + + const button = getByTestId('submitActionButton'); + fireEvent.click(button); + await waitFor(() => {}); +}); diff --git a/src/containers/Flow/Flow.tsx b/src/containers/Flow/Flow.tsx index 901b32642e..1507673454 100644 --- a/src/containers/Flow/Flow.tsx +++ b/src/containers/Flow/Flow.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import * as Yup from 'yup'; import { useTranslation } from 'react-i18next'; -import { useLocation, useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useQuery, useMutation } from '@apollo/client'; import { FormLayout } from 'containers/Form/FormLayout'; @@ -31,6 +31,7 @@ const queries = { export const Flow = () => { const location = useLocation(); + const navigate = useNavigate(); const params = useParams(); const [name, setName] = useState(''); const [isPinnedDisable, setIsPinnedDisable] = useState(false); @@ -44,6 +45,10 @@ export const Flow = () => { const [ignoreKeywords, setIgnoreKeywords] = useState(false); const { t } = useTranslation(); + let isTemplate = false; + if (location.state === 'template') { + isTemplate = true; + } const { data: tag } = useQuery(GET_TAGS, { variables: {}, fetchPolicy: 'network-only', @@ -94,11 +99,17 @@ export const Flow = () => { // Override name & keywords when creating Flow Copy let fieldName = nameValue; let fieldKeywords = keywordsValue; + let description = descriptionValue; + let tags = tagValue; if (location.state === 'copy') { fieldName = `Copy of ${nameValue}`; fieldKeywords = ''; + } else if (location.state === 'copyTemplate') { + fieldName = `Copy of ${nameValue}`; + description = ''; + tags = null; + fieldKeywords = ''; } - const { organization: { organization: { newcontactFlowId }, @@ -114,7 +125,7 @@ export const Flow = () => { setIsPinned(isPinnedValue); setIsBackground(isBackgroundValue); setRoles(rolesValue); - setDescription(descriptionValue); + setDescription(description); // we are receiving keywords as an array object if (fieldKeywords.length > 0) { @@ -122,7 +133,8 @@ export const Flow = () => { setKeywords(fieldKeywords.join(',')); } setIgnoreKeywords(ignoreKeywordsValue); - const getTagId = tag && tag.tags.filter((tags: any) => tags.id === tagValue?.id); + const getTagId = tag && tag.tags.filter((t: any) => t.id === tags?.id); + if (getTagId.length > 0) { setTagId(getTagId[0]); } @@ -137,8 +149,29 @@ export const Flow = () => { }); const dialogMessage = t("You won't be able to use this flow again."); + let backLink = '/flow'; + let cancelLink = 'flow'; + if (isTemplate || location.state === 'copyTemplate') { + backLink = '/flow?isTemplate=true'; + cancelLink = 'flow?isTemplate=true'; + } - const additionalAction = { label: t('Configure'), link: '/flow/configure' }; + const configureAction = { + label: t('Configure'), + link: '/flow/configure', + }; + + const viewAction = { + label: t('View'), + link: '/flow/configure', + action: (link: any) => { + navigate(link, { + state: 'template', + }); + }, + }; + + const additionalAction = isTemplate ? viewAction : configureAction; const formFields = [ { @@ -146,6 +179,7 @@ export const Flow = () => { name: 'name', type: 'text', label: t('Name'), + disabled: isTemplate, }, { component: Input, @@ -153,6 +187,7 @@ export const Flow = () => { type: 'text', label: t('Keywords'), helperText: t('Enter comma separated keywords that trigger this flow.'), + disabled: isTemplate, }, { component: Input, @@ -161,13 +196,14 @@ export const Flow = () => { textArea: true, rows: 2, label: t('Description'), + disabled: isTemplate, }, { component: AutoComplete, name: 'tagId', options: tag ? tag.tags : [], optionLabel: 'label', - disabled: false, + disabled: isTemplate, handleCreateItem: handleCreateLabel, hasCreateOption: true, multiple: false, @@ -185,25 +221,28 @@ export const Flow = () => { }, darkCheckbox: true, className: styles.Checkbox, + disabled: isTemplate, }, { component: Checkbox, name: 'isActive', title: t('Is active?'), darkCheckbox: true, + disabled: isTemplate, }, { component: Checkbox, name: 'isPinned', title: t('Is pinned?'), darkCheckbox: !isPinnedDisable, - disabled: isPinnedDisable, + disabled: isPinnedDisable || isTemplate, }, { component: Checkbox, name: 'isBackground', title: t('Run this flow in the background'), darkCheckbox: true, + disabled: isTemplate, }, ]; @@ -237,10 +276,20 @@ export const Flow = () => { // alter header & update/copy queries let title; let type; + let copyNotification; + if (location.state === 'copy') { queries.updateItemQuery = CREATE_FLOW_COPY; title = t('Copy flow'); type = 'copy'; + copyNotification = t('Copy of the flow has been created!'); + } else if (location.state === 'copyTemplate') { + queries.updateItemQuery = CREATE_FLOW_COPY; + title = t('Template flow copy'); + type = 'copy'; + copyNotification = t('Flow created successfully from template!'); + } else if (location.state === 'template') { + title = t('Template Flow'); } else { queries.updateItemQuery = UPDATE_FLOW; } @@ -270,7 +319,7 @@ export const Flow = () => { dialogMessage={dialogMessage} formFields={formFields} redirectionLink="flow" - cancelLink="flow" + cancelLink={cancelLink} linkParameter="uuid" listItem="flow" icon={flowIcon} @@ -278,10 +327,12 @@ export const Flow = () => { languageSupport={false} title={title} type={type} - copyNotification={t('Copy of the flow has been created!')} + copyNotification={copyNotification} customHandler={customHandler} helpData={flowInfo} - backLinkButton="/flow" + backLinkButton={backLink} + buttonState={{ text: 'Save', status: isTemplate }} + restrictButtonStatus={{ status: isTemplate }} /> ); }; diff --git a/src/containers/Flow/FlowList/FlowList.module.css b/src/containers/Flow/FlowList/FlowList.module.css index b762fe109b..f8c1b52445 100644 --- a/src/containers/Flow/FlowList/FlowList.module.css +++ b/src/containers/Flow/FlowList/FlowList.module.css @@ -191,3 +191,7 @@ .ImportDialog { max-width: 395px; } + +.DialogContent { + text-align: center; +} diff --git a/src/containers/Flow/FlowList/FlowList.test.tsx b/src/containers/Flow/FlowList/FlowList.test.tsx index e04ac7ea00..18376583ef 100644 --- a/src/containers/Flow/FlowList/FlowList.test.tsx +++ b/src/containers/Flow/FlowList/FlowList.test.tsx @@ -12,6 +12,7 @@ import { importFlow, exportFlow, releaseFlow, + filterTemplateFlows, } from 'mocks/Flow'; import { getOrganizationQuery } from 'mocks/Organization'; import testJSON from 'mocks/ImportFlow.json'; @@ -21,15 +22,17 @@ import { Flow } from '../Flow'; import { getFilterTagQuery } from 'mocks/Tag'; import { getRoleNameQuery } from 'mocks/Role'; +const isActiveFilter = { isActive: true, isTemplate: false }; + const mocks = [ - getFlowCountQuery, - getFlowCountQuery, - getFlowCountQuery, - getFlowCountQuery, - filterFlowQuery, - filterFlowQuery, - filterFlowQuery, - filterFlowQuery, + getFlowCountQuery(isActiveFilter), + getFlowCountQuery(isActiveFilter), + getFlowCountQuery(isActiveFilter), + getFlowCountQuery(isActiveFilter), + filterFlowQuery(isActiveFilter), + filterFlowQuery(isActiveFilter), + filterFlowQuery(isActiveFilter), + filterFlowQuery(isActiveFilter), filterFlowNewQuery, getFlowCountNewQuery, getFlowQuery({ id: 1 }), @@ -39,6 +42,8 @@ const mocks = [ exportFlow, getFilterTagQuery, getRoleNameQuery, + getFlowCountQuery({ isTemplate: true }), + filterTemplateFlows, ...getOrganizationQuery, ]; @@ -52,11 +57,13 @@ const flowList = ( HTMLAnchorElement.prototype.click = vi.fn(); +const mockedUsedNavigate = vi.fn(); vi.mock('react-router-dom', async () => { return { ...(await vi.importActual('react-router-dom')), useLocation: () => ({ state: 'copy', pathname: '/flow/1/edit' }), useParams: () => ({ id: 1 }), + useNavigate: () => mockedUsedNavigate, }; }); @@ -108,8 +115,13 @@ describe('', () => { fireEvent.click(getAllByTestId('MoreIcon')[0]); await waitFor(() => { - expect(screen.getAllByTestId('additionalButton')[0]).toBeInTheDocument(); - fireEvent.click(screen.getAllByTestId('additionalButton')[0]); + expect(screen.getByText('Copy')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Copy')); + + await waitFor(() => { + expect(mockedUsedNavigate).toHaveBeenCalled(); }); }); @@ -137,7 +149,11 @@ describe('', () => { const input = screen.getByTestId('import'); fireEvent.change(input); - await waitFor(() => {}); + await waitFor(() => { + expect(screen.getByText('Import flow Status')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('ok-button')); }); test('should export flow to json file', async () => { @@ -157,3 +173,56 @@ describe('', () => { }); }); }); + +describe('Template flows', () => { + test('it opens and closes dialog box', async () => { + render(flowList); + + await waitFor(() => { + expect(screen.getByText('Flows')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('newItemButton')); + + // test if it closes the dialog + fireEvent.click(screen.getByTestId('CloseIcon')); + + fireEvent.click(screen.getByTestId('newItemButton')); + + await waitFor(() => { + expect(screen.getByText('Create flow')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('ok-button')); + + await waitFor(() => { + expect(mockedUsedNavigate).toHaveBeenCalled(); + }); + }); + + test('it shows and creates a template flows', async () => { + render(flowList); + + await waitFor(() => { + expect(screen.getByText('Flows')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('newItemButton')); + + await waitFor(() => { + expect(screen.getByText('Create flow')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('ok-button')); + + await waitFor(() => { + expect(screen.getByText('Template Flows')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('viewIt')[0]); + + await waitFor(() => { + expect(mockedUsedNavigate).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/containers/Flow/FlowList/FlowList.tsx b/src/containers/Flow/FlowList/FlowList.tsx index 3c5a2b5ebd..ada308699a 100644 --- a/src/containers/Flow/FlowList/FlowList.tsx +++ b/src/containers/Flow/FlowList/FlowList.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; import { useTranslation } from 'react-i18next'; import { useLazyQuery, useMutation, useQuery } from '@apollo/client'; @@ -11,6 +11,7 @@ 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'; +import ViewIcon from 'assets/images/icons/ViewLight.svg?react'; import { FILTER_FLOW, GET_FLOW_COUNT, EXPORT_FLOW, RELEASE_FLOW } from 'graphql/queries/Flow'; import { DELETE_FLOW, IMPORT_FLOW } from 'graphql/mutations/Flow'; import { List } from 'containers/List/List'; @@ -78,15 +79,18 @@ const queries = { }; const configureIcon = ; +const viewIcon = ; export const FlowList = () => { const navigate = useNavigate(); + const location = useLocation(); const { t } = useTranslation(); const [filter, setFilter] = useState(true); const [selectedtag, setSelectedTag] = useState(null); const [flowName, setFlowName] = useState(''); const [importing, setImporting] = useState(false); const [importStatus, setImportStatus] = useState([]); + const [showDialog, setShowDialog] = useState(false); const [releaseFlow] = useLazyQuery(RELEASE_FLOW); @@ -118,7 +122,7 @@ export const FlowList = () => { }, }); - const setDialog = (id: any) => { + const handleCopy = (id: any) => { navigate(`/flow/${id}/edit`, { state: 'copy' }); }; @@ -156,7 +160,28 @@ export const FlowList = () => { /> ); - const additionalAction = () => [ + const templateFlowActions = [ + { + label: 'View it', + icon: viewIcon, + parameter: 'id', + insideMore: false, + dialog: (id: any) => { + navigate(`/flow/${id}/edit`, { state: 'template' }); + }, + }, + { + label: 'Use it', + icon: , + parameter: 'id', + insideMore: false, + dialog: (id: any) => { + navigate(`/flow/${id}/edit`, { state: 'copyTemplate' }); + }, + }, + ]; + + const actions = [ { label: t('Configure'), icon: configureIcon, @@ -168,7 +193,7 @@ export const FlowList = () => { icon: , parameter: 'id', insideMore: true, - dialog: setDialog, + dialog: handleCopy, }, { label: t('Export'), @@ -179,6 +204,8 @@ export const FlowList = () => { }, ]; + const additionalAction = () => (filter === 'isTemplate' ? templateFlowActions : actions); + const getColumns = ({ name, keywords, @@ -215,6 +242,7 @@ export const FlowList = () => { const filterList = [ { label: 'Active', value: true }, { label: 'Inactive', value: false }, + { label: 'Template', value: 'isTemplate' }, ]; const { data: tag } = useQuery(GET_TAGS, { variables: {}, @@ -230,7 +258,7 @@ export const FlowList = () => { value={filter} onChange={(event) => { const { value } = event.target; - setFilter(JSON.parse(value)); + setFilter(value); }} className={styles.SearchBar} > @@ -259,20 +287,64 @@ export const FlowList = () => { ); - const filters = useMemo( - () => ({ - isActive: filter, + const filters = useMemo(() => { + let filters = { ...(selectedtag?.id && { tagIds: [parseInt(selectedtag?.id)] }), - }), - [filter, selectedtag, importing] - ); + }; + if (filter === 'isTemplate') { + filters = { ...filters, isTemplate: true }; + } else { + filters = { ...filters, isActive: filter, isTemplate: false }; + } + return filters; + }, [filter, selectedtag, importing]); + + const restrictedAction = () => + filter === 'isTemplate' ? { delete: false, edit: false } : { edit: true, delete: true }; + + let dialogBox: any; + if (showDialog) { + dialogBox = ( + { + navigate('/flow/add'); + }} + handleOk={() => { + setFilter('isTemplate'); + setShowDialog(false); + }} + handleCancel={() => { + setShowDialog(false); + }} + > +
How do you want to create a flow?
+
+ ); + } + + useEffect(() => { + if (location.search) { + const isTemplate = new URLSearchParams(location.search).get('isTemplate'); + if (isTemplate === 'true') { + setFilter('isTemplate'); + } + } + }, [location]); + + const title = filter === 'isTemplate' ? t('Template Flows') : t('Flows'); return ( <> {dialog} + {dialogBox} { {...columnAttributes} searchParameter={['name_or_keyword_or_tags']} additionalAction={additionalAction} - button={{ show: true, label: t('Create') }} + button={{ show: true, label: t('Create'), action: () => setShowDialog(true) }} secondaryButton={importButton} filters={filters} filterList={activeFilter} loadingList={importing} + restrictedAction={restrictedAction} /> ); diff --git a/src/containers/Form/FormLayout.test.helper.ts b/src/containers/Form/FormLayout.test.helper.ts index cae0e1a430..32646a5588 100644 --- a/src/containers/Form/FormLayout.test.helper.ts +++ b/src/containers/Form/FormLayout.test.helper.ts @@ -87,9 +87,9 @@ export const LIST_ITEM_MOCKS = [ ...getOrganizationQuery, getOrganizationLanguagesQuery, getFlowQuery({ id: 1 }), - filterFlowQuery, - filterFlowQuery, - getFlowCountQuery, - getFlowCountQuery, + filterFlowQuery({ isActive: true, isTemplate: false }), + filterFlowQuery({ isActive: true, isTemplate: false }), + getFlowCountQuery({ isTemplate: false, isActive: true }), + getFlowCountQuery({ isTemplate: false, isActive: true }), getFilterTagQuery, ]; diff --git a/src/containers/Form/FormLayout.tsx b/src/containers/Form/FormLayout.tsx index b707e273ef..a1a6196ff6 100644 --- a/src/containers/Form/FormLayout.tsx +++ b/src/containers/Form/FormLayout.tsx @@ -81,6 +81,10 @@ export interface FormLayoutProps { title: string; message: string; }; + restrictButtonStatus?: { + text?: string; + status?: boolean; + }; } export const FormLayout = ({ @@ -133,6 +137,7 @@ export const FormLayout = ({ noHeading = false, partialPage = false, confirmationState, + restrictButtonStatus, }: FormLayoutProps) => { const [showDialog, setShowDialog] = useState(false); const [formSubmitted, setFormSubmitted] = useState(false); @@ -523,9 +528,10 @@ export const FormLayout = ({ data-testid="remove-icon" className={styles.DeleteButton} onClick={() => setShowDialog(true)} + disabled={restrictButtonStatus?.status} > - Remove + {restrictButtonStatus?.text || 'Remove'} ) : null; @@ -578,8 +584,13 @@ export const FormLayout = ({ variant="outlined" color="primary" onClick={() => { - formik.submitForm(); setAction(true); + + if (additionalAction?.action) { + additionalAction.action(`${additionalAction.link}/${link}`); + } else { + formik.submitForm(); + } }} data-testid="additionalActionButton" > diff --git a/src/containers/List/List.test.helper.ts b/src/containers/List/List.test.helper.ts index f470f0c295..ad856bba25 100644 --- a/src/containers/List/List.test.helper.ts +++ b/src/containers/List/List.test.helper.ts @@ -45,11 +45,11 @@ export const defaultProps = { export const LIST_MOCKS = [ getCurrentUserQuery, - filterFlowQuery, - getFlowCountQuery, - filterFlowQuery, + filterFlowQuery({ isActive: true }), + getFlowCountQuery({ isActive: true }), + filterFlowQuery({ isActive: true }), filterFlowSortQuery, - getFlowCountQuery, + getFlowCountQuery({ isActive: true }), filterFlowWithNameOrKeywordOrTagQuery, getFlowCountWithFilterQuery, getCurrentUserQuery, diff --git a/src/containers/Profile/Profile.test.tsx b/src/containers/Profile/Profile.test.tsx index 9447100198..600e2a7ef8 100644 --- a/src/containers/Profile/Profile.test.tsx +++ b/src/containers/Profile/Profile.test.tsx @@ -1,5 +1,6 @@ import { render, waitFor } from '@testing-library/react'; import { MockedProvider } from '@apollo/client/testing'; +import { BrowserRouter as Router } from 'react-router-dom'; import { LOGGED_IN_USER_MOCK } from 'mocks/Contact'; import { Profile } from './Profile'; @@ -12,7 +13,9 @@ const props: any = { }; const wrapper = ( - + + + ); @@ -30,7 +33,9 @@ it('should render profile page for contact profile', async () => { render( - + + + ); diff --git a/src/containers/Search/Search.test.tsx b/src/containers/Search/Search.test.tsx index d14ffe84d1..e2d07f9d6e 100644 --- a/src/containers/Search/Search.test.tsx +++ b/src/containers/Search/Search.test.tsx @@ -1,5 +1,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { MockedProvider } from '@apollo/client/testing'; +import { BrowserRouter as Router } from 'react-router-dom'; import { Search } from './Search'; import { LIST_ITEM_MOCKS } from './Search.test.helper'; @@ -48,7 +49,9 @@ test('should load the search edit', async () => { const wrapper = ( - + + + ); @@ -71,7 +74,9 @@ test('should load the search edit', async () => { test('it renders component with saveSearch params', async () => { const wrapper = ( - + + + ); diff --git a/src/i18n/en/en.json b/src/i18n/en/en.json index c9bd01044d..ec7930c355 100644 --- a/src/i18n/en/en.json +++ b/src/i18n/en/en.json @@ -508,5 +508,9 @@ "Export without translations": "Export without translations", "Export the translated content of the message as a csv.": "Export the translated content of the message as a csv.", "Export the content of the message as a csv.": "Export the content of the message as a csv.", - "Import the csv with translations for interactive message.": "Import the csv with translations for interactive message." + "Import the csv with translations for interactive message.": "Import the csv with translations for interactive message.", + "Template Flows": "Template Flows", + "Template Flow": "Template Flow", + "Template flow copy": "Template flow copy", + "Flow created successfully from template!": "Flow created successfully from template!" } diff --git a/src/mocks/Flow.tsx b/src/mocks/Flow.tsx index da987a76cb..ef4a0077f7 100644 --- a/src/mocks/Flow.tsx +++ b/src/mocks/Flow.tsx @@ -174,11 +174,28 @@ const filterFlowResult = { }, }; -export const filterFlowQuery = { +export const filterFlowQuery = (filter: any) => ({ request: { query: FILTER_FLOW, variables: { - filter: { isActive: true }, + filter, + opts: { + limit: 50, + offset: 0, + order: 'DESC', + orderWith: 'is_pinned', + }, + }, + }, + + result: filterFlowResult, +}); + +export const filterTemplateFlows = { + request: { + query: FILTER_FLOW, + variables: { + filter: { isTemplate: true }, opts: { limit: 50, offset: 0, @@ -279,11 +296,11 @@ export const getActiveFlow = getFlowDetails(); export const getInactiveFlow = getFlowDetails(false); export const getFlowWithoutKeyword = getFlowDetails(true, []); -export const getFlowCountQuery = { +export const getFlowCountQuery = (filter: any) => ({ request: { query: GET_FLOW_COUNT, variables: { - filter: { isActive: true }, + filter, }, }, @@ -292,7 +309,7 @@ export const getFlowCountQuery = { countFlows: 1, }, }, -}; +}); export const getFlowCountWithFilterQuery = { request: { @@ -427,10 +444,16 @@ export const importFlow = { result: { data: { importFlow: { - status: { - flowName: 'flow 1', - status: 'success', - }, + status: [ + { + flowName: 'Registration flow', + status: 'Successfully imported', + }, + { + flowName: 'Optin', + status: 'Successfully imported', + }, + ], }, }, }, @@ -640,46 +663,53 @@ export const exportFlowTranslationsWithErrors = { }, }; -export const copyFlowQuery = { - request: { - query: CREATE_FLOW_COPY, - variables: { - id: '1', - input: { - isActive: true, - isPinned: false, - isBackground: false, - name: 'Copy of Help', - keywords: ['help', 'activity'], - description: 'Help flow', - ignoreKeywords: false, - addRoleIds: [], - deleteRoleIds: [], - tag_id: '1', +export const copyFlowQuery = (template: boolean = false) => { + let input = { + isActive: true, + isPinned: false, + isBackground: false, + name: 'Copy of Help', + keywords: template ? [] : ['help', 'activity'], + description: template ? '' : 'Help flow', + ignoreKeywords: false, + addRoleIds: [], + deleteRoleIds: [], + }; + + if (!template) { + Object.assign(input, { tag_id: '1' }); + } + + return { + request: { + query: CREATE_FLOW_COPY, + variables: { + id: '1', + input, }, }, - }, - result: { - data: { - copyFlow: { - flow: { - roles: [], - tag: { id: '1', label: 'New tag' }, - id: '1', - isActive: true, - keywords: ['help', 'activity'], - description: 'Help flow', - name: 'Copy of Help', - isBackground: false, - updatedAt: '2021-03-05T04:32:23Z', - uuid: '3fa22108-f464-41e5-81d9-d8a298854429', - isPinned: false, - }, + result: { + data: { + copyFlow: { + flow: { + roles: [], + tag: { id: '1', label: 'New tag' }, + id: '1', + isActive: true, + keywords: ['help', 'activity'], + description: 'Help flow', + name: 'Copy of Help', + isBackground: false, + updatedAt: '2021-03-05T04:32:23Z', + uuid: '3fa22108-f464-41e5-81d9-d8a298854429', + isPinned: false, + }, - errors: null, + errors: null, + }, }, }, - }, + }; }; export const createFlowQuery = { diff --git a/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx b/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx index 815a89a6a4..cb51d5b93c 100644 --- a/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx +++ b/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx @@ -97,6 +97,7 @@ const routeAdmin = ( } /> } /> } /> + } /> } /> } /> } />