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?
|
* 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 😅
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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",
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue