can submit the actual body ID Support mutation!

it seems to actually be changing the things correctly aaaa
This commit is contained in:
Emi Matchu 2020-08-01 15:30:26 -07:00
parent e8917936d6
commit 8fdc986ee9
4 changed files with 211 additions and 28 deletions

View file

@ -1,4 +1,6 @@
import * as React from "react"; import * as React from "react";
import gql from "graphql-tag";
import { useMutation } from "@apollo/client";
import { import {
Button, Button,
Box, Box,
@ -17,12 +19,16 @@ import {
Radio, Radio,
RadioGroup, RadioGroup,
Spinner, Spinner,
useToast,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { ExternalLinkIcon } from "@chakra-ui/icons"; import { ExternalLinkIcon } from "@chakra-ui/icons";
import { OutfitLayers } from "../../components/OutfitPreview"; import { OutfitLayers } from "../../components/OutfitPreview";
import SpeciesColorPicker from "../../components/SpeciesColorPicker"; import SpeciesColorPicker from "../../components/SpeciesColorPicker";
import useOutfitAppearance from "../../components/useOutfitAppearance"; import useOutfitAppearance, {
itemAppearanceFragment,
} from "../../components/useOutfitAppearance";
import useSupportSecret from "./useSupportSecret";
function ItemSupportAppearanceLayerModal({ function ItemSupportAppearanceLayerModal({
item, item,
@ -31,6 +37,82 @@ function ItemSupportAppearanceLayerModal({
isOpen, isOpen,
onClose, onClose,
}) { }) {
const [selectedBodyId, setSelectedBodyId] = React.useState(itemLayer.bodyId);
const [previewBiology, setPreviewBiology] = React.useState({
speciesId: outfitState.speciesId,
colorId: outfitState.colorId,
pose: outfitState.pose,
isValid: true,
});
const supportSecret = useSupportSecret();
const toast = useToast();
const [
mutate,
{ loading: mutationLoading, error: mutationError },
] = useMutation(
gql`
mutation ItemSupportSetLayerBodyId(
$layerId: ID!
$bodyId: ID!
$supportSecret: String!
$outfitSpeciesId: ID!
$outfitColorId: ID!
$formPreviewSpeciesId: ID!
$formPreviewColorId: ID!
) {
setLayerBodyId(
layerId: $layerId
bodyId: $bodyId
supportSecret: $supportSecret
) {
# This mutation returns the affected AppearanceLayer. Fetch the
# updated fields, including the appearance on the outfit pet and the
# form preview pet, to automatically update our cached appearance in
# the rest of the app. That means you should be able to see your
# changes immediately!
id
bodyId
item {
id
appearanceOnOutfit: appearanceOn(
speciesId: $outfitSpeciesId
colorId: $outfitColorId
) {
...ItemAppearanceForOutfitPreview
}
appearanceOnFormPreviewPet: appearanceOn(
speciesId: $formPreviewSpeciesId
colorId: $formPreviewColorId
) {
...ItemAppearanceForOutfitPreview
}
}
}
}
${itemAppearanceFragment}
`,
{
variables: {
layerId: itemLayer.id,
bodyId: selectedBodyId,
supportSecret,
outfitSpeciesId: outfitState.speciesId,
outfitColorId: outfitState.colorId,
formPreviewSpeciesId: previewBiology.speciesId,
formPreviewColorId: previewBiology.colorId,
},
onCompleted: () => {
onClose();
toast({
status: "success",
title: `Saved layer ${itemLayer.id}: ${item.name}`,
});
},
}
);
return ( return (
<Modal size="xl" isOpen={isOpen} onClose={onClose}> <Modal size="xl" isOpen={isOpen} onClose={onClose}>
<ModalOverlay> <ModalOverlay>
@ -41,7 +123,7 @@ function ItemSupportAppearanceLayerModal({
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<Metadata> <Metadata>
<MetadataLabel>ID:</MetadataLabel> <MetadataLabel>DTI ID:</MetadataLabel>
<MetadataValue>{itemLayer.id}</MetadataValue> <MetadataValue>{itemLayer.id}</MetadataValue>
<MetadataLabel>Zone:</MetadataLabel> <MetadataLabel>Zone:</MetadataLabel>
<MetadataValue> <MetadataValue>
@ -88,10 +170,30 @@ function ItemSupportAppearanceLayerModal({
item={item} item={item}
itemLayer={itemLayer} itemLayer={itemLayer}
outfitState={outfitState} outfitState={outfitState}
selectedBodyId={selectedBodyId}
previewBiology={previewBiology}
onChangeBodyId={setSelectedBodyId}
onChangePreviewBiology={setPreviewBiology}
/> />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button colorScheme="green">Save changes</Button> {mutationError && (
<Box
color="red.400"
fontSize="sm"
marginRight="2"
textAlign="right"
>
{mutationError.message}
</Box>
)}
<Button
isLoading={mutationLoading}
colorScheme="green"
onClick={mutate}
>
Save changes
</Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</ModalOverlay> </ModalOverlay>
@ -103,15 +205,12 @@ function ItemSupportAppearanceLayerPetCompatibility({
item, item,
itemLayer, itemLayer,
outfitState, outfitState,
selectedBodyId,
previewBiology,
onChangeBodyId,
onChangePreviewBiology,
}) { }) {
const [bodyId, setBodyId] = React.useState(itemLayer.bodyId); const [selectedBiology, setSelectedBiology] = React.useState(previewBiology);
const [selectedBiology, setSelectedBiology] = React.useState({
speciesId: outfitState.speciesId,
colorId: outfitState.colorId,
pose: outfitState.pose,
isValid: true,
});
const [visibleBiology, setVisibleBiology] = React.useState(selectedBiology);
const { const {
loading, loading,
@ -119,9 +218,9 @@ function ItemSupportAppearanceLayerPetCompatibility({
visibleLayers, visibleLayers,
bodyId: appearanceBodyId, bodyId: appearanceBodyId,
} = useOutfitAppearance({ } = useOutfitAppearance({
speciesId: visibleBiology.speciesId, speciesId: previewBiology.speciesId,
colorId: visibleBiology.colorId, colorId: previewBiology.colorId,
pose: visibleBiology.pose, pose: previewBiology.pose,
wornItemIds: [item.id], wornItemIds: [item.id],
}); });
@ -130,18 +229,18 @@ function ItemSupportAppearanceLayerPetCompatibility({
// When the appearance body ID changes, select it as the new body ID. (This // When the appearance body ID changes, select it as the new body ID. (This
// is an effect because it happens after the appearance finishes loading!) // is an effect because it happens after the appearance finishes loading!)
React.useEffect(() => { React.useEffect(() => {
if (bodyId !== "0") { if (selectedBodyId !== "0") {
setBodyId(appearanceBodyId); onChangeBodyId(appearanceBodyId);
} }
}, [bodyId, appearanceBodyId]); }, [selectedBodyId, appearanceBodyId, onChangeBodyId]);
return ( return (
<FormControl isInvalid={error || !selectedBiology.isValid ? true : false}> <FormControl isInvalid={error || !selectedBiology.isValid ? true : false}>
<FormLabel>Pet compatibility</FormLabel> <FormLabel>Pet compatibility</FormLabel>
<RadioGroup <RadioGroup
colorScheme="green" colorScheme="green"
value={bodyId} value={selectedBodyId}
onChange={(newBodyId) => setBodyId(newBodyId)} onChange={(newBodyId) => onChangeBodyId(newBodyId)}
marginBottom="4" marginBottom="4"
> >
<Radio value="0"> <Radio value="0">
@ -189,7 +288,7 @@ function ItemSupportAppearanceLayerPetCompatibility({
setSelectedBiology({ speciesId, colorId, isValid, pose }); setSelectedBiology({ speciesId, colorId, isValid, pose });
if (isValid) { if (isValid) {
setVisibleBiology({ speciesId, colorId, isValid, pose }); onChangePreviewBiology({ speciesId, colorId, isValid, pose });
} }
}} }}
/> />

View file

@ -250,6 +250,7 @@ function ItemSupportAppearanceFields({ item, outfitState }) {
<HStack spacing="4" overflow="auto" paddingX="1"> <HStack spacing="4" overflow="auto" paddingX="1">
{itemLayers.map((itemLayer) => ( {itemLayers.map((itemLayer) => (
<ItemSupportAppearanceLayer <ItemSupportAppearanceLayer
key={itemLayer.id}
item={item} item={item}
itemLayer={itemLayer} itemLayer={itemLayer}
biologyLayers={biologyLayers} biologyLayers={biologyLayers}
@ -290,7 +291,7 @@ function ItemSupportAppearanceLayer({
</Box> </Box>
<Box fontWeight="bold">{itemLayer.zone.label}</Box> <Box fontWeight="bold">{itemLayer.zone.label}</Box>
<Box>Zone ID: {itemLayer.zone.id}</Box> <Box>Zone ID: {itemLayer.zone.id}</Box>
<Box>Layer ID: {itemLayer.id}</Box> <Box>DTI ID: {itemLayer.id}</Box>
<Box <Box
className={css` className={css`
opacity: 0; opacity: 0;

View file

@ -110,6 +110,11 @@ const typeDefs = gql`
special body ID that indicates it fits all PetAppearances. special body ID that indicates it fits all PetAppearances.
""" """
bodyId: ID! bodyId: ID!
"""
The item this layer is for, if any. (For pet layers, this is null.)
"""
item: Item
} }
# Cache for 1 week (unlikely to change) # Cache for 1 week (unlikely to change)
@ -184,6 +189,12 @@ const typeDefs = gql`
colorId: ID colorId: ID
supportSecret: String! supportSecret: String!
): Item! ): Item!
setLayerBodyId(
layerId: ID!
bodyId: ID!
supportSecret: String!
): AppearanceLayer!
} }
`; `;
@ -288,11 +299,18 @@ const resolvers = {
}, },
}, },
AppearanceLayer: { AppearanceLayer: {
zone: async (layer, _, { zoneLoader }) => { bodyId: async ({ id }, _, { swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id);
return layer.bodyId;
},
zone: async ({ id }, _, { swfAssetLoader, zoneLoader }) => {
const layer = await swfAssetLoader.load(id);
const zone = await zoneLoader.load(layer.zoneId); const zone = await zoneLoader.load(layer.zoneId);
return zone; return zone;
}, },
imageUrl: (layer, { size }) => { imageUrl: async ({ id }, { size }, { swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id);
if (!layer.hasImage) { if (!layer.hasImage) {
return null; return null;
} }
@ -311,7 +329,9 @@ const resolvers = {
`/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?v2-${time}` `/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?v2-${time}`
); );
}, },
svgUrl: async (layer, _, { svgLogger }) => { svgUrl: async ({ id }, _, { swfAssetLoader, svgLogger }) => {
const layer = await swfAssetLoader.load(id);
const manifest = await neopets.loadAssetManifest(layer.url); const manifest = await neopets.loadAssetManifest(layer.url);
if (!manifest) { if (!manifest) {
svgLogger.log("no-manifest"); svgLogger.log("no-manifest");
@ -339,6 +359,24 @@ const resolvers = {
const url = new URL(assetDatum.path, "http://images.neopets.com"); const url = new URL(assetDatum.path, "http://images.neopets.com");
return url.toString(); return url.toString();
}, },
item: async ({ id }, _, { db }) => {
// TODO: If this becomes a popular request, we'll definitely need to
// loaderize this! I'm cheating for now because it's just Support, one at
// a time.
const [rows] = await db.query(
`
SELECT parent_id FROM parents_swf_assets
WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1;
`,
[id]
);
if (rows.length === 0) {
return null;
}
return { id: String(rows[0].parent_id) };
},
}, },
Zone: { Zone: {
depth: async ({ id }, _, { zoneLoader }) => { depth: async ({ id }, _, { zoneLoader }) => {
@ -515,6 +553,27 @@ const resolvers = {
return { id: itemId }; return { id: itemId };
}, },
setLayerBodyId: async (_, { layerId, bodyId, supportSecret }, { db }) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const [
result,
] = await db.execute(
`UPDATE swf_assets SET body_id = ? WHERE id = ? LIMIT 1`,
[bodyId, layerId]
);
if (result.affectedRows !== 1) {
throw new Error(
`Expected to affect 1 layer, but affected ${result.affectedRows}`
);
}
return { id: layerId };
},
}, },
}; };

View file

@ -223,7 +223,22 @@ const buildPetTypeBySpeciesAndColorLoader = (db, loaders) =>
); );
}); });
const buildItemSwfAssetLoader = (db) => const buildSwfAssetLoader = (db) =>
new DataLoader(async (swfAssetIds) => {
const qs = swfAssetIds.map((_) => "?").join(",");
const [rows, _] = await db.execute(
`SELECT * FROM swf_assets WHERE id IN (${qs})`,
swfAssetIds
);
const entities = rows.map(normalizeRow);
return swfAssetIds.map((swfAssetId) =>
entities.find((e) => e.id === swfAssetId)
);
});
const buildItemSwfAssetLoader = (db, loaders) =>
new DataLoader(async (itemAndBodyPairs) => { new DataLoader(async (itemAndBodyPairs) => {
const conditions = []; const conditions = [];
const values = []; const values = [];
@ -245,6 +260,10 @@ const buildItemSwfAssetLoader = (db) =>
const entities = rows.map(normalizeRow); const entities = rows.map(normalizeRow);
for (const swfAsset of entities) {
loaders.swfAssetLoader.prime(swfAsset.id, swfAsset);
}
return itemAndBodyPairs.map(({ itemId, bodyId }) => return itemAndBodyPairs.map(({ itemId, bodyId }) =>
entities.filter( entities.filter(
(e) => (e) =>
@ -253,7 +272,7 @@ const buildItemSwfAssetLoader = (db) =>
); );
}); });
const buildPetSwfAssetLoader = (db) => const buildPetSwfAssetLoader = (db, loaders) =>
new DataLoader(async (petStateIds) => { new DataLoader(async (petStateIds) => {
const qs = petStateIds.map((_) => "?").join(","); const qs = petStateIds.map((_) => "?").join(",");
const [rows, _] = await db.execute( const [rows, _] = await db.execute(
@ -267,6 +286,10 @@ const buildPetSwfAssetLoader = (db) =>
const entities = rows.map(normalizeRow); const entities = rows.map(normalizeRow);
for (const swfAsset of entities) {
loaders.swfAssetLoader.prime(swfAsset.id, swfAsset);
}
return petStateIds.map((petStateId) => return petStateIds.map((petStateId) =>
entities.filter((e) => e.parentId === petStateId) entities.filter((e) => e.parentId === petStateId)
); );
@ -388,8 +411,9 @@ function buildLoaders(db) {
db, db,
loaders loaders
); );
loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db); loaders.swfAssetLoader = buildSwfAssetLoader(db);
loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db); loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db, loaders);
loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db, loaders);
loaders.outfitLoader = buildOutfitLoader(db); loaders.outfitLoader = buildOutfitLoader(db);
loaders.itemOutfitRelationshipsLoader = buildItemOutfitRelationshipsLoader( loaders.itemOutfitRelationshipsLoader = buildItemOutfitRelationshipsLoader(
db db