Show real zone data on item page
And some Cypress specs to test the basic cases!
This commit is contained in:
parent
20f9573e50
commit
614bad72d2
3 changed files with 148 additions and 43 deletions
45
cypress/integration/ItemPage/ItemZonesInfo.spec.js
Normal file
45
cypress/integration/ItemPage/ItemZonesInfo.spec.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Give network requests a bit of breathing room! (A bit more, because this
|
||||
// page has a lot of data!)
|
||||
const networkTimeout = { timeout: 10000 };
|
||||
|
||||
describe("ItemZonesInfo", () => {
|
||||
it("shows simple zone data for an all-species Background", () => {
|
||||
cy.visit("/items/37375");
|
||||
cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
|
||||
"have.text",
|
||||
"Zone: Background"
|
||||
);
|
||||
});
|
||||
|
||||
it("shows simple zone data for a species-specific Hat", () => {
|
||||
cy.visit("/items/34985");
|
||||
cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
|
||||
"have.text",
|
||||
"Zone: Hat"
|
||||
);
|
||||
});
|
||||
|
||||
it("shows distinct zone data for an all-species in-hand item", () => {
|
||||
cy.visit("/items/43397");
|
||||
cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
|
||||
"have.text",
|
||||
"Zones: Right-hand Item (52 species)" + "Left-hand Item (2 species)"
|
||||
);
|
||||
|
||||
cy.contains("(52 species)").focus();
|
||||
cy.contains(
|
||||
"Acara, Aisha, Blumaroo, Bori, Bruce, Buzz, Chia, Chomby, Cybunny, Draik, Elephante, Eyrie, Flotsam, Gelert, Gnorbu, Grarrl, Grundo, Hissi, Ixi, Jetsam, Jubjub, Kacheek, Kau, Kiko, Koi, Korbat, Kougra, Krawk, Kyrii, Lupe, Lutari, Meerca, Moehog, Mynci, Nimmo, Ogrin, Peophin, Poogle, Pteri, Quiggle, Ruki, Scorchio, Shoyru, Skeith, Techo, Tonu, Uni, Usul, Wocky, Xweetok, Yurble, Zafara"
|
||||
).should("exist");
|
||||
|
||||
cy.contains("(2 species)").focus();
|
||||
cy.contains("Lenny, Tuskaninny").should("exist");
|
||||
});
|
||||
|
||||
it("shows simple zone data for a Mutant-only Dress", () => {
|
||||
cy.visit("/items/70564");
|
||||
cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
|
||||
"have.text",
|
||||
"Zone: Shirt/Dress"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -574,9 +574,19 @@ function ItemPageOutfitPreview({ itemId }) {
|
|||
item(id: $itemId) {
|
||||
id
|
||||
name
|
||||
compatibleBodies {
|
||||
id
|
||||
representsAllBodies
|
||||
compatibleBodiesAndTheirZones {
|
||||
body {
|
||||
id
|
||||
representsAllBodies
|
||||
species {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
zones {
|
||||
id
|
||||
label @client
|
||||
}
|
||||
}
|
||||
canonicalAppearance(
|
||||
preferredSpeciesId: $preferredSpeciesId
|
||||
|
@ -628,7 +638,10 @@ function ItemPageOutfitPreview({ itemId }) {
|
|||
}
|
||||
);
|
||||
|
||||
const compatibleBodies = data?.item?.compatibleBodies || [];
|
||||
const compatibleBodies =
|
||||
data?.item?.compatibleBodiesAndTheirZones?.map(({ body }) => body) || [];
|
||||
const compatibleBodiesAndTheirZones =
|
||||
data?.item?.compatibleBodiesAndTheirZones || [];
|
||||
|
||||
// If there's only one compatible body, and the canonical species's name
|
||||
// appears in the item name, then this is probably a species-specific item,
|
||||
|
@ -825,29 +838,11 @@ function ItemPageOutfitPreview({ itemId }) {
|
|||
/>
|
||||
</Box>
|
||||
<Box gridArea="zones" alignSelf="center" justifySelf="center">
|
||||
<ItemZonesInfo
|
||||
zonesAndTheirBodies={[
|
||||
{
|
||||
zone: { id: "1", label: "Foreground" },
|
||||
bodies: [{ id: "0", representsAllBodies: true }],
|
||||
},
|
||||
{
|
||||
zone: { id: "1", label: "Background" },
|
||||
bodies: [
|
||||
{
|
||||
id: "1",
|
||||
representsAllBodies: false,
|
||||
species: { name: "Blumaroo" },
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
representsAllBodies: false,
|
||||
species: { name: "Aisha" },
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{compatibleBodiesAndTheirZones.length > 0 && (
|
||||
<ItemZonesInfo
|
||||
compatibleBodiesAndTheirZones={compatibleBodiesAndTheirZones}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
|
@ -1276,34 +1271,55 @@ function SpeciesFaceOption({
|
|||
);
|
||||
}
|
||||
|
||||
function ItemZonesInfo({ zonesAndTheirBodies }) {
|
||||
const sortedZonesAndTheirBodies = [...zonesAndTheirBodies].sort((a, b) =>
|
||||
zonesAndTheirBodiesSortKey(a).localeCompare(zonesAndTheirBodiesSortKey(b))
|
||||
function ItemZonesInfo({ compatibleBodiesAndTheirZones }) {
|
||||
// Reorganize the body-and-zones data, into zone-and-bodies data. Also, we're
|
||||
// merging zones with the same label, because that's how user-facing zone UI
|
||||
// generally works!
|
||||
const zoneLabelsAndTheirBodiesMap = {};
|
||||
for (const { body, zones } of compatibleBodiesAndTheirZones) {
|
||||
for (const zone of zones) {
|
||||
if (!zoneLabelsAndTheirBodiesMap[zone.label]) {
|
||||
zoneLabelsAndTheirBodiesMap[zone.label] = {
|
||||
zoneLabel: zone.label,
|
||||
bodies: [],
|
||||
};
|
||||
}
|
||||
zoneLabelsAndTheirBodiesMap[zone.label].bodies.push(body);
|
||||
}
|
||||
}
|
||||
const zoneLabelsAndTheirBodies = Object.values(zoneLabelsAndTheirBodiesMap);
|
||||
|
||||
const sortedZonesAndTheirBodies = [...zoneLabelsAndTheirBodies].sort((a, b) =>
|
||||
buildSortKeyForZoneLabelsAndTheirBodies(a).localeCompare(
|
||||
buildSortKeyForZoneLabelsAndTheirBodies(b)
|
||||
)
|
||||
);
|
||||
|
||||
// We only show body info if there's more than one group of bodies to talk
|
||||
// about. If they all have the same zones, it's clear from context that any
|
||||
// preview available in the list has the zones listed here.
|
||||
const bodyGroups = new Set(
|
||||
zonesAndTheirBodies.map(({ bodies }) => bodies.map((b) => b.id).join(","))
|
||||
zoneLabelsAndTheirBodies.map(({ bodies }) =>
|
||||
bodies.map((b) => b.id).join(",")
|
||||
)
|
||||
);
|
||||
const showBodyInfo = bodyGroups.size > 1;
|
||||
|
||||
return (
|
||||
<Box fontSize="sm" textAlign="center">
|
||||
<Box fontSize="sm" textAlign="center" data-test-id="item-zones-info">
|
||||
<Box as="header" fontWeight="bold" display="inline">
|
||||
Zones:
|
||||
{sortedZonesAndTheirBodies.length > 1 ? "Zones" : "Zone"}:
|
||||
</Box>{" "}
|
||||
<Box as="ul" listStyleType="none" display="inline">
|
||||
{sortedZonesAndTheirBodies.map(({ zone, bodies }) => (
|
||||
{sortedZonesAndTheirBodies.map(({ zoneLabel, bodies }) => (
|
||||
<Box
|
||||
key={zone.id}
|
||||
key={zoneLabel}
|
||||
as="li"
|
||||
display="inline"
|
||||
_notLast={{ _after: { content: '", "' } }}
|
||||
>
|
||||
<ItemZonesInfoListItem
|
||||
zone={zone}
|
||||
zoneLabel={zoneLabel}
|
||||
bodies={bodies}
|
||||
showBodyInfo={showBodyInfo}
|
||||
/>
|
||||
|
@ -1314,8 +1330,8 @@ function ItemZonesInfo({ zonesAndTheirBodies }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
|
||||
let content = zone.label;
|
||||
function ItemZonesInfoListItem({ zoneLabel, bodies, showBodyInfo }) {
|
||||
let content = zoneLabel;
|
||||
|
||||
if (showBodyInfo) {
|
||||
if (bodies.some((b) => b.representsAllBodies)) {
|
||||
|
@ -1332,7 +1348,7 @@ function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
|
|||
content = (
|
||||
<>
|
||||
{content}{" "}
|
||||
<Tooltip label={speciesListString}>
|
||||
<Tooltip label={speciesListString} textAlign="center" placement="top">
|
||||
<Box
|
||||
as="span"
|
||||
tabIndex="0"
|
||||
|
@ -1354,11 +1370,20 @@ function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
|
|||
return content;
|
||||
}
|
||||
|
||||
function zonesAndTheirBodiesSortKey({ zone, bodies }) {
|
||||
// Sort by "represents all bodies", then by body count, then alphabetically.
|
||||
function buildSortKeyForZoneLabelsAndTheirBodies({ zoneLabel, bodies }) {
|
||||
// Sort by "represents all bodies", then by body count descending, then
|
||||
// alphabetically.
|
||||
const representsAllBodies = bodies.some((body) => body.representsAllBodies);
|
||||
const bodyCountString = bodies.length.toString().padStart(4, "0");
|
||||
return `${representsAllBodies ? "A" : "Z"}-${bodyCountString}-${zone.label}`;
|
||||
|
||||
// To sort by body count _descending_, we subtract it from a large number.
|
||||
// Then, to make it work in string comparison, we pad it with leading zeroes.
|
||||
// Hacky but solid!
|
||||
const inverseBodyCount = (9999 - bodies.length).toString().padStart(4, "0");
|
||||
|
||||
console.log(
|
||||
`${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`
|
||||
);
|
||||
return `${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -85,7 +85,13 @@ const typeDefs = gql`
|
|||
|
||||
# All bodies that this item is compatible with. Note that this might return
|
||||
# the special representsAllPets body, e.g. if this is just a Background!
|
||||
# Deprecated: Impress 2020 now uses compatibleBodiesAndTheirZones.
|
||||
compatibleBodies: [Body!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
|
||||
|
||||
# All bodies that this item is compatible with, and the zones this item
|
||||
# occupies for that body. Note that this might return the special
|
||||
# representsAllPets body, e.g. if this is just a Background!
|
||||
compatibleBodiesAndTheirZones: [BodyAndZones!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
|
||||
}
|
||||
|
||||
type ItemAppearance {
|
||||
|
@ -97,6 +103,11 @@ const typeDefs = gql`
|
|||
restrictedZones: [Zone!]!
|
||||
}
|
||||
|
||||
type BodyAndZones {
|
||||
body: Body!
|
||||
zones: [Zone!]!
|
||||
}
|
||||
|
||||
input FitsPetSearchFilter {
|
||||
speciesId: ID!
|
||||
colorId: ID!
|
||||
|
@ -404,6 +415,30 @@ const resolvers = {
|
|||
const bodies = bodyIds.map((id) => ({ id }));
|
||||
return bodies;
|
||||
},
|
||||
compatibleBodiesAndTheirZones: async ({ id }, _, { db }) => {
|
||||
const [rows, __] = await db.query(
|
||||
`
|
||||
SELECT
|
||||
swf_assets.body_id AS bodyId,
|
||||
(SELECT species_id FROM pet_types WHERE body_id = bodyId LIMIT 1)
|
||||
AS speciesId,
|
||||
GROUP_CONCAT(DISTINCT swf_assets.zone_id) AS zoneIds
|
||||
FROM items
|
||||
INNER JOIN parents_swf_assets ON
|
||||
items.id = parents_swf_assets.parent_id AND
|
||||
parents_swf_assets.parent_type = "Item"
|
||||
INNER JOIN swf_assets ON
|
||||
parents_swf_assets.swf_asset_id = swf_assets.id
|
||||
WHERE items.id = ?
|
||||
GROUP BY swf_assets.body_id
|
||||
`,
|
||||
[id]
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
body: { id: row.bodyId, species: { id: row.speciesId } },
|
||||
zones: row.zoneIds.split(",").map((zoneId) => ({ id: zoneId })),
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
||||
ItemAppearance: {
|
||||
|
|
Loading…
Reference in a new issue