Add AppProvider to wardrobe-2020
Hey the app runs now! How exciting! It doesn't run *correctly* but it renders at all!!
This commit is contained in:
parent
aa76fbc933
commit
8d7eabf1e3
14 changed files with 549 additions and 64 deletions
|
@ -1,7 +1,19 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
|
||||||
|
|
||||||
import { WardrobePage } from "./wardrobe-2020";
|
import { AppProvider, WardrobePage } from "./wardrobe-2020";
|
||||||
|
|
||||||
|
// Use Apollo's error messages in development.
|
||||||
|
if (process.env["NODE_ENV"] === "development") {
|
||||||
|
loadErrorMessages();
|
||||||
|
loadDevMessages();
|
||||||
|
}
|
||||||
|
|
||||||
const rootNode = document.querySelector("#wardrobe-2020-root");
|
const rootNode = document.querySelector("#wardrobe-2020-root");
|
||||||
ReactDOM.render(<WardrobePage />, rootNode);
|
ReactDOM.render(
|
||||||
|
<AppProvider>
|
||||||
|
<WardrobePage />
|
||||||
|
</AppProvider>,
|
||||||
|
rootNode
|
||||||
|
);
|
||||||
|
|
157
app/javascript/wardrobe-2020/AppProvider.js
Normal file
157
app/javascript/wardrobe-2020/AppProvider.js
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { Integrations } from "@sentry/tracing";
|
||||||
|
import { Auth0Provider } from "@auth0/auth0-react";
|
||||||
|
import { CSSReset, ChakraProvider, extendTheme } from "@chakra-ui/react";
|
||||||
|
import { ApolloProvider } from "@apollo/client";
|
||||||
|
import { useAuth0 } from "@auth0/auth0-react";
|
||||||
|
import { mode } from "@chakra-ui/theme-tools";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
import buildApolloClient from "./apolloClient";
|
||||||
|
|
||||||
|
const theme = extendTheme({
|
||||||
|
styles: {
|
||||||
|
global: (props) => ({
|
||||||
|
html: {
|
||||||
|
// HACK: Chakra sets body as the relative position element, which is
|
||||||
|
// fine, except its `min-height: 100%` doesn't actually work
|
||||||
|
// unless paired with height on the root element too!
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
background: mode("gray.50", "gray.800")(props),
|
||||||
|
color: mode("green.800", "green.50")(props),
|
||||||
|
transition: "all 0.25s",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function AppProvider({ children }) {
|
||||||
|
React.useEffect(() => setupLogging(), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Auth0Provider
|
||||||
|
domain="openneo.us.auth0.com"
|
||||||
|
clientId="8LjFauVox7shDxVufQqnviUIywMuuC4r"
|
||||||
|
redirectUri={
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? "http://localhost:3000"
|
||||||
|
: "https://impress-2020.openneo.net"
|
||||||
|
}
|
||||||
|
audience="https://impress-2020.openneo.net/api"
|
||||||
|
scope=""
|
||||||
|
>
|
||||||
|
<DTIApolloProvider>
|
||||||
|
<ChakraProvider theme={theme}>
|
||||||
|
<CSSReset />
|
||||||
|
{children}
|
||||||
|
</ChakraProvider>
|
||||||
|
</DTIApolloProvider>
|
||||||
|
</Auth0Provider>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DTIApolloProvider({ children, additionalCacheState = {} }) {
|
||||||
|
const auth0 = useAuth0();
|
||||||
|
const auth0Ref = React.useRef(auth0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
auth0Ref.current = auth0;
|
||||||
|
}, [auth0]);
|
||||||
|
|
||||||
|
// Save the first `additionalCacheState` we get as our `initialCacheState`,
|
||||||
|
// which we'll use to initialize the client without having to wait a tick.
|
||||||
|
const [initialCacheState, unusedSetInitialCacheState] =
|
||||||
|
React.useState(additionalCacheState);
|
||||||
|
|
||||||
|
const client = React.useMemo(
|
||||||
|
() =>
|
||||||
|
buildApolloClient({
|
||||||
|
getAuth0: () => auth0Ref.current,
|
||||||
|
initialCacheState,
|
||||||
|
}),
|
||||||
|
[initialCacheState]
|
||||||
|
);
|
||||||
|
|
||||||
|
// When we get a new `additionalCacheState` object, merge it into the cache:
|
||||||
|
// copy the previous cache state, merge the new cache state's entries in,
|
||||||
|
// and "restore" the new merged cache state.
|
||||||
|
//
|
||||||
|
// HACK: Using `useMemo` for this is a dastardly trick!! What we want is the
|
||||||
|
// semantics of `useEffect` kinda, but we need to ensure it happens
|
||||||
|
// *before* all the children below get rendered, so they don't fire off
|
||||||
|
// unnecessary network requests. Using `useMemo` but throwing away the
|
||||||
|
// result kinda does that. It's evil! It's nasty! It's... perfect?
|
||||||
|
// (This operation is safe to run multiple times too, in case memo
|
||||||
|
// re-runs it. It's just, y'know, a performance loss. Maybe it's
|
||||||
|
// actually kinda perfect lol)
|
||||||
|
//
|
||||||
|
// I feel like there's probably a better way to do this... like, I want
|
||||||
|
// the semantic of replacing this client with an updated client - but I
|
||||||
|
// don't want to actually replace the client, because that'll break
|
||||||
|
// other kinds of state, like requests loading in the shared layout.
|
||||||
|
// Idk! I'll see how it goes!
|
||||||
|
React.useMemo(() => {
|
||||||
|
const previousCacheState = client.cache.extract();
|
||||||
|
const mergedCacheState = { ...previousCacheState };
|
||||||
|
for (const key of Object.keys(additionalCacheState)) {
|
||||||
|
mergedCacheState[key] = {
|
||||||
|
...mergedCacheState[key],
|
||||||
|
...additionalCacheState[key],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.debug(
|
||||||
|
"Merging Apollo cache:",
|
||||||
|
additionalCacheState,
|
||||||
|
mergedCacheState
|
||||||
|
);
|
||||||
|
client.cache.restore(mergedCacheState);
|
||||||
|
}, [client, additionalCacheState]);
|
||||||
|
|
||||||
|
return <ApolloProvider client={client}>{children}</ApolloProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupLogging() {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://c55875c3b0904264a1a99e5b741a221e@o506079.ingest.sentry.io/5595379",
|
||||||
|
autoSessionTracking: true,
|
||||||
|
integrations: [
|
||||||
|
new Integrations.BrowserTracing({
|
||||||
|
beforeNavigate: (context) => ({
|
||||||
|
...context,
|
||||||
|
// Assume any path segment starting with a digit is an ID, and replace
|
||||||
|
// it with `:id`. This will help group related routes in Sentry stats.
|
||||||
|
// NOTE: I'm a bit uncertain about the timing on this for tracking
|
||||||
|
// client-side navs... but we now only track first-time
|
||||||
|
// pageloads, and it definitely works correctly for them!
|
||||||
|
name: window.location.pathname.replaceAll(/\/[0-9][^/]*/g, "/:id"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// We have a _lot_ of location changes that don't actually signify useful
|
||||||
|
// navigations, like in the wardrobe page. It could be useful to trace
|
||||||
|
// them with better filtering someday, but frankly we don't use the perf
|
||||||
|
// features besides Web Vitals right now, and those only get tracked on
|
||||||
|
// first-time pageloads, anyway. So, don't track client-side navs!
|
||||||
|
startTransactionOnLocationChange: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
denyUrls: [
|
||||||
|
// Don't log errors that were probably triggered by extensions and not by
|
||||||
|
// our own app. (Apparently Sentry's setting to ignore browser extension
|
||||||
|
// errors doesn't do this anywhere near as consistently as I'd expect?)
|
||||||
|
//
|
||||||
|
// Adapted from https://gist.github.com/impressiver/5092952, as linked in
|
||||||
|
// https://docs.sentry.io/platforms/javascript/configuration/filtering/.
|
||||||
|
/^chrome-extension:\/\//,
|
||||||
|
/^moz-extension:\/\//,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Since we're only tracking first-page loads and not navigations, 100%
|
||||||
|
// sampling isn't actually so much! Tune down if it becomes a problem, tho.
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
}
|
|
@ -854,7 +854,7 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
name
|
name
|
||||||
restrictedZones {
|
restrictedZones {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
}
|
}
|
||||||
compatibleBodiesAndTheirZones {
|
compatibleBodiesAndTheirZones {
|
||||||
body {
|
body {
|
||||||
|
@ -867,7 +867,7 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
}
|
}
|
||||||
zones {
|
zones {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
canonicalAppearance(
|
canonicalAppearance(
|
||||||
|
|
|
@ -80,40 +80,34 @@ function WardrobePage() {
|
||||||
// that need it, where it's more useful and more performant to access
|
// that need it, where it's more useful and more performant to access
|
||||||
// via context.
|
// via context.
|
||||||
return (
|
return (
|
||||||
<>
|
<OutfitStateContext.Provider value={outfitState}>
|
||||||
<OutfitStateContext.Provider value={outfitState}>
|
<WardrobePageLayout
|
||||||
<SupportOnly>
|
previewAndControls={
|
||||||
<WardrobeDevHacks />
|
<WardrobePreviewAndControls
|
||||||
</SupportOnly>
|
isLoading={loading}
|
||||||
|
outfitState={outfitState}
|
||||||
<WardrobePageLayout
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
previewAndControls={
|
/>
|
||||||
<WardrobePreviewAndControls
|
}
|
||||||
isLoading={loading}
|
itemsAndMaybeSearchPanel={
|
||||||
outfitState={outfitState}
|
<ItemsAndSearchPanels
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
loading={loading}
|
||||||
/>
|
searchQuery={searchQuery}
|
||||||
}
|
onChangeSearchQuery={setSearchQuery}
|
||||||
itemsAndMaybeSearchPanel={
|
outfitState={outfitState}
|
||||||
<ItemsAndSearchPanels
|
outfitSaving={outfitSaving}
|
||||||
loading={loading}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
searchQuery={searchQuery}
|
/>
|
||||||
onChangeSearchQuery={setSearchQuery}
|
}
|
||||||
outfitState={outfitState}
|
searchFooter={
|
||||||
outfitSaving={outfitSaving}
|
<SearchFooter
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
searchQuery={searchQuery}
|
||||||
/>
|
onChangeSearchQuery={setSearchQuery}
|
||||||
}
|
outfitState={outfitState}
|
||||||
searchFooter={
|
/>
|
||||||
<SearchFooter
|
}
|
||||||
searchQuery={searchQuery}
|
/>
|
||||||
onChangeSearchQuery={setSearchQuery}
|
</OutfitStateContext.Provider>
|
||||||
outfitState={outfitState}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</OutfitStateContext.Provider>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ function PosePickerSupport({
|
||||||
id
|
id
|
||||||
zone {
|
zone {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
}
|
}
|
||||||
|
|
||||||
# For AppearanceLayerSupportModal
|
# For AppearanceLayerSupportModal
|
||||||
|
@ -59,7 +59,7 @@ function PosePickerSupport({
|
||||||
}
|
}
|
||||||
restrictedZones {
|
restrictedZones {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
}
|
}
|
||||||
|
|
||||||
# For AppearanceLayerSupportModal to know the name
|
# For AppearanceLayerSupportModal to know the name
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import produce, { enableMapSet } from "immer";
|
import produce, { enableMapSet } from "immer";
|
||||||
import { useQuery, useApolloClient } from "@apollo/client";
|
import { useQuery, useApolloClient } from "@apollo/client";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
||||||
|
|
||||||
|
@ -156,13 +157,13 @@ function useOutfitState() {
|
||||||
layers {
|
layers {
|
||||||
zone {
|
zone {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
restrictedZones {
|
restrictedZones {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
isCommonlyUsedByItems @client
|
isCommonlyUsedByItems
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -387,7 +388,7 @@ function useParseOutfitUrl() {
|
||||||
// stable object!
|
// stable object!
|
||||||
const memoizedOutfitState = React.useMemo(
|
const memoizedOutfitState = React.useMemo(
|
||||||
() => readOutfitStateFromSearchParams(searchParams),
|
() => readOutfitStateFromSearchParams(searchParams),
|
||||||
[query]
|
[searchParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
return memoizedOutfitState;
|
return memoizedOutfitState;
|
||||||
|
|
|
@ -87,13 +87,13 @@ export function useSearchResults(
|
||||||
layers {
|
layers {
|
||||||
zone {
|
zone {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
restrictedZones {
|
restrictedZones {
|
||||||
id
|
id
|
||||||
label @client
|
label
|
||||||
isCommonlyUsedByItems @client
|
isCommonlyUsedByItems
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
230
app/javascript/wardrobe-2020/apolloClient.js
Normal file
230
app/javascript/wardrobe-2020/apolloClient.js
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
|
||||||
|
import { setContext } from "@apollo/client/link/context";
|
||||||
|
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
||||||
|
|
||||||
|
import { getAuthModeFeatureFlag } from "./components/useCurrentUser";
|
||||||
|
|
||||||
|
// Teach Apollo to load certain fields from the cache, to avoid extra network
|
||||||
|
// requests. This happens a lot - e.g. reusing data from item search on the
|
||||||
|
// outfit immediately!
|
||||||
|
const typePolicies = {
|
||||||
|
Query: {
|
||||||
|
fields: {
|
||||||
|
closetList: (_, { args, toReference }) => {
|
||||||
|
return toReference({ __typename: "ClosetList", id: args.id }, true);
|
||||||
|
},
|
||||||
|
items: (_, { args, toReference }) => {
|
||||||
|
return args.ids.map((id) =>
|
||||||
|
toReference({ __typename: "Item", id }, true)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
item: (_, { args, toReference }) => {
|
||||||
|
return toReference({ __typename: "Item", id: args.id }, true);
|
||||||
|
},
|
||||||
|
petAppearanceById: (_, { args, toReference }) => {
|
||||||
|
return toReference({ __typename: "PetAppearance", id: args.id }, true);
|
||||||
|
},
|
||||||
|
species: (_, { args, toReference }) => {
|
||||||
|
return toReference({ __typename: "Species", id: args.id }, true);
|
||||||
|
},
|
||||||
|
color: (_, { args, toReference }) => {
|
||||||
|
return toReference({ __typename: "Color", id: args.id }, true);
|
||||||
|
},
|
||||||
|
outfit: (_, { args, toReference }) => {
|
||||||
|
return toReference({ __typename: "Outfit", id: args.id }, true);
|
||||||
|
},
|
||||||
|
user: (_, { args, toReference }) => {
|
||||||
|
return toReference({ __typename: "User", id: args.id }, true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Item: {
|
||||||
|
fields: {
|
||||||
|
appearanceOn: (appearance, { args, readField, toReference }) => {
|
||||||
|
// If we already have this exact appearance in the cache, serve it!
|
||||||
|
if (appearance) {
|
||||||
|
return appearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we're going to see if this is a standard color, in which
|
||||||
|
// case we can reuse the standard color appearance if we already have
|
||||||
|
// it! This helps for fast loading when switching between standard
|
||||||
|
// colors.
|
||||||
|
const { speciesId, colorId } = args;
|
||||||
|
console.debug(
|
||||||
|
"[appearanceOn] seeking cached appearance",
|
||||||
|
speciesId,
|
||||||
|
colorId,
|
||||||
|
readField("id")
|
||||||
|
);
|
||||||
|
const speciesStandardBodyId = readField(
|
||||||
|
"standardBodyId",
|
||||||
|
toReference({ __typename: "Species", id: speciesId })
|
||||||
|
);
|
||||||
|
const colorIsStandard = readField(
|
||||||
|
"isStandard",
|
||||||
|
toReference({ __typename: "Color", id: colorId })
|
||||||
|
);
|
||||||
|
if (speciesStandardBodyId == null || colorIsStandard == null) {
|
||||||
|
// We haven't loaded all the species/colors into cache yet. We might
|
||||||
|
// be loading them, depending on the page? Either way, return
|
||||||
|
// `undefined`, meaning we don't know how to serve this from cache.
|
||||||
|
// This will cause us to start loading it from the server.
|
||||||
|
console.debug("[appearanceOn] species/colors not loaded yet");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorIsStandard) {
|
||||||
|
const itemId = readField("id");
|
||||||
|
console.debug(
|
||||||
|
"[appearanceOn] standard color, will read:",
|
||||||
|
`item-${itemId}-body-${speciesStandardBodyId}`
|
||||||
|
);
|
||||||
|
return toReference({
|
||||||
|
__typename: "ItemAppearance",
|
||||||
|
id: `item-${itemId}-body-${speciesStandardBodyId}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.debug("[appearanceOn] non-standard color, failure");
|
||||||
|
// This isn't a standard color, so we don't support special
|
||||||
|
// cross-color caching for it. Return `undefined`, meaning we don't
|
||||||
|
// know how to serve this from cache. This will cause us to start
|
||||||
|
// loading it from the server.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
currentUserOwnsThis: (cachedValue, { readField }) => {
|
||||||
|
if (cachedValue != null) {
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we know what items this user owns? If so, scan for this item.
|
||||||
|
const currentUserRef = readField("currentUser", {
|
||||||
|
__ref: "ROOT_QUERY",
|
||||||
|
});
|
||||||
|
if (!currentUserRef) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const thisItemId = readField("id");
|
||||||
|
const itemsTheyOwn = readField("itemsTheyOwn", currentUserRef);
|
||||||
|
if (!itemsTheyOwn) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theyOwnThisItem = itemsTheyOwn.some(
|
||||||
|
(itemRef) => readField("id", itemRef) === thisItemId
|
||||||
|
);
|
||||||
|
return theyOwnThisItem;
|
||||||
|
},
|
||||||
|
currentUserWantsThis: (cachedValue, { readField }) => {
|
||||||
|
if (cachedValue != null) {
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we know what items this user owns? If so, scan for this item.
|
||||||
|
const currentUserRef = readField("currentUser", {
|
||||||
|
__ref: "ROOT_QUERY",
|
||||||
|
});
|
||||||
|
if (!currentUserRef) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const thisItemId = readField("id");
|
||||||
|
const itemsTheyWant = readField("itemsTheyWant", currentUserRef);
|
||||||
|
if (!itemsTheyWant) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theyWantThisItem = itemsTheyWant.some(
|
||||||
|
(itemRef) => readField("id", itemRef) === thisItemId
|
||||||
|
);
|
||||||
|
return theyWantThisItem;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ClosetList: {
|
||||||
|
fields: {
|
||||||
|
// When loading the updated contents of a list, replace it entirely.
|
||||||
|
items: { merge: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpLink = createHttpLink({
|
||||||
|
uri: "https://impress-2020.openneo.net/api/graphql",
|
||||||
|
});
|
||||||
|
const buildAuthLink = (getAuth0) =>
|
||||||
|
setContext(async (_, { headers = {}, sendAuth = false }) => {
|
||||||
|
if (!sendAuth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await getAccessToken(getAuth0);
|
||||||
|
if (token) {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
authorization: token ? `Bearer ${token}` : "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is a temporary way to pass the DTIAuthMode feature flag back to the
|
||||||
|
// server!
|
||||||
|
const authModeLink = setContext((_, { headers = {} }) => {
|
||||||
|
const authMode = getAuthModeFeatureFlag();
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"DTI-Auth-Mode": authMode,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getAccessToken(getAuth0) {
|
||||||
|
// Wait for auth0 to stop loading, so we can maybe get a token!
|
||||||
|
// We'll do this hackily by checking every 100ms until it's true.
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
function check() {
|
||||||
|
if (getAuth0().isLoading) {
|
||||||
|
setTimeout(check, 100);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAuthenticated, getAccessTokenSilently } = getAuth0();
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const token = await getAccessTokenSilently();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildLink = (getAuth0) =>
|
||||||
|
buildAuthLink(getAuth0)
|
||||||
|
.concat(authModeLink)
|
||||||
|
.concat(
|
||||||
|
createPersistedQueryLink({
|
||||||
|
useGETForHashedQueries: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.concat(httpLink);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apolloClient is the global Apollo Client instance we use for GraphQL
|
||||||
|
* queries. This is how we communicate with the server!
|
||||||
|
*/
|
||||||
|
const buildClient = ({ getAuth0, initialCacheState }) => {
|
||||||
|
return new ApolloClient({
|
||||||
|
link: buildLink(getAuth0),
|
||||||
|
cache: new InMemoryCache({ typePolicies }).restore(initialCacheState),
|
||||||
|
connectToDevTools: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default buildClient;
|
|
@ -31,7 +31,11 @@ function SpeciesColorPicker({
|
||||||
colorTestId = null,
|
colorTestId = null,
|
||||||
onChange,
|
onChange,
|
||||||
}) {
|
}) {
|
||||||
const { loading: loadingMeta, error: errorMeta, data: meta } = useQuery(gql`
|
const {
|
||||||
|
loading: loadingMeta,
|
||||||
|
error: errorMeta,
|
||||||
|
data: meta,
|
||||||
|
} = useQuery(gql`
|
||||||
query SpeciesColorPicker {
|
query SpeciesColorPicker {
|
||||||
allSpecies {
|
allSpecies {
|
||||||
id
|
id
|
||||||
|
@ -340,11 +344,14 @@ let cachedResponseForAllValidPetPoses = null;
|
||||||
* data from GraphQL serves on the first render, without a loading state.
|
* data from GraphQL serves on the first render, without a loading state.
|
||||||
*/
|
*/
|
||||||
export function useAllValidPetPoses() {
|
export function useAllValidPetPoses() {
|
||||||
const networkResponse = useFetch("/api/validPetPoses", {
|
const networkResponse = useFetch(
|
||||||
responseType: "arrayBuffer",
|
"https://impress-2020.openneo.net/api/validPetPoses",
|
||||||
// If we already have globally-cached valids, skip the request.
|
{
|
||||||
skip: cachedResponseForAllValidPetPoses != null,
|
responseType: "arrayBuffer",
|
||||||
});
|
// If we already have globally-cached valids, skip the request.
|
||||||
|
skip: cachedResponseForAllValidPetPoses != null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Use the globally-cached response if we have one, or await the network
|
// Use the globally-cached response if we have one, or await the network
|
||||||
// response if not.
|
// response if not.
|
||||||
|
|
|
@ -94,8 +94,6 @@ function getVisibleLayers(petAppearance, itemAppearances) {
|
||||||
return visibleLayers;
|
return visibleLayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: The web client could save bandwidth by applying @client to the `depth`
|
|
||||||
// field, because it already has zone depths cached.
|
|
||||||
export const itemAppearanceFragmentForGetVisibleLayers = gql`
|
export const itemAppearanceFragmentForGetVisibleLayers = gql`
|
||||||
fragment ItemAppearanceForGetVisibleLayers on ItemAppearance {
|
fragment ItemAppearanceForGetVisibleLayers on ItemAppearance {
|
||||||
id
|
id
|
||||||
|
@ -113,8 +111,6 @@ export const itemAppearanceFragmentForGetVisibleLayers = gql`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO: The web client could save bandwidth by applying @client to the `depth`
|
|
||||||
// field, because it already has zone depths cached.
|
|
||||||
export const petAppearanceFragmentForGetVisibleLayers = gql`
|
export const petAppearanceFragmentForGetVisibleLayers = gql`
|
||||||
fragment PetAppearanceForGetVisibleLayers on PetAppearance {
|
fragment PetAppearanceForGetVisibleLayers on PetAppearance {
|
||||||
id
|
id
|
||||||
|
|
|
@ -136,8 +136,8 @@ export const appearanceLayerFragment = gql`
|
||||||
knownGlitches # For HTML5 & Known Glitches UI
|
knownGlitches # For HTML5 & Known Glitches UI
|
||||||
zone {
|
zone {
|
||||||
id
|
id
|
||||||
depth @client
|
depth
|
||||||
label @client
|
label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -149,7 +149,7 @@ export const appearanceLayerFragmentForSupport = gql`
|
||||||
swfUrl # HACK: This is for Support tools, but other views don't need it
|
swfUrl # HACK: This is for Support tools, but other views don't need it
|
||||||
zone {
|
zone {
|
||||||
id
|
id
|
||||||
label @client # HACK: This is for Support tools, but other views don't need it
|
label # HACK: This is for Support tools, but other views don't need it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import AppProvider from "./AppProvider";
|
||||||
import WardrobePage from "./WardrobePage";
|
import WardrobePage from "./WardrobePage";
|
||||||
|
|
||||||
export { WardrobePage };
|
export { AppProvider, WardrobePage };
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
"@emotion/styled": "^11.0.0",
|
"@emotion/styled": "^11.0.0",
|
||||||
"@loadable/component": "^5.12.0",
|
"@loadable/component": "^5.12.0",
|
||||||
"@sentry/react": "^5.30.0",
|
"@sentry/react": "^5.30.0",
|
||||||
|
"@sentry/tracing": "^5.30.0",
|
||||||
|
"apollo-link-persisted-queries": "^0.2.2",
|
||||||
"easeljs": "^1.0.2",
|
"easeljs": "^1.0.2",
|
||||||
"esbuild": "^0.19.0",
|
"esbuild": "^0.19.0",
|
||||||
"framer-motion": "^4.1.11",
|
"framer-motion": "^4.1.11",
|
||||||
|
@ -25,6 +27,7 @@
|
||||||
"tweenjs": "^1.0.2"
|
"tweenjs": "^1.0.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text"
|
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text",
|
||||||
|
"build:production": "yarn build --minify"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
88
yarn.lock
88
yarn.lock
|
@ -985,6 +985,17 @@
|
||||||
hoist-non-react-statics "^3.3.2"
|
hoist-non-react-statics "^3.3.2"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/tracing@^5.30.0":
|
||||||
|
version "5.30.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f"
|
||||||
|
integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/hub" "5.30.0"
|
||||||
|
"@sentry/minimal" "5.30.0"
|
||||||
|
"@sentry/types" "5.30.0"
|
||||||
|
"@sentry/utils" "5.30.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/types@5.30.0":
|
"@sentry/types@5.30.0":
|
||||||
version "5.30.0"
|
version "5.30.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402"
|
||||||
|
@ -1046,6 +1057,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.3.0"
|
tslib "^2.3.0"
|
||||||
|
|
||||||
|
"@wry/equality@^0.1.2":
|
||||||
|
version "0.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790"
|
||||||
|
integrity sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@wry/equality@^0.5.6":
|
"@wry/equality@^0.5.6":
|
||||||
version "0.5.6"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.6.tgz#cd4a533c72c3752993ab8cbf682d3d20e3cb601e"
|
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.6.tgz#cd4a533c72c3752993ab8cbf682d3d20e3cb601e"
|
||||||
|
@ -1072,6 +1090,34 @@ ansi-styles@^3.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^1.9.0"
|
color-convert "^1.9.0"
|
||||||
|
|
||||||
|
apollo-link-persisted-queries@^0.2.2:
|
||||||
|
version "0.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/apollo-link-persisted-queries/-/apollo-link-persisted-queries-0.2.5.tgz#76deabf68dac218d83f2fa23eebc3b25772fd914"
|
||||||
|
integrity sha512-PYWsMFcRGT9NZ6e6EK5rlhNDtcK6FR76JDy1RIngEfR6RdM5a2Z0IhZdn9RTTNB3V/+s7iWviQmoCfQrTVXu0A==
|
||||||
|
dependencies:
|
||||||
|
apollo-link "^1.2.1"
|
||||||
|
hash.js "^1.1.7"
|
||||||
|
|
||||||
|
apollo-link@^1.2.1:
|
||||||
|
version "1.2.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9"
|
||||||
|
integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==
|
||||||
|
dependencies:
|
||||||
|
apollo-utilities "^1.3.0"
|
||||||
|
ts-invariant "^0.4.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
zen-observable-ts "^0.8.21"
|
||||||
|
|
||||||
|
apollo-utilities@^1.3.0:
|
||||||
|
version "1.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.4.tgz#6129e438e8be201b6c55b0f13ce49d2c7175c9cf"
|
||||||
|
integrity sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==
|
||||||
|
dependencies:
|
||||||
|
"@wry/equality" "^0.1.2"
|
||||||
|
fast-json-stable-stringify "^2.0.0"
|
||||||
|
ts-invariant "^0.4.0"
|
||||||
|
tslib "^1.10.0"
|
||||||
|
|
||||||
aria-hidden@^1.1.1:
|
aria-hidden@^1.1.1:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
|
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
|
||||||
|
@ -1244,6 +1290,11 @@ escape-string-regexp@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||||
|
|
||||||
|
fast-json-stable-stringify@^2.0.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||||
|
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||||
|
|
||||||
fast-text-encoding@^1.0.6:
|
fast-text-encoding@^1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
|
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
|
||||||
|
@ -1315,6 +1366,14 @@ has@^1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
hash.js@^1.1.7:
|
||||||
|
version "1.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
|
||||||
|
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
|
||||||
|
dependencies:
|
||||||
|
inherits "^2.0.3"
|
||||||
|
minimalistic-assert "^1.0.1"
|
||||||
|
|
||||||
hey-listen@^1.0.8:
|
hey-listen@^1.0.8:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
|
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
|
||||||
|
@ -1340,6 +1399,11 @@ import-fresh@^3.2.1:
|
||||||
parent-module "^1.0.0"
|
parent-module "^1.0.0"
|
||||||
resolve-from "^4.0.0"
|
resolve-from "^4.0.0"
|
||||||
|
|
||||||
|
inherits@^2.0.3:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
invariant@^2.2.4:
|
invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
|
@ -1398,6 +1462,11 @@ lru-cache@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
minimalistic-assert@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||||
|
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||||
|
|
||||||
object-assign@^3.0.0:
|
object-assign@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
|
||||||
|
@ -1689,7 +1758,14 @@ ts-invariant@^0.10.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
tslib@^1.0.0, tslib@^1.9.3:
|
ts-invariant@^0.4.0:
|
||||||
|
version "0.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
|
||||||
|
integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
tslib@^1.0.0, tslib@^1.10.0, tslib@^1.9.3:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
@ -1741,6 +1817,14 @@ yaml@^1.10.0:
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||||
|
|
||||||
|
zen-observable-ts@^0.8.21:
|
||||||
|
version "0.8.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d"
|
||||||
|
integrity sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^1.9.3"
|
||||||
|
zen-observable "^0.8.0"
|
||||||
|
|
||||||
zen-observable-ts@^1.2.5:
|
zen-observable-ts@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58"
|
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58"
|
||||||
|
@ -1748,7 +1832,7 @@ zen-observable-ts@^1.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
zen-observable "0.8.15"
|
zen-observable "0.8.15"
|
||||||
|
|
||||||
zen-observable@0.8.15:
|
zen-observable@0.8.15, zen-observable@^0.8.0:
|
||||||
version "0.8.15"
|
version "0.8.15"
|
||||||
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
|
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
|
||||||
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
|
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
|
||||||
|
|
Loading…
Reference in a new issue