Add support tools for pet layers

This commit is contained in:
Emi Matchu 2021-04-21 22:06:07 -07:00
parent e8a2b8ba28
commit b9b6667414
3 changed files with 210 additions and 80 deletions

View file

@ -41,7 +41,8 @@ import useSupport from "./useSupport";
* appearance layer. Open it by clicking a layer from ItemSupportDrawer. * appearance layer. Open it by clicking a layer from ItemSupportDrawer.
*/ */
function AppearanceLayerSupportModal({ function AppearanceLayerSupportModal({
item, item, // Specify this or `petAppearance`
petAppearance, // Specify this or `item`
layer, layer,
outfitState, // speciesId, colorId, pose outfitState, // speciesId, colorId, pose
isOpen, isOpen,
@ -62,12 +63,16 @@ function AppearanceLayerSupportModal({
const { supportSecret } = useSupport(); const { supportSecret } = useSupport();
const toast = useToast(); const toast = useToast();
const parentName = item
? item.name
: `${petAppearance.color.name} ${petAppearance.species.name} ${petAppearance.id}`;
const [ const [
mutate, mutate,
{ loading: mutationLoading, error: mutationError }, { loading: mutationLoading, error: mutationError },
] = useMutation( ] = useMutation(
gql` gql`
mutation ItemSupportSetLayerBodyId( mutation ApperanceLayerSupportSetLayerBodyId(
$layerId: ID! $layerId: ID!
$bodyId: ID! $bodyId: ID!
$knownGlitches: [AppearanceLayerKnownGlitch!]! $knownGlitches: [AppearanceLayerKnownGlitch!]!
@ -134,7 +139,7 @@ function AppearanceLayerSupportModal({
onClose(); onClose();
toast({ toast({
status: "success", status: "success",
title: `Saved layer ${layer.id}: ${item.name}`, title: `Saved layer ${layer.id}: ${parentName}`,
}); });
}, },
} }
@ -153,7 +158,7 @@ function AppearanceLayerSupportModal({
<ModalOverlay> <ModalOverlay>
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>
Layer {layer.id}: {item.name} Layer {layer.id}: {parentName}
</ModalHeader> </ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
@ -244,6 +249,8 @@ function AppearanceLayerSupportModal({
SWF <ExternalLinkIcon ml="1" /> SWF <ExternalLinkIcon ml="1" />
</Button> </Button>
<Box flex="1 1 0" /> <Box flex="1 1 0" />
{item && (
<>
<Button <Button
size="xs" size="xs"
colorScheme="gray" colorScheme="gray"
@ -257,10 +264,14 @@ function AppearanceLayerSupportModal({
isOpen={uploadModalIsOpen} isOpen={uploadModalIsOpen}
onClose={() => setUploadModalIsOpen(false)} onClose={() => setUploadModalIsOpen(false)}
/> />
</>
)}
</HStack> </HStack>
</MetadataValue> </MetadataValue>
</Metadata> </Metadata>
<Box height="8" /> <Box height="8" />
{item && (
<>
<AppearanceLayerSupportPetCompatibilityFields <AppearanceLayerSupportPetCompatibilityFields
item={item} item={item}
layer={layer} layer={layer}
@ -271,18 +282,22 @@ function AppearanceLayerSupportModal({
onChangePreviewBiology={setPreviewBiology} onChangePreviewBiology={setPreviewBiology}
/> />
<Box height="8" /> <Box height="8" />
</>
)}
<AppearanceLayerSupportKnownGlitchesFields <AppearanceLayerSupportKnownGlitchesFields
selectedKnownGlitches={selectedKnownGlitches} selectedKnownGlitches={selectedKnownGlitches}
onChange={setSelectedKnownGlitches} onChange={setSelectedKnownGlitches}
/> />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{item && (
<AppearanceLayerSupportModalRemoveButton <AppearanceLayerSupportModalRemoveButton
item={item} item={item}
layer={layer} layer={layer}
outfitState={outfitState} outfitState={outfitState}
onRemoveSuccess={onClose} onRemoveSuccess={onClose}
/> />
)}
<Box flex="1 0 0" /> <Box flex="1 0 0" />
{mutationError && ( {mutationError && (
<Box <Box
@ -298,7 +313,11 @@ function AppearanceLayerSupportModal({
<Button <Button
isLoading={mutationLoading} isLoading={mutationLoading}
colorScheme="green" colorScheme="green"
onClick={mutate} onClick={() =>
mutate().catch((e) => {
/* Discard errors here; we'll show them in the UI! */
})
}
flex="0 0 auto" flex="0 0 auto"
> >
Save changes Save changes

View file

@ -1,16 +1,28 @@
import React from "react"; import React from "react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { Box, IconButton, Select, Spinner, Switch } from "@chakra-ui/react"; import {
Box,
Button,
IconButton,
Select,
Spinner,
Switch,
Wrap,
WrapItem,
useDisclosure,
} from "@chakra-ui/react";
import { import {
ArrowBackIcon, ArrowBackIcon,
ArrowForwardIcon, ArrowForwardIcon,
CheckCircleIcon, CheckCircleIcon,
EditIcon,
} from "@chakra-ui/icons"; } from "@chakra-ui/icons";
import HangerSpinner from "../../components/HangerSpinner"; import HangerSpinner from "../../components/HangerSpinner";
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata"; import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
import useSupport from "./useSupport"; import useSupport from "./useSupport";
import AppearanceLayerSupportModal from "./AppearanceLayerSupportModal";
function PosePickerSupport({ function PosePickerSupport({
speciesId, speciesId,
@ -33,6 +45,24 @@ function PosePickerSupport({
id id
label @client label @client
} }
# For AppearanceLayerSupportModal
remoteId
bodyId
swfUrl
svgUrl
imageUrl
canvasMovieLibraryUrl
}
# For AppearanceLayerSupportModal to know the name
species {
id
name
}
color {
id
name
} }
} }
@ -128,19 +158,49 @@ function PosePickerSupport({
colorId={colorId} colorId={colorId}
/> />
</MetadataValue> </MetadataValue>
<MetadataLabel>Zones:</MetadataLabel> <MetadataLabel>Layers:</MetadataLabel>
<MetadataValue> <MetadataValue>
<Wrap spacing="1">
{currentPetAppearance.layers {currentPetAppearance.layers
.map((l) => l.zone) .map((layer) => [`${layer.zone.label} (${layer.zone.id})`, layer])
.map((z) => `${z.label} (${z.id})`) .sort((a, b) => a[0].localeCompare(b[0]))
.sort() .map(([text, layer]) => (
.join(", ")} <WrapItem>
<PetLayerSupportLink
outfitState={{ speciesId, colorId, pose }}
petAppearance={currentPetAppearance}
layer={layer}
>
{text}
<EditIcon marginLeft="1" />
</PetLayerSupportLink>
</WrapItem>
))}
</Wrap>
</MetadataValue> </MetadataValue>
</Metadata> </Metadata>
</Box> </Box>
); );
} }
function PetLayerSupportLink({ outfitState, petAppearance, layer, children }) {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button size="xs" onClick={onOpen}>
{children}
</Button>
<AppearanceLayerSupportModal
outfitState={outfitState}
petAppearance={petAppearance}
layer={layer}
isOpen={isOpen}
onClose={onClose}
/>
</>
);
}
function PosePickerSupportNavigator({ function PosePickerSupportNavigator({
petAppearances, petAppearances,
currentPetAppearance, currentPetAppearance,

View file

@ -24,7 +24,7 @@ function getAuth0() {
clientSecret: process.env.AUTH0_SUPPORT_CLIENT_SECRET, clientSecret: process.env.AUTH0_SUPPORT_CLIENT_SECRET,
scope: "read:users update:users", scope: "read:users update:users",
}); });
} }
return auth0; return auth0;
} }
@ -412,6 +412,10 @@ const resolvers = {
itemTranslationLoader, itemTranslationLoader,
swfAssetLoader, swfAssetLoader,
zoneTranslationLoader, zoneTranslationLoader,
petStateLoader,
petTypeLoader,
colorTranslationLoader,
speciesTranslationLoader,
db, db,
} }
) => { ) => {
@ -453,17 +457,18 @@ const resolvers = {
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) { if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
try { try {
const itemId = await db const { parentType, parentId } = await db
.execute( .execute(
`SELECT parent_id FROM parents_swf_assets `SELECT parent_type, parent_id FROM parents_swf_assets
WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1;`, WHERE swf_asset_id = ? LIMIT 1;`,
[layerId] [layerId]
) )
.then(([rows]) => normalizeRow(rows[0]).parentId); .then(([rows]) => normalizeRow(rows[0]));
if (parentType === "Item") {
const [item, itemTranslation, zoneTranslation] = await Promise.all([ const [item, itemTranslation, zoneTranslation] = await Promise.all([
itemLoader.load(itemId), itemLoader.load(parentId),
itemTranslationLoader.load(itemId), itemTranslationLoader.load(parentId),
zoneTranslationLoader.load(oldSwfAsset.zoneId), zoneTranslationLoader.load(oldSwfAsset.zoneId),
]); ]);
@ -487,10 +492,56 @@ const resolvers = {
}, },
], ],
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
url: `https://impress.openneo.net/items/${itemId}`, url: `https://impress.openneo.net/items/${parentId}`,
}, },
], ],
}); });
} else if (parentType === "PetState") {
const petState = await petStateLoader.load(parentId);
const petType = await petTypeLoader.load(petState.petTypeId);
const [
colorTranslation,
speciesTranslation,
zoneTranslation,
] = await Promise.all([
colorTranslationLoader.load(petType.colorId),
speciesTranslationLoader.load(petType.speciesId),
zoneTranslationLoader.load(oldSwfAsset.zoneId),
]);
const colorName = capitalize(colorTranslation.name);
const speciesName = capitalize(speciesTranslation.name);
const pose = getPoseFromPetState(petState);
await logToDiscord({
embeds: [
{
title: `🛠 ${colorName} ${speciesName}`,
thumbnail: {
url: `http://pets.neopets.com/cp/${
petType.basicImageHash || petType.imageHash
}/1/6.png`,
height: 150,
width: 150,
},
fields: [
{
name: `Appearance ${petState.id}, Layer ${layerId} (${zoneTranslation.label}): Known glitches`,
value: `${oldSwfAsset.knownGlitches || "<none>"} → **${
newKnownGlitchesString || "<none>"
}**`,
},
],
timestamp: new Date().toISOString(),
url: `https://impress-2020.openneo.net/outfits/new?species=${petType.speciesId}&color=${petType.colorId}&pose=${pose}&state=${petState.id}`,
},
],
});
} else {
console.error(
`Error building Discord support log: unexpected parent type ${parentType}`
);
}
} catch (e) { } catch (e) {
console.error("Error sending Discord support log", e); console.error("Error sending Discord support log", e);
} }