Merge pull request #2293 from automatisch/import-flow-ui
feat(web): add import flow functionality
This commit is contained in:
@@ -9,6 +9,7 @@ function ConditionalIconButton(props) {
|
||||
const { icon, ...buttonProps } = props;
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
if (matchSmallScreens) {
|
||||
return (
|
||||
<IconButton
|
||||
@@ -24,7 +25,8 @@ function ConditionalIconButton(props) {
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
return <Button {...buttonProps} />;
|
||||
|
||||
return <Button {...buttonProps} startIcon={icon} />;
|
||||
}
|
||||
|
||||
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_CONNECTIONS = (appKey) => `/app/${appKey}/connections`;
|
||||
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
||||
|
||||
export const APP_ADD_CONNECTION = (appKey, shared = false) =>
|
||||
`/app/${appKey}/connections/add?shared=${shared}`;
|
||||
|
||||
export const APP_ADD_CONNECTION_WITH_OAUTH_CLIENT_ID = (
|
||||
appKey,
|
||||
oauthClientId,
|
||||
) => `/app/${appKey}/connections/add?oauthClientId=${oauthClientId}`;
|
||||
|
||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||
|
||||
export const APP_RECONNECT_CONNECTION = (
|
||||
appKey,
|
||||
connectionId,
|
||||
oauthClientId,
|
||||
) => {
|
||||
const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
|
||||
if (oauthClientId) {
|
||||
return `${path}?oauthClientId=${oauthClientId}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const APP_RECONNECT_CONNECTION_PATTERN =
|
||||
'/app/:appKey/connections/:connectionId/reconnect';
|
||||
export const APP_FLOWS = (appKey) => `/app/${appKey}/flows`;
|
||||
|
||||
export const APP_FLOWS_FOR_CONNECTION = (appKey, connectionId) =>
|
||||
`/app/${appKey}/flows?connectionId=${connectionId}`;
|
||||
|
||||
export const APP_FLOWS = (appKey) => `/app/${appKey}/flows`;
|
||||
export const APP_FLOWS_PATTERN = '/app/:appKey/flows';
|
||||
export const EDITOR = '/editor';
|
||||
export const CREATE_FLOW = '/editor/create';
|
||||
export const IMPORT_FLOW = '/flows/import';
|
||||
export const FLOW_EDITOR = (flowId) => `/editor/${flowId}`;
|
||||
export const FLOWS = '/flows';
|
||||
// TODO: revert this back to /flows/:flowId once we have a proper single flow page
|
||||
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_DASHBOARD = SETTINGS;
|
||||
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_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 = (appKey) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
|
||||
|
||||
export const ADMIN_APP_SETTINGS = (appKey) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
|
||||
|
||||
export const ADMIN_APP_AUTH_CLIENTS = (appKey) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients`;
|
||||
|
||||
export const ADMIN_APP_AUTH_CLIENT = (appKey, id) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/${id}`;
|
||||
|
||||
export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/create`;
|
||||
|
||||
export const DASHBOARD = FLOWS;
|
||||
|
||||
// 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.reconnectConnection": "Reconnect connection",
|
||||
"app.createFlow": "Create flow",
|
||||
"app.importFlow": "Import flow",
|
||||
"app.settings": "Settings",
|
||||
"app.connections": "Connections",
|
||||
"app.noConnections": "You don't have any connections yet.",
|
||||
@@ -89,6 +90,7 @@
|
||||
"flowStep.triggerType": "Trigger",
|
||||
"flowStep.actionType": "Action",
|
||||
"flows.create": "Create flow",
|
||||
"flows.import": "Import flow",
|
||||
"flows.title": "Flows",
|
||||
"flows.noFlows": "You don't have any flows yet.",
|
||||
"flowEditor.goBack": "Go back to flows",
|
||||
@@ -317,5 +319,13 @@
|
||||
"oauthClient.inputActive": "Active",
|
||||
"updateOAuthClient.title": "Update OAuth client",
|
||||
"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 Box from '@mui/material/Box';
|
||||
import Grid from '@mui/material/Grid';
|
||||
|
||||
import Container from 'components/Container';
|
||||
|
||||
export default function Flow() {
|
||||
const { flowId } = useParams();
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Container>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
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 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';
|
||||
@@ -16,6 +23,7 @@ import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||
import Container from 'components/Container';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import SearchInput from 'components/SearchInput';
|
||||
import ImportFlowDialog from 'components/ImportFlowDialog';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||
import * as URLS from 'config/urls';
|
||||
@@ -98,85 +106,120 @@ export default function Flows() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Container>
|
||||
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
||||
<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>
|
||||
|
||||
<>
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Container>
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
xs="auto"
|
||||
sm="auto"
|
||||
alignItems="center"
|
||||
order={{ xs: 1, sm: 2 }}
|
||||
sx={{ mb: [0, 3] }}
|
||||
columnSpacing={1.5}
|
||||
rowSpacing={3}
|
||||
>
|
||||
<Can I="create" a="Flow" passThrough>
|
||||
{(allowed) => (
|
||||
<ConditionalIconButton
|
||||
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>
|
||||
<Grid container item xs sm alignItems="center" order={{ xs: 0 }}>
|
||||
<PageTitle>{formatMessage('flows.title')}</PageTitle>
|
||||
</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,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{!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}
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
display="flex"
|
||||
direction="row"
|
||||
xs="auto"
|
||||
sm="auto"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
order={{ xs: 1, sm: 2 }}
|
||||
>
|
||||
<Can I="create" a="Flow" passThrough>
|
||||
{(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>
|
||||
|
||||
<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 config = configData?.data;
|
||||
|
||||
const installed = isSuccess ? automatischInfo.data.installationCompleted : true;
|
||||
const installed = isSuccess
|
||||
? automatischInfo.data.installationCompleted
|
||||
: true;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,7 +70,7 @@ function Routes() {
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.FLOWS}
|
||||
path={`${URLS.FLOWS}/*`}
|
||||
element={
|
||||
<Layout>
|
||||
<Flows />
|
||||
@@ -76,15 +78,6 @@ function Routes() {
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.FLOW_PATTERN}
|
||||
element={
|
||||
<Layout>
|
||||
<Flow />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={`${URLS.APPS}/*`}
|
||||
element={
|
||||
@@ -186,6 +179,7 @@ function Routes() {
|
||||
<Route path={URLS.ADMIN_SETTINGS} element={<AdminSettingsLayout />}>
|
||||
{adminSettingsRoutes}
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<NoResultFound />} />
|
||||
</ReactRouterRoutes>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user