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}
/>
</Box>
{searchQuery.value || searchQuery.filterToZoneLabel ? (
{searchQuery.value ||
searchQuery.filterToItemKind ||
searchQuery.filterToZoneLabel ? (
<Box
key="search-panel"
gridArea="items"

View file

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

View file

@ -61,7 +61,7 @@ function SearchToolbar({
const highlightedBgColor = useColorModeValue("gray.100", "whiteAlpha.300");
const renderSuggestion = React.useCallback(
(zoneLabel, { isHighlighted }) => (
({ text }, { isHighlighted }) => (
<Box
fontWeight={isHighlighted ? "bold" : "normal"}
background={isHighlighted ? highlightedBgColor : suggestionBgColor}
@ -69,7 +69,7 @@ function SearchToolbar({
paddingLeft="2.5rem"
fontSize="sm"
>
{zoneLabel}
{text}
</Box>
),
[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(() => {
setSuggestions([]);
}, [query.filterToZoneLabel]);
}, [query.filterToItemKind, query.filterToZoneLabel]);
const queryFilterText = getQueryFilterText(query);
const focusBorderColor = useColorModeValue("green.600", "green.400");
@ -112,7 +114,7 @@ function SearchToolbar({
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={({ value }) =>
setSuggestions(getSuggestions(value, zoneLabels))
setSuggestions(getSuggestions(value, query, zoneLabels))
}
onSuggestionsClearRequested={() => setSuggestions([])}
onSuggestionSelected={(e, { suggestion }) => {
@ -120,20 +122,20 @@ function SearchToolbar({
onChange({
...query,
value: valueWithoutLastWord,
filterToZoneLabel: suggestion,
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
});
}}
getSuggestionValue={(zl) => zl}
shouldRenderSuggestions={() => query.filterToZoneLabel == null}
highlightFirstSuggestion={true}
renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer}
renderInputComponent={(props) => (
<InputGroup>
{query.filterToZoneLabel ? (
{queryFilterText ? (
<InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{query.filterToZoneLabel}</Box>
<Box fontSize="sm">{queryFilterText}</Box>
</InputLeftAddon>
) : (
<InputLeftElement>
@ -141,7 +143,7 @@ function SearchToolbar({
</InputLeftElement>
)}
<Input {...props} />
{(query.value || query.filterToZoneLabel) && (
{(query.value || queryFilterText) && (
<InputRightElement>
<IconButton
icon={<CloseIcon />}
@ -172,13 +174,11 @@ function SearchToolbar({
// for the InputLeftAddon, so the styles aren't updating...
// Hard override!
className: css`
padding-left: ${query.filterToZoneLabel
? "1rem"
: "2.5rem"} !important;
border-bottom-left-radius: ${query.filterToZoneLabel
padding-left: ${queryFilterText ? "1rem" : "2.5rem"} !important;
border-bottom-left-radius: ${queryFilterText
? "0"
: "0.25rem"} !important;
border-top-left-radius: ${query.filterToZoneLabel
border-top-left-radius: ${queryFilterText
? "0"
: "0.25rem"} !important;
`,
@ -204,7 +204,11 @@ function SearchToolbar({
}
onMoveFocusDownToResults(e);
} 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 lastWord = words[words.length - 1];
if (lastWord.length < 2) {
return [];
}
const matchingZoneLabels = zoneLabels.filter((zl) =>
zl.toLowerCase().includes(lastWord.toLowerCase())
);
return matchingZoneLabels;
const suggestions = [];
if (query.filterToItemKind == null) {
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;

View file

@ -219,12 +219,20 @@ const buildItemSearchLoader = (db, loaders) =>
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) =>
new DataLoader(async (queryAndBodyIdPairs) => {
// This isn't actually optimized as a batch query, we're just using a
// DataLoader API consistency with our other loaders!
const queryPromises = queryAndBodyIdPairs.map(
async ({ query, bodyId, zoneIds = [], offset, limit }) => {
async ({ query, bodyId, itemKind, zoneIds = [], offset, limit }) => {
const actualOffset = offset || 0;
const actualLimit = Math.min(limit || 30, 30);
@ -235,6 +243,7 @@ const buildItemSearchToFitLoader = (db, loaders) =>
const matcherPlaceholders = words
.map((_) => "t.name LIKE ?")
.join(" AND ");
const itemKindCondition = itemSearchKindConditions[itemKind] || "1";
const zoneIdsPlaceholder =
zoneIds.length > 0
? `swf_assets.zone_id IN (${zoneIds.map((_) => "?").join(", ")})`
@ -247,7 +256,7 @@ const buildItemSearchToFitLoader = (db, loaders) =>
INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id
WHERE ${matcherPlaceholders} AND t.locale = "en" AND
(swf_assets.body_id = ? OR swf_assets.body_id = 0) AND
${zoneIdsPlaceholder}
${zoneIdsPlaceholder} AND ${itemKindCondition}
ORDER BY t.name
LIMIT ? OFFSET ?`,
[

View file

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