3.2 -> 3.3
This page lists the highlights for upgrading a project from Front-Commerce 3.2 to 3.3.
Update dependencies
Update all your @front-commerce/*
dependencies to this version:
pnpm update "@front-commerce/*@3.3.0"
Automated Migration
We provide a codemod to automatically update your codebase to the latest version
of Front-Commerce. This tool will update your code when possible and flag the
places where you need to manually update your code (with // TODO Codemod
comments).
pnpm run front-commerce migrate --transform 3.3.0
Search for TODO Codemod
comments in your codebase. They could be added to for
the following manual updates:
- removing
Set-Cookie
headers defined in routes, they are now automatically set by Front-Commerce when needed (for a user session commit). See Automatic session cookie headers for details. - simplifying the
entry.server.tsx
file after automatic removal of transformers
Code changes
Remix entrypoints
In this release, we improved features that required to hook into your Remix
application files created with the initial skeleton.
You must update these
files in your application, as detailed below or copy the ones from the latest
skeleton.
root.tsx
file
diff --git a/app/root.tsx b/app/root.tsx
index a28dcf87e..68e788d96 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -21,30 +21,15 @@ import {
import { usePageProgress } from "theme/components/helpers/usePageProgress";
import "theme/main.css";
import config from "~/config/website";
-import { AppQueryDocument } from "~/graphql/graphql";
import { LiveReload, useSWEffect } from "@remix-pwa/sw";
import manifest from "~/manifest";
export const loader = async ({ context }: LoaderFunctionArgs) => {
const app = new FrontCommerceApp(context.frontCommerce);
- const response = await app.graphql.query(AppQueryDocument);
-
- if (!response.shop) {
- throw new Response("", { status: 500, statusText: "Shop not found" });
- }
-
return json({
- shop: response.shop,
device: app.config.device,
- process: {
- env: {
- NODE_ENV: process.env.NODE_ENV,
- SERVER: false,
- FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE:
- process.env.FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE,
- },
- },
+ publicConfig: app.config.public,
});
};
@@ -62,7 +62,7 @@ export const meta: MetaFunction = (args) => {
{ title: config.defaultTitle },
{ name: "robots", content: "Index,Follow" },
{ name: "description", content: config.defaultDescription },
- { name: "baseUrl", content: args.data?.shop?.url },
+ { name: "baseUrl", content: args.data?.publicConfig?.shop?.url },
...manifest?.pwa.metas,
];
if (config.themeColor) {
entry.client.tsx
file
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
index e1530e9f8..dcb9118c1 100644
--- a/app/entry.client.tsx
+++ b/app/entry.client.tsx
@@ -21,7 +21,9 @@ const StrictModeOrNoop = appManifest.v2_compat?.useApolloClientQueries
: StrictMode;
const hydrate = async () => {
- const shop = __remixContext.state.loaderData?.root.shop;
+ const rootLoaderData = __remixContext.state.loaderData?.root;
+ const shop = rootLoaderData?.publicConfig?.shop;
+
const messages = await fetch(`/translations/${shop?.locale}`).then(
(res) => res.json() || {}
);
entry.server.tsx
file
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index ffd0db3d2..b39a41f34 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -32,7 +32,8 @@ export default async function handleRequest(
remixContext: EntryContext,
loadContext: AppLoadContext
) {
- const shop = remixContext.staticHandlerContext.loaderData.root.shop;
+ const rootLoaderData = remixContext.state.loaderData?.root;
+ const shop = rootLoaderData?.publicConfig?.shop;
const messages = await loadTranslationMessages(shop.locale);
return isbot(request.headers.get("user-agent"))
Front-Commerce post-install
script
In this release, we
added a post-install
script
in order to patch an issue with the current version of @remix-run/dev
we are
using. To cope with this, please ensure you applied have the following change in
your
package.json
:
// ...
"scripts": {
"build": "front-commerce build",
"dev": "front-commerce dev --manual -c \"pnpm run dev:server\"",
"dev:debug": "front-commerce dev --manual -c \"pnpm run dev:server --inspect\"",
"dev:server": "tsx watch --ignore ./build/version.txt --ignore ./build/index.js --clear-screen=false -r tsconfig-paths/register server.ts",
"start": "cross-env NODE_ENV=production tsx -r tsconfig-paths/register ./server.ts",
"translate": "front-commerce translate ./app/**/*.{js,jsx,ts,tsx} --locale en",
"front-commerce": "front-commerce",
- "typecheck": "tsc"
+ "typecheck": "tsc",
+ "postinstall": "front-commerce postinstall"
},
// ...
Automatic session cookie headers
In this release, we added a feature that will automatically set the Set-Cookie
header for the session cookie when the user session needs to be commited.
import { FrontCommerceApp } from "@front-commerce/remix";
import { json } from "@front-commerce/remix/node";
import { MyQuery } from "~/graphql/graphql";
const loader = async ({ context, request }) => {
const app = new FrontCommerceApp(context.frontCommerce);
const data = app.query(MyQuery);
- return json(data, {
- headers: {
- // TODO Codemod FC-3.3: please, remove the `Set-Cookie` header manually. Session is now automatically commited by FC.
- "Set-Cookie": await app.user.session.commit(),
- },
- });
+ return json(data);
}
Deprecate process.env.FRONT_COMMERCE_WEB_*
usage
Breaking: In this release, we updated the way we handle public configuration.
To cope with these changes, you will need to ensure that you replace any of
these usages with the new publicConfig
values. We introduced a 3.3.0
codemod
to automatically update your codebase, follow the
Automated Migration to run the codemod.
Before:
const {
FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE,
FRONT_COMMERCE_WEB_DEV_DISABLE_PASSWORD_HINT,
} = process.env;
const example = {
analyticsWarningDisable: FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE,
disablePasswordHint: FRONT_COMMERCE_WEB_DEV_DISABLE_PASSWORD_HINT,
myCustomValue: process.env.FRONT_COMMERCE_WEB_MY_CUSTOM_VALUE,
};
After:
import { getPublicConfig } from "@front-commerce/core/react";
// After running your application for the first time,
// you should have all typed values in the public config
const publicConfig = getPublicConfig();
const example = {
analyticsWarningDisable: publicConfig?.analytics?.disableDevWarning ?? false,
disablePasswordHint: publicConfig?.password?.disablePasswordHint ?? false,
myCustomValue: getPublicConfig("FRONT_COMMERCE_WEB_MY_CUSTOM_VALUE"), // This will warn with a deprecation message
};
We highly suggest that you move any of your custom values to a new config
provider which extends the public
schema.
See Public configurations documentation for more details.
Deprecate getCurrentShopConfig
usage
In this version we have deprecated the usages of getCurrentShopConfig
in favor
config.shop
, the function has been moved to
@front-commerce/compat/shop/getShopConfig
for compatibility reasons, but we
suggest you to use config.shop
instead.
// from a loader
export const loader = async ({ context }) => {
const app = new FrontCommerceApp(context.frontCommerce);
const currentShopConfig = app.config.shop;
// ...
};
// from an action
export const action = async ({ context }) => {
const app = new FrontCommerceApp(context.frontCommerce);
const currentShopConfig = app.config.shop;
// ...
};
// from a graphql resolver
export default {
Query: {
myQuery: async (parent, args, context, info) => {
const currentShopConfig = context.config.shop;
// ...
},
},
};
Theme chocolatine styles refactor
In this release, we refactored how the design tokens are included in the style
sheets. They are now included through the
_design-tokens.scss
file.
If you have overwritten the
theme/main.scss
then you should either apply the changes as detailed below or
copy the file from the latest skeleton.
theme/components/_components.scss
has been completely removedtheme/main.scss
design tokens importationtheme/main.scss@import "theme/normalize";
@import "theme/variables";
@import "theme/components/components";
@import "theme/design-tokens";
Requisition list related theme changes
Since version 3.3.3
In this release, we fixed some issues related to the requisition lists in the
theme.
If your project is using the requisition lists feature, you will
need to update those files as detailed below, or copy the ones from the latest
skeleton:
theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx
file
diff --git a/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx b/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx
index 3d7bcfc99..6c039d270 100644
--- a/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx
+++ b/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx
@@ -2,6 +2,7 @@ import { useMemo } from "react";
import PropTypes from "prop-types";
import AddToRequisitionList from "theme/modules/RequisitionList/AddToRequisitionList";
import { resolveSelectedOptions } from "theme/pages/Product/useSelectedProductWithConfigurableOptions";
+import { resolveSelectedBundleOptions } from "theme/pages/Product/useSelectedProductWithBundleOptions";
import {
productPropTypes,
selectedConfigurableOptionsPropTypes,
@@ -11,7 +12,7 @@ import {
const AddCartToRequisitionList = ({ id = "cart", cart, size }) => {
const cartItems = cart.items;
const items = useMemo(() => {
- return cartItems.map(({ product, options, qty }) => {
+ return cartItems.map(({ product, options, bundleOptions, qty }) => {
const productOptions = product.options?.map((option) => ({
id: option.attribute.id,
label: option.attribute.label,
@@ -23,6 +24,10 @@ const AddCartToRequisitionList = ({ id = "cart", cart, size }) => {
product.options,
options
),
+ selectedBundleOptions: resolveSelectedBundleOptions(
+ product.bundleOptions,
+ bundleOptions
+ ),
quantity: qty,
};
});
theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx
file
diff --git a/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx b/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx
index 0e5cc0958..d73bde2aa 100644
--- a/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx
+++ b/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx
@@ -34,6 +34,10 @@ const AddProductToRequisitionList = ({
showOptionsModalIfNotFullyConfigured={
showOptionsModalIfNotFullyConfigured
}
+ redirectOnAddToRequisitionList={
+ // URL to the bundle product
+ product.bundleOptions?.length ? `/product/${product.sku}` : null
+ }
/>
);
};
theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx
file
diff --git a/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx b/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx
index 13efd3b41..a79e5b025 100644
--- a/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx
+++ b/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx
@@ -5,6 +5,7 @@ import Icon from "theme/components/atoms/Icon";
import SelectMenu from "theme/components/molecules/SelectMenu";
import messages from "theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionListMessages";
import { useIntl } from "react-intl";
+import { useNavigate } from "@remix-run/react";
/** @type {import('./EnhanceAddToRequisitionList').BaseComponent} */
const AddToRequisitionList = ({
@@ -22,11 +23,24 @@ const AddToRequisitionList = ({
addToRequisitionListError,
newRequisitionListModal,
showOptionsModalIfNotFullyConfigured,
+ redirectOnAddToRequisitionList,
productConfigurationModal,
}) => {
const intl = useIntl();
const [isRequisitionListMenuOpen, setIsRequisitionListMenuOpen] =
useState(false);
+ const navigate = useNavigate();
+
+ const shouldRedirect =
+ showOptionsModalIfNotFullyConfigured &&
+ isRequisitionListMenuOpen &&
+ redirectOnAddToRequisitionList;
+
+ useEffect(() => {
+ if (shouldRedirect) {
+ navigate(redirectOnAddToRequisitionList);
+ }
+ }, [shouldRedirect]);
const selectItems = useMemo(() => {
if (!requisitionLists) {
theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx
file
diff --git a/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx b/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx
index ff92ad7e1..e0ca0580e 100644
--- a/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx
+++ b/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx
@@ -75,20 +75,29 @@ const withAddMultipleItemsToRequisitionListMutation =
}) => ({
sku: product.sku,
quantity,
- selectedConfigurableOptions: Object.entries(
- selectedConfigurableOptions
- ).map(([option_id, option_value]) => ({
- option_id,
- option_value,
- })),
- selectedBundleOptions: Object.entries(
- selectedBundleOptions
- ).map(([option_id, { quantity, values }]) => {
- return {
- option_id,
- option_values: values.map((value) => ({ quantity, value })),
- };
- }),
+ selectedConfigurableOptions:
+ Object.keys(selectedConfigurableOptions || {}).length > 0
+ ? Object.entries(selectedConfigurableOptions).map(
+ ([option_id, option_value]) => ({
+ option_id,
+ option_value,
+ })
+ )
+ : undefined,
+ selectedBundleOptions:
+ Object.keys(selectedBundleOptions || {}).length > 0
+ ? Object.entries(selectedBundleOptions).map(
+ ([option_id, { quantity, values }]) => {
+ return {
+ option_id,
+ option_values: values.map((value) => ({
+ quantity,
+ value,
+ })),
+ };
+ }
+ )
+ : undefined,
})
),
},
theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx
file
diff --git a/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx b/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx
index 5883fae10..e0449a188 100644
--- a/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx
+++ b/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx
@@ -6,7 +6,7 @@ import { FormattedMessage } from "react-intl";
import useSelectedProductWithConfigurableOptions from "theme/pages/Product/useSelectedProductWithConfigurableOptions";
import ConfigurableOptions from "theme/modules/Cart/CartItem/CartItemOptionsUpdater/ConfigurableOptions";
import { H2 } from "theme/components/atoms/Typography/Heading";
-import Form from "theme/compat/components/atoms/Form/Form";
+import { Form } from "@remix-run/react";
import FormTitle from "theme/components/molecules/Form/FormTitle";
import useProductBySkuLoader from "theme/hooks/useProductBySkuLoader";
import Stack from "theme/components/atoms/Layout/Stack";
@@ -74,21 +74,22 @@ const ProductConfigurationModalContent = ({
selectedConfigurableOptions
);
- const formRef = useRef();
const [showNotAllOptionsSelected, setShowNotAllOptionsSelected] =
useState(false);
- const onChangeOptions = useCallback(() => {
- const model = formRef.current.getModel();
- Object.keys(model)
- .filter(
- (key) =>
- key.indexOf("custom:") !== 0 && typeof model[key] !== "undefined"
- )
- .forEach((optionId) =>
- setOption(optionId, model[optionId].value || model[optionId])
- );
- }, [setOption]);
+ const onChangeOptions = useCallback(
+ (e) => {
+ const input = e.target;
+ const form = input.form;
+ const data = new FormData(form);
+ for (const pair of data.entries()) {
+ if (pair[0].indexOf("custom:") !== 0) {
+ setOption(pair[0], pair[1]);
+ }
+ }
+ },
+ [setOption]
+ );
const allOptionsSet = useMemo(
() => selectedProduct && areAllOptionsSet(selectedProduct, selectedOptions),
@@ -119,8 +120,7 @@ const ProductConfigurationModalContent = ({
return (
<Form
- setRef={(form) => (formRef.current = form)}
- onValidSubmit={() => onConfiurationsSelected(selectedOptions)}
+ onSubmit={() => onConfiurationsSelected(selectedOptions)}
onChange={onChangeOptions}
>
<Stack>
theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql
file
diff --git a/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql b/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql
index 19f85b84e..954bbb896 100644
--- a/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql
+++ b/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql
@@ -17,6 +17,14 @@ fragment CartItemCustomOption on Product {
id
}
}
+ bundleOptions {
+ id
+ label
+ values {
+ label
+ value
+ }
+ }
custom_options {
option_id
title