From 5334801aba87105405e959931b2a3716710e806a Mon Sep 17 00:00:00 2001 From: Matchu Date: Fri, 28 Aug 2020 00:12:41 -0700 Subject: [PATCH] add zone restrict hack tools similar to the layer zoning tools I just rolled out! not thrilled about the outfit state hacks here bc of how we cache restrict on the appearance rather than the item, but oh well! this escape hatch is pretty easy and solid, and it's a cleanup for another day Also did a code split here, now that this file is getting larger, to only load this for support users. I don't actually care about restricting console stuff to support users (I'd honestly rather not), but saving the bytes is worth it I think, since support mode is pretty easy to enter when we need to --- src/app/WardrobePage/WardrobeDevHacks.js | 307 ++++++++++++++++++++ src/app/WardrobePage/index.js | 8 +- src/app/WardrobePage/useWardrobeDevHacks.js | 112 ------- 3 files changed, 312 insertions(+), 115 deletions(-) create mode 100644 src/app/WardrobePage/WardrobeDevHacks.js delete mode 100644 src/app/WardrobePage/useWardrobeDevHacks.js diff --git a/src/app/WardrobePage/WardrobeDevHacks.js b/src/app/WardrobePage/WardrobeDevHacks.js new file mode 100644 index 0000000..e8262cd --- /dev/null +++ b/src/app/WardrobePage/WardrobeDevHacks.js @@ -0,0 +1,307 @@ +import React from "react"; +import gql from "graphql-tag"; +import { useApolloClient } from "@apollo/client"; + +import { OutfitStateContext } from "./useOutfitState"; +import zones from "../cached-data/zones.json"; + +/** + * WardrobeDevHacks adds some hacky dev tools to the browser console, by + * attaching them to the global window object! + * + * This is for debug tools / hacky Support tools that don't really need a + * fully-powered UI. + */ +function WardrobeDevHacks() { + const client = useApolloClient(); + const outfitState = React.useContext(OutfitStateContext); + + /** + * DTIHackLayerZone temporarily sets the given layer to the given zone, on + * your machine only. It resets once you reload the page. This can be useful + * for testing alternate zones, when making bug reports to TNT! + * + * Arguments: + * - layerId: The "DTI ID" of the layer to change. + * - zoneIdOrName: The ID or the name of the zone to set it to. (If there's + * more than one zone matching the name, we use the one + * with the smaller ID number.) + * + * Example: + * - `DTIHackLayerZone(449653, "Foreground")` shows the #1 Fan Room + * Background as if it were a Foreground. + * - `DTIHackLayerZone(142880, 36)` shows the Beaded Shell Earrings on + * Skeith as if they used the further-back Earrings zone, instead of the + * further-forward Earrings zone. + */ + const DTIHackLayerZone = React.useCallback( + (layerId, zoneIdOrName, { force = false } = {}) => { + const zone = findZone(zoneIdOrName, force); + + const layer = client.readFragment({ + id: `AppearanceLayer:${layerId}`, + fragment: gql` + fragment HackReadAppearanceLayer on AppearanceLayer { + zone { + id + } + } + `, + }); + if (!layer && !force) { + throw new Error( + `no layer found with ID ${JSON.stringify(layerId)}. ` + + `is it loaded in the outfit yet? ` + + `call again with {force: true} to do it anyway!` + ); + } + + // Add a zone record to the Apollo cache, in case it's not there yet! + client.writeFragment({ + id: `Zone:${zone.id}`, + fragment: gql` + fragment HackWriteZone on Zone { + id + } + `, + data: { __typename: "Zone", id: zone.id }, + }); + + client.writeFragment({ + id: `AppearanceLayer:${layerId}`, + fragment: gql` + fragment HackWriteAppearanceLayer on AppearanceLayer { + zone + } + `, + data: { zone: { __ref: `Zone:${zone.id}` } }, + }); + + console.log( + `Updated layer ${layerId} to zone ${zone.id} (was ${layer?.zone?.id})` + ); + }, + [client] + ); + useExposedGlobal("DTIHackLayerZone", DTIHackLayerZone); + + /** + * DTIHackRestrictedZoneAdd temporarily restricts the given zone on the given + * item, on your machine only. It resets once you reload the page. This can + * be useful for testing alternate restrictions, when making bug reports to + * TNT! + * + * Note that, due to hacky limitations on how we currently store data in the + * app, these changes will only apply to the species/color of your current + * outfit. Other pets will still have the default restrictions, unless you + * hack them separately. + * + * Arguments: + * - itemId: The "Item ID" of the item to change. + * - zoneIdOrName: The ID or the name of the zone to restrict. (If there's + * more than one zone matching the name, we use the one + * with the smaller ID number.) + */ + const DTIHackRestrictedZoneAdd = React.useCallback( + (itemId, zoneIdOrName, { force = false } = {}) => { + const zone = findZone(zoneIdOrName, force); + + // TODO: This stuff makes it a bit annoying that we put restricts on the + // appearance instead of the item in the GraphQL API... could move! + const data = client.readQuery({ + query: gql` + query DTIHackRestrictedZoneAddRead( + $itemId: ID! + $speciesId: ID! + $colorId: ID! + ) { + item(id: $itemId) { + id + appearanceOn(speciesId: $speciesId, colorId: $colorId) { + id + restrictedZones { + id + } + } + } + } + `, + variables: { + itemId, + speciesId: outfitState.speciesId, + colorId: outfitState.colorId, + }, + }); + if (!data && !force) { + throw new Error( + `no item found with ID ${JSON.stringify(itemId)}. ` + + `is it loaded in the outfit yet? ` + + `call again with {force: true} to do it anyway!` + ); + } + const { item } = data; + + const restrictedZoneIds = new Set( + item.appearanceOn.restrictedZones.map((z) => z.id) + ); + restrictedZoneIds.add(zone.id); + + // Add a zone record to the Apollo cache, in case it's not there yet! + client.writeFragment({ + id: `Zone:${zone.id}`, + fragment: gql` + fragment HackWriteZone on Zone { + id + } + `, + data: { __typename: "Zone", id: zone.id }, + }); + + client.writeFragment({ + id: `ItemAppearance:${item.appearanceOn.id}`, + fragment: gql` + fragment HackDTIAddRestrictedZoneWrite on ItemAppearance { + restrictedZones + } + `, + data: { + restrictedZones: [...restrictedZoneIds].map((id) => ({ + __ref: `Zone:${id}`, + })), + }, + }); + + console.log( + `Added restricted zone ${zone.id} to item ${itemId} ` + + `(now it's zones ${[...restrictedZoneIds].join(", ")})` + ); + }, + [client, outfitState] + ); + useExposedGlobal("DTIHackRestrictedZoneAdd", DTIHackRestrictedZoneAdd); + + /** + * DTIHackRestrictedZoneRemove temporarily un-restricts the given zone on the + * given item, on your machine only. It resets once you reload the page. This + * can be useful for testing alternate restrictions, when making bug reports + * to TNT! + * + * Note that, due to hacky limitations on how we currently store data in the + * app, these changes will only apply to the species/color of your current + * outfit. Other pets will still have the default restrictions, unless you + * hack them separately. + * + * Arguments: + * - itemId: The "Item ID" of the item to change. + * - zoneIdOrName: The ID or the name of the zone to un-restrict. (If + * there's more than one zone matching the name, we use the + * one with the smaller ID number.) + */ + const DTIHackRestrictedZoneRemove = React.useCallback( + (itemId, zoneIdOrName, { force = false } = {}) => { + const zone = findZone(zoneIdOrName, force); + + // TODO: This stuff makes it a bit annoying that we put restricts on the + // appearance instead of the item in the GraphQL API... could move! + const data = client.readQuery({ + query: gql` + query DTIHackRestrictedZoneRemoveRead( + $itemId: ID! + $speciesId: ID! + $colorId: ID! + ) { + item(id: $itemId) { + id + appearanceOn(speciesId: $speciesId, colorId: $colorId) { + id + restrictedZones { + id + } + } + } + } + `, + variables: { + itemId, + speciesId: outfitState.speciesId, + colorId: outfitState.colorId, + }, + }); + if (!data && !force) { + throw new Error( + `no item found with ID ${JSON.stringify(itemId)}. ` + + `is it loaded in the outfit yet? ` + + `call again with {force: true} to do it anyway!` + ); + } + const { item } = data; + + const restrictedZoneIds = new Set( + item.appearanceOn.restrictedZones.map((z) => z.id) + ); + if (!restrictedZoneIds.has(String(zone.id)) && !force) { + throw new Error( + `zone ${JSON.stringify(zoneIdOrName)} is not restricted. ` + + `(restricted zones: ${[...restrictedZoneIds].join(", ")}). ` + + `call again with {force: true} to do it anyway!` + ); + } + restrictedZoneIds.delete(zone.id); + + client.writeFragment({ + id: `ItemAppearance:${item.appearanceOn.id}`, + fragment: gql` + fragment HackDTIRemoveRestrictedZoneWrite on ItemAppearance { + restrictedZones + } + `, + data: { + restrictedZones: [...restrictedZoneIds].map((id) => ({ + __ref: `Zone:${id}`, + })), + }, + }); + + console.log( + `Removed restricted zone ${zone.id} from item ${itemId} ` + + `(now it's zones ${[...restrictedZoneIds].join(", ")})` + ); + }, + [client, outfitState] + ); + useExposedGlobal("DTIHackRestrictedZoneRemove", DTIHackRestrictedZoneRemove); + + return null; +} + +/** + * useExposedGlobal sets window[name] to the given value, while this component + * is mounted. Afterwards, it resets to the previous value, from before the + * component mounted. + * + * This means you can access it from the dev console! + */ +function useExposedGlobal(name, value) { + React.useEffect(() => { + const prev = window[name]; + window[name] = value; + return () => { + window[name] = prev; + }; + }, [name, value]); +} + +function findZone(zoneIdOrName, force) { + const zone = zones.find( + (z) => z.id === String(zoneIdOrName) || z.label === zoneIdOrName + ); + if (!zone && !force) { + throw new Error( + `no zone found with ID or name ${JSON.stringify(zoneIdOrName)}. ` + + `call again with {force: true} to do it anyway!` + ); + } + return zone; +} + +export default WardrobeDevHacks; diff --git a/src/app/WardrobePage/index.js b/src/app/WardrobePage/index.js index 49ecc1c..0032e3b 100644 --- a/src/app/WardrobePage/index.js +++ b/src/app/WardrobePage/index.js @@ -4,13 +4,14 @@ import loadable from "@loadable/component"; import ItemsAndSearchPanels from "./ItemsAndSearchPanels"; import OutfitPreview from "../components/OutfitPreview"; +import SupportOnly from "./support/SupportOnly"; import useOutfitState, { OutfitStateContext } from "./useOutfitState"; import { usePageTitle } from "../util"; -import useWardrobeDevHacks from "./useWardrobeDevHacks"; const OutfitControls = loadable(() => import(/* webpackPreload: true */ "./OutfitControls") ); +const WardrobeDevHacks = loadable(() => import("./WardrobeDevHacks")); /** * WardrobePage is the most fun page on the site - it's where you create @@ -29,8 +30,6 @@ function WardrobePage() { usePageTitle(`${outfitState.name || "Untitled outfit"} | Dress to Impress`); - useWardrobeDevHacks(); - // TODO: I haven't found a great place for this error UI yet, and this case // isn't very common, so this lil toast notification seems good enough! React.useEffect(() => { @@ -52,6 +51,9 @@ function WardrobePage() { // via context. return ( + + + { - const zone = zones.find( - (z) => z.id === String(zoneIdOrName) || z.label === zoneIdOrName - ); - if (!zone && !force) { - throw new Error( - `no zone found with ID or name ${JSON.stringify(zoneIdOrName)}. ` + - `call again with {force: true} to do it anyway!` - ); - } - - const data = client.readFragment({ - id: `AppearanceLayer:${layerId}`, - fragment: gql` - fragment HackReadAppearanceLayer on AppearanceLayer { - zone { - id - } - } - `, - }); - if (!data && !force) { - throw new Error( - `no layer found with ID ${JSON.stringify(layerId)}. ` + - `is it loaded in the outfit yet? ` + - `call again with {force: true} to do it anyway!` - ); - } - - // Add a zone record to the Apollo cache, in case it's not there yet! - client.writeFragment({ - id: `Zone:${zone.id}`, - fragment: gql` - fragment HackWriteZone on Zone { - id - } - `, - data: { __typename: "Zone", id: zone.id }, - }); - - client.writeFragment({ - id: `AppearanceLayer:${layerId}`, - fragment: gql` - fragment HackWriteAppearanceLayer on AppearanceLayer { - zone - } - `, - data: { zone: { __ref: `Zone:${zone.id}` } }, - }); - - console.log( - `Updated layer ${layerId} to zone ${zone.id} (was ${data?.zone?.id})` - ); - }, - [client] - ); - useExposedGlobal("DTIHackLayerZone", DTIHackLayerZone); -} - -/** - * useExposedGlobal sets window[name] to the given value, while this component - * is mounted. Afterwards, it resets to the previous value, from before the - * component mounted. - * - * This means you can access it from the dev console! - */ -function useExposedGlobal(name, value) { - React.useEffect(() => { - const prev = window[name]; - window[name] = value; - return () => { - window[name] = prev; - }; - }, [name, value]); -} - -export default useWardrobeDevHacks;