feat(web): add import flow functionality
This commit is contained in:
@@ -9,6 +9,7 @@ function ConditionalIconButton(props) {
|
|||||||
const { icon, ...buttonProps } = props;
|
const { icon, ...buttonProps } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
if (matchSmallScreens) {
|
if (matchSmallScreens) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -24,7 +25,8 @@ function ConditionalIconButton(props) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <Button {...buttonProps} />;
|
|
||||||
|
return <Button {...buttonProps} startIcon={icon} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConditionalIconButton.propTypes = {
|
ConditionalIconButton.propTypes = {
|
||||||
|
|||||||
38
packages/web/src/components/FileUploadInput/index.js
Normal file
38
packages/web/src/components/FileUploadInput/index.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||||
|
|
||||||
|
const VisuallyHiddenInput = styled('input')({
|
||||||
|
clip: 'rect(0 0 0 0)',
|
||||||
|
clipPath: 'inset(50%)',
|
||||||
|
height: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function FileUploadInput(props) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
component="label"
|
||||||
|
role={undefined}
|
||||||
|
variant="contained"
|
||||||
|
tabIndex={-1}
|
||||||
|
startIcon={<AttachFileIcon />}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
|
||||||
|
<VisuallyHiddenInput type="file" onChange={props.onChange} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUploadInput.propTypes = {
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
158
packages/web/src/components/ImportFlowDialog/index.jsx
Normal file
158
packages/web/src/components/ImportFlowDialog/index.jsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
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 Typography from '@mui/material/Typography';
|
||||||
|
import UploadIcon from '@mui/icons-material/Upload';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import FileUploadInput from 'components/FileUploadInput';
|
||||||
|
import useImportFlow from 'hooks/useImportFlow';
|
||||||
|
|
||||||
|
function ImportFlowDialog(props) {
|
||||||
|
const {
|
||||||
|
onClose,
|
||||||
|
open = true,
|
||||||
|
'data-test': dataTest = 'import-flow-dialog',
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [hasParsingError, setParsingError] = React.useState(false);
|
||||||
|
const [selectedFile, setSelectedFile] = React.useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: importFlow,
|
||||||
|
data: importedFlow,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
|
isSuccess,
|
||||||
|
reset,
|
||||||
|
} = useImportFlow();
|
||||||
|
|
||||||
|
const handleFileSelection = (event) => {
|
||||||
|
reset();
|
||||||
|
setParsingError(false);
|
||||||
|
|
||||||
|
const file = event.target.files[0];
|
||||||
|
setSelectedFile(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseFlowFile = (fileContents) => {
|
||||||
|
try {
|
||||||
|
const flowData = JSON.parse(fileContents);
|
||||||
|
|
||||||
|
return flowData;
|
||||||
|
} catch {
|
||||||
|
setParsingError(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportFlow = (event) => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
|
||||||
|
fileReader.onload = async function readFileLoaded(e) {
|
||||||
|
const flowData = parseFlowFile(e.target.result);
|
||||||
|
|
||||||
|
if (flowData) {
|
||||||
|
importFlow(flowData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsText(selectedFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} data-test={dataTest}>
|
||||||
|
<DialogTitle>{formatMessage('importFlowDialog.title')}</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{formatMessage('importFlowDialog.description')}
|
||||||
|
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2} mt={4}>
|
||||||
|
<FileUploadInput
|
||||||
|
onChange={handleFileSelection}
|
||||||
|
data-test="import-flow-dialog-button"
|
||||||
|
>
|
||||||
|
{formatMessage('importFlowDialog.selectFile')}
|
||||||
|
</FileUploadInput>
|
||||||
|
|
||||||
|
{selectedFile && (
|
||||||
|
<Typography>
|
||||||
|
{formatMessage('importFlowDialog.selectedFileInformation', {
|
||||||
|
fileName: selectedFile.name,
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => navigate('..')}
|
||||||
|
data-test="import-flow-dialog-close-button"
|
||||||
|
>
|
||||||
|
{formatMessage('importFlowDialog.close')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleImportFlow}
|
||||||
|
data-test="import-flow-dialog-import-button"
|
||||||
|
startIcon={<UploadIcon />}
|
||||||
|
>
|
||||||
|
{formatMessage('importFlowDialog.import')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
|
||||||
|
{hasParsingError && (
|
||||||
|
<Alert
|
||||||
|
data-test="import-flow-dialog-parsing-error-alert"
|
||||||
|
severity="error"
|
||||||
|
>
|
||||||
|
{formatMessage('importFlowDialog.parsingError')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<Alert
|
||||||
|
data-test="import-flow-dialog-generic-error-alert"
|
||||||
|
severity="error"
|
||||||
|
>
|
||||||
|
{error.data || formatMessage('genericError')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSuccess && (
|
||||||
|
<Alert data-test="import-flow-dialog-success-alert" severity="success">
|
||||||
|
{formatMessage('importFlowDialog.successfullyImportedFlow', {
|
||||||
|
link: (str) => (
|
||||||
|
<Link to={URLS.FLOW(importedFlow.data.id)}>{str}</Link>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportFlowDialog.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
open: PropTypes.bool,
|
||||||
|
'data-test': PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportFlowDialog;
|
||||||
@@ -15,37 +15,47 @@ export const APP = (appKey) => `/app/${appKey}`;
|
|||||||
export const APP_PATTERN = '/app/:appKey';
|
export const APP_PATTERN = '/app/:appKey';
|
||||||
export const APP_CONNECTIONS = (appKey) => `/app/${appKey}/connections`;
|
export const APP_CONNECTIONS = (appKey) => `/app/${appKey}/connections`;
|
||||||
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
||||||
|
|
||||||
export const APP_ADD_CONNECTION = (appKey, shared = false) =>
|
export const APP_ADD_CONNECTION = (appKey, shared = false) =>
|
||||||
`/app/${appKey}/connections/add?shared=${shared}`;
|
`/app/${appKey}/connections/add?shared=${shared}`;
|
||||||
|
|
||||||
export const APP_ADD_CONNECTION_WITH_OAUTH_CLIENT_ID = (
|
export const APP_ADD_CONNECTION_WITH_OAUTH_CLIENT_ID = (
|
||||||
appKey,
|
appKey,
|
||||||
oauthClientId,
|
oauthClientId,
|
||||||
) => `/app/${appKey}/connections/add?oauthClientId=${oauthClientId}`;
|
) => `/app/${appKey}/connections/add?oauthClientId=${oauthClientId}`;
|
||||||
|
|
||||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||||
|
|
||||||
export const APP_RECONNECT_CONNECTION = (
|
export const APP_RECONNECT_CONNECTION = (
|
||||||
appKey,
|
appKey,
|
||||||
connectionId,
|
connectionId,
|
||||||
oauthClientId,
|
oauthClientId,
|
||||||
) => {
|
) => {
|
||||||
const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
|
const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||||
|
|
||||||
if (oauthClientId) {
|
if (oauthClientId) {
|
||||||
return `${path}?oauthClientId=${oauthClientId}`;
|
return `${path}?oauthClientId=${oauthClientId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const APP_RECONNECT_CONNECTION_PATTERN =
|
export const APP_RECONNECT_CONNECTION_PATTERN =
|
||||||
'/app/:appKey/connections/:connectionId/reconnect';
|
'/app/:appKey/connections/:connectionId/reconnect';
|
||||||
export const APP_FLOWS = (appKey) => `/app/${appKey}/flows`;
|
|
||||||
export const APP_FLOWS_FOR_CONNECTION = (appKey, connectionId) =>
|
export const APP_FLOWS_FOR_CONNECTION = (appKey, connectionId) =>
|
||||||
`/app/${appKey}/flows?connectionId=${connectionId}`;
|
`/app/${appKey}/flows?connectionId=${connectionId}`;
|
||||||
|
|
||||||
|
export const APP_FLOWS = (appKey) => `/app/${appKey}/flows`;
|
||||||
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 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';
|
||||||
// TODO: revert this back to /flows/:flowId once we have a proper single flow page
|
// TODO: revert this back to /flows/:flowId once we have a proper single flow page
|
||||||
export const FLOW = (flowId) => `/editor/${flowId}`;
|
export const FLOW = (flowId) => `/editor/${flowId}`;
|
||||||
export const FLOW_PATTERN = '/flows/:flowId';
|
export const FLOWS_PATTERN = '/flows/:flowId';
|
||||||
export const SETTINGS = '/settings';
|
export const SETTINGS = '/settings';
|
||||||
export const SETTINGS_DASHBOARD = SETTINGS;
|
export const SETTINGS_DASHBOARD = SETTINGS;
|
||||||
export const PROFILE = 'profile';
|
export const PROFILE = 'profile';
|
||||||
@@ -73,16 +83,22 @@ 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_APP_CONNECTIONS = (appKey) =>
|
export const ADMIN_APP_CONNECTIONS = (appKey) =>
|
||||||
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
|
||||||
|
|
||||||
export const ADMIN_APP_SETTINGS = (appKey) =>
|
export const ADMIN_APP_SETTINGS = (appKey) =>
|
||||||
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
|
||||||
|
|
||||||
export const ADMIN_APP_AUTH_CLIENTS = (appKey) =>
|
export const ADMIN_APP_AUTH_CLIENTS = (appKey) =>
|
||||||
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients`;
|
||||||
|
|
||||||
export const ADMIN_APP_AUTH_CLIENT = (appKey, id) =>
|
export const ADMIN_APP_AUTH_CLIENT = (appKey, id) =>
|
||||||
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/${id}`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/${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 DASHBOARD = FLOWS;
|
export const DASHBOARD = FLOWS;
|
||||||
|
|
||||||
// External links and paths
|
// External links and paths
|
||||||
|
|||||||
15
packages/web/src/hooks/useImportFlow.js
Normal file
15
packages/web/src/hooks/useImportFlow.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useImportFlow() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async (flowData) => {
|
||||||
|
const { data } = await api.post('/v1/flows/import', flowData);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"app.addConnectionWithOAuthClient": "Add connection with OAuth client",
|
"app.addConnectionWithOAuthClient": "Add connection with OAuth client",
|
||||||
"app.reconnectConnection": "Reconnect connection",
|
"app.reconnectConnection": "Reconnect connection",
|
||||||
"app.createFlow": "Create flow",
|
"app.createFlow": "Create flow",
|
||||||
|
"app.importFlow": "Import flow",
|
||||||
"app.settings": "Settings",
|
"app.settings": "Settings",
|
||||||
"app.connections": "Connections",
|
"app.connections": "Connections",
|
||||||
"app.noConnections": "You don't have any connections yet.",
|
"app.noConnections": "You don't have any connections yet.",
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
"flowStep.triggerType": "Trigger",
|
"flowStep.triggerType": "Trigger",
|
||||||
"flowStep.actionType": "Action",
|
"flowStep.actionType": "Action",
|
||||||
"flows.create": "Create flow",
|
"flows.create": "Create flow",
|
||||||
|
"flows.import": "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",
|
||||||
@@ -317,5 +319,13 @@
|
|||||||
"oauthClient.inputActive": "Active",
|
"oauthClient.inputActive": "Active",
|
||||||
"updateOAuthClient.title": "Update OAuth client",
|
"updateOAuthClient.title": "Update OAuth client",
|
||||||
"notFoundPage.title": "We can't seem to find a page you're looking for.",
|
"notFoundPage.title": "We can't seem to find a page you're looking for.",
|
||||||
"notFoundPage.button": "Back to home page"
|
"notFoundPage.button": "Back to home page",
|
||||||
|
"importFlowDialog.title": "Import flow",
|
||||||
|
"importFlowDialog.description": "You can import a flow by uploading the exported flow file below.",
|
||||||
|
"importFlowDialog.parsingError": "Something has gone wrong with parsing the selected file.",
|
||||||
|
"importFlowDialog.selectFile": "Select file",
|
||||||
|
"importFlowDialog.close": "Close",
|
||||||
|
"importFlowDialog.import": "Import",
|
||||||
|
"importFlowDialog.selectedFileInformation": "Selected file: {fileName}",
|
||||||
|
"importFlowDialog.successfullyImportedFlow": "The flow has been successfully imported. You can view it <link>here</link>."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import * as React from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
|
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
|
|
||||||
export default function Flow() {
|
export default function Flow() {
|
||||||
const { flowId } = useParams();
|
const { flowId } = useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 3 }}>
|
<Box sx={{ py: 3 }}>
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import {
|
||||||
|
Link,
|
||||||
|
useNavigate,
|
||||||
|
useSearchParams,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
} from 'react-router-dom';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import UploadIcon from '@mui/icons-material/Upload';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import Pagination from '@mui/material/Pagination';
|
import Pagination from '@mui/material/Pagination';
|
||||||
@@ -16,6 +23,7 @@ import ConditionalIconButton from 'components/ConditionalIconButton';
|
|||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
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 useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
@@ -98,85 +106,120 @@ export default function Flows() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 3 }}>
|
<>
|
||||||
<Container>
|
<Box sx={{ py: 3 }}>
|
||||||
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
<Container>
|
||||||
<Grid container item xs sm alignItems="center" order={{ xs: 0 }}>
|
|
||||||
<PageTitle>{formatMessage('flows.title')}</PageTitle>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
|
||||||
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
item
|
sx={{ mb: [0, 3] }}
|
||||||
xs="auto"
|
columnSpacing={1.5}
|
||||||
sm="auto"
|
rowSpacing={3}
|
||||||
alignItems="center"
|
|
||||||
order={{ xs: 1, sm: 2 }}
|
|
||||||
>
|
>
|
||||||
<Can I="create" a="Flow" passThrough>
|
<Grid container item xs sm alignItems="center" order={{ xs: 0 }}>
|
||||||
{(allowed) => (
|
<PageTitle>{formatMessage('flows.title')}</PageTitle>
|
||||||
<ConditionalIconButton
|
</Grid>
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
component={Link}
|
|
||||||
fullWidth
|
|
||||||
disabled={!allowed}
|
|
||||||
icon={<AddIcon />}
|
|
||||||
to={URLS.CREATE_FLOW}
|
|
||||||
data-test="create-flow-button"
|
|
||||||
>
|
|
||||||
{formatMessage('flows.create')}
|
|
||||||
</ConditionalIconButton>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||||
{(isLoading || navigateToLastPage) && (
|
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
||||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
</Grid>
|
||||||
)}
|
|
||||||
{!isLoading &&
|
<Grid
|
||||||
flows?.map((flow) => (
|
container
|
||||||
<FlowRow
|
item
|
||||||
key={flow.id}
|
display="flex"
|
||||||
flow={flow}
|
direction="row"
|
||||||
onDuplicateFlow={onDuplicateFlow}
|
xs="auto"
|
||||||
onDeleteFlow={fetchFlows}
|
sm="auto"
|
||||||
/>
|
gap={1}
|
||||||
))}
|
alignItems="center"
|
||||||
{!isLoading && !navigateToLastPage && !hasFlows && (
|
order={{ xs: 1, sm: 2 }}
|
||||||
<NoResultFound
|
>
|
||||||
text={formatMessage('flows.noFlows')}
|
<Can I="create" a="Flow" passThrough>
|
||||||
{...(currentUserAbility.can('create', 'Flow') && {
|
{(allowed) => (
|
||||||
to: URLS.CREATE_FLOW,
|
<ConditionalIconButton
|
||||||
})}
|
type="submit"
|
||||||
/>
|
variant="outlined"
|
||||||
)}
|
color="info"
|
||||||
{!isLoading &&
|
size="large"
|
||||||
!navigateToLastPage &&
|
component={Link}
|
||||||
pageInfo &&
|
disabled={!allowed}
|
||||||
pageInfo.totalPages > 1 && (
|
icon={<UploadIcon />}
|
||||||
<Pagination
|
to={URLS.IMPORT_FLOW}
|
||||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
data-test="import-flow-button"
|
||||||
page={pageInfo?.currentPage}
|
>
|
||||||
count={pageInfo?.totalPages}
|
{formatMessage('flows.import')}
|
||||||
renderItem={(item) => (
|
</ConditionalIconButton>
|
||||||
<PaginationItem
|
)}
|
||||||
component={Link}
|
</Can>
|
||||||
to={getPathWithSearchParams(item.page, flowName)}
|
|
||||||
{...item}
|
<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>
|
||||||
|
|
||||||
|
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||||
|
|
||||||
|
{(isLoading || navigateToLastPage) && (
|
||||||
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading &&
|
||||||
|
flows?.map((flow) => (
|
||||||
|
<FlowRow
|
||||||
|
key={flow.id}
|
||||||
|
flow={flow}
|
||||||
|
onDuplicateFlow={onDuplicateFlow}
|
||||||
|
onDeleteFlow={fetchFlows}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!isLoading && !navigateToLastPage && !hasFlows && (
|
||||||
|
<NoResultFound
|
||||||
|
text={formatMessage('flows.noFlows')}
|
||||||
|
{...(currentUserAbility.can('create', 'Flow') && {
|
||||||
|
to: URLS.CREATE_FLOW,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
|
||||||
</Box>
|
{!isLoading &&
|
||||||
|
!navigateToLastPage &&
|
||||||
|
pageInfo &&
|
||||||
|
pageInfo.totalPages > 1 && (
|
||||||
|
<Pagination
|
||||||
|
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||||
|
page={pageInfo?.currentPage}
|
||||||
|
count={pageInfo?.totalPages}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<PaginationItem
|
||||||
|
component={Link}
|
||||||
|
to={getPathWithSearchParams(item.page, flowName)}
|
||||||
|
{...item}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route path="/import" element={<ImportFlowDialog />} />
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ function Routes() {
|
|||||||
const { isAuthenticated } = useAuthentication();
|
const { isAuthenticated } = useAuthentication();
|
||||||
const config = configData?.data;
|
const config = configData?.data;
|
||||||
|
|
||||||
const installed = isSuccess ? automatischInfo.data.installationCompleted : true;
|
const installed = isSuccess
|
||||||
|
? automatischInfo.data.installationCompleted
|
||||||
|
: true;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,7 +70,7 @@ function Routes() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={URLS.FLOWS}
|
path={`${URLS.FLOWS}/*`}
|
||||||
element={
|
element={
|
||||||
<Layout>
|
<Layout>
|
||||||
<Flows />
|
<Flows />
|
||||||
@@ -76,15 +78,6 @@ function Routes() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
|
||||||
path={URLS.FLOW_PATTERN}
|
|
||||||
element={
|
|
||||||
<Layout>
|
|
||||||
<Flow />
|
|
||||||
</Layout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={`${URLS.APPS}/*`}
|
path={`${URLS.APPS}/*`}
|
||||||
element={
|
element={
|
||||||
@@ -186,6 +179,7 @@ function Routes() {
|
|||||||
<Route path={URLS.ADMIN_SETTINGS} element={<AdminSettingsLayout />}>
|
<Route path={URLS.ADMIN_SETTINGS} element={<AdminSettingsLayout />}>
|
||||||
{adminSettingsRoutes}
|
{adminSettingsRoutes}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="*" element={<NoResultFound />} />
|
<Route path="*" element={<NoResultFound />} />
|
||||||
</ReactRouterRoutes>
|
</ReactRouterRoutes>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user