diff --git a/setup-mysql-user.sql b/setup-mysql-user.sql index e0adcd4..db9a0db 100644 --- a/setup-mysql-user.sql +++ b/setup-mysql-user.sql @@ -15,6 +15,7 @@ GRANT SELECT ON openneo_impress.zone_translations TO impress2020; -- Public data tables: write GRANT UPDATE ON openneo_impress.items TO impress2020; GRANT UPDATE ON openneo_impress.swf_assets TO impress2020; +GRANT DELETE ON openneo_impress.parents_swf_assets TO impress2020; -- User data tables GRANT SELECT ON openneo_impress.item_outfit_relationships TO impress2020; diff --git a/src/app/WardrobePage/support/ItemLayerSupportModal.js b/src/app/WardrobePage/support/ItemLayerSupportModal.js index 519d68c..23406d3 100644 --- a/src/app/WardrobePage/support/ItemLayerSupportModal.js +++ b/src/app/WardrobePage/support/ItemLayerSupportModal.js @@ -19,6 +19,7 @@ import { Radio, RadioGroup, Spinner, + useDisclosure, useToast, } from "@chakra-ui/core"; import { ChevronRightIcon, ExternalLinkIcon } from "@chakra-ui/icons"; @@ -209,10 +210,18 @@ function ItemLayerSupportModal({ /> + + {mutationError && ( @@ -223,6 +232,7 @@ function ItemLayerSupportModal({ isLoading={mutationLoading} colorScheme="green" onClick={mutate} + flex="0 0 auto" > Save changes @@ -344,4 +354,133 @@ function ItemLayerSupportPetCompatibilityFields({ ); } +function ItemLayerSupportModalRemoveButton({ + item, + itemLayer, + outfitState, + onRemoveSuccess, +}) { + const { isOpen, onOpen, onClose } = useDisclosure(); + const toast = useToast(); + const supportSecret = useSupportSecret(); + + const [mutate, { loading, error }] = useMutation( + gql` + mutation ItemLayerSupportRemoveButton( + $layerId: ID! + $itemId: ID! + $outfitSpeciesId: ID! + $outfitColorId: ID! + $supportSecret: String! + ) { + removeLayerFromItem( + layerId: $layerId + itemId: $itemId + supportSecret: $supportSecret + ) { + # This mutation returns the affected layer, and the affected item. + # Fetch the updated appearance for the current outfit, which should + # no longer include this layer. This means you should be able to see + # your changes immediately! + item { + id + appearanceOn(speciesId: $outfitSpeciesId, colorId: $outfitColorId) { + ...ItemAppearanceForOutfitPreview + } + } + + # The layer's item should be null now, fetch to confirm and update! + layer { + id + item { + id + } + } + } + } + ${itemAppearanceFragment} + `, + { + variables: { + layerId: itemLayer.id, + itemId: item.id, + outfitSpeciesId: outfitState.speciesId, + outfitColorId: outfitState.colorId, + supportSecret, + }, + onCompleted: () => { + onClose(); + onRemoveSuccess(); + toast({ + status: "success", + title: `Removed layer ${itemLayer.id} from ${item.name}`, + }); + }, + } + ); + + return ( + <> + + + + + + + Remove Layer {itemLayer.id} ({itemLayer.zone.label}) from{" "} + {item.name}? + + + + This will permanently-ish remove Layer {itemLayer.id} ( + {itemLayer.zone.label}) from this item. + + + If you remove a correct layer by mistake, re-modeling should fix + it, or Matchu can restore it if you write down the layer ID + before proceeding! + + + Are you sure you want to remove Layer {itemLayer.id} from this + item? + + + + + + {error && ( + + {error.message} + + )} + + + + + + + ); +} + export default ItemLayerSupportModal; diff --git a/src/server/index.js b/src/server/index.js index 48db041..809869e 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -210,6 +210,11 @@ const typeDefs = gql` petOnNeopetsDotCom(petName: String!): Outfit } + type RemoveLayerFromItemMutationResult { + layer: AppearanceLayer! + item: Item! + } + type Mutation { setManualSpecialColor( itemId: ID! @@ -228,6 +233,12 @@ const typeDefs = gql` bodyId: ID! supportSecret: String! ): AppearanceLayer! + + removeLayerFromItem( + layerId: ID! + itemId: ID! + supportSecret: String! + ): RemoveLayerFromItemMutationResult! } `; @@ -672,6 +683,31 @@ const resolvers = { return { id: layerId }; }, + + removeLayerFromItem: async ( + _, + { layerId, itemId, supportSecret }, + { db } + ) => { + if (supportSecret !== process.env["SUPPORT_SECRET"]) { + throw new Error(`Support secret is incorrect. Try setting up again?`); + } + + const [result] = await db.execute( + `DELETE FROM parents_swf_assets ` + + `WHERE swf_asset_id = ? AND parent_type = "Item" AND parent_id = ? ` + + `LIMIT 1`, + [layerId, itemId] + ); + + if (result.affectedRows !== 1) { + throw new Error( + `Expected to affect 1 layer, but affected ${result.affectedRows}` + ); + } + + return { layer: { id: layerId }, item: { id: itemId } }; + }, }, };