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) {
|
item(id: $itemId) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
compatibleBodies {
|
compatibleBodiesAndTheirZones {
|
||||||
id
|
body {
|
||||||
representsAllBodies
|
id
|
||||||
|
representsAllBodies
|
||||||
|
species {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zones {
|
||||||
|
id
|
||||||
|
label @client
|
||||||
|
}
|
||||||
}
|
}
|
||||||
canonicalAppearance(
|
canonicalAppearance(
|
||||||
preferredSpeciesId: $preferredSpeciesId
|
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
|
// 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,
|
// appears in the item name, then this is probably a species-specific item,
|
||||||
|
@ -825,29 +838,11 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box gridArea="zones" alignSelf="center" justifySelf="center">
|
<Box gridArea="zones" alignSelf="center" justifySelf="center">
|
||||||
<ItemZonesInfo
|
{compatibleBodiesAndTheirZones.length > 0 && (
|
||||||
zonesAndTheirBodies={[
|
<ItemZonesInfo
|
||||||
{
|
compatibleBodiesAndTheirZones={compatibleBodiesAndTheirZones}
|
||||||
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" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
@ -1276,34 +1271,55 @@ function SpeciesFaceOption({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemZonesInfo({ zonesAndTheirBodies }) {
|
function ItemZonesInfo({ compatibleBodiesAndTheirZones }) {
|
||||||
const sortedZonesAndTheirBodies = [...zonesAndTheirBodies].sort((a, b) =>
|
// Reorganize the body-and-zones data, into zone-and-bodies data. Also, we're
|
||||||
zonesAndTheirBodiesSortKey(a).localeCompare(zonesAndTheirBodiesSortKey(b))
|
// 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
|
// 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
|
// 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.
|
// preview available in the list has the zones listed here.
|
||||||
const bodyGroups = new Set(
|
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;
|
const showBodyInfo = bodyGroups.size > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box fontSize="sm" textAlign="center">
|
<Box fontSize="sm" textAlign="center" data-test-id="item-zones-info">
|
||||||
<Box as="header" fontWeight="bold" display="inline">
|
<Box as="header" fontWeight="bold" display="inline">
|
||||||
Zones:
|
{sortedZonesAndTheirBodies.length > 1 ? "Zones" : "Zone"}:
|
||||||
</Box>{" "}
|
</Box>{" "}
|
||||||
<Box as="ul" listStyleType="none" display="inline">
|
<Box as="ul" listStyleType="none" display="inline">
|
||||||
{sortedZonesAndTheirBodies.map(({ zone, bodies }) => (
|
{sortedZonesAndTheirBodies.map(({ zoneLabel, bodies }) => (
|
||||||
<Box
|
<Box
|
||||||
key={zone.id}
|
key={zoneLabel}
|
||||||
as="li"
|
as="li"
|
||||||
display="inline"
|
display="inline"
|
||||||
_notLast={{ _after: { content: '", "' } }}
|
_notLast={{ _after: { content: '", "' } }}
|
||||||
>
|
>
|
||||||
<ItemZonesInfoListItem
|
<ItemZonesInfoListItem
|
||||||
zone={zone}
|
zoneLabel={zoneLabel}
|
||||||
bodies={bodies}
|
bodies={bodies}
|
||||||
showBodyInfo={showBodyInfo}
|
showBodyInfo={showBodyInfo}
|
||||||
/>
|
/>
|
||||||
|
@ -1314,8 +1330,8 @@ function ItemZonesInfo({ zonesAndTheirBodies }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
|
function ItemZonesInfoListItem({ zoneLabel, bodies, showBodyInfo }) {
|
||||||
let content = zone.label;
|
let content = zoneLabel;
|
||||||
|
|
||||||
if (showBodyInfo) {
|
if (showBodyInfo) {
|
||||||
if (bodies.some((b) => b.representsAllBodies)) {
|
if (bodies.some((b) => b.representsAllBodies)) {
|
||||||
|
@ -1332,7 +1348,7 @@ function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
{content}{" "}
|
{content}{" "}
|
||||||
<Tooltip label={speciesListString}>
|
<Tooltip label={speciesListString} textAlign="center" placement="top">
|
||||||
<Box
|
<Box
|
||||||
as="span"
|
as="span"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
|
@ -1354,11 +1370,20 @@ function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
function zonesAndTheirBodiesSortKey({ zone, bodies }) {
|
function buildSortKeyForZoneLabelsAndTheirBodies({ zoneLabel, bodies }) {
|
||||||
// Sort by "represents all bodies", then by body count, then alphabetically.
|
// Sort by "represents all bodies", then by body count descending, then
|
||||||
|
// alphabetically.
|
||||||
const representsAllBodies = bodies.some((body) => body.representsAllBodies);
|
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
|
# 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!
|
# 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})
|
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 {
|
type ItemAppearance {
|
||||||
|
@ -97,6 +103,11 @@ const typeDefs = gql`
|
||||||
restrictedZones: [Zone!]!
|
restrictedZones: [Zone!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BodyAndZones {
|
||||||
|
body: Body!
|
||||||
|
zones: [Zone!]!
|
||||||
|
}
|
||||||
|
|
||||||
input FitsPetSearchFilter {
|
input FitsPetSearchFilter {
|
||||||
speciesId: ID!
|
speciesId: ID!
|
||||||
colorId: ID!
|
colorId: ID!
|
||||||
|
@ -404,6 +415,30 @@ const resolvers = {
|
||||||
const bodies = bodyIds.map((id) => ({ id }));
|
const bodies = bodyIds.map((id) => ({ id }));
|
||||||
return bodies;
|
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: {
|
ItemAppearance: {
|
||||||
|
|
Loading…
Reference in a new issue