restrict search results to items that fit!
This commit is contained in:
parent
5264509b53
commit
8b8d67e5b1
7 changed files with 190 additions and 29 deletions
|
@ -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 😅
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -65,23 +65,28 @@ function WardrobePage() {
|
|||
<SearchToolbar query={searchQuery} onChange={setSearchQuery} />
|
||||
</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
|
||||
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>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
],
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue