mirror of
https://github.com/strapi/strapi.git
synced 2025-12-30 00:37:24 +00:00
Add search
Signed-off-by: soupette <cyril@strapi.io>
This commit is contained in:
parent
d7abe60c72
commit
93296ab300
@ -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,
|
||||
|
||||
@ -15,7 +15,7 @@ const FilterList = () => {
|
||||
return f[name]?.[filterType] !== value;
|
||||
});
|
||||
|
||||
setQuery({ filters: { $and: nextFilters } });
|
||||
setQuery({ filters: { $and: nextFilters }, page: 1 });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
28
tmp.js
Normal 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();
|
||||
16
yarn.lock
16
yarn.lock
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user