Merge pull request #2392 from automatisch/templates-ui
feat(web): introduce templates
This commit is contained in:
@@ -11,6 +11,7 @@ export default async (request, response) => {
|
|||||||
|
|
||||||
const configParams = (request) => {
|
const configParams = (request) => {
|
||||||
const {
|
const {
|
||||||
|
enableTemplates,
|
||||||
enableFooter,
|
enableFooter,
|
||||||
footerBackgroundColor,
|
footerBackgroundColor,
|
||||||
footerCopyrightText,
|
footerCopyrightText,
|
||||||
@@ -28,6 +29,7 @@ const configParams = (request) => {
|
|||||||
} = request.body;
|
} = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
enableTemplates,
|
||||||
enableFooter,
|
enableFooter,
|
||||||
footerBackgroundColor,
|
footerBackgroundColor,
|
||||||
footerCopyrightText,
|
footerCopyrightText,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('PATCH /api/v1/admin/templates/:templateId', () => {
|
|||||||
|
|
||||||
const expectedPayload = await updateTemplateMock({
|
const expectedPayload = await updateTemplateMock({
|
||||||
...refetchedTemplate,
|
...refetchedTemplate,
|
||||||
|
flowData: refetchedTemplate.getFlowDataWithIconUrls(),
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const adminTemplateSerializer = (template) => {
|
|||||||
return {
|
return {
|
||||||
id: template.id,
|
id: template.id,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
flowData: template.flowData,
|
flowData: template.getFlowDataWithIconUrls(),
|
||||||
createdAt: template.createdAt.getTime(),
|
createdAt: template.createdAt.getTime(),
|
||||||
updatedAt: template.updatedAt.getTime(),
|
updatedAt: template.updatedAt.getTime(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe('adminTemplateSerializer', () => {
|
|||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
id: template.id,
|
id: template.id,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
flowData: template.flowData,
|
flowData: template.getFlowDataWithIconUrls(),
|
||||||
createdAt: template.createdAt.getTime(),
|
createdAt: template.createdAt.getTime(),
|
||||||
updatedAt: template.updatedAt.getTime(),
|
updatedAt: template.updatedAt.getTime(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const createTemplateMock = async (template) => {
|
|||||||
name: template.name,
|
name: template.name,
|
||||||
createdAt: template.createdAt.getTime(),
|
createdAt: template.createdAt.getTime(),
|
||||||
updatedAt: template.updatedAt.getTime(),
|
updatedAt: template.updatedAt.getTime(),
|
||||||
flowData: template.flowData,
|
flowData: template.getFlowDataWithIconUrls(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const getTemplateMock = async (template) => {
|
|||||||
name: template.name,
|
name: template.name,
|
||||||
createdAt: template.createdAt.getTime(),
|
createdAt: template.createdAt.getTime(),
|
||||||
updatedAt: template.updatedAt.getTime(),
|
updatedAt: template.updatedAt.getTime(),
|
||||||
flowData: template.flowData,
|
flowData: template.getFlowDataWithIconUrls(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const getTemplatesMock = async (templates) => {
|
|||||||
name: template.name,
|
name: template.name,
|
||||||
createdAt: template.createdAt.getTime(),
|
createdAt: template.createdAt.getTime(),
|
||||||
updatedAt: template.updatedAt.getTime(),
|
updatedAt: template.updatedAt.getTime(),
|
||||||
flowData: template.flowData,
|
flowData: template.getFlowDataWithIconUrls(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import * as URLS from 'config/urls';
|
|||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import AdminApplications from 'pages/AdminApplications';
|
import AdminApplications from 'pages/AdminApplications';
|
||||||
import AdminApplication from 'pages/AdminApplication';
|
import AdminApplication from 'pages/AdminApplication';
|
||||||
|
import AdminTemplates from 'pages/AdminTemplates';
|
||||||
|
import AdminCreateTemplate from 'pages/AdminCreateTemplate';
|
||||||
|
import AdminUpdateTemplate from 'pages/AdminUpdateTemplate';
|
||||||
|
|
||||||
// TODO: consider introducing redirections to `/` as fallback
|
// TODO: consider introducing redirections to `/` as fallback
|
||||||
export default (
|
export default (
|
||||||
<>
|
<>
|
||||||
@@ -109,6 +113,33 @@ export default (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={`${URLS.ADMIN_TEMPLATES}/*`}
|
||||||
|
element={
|
||||||
|
<Can I="update" a="Config">
|
||||||
|
<AdminTemplates />
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={`${URLS.ADMIN_CREATE_TEMPLATE_PATTERN}/*`}
|
||||||
|
element={
|
||||||
|
<Can I="update" a="Config">
|
||||||
|
<AdminCreateTemplate />
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={`${URLS.ADMIN_UPDATE_TEMPLATE_PATTERN}/*`}
|
||||||
|
element={
|
||||||
|
<Can I="update" a="Config">
|
||||||
|
<AdminUpdateTemplate />
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={URLS.ADMIN_SETTINGS}
|
path={URLS.ADMIN_SETTINGS}
|
||||||
element={<Navigate to={URLS.USERS} replace />}
|
element={<Navigate to={URLS.USERS} replace />}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
|||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
|
|
||||||
function createDrawerLinks({
|
function createDrawerLinks({
|
||||||
|
canCreateFlows,
|
||||||
canReadRole,
|
canReadRole,
|
||||||
canReadUser,
|
canReadUser,
|
||||||
canUpdateConfig,
|
canUpdateConfig,
|
||||||
@@ -69,7 +70,16 @@ function createDrawerLinks({
|
|||||||
dataTest: 'apps-drawer-link',
|
dataTest: 'apps-drawer-link',
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
canUpdateConfig
|
||||||
|
? {
|
||||||
|
Icon: AppsIcon,
|
||||||
|
primary: 'adminSettingsDrawer.templates',
|
||||||
|
to: URLS.ADMIN_TEMPLATES,
|
||||||
|
dataTest: 'templates-drawer-link',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +91,9 @@ function SettingsLayout() {
|
|||||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||||
const openDrawer = () => setDrawerOpen(true);
|
const openDrawer = () => setDrawerOpen(true);
|
||||||
const closeDrawer = () => setDrawerOpen(false);
|
const closeDrawer = () => setDrawerOpen(false);
|
||||||
|
|
||||||
const drawerLinks = createDrawerLinks({
|
const drawerLinks = createDrawerLinks({
|
||||||
|
canCreateFlows: currentUserAbility.can('create', 'Flow'),
|
||||||
canReadUser: currentUserAbility.can('read', 'User'),
|
canReadUser: currentUserAbility.can('read', 'User'),
|
||||||
canReadRole: currentUserAbility.can('read', 'Role'),
|
canReadRole: currentUserAbility.can('read', 'Role'),
|
||||||
canUpdateConfig: currentUserAbility.can('update', 'Config'),
|
canUpdateConfig: currentUserAbility.can('update', 'Config'),
|
||||||
@@ -91,6 +103,7 @@ function SettingsLayout() {
|
|||||||
currentUserAbility.can('create', 'SamlAuthProvider'),
|
currentUserAbility.can('create', 'SamlAuthProvider'),
|
||||||
canUpdateApp: currentUserAbility.can('update', 'App'),
|
canUpdateApp: currentUserAbility.can('update', 'App'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const drawerBottomLinks = [
|
const drawerBottomLinks = [
|
||||||
{
|
{
|
||||||
Icon: ArrowBackIosNewIcon,
|
Icon: ArrowBackIosNewIcon,
|
||||||
@@ -99,6 +112,7 @@ function SettingsLayout() {
|
|||||||
dataTest: 'go-back-drawer-link',
|
dataTest: 'go-back-drawer-link',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Can I="read" a="User">
|
<Can I="read" a="User">
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -106,6 +120,7 @@ function SettingsLayout() {
|
|||||||
onDrawerOpen={openDrawer}
|
onDrawerOpen={openDrawer}
|
||||||
onDrawerClose={closeDrawer}
|
onDrawerClose={closeDrawer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flex: 1 }}>
|
<Box sx={{ display: 'flex', flex: 1 }}>
|
||||||
<Drawer
|
<Drawer
|
||||||
links={drawerLinks}
|
links={drawerLinks}
|
||||||
@@ -114,6 +129,7 @@ function SettingsLayout() {
|
|||||||
onOpen={openDrawer}
|
onOpen={openDrawer}
|
||||||
onClose={closeDrawer}
|
onClose={closeDrawer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack sx={{ flex: 1 }}>
|
<Stack sx={{ flex: 1 }}>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Can from 'components/Can';
|
||||||
|
import FlowFolderChangeDialog from 'components/FlowFolderChangeDialog';
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import useAdminDeleteTemplate from 'hooks/useAdminDeleteTemplate.ee';
|
||||||
|
import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile';
|
||||||
|
import useDuplicateFlow from 'hooks/useDuplicateFlow';
|
||||||
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
import useExportFlow from 'hooks/useExportFlow';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useIsCurrentUserAdmin from 'hooks/useIsCurrentUserAdmin';
|
||||||
|
|
||||||
|
function AdminTemplateContextMenu(props) {
|
||||||
|
const { templateId, onClose, anchorEl } = props;
|
||||||
|
|
||||||
|
const [showFlowFolderChangeDialog, setShowFlowFolderChangeDialog] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const isCurrentUserAdmin = useIsCurrentUserAdmin();
|
||||||
|
const { mutateAsync: deleteTemplate } = useAdminDeleteTemplate(templateId);
|
||||||
|
|
||||||
|
const onTemplateDelete = React.useCallback(async () => {
|
||||||
|
await deleteTemplate();
|
||||||
|
|
||||||
|
enqueueSnackbar(
|
||||||
|
formatMessage('adminTemplateContextMenu.successfullyDeleted'),
|
||||||
|
{
|
||||||
|
variant: 'success',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}, [deleteTemplate, enqueueSnackbar, formatMessage, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
open={true}
|
||||||
|
onClose={onClose}
|
||||||
|
hideBackdrop={false}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
>
|
||||||
|
<Can I="delete" a="Flow" passThrough>
|
||||||
|
{(allowed) => (
|
||||||
|
<MenuItem disabled={!allowed} onClick={onTemplateDelete}>
|
||||||
|
{formatMessage('adminTemplateContextMenu.delete')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Can>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminTemplateContextMenu.propTypes = {
|
||||||
|
templateId: PropTypes.string.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
anchorEl: PropTypes.oneOfType([
|
||||||
|
PropTypes.func,
|
||||||
|
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||||
|
]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminTemplateContextMenu;
|
||||||
@@ -2,10 +2,8 @@ import Menu from '@mui/material/Menu';
|
|||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import FlowFolderChangeDialog from 'components/FlowFolderChangeDialog';
|
import FlowFolderChangeDialog from 'components/FlowFolderChangeDialog';
|
||||||
@@ -13,16 +11,22 @@ import * as URLS from 'config/urls';
|
|||||||
import useDeleteFlow from 'hooks/useDeleteFlow';
|
import useDeleteFlow from 'hooks/useDeleteFlow';
|
||||||
import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile';
|
import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile';
|
||||||
import useDuplicateFlow from 'hooks/useDuplicateFlow';
|
import useDuplicateFlow from 'hooks/useDuplicateFlow';
|
||||||
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import useExportFlow from 'hooks/useExportFlow';
|
import useExportFlow from 'hooks/useExportFlow';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useIsCurrentUserAdmin from 'hooks/useIsCurrentUserAdmin';
|
||||||
|
|
||||||
function ContextMenu(props) {
|
function ContextMenu(props) {
|
||||||
const { flowId, onClose, anchorEl, onDuplicateFlow, appKey } = props;
|
const { flowId, onClose, anchorEl, onDuplicateFlow, appKey } = props;
|
||||||
|
|
||||||
const [showFlowFolderChangeDialog, setShowFlowFolderChangeDialog] =
|
const [showFlowFolderChangeDialog, setShowFlowFolderChangeDialog] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const isCurrentUserAdmin = useIsCurrentUserAdmin();
|
||||||
const { mutateAsync: duplicateFlow } = useDuplicateFlow(flowId);
|
const { mutateAsync: duplicateFlow } = useDuplicateFlow(flowId);
|
||||||
const { mutateAsync: deleteFlow } = useDeleteFlow(flowId);
|
const { mutateAsync: deleteFlow } = useDeleteFlow(flowId);
|
||||||
const { mutateAsync: exportFlow } = useExportFlow(flowId);
|
const { mutateAsync: exportFlow } = useExportFlow(flowId);
|
||||||
@@ -56,6 +60,10 @@ function ContextMenu(props) {
|
|||||||
formatMessage,
|
formatMessage,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const onCreateTemplate = React.useCallback(async () => {
|
||||||
|
navigate(URLS.ADMIN_CREATE_TEMPLATE(flowId));
|
||||||
|
}, [flowId]);
|
||||||
|
|
||||||
const onFlowDelete = React.useCallback(async () => {
|
const onFlowDelete = React.useCallback(async () => {
|
||||||
await deleteFlow();
|
await deleteFlow();
|
||||||
|
|
||||||
@@ -126,6 +134,16 @@ function ContextMenu(props) {
|
|||||||
)}
|
)}
|
||||||
</Can>
|
</Can>
|
||||||
|
|
||||||
|
{isCurrentUserAdmin && (
|
||||||
|
<Can I="create" a="Flow" passThrough>
|
||||||
|
{(allowed) => (
|
||||||
|
<MenuItem disabled={!allowed} onClick={onCreateTemplate}>
|
||||||
|
{formatMessage('flow.createTemplateFromFlow')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Can>
|
||||||
|
)}
|
||||||
|
|
||||||
<Can I="update" a="Flow" passThrough>
|
<Can I="update" a="Flow" passThrough>
|
||||||
{(allowed) => (
|
{(allowed) => (
|
||||||
<MenuItem disabled={!allowed} onClick={onFlowFolderUpdate}>
|
<MenuItem disabled={!allowed} onClick={onFlowFolderUpdate}>
|
||||||
|
|||||||
110
packages/web/src/components/FlowsButtons/index.jsx
Normal file
110
packages/web/src/components/FlowsButtons/index.jsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import UploadIcon from '@mui/icons-material/Upload';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Pagination from '@mui/material/Pagination';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
|
import PaginationItem from '@mui/material/PaginationItem';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useNavigate,
|
||||||
|
useSearchParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import Can from 'components/Can';
|
||||||
|
import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import FlowRow from 'components/FlowRow';
|
||||||
|
import Folders from 'components/Folders';
|
||||||
|
import ImportFlowDialog from 'components/ImportFlowDialog';
|
||||||
|
import SplitButton from 'components/SplitButton';
|
||||||
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import SearchInput from 'components/SearchInput';
|
||||||
|
import TemplatesDialog from 'components/TemplatesDialog/index.ee';
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
|
import useFlows from 'hooks/useFlows';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||||
|
|
||||||
|
export default function FlowsButtons() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
const theme = useTheme();
|
||||||
|
const { data: config } = useAutomatischConfig();
|
||||||
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const canCreateFlow = currentUserAbility.can('create', 'Flow');
|
||||||
|
const enableTemplates = config?.data.enableTemplates === true;
|
||||||
|
|
||||||
|
const createFlowButtonData = {
|
||||||
|
label: formatMessage('flows.createFlow'),
|
||||||
|
key: 'createFlow',
|
||||||
|
'data-test': 'create-flow-button',
|
||||||
|
to: URLS.CREATE_FLOW,
|
||||||
|
startIcon: <AddIcon />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFlowFromTemplateButtonData = {
|
||||||
|
label: formatMessage('flows.createFlowFromTemplate'),
|
||||||
|
key: 'createFlowFromTemplate',
|
||||||
|
'data-test': 'create-flow-from-template-button',
|
||||||
|
hide: !enableTemplates,
|
||||||
|
to: URLS.VIEW_TEMPLATES,
|
||||||
|
};
|
||||||
|
|
||||||
|
const importFlowButtonData = {
|
||||||
|
label: formatMessage('flows.importFlow'),
|
||||||
|
key: 'importFlow',
|
||||||
|
'data-test': 'import-flow-button',
|
||||||
|
to: URLS.IMPORT_FLOW,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (matchSmallScreens) {
|
||||||
|
const connectionOptions = [
|
||||||
|
createFlowButtonData,
|
||||||
|
createFlowFromTemplateButtonData,
|
||||||
|
importFlowButtonData,
|
||||||
|
].filter((option) => !option.hide);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SplitButton disabled={!canCreateFlow} options={connectionOptions} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outlined"
|
||||||
|
color="info"
|
||||||
|
size="large"
|
||||||
|
component={Link}
|
||||||
|
disabled={!canCreateFlow}
|
||||||
|
startIcon={<UploadIcon />}
|
||||||
|
to={URLS.IMPORT_FLOW}
|
||||||
|
data-test="import-flow-button"
|
||||||
|
>
|
||||||
|
{formatMessage('flows.importFlow')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SplitButton
|
||||||
|
disabled={!canCreateFlow}
|
||||||
|
options={[
|
||||||
|
createFlowButtonData,
|
||||||
|
createFlowFromTemplateButtonData,
|
||||||
|
].filter((option) => !option.hide)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
import MuiCardContent from '@mui/material/CardContent';
|
import MuiCardContent from '@mui/material/CardContent';
|
||||||
|
|
||||||
export const CardContent = styled(MuiCardContent)`
|
export const CardContent = styled(MuiCardContent)`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ export default function SplitButton(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (event) => {
|
const handleClose = (event) => {
|
||||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +40,7 @@ export default function SplitButton(props) {
|
|||||||
data-test={selectedOption['data-test']}
|
data-test={selectedOption['data-test']}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={selectedOption.to}
|
to={selectedOption.to}
|
||||||
|
startIcon={selectedOption.startIcon}
|
||||||
sx={{
|
sx={{
|
||||||
// Link component causes style loss in ButtonGroup
|
// Link component causes style loss in ButtonGroup
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
@@ -105,7 +103,8 @@ SplitButton.propTypes = {
|
|||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
'data-test': PropTypes.string.isRequired,
|
'data-test': PropTypes.string.isRequired,
|
||||||
to: PropTypes.string.isRequired,
|
to: PropTypes.string.isRequired,
|
||||||
disabled: PropTypes.bool.isRequired,
|
disabled: PropTypes.bool,
|
||||||
|
startIcon: PropTypes.node,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
).isRequired,
|
).isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
|
|
||||||
|
import FlowAppIcons from 'components/FlowAppIcons';
|
||||||
|
import AdminTemplateContextMenu from 'components/AdminTemplateContextMenu';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import { Apps, CardContent, ContextMenu, Title, Typography } from './style';
|
||||||
|
import { FlowPropType } from 'propTypes/propTypes';
|
||||||
|
import useIsCurrentUserAdmin from 'hooks/useIsCurrentUserAdmin';
|
||||||
|
|
||||||
|
function TemplateItem(props) {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const contextButtonRef = React.useRef(null);
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
|
const isCurrentUserAdmin = useIsCurrentUserAdmin();
|
||||||
|
const { template, to } = props;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContextMenuClick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
setAnchorEl(contextButtonRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card sx={{ mb: 1 }} data-test="template-row">
|
||||||
|
<CardActionArea component={Link} to={to} data-test="card-action-area">
|
||||||
|
<CardContent>
|
||||||
|
<Apps direction="row" gap={1} sx={{ gridArea: 'apps' }}>
|
||||||
|
<FlowAppIcons steps={template.flowData.steps} />
|
||||||
|
</Apps>
|
||||||
|
|
||||||
|
<Title
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="flex-start"
|
||||||
|
spacing={1}
|
||||||
|
sx={{ gridArea: 'title' }}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" noWrap>
|
||||||
|
{template?.name}
|
||||||
|
</Typography>
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{isCurrentUserAdmin && (
|
||||||
|
<ContextMenu>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open context menu"
|
||||||
|
ref={contextButtonRef}
|
||||||
|
onClick={onContextMenuClick}
|
||||||
|
>
|
||||||
|
<MoreHorizIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ContextMenu>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{anchorEl && (
|
||||||
|
<AdminTemplateContextMenu
|
||||||
|
templateId={template.id}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateItem.propTypes = {
|
||||||
|
template: FlowPropType.isRequired,
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateItem;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import MuiStack from '@mui/material/Stack';
|
||||||
|
import MuiBox from '@mui/material/Box';
|
||||||
|
import MuiCardContent from '@mui/material/CardContent';
|
||||||
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateRows: 'auto',
|
||||||
|
gridTemplateColumns: 'calc(30px * 3 + 8px * 2) minmax(0, auto) min-content',
|
||||||
|
gridGap: theme.spacing(2),
|
||||||
|
gridTemplateAreas: `
|
||||||
|
"apps title menu"
|
||||||
|
`,
|
||||||
|
alignItems: 'center',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
gridTemplateAreas: `
|
||||||
|
"apps menu"
|
||||||
|
"title menu"
|
||||||
|
`,
|
||||||
|
gridTemplateColumns: 'minmax(0, auto) min-content',
|
||||||
|
gridTemplateRows: 'auto auto',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
export const Apps = styled(MuiStack)(() => ({
|
||||||
|
gridArea: 'apps',
|
||||||
|
}));
|
||||||
|
export const Title = styled(MuiStack)(() => ({
|
||||||
|
gridArea: 'title',
|
||||||
|
}));
|
||||||
|
export const ContextMenu = styled(MuiBox)(({ theme }) => ({
|
||||||
|
flexDirection: 'row',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(0.625),
|
||||||
|
gridArea: 'menu',
|
||||||
|
}));
|
||||||
|
export const Typography = styled(MuiTypography)(() => ({
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '85%',
|
||||||
|
}));
|
||||||
|
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}));
|
||||||
70
packages/web/src/components/TemplatesDialog/index.ee.jsx
Normal file
70
packages/web/src/components/TemplatesDialog/index.ee.jsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import { getUnifiedErrorMessage } from 'helpers/errors';
|
||||||
|
import useTemplates from 'hooks/useTemplates.ee';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import TemplateItem from './TemplateItem/TemplateItem.ee';
|
||||||
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
|
||||||
|
export default function TemplatesDialog(props) {
|
||||||
|
const { open = true } = props;
|
||||||
|
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { data: templates } = useTemplates();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate('..');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={handleClose} data-test="templates-dialog">
|
||||||
|
<DialogTitle>{formatMessage('templatesDialog.title')}</DialogTitle>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
color: (theme) => theme.palette.grey[500],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText mb={2}>
|
||||||
|
{templates?.meta.count !== 0 &&
|
||||||
|
formatMessage('templatesDialog.description')}
|
||||||
|
|
||||||
|
{templates?.meta.count === 0 &&
|
||||||
|
formatMessage('adminTemplatesPage.noResult')}
|
||||||
|
</DialogContentText>
|
||||||
|
|
||||||
|
{templates?.data.map((template) => (
|
||||||
|
<TemplateItem
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
to={URLS.CREATE_FLOW_FROM_TEMPLATE(template.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ export const FOLDER_FLOWS = (folderId) => `/flows?folderId=${folderId}`;
|
|||||||
export const APP_FLOWS_PATTERN = '/app/:appKey/flows';
|
export const APP_FLOWS_PATTERN = '/app/:appKey/flows';
|
||||||
export const EDITOR = '/editor';
|
export const EDITOR = '/editor';
|
||||||
export const CREATE_FLOW = '/editor/create';
|
export const CREATE_FLOW = '/editor/create';
|
||||||
|
export const VIEW_TEMPLATES = '/flows/templates';
|
||||||
export const IMPORT_FLOW = '/flows/import';
|
export const IMPORT_FLOW = '/flows/import';
|
||||||
export const FLOW_EDITOR = (flowId) => `/editor/${flowId}`;
|
export const FLOW_EDITOR = (flowId) => `/editor/${flowId}`;
|
||||||
export const FLOWS = '/flows';
|
export const FLOWS = '/flows';
|
||||||
@@ -84,6 +85,12 @@ export const ADMIN_APP_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey`;
|
|||||||
export const ADMIN_APP_SETTINGS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/settings`;
|
export const ADMIN_APP_SETTINGS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/settings`;
|
||||||
export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/oauth-clients`;
|
export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/oauth-clients`;
|
||||||
export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`;
|
export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`;
|
||||||
|
export const ADMIN_TEMPLATES = `${ADMIN_SETTINGS}/templates`;
|
||||||
|
export const ADMIN_CREATE_TEMPLATE_PATTERN = `${ADMIN_SETTINGS}/templates/create/:flowId`;
|
||||||
|
export const ADMIN_UPDATE_TEMPLATE_PATTERN = `${ADMIN_SETTINGS}/templates/update/:templateId`;
|
||||||
|
|
||||||
|
export const CREATE_FLOW_FROM_TEMPLATE = (templateId) =>
|
||||||
|
`/editor/create?templateId=${templateId}`;
|
||||||
|
|
||||||
export const ADMIN_APP_CONNECTIONS = (appKey) =>
|
export const ADMIN_APP_CONNECTIONS = (appKey) =>
|
||||||
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
|
||||||
@@ -100,6 +107,12 @@ export const ADMIN_APP_AUTH_CLIENT = (appKey, id) =>
|
|||||||
export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey) =>
|
export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey) =>
|
||||||
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/create`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/create`;
|
||||||
|
|
||||||
|
export const ADMIN_CREATE_TEMPLATE = (flowId) =>
|
||||||
|
`${ADMIN_SETTINGS}/templates/create/${flowId}`;
|
||||||
|
|
||||||
|
export const ADMIN_UPDATE_TEMPLATE = (templateId) =>
|
||||||
|
`${ADMIN_SETTINGS}/templates/update/${templateId}`;
|
||||||
|
|
||||||
export const DASHBOARD = FLOWS;
|
export const DASHBOARD = FLOWS;
|
||||||
|
|
||||||
// External links and paths
|
// External links and paths
|
||||||
|
|||||||
15
packages/web/src/hooks/useAdminCreateTemplate.ee.js
Normal file
15
packages/web/src/hooks/useAdminCreateTemplate.ee.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminCreateTemplate() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async ({ flowId, name }) => {
|
||||||
|
const { data } = await api.post(`/v1/admin/templates`, { flowId, name });
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
23
packages/web/src/hooks/useAdminDeleteTemplate.ee.js
Normal file
23
packages/web/src/hooks/useAdminDeleteTemplate.ee.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminDeleteTemplate(templateId) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const { data } = await api.delete(`/v1/admin/templates/${templateId}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin', 'templates'],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
17
packages/web/src/hooks/useAdminTemplate.ee.js
Normal file
17
packages/web/src/hooks/useAdminTemplate.ee.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminTemplate(templateId) {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['admin', 'templates', templateId],
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const { data } = await api.get(`/v1/admin/templates/${templateId}`, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
17
packages/web/src/hooks/useAdminTemplates.ee.js
Normal file
17
packages/web/src/hooks/useAdminTemplates.ee.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminTemplates() {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['admin', 'templates'],
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const { data } = await api.get('/v1/admin/templates', {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import api from 'helpers/api';
|
import api from 'helpers/api';
|
||||||
|
|
||||||
export default function useAdminUpdateConfig(appKey) {
|
export default function useAdminUpdateConfig() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const query = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (payload) => {
|
mutationFn: async (payload) => {
|
||||||
const { data } = await api.patch('/v1/admin/config', payload);
|
const { data } = await api.patch('/v1/admin/config', payload);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ['automatisch', 'config'],
|
queryKey: ['automatisch', 'config'],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return query;
|
return mutation;
|
||||||
}
|
}
|
||||||
|
|||||||
17
packages/web/src/hooks/useAdminUpdateTemplate.ee.js
Normal file
17
packages/web/src/hooks/useAdminUpdateTemplate.ee.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminUpdateTemplate(templateId) {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async ({ name }) => {
|
||||||
|
const { data } = await api.patch(`/v1/admin/templates/${templateId}`, {
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
@@ -5,19 +5,21 @@ import api from 'helpers/api';
|
|||||||
export default function useCreateFlow() {
|
export default function useCreateFlow() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const query = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async ({ templateId }) => {
|
||||||
const { data } = await api.post('/v1/flows');
|
const { data } = await api.post('/v1/flows', null, {
|
||||||
|
params: { templateId },
|
||||||
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ['flows'],
|
queryKey: ['flows'],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return query;
|
return mutation;
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/web/src/hooks/useIsCurrentUserAdmin.js
Normal file
8
packages/web/src/hooks/useIsCurrentUserAdmin.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import userAbility from 'helpers/userAbility';
|
||||||
|
import useCurrentUser from 'hooks/useCurrentUser';
|
||||||
|
|
||||||
|
export default function useIsCurrentUserAdmin() {
|
||||||
|
const { data: currentUser } = useCurrentUser();
|
||||||
|
|
||||||
|
return currentUser?.data.role.isAdmin === true;
|
||||||
|
}
|
||||||
17
packages/web/src/hooks/useTemplates.ee.js
Normal file
17
packages/web/src/hooks/useTemplates.ee.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useTemplates() {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['templates'],
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const { data } = await api.get('/v1/templates', {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"adminSettingsDrawer.userInterface": "User Interface",
|
"adminSettingsDrawer.userInterface": "User Interface",
|
||||||
"adminSettingsDrawer.goBack": "Go to the dashboard",
|
"adminSettingsDrawer.goBack": "Go to the dashboard",
|
||||||
"adminSettingsDrawer.apps": "Applications",
|
"adminSettingsDrawer.apps": "Applications",
|
||||||
|
"adminSettingsDrawer.templates": "Templates",
|
||||||
"adminSettingsFooter.version": "Version {version}",
|
"adminSettingsFooter.version": "Version {version}",
|
||||||
"app.connectionCount": "{count} connections",
|
"app.connectionCount": "{count} connections",
|
||||||
"app.flowCount": "{count} flows",
|
"app.flowCount": "{count} flows",
|
||||||
@@ -89,10 +90,12 @@
|
|||||||
"flow.moveTo": "Move to",
|
"flow.moveTo": "Move to",
|
||||||
"flow.delete": "Delete",
|
"flow.delete": "Delete",
|
||||||
"flow.export": "Export",
|
"flow.export": "Export",
|
||||||
|
"flow.createTemplateFromFlow": "Use as template",
|
||||||
"flowStep.triggerType": "Trigger",
|
"flowStep.triggerType": "Trigger",
|
||||||
"flowStep.actionType": "Action",
|
"flowStep.actionType": "Action",
|
||||||
"flows.create": "Create flow",
|
"flows.createFlow": "Create flow",
|
||||||
"flows.import": "Import flow",
|
"flows.createFlowFromTemplate": "Create from template",
|
||||||
|
"flows.importFlow": "Import flow",
|
||||||
"flows.title": "Flows",
|
"flows.title": "Flows",
|
||||||
"flows.noFlows": "You don't have any flows yet.",
|
"flows.noFlows": "You don't have any flows yet.",
|
||||||
"flowEditor.goBack": "Go back to flows",
|
"flowEditor.goBack": "Go back to flows",
|
||||||
@@ -370,5 +373,17 @@
|
|||||||
"footer.docsLinkText": "Documentation",
|
"footer.docsLinkText": "Documentation",
|
||||||
"footer.tosLinkText": "Terms of Service",
|
"footer.tosLinkText": "Terms of Service",
|
||||||
"footer.privacyPolicyLinkText": "Privacy",
|
"footer.privacyPolicyLinkText": "Privacy",
|
||||||
"footer.imprintLinkText": "Imprint"
|
"footer.imprintLinkText": "Imprint",
|
||||||
|
"templatesDialog.title": "Templates",
|
||||||
|
"templatesDialog.description": "You may choose one of the templates below to create a new flow.",
|
||||||
|
"templatesDialog.close": "Close",
|
||||||
|
"adminTemplatesPage.title": "Templates",
|
||||||
|
"adminTemplatesPage.noResult": "There are currently no templates.",
|
||||||
|
"adminTemplatePage.title": "Template",
|
||||||
|
"adminUpdateTemplate.titleFieldLabel": "Template name",
|
||||||
|
"adminUpdateTemplate.submit": "Update",
|
||||||
|
"adminCreateTemplate.titleFieldLabel": "Template name",
|
||||||
|
"adminCreateTemplate.submit": "Create",
|
||||||
|
"adminTemplateContextMenu.delete": "Delete",
|
||||||
|
"adminTemplateContextMenu.successfullyDeleted": "The template has been deleted."
|
||||||
}
|
}
|
||||||
|
|||||||
72
packages/web/src/pages/AdminCreateTemplate/index.jsx
Normal file
72
packages/web/src/pages/AdminCreateTemplate/index.jsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Skeleton from '@mui/material/Skeleton';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import Form from 'components/Form';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
import useFlow from 'hooks/useFlow';
|
||||||
|
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||||
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAdminCreateTemplate from 'hooks/useAdminCreateTemplate.ee';
|
||||||
|
|
||||||
|
function AdminCreateTemplatePage() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { flowId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { data: flow, isLoading: isTemplateLoading } = useFlow(flowId);
|
||||||
|
|
||||||
|
const { mutateAsync: createTemplate, isPending } = useAdminCreateTemplate();
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data) => {
|
||||||
|
await createTemplate({
|
||||||
|
name: data.name,
|
||||||
|
flowId: flowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(URLS.ADMIN_TEMPLATES);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
|
<Grid container item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
|
<PageTitle>{formatMessage('adminTemplatePage.title')}</PageTitle>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sx={{ pt: 5, pb: 5 }}>
|
||||||
|
<Stack spacing={5}></Stack>
|
||||||
|
{!isTemplateLoading && (
|
||||||
|
<Form onSubmit={handleFormSubmit} defaultValues={flow.data}>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
<TextField
|
||||||
|
name="name"
|
||||||
|
label={formatMessage('adminCreateTemplate.titleFieldLabel')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2 }}
|
||||||
|
loading={isPending}
|
||||||
|
data-test="update-button"
|
||||||
|
>
|
||||||
|
{formatMessage('adminCreateTemplate.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</Stack>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default AdminCreateTemplatePage;
|
||||||
103
packages/web/src/pages/AdminTemplates/index.jsx
Normal file
103
packages/web/src/pages/AdminTemplates/index.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
|
||||||
|
import Switch from 'components/Switch';
|
||||||
|
import Form from 'components/Form';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import SearchInput from 'components/SearchInput';
|
||||||
|
import TemplateItem from 'components/TemplatesDialog/TemplateItem/TemplateItem.ee.jsx';
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAdminTemplates from 'hooks/useAdminTemplates.ee';
|
||||||
|
import useAdminUpdateConfig from 'hooks/useAdminUpdateConfig';
|
||||||
|
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||||
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
|
||||||
|
function AdminTemplates() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const [templateName, setTemplateName] = React.useState('');
|
||||||
|
|
||||||
|
const { data: config } = useAutomatischConfig();
|
||||||
|
|
||||||
|
const { data: templates, isLoading: isTemplatesLoading } = useAdminTemplates({
|
||||||
|
name: templateName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateConfig, isPending: isUpdateConfigPending } =
|
||||||
|
useAdminUpdateConfig();
|
||||||
|
|
||||||
|
const onSearchChange = React.useCallback((event) => {
|
||||||
|
setTemplateName(event.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChangeOnFeatureToggle = async (event) => {
|
||||||
|
const value = event.target.checked;
|
||||||
|
|
||||||
|
await updateConfig({ enableTemplates: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
|
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
||||||
|
<Grid container item xs sm alignItems="center" order={{ xs: 0 }}>
|
||||||
|
<PageTitle>{formatMessage('adminTemplatesPage.title')}</PageTitle>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||||
|
<SearchInput onChange={onSearchChange} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sx={{ mb: 3 }}>
|
||||||
|
<Form
|
||||||
|
defaultValues={{ enableTemplates: config?.data.enableTemplates }}
|
||||||
|
noValidate
|
||||||
|
automaticValidation={false}
|
||||||
|
render={({ formState: { errors, isDirty } }) => (
|
||||||
|
<Switch
|
||||||
|
name="enableTemplates"
|
||||||
|
disabled={isUpdateConfigPending}
|
||||||
|
onChange={handleChangeOnFeatureToggle}
|
||||||
|
label={formatMessage('authenticationForm.active')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{isTemplatesLoading && (
|
||||||
|
<CircularProgress
|
||||||
|
data-test="templates-loader"
|
||||||
|
sx={{ display: 'block', margin: '20px auto' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
{!isTemplatesLoading &&
|
||||||
|
templates?.data?.map((template) => (
|
||||||
|
<TemplateItem
|
||||||
|
key={template.name}
|
||||||
|
template={template}
|
||||||
|
to={URLS.ADMIN_UPDATE_TEMPLATE(template.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!isTemplatesLoading && templates?.meta.count === 0 && (
|
||||||
|
<NoResultFound
|
||||||
|
text={formatMessage('adminTemplatesPage.noResult')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminTemplates;
|
||||||
67
packages/web/src/pages/AdminUpdateTemplate/index.jsx
Normal file
67
packages/web/src/pages/AdminUpdateTemplate/index.jsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Skeleton from '@mui/material/Skeleton';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import Form from 'components/Form';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
import useAdminTemplate from 'hooks/useAdminTemplate.ee';
|
||||||
|
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||||
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAdminUpdateTemplate from 'hooks/useAdminUpdateTemplate.ee';
|
||||||
|
|
||||||
|
function AdminUpdateTemplatePage() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { templateId } = useParams();
|
||||||
|
|
||||||
|
const { data: template, isLoading: isTemplateLoading } =
|
||||||
|
useAdminTemplate(templateId);
|
||||||
|
|
||||||
|
const { mutateAsync: updateTemplate, isPending } =
|
||||||
|
useAdminUpdateTemplate(templateId);
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data) => {
|
||||||
|
updateTemplate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
|
<Grid container item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
|
<PageTitle>{formatMessage('adminTemplatePage.title')}</PageTitle>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sx={{ pt: 5, pb: 5 }}>
|
||||||
|
<Stack spacing={5}></Stack>
|
||||||
|
{!isTemplateLoading && (
|
||||||
|
<Form onSubmit={handleFormSubmit} defaultValues={template.data}>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
<TextField
|
||||||
|
name="name"
|
||||||
|
label={formatMessage('adminUpdateTemplate.titleFieldLabel')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2 }}
|
||||||
|
loading={isPending}
|
||||||
|
data-test="update-button"
|
||||||
|
>
|
||||||
|
{formatMessage('adminUpdateTemplate.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</Stack>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default AdminUpdateTemplatePage;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
@@ -10,21 +10,26 @@ import Box from '@mui/material/Box';
|
|||||||
export default function CreateFlow() {
|
export default function CreateFlow() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { mutateAsync: createFlow, isError } = useCreateFlow();
|
const [searchParams] = useSearchParams();
|
||||||
|
const { mutateAsync: createFlow, isCreateFlowError } = useCreateFlow();
|
||||||
|
|
||||||
|
const navigateToEditor = (flowId) =>
|
||||||
|
navigate(URLS.FLOW_EDITOR(flowId), { replace: true });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function initiate() {
|
async function initiate() {
|
||||||
const response = await createFlow();
|
const templateId = searchParams.get('templateId');
|
||||||
|
const response = await createFlow({ templateId });
|
||||||
|
|
||||||
const flowId = response.data?.id;
|
const flowId = response.data?.id;
|
||||||
|
|
||||||
navigate(URLS.FLOW_EDITOR(flowId), { replace: true });
|
navigateToEditor(flowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
initiate();
|
initiate();
|
||||||
}, [createFlow, navigate]);
|
}, [createFlow, navigate, searchParams]);
|
||||||
|
|
||||||
if (isError) {
|
if (isCreateFlowError) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +45,7 @@ export default function CreateFlow() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress size={16} thickness={7.5} />
|
<CircularProgress size={16} thickness={7.5} />
|
||||||
|
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{formatMessage('createFlow.creating')}
|
{formatMessage('createFlow.creating')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import UploadIcon from '@mui/icons-material/Upload';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Pagination from '@mui/material/Pagination';
|
||||||
|
import PaginationItem from '@mui/material/PaginationItem';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Link,
|
Link,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
Routes,
|
|
||||||
Route,
|
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Grid from '@mui/material/Grid';
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
|
||||||
import UploadIcon from '@mui/icons-material/Upload';
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
|
||||||
import Divider from '@mui/material/Divider';
|
|
||||||
import Pagination from '@mui/material/Pagination';
|
|
||||||
import PaginationItem from '@mui/material/PaginationItem';
|
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import Folders from 'components/Folders';
|
import FlowsButtons from 'components/FlowsButtons';
|
||||||
import FlowRow from 'components/FlowRow';
|
|
||||||
import NoResultFound from 'components/NoResultFound';
|
|
||||||
import ConditionalIconButton from 'components/ConditionalIconButton';
|
import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
|
import FlowRow from 'components/FlowRow';
|
||||||
|
import Folders from 'components/Folders';
|
||||||
|
import ImportFlowDialog from 'components/ImportFlowDialog';
|
||||||
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import SearchInput from 'components/SearchInput';
|
import SearchInput from 'components/SearchInput';
|
||||||
import ImportFlowDialog from 'components/ImportFlowDialog';
|
import TemplatesDialog from 'components/TemplatesDialog/index.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
|
||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
import useFlows from 'hooks/useFlows';
|
import useFlows from 'hooks/useFlows';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
export default function Flows() {
|
export default function Flows() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
@@ -96,7 +98,7 @@ export default function Flows() {
|
|||||||
<PageTitle>{formatMessage('flows.title')}</PageTitle>
|
<PageTitle>{formatMessage('flows.title')}</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
<Grid item xs={12} md="auto" order={{ xs: 2, md: 1 }}>
|
||||||
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -109,51 +111,19 @@ export default function Flows() {
|
|||||||
sm="auto"
|
sm="auto"
|
||||||
gap={1}
|
gap={1}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
order={{ xs: 1, sm: 2 }}
|
order={{ xs: 1 }}
|
||||||
>
|
>
|
||||||
<Can I="create" a="Flow" passThrough>
|
<FlowsButtons />
|
||||||
{(allowed) => (
|
|
||||||
<ConditionalIconButton
|
|
||||||
type="submit"
|
|
||||||
variant="outlined"
|
|
||||||
color="info"
|
|
||||||
size="large"
|
|
||||||
component={Link}
|
|
||||||
disabled={!allowed}
|
|
||||||
icon={<UploadIcon />}
|
|
||||||
to={URLS.IMPORT_FLOW}
|
|
||||||
data-test="import-flow-button"
|
|
||||||
>
|
|
||||||
{formatMessage('flows.import')}
|
|
||||||
</ConditionalIconButton>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
|
|
||||||
<Can I="create" a="Flow" passThrough>
|
|
||||||
{(allowed) => (
|
|
||||||
<ConditionalIconButton
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
component={Link}
|
|
||||||
disabled={!allowed}
|
|
||||||
icon={<AddIcon />}
|
|
||||||
to={URLS.CREATE_FLOW}
|
|
||||||
data-test="create-flow-button"
|
|
||||||
>
|
|
||||||
{formatMessage('flows.create')}
|
|
||||||
</ConditionalIconButton>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||||
|
|
||||||
<Grid container columnSpacing={2}>
|
<Grid container columnSpacing={2} rowSpacing={2}>
|
||||||
<Grid item xs={12} sm={3}>
|
<Grid item xs={12} sm={3}>
|
||||||
<Folders />
|
<Folders />
|
||||||
|
|
||||||
|
<Divider sx={{ mt: { xs: 2 }, display: { sm: 'none' } }} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={9}>
|
<Grid item xs={12} sm={9}>
|
||||||
@@ -205,6 +175,7 @@ export default function Flows() {
|
|||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/import" element={<ImportFlowDialog />} />
|
<Route path="/import" element={<ImportFlowDialog />} />
|
||||||
|
<Route path="/templates" element={<TemplatesDialog />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user