Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ci] release 2024-10 #2657

Merged
merged 1 commit into from
Dec 11, 2024
Merged

[ci] release 2024-10 #2657

merged 1 commit into from
Dec 11, 2024

Conversation

shopify-github-actions-access[bot]
Copy link
Contributor

@shopify-github-actions-access shopify-github-actions-access bot commented Nov 26, 2024

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@shopify/cli-hydrogen@9.0.3

Patch Changes

@shopify/create-hydrogen@5.0.12

Patch Changes

@shopify/hydrogen@2024.10.1

Patch Changes

  • Added namespace support to prevent conflicts when using multiple Pagination components: (#2649) by @scottdixon

    • New optional namespace prop for the <Pagination/> component
    • New optional namespace option for getPaginationVariables() utility
    • When specified, pagination URL parameters are prefixed with the namespace (e.g., products_cursor instead of cursor)
    • Maintains backwards compatibility when no namespace is provided
  • Introduce getProductOptions, getAdjacentAndFirstAvailableVariants, useSelectedOptionInUrlParam, and mapSelectedProductOptionToObject to support combined listing products and products with 2000 variants limit. (#2659) by @wizardlyhel

  • Add params to override the login and authorize paths: (#2648) by @blittle

    const hydrogenContext = createHydrogenContext({
      // ...
      customerAccount: {
        loginPath = '/account/login',
        authorizePath = '/account/authorize',
        defaultRedirectPath = '/account',
      },
    });
  • Add selectedVariant prop to the VariantSelector to use for the initial state if no URL parameters are set (#2643) by @scottdixon

  • Updated dependencies [a57d5267]:

    • @shopify/hydrogen-react@2024.10.1

@shopify/hydrogen-react@2024.10.1

Patch Changes

  • Introduce getProductOptions, getAdjacentAndFirstAvailableVariants, useSelectedOptionInUrlParam, and mapSelectedProductOptionToObject to support combined listing products and products with 2000 variants limit. (#2659) by @wizardlyhel

skeleton@2024.10.2

Patch Changes

  • Remove initial redirect from product display page (#2643) by @scottdixon

  • Optional updates for the product route and product form to handle combined listing and 2000 variant limit. (#2659) by @wizardlyhel

    1. Update your SFAPI product query to bring in the new query fields:
    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;
    1. Update loadDeferredData function. We no longer need to load in all the variants. You can also remove VARIANTS_QUERY variable.
    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,
    -  };
    }
    1. Remove the redirect logic in the loadCriticalData function and completely remove redirectToFirstVariant function
    async function loadCriticalData({
      context,
      params,
      request,
    }: LoaderFunctionArgs) {
      const {handle} = params;
      const {storefront} = context;
      if (!handle) {
        throw new Error('Expected product handle to be defined');
      }
      const [{product}] = await Promise.all([
        storefront.query(PRODUCT_QUERY, {
          variables: {handle, selectedOptions: getSelectedProductOptions(request)},
        }),
        // Add other queries here, so that they are loaded in parallel
      ]);
    
      if (!product?.id) {
        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,
      };
    }
    
    ...
    
    -  function redirectToFirstVariant({
    -    product,
    -    request,
    -  }: {
    -    product: ProductFragment;
    -    request: Request;
    -  }) {
    -    ...
    -  }
    1. Update the Product component to use the new data fields.
    import {
      getSelectedProductOptions,
      Analytics,
      useOptimisticVariant,
    +  getAdjacentAndFirstAvailableVariants,
    } from '@shopify/hydrogen';
    
    export default function Product() {
    +  const {product} = useLoaderData<typeof loader>();
    -  const {product, variants} = useLoaderData<typeof loader>();
    
    +  // Optimistically selects a variant with given available variant information
    +  const selectedVariant = useOptimisticVariant(
    +    product.selectedOrFirstAvailableVariant,
    +    getAdjacentAndFirstAvailableVariants(product),
    +  );
    -  const selectedVariant = useOptimisticVariant(
    -    product.selectedVariant,
    -    variants,
    -  );
    1. Handle missing search query param in url from selecting a first variant
    import {
      getSelectedProductOptions,
      Analytics,
      useOptimisticVariant,
      getAdjacentAndFirstAvailableVariants,
    +  useSelectedOptionInUrlParam,
    } from '@shopify/hydrogen';
    
    export default function Product() {
      const {product} = useLoaderData<typeof loader>();
    
      // 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
    +  useSelectedOptionInUrlParam(selectedVariant.selectedOptions);
    1. Get the product options array using getProductOptions
    import {
      getSelectedProductOptions,
      Analytics,
      useOptimisticVariant,
    +  getProductOptions,
      getAdjacentAndFirstAvailableVariants,
      useSelectedOptionInUrlParam,
    } from '@shopify/hydrogen';
    
    export default function Product() {
      const {product} = useLoaderData<typeof loader>();
    
      // 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
      useSelectedOptionInUrlParam(selectedVariant.selectedOptions);
    
    +  // Get the product options array
    +  const productOptions = getProductOptions({
    +    ...product,
    +    selectedOrFirstAvailableVariant: selectedVariant,
    +  });
    1. Remove the Await and Suspense from the ProductForm. We no longer have any queries that we need to wait for.
    export default function Product() {
    
      ...
    
      return (
        ...
    +        <ProductForm
    +          productOptions={productOptions}
    +          selectedVariant={selectedVariant}
    +        />
    -        <Suspense
    -          fallback={
    -            <ProductForm
    -              product={product}
    -              selectedVariant={selectedVariant}
    -              variants={[]}
    -            />
    -          }
    -        >
    -          <Await
    -            errorElement="There was a problem loading product variants"
    -            resolve={variants}
    -          >
    -            {(data) => (
    -              <ProductForm
    -                product={product}
    -                selectedVariant={selectedVariant}
    -                variants={data?.product?.variants.nodes || []}
    -              />
    -            )}
    -          </Await>
    -        </Suspense>
    1. Update the ProductForm component.
    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 (
        <div className="product-form">
          {productOptions.map((option) => (
            <div className="product-options" key={option.name}>
              <h5>{option.name}</h5>
              <div className="product-options-grid">
                {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 (
                      <Link
                        className="product-options-item"
                        key={option.name + name}
                        prefetch="intent"
                        preventScrollReset
                        replace
                        to={`/products/${handle}?${variantUriQuery}`}
                        style={{
                          border: selected
                            ? '1px solid black'
                            : '1px solid transparent',
                          opacity: available ? 1 : 0.3,
                        }}
                      >
                        <ProductOptionSwatch swatch={swatch} name={name} />
                      </Link>
                    );
                  } 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 (
                      <button
                        type="button"
                        className={`product-options-item${
                          exists && !selected ? ' link' : ''
                        }`}
                        key={option.name + name}
                        style={{
                          border: selected
                            ? '1px solid black'
                            : '1px solid transparent',
                          opacity: available ? 1 : 0.3,
                        }}
                        disabled={!exists}
                        onClick={() => {
                          if (!selected) {
                            navigate(`?${variantUriQuery}`, {
                              replace: true,
                            });
                          }
                        }}
                      >
                        <ProductOptionSwatch swatch={swatch} name={name} />
                      </button>
                    );
                  }
                })}
              </div>
              <br />
            </div>
          ))}
          <AddToCartButton
            disabled={!selectedVariant || !selectedVariant.availableForSale}
            onClick={() => {
              open('cart');
            }}
            lines={
              selectedVariant
                ? [
                    {
                      merchandiseId: selectedVariant.id,
                      quantity: 1,
                      selectedVariant,
                    },
                  ]
                : []
            }
          >
            {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
          </AddToCartButton>
        </div>
      );
    }
    
    function ProductOptionSwatch({
      swatch,
      name,
    }: {
      swatch?: Maybe<ProductOptionValueSwatch> | undefined;
      name: string;
    }) {
      const image = swatch?.image?.previewImage?.url;
      const color = swatch?.color;
    
      if (!image && !color) return name;
    
      return (
        <div
          aria-label={name}
          className="product-option-label-swatch"
          style={{
            backgroundColor: color || 'transparent',
          }}
        >
          {!!image && <img src={image} alt={name} />}
        </div>
      );
    }
    1. Update app.css
    +  /*
    +  * --------------------------------------------------
    +  * 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%;
    +  }
    1. Update lib/variants.ts

    Make useVariantUrl and getVariantUrl flexible to supplying a selected option param

    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);
      });
    1. 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:

    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

    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 (
    1. Update routes/collections.all.tsx

    Same reasoning as collections.$handle.tsx

    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

    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 (
    1. Update routes/search.tsx

    Instead of using the first variant, use selectedOrFirstAvailableVariant

    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;
    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
            }
         }
    -    }
      }
    1. Update components/SearchResults.tsx
    function SearchResultsProducts({
      term,
      products,
    }: PartialSearchResult<'products'>) {
      if (!products?.nodes.length) {
        return null;
      }
    
      return (
        <div className="search-result">
          <h2>Products</h2>
          <Pagination connection={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 (
                  <div className="search-results-item" key={product.id}>
                    <Link prefetch="intent" to={productUrl}>
    -                  {product.variants.nodes[0].image && (
    +                  {image && (
                        <Image
    -                      data={product.variants.nodes[0].image}
    +                      data={image}
                          alt={product.title}
                          width={50}
                        />
                      )}
                      <div>
                        <p>{product.title}</p>
                        <small>
    -                      <Money data={product.variants.nodes[0].price} />
    +                      {price &&
    +                        <Money data={price} />
    +                      }
                        </small>
                      </div>
                    </Link>
                  </div>
                );
              });
    1. Update components/SearchResultsPredictive.tsx
    function SearchResultsPredictiveProducts({
      term,
      products,
      closeSearch,
    }: PartialPredictiveSearchResult<'products'>) {
      if (!products.length) return null;
    
      return (
        <div className="predictive-search-result" key="products">
          <h5>Products</h5>
          <ul>
            {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 (
                <li className="predictive-search-result-item" key={product.id}>
                  <Link to={productUrl} onClick={closeSearch}>
                    {image && (
                      <Image
                        alt={image.altText ?? ''}
                        src={image.url}
                        width={50}
                        height={50}
                      />
                    )}
                    <div>
                      <p>{product.title}</p>
                      <small>
    -                    {product?.variants?.nodes?.[0].price && (
    +                    {price && (
    -                      <Money data={product.variants.nodes[0].price} />
    +                      <Money data={price} />
                        )}
                      </small>
                    </div>
                  </Link>
                </li>
              );
            })}
          </ul>
        </div>
      );
    }
  • Update Aside to have an accessible close button label (#2639) by @lb-

  • Fix cart route so that it works with no-js (#2665) by @wizardlyhel

  • Bump Shopify cli version (#2667) by @wizardlyhel

  • Updated dependencies [8f64915e, a57d5267, 91d60fd2, 23a80f3e]:

    • @shopify/hydrogen@2024.10.1

Copy link
Contributor

shopify bot commented Nov 26, 2024

Oxygen deployed a preview of your changeset-release/main branch. Details:

Storefront Status Preview link Deployment details Last update (UTC)
Skeleton (skeleton.hydrogen.shop) ✅ Successful (Logs) Preview deployment Inspect deployment November 26, 2024 9:18 PM
custom-cart-method ✅ Successful (Logs) Preview deployment Inspect deployment November 26, 2024 9:18 PM
classic-remix ✅ Successful (Logs) Preview deployment Inspect deployment November 26, 2024 9:18 PM
metaobjects ✅ Successful (Logs) Preview deployment Inspect deployment November 26, 2024 9:18 PM
third-party-queries-caching ✅ Successful (Logs) Preview deployment Inspect deployment November 26, 2024 9:18 PM

Learn more about Hydrogen's GitHub integration.

@github-actions github-actions bot force-pushed the changeset-release/main branch from 51653d1 to db3b71e Compare November 27, 2024 18:18
@github-actions github-actions bot force-pushed the changeset-release/main branch 9 times, most recently from 6acb9aa to 594be1b Compare December 11, 2024 16:16
@github-actions github-actions bot force-pushed the changeset-release/main branch from 594be1b to 870b943 Compare December 11, 2024 16:53
@wizardlyhel wizardlyhel merged commit c915b67 into main Dec 11, 2024
@wizardlyhel wizardlyhel deleted the changeset-release/main branch December 11, 2024 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant