import * as React from "react"; import gql from "graphql-tag"; import { useQuery, useMutation } from "@apollo/client"; import { css } from "emotion"; import { Badge, Box, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay, FormControl, FormErrorMessage, FormHelperText, FormLabel, HStack, Link, Select, Spinner, Stack, Text, useBreakpointValue, useColorModeValue, useDisclosure, } from "@chakra-ui/core"; import { CheckCircleIcon, EditIcon, ExternalLinkIcon } from "@chakra-ui/icons"; import ItemLayerSupportModal from "./ItemLayerSupportModal"; import Metadata, { MetadataLabel, MetadataValue } from "./Metadata"; import { OutfitLayers } from "../../components/OutfitPreview"; import useOutfitAppearance from "../../components/useOutfitAppearance"; import { OutfitStateContext } from "../useOutfitState"; import useSupport from "./useSupport"; /** * ItemSupportDrawer shows Support UI for the item when open. * * This component controls the drawer element. The actual content is imported * from another lazy-loaded component! */ function ItemSupportDrawer({ item, isOpen, onClose }) { const placement = useBreakpointValue({ base: "bottom", lg: "right", // TODO: There's a bug in the Chakra RC that doesn't read the breakpoint // specification correctly - we need these extra keys until it's fixed! // https://github.com/chakra-ui/chakra-ui/issues/1444 0: "bottom", 1: "bottom", 2: "right", 3: "right", }); return ( {item.name} Support Item ID: {item.id} Restricted zones: ); } function ItemSupportRestrictedZones({ item }) { const { speciesId, colorId } = React.useContext(OutfitStateContext); // NOTE: It would be a better reflection of the data to just query restricted // zones right off the item... but we already have them in cache from // the appearance, so query them that way to be instant in practice! const { loading, error, data } = useQuery( gql` query ItemSupportRestrictedZones( $itemId: ID! $speciesId: ID! $colorId: ID! ) { item(id: $itemId) { id appearanceOn(speciesId: $speciesId, colorId: $colorId) { restrictedZones { id label } } } } `, { variables: { itemId: item.id, speciesId, colorId } } ); if (loading) { return ; } if (error) { return {error.message}; } const restrictedZones = data?.item?.appearanceOn?.restrictedZones || []; if (restrictedZones.length === 0) { return "None"; } return restrictedZones .map((z) => `${z.label} (${z.id})`) .sort() .join(", "); } function ItemSupportFields({ item }) { const { loading, error, data } = useQuery( gql` query ItemSupportFields($itemId: ID!) { item(id: $itemId) { id manualSpecialColor { id } explicitlyBodySpecific } } `, { variables: { itemId: item.id }, // HACK: I think it's a bug in @apollo/client 3.1.1 that, if the // optimistic response sets `manualSpecialColor` to null, the query // doesn't update, even though its cache has updated :/ // // This cheap trick of changing the display name every re-render // persuades Apollo that this is a different query, so it re-checks // its cache and finds the empty `manualSpecialColor`. Weird! displayName: `ItemSupportFields-${new Date()}`, } ); const errorColor = useColorModeValue("red.500", "red.300"); return ( <> {error && {error.message}} ); } function ItemSupportSpecialColorFields({ loading, error, item, manualSpecialColor, }) { const { supportSecret } = useSupport(); const { loading: colorsLoading, error: colorsError, data: colorsData, } = useQuery( gql` query ItemSupportDrawerAllColors { allColors { id name isStandard } } ` ); const [ mutate, { loading: mutationLoading, error: mutationError, data: mutationData }, ] = useMutation(gql` mutation ItemSupportDrawerSetManualSpecialColor( $itemId: ID! $colorId: ID $supportSecret: String! ) { setManualSpecialColor( itemId: $itemId colorId: $colorId supportSecret: $supportSecret ) { id manualSpecialColor { id } } } `); const onChange = React.useCallback( (e) => { const colorId = e.target.value || null; const color = colorId != null ? { __typename: "Color", id: colorId } : null; mutate({ variables: { itemId: item.id, colorId, supportSecret, }, optimisticResponse: { __typename: "Mutation", setManualSpecialColor: { __typename: "Item", id: item.id, manualSpecialColor: color, }, }, }).catch((e) => { // Ignore errors from the promise, because we'll handle them on render! }); }, [item.id, mutate, supportSecret] ); const nonStandardColors = colorsData?.allColors?.filter((c) => !c.isStandard) || []; nonStandardColors.sort((a, b) => a.name.localeCompare(b.name)); const linkColor = useColorModeValue("green.500", "green.300"); return ( Special color {colorsError && ( {colorsError.message} )} {mutationError && ( {mutationError.message} )} {!colorsError && !mutationError && ( This controls which previews we show on the{" "} item page . )} ); } function ItemSupportPetCompatibilityRuleFields({ loading, error, item, explicitlyBodySpecific, }) { const { supportSecret } = useSupport(); const [ mutate, { loading: mutationLoading, error: mutationError, data: mutationData }, ] = useMutation(gql` mutation ItemSupportDrawerSetItemExplicitlyBodySpecific( $itemId: ID! $explicitlyBodySpecific: Boolean! $supportSecret: String! ) { setItemExplicitlyBodySpecific( itemId: $itemId explicitlyBodySpecific: $explicitlyBodySpecific supportSecret: $supportSecret ) { id explicitlyBodySpecific } } `); const onChange = React.useCallback( (e) => { const explicitlyBodySpecific = e.target.value === "true"; mutate({ variables: { itemId: item.id, explicitlyBodySpecific, supportSecret, }, optimisticResponse: { __typename: "Mutation", setItemExplicitlyBodySpecific: { __typename: "Item", id: item.id, explicitlyBodySpecific, }, }, }).catch((e) => { // Ignore errors from the promise, because we'll handle them on render! }); }, [item.id, mutate, supportSecret] ); return ( Pet compatibility rule {mutationError && ( {mutationError.message} )} {!mutationError && ( By default, we assume Background-y zones fit all pets the same. When items don't follow that rule, we can override it. )} ); } /** * NOTE: This component takes `outfitState` from context, rather than as a prop * from its parent, for performance reasons. We want `Item` to memoize * and generally skip re-rendering on `outfitState` changes, and to make * sure the context isn't accessed when the drawer is closed. So we use * it here, only when the drawer is open! */ function ItemSupportAppearanceLayers({ item }) { const outfitState = React.useContext(OutfitStateContext); const { speciesId, colorId, pose, appearanceId } = outfitState; const { error, visibleLayers } = useOutfitAppearance({ speciesId, colorId, pose, appearanceId, wornItemIds: [item.id], }); const biologyLayers = visibleLayers.filter((l) => l.source === "pet"); const itemLayers = visibleLayers.filter((l) => l.source === "item"); itemLayers.sort((a, b) => a.zone.depth - b.zone.depth); return ( Appearance layers {itemLayers.map((itemLayer) => ( ))} {error && {error.message}} ); } function ItemSupportAppearanceLayer({ item, itemLayer, biologyLayers, outfitState, }) { const { isOpen, onOpen, onClose } = useDisclosure(); const iconButtonBgColor = useColorModeValue("green.100", "green.300"); const iconButtonColor = useColorModeValue("green.800", "gray.900"); return ( {itemLayer.zone.label} Zone ID: {itemLayer.zone.id} DTI ID: {itemLayer.id} ); } export default ItemSupportDrawer;