3.1 -> 3.2
This page lists the highlights for upgrading a project from Front-Commerce 3.1 to 3.2.
Update dependencies
Update all your @front-commerce/*
dependencies to this version:
pnpm update "@front-commerce/*@3.2.0"
Code changes
Remix entrypoints
In this release, we added new features that required to hook into your Remix
application entrypoints created with the initial skeleton.
You must update
these files in your application, as detailed below or copy the ones from the
latest skeleton.
entry.worker.ts
file
diff --git a/app/entry.worker.ts b/app/entry.worker.ts
index 84d71494..62c36c4c 100644
--- a/app/entry.worker.ts
+++ b/app/entry.worker.ts
@@ -1,4 +1,6 @@
/// <reference lib="WebWorker" />
+import type { WorkerDataFunctionArgs } from '@remix-pwa/sw'
+import { synchronizeStorefrontContentFromResponse } from 'theme/modules/StorefrontContent/serviceWorker'
export type {}
declare let self: ServiceWorkerGlobalScope
@@ -10,3 +12,14 @@ self.addEventListener('install', (event: ExtendableEvent) => {
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(self.clients.claim())
})
+
+export const defaultFetchHandler = async ({ request }: WorkerDataFunctionArgs) => {
+ const response = await fetch(request)
+ if (!response.ok) {
+ return response
+ }
+
+ synchronizeStorefrontContentFromResponse(response, self.clients)
+
+ return response
+}
diff --git a/app/theme/layouts/GenericLayout.jsx b/app/theme/layouts/GenericLayout.jsx
index 398d02c2..9825d487 100644
entry.server.ts
file
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index bdb7e77a..4ece0e63 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -5,7 +5,7 @@
*/
import { PassThrough } from 'node:stream'
-import type { EntryContext } from '@remix-run/node'
+import type { AppLoadContext, DataFunctionArgs, EntryContext } from '@remix-run/node'
import { RemixServer } from '@remix-run/react'
import isbot from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
@@ -13,6 +13,7 @@ import { FrontCommerceServer } from '@front-commerce/remix/react'
import prepareV2ServerRenderedApp from '@front-commerce/compat/apollo/prepareV2ServerRenderedApp'
import { loadTranslationMessages } from '@front-commerce/core'
import appManifest from '~/manifest'
+import { transformBrowserResponse, transformDataResponse, transformBotResponse } from '@front-commerce/remix'
const ABORT_DELAY = 5_000
@@ -20,14 +21,15 @@ export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
- remixContext: EntryContext
+ remixContext: EntryContext,
+ loadContext: AppLoadContext
) {
const shop = remixContext.staticHandlerContext.loaderData.root.shop
const messages = await loadTranslationMessages(shop.locale)
return isbot(request.headers.get('user-agent'))
- ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext, messages)
- : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext, messages)
+ ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext, loadContext, messages)
+ : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext, loadContext, messages)
}
function handleBotRequest(
@@ -35,6 +37,7 @@ function handleBotRequest(
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
+ loadContext: AppLoadContext,
messages: Record<string, string>
) {
return new Promise(async (resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
await prepareV2ServerRenderedApp(
<FrontCommerceServer
+ context={loadContext.frontCommerce}
remixContext={remixContext}
messages={messages}
>
@@ -52,12 +55,12 @@ function handleBotRequest(
responseHeaders.set('Content-Type', 'text/html')
- resolve(
- new Response(body, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- )
+ const response = new Response(body, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ transformBotResponse(response, loadContext.frontCommerce)
+ resolve(response)
pipe(body)
},
@@ -80,6 +83,7 @@ function handleBrowserRequest(
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
+ loadContext: AppLoadContext,
messages: Record<string, string>
) {
return new Promise(async (resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
await prepareV2ServerRenderedApp(
<FrontCommerceServer
+ context={loadContext.frontCommerce}
remixContext={remixContext}
messages={messages}
>
@@ -97,12 +101,12 @@ function handleBrowserRequest(
responseHeaders.set('Content-Type', 'text/html')
- resolve(
- new Response(body, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- )
+ const response = new Response(body, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ transformBrowserResponse(response, loadContext.frontCommerce)
+ resolve(response)
pipe(body)
},
@@ -119,3 +123,8 @@ function handleBrowserRequest(
setTimeout(abort, ABORT_DELAY)
})
}
+
+export function handleDataRequest(response: Response, { context }: DataFunctionArgs) {
+ transformDataResponse(response, context.frontCommerce)
+ return response
+}
root.tsx
file
diff --git a/app/root.tsx b/app/root.tsx
index 71936d5a1..a28dcf87e 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,7 +1,7 @@
import { CompatScripts } from "@front-commerce/compat/CompatProvider";
import { FrontCommerceApp } from "@front-commerce/remix";
import { generateMetas, json } from "@front-commerce/remix/node";
-import { useLoaderData } from "@front-commerce/remix/react";
+import { FrontCommerceScripts } from "@front-commerce/remix/react";
import { cssBundleHref } from "@remix-run/css-bundle";
import type {
LinksFunction,
@@ -73,8 +73,6 @@ export const meta: MetaFunction = (args) => {
};
export default function App() {
- const { process } = useLoaderData<typeof loader>();
-
useSWEffect();
const navigation = useNavigation();
@@ -93,11 +91,7 @@ export default function App() {
<ScrollRestoration />
<Scripts />
<LiveReload />
- <script
- dangerouslySetInnerHTML={{
- __html: `window.process = ${JSON.stringify(process)}`,
- }}
- />
+ <FrontCommerceScripts />
<CompatScripts />
</body>
</html>
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"
},
// ...
Order filters
Breaking: In this release, we updated the order filters feature (released in 3.1.0) to support having filters fields linked by a "OR" logic. This changes required a change in our GraphQL API which will break your application if you're using this feature.
To cope with these changes, you will have to update the usage of orderList
query in your application, specifically the format of the attributes
argument.
You should check for all components and / or routes you're using orderList
query from, and fix the usage of attributes
argument:
input OrderAttributesInput {
"The attribute's name"
name: String!
"A list of value for the attribute"
value: [String!]!
"The type of filtering to use for this attribute"
type: OrderAttributeInputType = CONTAINS
}
Became:
input OrderAttributesInput {
"The fields filtered"
fields: [String!]!
"A list of value for the attribute"
values: [String!]!
"The type of filtering to use for this attribute"
type: OrderAttributeInputType = CONTAINS
}
fields
is now an array of fields the filter should be applied to.
Related commit: 4ef9368f.
Example
Given this attributes
object:
[
{
"fields": ["id", "name"],
"value": ["Foo", "123"]
},
{
"fields": ["comment"],
"value": ["Bar", "456"]
}
]
The orders will be filtered with this logic:
(order.id contains "Foo"
OR order.id contains "123"
OR order.name contains "Foo"
OR order.name contains "123")
AND
(order.comment contains "Bar"
OR order.comment contains "456")
Requisition list related theme changes
Since version 3.2.7
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