restrict search results to items that fit!

This commit is contained in:
Matt Dunn-Rankin 2020-04-25 00:43:01 -07:00
parent 5264509b53
commit 8b8d67e5b1
7 changed files with 190 additions and 29 deletions

View file

@ -1,4 +1,5 @@
* Use accessible click targets for item lists! Honestly, can they be checkboxes?
* Pagination for search queries, right now we LIMIT 30
* Search needs to restrict by fit!
* Update skeletons for ItemList and ItemsPanel
* Merge zones with the same name
* Undo the local linking we did for @chakra-ui/core, react, and react-dom on Matchu's machine 😅

View file

@ -17,7 +17,7 @@ import ItemList, { ItemListSkeleton } from "./ItemList";
import "./ItemsPanel.css";
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
const { zonesAndItems, wornItemIds } = outfitState;
const { zonesAndItems } = outfitState;
return (
<Box color="green.800">

View file

@ -10,7 +10,7 @@ import { itemAppearanceFragment } from "./OutfitPreview";
function SearchPanel({ query, outfitState, dispatchToOutfit }) {
return (
<Box color="green.800">
<Heading1 mb="6">Searching for "{query}"</Heading1>
<Heading1 mb="4">Searching for "{query}"</Heading1>
<SearchResults
query={query}
outfitState={outfitState}
@ -21,14 +21,18 @@ function SearchPanel({ query, outfitState, dispatchToOutfit }) {
}
function SearchResults({ query, outfitState, dispatchToOutfit }) {
const { wornItemIds, speciesId, colorId } = outfitState;
const { speciesId, colorId } = outfitState;
const debouncedQuery = useDebounce(query, 300, { waitForFirstPause: true });
const { loading, error, data, variables } = useQuery(
gql`
query($query: String!, $speciesId: ID!, $colorId: ID!) {
itemSearch(query: $query) {
itemSearchToFit(
query: $query
speciesId: $speciesId
colorId: $colorId
) {
# TODO: De-dupe this from useOutfitState?
id
name
@ -77,7 +81,7 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
);
}
const items = data.itemSearch;
const items = data.itemSearchToFit;
if (items.length === 0) {
return (

View file

@ -65,23 +65,28 @@ function WardrobePage() {
<SearchToolbar query={searchQuery} onChange={setSearchQuery} />
</Box>
</Box>
<Box gridArea="items" overflow="auto">
<Box px="5" py="5">
{searchQuery ? (
<Box gridArea="items" overflow="auto" key="search-panel">
<Box px="5" py="5">
<SearchPanel
query={searchQuery}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
</Box>
</Box>
) : (
<Box gridArea="items" overflow="auto" key="items-panel">
<Box px="5" py="5">
<ItemsPanel
loading={loading}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
</Box>
</Box>
)}
</Box>
</Box>
</Grid>
</Box>
);

View file

@ -37,6 +37,7 @@ const typeDefs = gql`
type Query {
items(ids: [ID!]!): [Item!]!
itemSearch(query: String!): [Item!]!
itemSearchToFit(query: String!, speciesId: ID!, colorId: ID!): [Item!]!
petAppearance(speciesId: ID!, colorId: ID!): Appearance
}
`;
@ -44,6 +45,9 @@ const typeDefs = gql`
const resolvers = {
Item: {
name: async (item, _, { itemTranslationLoader }) => {
// Search queries pre-fill this!
if (item.name) return item.name;
const translation = await itemTranslationLoader.load(item.id);
return translation.name;
},
@ -112,6 +116,16 @@ const resolvers = {
const items = await itemSearchLoader.load(query);
return items;
},
itemSearchToFit: async (
_,
{ query, speciesId, colorId },
{ petTypeLoader, itemSearchToFitLoader }
) => {
const petType = await petTypeLoader.load({ speciesId, colorId });
const { bodyId } = petType;
const items = await itemSearchToFitLoader.load({ query, bodyId });
return items;
},
petAppearance: async (
_,
{ speciesId, colorId },

View file

@ -371,11 +371,11 @@ describe("PetAppearance", () => {
});
describe("Search", () => {
it("loads Zafara Agent items", async () => {
it("loads Neopian Times items", async () => {
const res = await query({
query: gql`
query {
itemSearch(query: "Zafara Agent") {
itemSearch(query: "Neopian Times") {
id
name
}
@ -388,16 +388,52 @@ describe("Search", () => {
Object {
"itemSearch": Array [
Object {
"id": "38913",
"name": "Zafara Agent Gloves",
"id": "40431",
"name": "Neopian Times Background",
},
Object {
"id": "38911",
"name": "Zafara Agent Hood",
"id": "59391",
"name": "Neopian Times Eyrie Hat",
},
Object {
"id": "38912",
"name": "Zafara Agent Robe",
"id": "59392",
"name": "Neopian Times Eyrie Shirt and Vest",
},
Object {
"id": "59394",
"name": "Neopian Times Eyrie Shoes",
},
Object {
"id": "59393",
"name": "Neopian Times Eyrie Trousers",
},
Object {
"id": "59390",
"name": "Neopian Times Eyries Paper",
},
Object {
"id": "51098",
"name": "Neopian Times Writing Quill",
},
Object {
"id": "61101",
"name": "Neopian Times Zafara Handkerchief",
},
Object {
"id": "61100",
"name": "Neopian Times Zafara Hat",
},
Object {
"id": "61102",
"name": "Neopian Times Zafara Shirt and Vest",
},
Object {
"id": "61104",
"name": "Neopian Times Zafara Shoes",
},
Object {
"id": "61103",
"name": "Neopian Times Zafara Trousers",
},
],
}
@ -405,21 +441,92 @@ describe("Search", () => {
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"SELECT items.* FROM items
"SELECT items.*, t.name FROM items
INNER JOIN item_translations t ON t.item_id = items.id
WHERE t.name LIKE ? AND locale=\\"en\\"
WHERE t.name LIKE ? AND t.locale=\\"en\\"
ORDER BY t.name
LIMIT 30",
Array [
"%Zafara Agent%",
"%Neopian Times%",
],
],
]
`);
});
it("loads Neopian Times items that fit the Starry Zafara", async () => {
const res = await query({
query: gql`
query {
itemSearchToFit(
query: "Neopian Times"
speciesId: "54"
colorId: "75"
) {
id
name
}
}
`,
});
expect(res).toHaveNoErrors();
expect(res.data).toMatchInlineSnapshot(`
Object {
"itemSearchToFit": Array [
Object {
"id": "40431",
"name": "Neopian Times Background",
},
Object {
"id": "51098",
"name": "Neopian Times Writing Quill",
},
Object {
"id": "61101",
"name": "Neopian Times Zafara Handkerchief",
},
Object {
"id": "61100",
"name": "Neopian Times Zafara Hat",
},
Object {
"id": "61102",
"name": "Neopian Times Zafara Shirt and Vest",
},
Object {
"id": "61104",
"name": "Neopian Times Zafara Shoes",
},
Object {
"id": "61103",
"name": "Neopian Times Zafara Trousers",
},
],
}
`);
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)",
Array [
"54",
"75",
],
],
Array [
"SELECT * FROM item_translations WHERE item_id IN (?,?,?) AND locale = \\"en\\"",
"SELECT items.*, t.name FROM items
INNER JOIN item_translations t ON t.item_id = items.id
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 t.name LIKE ? AND t.locale=\\"en\\" AND
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
ORDER BY t.name
LIMIT 30",
Array [
"38913",
"38911",
"38912",
"%Neopian Times%",
"180",
],
],
]

View file

@ -42,9 +42,9 @@ const buildItemSearchLoader = (db) =>
const queryPromises = queries.map(async (query) => {
const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%";
const [rows, _] = await db.execute(
`SELECT items.* FROM items
`SELECT items.*, t.name FROM items
INNER JOIN item_translations t ON t.item_id = items.id
WHERE t.name LIKE ? AND locale="en"
WHERE t.name LIKE ? AND t.locale="en"
ORDER BY t.name
LIMIT 30`,
[queryForMysql]
@ -60,6 +60,35 @@ const buildItemSearchLoader = (db) =>
return responses;
});
const buildItemSearchToFitLoader = (db) =>
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 }) => {
const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%";
const [rows, _] = await db.execute(
`SELECT items.*, t.name FROM items
INNER JOIN item_translations t ON t.item_id = items.id
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 t.name LIKE ? AND t.locale="en" AND
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
ORDER BY t.name
LIMIT 30`,
[queryForMysql, bodyId]
);
const entities = rows.map(normalizeRow);
return entities;
});
const responses = await Promise.all(queryPromises);
return responses;
});
const buildPetTypeLoader = (db) =>
new DataLoader(async (speciesAndColorPairs) => {
const conditions = [];
@ -200,6 +229,7 @@ function buildLoaders(db) {
itemLoader: buildItemsLoader(db),
itemTranslationLoader: buildItemTranslationLoader(db),
itemSearchLoader: buildItemSearchLoader(db),
itemSearchToFitLoader: buildItemSearchToFitLoader(db),
petTypeLoader: buildPetTypeLoader(db),
itemSwfAssetLoader: buildItemSwfAssetLoader(db),
petSwfAssetLoader: buildPetSwfAssetLoader(db),