can submit the actual body ID Support mutation!
it seems to actually be changing the things correctly aaaa
This commit is contained in:
parent
e8917936d6
commit
8fdc986ee9
4 changed files with 211 additions and 28 deletions
|
@ -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 });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue