diff --git a/src/app/WardrobePage/ItemsAndSearchPanels.js b/src/app/WardrobePage/ItemsAndSearchPanels.js
index b60e17b..0b3657c 100644
--- a/src/app/WardrobePage/ItemsAndSearchPanels.js
+++ b/src/app/WardrobePage/ItemsAndSearchPanels.js
@@ -21,7 +21,7 @@ import SearchPanel from "./SearchPanel";
* performing some wiring to help them interact with each other via simple
* state and refs.
*/
-function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) {
+function ItemsAndSearchPanels({ loading, outfitState, outfitSaving, dispatchToOutfit }) {
const [searchQuery, setSearchQuery] = React.useState(emptySearchQuery);
const scrollContainerRef = React.useRef();
const searchQueryRef = React.useRef();
@@ -68,6 +68,7 @@ function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) {
diff --git a/src/app/WardrobePage/ItemsPanel.js b/src/app/WardrobePage/ItemsPanel.js
index d9fbf00..f8402d6 100644
--- a/src/app/WardrobePage/ItemsPanel.js
+++ b/src/app/WardrobePage/ItemsPanel.js
@@ -16,7 +16,6 @@ import {
MenuItem,
Portal,
Button,
- useToast,
Spinner,
useColorModeValue,
} from "@chakra-ui/react";
@@ -27,17 +26,12 @@ import {
WarningTwoIcon,
} from "@chakra-ui/icons";
import { CSSTransition, TransitionGroup } from "react-transition-group";
-import { useHistory } from "react-router-dom";
-import { Delay, Heading1, Heading2, useDebounce } from "../util";
+import { Delay, Heading1, Heading2 } from "../util";
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
import { BiRename } from "react-icons/bi";
import { IoCloudUploadOutline } from "react-icons/io5";
import { MdMoreVert } from "react-icons/md";
-import useCurrentUser from "../components/useCurrentUser";
-import gql from "graphql-tag";
-import { useMutation } from "@apollo/client";
-import { outfitStatesAreEqual } from "./useOutfitState";
/**
* ItemsPanel shows the items in the current outfit, and lets the user toggle
@@ -52,7 +46,7 @@ import { outfitStatesAreEqual } from "./useOutfitState";
* to have extra padding. Essentially: while the Items _do_ stretch out the
* full width of the container, it doesn't look like it!
*/
-function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
+function ItemsPanel({ outfitState, outfitSaving, loading, dispatchToOutfit }) {
const { zonesAndItems, incompatibleItems } = outfitState;
return (
@@ -62,6 +56,7 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
@@ -258,234 +253,11 @@ function ItemZoneGroupSkeleton({ itemCount }) {
);
}
-function useOutfitSaving(outfitState, dispatchToOutfit) {
- const { isLoggedIn, id: currentUserId } = useCurrentUser();
- const history = useHistory();
- const toast = useToast();
-
- // There's not a way to reset an Apollo mutation state to clear out the error
- // when the outfit changes… so we track the error state ourselves!
- const [saveError, setSaveError] = React.useState(null);
-
- // Whether this outfit is new, i.e. local-only, i.e. has _never_ been saved
- // to the server.
- const isNewOutfit = outfitState.id == null;
-
- // Whether this outfit's latest local changes have been saved to the server.
- // And log it to the console!
- const latestVersionIsSaved =
- outfitState.savedOutfitState &&
- outfitStatesAreEqual(
- outfitState.outfitStateWithoutExtras,
- outfitState.savedOutfitState
- );
- React.useEffect(() => {
- console.debug(
- "[useOutfitSaving] Latest version is saved? %s\nCurrent: %o\nSaved: %o",
- latestVersionIsSaved,
- outfitState.outfitStateWithoutExtras,
- outfitState.savedOutfitState
- );
- }, [
- latestVersionIsSaved,
- outfitState.outfitStateWithoutExtras,
- outfitState.savedOutfitState,
- ]);
-
- // Only logged-in users can save outfits - and they can only save new outfits,
- // or outfits they created.
- const canSaveOutfit =
- isLoggedIn && (isNewOutfit || outfitState.creator?.id === currentUserId);
-
- const [sendSaveOutfitMutation, { loading: isSaving }] = useMutation(
- gql`
- mutation UseOutfitSaving_SaveOutfit(
- $id: ID # Optional, is null when saving new outfits.
- $name: String # Optional, server may fill in a placeholder.
- $speciesId: ID!
- $colorId: ID!
- $pose: Pose!
- $wornItemIds: [ID!]!
- $closetedItemIds: [ID!]!
- ) {
- outfit: saveOutfit(
- id: $id
- name: $name
- speciesId: $speciesId
- colorId: $colorId
- pose: $pose
- wornItemIds: $wornItemIds
- closetedItemIds: $closetedItemIds
- ) {
- id
- name
- petAppearance {
- id
- species {
- id
- }
- color {
- id
- }
- pose
- }
- wornItems {
- id
- }
- closetedItems {
- id
- }
- creator {
- id
- }
- }
- }
- `,
- {
- context: { sendAuth: true },
- update: (cache, { data: { outfit } }) => {
- // After save, add this outfit to the current user's outfit list. This
- // will help when navigating back to Your Outfits, to force a refresh.
- // https://www.apollographql.com/docs/react/caching/cache-interaction/#example-updating-the-cache-after-a-mutation
- cache.modify({
- id: cache.identify(outfit.creator),
- fields: {
- outfits: (existingOutfitRefs = []) => {
- const newOutfitRef = cache.writeFragment({
- data: outfit,
- fragment: gql`
- fragment NewOutfit on Outfit {
- id
- }
- `,
- });
- return [...existingOutfitRefs, newOutfitRef];
- },
- },
- });
-
- // Also, send a `reset` action, to show whatever the server returned.
- // This is important for suffix changes to `name`, but can also be
- // relevant for graceful failure when a bug causes a change not to
- // persist. (But don't do it if it's not the current outfit anymore,
- // we don't want laggy mutations to reset the outfit!)
- if (outfit.id === outfitState.id) {
- dispatchToOutfit({
- type: "resetToSavedOutfitData",
- savedOutfitData: outfit,
- });
- }
- },
- }
- );
-
- const saveOutfitFromProvidedState = React.useCallback(
- (outfitState) => {
- sendSaveOutfitMutation({
- variables: {
- id: outfitState.id, // Optional, is null when saving new outfits
- name: outfitState.name, // Optional, server may fill in a placeholder
- speciesId: outfitState.speciesId,
- colorId: outfitState.colorId,
- pose: outfitState.pose,
- wornItemIds: [...outfitState.wornItemIds],
- closetedItemIds: [...outfitState.closetedItemIds],
- },
- })
- .then(({ data: { outfit } }) => {
- // Navigate to the new saved outfit URL. Our Apollo cache should pick
- // up the data from this mutation response, and combine it with the
- // existing cached data, to make this smooth without any loading UI.
- history.push(`/outfits/${outfit.id}`);
- })
- .catch((e) => {
- console.error(e);
- setSaveError(e);
- toast({
- status: "error",
- title: "Sorry, there was an error saving this outfit!",
- description: "Maybe check your connection and try again.",
- });
- });
- },
- // It's important that this callback _doesn't_ change when the outfit
- // changes, so that the auto-save effect is only responding to the
- // debounced state!
- [sendSaveOutfitMutation, history, toast]
- );
-
- const saveOutfit = React.useCallback(
- () => saveOutfitFromProvidedState(outfitState.outfitStateWithoutExtras),
- [saveOutfitFromProvidedState, outfitState.outfitStateWithoutExtras]
- );
-
- // Auto-saving! First, debounce the outfit state. Use `outfitStateWithoutExtras`,
- // which only contains the basic fields, and will keep a stable object
- // identity until actual changes occur. Then, save the outfit after the user
- // has left it alone for long enough, so long as it's actually different
- // than the saved state.
- const debouncedOutfitState = useDebounce(
- outfitState.outfitStateWithoutExtras,
- 2000,
- {
- // When the outfit ID changes, update the debounced state immediately!
- forceReset: (debouncedOutfitState, newOutfitState) =>
- debouncedOutfitState.id !== newOutfitState.id,
- }
- );
- // HACK: This prevents us from auto-saving the outfit state that's still
- // loading. I worry that this might not catch other loading scenarios
- // though, like if the species/color/pose is in the GQL cache, but the
- // items are still loading in... not sure where this would happen tho!
- const debouncedOutfitStateIsSaveable =
- debouncedOutfitState.speciesId &&
- debouncedOutfitState.colorId &&
- debouncedOutfitState.pose;
- React.useEffect(() => {
- if (
- !isNewOutfit &&
- canSaveOutfit &&
- debouncedOutfitStateIsSaveable &&
- !outfitStatesAreEqual(debouncedOutfitState, outfitState.savedOutfitState)
- ) {
- console.info(
- "[useOutfitSaving] Auto-saving outfit\nSaved: %o\nCurrent (debounced): %o",
- outfitState.savedOutfitState,
- debouncedOutfitState
- );
- saveOutfitFromProvidedState(debouncedOutfitState);
- }
- }, [
- isNewOutfit,
- canSaveOutfit,
- debouncedOutfitState,
- debouncedOutfitStateIsSaveable,
- outfitState.savedOutfitState,
- saveOutfitFromProvidedState,
- ]);
-
- // When the outfit changes, clear out the error state from previous saves.
- // We'll send the mutation again after the debounce, and we don't want to
- // show the error UI in the meantime!
- React.useEffect(() => {
- setSaveError(null);
- }, [outfitState.outfitStateWithoutExtras]);
-
- return {
- canSaveOutfit,
- isNewOutfit,
- isSaving,
- latestVersionIsSaved,
- saveError,
- saveOutfit,
- };
-}
-
/**
* OutfitSavingIndicator shows a Save button, or the "Saved" or "Saving" state,
* if the user can save this outfit. If not, this is empty!
*/
-function OutfitSavingIndicator({ outfitState, dispatchToOutfit }) {
+function OutfitSavingIndicator({ outfitSaving }) {
const {
canSaveOutfit,
isNewOutfit,
@@ -493,7 +265,7 @@ function OutfitSavingIndicator({ outfitState, dispatchToOutfit }) {
latestVersionIsSaved,
saveError,
saveOutfit,
- } = useOutfitSaving(outfitState, dispatchToOutfit);
+ } = outfitSaving;
const errorTextColor = useColorModeValue("red.600", "red.400");
@@ -586,7 +358,7 @@ function OutfitSavingIndicator({ outfitState, dispatchToOutfit }) {
* OutfitHeading is an editable outfit name, as a big pretty page heading!
* It also contains the outfit menu, for saving etc.
*/
-function OutfitHeading({ outfitState, dispatchToOutfit }) {
+function OutfitHeading({ outfitState, outfitSaving, dispatchToOutfit }) {
return (
// The Editable wraps everything, including the menu, because the menu has
// a Rename option.
@@ -612,10 +384,7 @@ function OutfitHeading({ outfitState, dispatchToOutfit }) {
-
+