Merge pull request #2408 from automatisch/flow-filters
feat(flows): add flow filters
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
|||||||
mongoQueryMatcher,
|
mongoQueryMatcher,
|
||||||
} from '@casl/ability';
|
} from '@casl/ability';
|
||||||
|
|
||||||
// Must be kept in sync with `packages/web/src/helpers/userAbility.ts`!
|
// Must be kept in sync with `packages/web/src/helpers/userAbility.js`!
|
||||||
export default function userAbility(user) {
|
export default function userAbility(user) {
|
||||||
const permissions = user?.permissions;
|
const permissions = user?.permissions;
|
||||||
const role = user?.role;
|
const role = user?.role;
|
||||||
|
|||||||
121
packages/web/src/components/FlowFilters/index.jsx
Normal file
121
packages/web/src/components/FlowFilters/index.jsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
Collapse,
|
||||||
|
} from '@mui/material';
|
||||||
|
import FilterAltIcon from '@mui/icons-material/FilterAlt';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import Can from 'components/Can';
|
||||||
|
import useCurrentUserRuleConditions from 'hooks/useCurrentUserRuleConditions';
|
||||||
|
import useFlowFilters from 'hooks/useFlowFilters';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
|
export default function FlowFilters({ onFilterChange }) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const currentUserRuleConditions = useCurrentUserRuleConditions();
|
||||||
|
|
||||||
|
const { filters, filterByStatus, filterByOwnership } = useFlowFilters();
|
||||||
|
|
||||||
|
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
||||||
|
|
||||||
|
const currentUserReadFlowsConditions = currentUserRuleConditions(
|
||||||
|
'read',
|
||||||
|
'Flow',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* Mobile: Toggle Button for Filters */}
|
||||||
|
{isMobile && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<FilterAltIcon />}
|
||||||
|
onClick={() => setMobileFiltersOpen((prev) => !prev)}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
{mobileFiltersOpen
|
||||||
|
? formatMessage('flowFilters.hideFilters')
|
||||||
|
: formatMessage('flowFilters.showFilters')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filters Box (Always Visible on Large Screens) */}
|
||||||
|
<Collapse in={!isMobile || mobileFiltersOpen}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
|
gap: 2,
|
||||||
|
mb: 2,
|
||||||
|
alignItems: { md: 'center' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* User Flows Filter */}
|
||||||
|
{currentUserReadFlowsConditions &&
|
||||||
|
!currentUserReadFlowsConditions?.isCreator && (
|
||||||
|
<FormControl
|
||||||
|
fullWidth
|
||||||
|
sx={{ maxWidth: { md: 200 } }}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<InputLabel shrink>
|
||||||
|
{formatMessage('flowFilters.flowsFilterLabel')}
|
||||||
|
</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={formatMessage('flowFilters.flowsFilterLabel')}
|
||||||
|
value={filters.onlyOwnedFlows}
|
||||||
|
displayEmpty
|
||||||
|
onChange={(e) => filterByOwnership(e.target.value)}
|
||||||
|
key={filters.onlyOwnedFlows}
|
||||||
|
>
|
||||||
|
<MenuItem value={undefined}>
|
||||||
|
{formatMessage('flowFilters.flowsFilterAllOption')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={true}>
|
||||||
|
{formatMessage('flowFilters.flowsFilterOnlyMineOption')}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Filter */}
|
||||||
|
<FormControl fullWidth sx={{ maxWidth: { md: 200 } }}>
|
||||||
|
<InputLabel shrink>
|
||||||
|
{formatMessage('flowFilters.statusFilterLabel')}
|
||||||
|
</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={formatMessage('flowFilters.statusFilterLabel')}
|
||||||
|
value={filters.status}
|
||||||
|
displayEmpty
|
||||||
|
onChange={(e) => filterByStatus(e.target.value)}
|
||||||
|
key={filters.status}
|
||||||
|
>
|
||||||
|
<MenuItem value={undefined}>
|
||||||
|
{formatMessage('flowFilters.statusFilterAnyOption')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="published">
|
||||||
|
{formatMessage('flowFilters.statusFilterPublishedOption')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="draft">
|
||||||
|
{formatMessage('flowFilters.statusFilterDraftOption')}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
@@ -23,6 +24,8 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
|||||||
import useFolders from 'hooks/useFolders';
|
import useFolders from 'hooks/useFolders';
|
||||||
import useDeleteFolder from 'hooks/useDeleteFolder';
|
import useDeleteFolder from 'hooks/useDeleteFolder';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
import objectifyUrlSearchParams from 'helpers/objectifyUrlSearchParams';
|
||||||
|
import useFlowFilters from 'hooks/useFlowFilters';
|
||||||
|
|
||||||
import { ListItemIcon } from './style';
|
import { ListItemIcon } from './style';
|
||||||
|
|
||||||
@@ -35,6 +38,8 @@ export default function Folders() {
|
|||||||
const { data: folders } = useFolders();
|
const { data: folders } = useFolders();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
|
const { enhanceExistingSearchParams } = useFlowFilters();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: deleteFolder,
|
mutateAsync: deleteFolder,
|
||||||
error: deleteFolderError,
|
error: deleteFolderError,
|
||||||
@@ -53,8 +58,8 @@ export default function Folders() {
|
|||||||
(folder) => folder.id === selectedFolderId,
|
(folder) => folder.id === selectedFolderId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const allFlowsFolder = new URLSearchParams().toString();
|
const allFlowsFolder = enhanceExistingSearchParams('folderId', undefined);
|
||||||
const unassignedFlowsFolder = new URLSearchParams('folderId=null').toString();
|
const unassignedFlowsFolder = enhanceExistingSearchParams('folderId', 'null');
|
||||||
|
|
||||||
const allFlowsFolderSelected = selectedFolderId === null;
|
const allFlowsFolderSelected = selectedFolderId === null;
|
||||||
const unassignedFlowsFolderSelected = selectedFolderId === 'null'; // intendedly stringified
|
const unassignedFlowsFolderSelected = selectedFolderId === 'null'; // intendedly stringified
|
||||||
@@ -86,9 +91,7 @@ export default function Folders() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getFolderSearchParams = (folderId) => {
|
const getFolderSearchParams = (folderId) => {
|
||||||
const searchParams = new URLSearchParams(`folderId=${folderId}`);
|
return enhanceExistingSearchParams('folderId', folderId).toString();
|
||||||
|
|
||||||
return searchParams.toString();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFolderItem = (folder) => {
|
const generateFolderItem = (folder) => {
|
||||||
@@ -148,10 +151,7 @@ export default function Folders() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box component={Card}>
|
||||||
component={Card}
|
|
||||||
// sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
|
|
||||||
>
|
|
||||||
<List component="nav" aria-label="static folders">
|
<List component="nav" aria-label="static folders">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
component={Link}
|
component={Link}
|
||||||
|
|||||||
5
packages/web/src/helpers/objectifyUrlSearchParams.js
Normal file
5
packages/web/src/helpers/objectifyUrlSearchParams.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default function objectifyUrlSearchParams(searchParams) {
|
||||||
|
return Array.from(searchParams.entries()).reduce((acc, [key, value]) => {
|
||||||
|
return { ...acc, [key]: value };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
@@ -3,17 +3,21 @@ import {
|
|||||||
fieldPatternMatcher,
|
fieldPatternMatcher,
|
||||||
mongoQueryMatcher,
|
mongoQueryMatcher,
|
||||||
} from '@casl/ability';
|
} from '@casl/ability';
|
||||||
// Must be kept in sync with `packages/backend/src/helpers/user-ability.ts`!
|
|
||||||
|
// Must be kept in sync with `packages/backend/src/helpers/user-ability.js`!
|
||||||
export default function userAbility(user) {
|
export default function userAbility(user) {
|
||||||
const permissions = user?.permissions;
|
const permissions = user?.permissions;
|
||||||
const role = user?.role;
|
const role = user?.role;
|
||||||
|
|
||||||
// We're not using mongo, but our fields, conditions match
|
// We're not using mongo, but our fields, conditions match
|
||||||
const options = {
|
const options = {
|
||||||
conditionsMatcher: mongoQueryMatcher,
|
conditionsMatcher: mongoQueryMatcher,
|
||||||
fieldMatcher: fieldPatternMatcher,
|
fieldMatcher: fieldPatternMatcher,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!role || !permissions) {
|
if (!role || !permissions) {
|
||||||
return new PureAbility([], options);
|
return new PureAbility([], options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PureAbility(permissions, options);
|
return new PureAbility(permissions, options);
|
||||||
}
|
}
|
||||||
|
|||||||
20
packages/web/src/hooks/useCurrentUserRuleConditions.js
Normal file
20
packages/web/src/hooks/useCurrentUserRuleConditions.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
|
|
||||||
|
export default function useCurrentUserRuleConditions() {
|
||||||
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
|
||||||
|
return function canCurrentUser(action, subject) {
|
||||||
|
const can = currentUserAbility.can(action, subject);
|
||||||
|
|
||||||
|
if (!can) return false;
|
||||||
|
|
||||||
|
const relevantRule = currentUserAbility.relevantRuleFor(action, subject);
|
||||||
|
|
||||||
|
const conditions = relevantRule?.conditions || [];
|
||||||
|
const conditionMap = Object.fromEntries(
|
||||||
|
conditions.map((condition) => [condition, true]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return conditionMap;
|
||||||
|
};
|
||||||
|
}
|
||||||
67
packages/web/src/hooks/useFlowFilters.js
Normal file
67
packages/web/src/hooks/useFlowFilters.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import api from 'helpers/api';
|
||||||
|
import objectifyUrlSearchParams from 'helpers/objectifyUrlSearchParams';
|
||||||
|
|
||||||
|
export default function useFlowFilters() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const searchParamsObject = objectifyUrlSearchParams(searchParams);
|
||||||
|
|
||||||
|
const { folderId, status } = searchParamsObject;
|
||||||
|
const onlyOwnedFlows =
|
||||||
|
searchParamsObject.onlyOwnedFlows === 'true' || undefined;
|
||||||
|
|
||||||
|
const filterByStatus = (status) => {
|
||||||
|
setSearchParams((current) => {
|
||||||
|
const { status: currentStatus, ...rest } = searchParamsObject;
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
return { ...rest, status };
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterByOwnership = (onlyOwnedFlows) => {
|
||||||
|
setSearchParams((current) => {
|
||||||
|
const { onlyOwnedFlows: currentOnlyOwnedFlows, ...rest } =
|
||||||
|
searchParamsObject;
|
||||||
|
|
||||||
|
if (onlyOwnedFlows) {
|
||||||
|
return { ...rest, onlyOwnedFlows: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhanceExistingSearchParams = (key, value) => {
|
||||||
|
const searchParamsObject = objectifyUrlSearchParams(searchParams);
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
const { [key]: keyToRemove, ...remainingSearchParams } =
|
||||||
|
searchParamsObject;
|
||||||
|
|
||||||
|
return new URLSearchParams(remainingSearchParams).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URLSearchParams({
|
||||||
|
...searchParamsObject,
|
||||||
|
[key]: value,
|
||||||
|
}).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
folderId,
|
||||||
|
status,
|
||||||
|
onlyOwnedFlows,
|
||||||
|
},
|
||||||
|
filterByStatus,
|
||||||
|
filterByOwnership,
|
||||||
|
enhanceExistingSearchParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
import api from 'helpers/api';
|
import api from 'helpers/api';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
export default function useFlows({ flowName, page, folderId }) {
|
export default function useFlows({
|
||||||
|
flowName,
|
||||||
|
page,
|
||||||
|
folderId,
|
||||||
|
status,
|
||||||
|
onlyOwnedFlows,
|
||||||
|
}) {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['flows', flowName, { page, folderId }],
|
queryKey: ['flows', { flowName, page, folderId, status, onlyOwnedFlows }],
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const { data } = await api.get('/v1/flows', {
|
const { data } = await api.get('/v1/flows', {
|
||||||
params: { name: flowName, page, folderId },
|
params: { name: flowName, page, folderId, status, onlyOwnedFlows },
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -386,5 +386,14 @@
|
|||||||
"adminCreateTemplate.titleFieldLabel": "Template name",
|
"adminCreateTemplate.titleFieldLabel": "Template name",
|
||||||
"adminCreateTemplate.submit": "Create",
|
"adminCreateTemplate.submit": "Create",
|
||||||
"adminTemplateContextMenu.delete": "Delete",
|
"adminTemplateContextMenu.delete": "Delete",
|
||||||
"adminTemplateContextMenu.successfullyDeleted": "The template has been deleted."
|
"adminTemplateContextMenu.successfullyDeleted": "The template has been deleted.",
|
||||||
|
"flowFilters.hideFilters": "Hide filters",
|
||||||
|
"flowFilters.showFilters": "Show Filters",
|
||||||
|
"flowFilters.flowsFilterLabel": "Flows",
|
||||||
|
"flowFilters.flowsFilterAllOption": "All",
|
||||||
|
"flowFilters.flowsFilterOnlyMineOption": "Only mine",
|
||||||
|
"flowFilters.statusFilterLabel": "Status",
|
||||||
|
"flowFilters.statusFilterAnyOption": "All",
|
||||||
|
"flowFilters.statusFilterPublishedOption": "Published",
|
||||||
|
"flowFilters.statusFilterDraftOption": "Draft"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
|
import FlowFilters from 'components/FlowFilters';
|
||||||
import FlowsButtons from 'components/FlowsButtons';
|
import FlowsButtons from 'components/FlowsButtons';
|
||||||
import ConditionalIconButton from 'components/ConditionalIconButton';
|
import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
@@ -30,6 +31,7 @@ import * as URLS from 'config/urls';
|
|||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
import useFlows from 'hooks/useFlows';
|
import useFlows from 'hooks/useFlows';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import objectifyUrlSearchParams from 'helpers/objectifyUrlSearchParams';
|
||||||
|
|
||||||
export default function Flows() {
|
export default function Flows() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
@@ -38,10 +40,18 @@ export default function Flows() {
|
|||||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||||
const flowName = searchParams.get('flowName') || '';
|
const flowName = searchParams.get('flowName') || '';
|
||||||
const folderId = searchParams.get('folderId');
|
const folderId = searchParams.get('folderId');
|
||||||
|
const status = searchParams.get('status');
|
||||||
|
const onlyOwnedFlows = searchParams.get('onlyOwnedFlows');
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
const [searchValue, setSearchValue] = React.useState(flowName);
|
const [searchValue, setSearchValue] = React.useState(flowName);
|
||||||
|
|
||||||
const { data, isSuccess, isLoading } = useFlows({ flowName, page, folderId });
|
const { data, isSuccess, isLoading } = useFlows({
|
||||||
|
flowName,
|
||||||
|
page,
|
||||||
|
folderId,
|
||||||
|
status,
|
||||||
|
onlyOwnedFlows,
|
||||||
|
});
|
||||||
|
|
||||||
const flows = data?.data || [];
|
const flows = data?.data || [];
|
||||||
const pageInfo = data?.meta;
|
const pageInfo = data?.meta;
|
||||||
@@ -51,36 +61,33 @@ export default function Flows() {
|
|||||||
const onSearchChange = React.useCallback(
|
const onSearchChange = React.useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
|
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
flowName: value,
|
flowName: value,
|
||||||
...(folderId && { folderId }),
|
...(folderId && { folderId }),
|
||||||
|
...(onlyOwnedFlows && { onlyOwnedFlows }),
|
||||||
|
...(status && { status }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[folderId, setSearchParams],
|
[folderId, setSearchParams, onlyOwnedFlows, status],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getPathWithSearchParams = (page, flowName) => {
|
const getPathWithSearchParams = (page) => {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParamsObject = objectifyUrlSearchParams(searchParams);
|
||||||
|
|
||||||
if (folderId) {
|
const newSearchParams = new URLSearchParams({
|
||||||
searchParams.set('folderId', folderId);
|
...searchParamsObject,
|
||||||
}
|
page,
|
||||||
|
});
|
||||||
|
|
||||||
if (flowName) {
|
return { search: newSearchParams.toString() };
|
||||||
searchParams.set('flowName', flowName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page > 1) {
|
|
||||||
searchParams.set('page', page);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { search: searchParams.toString() };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDuplicateFlow = () => {
|
const onDuplicateFlow = () => {
|
||||||
if (pageInfo?.currentPage > 1) {
|
if (pageInfo?.currentPage > 1) {
|
||||||
navigate(getPathWithSearchParams(1, flowName));
|
navigate(getPathWithSearchParams(1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +103,7 @@ export default function Flows() {
|
|||||||
React.useEffect(
|
React.useEffect(
|
||||||
function redirectToLastPage() {
|
function redirectToLastPage() {
|
||||||
if (navigateToLastPage) {
|
if (navigateToLastPage) {
|
||||||
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
|
navigate(getPathWithSearchParams(pageInfo.totalPages));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigateToLastPage],
|
[navigateToLastPage],
|
||||||
@@ -145,6 +152,8 @@ export default function Flows() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={9}>
|
<Grid item xs={12} sm={9}>
|
||||||
|
<FlowFilters />
|
||||||
|
|
||||||
{(isLoading || navigateToLastPage) && (
|
{(isLoading || navigateToLastPage) && (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
sx={{ display: 'block', margin: '20px auto' }}
|
sx={{ display: 'block', margin: '20px auto' }}
|
||||||
@@ -180,7 +189,7 @@ export default function Flows() {
|
|||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to={getPathWithSearchParams(item.page, flowName)}
|
to={getPathWithSearchParams(item.page)}
|
||||||
{...item}
|
{...item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user