Design Table

Signed-off-by: soupette <cyril@strapi.io>
This commit is contained in:
soupette 2021-08-31 16:23:31 +02:00
parent a4b427b45d
commit 731fe93c92
12 changed files with 433 additions and 88 deletions

View File

@ -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')};
}

View File

@ -91,6 +91,8 @@ const List = forwardRef(
push(`/settings/users/${id}`);
};
console.log({ headers });
return (
<Wrapper withHigherHeight={!data.length}>
<Table

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Row, Box } from '@strapi/parts';
import { EmptyStateDocument } from '@strapi/icons';
const TableEmpty = () => {
return (
<Box paddingTop={11} background="neutral0" hasRadius shadow="filterShadow">
<Row justifyContent="space-around">
<EmptyStateDocument height="100%" width="18rem" />
</Row>
</Box>
);
};
export default TableEmpty;

View File

@ -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 (
<Thead>
<Tr>
<Th>
<BaseCheckbox aria-label="Select all entries" />
</Th>
{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 (
<Th
key={value}
action={
isSorted ? (
<IconButton
label={`Sort on ${label}`}
onClick={handleClickSort}
icon={isSorted ? <SortIcon isUp={isUp} /> : undefined}
noBorder
/>
) : (
undefined
)
}
>
<Tooltip label={`Sort on ${label}`}>
<TableLabel
as={!isSorted && isSortable ? 'button' : 'span'}
label={`Sort on ${label}`}
onClick={() => handleClickSort(!isSorted)}
>
{label}
</TableLabel>
</Tooltip>
</Th>
);
})}
<Th>
<VisuallyHidden>Actions</VisuallyHidden>
</Th>
</Tr>
</Thead>
);
};
export default TableHead;

View File

@ -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 <TableEmpty />;
}
const ROW_COUNT = rows.length;
const COL_COUNT = 7;
return (
<TableCompo
colCount={COL_COUNT}
rowCount={ROW_COUNT}
footer={
canCreate ? (
<TFooter icon={<AddIcon />}>Add another field to this collection type</TFooter>
) : (
undefined
)
}
>
<TableHead />
<Tbody>
{rows.map(entry => (
<Tr key={entry.id}>
<Td>
<BaseCheckbox aria-label={`Select ${entry.email}`} />
</Td>
<Td>
<Text textColor="neutral800">
{entry.firstname} {entry.lastname}
</Text>
</Td>
<Td>{entry.email}</Td>
<Td>
<Text textColor="neutral800">{entry.roles.map(role => role.name).join(',\n')}</Text>
</Td>
<Td>
<Text textColor="neutral800">{entry.username || '-'}</Text>
</Td>
<Td>
<Row>
<Status isActive={entry.isActive} variant={entry.isActive ? 'success' : 'danger'} />
<Text textColor="neutral800">{entry.isActive ? 'Active' : 'Inactive'}</Text>
</Row>
</Td>
<Td>
<Row>
<IconButton
onClick={() => console.log('edit')}
label="Edit"
noBorder
icon={<EditIcon />}
/>
<Box paddingLeft={1}>
<IconButton
onClick={() => console.log('delete')}
label="Delete"
noBorder
icon={<DeleteIcon />}
/>
</Box>
</Row>
</Td>
</Tr>
))}
</Tbody>
</TableCompo>
);
};
Table.defaultProps = {
rows: [],
};
Table.propTypes = {
canCreate: PropTypes.bool.isRequired,
rows: PropTypes.array,
};
export default Table;

View File

@ -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 (
<Main labelledBy="title">
<SettingsPageTitle name="Users" />
@ -272,7 +263,15 @@ const ListPage = () => {
{ number: total }
)}
/>
{isLoading ? <LoadingIndicatorPage /> : undefined}
<ContentLayout>
{!canRead && <div>TODO no permissions</div>}
{status === 'error' && <div>An error occurred</div>}
{canRead && isLoading ? (
<LoadingIndicatorPage />
) : (
<Table canCreate={canCreate} rows={data?.results} reows={[]} />
)}
</ContentLayout>
</Main>
);
@ -340,5 +339,3 @@ const ListPage = () => {
};
export default ListPage;
// export default () => 'User - LV';

View File

@ -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;

View File

@ -0,0 +1,46 @@
<!--- Status.stories.mdx --->
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import { Row, Text } from '@strapi/parts';
import Status from './index';
<Meta title="components/Status" />
# Status
This component is used in order to display the status.
## Usage
<Canvas>
<Story name="base">
<Row>
<Status variant="primary" />
<Text>primary</Text>
</Row>
<Row>
<Status variant="alternative" />
<Text>alternative</Text>
</Row>
<Row>
<Status variant="danger" />
<Text>danger</Text>
</Row>
<Row>
<Status variant="neutral" />
<Text>neutral</Text>
</Row>
<Row>
<Status variant="secondary" />
<Text>secondary</Text>
</Row>
<Row>
<Status variant="success" />
<Text>success</Text>
</Row>
<Row>
<Status variant="warning" />
<Text>warning</Text>
</Row>
</Story>
</Canvas>

View File

@ -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 <Bullet backgroundColor={backgroundColor} />;
};
Status.defaultProps = {
variant: 'primary',
};
Status.propTypes = {
variant: PropTypes.oneOf([
'alternative',
'danger',
'neutral',
'primary',
'secondary',
'success',
'warning',
]),
};
export default Status;

View File

@ -0,0 +1,27 @@
<!--- SortIcon.stories.mdx --->
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import { Row, Text } from '@strapi/parts';
import { FilterDropdown } from '@strapi/icons';
import SortIcon from './index';
<Meta title="icons/SortIcon" />
# SortIcon
This component is used in order to display the SortIcon.
## Usage
<Canvas>
<Story name="base">
<Row>
<SortIcon />
<Text>Arrow down</Text>
</Row>
<Row>
<SortIcon isUp />
<Text>Arrow up</Text>
</Row>
</Story>
</Canvas>

View File

@ -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;

View File

@ -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';