nc/np/pb search filters
This commit is contained in:
parent
63a17824e5
commit
02e173d7de
5 changed files with 108 additions and 32 deletions
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(", ")})`
|
||||
|
@ -245,9 +254,9 @@ const buildItemSearchToFitLoader = (db, loaders) =>
|
|||
INNER JOIN parents_swf_assets rel
|
||||
ON rel.parent_type = "Item" AND rel.parent_id = items.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
|
||||
${zoneIdsPlaceholder}
|
||||
${zoneIdsPlaceholder} AND ${itemKindCondition}
|
||||
ORDER BY t.name
|
||||
LIMIT ? OFFSET ?`,
|
||||
[
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue