diff --git a/.changeset/lemon-beans-drum.md b/.changeset/lemon-beans-drum.md new file mode 100644 index 0000000000..b086005aaa --- /dev/null +++ b/.changeset/lemon-beans-drum.md @@ -0,0 +1,754 @@ +--- +'skeleton': patch +--- + +Optional updates for the product route and product form to handle combined listing and 2000 variant limit. + +1. Update your SFAPI product query to bring in the new query fields: + +```diff +const PRODUCT_FRAGMENT = `#graphql + fragment Product on Product { + id + title + vendor + handle + descriptionHtml + description ++ encodedVariantExistence ++ encodedVariantAvailability + options { + name + optionValues { + name ++ firstSelectableVariant { ++ ...ProductVariant ++ } ++ swatch { ++ color ++ image { ++ previewImage { ++ url ++ } ++ } ++ } + } + } +- selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { ++ selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { ++ ...ProductVariant ++ } ++ adjacentVariants (selectedOptions: $selectedOptions) { ++ ...ProductVariant ++ } +- variants(first: 1) { +- nodes { +- ...ProductVariant +- } +- } + seo { + description + title + } + } + ${PRODUCT_VARIANT_FRAGMENT} +` as const; +``` + +2. Update `loadDeferredData` function. We no longer need to load in all the variants. You can also remove `VARIANTS_QUERY` variable. + +```diff +function loadDeferredData({context, params}: LoaderFunctionArgs) { ++ // Put any API calls that is not critical to be available on first page render ++ // For example: product reviews, product recommendations, social feeds. +- // In order to show which variants are available in the UI, we need to query +- // all of them. But there might be a *lot*, so instead separate the variants +- // into it's own separate query that is deferred. So there's a brief moment +- // where variant options might show as available when they're not, but after +- // this deferred query resolves, the UI will update. +- const variants = context.storefront +- .query(VARIANTS_QUERY, { +- variables: {handle: params.handle!}, +- }) +- .catch((error) => { +- // Log query errors, but don't throw them so the page can still render +- console.error(error); +- return null; +- }); + ++ return {} +- return { +- variants, +- }; +} +``` + +3. Update the `Product` component to use the new data fields. + +```diff +import { + getSelectedProductOptions, + Analytics, + useOptimisticVariant, ++ getAdjacentAndFirstAvailableVariants, +} from '@shopify/hydrogen'; + +export default function Product() { ++ const {product} = useLoaderData(); +- const {product, variants} = useLoaderData(); + ++ // Optimistically selects a variant with given available variant information ++ const selectedVariant = useOptimisticVariant( ++ product.selectedOrFirstAvailableVariant, ++ getAdjacentAndFirstAvailableVariants(product), ++ ); +- const selectedVariant = useOptimisticVariant( +- product.selectedVariant, +- variants, +- ); +``` + +4. Handle missing search query param in url from selecting a first variant + +```diff +import { + getSelectedProductOptions, + Analytics, + useOptimisticVariant, + getAdjacentAndFirstAvailableVariants, ++ mapSelectedProductOptionToObject, +} from '@shopify/hydrogen'; + +export default function Product() { + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information + const selectedVariant = useOptimisticVariant( + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), + ); + ++ // Sets the search param to the selected variant without navigation ++ // only when no search params are set in the url ++ useEffect(() => { ++ const searchParams = new URLSearchParams( ++ mapSelectedProductOptionToObject( ++ selectedVariant.selectedOptions || [], ++ ), ++ ); + ++ if (window.location.search === '' && searchParams.toString() !== '') { ++ window.history.replaceState( ++ {}, ++ '', ++ `${location.pathname}?${searchParams.toString()}`, ++ ); ++ } ++ }, [ ++ JSON.stringify(selectedVariant.selectedOptions), ++ ]); +``` + +5. Get the product options array using `getProductOptions` + +```diff +import { + getSelectedProductOptions, + Analytics, + useOptimisticVariant, ++ getProductOptions, + getAdjacentAndFirstAvailableVariants, + mapSelectedProductOptionToObject, +} from '@shopify/hydrogen'; + +export default function Product() { + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information + const selectedVariant = useOptimisticVariant( + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), + ); + + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useEffect(() => { + // ... + }, [ + JSON.stringify(selectedVariant.selectedOptions), + ]); + ++ // Get the product options array ++ const productOptions = getProductOptions({ ++ ...product, ++ selectedOrFirstAvailableVariant: selectedVariant, ++ }); +``` + +6. Remove the `Await` and `Suspense` from the `ProductForm`. We no longer have any queries that we need to wait for. + +```diff +export default function Product() { + + ... + + return ( + ... ++ +- +- } +- > +- +- {(data) => ( +- +- )} +- +- +``` + +7. Update the `ProductForm` component. + +```tsx +import {Link, useNavigate} from '@remix-run/react'; +import {type MappedProductOptions} from '@shopify/hydrogen'; +import type { + Maybe, + ProductOptionValueSwatch, +} from '@shopify/hydrogen/storefront-api-types'; +import {AddToCartButton} from './AddToCartButton'; +import {useAside} from './Aside'; +import type {ProductFragment} from 'storefrontapi.generated'; + +export function ProductForm({ + productOptions, + selectedVariant, +}: { + productOptions: MappedProductOptions[]; + selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; +}) { + const navigate = useNavigate(); + const {open} = useAside(); + return ( +
+ {productOptions.map((option) => ( +
+
{option.name}
+
+ {option.optionValues.map((value) => { + const { + name, + handle, + variantUriQuery, + selected, + available, + exists, + isDifferentProduct, + swatch, + } = value; + + if (isDifferentProduct) { + // SEO + // When the variant is a combined listing child product + // that leads to a different url, we need to render it + // as an anchor tag + return ( + + + + ); + } else { + // SEO + // When the variant is an update to the search param, + // render it as a button with javascript navigating to + // the variant so that SEO bots do not index these as + // duplicated links + return ( + + ); + } + })} +
+
+
+ ))} + { + open('cart'); + }} + lines={ + selectedVariant + ? [ + { + merchandiseId: selectedVariant.id, + quantity: 1, + selectedVariant, + }, + ] + : [] + } + > + {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} + +
+ ); +} + +function ProductOptionSwatch({ + swatch, + name, +}: { + swatch?: Maybe | undefined; + name: string; +}) { + const image = swatch?.image?.previewImage?.url; + const color = swatch?.color; + + if (!image && !color) return name; + + return ( +
+ {!!image && {name}} +
+ ); +} +``` + +8. Update `app.css` + +```diff ++ /* ++ * -------------------------------------------------- ++ * Non anchor links ++ * -------------------------------------------------- ++ */ ++ .link:hover { ++ text-decoration: underline; ++ cursor: pointer; ++ } + +... + +- .product-options-item { ++ .product-options-item, ++ .product-options-item:disabled { ++ padding: 0.25rem 0.5rem; ++ background-color: transparent; ++ font-size: 1rem; ++ font-family: inherit; ++ } + ++ .product-option-label-swatch { ++ width: 1.25rem; ++ height: 1.25rem; ++ margin: 0.25rem 0; ++ } + ++ .product-option-label-swatch img { ++ width: 100%; ++ } +``` + +9. Update `lib/variants.ts` + +Make `useVariantUrl` flexible to supplying a selected option param + +```diff +export function useVariantUrl( + handle: string, +- selectedOptions: SelectedOption[], ++ selectedOptions?: SelectedOption[], +) { + const {pathname} = useLocation(); + + return useMemo(() => { + return getVariantUrl({ + handle, + pathname, + searchParams: new URLSearchParams(), + selectedOptions, + }); + }, [handle, selectedOptions, pathname]); +} +export function getVariantUrl({ + handle, + pathname, + searchParams, + selectedOptions, +}: { + handle: string; + pathname: string; + searchParams: URLSearchParams; +- selectedOptions: SelectedOption[]; ++ selectedOptions?: SelectedOption[], +}) { + const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); + const isLocalePathname = match && match.length > 0; + const path = isLocalePathname + ? `${match![0]}products/${handle}` + : `/products/${handle}`; + +- selectedOptions.forEach((option) => { ++ selectedOptions?.forEach((option) => { + searchParams.set(option.name, option.value); + }); +``` + +10. Update `routes/collections.$handle.tsx` + +We no longer need to query for the variants since product route can efficiently +obtain the first available variants. Update the code to reflect that: + +```diff +const PRODUCT_ITEM_FRAGMENT = `#graphql + fragment MoneyProductItem on MoneyV2 { + amount + currencyCode + } + fragment ProductItem on Product { + id + handle + title + featuredImage { + id + altText + url + width + height + } + priceRange { + minVariantPrice { + ...MoneyProductItem + } + maxVariantPrice { + ...MoneyProductItem + } + } +- variants(first: 1) { +- nodes { +- selectedOptions { +- name +- value +- } +- } +- } + } +` as const; +``` + +and remove the variant reference +```diff +function ProductItem({ + product, + loading, +}: { + product: ProductItemFragment; + loading?: 'eager' | 'lazy'; +}) { +- const variant = product.variants.nodes[0]; +- const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); ++ const variantUrl = useVariantUrl(product.handle); + return ( +``` + +11. Update `routes/collections.all.tsx` + +Same reasoning as `collections.$handle.tsx` + +```diff +const PRODUCT_ITEM_FRAGMENT = `#graphql + fragment MoneyProductItem on MoneyV2 { + amount + currencyCode + } + fragment ProductItem on Product { + id + handle + title + featuredImage { + id + altText + url + width + height + } + priceRange { + minVariantPrice { + ...MoneyProductItem + } + maxVariantPrice { + ...MoneyProductItem + } + } +- variants(first: 1) { +- nodes { +- selectedOptions { +- name +- value +- } +- } +- } + } +` as const; +``` + +and remove the variant reference +```diff +function ProductItem({ + product, + loading, +}: { + product: ProductItemFragment; + loading?: 'eager' | 'lazy'; +}) { +- const variant = product.variants.nodes[0]; +- const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); ++ const variantUrl = useVariantUrl(product.handle); + return ( +``` + +12. Update `routes/search.tsx` + +Instead of using the first variant, use `selectedOrFirstAvailableVariant` + +```diff +const SEARCH_PRODUCT_FRAGMENT = `#graphql + fragment SearchProduct on Product { + __typename + handle + id + publishedAt + title + trackingParameters + vendor +- variants(first: 1) { +- nodes { ++ selectedOrFirstAvailableVariant( ++ selectedOptions: [] ++ ignoreUnknownOptions: true ++ caseInsensitiveMatch: true ++ ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode + } + compareAtPrice { + amount + currencyCode + } + selectedOptions { + name + value + } + product { + handle + title + } + } +- } + } +` as const; +``` + +```diff +const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql + fragment PredictiveProduct on Product { + __typename + id + title + handle + trackingParameters +- variants(first: 1) { +- nodes { ++ selectedOrFirstAvailableVariant( ++ selectedOptions: [] ++ ignoreUnknownOptions: true ++ caseInsensitiveMatch: true ++ ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode + } + } +- } + } +``` + +13. Update `components/SearchResults.tsx` + +```diff +function SearchResultsProducts({ + term, + products, +}: PartialSearchResult<'products'>) { + if (!products?.nodes.length) { + return null; + } + + return ( +
+

Products

+ + {({nodes, isLoading, NextLink, PreviousLink}) => { + const ItemsMarkup = nodes.map((product) => { + const productUrl = urlWithTrackingParams({ + baseUrl: `/products/${product.handle}`, + trackingParams: product.trackingParameters, + term, + }); + ++ const price = product?.selectedOrFirstAvailableVariant?.price; ++ const image = product?.selectedOrFirstAvailableVariant?.image; + + return ( +
+ +- {product.variants.nodes[0].image && ( ++ {image && ( + {product.title} + )} +
+

{product.title}

+ +- ++ {price && ++ ++ } + +
+ +
+ ); + }); +``` + +14. Update `components/SearchResultsPredictive.tsx` + +```diff +function SearchResultsPredictiveProducts({ + term, + products, + closeSearch, +}: PartialPredictiveSearchResult<'products'>) { + if (!products.length) return null; + + return ( +
+
Products
+
    + {products.map((product) => { + const productUrl = urlWithTrackingParams({ + baseUrl: `/products/${product.handle}`, + trackingParams: product.trackingParameters, + term: term.current, + }); + ++ const price = product?.selectedOrFirstAvailableVariant?.price; +- const image = product?.variants?.nodes?.[0].image; ++ const image = product?.selectedOrFirstAvailableVariant?.image; + return ( +
  • + + {image && ( + {image.altText + )} +
    +

    {product.title}

    + +- {product?.variants?.nodes?.[0].price && ( ++ {price && ( +- ++ + )} + +
    + +
  • + ); + })} +
+
+ ); +} +``` diff --git a/.changeset/three-cows-shave.md b/.changeset/three-cows-shave.md new file mode 100644 index 0000000000..c742a88a4b --- /dev/null +++ b/.changeset/three-cows-shave.md @@ -0,0 +1,6 @@ +--- +'@shopify/hydrogen-react': patch +'@shopify/hydrogen': patch +--- + +Introduce `getProductOptions`, `getAdjacentAndFirstAvailableVariants`, `useSelectedOptionInUrlParam`, and `mapSelectedProductOptionToObject` to support combined listing products and products with 2000 variants limit. diff --git a/docs/shopify-dev/analytics-setup/js/app/routes/collections.$handle.jsx b/docs/shopify-dev/analytics-setup/js/app/routes/collections.$handle.jsx index d78fadc221..9360370805 100644 --- a/docs/shopify-dev/analytics-setup/js/app/routes/collections.$handle.jsx +++ b/docs/shopify-dev/analytics-setup/js/app/routes/collections.$handle.jsx @@ -114,8 +114,7 @@ export default function Collection() { * }} */ function ProductItem({product, loading}) { - const variant = product.variants.nodes[0]; - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + const variantUrl = useVariantUrl(product.handle); return ( option.name === 'Title' && option.value === 'Default Title', - ), - ); - - if (firstVariantIsDefault) { - product.selectedVariant = firstVariant; - } else { - // if no selected variant was returned from the selected options, - // we redirect to the first variant's url with it's selected options applied - if (!product.selectedVariant) { - throw redirectToFirstVariant({product, request}); - } - } - return { product, }; @@ -86,57 +71,32 @@ async function loadCriticalData({context, params, request}) { * @param {LoaderFunctionArgs} */ function loadDeferredData({context, params}) { - // In order to show which variants are available in the UI, we need to query - // all of them. But there might be a *lot*, so instead separate the variants - // into it's own separate query that is deferred. So there's a brief moment - // where variant options might show as available when they're not, but after - // this deffered query resolves, the UI will update. - const variants = context.storefront - .query(VARIANTS_QUERY, { - variables: {handle: params.handle}, - }) - .catch((error) => { - // Log query errors, but don't throw them so the page can still render - console.error(error); - return null; - }); + // Put any API calls that is not critical to be available on first page render + // For example: product reviews, product recommendations, social feeds. - return { - variants, - }; -} - -/** - * @param {{ - * product: ProductFragment; - * request: Request; - * }} - */ -function redirectToFirstVariant({product, request}) { - const url = new URL(request.url); - const firstVariant = product.variants.nodes[0]; - - return redirect( - getVariantUrl({ - pathname: url.pathname, - handle: product.handle, - selectedOptions: firstVariant.selectedOptions, - searchParams: new URLSearchParams(url.search), - }), - { - status: 302, - }, - ); + return {}; } export default function Product() { /** @type {LoaderReturnData} */ - const {product, variants} = useLoaderData(); + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( - product.selectedVariant, - variants, + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), ); + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + + // Get the product options array + const productOptions = getProductOptions({ + ...product, + selectedOrFirstAvailableVariant: selectedVariant, + }); + const {title, descriptionHtml} = product; return ( @@ -149,28 +109,10 @@ export default function Product() { compareAtPrice={selectedVariant?.compareAtPrice} />
- - } - > - - {(data) => ( - - )} - - +

@@ -246,19 +188,30 @@ const PRODUCT_FRAGMENT = `#graphql handle descriptionHtml description + encodedVariantExistence + encodedVariantAvailability options { name optionValues { name + firstSelectableVariant { + ...ProductVariant + } + swatch { + color + image { + previewImage { + url + } + } + } } } - selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { ...ProductVariant } - variants(first: 1) { - nodes { - ...ProductVariant - } + adjacentVariants (selectedOptions: $selectedOptions) { + ...ProductVariant } seo { description @@ -282,30 +235,6 @@ const PRODUCT_QUERY = `#graphql ${PRODUCT_FRAGMENT} `; -const PRODUCT_VARIANTS_FRAGMENT = `#graphql - fragment ProductVariants on Product { - variants(first: 250) { - nodes { - ...ProductVariant - } - } - } - ${PRODUCT_VARIANT_FRAGMENT} -`; - -const VARIANTS_QUERY = `#graphql - ${PRODUCT_VARIANTS_FRAGMENT} - query ProductVariants( - $country: CountryCode - $language: LanguageCode - $handle: String! - ) @inContext(country: $country, language: $language) { - product(handle: $handle) { - ...ProductVariants - } - } -`; - /** @typedef {import('@shopify/remix-oxygen').LoaderFunctionArgs} LoaderFunctionArgs */ /** @template T @typedef {import('@remix-run/react').MetaFunction} MetaFunction */ /** @typedef {import('storefrontapi.generated').ProductFragment} ProductFragment */ diff --git a/docs/shopify-dev/analytics-setup/ts/app/routes/collections.$handle.tsx b/docs/shopify-dev/analytics-setup/ts/app/routes/collections.$handle.tsx index ea1320fb21..1e1b4c9171 100644 --- a/docs/shopify-dev/analytics-setup/ts/app/routes/collections.$handle.tsx +++ b/docs/shopify-dev/analytics-setup/ts/app/routes/collections.$handle.tsx @@ -110,8 +110,7 @@ function ProductItem({ product: ProductItemFragment; loading?: 'eager' | 'lazy'; }) { - const variant = product.variants.nodes[0]; - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + const variantUrl = useVariantUrl(product.handle); return ( - option.name === 'Title' && option.value === 'Default Title', - ), - ); - - if (firstVariantIsDefault) { - product.selectedVariant = firstVariant; - } else { - // if no selected variant was returned from the selected options, - // we redirect to the first variant's url with it's selected options applied - if (!product.selectedVariant) { - throw redirectToFirstVariant({product, request}); - } - } - return { product, }; @@ -85,56 +69,31 @@ async function loadCriticalData({ * Make sure to not throw any errors here, as it will cause the page to 500. */ function loadDeferredData({context, params}: LoaderFunctionArgs) { - // In order to show which variants are available in the UI, we need to query - // all of them. But there might be a *lot*, so instead separate the variants - // into it's own separate query that is deferred. So there's a brief moment - // where variant options might show as available when they're not, but after - // this deffered query resolves, the UI will update. - const variants = context.storefront - .query(VARIANTS_QUERY, { - variables: {handle: params.handle!}, - }) - .catch((error) => { - // Log query errors, but don't throw them so the page can still render - console.error(error); - return null; - }); - - return { - variants, - }; -} + // Put any API calls that is not critical to be available on first page render + // For example: product reviews, product recommendations, social feeds. -function redirectToFirstVariant({ - product, - request, -}: { - product: ProductFragment; - request: Request; -}) { - const url = new URL(request.url); - const firstVariant = product.variants.nodes[0]; - - return redirect( - getVariantUrl({ - pathname: url.pathname, - handle: product.handle, - selectedOptions: firstVariant.selectedOptions, - searchParams: new URLSearchParams(url.search), - }), - { - status: 302, - }, - ); + return {}; } export default function Product() { - const {product, variants} = useLoaderData(); + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( - product.selectedVariant, - variants, + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), ); + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + + // Get the product options array + const productOptions = getProductOptions({ + ...product, + selectedOrFirstAvailableVariant: selectedVariant, + }); + const {title, descriptionHtml} = product; return ( @@ -147,28 +106,10 @@ export default function Product() { compareAtPrice={selectedVariant?.compareAtPrice} />
- - } - > - - {(data) => ( - - )} - - +

@@ -249,19 +190,30 @@ const PRODUCT_FRAGMENT = `#graphql handle descriptionHtml description + encodedVariantExistence + encodedVariantAvailability options { name optionValues { name + firstSelectableVariant { + ...ProductVariant + } + swatch { + color + image { + previewImage { + url + } + } + } } } - selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { ...ProductVariant } - variants(first: 1) { - nodes { - ...ProductVariant - } + adjacentVariants (selectedOptions: $selectedOptions) { + ...ProductVariant } seo { description @@ -284,27 +236,3 @@ const PRODUCT_QUERY = `#graphql } ${PRODUCT_FRAGMENT} ` as const; - -const PRODUCT_VARIANTS_FRAGMENT = `#graphql - fragment ProductVariants on Product { - variants(first: 250) { - nodes { - ...ProductVariant - } - } - } - ${PRODUCT_VARIANT_FRAGMENT} -` as const; - -const VARIANTS_QUERY = `#graphql - ${PRODUCT_VARIANTS_FRAGMENT} - query ProductVariants( - $country: CountryCode - $language: LanguageCode - $handle: String! - ) @inContext(country: $country, language: $language) { - product(handle: $handle) { - ...ProductVariants - } - } -` as const; diff --git a/examples/b2b/app/components/ProductForm.tsx b/examples/b2b/app/components/ProductForm.tsx index e72710a971..43efea7aef 100644 --- a/examples/b2b/app/components/ProductForm.tsx +++ b/examples/b2b/app/components/ProductForm.tsx @@ -1,37 +1,111 @@ -import {Link} from '@remix-run/react'; -import {type VariantOption, VariantSelector} from '@shopify/hydrogen'; +import {Link, useNavigate} from '@remix-run/react'; +import {type MappedProductOptions} from '@shopify/hydrogen'; import type { - ProductFragment, - ProductVariantFragment, -} from 'storefrontapi.generated'; + Maybe, + ProductOptionValueSwatch, +} from '@shopify/hydrogen/storefront-api-types'; import {AddToCartButton} from '~/components/AddToCartButton'; import {useAside} from '~/components/Aside'; +import type {ProductFragment} from 'storefrontapi.generated'; /***********************************************/ /********** EXAMPLE UPDATE STARTS ************/ export function ProductForm({ - product, + productOptions, selectedVariant, - variants, quantity, }: { - product: ProductFragment; - selectedVariant: ProductFragment['selectedVariant']; - variants: Array; + productOptions: MappedProductOptions[]; + selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; quantity: number; }) { /********** EXAMPLE UPDATE END ************/ /***********************************************/ + const navigate = useNavigate(); const {open} = useAside(); return (

- option.optionValues.length > 1)} - variants={variants} - > - {({option}) => } - + {productOptions.map((option) => { + // If there is only a single value in the option values, don't display the option + if (option.optionValues.length === 1) return null; + + return ( +
+
{option.name}
+
+ {option.optionValues.map((value) => { + const { + name, + handle, + variantUriQuery, + selected, + available, + exists, + isDifferentProduct, + swatch, + } = value; + + if (isDifferentProduct) { + // SEO + // When the variant is a combined listing child product + // that leads to a different url, we need to render it + // as an anchor tag + return ( + + + + ); + } else { + // SEO + // When the variant is an update to the search param, + // render it as a button with javascript navigating to + // the variant so that SEO bots do not index these as + // duplicated links + return ( + + ); + } + })} +
+
+
+ ) + })}
| undefined; + name: string; +}) { + const image = swatch?.image?.previewImage?.url; + const color = swatch?.color; + + if (!image && !color) return name; + return ( -
-
{option.name}
-
- {option.values.map(({value, isAvailable, isActive, to}) => { - return ( - - {value} - - ); - })} -
-
+
+ {!!image && {name}}
); } diff --git a/examples/b2b/app/routes/products.$handle.tsx b/examples/b2b/app/routes/products.$handle.tsx index 586955217d..02f350ea5c 100644 --- a/examples/b2b/app/routes/products.$handle.tsx +++ b/examples/b2b/app/routes/products.$handle.tsx @@ -1,14 +1,13 @@ -import {Suspense} from 'react'; -import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen'; -import {Await, useLoaderData, type MetaFunction} from '@remix-run/react'; -import type {ProductFragment} from 'storefrontapi.generated'; +import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen'; +import {useLoaderData, type MetaFunction} from '@remix-run/react'; import { getSelectedProductOptions, Analytics, useOptimisticVariant, + getProductOptions, + getAdjacentAndFirstAvailableVariants, + useSelectedOptionInUrlParam, } from '@shopify/hydrogen'; -import type {SelectedOption} from '@shopify/hydrogen/storefront-api-types'; -import {getVariantUrl} from '~/lib/variants'; import {ProductPrice} from '~/components/ProductPrice'; import {ProductImage} from '~/components/ProductImage'; import {ProductForm} from '~/components/ProductForm'; @@ -93,24 +92,6 @@ async function loadCriticalData({ throw new Response(null, {status: 404}); } - const firstVariant = product.variants.nodes[0]; - const firstVariantIsDefault = Boolean( - firstVariant.selectedOptions.find( - (option: SelectedOption) => - option.name === 'Title' && option.value === 'Default Title', - ), - ); - - if (firstVariantIsDefault) { - product.selectedVariant = firstVariant; - } else { - // if no selected variant was returned from the selected options, - // we redirect to the first variant's url with it's selected options applied - if (!product.selectedVariant) { - throw redirectToFirstVariant({product, request}); - } - } - return { product, }; @@ -124,61 +105,35 @@ async function loadCriticalData({ /***********************************************/ /********** EXAMPLE UPDATE STARTS ************/ function loadDeferredData({context, params}: LoaderFunctionArgs, buyerVariables: BuyerVariables) { - const {storefront} = context; + // Put any API calls that is not critical to be available on first page render + // For example: product reviews, product recommendations, social feeds. - // In order to show which variants are available in the UI, we need to query - // all of them. But there might be a *lot*, so instead separate the variants - // into it's own separate query that is deferred. So there's a brief moment - // where variant options might show as available when they're not, but after - // this deferred query resolves, the UI will update. - const variants = context.storefront - .query(VARIANTS_QUERY, { - variables: {handle: params.handle!, ...buyerVariables}, - cache: storefront.CacheNone(), - }) - .catch((error) => { - // Log query errors, but don't throw them so the page can still render - console.error(error); - return null; - }); + // Make sure to pass in buyerVariables to any deferred queries to SFAPI - return { - variants, - }; + return {}; } /********** EXAMPLE UPDATE END *************/ /***********************************************/ -function redirectToFirstVariant({ - product, - request, -}: { - product: ProductFragment; - request: Request; -}) { - const url = new URL(request.url); - const firstVariant = product.variants.nodes[0]; - - return redirect( - getVariantUrl({ - pathname: url.pathname, - handle: product.handle, - selectedOptions: firstVariant.selectedOptions, - searchParams: new URLSearchParams(url.search), - }), - { - status: 302, - }, - ); -} - export default function Product() { - const {product, variants} = useLoaderData(); + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( - product.selectedVariant, - variants, + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), ); + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + + // Get the product options array + const productOptions = getProductOptions({ + ...product, + selectedOrFirstAvailableVariant: selectedVariant, + }); + const {title, descriptionHtml} = product; return ( @@ -191,38 +146,15 @@ export default function Product() { compareAtPrice={selectedVariant?.compareAtPrice} />
- - } - > - - {(data) => ( - - )} - - +
{ /***********************************************/ @@ -336,19 +268,30 @@ const PRODUCT_FRAGMENT = `#graphql handle descriptionHtml description + encodedVariantExistence + encodedVariantAvailability options { name optionValues { name + firstSelectableVariant { + ...ProductVariant + } + swatch { + color + image { + previewImage { + url + } + } + } } } - selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { ...ProductVariant } - variants(first: 1) { - nodes { - ...ProductVariant - } + adjacentVariants (selectedOptions: $selectedOptions) { + ...ProductVariant } seo { description @@ -376,32 +319,3 @@ const PRODUCT_QUERY = `#graphql ` as const; /********** EXAMPLE UPDATE END ************/ /***********************************************/ - -const PRODUCT_VARIANTS_FRAGMENT = `#graphql - fragment ProductVariants on Product { - variants(first: 250) { - nodes { - ...ProductVariant - } - } - } - ${PRODUCT_VARIANT_FRAGMENT} -` as const; - -/***********************************************/ -/********** EXAMPLE UPDATE STARTS ************/ -const VARIANTS_QUERY = `#graphql - ${PRODUCT_VARIANTS_FRAGMENT} - query ProductVariants( - $country: CountryCode - $buyer: BuyerInput - $language: LanguageCode - $handle: String! - ) @inContext(country: $country, language: $language, buyer: $buyer) { - product(handle: $handle) { - ...ProductVariants - } - } -` as const; -/********** EXAMPLE UPDATE END ************/ -/***********************************************/ diff --git a/examples/b2b/app/styles/app.css b/examples/b2b/app/styles/app.css index 5b4f806063..5d73d7681c 100644 --- a/examples/b2b/app/styles/app.css +++ b/examples/b2b/app/styles/app.css @@ -12,6 +12,16 @@ img { border-radius: 4px; } +/* +* -------------------------------------------------- +* Non anchor links +* -------------------------------------------------- +*/ +.link:hover { + text-decoration: underline; + cursor: pointer; +} + /* * -------------------------------------------------- * components/Aside @@ -440,8 +450,22 @@ button.reset:hover:not(:has(> *)) { grid-gap: 0.75rem; } -.product-options-item { +.product-options-item, +.product-options-item:disabled { padding: 0.25rem 0.5rem; + background-color: transparent; + font-size: 1rem; + font-family: inherit; +} + +.product-option-label-swatch { + width: 1.25rem; + height: 1.25rem; + margin: 0.25rem 0; +} + +.product-option-label-swatch img { + width: 100%; } /* diff --git a/examples/b2b/storefrontapi.generated.d.ts b/examples/b2b/storefrontapi.generated.d.ts index 1b245ca8a7..4d7f66da80 100644 --- a/examples/b2b/storefrontapi.generated.d.ts +++ b/examples/b2b/storefrontapi.generated.d.ts @@ -252,36 +252,6 @@ export type StoreRobotsQueryVariables = StorefrontAPI.Exact<{ export type StoreRobotsQuery = {shop: Pick}; -export type SitemapQueryVariables = StorefrontAPI.Exact<{ - urlLimits?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type SitemapQuery = { - products: { - nodes: Array< - Pick< - StorefrontAPI.Product, - 'updatedAt' | 'handle' | 'onlineStoreUrl' | 'title' - > & { - featuredImage?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - collections: { - nodes: Array< - Pick - >; - }; - pages: { - nodes: Array< - Pick - >; - }; -}; - export type FeaturedCollectionFragment = Pick< StorefrontAPI.Collection, 'id' | 'title' | 'handle' @@ -481,13 +451,6 @@ export type ProductItemFragment = Pick< minVariantPrice: Pick; maxVariantPrice: Pick; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; }; export type CollectionQueryVariables = StorefrontAPI.Exact<{ @@ -529,13 +492,6 @@ export type CollectionQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -621,13 +577,6 @@ export type CatalogQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -748,14 +697,71 @@ export type ProductVariantFragment = Pick< export type ProductFragment = Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { options: Array< Pick & { - optionValues: Array>; + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + quantityRule: Pick< + StorefrontAPI.QuantityRule, + 'maximum' | 'minimum' | 'increment' + >; + quantityPriceBreaks: { + nodes: Array< + Pick & { + price: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + } + >; + }; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; } >; - selectedVariant?: StorefrontAPI.Maybe< + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -790,43 +796,41 @@ export type ProductFragment = Pick< >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - quantityRule: Pick< - StorefrontAPI.QuantityRule, - 'maximum' | 'minimum' | 'increment' - >; - quantityPriceBreaks: { - nodes: Array< - Pick & { - price: Pick; - } - >; - }; - unitPrice?: StorefrontAPI.Maybe< - Pick + adjacentVariants: Array< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + quantityRule: Pick< + StorefrontAPI.QuantityRule, + 'maximum' | 'minimum' | 'increment' + >; + quantityPriceBreaks: { + nodes: Array< + Pick & { + price: Pick; + } >; - } - >; - }; + }; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; seo: Pick; }; @@ -844,14 +848,74 @@ export type ProductQuery = { product?: StorefrontAPI.Maybe< Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { options: Array< Pick & { - optionValues: Array>; + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + quantityRule: Pick< + StorefrontAPI.QuantityRule, + 'maximum' | 'minimum' | 'increment' + >; + quantityPriceBreaks: { + nodes: Array< + Pick< + StorefrontAPI.QuantityPriceBreak, + 'minimumQuantity' + > & { + price: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + } + >; + }; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; } >; - selectedVariant?: StorefrontAPI.Maybe< + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -886,99 +950,7 @@ export type ProductQuery = { >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - quantityRule: Pick< - StorefrontAPI.QuantityRule, - 'maximum' | 'minimum' | 'increment' - >; - quantityPriceBreaks: { - nodes: Array< - Pick & { - price: Pick; - } - >; - }; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - seo: Pick; - } - >; -}; - -export type ProductVariantsFragment = { - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - quantityRule: Pick< - StorefrontAPI.QuantityRule, - 'maximum' | 'minimum' | 'increment' - >; - quantityPriceBreaks: { - nodes: Array< - Pick & { - price: Pick; - } - >; - }; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; -}; - -export type ProductVariantsQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - buyer?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; - handle: StorefrontAPI.Scalars['String']['input']; -}>; - -export type ProductVariantsQuery = { - product?: StorefrontAPI.Maybe<{ - variants: { - nodes: Array< + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -1013,31 +985,30 @@ export type ProductVariantsQuery = { >; } >; - }; - }>; + seo: Pick; + } + >; }; export type SearchProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'handle' | 'id' | 'publishedAt' | 'title' | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; }; export type SearchPageFragment = {__typename: 'Page'} & Pick< @@ -1097,26 +1068,24 @@ export type RegularSearchQuery = { | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; } >; pageInfo: Pick< @@ -1154,16 +1123,14 @@ export type PredictiveProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + } + >; }; export type PredictiveQueryFragment = { @@ -1219,19 +1186,17 @@ export type PredictiveSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + } + >; } >; queries: Array< @@ -1256,10 +1221,6 @@ interface GeneratedQueryTypes { return: StoreRobotsQuery; variables: StoreRobotsQueryVariables; }; - '#graphql\n query Sitemap($urlLimits: Int, $language: LanguageCode)\n @inContext(language: $language) {\n products(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n title\n featuredImage {\n url\n altText\n }\n }\n }\n collections(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n pages(first: $urlLimits, query: "published_status:\'published\'") {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n }\n': { - return: SitemapQuery; - variables: SitemapQueryVariables; - }; '#graphql\n fragment FeaturedCollection on Collection {\n id\n title\n image {\n id\n url\n altText\n width\n height\n }\n handle\n }\n query FeaturedCollection($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n collections(first: 1, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...FeaturedCollection\n }\n }\n }\n': { return: FeaturedCollectionQuery; variables: FeaturedCollectionQueryVariables; @@ -1280,7 +1241,7 @@ interface GeneratedQueryTypes { return: BlogsQuery; variables: BlogsQueryVariables; }; - '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { + '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { return: CollectionQuery; variables: CollectionQueryVariables; }; @@ -1288,7 +1249,7 @@ interface GeneratedQueryTypes { return: StoreCollectionsQuery; variables: StoreCollectionsQueryVariables; }; - '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n': { + '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': { return: CatalogQuery; variables: CatalogQueryVariables; }; @@ -1304,19 +1265,15 @@ interface GeneratedQueryTypes { return: PoliciesQuery; variables: PoliciesQueryVariables; }; - '#graphql\n query Product(\n $country: CountryCode\n $buyer: BuyerInput\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language, buyer: $buyer) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n optionValues {\n name\n }\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n quantityRule {\n maximum\n minimum\n increment\n }\n quantityPriceBreaks(first: 5) {\n nodes {\n minimumQuantity\n price {\n amount\n currencyCode\n }\n }\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { + '#graphql\n query Product(\n $country: CountryCode\n $buyer: BuyerInput\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language, buyer: $buyer) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n encodedVariantExistence\n encodedVariantAvailability\n options {\n name\n optionValues {\n name\n firstSelectableVariant {\n ...ProductVariant\n }\n swatch {\n color\n image {\n previewImage {\n url\n }\n }\n }\n }\n }\n selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n adjacentVariants (selectedOptions: $selectedOptions) {\n ...ProductVariant\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n quantityRule {\n maximum\n minimum\n increment\n }\n quantityPriceBreaks(first: 5) {\n nodes {\n minimumQuantity\n price {\n amount\n currencyCode\n }\n }\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { return: ProductQuery; variables: ProductQueryVariables; }; - '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n quantityRule {\n maximum\n minimum\n increment\n }\n quantityPriceBreaks(first: 5) {\n nodes {\n minimumQuantity\n price {\n amount\n currencyCode\n }\n }\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $buyer: BuyerInput\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language, buyer: $buyer) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': { - return: ProductVariantsQuery; - variables: ProductVariantsQueryVariables; - }; - '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { + '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { return: RegularSearchQuery; variables: RegularSearchQueryVariables; }; - '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { + '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; }; diff --git a/examples/custom-cart-method/storefrontapi.generated.d.ts b/examples/custom-cart-method/storefrontapi.generated.d.ts index 6d4cee96d7..e96658f65f 100644 --- a/examples/custom-cart-method/storefrontapi.generated.d.ts +++ b/examples/custom-cart-method/storefrontapi.generated.d.ts @@ -254,36 +254,6 @@ export type StoreRobotsQueryVariables = StorefrontAPI.Exact<{ export type StoreRobotsQuery = {shop: Pick}; -export type SitemapQueryVariables = StorefrontAPI.Exact<{ - urlLimits?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type SitemapQuery = { - products: { - nodes: Array< - Pick< - StorefrontAPI.Product, - 'updatedAt' | 'handle' | 'onlineStoreUrl' | 'title' - > & { - featuredImage?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - collections: { - nodes: Array< - Pick - >; - }; - pages: { - nodes: Array< - Pick - >; - }; -}; - export type FeaturedCollectionFragment = Pick< StorefrontAPI.Collection, 'id' | 'title' | 'handle' @@ -483,13 +453,6 @@ export type ProductItemFragment = Pick< minVariantPrice: Pick; maxVariantPrice: Pick; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; }; export type CollectionQueryVariables = StorefrontAPI.Exact<{ @@ -531,13 +494,6 @@ export type CollectionQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -623,13 +579,6 @@ export type CatalogQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -739,10 +688,81 @@ export type ProductVariantFragment = Pick< export type ProductFragment = Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -766,32 +786,6 @@ export type ProductFragment = Pick< >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; seo: Pick; }; @@ -808,10 +802,57 @@ export type ProductQuery = { product?: StorefrontAPI.Maybe< Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -835,76 +876,7 @@ export type ProductQuery = { >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - seo: Pick; - } - >; -}; - -export type ProductVariantsFragment = { - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; -}; - -export type ProductVariantsQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; - handle: StorefrontAPI.Scalars['String']['input']; -}>; - -export type ProductVariantsQuery = { - product?: StorefrontAPI.Maybe<{ - variants: { - nodes: Array< + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -928,31 +900,30 @@ export type ProductVariantsQuery = { >; } >; - }; - }>; + seo: Pick; + } + >; }; export type SearchProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'handle' | 'id' | 'publishedAt' | 'title' | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; }; export type SearchPageFragment = {__typename: 'Page'} & Pick< @@ -1012,26 +983,24 @@ export type RegularSearchQuery = { | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; } >; pageInfo: Pick< @@ -1069,16 +1038,14 @@ export type PredictiveProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + } + >; }; export type PredictiveQueryFragment = { @@ -1134,19 +1101,17 @@ export type PredictiveSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + } + >; } >; queries: Array< @@ -1175,10 +1140,6 @@ interface GeneratedQueryTypes { return: StoreRobotsQuery; variables: StoreRobotsQueryVariables; }; - '#graphql\n query Sitemap($urlLimits: Int, $language: LanguageCode)\n @inContext(language: $language) {\n products(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n title\n featuredImage {\n url\n altText\n }\n }\n }\n collections(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n pages(first: $urlLimits, query: "published_status:\'published\'") {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n }\n': { - return: SitemapQuery; - variables: SitemapQueryVariables; - }; '#graphql\n fragment FeaturedCollection on Collection {\n id\n title\n image {\n id\n url\n altText\n width\n height\n }\n handle\n }\n query FeaturedCollection($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n collections(first: 1, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...FeaturedCollection\n }\n }\n }\n': { return: FeaturedCollectionQuery; variables: FeaturedCollectionQueryVariables; @@ -1199,7 +1160,7 @@ interface GeneratedQueryTypes { return: BlogsQuery; variables: BlogsQueryVariables; }; - '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { + '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { return: CollectionQuery; variables: CollectionQueryVariables; }; @@ -1207,7 +1168,7 @@ interface GeneratedQueryTypes { return: StoreCollectionsQuery; variables: StoreCollectionsQueryVariables; }; - '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n': { + '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': { return: CatalogQuery; variables: CatalogQueryVariables; }; @@ -1223,19 +1184,15 @@ interface GeneratedQueryTypes { return: PoliciesQuery; variables: PoliciesQueryVariables; }; - '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n values\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { + '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n encodedVariantExistence\n encodedVariantAvailability\n options {\n name\n optionValues {\n name\n firstSelectableVariant {\n ...ProductVariant\n }\n swatch {\n color\n image {\n previewImage {\n url\n }\n }\n }\n }\n }\n selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n adjacentVariants (selectedOptions: $selectedOptions) {\n ...ProductVariant\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { return: ProductQuery; variables: ProductQueryVariables; }; - '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': { - return: ProductVariantsQuery; - variables: ProductVariantsQueryVariables; - }; - '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { + '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { return: RegularSearchQuery; variables: RegularSearchQueryVariables; }; - '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { + '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; }; diff --git a/examples/infinite-scroll/app/routes/collections.$handle.tsx b/examples/infinite-scroll/app/routes/collections.$handle.tsx index 2a029fefb2..66186a8ba8 100644 --- a/examples/infinite-scroll/app/routes/collections.$handle.tsx +++ b/examples/infinite-scroll/app/routes/collections.$handle.tsx @@ -168,8 +168,7 @@ function ProductItem({ product: ProductItemFragment; loading?: 'eager' | 'lazy'; }) { - const variant = product.variants.nodes[0]; - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + const variantUrl = useVariantUrl(product.handle); return ( }; -export type SitemapQueryVariables = StorefrontAPI.Exact<{ - urlLimits?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type SitemapQuery = { - products: { - nodes: Array< - Pick< - StorefrontAPI.Product, - 'updatedAt' | 'handle' | 'onlineStoreUrl' | 'title' - > & { - featuredImage?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - collections: { - nodes: Array< - Pick - >; - }; - pages: { - nodes: Array< - Pick - >; - }; -}; - export type FeaturedCollectionFragment = Pick< StorefrontAPI.Collection, 'id' | 'title' | 'handle' @@ -1244,13 +1214,6 @@ export type ProductItemFragment = Pick< minVariantPrice: Pick; maxVariantPrice: Pick; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; }; export type CollectionQueryVariables = StorefrontAPI.Exact<{ @@ -1292,13 +1255,6 @@ export type CollectionQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -1384,13 +1340,6 @@ export type CatalogQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -1500,10 +1449,81 @@ export type ProductVariantFragment = Pick< export type ProductFragment = Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -1527,32 +1547,6 @@ export type ProductFragment = Pick< >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; seo: Pick; }; @@ -1569,10 +1563,57 @@ export type ProductQuery = { product?: StorefrontAPI.Maybe< Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -1596,76 +1637,7 @@ export type ProductQuery = { >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - seo: Pick; - } - >; -}; - -export type ProductVariantsFragment = { - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; -}; - -export type ProductVariantsQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; - handle: StorefrontAPI.Scalars['String']['input']; -}>; - -export type ProductVariantsQuery = { - product?: StorefrontAPI.Maybe<{ - variants: { - nodes: Array< + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -1689,31 +1661,30 @@ export type ProductVariantsQuery = { >; } >; - }; - }>; + seo: Pick; + } + >; }; export type SearchProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'handle' | 'id' | 'publishedAt' | 'title' | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; }; export type SearchPageFragment = {__typename: 'Page'} & Pick< @@ -1773,26 +1744,24 @@ export type RegularSearchQuery = { | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; } >; pageInfo: Pick< @@ -1830,16 +1799,14 @@ export type PredictiveProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + } + >; }; export type PredictiveQueryFragment = { @@ -1895,19 +1862,17 @@ export type PredictiveSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + } + >; } >; queries: Array< @@ -1932,10 +1897,6 @@ interface GeneratedQueryTypes { return: StoreRobotsQuery; variables: StoreRobotsQueryVariables; }; - '#graphql\n query Sitemap($urlLimits: Int, $language: LanguageCode)\n @inContext(language: $language) {\n products(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n title\n featuredImage {\n url\n altText\n }\n }\n }\n collections(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n pages(first: $urlLimits, query: "published_status:\'published\'") {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n }\n': { - return: SitemapQuery; - variables: SitemapQueryVariables; - }; '#graphql\n fragment FeaturedCollection on Collection {\n id\n title\n image {\n id\n url\n altText\n width\n height\n }\n handle\n }\n query FeaturedCollection($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n collections(first: 1, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...FeaturedCollection\n }\n }\n }\n': { return: FeaturedCollectionQuery; variables: FeaturedCollectionQueryVariables; @@ -1968,7 +1929,7 @@ interface GeneratedQueryTypes { return: BlogsQuery; variables: BlogsQueryVariables; }; - '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { + '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { return: CollectionQuery; variables: CollectionQueryVariables; }; @@ -1976,7 +1937,7 @@ interface GeneratedQueryTypes { return: StoreCollectionsQuery; variables: StoreCollectionsQueryVariables; }; - '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n': { + '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': { return: CatalogQuery; variables: CatalogQueryVariables; }; @@ -1992,19 +1953,15 @@ interface GeneratedQueryTypes { return: PoliciesQuery; variables: PoliciesQueryVariables; }; - '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n values\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { + '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n encodedVariantExistence\n encodedVariantAvailability\n options {\n name\n optionValues {\n name\n firstSelectableVariant {\n ...ProductVariant\n }\n swatch {\n color\n image {\n previewImage {\n url\n }\n }\n }\n }\n }\n selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n adjacentVariants (selectedOptions: $selectedOptions) {\n ...ProductVariant\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { return: ProductQuery; variables: ProductQueryVariables; }; - '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': { - return: ProductVariantsQuery; - variables: ProductVariantsQueryVariables; - }; - '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { + '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { return: RegularSearchQuery; variables: RegularSearchQueryVariables; }; - '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { + '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; }; diff --git a/examples/metaobjects/storefrontapi.generated.d.ts b/examples/metaobjects/storefrontapi.generated.d.ts index dc2d3e7c26..1347942362 100644 --- a/examples/metaobjects/storefrontapi.generated.d.ts +++ b/examples/metaobjects/storefrontapi.generated.d.ts @@ -295,36 +295,6 @@ export type StoreRobotsQueryVariables = StorefrontAPI.Exact<{ export type StoreRobotsQuery = {shop: Pick}; -export type SitemapQueryVariables = StorefrontAPI.Exact<{ - urlLimits?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type SitemapQuery = { - products: { - nodes: Array< - Pick< - StorefrontAPI.Product, - 'updatedAt' | 'handle' | 'onlineStoreUrl' | 'title' - > & { - featuredImage?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - collections: { - nodes: Array< - Pick - >; - }; - pages: { - nodes: Array< - Pick - >; - }; -}; - export type ArticleQueryVariables = StorefrontAPI.Exact<{ articleHandle: StorefrontAPI.Scalars['String']['input']; blogHandle: StorefrontAPI.Scalars['String']['input']; @@ -453,13 +423,6 @@ export type ProductItemFragment = Pick< minVariantPrice: Pick; maxVariantPrice: Pick; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; }; export type CollectionQueryVariables = StorefrontAPI.Exact<{ @@ -501,13 +464,6 @@ export type CollectionQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -593,13 +549,6 @@ export type CatalogQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -709,10 +658,81 @@ export type ProductVariantFragment = Pick< export type ProductFragment = Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -736,32 +756,6 @@ export type ProductFragment = Pick< >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; seo: Pick; }; @@ -778,10 +772,57 @@ export type ProductQuery = { product?: StorefrontAPI.Maybe< Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -805,76 +846,7 @@ export type ProductQuery = { >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - seo: Pick; - } - >; -}; - -export type ProductVariantsFragment = { - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; -}; - -export type ProductVariantsQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; - handle: StorefrontAPI.Scalars['String']['input']; -}>; - -export type ProductVariantsQuery = { - product?: StorefrontAPI.Maybe<{ - variants: { - nodes: Array< + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -898,31 +870,30 @@ export type ProductVariantsQuery = { >; } >; - }; - }>; + seo: Pick; + } + >; }; export type SearchProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'handle' | 'id' | 'publishedAt' | 'title' | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; }; export type SearchPageFragment = {__typename: 'Page'} & Pick< @@ -982,26 +953,24 @@ export type RegularSearchQuery = { | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; } >; pageInfo: Pick< @@ -1039,16 +1008,14 @@ export type PredictiveProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + } + >; }; export type PredictiveQueryFragment = { @@ -1104,19 +1071,17 @@ export type PredictiveSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + } + >; } >; queries: Array< @@ -1769,10 +1734,6 @@ interface GeneratedQueryTypes { return: StoreRobotsQuery; variables: StoreRobotsQueryVariables; }; - '#graphql\n query Sitemap($urlLimits: Int, $language: LanguageCode)\n @inContext(language: $language) {\n products(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n title\n featuredImage {\n url\n altText\n }\n }\n }\n collections(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n pages(first: $urlLimits, query: "published_status:\'published\'") {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n }\n': { - return: SitemapQuery; - variables: SitemapQueryVariables; - }; '#graphql\n query Article(\n $articleHandle: String!\n $blogHandle: String!\n $country: CountryCode\n $language: LanguageCode\n ) @inContext(language: $language, country: $country) {\n blog(handle: $blogHandle) {\n articleByHandle(handle: $articleHandle) {\n title\n contentHtml\n publishedAt\n author: authorV2 {\n name\n }\n image {\n id\n altText\n url\n width\n height\n }\n seo {\n description\n title\n }\n }\n }\n }\n': { return: ArticleQuery; variables: ArticleQueryVariables; @@ -1785,7 +1746,7 @@ interface GeneratedQueryTypes { return: BlogsQuery; variables: BlogsQueryVariables; }; - '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { + '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { return: CollectionQuery; variables: CollectionQueryVariables; }; @@ -1793,7 +1754,7 @@ interface GeneratedQueryTypes { return: StoreCollectionsQuery; variables: StoreCollectionsQueryVariables; }; - '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n': { + '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': { return: CatalogQuery; variables: CatalogQueryVariables; }; @@ -1809,19 +1770,15 @@ interface GeneratedQueryTypes { return: PoliciesQuery; variables: PoliciesQueryVariables; }; - '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n values\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { + '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n encodedVariantExistence\n encodedVariantAvailability\n options {\n name\n optionValues {\n name\n firstSelectableVariant {\n ...ProductVariant\n }\n swatch {\n color\n image {\n previewImage {\n url\n }\n }\n }\n }\n }\n selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n adjacentVariants (selectedOptions: $selectedOptions) {\n ...ProductVariant\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { return: ProductQuery; variables: ProductQueryVariables; }; - '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': { - return: ProductVariantsQuery; - variables: ProductVariantsQueryVariables; - }; - '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { + '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { return: RegularSearchQuery; variables: RegularSearchQueryVariables; }; - '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { + '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; }; diff --git a/examples/multipass/storefrontapi.generated.d.ts b/examples/multipass/storefrontapi.generated.d.ts index 90e3c820e1..c661c3ebff 100644 --- a/examples/multipass/storefrontapi.generated.d.ts +++ b/examples/multipass/storefrontapi.generated.d.ts @@ -295,36 +295,6 @@ export type StoreRobotsQueryVariables = StorefrontAPI.Exact<{ export type StoreRobotsQuery = {shop: Pick}; -export type SitemapQueryVariables = StorefrontAPI.Exact<{ - urlLimits?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type SitemapQuery = { - products: { - nodes: Array< - Pick< - StorefrontAPI.Product, - 'updatedAt' | 'handle' | 'onlineStoreUrl' | 'title' - > & { - featuredImage?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - collections: { - nodes: Array< - Pick - >; - }; - pages: { - nodes: Array< - Pick - >; - }; -}; - export type FeaturedCollectionFragment = Pick< StorefrontAPI.Collection, 'id' | 'title' | 'handle' @@ -1257,13 +1227,6 @@ export type ProductItemFragment = Pick< minVariantPrice: Pick; maxVariantPrice: Pick; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; }; export type CollectionQueryVariables = StorefrontAPI.Exact<{ @@ -1305,13 +1268,6 @@ export type CollectionQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -1397,13 +1353,6 @@ export type CatalogQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -1513,10 +1462,81 @@ export type ProductVariantFragment = Pick< export type ProductFragment = Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -1540,32 +1560,6 @@ export type ProductFragment = Pick< >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; seo: Pick; }; @@ -1582,10 +1576,57 @@ export type ProductQuery = { product?: StorefrontAPI.Maybe< Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { - options: Array>; - selectedVariant?: StorefrontAPI.Maybe< + options: Array< + Pick & { + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; + } + >; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -1609,76 +1650,7 @@ export type ProductQuery = { >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - seo: Pick; - } - >; -}; - -export type ProductVariantsFragment = { - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; -}; - -export type ProductVariantsQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; - handle: StorefrontAPI.Scalars['String']['input']; -}>; - -export type ProductVariantsQuery = { - product?: StorefrontAPI.Maybe<{ - variants: { - nodes: Array< + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -1702,31 +1674,30 @@ export type ProductVariantsQuery = { >; } >; - }; - }>; + seo: Pick; + } + >; }; export type SearchProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'handle' | 'id' | 'publishedAt' | 'title' | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; }; export type SearchPageFragment = {__typename: 'Page'} & Pick< @@ -1786,26 +1757,24 @@ export type RegularSearchQuery = { | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; } >; pageInfo: Pick< @@ -1843,16 +1812,14 @@ export type PredictiveProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + } + >; }; export type PredictiveQueryFragment = { @@ -1908,19 +1875,17 @@ export type PredictiveSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + } + >; } >; queries: Array< @@ -1945,10 +1910,6 @@ interface GeneratedQueryTypes { return: StoreRobotsQuery; variables: StoreRobotsQueryVariables; }; - '#graphql\n query Sitemap($urlLimits: Int, $language: LanguageCode)\n @inContext(language: $language) {\n products(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n title\n featuredImage {\n url\n altText\n }\n }\n }\n collections(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n pages(first: $urlLimits, query: "published_status:\'published\'") {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n }\n': { - return: SitemapQuery; - variables: SitemapQueryVariables; - }; '#graphql\n fragment FeaturedCollection on Collection {\n id\n title\n image {\n id\n url\n altText\n width\n height\n }\n handle\n }\n query FeaturedCollection($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n collections(first: 1, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...FeaturedCollection\n }\n }\n }\n': { return: FeaturedCollectionQuery; variables: FeaturedCollectionQueryVariables; @@ -1985,7 +1946,7 @@ interface GeneratedQueryTypes { return: BlogsQuery; variables: BlogsQueryVariables; }; - '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { + '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { return: CollectionQuery; variables: CollectionQueryVariables; }; @@ -1993,7 +1954,7 @@ interface GeneratedQueryTypes { return: StoreCollectionsQuery; variables: StoreCollectionsQueryVariables; }; - '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n': { + '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': { return: CatalogQuery; variables: CatalogQueryVariables; }; @@ -2009,19 +1970,15 @@ interface GeneratedQueryTypes { return: PoliciesQuery; variables: PoliciesQueryVariables; }; - '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n values\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { + '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n encodedVariantExistence\n encodedVariantAvailability\n options {\n name\n optionValues {\n name\n firstSelectableVariant {\n ...ProductVariant\n }\n swatch {\n color\n image {\n previewImage {\n url\n }\n }\n }\n }\n }\n selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n adjacentVariants (selectedOptions: $selectedOptions) {\n ...ProductVariant\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { return: ProductQuery; variables: ProductQueryVariables; }; - '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': { - return: ProductVariantsQuery; - variables: ProductVariantsQueryVariables; - }; - '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { + '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { return: RegularSearchQuery; variables: RegularSearchQueryVariables; }; - '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { + '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; }; diff --git a/examples/partytown/app/utils.ts b/examples/partytown/app/utils.ts index ffea0a7306..e74840a8b1 100644 --- a/examples/partytown/app/utils.ts +++ b/examples/partytown/app/utils.ts @@ -4,7 +4,7 @@ import {useMemo} from 'react'; export function useVariantUrl( handle: string, - selectedOptions: SelectedOption[], + selectedOptions?: SelectedOption[], ) { const {pathname} = useLocation(); @@ -27,7 +27,7 @@ export function getVariantUrl({ handle: string; pathname: string; searchParams: URLSearchParams; - selectedOptions: SelectedOption[]; + selectedOptions?: SelectedOption[]; }) { const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); const isLocalePathname = match && match.length > 0; @@ -36,7 +36,7 @@ export function getVariantUrl({ ? `${match![0]}products/${handle}` : `/products/${handle}`; - selectedOptions.forEach((option) => { + selectedOptions?.forEach((option) => { searchParams.set(option.name, option.value); }); diff --git a/examples/subscriptions/storefrontapi.generated.d.ts b/examples/subscriptions/storefrontapi.generated.d.ts index c989df3806..2e074038b6 100644 --- a/examples/subscriptions/storefrontapi.generated.d.ts +++ b/examples/subscriptions/storefrontapi.generated.d.ts @@ -236,36 +236,6 @@ export type StoreRobotsQueryVariables = StorefrontAPI.Exact<{ export type StoreRobotsQuery = {shop: Pick}; -export type SitemapQueryVariables = StorefrontAPI.Exact<{ - urlLimits?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type SitemapQuery = { - products: { - nodes: Array< - Pick< - StorefrontAPI.Product, - 'updatedAt' | 'handle' | 'onlineStoreUrl' | 'title' - > & { - featuredImage?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - collections: { - nodes: Array< - Pick - >; - }; - pages: { - nodes: Array< - Pick - >; - }; -}; - export type ArticleQueryVariables = StorefrontAPI.Exact<{ articleHandle: StorefrontAPI.Scalars['String']['input']; blogHandle: StorefrontAPI.Scalars['String']['input']; @@ -394,13 +364,6 @@ export type ProductItemFragment = Pick< minVariantPrice: Pick; maxVariantPrice: Pick; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; }; export type CollectionQueryVariables = StorefrontAPI.Exact<{ @@ -442,13 +405,6 @@ export type CollectionQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -534,13 +490,6 @@ export type CatalogQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -1040,23 +989,21 @@ export type SearchProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'handle' | 'id' | 'publishedAt' | 'title' | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; }; export type SearchPageFragment = {__typename: 'Page'} & Pick< @@ -1116,26 +1063,24 @@ export type RegularSearchQuery = { | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; } >; pageInfo: Pick< @@ -1173,16 +1118,14 @@ export type PredictiveProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + } + >; }; export type PredictiveQueryFragment = { @@ -1238,19 +1181,17 @@ export type PredictiveSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + } + >; } >; queries: Array< @@ -1275,10 +1216,6 @@ interface GeneratedQueryTypes { return: StoreRobotsQuery; variables: StoreRobotsQueryVariables; }; - '#graphql\n query Sitemap($urlLimits: Int, $language: LanguageCode)\n @inContext(language: $language) {\n products(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n title\n featuredImage {\n url\n altText\n }\n }\n }\n collections(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n pages(first: $urlLimits, query: "published_status:\'published\'") {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n }\n': { - return: SitemapQuery; - variables: SitemapQueryVariables; - }; '#graphql\n query Article(\n $articleHandle: String!\n $blogHandle: String!\n $country: CountryCode\n $language: LanguageCode\n ) @inContext(language: $language, country: $country) {\n blog(handle: $blogHandle) {\n articleByHandle(handle: $articleHandle) {\n title\n contentHtml\n publishedAt\n author: authorV2 {\n name\n }\n image {\n id\n altText\n url\n width\n height\n }\n seo {\n description\n title\n }\n }\n }\n }\n': { return: ArticleQuery; variables: ArticleQueryVariables; @@ -1291,7 +1228,7 @@ interface GeneratedQueryTypes { return: BlogsQuery; variables: BlogsQueryVariables; }; - '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { + '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { return: CollectionQuery; variables: CollectionQueryVariables; }; @@ -1299,7 +1236,7 @@ interface GeneratedQueryTypes { return: StoreCollectionsQuery; variables: StoreCollectionsQueryVariables; }; - '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n': { + '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': { return: CatalogQuery; variables: CatalogQueryVariables; }; @@ -1323,11 +1260,11 @@ interface GeneratedQueryTypes { return: ProductVariantsQuery; variables: ProductVariantsQueryVariables; }; - '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { + '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { return: RegularSearchQuery; variables: RegularSearchQueryVariables; }; - '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { + '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; }; diff --git a/packages/hydrogen-react/.eslintrc.cjs b/packages/hydrogen-react/.eslintrc.cjs index a40726e04f..a3a13696a3 100644 --- a/packages/hydrogen-react/.eslintrc.cjs +++ b/packages/hydrogen-react/.eslintrc.cjs @@ -12,6 +12,7 @@ module.exports = { plugins: ['eslint-plugin-tsdoc'], ignorePatterns: [ '**/storefront-api-types.d.ts', + '**/customer-account-api-types.d.ts', '**/codegen.ts', '**/dist/**', '**/coverage/**', diff --git a/packages/hydrogen-react/docs/generated/generated_docs_data.json b/packages/hydrogen-react/docs/generated/generated_docs_data.json index 4eb0a519ac..c0cc49f911 100644 --- a/packages/hydrogen-react/docs/generated/generated_docs_data.json +++ b/packages/hydrogen-react/docs/generated/generated_docs_data.json @@ -6465,6 +6465,125 @@ } ] }, + { + "name": "getProductOptions", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "mapSelectedProductOptionToObject", + "type": "gear", + "url": "/api/hydrogen-react/utilities/mapselectedproductoptiontoobject" + }, + { + "name": "getAdjacentAndFirstAvailableVariants", + "type": "gear", + "url": "/api/hydrogen-react/utilities/getadjacentandfirstavailablevariants" + }, + { + "name": "useSelectedOptionInUrlParam", + "type": "gear", + "url": "/api/hydrogen-react/utilities/useselectedoptioninurlparam" + } + ], + "description": "Returns a product options array with its relevant information about the variant. This function supports combined listing products and products with 2000 variants limit.", + "type": "utility", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "JavaScript", + "code": "import React from 'react';\nimport {getProductOptions} from '@shopify/hydrogen-react';\n\n// Make sure you are querying for the following fields:\n// - product.handle\n// - product.encodedVariantExistence\n// - product.encodedVariantAvailability\n// - product.options.name\n// - product.options.optionValues.name\n// - product.options.optionValues.firstSelectableVariant\n// - product.selectedOrFirstAvailableVariant\n// - product.adjacentVariants\n//\n// For any fields that are ProductVariant type, make sure to query for:\n// - variant.product.handle\n// - variant.selectedOptions.name\n// - variant.selectedOptions.value\n\nexport default function ProductForm() {\n const product = {\n /* Result from querying the SFAPI for a product */\n };\n\n const productOptions = getProductOptions(product);\n\n return (\n <>\n {productOptions.map((option) => (\n <div key={option.name}>\n <h5>{option.name}</h5>\n <div>\n {option.optionValues.map((value) => {\n const {\n name,\n handle,\n variantUriQuery,\n selected,\n available,\n exists,\n isDifferentProduct,\n swatch,\n } = value;\n\n if (isDifferentProduct) {\n // SEO - When the variant is a\n // combined listing child product\n // that leads to a different url,\n // we need to render it\n // as an anchor tag\n return (\n <a\n key={option.name + name}\n href={`/products/${handle}?${variantUriQuery}`}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </a>\n );\n } else {\n // SEO - When the variant is an\n // update to the search param,\n // render it as a button with\n // javascript navigating to\n // the variant so that SEO bots\n // do not index these as\n // duplicated links\n return (\n <button\n type=\"button\"\n key={option.name + name}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n disabled={!exists}\n onClick={() => {\n if (!selected) {\n // Navigate to `?${variantUriQuery}`\n }\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </button>\n );\n }\n })}\n </div>\n <br />\n </div>\n ))}\n </>\n );\n}\n\nfunction ProductOptionSwatch({swatch, name}) {\n const image = swatch?.image?.previewImage?.url;\n const color = swatch?.color;\n\n if (!image && !color) return name;\n\n return (\n <div\n aria-label={name}\n className=\"product-option-label-swatch\"\n style={{\n backgroundColor: color || 'transparent',\n }}\n >\n {!!image && <img src={image} alt={name} />}\n </div>\n );\n}\n", + "language": "jsx" + }, + { + "title": "TypeScript", + "code": "import React from 'react';\nimport {\n getProductOptions,\n type MappedProductOptions,\n} from '@shopify/hydrogen-react';\nimport type {\n ProductOptionValueSwatch,\n Maybe,\n} from '@shopify/hydrogen-react/storefront-api-types';\n\n// Make sure you are querying for the following fields:\n// - product.handle\n// - product.encodedVariantExistence\n// - product.encodedVariantAvailability\n// - product.options.name\n// - product.options.optionValues.name\n// - product.options.optionValues.firstSelectableVariant\n// - product.selectedOrFirstAvailableVariant\n// - product.adjacentVariants\n//\n// For any fields that are ProductVariant type, make sure to query for:\n// - variant.product.handle\n// - variant.selectedOptions.name\n// - variant.selectedOptions.value\n\nexport default function ProductForm() {\n const product = {\n /* Result from querying the SFAPI for a product */\n };\n\n const productOptions: MappedProductOptions[] = getProductOptions(product);\n\n return (\n <>\n {productOptions.map((option) => (\n <div key={option.name}>\n <h5>{option.name}</h5>\n <div>\n {option.optionValues.map((value) => {\n const {\n name,\n handle,\n variantUriQuery,\n selected,\n available,\n exists,\n isDifferentProduct,\n swatch,\n } = value;\n\n if (isDifferentProduct) {\n // SEO - When the variant is a\n // combined listing child product\n // that leads to a different url,\n // we need to render it\n // as an anchor tag\n return (\n <a\n key={option.name + name}\n href={`/products/${handle}?${variantUriQuery}`}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </a>\n );\n } else {\n // SEO - When the variant is an\n // update to the search param,\n // render it as a button with\n // javascript navigating to\n // the variant so that SEO bots\n // do not index these as\n // duplicated links\n return (\n <button\n type=\"button\"\n key={option.name + name}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n disabled={!exists}\n onClick={() => {\n if (!selected) {\n // Navigate to `?${variantUriQuery}`\n }\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </button>\n );\n }\n })}\n </div>\n <br />\n </div>\n ))}\n </>\n );\n}\n\nfunction ProductOptionSwatch({\n swatch,\n name,\n}: {\n swatch?: Maybe<ProductOptionValueSwatch> | undefined;\n name: string;\n}) {\n const image = swatch?.image?.previewImage?.url;\n const color = swatch?.color;\n\n if (!image && !color) return name;\n\n return (\n <div\n aria-label={name}\n style={{\n backgroundColor: color || 'transparent',\n }}\n >\n {!!image && <img src={image} alt={name} />}\n </div>\n );\n}\n", + "language": "tsx" + } + ], + "title": "Example code" + } + }, + "definitions": [] + }, + { + "name": "getAdjacentAndFirstAvailableVariants", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "getProductOptions", + "type": "gear", + "url": "/api/hydrogen-react/utilities/getproductoptions" + }, + { + "name": "mapSelectedProductOptionToObject", + "type": "gear", + "url": "/api/hydrogen-react/utilities/mapselectedproductoptiontoobject" + }, + { + "name": "useSelectedOptionInUrlParam", + "type": "gear", + "url": "/api/hydrogen-react/utilities/useselectedoptioninurlparam" + } + ], + "description": "Finds all the variants provided by `adjacentVariants`, `options.optionValues.firstAvailableVariant`, and `selectedOrFirstAvailableVariant` and return them in a single array. This function will remove any duplicated variants found.", + "type": "utility", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "getAdjacentAndFirstAvailableVariants example", + "code": "import {getAdjacentAndFirstAvailableVariants} from '@shopify/hydrogen-react';\n\n// Make sure you are querying for the following fields:\n// - product.options.optionValues.firstSelectableVariant\n// - product.selectedOrFirstAvailableVariant\n// - product.adjacentVariants\n//\n// For any fields that are ProductVariant type, make sure to query for:\n// - variant.selectedOptions.name\n// - variant.selectedOptions.value\n\nconst product = {\n /* Result from querying the SFAPI for a product */\n};\n\n// Returns a list of unique variants found in product query\nconst variants = getAdjacentAndFirstAvailableVariants(product);\n", + "language": "js" + } + ], + "title": "getAdjacentAndFirstAvailableVariants.js" + } + }, + "definitions": [] + }, + { + "name": "mapSelectedProductOptionToObject", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "getProductOptions", + "type": "gear", + "url": "/api/hydrogen-react/utilities/getproductoptions" + }, + { + "name": "getAdjacentAndFirstAvailableVariants", + "type": "gear", + "url": "/api/hydrogen-react/utilities/getadjacentandfirstavailablevariants" + }, + { + "name": "useSelectedOptionInUrlParam", + "type": "gear", + "url": "/api/hydrogen-react/utilities/useselectedoptioninurlparam" + } + ], + "description": "Converts the product selected option into an `Object` format for building URL query params", + "type": "utility", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "mapSelectedProductOptionToObject example", + "code": "import {mapSelectedProductOptionToObject} from '@shopify/hydrogen-react';\n\nconst selectedOption = [\n {\n name: 'Color',\n value: 'Red',\n },\n {\n name: 'Size',\n value: 'Medium',\n },\n];\n\nconst optionsObject = mapSelectedProductOptionToObject(selectedOption);\n\n// Output of optionsObject\n// {\n// Color: 'Red',\n// Size: 'Medium',\n// }\n\nconst searchParams = new URLSearchParams(optionsObject);\nsearchParams.toString(); // '?Color=Red&Size=Medium'\n", + "language": "js" + } + ], + "title": "mapSelectedProductOptionToObject.js" + } + }, + "definitions": [] + }, { "name": "useLoadScript", "category": "hooks", @@ -7772,6 +7891,39 @@ } ] }, + { + "name": "useSelectedOptionInUrlParam", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "getProductOptions", + "type": "gear", + "url": "/api/hydrogen-react/utilities/getproductoptions" + }, + { + "name": "getAdjacentAndFirstAvailableVariants", + "type": "gear", + "url": "/api/hydrogen-react/utilities/getadjacentandfirstavailablevariants" + } + ], + "description": "Sets the url params to the selected option.", + "type": "utility", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "useSelectedOptionInUrlParam example", + "code": "import {useSelectedOptionInUrlParam} from '@shopify/hydrogen-react';\n\nconst selectedOption = [\n {\n name: 'Color',\n value: 'Red',\n },\n {\n name: 'Size',\n value: 'Medium',\n },\n];\n\nuseSelectedOptionInUrlParam(selectedOption);\n\n// URL will be updated to <original product url>?Color=Red&Size=Medium\n", + "language": "js" + } + ], + "title": "Example" + } + }, + "definitions": [] + }, { "name": "useShop", "category": "hooks", diff --git a/packages/hydrogen-react/src/getProductOptions.doc.ts b/packages/hydrogen-react/src/getProductOptions.doc.ts new file mode 100644 index 0000000000..0bed5ef6dc --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.doc.ts @@ -0,0 +1,47 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'getProductOptions', + category: 'utilities', + isVisualComponent: false, + related: [ + { + name: 'mapSelectedProductOptionToObject', + type: 'gear', + url: '/api/hydrogen-react/utilities/mapselectedproductoptiontoobject', + }, + { + name: 'getAdjacentAndFirstAvailableVariants', + type: 'gear', + url: '/api/hydrogen-react/utilities/getadjacentandfirstavailablevariants', + }, + { + name: 'useSelectedOptionInUrlParam', + type: 'gear', + url: '/api/hydrogen-react/utilities/useselectedoptioninurlparam', + }, + ], + description: `Returns a product options array with its relevant information about the variant. This function supports combined listing products and products with 2000 variants limit.`, + type: 'utility', + defaultExample: { + description: 'I am the default example', + codeblock: { + tabs: [ + { + title: 'JavaScript', + code: './getProductOptions.example.jsx', + language: 'jsx', + }, + { + title: 'TypeScript', + code: './getProductOptions.example.tsx', + language: 'tsx', + }, + ], + title: 'Example code', + }, + }, + definitions: [], +}; + +export default data; diff --git a/packages/hydrogen-react/src/getProductOptions.example.jsx b/packages/hydrogen-react/src/getProductOptions.example.jsx new file mode 100644 index 0000000000..742374d477 --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.example.jsx @@ -0,0 +1,119 @@ +import React from 'react'; +import {getProductOptions} from '@shopify/hydrogen-react'; + +// Make sure you are querying for the following fields: +// - product.handle +// - product.encodedVariantExistence +// - product.encodedVariantAvailability +// - product.options.name +// - product.options.optionValues.name +// - product.options.optionValues.firstSelectableVariant +// - product.selectedOrFirstAvailableVariant +// - product.adjacentVariants +// +// For any fields that are ProductVariant type, make sure to query for: +// - variant.product.handle +// - variant.selectedOptions.name +// - variant.selectedOptions.value + +export default function ProductForm() { + const product = { + /* Result from querying the SFAPI for a product */ + }; + + const productOptions = getProductOptions(product); + + return ( + <> + {productOptions.map((option) => ( +
+
{option.name}
+
+ {option.optionValues.map((value) => { + const { + name, + handle, + variantUriQuery, + selected, + available, + exists, + isDifferentProduct, + swatch, + } = value; + + if (isDifferentProduct) { + // SEO - When the variant is a + // combined listing child product + // that leads to a different url, + // we need to render it + // as an anchor tag + return ( + + + + ); + } else { + // SEO - When the variant is an + // update to the search param, + // render it as a button with + // javascript navigating to + // the variant so that SEO bots + // do not index these as + // duplicated links + return ( + + ); + } + })} +
+
+
+ ))} + + ); +} + +function ProductOptionSwatch({swatch, name}) { + const image = swatch?.image?.previewImage?.url; + const color = swatch?.color; + + if (!image && !color) return name; + + return ( +
+ {!!image && {name}} +
+ ); +} diff --git a/packages/hydrogen-react/src/getProductOptions.example.tsx b/packages/hydrogen-react/src/getProductOptions.example.tsx new file mode 100644 index 0000000000..59fb39652b --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.example.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { + getProductOptions, + type MappedProductOptions, +} from '@shopify/hydrogen-react'; +import type { + ProductOptionValueSwatch, + Maybe, +} from '@shopify/hydrogen-react/storefront-api-types'; + +// Make sure you are querying for the following fields: +// - product.handle +// - product.encodedVariantExistence +// - product.encodedVariantAvailability +// - product.options.name +// - product.options.optionValues.name +// - product.options.optionValues.firstSelectableVariant +// - product.selectedOrFirstAvailableVariant +// - product.adjacentVariants +// +// For any fields that are ProductVariant type, make sure to query for: +// - variant.product.handle +// - variant.selectedOptions.name +// - variant.selectedOptions.value + +export default function ProductForm() { + const product = { + /* Result from querying the SFAPI for a product */ + }; + + const productOptions: MappedProductOptions[] = getProductOptions(product); + + return ( + <> + {productOptions.map((option) => ( +
+
{option.name}
+
+ {option.optionValues.map((value) => { + const { + name, + handle, + variantUriQuery, + selected, + available, + exists, + isDifferentProduct, + swatch, + } = value; + + if (isDifferentProduct) { + // SEO - When the variant is a + // combined listing child product + // that leads to a different url, + // we need to render it + // as an anchor tag + return ( + + + + ); + } else { + // SEO - When the variant is an + // update to the search param, + // render it as a button with + // javascript navigating to + // the variant so that SEO bots + // do not index these as + // duplicated links + return ( + + ); + } + })} +
+
+
+ ))} + + ); +} + +function ProductOptionSwatch({ + swatch, + name, +}: { + swatch?: Maybe | undefined; + name: string; +}) { + const image = swatch?.image?.previewImage?.url; + const color = swatch?.color; + + if (!image && !color) return name; + + return ( +
+ {!!image && {name}} +
+ ); +} diff --git a/packages/hydrogen-react/src/getProductOptions.getAdjacentAndFirstAvailableVariants.doc.ts b/packages/hydrogen-react/src/getProductOptions.getAdjacentAndFirstAvailableVariants.doc.ts new file mode 100644 index 0000000000..1713583eb1 --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.getAdjacentAndFirstAvailableVariants.doc.ts @@ -0,0 +1,43 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'getAdjacentAndFirstAvailableVariants', + category: 'utilities', + isVisualComponent: false, + related: [ + { + name: 'getProductOptions', + type: 'gear', + url: '/api/hydrogen-react/utilities/getproductoptions', + }, + { + name: 'mapSelectedProductOptionToObject', + type: 'gear', + url: '/api/hydrogen-react/utilities/mapselectedproductoptiontoobject', + }, + { + name: 'useSelectedOptionInUrlParam', + type: 'gear', + url: '/api/hydrogen-react/utilities/useselectedoptioninurlparam', + }, + ], + description: + 'Finds all the variants provided by `adjacentVariants`, `options.optionValues.firstAvailableVariant`, and `selectedOrFirstAvailableVariant` and return them in a single array. This function will remove any duplicated variants found.', + type: 'utility', + defaultExample: { + description: 'I am the default example', + codeblock: { + tabs: [ + { + title: 'getAdjacentAndFirstAvailableVariants example', + code: './getProductOptions.getAdjacentAndFirstAvailableVariants.example.js', + language: 'js', + }, + ], + title: 'getAdjacentAndFirstAvailableVariants.js', + }, + }, + definitions: [], +}; + +export default data; diff --git a/packages/hydrogen-react/src/getProductOptions.getAdjacentAndFirstAvailableVariants.example.js b/packages/hydrogen-react/src/getProductOptions.getAdjacentAndFirstAvailableVariants.example.js new file mode 100644 index 0000000000..258e1159c8 --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.getAdjacentAndFirstAvailableVariants.example.js @@ -0,0 +1,17 @@ +import {getAdjacentAndFirstAvailableVariants} from '@shopify/hydrogen-react'; + +// Make sure you are querying for the following fields: +// - product.options.optionValues.firstSelectableVariant +// - product.selectedOrFirstAvailableVariant +// - product.adjacentVariants +// +// For any fields that are ProductVariant type, make sure to query for: +// - variant.selectedOptions.name +// - variant.selectedOptions.value + +const product = { + /* Result from querying the SFAPI for a product */ +}; + +// Returns a list of unique variants found in product query +const variants = getAdjacentAndFirstAvailableVariants(product); diff --git a/packages/hydrogen-react/src/getProductOptions.mapSelectedProductOptionToObject.doc.ts b/packages/hydrogen-react/src/getProductOptions.mapSelectedProductOptionToObject.doc.ts new file mode 100644 index 0000000000..a904a0d93a --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.mapSelectedProductOptionToObject.doc.ts @@ -0,0 +1,43 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'mapSelectedProductOptionToObject', + category: 'utilities', + isVisualComponent: false, + related: [ + { + name: 'getProductOptions', + type: 'gear', + url: '/api/hydrogen-react/utilities/getproductoptions', + }, + { + name: 'getAdjacentAndFirstAvailableVariants', + type: 'gear', + url: '/api/hydrogen-react/utilities/getadjacentandfirstavailablevariants', + }, + { + name: 'useSelectedOptionInUrlParam', + type: 'gear', + url: '/api/hydrogen-react/utilities/useselectedoptioninurlparam', + }, + ], + description: + 'Converts the product selected option into an `Object` format for building URL query params', + type: 'utility', + defaultExample: { + description: 'I am the default example', + codeblock: { + tabs: [ + { + title: 'mapSelectedProductOptionToObject example', + code: './getProductOptions.mapSelectedProductOptionToObject.example.js', + language: 'js', + }, + ], + title: 'mapSelectedProductOptionToObject.js', + }, + }, + definitions: [], +}; + +export default data; diff --git a/packages/hydrogen-react/src/getProductOptions.mapSelectedProductOptionToObject.example.js b/packages/hydrogen-react/src/getProductOptions.mapSelectedProductOptionToObject.example.js new file mode 100644 index 0000000000..387faae0f1 --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.mapSelectedProductOptionToObject.example.js @@ -0,0 +1,23 @@ +import {mapSelectedProductOptionToObject} from '@shopify/hydrogen-react'; + +const selectedOption = [ + { + name: 'Color', + value: 'Red', + }, + { + name: 'Size', + value: 'Medium', + }, +]; + +const optionsObject = mapSelectedProductOptionToObject(selectedOption); + +// Output of optionsObject +// { +// Color: 'Red', +// Size: 'Medium', +// } + +const searchParams = new URLSearchParams(optionsObject); +searchParams.toString(); // '?Color=Red&Size=Medium' diff --git a/packages/hydrogen-react/src/getProductOptions.test.ts b/packages/hydrogen-react/src/getProductOptions.test.ts new file mode 100644 index 0000000000..7c6e52f80a --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.test.ts @@ -0,0 +1,1222 @@ +import {afterEach, describe, expect, it, vi} from 'vitest'; +import { + checkProductParam, + getAdjacentAndFirstAvailableVariants, + getProductOptions, + mapSelectedProductOptionToObject, + RecursivePartial, +} from './getProductOptions.js'; +import {Product} from './storefront-api-types.js'; + +const ERROR_MSG_START = '[h2:error:getProductOptions] product.'; +const ERROR_MSG_END = + ' is missing. Make sure you query for this field from the Storefront API.'; + +describe('getProductOptions', () => { + it('returns the options array with variant information', () => { + const options = getProductOptions( + PRODUCT as unknown as RecursivePartial, + ); + expect(options).toMatchInlineSnapshot(` + [ + { + "name": "Size", + "optionValues": [ + { + "available": true, + "exists": true, + "firstSelectableVariant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290613816", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "154cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "handle": "mail-it-in-freestyle-snowboard", + "isDifferentProduct": false, + "name": "154cm", + "selected": true, + "swatch": null, + "variant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290613816", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "154cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "variantUriQuery": "Size=154cm&Color=Sea+Green+%2F+Desert", + }, + { + "available": true, + "exists": true, + "firstSelectableVariant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290646584", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "158cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "handle": "mail-it-in-freestyle-snowboard", + "isDifferentProduct": false, + "name": "158cm", + "selected": false, + "swatch": null, + "variant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290646584", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "158cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "variantUriQuery": "Size=158cm&Color=Sea+Green+%2F+Desert", + }, + { + "available": true, + "exists": true, + "firstSelectableVariant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290679352", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "160cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "handle": "mail-it-in-freestyle-snowboard", + "isDifferentProduct": false, + "name": "160cm", + "selected": false, + "swatch": null, + "variant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290679352", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "160cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "variantUriQuery": "Size=160cm&Color=Sea+Green+%2F+Desert", + }, + ], + }, + { + "name": "Color", + "optionValues": [ + { + "available": true, + "exists": true, + "firstSelectableVariant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290613816", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "154cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "handle": "mail-it-in-freestyle-snowboard", + "isDifferentProduct": false, + "name": "Sea Green / Desert", + "selected": true, + "swatch": null, + "variant": { + "availableForSale": true, + "id": "gid://shopify/ProductVariant/41007290613816", + "product": { + "handle": "mail-it-in-freestyle-snowboard", + }, + "selectedOptions": [ + { + "name": "Size", + "value": "154cm", + }, + { + "name": "Color", + "value": "Sea Green / Desert", + }, + ], + }, + "variantUriQuery": "Size=154cm&Color=Sea+Green+%2F+Desert", + }, + ], + }, + ] + `); + }); +}); + +describe('getAdjacentAndFirstAvailableVariants', () => { + it('returns the correct number of variants found', () => { + const variants = getAdjacentAndFirstAvailableVariants({ + options: [ + { + optionValues: [ + { + firstSelectableVariant: { + id: 'snowboard', + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + }, + ], + }, + ], + selectedOrFirstAvailableVariant: { + id: 'snowboard', + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + adjacentVariants: [ + { + id: 'snowboard-2', + selectedOptions: [ + { + name: 'Color', + value: 'Ember', + }, + ], + }, + ], + } as unknown as RecursivePartial); + + expect(variants.length).toBe(2); + expect(variants).toMatchInlineSnapshot(` + [ + { + "id": "snowboard", + "selectedOptions": [ + { + "name": "Color", + "value": "Turquoise", + }, + ], + }, + { + "id": "snowboard-2", + "selectedOptions": [ + { + "name": "Color", + "value": "Ember", + }, + ], + }, + ] + `); + }); +}); + +describe('mapSelectedProductOptionToObject', () => { + it('returns the selected option in an object form', () => { + const option = mapSelectedProductOptionToObject([ + { + name: 'Color', + value: 'Turquoise', + }, + { + name: 'Size', + value: 'Small', + }, + ]); + + expect(option).toMatchInlineSnapshot(` + { + "Color": "Turquoise", + "Size": "Small", + } + `); + }); +}); + +describe('checkProductParam', () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('logs nothing when provided a valid product input', () => { + const product = { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + }; + + const checkedProduct = checkProductParam( + product as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(0); + expect(checkedProduct).toBe(product); + }); + + it('logs nothing when provided a valid product input without checkAll flag', () => { + checkProductParam({ + options: [ + { + optionValues: [ + { + firstSelectableVariant: { + id: 'snowboard', + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + }, + ], + }, + ], + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial); + + expect(console.error).toHaveBeenCalledTimes(0); + }); + + it('logs warnings for each missing field when provided an invalid product input and returns an empty object', () => { + const checkedProduct = checkProductParam( + { + id: 'snowboard', + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(6); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}handle${ERROR_MSG_END}`, + ); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options${ERROR_MSG_END}`, + ); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}encodedVariantExistence${ERROR_MSG_END}`, + ); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}encodedVariantAvailability${ERROR_MSG_END}`, + ); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}selectedOrFirstAvailableVariant${ERROR_MSG_END}`, + ); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}adjacentVariants${ERROR_MSG_END}`, + ); + expect(checkedProduct).toStrictEqual({}); + }); + + it('logs warnings when provided an invalid options input - missing optionValues', () => { + checkProductParam( + { + id: 'snowboard', + handle: 'snowboard', + options: [ + { + name: 'Color', + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options.optionValues${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when provided an invalid options input - missing optionValues.name', () => { + checkProductParam( + { + id: 'snowboard', + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options.optionValues.name${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant', () => { + checkProductParam( + { + id: 'snowboard', + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options.optionValues.firstSelectableVariant${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.handle', () => { + checkProductParam( + { + id: 'snowboard', + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options.optionValues.firstSelectableVariant.product.handle${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.selectedOptions', () => { + checkProductParam( + { + id: 'snowboard', + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.selectedOptions.name', () => { + checkProductParam( + { + id: 'snowboard', + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + value: 'Turquoise', + }, + ], + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions.name${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.selectedOptions.value', () => { + checkProductParam( + { + id: 'snowboard', + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + }, + ], + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions.value${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.product.handle', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: { + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}selectedOrFirstAvailableVariant.product.handle${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.selectedOptions', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: { + product: { + handle: 'snowboard', + }, + }, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.selectedOptions.name', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + value: 'Turquoise', + }, + ], + }, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions.name${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.selectedOptions.value', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + }, + ], + }, + adjacentVariants: [], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions.value${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.product.handle', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [ + { + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + ], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}adjacentVariants.product.handle${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.selectedOptions', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [ + { + product: { + handle: 'snowboard', + }, + }, + ], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}adjacentVariants.selectedOptions${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.selectedOptions.name', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [ + { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + value: 'Turquoise', + }, + ], + }, + ], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}adjacentVariants.selectedOptions.name${ERROR_MSG_END}`, + ); + }); + + it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.selectedOptions.value', () => { + checkProductParam( + { + handle: 'snowboard', + options: [ + { + name: 'Color', + optionValues: [ + { + name: 'Turquoise', + firstSelectableVariant: { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + value: 'Turquoise', + }, + ], + }, + swatch: { + color: '#6cbfc0', + image: null, + }, + }, + ], + }, + ], + encodedVariantExistence: '', + encodedVariantAvailability: '', + selectedOrFirstAvailableVariant: null, + adjacentVariants: [ + { + product: { + handle: 'snowboard', + }, + selectedOptions: [ + { + name: 'Color', + }, + ], + }, + ], + } as unknown as RecursivePartial, + true, + ); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + `${ERROR_MSG_START}adjacentVariants.selectedOptions.value${ERROR_MSG_END}`, + ); + }); +}); + +const PRODUCT = { + id: 'gid://shopify/Product/6730949034040', + handle: 'mail-it-in-freestyle-snowboard', + encodedVariantExistence: 'v1_0:0,1:0,2:0,', + encodedVariantAvailability: 'v1_0:0,1:0,2:0,', + options: [ + { + name: 'Size', + optionValues: [ + { + name: '154cm', + firstSelectableVariant: { + availableForSale: true, + id: 'gid://shopify/ProductVariant/41007290613816', + product: { + handle: 'mail-it-in-freestyle-snowboard', + }, + selectedOptions: [ + { + name: 'Size', + value: '154cm', + }, + { + name: 'Color', + value: 'Sea Green / Desert', + }, + ], + }, + swatch: null, + }, + { + name: '158cm', + firstSelectableVariant: { + availableForSale: true, + id: 'gid://shopify/ProductVariant/41007290646584', + product: { + handle: 'mail-it-in-freestyle-snowboard', + }, + selectedOptions: [ + { + name: 'Size', + value: '158cm', + }, + { + name: 'Color', + value: 'Sea Green / Desert', + }, + ], + }, + swatch: null, + }, + { + name: '160cm', + firstSelectableVariant: { + availableForSale: true, + id: 'gid://shopify/ProductVariant/41007290679352', + product: { + handle: 'mail-it-in-freestyle-snowboard', + }, + selectedOptions: [ + { + name: 'Size', + value: '160cm', + }, + { + name: 'Color', + value: 'Sea Green / Desert', + }, + ], + }, + swatch: null, + }, + ], + }, + { + name: 'Color', + optionValues: [ + { + name: 'Sea Green / Desert', + firstSelectableVariant: { + availableForSale: true, + id: 'gid://shopify/ProductVariant/41007290613816', + product: { + handle: 'mail-it-in-freestyle-snowboard', + }, + selectedOptions: [ + { + name: 'Size', + value: '154cm', + }, + { + name: 'Color', + value: 'Sea Green / Desert', + }, + ], + }, + swatch: null, + }, + ], + }, + ], + selectedOrFirstAvailableVariant: { + availableForSale: true, + id: 'gid://shopify/ProductVariant/41007290613816', + product: { + handle: 'mail-it-in-freestyle-snowboard', + }, + selectedOptions: [ + { + name: 'Size', + value: '154cm', + }, + { + name: 'Color', + value: 'Sea Green / Desert', + }, + ], + }, + adjacentVariants: [ + { + availableForSale: true, + id: 'gid://shopify/ProductVariant/41007290646584', + product: { + handle: 'mail-it-in-freestyle-snowboard', + }, + selectedOptions: [ + { + name: 'Size', + value: '158cm', + }, + { + name: 'Color', + value: 'Sea Green / Desert', + }, + ], + }, + { + availableForSale: true, + id: 'gid://shopify/ProductVariant/41007290679352', + product: { + handle: 'mail-it-in-freestyle-snowboard', + }, + selectedOptions: [ + { + name: 'Size', + value: '160cm', + }, + { + name: 'Color', + value: 'Sea Green / Desert', + }, + ], + }, + ], +}; diff --git a/packages/hydrogen-react/src/getProductOptions.ts b/packages/hydrogen-react/src/getProductOptions.ts new file mode 100644 index 0000000000..a2d0c7a042 --- /dev/null +++ b/packages/hydrogen-react/src/getProductOptions.ts @@ -0,0 +1,442 @@ +import {isOptionValueCombinationInEncodedVariant} from './optionValueDecoder.js'; +import type { + Product, + ProductOption, + ProductOptionValue, + ProductVariant, + SelectedOption, +} from './storefront-api-types'; + +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; +type ProductOptionsMapping = Record; +type ProductOptionValueState = { + variant: ProductVariant; + handle: string; + variantUriQuery: string; + selected: boolean; + exists: boolean; + available: boolean; + isDifferentProduct: boolean; +}; +type MappedProductOptionValue = ProductOptionValue & ProductOptionValueState; + +/** + * Creates a mapping of product options to their index for matching encoded values + * For example, a product option of + * [ + * \{ + * name: 'Color', + * optionValues: [\{name: 'Red'\}, \{name: 'Blue'\}] + * \}, + * \{ + * name: 'Size', + * optionValues: [\{name: 'Small'\}, \{name: 'Medium'\}, \{name: 'Large'\}] + * \} + * ] + * Would return + * [ + * \{Red: 0, Blue: 1\}, + * \{Small: 0, Medium: 1, Large: 2\} + * ] + */ +function mapProductOptions(options: ProductOption[]): ProductOptionsMapping[] { + return options.map((option: ProductOption) => { + return Object.assign( + {}, + ...(option?.optionValues + ? option.optionValues.map((value, index) => { + return {[value.name]: index}; + }) + : []), + ) as ProductOptionsMapping; + }); +} + +/** + * Converts the product option into an Object\ for building query params + * For example, a selected product option of + * [ + * \{ + * name: 'Color', + * value: 'Red', + * \}, + * \{ + * name: 'Size', + * value: 'Medium', + * \} + * ] + * Would return + * \{ + * Color: 'Red', + * Size: 'Medium', + * \} + */ +export function mapSelectedProductOptionToObject( + options: Pick[], +): Record { + return Object.assign( + {}, + ...options.map((key) => { + return {[key.name]: key.value}; + }), + ) as Record; +} + +/** + * Returns the JSON stringify result of mapSelectedProductOptionToObject + */ +function mapSelectedProductOptionToObjectAsString( + options: Pick[], +): string { + return JSON.stringify(mapSelectedProductOptionToObject(options)); +} + +/** + * Encode the selected product option as a key for mapping to the encoded variants + * For example, a selected product option of + * [ + * \{ + * name: 'Color', + * value: 'Red', + * \}, + * \{ + * name: 'Size', + * value: 'Medium', + * \} + * ] + * Would return + * [0,1] + * + * Also works with the result of mapSelectedProductOption. For example: + * \{ + * Color: 'Red', + * Size: 'Medium', + * \} + * Would return + * [0,1] + * + * @param selectedOption - The selected product option + * @param productOptionMappings - The result of product option mapping from mapProductOptions + * @returns + */ +function encodeSelectedProductOptionAsKey( + selectedOption: + | Pick[] + | Record, + productOptionMappings: ProductOptionsMapping[], +): string { + if (Array.isArray(selectedOption)) { + return JSON.stringify( + selectedOption.map((key, index) => { + return productOptionMappings[index][key.value]; + }), + ); + } else { + return JSON.stringify( + Object.keys(selectedOption).map((key, index) => { + return productOptionMappings[index][selectedOption[key]]; + }), + ); + } +} + +/** + * Takes an array of product variants and maps them to an object with the encoded selected option values as the key. + * For example, a product variant of + * [ + * \{ + * id: 1, + * selectedOptions: [ + * \{name: 'Color', value: 'Red'\}, + * \{name: 'Size', value: 'Small'\}, + * ], + * \}, + * \{ + * id: 2, + * selectedOptions: [ + * \{name: 'Color', value: 'Red'\}, + * \{name: 'Size', value: 'Medium'\}, + * ], + * \} + * ] + * Would return + * \{ + * '[0,0]': \{id: 1, selectedOptions: [\{name: 'Color', value: 'Red'\}, \{name: 'Size', value: 'Small'\}]\}, + * '[0,1]': \{id: 2, selectedOptions: [\{name: 'Color', value: 'Red'\}, \{name: 'Size', value: 'Medium'\}]\}, + * \} + */ +function mapVariants( + variants: ProductVariant[], + productOptionMappings: ProductOptionsMapping[], +): Record { + return Object.assign( + {}, + ...variants.map((variant) => { + const variantKey = encodeSelectedProductOptionAsKey( + variant.selectedOptions || [], + productOptionMappings, + ); + return {[variantKey]: variant}; + }), + ) as Record; +} + +export type MappedProductOptions = Omit & { + optionValues: MappedProductOptionValue[]; +}; + +const PRODUCT_INPUTS = [ + 'options', + 'selectedOrFirstAvailableVariant', + 'adjacentVariants', +]; + +const PRODUCT_INPUTS_EXTRA = [ + 'handle', + 'encodedVariantExistence', + 'encodedVariantAvailability', +]; + +function logErrorAndReturnFalse(key: string): boolean { + console.error( + `[h2:error:getProductOptions] product.${key} is missing. Make sure you query for this field from the Storefront API.`, + ); + return false; +} + +export function checkProductParam( + product: RecursivePartial, + checkAll = false, +): Product { + let validParam = true; + const productKeys = Object.keys(product); + + // Check product input + (checkAll + ? [...PRODUCT_INPUTS, ...PRODUCT_INPUTS_EXTRA] + : PRODUCT_INPUTS + ).forEach((key) => { + if (!productKeys.includes(key)) { + validParam = logErrorAndReturnFalse(key); + } + }); + + // Check for nested options requirements + if (product.options) { + const firstOption = product?.options[0]; + + if (checkAll && !firstOption?.name) { + validParam = logErrorAndReturnFalse('options.name'); + } + + // Check for options.optionValues + if (product?.options[0]?.optionValues) { + const firstOptionValues = product.options[0].optionValues[0]; + + // Check for options.optionValues.name + if (checkAll && !firstOptionValues?.name) { + validParam = logErrorAndReturnFalse('options.optionValues.name'); + } + + // Check for options.optionValues.firstSelectableVariant + if (firstOptionValues?.firstSelectableVariant) { + // check product variant + validParam = checkProductVariantParam( + firstOptionValues.firstSelectableVariant, + 'options.optionValues.firstSelectableVariant', + validParam, + checkAll, + ); + } else { + validParam = logErrorAndReturnFalse( + 'options.optionValues.firstSelectableVariant', + ); + } + } else { + validParam = logErrorAndReturnFalse('options.optionValues'); + } + } + + // Check for nested selectedOrFirstAvailableVariant requirements + if (product.selectedOrFirstAvailableVariant) { + validParam = checkProductVariantParam( + product.selectedOrFirstAvailableVariant, + 'selectedOrFirstAvailableVariant', + validParam, + checkAll, + ); + } + + // Check for nested adjacentVariants requirements + if (!!product.adjacentVariants && product.adjacentVariants[0]) { + validParam = checkProductVariantParam( + product.adjacentVariants[0], + 'adjacentVariants', + validParam, + checkAll, + ); + } + + return (validParam ? product : {}) as Product; +} + +function checkProductVariantParam( + variant: RecursivePartial, + key: string, + currentValidParamState: boolean, + checkAll: boolean, +): boolean { + let validParam = currentValidParamState; + + if (checkAll && !variant.product?.handle) { + validParam = logErrorAndReturnFalse(`${key}.product.handle`); + } + if (variant.selectedOptions) { + const firstSelectedOption = variant.selectedOptions[0]; + if (!firstSelectedOption?.name) { + validParam = logErrorAndReturnFalse(`${key}.selectedOptions.name`); + } + if (!firstSelectedOption?.value) { + validParam = logErrorAndReturnFalse(`${key}.selectedOptions.value`); + } + } else { + validParam = logErrorAndReturnFalse(`${key}.selectedOptions`); + } + + return validParam; +} + +/** + * Finds all the variants provided by adjacentVariants, options.optionValues.firstAvailableVariant, + * and selectedOrFirstAvailableVariant and return them in a single array + */ +export function getAdjacentAndFirstAvailableVariants( + product: RecursivePartial, +): ProductVariant[] { + // Checks for valid product input + const checkedProduct = checkProductParam(product); + + if (!checkedProduct.options) return []; + + const availableVariants: Record = {}; + checkedProduct.options.map((option) => { + option.optionValues?.map((value) => { + if (value.firstSelectableVariant) { + const variantKey = mapSelectedProductOptionToObjectAsString( + value.firstSelectableVariant.selectedOptions, + ); + availableVariants[variantKey] = value.firstSelectableVariant; + } + }); + }); + + checkedProduct.adjacentVariants.map((variant) => { + const variantKey = mapSelectedProductOptionToObjectAsString( + variant.selectedOptions, + ); + availableVariants[variantKey] = variant; + }); + + const selectedVariant = checkedProduct.selectedOrFirstAvailableVariant; + if (selectedVariant) { + const variantKey = mapSelectedProductOptionToObjectAsString( + selectedVariant.selectedOptions, + ); + availableVariants[variantKey] = selectedVariant; + } + + return Object.values(availableVariants); +} + +/** + * Returns a product options array with its relevant information + * about the variant + */ +export function getProductOptions( + product: RecursivePartial, +): MappedProductOptions[] { + // Checks for valid product input + const checkedProduct = checkProductParam(product, true); + + if (!checkedProduct.options) return []; + + const { + options, + selectedOrFirstAvailableVariant: selectedVariant, + adjacentVariants, + encodedVariantExistence, + encodedVariantAvailability, + handle: productHandle, + } = checkedProduct; + // Get a mapping of product option names to their index for matching encoded values + const productOptionMappings = mapProductOptions(options); + + // Get the adjacent variants mapped to the encoded selected option values + const variants = mapVariants( + selectedVariant ? [selectedVariant, ...adjacentVariants] : adjacentVariants, + productOptionMappings, + ); + + // Get the key:value version of selected options for building url query params + const selectedOptions = mapSelectedProductOptionToObject( + selectedVariant ? selectedVariant.selectedOptions : [], + ); + + const productOptions = options.map((option, optionIndex) => { + return { + ...option, + optionValues: option.optionValues.map((value) => { + const targetOptionParams = {...selectedOptions}; // Clones the selected options + + // Modify the selected option value to the current option value + targetOptionParams[option.name] = value.name; + + // Encode the new selected option values as a key for mapping to the product variants + const targetKey = encodeSelectedProductOptionAsKey( + targetOptionParams || [], + productOptionMappings, + ); + + // Top-down option check for existence and availability + const topDownKey = (JSON.parse(targetKey) as number[]).slice( + 0, + optionIndex + 1, + ); + const exists = isOptionValueCombinationInEncodedVariant( + topDownKey, + encodedVariantExistence || '', + ); + const available = isOptionValueCombinationInEncodedVariant( + topDownKey, + encodedVariantAvailability || '', + ); + + // Get the variant for the current option value if exists, else use the first selectable variant + const variant: ProductVariant = + variants[targetKey] || value.firstSelectableVariant; + + // Build the query params for this option value + const variantOptionParam = mapSelectedProductOptionToObject( + variant.selectedOptions || [], + ); + const searchParams = new URLSearchParams(variantOptionParam); + const handle = variant?.product?.handle; + + return { + ...value, + variant, + handle, + variantUriQuery: searchParams.toString(), + selected: selectedOptions[option.name] === value.name, + exists, + available, + isDifferentProduct: handle !== productHandle, + }; + }), + }; + }); + + return productOptions; +} diff --git a/packages/hydrogen-react/src/index.ts b/packages/hydrogen-react/src/index.ts index b6d0e9fcf2..0c34289693 100644 --- a/packages/hydrogen-react/src/index.ts +++ b/packages/hydrogen-react/src/index.ts @@ -45,6 +45,12 @@ export { export {getShopifyCookies} from './cookies-utils.js'; export {ExternalVideo} from './ExternalVideo.js'; export {flattenConnection} from './flatten-connection.js'; +export { + getAdjacentAndFirstAvailableVariants, + getProductOptions, + type MappedProductOptions, + mapSelectedProductOptionToObject, +} from './getProductOptions.js'; export {Image, IMAGE_FRAGMENT} from './Image.js'; export {useLoadScript} from './load-script.js'; export {MediaFile} from './MediaFile.js'; @@ -70,5 +76,6 @@ export type { export type {StorefrontClientProps} from './storefront-client.js'; export {createStorefrontClient} from './storefront-client.js'; export {useMoney} from './useMoney.js'; +export {useSelectedOptionInUrlParam} from './useSelectedOptionInUrlParam.js'; export {useShopifyCookies} from './useShopifyCookies.js'; export {Video} from './Video.js'; diff --git a/packages/hydrogen-react/src/useSelectOptionInUrlParam.test.tsx b/packages/hydrogen-react/src/useSelectOptionInUrlParam.test.tsx new file mode 100644 index 0000000000..7369c10bfa --- /dev/null +++ b/packages/hydrogen-react/src/useSelectOptionInUrlParam.test.tsx @@ -0,0 +1,98 @@ +import {vi, afterEach, describe, expect, it} from 'vitest'; +import {renderHook} from '@testing-library/react'; +import {useSelectedOptionInUrlParam} from './useSelectedOptionInUrlParam.js'; + +type mockOptions = {search?: string; pathname?: string}; + +const globalMocks = ({search = '', pathname = ''}: mockOptions) => { + let currentSearch = search; + let currentPathname = pathname; + + return { + location: { + get search() { + return currentSearch; + }, + set search(search: string) { + currentSearch = search; + }, + get pathname() { + return currentPathname; + }, + set pathname(pathname: string) { + currentPathname = pathname; + }, + }, + history: { + replaceState: (_state: unknown, _unused: unknown, url: string) => { + const newUrl = new URL(url, 'https://placeholder.shopify.com'); + currentSearch = newUrl.search; + currentPathname = newUrl.pathname; + }, + }, + }; +}; + +const mockGlobals = (options?: mockOptions) => { + const mocks = globalMocks(options || {}); + + vi.stubGlobal('location', mocks.location); + vi.stubGlobal('history', mocks.history); +}; + +describe(`useSelectedOptionInUrlParam`, () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('updates url with selected options search params when url itself has no search params', () => { + mockGlobals(); + renderHook(() => + useSelectedOptionInUrlParam([ + { + name: 'Color', + value: 'Red', + }, + { + name: 'Size', + value: 'Medium', + }, + ]), + ); + expect(location.search).toBe('?Color=Red&Size=Medium'); + }); + + it('updates url with selected options search params when url itself has other search params', () => { + mockGlobals({search: '?test=test'}); + renderHook(() => + useSelectedOptionInUrlParam([ + { + name: 'Color', + value: 'Red', + }, + { + name: 'Size', + value: 'Medium', + }, + ]), + ); + expect(location.search).toBe('?test=test&Color=Red&Size=Medium'); + }); + + it('updates url with selected options search params when url itself has other duplicated search params', () => { + mockGlobals({search: '?Color=blue'}); + renderHook(() => + useSelectedOptionInUrlParam([ + { + name: 'Color', + value: 'Red', + }, + { + name: 'Size', + value: 'Medium', + }, + ]), + ); + expect(location.search).toBe('?Color=Red&Size=Medium'); + }); +}); diff --git a/packages/hydrogen-react/src/useSelectedOptionInUrlParam.doc.ts b/packages/hydrogen-react/src/useSelectedOptionInUrlParam.doc.ts new file mode 100644 index 0000000000..d876701b4a --- /dev/null +++ b/packages/hydrogen-react/src/useSelectedOptionInUrlParam.doc.ts @@ -0,0 +1,37 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'useSelectedOptionInUrlParam', + category: 'utilities', + isVisualComponent: false, + related: [ + { + name: 'getProductOptions', + type: 'gear', + url: '/api/hydrogen-react/utilities/getproductoptions', + }, + { + name: 'getAdjacentAndFirstAvailableVariants', + type: 'gear', + url: '/api/hydrogen-react/utilities/getadjacentandfirstavailablevariants', + }, + ], + description: 'Sets the url params to the selected option.', + type: 'utility', + defaultExample: { + description: 'I am the default example', + codeblock: { + tabs: [ + { + title: 'useSelectedOptionInUrlParam example', + code: './useSelectedOptionInUrlParam.example.js', + language: 'js', + }, + ], + title: 'Example', + }, + }, + definitions: [], +}; + +export default data; diff --git a/packages/hydrogen-react/src/useSelectedOptionInUrlParam.example.js b/packages/hydrogen-react/src/useSelectedOptionInUrlParam.example.js new file mode 100644 index 0000000000..199d550ece --- /dev/null +++ b/packages/hydrogen-react/src/useSelectedOptionInUrlParam.example.js @@ -0,0 +1,16 @@ +import {useSelectedOptionInUrlParam} from '@shopify/hydrogen-react'; + +const selectedOption = [ + { + name: 'Color', + value: 'Red', + }, + { + name: 'Size', + value: 'Medium', + }, +]; + +useSelectedOptionInUrlParam(selectedOption); + +// URL will be updated to ?Color=Red&Size=Medium diff --git a/packages/hydrogen-react/src/useSelectedOptionInUrlParam.tsx b/packages/hydrogen-react/src/useSelectedOptionInUrlParam.tsx new file mode 100644 index 0000000000..cf77ced5f0 --- /dev/null +++ b/packages/hydrogen-react/src/useSelectedOptionInUrlParam.tsx @@ -0,0 +1,37 @@ +import {useEffect} from 'react'; +import {mapSelectedProductOptionToObject} from './getProductOptions.js'; +import {SelectedOption} from './storefront-api-types.js'; + +export function useSelectedOptionInUrlParam( + selectedOptions: Pick[], +): null { + useEffect(() => { + const optionsSearchParams = new URLSearchParams( + mapSelectedProductOptionToObject(selectedOptions || []), + ); + const currentSearchParams = new URLSearchParams(window.location.search); + + // ts ignoring the URLSearchParams not iterable error for now + // https://stackoverflow.com/questions/72522489/urlsearchparams-not-accepting-string#answer-72522838 + // TODO: update ts lib + const combinedSearchParams = new URLSearchParams({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...Object.fromEntries(currentSearchParams), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...Object.fromEntries(optionsSearchParams), + }); + + if (combinedSearchParams.size > 0) { + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${combinedSearchParams.toString()}`, + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(selectedOptions)]); + + return null; +} diff --git a/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs b/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs index a051b553d4..cb8ac3a88e 100644 --- a/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs +++ b/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs @@ -13,17 +13,21 @@ const docsToCopy = [ 'useMoney', 'useLoadScript', 'useShopifyCookies', + 'decodeEncodedVariant', 'flattenConnection', + 'getAdjacentAndFirstAvailableVariants', 'getClientBrowserParameters', + 'getProductOptions', 'getShopifyCookies', + 'isOptionValueCombinationInEncodedVariant', + 'mapSelectedProductOptionToObject', + 'parseGid', 'parseMetafield', 'sendShopifyAnalytics', + 'useSelectedOptionInUrlParam', 'storefrontApiCustomScalars', - 'parseGid', 'Storefront Schema', 'Storefront API Types', - 'decodeEncodedVariant', - 'isOptionValueCombinationInEncodedVariant', ]; async function copyFiles() { diff --git a/packages/hydrogen/docs/generated/generated_docs_data.json b/packages/hydrogen/docs/generated/generated_docs_data.json index b208b10c06..79dc02fb30 100644 --- a/packages/hydrogen/docs/generated/generated_docs_data.json +++ b/packages/hydrogen/docs/generated/generated_docs_data.json @@ -20578,7 +20578,7 @@ "filePath": "src/product/VariantSelector.ts", "syntaxKind": "TypeAliasDeclaration", "name": "VariantSelectorProps", - "value": "{\n /** The product handle for all of the variants */\n handle: string;\n /** Product options from the [Storefront API](/docs/api/storefront/2024-10/objects/ProductOption). Make sure both `name` and `values` are a part of your query. */\n options: Array | undefined;\n /** Product variants from the [Storefront API](/docs/api/storefront/2024-10/objects/ProductVariant). You only need to pass this prop if you want to show product availability. If a product option combination is not found within `variants`, it is assumed to be available. Make sure to include `availableForSale` and `selectedOptions.name` and `selectedOptions.value`. */\n variants?:\n | PartialDeep\n | Array>;\n /** By default all products are under /products. Use this prop to provide a custom path. */\n productPath?: string;\n /** Should the VariantSelector wait to update until after the browser navigates to a variant. */\n waitForNavigation?: boolean;\n children: ({option}: {option: VariantOption}) => ReactNode;\n}", + "value": "{\n /** The product handle for all of the variants */\n handle: string;\n /** Product options from the [Storefront API](/docs/api/storefront/2024-10/objects/ProductOption). Make sure both `name` and `values` are a part of your query. */\n options: Array | undefined;\n /** Product variants from the [Storefront API](/docs/api/storefront/2024-10/objects/ProductVariant). You only need to pass this prop if you want to show product availability. If a product option combination is not found within `variants`, it is assumed to be available. Make sure to include `availableForSale` and `selectedOptions.name` and `selectedOptions.value`. */\n variants?:\n | PartialDeep\n | Array>;\n /** By default all products are under /products. Use this prop to provide a custom path. */\n productPath?: string;\n /** Should the VariantSelector wait to update until after the browser navigates to a variant. */\n waitForNavigation?: boolean;\n /** An optional selected variant to use for the initial state if no URL parameters are set */\n selectedVariant?: Maybe>;\n children: ({option}: {option: VariantOption}) => ReactNode;\n}", "description": "", "members": [ { @@ -20610,6 +20610,14 @@ "description": "By default all products are under /products. Use this prop to provide a custom path.", "isOptional": true }, + { + "filePath": "src/product/VariantSelector.ts", + "syntaxKind": "PropertySignature", + "name": "selectedVariant", + "value": "Maybe>", + "description": "An optional selected variant to use for the initial state if no URL parameters are set", + "isOptional": true + }, { "filePath": "src/product/VariantSelector.ts", "syntaxKind": "PropertySignature", @@ -20865,7 +20873,7 @@ "name": "OptimisticVariant", "value": "OptimisticVariant" }, - "value": "export function useOptimisticVariant<\n SelectedVariant = OptimisticVariantInput,\n Variants = OptimisticProductVariants,\n>(\n selectedVariant: SelectedVariant,\n variants: Variants,\n): OptimisticVariant {\n const navigation = useNavigation();\n const [resolvedVariants, setResolvedVariants] = useState<\n Array>\n >([]);\n\n useEffect(() => {\n Promise.resolve(variants)\n .then((productWithVariants) => {\n if (productWithVariants) {\n setResolvedVariants(\n productWithVariants instanceof Array\n ? productWithVariants\n : (productWithVariants as PartialDeep).product\n ?.variants?.nodes || [],\n );\n }\n })\n .catch((error) => {\n reportError(\n new Error(\n '[h2:error:useOptimisticVariant] An error occurred while resolving the variants for the optimistic product hook.',\n {\n cause: error,\n },\n ),\n );\n });\n }, [variants]);\n\n if (navigation.state === 'loading') {\n const queryParams = new URLSearchParams(navigation.location.search);\n let reportedError = false;\n\n // Find matching variant\n const matchingVariant = resolvedVariants.find((variant) => {\n if (!variant.selectedOptions) {\n if (!reportedError) {\n reportedError = true;\n reportError(\n new Error(\n '[h2:error:useOptimisticVariant] The optimistic product hook requires your product query to include variants with the selectedOptions field.',\n ),\n );\n }\n return false;\n }\n\n return variant.selectedOptions.every((option) => {\n return queryParams.get(option.name) === option.value;\n });\n });\n\n if (matchingVariant) {\n return {\n ...matchingVariant,\n isOptimistic: true,\n } as OptimisticVariant;\n }\n }\n\n return selectedVariant as OptimisticVariant;\n}" + "value": "export function useOptimisticVariant<\n SelectedVariant = OptimisticVariantInput,\n Variants = OptimisticProductVariants,\n>(\n selectedVariant: SelectedVariant,\n variants: Variants,\n): OptimisticVariant {\n const navigation = useNavigation();\n const [resolvedVariants, setResolvedVariants] = useState<\n Array>\n >([]);\n\n useEffect(() => {\n Promise.resolve(variants)\n .then((productWithVariants) => {\n if (productWithVariants) {\n setResolvedVariants(\n productWithVariants instanceof Array\n ? productWithVariants\n : (productWithVariants as PartialDeep).product\n ?.variants?.nodes || [],\n );\n }\n })\n .catch((error) => {\n reportError(\n new Error(\n '[h2:error:useOptimisticVariant] An error occurred while resolving the variants for the optimistic product hook.',\n {\n cause: error,\n },\n ),\n );\n });\n }, [JSON.stringify(variants)]);\n\n if (navigation.state === 'loading') {\n const queryParams = new URLSearchParams(navigation.location.search);\n let reportedError = false;\n\n // Find matching variant\n const matchingVariant = resolvedVariants.find((variant) => {\n if (!variant.selectedOptions) {\n if (!reportedError) {\n reportedError = true;\n reportError(\n new Error(\n '[h2:error:useOptimisticVariant] The optimistic product hook requires your product query to include variants with the selectedOptions field.',\n ),\n );\n }\n return false;\n }\n\n return variant.selectedOptions.every((option) => {\n return queryParams.get(option.name) === option.value;\n });\n });\n\n if (matchingVariant) {\n return {\n ...matchingVariant,\n isOptimistic: true,\n } as OptimisticVariant;\n }\n }\n\n return selectedVariant as OptimisticVariant;\n}" }, "OptimisticVariant": { "filePath": "src/product/useOptimisticVariant.ts", @@ -26825,6 +26833,69 @@ } ] }, + { + "name": "decodeEncodedVariant", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "isOptionValueCombinationInEncodedVariant", + "type": "utility", + "url": "/docs/api/hydrogen/2024-10/utilities/isOptionValueCombinationInEncodedVariant" + } + ], + "description": "Decodes an encoded option value string into an array of option value combinations.", + "type": "utility", + "defaultExample": { + "description": "Decode an encoded option value string", + "codeblock": { + "tabs": [ + { + "title": "JavaScript", + "code": "import {decodeEncodedVariant} from '@shopify/hydrogen';\n\n// product.options = [\n// {\n// name: 'Color',\n// optionValues: [\n// {name: 'Red'},\n// {name: 'Blue'},\n// {name: 'Green'},\n// ]\n// },\n// {\n// name: 'Size',\n// optionValues: [\n// {name: 'S'},\n// {name: 'M'},\n// {name: 'L'},\n// ]\n// }\n// ]\n\nconst encodedVariantAvailability = 'v1_0:0-2,1:2,';\n\nconst decodedVariantAvailability = decodeEncodedVariant(\n encodedVariantAvailability,\n);\n\n// decodedVariantAvailability\n// {\n// [0,0], // Red, S\n// [0,1], // Red, M\n// [0,2], // Red, L\n// [1,2] // Blue, L\n// }\n", + "language": "js" + } + ], + "title": "Example code" + } + }, + "definitions": [ + { + "title": "Props", + "description": "", + "type": "DecodeEncodedVariantGeneratedType", + "typeDefinitions": { + "DecodeEncodedVariantGeneratedType": { + "filePath": "src/optionValueDecoder.ts", + "name": "DecodeEncodedVariantGeneratedType", + "description": "For an encoded option value string, decode into option value combinations. Entries represent a valid combination formatted as an array of option value positions.", + "params": [ + { + "name": "encodedVariantField", + "description": "Encoded option value string from the Storefront API, e.g. [product.encodedVariantExistence](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantexistence) or [product.encodedVariantAvailability](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantavailability)", + "value": "string", + "filePath": "src/optionValueDecoder.ts" + } + ], + "returns": { + "filePath": "src/optionValueDecoder.ts", + "description": "Decoded option value combinations", + "name": "DecodedOptionValues", + "value": "DecodedOptionValues" + }, + "value": "export function decodeEncodedVariant(\n encodedVariantField: EncodedVariantField,\n): DecodedOptionValues {\n if (!encodedVariantField) return [];\n\n if (encodedVariantField.startsWith('v1_')) {\n return v1Decoder(stripVersion(encodedVariantField));\n }\n\n throw new Error('Unsupported option value encoding');\n}" + }, + "DecodedOptionValues": { + "filePath": "src/optionValueDecoder.ts", + "syntaxKind": "TypeAliasDeclaration", + "name": "DecodedOptionValues", + "value": "number[][]", + "description": "" + } + } + } + ] + }, { "name": "flattenConnection", "category": "utilities", @@ -27169,6 +27240,44 @@ } ] }, + { + "name": "getAdjacentAndFirstAvailableVariants", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "getProductOptions", + "type": "gear", + "url": "/api/hydrogen/utilities/getproductoptions" + }, + { + "name": "mapSelectedProductOptionToObject", + "type": "gear", + "url": "/api/hydrogen/utilities/mapselectedproductoptiontoobject" + }, + { + "name": "useSelectedOptionInUrlParam", + "type": "gear", + "url": "/api/hydrogen/utilities/useselectedoptioninurlparam" + } + ], + "description": "Finds all the variants provided by `adjacentVariants`, `options.optionValues.firstAvailableVariant`, and `selectedOrFirstAvailableVariant` and return them in a single array. This function will remove any duplicated variants found.", + "type": "utility", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "getAdjacentAndFirstAvailableVariants example", + "code": "import {getAdjacentAndFirstAvailableVariants} from '@shopify/hydrogen';\n\n// Make sure you are querying for the following fields:\n// - product.options.optionValues.firstSelectableVariant\n// - product.selectedOrFirstAvailableVariant\n// - product.adjacentVariants\n//\n// For any fields that are ProductVariant type, make sure to query for:\n// - variant.selectedOptions.name\n// - variant.selectedOptions.value\n\nconst product = {\n /* Result from querying the SFAPI for a product */\n};\n\n// Returns a list of unique variants found in product query\nconst variants = getAdjacentAndFirstAvailableVariants(product);\n", + "language": "js" + } + ], + "title": "getAdjacentAndFirstAvailableVariants.js" + } + }, + "definitions": [] + }, { "name": "getClientBrowserParameters", "category": "utilities", @@ -27309,6 +27418,49 @@ } ] }, + { + "name": "getProductOptions", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "mapSelectedProductOptionToObject", + "type": "gear", + "url": "/api/hydrogen/utilities/mapselectedproductoptiontoobject" + }, + { + "name": "getAdjacentAndFirstAvailableVariants", + "type": "gear", + "url": "/api/hydrogen/utilities/getadjacentandfirstavailablevariants" + }, + { + "name": "useSelectedOptionInUrlParam", + "type": "gear", + "url": "/api/hydrogen/utilities/useselectedoptioninurlparam" + } + ], + "description": "Returns a product options array with its relevant information about the variant. This function supports combined listing products and products with 2000 variants limit.", + "type": "utility", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "JavaScript", + "code": "import React from 'react';\nimport {getProductOptions} from '@shopify/hydrogen';\n\n// Make sure you are querying for the following fields:\n// - product.handle\n// - product.encodedVariantExistence\n// - product.encodedVariantAvailability\n// - product.options.name\n// - product.options.optionValues.name\n// - product.options.optionValues.firstSelectableVariant\n// - product.selectedOrFirstAvailableVariant\n// - product.adjacentVariants\n//\n// For any fields that are ProductVariant type, make sure to query for:\n// - variant.product.handle\n// - variant.selectedOptions.name\n// - variant.selectedOptions.value\n\nexport default function ProductForm() {\n const product = {\n /* Result from querying the SFAPI for a product */\n };\n\n const productOptions = getProductOptions(product);\n\n return (\n <>\n {productOptions.map((option) => (\n <div key={option.name}>\n <h5>{option.name}</h5>\n <div>\n {option.optionValues.map((value) => {\n const {\n name,\n handle,\n variantUriQuery,\n selected,\n available,\n exists,\n isDifferentProduct,\n swatch,\n } = value;\n\n if (isDifferentProduct) {\n // SEO - When the variant is a\n // combined listing child product\n // that leads to a different url,\n // we need to render it\n // as an anchor tag\n return (\n <a\n key={option.name + name}\n href={`/products/${handle}?${variantUriQuery}`}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </a>\n );\n } else {\n // SEO - When the variant is an\n // update to the search param,\n // render it as a button with\n // javascript navigating to\n // the variant so that SEO bots\n // do not index these as\n // duplicated links\n return (\n <button\n type=\"button\"\n key={option.name + name}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n disabled={!exists}\n onClick={() => {\n if (!selected) {\n // Navigate to `?${variantUriQuery}`\n }\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </button>\n );\n }\n })}\n </div>\n <br />\n </div>\n ))}\n </>\n );\n}\n\nfunction ProductOptionSwatch({swatch, name}) {\n const image = swatch?.image?.previewImage?.url;\n const color = swatch?.color;\n\n if (!image && !color) return name;\n\n return (\n <div\n aria-label={name}\n className=\"product-option-label-swatch\"\n style={{\n backgroundColor: color || 'transparent',\n }}\n >\n {!!image && <img src={image} alt={name} />}\n </div>\n );\n}\n", + "language": "jsx" + }, + { + "title": "TypeScript", + "code": "import React from 'react';\nimport {\n getProductOptions,\n type MappedProductOptions,\n} from '@shopify/hydrogen';\nimport type {\n ProductOptionValueSwatch,\n Maybe,\n} from '@shopify/hydrogen/storefront-api-types';\n\n// Make sure you are querying for the following fields:\n// - product.handle\n// - product.encodedVariantExistence\n// - product.encodedVariantAvailability\n// - product.options.name\n// - product.options.optionValues.name\n// - product.options.optionValues.firstSelectableVariant\n// - product.selectedOrFirstAvailableVariant\n// - product.adjacentVariants\n//\n// For any fields that are ProductVariant type, make sure to query for:\n// - variant.product.handle\n// - variant.selectedOptions.name\n// - variant.selectedOptions.value\n\nexport default function ProductForm() {\n const product = {\n /* Result from querying the SFAPI for a product */\n };\n\n const productOptions: MappedProductOptions[] = getProductOptions(product);\n\n return (\n <>\n {productOptions.map((option) => (\n <div key={option.name}>\n <h5>{option.name}</h5>\n <div>\n {option.optionValues.map((value) => {\n const {\n name,\n handle,\n variantUriQuery,\n selected,\n available,\n exists,\n isDifferentProduct,\n swatch,\n } = value;\n\n if (isDifferentProduct) {\n // SEO - When the variant is a\n // combined listing child product\n // that leads to a different url,\n // we need to render it\n // as an anchor tag\n return (\n <a\n key={option.name + name}\n href={`/products/${handle}?${variantUriQuery}`}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </a>\n );\n } else {\n // SEO - When the variant is an\n // update to the search param,\n // render it as a button with\n // javascript navigating to\n // the variant so that SEO bots\n // do not index these as\n // duplicated links\n return (\n <button\n type=\"button\"\n key={option.name + name}\n style={{\n border: selected\n ? '1px solid black'\n : '1px solid transparent',\n opacity: available ? 1 : 0.3,\n }}\n disabled={!exists}\n onClick={() => {\n if (!selected) {\n // Navigate to `?${variantUriQuery}`\n }\n }}\n >\n <ProductOptionSwatch swatch={swatch} name={name} />\n </button>\n );\n }\n })}\n </div>\n <br />\n </div>\n ))}\n </>\n );\n}\n\nfunction ProductOptionSwatch({\n swatch,\n name,\n}: {\n swatch?: Maybe<ProductOptionValueSwatch> | undefined;\n name: string;\n}) {\n const image = swatch?.image?.previewImage?.url;\n const color = swatch?.color;\n\n if (!image && !color) return name;\n\n return (\n <div\n aria-label={name}\n style={{\n backgroundColor: color || 'transparent',\n }}\n >\n {!!image && <img src={image} alt={name} />}\n </div>\n );\n}\n", + "language": "tsx" + } + ], + "title": "Example code" + } + }, + "definitions": [] + }, { "name": "getShopifyCookies", "category": "utilities", @@ -27424,6 +27576,175 @@ } ] }, + { + "name": "isOptionValueCombinationInEncodedVariant", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "decodeEncodedVariant", + "type": "utility", + "url": "/docs/api/hydrogen/2024-10/utilities/decodeEncodedVariant" + } + ], + "description": "\n Determines whether an option value combination is present in an encoded option value string.\n\n`targetOptionValueCombination` - Indices of option values to look up in the encoded option value string. A partial set of indices may be passed to determine whether a node or any children is present. For example, if a product has 3 options, passing `[0]` will return true if any option value combination for the first option's option value is present in the encoded string.\n ", + "type": "utility", + "defaultExample": { + "description": "Check if option value is in encoding", + "codeblock": { + "tabs": [ + { + "title": "JavaScript", + "code": "import {isOptionValueCombinationInEncodedVariant} from '@shopify/hydrogen';\n\n// product.options = [\n// {\n// name: 'Color',\n// optionValues: [\n// {name: 'Red'},\n// {name: 'Blue'},\n// {name: 'Green'},\n// ]\n// },\n// {\n// name: 'Size',\n// optionValues: [\n// {name: 'S'},\n// {name: 'M'},\n// {name: 'L'},\n// ]\n// }\n// ]\nconst encodedVariantExistence = 'v1_0:0-1,1:2,';\n\n// For reference: decoded encodedVariantExistence\n// {\n// [0,0], // Red, S\n// [0,1], // Red, M\n// [1,2] // Blue, L\n// }\n\n// Returns true since there are variants exist for [Red]\nisOptionValueCombinationInEncodedVariant([0], encodedVariantExistence); // true\n\nisOptionValueCombinationInEncodedVariant([0, 0], encodedVariantExistence); // true\nisOptionValueCombinationInEncodedVariant([0, 1], encodedVariantExistence); // true\nisOptionValueCombinationInEncodedVariant([0, 2], encodedVariantExistence); // false - no variant exist for [Red, L]\n\n// Returns true since there is a variant exist for [Blue]\nisOptionValueCombinationInEncodedVariant([1], encodedVariantExistence); // true\n\nisOptionValueCombinationInEncodedVariant([1, 0], encodedVariantExistence); // false - no variant exist for [Blue, S]\nisOptionValueCombinationInEncodedVariant([1, 1], encodedVariantExistence); // false - no variant exist for [Blue, M]\nisOptionValueCombinationInEncodedVariant([1, 2], encodedVariantExistence); // true\n\n// Returns false since there is no variant exist for [Green]\nisOptionValueCombinationInEncodedVariant([2], encodedVariantExistence); // false\n", + "language": "js" + } + ], + "title": "Example code" + } + }, + "definitions": [ + { + "title": "Props", + "description": "", + "type": "IsOptionValueCombinationInEncodedVariant", + "typeDefinitions": { + "IsOptionValueCombinationInEncodedVariant": { + "filePath": "src/optionValueDecoder.ts", + "name": "IsOptionValueCombinationInEncodedVariant", + "description": "", + "params": [ + { + "name": "targetOptionValueCombination", + "description": "", + "value": "number[]", + "filePath": "src/optionValueDecoder.ts" + }, + { + "name": "encodedVariantField", + "description": "", + "value": "string", + "filePath": "src/optionValueDecoder.ts" + } + ], + "returns": { + "filePath": "src/optionValueDecoder.ts", + "description": "", + "name": "boolean", + "value": "boolean" + }, + "value": "export type IsOptionValueCombinationInEncodedVariant = (\n targetOptionValueCombination: number[],\n encodedVariantField: string,\n) => boolean;" + } + } + } + ] + }, + { + "name": "mapSelectedProductOptionToObject", + "category": "utilities", + "isVisualComponent": false, + "related": [ + { + "name": "getProductOptions", + "type": "gear", + "url": "/api/hydrogen/utilities/getproductoptions" + }, + { + "name": "getAdjacentAndFirstAvailableVariants", + "type": "gear", + "url": "/api/hydrogen/utilities/getadjacentandfirstavailablevariants" + }, + { + "name": "useSelectedOptionInUrlParam", + "type": "gear", + "url": "/api/hydrogen/utilities/useselectedoptioninurlparam" + } + ], + "description": "Converts the product selected option into an `Object` format for building URL query params", + "type": "utility", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "mapSelectedProductOptionToObject example", + "code": "import {mapSelectedProductOptionToObject} from '@shopify/hydrogen';\n\nconst selectedOption = [\n {\n name: 'Color',\n value: 'Red',\n },\n {\n name: 'Size',\n value: 'Medium',\n },\n];\n\nconst optionsObject = mapSelectedProductOptionToObject(selectedOption);\n\n// Output of optionsObject\n// {\n// Color: 'Red',\n// Size: 'Medium',\n// }\n\nconst searchParams = new URLSearchParams(optionsObject);\nsearchParams.toString(); // '?Color=Red&Size=Medium'\n", + "language": "js" + } + ], + "title": "mapSelectedProductOptionToObject.js" + } + }, + "definitions": [] + }, + { + "name": "parseGid", + "category": "utilities", + "isVisualComponent": false, + "related": [], + "description": "\n Parses [Shopify Global ID (GID)](https://shopify.dev/api/usage/gids) and returns the resource type and ID.\n ", + "type": "gear", + "defaultExample": { + "description": "I am the default example", + "codeblock": { + "tabs": [ + { + "title": "JavaScript", + "code": "import {parseGid} from '@shopify/hydrogen';\n\nconst {id, resource} = parseGid('gid://shopify/Order/123');\n\nconsole.log(id); // 123\nconsole.log(resource); // Order\n", + "language": "js" + } + ], + "title": "Example code" + } + }, + "definitions": [ + { + "title": "Props", + "description": "", + "type": "ParseGidGeneratedType", + "typeDefinitions": { + "ParseGidGeneratedType": { + "filePath": "src/analytics-utils.ts", + "name": "ParseGidGeneratedType", + "description": "Parses global id (gid) and returns the resource type and id.", + "params": [ + { + "name": "gid", + "description": "A shopify GID (string)", + "value": "string", + "filePath": "src/analytics-utils.ts" + } + ], + "returns": { + "filePath": "src/analytics-utils.ts", + "description": "", + "name": "ShopifyGid", + "value": "ShopifyGid" + }, + "value": "export function parseGid(gid: string | undefined): ShopifyGid {\n const defaultReturn: ShopifyGid = {\n id: '',\n resource: null,\n resourceId: null,\n search: '',\n searchParams: new URLSearchParams(),\n hash: '',\n };\n\n if (typeof gid !== 'string') {\n return defaultReturn;\n }\n\n try {\n const {search, searchParams, pathname, hash} = new URL(gid);\n const pathnameParts = pathname.split('/');\n const lastPathnamePart = pathnameParts[pathnameParts.length - 1];\n const resourcePart = pathnameParts[pathnameParts.length - 2];\n\n if (!lastPathnamePart || !resourcePart) {\n return defaultReturn;\n }\n\n const id = `${lastPathnamePart}${search}${hash}` || '';\n const resourceId = lastPathnamePart || null;\n const resource = resourcePart ?? null;\n\n return {id, resource, resourceId, search, searchParams, hash};\n } catch {\n return defaultReturn;\n }\n}", + "examples": [ + { + "title": "Example", + "description": "", + "tabs": [ + { + "code": "const {id, resource} = parseGid('gid://shopify/Order/123')\n// => id = \"123\", resource = 'Order'\n\n * const {id, resource} = parseGid('gid://shopify/Cart/abc123')\n// => id = \"abc123\", resource = 'Cart'", + "title": "Example" + } + ] + } + ] + }, + "ShopifyGid": { + "filePath": "src/analytics-types.ts", + "syntaxKind": "TypeAliasDeclaration", + "name": "ShopifyGid", + "value": "Pick & {\n id: string;\n resource: string | null;\n resourceId: string | null;\n}", + "description": "" + } + } + } + ] + }, { "name": "parseMetafield", "category": "utilities", @@ -28402,106 +28723,70 @@ ] }, { - "name": "storefrontApiCustomScalars", + "name": "useSelectedOptionInUrlParam", "category": "utilities", "isVisualComponent": false, "related": [ { - "name": "Storefront Schema", + "name": "getProductOptions", "type": "gear", - "url": "/api/hydrogen/utilities/storefront-schema" + "url": "/api/hydrogen/utilities/getproductoptions" }, { - "name": "Storefront API Types", + "name": "getAdjacentAndFirstAvailableVariants", "type": "gear", - "url": "/api/hydrogen/utilities/storefront-api-types" + "url": "/api/hydrogen/utilities/getadjacentandfirstavailablevariants" } ], - "description": "\n Meant to be used with GraphQL CodeGen to type the Storefront API's custom scalars correctly when using TypeScript.By default, GraphQL CodeGen uses `any` for custom scalars; by using these definitions, GraphQL Codegen will generate the correct types for the Storefront API's custom scalars.\n\nSee more about [GraphQL CodeGen](https://graphql-code-generator.com/) and [custom scalars for TypeScript](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#scalars).\n\nNote that `@shopify/hydrogen-react` has already generated types for the Storefront API, so you may not need to setup GraphQL Codegen on your own.\n ", + "description": "Sets the url params to the selected option.", "type": "utility", "defaultExample": { "description": "I am the default example", "codeblock": { "tabs": [ { - "title": "Codegen Config", - "code": "import {storefrontApiCustomScalars} from '@shopify/hydrogen';\n\nconst config = {\n overwrite: true,\n schema: require.resolve('@shopify/hydrogen/storefront.schema.json'),\n documents: 'pages/**/*.tsx',\n generates: {\n './gql/': {\n preset: 'client',\n plugins: [],\n config: {\n // defines the custom scalars used in the Storefront API\n scalars: storefrontApiCustomScalars,\n },\n },\n },\n};\n\nexport default config;\n", + "title": "useSelectedOptionInUrlParam example", + "code": "import {useSelectedOptionInUrlParam} from '@shopify/hydrogen';\n\nconst selectedOption = [\n {\n name: 'Color',\n value: 'Red',\n },\n {\n name: 'Size',\n value: 'Medium',\n },\n];\n\nuseSelectedOptionInUrlParam(selectedOption);\n\n// URL will be updated to <original product url>?Color=Red&Size=Medium\n", "language": "js" } ], - "title": "codegen.ts" + "title": "Example" } }, "definitions": [] }, { - "name": "parseGid", + "name": "storefrontApiCustomScalars", "category": "utilities", "isVisualComponent": false, - "related": [], - "description": "\n Parses [Shopify Global ID (GID)](https://shopify.dev/api/usage/gids) and returns the resource type and ID.\n ", - "type": "gear", + "related": [ + { + "name": "Storefront Schema", + "type": "gear", + "url": "/api/hydrogen/utilities/storefront-schema" + }, + { + "name": "Storefront API Types", + "type": "gear", + "url": "/api/hydrogen/utilities/storefront-api-types" + } + ], + "description": "\n Meant to be used with GraphQL CodeGen to type the Storefront API's custom scalars correctly when using TypeScript.By default, GraphQL CodeGen uses `any` for custom scalars; by using these definitions, GraphQL Codegen will generate the correct types for the Storefront API's custom scalars.\n\nSee more about [GraphQL CodeGen](https://graphql-code-generator.com/) and [custom scalars for TypeScript](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#scalars).\n\nNote that `@shopify/hydrogen-react` has already generated types for the Storefront API, so you may not need to setup GraphQL Codegen on your own.\n ", + "type": "utility", "defaultExample": { "description": "I am the default example", "codeblock": { "tabs": [ { - "title": "JavaScript", - "code": "import {parseGid} from '@shopify/hydrogen';\n\nconst {id, resource} = parseGid('gid://shopify/Order/123');\n\nconsole.log(id); // 123\nconsole.log(resource); // Order\n", + "title": "Codegen Config", + "code": "import {storefrontApiCustomScalars} from '@shopify/hydrogen';\n\nconst config = {\n overwrite: true,\n schema: require.resolve('@shopify/hydrogen/storefront.schema.json'),\n documents: 'pages/**/*.tsx',\n generates: {\n './gql/': {\n preset: 'client',\n plugins: [],\n config: {\n // defines the custom scalars used in the Storefront API\n scalars: storefrontApiCustomScalars,\n },\n },\n },\n};\n\nexport default config;\n", "language": "js" } ], - "title": "Example code" + "title": "codegen.ts" } }, - "definitions": [ - { - "title": "Props", - "description": "", - "type": "ParseGidGeneratedType", - "typeDefinitions": { - "ParseGidGeneratedType": { - "filePath": "src/analytics-utils.ts", - "name": "ParseGidGeneratedType", - "description": "Parses global id (gid) and returns the resource type and id.", - "params": [ - { - "name": "gid", - "description": "A shopify GID (string)", - "value": "string", - "filePath": "src/analytics-utils.ts" - } - ], - "returns": { - "filePath": "src/analytics-utils.ts", - "description": "", - "name": "ShopifyGid", - "value": "ShopifyGid" - }, - "value": "export function parseGid(gid: string | undefined): ShopifyGid {\n const defaultReturn: ShopifyGid = {\n id: '',\n resource: null,\n resourceId: null,\n search: '',\n searchParams: new URLSearchParams(),\n hash: '',\n };\n\n if (typeof gid !== 'string') {\n return defaultReturn;\n }\n\n try {\n const {search, searchParams, pathname, hash} = new URL(gid);\n const pathnameParts = pathname.split('/');\n const lastPathnamePart = pathnameParts[pathnameParts.length - 1];\n const resourcePart = pathnameParts[pathnameParts.length - 2];\n\n if (!lastPathnamePart || !resourcePart) {\n return defaultReturn;\n }\n\n const id = `${lastPathnamePart}${search}${hash}` || '';\n const resourceId = lastPathnamePart || null;\n const resource = resourcePart ?? null;\n\n return {id, resource, resourceId, search, searchParams, hash};\n } catch {\n return defaultReturn;\n }\n}", - "examples": [ - { - "title": "Example", - "description": "", - "tabs": [ - { - "code": "const {id, resource} = parseGid('gid://shopify/Order/123')\n// => id = \"123\", resource = 'Order'\n\n * const {id, resource} = parseGid('gid://shopify/Cart/abc123')\n// => id = \"abc123\", resource = 'Cart'", - "title": "Example" - } - ] - } - ] - }, - "ShopifyGid": { - "filePath": "src/analytics-types.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "ShopifyGid", - "value": "Pick & {\n id: string;\n resource: string | null;\n resourceId: string | null;\n}", - "description": "" - } - } - } - ] + "definitions": [] }, { "name": "Storefront Schema", @@ -28568,130 +28853,5 @@ } }, "definitions": [] - }, - { - "name": "decodeEncodedVariant", - "category": "utilities", - "isVisualComponent": false, - "related": [ - { - "name": "isOptionValueCombinationInEncodedVariant", - "type": "utility", - "url": "/docs/api/hydrogen/2024-10/utilities/isOptionValueCombinationInEncodedVariant" - } - ], - "description": "Decodes an encoded option value string into an array of option value combinations.", - "type": "utility", - "defaultExample": { - "description": "Decode an encoded option value string", - "codeblock": { - "tabs": [ - { - "title": "JavaScript", - "code": "import {decodeEncodedVariant} from '@shopify/hydrogen';\n\n// product.options = [\n// {\n// name: 'Color',\n// optionValues: [\n// {name: 'Red'},\n// {name: 'Blue'},\n// {name: 'Green'},\n// ]\n// },\n// {\n// name: 'Size',\n// optionValues: [\n// {name: 'S'},\n// {name: 'M'},\n// {name: 'L'},\n// ]\n// }\n// ]\n\nconst encodedVariantAvailability = 'v1_0:0-2,1:2,';\n\nconst decodedVariantAvailability = decodeEncodedVariant(\n encodedVariantAvailability,\n);\n\n// decodedVariantAvailability\n// {\n// [0,0], // Red, S\n// [0,1], // Red, M\n// [0,2], // Red, L\n// [1,2] // Blue, L\n// }\n", - "language": "js" - } - ], - "title": "Example code" - } - }, - "definitions": [ - { - "title": "Props", - "description": "", - "type": "DecodeEncodedVariantGeneratedType", - "typeDefinitions": { - "DecodeEncodedVariantGeneratedType": { - "filePath": "src/optionValueDecoder.ts", - "name": "DecodeEncodedVariantGeneratedType", - "description": "For an encoded option value string, decode into option value combinations. Entries represent a valid combination formatted as an array of option value positions.", - "params": [ - { - "name": "encodedVariantField", - "description": "Encoded option value string from the Storefront API, e.g. [product.encodedVariantExistence](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantexistence) or [product.encodedVariantAvailability](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantavailability)", - "value": "string", - "filePath": "src/optionValueDecoder.ts" - } - ], - "returns": { - "filePath": "src/optionValueDecoder.ts", - "description": "Decoded option value combinations", - "name": "DecodedOptionValues", - "value": "DecodedOptionValues" - }, - "value": "export function decodeEncodedVariant(\n encodedVariantField: EncodedVariantField,\n): DecodedOptionValues {\n if (!encodedVariantField) return [];\n\n if (encodedVariantField.startsWith('v1_')) {\n return v1Decoder(stripVersion(encodedVariantField));\n }\n\n throw new Error('Unsupported option value encoding');\n}" - }, - "DecodedOptionValues": { - "filePath": "src/optionValueDecoder.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "DecodedOptionValues", - "value": "number[][]", - "description": "" - } - } - } - ] - }, - { - "name": "isOptionValueCombinationInEncodedVariant", - "category": "utilities", - "isVisualComponent": false, - "related": [ - { - "name": "decodeEncodedVariant", - "type": "utility", - "url": "/docs/api/hydrogen/2024-10/utilities/decodeEncodedVariant" - } - ], - "description": "\n Determines whether an option value combination is present in an encoded option value string.\n\n`targetOptionValueCombination` - Indices of option values to look up in the encoded option value string. A partial set of indices may be passed to determine whether a node or any children is present. For example, if a product has 3 options, passing `[0]` will return true if any option value combination for the first option's option value is present in the encoded string.\n ", - "type": "utility", - "defaultExample": { - "description": "Check if option value is in encoding", - "codeblock": { - "tabs": [ - { - "title": "JavaScript", - "code": "import {isOptionValueCombinationInEncodedVariant} from '@shopify/hydrogen';\n\n// product.options = [\n// {\n// name: 'Color',\n// optionValues: [\n// {name: 'Red'},\n// {name: 'Blue'},\n// {name: 'Green'},\n// ]\n// },\n// {\n// name: 'Size',\n// optionValues: [\n// {name: 'S'},\n// {name: 'M'},\n// {name: 'L'},\n// ]\n// }\n// ]\nconst encodedVariantExistence = 'v1_0:0-1,1:2,';\n\n// For reference: decoded encodedVariantExistence\n// {\n// [0,0], // Red, S\n// [0,1], // Red, M\n// [1,2] // Blue, L\n// }\n\n// Returns true since there are variants exist for [Red]\nisOptionValueCombinationInEncodedVariant([0], encodedVariantExistence); // true\n\nisOptionValueCombinationInEncodedVariant([0, 0], encodedVariantExistence); // true\nisOptionValueCombinationInEncodedVariant([0, 1], encodedVariantExistence); // true\nisOptionValueCombinationInEncodedVariant([0, 2], encodedVariantExistence); // false - no variant exist for [Red, L]\n\n// Returns true since there is a variant exist for [Blue]\nisOptionValueCombinationInEncodedVariant([1], encodedVariantExistence); // true\n\nisOptionValueCombinationInEncodedVariant([1, 0], encodedVariantExistence); // false - no variant exist for [Blue, S]\nisOptionValueCombinationInEncodedVariant([1, 1], encodedVariantExistence); // false - no variant exist for [Blue, M]\nisOptionValueCombinationInEncodedVariant([1, 2], encodedVariantExistence); // true\n\n// Returns false since there is no variant exist for [Green]\nisOptionValueCombinationInEncodedVariant([2], encodedVariantExistence); // false\n", - "language": "js" - } - ], - "title": "Example code" - } - }, - "definitions": [ - { - "title": "Props", - "description": "", - "type": "IsOptionValueCombinationInEncodedVariant", - "typeDefinitions": { - "IsOptionValueCombinationInEncodedVariant": { - "filePath": "src/optionValueDecoder.ts", - "name": "IsOptionValueCombinationInEncodedVariant", - "description": "", - "params": [ - { - "name": "targetOptionValueCombination", - "description": "", - "value": "number[]", - "filePath": "src/optionValueDecoder.ts" - }, - { - "name": "encodedVariantField", - "description": "", - "value": "string", - "filePath": "src/optionValueDecoder.ts" - } - ], - "returns": { - "filePath": "src/optionValueDecoder.ts", - "description": "", - "name": "boolean", - "value": "boolean" - }, - "value": "export type IsOptionValueCombinationInEncodedVariant = (\n targetOptionValueCombination: number[],\n encodedVariantField: string,\n) => boolean;" - } - } - } - ] } ] diff --git a/packages/hydrogen/src/index.ts b/packages/hydrogen/src/index.ts index a42253d7ba..b6b2e2457f 100644 --- a/packages/hydrogen/src/index.ts +++ b/packages/hydrogen/src/index.ts @@ -142,11 +142,16 @@ export { Video, isOptionValueCombinationInEncodedVariant, decodeEncodedVariant, + getProductOptions, + getAdjacentAndFirstAvailableVariants, + mapSelectedProductOptionToObject, + useSelectedOptionInUrlParam, } from '@shopify/hydrogen-react'; export {RichText} from './RichText'; export type { ClientBrowserParameters, + MappedProductOptions, ParsedMetafields, ShopifyAddToCart, ShopifyAddToCartPayload, diff --git a/packages/hydrogen/src/product/useOptimisticVariant.ts b/packages/hydrogen/src/product/useOptimisticVariant.ts index bc702d6d62..b7c88dc851 100644 --- a/packages/hydrogen/src/product/useOptimisticVariant.ts +++ b/packages/hydrogen/src/product/useOptimisticVariant.ts @@ -54,7 +54,7 @@ export function useOptimisticVariant< ), ); }); - }, [variants]); + }, [JSON.stringify(variants)]); if (navigation.state === 'loading') { const queryParams = new URLSearchParams(navigation.location.search); diff --git a/templates/skeleton/app/components/PaginatedResourceSection.tsx b/templates/skeleton/app/components/PaginatedResourceSection.tsx index 804a36f073..d70b6a5e42 100644 --- a/templates/skeleton/app/components/PaginatedResourceSection.tsx +++ b/templates/skeleton/app/components/PaginatedResourceSection.tsx @@ -16,7 +16,7 @@ export function PaginatedResourceSection({ return ( {({nodes, isLoading, PreviousLink, NextLink}) => { - const resoucesMarkup = nodes.map((node, index) => + const resourcesMarkup = nodes.map((node, index) => children({node, index}), ); @@ -26,9 +26,9 @@ export function PaginatedResourceSection({ {isLoading ? 'Loading...' : ↑ Load previous} {resourcesClassName ? ( -
{resoucesMarkup}
+
{resourcesMarkup}
) : ( - resoucesMarkup + resourcesMarkup )} {isLoading ? 'Loading...' : Load more ↓} diff --git a/templates/skeleton/app/components/ProductForm.tsx b/templates/skeleton/app/components/ProductForm.tsx index c5531c5a9f..ac38f68521 100644 --- a/templates/skeleton/app/components/ProductForm.tsx +++ b/templates/skeleton/app/components/ProductForm.tsx @@ -1,35 +1,105 @@ -import {Link} from '@remix-run/react'; -import {type VariantOption, VariantSelector} from '@shopify/hydrogen'; +import {Link, useNavigate} from '@remix-run/react'; +import {type MappedProductOptions} from '@shopify/hydrogen'; import type { - ProductFragment, - ProductVariantFragment, -} from 'storefrontapi.generated'; -import {AddToCartButton} from '~/components/AddToCartButton'; -import {useAside} from '~/components/Aside'; + Maybe, + ProductOptionValueSwatch, +} from '@shopify/hydrogen/storefront-api-types'; +import {AddToCartButton} from './AddToCartButton'; +import {useAside} from './Aside'; +import type {ProductFragment} from 'storefrontapi.generated'; export function ProductForm({ - product, + productOptions, selectedVariant, - variants, }: { - product: ProductFragment; - selectedVariant: ProductFragment['selectedVariant']; - variants: Array; + productOptions: MappedProductOptions[]; + selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; }) { + const navigate = useNavigate(); const {open} = useAside(); return (
- option.optionValues.length > 1, - )} - variants={variants} - selectedVariant={selectedVariant} - > - {({option}) => } - -
+ {productOptions.map((option) => { + // If there is only a single value in the option values, don't display the option + if (option.optionValues.length === 1) return null; + + return ( +
+
{option.name}
+
+ {option.optionValues.map((value) => { + const { + name, + handle, + variantUriQuery, + selected, + available, + exists, + isDifferentProduct, + swatch, + } = value; + + if (isDifferentProduct) { + // SEO + // When the variant is a combined listing child product + // that leads to a different url, we need to render it + // as an anchor tag + return ( + + + + ); + } else { + // SEO + // When the variant is an update to the search param, + // render it as a button with javascript navigating to + // the variant so that SEO bots do not index these as + // duplicated links + return ( + + ); + } + })} +
+
+
+ ) + })} { @@ -53,31 +123,27 @@ export function ProductForm({ ); } -function ProductOptions({option}: {option: VariantOption}) { +function ProductOptionSwatch({ + swatch, + name, +}: { + swatch?: Maybe | undefined; + name: string; +}) { + const image = swatch?.image?.previewImage?.url; + const color = swatch?.color; + + if (!image && !color) return name; + return ( -
-
{option.name}
-
- {option.values.map(({value, isAvailable, isActive, to}) => { - return ( - - {value} - - ); - })} -
-
+
+ {!!image && {name}}
); } diff --git a/templates/skeleton/app/components/SearchResults.tsx b/templates/skeleton/app/components/SearchResults.tsx index c0c63e1e02..70db63af07 100644 --- a/templates/skeleton/app/components/SearchResults.tsx +++ b/templates/skeleton/app/components/SearchResults.tsx @@ -113,12 +113,15 @@ function SearchResultsProducts({ term, }); + const price = product?.selectedOrFirstAvailableVariant?.price; + const image = product?.selectedOrFirstAvailableVariant?.image; + return (
- {product.variants.nodes[0].image && ( + {image && ( {product.title} @@ -126,7 +129,9 @@ function SearchResultsProducts({

{product.title}

- + {price && + + }
diff --git a/templates/skeleton/app/components/SearchResultsPredictive.tsx b/templates/skeleton/app/components/SearchResultsPredictive.tsx index bb4a14735b..901fa1d891 100644 --- a/templates/skeleton/app/components/SearchResultsPredictive.tsx +++ b/templates/skeleton/app/components/SearchResultsPredictive.tsx @@ -213,7 +213,8 @@ function SearchResultsPredictiveProducts({ term: term.current, }); - const image = product?.variants?.nodes?.[0].image; + const price = product?.selectedOrFirstAvailableVariant?.price; + const image = product?.selectedOrFirstAvailableVariant?.image; return (
  • @@ -228,8 +229,8 @@ function SearchResultsPredictiveProducts({

    {product.title}

    - {product?.variants?.nodes?.[0].price && ( - + {price && ( + )}
    diff --git a/templates/skeleton/app/lib/variants.ts b/templates/skeleton/app/lib/variants.ts index ffea0a7306..e74840a8b1 100644 --- a/templates/skeleton/app/lib/variants.ts +++ b/templates/skeleton/app/lib/variants.ts @@ -4,7 +4,7 @@ import {useMemo} from 'react'; export function useVariantUrl( handle: string, - selectedOptions: SelectedOption[], + selectedOptions?: SelectedOption[], ) { const {pathname} = useLocation(); @@ -27,7 +27,7 @@ export function getVariantUrl({ handle: string; pathname: string; searchParams: URLSearchParams; - selectedOptions: SelectedOption[]; + selectedOptions?: SelectedOption[]; }) { const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); const isLocalePathname = match && match.length > 0; @@ -36,7 +36,7 @@ export function getVariantUrl({ ? `${match![0]}products/${handle}` : `/products/${handle}`; - selectedOptions.forEach((option) => { + selectedOptions?.forEach((option) => { searchParams.set(option.name, option.value); }); diff --git a/templates/skeleton/app/routes/collections.$handle.tsx b/templates/skeleton/app/routes/collections.$handle.tsx index f7e4a78bf2..9dcddfb3b1 100644 --- a/templates/skeleton/app/routes/collections.$handle.tsx +++ b/templates/skeleton/app/routes/collections.$handle.tsx @@ -108,8 +108,7 @@ function ProductItem({ product: ProductItemFragment; loading?: 'eager' | 'lazy'; }) { - const variant = product.variants.nodes[0]; - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + const variantUrl = useVariantUrl(product.handle); return ( { - // Log query errors, but don't throw them so the page can still render - console.error(error); - return null; - }); + // Put any API calls that is not critical to be available on first page render + // For example: product reviews, product recommendations, social feeds. - return { - variants, - }; + return {}; } export default function Product() { - const {product, variants} = useLoaderData(); + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( - product.selectedVariant, - variants, + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), ); + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + + // Get the product options array + const productOptions = getProductOptions({ + ...product, + selectedOrFirstAvailableVariant: selectedVariant, + }); + const {title, descriptionHtml} = product; return ( @@ -107,28 +107,10 @@ export default function Product() { compareAtPrice={selectedVariant?.compareAtPrice} />
    - - } - > - - {(data) => ( - - )} - - +

    @@ -202,19 +184,30 @@ const PRODUCT_FRAGMENT = `#graphql handle descriptionHtml description + encodedVariantExistence + encodedVariantAvailability options { name optionValues { name + firstSelectableVariant { + ...ProductVariant + } + swatch { + color + image { + previewImage { + url + } + } + } } } - selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { ...ProductVariant } - variants(first: 1) { - nodes { - ...ProductVariant - } + adjacentVariants (selectedOptions: $selectedOptions) { + ...ProductVariant } seo { description @@ -237,27 +230,3 @@ const PRODUCT_QUERY = `#graphql } ${PRODUCT_FRAGMENT} ` as const; - -const PRODUCT_VARIANTS_FRAGMENT = `#graphql - fragment ProductVariants on Product { - variants(first: 250) { - nodes { - ...ProductVariant - } - } - } - ${PRODUCT_VARIANT_FRAGMENT} -` as const; - -const VARIANTS_QUERY = `#graphql - ${PRODUCT_VARIANTS_FRAGMENT} - query ProductVariants( - $country: CountryCode - $language: LanguageCode - $handle: String! - ) @inContext(country: $country, language: $language) { - product(handle: $handle) { - ...ProductVariants - } - } -` as const; diff --git a/templates/skeleton/app/routes/search.tsx b/templates/skeleton/app/routes/search.tsx index 8a7b11e9c3..2e453c221e 100644 --- a/templates/skeleton/app/routes/search.tsx +++ b/templates/skeleton/app/routes/search.tsx @@ -89,31 +89,33 @@ const SEARCH_PRODUCT_FRAGMENT = `#graphql title trackingParameters vendor - variants(first: 1) { - nodes { - id - image { - url - altText - width - height - } - price { - amount - currencyCode - } - compareAtPrice { - amount - currencyCode - } - selectedOptions { - name - value - } - product { - handle - title - } + selectedOrFirstAvailableVariant( + selectedOptions: [] + ignoreUnknownOptions: true + caseInsensitiveMatch: true + ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode + } + compareAtPrice { + amount + currencyCode + } + selectedOptions { + name + value + } + product { + handle + title } } } @@ -299,19 +301,21 @@ const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql title handle trackingParameters - variants(first: 1) { - nodes { - id - image { - url - altText - width - height - } - price { - amount - currencyCode - } + selectedOrFirstAvailableVariant( + selectedOptions: [] + ignoreUnknownOptions: true + caseInsensitiveMatch: true + ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode } } } diff --git a/templates/skeleton/app/styles/app.css b/templates/skeleton/app/styles/app.css index 819ff58850..d4f9e157a6 100644 --- a/templates/skeleton/app/styles/app.css +++ b/templates/skeleton/app/styles/app.css @@ -12,6 +12,16 @@ img { border-radius: 4px; } +/* +* -------------------------------------------------- +* Non anchor links +* -------------------------------------------------- +*/ +.link:hover { + text-decoration: underline; + cursor: pointer; +} + /* * -------------------------------------------------- * components/Aside @@ -440,8 +450,22 @@ button.reset:hover:not(:has(> *)) { grid-gap: 0.75rem; } -.product-options-item { +.product-options-item, +.product-options-item:disabled { padding: 0.25rem 0.5rem; + background-color: transparent; + font-size: 1rem; + font-family: inherit; +} + +.product-option-label-swatch { + width: 1.25rem; + height: 1.25rem; + margin: 0.25rem 0; +} + +.product-option-label-swatch img { + width: 100%; } /* diff --git a/templates/skeleton/guides/predictiveSearch/predictiveSearch.md b/templates/skeleton/guides/predictiveSearch/predictiveSearch.md index 6a6b32107c..3f3d45835a 100644 --- a/templates/skeleton/guides/predictiveSearch/predictiveSearch.md +++ b/templates/skeleton/guides/predictiveSearch/predictiveSearch.md @@ -87,19 +87,21 @@ const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql title handle trackingParameters - variants(first: 1) { - nodes { - id - image { - url - altText - width - height - } - price { - amount - currencyCode - } + selectedOrFirstAvailableVariant( + selectedOptions: [] + ignoreUnknownOptions: true + caseInsensitiveMatch: true + ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode } } } @@ -358,7 +360,8 @@ SearchResultsPredictive.Products = function ({ trackingParams: product.trackingParameters, term: term.current, }); -+ const image = product?.variants?.nodes?.[0].image; ++ const price = product?.selectedOrFirstAvailableVariant?.price; ++ const image = product?.selectedOrFirstAvailableVariant?.image; return (

  • @@ -373,11 +376,11 @@ SearchResultsPredictive.Products = function ({

    {product.title}

    - {product?.variants?.nodes?.[0].price && ( - - )} ++ {price && ( ++ ++ )}
    diff --git a/templates/skeleton/guides/search/search.md b/templates/skeleton/guides/search/search.md index 537672e5c3..5925d5e017 100644 --- a/templates/skeleton/guides/search/search.md +++ b/templates/skeleton/guides/search/search.md @@ -41,31 +41,33 @@ const SEARCH_PRODUCT_FRAGMENT = `#graphql title trackingParameters vendor - variants(first: 1) { - nodes { - id - image { - url - altText - width - height - } - price { - amount - currencyCode - } - compareAtPrice { - amount - currencyCode - } - selectedOptions { - name - value - } - product { - handle - title - } + selectedOrFirstAvailableVariant( + selectedOptions: [] + ignoreUnknownOptions: true + caseInsensitiveMatch: true + ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode + } + compareAtPrice { + amount + currencyCode + } + selectedOptions { + name + value + } + product { + handle + title } } } diff --git a/templates/skeleton/storefrontapi.generated.d.ts b/templates/skeleton/storefrontapi.generated.d.ts index ee10abe334..d27c594213 100644 --- a/templates/skeleton/storefrontapi.generated.d.ts +++ b/templates/skeleton/storefrontapi.generated.d.ts @@ -494,13 +494,6 @@ export type ProductItemFragment = Pick< minVariantPrice: Pick; maxVariantPrice: Pick; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; }; export type CollectionQueryVariables = StorefrontAPI.Exact<{ @@ -542,13 +535,6 @@ export type CollectionQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -634,13 +620,6 @@ export type CatalogQuery = { 'amount' | 'currencyCode' >; }; - variants: { - nodes: Array<{ - selectedOptions: Array< - Pick - >; - }>; - }; } >; pageInfo: Pick< @@ -750,14 +729,81 @@ export type ProductVariantFragment = Pick< export type ProductFragment = Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { options: Array< Pick & { - optionValues: Array>; + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; } >; - selectedVariant?: StorefrontAPI.Maybe< + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -781,32 +827,6 @@ export type ProductFragment = Pick< >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; seo: Pick; }; @@ -823,14 +843,57 @@ export type ProductQuery = { product?: StorefrontAPI.Maybe< Pick< StorefrontAPI.Product, - 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description' + | 'id' + | 'title' + | 'vendor' + | 'handle' + | 'descriptionHtml' + | 'description' + | 'encodedVariantExistence' + | 'encodedVariantAvailability' > & { options: Array< Pick & { - optionValues: Array>; + optionValues: Array< + Pick & { + firstSelectableVariant?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.ProductVariant, + 'availableForSale' | 'id' | 'sku' | 'title' + > & { + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + image?: StorefrontAPI.Maybe< + {__typename: 'Image'} & Pick< + StorefrontAPI.Image, + 'id' | 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + product: Pick; + selectedOptions: Array< + Pick + >; + unitPrice?: StorefrontAPI.Maybe< + Pick + >; + } + >; + swatch?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe<{ + previewImage?: StorefrontAPI.Maybe< + Pick + >; + }>; + } + >; + } + >; } >; - selectedVariant?: StorefrontAPI.Maybe< + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -854,76 +917,7 @@ export type ProductQuery = { >; } >; - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; - seo: Pick; - } - >; -}; - -export type ProductVariantsFragment = { - variants: { - nodes: Array< - Pick< - StorefrontAPI.ProductVariant, - 'availableForSale' | 'id' | 'sku' | 'title' - > & { - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - image?: StorefrontAPI.Maybe< - {__typename: 'Image'} & Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - product: Pick; - selectedOptions: Array< - Pick - >; - unitPrice?: StorefrontAPI.Maybe< - Pick - >; - } - >; - }; -}; - -export type ProductVariantsQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; - handle: StorefrontAPI.Scalars['String']['input']; -}>; - -export type ProductVariantsQuery = { - product?: StorefrontAPI.Maybe<{ - variants: { - nodes: Array< + adjacentVariants: Array< Pick< StorefrontAPI.ProductVariant, 'availableForSale' | 'id' | 'sku' | 'title' @@ -947,31 +941,30 @@ export type ProductVariantsQuery = { >; } >; - }; - }>; + seo: Pick; + } + >; }; export type SearchProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'handle' | 'id' | 'publishedAt' | 'title' | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; }; export type SearchPageFragment = {__typename: 'Page'} & Pick< @@ -1031,26 +1024,24 @@ export type RegularSearchQuery = { | 'trackingParameters' | 'vendor' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - compareAtPrice?: StorefrontAPI.Maybe< - Pick - >; - selectedOptions: Array< - Pick - >; - product: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + compareAtPrice?: StorefrontAPI.Maybe< + Pick + >; + selectedOptions: Array< + Pick + >; + product: Pick; + } + >; } >; pageInfo: Pick< @@ -1088,16 +1079,14 @@ export type PredictiveProductFragment = {__typename: 'Product'} & Pick< StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick + >; + price: Pick; + } + >; }; export type PredictiveQueryFragment = { @@ -1153,19 +1142,17 @@ export type PredictiveSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'handle' | 'trackingParameters' > & { - variants: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'url' | 'altText' | 'width' | 'height' - > - >; - price: Pick; - } - >; - }; + selectedOrFirstAvailableVariant?: StorefrontAPI.Maybe< + Pick & { + image?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Image, + 'url' | 'altText' | 'width' | 'height' + > + >; + price: Pick; + } + >; } >; queries: Array< @@ -1210,7 +1197,7 @@ interface GeneratedQueryTypes { return: BlogsQuery; variables: BlogsQueryVariables; }; - '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { + '#graphql\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n query Collection(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n': { return: CollectionQuery; variables: CollectionQueryVariables; }; @@ -1218,7 +1205,7 @@ interface GeneratedQueryTypes { return: StoreCollectionsQuery; variables: StoreCollectionsQueryVariables; }; - '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n variants(first: 1) {\n nodes {\n selectedOptions {\n name\n value\n }\n }\n }\n }\n\n': { + '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': { return: CatalogQuery; variables: CatalogQueryVariables; }; @@ -1234,19 +1221,15 @@ interface GeneratedQueryTypes { return: PoliciesQuery; variables: PoliciesQueryVariables; }; - '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n optionValues {\n name\n }\n }\n selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { + '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n encodedVariantExistence\n encodedVariantAvailability\n options {\n name\n optionValues {\n name\n firstSelectableVariant {\n ...ProductVariant\n }\n swatch {\n color\n image {\n previewImage {\n url\n }\n }\n }\n }\n }\n selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n adjacentVariants (selectedOptions: $selectedOptions) {\n ...ProductVariant\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': { return: ProductQuery; variables: ProductQueryVariables; }; - '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': { - return: ProductVariantsQuery; - variables: ProductVariantsQueryVariables; - }; - '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { + '#graphql\n query RegularSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $term: String!\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n articles: search(\n query: $term,\n types: [ARTICLE],\n first: $first,\n ) {\n nodes {\n ...on Article {\n ...SearchArticle\n }\n }\n }\n pages: search(\n query: $term,\n types: [PAGE],\n first: $first,\n ) {\n nodes {\n ...on Page {\n ...SearchPage\n }\n }\n }\n products: search(\n after: $endCursor,\n before: $startCursor,\n first: $first,\n last: $last,\n query: $term,\n sortKey: RELEVANCE,\n types: [PRODUCT],\n unavailableProducts: HIDE,\n ) {\n nodes {\n ...on Product {\n ...SearchProduct\n }\n }\n pageInfo {\n ...PageInfoFragment\n }\n }\n }\n #graphql\n fragment SearchProduct on Product {\n __typename\n handle\n id\n publishedAt\n title\n trackingParameters\n vendor\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n\n #graphql\n fragment SearchPage on Page {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment SearchArticle on Article {\n __typename\n handle\n id\n title\n trackingParameters\n }\n\n #graphql\n fragment PageInfoFragment on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n\n': { return: RegularSearchQuery; variables: RegularSearchQueryVariables; }; - '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { + '#graphql\n query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $term: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $term,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n #graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n #graphql\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n #graphql\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n #graphql\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; };