mirror of
https://github.com/strapi/strapi.git
synced 2025-09-26 00:39:49 +00:00
Merge pull request #10857 from strapi/migrations/admin-users-filters
Migrations/admin users filters
This commit is contained in:
commit
d4c94aa637
@ -54,10 +54,6 @@ describe('<ListPage />', () => {
|
||||
} = render(App);
|
||||
|
||||
expect(firstChild).toMatchInlineSnapshot(`
|
||||
.c37 {
|
||||
font-family: Lato;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
font-weight: 600;
|
||||
font-size: 2rem;
|
||||
@ -404,6 +400,10 @@ describe('<ListPage />', () => {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.c37 {
|
||||
font-family: Lato;
|
||||
}
|
||||
|
||||
<main
|
||||
aria-labelledby="title"
|
||||
class="c0"
|
||||
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* This component is temporary
|
||||
* FIXME migrate to the common one
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Button } from '@strapi/parts/Button';
|
||||
import { Dialog, DialogBody, DialogFooter } from '@strapi/parts/Dialog';
|
||||
import { Row } from '@strapi/parts/Row';
|
||||
import { Stack } from '@strapi/parts/Stack';
|
||||
import { Text } from '@strapi/parts/Text';
|
||||
import { AlertWarningIcon, DeleteIcon } from '@strapi/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const ConfirmDialog = ({ isConfirmButtonLoading, onConfirm, onToggle, show }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onClose={onToggle}
|
||||
title={formatMessage({
|
||||
id: 'Settings.webhooks.confirmation',
|
||||
defaultMessage: 'Confirmation',
|
||||
})}
|
||||
labelledBy="confirmation"
|
||||
describedBy="confirm-description"
|
||||
>
|
||||
<DialogBody icon={<AlertWarningIcon />}>
|
||||
<Stack size={2}>
|
||||
<Row justifyContent="center">
|
||||
<Text id="confirm-description">
|
||||
{formatMessage({
|
||||
id: 'Settings.webhooks.confirmation.delete',
|
||||
defaultMessage: 'Are you sure you want to delete this?',
|
||||
})}
|
||||
</Text>
|
||||
</Row>
|
||||
</Stack>
|
||||
</DialogBody>
|
||||
<DialogFooter
|
||||
startAction={
|
||||
<Button onClick={onToggle} variant="tertiary">
|
||||
{formatMessage({ id: 'app.components.Button.cancel', defaultMessage: 'Cancel' })}
|
||||
</Button>
|
||||
}
|
||||
endAction={
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
variant="danger-light"
|
||||
startIcon={<DeleteIcon />}
|
||||
id="confirm-delete"
|
||||
loading={isConfirmButtonLoading}
|
||||
>
|
||||
{formatMessage({ id: 'app.components.Button.confirm', defaultMessage: 'Confirm' })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
ConfirmDialog.defaultProps = {
|
||||
isConfirmButtonLoading: false,
|
||||
};
|
||||
|
||||
ConfirmDialog.propTypes = {
|
||||
isConfirmButtonLoading: PropTypes.bool,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default ConfirmDialog;
|
@ -3,12 +3,14 @@ import PropTypes from 'prop-types';
|
||||
import { BaseCheckbox, Box, IconButton, Tbody, Td, Text, Tr, Row } from '@strapi/parts';
|
||||
import { EditIcon, DeleteIcon } from '@strapi/icons';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const TableRows = ({
|
||||
canUpdate,
|
||||
canDelete,
|
||||
headers,
|
||||
entriesToDelete,
|
||||
onClickDelete,
|
||||
onSelectRow,
|
||||
withMainAction,
|
||||
withBulkActions,
|
||||
@ -18,6 +20,7 @@ const TableRows = ({
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<Tbody>
|
||||
@ -55,7 +58,7 @@ const TableRows = ({
|
||||
{canUpdate && (
|
||||
<IconButton
|
||||
onClick={() => push(`${pathname}/${data.id}`)}
|
||||
label="Edit"
|
||||
label={formatMessage({ id: 'app.utils.edit', defaultMessage: 'Edit' })}
|
||||
noBorder
|
||||
icon={<EditIcon />}
|
||||
/>
|
||||
@ -63,8 +66,8 @@ const TableRows = ({
|
||||
{canDelete && (
|
||||
<Box paddingLeft={1}>
|
||||
<IconButton
|
||||
onClick={() => console.log('delete')}
|
||||
label="Delete"
|
||||
onClick={() => onClickDelete(data.id)}
|
||||
label={formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' })}
|
||||
noBorder
|
||||
icon={<DeleteIcon />}
|
||||
/>
|
||||
@ -83,6 +86,8 @@ const TableRows = ({
|
||||
TableRows.defaultProps = {
|
||||
canDelete: false,
|
||||
canUpdate: false,
|
||||
onClickDelete: () => {},
|
||||
onSelectRow: () => {},
|
||||
rows: [],
|
||||
withBulkActions: false,
|
||||
withMainAction: false,
|
||||
@ -93,7 +98,8 @@ TableRows.propTypes = {
|
||||
canUpdate: PropTypes.bool,
|
||||
entriesToDelete: PropTypes.array.isRequired,
|
||||
headers: PropTypes.array.isRequired,
|
||||
onSelectRow: PropTypes.func.isRequired,
|
||||
onClickDelete: PropTypes.func,
|
||||
onSelectRow: PropTypes.func,
|
||||
rows: PropTypes.array,
|
||||
withBulkActions: PropTypes.bool,
|
||||
withMainAction: PropTypes.bool,
|
||||
|
@ -1,13 +1,38 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Table as TableCompo } from '@strapi/parts';
|
||||
import { Box, Row, Button, Table as TableCompo, Subtitle } from '@strapi/parts';
|
||||
import { EmptyBodyTable, useQueryParams } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { DeleteIcon } from '@strapi/icons';
|
||||
import styled from 'styled-components';
|
||||
import ConfirmDialog from '../ConfirmDialog';
|
||||
import TableHead from './TableHead';
|
||||
import TableRows from './TableRows';
|
||||
|
||||
const Table = ({ canDelete, canUpdate, headers, rows, withBulkActions, withMainAction }) => {
|
||||
const BlockActions = styled(Row)`
|
||||
& > * + * {
|
||||
margin-left: ${({ theme }) => theme.spaces[2]};
|
||||
}
|
||||
|
||||
margin-left: ${({ pullRight }) => (pullRight ? 'auto' : undefined)};
|
||||
`;
|
||||
|
||||
const Table = ({
|
||||
canDelete,
|
||||
canUpdate,
|
||||
headers,
|
||||
isLoading,
|
||||
onConfirmDeleteAll,
|
||||
rows,
|
||||
withBulkActions,
|
||||
withMainAction,
|
||||
}) => {
|
||||
const [entriesToDelete, setEntriesToDelete] = useState([]);
|
||||
const [showConfirmDeleteAll, setShowConfirmDeleteAll] = useState(false);
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||
const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false);
|
||||
const [{ query }] = useQueryParams();
|
||||
const { formatMessage } = useIntl();
|
||||
const ROW_COUNT = rows.length + 1;
|
||||
const COL_COUNT = headers.length + (withBulkActions ? 1 : 0) + (withMainAction ? 1 : 0);
|
||||
const hasFilters = query.filters !== undefined;
|
||||
@ -21,6 +46,28 @@ const Table = ({ canDelete, canUpdate, headers, rows, withBulkActions, withMainA
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const handleConfirmDeleteAll = async () => {
|
||||
try {
|
||||
setIsConfirmButtonLoading(true);
|
||||
await onConfirmDeleteAll(entriesToDelete);
|
||||
handleToggleConfirmDeleteAll();
|
||||
} catch (err) {
|
||||
setIsConfirmButtonLoading(false);
|
||||
handleToggleConfirmDeleteAll();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
try {
|
||||
setIsConfirmButtonLoading(true);
|
||||
await onConfirmDeleteAll(entriesToDelete);
|
||||
handleToggleConfirmDelete();
|
||||
} catch (err) {
|
||||
setIsConfirmButtonLoading(false);
|
||||
handleToggleConfirmDelete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (!areAllEntriesSelected) {
|
||||
setEntriesToDelete(rows.map(row => row.id));
|
||||
@ -29,6 +76,23 @@ const Table = ({ canDelete, canUpdate, headers, rows, withBulkActions, withMainA
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleConfirmDeleteAll = () => {
|
||||
setShowConfirmDeleteAll(prev => !prev);
|
||||
};
|
||||
|
||||
const handleToggleConfirmDelete = () => {
|
||||
if (showConfirmDelete) {
|
||||
setEntriesToDelete([]);
|
||||
}
|
||||
setShowConfirmDelete(prev => !prev);
|
||||
};
|
||||
|
||||
const handleClickDelete = id => {
|
||||
setEntriesToDelete([id]);
|
||||
|
||||
handleToggleConfirmDelete();
|
||||
};
|
||||
|
||||
const handleSelectRow = ({ name, value }) => {
|
||||
setEntriesToDelete(prev => {
|
||||
if (value) {
|
||||
@ -40,35 +104,79 @@ const Table = ({ canDelete, canUpdate, headers, rows, withBulkActions, withMainA
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCompo colCount={COL_COUNT} rowCount={ROW_COUNT}>
|
||||
<TableHead
|
||||
areAllEntriesSelected={areAllEntriesSelected}
|
||||
entriesToDelete={entriesToDelete}
|
||||
headers={headers}
|
||||
onSelectAll={handleSelectAll}
|
||||
withMainAction={withMainAction}
|
||||
withBulkActions={withBulkActions}
|
||||
/>
|
||||
{!rows.length ? (
|
||||
<EmptyBodyTable colSpan={COL_COUNT} content={content} />
|
||||
) : (
|
||||
<TableRows
|
||||
canDelete={canDelete}
|
||||
canUpdate={canUpdate}
|
||||
<>
|
||||
{entriesToDelete.length > 0 && (
|
||||
<Box>
|
||||
<Box paddingBottom={4}>
|
||||
<Row justifyContent="space-between">
|
||||
<BlockActions>
|
||||
<Subtitle textColor="neutral600">
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.components.TableDelete.label',
|
||||
defaultMessage: '{number, plural, one {# entry} other {# entries}} selected',
|
||||
},
|
||||
{ number: entriesToDelete.length }
|
||||
)}
|
||||
</Subtitle>
|
||||
<Button
|
||||
onClick={handleToggleConfirmDeleteAll}
|
||||
startIcon={<DeleteIcon />}
|
||||
size="L"
|
||||
variant="danger-light"
|
||||
>
|
||||
{formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' })}
|
||||
</Button>
|
||||
</BlockActions>
|
||||
</Row>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<TableCompo colCount={COL_COUNT} rowCount={ROW_COUNT}>
|
||||
<TableHead
|
||||
areAllEntriesSelected={areAllEntriesSelected}
|
||||
entriesToDelete={entriesToDelete}
|
||||
headers={headers}
|
||||
onSelectRow={handleSelectRow}
|
||||
rows={rows}
|
||||
withBulkActions={withBulkActions}
|
||||
onSelectAll={handleSelectAll}
|
||||
withMainAction={withMainAction}
|
||||
withBulkActions={withBulkActions}
|
||||
/>
|
||||
)}
|
||||
</TableCompo>
|
||||
{!rows.length || isLoading ? (
|
||||
<EmptyBodyTable colSpan={COL_COUNT} content={content} isLoading={isLoading} />
|
||||
) : (
|
||||
<TableRows
|
||||
canDelete={canDelete}
|
||||
canUpdate={canUpdate}
|
||||
entriesToDelete={entriesToDelete}
|
||||
headers={headers}
|
||||
onClickDelete={handleClickDelete}
|
||||
onSelectRow={handleSelectRow}
|
||||
rows={rows}
|
||||
withBulkActions={withBulkActions}
|
||||
withMainAction={withMainAction}
|
||||
/>
|
||||
)}
|
||||
</TableCompo>
|
||||
<ConfirmDialog
|
||||
isConfirmButtonLoading={isConfirmButtonLoading}
|
||||
onConfirm={handleConfirmDeleteAll}
|
||||
onToggle={handleToggleConfirmDeleteAll}
|
||||
show={showConfirmDeleteAll}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isConfirmButtonLoading={isConfirmButtonLoading}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onToggle={handleToggleConfirmDelete}
|
||||
show={showConfirmDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Table.defaultProps = {
|
||||
headers: [],
|
||||
isLoading: false,
|
||||
onConfirmDeleteAll: () => {},
|
||||
rows: [],
|
||||
withBulkActions: false,
|
||||
withMainAction: false,
|
||||
@ -78,6 +186,8 @@ Table.propTypes = {
|
||||
canDelete: PropTypes.bool.isRequired,
|
||||
canUpdate: PropTypes.bool.isRequired,
|
||||
headers: PropTypes.array,
|
||||
isLoading: PropTypes.bool,
|
||||
onConfirmDeleteAll: PropTypes.func,
|
||||
rows: PropTypes.array,
|
||||
withBulkActions: PropTypes.bool,
|
||||
withMainAction: PropTypes.bool,
|
||||
|
@ -55,74 +55,6 @@ describe('DynamicTable', () => {
|
||||
const { container, getByText } = render(app);
|
||||
|
||||
expect(container.firstChild).toMatchInlineSnapshot(`
|
||||
.c24 {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
color: #666687;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
background: #ffffff;
|
||||
padding: 64px;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.c22 svg {
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.c16 tr:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
border-bottom: 1px solid #eaeaef;
|
||||
}
|
||||
|
||||
.c17 td,
|
||||
.c17 th {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.c17 td:first-of-type,
|
||||
.c17 th:first-of-type {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
color: #666687;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
.c18 input {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
@ -316,6 +248,74 @@ describe('DynamicTable', () => {
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
color: #666687;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
background: #ffffff;
|
||||
padding: 64px;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.c22 svg {
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.c16 tr:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
border-bottom: 1px solid #eaeaef;
|
||||
}
|
||||
|
||||
.c17 td,
|
||||
.c17 th {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.c17 td:first-of-type,
|
||||
.c17 th:first-of-type {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
color: #666687;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
.c18 input {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
|
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Box, Tag } from '@strapi/parts';
|
||||
import { Close } from '@strapi/icons';
|
||||
import { useQueryParams } from '@strapi/helper-plugin';
|
||||
|
||||
const FilterList = () => {
|
||||
const [{ query }, setQuery] = useQueryParams();
|
||||
|
||||
const handleClick = filter => {
|
||||
const nextFilters = query.filters.$and.filter(f => {
|
||||
const name = Object.keys(filter)[0];
|
||||
const filterType = Object.keys(filter[name])[0];
|
||||
const value = filter[name][filterType];
|
||||
|
||||
return f[name]?.[filterType] !== value;
|
||||
});
|
||||
|
||||
setQuery({ filters: { $and: nextFilters }, page: 1 });
|
||||
};
|
||||
|
||||
return (
|
||||
query.filters?.$and.map((filter, i) => {
|
||||
const name = Object.keys(filter)[0];
|
||||
const filterType = Object.keys(filter[name])[0];
|
||||
const value = filter[name][filterType];
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Box key={`${name}-${i}`} padding={1} onClick={() => handleClick(filter)}>
|
||||
<Tag icon={<Close />}>
|
||||
{name} {filterType} {value}
|
||||
</Tag>
|
||||
</Box>
|
||||
);
|
||||
}) || null
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterList;
|
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Select, Field, Stack, FieldInput, Option } from '@strapi/parts';
|
||||
|
||||
const Inputs = ({ onChange, type, value }) => {
|
||||
if (type === 'boolean') {
|
||||
return (
|
||||
<Select onChange={onChange} value={value}>
|
||||
<Option value="true">true</Option>
|
||||
<Option value="false">false</Option>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO improve
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<Stack>
|
||||
<FieldInput onChange={({ target: { value } }) => onChange(value)} value={value} size="S" />
|
||||
</Stack>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
Inputs.defaultProps = {
|
||||
value: '',
|
||||
};
|
||||
|
||||
Inputs.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
export default Inputs;
|
@ -0,0 +1,132 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Button, Box, Popover, Stack, Select, Option, FocusTrap } from '@strapi/parts';
|
||||
import { AddIcon } from '@strapi/icons';
|
||||
import { useQueryParams } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Inputs from './Inputs';
|
||||
import getFilterList from './utils/getFilterList';
|
||||
|
||||
const FullWidthButton = styled(Button)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FilterPopover = ({ displayedFilters, isVisible, onToggle, source }) => {
|
||||
const [{ query }, setQuery] = useQueryParams();
|
||||
const { formatMessage } = useIntl();
|
||||
const [modifiedData, setModifiedData] = useState({
|
||||
name: displayedFilters[0].name,
|
||||
filter: getFilterList(displayedFilters[0])[0].value,
|
||||
value: '',
|
||||
});
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleChangeFilterField = value => {
|
||||
const nextField = displayedFilters.find(f => f.name === value);
|
||||
const {
|
||||
fieldSchema: { type },
|
||||
} = nextField;
|
||||
|
||||
setModifiedData({ name: value, filter: '$eq', value: type === 'boolean' ? 'true' : '' });
|
||||
};
|
||||
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const hasFilter =
|
||||
query?.filters?.$and.find(filter => {
|
||||
return (
|
||||
filter[modifiedData.name] &&
|
||||
filter[modifiedData.name]?.[modifiedData.filter] === modifiedData.value
|
||||
);
|
||||
}) !== undefined;
|
||||
|
||||
if (modifiedData.value && !hasFilter) {
|
||||
const filters = [
|
||||
...(query?.filters?.$and || []),
|
||||
{ [modifiedData.name]: { [modifiedData.filter]: modifiedData.value } },
|
||||
];
|
||||
|
||||
setQuery({ filters: { $and: filters }, page: 1 });
|
||||
}
|
||||
onToggle();
|
||||
};
|
||||
|
||||
const appliedFilter = displayedFilters.find(filter => filter.name === modifiedData.name);
|
||||
|
||||
return (
|
||||
<Popover source={source} padding={3} spacingTop={1}>
|
||||
<FocusTrap onEscape={onToggle}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack size={1} style={{ minWidth: 184 }}>
|
||||
<Box>
|
||||
<Select
|
||||
aria-label="Select field"
|
||||
name="name"
|
||||
size="S"
|
||||
onChange={handleChangeFilterField}
|
||||
value={modifiedData.name}
|
||||
>
|
||||
{displayedFilters.map(filter => {
|
||||
return (
|
||||
<Option key={filter.name} value={filter.name}>
|
||||
{filter.metadatas.label}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
<Box>
|
||||
<Select
|
||||
aria-label="Select filter"
|
||||
name="filter"
|
||||
size="S"
|
||||
value={modifiedData.filter}
|
||||
onChange={val => setModifiedData(prev => ({ ...prev, filter: val }))}
|
||||
>
|
||||
{getFilterList(appliedFilter).map(option => {
|
||||
return (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
<Box>
|
||||
<Inputs
|
||||
{...appliedFilter.fieldSchema}
|
||||
value={modifiedData.value}
|
||||
onChange={value => setModifiedData(prev => ({ ...prev, value }))}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<FullWidthButton variant="secondary" startIcon={<AddIcon />} type="submit">
|
||||
{formatMessage({ id: 'app.utils.add-filter', defaultMessage: 'Add filter' })}
|
||||
</FullWidthButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</form>
|
||||
</FocusTrap>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
FilterPopover.propTypes = {
|
||||
displayedFilters: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
metadatas: PropTypes.shape({ label: PropTypes.string }),
|
||||
fieldSchema: PropTypes.shape({ type: PropTypes.string }),
|
||||
})
|
||||
).isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
source: PropTypes.shape({ current: PropTypes.instanceOf(Element) }).isRequired,
|
||||
};
|
||||
|
||||
export default FilterPopover;
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Depending on the selected field find the possible filters to apply
|
||||
* @param {Object} fieldSchema.type the type of the filter
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
const getFilterList = ({ fieldSchema: { type } }) => {
|
||||
// TODO needs to be improved for the CM
|
||||
switch (type) {
|
||||
case 'email':
|
||||
case 'string': {
|
||||
return [
|
||||
{ label: 'is', value: '$eq' },
|
||||
{ label: 'is not', value: '$ne' },
|
||||
{ label: 'contains', value: '$contains' },
|
||||
];
|
||||
}
|
||||
default:
|
||||
return [
|
||||
{ label: 'is', value: '$eq' },
|
||||
{ label: 'is not', value: '$ne' },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export default getFilterList;
|
@ -0,0 +1,54 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Button, Box } from '@strapi/parts';
|
||||
import { FilterIcon } from '@strapi/icons';
|
||||
import FilterList from './FilterList';
|
||||
import FilterPopover from './FilterPopover';
|
||||
|
||||
const Filters = ({ displayedFilters }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
const buttonRef = useRef();
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsVisible(prev => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box padding={1}>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
ref={buttonRef}
|
||||
startIcon={<FilterIcon />}
|
||||
onClick={handleToggle}
|
||||
size="S"
|
||||
>
|
||||
{formatMessage({ id: 'app.utils.filters', defaultMessage: 'Filters' })}
|
||||
</Button>
|
||||
{isVisible && (
|
||||
<FilterPopover
|
||||
displayedFilters={displayedFilters}
|
||||
isVisible={isVisible}
|
||||
onToggle={handleToggle}
|
||||
source={buttonRef}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<FilterList />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Filters.propTypes = {
|
||||
displayedFilters: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
metadatas: PropTypes.shape({ label: PropTypes.string }),
|
||||
fieldSchema: PropTypes.shape({ type: PropTypes.string }),
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default Filters;
|
@ -23,7 +23,7 @@ const PageSize = () => {
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Select onChange={handleChange} value={pageSize}>
|
||||
<Select aria-label="Entries per page" onChange={handleChange} value={pageSize}>
|
||||
<Option value="10">10</Option>
|
||||
<Option value="20">20</Option>
|
||||
<Option value="50">50</Option>
|
||||
|
@ -60,7 +60,13 @@ const Pagination = ({ pagination: { pageCount } }) => {
|
||||
}
|
||||
|
||||
if (activePage === 2 && pageCount >= 3) {
|
||||
firstLinksToCreate = pageCount === 5 ? [2, 3, 4] : [2, 3];
|
||||
if (pageCount === 5) {
|
||||
firstLinksToCreate = [2, 3, 4];
|
||||
} else if (pageCount === 3) {
|
||||
firstLinksToCreate = [2];
|
||||
} else {
|
||||
firstLinksToCreate = [2, 3];
|
||||
}
|
||||
}
|
||||
|
||||
if (activePage === 4 && pageCount >= 3) {
|
||||
@ -71,15 +77,15 @@ const Pagination = ({ pagination: { pageCount } }) => {
|
||||
lastLinksToCreate = [pageCount - 1];
|
||||
}
|
||||
|
||||
if (activePage === pageCount - 2 && pageCount >= 3) {
|
||||
if (activePage === pageCount - 2 && pageCount > 3) {
|
||||
lastLinksToCreate = [activePage + 1, activePage, activePage - 1];
|
||||
}
|
||||
|
||||
if (activePage === pageCount - 3 && pageCount >= 3 && activePage > 5) {
|
||||
if (activePage === pageCount - 3 && pageCount > 3 && activePage > 5) {
|
||||
lastLinksToCreate = [activePage + 2, activePage + 1, activePage, activePage - 1];
|
||||
}
|
||||
|
||||
if (activePage === pageCount - 1 && pageCount >= 3) {
|
||||
if (activePage === pageCount - 1 && pageCount > 3) {
|
||||
lastLinksToCreate = [activePage, activePage - 1];
|
||||
}
|
||||
|
||||
@ -99,7 +105,11 @@ const Pagination = ({ pagination: { pageCount } }) => {
|
||||
);
|
||||
});
|
||||
|
||||
if (![1, 2].includes(activePage) && activePage < pageCount - 3) {
|
||||
if (
|
||||
![1, 2].includes(activePage) &&
|
||||
activePage <= pageCount - 3 &&
|
||||
firstLinks.length + lastLinks.length < 6
|
||||
) {
|
||||
const middleLinksToCreate = [activePage - 1, activePage, activePage + 1];
|
||||
|
||||
middleLinksToCreate.forEach(number => {
|
||||
|
@ -283,6 +283,7 @@ exports[`DynamicTable renders and matches the snapshot 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Entries per page"
|
||||
aria-labelledby="select-1-label select-1-content"
|
||||
class="c6"
|
||||
id="select-1"
|
||||
|
@ -0,0 +1,44 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useQueryParams } from '@strapi/helper-plugin';
|
||||
import { SearchIcon } from '@strapi/icons';
|
||||
import { IconButton } from '@strapi/parts/IconButton';
|
||||
import { TextInput } from '@strapi/parts/TextInput';
|
||||
|
||||
const Search = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [{ query }, setQuery] = useQueryParams();
|
||||
const [value, setValue] = useState(query._q || '');
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
if (value) {
|
||||
setQuery({ _q: value, page: 1 });
|
||||
} else {
|
||||
setQuery({ _q: '' }, 'remove');
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(prev => !prev);
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<TextInput
|
||||
onBlur={() => setIsOpen(false)}
|
||||
name="search"
|
||||
onChange={({ target: { value } }) => setValue(value)}
|
||||
type="text"
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <IconButton icon={<SearchIcon />} label="Search" onClick={handleToggle} />;
|
||||
};
|
||||
|
||||
export default Search;
|
@ -1,42 +1,36 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
CustomContentLayout,
|
||||
LoadingIndicatorPage,
|
||||
useRBAC,
|
||||
SettingsPageTitle,
|
||||
useNotification,
|
||||
useFocusWhenNavigate,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Button, HeaderLayout, Main } from '@strapi/parts';
|
||||
import { Button, Box, HeaderLayout, Main, Row } from '@strapi/parts';
|
||||
import { Mail } from '@strapi/icons';
|
||||
import {
|
||||
// useHistory,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
import get from 'lodash/get';
|
||||
import adminPermissions from '../../../permissions';
|
||||
import DynamicTable from './DynamicTable';
|
||||
import Filters from './Filters';
|
||||
import Search from './Search';
|
||||
import PaginationFooter from './PaginationFooter';
|
||||
import fetchData from './utils/api';
|
||||
import { deleteData, fetchData } from './utils/api';
|
||||
import displayedFilters from './utils/displayedFilters';
|
||||
import tableHeaders from './utils/tableHeaders';
|
||||
|
||||
const ListPage = () => {
|
||||
const {
|
||||
allowedActions: { canCreate, canDelete, canRead, canUpdate },
|
||||
} = useRBAC(adminPermissions.settings.users);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const toggleNotification = useNotification();
|
||||
|
||||
// const [isWarningDeleteAllOpened, setIsWarningDeleteAllOpened] = useState(false);
|
||||
// const [isModalOpened, setIsModalOpened] = useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
// const query = useQuery();
|
||||
// const { push } = useHistory();
|
||||
const { search } = useLocation();
|
||||
|
||||
const { status, data, isFetching } = useQuery(['projects', search], () => fetchData(search), {
|
||||
const { status, data, isFetching } = useQuery(['users', search], () => fetchData(search), {
|
||||
enabled: canRead,
|
||||
keepPreviousData: true,
|
||||
retry: false,
|
||||
@ -53,181 +47,21 @@ const ListPage = () => {
|
||||
|
||||
const total = get(data, 'pagination.total', 0);
|
||||
|
||||
// const filters = useMemo(() => {
|
||||
// return getFilters(search);
|
||||
// }, [search]);
|
||||
|
||||
// const [
|
||||
// {
|
||||
// // data,
|
||||
// // dataToDelete,
|
||||
// // isLoading,
|
||||
// pagination: { total },
|
||||
// // shouldRefetchData,
|
||||
// // showModalConfirmButtonLoading,
|
||||
// },
|
||||
// dispatch,
|
||||
// ] = useReducer(reducer, initialState, init);
|
||||
// const pageSize = parseInt(query.get('pageSize') || 10, 10);
|
||||
// const page = parseInt(query.get('page') || 0, 10);
|
||||
// const sort = decodeURIComponent(query.get('sort'));
|
||||
// const _q = decodeURIComponent(query.get('_q') || '');
|
||||
// const getDataRef = useRef();
|
||||
|
||||
// const listRef = useRef();
|
||||
|
||||
// getDataRef.current = async () => {
|
||||
// if (!canRead) {
|
||||
// dispatch({
|
||||
// type: 'UNSET_IS_LOADING',
|
||||
// });
|
||||
|
||||
// return;
|
||||
// }
|
||||
// // Show the loading state and reset the state
|
||||
// dispatch({
|
||||
// type: 'GET_DATA',
|
||||
// });
|
||||
|
||||
// try {
|
||||
// const {
|
||||
// data: { results, pagination },
|
||||
// } = await request(`/admin/users${search}`, { method: 'GET' });
|
||||
|
||||
// dispatch({
|
||||
// type: 'GET_DATA_SUCCEEDED',
|
||||
// data: results,
|
||||
// pagination,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// console.error(err.response);
|
||||
// toggleNotification({
|
||||
// type: 'warning',
|
||||
// message: { id: 'notification.error' },
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!isLoadingForPermissions) {
|
||||
// getDataRef.current();
|
||||
// }
|
||||
// }, [search, isLoadingForPermissions]);
|
||||
|
||||
// const handleChangeDataToDelete = ids => {
|
||||
// dispatch({
|
||||
// type: 'ON_CHANGE_DATA_TO_DELETE',
|
||||
// dataToDelete: ids,
|
||||
// });
|
||||
// };
|
||||
|
||||
// const handleChangeFilter = ({ filter, name, value }) => {
|
||||
// const filterName = `${name}${filter}`;
|
||||
|
||||
// updateSearchParams(filterName, encodeURIComponent(value), true);
|
||||
// };
|
||||
|
||||
// const handleChangeFooterParams = ({ target: { name, value } }) => {
|
||||
// let paramName = name.split('.')[1].replace('_', '');
|
||||
|
||||
// if (paramName === 'limit') {
|
||||
// paramName = 'pageSize';
|
||||
// }
|
||||
|
||||
// updateSearchParams(paramName, value);
|
||||
// };
|
||||
|
||||
// const handleChangeSort = ({ target: { name, value } }) => {
|
||||
// updateSearchParams(name, value);
|
||||
// };
|
||||
|
||||
// const handleClickDeleteFilter = ({ target: { name } }) => {
|
||||
// const currentSearch = new URLSearchParams(search);
|
||||
|
||||
// currentSearch.delete(name);
|
||||
|
||||
// push({ search: currentSearch.toString() });
|
||||
// };
|
||||
|
||||
// const handleClickDelete = useCallback(id => {
|
||||
// handleToggleModal();
|
||||
|
||||
// dispatch({
|
||||
// type: 'ON_CHANGE_DATA_TO_DELETE',
|
||||
// dataToDelete: [id],
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// const handleCloseModal = () => {
|
||||
// // Refetch data
|
||||
// getDataRef.current();
|
||||
// };
|
||||
|
||||
// const handleClosedModalDelete = () => {
|
||||
// if (shouldRefetchData) {
|
||||
// getDataRef.current();
|
||||
// } else {
|
||||
// // Empty the selected ids when the modal closes
|
||||
// dispatch({
|
||||
// type: 'RESET_DATA_TO_DELETE',
|
||||
// });
|
||||
|
||||
// // Reset the list's reducer dataToDelete state using a ref so we don't need an effect
|
||||
// listRef.current.resetDataToDelete();
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleConfirmDeleteData = useCallback(async () => {
|
||||
// dispatch({
|
||||
// type: 'ON_DELETE_USERS',
|
||||
// });
|
||||
|
||||
// let shouldDispatchSucceededAction = false;
|
||||
|
||||
// try {
|
||||
// await request('/admin/users/batch-delete', {
|
||||
// method: 'POST',
|
||||
// body: {
|
||||
// ids: dataToDelete,
|
||||
// },
|
||||
// });
|
||||
// shouldDispatchSucceededAction = true;
|
||||
// } catch (err) {
|
||||
// const errorMessage = get(err, 'response.payload.data', 'An error occured');
|
||||
|
||||
// toggleNotification({
|
||||
// type: 'warning',
|
||||
// message: errorMessage,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Only dispatch the action once
|
||||
// if (shouldDispatchSucceededAction) {
|
||||
// dispatch({
|
||||
// type: 'ON_DELETE_USERS_SUCCEEDED',
|
||||
// });
|
||||
// }
|
||||
|
||||
// handleToggleModal();
|
||||
// }, [dataToDelete, toggleNotification]);
|
||||
|
||||
// const handleToggle = () => setIsModalOpened(prev => !prev);
|
||||
|
||||
// const handleToggleModal = () => setIsWarningDeleteAllOpened(prev => !prev);
|
||||
|
||||
// const updateSearchParams = (name, value, shouldDeleteSearch = false) => {
|
||||
// const currentSearch = new URLSearchParams(search);
|
||||
// // Update the currentSearch
|
||||
// currentSearch.set(name, value);
|
||||
|
||||
// if (shouldDeleteSearch) {
|
||||
// currentSearch.delete('_q');
|
||||
// }
|
||||
|
||||
// push({
|
||||
// search: currentSearch.toString(),
|
||||
// });
|
||||
// };
|
||||
const deleteAllMutation = useMutation(ids => deleteData(ids), {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries(['users', search]);
|
||||
},
|
||||
onError: err => {
|
||||
if (err?.response?.data?.data) {
|
||||
toggleNotification({ type: 'warning', message: err.response.data.data });
|
||||
} else {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: { id: 'notification.error', defaultMessage: 'An error occured' },
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// This can be improved but we need to show an something to the user
|
||||
const isLoading =
|
||||
@ -266,16 +100,26 @@ const ListPage = () => {
|
||||
{ number: total }
|
||||
)}
|
||||
/>
|
||||
<CustomContentLayout action={createAction} canRead={canRead}>
|
||||
<CustomContentLayout canRead={canRead}>
|
||||
{status === 'error' && <div>TODO: An error occurred</div>}
|
||||
{canRead && isLoading ? (
|
||||
<LoadingIndicatorPage />
|
||||
) : (
|
||||
{canRead && (
|
||||
<>
|
||||
<Box paddingBottom={4}>
|
||||
<Row style={{ flexWrap: 'wrap' }}>
|
||||
<Search />
|
||||
<Filters displayedFilters={displayedFilters} />
|
||||
</Row>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{canRead && (
|
||||
<>
|
||||
<DynamicTable
|
||||
canCreate={canCreate}
|
||||
canDelete={canDelete}
|
||||
canUpdate={canUpdate}
|
||||
isLoading={isLoading}
|
||||
onConfirmDeleteAll={deleteAllMutation.mutateAsync}
|
||||
headers={tableHeaders}
|
||||
rows={data?.results}
|
||||
withBulkActions
|
||||
|
@ -1,57 +0,0 @@
|
||||
/* eslint-disable consistent-return */
|
||||
import produce from 'immer';
|
||||
|
||||
const initialState = {
|
||||
data: [],
|
||||
dataToDelete: [],
|
||||
isLoading: true,
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageCount: 0,
|
||||
total: 0,
|
||||
},
|
||||
showModalConfirmButtonLoading: false,
|
||||
shouldRefetchData: false,
|
||||
};
|
||||
|
||||
const reducer = (state, action) =>
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case 'GET_DATA': {
|
||||
return initialState;
|
||||
}
|
||||
case 'GET_DATA_SUCCEEDED': {
|
||||
draftState.data = action.data;
|
||||
draftState.isLoading = false;
|
||||
draftState.pagination = action.pagination;
|
||||
break;
|
||||
}
|
||||
case 'ON_CHANGE_DATA_TO_DELETE': {
|
||||
draftState.dataToDelete = action.dataToDelete;
|
||||
break;
|
||||
}
|
||||
case 'ON_DELETE_USERS': {
|
||||
draftState.showModalConfirmButtonLoading = true;
|
||||
break;
|
||||
}
|
||||
case 'ON_DELETE_USERS_SUCCEEDED': {
|
||||
draftState.shouldRefetchData = true;
|
||||
break;
|
||||
}
|
||||
case 'RESET_DATA_TO_DELETE': {
|
||||
draftState.shouldRefetchData = false;
|
||||
draftState.dataToDelete = [];
|
||||
draftState.showModalConfirmButtonLoading = false;
|
||||
break;
|
||||
}
|
||||
case 'UNSET_IS_LOADING': {
|
||||
draftState.isLoading = false;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return draftState;
|
||||
}
|
||||
});
|
||||
|
||||
export { initialState, reducer };
|
File diff suppressed because one or more lines are too long
@ -1,182 +0,0 @@
|
||||
import { reducer } from '../reducer';
|
||||
|
||||
describe('ADMIN | CONTAINERS | USERS | ListPage | reducer', () => {
|
||||
describe('DEFAULT_ACTION', () => {
|
||||
it('should return the initialState', () => {
|
||||
const initialState = {
|
||||
test: true,
|
||||
};
|
||||
|
||||
expect(reducer(initialState, {})).toEqual(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET_DATA', () => {
|
||||
it('should return the initialState', () => {
|
||||
const initialState = {
|
||||
test: true,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: 'GET_DATA',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
data: [],
|
||||
dataToDelete: [],
|
||||
isLoading: true,
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageCount: 0,
|
||||
total: 0,
|
||||
},
|
||||
showModalConfirmButtonLoading: false,
|
||||
shouldRefetchData: false,
|
||||
};
|
||||
|
||||
expect(reducer(initialState, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET_DATA_SUCCEEDED', () => {
|
||||
it('Should set the data correctly', () => {
|
||||
const action = {
|
||||
type: 'GET_DATA_SUCCEEDED',
|
||||
data: [1, 2, 3],
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageCount: 5,
|
||||
total: 20,
|
||||
},
|
||||
};
|
||||
const initialState = {
|
||||
data: [],
|
||||
dataToDelete: [],
|
||||
isLoading: true,
|
||||
pagination: {},
|
||||
};
|
||||
const expected = {
|
||||
data: [1, 2, 3],
|
||||
dataToDelete: [],
|
||||
isLoading: false,
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageCount: 5,
|
||||
total: 20,
|
||||
},
|
||||
};
|
||||
|
||||
expect(reducer(initialState, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ON_CHANGE_DATA_TO_DELETE', () => {
|
||||
it('should change the data correctly', () => {
|
||||
const initialState = {
|
||||
data: [],
|
||||
dataToDelete: [],
|
||||
isLoading: true,
|
||||
};
|
||||
const action = {
|
||||
type: 'ON_CHANGE_DATA_TO_DELETE',
|
||||
dataToDelete: [1, 2],
|
||||
};
|
||||
const expected = {
|
||||
data: [],
|
||||
dataToDelete: [1, 2],
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
expect(reducer(initialState, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ON_DELETE_USERS', () => {
|
||||
it('should set the showModalConfirmButtonLoading to true', () => {
|
||||
const initialState = {
|
||||
data: [1],
|
||||
dataToDelete: [1],
|
||||
showModalConfirmButtonLoading: false,
|
||||
};
|
||||
const action = {
|
||||
type: 'ON_DELETE_USERS',
|
||||
};
|
||||
const expected = {
|
||||
data: [1],
|
||||
dataToDelete: [1],
|
||||
showModalConfirmButtonLoading: true,
|
||||
};
|
||||
|
||||
expect(reducer(initialState, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ON_DELETE_USERS_SUCCEEDED', () => {
|
||||
it('should set the shouldRefetchData to true', () => {
|
||||
const initialState = {
|
||||
data: [1],
|
||||
dataToDelete: [1],
|
||||
showModalConfirmButtonLoading: true,
|
||||
shouldRefetchData: false,
|
||||
};
|
||||
const action = {
|
||||
type: 'ON_DELETE_USERS_SUCCEEDED',
|
||||
};
|
||||
const expected = {
|
||||
data: [1],
|
||||
dataToDelete: [1],
|
||||
showModalConfirmButtonLoading: true,
|
||||
shouldRefetchData: true,
|
||||
};
|
||||
|
||||
expect(reducer(initialState, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RESET_DATA_TO_DELETE', () => {
|
||||
it('should empty the dataToDelete array', () => {
|
||||
const initialState = {
|
||||
data: [1],
|
||||
dataToDelete: [1],
|
||||
showModalConfirmButtonLoading: true,
|
||||
shouldRefetchData: true,
|
||||
};
|
||||
const action = {
|
||||
type: 'RESET_DATA_TO_DELETE',
|
||||
};
|
||||
const expected = {
|
||||
data: [1],
|
||||
dataToDelete: [],
|
||||
showModalConfirmButtonLoading: false,
|
||||
shouldRefetchData: false,
|
||||
};
|
||||
|
||||
expect(reducer(initialState, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UNSET_IS_LOADING', () => {
|
||||
it('should set the isLoading to false', () => {
|
||||
const action = {
|
||||
type: 'UNSET_IS_LOADING',
|
||||
};
|
||||
const initialState = {
|
||||
data: [],
|
||||
dataToDelete: [],
|
||||
isLoading: true,
|
||||
pagination: {},
|
||||
};
|
||||
const expected = {
|
||||
data: [],
|
||||
dataToDelete: [],
|
||||
isLoading: false,
|
||||
pagination: {},
|
||||
};
|
||||
|
||||
expect(reducer(initialState, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,15 +1,15 @@
|
||||
import { axiosInstance } from '../../../../core/utils';
|
||||
|
||||
const fetchData = async search => {
|
||||
try {
|
||||
const {
|
||||
data: { data },
|
||||
} = await axiosInstance.get(`/admin/users${search}`);
|
||||
const {
|
||||
data: { data },
|
||||
} = await axiosInstance.get(`/admin/users${search}`);
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export default fetchData;
|
||||
const deleteData = async ids => {
|
||||
await axiosInstance.post('/admin/users/batch-delete', { ids });
|
||||
};
|
||||
|
||||
export { deleteData, fetchData };
|
||||
|
@ -0,0 +1,29 @@
|
||||
const displayedFilters = [
|
||||
{
|
||||
name: 'firstname',
|
||||
metadatas: { label: 'Firstname' },
|
||||
fieldSchema: { type: 'string' },
|
||||
},
|
||||
{
|
||||
name: 'lastname',
|
||||
metadatas: { label: 'Lastname' },
|
||||
fieldSchema: { type: 'string' },
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
metadatas: { label: 'Email' },
|
||||
fieldSchema: { type: 'email' },
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
metadatas: { label: 'Username' },
|
||||
fieldSchema: { type: 'string' },
|
||||
},
|
||||
{
|
||||
name: 'isActive',
|
||||
metadatas: { label: 'Active user' },
|
||||
fieldSchema: { type: 'boolean' },
|
||||
},
|
||||
];
|
||||
|
||||
export default displayedFilters;
|
@ -1,30 +0,0 @@
|
||||
const getFilters = search => {
|
||||
const query = new URLSearchParams(search);
|
||||
const filters = [];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (let [key, queryValue] of query.entries()) {
|
||||
if (!['sort', 'pageSize', 'page', '_q'].includes(key)) {
|
||||
const splitted = key.split('_');
|
||||
let filterName;
|
||||
let filterType;
|
||||
|
||||
// Filter type === '=')
|
||||
if (splitted.length === 1) {
|
||||
filterType = '=';
|
||||
filterName = key;
|
||||
} else {
|
||||
filterType = `_${splitted[1]}`;
|
||||
filterName = splitted[0];
|
||||
}
|
||||
|
||||
const value = decodeURIComponent(queryValue);
|
||||
|
||||
filters.push({ displayName: filterName, name: key, filter: filterType, value });
|
||||
}
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
export default getFilters;
|
@ -1,29 +0,0 @@
|
||||
import getFilters from '../getFilters';
|
||||
|
||||
describe('ADMIN | CONTAINERS | USERS | ListPage | utils | getFilters', () => {
|
||||
it('should return an empty array if there is not filter', () => {
|
||||
const search = '_q=test&sort=firstname&page=1&pageSize=1';
|
||||
|
||||
expect(getFilters(search)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle the = filter correctly ', () => {
|
||||
const search = 'sort=firstname&page=1&pageSize=1&firstname=test&firstname_ne=something';
|
||||
const expected = [
|
||||
{
|
||||
displayName: 'firstname',
|
||||
name: 'firstname',
|
||||
filter: '=',
|
||||
value: 'test',
|
||||
},
|
||||
{
|
||||
displayName: 'firstname',
|
||||
name: 'firstname_ne',
|
||||
filter: '_ne',
|
||||
value: 'something',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getFilters(search)).toEqual(expected);
|
||||
});
|
||||
});
|
@ -420,8 +420,7 @@
|
||||
"content-manager.components.SettingsViewWrapper.pluginHeader.title": "Configure the view - {name}",
|
||||
"content-manager.components.TableDelete.delete": "Delete all",
|
||||
"content-manager.components.TableDelete.deleteSelected": "Delete selected",
|
||||
"content-manager.components.TableDelete.entries.plural": "{number} entries selected",
|
||||
"content-manager.components.TableDelete.entries.singular": "{number} entry selected",
|
||||
"content-manager.components.TableDelete.label": "{number, plural, one {# entry} other {# entries}} selected",
|
||||
"content-manager.components.TableEmpty.withFilters": "There are no {contentType} with the applied filters...",
|
||||
"content-manager.components.TableEmpty.withSearch": "There are no {contentType} corresponding to the search ({search})...",
|
||||
"content-manager.components.TableEmpty.withoutFilter": "There are no {contentType}...",
|
||||
|
@ -19,7 +19,7 @@ import EmptyBodyTable from './index';
|
||||
|
||||
This component is used to display an empty state in a table.
|
||||
|
||||
## Usage
|
||||
## Usage default
|
||||
|
||||
<Canvas>
|
||||
<Story name="base">
|
||||
@ -31,6 +31,18 @@ This component is used to display an empty state in a table.
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Usage loading
|
||||
|
||||
<Canvas>
|
||||
<Story name="loading">
|
||||
<Box>
|
||||
<Table colCount={1} rowCount={2} footer={undefined}>
|
||||
<EmptyBodyTable isLoading />
|
||||
</Table>
|
||||
</Box>
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={EmptyBodyTable} />
|
||||
|
@ -1,10 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Tbody, Tr, Td } from '@strapi/parts/Table';
|
||||
import styled from 'styled-components';
|
||||
import { Box, Row, Loader } from '@strapi/parts';
|
||||
import EmptyStateLayout from '../EmptyStateLayout';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const EmptyBodyTable = ({ colSpan, ...rest }) => {
|
||||
const EmptyBodyTable = ({ colSpan, isLoading, ...rest }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td colSpan={colSpan}>
|
||||
<Row justifyContent="center">
|
||||
<Box padding={11} background="neutral0">
|
||||
<Loader />
|
||||
</Box>
|
||||
</Row>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tbody>
|
||||
<Tr>
|
||||
@ -20,8 +35,8 @@ EmptyBodyTable.defaultProps = {
|
||||
action: undefined,
|
||||
colSpan: 1,
|
||||
content: undefined,
|
||||
|
||||
icon: undefined,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
EmptyBodyTable.propTypes = {
|
||||
@ -33,6 +48,7 @@ EmptyBodyTable.propTypes = {
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
icon: PropTypes.oneOf(['document', 'media', 'permissions']),
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default EmptyBodyTable;
|
||||
|
Loading…
x
Reference in New Issue
Block a user