first draft of search zones

It doesn't affect the actual query yet, and it looks bad! But it exists!
This commit is contained in:
Emi Matchu 2020-09-01 18:59:05 -07:00
parent d013dd6d89
commit 0088c3f193
3 changed files with 102 additions and 15 deletions

View file

@ -19,7 +19,7 @@ import SearchPanel from "./SearchPanel";
* state and refs. * state and refs.
*/ */
function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) { function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) {
const [searchQuery, setSearchQuery] = React.useState(""); const [searchQuery, setSearchQuery] = React.useState(null);
const scrollContainerRef = React.useRef(); const scrollContainerRef = React.useRef();
const searchQueryRef = React.useRef(); const searchQueryRef = React.useRef();
const firstSearchResultRef = React.useRef(); const firstSearchResultRef = React.useRef();
@ -44,7 +44,7 @@ function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) {
> >
<Box px="4" py="5"> <Box px="4" py="5">
<SearchPanel <SearchPanel
query={searchQuery} query={searchQuery.value}
outfitState={outfitState} outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit} dispatchToOutfit={dispatchToOutfit}
scrollContainerRef={scrollContainerRef} scrollContainerRef={scrollContainerRef}

View file

@ -1,8 +1,12 @@
import React from "react"; import React from "react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import { import {
Box,
IconButton, IconButton,
Input, Input,
InputGroup, InputGroup,
InputLeftAddon,
InputLeftElement, InputLeftElement,
InputRightElement, InputRightElement,
useColorModeValue, useColorModeValue,
@ -27,6 +31,24 @@ function SearchToolbar({
}) { }) {
const [suggestions, setSuggestions] = React.useState([]); const [suggestions, setSuggestions] = React.useState([]);
// NOTE: This query should always load ~instantly, from the client cache.
const { data } = useQuery(gql`
query SearchToolbarZones {
allZones {
id
label
depth
isCommonlyUsedByItems
}
}
`);
const zones = data?.allZones || [];
const itemZones = zones.filter((z) => z.isCommonlyUsedByItems);
let zoneLabels = itemZones.map((z) => z.label);
zoneLabels = [...new Set(zoneLabels)];
zoneLabels.sort();
const onMoveFocusDownToResults = (e) => { const onMoveFocusDownToResults = (e) => {
if (firstSearchResultRef.current) { if (firstSearchResultRef.current) {
firstSearchResultRef.current.focus(); firstSearchResultRef.current.focus();
@ -34,22 +56,45 @@ function SearchToolbar({
} }
}; };
const renderSuggestion = React.useCallback(
(zoneLabel, { isHighlighted }) => (
<Box fontWeight={isHighlighted ? "bold" : "normal"}>{zoneLabel}</Box>
),
[]
);
const focusBorderColor = useColorModeValue("green.600", "green.400"); const focusBorderColor = useColorModeValue("green.600", "green.400");
return ( return (
<Autosuggest <Autosuggest
suggestions={suggestions} suggestions={suggestions}
onSuggestionsFetchRequested={({ value }) => { onSuggestionsFetchRequested={({ value }) =>
if (value.includes("hat")) setSuggestions(["Zone: Hat"]); setSuggestions(getSuggestions(value, zoneLabels))
else setSuggestions([]); }
onSuggestionsClearRequested={() => setSuggestions([])}
onSuggestionSelected={(e, { suggestion }) => {
const valueWithoutLastWord = query.value.match(/^(.*?)\s*\S+$/)[1];
onChange({
...query,
value: valueWithoutLastWord,
filterToZoneLabel: suggestion,
});
}} }}
onSuggestionsClearRequested={() => {}} getSuggestionValue={(zl) => zl}
renderSuggestion={() => "Hat"} shouldRenderSuggestions={() => query?.filterToZoneLabel == null}
renderSuggestion={renderSuggestion}
renderInputComponent={(props) => ( renderInputComponent={(props) => (
<InputGroup> <InputGroup>
{query?.filterToZoneLabel ? (
<InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{query.filterToZoneLabel}</Box>
</InputLeftAddon>
) : (
<InputLeftElement> <InputLeftElement>
<SearchIcon color="gray.400" /> <SearchIcon color="gray.400" />
</InputLeftElement> </InputLeftElement>
)}
<Input {...props} /> <Input {...props} />
{query && ( {query && (
<InputRightElement> <InputRightElement>
@ -59,7 +104,9 @@ function SearchToolbar({
variant="ghost" variant="ghost"
colorScheme="green" colorScheme="green"
aria-label="Clear search" aria-label="Clear search"
onClick={() => onChange("")} onClick={() => {
onChange(null);
}}
// Big style hacks here! // Big style hacks here!
height="calc(100% - 2px)" height="calc(100% - 2px)"
marginRight="2px" marginRight="2px"
@ -69,18 +116,40 @@ function SearchToolbar({
</InputGroup> </InputGroup>
)} )}
inputProps={{ inputProps={{
placeholder: "Search for items to add…", // placeholder: "Search for items to add…",
"aria-label": "Search for items to add…", "aria-label": "Search for items to add…",
focusBorderColor: focusBorderColor, focusBorderColor: focusBorderColor,
value: query, value: query?.value || "",
ref: searchQueryRef, ref: searchQueryRef,
onChange: (e) => onChange(e.target.value), minWidth: 0,
// HACK: Chakra isn't noticing the InputLeftElement swapping out
// for the InputLeftAddon, so the padding isn't updating.
paddingLeft: query?.filterToZoneLabel ? "1rem" : "2.5rem",
onChange: (e, { newValue, method }) => {
// The Autosuggest tries to change the _entire_ value of the element
// when navigating suggestions, which isn't actually what we want.
// Only accept value changes that are typed by the user!
if (method === "type") {
onChange({ ...query, value: newValue });
}
},
onKeyDown: (e) => { onKeyDown: (e) => {
if (e.key === "Escape") { if (e.key === "Escape") {
onChange(""); if (suggestions.length > 0) {
setSuggestions([]);
return;
}
onChange(null);
e.target.blur(); e.target.blur();
} else if (e.key === "ArrowDown") { } else if (e.key === "ArrowDown") {
if (suggestions.length > 0) {
return;
}
onMoveFocusDownToResults(e); onMoveFocusDownToResults(e);
} else if (e.key === "Backspace") {
if (query.value === "") {
onChange({ ...query, filterToZoneLabel: null });
}
} }
}, },
}} }}
@ -88,4 +157,17 @@ function SearchToolbar({
); );
} }
function getSuggestions(value, zoneLabels) {
const words = value.split(/\s+/);
const lastWord = words[words.length - 1];
if (lastWord.length < 2) {
return [];
}
const matchingZoneLabels = zoneLabels.filter((zl) =>
zl.toLowerCase().includes(lastWord.toLowerCase())
);
return matchingZoneLabels;
}
export default SearchToolbar; export default SearchToolbar;

View file

@ -10,6 +10,11 @@ import cachedZones from "./cached-data/zones.json";
const typePolicies = { const typePolicies = {
Query: { Query: {
fields: { fields: {
allZones: (_, { toReference }) => {
return cachedZones.map((z) =>
toReference({ __typename: "Zone", id: z.id }, true)
);
},
items: (_, { args, toReference }) => { items: (_, { args, toReference }) => {
return args.ids.map((id) => return args.ids.map((id) =>
toReference({ __typename: "Item", id }, true) toReference({ __typename: "Item", id }, true)