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 @@
+
+
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 = (
} />
} />
} />
+ } />
} />
} />
} />