mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 02:44:55 +00:00
Design Table
Signed-off-by: soupette <cyril@strapi.io>
This commit is contained in:
parent
a4b427b45d
commit
731fe93c92
@ -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')};
|
||||
}
|
||||
|
||||
@ -91,6 +91,8 @@ const List = forwardRef(
|
||||
push(`/settings/users/${id}`);
|
||||
};
|
||||
|
||||
console.log({ headers });
|
||||
|
||||
return (
|
||||
<Wrapper withHigherHeight={!data.length}>
|
||||
<Table
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
@ -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;
|
||||
@ -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>
|
||||
23
packages/core/helper-plugin/lib/src/icons/SortIcon/index.js
Normal file
23
packages/core/helper-plugin/lib/src/icons/SortIcon/index.js
Normal 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;
|
||||
@ -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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user