Skip to content

Commit

Permalink
feat: basic search (#30)
Browse files Browse the repository at this point in the history
* feat: add basic search functionality (#29)

* refactor: misc refactors and style updates

Co-authored-by: Jesper Paulsen <[email protected]>
  • Loading branch information
ricokahler and Jesperpaulsen authored Aug 15, 2021
1 parent e4e2576 commit bcde006
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 24 deletions.
41 changes: 37 additions & 4 deletions super-pane/create-super-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import {
SyncIcon,
SpinnerIcon,
ControlsIcon,
SearchIcon,
} from '@sanity/icons';
import styles from './styles.module.css';
import SearchField from './search-field';

function parentHasClass(el: HTMLElement | null, className: string): boolean {
if (!el) return false;
Expand All @@ -39,20 +41,32 @@ function createSuperPane(typeName: string, S: any) {
const schemaType = schema.get(typeName);
const selectColumns = createEmitter();
const refresh = createEmitter();
const search = createEmitter();

const fieldsToChooseFrom = (schemaType.fields as any[])
.filter((field: any) => field?.type?.jsonType === 'string')
.map((field: any) => ({
name: field.name as string,
title: field.type.title as string,
}));

function SuperPane() {
const router = useRouter();
const [pageSize, setPageSize] = useState(25);
const [columnSelectorOpen, setColumnSelectorOpen] = useState(false);
const [selectedColumns, setSelectedColumns] = useState(new Set<string>());
const [selectedIds, setSelectedIds] = useState(new Set<string>());

const [selectedSearchField, setSelectedSearchField] = useState<
string | null
>(fieldsToChooseFrom[0].name || null);
const [showSearch, setShowSearch] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);

const client = usePaginatedClient({
typeName,
pageSize,
selectedColumns,
searchField: selectedSearchField,
});

useEffect(() => {
Expand All @@ -63,6 +77,10 @@ function createSuperPane(typeName: string, S: any) {
return refresh.subscribe(client.refresh);
}, [client.refresh]);

useEffect(() => {
return search.subscribe(() => setShowSearch((prev) => !prev));
}, []);

const fields = schemaType.fields.filter((field: any) =>
selectedColumns.has(field.name)
);
Expand Down Expand Up @@ -106,7 +124,16 @@ function createSuperPane(typeName: string, S: any) {
/>
</div>
</div>

{showSearch && (
<div>
<SearchField
currentField={selectedSearchField}
fieldsToChooseFrom={fieldsToChooseFrom}
onSearch={client.setUserQuery}
onFieldSelected={setSelectedSearchField}
/>
</div>
)}
<div className={styles.tableWrapper}>
<div
className={classNames(styles.loadingOverlay, {
Expand Down Expand Up @@ -320,6 +347,8 @@ function createSuperPane(typeName: string, S: any) {
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={250}>250</option>
<option value={500}>500</option>
</Select>
</div>
</label>
Expand All @@ -332,7 +361,8 @@ function createSuperPane(typeName: string, S: any) {
mode="bleed"
/>
<Label>
{client.page + 1}&nbsp;/&nbsp;{client.totalPages}
{client.totalPages === 0 ? 0 : client.page + 1}&nbsp;/&nbsp;
{client.totalPages}
</Label>
<Button
fontSize={1}
Expand Down Expand Up @@ -370,11 +400,14 @@ function createSuperPane(typeName: string, S: any) {
menuItems: S.documentTypeList(typeName)
.menuItems([
S.menuItem().title('Refresh').icon(SyncIcon).action(refresh.notify),
fieldsToChooseFrom.length
? S.menuItem().title('Search').icon(SearchIcon).action(search.notify)
: null,
S.menuItem()
.title('Select Columns')
.icon(ControlsIcon)
.action(selectColumns.notify),
])
].filter(Boolean))
.serialize().menuItems,
});
}
Expand Down
57 changes: 57 additions & 0 deletions super-pane/search-field/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { TextInput, Select } from '@sanity/ui';
import React, { useEffect, useState } from 'react';
import styles from './styles.module.css';

interface Props {
fieldsToChooseFrom: Array<{ name: string; title: string }>;
currentField: string | null;
onSearch: (userQuery: string) => void;
onFieldSelected: (field: string) => void;
}

function SearchField({
currentField,
fieldsToChooseFrom,
onSearch,
onFieldSelected,
}: Props) {
const [userQuery, setUserQuery] = useState('');

useEffect(() => {
if (!userQuery.length) {
onSearch('');
return;
}

const timeout = setTimeout(() => {
onSearch(userQuery);
}, 700);

return () => clearTimeout(timeout);
}, [userQuery, onSearch]);

return (
<form onSubmit={(e) => e.preventDefault()} className={styles.searchForm}>
<TextInput
onChange={(event) => setUserQuery(event.currentTarget.value)}
placeholder="Search"
value={userQuery}
/>

<div className={styles.searchSelect}>
<Select
value={currentField || undefined}
onChange={(e) => onFieldSelected(e.currentTarget.value)}
>
{fieldsToChooseFrom.map((field) => (
<option key={field.name} value={field.name}>
{field.title}
</option>
))}
</Select>
</div>
</form>
);
}

export default SearchField;
9 changes: 9 additions & 0 deletions super-pane/search-field/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.searchForm {
margin: 0.5rem;
display: flex;
gap: 1rem;
}

.searchSelect {
width: 5rem;
}
57 changes: 37 additions & 20 deletions super-pane/use-paginated-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ interface Params {
typeName: string;
pageSize: number;
selectedColumns: Set<string>;
searchField: string | null;
}

function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
function usePaginatedClient({
typeName,
pageSize,
selectedColumns,
searchField,
}: Params) {
// the loading statuses are a set of strings
// when it's empty, nothing is loading
const [loadingStatuses, setLoadingStatuses] = useState(new Set<string>());
Expand All @@ -46,6 +52,15 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
const [refreshId, setRefreshId] = useState(nanoid());
const refresh = useCallback(() => setRefreshId(nanoid()), []);

const [userQuery, setUserQuery] = useState('');
// Builds the string to use when a custom filter has been entered
const searchQuery =
userQuery.length && searchField
? ` && ${searchField} match "${userQuery}*"`
: '';

console.log({ searchQuery });

// get total count
useEffect(() => {
let canceled = false;
Expand All @@ -60,8 +75,8 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {

// fetch all the draft IDs in this document type
const draftIds = await client.fetch<string[]>(
`*[_type == $typeName && _id in path("drafts.**")]._id`,
{ typeName },
`*[_type == $typeName && _id in path("drafts.**") ${searchQuery}]._id`,
{ typeName }
);

const { draftsWithPublishedVersion, notDraftCount } = await client.fetch<{
Expand All @@ -71,10 +86,10 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
notDraftCount: number;
}>(
`{
"draftsWithPublishedVersion": *[_type == $typeName && _id in $ids]._id,
"notDraftCount": count(*[_type == $typeName && !(_id in path("drafts.**"))]),
"draftsWithPublishedVersion": *[_type == $typeName && _id in $ids ${searchQuery}]._id,
"notDraftCount": count(*[_type == $typeName && !(_id in path("drafts.**")) ${searchQuery}]),
}`,
{ ids: draftIds.map(removeDraftPrefix), typeName },
{ ids: draftIds.map(removeDraftPrefix), typeName }
);

// the calculation for the total is then:
Expand Down Expand Up @@ -102,7 +117,7 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
return () => {
canceled = true;
};
}, [typeName, refreshId]);
}, [typeName, refreshId, searchQuery]);

// get page IDs
useEffect(() => {
Expand All @@ -116,8 +131,8 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {

// query for all the draft IDs
const draftIds = await client.fetch<string[]>(
'*[_type == $typeName && _id in path("drafts.**")]._id',
{ typeName },
`*[_type == $typeName && _id in path("drafts.**") ${searchQuery}]._id`,
{ typeName }
);

// create a set of drafts IDs.
Expand All @@ -143,9 +158,10 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
// where we have to remove half the result set in the case of
// duplicate `draft.` document
pageSize * 2;

const pageIds = await client.fetch<string[]>(
'*[_type == $typeName][$start...$end]._id',
{ typeName, start, end },
`*[_type == $typeName ${searchQuery}][$start...$end]._id`,
{ typeName, start, end }
);

const filteredIds = pageIds
Expand Down Expand Up @@ -190,16 +206,16 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
// TODO: proper error handling
console.warn(e);
});
}, [page, pageSize, typeName, refreshId]);
}, [page, pageSize, typeName, refreshId, searchQuery]);

// get results
useEffect(() => {
// take all the input IDs and duplicate them with the prefix `drafts.`
const ids = pageIds.map((id) => [id, `drafts.${id}`]).flat();
// these IDs will go into a specific query. if the draft or published
// version happens to not exist, that's okay.
const query = `*[_id in $ids] { _id, _type, ${Array.from(
selectedColumns,
const query = `*[_id in $ids ${searchQuery}] { _id, _type, ${Array.from(
selectedColumns
).join(', ')} }`;

async function getResults() {
Expand All @@ -218,7 +234,7 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
acc[id] = index;
return acc;
},
{},
{}
);

const results = await client.fetch<any[]>(query, { ids });
Expand All @@ -245,7 +261,7 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
acc[id]._normalizedId = id;

return acc;
}, {}),
}, {})
);

// delete the `results` from the loading statuses
Expand All @@ -262,8 +278,8 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
.sort(
(a, b) =>
indexes[removeDraftPrefix(a._id)] -
indexes[removeDraftPrefix(b._id)],
),
indexes[removeDraftPrefix(b._id)]
)
);
}

Expand All @@ -284,12 +300,12 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
return next;
});
}),
debounceTime(1000),
debounceTime(1000)
)
.subscribe(getResults);

return () => subscription.unsubscribe();
}, [pageIds, selectedColumns, refreshId]);
}, [pageIds, selectedColumns, refreshId, searchQuery]);

// reset page
useEffect(() => {
Expand All @@ -309,6 +325,7 @@ function usePaginatedClient({ typeName, pageSize, selectedColumns }: Params) {
pageIds,
total,
refresh,
setUserQuery,
};
}

Expand Down

0 comments on commit bcde006

Please sign in to comment.