Skip to content

Commit

Permalink
Add a search input to the list view (#7841)
Browse files Browse the repository at this point in the history
Co-authored-by: mitchellhamilton <mitchell@hamil.town>
Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 19, 2022
1 parent f3c1a2b commit d7cfada
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-glasses-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': minor
---

Adds a search input field to the list view, resulting in a `contains` query across `ui.searchFields` joined with the list view filters
2 changes: 1 addition & 1 deletion docs/pages/docs/apis/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Options:

- `labelField`: Selects the field which will be used as the label column in the Admin UI.
By default looks for a field called `'label'`, then falls back to `'name'`, then `'title'`, and finally `'id'`, which is guaranteed to exist.
- `searchFields`: The fields used by the Admin UI when searching this list.
- `searchFields`: The fields used by the Admin UI when searching this list on the list view and in relationship fields.
It is always possible to search by an id and `'id'` should not be specified in this option.
By default, the `labelField` is used if it has a string `contains` filter, otherwise none.
- `description` (default: `undefined`): Sets the list description displayed in the Admin UI.
Expand Down
3 changes: 3 additions & 0 deletions examples/blog/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { select, relationship, text, timestamp } from '@keystone-6/core/fields';
export const lists = {
Post: list({
access: allowAll,
ui: {
searchFields: ['title', 'content'],
},
fields: {
title: text({ validation: { isRequired: true } }),
status: select({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,7 @@ export function FilterAdd({

return (
<Fragment>
<Button
tone="active"
size="small"
{...trigger.props}
ref={trigger.ref}
onClick={() => setOpen(!isOpen)}
>
<Button tone="active" {...trigger.props} ref={trigger.ref} onClick={() => setOpen(!isOpen)}>
<Box as="span" marginRight="xsmall">
Filter List
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { Fragment, HTMLAttributes, ReactNode, useEffect, useMemo, useState } fro

import { Button } from '@keystone-ui/button';
import { Box, Center, Heading, jsx, Stack, useTheme, VisuallyHidden } from '@keystone-ui/core';
import { CheckboxControl } from '@keystone-ui/fields';
import { CheckboxControl, TextInput } from '@keystone-ui/fields';
import { ArrowRightCircleIcon } from '@keystone-ui/icons/icons/ArrowRightCircleIcon';
import { LoadingDots } from '@keystone-ui/loading';
import { AlertDialog } from '@keystone-ui/modals';
import { useToasts } from '@keystone-ui/toast';

import { SearchIcon } from '@keystone-ui/icons/icons/SearchIcon';
import { ListMeta } from '../../../../types';
import {
getRootGraphQLFieldsFromFieldController,
Expand All @@ -24,6 +25,7 @@ import { PageContainer, HEADER_HEIGHT } from '../../../../admin-ui/components/Pa
import { Pagination, PaginationLabel } from '../../../../admin-ui/components/Pagination';
import { useList } from '../../../../admin-ui/context';
import { Link, useRouter } from '../../../../admin-ui/router';
import { useFilter } from '../../../../fields/types/relationship/views/RelationshipSelect';
import { CreateButtonLink } from '../../../../admin-ui/components/CreateButtonLink';
import { FieldSelection } from './FieldSelection';
import { FilterAdd } from './FilterAdd';
Expand Down Expand Up @@ -131,7 +133,7 @@ export const getListPage = (props: ListPageProps) => () => <ListPage {...props}
const ListPage = ({ listKey }: ListPageProps) => {
const list = useList(listKey);

const { query } = useRouter();
const { query, push } = useRouter();

const { resetToDefaults } = useQueryParamsFromLocalStorage(listKey);

Expand Down Expand Up @@ -162,9 +164,26 @@ const ListPage = ({ listKey }: ListPageProps) => {
}, [metaQuery.data?.keystone.adminMeta.list?.fields]);

const sort = useSort(list, orderableFields);

const filters = useFilters(list, filterableFields);

const searchFields = Object.values(list.fields)
.filter(({ search }) => search !== null)
.map(({ label }) => label);

const searchParam = typeof query.search === 'string' ? query.search : '';
const [searchString, updateSearchString] = useState(searchParam);
const search = useFilter(searchParam, list);

const updateSearch = (value: string) => {
const { search, ...queries } = query;

if (value.trim()) {
push({ query: { ...queries, search: value } });
} else {
push({ query: queries });
}
};

let selectedFields = useSelectedFields(list, listViewFieldModesByField);

let {
Expand Down Expand Up @@ -201,7 +220,7 @@ const ListPage = ({ listKey }: ListPageProps) => {
errorPolicy: 'all',
skip: !metaQuery.data,
variables: {
where: filters.where,
where: { ...filters.where, ...search },
take: pageSize,
skip: (currentPage - 1) * pageSize,
orderBy: sort ? [{ [sort.field]: sort.direction.toLowerCase() }] : undefined,
Expand Down Expand Up @@ -252,12 +271,31 @@ const ListPage = ({ listKey }: ListPageProps) => {
<p css={{ marginTop: '24px', maxWidth: '704px' }}>{list.description}</p>
)}
<Stack across gap="medium" align="center" marginTop="xlarge">
<form
onSubmit={e => {
e.preventDefault();
updateSearch(searchString);
}}
>
<Stack across>
<TextInput
css={{ borderRadius: '4px 0px 0px 4px' }}
autoFocus
value={searchString}
onChange={e => updateSearchString(e.target.value)}
placeholder={`Search by ${searchFields.length ? searchFields.join(', ') : 'ID'}`}
/>
<Button css={{ borderRadius: '0px 4px 4px 0px' }} type="submit">
<SearchIcon />
</Button>
</Stack>
</form>
{showCreate && <CreateButtonLink list={list} />}
{data.count || filters.filters.length ? (
<FilterAdd listKey={listKey} filterableFields={filterableFields} />
) : null}
{filters.filters.length ? <FilterList filters={filters.filters} list={list} /> : null}
{Boolean(filters.filters.length || query.sortBy || query.fields) && (
{Boolean(filters.filters.length || query.sortBy || query.fields || query.search) && (
<Button size="small" onClick={resetToDefaults}>
Reset to defaults
</Button>
Expand Down Expand Up @@ -351,7 +389,6 @@ const ListPageHeader = ({ listKey }: { listKey: string }) => {
}}
>
<Heading type="h3">{list.label}</Heading>
{/* <CreateButton listKey={listKey} /> */}
</div>
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,30 @@ function useDebouncedValue<T>(value: T, limitMs: number): T {
return debouncedValue;
}

function useFilter(search: string, list: ListMeta) {
export function useFilter(search: string, list: ListMeta) {
return useMemo(() => {
let conditions: Record<string, any>[] = [];
if (search.length) {
const idFieldKind: IdFieldConfig['kind'] = (list.fields.id.controller as any).idFieldKind;
const trimmedSearch = search.trim();
const isValidId = idValidators[idFieldKind](trimmedSearch);
if (isValidId) {
conditions.push({ id: { equals: trimmedSearch } });
}
for (const field of Object.values(list.fields)) {
if (field.search !== null) {
conditions.push({
[field.path]: {
contains: trimmedSearch,
mode: field.search === 'insensitive' ? 'insensitive' : undefined,
},
});
}
}
if (!search.length) return { OR: [] };

const idFieldKind: IdFieldConfig['kind'] = (list.fields.id.controller as any).idFieldKind;
const trimmedSearch = search.trim();
const isValidId = idValidators[idFieldKind](trimmedSearch);

const conditions: Record<string, any>[] = [];
if (isValidId) {
conditions.push({ id: { equals: trimmedSearch } });
}

for (const field of Object.values(list.fields)) {
if (field.search === null) continue; // in ui.searchFields

conditions.push({
[field.path]: {
contains: trimmedSearch,
mode: field.search === 'insensitive' ? 'insensitive' : undefined,
},
});
}

return { OR: conditions };
}, [search, list]);
}
Expand Down

0 comments on commit d7cfada

Please sign in to comment.