diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterList/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterList/index.js
new file mode 100644
index 0000000000..74de82b164
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterList/index.js
@@ -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
+
handleClick(filter)}>
+ }>
+ {name} {filterType} {value}
+
+
+ );
+ }) || null
+ );
+};
+
+export default FilterList;
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/Inputs.js b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/Inputs.js
new file mode 100644
index 0000000000..3a436d328a
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/Inputs.js
@@ -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 (
+
+ );
+ }
+
+ // TODO improve
+
+ return (
+
+
+ onChange(value)} value={value} size="S" />
+
+
+ );
+};
+
+Inputs.defaultProps = {
+ value: '',
+};
+
+Inputs.propTypes = {
+ onChange: PropTypes.func.isRequired,
+ type: PropTypes.string.isRequired,
+ value: PropTypes.any,
+};
+
+export default Inputs;
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/index.js
new file mode 100644
index 0000000000..19898a6b10
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/index.js
@@ -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 (
+
+
+
+
+
+ );
+};
+
+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;
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/utils/getFilterList.js b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/utils/getFilterList.js
new file mode 100644
index 0000000000..f31cf0fc5c
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/FilterPopover/utils/getFilterList.js
@@ -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;
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Filters/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/index.js
new file mode 100644
index 0000000000..ab3fd773b9
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/Filters/index.js
@@ -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 (
+ <>
+
+ }
+ onClick={handleToggle}
+ size="S"
+ >
+ {formatMessage({ id: 'app.utils.filters', defaultMessage: 'Filters' })}
+
+ {isVisible && (
+
+ )}
+
+
+ >
+ );
+};
+
+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;
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/PageSize.js b/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/PageSize.js
index 8f878178c2..40a309eb9c 100644
--- a/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/PageSize.js
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/PageSize.js
@@ -23,7 +23,7 @@ const PageSize = () => {
return (
-
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/tests/reducer.test.js b/packages/core/admin/admin/src/pages/Users/ListPage/tests/reducer.test.js
deleted file mode 100644
index b54a60244a..0000000000
--- a/packages/core/admin/admin/src/pages/Users/ListPage/tests/reducer.test.js
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/utils/api.js b/packages/core/admin/admin/src/pages/Users/ListPage/utils/api.js
index 7c3a7dd90e..8341825fd3 100644
--- a/packages/core/admin/admin/src/pages/Users/ListPage/utils/api.js
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/utils/api.js
@@ -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 };
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/utils/displayedFilters.js b/packages/core/admin/admin/src/pages/Users/ListPage/utils/displayedFilters.js
new file mode 100644
index 0000000000..660a1747b8
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Users/ListPage/utils/displayedFilters.js
@@ -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;
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/utils/getFilters.js b/packages/core/admin/admin/src/pages/Users/ListPage/utils/getFilters.js
deleted file mode 100644
index 29b451edce..0000000000
--- a/packages/core/admin/admin/src/pages/Users/ListPage/utils/getFilters.js
+++ /dev/null
@@ -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;
diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/utils/tests/getFilters.test.js b/packages/core/admin/admin/src/pages/Users/ListPage/utils/tests/getFilters.test.js
deleted file mode 100644
index 288dc82d06..0000000000
--- a/packages/core/admin/admin/src/pages/Users/ListPage/utils/tests/getFilters.test.js
+++ /dev/null
@@ -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);
- });
-});
diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json
index d6db1fed25..1d359d792c 100644
--- a/packages/core/admin/admin/src/translations/en.json
+++ b/packages/core/admin/admin/src/translations/en.json
@@ -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}...",
diff --git a/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/EmptyBodyTable.stories.mdx b/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/EmptyBodyTable.stories.mdx
index c62b494bca..bf2c2f8022 100644
--- a/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/EmptyBodyTable.stories.mdx
+++ b/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/EmptyBodyTable.stories.mdx
@@ -19,7 +19,7 @@ import EmptyBodyTable from './index';
This component is used to display an empty state in a table.
-## Usage
+## Usage default