Add search

Signed-off-by: soupette <cyril@strapi.io>
This commit is contained in:
soupette 2021-09-02 12:48:36 +02:00
parent d7abe60c72
commit 93296ab300
14 changed files with 211 additions and 101 deletions

View File

@ -21,6 +21,7 @@ const Table = ({
canDelete,
canUpdate,
headers,
isLoading,
onConfirmDeleteAll,
rows,
withBulkActions,
@ -138,8 +139,8 @@ const Table = ({
withMainAction={withMainAction}
withBulkActions={withBulkActions}
/>
{!rows.length ? (
<EmptyBodyTable colSpan={COL_COUNT} content={content} />
{!rows.length || isLoading ? (
<EmptyBodyTable colSpan={COL_COUNT} content={content} isLoading={isLoading} />
) : (
<TableRows
canDelete={canDelete}
@ -172,6 +173,7 @@ const Table = ({
Table.defaultProps = {
headers: [],
isLoading: false,
onConfirmDeleteAll: () => {},
rows: [],
withBulkActions: false,
@ -182,6 +184,7 @@ 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,

View File

@ -15,7 +15,7 @@ const FilterList = () => {
return f[name]?.[filterType] !== value;
});
setQuery({ filters: { $and: nextFilters } });
setQuery({ filters: { $and: nextFilters }, page: 1 });
};
return (

View File

@ -17,7 +17,7 @@ const Inputs = ({ onChange, type, value }) => {
return (
<Field>
<Stack>
<FieldInput onChange={({ target: { value } }) => onChange(value)} value={value} />
<FieldInput onChange={({ target: { value } }) => onChange(value)} value={value} size="S" />
</Stack>
</Field>
);

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Button, Box, Popover, Stack, Select, Option } from '@strapi/parts';
import { Button, Box, Popover, Stack, Select, Option, FocusTrap } from '@strapi/parts';
import { AddIcon } from '@strapi/icons';
import { useQueryParams } from '@strapi/helper-plugin';
import Inputs from './Inputs';
@ -49,7 +49,7 @@ const FilterPicker = ({ displayedFilters, isVisible, onToggle, source }) => {
{ [modifiedData.name]: { [modifiedData.filter]: modifiedData.value } },
];
setQuery({ filters: { $and: filters } });
setQuery({ filters: { $and: filters }, page: 1 });
}
onToggle();
};
@ -58,54 +58,56 @@ const FilterPicker = ({ displayedFilters, isVisible, onToggle, source }) => {
return (
<Popover source={source} padding={3}>
<form onSubmit={handleSubmit}>
<Stack size={1} style={{ minWidth: 184 }}>
<Box>
<Select
name="name"
size="S"
onChange={handleChangeFilterField}
value={modifiedData.name}
>
{displayedFilters.map(filter => {
return (
<Option key={filter.name} value={filter.name}>
{filter.metadatas.label}
</Option>
);
})}
</Select>
</Box>
<Box>
<Select
name="filter"
size="S"
value={modifiedData.filter}
onChange={val => setModifiedData(prev => ({ ...prev, filter: val }))}
>
{getFilterList(appliedFilter).map(option => {
return (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
);
})}
</Select>
</Box>
<Box>
<Inputs
{...appliedFilter.fieldSchema}
value={modifiedData.value}
onChange={value => setModifiedData(prev => ({ ...prev, value }))}
/>
</Box>
<Box>
<FullWidthButton variant="secondary" startIcon={<AddIcon />} type="submit">
Add filter
</FullWidthButton>
</Box>
</Stack>
</form>
<FocusTrap onEscape={onToggle}>
<form onSubmit={handleSubmit}>
<Stack size={1} style={{ minWidth: 184 }}>
<Box>
<Select
name="name"
size="S"
onChange={handleChangeFilterField}
value={modifiedData.name}
>
{displayedFilters.map(filter => {
return (
<Option key={filter.name} value={filter.name}>
{filter.metadatas.label}
</Option>
);
})}
</Select>
</Box>
<Box>
<Select
name="filter"
size="S"
value={modifiedData.filter}
onChange={val => setModifiedData(prev => ({ ...prev, filter: val }))}
>
{getFilterList(appliedFilter).map(option => {
return (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
);
})}
</Select>
</Box>
<Box>
<Inputs
{...appliedFilter.fieldSchema}
value={modifiedData.value}
onChange={value => setModifiedData(prev => ({ ...prev, value }))}
/>
</Box>
<Box>
<FullWidthButton variant="secondary" startIcon={<AddIcon />} type="submit">
Add filter
</FullWidthButton>
</Box>
</Stack>
</form>
</FocusTrap>
</Popover>
);
};

View File

@ -1,6 +1,6 @@
import React, { useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Box, Row } from '@strapi/parts';
import { Button, Box } from '@strapi/parts';
import { FilterIcon } from '@strapi/icons';
import FilterList from './FilterList';
import FilterPicker from './FilterPicker';
@ -14,27 +14,26 @@ const Filters = ({ displayedFilters }) => {
};
return (
<Box paddingBottom={4}>
<Row style={{ flexWrap: 'wrap' }}>
<Box padding={1}>
<Button
variant="tertiary"
ref={buttonRef}
endIcon={<FilterIcon />}
onClick={handleToggle}
>
Filters
</Button>
<FilterPicker
displayedFilters={displayedFilters}
isVisible={isVisible}
onToggle={handleToggle}
source={buttonRef}
/>
</Box>
<FilterList />
</Row>
</Box>
<>
<Box padding={1}>
<Button
variant="tertiary"
ref={buttonRef}
startIcon={<FilterIcon />}
onClick={handleToggle}
size="S"
>
Filters
</Button>
<FilterPicker
displayedFilters={displayedFilters}
isVisible={isVisible}
onToggle={handleToggle}
source={buttonRef}
/>
</Box>
<FilterList />
</>
);
};

View File

@ -132,8 +132,6 @@ const Pagination = ({ pagination: { pageCount } }) => {
? pageCount - firstLinks.length - lastLinks.length
: pageCount - activePage - 1;
console.log({ firstLinks, middleLinks, lastLinks });
return (
<PaginationCompo activePage={activePage} pageCount={pageCount}>
<PreviousLink to={`${pathname}?${previousSearch}`}>Go to previous page</PreviousLink>

View File

@ -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 (
<TextInput
onBlur={() => setIsOpen(false)}
name="search"
onChange={({ target: { value } }) => setValue(value)}
type="text"
value={value}
/>
);
}
return <IconButton icon={<SearchIcon />} label="Search" onClick={handleToggle} />;
};
export default Search;

View File

@ -1,13 +1,12 @@
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 { useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl';
@ -16,6 +15,7 @@ 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 { deleteData, fetchData } from './utils/api';
import displayedFilters from './utils/displayedFilters';
@ -100,17 +100,25 @@ const ListPage = () => {
{ number: total }
)}
/>
<CustomContentLayout action={createAction} canRead={canRead}>
<CustomContentLayout canRead={canRead}>
{status === 'error' && <div>TODO: An error occurred</div>}
{canRead && isLoading ? (
<LoadingIndicatorPage />
) : (
{canRead && (
<>
<Box paddingBottom={4}>
<Row style={{ flexWrap: 'wrap' }}>
<Search />
<Filters displayedFilters={displayedFilters} />
</Row>
</Box>
</>
)}
{canRead && (
<>
<Filters displayedFilters={displayedFilters} />
<DynamicTable
canCreate={canCreate}
canDelete={canDelete}
canUpdate={canUpdate}
isLoading={isLoading}
onConfirmDeleteAll={deleteAllMutation.mutateAsync}
headers={tableHeaders}
rows={data?.results}

View File

@ -40,8 +40,8 @@
"@fortawesome/react-fontawesome": "^0.1.14",
"@strapi/babel-plugin-switch-ee-ce": "1.0.0",
"@strapi/helper-plugin": "3.6.7",
"@strapi/icons": "0.0.1-alpha.18",
"@strapi/parts": "0.0.1-alpha.18",
"@strapi/icons": "0.0.1-alpha.19",
"@strapi/parts": "0.0.1-alpha.19",
"@strapi/utils": "3.6.7",
"axios": "^0.21.1",
"babel-loader": "8.2.2",

View File

@ -19,7 +19,7 @@ import EmptyBodyTable from './index';
This component is used to display an empty state in a table.
## Usage
## Usage default
<Canvas>
<Story name="base">
@ -31,6 +31,18 @@ This component is used to display an empty state in a table.
</Story>
</Canvas>
## Usage loading
<Canvas>
<Story name="loading">
<Box>
<Table colCount={1} rowCount={2} footer={undefined}>
<EmptyBodyTable isLoading />
</Table>
</Box>
</Story>
</Canvas>
## Props
<ArgsTable of={EmptyBodyTable} />

View File

@ -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 (
<Tbody>
<Tr>
<Td colSpan={colSpan}>
<Row justifyContent="center">
<Box padding={11} background="neutral0">
<Loader />
</Box>
</Row>
</Td>
</Tr>
</Tbody>
);
}
return (
<Tbody>
<Tr>
@ -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;

View File

@ -53,8 +53,8 @@
"@storybook/builder-webpack5": "^6.3.7",
"@storybook/manager-webpack5": "^6.3.7",
"@storybook/react": "^6.3.7",
"@strapi/icons": "0.0.1-alpha.18",
"@strapi/parts": "0.0.1-alpha.18",
"@strapi/icons": "0.0.1-alpha.19",
"@strapi/parts": "0.0.1-alpha.19",
"babel-loader": "^8.2.2",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.15.6",

28
tmp.js Normal file
View File

@ -0,0 +1,28 @@
const axios = require('axios');
const entries = [];
for (i = 0; i < 30; i++) {
const name = 'tota' + i;
entries.push({
firstname: name,
lastname: name,
email: name + '@s.co',
roles: [1],
useSSORegistration: false,
});
}
const run = async () => {
entries.forEach(async entry => {
await axios.post('http://localhost:1337/admin/users', entry, {
headers: {
Authorization:
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjMwNTc2Mzg3LCJleHAiOjE2MzMxNjgzODd9.oGdlhhNY8kODeLv-00INSdkke4SsM189zhtshCrlIZw',
},
});
});
};
run();

View File

@ -4915,15 +4915,15 @@
resolve-from "^5.0.0"
store2 "^2.12.0"
"@strapi/icons@0.0.1-alpha.18":
version "0.0.1-alpha.18"
resolved "https://registry.yarnpkg.com/@strapi/icons/-/icons-0.0.1-alpha.18.tgz#c64ca4a896b965b1ea02b2863352391daa5406ff"
integrity sha512-QDjCckoI5vGQc6eH/rgFxLMGrBkYqmbNsMAb7T5GOUhJxoSymPM4VPKOjqL36u6MRpi1xqYdw3GBiv48tnxZVw==
"@strapi/icons@0.0.1-alpha.19":
version "0.0.1-alpha.19"
resolved "https://registry.yarnpkg.com/@strapi/icons/-/icons-0.0.1-alpha.19.tgz#88153c77ff02deccfedffb1dc2e7f0f4cc2f830a"
integrity sha512-nFpT3Gsp6nQ4+FNd9DkbqhMZrX9KJbWXLQ6XFHhtYdNVA3n5vr2/0MFaqWrvYwxpnxMs3gn1xZOjG4+VzLBwbA==
"@strapi/parts@0.0.1-alpha.18":
version "0.0.1-alpha.18"
resolved "https://registry.yarnpkg.com/@strapi/parts/-/parts-0.0.1-alpha.18.tgz#19306892ba98404f7554360760f1870a2e57e381"
integrity sha512-x0LcIounSdgFY7lVsOjinh6ksS9/Nw2/CHNDVW7ZZTuXS5QHODebNlZibrTyPVZNLSJniYqn8P0rxAtc+FcLjQ==
"@strapi/parts@0.0.1-alpha.19":
version "0.0.1-alpha.19"
resolved "https://registry.yarnpkg.com/@strapi/parts/-/parts-0.0.1-alpha.19.tgz#e4c64c1f63256ab21f4ad643f684738a283c0efc"
integrity sha512-NzhwmCJnMNlCcRHpXTxILm0rVH+qLl2JoaCxx7XoMRnq4JZHHBaLjEiyTZHYJXRTdro+YUcWeayssL9kqEX8cw==
dependencies:
"@internationalized/number" "^3.0.2"
compute-scroll-into-view "^1.0.17"