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? * Use accessible click targets for item lists! Honestly, can they be checkboxes?
* Pagination for search queries, right now we LIMIT 30 * 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 😅 * 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"; import "./ItemsPanel.css";
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) { function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
const { zonesAndItems, wornItemIds } = outfitState; const { zonesAndItems } = outfitState;
return ( return (
<Box color="green.800"> <Box color="green.800">

View file

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

View file

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

View file

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

View file

@ -371,11 +371,11 @@ describe("PetAppearance", () => {
}); });
describe("Search", () => { describe("Search", () => {
it("loads Zafara Agent items", async () => { it("loads Neopian Times items", async () => {
const res = await query({ const res = await query({
query: gql` query: gql`
query { query {
itemSearch(query: "Zafara Agent") { itemSearch(query: "Neopian Times") {
id id
name name
} }
@ -388,16 +388,52 @@ describe("Search", () => {
Object { Object {
"itemSearch": Array [ "itemSearch": Array [
Object { Object {
"id": "38913", "id": "40431",
"name": "Zafara Agent Gloves", "name": "Neopian Times Background",
}, },
Object { Object {
"id": "38911", "id": "59391",
"name": "Zafara Agent Hood", "name": "Neopian Times Eyrie Hat",
}, },
Object { Object {
"id": "38912", "id": "59392",
"name": "Zafara Agent Robe", "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(` expect(queryFn.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"SELECT items.* FROM items "SELECT items.*, t.name FROM items
INNER JOIN item_translations t ON t.item_id = items.id 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 ORDER BY t.name
LIMIT 30", LIMIT 30",
Array [ 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 [ 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 [ Array [
"38913", "%Neopian Times%",
"38911", "180",
"38912",
], ],
], ],
] ]

View file

@ -42,9 +42,9 @@ const buildItemSearchLoader = (db) =>
const queryPromises = queries.map(async (query) => { const queryPromises = queries.map(async (query) => {
const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%"; const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%";
const [rows, _] = await db.execute( 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 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 ORDER BY t.name
LIMIT 30`, LIMIT 30`,
[queryForMysql] [queryForMysql]
@ -60,6 +60,35 @@ const buildItemSearchLoader = (db) =>
return responses; 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) => const buildPetTypeLoader = (db) =>
new DataLoader(async (speciesAndColorPairs) => { new DataLoader(async (speciesAndColorPairs) => {
const conditions = []; const conditions = [];
@ -200,6 +229,7 @@ function buildLoaders(db) {
itemLoader: buildItemsLoader(db), itemLoader: buildItemsLoader(db),
itemTranslationLoader: buildItemTranslationLoader(db), itemTranslationLoader: buildItemTranslationLoader(db),
itemSearchLoader: buildItemSearchLoader(db), itemSearchLoader: buildItemSearchLoader(db),
itemSearchToFitLoader: buildItemSearchToFitLoader(db),
petTypeLoader: buildPetTypeLoader(db), petTypeLoader: buildPetTypeLoader(db),
itemSwfAssetLoader: buildItemSwfAssetLoader(db), itemSwfAssetLoader: buildItemSwfAssetLoader(db),
petSwfAssetLoader: buildPetSwfAssetLoader(db), petSwfAssetLoader: buildPetSwfAssetLoader(db),