diff --git a/packages/core/admin/admin/src/components/Users/List/ActiveStatus.js b/packages/core/admin/admin/src/components/Users/List/ActiveStatus.js index 095d7cd6a9..f698658935 100644 --- a/packages/core/admin/admin/src/components/Users/List/ActiveStatus.js +++ b/packages/core/admin/admin/src/components/Users/List/ActiveStatus.js @@ -8,10 +8,8 @@ const ActiveStatus = styled.div` &:before { content: ''; display: inline-block; - width: 6px; - height: 6px; - margin-bottom: 2px; - margin-right: 10px; + width: 5px; + height: 5px; border-radius: 50%; background-color: ${({ isActive }) => (isActive ? '#38cd29' : '#f64d0a')}; } diff --git a/packages/core/admin/admin/src/components/Users/List/index.js b/packages/core/admin/admin/src/components/Users/List/index.js index 691a94f479..98d57ecc5b 100644 --- a/packages/core/admin/admin/src/components/Users/List/index.js +++ b/packages/core/admin/admin/src/components/Users/List/index.js @@ -91,6 +91,8 @@ const List = forwardRef( push(`/settings/users/${id}`); }; + console.log({ headers }); + return ( { + return ( + + + + + + ); +}; + +export default TableEmpty; diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Table/TableHead/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/Table/TableHead/index.js new file mode 100644 index 0000000000..55a1ade93f --- /dev/null +++ b/packages/core/admin/admin/src/pages/Users/ListPage/Table/TableHead/index.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { + BaseCheckbox, + IconButton, + TableLabel, + Th, + Thead, + Tr, + Tooltip, + VisuallyHidden, +} from '@strapi/parts'; +import { SortIcon, useQueryParams } from '@strapi/helper-plugin'; + +const TableHead = () => { + const [{ query }, setQuery] = useQueryParams(); + const sort = query.sort; + const [sortBy, sortOrder] = sort.split(':'); + const headers = [ + { label: 'Name', value: 'firstname', isSortable: true }, + { label: 'Email', value: 'email', isSortable: true }, + { label: 'Roles', value: 'roles', isSortable: false }, + { label: 'Username', value: 'username', isSortable: true }, + { label: 'Active User', value: 'isActive', isSortable: false }, + ]; + + return ( + + + + {headers.map(({ label, value, isSortable }) => { + const isSorted = sortBy === value; + const isUp = sortOrder === 'ASC'; + + const handleClickSort = (shouldAllowClick = true) => { + if (isSortable && shouldAllowClick) { + const nextSortOrder = isSorted && sortOrder === 'ASC' ? 'DESC' : 'ASC'; + const nextSort = `${value}:${nextSortOrder}`; + + setQuery({ + sort: nextSort, + }); + } + }; + + return ( + + ); + })} + + + + + ); +}; + +export default TableHead; diff --git a/packages/core/admin/admin/src/pages/Users/ListPage/Table/index.js b/packages/core/admin/admin/src/pages/Users/ListPage/Table/index.js new file mode 100644 index 0000000000..21a0ff5a54 --- /dev/null +++ b/packages/core/admin/admin/src/pages/Users/ListPage/Table/index.js @@ -0,0 +1,99 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + BaseCheckbox, + Box, + IconButton, + Table as TableCompo, + Tbody, + Td, + TFooter, + Text, + Tr, + Row, +} from '@strapi/parts'; +import { AddIcon, EditIcon, DeleteIcon } from '@strapi/icons'; +import { Status } from '@strapi/helper-plugin'; +import TableHead from './TableHead'; +import TableEmpty from './TableEmpty'; + +const Table = ({ canCreate, rows }) => { + if (!rows.length) { + return ; + } + + const ROW_COUNT = rows.length; + const COL_COUNT = 7; + + return ( + }>Add another field to this collection type + ) : ( + undefined + ) + } + > + + + {rows.map(entry => ( + + + + + + + + + + ))} + + + ); +}; + +Table.defaultProps = { + rows: [], +}; + +Table.propTypes = { + canCreate: PropTypes.bool.isRequired, + rows: PropTypes.array, +}; + +export default Table; 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 12bb96ba59..dc19483215 100644 --- a/packages/core/admin/admin/src/pages/Users/ListPage/index.js +++ b/packages/core/admin/admin/src/pages/Users/ListPage/index.js @@ -1,56 +1,26 @@ -import React, { - // useCallback, - useEffect, - // useMemo, - useReducer, - useRef, - // useState -} from 'react'; +import React from 'react'; import { - // BaselineAlignment, - // useQuery, - request, useRBAC, LoadingIndicatorPage, - // PopUpWarning, SettingsPageTitle, useNotification, useFocusWhenNavigate, } from '@strapi/helper-plugin'; -import { - Button, - // ContentLayout, - HeaderLayout, - // Table, - // TableLabel, - // Tbody, - // TFooter, - // Th, - // Thead, - // Tr, - // VisuallyHidden, - Main, -} from '@strapi/parts'; +import { Button, ContentLayout, HeaderLayout, Main } from '@strapi/parts'; import { Mail } from '@strapi/icons'; import { // useHistory, useLocation, } from 'react-router-dom'; import { useIntl } from 'react-intl'; -// import get from 'lodash/get'; -// import { Flex, Padded } from '@buffetjs/core'; -// import { useSettingsHeaderSearchContext } from '../../../hooks'; -// import { Footer, List, Filter, FilterPicker, SortPicker } from '../../../components/Users'; +import { useQuery } from 'react-query'; +import get from 'lodash/get'; import adminPermissions from '../../../permissions'; -// import Header from './Header'; -// import ModalForm from './ModalForm'; -// import getFilters from './utils/getFilters'; -import init from './init'; -import { initialState, reducer } from './reducer'; +import Table from './Table'; +import fetchData from './utils/api'; const ListPage = () => { const { - isLoading: isLoadingForPermissions, allowedActions: { canCreate, // canDelete, @@ -59,6 +29,7 @@ const ListPage = () => { }, } = useRBAC(adminPermissions.settings.users); const toggleNotification = useNotification(); + // const [isWarningDeleteAllOpened, setIsWarningDeleteAllOpened] = useState(false); // const [isModalOpened, setIsModalOpened] = useState(false); const { formatMessage } = useIntl(); @@ -66,67 +37,83 @@ const ListPage = () => { // const { push } = useHistory(); const { search } = useLocation(); + const { status, data, isFetching } = useQuery(['projects', search], () => fetchData(search), { + enabled: canRead, + keepPreviousData: true, + retry: false, + staleTime: 5000, + onError: () => { + toggleNotification({ + type: 'warning', + message: { id: 'notification.error', defaultMessage: 'An error occured' }, + }); + }, + }); + useFocusWhenNavigate(); + 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 [ + // { + // // 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 getDataRef = useRef(); + // const listRef = useRef(); - getDataRef.current = async () => { - if (!canRead) { - dispatch({ - type: 'UNSET_IS_LOADING', - }); + // getDataRef.current = async () => { + // if (!canRead) { + // dispatch({ + // type: 'UNSET_IS_LOADING', + // }); - return; - } - // Show the loading state and reset the state - dispatch({ - type: 'GET_DATA', - }); + // 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' }); + // 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' }, - }); - } - }; + // 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]); + // useEffect(() => { + // if (!isLoadingForPermissions) { + // getDataRef.current(); + // } + // }, [search, isLoadingForPermissions]); // const handleChangeDataToDelete = ids => { // dispatch({ @@ -243,6 +230,10 @@ const ListPage = () => { // }); // }; + // This can be improved but we need to show an something to the user + const isLoading = + (status !== 'success' && status !== 'error') || (status === 'success' && isFetching); + return (
@@ -272,7 +263,15 @@ const ListPage = () => { { number: total } )} /> - {isLoading ? : undefined} + + {!canRead &&
TODO no permissions
} + {status === 'error' &&
An error occurred
} + {canRead && isLoading ? ( + + ) : ( +
+ + : undefined} + noBorder + /> + ) : ( + undefined + ) + } + > + + handleClickSort(!isSorted)} + > + {label} + + + + Actions +
+ + + + {entry.firstname} {entry.lastname} + + {entry.email} + {entry.roles.map(role => role.name).join(',\n')} + + {entry.username || '-'} + + + + {entry.isActive ? 'Active' : 'Inactive'} + + + + console.log('edit')} + label="Edit" + noBorder + icon={} + /> + + console.log('delete')} + label="Delete" + noBorder + icon={} + /> + + +
+ )} + ); @@ -340,5 +339,3 @@ const ListPage = () => { }; export default ListPage; - -// export default () => 'User - LV'; 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 new file mode 100644 index 0000000000..7c3a7dd90e --- /dev/null +++ b/packages/core/admin/admin/src/pages/Users/ListPage/utils/api.js @@ -0,0 +1,15 @@ +import { axiosInstance } from '../../../../core/utils'; + +const fetchData = async search => { + try { + const { + data: { data }, + } = await axiosInstance.get(`/admin/users${search}`); + + return data; + } catch (err) { + throw new Error(err); + } +}; + +export default fetchData; diff --git a/packages/core/helper-plugin/lib/src/components/Status/Status.stories.mdx b/packages/core/helper-plugin/lib/src/components/Status/Status.stories.mdx new file mode 100644 index 0000000000..8fd87d3e77 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/components/Status/Status.stories.mdx @@ -0,0 +1,46 @@ + + +import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs'; +import { Row, Text } from '@strapi/parts'; +import Status from './index'; + + + +# Status + +This component is used in order to display the status. + +## Usage + + + + + + primary + + + + alternative + + + + danger + + + + neutral + + + + secondary + + + + success + + + + warning + + + diff --git a/packages/core/helper-plugin/lib/src/components/Status/index.js b/packages/core/helper-plugin/lib/src/components/Status/index.js new file mode 100644 index 0000000000..e4e4e8f1cf --- /dev/null +++ b/packages/core/helper-plugin/lib/src/components/Status/index.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +const Bullet = styled.div` + margin-right: ${({ theme }) => theme.spaces[3]}; + width: ${6 / 16}rem; + height: ${6 / 16}rem; + border-radius: 50%; + background: ${({ theme, backgroundColor }) => theme.colors[backgroundColor]}; +`; + +const Status = ({ variant }) => { + const backgroundColor = `${variant}600`; + + return ; +}; + +Status.defaultProps = { + variant: 'primary', +}; + +Status.propTypes = { + variant: PropTypes.oneOf([ + 'alternative', + 'danger', + 'neutral', + 'primary', + 'secondary', + 'success', + 'warning', + ]), +}; + +export default Status; diff --git a/packages/core/helper-plugin/lib/src/icons/SortIcon/SortIcon.stories.mdx b/packages/core/helper-plugin/lib/src/icons/SortIcon/SortIcon.stories.mdx new file mode 100644 index 0000000000..6006842708 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/icons/SortIcon/SortIcon.stories.mdx @@ -0,0 +1,27 @@ + + +import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs'; +import { Row, Text } from '@strapi/parts'; +import { FilterDropdown } from '@strapi/icons'; +import SortIcon from './index'; + + + +# SortIcon + +This component is used in order to display the SortIcon. + +## Usage + + + + + + Arrow down + + + + Arrow up + + + diff --git a/packages/core/helper-plugin/lib/src/icons/SortIcon/index.js b/packages/core/helper-plugin/lib/src/icons/SortIcon/index.js new file mode 100644 index 0000000000..a15ad3c0a0 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/icons/SortIcon/index.js @@ -0,0 +1,23 @@ +import styled from 'styled-components'; +import { FilterDropdown } from '@strapi/icons'; +import PropTypes from 'prop-types'; + +const transientProps = { + isUp: true, +}; + +const SortIcon = styled(FilterDropdown).withConfig({ + shouldForwardProp: (prop, defPropValFN) => !transientProps[prop] && defPropValFN(prop), +})` + transform: ${({ isUp }) => `rotate(${isUp ? '180' : '0'}deg)`}; +`; + +SortIcon.defaultProps = { + isUp: false, +}; + +SortIcon.propTypes = { + isUp: PropTypes.bool, +}; + +export default SortIcon; diff --git a/packages/core/helper-plugin/lib/src/index.js b/packages/core/helper-plugin/lib/src/index.js index 6b878fcf02..c3f8521d26 100644 --- a/packages/core/helper-plugin/lib/src/index.js +++ b/packages/core/helper-plugin/lib/src/index.js @@ -175,3 +175,7 @@ export { default as CheckPermissions } from './components/CheckPermissions'; export * from './components/InjectionZone'; export { default as LoadingIndicatorPage } from './components/LoadingIndicatorPage'; export { default as SettingsPageTitle } from './components/SettingsPageTitle'; +export { default as Status } from './components/Status'; + +// New icons +export { default as SortIcon } from './icons/SortIcon';