nc/np/pb search filters

This commit is contained in:
Emi Matchu 2020-11-08 15:04:17 -08:00
parent 63a17824e5
commit 02e173d7de
5 changed files with 108 additions and 32 deletions

View file

@ -41,7 +41,9 @@ function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) {
onChange={onChange} onChange={onChange}
/> />
</Box> </Box>
{searchQuery.value || searchQuery.filterToZoneLabel ? ( {searchQuery.value ||
searchQuery.filterToItemKind ||
searchQuery.filterToZoneLabel ? (
<Box <Box
key="search-panel" key="search-panel"
gridArea="items" gridArea="items"

View file

@ -217,7 +217,11 @@ function useSearchResults(query, outfitState) {
// the user types anything. // the user types anything.
const debouncedQuery = useDebounce(query, 300, { const debouncedQuery = useDebounce(query, 300, {
waitForFirstPause: true, waitForFirstPause: true,
initialValue: { value: "", filterToZoneLabel: null }, initialValue: {
value: "",
filterToItemKind: null,
filterToZoneLabel: null,
},
}); });
// When the query changes, we should update our impression of whether we've // When the query changes, we should update our impression of whether we've
@ -251,13 +255,15 @@ function useSearchResults(query, outfitState) {
gql` gql`
query SearchPanel( query SearchPanel(
$query: String! $query: String!
$speciesId: ID! $itemKind: ItemKindSearchFilter
$zoneIds: [ID!]! $zoneIds: [ID!]!
$speciesId: ID!
$colorId: ID! $colorId: ID!
$offset: Int! $offset: Int!
) { ) {
itemSearchToFit( itemSearchToFit(
query: $query query: $query
itemKind: $itemKind
zoneIds: $zoneIds zoneIds: $zoneIds
speciesId: $speciesId speciesId: $speciesId
colorId: $colorId colorId: $colorId
@ -303,12 +309,16 @@ function useSearchResults(query, outfitState) {
{ {
variables: { variables: {
query: debouncedQuery.value, query: debouncedQuery.value,
itemKind: debouncedQuery.filterToItemKind,
zoneIds: filterToZoneIds, zoneIds: filterToZoneIds,
speciesId, speciesId,
colorId, colorId,
offset: 0, offset: 0,
}, },
skip: !debouncedQuery.value && !debouncedQuery.filterToZoneLabel, skip:
!debouncedQuery.value &&
!debouncedQuery.filterToItemKind &&
!debouncedQuery.filterToZoneLabel,
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
onCompleted: (d) => { onCompleted: (d) => {
// This is called each time the query completes, including on // This is called each time the query completes, including on
@ -434,7 +444,11 @@ function useScrollTracker(scrollContainerRef, threshold, onScrolledToBottom) {
* JS comparison. * JS comparison.
*/ */
function serializeQuery(query) { function serializeQuery(query) {
return `${JSON.stringify([query.value, query.filterToZoneLabel])}`; return `${JSON.stringify([
query.value,
query.filterToItemKind,
query.filterToZoneLabel,
])}`;
} }
export default SearchPanel; export default SearchPanel;

View file

@ -61,7 +61,7 @@ function SearchToolbar({
const highlightedBgColor = useColorModeValue("gray.100", "whiteAlpha.300"); const highlightedBgColor = useColorModeValue("gray.100", "whiteAlpha.300");
const renderSuggestion = React.useCallback( const renderSuggestion = React.useCallback(
(zoneLabel, { isHighlighted }) => ( ({ text }, { isHighlighted }) => (
<Box <Box
fontWeight={isHighlighted ? "bold" : "normal"} fontWeight={isHighlighted ? "bold" : "normal"}
background={isHighlighted ? highlightedBgColor : suggestionBgColor} background={isHighlighted ? highlightedBgColor : suggestionBgColor}
@ -69,7 +69,7 @@ function SearchToolbar({
paddingLeft="2.5rem" paddingLeft="2.5rem"
fontSize="sm" fontSize="sm"
> >
{zoneLabel} {text}
</Box> </Box>
), ),
[suggestionBgColor, highlightedBgColor] [suggestionBgColor, highlightedBgColor]
@ -101,10 +101,12 @@ function SearchToolbar({
[] []
); );
// When we change the filter zone, clear out the suggestions. // When we change the query filters, clear out the suggestions.
React.useEffect(() => { React.useEffect(() => {
setSuggestions([]); setSuggestions([]);
}, [query.filterToZoneLabel]); }, [query.filterToItemKind, query.filterToZoneLabel]);
const queryFilterText = getQueryFilterText(query);
const focusBorderColor = useColorModeValue("green.600", "green.400"); const focusBorderColor = useColorModeValue("green.600", "green.400");
@ -112,7 +114,7 @@ function SearchToolbar({
<Autosuggest <Autosuggest
suggestions={suggestions} suggestions={suggestions}
onSuggestionsFetchRequested={({ value }) => onSuggestionsFetchRequested={({ value }) =>
setSuggestions(getSuggestions(value, zoneLabels)) setSuggestions(getSuggestions(value, query, zoneLabels))
} }
onSuggestionsClearRequested={() => setSuggestions([])} onSuggestionsClearRequested={() => setSuggestions([])}
onSuggestionSelected={(e, { suggestion }) => { onSuggestionSelected={(e, { suggestion }) => {
@ -120,20 +122,20 @@ function SearchToolbar({
onChange({ onChange({
...query, ...query,
value: valueWithoutLastWord, value: valueWithoutLastWord,
filterToZoneLabel: suggestion, filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
}); });
}} }}
getSuggestionValue={(zl) => zl} getSuggestionValue={(zl) => zl}
shouldRenderSuggestions={() => query.filterToZoneLabel == null}
highlightFirstSuggestion={true} highlightFirstSuggestion={true}
renderSuggestion={renderSuggestion} renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer} renderSuggestionsContainer={renderSuggestionsContainer}
renderInputComponent={(props) => ( renderInputComponent={(props) => (
<InputGroup> <InputGroup>
{query.filterToZoneLabel ? ( {queryFilterText ? (
<InputLeftAddon> <InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" /> <SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{query.filterToZoneLabel}</Box> <Box fontSize="sm">{queryFilterText}</Box>
</InputLeftAddon> </InputLeftAddon>
) : ( ) : (
<InputLeftElement> <InputLeftElement>
@ -141,7 +143,7 @@ function SearchToolbar({
</InputLeftElement> </InputLeftElement>
)} )}
<Input {...props} /> <Input {...props} />
{(query.value || query.filterToZoneLabel) && ( {(query.value || queryFilterText) && (
<InputRightElement> <InputRightElement>
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
@ -172,13 +174,11 @@ function SearchToolbar({
// for the InputLeftAddon, so the styles aren't updating... // for the InputLeftAddon, so the styles aren't updating...
// Hard override! // Hard override!
className: css` className: css`
padding-left: ${query.filterToZoneLabel padding-left: ${queryFilterText ? "1rem" : "2.5rem"} !important;
? "1rem" border-bottom-left-radius: ${queryFilterText
: "2.5rem"} !important;
border-bottom-left-radius: ${query.filterToZoneLabel
? "0" ? "0"
: "0.25rem"} !important; : "0.25rem"} !important;
border-top-left-radius: ${query.filterToZoneLabel border-top-left-radius: ${queryFilterText
? "0" ? "0"
: "0.25rem"} !important; : "0.25rem"} !important;
`, `,
@ -204,7 +204,11 @@ function SearchToolbar({
} }
onMoveFocusDownToResults(e); onMoveFocusDownToResults(e);
} else if (e.key === "Backspace" && e.target.selectionStart === 0) { } else if (e.key === "Backspace" && e.target.selectionStart === 0) {
onChange({ ...query, filterToZoneLabel: null }); onChange({
...query,
filterToItemKind: null,
filterToZoneLabel: null,
});
} }
}, },
}} }}
@ -212,17 +216,56 @@ function SearchToolbar({
); );
} }
function getSuggestions(value, zoneLabels) { function getSuggestions(value, query, zoneLabels) {
const words = value.split(/\s+/); const words = value.split(/\s+/);
const lastWord = words[words.length - 1]; const lastWord = words[words.length - 1];
if (lastWord.length < 2) { if (lastWord.length < 2) {
return []; return [];
} }
const matchingZoneLabels = zoneLabels.filter((zl) => const suggestions = [];
zl.toLowerCase().includes(lastWord.toLowerCase())
); if (query.filterToItemKind == null) {
return matchingZoneLabels; if (wordMatches("NC", lastWord) || wordMatches("Neocash", lastWord)) {
suggestions.push({ itemKind: "NC", text: "Neocash items" });
}
if (wordMatches("NP", lastWord) || wordMatches("Neopoints", lastWord)) {
suggestions.push({ itemKind: "NP", text: "Neopoint items" });
}
if (wordMatches("PB", lastWord) || wordMatches("Paintbrush", lastWord)) {
suggestions.push({ itemKind: "PB", text: "Paintbrush items" });
}
}
if (query.filterToZoneLabel == null) {
for (const zoneLabel of zoneLabels) {
if (wordMatches(zoneLabel, lastWord)) {
suggestions.push({ zoneLabel, text: zoneLabel });
}
}
}
return suggestions;
}
function wordMatches(target, word) {
return target.toLowerCase().includes(word.toLowerCase());
}
function getQueryFilterText(query) {
const textWords = [];
if (query.filterToItemKind) {
textWords.push(query.filterToItemKind);
}
if (query.filterToZoneLabel) {
textWords.push(query.filterToZoneLabel);
}
return textWords.join(" ");
} }
export default SearchToolbar; export default SearchToolbar;

View file

@ -219,12 +219,20 @@ const buildItemSearchLoader = (db, loaders) =>
return responses; return responses;
}); });
const itemSearchKindConditions = {
// NOTE: We assume that items cannot have NC rarity and the PB description,
// so we don't bother to filter out PB items in the NC filter, for perf.
NC: `rarity_index IN (0, 500)`,
NP: `rarity_index NOT IN (0, 500) AND description NOT LIKE "%This item is part of a deluxe paint brush set!%"`,
PB: `description LIKE "%This item is part of a deluxe paint brush set!%"`,
};
const buildItemSearchToFitLoader = (db, loaders) => const buildItemSearchToFitLoader = (db, loaders) =>
new DataLoader(async (queryAndBodyIdPairs) => { new DataLoader(async (queryAndBodyIdPairs) => {
// This isn't actually optimized as a batch query, we're just using a // This isn't actually optimized as a batch query, we're just using a
// DataLoader API consistency with our other loaders! // DataLoader API consistency with our other loaders!
const queryPromises = queryAndBodyIdPairs.map( const queryPromises = queryAndBodyIdPairs.map(
async ({ query, bodyId, zoneIds = [], offset, limit }) => { async ({ query, bodyId, itemKind, zoneIds = [], offset, limit }) => {
const actualOffset = offset || 0; const actualOffset = offset || 0;
const actualLimit = Math.min(limit || 30, 30); const actualLimit = Math.min(limit || 30, 30);
@ -235,6 +243,7 @@ const buildItemSearchToFitLoader = (db, loaders) =>
const matcherPlaceholders = words const matcherPlaceholders = words
.map((_) => "t.name LIKE ?") .map((_) => "t.name LIKE ?")
.join(" AND "); .join(" AND ");
const itemKindCondition = itemSearchKindConditions[itemKind] || "1";
const zoneIdsPlaceholder = const zoneIdsPlaceholder =
zoneIds.length > 0 zoneIds.length > 0
? `swf_assets.zone_id IN (${zoneIds.map((_) => "?").join(", ")})` ? `swf_assets.zone_id IN (${zoneIds.map((_) => "?").join(", ")})`
@ -245,9 +254,9 @@ const buildItemSearchToFitLoader = (db, loaders) =>
INNER JOIN parents_swf_assets rel INNER JOIN parents_swf_assets rel
ON rel.parent_type = "Item" AND rel.parent_id = items.id ON rel.parent_type = "Item" AND rel.parent_id = items.id
INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id
WHERE ${matcherPlaceholders} AND t.locale="en" AND WHERE ${matcherPlaceholders} AND t.locale = "en" AND
(swf_assets.body_id = ? OR swf_assets.body_id = 0) AND (swf_assets.body_id = ? OR swf_assets.body_id = 0) AND
${zoneIdsPlaceholder} ${zoneIdsPlaceholder} AND ${itemKindCondition}
ORDER BY t.name ORDER BY t.name
LIMIT ? OFFSET ?`, LIMIT ? OFFSET ?`,
[ [

View file

@ -69,6 +69,12 @@ const typeDefs = gql`
restrictedZones: [Zone!]! restrictedZones: [Zone!]!
} }
enum ItemKindSearchFilter {
NC
NP
PB
}
type ItemSearchResult { type ItemSearchResult {
query: String! query: String!
zones: [Zone!]! zones: [Zone!]!
@ -91,9 +97,10 @@ const typeDefs = gql`
itemSearch(query: String!): ItemSearchResult! itemSearch(query: String!): ItemSearchResult!
itemSearchToFit( itemSearchToFit(
query: String! query: String!
itemKind: ItemKindSearchFilter
zoneIds: [ID!]
speciesId: ID! speciesId: ID!
colorId: ID! colorId: ID!
zoneIds: [ID!]
offset: Int offset: Int
limit: Int limit: Int
): ItemSearchResult! ): ItemSearchResult!
@ -295,7 +302,7 @@ const resolvers = {
}, },
itemSearchToFit: async ( itemSearchToFit: async (
_, _,
{ query, speciesId, colorId, zoneIds = [], offset, limit }, { query, speciesId, colorId, itemKind, zoneIds = [], offset, limit },
{ petTypeBySpeciesAndColorLoader, itemSearchToFitLoader } { petTypeBySpeciesAndColorLoader, itemSearchToFitLoader }
) => { ) => {
const petType = await petTypeBySpeciesAndColorLoader.load({ const petType = await petTypeBySpeciesAndColorLoader.load({
@ -305,8 +312,9 @@ const resolvers = {
const { bodyId } = petType; const { bodyId } = petType;
const items = await itemSearchToFitLoader.load({ const items = await itemSearchToFitLoader.load({
query: query.trim(), query: query.trim(),
bodyId, itemKind,
zoneIds, zoneIds,
bodyId,
offset, offset,
limit, limit,
}); });