Can save new outfits w/o items
Just a basic e2e starting point! Simple logic, with simple gates to prevent saving outfits we're not ready for. Safe to ship, despite being very incomplete!
This commit is contained in:
parent
531ca3c22f
commit
f9b07dad24
6 changed files with 325 additions and 73 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import * as page from "./page";
|
||||||
|
|
||||||
// Give network requests a bit of breathing room!
|
// Give network requests a bit of breathing room!
|
||||||
const networkTimeout = { timeout: 12000 };
|
const networkTimeout = { timeout: 12000 };
|
||||||
|
|
||||||
|
@ -5,105 +7,81 @@ describe("WardrobePage: Basic outfit state", () => {
|
||||||
it("Initialize simple outfit from URL", () => {
|
it("Initialize simple outfit from URL", () => {
|
||||||
cy.visit("/outfits/new?species=1&color=8&objects[]=76789");
|
cy.visit("/outfits/new?species=1&color=8&objects[]=76789");
|
||||||
|
|
||||||
getSpeciesSelect(networkTimeout)
|
page
|
||||||
|
.getSpeciesSelect(networkTimeout)
|
||||||
.find(":selected")
|
.find(":selected")
|
||||||
.should("have.text", "Acara");
|
.should("have.text", "Acara");
|
||||||
getColorSelect().find(":selected").should("have.text", "Blue");
|
page.getColorSelect().find(":selected").should("have.text", "Blue");
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
|
|
||||||
cy.contains("A Warm Winters Night Background", networkTimeout).should(
|
cy.contains("A Warm Winters Night Background", networkTimeout).should(
|
||||||
"exist"
|
"exist"
|
||||||
);
|
);
|
||||||
|
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Changes species and color", () => {
|
it("Changes species and color", () => {
|
||||||
cy.visit("/outfits/new?species=1&color=8&objects[]=76789");
|
cy.visit("/outfits/new?species=1&color=8&objects[]=76789");
|
||||||
|
|
||||||
getSpeciesSelect(networkTimeout)
|
page
|
||||||
|
.getSpeciesSelect(networkTimeout)
|
||||||
.find(":selected")
|
.find(":selected")
|
||||||
.should("have.text", "Acara");
|
.should("have.text", "Acara");
|
||||||
getColorSelect().find(":selected").should("have.text", "Blue");
|
page.getColorSelect().find(":selected").should("have.text", "Blue");
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
|
|
||||||
getSpeciesSelect().select("Aisha");
|
page.getSpeciesSelect().select("Aisha");
|
||||||
|
|
||||||
getSpeciesSelect().find(":selected").should("have.text", "Aisha");
|
page.getSpeciesSelect().find(":selected").should("have.text", "Aisha");
|
||||||
getColorSelect().find(":selected").should("have.text", "Blue");
|
page.getColorSelect().find(":selected").should("have.text", "Blue");
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
|
|
||||||
getColorSelect().select("Red");
|
page.getColorSelect().select("Red");
|
||||||
|
|
||||||
getSpeciesSelect().find(":selected").should("have.text", "Aisha");
|
page.getSpeciesSelect().find(":selected").should("have.text", "Aisha");
|
||||||
getColorSelect().find(":selected").should("have.text", "Red");
|
page.getColorSelect().find(":selected").should("have.text", "Red");
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("Changes pose", () => {
|
it("Changes pose", () => {
|
||||||
cy.visit("/outfits/new?species=1&color=8&pose=HAPPY_FEM");
|
cy.visit("/outfits/new?species=1&color=8&pose=HAPPY_FEM");
|
||||||
|
|
||||||
getPosePickerButton(networkTimeout).click();
|
page.getPosePickerButton(networkTimeout).click();
|
||||||
getPosePickerOption("Happy and Feminine").should("be.checked");
|
page.getPosePickerOption("Happy and Feminine").should("be.checked");
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
|
|
||||||
getPosePickerOption("Sad and Masculine").check({ force: true });
|
page.getPosePickerOption("Sad and Masculine").check({ force: true });
|
||||||
getPosePickerOption("Sad and Masculine").should("be.checked");
|
page.getPosePickerOption("Sad and Masculine").should("be.checked");
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Toggles item", () => {
|
it("Toggles item", () => {
|
||||||
cy.visit("/outfits/new?species=1&color=8&objects[]=76789");
|
cy.visit("/outfits/new?species=1&color=8&objects[]=76789");
|
||||||
|
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
|
|
||||||
cy.contains("A Warm Winters Night Background").click();
|
cy.contains("A Warm Winters Night Background").click();
|
||||||
|
|
||||||
getOutfitPreview().toMatchImageSnapshot();
|
page.getOutfitPreview().toMatchImageSnapshot();
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Renames outfit", () => {
|
it("Renames outfit", () => {
|
||||||
cy.visit("/outfits/new?name=My+outfit&species=1&color=8");
|
cy.visit("/outfits/new?name=My+outfit&species=1&color=8");
|
||||||
|
|
||||||
getOutfitName(networkTimeout).should("have.text", "My outfit");
|
page.getOutfitName(networkTimeout).should("have.text", "My outfit");
|
||||||
|
|
||||||
getOutfitName().click().type("Awesome outfit{enter}");
|
page.getOutfitName().click();
|
||||||
|
cy.focused().type("Awesome outfit{enter}");
|
||||||
|
|
||||||
getOutfitName().should("have.text", "Awesome outfit");
|
page.getOutfitName().should("have.text", "Awesome outfit");
|
||||||
cy.location().toMatchSnapshot();
|
cy.location().toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getSpeciesSelect(options) {
|
|
||||||
return cy.get("[data-test-id=wardrobe-species-picker]", options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColorSelect(options) {
|
|
||||||
return cy.get("[data-test-id=wardrobe-color-picker]", options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPosePickerButton(options) {
|
|
||||||
return cy.get("[data-test-id=wardrobe-pose-picker]", options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPosePickerOption(label, options) {
|
|
||||||
return cy.get(`input[aria-label="${CSS.escape(label)}"]`, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOutfitPreview() {
|
|
||||||
return cy.get("[data-test-id=wardrobe-outfit-preview]:not([data-loading])", {
|
|
||||||
// A bit of an extra-long timeout, to await both server data and image data
|
|
||||||
timeout: 15000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOutfitName(options) {
|
|
||||||
return cy.get("[data-test-id=outfit-name]", options);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,24 @@
|
||||||
|
import * as page from "./page";
|
||||||
|
|
||||||
describe("WardrobePage: Outfit saving", () => {
|
describe("WardrobePage: Outfit saving", () => {
|
||||||
it("logs in", () => {
|
it("logs in", () => {
|
||||||
cy.logInAs("dti-test");
|
cy.logInAs("dti-test");
|
||||||
cy.visit("/");
|
cy.visit("/outfits/new?species=1&color=8");
|
||||||
|
|
||||||
|
// Give the outfit a unique timestamped name
|
||||||
|
const outfitName = `Cypress Test Outfit: ${new Date().toISOString()}`;
|
||||||
|
page.getOutfitName({ timeout: 12000 }).click();
|
||||||
|
cy.focused().type(outfitName + "{enter}");
|
||||||
|
|
||||||
|
// Save the outfit
|
||||||
|
page.getSaveOutfitButton().click().should("have.attr", "data-loading");
|
||||||
|
|
||||||
|
// Wait for the outfit to stop saving, and check that it redirected and
|
||||||
|
// still shows the correct outfit name.
|
||||||
|
page
|
||||||
|
.getSaveOutfitButton({ timeout: 12000 })
|
||||||
|
.should("not.have.attr", "data-loading");
|
||||||
|
cy.location("pathname").should("match", /^\/outfits\/[0-9]+$/);
|
||||||
|
page.getOutfitName().should("have.text", outfitName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -125,3 +125,19 @@ exports[`WardrobePage: Basic outfit state > Changes pose #1`] =
|
||||||
"search": "?name=&species=1&color=8&pose=SAD_MASC",
|
"search": "?name=&species=1&color=8&pose=SAD_MASC",
|
||||||
"superDomain": "localhost"
|
"superDomain": "localhost"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports[`WardrobePage: Basic outfit state > Renames outfit #0`] =
|
||||||
|
{
|
||||||
|
"auth": "",
|
||||||
|
"hash": "",
|
||||||
|
"host": "localhost:3000",
|
||||||
|
"hostname": "localhost",
|
||||||
|
"href": "http://localhost:3000/outfits/new?name=Awesome+outfit&species=1&color=8&pose=HAPPY_FEM",
|
||||||
|
"origin": "http://localhost:3000",
|
||||||
|
"originPolicy": "http://localhost:3000",
|
||||||
|
"pathname": "/outfits/new",
|
||||||
|
"port": "3000",
|
||||||
|
"protocol": "http:",
|
||||||
|
"search": "?name=Awesome+outfit&species=1&color=8&pose=HAPPY_FEM",
|
||||||
|
"superDomain": "localhost"
|
||||||
|
};
|
||||||
|
|
30
cypress/integration/WardrobePage/page.js
Normal file
30
cypress/integration/WardrobePage/page.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export function getSpeciesSelect(options) {
|
||||||
|
return cy.get("[data-test-id=wardrobe-species-picker]", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorSelect(options) {
|
||||||
|
return cy.get("[data-test-id=wardrobe-color-picker]", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPosePickerButton(options) {
|
||||||
|
return cy.get("[data-test-id=wardrobe-pose-picker]", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPosePickerOption(label, options) {
|
||||||
|
return cy.get(`input[aria-label="${CSS.escape(label)}"]`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutfitPreview() {
|
||||||
|
return cy.get("[data-test-id=wardrobe-outfit-preview]:not([data-loading])", {
|
||||||
|
// A bit of an extra-long timeout, to await both server data and image data
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutfitName(options) {
|
||||||
|
return cy.get("[data-test-id=outfit-name]", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSaveOutfitButton(options) {
|
||||||
|
return cy.get("[data-test-id=wardrobe-save-outfit-button]", options);
|
||||||
|
}
|
|
@ -16,9 +16,11 @@ import {
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Portal,
|
Portal,
|
||||||
Button,
|
Button,
|
||||||
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { EditIcon, QuestionIcon } from "@chakra-ui/icons";
|
import { EditIcon, QuestionIcon } from "@chakra-ui/icons";
|
||||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { Delay, Heading1, Heading2 } from "../util";
|
import { Delay, Heading1, Heading2 } from "../util";
|
||||||
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
|
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
|
||||||
|
@ -26,6 +28,8 @@ import { BiRename } from "react-icons/bi";
|
||||||
import { IoCloudUploadOutline } from "react-icons/io5";
|
import { IoCloudUploadOutline } from "react-icons/io5";
|
||||||
import { MdMoreVert } from "react-icons/md";
|
import { MdMoreVert } from "react-icons/md";
|
||||||
import useCurrentUser from "../components/useCurrentUser";
|
import useCurrentUser from "../components/useCurrentUser";
|
||||||
|
import gql from "graphql-tag";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ItemsPanel shows the items in the current outfit, and lets the user toggle
|
* ItemsPanel shows the items in the current outfit, and lets the user toggle
|
||||||
|
@ -246,12 +250,111 @@ function ItemZoneGroupSkeleton({ itemCount }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useOutfitSaving(outfitState) {
|
||||||
|
const { isLoggedIn, id: currentUserId } = useCurrentUser();
|
||||||
|
const history = useHistory();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const isSaved = outfitState.id;
|
||||||
|
|
||||||
|
// Only logged-in users can save outfits - and they can only save new outfits,
|
||||||
|
// or outfits they created.
|
||||||
|
const canSaveOutfit =
|
||||||
|
isLoggedIn &&
|
||||||
|
(!isSaved || outfitState.creator?.id === currentUserId) &&
|
||||||
|
// TODO: Add support for outfits with items
|
||||||
|
outfitState.wornItemIds.length === 0 &&
|
||||||
|
outfitState.closetedItemIds.length === 0;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
context: { sendAuth: true },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveOutfit = React.useCallback(() => {
|
||||||
|
sendSaveOutfitMutation()
|
||||||
|
.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);
|
||||||
|
toast({
|
||||||
|
status: "error",
|
||||||
|
title: "Sorry, there was an error saving this outfit!",
|
||||||
|
description: "Maybe check your connection and try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [sendSaveOutfitMutation, history, toast]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canSaveOutfit,
|
||||||
|
isSaving,
|
||||||
|
saveOutfit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OutfitHeading is an editable outfit name, as a big pretty page heading!
|
* OutfitHeading is an editable outfit name, as a big pretty page heading!
|
||||||
* It also contains the outfit menu, for saving etc.
|
* It also contains the outfit menu, for saving etc.
|
||||||
*/
|
*/
|
||||||
function OutfitHeading({ outfitState, dispatchToOutfit }) {
|
function OutfitHeading({ outfitState, dispatchToOutfit }) {
|
||||||
const { isLoggedIn } = useCurrentUser();
|
const { canSaveOutfit, isSaving, saveOutfit } = useOutfitSaving(outfitState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// The Editable wraps everything, including the menu, because the menu has
|
// The Editable wraps everything, including the menu, because the menu has
|
||||||
|
@ -277,25 +380,26 @@ function OutfitHeading({ outfitState, dispatchToOutfit }) {
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box width="4" flex="1 0 auto" />
|
<Box width="4" flex="1 0 auto" />
|
||||||
{isLoggedIn && (
|
{canSaveOutfit && (
|
||||||
<>
|
<>
|
||||||
<Tooltip label="Coming soon!" shouldWrapChildren>
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
size="sm"
|
||||||
size="sm"
|
isLoading={isSaving}
|
||||||
isDisabled
|
loadingText="Saving…"
|
||||||
leftIcon={
|
leftIcon={
|
||||||
<Box
|
<Box
|
||||||
// Adjust the visual balance toward the cloud
|
// Adjust the visual balance toward the cloud
|
||||||
marginBottom="-2px"
|
marginBottom="-2px"
|
||||||
>
|
>
|
||||||
<IoCloudUploadOutline />
|
<IoCloudUploadOutline />
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
onClick={saveOutfit}
|
||||||
Save
|
data-test-id="wardrobe-save-outfit-button"
|
||||||
</Button>
|
>
|
||||||
</Tooltip>
|
Save
|
||||||
|
</Button>
|
||||||
<Box width="2" />
|
<Box width="2" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { gql } from "apollo-server";
|
import { gql } from "apollo-server";
|
||||||
|
import { getPoseFromPetState } from "../util";
|
||||||
|
|
||||||
const typeDefs = gql`
|
const typeDefs = gql`
|
||||||
type Outfit {
|
type Outfit {
|
||||||
|
@ -17,6 +18,18 @@ const typeDefs = gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
outfit(id: ID!): Outfit
|
outfit(id: ID!): Outfit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
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!
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
|
@ -77,6 +90,7 @@ const resolvers = {
|
||||||
return { id: outfit.userId };
|
return { id: outfit.userId };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Query: {
|
Query: {
|
||||||
outfit: async (_, { id }, { outfitLoader }) => {
|
outfit: async (_, { id }, { outfitLoader }) => {
|
||||||
const outfit = await outfitLoader.load(id);
|
const outfit = await outfitLoader.load(id);
|
||||||
|
@ -87,6 +101,98 @@ const resolvers = {
|
||||||
return { id };
|
return { id };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Mutation: {
|
||||||
|
saveOutfit: async (
|
||||||
|
_,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name: rawName,
|
||||||
|
speciesId,
|
||||||
|
colorId,
|
||||||
|
pose,
|
||||||
|
wornItemIds,
|
||||||
|
closetedItemIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currentUserId,
|
||||||
|
db,
|
||||||
|
petTypeBySpeciesAndColorLoader,
|
||||||
|
petStatesForPetTypeLoader,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (!currentUserId) {
|
||||||
|
throw new Error(
|
||||||
|
"saveOutfit requires login for now. This might change!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
throw new Error("TODO: Add support for updating existing outfits");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wornItemIds.length > 0 || closetedItemIds.length > 0) {
|
||||||
|
throw new Error("TODO: Add support for outfits with items");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the base name of the provided name: trim it, and strip any "(1)"
|
||||||
|
// suffixes.
|
||||||
|
const baseName = rawName.replace(/\s*\([0-9]+\)\s*$/, "");
|
||||||
|
const namePlaceholder = baseName.trim().replace(/_%/g, "\\$0") + "%";
|
||||||
|
|
||||||
|
// Then, look for outfits from this user with the same base name.
|
||||||
|
const [outfitRows] = await db.query(
|
||||||
|
`
|
||||||
|
SELECT name FROM outfits WHERE user_id = ? AND name LIKE ?;
|
||||||
|
`,
|
||||||
|
[currentUserId, namePlaceholder]
|
||||||
|
);
|
||||||
|
const existingOutfitNames = new Set(
|
||||||
|
outfitRows.map(({ name }) => name.trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then, get the unique name to use for this outfit: try the base name
|
||||||
|
// first, but, if it's taken, keep incrementing the "(1)" suffix until
|
||||||
|
// it's not.
|
||||||
|
let name = baseName;
|
||||||
|
for (let i = 1; existingOutfitNames.has(name); i++) {
|
||||||
|
name = `${baseName} (${i})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, get the petState corresponding to this species/color/pose.
|
||||||
|
const petType = await petTypeBySpeciesAndColorLoader.load({
|
||||||
|
speciesId,
|
||||||
|
colorId,
|
||||||
|
});
|
||||||
|
if (!petType) {
|
||||||
|
throw new Error(
|
||||||
|
`could not find pet type for species=${speciesId}, color=${colorId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// TODO: We could query for this more directly, instead of loading all
|
||||||
|
// appearances 🤔
|
||||||
|
const petStates = await petStatesForPetTypeLoader.load(petType.id);
|
||||||
|
const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose);
|
||||||
|
if (!petState) {
|
||||||
|
throw new Error(
|
||||||
|
`could not find appearance for species=${speciesId}, color=${colorId}, pose=${pose}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await db.execute(
|
||||||
|
`
|
||||||
|
INSERT INTO outfits (name, pet_state_id, user_id, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP());
|
||||||
|
`,
|
||||||
|
[name, petState.id, currentUserId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newOutfitId = String(result.insertId);
|
||||||
|
console.log(`Saved outfit ${newOutfitId}`);
|
||||||
|
|
||||||
|
return { id: newOutfitId };
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { typeDefs, resolvers };
|
module.exports = { typeDefs, resolvers };
|
||||||
|
|
Loading…
Reference in a new issue