From 8b8d67e5b199154ce94bb5768a1a3f163c195072 Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Sat, 25 Apr 2020 00:43:01 -0700 Subject: [PATCH] restrict search results to items that fit! --- dev-todos.txt | 3 +- src/ItemsPanel.js | 2 +- src/SearchPanel.js | 12 ++-- src/WardrobePage.js | 17 +++-- src/server/index.js | 14 ++++ src/server/index.test.js | 137 ++++++++++++++++++++++++++++++++++----- src/server/loaders.js | 34 +++++++++- 7 files changed, 190 insertions(+), 29 deletions(-) diff --git a/dev-todos.txt b/dev-todos.txt index 33f3e64..fac3506 100644 --- a/dev-todos.txt +++ b/dev-todos.txt @@ -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 😅 diff --git a/src/ItemsPanel.js b/src/ItemsPanel.js index 6429243..ff8529d 100644 --- a/src/ItemsPanel.js +++ b/src/ItemsPanel.js @@ -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 ( diff --git a/src/SearchPanel.js b/src/SearchPanel.js index 9408a6c..41b89bc 100644 --- a/src/SearchPanel.js +++ b/src/SearchPanel.js @@ -10,7 +10,7 @@ import { itemAppearanceFragment } from "./OutfitPreview"; function SearchPanel({ query, outfitState, dispatchToOutfit }) { return ( - Searching for "{query}" + Searching for "{query}" - - - {searchQuery ? ( + + {searchQuery ? ( + + - ) : ( + + + ) : ( + + - )} + - + )} ); diff --git a/src/server/index.js b/src/server/index.js index 9f5b7ed..098ad7f 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -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 }, diff --git a/src/server/index.test.js b/src/server/index.test.js index 32ad6f4..41da19b 100644 --- a/src/server/index.test.js +++ b/src/server/index.test.js @@ -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", ], ], ] diff --git a/src/server/loaders.js b/src/server/loaders.js index c3230ec..86ca794 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -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),