diff --git a/packages/core/admin/admin/src/pages/Roles/ListPage/tests/index.test.js b/packages/core/admin/admin/src/pages/Roles/ListPage/tests/index.test.js index b7e3e88e12..a57ef79dc9 100644 --- a/packages/core/admin/admin/src/pages/Roles/ListPage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/Roles/ListPage/tests/index.test.js @@ -54,10 +54,6 @@ describe('', () => { } = render(App); expect(firstChild).toMatchInlineSnapshot(` - .c37 { - font-family: Lato; - } - .c5 { font-weight: 600; font-size: 2rem; @@ -404,6 +400,10 @@ describe('', () => { border: none; } + .c37 { + font-family: Lato; + } +
{ + const { formatMessage } = useIntl(); + + if (!show) { + return null; + } + + return ( + + }> + + + + {formatMessage({ + id: 'Settings.webhooks.confirmation.delete', + defaultMessage: 'Are you sure you want to delete this?', + })} + + + + + + {formatMessage({ id: 'app.components.Button.cancel', defaultMessage: 'Cancel' })} + + } + endAction={ + + } + /> + + ); +}; + +ConfirmDialog.defaultProps = { + isConfirmButtonLoading: false, +}; + +ConfirmDialog.propTypes = { + isConfirmButtonLoading: PropTypes.bool, + onConfirm: PropTypes.func.isRequired, + onToggle: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, +}; + +export default ConfirmDialog; diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/TableRows/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/TableRows/index.js index cc32180f6b..14daa6260d 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/TableRows/index.js +++ b/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/TableRows/index.js @@ -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 ( @@ -55,7 +58,7 @@ const TableRows = ({ {canUpdate && ( push(`${pathname}/${data.id}`)} - label="Edit" + label={formatMessage({ id: 'app.utils.edit', defaultMessage: 'Edit' })} noBorder icon={} /> @@ -63,8 +66,8 @@ const TableRows = ({ {canDelete && ( console.log('delete')} - label="Delete" + onClick={() => onClickDelete(data.id)} + label={formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' })} noBorder icon={} /> @@ -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, diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/index.js index 6055873365..5e9ec9920a 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/index.js +++ b/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/index.js @@ -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 ( - - - {!rows.length ? ( - - ) : ( - + {entriesToDelete.length > 0 && ( + + + + + + {formatMessage( + { + id: 'content-manager.components.TableDelete.label', + defaultMessage: '{number, plural, one {# entry} other {# entries}} selected', + }, + { number: entriesToDelete.length } + )} + + + + + + + )} + + - )} - + {!rows.length || isLoading ? ( + + ) : ( + + )} + + + + ); }; 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, diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/tests/index.test.js b/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/tests/index.test.js index 9fd53fa5cf..6b45b8a296 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/Users/ListPage/DynamicTable/tests/index.test.js @@ -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; + } +
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 ( + + +
+ + + + + + + + + setModifiedData(prev => ({ ...prev, value }))} + /> + + + } type="submit"> + {formatMessage({ id: 'app.utils.add-filter', defaultMessage: 'Add filter' })} + + + +
+
+
+ ); +}; + +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 ( + <> + + + {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/PaginationFooter/Pagination.js b/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/Pagination.js index cceed00375..dd85d971d5 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/Pagination.js +++ b/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/Pagination.js @@ -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 => { diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/tests/__snapshots__/index.test.js.snap b/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/tests/__snapshots__/index.test.js.snap index 1f11ca712b..c7e6808981 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/tests/__snapshots__/index.test.js.snap +++ b/packages/core/admin/admin/src/pages/Users/ListPage/PaginationFooter/tests/__snapshots__/index.test.js.snap @@ -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" diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Search/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/Search/index.js new file mode 100644 index 0000000000..eafce8e68e --- /dev/null +++ b/packages/core/admin/admin/src/pages/Users/ListPage/Search/index.js @@ -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 ( + setIsOpen(false)} + name="search" + onChange={({ target: { value } }) => setValue(value)} + type="text" + value={value} + /> + ); + } + + return } label="Search" onClick={handleToggle} />; +}; + +export default Search; diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/index.js index c256c1d741..29e6801671 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/index.js +++ b/packages/core/admin/admin/src/pages/Users/ListPage/index.js @@ -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 } )} /> - + {status === 'error' &&
TODO: An error occurred
} - {canRead && isLoading ? ( - - ) : ( + {canRead && ( + <> + + + + + + + + )} + {canRead && ( <> - 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 }; diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/tests/index.test.js b/packages/core/admin/admin/src/pages/Users/ListPage/tests/index.test.js index 22ff08826c..2da0e6561c 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/Users/ListPage/tests/index.test.js @@ -64,46 +64,6 @@ describe('ADMIN | Pages | USERS | ListPage', () => { const { container } = render(app); expect(container.firstChild).toMatchInlineSnapshot(` - .c13 { - padding-right: 56px; - padding-left: 56px; - } - - .c15 { - border: 0; - -webkit-clip: rect(0 0 0 0); - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - } - - .c16 { - -webkit-animation: gzYjWD 1s infinite linear; - animation: gzYjWD 1s infinite linear; - } - - .c14 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-pack: space-around; - -webkit-justify-content: space-around; - -ms-flex-pack: space-around; - justify-content: space-around; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - } - .c5 { font-weight: 600; font-size: 2rem; @@ -125,11 +85,37 @@ describe('ADMIN | Pages | USERS | ListPage', () => { color: #666687; } + .c30 { + font-weight: 400; + font-size: 0.875rem; + line-height: 1.43; + color: #32324d; + } + + .c51 { + font-weight: 400; + font-size: 0.875rem; + line-height: 1.43; + color: #8e8ea9; + } + .c12 { font-size: 1rem; line-height: 1.5; } + .c31 { + font-weight: 600; + line-height: 1.14; + } + + .c32 { + font-weight: 600; + font-size: 0.6875rem; + line-height: 1.45; + text-transform: uppercase; + } + .c2 { background: #f6f6f9; padding-top: 56px; @@ -142,6 +128,45 @@ describe('ADMIN | Pages | USERS | ListPage', () => { padding-right: 8px; } + .c14 { + padding-bottom: 16px; + } + + .c17 { + padding: 4px; + } + + .c19 { + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + } + + .c20 { + background: #ffffff; + } + + .c22 { + padding-right: 24px; + padding-left: 24px; + } + + .c40 { + background: #ffffff; + padding: 64px; + } + + .c42 { + padding-top: 24px; + } + + .c47 { + padding-right: 16px; + padding-left: 16px; + } + + .c48 { + padding-left: 12px; + } + .c3 { display: -webkit-box; display: -webkit-flex; @@ -174,6 +199,24 @@ describe('ADMIN | Pages | USERS | ListPage', () => { align-items: center; } + .c39 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + .c6 { display: -webkit-box; display: -webkit-flex; @@ -200,6 +243,85 @@ describe('ADMIN | Pages | USERS | ListPage', () => { pointer-events: none; } + .c28 { + margin: 0; + height: 18px; + min-width: 18px; + border-radius: 4px; + border: 1px solid #c0c0cf; + -webkit-appearance: none; + background-color: #ffffff; + } + + .c28:checked { + background-color: #4945ff; + border: 1px solid #4945ff; + } + + .c28:checked:after { + content: ''; + display: block; + position: relative; + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSJ3aGl0ZSIKICAvPgo8L3N2Zz4=) no-repeat no-repeat center center; + width: 10px; + height: 10px; + left: 50%; + top: 50%; + -webkit-transform: translateX(-50%) translateY(-50%); + -ms-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); + } + + .c28:checked:disabled:after { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSIjOEU4RUE5IgogIC8+Cjwvc3ZnPg==) no-repeat no-repeat center center; + } + + .c28:disabled { + background-color: #dcdce4; + border: 1px solid #c0c0cf; + } + + .c28:indeterminate { + background-color: #4945ff; + border: 1px solid #4945ff; + } + + .c28:indeterminate:after { + content: ''; + display: block; + position: relative; + color: white; + height: 2px; + width: 10px; + background-color: #ffffff; + left: 50%; + top: 50%; + -webkit-transform: translateX(-50%) translateY(-50%); + -ms-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); + } + + .c28:indeterminate:disabled { + background-color: #dcdce4; + border: 1px solid #c0c0cf; + } + + .c28:indeterminate:disabled:after { + background-color: #8e8ea9; + } + + .c35 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } + .c7 { padding: 8px 16px; background: #4945ff; @@ -261,10 +383,418 @@ describe('ADMIN | Pages | USERS | ListPage', () => { background: #4945ff; } + .c18 { + padding: 8px 16px; + background: #4945ff; + border: none; + border: 1px solid #dcdce4; + background: #ffffff; + } + + .c18 .c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c18 .c9 { + color: #ffffff; + } + + .c18[aria-disabled='true'] { + border: 1px solid #dcdce4; + background: #eaeaef; + } + + .c18[aria-disabled='true'] .c9 { + color: #666687; + } + + .c18[aria-disabled='true'] svg > g, + .c18[aria-disabled='true'] svg path { + fill: #666687; + } + + .c18[aria-disabled='true']:active { + border: 1px solid #dcdce4; + background: #eaeaef; + } + + .c18[aria-disabled='true']:active .c9 { + color: #666687; + } + + .c18[aria-disabled='true']:active svg > g, + .c18[aria-disabled='true']:active svg path { + fill: #666687; + } + + .c18:hover { + background-color: #f6f6f9; + } + + .c18:active { + background-color: #eaeaef; + } + + .c18 .c9 { + color: #32324d; + } + + .c18 svg > g, + .c18 svg path { + fill: #32324d; + } + + .c43 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + } + + .c43 > * { + margin-top: 0; + margin-bottom: 0; + } + + .c43 > * + * { + margin-top: 0px; + } + + .c33 { + border: none; + } + + .c33 svg > g, + .c33 svg path { + fill: #8e8ea9; + } + + .c33:hover svg > g, + .c33:hover svg path { + fill: #666687; + } + + .c33:active svg > g, + .c33:active svg path { + fill: #a5a5ba; + } + + .c33[aria-disabled='true'] { + background-color: #eaeaef; + } + + .c33[aria-disabled='true'] svg path { + fill: #666687; + } + + .c41 { + -webkit-animation: gzYjWD 1s infinite linear; + animation: gzYjWD 1s infinite linear; + } + .c0 { outline: none; } + .c53 > * + * { + margin-left: 4px; + } + + .c58 { + line-height: revert; + } + + .c54 { + padding: 12px; + border-radius: 4px; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .c56 { + padding: 12px; + border-radius: 4px; + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .c57 { + color: #271fe0; + background: #ffffff; + } + + .c57:hover { + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + } + + .c55 { + font-size: 0.7rem; + pointer-events: none; + } + + .c55 svg path { + fill: #c0c0cf; + } + + .c55:focus svg path, + .c55:hover svg path { + fill: #c0c0cf; + } + + .c59 { + font-size: 0.7rem; + } + + .c59 svg path { + fill: #666687; + } + + .c59:focus svg path, + .c59:hover svg path { + fill: #4a4a6a; + } + + .c45 { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + width: 100%; + background: transparent; + border: none; + } + + .c45:focus { + outline: none; + } + + .c44 { + position: relative; + border: 1px solid #dcdce4; + padding-right: 12px; + border-radius: 4px; + background: #ffffff; + overflow: hidden; + } + + .c44:focus-within { + border: 1px solid #4945ff; + } + + .c49 { + background: transparent; + border: none; + position: relative; + z-index: 1; + } + + .c49 svg { + height: 0.6875rem; + width: 0.6875rem; + } + + .c49 svg path { + fill: #666687; + } + + .c50 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + background: none; + border: none; + } + + .c50 svg { + width: 0.375rem; + } + + .c46 { + min-height: 2.5rem; + } + + .c24 { + width: 100%; + white-space: nowrap; + } + + .c21 { + position: relative; + border-radius: 4px 4px 0 0; + } + + .c21:before { + background: linear-gradient(90deg,#000000 0%,rgba(0,0,0,0) 100%); + opacity: 0.2; + position: absolute; + height: 100%; + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + width: 8px; + left: 0; + } + + .c21:after { + background: linear-gradient(270deg,#000000 0%,rgba(0,0,0,0) 100%); + opacity: 0.2; + position: absolute; + height: 100%; + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + width: 8px; + right: 0; + top: 0; + } + + .c23 { + overflow-x: auto; + } + + .c25 { + border-bottom: 1px solid #eaeaef; + } + + .c26 { + border-bottom: 1px solid #eaeaef; + } + + .c26 td, + .c26 th { + padding: 16px; + } + + .c26 td:first-of-type, + .c26 th:first-of-type { + padding: 0 4px; + } + + .c27 { + vertical-align: middle; + text-align: left; + color: #666687; + outline-offset: -4px; + } + + .c27 input { + vertical-align: sub; + } + + .c29 svg { + height: 0.25rem; + } + + .c13 { + padding-right: 56px; + padding-left: 56px; + } + + .c36 tr:last-of-type { + border-bottom: none; + } + + .c37 { + border-bottom: 1px solid #eaeaef; + } + + .c37 td, + .c37 th { + padding: 16px; + } + + .c37 td:first-of-type, + .c37 th:first-of-type { + padding: 0 4px; + } + + .c38 { + vertical-align: middle; + text-align: left; + color: #666687; + outline-offset: -4px; + } + + .c38 input { + vertical-align: sub; + } + + .c34 { + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + + .c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + cursor: pointer; + padding: 8px; + border-radius: 4px; + background: #ffffff; + border: 1px solid #dcdce4; + } + + .c15 svg { + height: 12px; + width: 12px; + } + + .c15 svg > g, + .c15 svg path { + fill: #ffffff; + } + + .c15[aria-disabled='true'] { + pointer-events: none; + } + + .c16 svg > g, + .c16 svg path { + fill: #8e8ea9; + } + + .c16:hover svg > g, + .c16:hover svg path { + fill: #666687; + } + + .c16:active svg > g, + .c16:active svg path { + fill: #a5a5ba; + } + + .c16[aria-disabled='true'] { + background-color: #eaeaef; + } + + .c16[aria-disabled='true'] svg path { + fill: #666687; + } + + .c52 { + margin-left: 5px; + } +
{ class="c13" >
+
+
+
- Loading content. + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+ + + Firstname + + + + + + + +
+
+
+ + + + +
+
+
+ + + + +
+
+
+ + + Roles + + + +
+
+
+ + + + +
+
+
+ + + Active User + + + +
+
+
+
+ Actions +
+ +
+
+
+
+
+
+ +
+
+
+
- +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
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 @@ -31,6 +31,18 @@ This component is used to display an empty state in a table. +## Usage loading + + + + + + +
+
+
+
+ ## Props diff --git a/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/index.js b/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/index.js index 89fd97aa38..3d9f3b7b29 100644 --- a/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/index.js +++ b/packages/core/helper-plugin/lib/src/components/EmptyBodyTable/index.js @@ -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 ( + + + + + + + + + + + + ); + } return ( @@ -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;