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}
|
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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 ?`,
|
||||||
[
|
[
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue