From 99d9888d9b1b9ee3f7ee775d69c12286680e03bf Mon Sep 17 00:00:00 2001 From: Lucas Calazans Date: Wed, 20 May 2020 14:27:13 -0300 Subject: [PATCH 1/2] [Docs] Adding section to docs about SASS and LESS (#2316) * Adding section to docs about SASS and LESS * Fixing diff and less blocks * Make requested changes * Account for recent changes to webpack.config * Fix lint errors and rename directory Co-authored-by: Tommy Wiebell --- pwa-devdocs/src/_data/tutorials.yml | 3 + .../enable-sass-less-support/index.md | 187 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 pwa-devdocs/src/tutorials/enable-sass-less-support/index.md diff --git a/pwa-devdocs/src/_data/tutorials.yml b/pwa-devdocs/src/_data/tutorials.yml index 818e6ddf64..2adbcaba7b 100644 --- a/pwa-devdocs/src/_data/tutorials.yml +++ b/pwa-devdocs/src/_data/tutorials.yml @@ -20,6 +20,9 @@ entries: - label: Create a TagList component url: /tutorials/create-taglist-component/ + - label: Enable SASS or LESS support + url: /tutorials/enable-sass-less-support/ + - label: PWA Studio fundamentals url: /tutorials/pwa-studio-fundamentals/ entries: diff --git a/pwa-devdocs/src/tutorials/enable-sass-less-support/index.md b/pwa-devdocs/src/tutorials/enable-sass-less-support/index.md new file mode 100644 index 0000000000..dd6dad33cf --- /dev/null +++ b/pwa-devdocs/src/tutorials/enable-sass-less-support/index.md @@ -0,0 +1,187 @@ +--- +title: Enable SASS or LESS support +--- + +This tutorial provides the steps to enable SASS or LESS support in your storefront project. + +Use these Webpack configurations to add support for SASS and LESS alongside [CSS Modules][]. + +## Add SASS support + +### Step 1. Install dependencies + +Use a package manager, such as `yarn` or `npm`, to install the SASS loader as a dev dependency. + +```sh +yarn add --dev sass-loader node-sass +``` + +### Step 2. Modify the Webpack configuration + +Edit `webpack.config.js` and add a new `config` rule entry: + +```diff + + config.plugins = [ + ...config.plugins, + new DefinePlugin({ + /** + * Make sure to add the same constants to + * the globals object in jest.config.js. + */ + UNION_AND_INTERFACE_TYPES: JSON.stringify(unionAndInterfaceTypes), + STORE_NAME: JSON.stringify('Venia') + }), + new HTMLWebpackPlugin({ + filename: 'index.html', + template: './template.html', + minify: { + collapseWhitespace: true, + removeComments: true + } + }) + ]; ++ ++ config.module.rules.push({ ++ test: /\.s[ca]ss$/, ++ use: [ ++ 'style-loader', ++ { ++ loader: 'css-loader', ++ options: { ++ modules: true, ++ sourceMap: true, ++ localIdentName: '[name]-[local]-[hash:base64:3]' ++ } ++ }, ++ 'sass-loader' ++ ] ++ }); +``` + +### Step 3. Create a SASS file and import it in a component + +Create the `myComponent.scss` file: + +```scss +$button-color: #ff495b; +$button-color-hover: #ff9c1a; + +.root { + padding: 15px; +} + +.button { + color: $button-color; + + &:hover { + color: $button-color-hover; + } +} +``` + +Create a component and import the SASS file: + +```jsx +import React from 'react'; +import defaultClasses from './myComponent.scss'; + +const MyComponent = () => ( +
+ +
+); + +export default MyComponent; +``` + +## Add LESS support + +### Step 1. Install dependencies + +Use a package manager, such as `yarn` or `npm`, to install the LESS loader as a dev dependency. + +```sh +yarn add --dev less-loader less +``` + +### Step 2. Modify the Webpack configuration + +Edit `webpack.config.js` and add a new `config` rule entry: + +```diff + + config.plugins = [ + ...config.plugins, + new DefinePlugin({ + /** + * Make sure to add the same constants to + * the globals object in jest.config.js. + */ + UNION_AND_INTERFACE_TYPES: JSON.stringify(unionAndInterfaceTypes), + STORE_NAME: JSON.stringify('Venia') + }), + new HTMLWebpackPlugin({ + filename: 'index.html', + template: './template.html', + minify: { + collapseWhitespace: true, + removeComments: true + } + }) + ]; ++ ++ config.module.rules.push({ ++ test: /\.less$/, ++ use: [ ++ 'style-loader', ++ { ++ loader: 'css-loader', ++ options: { ++ modules: true, ++ sourceMap: true, ++ localIdentName: '[name]-[local]-[hash:base64:3]' ++ } ++ }, ++ 'less-loader' ++ ] ++ }); +``` + +### Step 3. Create a LESS file and import it on your component + +Create the `myComponent.less` file: + +```less +@button-color: #ff495b; +@button-color-hover: #ff9c1a; + +.root { + padding: 15px; +} + +.button { + color: @button-color; + + &:hover { + color: @button-color-hover; + } +} +``` + +Create a component and import the LESS file: + +```jsx +import React from 'react'; +import defaultClasses from './myComponent.less'; + +const MyComponent = () => ( +
+ +
+); + +export default MyComponent; +``` + +[css modules]: <{%link technologies/basic-concepts/css-modules/index.md %}> From 2089baa7ea493760ebdc85ca37f985b1ef7940f5 Mon Sep 17 00:00:00 2001 From: Stephen Date: Wed, 20 May 2020 17:34:00 -0500 Subject: [PATCH 2/2] When auth token is invalid reset cart and refetch details (#2379) * When auth token is invalid reset cart in redux * Manually clear cart from apollo cache on getCartDetails retry Signed-off-by: sirugh * Handle use case where cart permission error happens without a page refresh * Fix tests * Handle view reset and add detail to comment * Also reset user state on add to cart failures Co-authored-by: Tommy Wiebell Co-authored-by: Devagouda <40405790+dpatil-magento@users.noreply.github.com> --- .../lib/store/actions/cart/asyncActions.js | 56 ++++++++++++++----- .../lib/store/actions/user/asyncActions.js | 41 ++++++++------ .../lib/talons/AuthModal/useAuthModal.js | 14 ++++- .../lib/talons/Header/useCartTrigger.js | 18 ++++-- .../Header/__tests__/cartTrigger.spec.js | 1 + 5 files changed, 92 insertions(+), 38 deletions(-) diff --git a/packages/peregrine/lib/store/actions/cart/asyncActions.js b/packages/peregrine/lib/store/actions/cart/asyncActions.js index ac8e70f26a..dc0646d4cc 100644 --- a/packages/peregrine/lib/store/actions/cart/asyncActions.js +++ b/packages/peregrine/lib/store/actions/cart/asyncActions.js @@ -1,4 +1,6 @@ +import { clearCartDataFromCache } from '../../../Apollo/clearCartDataFromCache'; import BrowserPersistence from '../../../util/simplePersistence'; +import { signOut } from '../user'; import actions from './actions'; const storage = new BrowserPersistence(); @@ -61,10 +63,11 @@ export const addItemToCart = (payload = {}) => { await writingImageToCache; dispatch(actions.addItem.request(payload)); - try { - const { cart } = getState(); - const { cartId } = cart; + const { cart, user } = getState(); + const { cartId } = cart; + const { isSignedIn } = user; + try { const variables = { cartId, parentSku, @@ -95,11 +98,18 @@ export const addItemToCart = (payload = {}) => { // Only retry if the cart is invalid or the cartId is missing. if (shouldRetry) { - // Delete the cached ID from local storage and Redux. - // In contrast to the save, make sure storage deletion is - // complete before dispatching the error--you don't want an - // upstream action to try and reuse the known-bad ID. - await dispatch(removeCart()); + if (isSignedIn) { + // Since simple persistence just deletes auth token without + // informing Redux, we need to perform the sign out action + // to reset the user and cart slices back to initial state. + await dispatch(signOut()); + } else { + // Delete the cached ID from local storage and Redux. + // In contrast to the save, make sure storage deletion is + // complete before dispatching the error--you don't want an + // upstream action to try and reuse the known-bad ID. + await dispatch(removeCart()); + } // then create a new one await dispatch( @@ -286,11 +296,12 @@ export const removeItemFromCart = payload => { }; export const getCartDetails = payload => { - const { fetchCartId, fetchCartDetails } = payload; + const { apolloClient, fetchCartId, fetchCartDetails } = payload; return async function thunk(dispatch, getState) { - const { cart } = getState(); + const { cart, user } = getState(); const { cartId } = cart; + const { isSignedIn } = user; // if there isn't a cart, create one then retry this operation if (!cartId) { @@ -319,8 +330,21 @@ export const getCartDetails = payload => { const shouldResetCart = !error.networkError && isInvalidCart(error); if (shouldResetCart) { - // Delete the cached ID from local storage. - await dispatch(removeCart()); + if (isSignedIn) { + // Since simple persistence just deletes auth token without + // informing Redux, we need to perform the sign out action + // to reset the user and cart slices back to initial state. + await dispatch(signOut()); + } else { + // Delete the cached ID from local storage. + await dispatch(removeCart()); + } + + // Clear the cart data from apollo client if we get here and + // have an apolloClient. + if (apolloClient) { + await clearCartDataFromCache(apolloClient); + } // Create a new one await dispatch( @@ -391,8 +415,12 @@ export async function writeImageToCache(item = {}) { function isInvalidCart(error) { return !!( error.graphQLErrors && - error.graphQLErrors.find(err => - err.message.includes('Could not find a cart') + error.graphQLErrors.find( + err => + err.message.includes('Could not find a cart') || + err.message.includes( + 'The current user cannot perform operations on cart' + ) ) ); } diff --git a/packages/peregrine/lib/store/actions/user/asyncActions.js b/packages/peregrine/lib/store/actions/user/asyncActions.js index e9167230e5..145d69cabf 100755 --- a/packages/peregrine/lib/store/actions/user/asyncActions.js +++ b/packages/peregrine/lib/store/actions/user/asyncActions.js @@ -6,24 +6,29 @@ import actions from './actions'; const storage = new BrowserPersistence(); -export const signOut = ({ revokeToken }) => async dispatch => { - // Send mutation to revoke token. - try { - await revokeToken(); - } catch (error) { - console.error('Error Revoking Token', error); - } - - // Remove token from local storage and Redux. - await dispatch(clearToken()); - await dispatch(actions.reset()); - await clearCheckoutDataFromStorage(); - - // Now that we're signed out, forget the old (customer) cart. - // We don't need to create a new cart here because we're going to refresh - // the page immediately after. - await dispatch(removeCart()); -}; +export const signOut = (payload = {}) => + async function thunk(dispatch) { + const { revokeToken } = payload; + + if (revokeToken) { + // Send mutation to revoke token. + try { + await revokeToken(); + } catch (error) { + console.error('Error Revoking Token', error); + } + } + + // Remove token from local storage and Redux. + await dispatch(clearToken()); + await dispatch(actions.reset()); + await clearCheckoutDataFromStorage(); + + // Now that we're signed out, forget the old (customer) cart. + // We don't need to create a new cart here because we're going to refresh + // the page immediately after. + await dispatch(removeCart()); + }; export const getUserDetails = ({ fetchUserDetails }) => async function thunk(...args) { diff --git a/packages/peregrine/lib/talons/AuthModal/useAuthModal.js b/packages/peregrine/lib/talons/AuthModal/useAuthModal.js index f2abeb6233..557b5bfbc8 100644 --- a/packages/peregrine/lib/talons/AuthModal/useAuthModal.js +++ b/packages/peregrine/lib/talons/AuthModal/useAuthModal.js @@ -39,8 +39,9 @@ export const useAuthModal = props => { } = props; const apolloClient = useApolloClient(); + const [isSigningOut, setIsSigningOut] = useState(false); const [username, setUsername] = useState(''); - const [{ currentUser }, { signOut }] = useUserContext(); + const [{ currentUser, isSignedIn }, { signOut }] = useUserContext(); const [revokeToken] = useMutation(signOutMutation); const history = useHistory(); @@ -52,6 +53,14 @@ export const useAuthModal = props => { } }, [currentUser, showMyAccount, view]); + // If the user token was invalidated by way of expiration, we need to reset + // the view back to the main menu. + useEffect(() => { + if (!isSignedIn && view === 'MY_ACCOUNT' && !isSigningOut) { + showMainMenu(); + } + }, [isSignedIn, isSigningOut, showMainMenu, view]); + const handleClose = useCallback(() => { showMainMenu(); closeDrawer(); @@ -62,9 +71,10 @@ export const useAuthModal = props => { }, [showMyAccount]); const handleSignOut = useCallback(async () => { + setIsSigningOut(true); + // Delete cart/user data from the redux store. await signOut({ revokeToken }); - await clearCartDataFromCache(apolloClient); // Refresh the page as a way to say "re-initialize". An alternative diff --git a/packages/peregrine/lib/talons/Header/useCartTrigger.js b/packages/peregrine/lib/talons/Header/useCartTrigger.js index 7b8aaa5478..c07d836482 100644 --- a/packages/peregrine/lib/talons/Header/useCartTrigger.js +++ b/packages/peregrine/lib/talons/Header/useCartTrigger.js @@ -1,5 +1,9 @@ import { useCallback, useEffect } from 'react'; -import { useLazyQuery, useMutation } from '@apollo/react-hooks'; +import { + useApolloClient, + useLazyQuery, + useMutation +} from '@apollo/react-hooks'; import { useCartContext } from '@magento/peregrine/lib/context/cart'; import { useAppContext } from '@magento/peregrine/lib/context/app'; import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery'; @@ -9,18 +13,24 @@ export const useCartTrigger = props => { mutations: { createCartMutation }, queries: { getCartDetailsQuery, getItemCountQuery } } = props; + + const apolloClient = useApolloClient(); const [, { toggleDrawer }] = useAppContext(); const [{ cartId }, { getCartDetails }] = useCartContext(); - const [getItemCount, { data }] = useLazyQuery(getItemCountQuery); + const [getItemCount, { data }] = useLazyQuery(getItemCountQuery, { + fetchPolicy: 'cache-and-network' + }); const [fetchCartId] = useMutation(createCartMutation); const fetchCartDetails = useAwaitQuery(getCartDetailsQuery); const itemCount = data ? data.cart.total_quantity : 0; useEffect(() => { - getCartDetails({ fetchCartId, fetchCartDetails }); - }, [fetchCartDetails, fetchCartId, getCartDetails]); + // Passing apolloClient to wipe the store in event of auth token expiry + // This will only happen if the user refreshes. + getCartDetails({ apolloClient, fetchCartId, fetchCartDetails }); + }, [apolloClient, fetchCartDetails, fetchCartId, getCartDetails]); useEffect(() => { if (cartId) { diff --git a/packages/venia-ui/lib/components/Header/__tests__/cartTrigger.spec.js b/packages/venia-ui/lib/components/Header/__tests__/cartTrigger.spec.js index 8af68b4134..8551d1aec9 100644 --- a/packages/venia-ui/lib/components/Header/__tests__/cartTrigger.spec.js +++ b/packages/venia-ui/lib/components/Header/__tests__/cartTrigger.spec.js @@ -5,6 +5,7 @@ import { createTestInstance } from '@magento/peregrine'; import CartTrigger from '../cartTrigger'; jest.mock('@apollo/react-hooks', () => ({ + useApolloClient: jest.fn().mockImplementation(() => {}), useLazyQuery: jest.fn().mockReturnValue([jest.fn(), { data: null }]), useMutation: jest.fn().mockImplementation(() => [ jest.fn(),