diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2173ce881..9df1dac435 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,217 +1,245 @@
-# Release 8.0.0
+# Release 9.0.0
**NOTE:**
-_This changelog only contains release notes for PWA Studio 8.0.0._
+_This changelog only contains release notes for PWA Studio and Venia 9.0.0._
_For older release notes, see [PWA Studio releases][]._
## Table of contents
-- [What's new in 8.0.0](#whats-new-in-800)
+- [What's new in 9.0.0](#whats-new-in-900)
- [Pull requests merged in this release](#pull-requests-merged-in-this-release)
- [Known issues](#known-issues)
- [Upgrading from a previous version](#upgrading-from-a-previous-version)
-## What's new in 8.0.0
+## What's new in 9.0.0
-PWA Studio 8.0.0 contains new features, refactors, and various improvements.
+PWA Studio 9.0.0 contains new features, refactors, and various improvements.
-### Improved performance
+### Extensibility framework improvements
-A lot of work has been done in this release to improve storefront performance.
-This means that any project using the latest PWA Studio components will benefit from these updates.
+This release adds several improvements to the extensibility framework in PWA Studio to make it easier for developers to customize their storefronts.
+For an overview of this framework, check out the new [Extensibility framework][] topic on the docs site.
-One of the bigger changes is the migration to [Apollo Client 3.0][].
-This version of the Apollo Client provides better cache controls and better network fetching performance in general.
+In previous releases, Peregrine talons had limited Target coverage.
+This release adds an automatic API generator to Peregrine that exposes all hooks and talons as Targets.
+Now, existing and future hooks and talons in Peregrine automatically get their own Targets API that developers may use to modify or extend functionality.
-This release also includes various refactors and improvements on the GraphQL queries themselves to reduce the API request times from Magento.
+This release also adds the _Targetables_ feature to the extensibility framework.
+These represent source files used in your PWA Studio project, and
+they give developers the ability to change the source code during the build process.
+With Targetables, developers no longer have to copy PWA Studio source code into their storefront projects to make minor modifications.
-[apollo client 3.0]: https://www.apollographql.com/docs/react/migrating/apollo-client-3-migration/
+[extensibility framework]:
-### Complete cart and checkout experience
+### PWA Studio extensions
-This release finishes the full page cart and checkout features introduced in previous releases.
-The complete cart and checkout workflow is based on research made by members of the UX team.
+PWA Studio's extensibility framework lets developers create extensions and install them as project dependencies in their storefronts.
+As part of the work on the new extensibility framework, we refactored and relocated existing Venia features into PWA Studio extensions.
+We also developed new extensions that provide useful Venia features that developers can add to their projects.
-Storefront developers can use this streamlined process as implemented or they can use the different components to customize their own cart and checkout workflow.
+The source code for these extensions are available under the [`packages/extensions`][] directory in the PWA Studio repository.
-#### Shopping Bag feature
+`upward-security-headers`
+: intercepts build targets to add security headers to UPWARD
-In addition to the complete full page cart and checkout experience, this release introduces a new Mini-Cart/Shopping Bag feature.
+`venia-adobe-data-layer`
+: provides [Adobe Client Data Layer][] support for your project
-This feature is a floating modal that appears when you click on the shopping bag icon.
-It replaces the old `MiniCart` component, which previously appeared as a drawer from the right side of the app.
+`venia-sample-backends`
+: provides demo Magento backends and backend validation utilities for your project
+ (this extension should be removed prior to going live)
-Instead of competing with the full page cart feature, it only contains a subset of actions, such as removing an item and checking out.
-For additional modifications to cart products, it links to the cart page.
+`venia-sample-language-packs`
+: provides example translations to illustrate how new languages can be installed into your storefronts
-Developers can still use the old `MiniCart` component in their projects, but it has been renamed to `LegacyMiniCart`.
+[`packages/extensions`]:
+[adobe client data layer]:
-### Branding updates
+### Internationalization and localization
-The UX team continues in their research to improve the look and feel of the Venia brand.
-This release includes many style updates to give the Venia storefront a more modern and accessible experience.
+The internationalization(i18n) feature in PWA Studio lets developers localize their storefront content according to different regions and languages.
+The Magento backend provides your storefront with this list of regions and languages and the I18n feature provides translated content using PWA Studio language pack extensions.
-Developers get all these improvements right away when they start their projects using this version of Venia as the base storefront or by upgrading their dependencies.
+As part of the i18n feature work, we refactored Venia UI components and gave them the ability to display the correct translations for multi-language storefronts.
-### My Account code preview
+This release also gives developers the ability to develop and install PWA Studio language packages as NPM dependencies.
+An example of a language pack extension is in the [`packages/extensions`][] directory in the PWA Studio repository.
-This releases includes a sneak peak at features associated with My Account, such as Wishlist, Order History, and Address Book.
-Even though the Communications Page is the only navigable page, curious developers can peek at the 8.0.0 release codebase to see the initial code for these features.
+For more information, read the new topic on the [Localization feature][].
-### Targets reference documentation
+[localization feature]:
-During 8.0.0 development, the PWA Studio doc site has published reference documentation for extensibility targets in the different packages.
-This documentation contains API descriptions and sample code to help developers discover the different PWA Studio extension points.
+### My Account
+
+This release adds components that support _My Account_ features for customers that create an account with a store.
+
+My Account features included in this release:
+
+- Wishlist
+- Saved Payments
+- Address Book
+- Order History
+
+### Increased test coverage
+
+Our continued commitment to stability and quality has seen an increase in overall unit test code coverage.
+
+Coverage as reported by [coveralls.io][]:
+
+Current coverage (9.0.0)
+: **84.19%**
+
+Previous coverage (8.0.0)
+: **79.21%**
+
+[coveralls.io]:
+
+### Magento release support change
+
+Previous releases of PWA Studio supported multiple versions of the Magento back-end.
+To help us deliver value faster, we modified our support matrix.
+
+Starting with PWA Studio & Venia 9.0.0, we will only support the most recent version of Magento.
+For example, version 9.0.0 only supports Magento 2.4.2.
+Minor versions of PWA Studio & Venia released between typical Magento releases will support the last publicly available release.
## Pull requests merged in this release
### Venia (storefront and visual component library)
-| Description | Change type | PR |
-| ------------------------------------------------------------------------------------ | ------------ | --------- |
-| Implemented initial code for the new MiniCart component | **Feature** | [#2494][] |
-| Created new components for an account menu | **Feature** | [#2550][] |
-| Created components for a Newsletter Subscription page | **Feature** | [#2571][] |
-| Implemented initial code for an Order History page | **Feature** | [#2611][] |
-| Implemented a way to access top/intermediate level categories through the left nav | **Feature** | [#2616][] |
-| Implemented initial UX and workflow for a Forgot Password feature for My Account | **Feature** | [#2619][] |
-| Implemented initial code for Wishlist | **Feature** | [#2620][] |
-| Implemented the ability to translate Venia's header / footer | **Feature** | [#2643][] |
-| Added ability to allow PWA Studio to use a different store to localize CMS Pages | **Feature** | [#2649][] |
-| Implemented initial code for Address Book | **Feature** | [#2653][] |
-| Implemented initial code for an Order History table | **Feature** | [#2660][] |
-| Updated Venia's button style to match new design | **Update** | [#2496][] |
-| Added product listing to the MiniCart | **Update** | [#2506][] |
-| Added a header section to the MiniCart | **Update** | [#2509][] |
-| Added a footer section to the MiniCart | **Update** | [#2511][] |
-| Adjusted styles for the header, main page, and footer components | **Update** | [#2513][] |
-| Update Gallery component to use `item.id` for key prop on GalleryItem | **Update** | [#2520][] |
-| Added product link for each product in the MiniCart | **Update** | [#2549][] |
-| Disabled the visibility of the MiniCart on the checkout page | **Update** | [#2554][] |
-| Added product links to the product listings on the cart page | **Update** | [#2557][] |
-| Updated Venia's filter modal styles to match the new design | **Update** | [#2559][] |
-| Added CSS to handle multiple configurable options | **Update** | [#2577][] |
-| Implemented consistent error state handling | **Update** | [#2588][] |
-| Added a sign-in section to the new My Account trigger in the header | **Update** | [#2590][] |
-| Enabled GET for GraphQL queries (but not mutations) | **Update** | [#2602][] |
-| Added React Refresh to improve development server performance | **Update** | [#2609][] |
-| Improved error handling when using the Sign-in dropdown | **Update** | [#2664][] |
-| Updated static images to reflect new logo | **Update** | [#2693][] |
-| Disabled account page routes for v8.0.0 | **Update** | [#2709][] |
-| Removed email from the reset password link | **Update** | [#2726][] |
-| Refactored code to use tokens for color and weight | **Refactor** | [#2500][] |
-| Refactored Venia's inputs to match new design | **Refactor** | [#2510][] |
-| Refactored Venia's accordions to match the style guidelines | **Refactor** | [#2527][] |
-| Refactored Venia's cards to match the style guidelines | **Refactor** | [#2545][] |
-| Fixed a "Data Fetch Error" on the product page during offline mode | **Bugfix** | [#2490][] |
-| Fixed a bug that made the product and category sorting component unavailable | **Bugfix** | [#2493][] |
-| Resolved remaining issues with Buttons component implementation | **Bugfix** | [#2523][] |
-| Fixed scroll lock page shifting | **Bugfix** | [#2543][] |
-| Fixed MiniCart blocking page interactions | **Bugfix** | [#2547][] |
-| Fixed a sticky sidebar on the checkout page overlapping the footer | **Bugfix** | [#2582][] |
-| Fixed a bug that kept rendering the loading spinner on the page | **Bugfix** | [#2583][] |
-| Fixed MiniCart bug that prevent guest users from adding a product to an expired cart | **Bugfix** | [#2612][] |
-| Fixed button CSS to handle mobile view | **Bugfix** | [#2655][] |
-| Fixed improper use of `formatMessage()` | **Bugfix** | [#2698][] |
-| Fixed Storybook bug caused by i18n work | **Bugfix** | [#2705][] |
-| Fixed Payment Information button to disable it while the Payment section loads | **Bugfix** | [#2723][] |
-| Fixed an offline homepage error | **Bugfix** | [#2727][] |
+| Description | Change type | PR |
+| ------------------------------------------------------------------------------------------------------- | ------------ | --------- |
+| Created UI skeleton for Saved Payments | **Feature** | [#2671][] |
+| Created component for displaying account information | **Feature** | [#2672][] |
+| Added ability for shoppers to change locale using a store view switcher | **Feature** | [#2686][] |
+| Created the main view for Wishlist | **Feature** | [#2692][] |
+| Added the expanded view in the order history table | **Feature** | [#2703][] |
+| Created a currency switcher component | **Feature** | [#2728][] |
+| Added `USE_STORE_CODE_IN_URL` environment variable configuration | **Feature** | [#2735][] |
+| Created Wishlist UI | **Feature** | [#2766][] |
+| Added ability to determine whether to unmount or just hide child components in the Dialog component | **Feature** | [#2767][] |
+| Added ability to remove products from Wishlist | **Feature** | [#2793][] |
+| Added a "maskable icon" to Venia for Google Lighthouse | **Feature** | [#2818][] |
+| Created message to display when no allowed or configured payment methods are present | **Feature** | [#2855][] |
+| Created the main view for Address Book in My Account | **Feature** | [#2857][] |
+| Added ability to Add and Edit addresses in the Address Book | **Feature** | [#2879][] |
+| Created the main view for Saved payment methods | **Feature** | [#2882][] |
+| Added ability to delete Address from Address Book | **Feature** | [#2888][] |
+| Created new Sign In view for Checkout flow | **Feature** | [#2889][] |
+| Updated logic for routes handling to accept an array of paths | **Feature** | [#2893][] |
+| Replaced hardcoded root category id with an actual value retrieved from a query | **Feature** | [#2902][] |
+| Added search by order number feature to the order history page | **Feature** | [#2916][] |
+| Added pagination for Order History | **Feature** | [#2928][] |
+| Refactored Edit Payment to use Dialog component | **Refactor** | [#2806][] |
+| Refactored Edit Product to use Dialog component | **Refactor** | [#2824][] |
+| Refactored Payment feature to make it extendable | **Refactor** | [#2838][] |
+| Added access to checkout's `useOverview()` talon | **Update** | [#2636][] |
+| Updated the Search trigger button in the site header to behave like My Account and Cart trigger buttons | **Update** | [#2685][] |
+| Replaced the ProductQuantity component on the Product page with a QuantityFields stepper component | **Update** | [#2690][] |
+| Localized My Account and Signed In sidebar | **Update** | [#2721][] |
+| Localized Mini Cart and Search | **Update** | [#2734][] |
+| Localized Cart | **Update** | [#2740][] |
+| Localized Checkout page | **Update** | [#2759][] |
+| Localized CMS | **Update** | [#2764][] |
+| Localized Category page | **Update** | [#2771][] |
+| Localized Product page | **Update** | [#2772][] |
+| Localized Form Validators | **Update** | [#2781][] |
+| Localized additional client-side strings | **Update** | [#2799][] |
+| Removed temp code | **Update** | [#2811][] |
+| Update service worker logic to handle all Venia images | **Update** | [#2846][] |
+| Fixed a bug where the Zip code field does not get cleared when switching country | **Bugfix** | [#2680][] |
+| Fixed a bug where the Filter and Sort buttons would not display at same time | **Bugfix** | [#2681][] |
+| Removed ability to submit form data prefixed/suffixed with spaces for all fields | **Bugfix** | [#2749][] |
+| Limited clickable link area for the product name on the product page | **Bugfix** | [#2755][] |
+| Fixed message on the Search Page when searching for less than 3 characters | **Bugfix** | [#2756][] |
+| Fixed a bug where clicking on a Label would not focus Input | **Bugfix** | [#2774][] |
+| Fixed `theme_color` value in the `manifest.json` | **Bugfix** | [#2823][] |
+| Fixed button type on product image carousel thumbnails | **Bugfix** | [#2844][] |
+| Fixed Service Worker caching for home page routes with store code | **Bugfix** | [#2856][] |
### Peregrine library
-| Description | Change type | PR |
-| --------------------------------------------------------------------------------------------------------- | ------------ | --------- |
-| Added access to the root component type for child components | **Feature** | [#2443][] |
-| Enable URL redirects when set in the Magento backend | **Feature** | [#2504][] |
-| Added Create Account functionality to the Sign-in trigger in the navigation menu | **Feature** | [#2657][] |
-| Added support for an app-wide configurable URL suffix | **Feature** | [#2665][] |
-| Added support for localized Catalog Products for different stores | **Feature** | [#2667][] |
-| Implemented scroll top when payment information processing completes | **Update** | [#2498][] |
-| Implemented logic for adding products to the MiniCart | **Update** | [#2505][] |
-| Updated fetching logic to use cache-and-network for the Cart/Checkout processes | **Update** | [#2634][] |
-| Moved cart creation logic out of cart trigger and into the cart context provider | **Refactor** | [#2572][] |
-| Fixed an infinite error loop when cart creation fails | **Bugfix** | [#2574][] |
-| Fixed a bug that prevented adding to cart when another product in the cart is out of stock | **Bugfix** | [#2576][] |
-| Fixed a bug on the Category page where it did not refresh data when clicking back, previous, or next page | **Bugfix** | [#2641][] |
-| Fixed a customer address bug related to addresses for countries without regions or states | **Bugfix** | [#2659][] |
-| Fixed bug caused by Apollo when upgrading from a previous release | **Bugfix** | [#2673][] |
-| Fixed bug that showed product types that are not supported yet | **Bugfix** | [#2697][] |
-| Fixed a data sort error | **Bugfix** | [#2736][] |
-| Fixed an error in the filters list associated with multiple filters having the same label | **Bugfix** | [#2739][] |
-| Fixed an address merging error during sign in | **Bugfix** | [#2744][] |
+| Description | Change type | PR |
+| ------------------------------------------------------------------------------------------- | ------------ | --------- |
+| Created a shallow merge utility for classes in UI components and merge operations in talons | **Feature** | [#2794][] |
+| Increased `useApp()` talon test coverage | **Update** | [#2782][] |
+| Increased `peregrine/lib/apollo` test coverage | **Update** | [#2785][] |
+| Increased CartPage test coverage | **Update** | [#2847][] |
+| Improved RootComponents talons test coverage | **Update** | [#2896][] |
+| Removed routes to features still in progress | **Update** | [#2918][] |
+| Migrated GQL related files and folders to the `peregrine` package | **Refactor** | [#2712][] |
+| Removed the `@client` directive in Order History queries | **Refactor** | [#2786][] |
+| Fixed spelling for a function name | **Refactor** | [#2807][] |
+| Refactored MagentoRoute to use ApolloClient | **Refactor** | [#2859][] |
+| Moved product detail GraphQl fields to fragment | **Refactor** | [#2868][] |
+| Fixed JavaScript errors thrown by the `useOrderConfirmationPage` talon | **Bugfix** | [#2850][] |
+| Fixed `apiBase` URL in `resolveUnknownRoute.js` | **Bugfix** | [#2877][] |
+| Fixed broken top level category navigation | **Bugfix** | [#2911][] |
+| Fixed offline cached search and category pages | **Bugfix** | [#2929][] |
### Build tools
+| Description | Change type | PR |
+| ------------------------------------------------------------------------- | ----------- | --------- |
+| Enabled PWA Studio packages and extensions to provide translations | **Feature** | [#2696][] |
+| Added support for GIF files in Webpack config | **Feature** | [#2714][] |
+| Created Targetables feature and expanded Peregrine talons Target coverage | **Feature** | [#2765][] |
+| Enabled self-signed certificates for backend validation | **Feature** | [#2891][] |
+| Fixed unsupported webp image format for Safari | **Bugfix** | [#2778][] |
+| Fixed bug related to `apicache` overriding good cache headers | **Bugfix** | [#2870][] |
+| Fixed missing `projectConfig` in `create-custom-origin` command | **Bugfix** | [#2897][] |
+
+### Extensions
+
| Description | Change type | PR |
| -------------------------------------------------------------------------------------------- | ----------- | --------- |
-| Created a staging server utility instead of a script | **Feature** | [#2618][] |
-| Deprecated `getUnionAndInterfaceTypes()` function | **Update** | [#2663][] |
-| Fixed scaffolding bug that did not include a `pwa-studio` section in the `package.json` file | **Bugfix** | [#2514][] |
-| Fixed Storybook files for scaffolded projects | **Bugfix** | [#2708][] |
-
-### UPWARD
-
-| Description | Change type | PR |
-| -------------------------------------------------------------------------------- | ----------- | --------- |
-| Implemented feature that allows HTTP for the Magento backend URL | **Feature** | [#2423][] |
-| Updated implementation code to improve WebPageTest score | **Update** | [#2548][] |
-| Added support for additional image types | **Update** | [#2562][] |
-| Updated upward-security-headers peer dependencies | **Update** | [#2605][] |
-| Fixed UPWARD bug that prevented the use of an allowable header character | **Bugfix** | [#2484][] |
-| Fixed image optimized middleware | **Bugfix** | [#2535][] |
-| Removed `rimraf` as a peer dependency in the `upward-security-headers` extension | **Bugfix** | [#2594][] |
-| Fixed YouTube and Vimeo urls being blocked | **Bugfix** | [#2656][] |
+| Enable async tapping of Targets | **Feature** | [#2718][] |
+| Added i18n feature as an extension with French language pack included | **Feature** | [#2840][] |
+| Added the Adobe Client Data Layer as an extension | **Feature** | [#2852][] |
+| Added new extension to pick from multiple sample backends | **Feature** | [#2853][] |
+| Updated PageBuilder form field/field group viewport to read non-media styles | **Update** | [#2881][] |
+| Converted PageBuilder style blocks to inline styles to prevent backward incompatible changes | **Bugfix** | [#2694][] |
### Documentation
-| Description | Change type | PR |
-| ----------------------------------------------------------------------------- | ----------------- | --------- |
-| Created Venia Targets reference documentation | **Documentation** | [#2472][] |
-| Created Peregrine Targets reference documentation | **Documentation** | [#2492][] |
-| Created Buildpack Targets reference documentation | **Documentation** | [#2508][] |
-| Created reference docs for the UI components and talons used in the cart page | **Documentation** | [#2637][] |
-| Added a note about Node 12 deprecation warnings | **Update** | [#2566][] |
-| Added upgrade steps in the changelog for scaffolded projects | **Update** | [#2587][] |
-| Updated `magento-research` references to `magento` | **Update** | [#2599][] |
-| Removed GraphQL limitation entry in the Page Builder docs | **Update** | [#2630][] |
-| Added Page Builder integration videos | **Update** | [#2632][] |
-| Updated out of date content in the custom footer tutorial | **Update** | [#2652][] |
-| Added more verbose upgrade installation instructions in the README | **Update** | [#2662][] |
-| Updated routing tutorial to use the extensibility framework | **Update** | [#2670][] |
-| Updated README | **Update** | [#2676][] |
-| Removed deprecated topics and files in the docs project | **Update** | [#2684][] |
-| Fixed a typo in the Add a static route docs | **Bugfix** | [#2553][] |
-| Fixed a broken link | **Bugfix** | [#2642][] |
+| Description | Change type | PR |
+| ---------------------------------------------------------------------------- | ----------------- | --------- |
+| Created docs for the Internationalization feature | **Documentation** | [#2741][] |
+| Created a new tutorial for intercepting talons | **Documentation** | [#2777][] |
+| Created a new tutorial on how to use environment variables in front end code | **Documentation** | [#2819][] |
+| Created extensibility framework overview topic | **Documentation** | [#2863][] |
+| Removed "scroll to top" code in GraphQL tutorial | **Update** | [#2715][] |
+| Update code sample in static route tutorial | **Update** | [#2725][] |
+| Updated extensibility doc with minor fixes | **Update** | [#2742][] |
+| Updated code samples in tutorial | **Update** | [#2746][] |
+| Added info about RAIL model | **Update** | [#2761][] |
+| Added the Adobe logo to the doc site header | **Update** | [#2812][] |
+| Updated cloud deployment topic | **Update** | [#2871][] |
+| Refactored tutorials section | **Refactor** | [#2907][] |
+| Removed duplicate word from doc | **Bugfix** | [#2865][] |
### Misc
-| Description | Change type | PR |
-| ------------------------------------------------------------------------ | ----------- | ------------------- |
-| Created simple README content for the create-pwa package | **Update** | [#2415][] |
-| Updated contributors list | **Update** | [#2518][] |
-| Upgraded to @apollo/client@3 | **Update** | [#2560][] |
-| Configure dependabot to only open 5 pull requests and restrict to semver | **Update** | [#2526][] [#2528][] |
-| Bumped http-proxy-middleware from 0.19.1 to 0.19.2 | **Update** | [#2532][] |
-| Bumped apollo-link-retry from 2.2.15 to 2.2.16 | **Update** | [#2530][] |
-| Bumped @apollo/react-hooks from 3.1.3 to 3.1.5 | **Update** | [#2529][] |
-| Bumped lodash from 4.17.14 to 4.17.19 in /docker | **Update** | [#2556][] |
-| Bumped lodash from 4.17.15 to 4.17.19 in /pwa-devdocs | **Update** | [#2555][] |
-| Bumped elliptic from 6.5.2 to 6.5.3 | **Update** | [#2593][] |
-| Bumped elliptic from 6.5.2 to 6.5.3 in /pwa-devdocs | **Update** | [#2596][] |
-| Bumbed dot-prop to version 5.1.1 or later | **Update** | [#2601][] |
-| Bumped kramdown from 2.2.1 to 2.3.0 in /pwa-devdocs | **Update** | [#2614][] |
-| Removed dependabot version bump settings | **Update** | [#2646][] |
-| Bumped bl from 3.0.0 to 3.0.1 | **Update** | [#2675][] |
-| Fixed a PageBuilder visibility bug affecting slider buttons and links | **Bugfix** | [#2722][] |
+| Description | Change type | PR |
+| ----------------------------------------------------------------------------- | ----------- | --------- |
+| Removed the `venia-styleguide` package | **Update** | [#2706][] |
+| Update use of `Whitelist` to `Allowlist` | **Update** | [#2779][] |
+| Added `jsx-no-literals` linting rule | **Update** | [#2789][] |
+| Update PR template to add translation entry to checklist | **Update** | [#2800][] |
+| Fixed a Storybook bug related to `fetchLocaleData` in storybook config | **Bugfix** | [#2801][] |
+| Fix failing unit tests related to race conditions | **Bugfix** | [#2880][] |
+| Fixed a Storybook bug related to the relative import of a local custom loader | **Bugfix** | [#2912][] |
## Known issues
-- PWA Studio 8.0.0 is not fully compatible with Magento 2.3.6, which can prevent Users from using the Reset Password feature.
-- When switching stores as a logged in customer, the shopping cart is not reassigned to the new store.
+- If you are using Multi-Source Inventory(MSI), a GraphQL issue prevents users from adding a configurable product to the shopping cart on non-default store views.
+- Prerender feature is unable to cache HTML on Fastly enabled environments.
+- The `yarn watch` process may run out of memory if left running for an extended amount of time.
+ If an error occurs because of this, restart the watcher.
## Upgrading from a previous version
-The method for updating to 8.0.0 from a previous version depends on how PWA Studio is incorporated into your project.
+The method for updating to 9.0.0 from a previous version depends on how PWA Studio is incorporated into your project.
The following are common use cases we have identified and how to update the project code.
### Scaffolded project
@@ -273,7 +301,7 @@ This is the easiest way to work with the released versions of PWA Studio.
#### Upgrade method: Update `package.json`
-To upgrade to the latest version (currently 8.0.0), simply call `yarn add` on each of the `@magento` packages. This will both update `package.json` in your project, as well as install the latest versions.
+To upgrade to the latest version (currently 9.0.0), simply call `yarn add` on each of the `@magento` packages. This will both update `package.json` in your project, as well as install the latest versions.
Sample command:
@@ -283,124 +311,102 @@ yarn add @magento/eslint-config @magento/pagebuilder @magento/peregrine @magento
[pwa studio releases]: https://github.com/magento/pwa-studio/releases
-[`venia-ui-declare.js`]: https://github.com/magento/pwa-studio/blob/release/8.0/packages/venia-ui/lib/targets/venia-ui-declare.js
-[`peregrine-declare.js`]: https://github.com/magento/pwa-studio/blob/release/8.0/packages/peregrine/lib/targets/peregrine-declare.js
-[`declare-base.js`]: https://github.com/magento/pwa-studio/blob/release/8.0/packages/pwa-buildpack/lib/BuildBus/declare-base.js
-[dialog component]: https://github.com/magento/pwa-studio/tree/release/8.0/packages/venia-ui/lib/components/Dialog
-[pwa studio fundamentals]: http://pwastudio.io/tutorials/pwa-studio-fundamentals/
-[page builder]: https://magento.com/products/magento-commerce/page-builder
-
+[#2912]: https://github.com/magento/pwa-studio/pull/2912
+[#2911]: https://github.com/magento/pwa-studio/pull/2911
+[#2907]: https://github.com/magento/pwa-studio/pull/2907
+[#2902]: https://github.com/magento/pwa-studio/pull/2902
+[#2897]: https://github.com/magento/pwa-studio/pull/2897
+[#2896]: https://github.com/magento/pwa-studio/pull/2896
+[#2893]: https://github.com/magento/pwa-studio/pull/2893
+[#2891]: https://github.com/magento/pwa-studio/pull/2891
+[#2889]: https://github.com/magento/pwa-studio/pull/2889
+[#2888]: https://github.com/magento/pwa-studio/pull/2888
+[#2882]: https://github.com/magento/pwa-studio/pull/2882
+[#2881]: https://github.com/magento/pwa-studio/pull/2881
+[#2880]: https://github.com/magento/pwa-studio/pull/2880
+[#2879]: https://github.com/magento/pwa-studio/pull/2879
+[#2877]: https://github.com/magento/pwa-studio/pull/2877
+[#2871]: https://github.com/magento/pwa-studio/pull/2871
+[#2870]: https://github.com/magento/pwa-studio/pull/2870
+[#2868]: https://github.com/magento/pwa-studio/pull/2868
+[#2865]: https://github.com/magento/pwa-studio/pull/2865
+[#2863]: https://github.com/magento/pwa-studio/pull/2863
+[#2859]: https://github.com/magento/pwa-studio/pull/2859
+[#2857]: https://github.com/magento/pwa-studio/pull/2857
+[#2856]: https://github.com/magento/pwa-studio/pull/2856
+[#2855]: https://github.com/magento/pwa-studio/pull/2855
+[#2853]: https://github.com/magento/pwa-studio/pull/2853
+[#2852]: https://github.com/magento/pwa-studio/pull/2852
+[#2850]: https://github.com/magento/pwa-studio/pull/2850
+[#2847]: https://github.com/magento/pwa-studio/pull/2847
+[#2846]: https://github.com/magento/pwa-studio/pull/2846
+[#2844]: https://github.com/magento/pwa-studio/pull/2844
+[#2840]: https://github.com/magento/pwa-studio/pull/2840
+[#2838]: https://github.com/magento/pwa-studio/pull/2838
+[#2824]: https://github.com/magento/pwa-studio/pull/2824
+[#2823]: https://github.com/magento/pwa-studio/pull/2823
+[#2819]: https://github.com/magento/pwa-studio/pull/2819
+[#2818]: https://github.com/magento/pwa-studio/pull/2818
+[#2812]: https://github.com/magento/pwa-studio/pull/2812
+[#2811]: https://github.com/magento/pwa-studio/pull/2811
+[#2807]: https://github.com/magento/pwa-studio/pull/2807
+[#2806]: https://github.com/magento/pwa-studio/pull/2806
+[#2801]: https://github.com/magento/pwa-studio/pull/2801
+[#2800]: https://github.com/magento/pwa-studio/pull/2800
+[#2799]: https://github.com/magento/pwa-studio/pull/2799
+[#2794]: https://github.com/magento/pwa-studio/pull/2794
+[#2793]: https://github.com/magento/pwa-studio/pull/2793
+[#2789]: https://github.com/magento/pwa-studio/pull/2789
+[#2786]: https://github.com/magento/pwa-studio/pull/2786
+[#2785]: https://github.com/magento/pwa-studio/pull/2785
+[#2783]: https://github.com/magento/pwa-studio/pull/2783
+[#2782]: https://github.com/magento/pwa-studio/pull/2782
+[#2781]: https://github.com/magento/pwa-studio/pull/2781
+[#2779]: https://github.com/magento/pwa-studio/pull/2779
+[#2778]: https://github.com/magento/pwa-studio/pull/2778
+[#2777]: https://github.com/magento/pwa-studio/pull/2777
+[#2774]: https://github.com/magento/pwa-studio/pull/2774
+[#2772]: https://github.com/magento/pwa-studio/pull/2772
+[#2771]: https://github.com/magento/pwa-studio/pull/2771
+[#2767]: https://github.com/magento/pwa-studio/pull/2767
+[#2766]: https://github.com/magento/pwa-studio/pull/2766
+[#2765]: https://github.com/magento/pwa-studio/pull/2765
+[#2764]: https://github.com/magento/pwa-studio/pull/2764
+[#2763]: https://github.com/magento/pwa-studio/pull/2763
+[#2761]: https://github.com/magento/pwa-studio/pull/2761
+[#2759]: https://github.com/magento/pwa-studio/pull/2759
+[#2756]: https://github.com/magento/pwa-studio/pull/2756
+[#2755]: https://github.com/magento/pwa-studio/pull/2755
+[#2749]: https://github.com/magento/pwa-studio/pull/2749
+[#2746]: https://github.com/magento/pwa-studio/pull/2746
+[#2742]: https://github.com/magento/pwa-studio/pull/2742
+[#2741]: https://github.com/magento/pwa-studio/pull/2741
+[#2740]: https://github.com/magento/pwa-studio/pull/2740
+[#2735]: https://github.com/magento/pwa-studio/pull/2735
+[#2734]: https://github.com/magento/pwa-studio/pull/2734
+[#2728]: https://github.com/magento/pwa-studio/pull/2728
+[#2725]: https://github.com/magento/pwa-studio/pull/2725
+[#2721]: https://github.com/magento/pwa-studio/pull/2721
+[#2718]: https://github.com/magento/pwa-studio/pull/2718
+[#2715]: https://github.com/magento/pwa-studio/pull/2715
+[#2714]: https://github.com/magento/pwa-studio/pull/2714
+[#2712]: https://github.com/magento/pwa-studio/pull/2712
[#2708]: https://github.com/magento/pwa-studio/pull/2708
-[#2705]: https://github.com/magento/pwa-studio/pull/2705
-[#2698]: https://github.com/magento/pwa-studio/pull/2698
-[#2697]: https://github.com/magento/pwa-studio/pull/2697
+[#2706]: https://github.com/magento/pwa-studio/pull/2706
+[#2703]: https://github.com/magento/pwa-studio/pull/2703
+[#2696]: https://github.com/magento/pwa-studio/pull/2696
+[#2694]: https://github.com/magento/pwa-studio/pull/2694
[#2693]: https://github.com/magento/pwa-studio/pull/2693
-[#2684]: https://github.com/magento/pwa-studio/pull/2684
-[#2676]: https://github.com/magento/pwa-studio/pull/2676
-[#2675]: https://github.com/magento/pwa-studio/pull/2675
-[#2673]: https://github.com/magento/pwa-studio/pull/2673
-[#2670]: https://github.com/magento/pwa-studio/pull/2670
-[#2667]: https://github.com/magento/pwa-studio/pull/2667
-[#2665]: https://github.com/magento/pwa-studio/pull/2665
-[#2664]: https://github.com/magento/pwa-studio/pull/2664
-[#2663]: https://github.com/magento/pwa-studio/pull/2663
-[#2662]: https://github.com/magento/pwa-studio/pull/2662
-[#2660]: https://github.com/magento/pwa-studio/pull/2660
-[#2659]: https://github.com/magento/pwa-studio/pull/2659
-[#2657]: https://github.com/magento/pwa-studio/pull/2657
-[#2656]: https://github.com/magento/pwa-studio/pull/2656
-[#2655]: https://github.com/magento/pwa-studio/pull/2655
-[#2653]: https://github.com/magento/pwa-studio/pull/2653
-[#2652]: https://github.com/magento/pwa-studio/pull/2652
-[#2649]: https://github.com/magento/pwa-studio/pull/2649
-[#2646]: https://github.com/magento/pwa-studio/pull/2646
-[#2643]: https://github.com/magento/pwa-studio/pull/2643
-[#2642]: https://github.com/magento/pwa-studio/pull/2642
-[#2641]: https://github.com/magento/pwa-studio/pull/2641
-[#2637]: https://github.com/magento/pwa-studio/pull/2637
-[#2634]: https://github.com/magento/pwa-studio/pull/2634
-[#2632]: https://github.com/magento/pwa-studio/pull/2632
-[#2630]: https://github.com/magento/pwa-studio/pull/2630
-[#2620]: https://github.com/magento/pwa-studio/pull/2620
-[#2619]: https://github.com/magento/pwa-studio/pull/2619
-[#2618]: https://github.com/magento/pwa-studio/pull/2618
-[#2616]: https://github.com/magento/pwa-studio/pull/2616
-[#2614]: https://github.com/magento/pwa-studio/pull/2614
-[#2612]: https://github.com/magento/pwa-studio/pull/2612
-[#2611]: https://github.com/magento/pwa-studio/pull/2611
-[#2609]: https://github.com/magento/pwa-studio/pull/2609
-[#2605]: https://github.com/magento/pwa-studio/pull/2605
-[#2602]: https://github.com/magento/pwa-studio/pull/2602
-[#2601]: https://github.com/magento/pwa-studio/pull/2601
-[#2599]: https://github.com/magento/pwa-studio/pull/2599
-[#2596]: https://github.com/magento/pwa-studio/pull/2596
-[#2594]: https://github.com/magento/pwa-studio/pull/2594
-[#2593]: https://github.com/magento/pwa-studio/pull/2593
-[#2590]: https://github.com/magento/pwa-studio/pull/2590
-[#2588]: https://github.com/magento/pwa-studio/pull/2588
-[#2587]: https://github.com/magento/pwa-studio/pull/2587
-[#2584]: https://github.com/magento/pwa-studio/pull/2584
-[#2583]: https://github.com/magento/pwa-studio/pull/2583
-[#2582]: https://github.com/magento/pwa-studio/pull/2582
-[#2577]: https://github.com/magento/pwa-studio/pull/2577
-[#2576]: https://github.com/magento/pwa-studio/pull/2576
-[#2574]: https://github.com/magento/pwa-studio/pull/2574
-[#2572]: https://github.com/magento/pwa-studio/pull/2572
-[#2571]: https://github.com/magento/pwa-studio/pull/2571
-[#2569]: https://github.com/magento/pwa-studio/pull/2569
-[#2566]: https://github.com/magento/pwa-studio/pull/2566
-[#2562]: https://github.com/magento/pwa-studio/pull/2562
-[#2560]: https://github.com/magento/pwa-studio/pull/2560
-[#2559]: https://github.com/magento/pwa-studio/pull/2559
-[#2557]: https://github.com/magento/pwa-studio/pull/2557
-[#2556]: https://github.com/magento/pwa-studio/pull/2556
-[#2555]: https://github.com/magento/pwa-studio/pull/2555
-[#2554]: https://github.com/magento/pwa-studio/pull/2554
-[#2553]: https://github.com/magento/pwa-studio/pull/2553
-[#2550]: https://github.com/magento/pwa-studio/pull/2550
-[#2549]: https://github.com/magento/pwa-studio/pull/2549
-[#2548]: https://github.com/magento/pwa-studio/pull/2548
-[#2547]: https://github.com/magento/pwa-studio/pull/2547
-[#2545]: https://github.com/magento/pwa-studio/pull/2545
-[#2543]: https://github.com/magento/pwa-studio/pull/2543
-[#2538]: https://github.com/magento/pwa-studio/pull/2538
-[#2535]: https://github.com/magento/pwa-studio/pull/2535
-[#2532]: https://github.com/magento/pwa-studio/pull/2532
-[#2530]: https://github.com/magento/pwa-studio/pull/2530
-[#2529]: https://github.com/magento/pwa-studio/pull/2529
-[#2528]: https://github.com/magento/pwa-studio/pull/2528
-[#2527]: https://github.com/magento/pwa-studio/pull/2527
-[#2526]: https://github.com/magento/pwa-studio/pull/2526
-[#2523]: https://github.com/magento/pwa-studio/pull/2523
-[#2520]: https://github.com/magento/pwa-studio/pull/2520
-[#2518]: https://github.com/magento/pwa-studio/pull/2518
-[#2516]: https://github.com/magento/pwa-studio/pull/2516
-[#2514]: https://github.com/magento/pwa-studio/pull/2514
-[#2513]: https://github.com/magento/pwa-studio/pull/2513
-[#2511]: https://github.com/magento/pwa-studio/pull/2511
-[#2510]: https://github.com/magento/pwa-studio/pull/2510
-[#2509]: https://github.com/magento/pwa-studio/pull/2509
-[#2508]: https://github.com/magento/pwa-studio/pull/2508
-[#2506]: https://github.com/magento/pwa-studio/pull/2506
-[#2505]: https://github.com/magento/pwa-studio/pull/2505
-[#2504]: https://github.com/magento/pwa-studio/pull/2504
-[#2500]: https://github.com/magento/pwa-studio/pull/2500
-[#2498]: https://github.com/magento/pwa-studio/pull/2498
-[#2496]: https://github.com/magento/pwa-studio/pull/2496
-[#2494]: https://github.com/magento/pwa-studio/pull/2494
-[#2493]: https://github.com/magento/pwa-studio/pull/2493
-[#2492]: https://github.com/magento/pwa-studio/pull/2492
-[#2490]: https://github.com/magento/pwa-studio/pull/2490
-[#2484]: https://github.com/magento/pwa-studio/pull/2484
-[#2472]: https://github.com/magento/pwa-studio/pull/2472
-[#2443]: https://github.com/magento/pwa-studio/pull/2443
-[#2423]: https://github.com/magento/pwa-studio/pull/2423
-[#2415]: https://github.com/magento/pwa-studio/pull/2415
-[#2709]: https://github.com/magento/pwa-studio/pull/2709
-[#2722]: https://github.com/magento/pwa-studio/pull/2722
-[#2723]: https://github.com/magento/pwa-studio/pull/2723
-[#2726]: https://github.com/magento/pwa-studio/pull/2726
-[#2736]: https://github.com/magento/pwa-studio/pull/2736
-[#2727]: https://github.com/magento/pwa-studio/pull/2727
-[#2739]: https://github.com/magento/pwa-studio/pull/2739
-[#2744]: https://github.com/magento/pwa-studio/pull/2744
\ No newline at end of file
+[#2692]: https://github.com/magento/pwa-studio/pull/2692
+[#2690]: https://github.com/magento/pwa-studio/pull/2690
+[#2686]: https://github.com/magento/pwa-studio/pull/2686
+[#2685]: https://github.com/magento/pwa-studio/pull/2685
+[#2681]: https://github.com/magento/pwa-studio/pull/2681
+[#2680]: https://github.com/magento/pwa-studio/pull/2680
+[#2672]: https://github.com/magento/pwa-studio/pull/2672
+[#2671]: https://github.com/magento/pwa-studio/pull/2671
+[#2636]: https://github.com/magento/pwa-studio/pull/2636
+[#2918]: https://github.com/magento/pwa-studio/pull/2918
+[#2916]: https://github.com/magento/pwa-studio/pull/2916
+[#2929]: https://github.com/magento/pwa-studio/pull/2929
+[#2928]: https://github.com/magento/pwa-studio/pull/2928
diff --git a/README.md b/README.md
index 18db1bb0fc..36309782ee 100644
--- a/README.md
+++ b/README.md
@@ -79,36 +79,33 @@ The following members are the community maintainers for this project:
### Top Community Contributors
The PWA Studio project welcomes all codebase and documentation contributions.
-We would like to recognize the following community members for their efforts on improving the PWA Studio project in 2020.
-
-| Author | Commits | Added Lines | Removed Lines | Avg. Files |
-| -------------------- | ------- | ----------- | ------------- | ---------- |
-| Lars Roettig | 17 | 1430 | 1101 | 5.824 |
-| Ross McHugh | 12 | 1089 | 11 | 2 |
-| Lucas Calazans | 5 | 378 | 194 | 3.2 |
-| Luke Denton | 5 | 85 | 39 | 1.4 |
-| Jordan Eisenburger | 4 | 334 | 39 | 3 |
-| Kristof, Fooman | 4 | 103 | 74 | 3 |
-| Adam | 3 | 328 | 16 | 2.667 |
-| Harald Deiser | 3 | 150 | 123 | 12.667 |
-| Brendan Falkowski | 2 | 1168 | 416 | 12.5 |
-| Huy Kon | 2 | 991 | 39 | 10 |
-| Miguel Balparda | 2 | 8 | 6 | 1 |
-| Shikha Mishra | 2 | 16 | 12 | 2 |
-| christopher daniel | 2 | 163 | 106 | 6.5 |
-| sivakumarkoduru | 2 | 7 | 4 | 1.5 |
-| Alexander Taranovsky | 1 | 4 | 1 | 2 |
-| Andrii Beziazychnyi | 1 | 4 | 0 | 1 |
-| Cody Nguyễn | 1 | 1 | 1 | 1 |
-| Davide | 1 | 9 | 7 | 1 |
-| Dominic Fernando | 1 | 0 | 1 | 1 |
-| Evan Burrell | 1 | 92 | 29 | 11 |
-
-_Last Updated: September 3, 2020_
+We would like to recognize the following community members for their efforts on improving the PWA Studio project in our latest release.
+
+| Author | Commits | Added Lines | Removed Lines | Avg. Files |
+| ------------------- | ------- | ----------- | ------------- | ---------- |
+| Huy Kon | 11 | 2939 | 848 | 9.091 |
+| Brendan Falkowski | 2 | 3 | 13 | 1 |
+| Abrar Pathan | 1 | 7 | 0 | 1 |
+| Adam | 1 | 12 | 6 | 3 |
+| Ankur Raiyani | 1 | 89 | 30 | 5 |
+| Hiren Patel | 1 | 1 | 1 | 1 |
+| James Murphy | 1 | 53 | 0 | 1 |
+| Jon Vaughan | 1 | 1 | 1 | 1 |
+| Kristof, Fooman | 1 | 4 | 4 | 3 |
+| Lars Roettig | 1 | 211 | 16 | 7 |
+| Marcin Kwiatkowski | 1 | 985 | 689 | 17 |
+| Max Chadwick | 1 | 3 | 1 | 1 |
+| Papilion Dániel | 1 | 68 | 2 | 2 |
+| Sathiya Prakash | 1 | 2 | 2 | 1 |
+| Sergey Kolodyazhnyy | 1 | 4 | 1 | 2 |
+| Shankar Konar | 1 | 6 | 0 | 2 |
+| Treiberg, Artur | 1 | 202 | 79 | 25 |
+
+_Last Updated: January 11, 2021_
**Source:** [statistic.magento.engineering][]
-[statistic.magento.engineering]:
+[statistic.magento.engineering]:
[Contribution guide]: .github/CONTRIBUTING.md
[Coverage Status]: https://coveralls.io/repos/github/magento/pwa-studio/badge.svg?branch=master
diff --git a/jest.config.js b/jest.config.js
index 1a923367b1..ecf172b50b 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -153,6 +153,14 @@ const testReactComponents = inPackage => ({
id: 1,
locale: 'en_US',
store_name: 'Default Store View'
+ },
+ {
+ base_currency_code: 'EUR',
+ code: 'fr',
+ default_display_currency_code: 'EUR',
+ id: 2,
+ locale: 'fr_FR',
+ store_name: 'French Store View'
}
]
}
diff --git a/magento-compatibility.js b/magento-compatibility.js
index c7167caa6a..f8192afd5c 100644
--- a/magento-compatibility.js
+++ b/magento-compatibility.js
@@ -4,6 +4,7 @@
// PWA Studio version -> Magento version.
module.exports = {
+ '9.0.0': '2.4.2',
'8.0.0': '2.4.0 - 2.4.1',
'7.0.0': '2.3.5 - 2.4.0',
'6.0.1': '2.3.4 - 2.3.5',
diff --git a/package.json b/package.json
index 4472c9b3ec..c3a583cdba 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/pwa-studio",
- "version": "8.0.0",
+ "version": "9.0.0",
"private": true,
"workspaces": [
"packages/babel-preset-peregrine",
diff --git a/packages/create-pwa/lib/index.js b/packages/create-pwa/lib/index.js
index c47286f29e..5233f9f193 100644
--- a/packages/create-pwa/lib/index.js
+++ b/packages/create-pwa/lib/index.js
@@ -1,5 +1,6 @@
const { basename, resolve } = require('path');
const os = require('os');
+const fetch = require('node-fetch');
const changeCase = require('change-case');
const inquirer = require('inquirer');
const execa = require('execa');
@@ -9,9 +10,37 @@ const isInvalidPath = require('is-invalid-path');
const isValidNpmName = require('is-valid-npm-name');
const pkg = require('../package.json');
const {
- sampleBackends
+ sampleBackends: defaultSampleBackends
} = require('@magento/pwa-buildpack/lib/cli/create-project');
+const uniqBy = (array, property) => {
+ const map = new Map();
+
+ for (const element of array) {
+ if (element && element.hasOwnProperty(property)) {
+ map.set(element[property], element);
+ }
+ }
+
+ return Array.from(map.values());
+};
+
+const removeDuplicateBackends = backendEnvironments =>
+ uniqBy(backendEnvironments, 'url');
+
+const fetchSampleBackends = async () => {
+ try {
+ const res = await fetch(
+ 'https://fvp0esmt8f.execute-api.us-east-1.amazonaws.com/default/getSampleBackends'
+ );
+ const { sampleBackends } = await res.json();
+
+ return sampleBackends.environments;
+ } catch {
+ return [];
+ }
+};
+
module.exports = async () => {
console.log(chalk.greenBright(`${pkg.name} v${pkg.version}`));
console.log(
@@ -20,6 +49,16 @@ module.exports = async () => {
const userAgent = process.env.npm_config_user_agent || '';
const isYarn = userAgent.includes('yarn');
+ const sampleBackendEnvironments = await fetchSampleBackends();
+ const filteredBackendEnvironments = removeDuplicateBackends([
+ ...sampleBackendEnvironments,
+ ...defaultSampleBackends.environments
+ ]);
+ const sampleBackends = {
+ ...defaultSampleBackends,
+ environments: filteredBackendEnvironments
+ };
+
const questions = [
{
name: 'directory',
diff --git a/packages/create-pwa/package.json b/packages/create-pwa/package.json
index 183a0fd852..b66e646349 100644
--- a/packages/create-pwa/package.json
+++ b/packages/create-pwa/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/create-pwa",
- "version": "1.1.2",
+ "version": "1.2.0",
"publishConfig": {
"access": "public"
},
@@ -29,7 +29,7 @@
},
"homepage": "https://github.com/magento/pwa-studio/tree/master/packages/create-pwa#readme",
"dependencies": {
- "@magento/pwa-buildpack": "~7.0.0",
+ "@magento/pwa-buildpack": "~8.0.0",
"chalk": "^2.4.2",
"change-case": "^3.1.0",
"execa": "^1.0.0",
@@ -37,6 +37,7 @@
"inquirer": "^6.3.1",
"is-invalid-path": "^1.0.2",
"is-valid-npm-name": "^0.0.4",
+ "node-fetch": "~2.3.0",
"webpack": "^4.29.5"
}
}
diff --git a/packages/extensions/upward-security-headers/package.json b/packages/extensions/upward-security-headers/package.json
index 0c63e8e055..b765adebf7 100644
--- a/packages/extensions/upward-security-headers/package.json
+++ b/packages/extensions/upward-security-headers/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/upward-security-headers",
- "version": "1.0.0",
+ "version": "1.0.1",
"publishConfig": {
"access": "public"
},
@@ -13,8 +13,8 @@
"author": "Magento Commerce",
"license": "(OSL-3.0 OR AFL-3.0)",
"peerDependencies": {
- "@magento/pwa-buildpack": "~7.0.0",
- "@magento/venia-ui": "~5.0.0",
+ "@magento/pwa-buildpack": "~8.0.0",
+ "@magento/venia-ui": "~6.0.0",
"webpack": "~4.38.0"
},
"pwa-studio": {
diff --git a/packages/extensions/venia-adobe-data-layer/index.js b/packages/extensions/venia-adobe-data-layer/index.js
new file mode 100644
index 0000000000..818baa5ce0
--- /dev/null
+++ b/packages/extensions/venia-adobe-data-layer/index.js
@@ -0,0 +1,16 @@
+const { useEffect } = require('react');
+
+module.exports = original => props => {
+ useEffect(() => {
+ // define the global as soon as possible
+ globalThis.adobeDataLayer = globalThis.adobeDataLayer || [];
+
+ // import the library as late as possible
+ import(/* webpackChunkName: "acdl" */
+ /* webpackMode: "lazy" */
+ /* webpackPrefetch: true */
+ '@adobe/adobe-client-data-layer');
+ }, []);
+
+ return original(props);
+};
diff --git a/packages/extensions/venia-adobe-data-layer/intercept.js b/packages/extensions/venia-adobe-data-layer/intercept.js
new file mode 100644
index 0000000000..e8fd0420a3
--- /dev/null
+++ b/packages/extensions/venia-adobe-data-layer/intercept.js
@@ -0,0 +1,7 @@
+module.exports = targets => {
+ const { talons } = targets.of('@magento/peregrine');
+
+ talons.tap(({ App }) => {
+ App.useApp.wrapWith('@magento/venia-adobe-data-layer');
+ });
+};
diff --git a/packages/extensions/venia-adobe-data-layer/package.json b/packages/extensions/venia-adobe-data-layer/package.json
new file mode 100644
index 0000000000..21be73d6af
--- /dev/null
+++ b/packages/extensions/venia-adobe-data-layer/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@magento/venia-adobe-data-layer",
+ "version": "0.0.1",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Provides Adobe Client Data Layer support in PWA Studio",
+ "main": "./index.js",
+ "scripts": {
+ "clean": " "
+ },
+ "repository": "github:magento/pwa-studio",
+ "license": "(OSL-3.0 OR AFL-3.0)",
+ "dependencies": {
+ "@adobe/adobe-client-data-layer": "~1.1.3"
+ },
+ "peerDependencies": {
+ "@magento/peregrine": "~9.0.0",
+ "react": "~16.9.0"
+ },
+ "pwa-studio": {
+ "targets": {
+ "intercept": "./intercept"
+ }
+ }
+}
diff --git a/packages/extensions/venia-sample-backends/__tests__/__snapshots__/intercept.spec.js.snap b/packages/extensions/venia-sample-backends/__tests__/__snapshots__/intercept.spec.js.snap
new file mode 100644
index 0000000000..90b9282f16
--- /dev/null
+++ b/packages/extensions/venia-sample-backends/__tests__/__snapshots__/intercept.spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should call onFail if backend is inactive 1`] = `
+"https://www.magento-backend-2.3.4.com/ is inactive. Please consider using one of these other backends:
+
+ [{\\"name\\":\\"2.3.3-venia-cloud\\",\\"description\\":\\"Magento 2.3.3 with Venia sample data installed\\",\\"url\\":\\"https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/\\"}]
+"
+`;
+
+exports[`should call onFail with a different error message if environments is empty 1`] = `
+"https://www.magento-backend-2.3.4.com/ is inactive. Please consider using one of these other backends:
+
+ [{\\"name\\":\\"2.3.3-venia-cloud\\",\\"description\\":\\"Magento 2.3.3 with Venia sample data installed\\",\\"url\\":\\"https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/\\"}]
+"
+`;
+
+exports[`should log warning message in the console 1`] = `
+"
+ venia-sample-backends is a \\"development-only\\" extension, please remove it from your project's package.json before going to production.
+"
+`;
diff --git a/packages/extensions/venia-sample-backends/__tests__/intercept.spec.js b/packages/extensions/venia-sample-backends/__tests__/intercept.spec.js
new file mode 100644
index 0000000000..75aeddbcfa
--- /dev/null
+++ b/packages/extensions/venia-sample-backends/__tests__/intercept.spec.js
@@ -0,0 +1,75 @@
+jest.mock('node-fetch');
+const fetch = require('node-fetch');
+
+const { validateSampleBackend } = require('../intercept');
+
+const env = {
+ MAGENTO_BACKEND_URL: 'https://www.magento-backend-2.3.4.com/'
+};
+const onFail = jest.fn().mockName('onFail');
+const debug = jest.fn().mockName('debug');
+
+const args = { env, onFail, debug };
+
+beforeAll(() => {
+ console.warn = jest.fn();
+});
+
+test('should not call onFail if backend is active', async () => {
+ fetch.mockResolvedValueOnce({ ok: true });
+
+ await validateSampleBackend(args);
+
+ expect(onFail).not.toHaveBeenCalled();
+});
+
+test('should call onFail if backend is inactive', async () => {
+ fetch.mockResolvedValueOnce({ ok: false }).mockResolvedValueOnce({
+ json: jest.fn().mockResolvedValue({
+ sampleBackends: {
+ environments: [
+ {
+ name: '2.3.3-venia-cloud',
+ description:
+ 'Magento 2.3.3 with Venia sample data installed',
+ url:
+ 'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/'
+ },
+ {
+ name: '2.3.4-venia-cloud',
+ description:
+ 'Magento 2.3.4 with Venia sample data installed',
+ url: 'https://www.magento-backend-2.3.4.com/'
+ }
+ ]
+ }
+ })
+ });
+
+ await validateSampleBackend(args);
+
+ expect(onFail).toHaveBeenCalled();
+ expect(onFail.mock.calls[0][0]).toMatchSnapshot();
+});
+
+test('should call onFail with a different error message if environments is empty', async () => {
+ fetch.mockResolvedValueOnce({ ok: false }).mockResolvedValueOnce({
+ json: jest.fn().mockResolvedValue({
+ sampleBackends: {
+ environments: []
+ }
+ })
+ });
+
+ await validateSampleBackend(args);
+
+ expect(onFail).toHaveBeenCalled();
+ expect(onFail.mock.calls[0][0]).toMatchSnapshot();
+});
+
+test('should log warning message in the console', async () => {
+ await validateSampleBackend(args);
+
+ expect(console.warn).toHaveBeenCalled();
+ expect(console.warn.mock.calls[0][0]).toMatchSnapshot();
+});
diff --git a/packages/extensions/venia-sample-backends/intercept.js b/packages/extensions/venia-sample-backends/intercept.js
new file mode 100644
index 0000000000..b5abfa8fbb
--- /dev/null
+++ b/packages/extensions/venia-sample-backends/intercept.js
@@ -0,0 +1,101 @@
+const https = require('https');
+const fetch = require('node-fetch');
+const agent = new https.Agent({
+ rejectUnauthorized: false
+});
+
+const fetchWithAgent = async url => {
+ return await fetch(url, { agent });
+};
+
+const isBackendActive = async (magentoBackend, debug) => {
+ try {
+ const res = await fetchWithAgent(magentoBackend);
+
+ return res.ok;
+ } catch (err) {
+ debug(err);
+
+ return false;
+ }
+};
+
+const fetchBackends = async debug => {
+ try {
+ const res = await fetch(
+ 'https://fvp0esmt8f.execute-api.us-east-1.amazonaws.com/default/getSampleBackends'
+ );
+ const { sampleBackends } = await res.json();
+
+ return sampleBackends.environments;
+ } catch (err) {
+ debug(err);
+
+ return [];
+ }
+};
+
+/**
+ * Validation function to check if the backend being used is one of the sample backends provided
+ * by PWA Studio. If yes, the function validates if the backend is active. If not, it reports an
+ * error by calling the onFail function. In the error being reported, it sends the other sample
+ * backends that the developers can use.
+ *
+ * @param {Object} config.env - The ENV provided to the app, usually avaialable through process.ENV
+ * @param {Function} config.onFail - callback function to call on validation fail
+ * @param {Function} config.debug - function to log debug messages in console in debug mode
+ *
+ * To watch the debug messages, run the command with DEBUG=*runEnvValidators*
+ */
+const validateSampleBackend = async config => {
+ console.warn(
+ '\n venia-sample-backends is a "development-only" extension, please remove it from your project\'s package.json before going to production.\n'
+ );
+
+ const { env, onFail, debug } = config;
+
+ const backendIsActive = await isBackendActive(
+ env.MAGENTO_BACKEND_URL,
+ debug
+ );
+
+ if (!backendIsActive) {
+ debug(`${env.MAGENTO_BACKEND_URL} is inactive`);
+
+ debug('Fetching other backends');
+
+ const sampleBackends = await fetchBackends(debug);
+ const otherBackends = sampleBackends.filter(
+ ({ url }) => url !== env.MAGENTO_BACKEND_URL
+ );
+
+ debug('PWA Studio supports the following backends', sampleBackends);
+
+ debug('Reporting backend URL validation failure');
+ if (otherBackends.length) {
+ onFail(
+ `${
+ env.MAGENTO_BACKEND_URL
+ } is inactive. Please consider using one of these other backends: \n\n ${JSON.stringify(
+ otherBackends
+ )} \n`
+ );
+ } else {
+ onFail(
+ `${
+ env.MAGENTO_BACKEND_URL
+ } is inactive. Please consider using an active backend \n`
+ );
+ }
+ } else {
+ debug(`${env.MAGENTO_BACKEND_URL} is active`);
+ }
+};
+
+module.exports = targets => {
+ targets
+ .of('@magento/pwa-buildpack')
+ .validateEnv.tapPromise(validateSampleBackend);
+};
+
+module.exports.validateSampleBackend = validateSampleBackend;
diff --git a/packages/extensions/venia-sample-backends/package.json b/packages/extensions/venia-sample-backends/package.json
new file mode 100644
index 0000000000..4a0ef50495
--- /dev/null
+++ b/packages/extensions/venia-sample-backends/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@magento/venia-sample-backends",
+ "version": "0.0.1",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Provides demo backends and backend validation utils for PWA Studio.",
+ "main": "./intercept.js",
+ "scripts": {
+ "clean": " ",
+ "test": "jest"
+ },
+ "repository": "github:magento/pwa-studio",
+ "license": "(OSL-3.0 OR AFL-3.0)",
+ "peerDependencies": {
+ "@magento/pwa-buildpack": "~8.0.0",
+ "node-fetch": "~2.3.0"
+ },
+ "pwa-studio": {
+ "targets": {
+ "intercept": "./intercept"
+ }
+ }
+}
diff --git a/packages/extensions/venia-sample-language-packs/package.json b/packages/extensions/venia-sample-language-packs/package.json
index 33117cfcc4..e137e538a8 100644
--- a/packages/extensions/venia-sample-language-packs/package.json
+++ b/packages/extensions/venia-sample-language-packs/package.json
@@ -12,12 +12,12 @@
"repository": "github:magento/pwa-studio",
"license": "(OSL-3.0 OR AFL-3.0)",
"peerDependencies": {
- "@magento/pwa-buildpack": "~7.0.0",
- "@magento/venia-ui": "~5.0.0"
+ "@magento/pwa-buildpack": "~8.0.0",
+ "@magento/venia-ui": "~6.0.0"
},
"pwa-studio": {
"targets": {
"intercept": "./i18n-intercept"
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/pagebuilder/lib/parseStorageHtml.js b/packages/pagebuilder/lib/parseStorageHtml.js
index 9669351557..3a7fad0535 100644
--- a/packages/pagebuilder/lib/parseStorageHtml.js
+++ b/packages/pagebuilder/lib/parseStorageHtml.js
@@ -90,15 +90,17 @@ const convertToInlineStyles = document => {
const cssRules = styleBlock.sheet.cssRules;
Array.from(cssRules).forEach(rule => {
- const selectors = rule.selectorText
- .split(',')
- .map(selector => selector.trim());
- selectors.forEach(selector => {
- if (!styles[selector]) {
- styles[selector] = [];
- }
- styles[selector].push(rule.style);
- });
+ if (rule instanceof CSSStyleRule) {
+ const selectors = rule.selectorText
+ .split(',')
+ .map(selector => selector.trim());
+ selectors.forEach(selector => {
+ if (!styles[selector]) {
+ styles[selector] = [];
+ }
+ styles[selector].push(rule.style);
+ });
+ }
});
});
}
diff --git a/packages/pagebuilder/package.json b/packages/pagebuilder/package.json
index 2da36b9254..93f414f183 100644
--- a/packages/pagebuilder/package.json
+++ b/packages/pagebuilder/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/pagebuilder",
- "version": "3.0.0",
+ "version": "4.0.0",
"publishConfig": {
"access": "public"
},
@@ -34,9 +34,9 @@
"homepage": "https://github.com/magento/pwa-studio/tree/master/packages/pagebuilder#readme",
"dependencies": {},
"devDependencies": {
- "@magento/peregrine": "~8.0.0",
- "@magento/pwa-buildpack": "~7.0.0",
- "@magento/venia-ui": "~5.0.0",
+ "@magento/peregrine": "~9.0.0",
+ "@magento/pwa-buildpack": "~8.0.0",
+ "@magento/venia-ui": "~6.0.0",
"@storybook/react": "~5.2.6",
"jarallax": "~1.11.1",
"load-google-maps-api": "~2.0.1",
@@ -50,9 +50,9 @@
"peerDependencies": {
"@apollo/client": "~3.1.2",
"@magento/babel-preset-peregrine": "~1.1.0",
- "@magento/peregrine": "~8.0.0",
- "@magento/pwa-buildpack": "~7.0.0",
- "@magento/venia-ui": "~5.0.0",
+ "@magento/peregrine": "~9.0.0",
+ "@magento/pwa-buildpack": "~8.0.0",
+ "@magento/venia-ui": "~6.0.0",
"jarallax": "~1.11.1",
"load-google-maps-api": "~2.0.1",
"lodash.escape": "~4.0.1",
diff --git a/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap b/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap
index fd59da86ce..c16c97b2bd 100644
--- a/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap
+++ b/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap
@@ -56,6 +56,14 @@ Object {
"merge": [Function],
"read": [Function],
},
+ "orders": Object {
+ "items": Object {
+ "merge": true,
+ },
+ "keyArgs": Array [
+ "filter",
+ ],
+ },
},
"keyFields": [Function],
},
@@ -66,6 +74,9 @@ Object {
},
},
},
+ "CustomerPaymentTokens": Object {
+ "keyFields": [Function],
+ },
"ProductImage": Object {
"keyFields": Array [
"url",
diff --git a/packages/peregrine/lib/Apollo/policies/index.js b/packages/peregrine/lib/Apollo/policies/index.js
index 5f5669b6c7..a152a29c80 100644
--- a/packages/peregrine/lib/Apollo/policies/index.js
+++ b/packages/peregrine/lib/Apollo/policies/index.js
@@ -130,6 +130,12 @@ const typePolicies = {
// If there are no cached addresses that's fine - the schema
// shows that it is a nullable field.
}
+ },
+ orders: {
+ keyArgs: ['filter'],
+ items: {
+ merge: true
+ }
}
}
},
@@ -143,6 +149,9 @@ const typePolicies = {
}
}
},
+ CustomerPaymentTokens: {
+ keyFields: () => 'CustomerPaymentTokens'
+ },
ProductImage: {
keyFields: ['url']
},
diff --git a/packages/peregrine/lib/PeregrineContextProvider/__tests__/__snapshots__/peregrineContextProvider.spec.js.snap b/packages/peregrine/lib/PeregrineContextProvider/__tests__/__snapshots__/peregrineContextProvider.spec.js.snap
index 678bc37b62..c15fe3aaab 100644
--- a/packages/peregrine/lib/PeregrineContextProvider/__tests__/__snapshots__/peregrineContextProvider.spec.js.snap
+++ b/packages/peregrine/lib/PeregrineContextProvider/__tests__/__snapshots__/peregrineContextProvider.spec.js.snap
@@ -7,7 +7,9 @@ exports[`render wraps children with all peregrine context providers 1`] = `
-
+
+
+
diff --git a/packages/peregrine/lib/PeregrineContextProvider/peregrineContextProvider.js b/packages/peregrine/lib/PeregrineContextProvider/peregrineContextProvider.js
index f2e79d627e..23fa383364 100644
--- a/packages/peregrine/lib/PeregrineContextProvider/peregrineContextProvider.js
+++ b/packages/peregrine/lib/PeregrineContextProvider/peregrineContextProvider.js
@@ -5,6 +5,7 @@ import CartContextProvider from '../context/cart';
import CatalogContextProvider from '../context/catalog';
import CheckoutContextProvider from '../context/checkout';
import ErrorContextProvider from '../context/unhandledErrors';
+import RootComponentsProvider from '../context/rootComponents';
import UserContextProvider from '../context/user';
/**
@@ -18,7 +19,8 @@ const contextProviders = [
UserContextProvider,
CatalogContextProvider,
CartContextProvider,
- CheckoutContextProvider
+ CheckoutContextProvider,
+ RootComponentsProvider
];
const PeregrineContextProvider = ({ children }) => {
diff --git a/packages/peregrine/lib/Router/__tests__/resolveUnknownRoute.test.js b/packages/peregrine/lib/Router/__tests__/resolveUnknownRoute.test.js
index b2b963bc54..20a89e9ea5 100644
--- a/packages/peregrine/lib/Router/__tests__/resolveUnknownRoute.test.js
+++ b/packages/peregrine/lib/Router/__tests__/resolveUnknownRoute.test.js
@@ -156,6 +156,9 @@ test('calls fetchRoute when response is not cached', async () => {
).resolves.toMatchObject(ProductPageResponse);
expect(fetch).toHaveBeenCalledTimes(1);
+ expect(fetch.mock.calls[0][0].toString()).toEqual(
+ 'https://store.com/?query=query+ResolveURL%28%24url%3A+String%21%29+%7B%0A++++++++urlResolver%28url%3A+%24url%29+%7B%0A++++++++++++type%0A++++++++++++id%0A++++++++++++relative_url%0A++++++++++++redirectCode%0A++++++++%7D%0A++++%7D&variables=%7B%22url%22%3A%22foo-bar.html%22%7D&operationName=ResolveURL'
+ );
});
test('urlResolver path: throws errors from GraphQL and does not cache', async () => {
diff --git a/packages/peregrine/lib/Router/resolveUnknownRoute.js b/packages/peregrine/lib/Router/resolveUnknownRoute.js
index 6e093370ef..7d8ce44e72 100644
--- a/packages/peregrine/lib/Router/resolveUnknownRoute.js
+++ b/packages/peregrine/lib/Router/resolveUnknownRoute.js
@@ -111,7 +111,7 @@ function fetchRoute(opts) {
}
}`;
- const url = new URL('/graphql', opts.apiBase);
+ const url = new URL(opts.apiBase);
url.searchParams.set('query', query);
url.searchParams.set('variables', JSON.stringify({ url: route }));
url.searchParams.set('operationName', 'ResolveURL');
diff --git a/packages/peregrine/lib/context/rootComponents.js b/packages/peregrine/lib/context/rootComponents.js
new file mode 100644
index 0000000000..711cf34110
--- /dev/null
+++ b/packages/peregrine/lib/context/rootComponents.js
@@ -0,0 +1,17 @@
+import React, { createContext, useContext, useState } from 'react';
+
+const RootComponentsContext = createContext();
+
+const RootComponentsProvider = props => {
+ const { children } = props;
+ const state = useState(new Map());
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default RootComponentsProvider;
+export const useRootComponents = () => useContext(RootComponentsContext);
diff --git a/packages/peregrine/lib/store/actions/checkout/asyncActions.js b/packages/peregrine/lib/store/actions/checkout/asyncActions.js
index 66e89c3e72..4a36dc2c5e 100644
--- a/packages/peregrine/lib/store/actions/checkout/asyncActions.js
+++ b/packages/peregrine/lib/store/actions/checkout/asyncActions.js
@@ -12,7 +12,7 @@ export const beginCheckout = () =>
// Before we begin, reset the state of checkout to clear out stale data.
dispatch(actions.reset());
- const storedAvailableShippingMethods = await retreiveAvailableShippingMethods();
+ const storedAvailableShippingMethods = await retrieveAvailableShippingMethods();
const storedBillingAddress = await retrieveBillingAddress();
const storedPaymentMethod = await retrievePaymentMethod();
const storedShippingAddress = await retrieveShippingAddress();
@@ -353,7 +353,7 @@ async function clearAvailableShippingMethods() {
return storage.removeItem('availableShippingMethods');
}
-async function retreiveAvailableShippingMethods() {
+async function retrieveAvailableShippingMethods() {
return storage.getItem('availableShippingMethods');
}
diff --git a/packages/peregrine/lib/store/reducers/catalog.js b/packages/peregrine/lib/store/reducers/catalog.js
index ba27e27abf..8c3643c907 100644
--- a/packages/peregrine/lib/store/reducers/catalog.js
+++ b/packages/peregrine/lib/store/reducers/catalog.js
@@ -18,8 +18,7 @@ const initialState = {
categories: {},
currentPage: 1,
pageSize: 6,
- prevPageTotal: null,
- rootCategoryId: 2
+ prevPageTotal: null
};
const reducerMap = {
@@ -71,12 +70,6 @@ const reducerMap = {
}
};
},
- [actions.setRootCategory]: (state, { payload }) => {
- return {
- ...state,
- rootCategoryId: payload
- };
- },
[actions.setCurrentPage.receive]: (state, { payload, error }) => {
if (error) {
return state;
diff --git a/packages/peregrine/lib/talons/AccountMenu/useAccountMenuItems.js b/packages/peregrine/lib/talons/AccountMenu/useAccountMenuItems.js
index 66700af02f..14447dc633 100644
--- a/packages/peregrine/lib/talons/AccountMenu/useAccountMenuItems.js
+++ b/packages/peregrine/lib/talons/AccountMenu/useAccountMenuItems.js
@@ -20,16 +20,17 @@ export const useAccountMenuItems = props => {
id: 'accountMenu.orderHistoryLink',
url: '/order-history'
},
- {
- name: 'Store Credit & Gift Cards',
- id: 'accountMenu.storeCreditLink',
- url: ''
- },
- {
- name: 'Favorites Lists',
- id: 'accountMenu.favoritesListsLink',
- url: '/wishlist'
- },
+ // Hide links until features are completed
+ // {
+ // name: 'Store Credit & Gift Cards',
+ // id: 'accountMenu.storeCreditLink',
+ // url: ''
+ // },
+ // {
+ // name: 'Favorites Lists',
+ // id: 'accountMenu.favoritesListsLink',
+ // url: '/wishlist'
+ // },
{
name: 'Address Book',
id: 'accountMenu.addressBookLink',
diff --git a/packages/peregrine/lib/talons/AddressBookPage/__tests__/useAddressBookPage.spec.js b/packages/peregrine/lib/talons/AddressBookPage/__tests__/useAddressBookPage.spec.js
index 691639d750..4ed9871a6f 100644
--- a/packages/peregrine/lib/talons/AddressBookPage/__tests__/useAddressBookPage.spec.js
+++ b/packages/peregrine/lib/talons/AddressBookPage/__tests__/useAddressBookPage.spec.js
@@ -10,7 +10,14 @@ jest.mock('@apollo/client', () => {
useQuery: jest.fn(() => ({
data: null,
loading: false
- }))
+ })),
+ useMutation: jest.fn(() => [
+ jest.fn(),
+ {
+ error: false,
+ loading: false
+ }
+ ])
};
});
@@ -62,7 +69,25 @@ test('it returns the proper shape', () => {
// Assert.
const talonProps = log.mock.calls[0][0];
const actualKeys = Object.keys(talonProps);
- const expectedKeys = ['customerAddresses', 'handleAddAddress'];
+ const expectedKeys = [
+ 'confirmDeleteAddressId',
+ 'countryDisplayNameMap',
+ 'customerAddresses',
+ 'formErrors',
+ 'formProps',
+ 'handleAddAddress',
+ 'handleCancelDeleteAddress',
+ 'handleCancelDialog',
+ 'handleConfirmDeleteAddress',
+ 'handleConfirmDialog',
+ 'handleDeleteAddress',
+ 'handleEditAddress',
+ 'isDeletingCustomerAddress',
+ 'isDialogBusy',
+ 'isDialogEditMode',
+ 'isDialogOpen',
+ 'isLoading'
+ ];
expect(actualKeys.sort()).toEqual(expectedKeys.sort());
});
@@ -71,6 +96,7 @@ test('it returns the customerAddresses correctly when present', () => {
const mockCustomerAddresses = ['a', 'b', 'c'];
useQuery.mockReturnValueOnce({
data: {
+ countries: [],
customer: {
addresses: mockCustomerAddresses
}
@@ -88,7 +114,7 @@ test('it returns the customerAddresses correctly when present', () => {
test('it returns an empty customerAddresses array when customer data is missing', () => {
// Arrange.
useQuery.mockReturnValueOnce({
- data: {}
+ data: null
});
// Act.
@@ -104,6 +130,7 @@ test('it returns an empty customerAddresses array when address data is missing',
// Arrange.
useQuery.mockReturnValueOnce({
data: {
+ countries: [],
customer: {}
}
});
@@ -116,3 +143,51 @@ test('it returns an empty customerAddresses array when address data is missing',
expect(customerAddresses).toBeInstanceOf(Array);
expect(customerAddresses).toHaveLength(0);
});
+
+test('isLoading is true without addresses in cache', () => {
+ useQuery.mockReturnValueOnce({
+ data: null,
+ loading: true
+ });
+
+ createTestInstance( );
+
+ const { isLoading } = log.mock.calls[0][0];
+ expect(isLoading).toBe(true);
+});
+
+test('isLoading is false when refetching in the background', () => {
+ useQuery.mockReturnValueOnce({
+ data: {
+ countries: [],
+ customer: {}
+ },
+ loading: true
+ });
+
+ createTestInstance( );
+
+ const { isLoading } = log.mock.calls[0][0];
+ expect(isLoading).toBe(false);
+});
+
+test('returns map of country display names', () => {
+ useQuery.mockReturnValueOnce({
+ data: {
+ countries: [
+ { id: 'US', full_name_locale: 'United States' },
+ { id: 'FR', full_name_locale: 'France' }
+ ]
+ }
+ });
+
+ createTestInstance( );
+
+ const { countryDisplayNameMap } = log.mock.calls[0][0];
+ expect(countryDisplayNameMap).toMatchInlineSnapshot(`
+ Map {
+ "US" => "United States",
+ "FR" => "France",
+ }
+ `);
+});
diff --git a/packages/peregrine/lib/talons/AddressBookPage/addressBookFragments.gql.js b/packages/peregrine/lib/talons/AddressBookPage/addressBookFragments.gql.js
new file mode 100644
index 0000000000..390b70396b
--- /dev/null
+++ b/packages/peregrine/lib/talons/AddressBookPage/addressBookFragments.gql.js
@@ -0,0 +1,23 @@
+import { gql } from '@apollo/client';
+
+export const CustomerAddressBookAddressFragment = gql`
+ fragment CustomerAddressBookAddressFragment on CustomerAddress {
+ __typename
+ id
+ city
+ country_code
+ default_billing
+ default_shipping
+ firstname
+ lastname
+ middlename
+ postcode
+ region {
+ region
+ region_code
+ region_id
+ }
+ street
+ telephone
+ }
+`;
diff --git a/packages/peregrine/lib/talons/AddressBookPage/addressBookPage.gql.js b/packages/peregrine/lib/talons/AddressBookPage/addressBookPage.gql.js
new file mode 100644
index 0000000000..2ae4071509
--- /dev/null
+++ b/packages/peregrine/lib/talons/AddressBookPage/addressBookPage.gql.js
@@ -0,0 +1,65 @@
+import { gql } from '@apollo/client';
+
+import { CustomerAddressBookAddressFragment } from './addressBookFragments.gql';
+
+export const GET_CUSTOMER_ADDRESSES = gql`
+ query GetCustomerAddressesForAddressBook {
+ customer {
+ id
+ addresses {
+ id
+ ...CustomerAddressBookAddressFragment
+ }
+ }
+ countries {
+ id
+ full_name_locale
+ }
+ }
+ ${CustomerAddressBookAddressFragment}
+`;
+
+/**
+ * We use the connection key directive here because Apollo will save
+ * this customer's PII in localStorage if not.
+ */
+export const ADD_NEW_CUSTOMER_ADDRESS = gql`
+ mutation AddNewCustomerAddressToAddressBook(
+ $address: CustomerAddressInput!
+ ) {
+ createCustomerAddress(input: $address)
+ @connection(key: "createCustomerAddress") {
+ # We don't manually write to the cache to update the collection
+ # after adding a new address so there's no need to query for a bunch
+ # of address fields here. We use refetchQueries to refresh the list.
+ id
+ }
+ }
+`;
+
+export const UPDATE_CUSTOMER_ADDRESS = gql`
+ mutation UpdateCustomerAddressInAddressBook(
+ $addressId: Int!
+ $updated_address: CustomerAddressInput!
+ ) {
+ updateCustomerAddress(id: $addressId, input: $updated_address)
+ @connection(key: "updateCustomerAddress") {
+ id
+ ...CustomerAddressBookAddressFragment
+ }
+ }
+ ${CustomerAddressBookAddressFragment}
+`;
+
+export const DELETE_CUSTOMER_ADDRESS = gql`
+ mutation DeleteCustomerAddressFromAddressBook($addressId: Int!) {
+ deleteCustomerAddress(id: $addressId)
+ }
+`;
+
+export default {
+ createCustomerAddressMutation: ADD_NEW_CUSTOMER_ADDRESS,
+ deleteCustomerAddressMutation: DELETE_CUSTOMER_ADDRESS,
+ getCustomerAddressesQuery: GET_CUSTOMER_ADDRESSES,
+ updateCustomerAddressMutation: UPDATE_CUSTOMER_ADDRESS
+};
diff --git a/packages/peregrine/lib/talons/AddressBookPage/useAddressBookPage.js b/packages/peregrine/lib/talons/AddressBookPage/useAddressBookPage.js
index 3632ad07d0..c23667cedf 100644
--- a/packages/peregrine/lib/talons/AddressBookPage/useAddressBookPage.js
+++ b/packages/peregrine/lib/talons/AddressBookPage/useAddressBookPage.js
@@ -1,26 +1,34 @@
-import { useCallback, useEffect } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
-import { useQuery } from '@apollo/client';
+import { useMutation, useQuery } from '@apollo/client';
import { useAppContext } from '@magento/peregrine/lib/context/app';
import { useUserContext } from '@magento/peregrine/lib/context/user';
+import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
+
+import defaultOperations from './addressBookPage.gql';
/**
* A talon to support the functionality of the Address Book page.
*
+ * @function
+ *
* @param {Object} props
- * @param {Object} props.queries - GraphQL queries to be run by the talon.
+ * @param {Object} props.operations - GraphQL operations to be run by the talon.
*
+ * @returns {AddressBookPageTalonProps}
*
- * @returns {Object} talonProps
- * @returns {Object} talonProps.data - The user's address book data.
- * @returns {Boolean} talonProps.isLoading - Indicates whether the user's
- * address book data is loading.
+ * @example Importing into your project
+ * import { useAddressBookPage } from '@magento/peregrine/lib/talons/AddressBookPage/useAddressBookPage';
*/
-export const useAddressBookPage = props => {
+export const useAddressBookPage = (props = {}) => {
+ const operations = mergeOperations(defaultOperations, props.operations);
const {
- queries: { getCustomerAddressesQuery }
- } = props;
+ createCustomerAddressMutation,
+ deleteCustomerAddressMutation,
+ getCustomerAddressesQuery,
+ updateCustomerAddressMutation
+ } = operations;
const [
,
@@ -28,14 +36,54 @@ export const useAddressBookPage = props => {
actions: { setPageLoading }
}
] = useAppContext();
- const history = useHistory();
const [{ isSignedIn }] = useUserContext();
+
+ const history = useHistory();
+
const { data: customerAddressesData, loading } = useQuery(
getCustomerAddressesQuery,
{
+ fetchPolicy: 'cache-and-network',
skip: !isSignedIn
}
);
+ const [
+ deleteCustomerAddress,
+ { loading: isDeletingCustomerAddress }
+ ] = useMutation(deleteCustomerAddressMutation);
+
+ const [confirmDeleteAddressId, setConfirmDeleteAddressId] = useState();
+
+ const isRefetching = !!customerAddressesData && loading;
+ const customerAddresses =
+ (customerAddressesData &&
+ customerAddressesData.customer &&
+ customerAddressesData.customer.addresses) ||
+ [];
+
+ const [
+ createCustomerAddress,
+ {
+ error: createCustomerAddressError,
+ loading: isCreatingCustomerAddress
+ }
+ ] = useMutation(createCustomerAddressMutation);
+ const [
+ updateCustomerAddress,
+ {
+ error: updateCustomerAddressError,
+ loading: isUpdatingCustomerAddress
+ }
+ ] = useMutation(updateCustomerAddressMutation);
+
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const [isDialogEditMode, setIsDialogEditMode] = useState(false);
+ const [formAddress, setFormAddress] = useState({});
+
+ // Use local state to determine whether to display errors or not.
+ // Could be replaced by a "reset mutation" function from apollo client.
+ // https://github.com/apollographql/apollo-feature-requests/issues/170
+ const [displayError, setDisplayError] = useState(false);
// If the user is no longer signed in, redirect to the home page.
useEffect(() => {
@@ -46,21 +94,181 @@ export const useAddressBookPage = props => {
// Update the page indicator if the GraphQL query is in flight.
useEffect(() => {
- setPageLoading(loading);
- }, [loading, setPageLoading]);
+ setPageLoading(isRefetching);
+ }, [isRefetching, setPageLoading]);
const handleAddAddress = useCallback(() => {
- alert('TODO!');
+ // Hide all previous errors when we open the dialog.
+ setDisplayError(false);
+
+ setIsDialogEditMode(false);
+ setFormAddress({ country_code: 'US' });
+ setIsDialogOpen(true);
}, []);
- const customerAddresses =
- (customerAddressesData &&
- customerAddressesData.customer &&
- customerAddressesData.customer.addresses) ||
- [];
+ const handleDeleteAddress = useCallback(addressId => {
+ setConfirmDeleteAddressId(addressId);
+ }, []);
+
+ const handleCancelDeleteAddress = useCallback(() => {
+ setConfirmDeleteAddressId(null);
+ }, []);
+
+ const handleConfirmDeleteAddress = useCallback(async () => {
+ try {
+ await deleteCustomerAddress({
+ variables: { addressId: confirmDeleteAddressId },
+ refetchQueries: [{ query: getCustomerAddressesQuery }],
+ awaitRefetchQueries: true
+ });
+
+ setConfirmDeleteAddressId(null);
+ } catch {
+ return;
+ }
+ }, [
+ confirmDeleteAddressId,
+ deleteCustomerAddress,
+ getCustomerAddressesQuery
+ ]);
+
+ const handleEditAddress = useCallback(address => {
+ // Hide all previous errors when we open the dialog.
+ setDisplayError(false);
+
+ setIsDialogEditMode(true);
+ setFormAddress(address);
+ setIsDialogOpen(true);
+ }, []);
+
+ const handleCancelDialog = useCallback(() => {
+ setIsDialogOpen(false);
+ }, []);
+
+ const handleConfirmDialog = useCallback(
+ async formValues => {
+ if (isDialogEditMode) {
+ try {
+ await updateCustomerAddress({
+ variables: {
+ addressId: formAddress.id,
+ updated_address: formValues
+ },
+ refetchQueries: [{ query: getCustomerAddressesQuery }],
+ awaitRefetchQueries: true
+ });
+
+ setIsDialogOpen(false);
+ } catch {
+ // Make sure any errors from the mutations are displayed.
+ setDisplayError(true);
+
+ // we have an onError link that logs errors, and FormError
+ // already renders this error, so just return to avoid
+ // triggering the success callback
+ return;
+ }
+ } else {
+ try {
+ await createCustomerAddress({
+ variables: { address: formValues },
+ refetchQueries: [{ query: getCustomerAddressesQuery }],
+ awaitRefetchQueries: true
+ });
+
+ setIsDialogOpen(false);
+ } catch {
+ // Make sure any errors from the mutations are displayed.
+ setDisplayError(true);
+
+ // we have an onError link that logs errors, and FormError
+ // already renders this error, so just return to avoid
+ // triggering the success callback
+ return;
+ }
+ }
+ },
+ [
+ createCustomerAddress,
+ formAddress,
+ getCustomerAddressesQuery,
+ isDialogEditMode,
+ updateCustomerAddress
+ ]
+ );
+
+ const formErrors = useMemo(() => {
+ if (displayError) {
+ return new Map([
+ ['createCustomerAddressMutation', createCustomerAddressError],
+ ['updateCustomerAddressMutation', updateCustomerAddressError]
+ ]);
+ } else return new Map();
+ }, [createCustomerAddressError, displayError, updateCustomerAddressError]);
+
+ // use data from backend until Intl.DisplayNames is widely supported
+ const countryDisplayNameMap = useMemo(() => {
+ const countryMap = new Map();
+
+ if (customerAddressesData) {
+ const { countries } = customerAddressesData;
+ countries.forEach(country => {
+ countryMap.set(country.id, country.full_name_locale);
+ });
+ }
+
+ return countryMap;
+ }, [customerAddressesData]);
+
+ const isDialogBusy = isCreatingCustomerAddress || isUpdatingCustomerAddress;
+ const isLoadingWithoutData = !customerAddressesData && loading;
+
+ const formProps = {
+ initialValues: formAddress
+ };
return {
+ confirmDeleteAddressId,
+ countryDisplayNameMap,
customerAddresses,
- handleAddAddress
+ formErrors,
+ formProps,
+ handleAddAddress,
+ handleCancelDeleteAddress,
+ handleCancelDialog,
+ handleConfirmDeleteAddress,
+ handleConfirmDialog,
+ handleDeleteAddress,
+ handleEditAddress,
+ isDeletingCustomerAddress,
+ isDialogBusy,
+ isDialogEditMode,
+ isDialogOpen,
+ isLoading: isLoadingWithoutData
};
};
+
+/**
+ * Object type returned by the {@link useAddressBookPage} talon.
+ * It provides props data to use when rendering the address book page component.
+ *
+ * @typedef {Object} AddressBookPageTalonProps
+ *
+ * @property {String} confirmDeleteAddressId - The id of the address that is waiting to be confirmed for deletion.
+ * @property {Map} countryDisplayNameMap - A Map of country id to its localized display name.
+ * @property {Array} customerAddresses - A list of customer addresses.
+ * @property {Map} formErrors - A Map of form errors.
+ * @property {Object} formProps - Properties to pass to the add/edit form.
+ * @property {Function} handleAddAdddress - Function to invoke when adding a new address.
+ * @property {Function} handleCancelDeleteAddress - Function to deny the confirmation of deleting an address.
+ * @property {Function} handleCancelDialog - Function to invoke when cancelling the add/edit dialog.
+ * @property {Function} handleConfirmDeleteAddress - Function to invoke to accept the confirmation of deleting an address.
+ * @property {Function} handleConfirmDialog - Function to invoke when submitting the add/edit dialog.
+ * @property {Function} handleDeleteAddress - Function to invoke to begin the address deletion process.
+ * @property {Function} handleEditAddress - Function to invoke when editing an existing address.
+ * @property {Boolean} isDeletingCustomerAddress - Whether an address deletion is currently in progress.
+ * @property {Boolean} isDialogBusy - Whether actions inside the dialog should be disabled.
+ * @property {Boolean} isDialogEditMode - Whether the dialog is in edit mode (true) or add new mode (false).
+ * @property {Boolean} isDialogOpen - Whether the dialog should be open.
+ * @property {Boolean} isLoading - Whether the page is loading.
+ */
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useEditModal.spec.js.snap b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useEditModal.spec.js.snap
index 0885cf15e2..648bf890ec 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useEditModal.spec.js.snap
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useEditModal.spec.js.snap
@@ -2,8 +2,6 @@
exports[`returns correct shape 1`] = `
Object {
- "handleClose": [MockFunction closeDrawer],
- "isOpen": false,
"setVariantPrice": [Function],
"variantPrice": null,
}
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useProductForm.spec.js.snap b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useProductForm.spec.js.snap
index e522015787..01a2b38b64 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useProductForm.spec.js.snap
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/__snapshots__/useProductForm.spec.js.snap
@@ -122,8 +122,10 @@ Object {
"updateQuantityMutation" => null,
"updateConfigurableOptionsMutation" => null,
},
+ "handleClose": [Function],
"handleOptionSelection": [Function],
"handleSubmit": [Function],
+ "isDialogOpen": true,
"isLoading": false,
"isSaving": false,
}
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useEditModal.spec.js b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useEditModal.spec.js
index c8ed7349c7..d684ab4284 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useEditModal.spec.js
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useEditModal.spec.js
@@ -13,34 +13,6 @@ const Component = props => {
return ;
};
-describe('return correct open status', () => {
- test('edit modal is closed', () => {
- useAppContext.mockReturnValueOnce([
- { drawer: null },
- { closeDrawer: jest.fn() }
- ]);
-
- const tree = createTestInstance( );
- const { root } = tree;
- const { talonProps } = root.findByType('i').props;
-
- expect(talonProps.isOpen).toEqual(false);
- });
-
- test('edit modal is open', () => {
- useAppContext.mockReturnValueOnce([
- { drawer: 'product.edit' },
- { closeDrawer: jest.fn() }
- ]);
-
- const tree = createTestInstance( );
- const { root } = tree;
- const { talonProps } = root.findByType('i').props;
-
- expect(talonProps.isOpen).toEqual(true);
- });
-});
-
test('returns correct shape', () => {
const closeDrawer = jest.fn().mockName('closeDrawer');
useAppContext.mockReturnValueOnce([{ drawer: null }, { closeDrawer }]);
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useProductForm.spec.js b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useProductForm.spec.js
index 1484531307..be4a08b06c 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useProductForm.spec.js
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/__tests__/useProductForm.spec.js
@@ -1,8 +1,6 @@
import React from 'react';
import { act } from 'react-test-renderer';
import { useMutation, useQuery } from '@apollo/client';
-
-import { useAppContext } from '../../../../../context/app';
import createTestInstance from '../../../../../util/createTestInstance';
import {
cartItem,
@@ -26,10 +24,7 @@ jest.mock('@apollo/client', () => ({
jest.mock('../../../../../context/app', () => {
const state = {};
- const api = {
- closeDrawer: jest.fn()
- };
- const useAppContext = jest.fn(() => [state, api]);
+ const useAppContext = jest.fn(() => [state]);
return { useAppContext };
});
@@ -52,7 +47,8 @@ const Component = props => {
const mockProps = {
cartItem,
setIsCartUpdating: jest.fn(),
- setVariantPrice: jest.fn()
+ setVariantPrice: jest.fn(),
+ setActiveEditItem: jest.fn()
};
test('returns correct shape with fetched options', () => {
@@ -132,9 +128,7 @@ describe('effect calls setIsCartUpdating', () => {
describe('form submission', () => {
const updateItemQuantity = jest.fn().mockResolvedValue();
const updateConfigurableOptions = jest.fn().mockResolvedValue();
- const closeDrawer = jest.fn();
const setupMockedReturns = () => {
- useAppContext.mockReturnValueOnce([{}, { closeDrawer }]);
useQuery.mockReturnValueOnce(configurableItemResponse);
useMutation
.mockReturnValueOnce([
@@ -154,7 +148,6 @@ describe('form submission', () => {
afterEach(() => {
updateItemQuantity.mockClear();
updateConfigurableOptions.mockClear();
- closeDrawer.mockClear();
});
test('does nothing if values do not change', () => {
@@ -169,7 +162,6 @@ describe('form submission', () => {
expect(updateItemQuantity).not.toHaveBeenCalled();
expect(updateConfigurableOptions).not.toHaveBeenCalled();
- expect(closeDrawer).toHaveBeenCalledTimes(1);
});
test('calls update quantity mutation when only quantity changes', async () => {
@@ -184,11 +176,11 @@ describe('form submission', () => {
expect(updateItemQuantity.mock.calls[0][0]).toMatchSnapshot();
expect(updateConfigurableOptions).not.toHaveBeenCalled();
- expect(closeDrawer).toHaveBeenCalledTimes(1);
});
test('calls configurable item mutation when options change', async () => {
// since this test renders twice, we need to double up the mocked returns
+
setupMockedReturns();
const tree = createTestInstance( );
const { root } = tree;
@@ -208,7 +200,6 @@ describe('form submission', () => {
expect(updateItemQuantity).not.toHaveBeenCalled();
expect(updateConfigurableOptions.mock.calls[0][0]).toMatchSnapshot();
- expect(closeDrawer).toHaveBeenCalledTimes(1);
});
test('does not call configurable item mutation when final options selection matches backend value', async () => {
@@ -232,15 +223,13 @@ describe('form submission', () => {
expect(updateItemQuantity).not.toHaveBeenCalled();
expect(updateConfigurableOptions).not.toHaveBeenCalled();
- expect(closeDrawer).toHaveBeenCalledTimes(1);
});
});
-test('does not close drawer on error', async () => {
+test('does not clear activeEditItem on error', async () => {
const updateItemQuantity = jest.fn().mockRejectedValue('Apollo Error');
- const closeDrawer = jest.fn();
+ const setActiveEditItem = jest.fn();
- useAppContext.mockReturnValueOnce([{}, { closeDrawer }]);
useMutation.mockReturnValueOnce([updateItemQuantity, {}]);
const tree = createTestInstance( );
@@ -253,5 +242,5 @@ test('does not close drawer on error', async () => {
});
expect(updateItemQuantity).toHaveBeenCalled();
- expect(closeDrawer).not.toHaveBeenCalled();
+ expect(setActiveEditItem).not.toHaveBeenCalledWith(null);
});
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useEditModal.js b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useEditModal.js
index c8be053c3d..b55442a20a 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useEditModal.js
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useEditModal.js
@@ -1,7 +1,5 @@
import { useState } from 'react';
-import { useAppContext } from '../../../../context/app';
-
/**
* This talon contains logic for a product edit modal used on a cart page.
* It returns prop data for rendering an interactive modal component.
@@ -14,14 +12,9 @@ import { useAppContext } from '../../../../context/app';
* import { useEditModal } from '@magento/peregrine/lib/talons/CartPage/ProductListing/EditModal/useEditModal';
*/
export const useEditModal = () => {
- const [{ drawer }, { closeDrawer }] = useAppContext();
- const isOpen = drawer === 'product.edit';
-
const [variantPrice, setVariantPrice] = useState(null);
return {
- handleClose: closeDrawer,
- isOpen,
setVariantPrice,
variantPrice
};
@@ -35,8 +28,6 @@ export const useEditModal = () => {
*
* @typedef {Object} EditModalTalonProps
*
- * @property {function} handleClose Callback function for handling the closing event of the modal.
- * @property {boolean} isOpen True if the modal is open. False otherwise.
* @property {function} setVariantPrice Function for setting a product's variant price.
* @property {Object} variantPrice The variant price for a product. See [Money object]{@link https://devdocs.magento.com/guides/v2.4/graphql/product/product-interface.html#Money}.
*/
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js
index cc8935d6ed..2524a57852 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js
@@ -1,7 +1,6 @@
import { useCallback, useState, useEffect, useMemo } from 'react';
import { useMutation, useQuery } from '@apollo/client';
-import { useAppContext } from '../../../../context/app';
import { useCartContext } from '../../../../context/cart';
import { findMatchingVariant } from '../../../../util/findMatchingProductVariant';
@@ -23,6 +22,7 @@ import { findMatchingVariant } from '../../../../util/findMatchingProductVariant
* @param {function} props.setVariantPrice Function for setting the variant price on a product.
* @param {GraphQLAST} props.updateConfigurableOptionsMutation GraphQL mutation for updating the configurable options for a product.
* @param {GraphQLAST} props.updateQuantityMutation GraphQL mutation for updating the quantity of a product in a cart.
+ * @param {function} props.setActiveEditItem Function for setting the actively editing item.
*
* @return {ProductFormTalonProps}
*
@@ -36,14 +36,17 @@ export const useProductForm = props => {
setIsCartUpdating,
setVariantPrice,
updateConfigurableOptionsMutation,
- updateQuantityMutation
+ updateQuantityMutation,
+ setActiveEditItem
} = props;
- const [, { closeDrawer }] = useAppContext();
const [{ cartId }] = useCartContext();
-
const [optionSelections, setOptionSelections] = useState(new Map());
+ const handleClose = useCallback(() => {
+ setActiveEditItem(null);
+ }, [setActiveEditItem]);
+
const [
updateItemQuantity,
{
@@ -71,8 +74,9 @@ export const useProductForm = props => {
}, [isSaving, setIsCartUpdating]);
const { data, error, loading } = useQuery(getConfigurableOptionsQuery, {
+ skip: !cartItem,
variables: {
- sku: cartItem.product.sku
+ sku: cartItem ? cartItem.product.sku : null
}
});
@@ -91,12 +95,14 @@ export const useProductForm = props => {
setOptionSelections(nextOptionSelections);
},
- [cartItem.configurable_options, optionSelections]
+ [cartItem, optionSelections]
);
- const configItem = !loading && !error ? data.products.items[0] : null;
+ const configItem =
+ !loading && !error && data ? data.products.items[0] : null;
const configurableOptionCodes = useMemo(() => {
const optionCodeMap = new Map();
+
if (configItem) {
configItem.configurable_options.forEach(option => {
optionCodeMap.set(option.attribute_id, option.attribute_code);
@@ -119,12 +125,7 @@ export const useProductForm = props => {
optionSelections
});
}
- }, [
- cartItem.configurable_options,
- configItem,
- configurableOptionCodes,
- optionSelections
- ]);
+ }, [cartItem, configItem, configurableOptionCodes, optionSelections]);
useEffect(() => {
let variantPrice = null;
@@ -152,6 +153,8 @@ export const useProductForm = props => {
quantity: formValues.quantity
}
});
+
+ setOptionSelections(new Map());
} else if (formValues.quantity !== cartItem.quantity) {
await updateItemQuantity({
variables: {
@@ -165,14 +168,12 @@ export const useProductForm = props => {
return;
}
- closeDrawer();
+ handleClose();
},
[
cartId,
- cartItem.id,
- cartItem.product.sku,
- cartItem.quantity,
- closeDrawer,
+ cartItem,
+ handleClose,
selectedVariant,
updateConfigurableOptions,
updateItemQuantity
@@ -194,7 +195,9 @@ export const useProductForm = props => {
handleOptionSelection,
handleSubmit,
isLoading: !!loading,
- isSaving
+ isSaving,
+ isDialogOpen: cartItem !== null,
+ handleClose
};
};
@@ -207,9 +210,11 @@ export const useProductForm = props => {
* @typedef {Object} ProductFormTalonProps
*
* @property {Object} configItem Cart item to configure
- * @property {Array} formErrors An array of form errors resulting from a configuration or quantity value
+ * @property {Array} errors An array of form errors resulting from a configuration or quantity value
* @property {function} handleOptionSelection A callback function handling an option selection event
* @property {function} handleSubmit A callback function for handling form submission
* @property {boolean} isLoading True if the form is loading data. False otherwise.
* @property {boolean} isSaving True if the form is saving data. False otherwise.
+ * @property {boolean} isDialogOpen True if the form is visible. False otherwise.
+ * @property {function} handleClose A callback function for handling form closing
*/
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/__tests__/useProduct.spec.js b/packages/peregrine/lib/talons/CartPage/ProductListing/__tests__/useProduct.spec.js
index f6442fadda..9cdbd87e64 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/__tests__/useProduct.spec.js
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/__tests__/useProduct.spec.js
@@ -210,12 +210,6 @@ test('it provides a way to toggle favorites', () => {
});
test('it handles editing the product', () => {
- const mockToggleDrawer = jest.fn();
- useAppContext.mockReturnValue([
- { drawer: null },
- { toggleDrawer: mockToggleDrawer }
- ]);
-
const setActiveEditItem = jest.fn();
const tree = createTestInstance(
@@ -230,9 +224,8 @@ test('it handles editing the product', () => {
handleEditItem();
});
- expect(mockToggleDrawer).toHaveBeenCalledWith('product.edit');
expect(setActiveEditItem).toHaveBeenCalled();
- expect(setActiveEditItem.mock.calls[1][0]).toMatchInlineSnapshot(`
+ expect(setActiveEditItem.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"id": "ItemID",
"prices": Object {
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/useProduct.js b/packages/peregrine/lib/talons/CartPage/ProductListing/useProduct.js
index 4e8ab272db..8e13b54d05 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/useProduct.js
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/useProduct.js
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation } from '@apollo/client';
-import { useAppContext } from '@magento/peregrine/lib/context/app';
import { useCartContext } from '@magento/peregrine/lib/context/cart';
import { deriveErrorMessage } from '../../../util/deriveErrorMessage';
@@ -11,7 +10,6 @@ import { deriveErrorMessage } from '../../../util/deriveErrorMessage';
* This talon performs the following effects:
*
* - Manage the updating state of the cart while a product is being updated or removed
- * - Reset the current item being edited item when the app drawer is closed
*
* @function
*
@@ -71,7 +69,6 @@ export const useProduct = props => {
]);
const [{ cartId }] = useCartContext();
- const [{ drawer }, { toggleDrawer }] = useAppContext();
const [isFavorite, setIsFavorite] = useState(false);
@@ -94,18 +91,11 @@ export const useProduct = props => {
const handleEditItem = useCallback(() => {
setActiveEditItem(item);
- toggleDrawer('product.edit');
// If there were errors from removing/updating the product, hide them
// when we open the modal.
setDisplayError(false);
- }, [item, setActiveEditItem, toggleDrawer]);
-
- useEffect(() => {
- if (drawer === null) {
- setActiveEditItem(null);
- }
- }, [drawer, setActiveEditItem]);
+ }, [item, setActiveEditItem]);
const handleRemoveFromCart = useCallback(() => {
try {
diff --git a/packages/peregrine/lib/talons/CartPage/ProductListing/useQuantity.js b/packages/peregrine/lib/talons/CartPage/ProductListing/useQuantity.js
index 83fda7519e..852a9ce177 100644
--- a/packages/peregrine/lib/talons/CartPage/ProductListing/useQuantity.js
+++ b/packages/peregrine/lib/talons/CartPage/ProductListing/useQuantity.js
@@ -75,7 +75,7 @@ export const useQuantity = props => {
try {
// For some storefronts decimal values are allowed.
const nextVal = parseFloat(value);
- if (isNaN(nextVal))
+ if (value && isNaN(nextVal))
throw new Error(`${value} is not a number.`);
if (nextVal < min) return min;
else return nextVal;
diff --git a/packages/peregrine/lib/talons/CartPage/__tests__/useCartPage.spec.js b/packages/peregrine/lib/talons/CartPage/__tests__/useCartPage.spec.js
index 0b9068ba0b..60faf519ac 100644
--- a/packages/peregrine/lib/talons/CartPage/__tests__/useCartPage.spec.js
+++ b/packages/peregrine/lib/talons/CartPage/__tests__/useCartPage.spec.js
@@ -3,7 +3,6 @@ import { createTestInstance } from '@magento/peregrine';
import { useQuery } from '@apollo/client';
import { useCartPage } from '../useCartPage';
-import { act } from 'react-test-renderer';
jest.mock('react', () => {
const React = jest.requireActual('react');
@@ -26,17 +25,6 @@ jest.mock('@apollo/client', () => {
return { useQuery };
});
-jest.mock('@magento/peregrine/lib/context/app', () => {
- const state = {};
- const api = {
- toggleDrawer: jest.fn(nav => {
- console.log(nav);
- })
- };
- const useAppContext = jest.fn(() => [state, api]);
-
- return { useAppContext };
-});
jest.mock('@magento/peregrine/lib/context/cart', () => {
const state = {
cartId: 'cart123'
@@ -46,15 +34,6 @@ jest.mock('@magento/peregrine/lib/context/cart', () => {
return { useCartContext };
});
-jest.mock('@magento/peregrine/lib/context/user', () => {
- const state = {
- isSignedIn: false
- };
- const api = {};
- const useUserContext = jest.fn(() => [state, api]);
-
- return { useUserContext };
-});
const log = jest.fn();
const Component = () => {
@@ -77,10 +56,8 @@ test('it returns the proper shape', () => {
// Assert.
expect(log).toHaveBeenCalledWith({
cartItems: expect.any(Array),
- handleSignIn: expect.any(Function),
hasItems: expect.any(Boolean),
isCartUpdating: expect.any(Boolean),
- isSignedIn: expect.any(Boolean),
setIsCartUpdating: expect.any(Function),
shouldShowLoadingIndicator: expect.any(Boolean)
});
@@ -129,19 +106,3 @@ test('it calls setIsCartUpdating false when loading is false', () => {
const { setIsCartUpdating } = log.mock.calls[0][0];
expect(setIsCartUpdating).toBeCalledWith(false);
});
-
-test('it toggles the drawer on sign in', () => {
- const consoleLogSpy = jest.spyOn(console, 'log');
-
- // Act.
- createTestInstance( );
-
- const { handleSignIn } = log.mock.calls[0][0];
-
- act(() => {
- handleSignIn();
- });
-
- expect(consoleLogSpy).toHaveBeenCalledTimes(1);
- expect(consoleLogSpy.mock.calls[0][0]).toEqual('nav');
-});
diff --git a/packages/peregrine/lib/talons/CartPage/useCartPage.js b/packages/peregrine/lib/talons/CartPage/useCartPage.js
index c91d8504ec..df4f71697b 100644
--- a/packages/peregrine/lib/talons/CartPage/useCartPage.js
+++ b/packages/peregrine/lib/talons/CartPage/useCartPage.js
@@ -1,8 +1,6 @@
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@apollo/client';
-import { useAppContext } from '@magento/peregrine/lib/context/app';
-import { useUserContext } from '@magento/peregrine/lib/context/user';
import { useCartContext } from '@magento/peregrine/lib/context/cart';
/**
@@ -28,8 +26,6 @@ export const useCartPage = props => {
queries: { getCartDetails }
} = props;
- const [, { toggleDrawer }] = useAppContext();
- const [{ isSignedIn }] = useUserContext();
const [{ cartId }] = useCartContext();
const [isCartUpdating, setIsCartUpdating] = useState(false);
@@ -41,11 +37,6 @@ export const useCartPage = props => {
variables: { cartId }
});
- const handleSignIn = useCallback(() => {
- // TODO: set navigation state to "SIGN_IN". useNavigation:showSignIn doesn't work.
- toggleDrawer('nav');
- }, [toggleDrawer]);
-
useEffect(() => {
// Let the cart page know it is updating while we're waiting on network data.
setIsCartUpdating(loading);
@@ -61,8 +52,6 @@ export const useCartPage = props => {
return {
cartItems,
hasItems,
- handleSignIn,
- isSignedIn,
isCartUpdating,
setIsCartUpdating,
shouldShowLoadingIndicator
@@ -89,8 +78,6 @@ export const useCartPage = props => {
*
* @property {Array} cartItems An array of item objects in the cart.
* @property {boolean} hasItems True if the cart has items. False otherwise.
- * @property {function} handleSignIn Callback function to call for handling a sign in event.
- * @property {boolean} isSignedIn True if the current user is signed in. False otherwise.
* @property {boolean} isCartUpdating True if the cart is updating. False otherwise.
* @property {function} setIsCartUpdating Callback function for setting the updating state of the cart page.
* @property {boolean} shouldShowLoadingIndicator True if the loading indicator should be rendered. False otherwise.
diff --git a/packages/peregrine/lib/talons/CategoryList/useCategoryList.js b/packages/peregrine/lib/talons/CategoryList/useCategoryList.js
index 675a4b90fb..378e1a5f49 100644
--- a/packages/peregrine/lib/talons/CategoryList/useCategoryList.js
+++ b/packages/peregrine/lib/talons/CategoryList/useCategoryList.js
@@ -27,7 +27,9 @@ export const useCategoryList = props => {
// Run the query immediately and every time id changes.
useEffect(() => {
- runQuery({ variables: { id } });
+ if (id) {
+ runQuery({ variables: { id } });
+ }
}, [id, runQuery]);
return {
diff --git a/packages/peregrine/lib/talons/CategoryTree/categoryTree.gql.js b/packages/peregrine/lib/talons/CategoryTree/categoryTree.gql.js
index 1df621492f..c278e61d78 100644
--- a/packages/peregrine/lib/talons/CategoryTree/categoryTree.gql.js
+++ b/packages/peregrine/lib/talons/CategoryTree/categoryTree.gql.js
@@ -14,6 +14,9 @@ export const GET_NAVIGATION_MENU = gql`
url_path
url_suffix
}
+ include_in_menu
+ url_path
+ url_suffix
}
}
`;
diff --git a/packages/peregrine/lib/talons/CategoryTree/useCategoryTree.js b/packages/peregrine/lib/talons/CategoryTree/useCategoryTree.js
index 9f0c32da9e..6b8a917532 100644
--- a/packages/peregrine/lib/talons/CategoryTree/useCategoryTree.js
+++ b/packages/peregrine/lib/talons/CategoryTree/useCategoryTree.js
@@ -51,6 +51,7 @@ export const useCategoryTree = props => {
}, [data, updateCategories]);
const rootCategory = data && data.category;
+
const { children = [] } = rootCategory || {};
const childCategories = useMemo(() => {
diff --git a/packages/peregrine/lib/talons/CheckoutPage/GuestSignIn/__tests__/useGuestSignIn.spec.js b/packages/peregrine/lib/talons/CheckoutPage/GuestSignIn/__tests__/useGuestSignIn.spec.js
new file mode 100644
index 0000000000..91d9b0ac59
--- /dev/null
+++ b/packages/peregrine/lib/talons/CheckoutPage/GuestSignIn/__tests__/useGuestSignIn.spec.js
@@ -0,0 +1,93 @@
+import React, { useEffect } from 'react';
+import { act } from 'react-test-renderer';
+import { createTestInstance } from '@magento/peregrine';
+
+import { useGuestSignIn } from '../useGuestSignIn';
+
+const mockToggleActiveContent = jest.fn();
+const mockProps = {
+ toggleActiveContent: mockToggleActiveContent
+};
+
+const log = jest.fn();
+const Component = props => {
+ const talonProps = useGuestSignIn(props);
+
+ useEffect(() => {
+ log(talonProps);
+ }, [talonProps]);
+
+ return null;
+};
+
+test('returns the correct shape', () => {
+ createTestInstance( );
+
+ expect(log).toHaveBeenCalledWith({
+ handleBackToCheckout: expect.any(Function),
+ toggleCreateAccountView: expect.any(Function),
+ toggleForgotPasswordView: expect.any(Function),
+ view: expect.any(String)
+ });
+});
+
+test('toggles forgot password view', () => {
+ createTestInstance( );
+
+ const initialTalonProps = log.mock.calls[0][0];
+ act(() => {
+ initialTalonProps.toggleForgotPasswordView();
+ });
+
+ const step1TalonProps = log.mock.calls[1][0];
+ act(() => {
+ step1TalonProps.toggleForgotPasswordView();
+ });
+
+ const finalTalonProps = log.mock.calls[2][0];
+
+ expect(initialTalonProps.view).toBe('SIGNIN');
+ expect(step1TalonProps.view).toBe('FORGOT_PASSWORD');
+ expect(finalTalonProps.view).toBe('SIGNIN');
+});
+
+test('toggles create account view', () => {
+ createTestInstance( );
+
+ const initialTalonProps = log.mock.calls[0][0];
+ act(() => {
+ initialTalonProps.toggleCreateAccountView();
+ });
+
+ const step1TalonProps = log.mock.calls[1][0];
+ act(() => {
+ step1TalonProps.toggleCreateAccountView();
+ });
+
+ const finalTalonProps = log.mock.calls[2][0];
+
+ expect(initialTalonProps.view).toBe('SIGNIN');
+ expect(step1TalonProps.view).toBe('CREATE_ACCOUNT');
+ expect(finalTalonProps.view).toBe('SIGNIN');
+});
+
+test('handles back to checkout', () => {
+ createTestInstance( );
+
+ const initialTalonProps = log.mock.calls[0][0];
+ act(() => {
+ initialTalonProps.toggleCreateAccountView();
+ });
+
+ const step1TalonProps = log.mock.calls[1][0];
+ act(() => {
+ step1TalonProps.handleBackToCheckout();
+ });
+
+ const finalTalonProps = log.mock.calls[2][0];
+
+ expect(initialTalonProps.view).toBe('SIGNIN');
+ expect(step1TalonProps.view).toBe('CREATE_ACCOUNT');
+ expect(finalTalonProps.view).toBe('SIGNIN');
+ expect(mockToggleActiveContent).toHaveBeenCalledTimes(1);
+});
diff --git a/packages/peregrine/lib/talons/CheckoutPage/GuestSignIn/useGuestSignIn.js b/packages/peregrine/lib/talons/CheckoutPage/GuestSignIn/useGuestSignIn.js
new file mode 100644
index 0000000000..135aa819ef
--- /dev/null
+++ b/packages/peregrine/lib/talons/CheckoutPage/GuestSignIn/useGuestSignIn.js
@@ -0,0 +1,30 @@
+import { useCallback, useState } from 'react';
+
+export const useGuestSignIn = props => {
+ const { toggleActiveContent } = props;
+ const [view, setView] = useState('SIGNIN');
+
+ const toggleForgotPasswordView = useCallback(() => {
+ setView(currentView =>
+ currentView === 'SIGNIN' ? 'FORGOT_PASSWORD' : 'SIGNIN'
+ );
+ }, []);
+
+ const toggleCreateAccountView = useCallback(() => {
+ setView(currentView =>
+ currentView === 'SIGNIN' ? 'CREATE_ACCOUNT' : 'SIGNIN'
+ );
+ }, []);
+
+ const handleBackToCheckout = useCallback(() => {
+ toggleActiveContent();
+ setView('SIGNIN');
+ }, [toggleActiveContent]);
+
+ return {
+ handleBackToCheckout,
+ toggleCreateAccountView,
+ toggleForgotPasswordView,
+ view
+ };
+};
diff --git a/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap b/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap
index c194b82e31..bf8490e9cc 100644
--- a/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap
+++ b/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap
@@ -12,7 +12,6 @@ Object {
"error": undefined,
"handlePlaceOrder": [Function],
"handleReviewOrder": [Function],
- "handleSignIn": [Function],
"hasError": false,
"isCartEmpty": true,
"isGuestCheckout": true,
@@ -29,6 +28,7 @@ Object {
"setPaymentInformationDone": [Function],
"setShippingInformationDone": [Function],
"setShippingMethodDone": [Function],
- "toggleActiveContent": [Function],
+ "toggleAddressBookContent": [Function],
+ "toggleSignInContent": [Function],
}
`;
diff --git a/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js b/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js
index 3e3116d801..b47b8a8506 100644
--- a/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js
+++ b/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js
@@ -31,10 +31,6 @@ jest.mock('../../../context/user', () => ({
useUserContext: jest.fn().mockReturnValue([{ isSignedIn: false }])
}));
-jest.mock('../../../context/app', () => ({
- useAppContext: jest.fn().mockReturnValue([{}, { toggleDrawer: jest.fn() }])
-}));
-
jest.mock('../../../context/cart', () => ({
useCartContext: jest
.fn()
@@ -590,3 +586,45 @@ test('resetReviewOrderButtonClicked should set reviewOrderButtonClicked to false
expect(step2Props.reviewOrderButtonClicked).toBeFalsy();
});
+
+test('toggles addressBook content', () => {
+ const { talonProps: initialProps, update } = getTalonProps(props);
+
+ initialProps.toggleAddressBookContent();
+ const step1Props = update();
+
+ expect(step1Props.activeContent).toBe('addressBook');
+
+ step1Props.toggleAddressBookContent();
+ const step2Props = update();
+
+ expect(step2Props.activeContent).toBe('checkout');
+});
+
+test('toggles signIn content', () => {
+ const { talonProps: initialProps, update } = getTalonProps(props);
+
+ initialProps.toggleSignInContent();
+ const step1Props = update();
+
+ expect(step1Props.activeContent).toBe('signIn');
+
+ step1Props.toggleSignInContent();
+ const step2Props = update();
+
+ expect(step2Props.activeContent).toBe('checkout');
+});
+
+test('resets active content to checkout on sign in', () => {
+ const { talonProps: initialProps, update } = getTalonProps(props);
+
+ initialProps.toggleSignInContent();
+ const step1Props = update();
+
+ expect(step1Props.activeContent).toBe('signIn');
+
+ useUserContext.mockReturnValueOnce([{ isSignedIn: true }]);
+ const step2Props = update();
+
+ expect(step2Props.activeContent).toBe('checkout');
+});
diff --git a/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js b/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js
index a54c262305..38222e5d8b 100644
--- a/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js
+++ b/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js
@@ -7,7 +7,6 @@ import {
} from '@apollo/client';
import { clearCartDataFromCache } from '../../Apollo/clearCartDataFromCache';
-import { useAppContext } from '../../context/app';
import { useUserContext } from '../../context/user';
import { useCartContext } from '../../context/cart';
import CheckoutError from './CheckoutError';
@@ -39,7 +38,6 @@ export const useCheckoutPage = props => {
const [checkoutStep, setCheckoutStep] = useState(
CHECKOUT_STEP.SHIPPING_ADDRESS
);
- const [, { toggleDrawer }] = useAppContext();
const [{ isSignedIn }] = useUserContext();
const [{ cartId }, { createCart, removeCart }] = useCartContext();
@@ -103,11 +101,16 @@ export const useCheckoutPage = props => {
const customer = customerData && customerData.customer;
- const toggleActiveContent = useCallback(() => {
- const nextContentState =
- activeContent === 'checkout' ? 'addressBook' : 'checkout';
- setActiveContent(nextContentState);
- }, [activeContent]);
+ const toggleAddressBookContent = useCallback(() => {
+ setActiveContent(currentlyActive =>
+ currentlyActive === 'checkout' ? 'addressBook' : 'checkout'
+ );
+ }, []);
+ const toggleSignInContent = useCallback(() => {
+ setActiveContent(currentlyActive =>
+ currentlyActive === 'checkout' ? 'signIn' : 'checkout'
+ );
+ }, []);
const checkoutError = useMemo(() => {
if (placeOrderError) {
@@ -115,11 +118,6 @@ export const useCheckoutPage = props => {
}
}, [placeOrderError]);
- const handleSignIn = useCallback(() => {
- // TODO: set navigation state to "SIGN_IN". useNavigation:showSignIn doesn't work.
- toggleDrawer('nav');
- }, [toggleDrawer]);
-
const handleReviewOrder = useCallback(() => {
setReviewOrderButtonClicked(true);
}, []);
@@ -163,6 +161,13 @@ export const useCheckoutPage = props => {
});
}, [cartId, getOrderDetails]);
+ // Go back to checkout if shopper logs in
+ useEffect(() => {
+ if (isSignedIn) {
+ setActiveContent('checkout');
+ }
+ }, [isSignedIn]);
+
useEffect(() => {
async function placeOrderAndCleanup() {
try {
@@ -209,7 +214,6 @@ export const useCheckoutPage = props => {
checkoutStep,
customer,
error: checkoutError,
- handleSignIn,
handlePlaceOrder,
hasError: !!checkoutError,
isCartEmpty: !(checkoutData && checkoutData.cart.total_quantity),
@@ -230,6 +234,7 @@ export const useCheckoutPage = props => {
resetReviewOrderButtonClicked,
handleReviewOrder,
reviewOrderButtonClicked,
- toggleActiveContent
+ toggleAddressBookContent,
+ toggleSignInContent
};
};
diff --git a/packages/peregrine/lib/talons/Cms/cmsPage.gql.js b/packages/peregrine/lib/talons/Cms/cmsPage.gql.js
index 0b25838f8f..cc5de33682 100644
--- a/packages/peregrine/lib/talons/Cms/cmsPage.gql.js
+++ b/packages/peregrine/lib/talons/Cms/cmsPage.gql.js
@@ -12,6 +12,10 @@ export const GET_CMS_PAGE = gql`
meta_keywords
meta_description
}
+ storeConfig {
+ id
+ root_category_id
+ }
}
`;
diff --git a/packages/peregrine/lib/talons/Cms/useCmsPage.js b/packages/peregrine/lib/talons/Cms/useCmsPage.js
index 92c81ebe77..0e14649d7b 100644
--- a/packages/peregrine/lib/talons/Cms/useCmsPage.js
+++ b/packages/peregrine/lib/talons/Cms/useCmsPage.js
@@ -51,6 +51,7 @@ export const useCmsPage = props => {
const shouldShowLoadingIndicator = !data;
const cmsPage = data ? data.cmsPage : null;
+ const rootCategoryId = data ? data.storeConfig.root_category_id : null;
// TODO: we shouldn't be validating strings to determine if the page has content or not
const hasContent = useMemo(() => {
@@ -64,8 +65,9 @@ export const useCmsPage = props => {
return {
cmsPage,
- hasContent,
error,
+ hasContent,
+ rootCategoryId,
shouldShowLoadingIndicator
};
};
diff --git a/packages/peregrine/lib/talons/MagentoRoute/__tests__/useMagentoRoute.spec.js b/packages/peregrine/lib/talons/MagentoRoute/__tests__/useMagentoRoute.spec.js
index b6acd6229c..e2564c5e81 100644
--- a/packages/peregrine/lib/talons/MagentoRoute/__tests__/useMagentoRoute.spec.js
+++ b/packages/peregrine/lib/talons/MagentoRoute/__tests__/useMagentoRoute.spec.js
@@ -1,202 +1,294 @@
-import React, { useEffect, useState } from 'react';
-import { createTestInstance } from '@magento/peregrine';
+import React, { useState } from 'react';
+import { replace } from 'react-router-dom';
+import { act, create } from 'react-test-renderer';
+import { useQuery } from '@apollo/client';
+import { useRootComponents } from '@magento/peregrine/lib/context/rootComponents';
-import getRouteComponent from '../getRouteComponent';
+import { getRootComponent } from '../helpers';
import { useMagentoRoute } from '../useMagentoRoute';
-import { act } from 'react-test-renderer';
-import { useQuery } from '@apollo/client';
-/*
- * Mocks.
- */
-jest.mock('react', () => {
- const React = jest.requireActual('react');
- const spy = jest.spyOn(React, 'useState');
+jest.mock('@apollo/client', () => {
+ const ApolloClient = jest.requireActual('@apollo/client');
+ const useQuery = jest.fn();
return {
- ...React,
- useState: spy
+ ...ApolloClient,
+ useQuery
};
});
-const mockHistoryReplace = jest.fn();
jest.mock('react-router-dom', () => {
- const ReactRouterDOM = jest.requireActual('react-router-dom');
+ const ReactRouter = jest.requireActual('react-router-dom');
+ const replace = jest.fn();
+ const useHistory = jest.fn(() => ({ replace }));
+ const useLocation = jest.fn(() => ({ pathname: '/foo.html' }));
return {
- ...ReactRouterDOM,
- useHistory: () => ({ replace: mockHistoryReplace }),
- useLocation: jest.fn(() => ({ pathname: 'Unit Test Pathname' }))
+ ...ReactRouter,
+ useHistory,
+ useLocation,
+ replace
};
});
-jest.mock('@apollo/client', () => ({
- useApolloClient: jest.fn(() => ({ apiBase: 'Unit Test API Base' })),
- useQuery: jest
- .fn()
- .mockReturnValue({ data: { storeConfig: { code: 'default' } } })
+
+jest.mock('@magento/peregrine/lib/context/rootComponents', () => ({
+ useRootComponents: jest.fn()
}));
-jest.mock('../getRouteComponent', () => jest.fn());
+useRootComponents.mockImplementation(() => useState(new Map()));
-/*
- * Members.
- */
-const log = jest.fn();
-const Component = props => {
- const hookProps = useMagentoRoute({ ...props });
+jest.mock('../helpers', () => {
+ const helpers = jest.requireActual('../helpers');
+ const getRootComponent = jest.fn();
+
+ return {
+ ...helpers,
+ getRootComponent
+ };
+});
- useEffect(() => {
- log(hookProps);
- }, [hookProps]);
+const resolve = jest.fn().mockName('resolve');
+const reject = jest.fn().mockName('reject');
+getRootComponent.mockImplementation(
+ () =>
+ new Promise((res, rej) => {
+ resolve.mockImplementation(res);
+ reject.mockImplementation(rej);
+ })
+);
+const log = jest.fn().mockName('log');
+const Component = () => {
+ log(useMagentoRoute());
return null;
};
-const routeComponentResults = {
- COMPONENT_FOUND: {
- component: {},
- id: 1,
- type: 'PRODUCT',
- store: 'default'
- },
- COMPONENT_NOT_FOUND: {
- isNotFound: true
- },
- ERROR: {
- hasError: true,
- routeError: 'INTERNAL_ERROR'
- },
- REDIRECT: {
- isRedirect: true,
- relativeUrl: '/some/redirect'
- }
-};
-
-const props = {};
-
-/*
- * Tests.
- */
beforeEach(() => {
- getRouteComponent.mockReset();
+ useQuery.mockReset();
+ useQuery.mockImplementation(() => {
+ return {
+ data: {
+ urlResolver: {
+ id: 1,
+ redirectCode: 0,
+ relative_url: '/foo.html',
+ type: 'CATEGORY'
+ }
+ },
+ loading: false
+ };
+ });
});
-it('fetches a component when it doesnt exist in local state', () => {
- // Arrange.
- useState.mockReturnValueOnce([new Map(), jest.fn()]);
- getRouteComponent.mockImplementationOnce(() => {
- return Promise.resolve(routeComponentResults.COMPONENT_FOUND);
- });
+describe('returns LOADING while queries are pending', () => {
+ test('urlResolver is loading', async () => {
+ useQuery.mockImplementation(() => {
+ return { loading: true };
+ });
+
+ await act(() => {
+ create( );
+ });
- // Act.
- act(() => {
- createTestInstance( );
+ expect(replace).toHaveBeenCalledTimes(0);
+ expect(log).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ isLoading: true
+ });
});
- // Assert.
- expect(getRouteComponent).toHaveBeenCalled();
-});
+ test('getRootComponent is pending', async () => {
+ await act(() => {
+ create( );
+ });
-it('does not fetch when a match exists in local state', () => {
- // Arrange.
- const componentMap = new Map().set(
- 'Unit Test Pathname',
- routeComponentResults.COMPONENT_FOUND
- );
- useState.mockReturnValueOnce([componentMap, jest.fn()]);
-
- // Act.
- act(() => {
- createTestInstance( );
+ expect(replace).toHaveBeenCalledTimes(0);
+ expect(log).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ isLoading: true
+ });
});
-
- // Assert.
- expect(getRouteComponent).not.toHaveBeenCalled();
});
-it('refetches when the stores do not match', () => {
- // Arrange.
- useQuery.mockReturnValueOnce({ data: { storeConfig: { code: 'other' } } });
- const componentMap = new Map().set(
- 'Unit Test Pathname',
- routeComponentResults.COMPONENT_FOUND
- );
- useState.mockReturnValueOnce([componentMap, jest.fn()]);
- getRouteComponent.mockImplementationOnce(() => {
- return Promise.resolve(routeComponentResults.COMPONENT_FOUND);
- });
+describe('returns ERROR when queries fail', () => {
+ test('urlResolver fails', async () => {
+ useQuery.mockImplementation(() => {
+ return { error: new Error() };
+ });
+
+ await act(() => {
+ create( );
+ });
- // Act.
- act(() => {
- createTestInstance( );
+ expect(replace).toHaveBeenCalledTimes(0);
+ expect(log).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ hasError: true,
+ routeError: expect.any(Error)
+ });
});
- // Assert.
- expect(getRouteComponent).toHaveBeenCalled();
-});
+ test('getRootComponent fails', async () => {
+ const routeError = new Error();
-it('redirects when instructed', () => {
- // Arrange.
- const componentMap = new Map().set(
- 'Unit Test Pathname',
- routeComponentResults.REDIRECT
- );
- useState.mockReturnValueOnce([componentMap, jest.fn()]);
-
- // Act.
- act(() => {
- createTestInstance( );
- });
+ await act(() => {
+ create( );
+ });
+
+ await act(() => {
+ reject(routeError);
+ });
- // Assert.
- expect(mockHistoryReplace).toHaveBeenCalledWith(
- routeComponentResults.REDIRECT.relativeUrl
- );
+ expect(replace).toHaveBeenCalledTimes(0);
+ expect(log).toHaveBeenCalledTimes(2);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ isLoading: true
+ });
+ expect(log).toHaveBeenNthCalledWith(2, {
+ hasError: true,
+ routeError
+ });
+ });
});
-it('refetches data when component is not found and user is online', () => {
- // Arrange.
- const componentMap = new Map().set(
- 'Unit Test Pathname',
- routeComponentResults.COMPONENT_NOT_FOUND
- );
- useState.mockReturnValueOnce([componentMap, jest.fn()]);
- getRouteComponent.mockImplementationOnce(() => {
- return Promise.resolve(routeComponentResults.COMPONENT_FOUND);
+describe('returns NOT_FOUND when queries come back empty', () => {
+ test('urlResolver is null', async () => {
+ useQuery.mockImplementation(() => {
+ return {
+ data: {
+ urlResolver: null
+ },
+ loading: false
+ };
+ });
+
+ await act(() => {
+ create( );
+ });
+
+ expect(replace).toHaveBeenCalledTimes(0);
+ expect(log).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ isNotFound: true
+ });
});
+});
+
+describe('returns REDIRECT after receiving a redirect code', () => {
+ test('redirect code 301', async () => {
+ useQuery.mockImplementation(() => {
+ return {
+ data: {
+ urlResolver: {
+ id: 1,
+ redirectCode: 301,
+ relative_url: '/foo.html',
+ type: 'CATEGORY'
+ }
+ },
+ loading: false
+ };
+ });
- // Mock being online.
- const onLineGetter = jest.spyOn(global.navigator, 'onLine', 'get');
- onLineGetter.mockReturnValue(true);
+ await act(() => {
+ create( );
+ });
- // Act.
- act(() => {
- createTestInstance( );
+ expect(replace).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ isRedirect: true,
+ relativeUrl: '/foo.html'
+ });
});
- // Assert.
- expect(getRouteComponent).toHaveBeenCalled();
+ test('redirect code 302', async () => {
+ useQuery.mockImplementation(() => {
+ return {
+ data: {
+ urlResolver: {
+ id: 1,
+ redirectCode: 302,
+ relative_url: '/foo.html',
+ type: 'CATEGORY'
+ }
+ },
+ loading: false
+ };
+ });
+
+ await act(() => {
+ create( );
+ });
+
+ expect(replace).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenCalledTimes(1);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ isRedirect: true,
+ relativeUrl: '/foo.html'
+ });
+ });
});
-it('does not refetch data when component is not found but user is offline', () => {
- // Arrange.
- const componentMap = new Map().set(
- 'Unit Test Pathname',
- routeComponentResults.COMPONENT_NOT_FOUND
- );
- useState.mockReturnValueOnce([componentMap, jest.fn()]);
- getRouteComponent.mockImplementationOnce(() => {
- return Promise.resolve(routeComponentResults.COMPONENT_FOUND);
+describe('returns FOUND after fetching a component', () => {
+ test('getRootComponent succeeds', async () => {
+ await act(() => {
+ create( );
+ });
+
+ await act(() => {
+ resolve('MockComponent');
+ });
+
+ expect(replace).toHaveBeenCalledTimes(0);
+ expect(log).toHaveBeenCalledTimes(2);
+ expect(log).toHaveBeenNthCalledWith(1, {
+ isLoading: true
+ });
+ expect(log).toHaveBeenNthCalledWith(2, {
+ component: 'MockComponent',
+ id: 1,
+ type: 'CATEGORY'
+ });
});
+});
+
+describe('avoids fetching the same component twice', () => {
+ test('getRootComponent succeeds', async () => {
+ let tree;
+
+ await act(() => {
+ tree = create( );
+ });
- // Mock being offline.
- const onLineGetter = jest.spyOn(global.navigator, 'onLine', 'get');
- onLineGetter.mockReturnValue(false);
+ await act(() => {
+ resolve('MockComponent');
+ });
- // Act.
- act(() => {
- createTestInstance( );
+ await act(() => {
+ tree.update( );
+ });
+
+ expect(getRootComponent).toHaveBeenCalledTimes(1);
+ expect(getRootComponent).toHaveBeenNthCalledWith(1, 'CATEGORY');
});
+});
- // Assert.
- expect(getRouteComponent).not.toHaveBeenCalled();
+describe('avoids setting state when unmounted', () => {
+ test('getRootComponent resolves after unmount', async () => {
+ let tree;
+
+ await act(() => {
+ tree = create( );
+ });
+
+ await act(() => {
+ tree.unmount();
+ });
+
+ await act(() => {
+ resolve('MockComponent');
+ });
+
+ expect(tree).toBeTruthy();
+ });
});
diff --git a/packages/peregrine/lib/talons/MagentoRoute/getRouteComponent.js b/packages/peregrine/lib/talons/MagentoRoute/getRouteComponent.js
deleted file mode 100644
index 5cfcde473c..0000000000
--- a/packages/peregrine/lib/talons/MagentoRoute/getRouteComponent.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import resolveUnknownRoute from '../../Router/resolveUnknownRoute';
-
-export const INTERNAL_ERROR = 'INTERNAL_ERROR';
-export const NOT_FOUND = 'NOT_FOUND';
-
-/**
- * Get the route component for a specific path
- *
- * @param apiBase
- * @param pathname
- * @param store
- * @returns {Promise<{component: *, id: *, type: *, redirectCode: *, relativeUrl: *, pathname: *}|{routeError: *, pathname: *}>}
- */
-const getRouteComponent = async (apiBase, pathname, store) => {
- // At build time, `fetchRootComponent` is injected as a global.
- // Depending on the environment, this global will be either an
- // ES module with a `default` property, or a plain CJS module.
- const fetchRoot =
- 'default' in fetchRootComponent
- ? fetchRootComponent.default
- : fetchRootComponent;
-
- try {
- // try to resolve the route
- // if this throws, we essentially have a 500 Internal Error
- const resolvedRoute = await resolveUnknownRoute({
- apiBase,
- route: pathname,
- store: store
- });
-
- // urlResolver query returns null if a route can't be found
- if (!resolvedRoute) {
- throw new Error('404');
- }
-
- const { type, id, redirectCode, relative_url } = resolvedRoute;
- // if resolution and destructuring succeed but return no match
- // then we have a straightforward 404 Not Found
- if (!type || !id) {
- throw new Error('404');
- }
-
- // at this point we should have a matching RootComponent
- // if this throws, we essentially have a 500 Internal Error
- const component = await fetchRoot(type);
-
- // associate the matching RootComponent with this location
- return {
- component,
- id,
- pathname,
- type,
- redirectCode,
- relativeUrl: relative_url
- };
- } catch (e) {
- const routeError = e.message === '404' ? NOT_FOUND : INTERNAL_ERROR;
-
- console.error(e);
-
- // we don't have a matching RootComponent, but we've checked for one
- // so associate the appropriate error case with this location
- return { pathname, routeError };
- }
-};
-
-export default getRouteComponent;
diff --git a/packages/peregrine/lib/talons/MagentoRoute/helpers.js b/packages/peregrine/lib/talons/MagentoRoute/helpers.js
new file mode 100644
index 0000000000..9935bbe06d
--- /dev/null
+++ b/packages/peregrine/lib/talons/MagentoRoute/helpers.js
@@ -0,0 +1,10 @@
+// 301 is permanent; 302 is temporary.
+const REDIRECT_CODES = new Set().add(301).add(302);
+export const isRedirect = code => REDIRECT_CODES.has(code);
+
+// Webpack injects `fetchRootComponent` as a global during the build.
+// Depending on the environment, it may be a CommonJS or ES module.
+const warning = () => new Error('fetchRootComponent is not defined');
+const { fetchRootComponent = warning } = window || {};
+export const getRootComponent =
+ fetchRootComponent.default || fetchRootComponent;
diff --git a/packages/peregrine/lib/talons/MagentoRoute/index.js b/packages/peregrine/lib/talons/MagentoRoute/index.js
index a6ce9eb29d..3415e3c540 100644
--- a/packages/peregrine/lib/talons/MagentoRoute/index.js
+++ b/packages/peregrine/lib/talons/MagentoRoute/index.js
@@ -1,2 +1 @@
-export { INTERNAL_ERROR, NOT_FOUND } from './getRouteComponent';
export { useMagentoRoute } from './useMagentoRoute';
diff --git a/packages/peregrine/lib/talons/MagentoRoute/magentoRoute.gql.js b/packages/peregrine/lib/talons/MagentoRoute/magentoRoute.gql.js
new file mode 100644
index 0000000000..952436fc3f
--- /dev/null
+++ b/packages/peregrine/lib/talons/MagentoRoute/magentoRoute.gql.js
@@ -0,0 +1,16 @@
+import { gql } from '@apollo/client';
+
+export const RESOLVE_URL = gql`
+ query ResolveURL($url: String!) {
+ urlResolver(url: $url) {
+ id
+ relative_url
+ redirectCode
+ type
+ }
+ }
+`;
+
+export default {
+ resolveUrlQuery: RESOLVE_URL
+};
diff --git a/packages/peregrine/lib/talons/MagentoRoute/useMagentoRoute.js b/packages/peregrine/lib/talons/MagentoRoute/useMagentoRoute.js
index 4703496d69..673e465919 100644
--- a/packages/peregrine/lib/talons/MagentoRoute/useMagentoRoute.js
+++ b/packages/peregrine/lib/talons/MagentoRoute/useMagentoRoute.js
@@ -1,103 +1,86 @@
-import { useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
-import { useApolloClient, useQuery } from '@apollo/client';
+import { useQuery } from '@apollo/client';
+import { useRootComponents } from '@magento/peregrine/lib/context/rootComponents';
+import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
-import getRouteComponent from './getRouteComponent';
+import { getRootComponent, isRedirect } from './helpers';
+import DEFAULT_OPERATIONS from './magentoRoute.gql';
-const CODE_PERMANENT_REDIRECT = 301;
-const CODE_TEMPORARY_REDIRECT = 302;
-const REDIRECT_CODES = [CODE_PERMANENT_REDIRECT, CODE_TEMPORARY_REDIRECT];
-
-const talonResponses = {
- ERROR: routeError => ({ hasError: true, routeError }),
- LOADING: { isLoading: true },
- NOT_FOUND: { isNotFound: true },
- FOUND: (component, id, type, store) => ({ component, id, type, store }),
- REDIRECT: relativeUrl => ({ isRedirect: true, relativeUrl })
-};
-
-const shouldFetch = (data, store) => {
- // Should fetch if we don't have any data.
- if (!data) return true;
-
- // Should fetch again following a prior failure.
- if (data.isNotFound && navigator.onLine) {
- return true;
- }
-
- // If we have data for the route, but the stores don't match fetch the correct route
- return !!(store && data.id && data.store !== store);
-};
-
-export const useMagentoRoute = props => {
- const { getStoreCode } = props;
- const [componentMap, setComponentMap] = useState(new Map());
- const { apiBase } = useApolloClient();
- const history = useHistory();
+export const useMagentoRoute = (props = {}) => {
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
+ const { resolveUrlQuery } = operations;
+ const { replace } = useHistory();
const { pathname } = useLocation();
- const isMountedRef = useRef(false);
- const routeData = componentMap.get(pathname);
+ const [componentMap, setComponentMap] = useRootComponents();
- const { data } = useQuery(getStoreCode, {
+ const setComponent = useCallback(
+ (key, value) => {
+ setComponentMap(prevMap => new Map(prevMap).set(key, value));
+ },
+ [setComponentMap]
+ );
+
+ const queryResult = useQuery(resolveUrlQuery, {
fetchPolicy: 'cache-and-network',
- nextFetchPolicy: 'cache-first'
+ nextFetchPolicy: 'cache-first',
+ variables: { url: pathname }
});
- const store = data && data.storeConfig.code;
+ // destructure the query result
+ const { data, error, loading } = queryResult;
+ const { urlResolver } = data || {};
+ const { id, redirectCode, relative_url, type } = urlResolver || {};
+
+ // evaluate both results and determine the response type
+ const component = componentMap.get(pathname);
+ const empty = !urlResolver || !type || id < 1;
+ const redirect = isRedirect(redirectCode);
+ const fetchError = component instanceof Error && component;
+ const routeError = fetchError || error;
+ let routeData;
+
+ if (component && !fetchError) {
+ // FOUND
+ routeData = component;
+ } else if (routeError) {
+ // ERROR
+ routeData = { hasError: true, routeError };
+ } else if (redirect) {
+ // REDIRECT
+ routeData = { isRedirect: true, relativeUrl: relative_url };
+ } else if (empty && !loading) {
+ // NOT FOUND
+ routeData = { isNotFound: true };
+ } else {
+ // LOADING
+ routeData = { isLoading: true };
+ }
- // Keep track of whether we have been mounted yet.
- // Note that we are not unmounted on page transitions.
+ // fetch a component if necessary
useEffect(() => {
- isMountedRef.current = true;
-
- return () => {
- isMountedRef.current = false;
- };
- }, []);
-
- // If the entry for this pathname is a redirect, perform the redirect.
+ (async () => {
+ // don't fetch if we don't have data yet
+ if (loading || empty) return;
+
+ // don't fetch more than once
+ if (component) return;
+
+ try {
+ const component = await getRootComponent(type);
+ setComponent(pathname, { component, id, type });
+ } catch (error) {
+ setComponent(pathname, error);
+ }
+ })();
+ }, [component, empty, id, loading, pathname, setComponent, type]);
+
+ // perform a redirect if necesssary
useEffect(() => {
if (routeData && routeData.isRedirect) {
- history.replace(routeData.relativeUrl);
- }
- }, [componentMap, history, pathname, routeData]);
-
- // ask Magento for a RootComponent that matches the current pathname
- useEffect(() => {
- // Avoid setting state if unmounted.
- if (!isMountedRef.current) {
- return;
- }
-
- if (shouldFetch(routeData, store)) {
- getRouteComponent(apiBase, pathname, store).then(
- ({
- component,
- id,
- pathname,
- redirectCode,
- relativeUrl,
- routeError,
- type
- }) => {
- // Update our Map in local state for this path.
- setComponentMap(prevMap => {
- const nextMap = new Map(prevMap);
-
- const nextValue = routeError
- ? talonResponses.ERROR(routeError)
- : id === -1
- ? talonResponses.NOT_FOUND
- : REDIRECT_CODES.includes(redirectCode)
- ? talonResponses.REDIRECT(relativeUrl)
- : talonResponses.FOUND(component, id, type, store);
-
- return nextMap.set(pathname, nextValue);
- });
- }
- );
+ replace(routeData.relativeUrl);
}
- }, [apiBase, componentMap, history, pathname, routeData, store]);
+ }, [pathname, replace, routeData]);
- return routeData || talonResponses.LOADING;
+ return routeData;
};
diff --git a/packages/peregrine/lib/talons/Navigation/__tests__/useNavigation.spec.js b/packages/peregrine/lib/talons/Navigation/__tests__/useNavigation.spec.js
index e88c965980..1aee87837c 100644
--- a/packages/peregrine/lib/talons/Navigation/__tests__/useNavigation.spec.js
+++ b/packages/peregrine/lib/talons/Navigation/__tests__/useNavigation.spec.js
@@ -29,8 +29,7 @@ jest.mock('@magento/peregrine/lib/context/catalog', () => {
categories: {
1: { parentId: 0 },
2: { parentId: 1 }
- },
- rootCategoryId: 1
+ }
},
{
actions: { updateCategories }
@@ -55,6 +54,16 @@ jest.mock('@magento/peregrine/lib/hooks/useAwaitQuery', () => {
return { useAwaitQuery };
});
+jest.mock('@apollo/client', () => {
+ const apolloClient = jest.requireActual('@apollo/client');
+ return {
+ ...apolloClient,
+ useQuery: jest
+ .fn()
+ .mockReturnValue({ data: { storeConfig: { root_category_id: 1 } } })
+ };
+});
+
/*
* Members.
*/
diff --git a/packages/peregrine/lib/talons/Navigation/navigation.gql.js b/packages/peregrine/lib/talons/Navigation/navigation.gql.js
index 1316b011dc..16137f3344 100644
--- a/packages/peregrine/lib/talons/Navigation/navigation.gql.js
+++ b/packages/peregrine/lib/talons/Navigation/navigation.gql.js
@@ -12,6 +12,16 @@ export const GET_CUSTOMER = gql`
}
`;
+const GET_ROOT_CATEGORY_ID = gql`
+ query getRootCategoryId {
+ storeConfig {
+ id
+ root_category_id
+ }
+ }
+`;
+
export default {
- getCustomerQuery: GET_CUSTOMER
+ getCustomerQuery: GET_CUSTOMER,
+ getRootCategoryId: GET_ROOT_CATEGORY_ID
};
diff --git a/packages/peregrine/lib/talons/Navigation/useNavigation.js b/packages/peregrine/lib/talons/Navigation/useNavigation.js
index 8b41b6f427..0e63a84987 100644
--- a/packages/peregrine/lib/talons/Navigation/useNavigation.js
+++ b/packages/peregrine/lib/talons/Navigation/useNavigation.js
@@ -1,4 +1,5 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useQuery } from '@apollo/client';
import mergeOperations from '../../util/shallowMerge';
import { useAppContext } from '../../context/app';
@@ -18,7 +19,7 @@ const ancestors = {
export const useNavigation = (props = {}) => {
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
- const { getCustomerQuery } = operations;
+ const { getCustomerQuery, getRootCategoryId } = operations;
// retrieve app state from context
const [appState, { closeDrawer }] = useAppContext();
const [catalogState, { actions: catalogActions }] = useCatalogContext();
@@ -30,15 +31,33 @@ export const useNavigation = (props = {}) => {
getUserDetails({ fetchUserDetails });
}, [fetchUserDetails, getUserDetails]);
+ const { data: getRootCategoryData } = useQuery(getRootCategoryId, {
+ fetchPolicy: 'cache-and-network'
+ });
+
+ const rootCategoryId = useMemo(() => {
+ if (getRootCategoryData) {
+ return getRootCategoryData.storeConfig.root_category_id;
+ }
+ }, [getRootCategoryData]);
+
// extract relevant data from app state
const { drawer } = appState;
const isOpen = drawer === 'nav';
- const { categories, rootCategoryId } = catalogState;
+ const { categories } = catalogState;
// get local state
const [view, setView] = useState('MENU');
const [categoryId, setCategoryId] = useState(rootCategoryId);
+ useEffect(() => {
+ // On a fresh render with cold cache set the current category as root
+ // once the root category query completes.
+ if (rootCategoryId && !categoryId) {
+ setCategoryId(rootCategoryId);
+ }
+ }, [categoryId, rootCategoryId]);
+
// define local variables
const category = categories[categoryId];
const isTopLevel = categoryId === rootCategoryId;
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/orderHistoryContext.spec.js.snap b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/orderHistoryContext.spec.js.snap
new file mode 100644
index 0000000000..8f1f83c8cd
--- /dev/null
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/orderHistoryContext.spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders children 1`] = `"Context Provider Children"`;
+
+exports[`returns default value for suffix 1`] = `
+Object {
+ "productURLSuffix": ".html",
+}
+`;
+
+exports[`returns value from backend 1`] = `
+Object {
+ "productURLSuffix": ".jsx",
+}
+`;
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderHistoryPage.spec.js.snap b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderHistoryPage.spec.js.snap
index f4b59b2def..e1c7258d17 100644
--- a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderHistoryPage.spec.js.snap
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderHistoryPage.spec.js.snap
@@ -2,17 +2,34 @@
exports[`it returns the proper shape while loading without data 1`] = `
Object {
+ "errorMessage": null,
+ "handleReset": [Function],
+ "handleSubmit": [Function],
+ "isBackgroundLoading": false,
"isLoadingWithoutData": true,
+ "loadMoreOrders": null,
"orders": Array [],
+ "pageInfo": null,
+ "searchText": "",
}
`;
exports[`it returns the proper shape with data 1`] = `
Object {
+ "errorMessage": null,
+ "handleReset": [Function],
+ "handleSubmit": [Function],
+ "isBackgroundLoading": false,
"isLoadingWithoutData": false,
+ "loadMoreOrders": [Function],
"orders": Array [
"order1",
"order2",
],
+ "pageInfo": Object {
+ "current": 4,
+ "total": 4,
+ },
+ "searchText": "",
}
`;
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderRow.spec.js.snap b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderRow.spec.js.snap
index 367204139f..bb3ec65187 100644
--- a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderRow.spec.js.snap
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/__snapshots__/useOrderRow.spec.js.snap
@@ -8,11 +8,13 @@ Object {
"thumbnail": Object {
"url": "sku1 thumbnail url",
},
+ "url_key": "sku1",
},
Object {
"thumbnail": Object {
"url": "sku2 thumbnail url",
},
+ "url_key": "sku2",
},
],
"isOpen": false,
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/orderHistoryContext.spec.js b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/orderHistoryContext.spec.js
new file mode 100644
index 0000000000..e4be09058e
--- /dev/null
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/orderHistoryContext.spec.js
@@ -0,0 +1,59 @@
+import React, { useEffect } from 'react';
+
+import createTestInstance from '@magento/peregrine/lib/util/createTestInstance';
+import OrderHistoryContextProvider, {
+ useOrderHistoryContext
+} from '../orderHistoryContext';
+import { useQuery } from '@apollo/client';
+
+jest.mock('@apollo/client', () => {
+ const apolloClient = jest.requireActual('@apollo/client');
+
+ return {
+ ...apolloClient,
+ useQuery: jest.fn().mockReturnValue({})
+ };
+});
+
+const log = jest.fn();
+const Consumer = jest.fn(() => {
+ const contextValue = useOrderHistoryContext();
+
+ useEffect(() => {
+ log(contextValue);
+ }, [contextValue]);
+
+ return null;
+});
+
+test('renders children', () => {
+ const tree = createTestInstance(
+
+ );
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('returns default value for suffix', () => {
+ createTestInstance(
+
+
+
+ );
+
+ expect(log.mock.calls[0][0]).toMatchSnapshot();
+});
+
+test('returns value from backend', () => {
+ useQuery.mockReturnValue({
+ data: { storeConfig: { product_url_suffix: '.jsx' } }
+ });
+
+ createTestInstance(
+
+
+
+ );
+
+ expect(log.mock.calls[0][0]).toMatchSnapshot();
+});
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderHistoryPage.spec.js b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderHistoryPage.spec.js
index a2604bbcf3..ea6d9adc43 100644
--- a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderHistoryPage.spec.js
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderHistoryPage.spec.js
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React from 'react';
import { act } from 'react-test-renderer';
import { useQuery } from '@apollo/client';
@@ -12,18 +12,14 @@ jest.mock('react-router-dom', () => {
};
});
-jest.mock('@apollo/client', () => ({
- useQuery: jest.fn().mockReturnValue({
- data: {
- customer: {
- orders: {
- items: ['order1', 'order2']
- }
- }
- },
- loading: false
- })
-}));
+jest.mock('@apollo/client', () => {
+ const apolloClient = jest.requireActual('@apollo/client');
+
+ return {
+ ...apolloClient,
+ useQuery: jest.fn().mockReturnValue({})
+ };
+});
jest.mock('@magento/peregrine/lib/context/app', () => {
const state = {};
@@ -47,60 +43,127 @@ jest.mock('../../../hooks/useTypePolicies', () => ({
useTypePolicies: jest.fn()
}));
-const log = jest.fn();
-const Component = props => {
- const talonProps = useOrderHistoryPage({ ...props });
+jest.mock('../../../util/deriveErrorMessage', () => ({
+ deriveErrorMessage: jest.fn().mockReturnValue(null)
+}));
- useEffect(() => {
- log(talonProps);
- }, [talonProps]);
+const props = {
+ operations: {
+ getCustomerOrdersQuery: 'getCustomerOrdersQuery'
+ }
+};
+
+const orderResponse = {
+ customer: {
+ orders: {
+ items: ['order1', 'order2'],
+ page_info: {
+ current_page: 1,
+ total_pages: 2
+ },
+ total_count: 4
+ }
+ }
+};
+
+const Component = props => {
+ const talonProps = useOrderHistoryPage(props);
- return null;
+ return ;
};
-const props = { queries: {} };
+const getTalonProps = props => {
+ const tree = createTestInstance( );
+ const { root } = tree;
+ const { talonProps } = root.findByType('i').props;
+
+ const update = newProps => {
+ act(() => {
+ tree.update( );
+ });
+
+ return root.findByType('i').props.talonProps;
+ };
+
+ return { talonProps, tree, update };
+};
describe('it returns the proper shape', () => {
test('with data', () => {
- createTestInstance( );
+ useQuery.mockReturnValue({ data: orderResponse, loading: false });
+ const { talonProps } = getTalonProps(props);
- const talonProps = log.mock.calls[0][0];
expect(talonProps).toMatchSnapshot();
});
test('while loading without data', () => {
- useQuery.mockReturnValue({
+ useQuery.mockReturnValueOnce({
+ error: null,
loading: true
});
- createTestInstance( );
+ const { talonProps } = getTalonProps(props);
- const talonProps = log.mock.calls[0][0];
expect(talonProps).toMatchSnapshot();
});
});
test('syncs background loading state', () => {
- const data = {
- customer: {
- orders: {
- items: ['order1', 'order2']
- }
- }
- };
- useQuery.mockReturnValue({ data, loading: false }).mockReturnValueOnce({
- data,
+ useQuery.mockReturnValueOnce({
+ data: orderResponse,
+ error: null,
loading: true
});
const [, { actions }] = useAppContext();
const { setPageLoading } = actions;
- const root = createTestInstance( );
- act(() => {
- root.update( );
- });
+ const { update } = getTalonProps(props);
+ update();
expect(setPageLoading).toHaveBeenNthCalledWith(1, true);
expect(setPageLoading).toHaveBeenNthCalledWith(2, false);
});
+
+test('submit and reset handlers modify search text', () => {
+ const { talonProps: initialTalonProps, update } = getTalonProps(props);
+ initialTalonProps.handleSubmit({
+ search: '000123'
+ });
+
+ const step1TalonProps = update();
+ step1TalonProps.handleReset();
+
+ const step2TalonProps = update();
+
+ expect(initialTalonProps.searchText).toBe('');
+ expect(step1TalonProps.searchText).toBe('000123');
+ expect(useQuery.mock.calls[1][1].variables.filter.number.match).toBe(
+ '000123'
+ );
+ expect(step2TalonProps.searchText).toBe('');
+});
+
+test('load more orders increases page size argument of query', () => {
+ useQuery.mockReturnValue({
+ data: {
+ customer: {
+ orders: {
+ items: [],
+ page_info: {
+ current_page: 1,
+ total_pages: 2
+ },
+ total_count: 15
+ }
+ }
+ }
+ });
+
+ const { talonProps, update } = getTalonProps();
+ talonProps.loadMoreOrders();
+ update();
+
+ expect(useQuery.mock.calls[0][1].variables.pageSize).toBe(10);
+ expect(useQuery.mock.calls[1][1].variables.pageSize).toBe(20);
+});
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderRow.spec.js b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderRow.spec.js
index 14e72f41c0..c6199c4d88 100644
--- a/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderRow.spec.js
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/__tests__/useOrderRow.spec.js
@@ -5,9 +5,14 @@ import { useQuery } from '@apollo/client';
import createTestInstance from '../../../util/createTestInstance';
import { useOrderRow } from '../useOrderRow';
-jest.mock('@apollo/client', () => ({
- useQuery: jest.fn()
-}));
+jest.mock('@apollo/client', () => {
+ const apolloClient = jest.requireActual('@apollo/client');
+
+ return {
+ ...apolloClient,
+ useQuery: jest.fn()
+ };
+});
const log = jest.fn();
const Component = props => {
@@ -20,36 +25,58 @@ const Component = props => {
return null;
};
+const defaultProps = {
+ queries: { getProductThumbnailsQuery: 'getProductThumbnailsQuery' },
+ items: [{ product_url_key: 'sku1' }, { product_url_key: 'sku2' }]
+};
+const items = [
+ { thumbnail: { url: 'sku1 thumbnail url' }, url_key: 'sku1' },
+ { thumbnail: { url: 'sku2 thumbnail url' }, url_key: 'sku2' }
+];
+const dataResponse = {
+ data: {
+ products: {
+ items
+ }
+ },
+ loading: false
+};
+
test('returns correct shape', () => {
- const items = [
- { thumbnail: { url: 'sku1 thumbnail url' } },
- { thumbnail: { url: 'sku2 thumbnail url' } }
- ];
- useQuery.mockReturnValue({ data: { products: { items } }, loading: false });
- createTestInstance(
-
- );
+ useQuery.mockReturnValue(dataResponse);
+ createTestInstance( );
const talonProps = log.mock.calls[0][0];
expect(talonProps).toMatchSnapshot();
});
+test('filters out items not in the request', () => {
+ useQuery.mockReturnValue({
+ data: {
+ products: {
+ items: [
+ ...items,
+ {
+ thumbnail: { url: 'bundle-sku thumbnail url' },
+ url_key: 'bundle-sku'
+ }
+ ]
+ }
+ },
+ loading: false
+ });
+
+ createTestInstance( );
+
+ const talonProps = log.mock.calls[0][0];
+
+ expect(talonProps.imagesData).toHaveLength(2);
+});
+
test('callback toggles open state', () => {
- const items = [
- { thumbnail: { url: 'sku1 thumbnail url' } },
- { thumbnail: { url: 'sku2 thumbnail url' } }
- ];
- useQuery.mockReturnValue({ data: { products: { items } }, loading: false });
- createTestInstance(
-
- );
+ useQuery.mockReturnValue(dataResponse);
+ createTestInstance( );
const talonProps = log.mock.calls[0][0];
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext.gql.js b/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext.gql.js
new file mode 100644
index 0000000000..76b331328d
--- /dev/null
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext.gql.js
@@ -0,0 +1,14 @@
+import { gql } from '@apollo/client';
+
+const GET_PRODUCT_URL_SUFFIX = gql`
+ query GetProductURLSuffix {
+ storeConfig {
+ id
+ product_url_suffix
+ }
+ }
+`;
+
+export default {
+ getProductURLSuffixQuery: GET_PRODUCT_URL_SUFFIX
+};
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext.js b/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext.js
new file mode 100644
index 0000000000..b75069ba35
--- /dev/null
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext.js
@@ -0,0 +1,34 @@
+import React, { createContext, useContext, useMemo } from 'react';
+import { useQuery } from '@apollo/client';
+
+import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
+import DEFAULT_OPERATIONS from './orderHistoryContext.gql';
+
+const OrderHistoryContext = createContext();
+
+const OrderHistoryContextProvider = props => {
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
+ const { getProductURLSuffixQuery } = operations;
+
+ const { data } = useQuery(getProductURLSuffixQuery, {
+ fetchPolicy: 'cache-and-network'
+ });
+
+ const storeConfig = useMemo(() => {
+ return {
+ productURLSuffix: data
+ ? data.storeConfig.product_url_suffix
+ : '.html'
+ };
+ }, [data]);
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default OrderHistoryContextProvider;
+
+export const useOrderHistoryContext = () => useContext(OrderHistoryContext);
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryPage.gql.js b/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryPage.gql.js
new file mode 100644
index 0000000000..2deeadefa6
--- /dev/null
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/orderHistoryPage.gql.js
@@ -0,0 +1,113 @@
+import { gql } from '@apollo/client';
+
+const CustomerOrdersFragment = gql`
+ fragment CustomerOrdersFragment on CustomerOrders {
+ items {
+ billing_address {
+ city
+ country_code
+ firstname
+ lastname
+ postcode
+ region
+ street
+ telephone
+ }
+ id
+ invoices {
+ id
+ }
+ items {
+ id
+ product_name
+ product_sale_price {
+ currency
+ value
+ }
+ product_sku
+ product_url_key
+ selected_options {
+ label
+ value
+ }
+ quantity_ordered
+ }
+ number
+ order_date
+ payment_methods {
+ name
+ type
+ additional_data {
+ name
+ value
+ }
+ }
+ shipments {
+ id
+ tracking {
+ number
+ }
+ }
+ shipping_address {
+ city
+ country_code
+ firstname
+ lastname
+ postcode
+ region
+ street
+ telephone
+ }
+ shipping_method
+ status
+ total {
+ discounts {
+ amount {
+ currency
+ value
+ }
+ }
+ grand_total {
+ currency
+ value
+ }
+ subtotal {
+ currency
+ value
+ }
+ total_shipping {
+ currency
+ value
+ }
+ total_tax {
+ currency
+ value
+ }
+ }
+ }
+ page_info {
+ current_page
+ total_pages
+ }
+ total_count
+ }
+`;
+
+export const GET_CUSTOMER_ORDERS = gql`
+ query GetCustomerOrders(
+ $filter: CustomerOrdersFilterInput
+ $pageSize: Int!
+ ) {
+ customer {
+ id
+ orders(filter: $filter, pageSize: $pageSize) {
+ ...CustomerOrdersFragment
+ }
+ }
+ }
+ ${CustomerOrdersFragment}
+`;
+
+export default {
+ getCustomerOrdersQuery: GET_CUSTOMER_ORDERS
+};
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/orderRow.gql.js b/packages/peregrine/lib/talons/OrderHistoryPage/orderRow.gql.js
similarity index 55%
rename from packages/venia-ui/lib/components/OrderHistoryPage/orderRow.gql.js
rename to packages/peregrine/lib/talons/OrderHistoryPage/orderRow.gql.js
index b16bc25167..0068d4dada 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/orderRow.gql.js
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/orderRow.gql.js
@@ -1,8 +1,8 @@
import { gql } from '@apollo/client';
-export const GET_PRODUCT_THUMBNAILS_BY_SKU = gql`
- query GetProductThumbnailsBySku($skus: [String!]!) {
- products(filter: { sku: { in: $skus } }) {
+export const GET_PRODUCT_THUMBNAILS_BY_URL_KEY = gql`
+ query GetProductThumbnailsByURLKey($urlKeys: [String!]!) {
+ products(filter: { url_key: { in: $urlKeys } }) {
items {
id
sku
@@ -18,7 +18,5 @@ export const GET_PRODUCT_THUMBNAILS_BY_SKU = gql`
`;
export default {
- queries: {
- getProductThumbnailsQuery: GET_PRODUCT_THUMBNAILS_BY_SKU
- }
+ getProductThumbnailsQuery: GET_PRODUCT_THUMBNAILS_BY_URL_KEY
};
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/useOrderHistoryPage.js b/packages/peregrine/lib/talons/OrderHistoryPage/useOrderHistoryPage.js
index bb0c7e905f..02aa60884c 100644
--- a/packages/peregrine/lib/talons/OrderHistoryPage/useOrderHistoryPage.js
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/useOrderHistoryPage.js
@@ -1,16 +1,20 @@
-import { useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useQuery } from '@apollo/client';
+import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
+
import { useAppContext } from '../../context/app';
import { useUserContext } from '../../context/user';
-import { useTypePolicies } from '../../hooks/useTypePolicies';
+import { deriveErrorMessage } from '../../util/deriveErrorMessage';
+
+import DEFAULT_OPERATIONS from './orderHistoryPage.gql';
-export const useOrderHistoryPage = props => {
- const { queries, types } = props;
- const { getCustomerOrdersQuery } = queries;
+const PAGE_SIZE = 10;
- useTypePolicies(types);
+export const useOrderHistoryPage = (props = {}) => {
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
+ const { getCustomerOrdersQuery } = operations;
const [
,
@@ -21,20 +25,68 @@ export const useOrderHistoryPage = props => {
const history = useHistory();
const [{ isSignedIn }] = useUserContext();
- const { data, loading } = useQuery(getCustomerOrdersQuery, {
+ const [pageSize, setPageSize] = useState(PAGE_SIZE);
+ const [searchText, setSearchText] = useState('');
+
+ const {
+ data: orderData,
+ error: getOrderError,
+ loading: orderLoading
+ } = useQuery(getCustomerOrdersQuery, {
fetchPolicy: 'cache-and-network',
- skip: !isSignedIn
+ variables: {
+ filter: {
+ number: {
+ match: searchText
+ }
+ },
+ pageSize
+ }
});
- const isLoadingWithoutData = !data && loading;
- const isBackgroundLoading = !!data && loading;
- const orders = useMemo(() => {
- if (data) {
- return data.customer.orders.items;
+ const orders = orderData ? orderData.customer.orders.items : [];
+
+ const isLoadingWithoutData = !orderData && orderLoading;
+ const isBackgroundLoading = !!orderData && orderLoading;
+
+ const pageInfo = useMemo(() => {
+ if (orderData) {
+ const { total_count } = orderData.customer.orders;
+
+ return {
+ current: pageSize < total_count ? pageSize : total_count,
+ total: total_count
+ };
+ }
+
+ return null;
+ }, [orderData, pageSize]);
+
+ const derivedErrorMessage = useMemo(
+ () => deriveErrorMessage([getOrderError]),
+ [getOrderError]
+ );
+
+ const handleReset = useCallback(() => {
+ setSearchText('');
+ }, []);
+
+ const handleSubmit = useCallback(({ search }) => {
+ setSearchText(search);
+ }, []);
+
+ const loadMoreOrders = useMemo(() => {
+ if (orderData) {
+ const { page_info } = orderData.customer.orders;
+ const { current_page, total_pages } = page_info;
+
+ if (current_page < total_pages) {
+ return () => setPageSize(current => current + PAGE_SIZE);
+ }
}
- return [];
- }, [data]);
+ return null;
+ }, [orderData]);
// If the user is no longer signed in, redirect to the home page.
useEffect(() => {
@@ -49,7 +101,14 @@ export const useOrderHistoryPage = props => {
}, [isBackgroundLoading, setPageLoading]);
return {
+ errorMessage: derivedErrorMessage,
+ handleReset,
+ handleSubmit,
+ isBackgroundLoading,
isLoadingWithoutData,
- orders
+ loadMoreOrders,
+ orders,
+ pageInfo,
+ searchText
};
};
diff --git a/packages/peregrine/lib/talons/OrderHistoryPage/useOrderRow.js b/packages/peregrine/lib/talons/OrderHistoryPage/useOrderRow.js
index 16d39e2ac1..c4da57f8ba 100644
--- a/packages/peregrine/lib/talons/OrderHistoryPage/useOrderRow.js
+++ b/packages/peregrine/lib/talons/OrderHistoryPage/useOrderRow.js
@@ -1,37 +1,45 @@
import { useCallback, useState, useMemo } from 'react';
import { useQuery } from '@apollo/client';
+import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
+import DEFAULT_OPERATIONS from './orderRow.gql';
+
/**
* @function
*
* @param {Object} props
* @param {Array} props.items Collection of items in Order
- * @param {OrderRowQueries} props.queries GraphQL queries for the Order Row Component
+ * @param {OrderRowOperations} props.operations GraphQL queries for the Order Row Component
*
* @returns {OrderRowTalonProps}
*/
export const useOrderRow = props => {
- const { items, queries } = props;
- const { getProductThumbnailsQuery } = queries;
+ const { items } = props;
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
+ const { getProductThumbnailsQuery } = operations;
- const skus = useMemo(() => {
- return items.map(item => item.product_sku).sort();
+ const urlKeys = useMemo(() => {
+ return items.map(item => item.product_url_key).sort();
}, [items]);
const { data, loading } = useQuery(getProductThumbnailsQuery, {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
variables: {
- skus
+ urlKeys
}
});
const imagesData = useMemo(() => {
if (data) {
- return data.products.items;
+ // filter out items returned that we didn't query for
+ const filteredItems = data.products.items.filter(item =>
+ urlKeys.includes(item.url_key)
+ );
+ return filteredItems;
} else {
return [];
}
- }, [data]);
+ }, [data, urlKeys]);
const [isOpen, setIsOpen] = useState(false);
@@ -52,9 +60,9 @@ export const useOrderRow = props => {
*/
/**
- * GraphQL queries for the Order Row Component
+ * GraphQL operations for the Order Row Component
*
- * @typedef {Object} OrderRowQueries
+ * @typedef {Object} OrderRowOperations
*
* @property {GraphQLAST} getProductThumbnailsQuery The query used to get product thumbnails of items in the Order.
*
diff --git a/packages/peregrine/lib/talons/RootComponents/Category/__tests__/__snapshots__/useCategory.spec.js.snap b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/__snapshots__/useCategory.spec.js.snap
index aeac606212..8519f94a26 100644
--- a/packages/peregrine/lib/talons/RootComponents/Category/__tests__/__snapshots__/useCategory.spec.js.snap
+++ b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/__snapshots__/useCategory.spec.js.snap
@@ -3,6 +3,9 @@
exports[`returns the correct shape 1`] = `
Object {
"categoryData": Object {
+ "category": Object {
+ "meta_description": "Category meta-description",
+ },
"products": Object {
"page_info": Object {
"total_pages": 6,
@@ -11,7 +14,7 @@ Object {
},
"error": null,
"loading": false,
- "metaDescription": "",
+ "metaDescription": "Category meta-description",
"pageControl": Object {
"currentPage": 3,
"setPage": [MockFunction],
@@ -28,3 +31,25 @@ Object {
],
}
`;
+
+exports[`runs the category query 1`] = `
+Object {
+ "variables": Object {
+ "currentPage": 3,
+ "filters": Object {
+ "category_id": Object {
+ "eq": "7",
+ },
+ "price": Object {
+ "from": "0",
+ "to": "100",
+ },
+ },
+ "id": 7,
+ "pageSize": 12,
+ "sort": Object {
+ "relevance": "DESC",
+ },
+ },
+}
+`;
diff --git a/packages/peregrine/lib/talons/RootComponents/Category/__tests__/__snapshots__/useCategoryContent.spec.js.snap b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/__snapshots__/useCategoryContent.spec.js.snap
new file mode 100644
index 0000000000..d1cdad66db
--- /dev/null
+++ b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/__snapshots__/useCategoryContent.spec.js.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`handles no data prop 1`] = `
+Object {
+ "categoryDescription": null,
+ "categoryName": null,
+ "filters": null,
+ "handleLoadFilters": [Function],
+ "handleOpenFilters": [Function],
+ "items": Array [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ ],
+ "loadFilters": false,
+ "pageTitle": "Venia",
+ "totalPagesFromData": null,
+}
+`;
+
+exports[`returns the proper shape 1`] = `
+Object {
+ "categoryDescription": "Jewelry category",
+ "categoryName": "Jewelry",
+ "filters": Array [
+ Object {
+ "label": "Label",
+ },
+ ],
+ "handleLoadFilters": [Function],
+ "handleOpenFilters": [Function],
+ "items": Array [
+ Object {
+ "id": 1,
+ "name": "Ring",
+ },
+ Object {
+ "id": 2,
+ "name": "Necklace",
+ },
+ ],
+ "loadFilters": false,
+ "pageTitle": "Jewelry - Venia",
+ "totalPagesFromData": 1,
+}
+`;
diff --git a/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useCategory.spec.js b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useCategory.spec.js
index c4e5ad6937..acd6c19968 100644
--- a/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useCategory.spec.js
+++ b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useCategory.spec.js
@@ -2,6 +2,8 @@ import React from 'react';
import { act } from 'react-test-renderer';
import createTestInstance from '@magento/peregrine/lib/util/createTestInstance';
import { useCategory } from '../useCategory';
+import { useQuery, useLazyQuery } from '@apollo/client';
+import { useLocation } from 'react-router-dom';
jest.mock('react-router-dom', () => ({
useHistory: jest.fn(() => ({ push: jest.fn() })),
@@ -43,7 +45,7 @@ jest.mock('../../../../hooks/usePagination', () => ({
{
setCurrentPage: jest
.fn()
- .mockImplementation(() => mockSetCurrentPage()),
+ .mockImplementation(page => mockSetCurrentPage(page)),
setTotalPages: jest.fn()
}
])
@@ -55,53 +57,208 @@ jest.mock('../../../../hooks/useSort', () => ({
jest.mock('@apollo/client', () => {
const apolloClient = jest.requireActual('@apollo/client');
- const useQuery = jest.fn().mockReturnValue({
- data: {
- __type: {
- inputFields: []
- },
- storeConfig: {
- grid_per_page: 12
- }
- },
- error: null,
- loading: false
- });
- const runQuery = jest.fn();
- const queryResult = {
- data: {
- products: {
- page_info: {
- total_pages: 6
- }
- }
- },
- error: null,
+ const useQuery = jest.fn(() => ({
+ called: false,
+ data: null,
loading: false
- };
- const useLazyQuery = jest.fn(() => [runQuery, queryResult]);
+ }));
+ const useLazyQuery = jest.fn();
return { ...apolloClient, useLazyQuery, useQuery };
});
const mockProps = {
+ id: 7,
queries: {}
};
+const mockPageSizeData = {
+ called: true,
+ data: {
+ __type: {
+ inputFields: []
+ },
+ storeConfig: {
+ grid_per_page: 12
+ }
+ },
+ error: null,
+ loading: false
+};
+
+const mockFilterInputsData = {
+ called: true,
+ error: null,
+ loading: false,
+ data: {
+ __type: {
+ inputFields: [
+ {
+ name: 'category_id',
+ type: {
+ name: 'FilterEqualTypeInput'
+ }
+ },
+ {
+ name: 'price',
+ type: {
+ name: 'FilterRangeTypeInput'
+ }
+ }
+ ]
+ }
+ }
+};
+
+const mockCategoryData = {
+ called: true,
+ error: null,
+ loading: false,
+ data: {
+ category: {
+ meta_description: 'Category meta-description'
+ },
+ products: {
+ page_info: {
+ total_pages: 6
+ }
+ }
+ }
+};
+
+const mockRunQuery = jest.fn();
+
const Component = props => {
const talonProps = useCategory(props);
return ;
};
-const tree = createTestInstance( );
-
test('returns the correct shape', () => {
+ useQuery.mockReturnValue(mockPageSizeData);
+ useLazyQuery.mockReturnValue([mockRunQuery, mockCategoryData]);
+
+ const tree = createTestInstance( );
const { root } = tree;
const { talonProps } = root.findByType('i').props;
expect(talonProps).toMatchSnapshot();
});
+test('runs the category query', () => {
+ useLazyQuery.mockReturnValue([mockRunQuery, mockCategoryData]);
+ useQuery
+ .mockReturnValueOnce(mockPageSizeData)
+ .mockReturnValueOnce(mockFilterInputsData);
+
+ useLocation.mockReturnValue({
+ pathname: '',
+ search: 'page=1&price%5Bfilter%5D=0-100%2C0_100'
+ });
+
+ createTestInstance( );
+
+ expect(mockRunQuery).toHaveBeenCalledTimes(1);
+ expect(mockRunQuery.mock.calls[0][0]).toMatchSnapshot();
+});
+
+test('resets the current page on error', () => {
+ useLazyQuery.mockReturnValue([
+ mockRunQuery,
+ {
+ loading: false,
+ error: {
+ message: 'An error ocurred!'
+ },
+ data: null
+ }
+ ]);
+ useQuery
+ .mockReturnValueOnce(mockPageSizeData)
+ .mockReturnValueOnce(mockFilterInputsData);
+
+ createTestInstance( );
+
+ expect(mockSetCurrentPage).toHaveBeenCalledTimes(1);
+ expect(mockSetCurrentPage).toHaveBeenCalledWith(1);
+});
+
+test('handles no filter type data available', () => {
+ useLazyQuery.mockReturnValue([mockRunQuery, mockCategoryData]);
+ useQuery.mockReturnValueOnce(mockPageSizeData).mockReturnValueOnce({
+ error: null,
+ loading: true,
+ data: null
+ });
+ createTestInstance( );
+
+ expect(mockRunQuery).toHaveBeenCalledTimes(0);
+});
+
+test('category query loading state', () => {
+ useLazyQuery.mockReturnValue([
+ mockRunQuery,
+ {
+ loading: true,
+ error: null,
+ data: null
+ }
+ ]);
+ useQuery
+ .mockReturnValueOnce(mockPageSizeData)
+ .mockReturnValueOnce(mockFilterInputsData);
+
+ const tree = createTestInstance( );
+ const { root } = tree;
+ const { talonProps } = root.findByType('i').props;
+
+ const { loading, categoryData } = talonProps;
+
+ expect(loading).toBeTruthy();
+ expect(categoryData).toBeNull();
+});
+
+test('loading state when only categoryLoading is involved', () => {
+ useLazyQuery.mockReturnValue([
+ mockRunQuery,
+ {
+ called: true,
+ loading: true,
+ error: null,
+ data: null
+ }
+ ]);
+ useQuery.mockReturnValueOnce(mockPageSizeData).mockReturnValueOnce({
+ called: false,
+ loading: false,
+ error: null,
+ data: null
+ });
+
+ const tree = createTestInstance( );
+ const { root } = tree;
+ const { talonProps } = root.findByType('i').props;
+
+ const { loading } = talonProps;
+
+ expect(loading).toBeTruthy();
+});
+
+test('sets current page to 1 if error, !loading, !data, and currentPage != 1', () => {
+ useLazyQuery.mockReturnValue([
+ mockRunQuery,
+ {
+ ...mockCategoryData,
+ loading: false,
+ data: null,
+ error: true
+ }
+ ]);
+
+ createTestInstance( );
+
+ expect(mockSetCurrentPage).toHaveBeenCalledWith(1);
+});
+
const testCases = [
[
'sortText does not reset',
@@ -135,6 +292,11 @@ const testCases = [
test.each(testCases)(
'Changing %s current page to 1.',
(description, sortParams, expected) => {
+ useQuery.mockReturnValue(mockPageSizeData);
+ useLazyQuery.mockReturnValue([mockRunQuery, mockCategoryData]);
+
+ const tree = createTestInstance( );
+
mockUseSort.mockReturnValueOnce([sortParams, jest.fn()]);
act(() => {
tree.update( );
diff --git a/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useCategoryContent.spec.js b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useCategoryContent.spec.js
new file mode 100644
index 0000000000..2fccc69fd9
--- /dev/null
+++ b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useCategoryContent.spec.js
@@ -0,0 +1,162 @@
+import React from 'react';
+import { createTestInstance } from '@magento/peregrine';
+
+import { useCategoryContent } from '../useCategoryContent';
+import { useLazyQuery } from '@apollo/client';
+import { act } from 'react-test-renderer';
+
+import { useAppContext } from '../../../../context/app';
+
+global.STORE_NAME = 'Venia';
+
+jest.mock('../../../../context/app', () => {
+ const state = {};
+ const api = {
+ actions: { toggleDrawer: jest.fn() }
+ };
+ const useAppContext = jest.fn(() => [state, api]);
+
+ return { useAppContext };
+});
+
+jest.mock('@apollo/client', () => {
+ const apolloClient = jest.requireActual('@apollo/client');
+ return {
+ ...apolloClient,
+ useLazyQuery: jest.fn()
+ };
+});
+const Component = props => {
+ const talonprops = useCategoryContent(props);
+
+ return ;
+};
+
+const mockProps = {
+ categoryId: 3,
+ data: {
+ category: {
+ name: 'Jewelry',
+ description: 'Jewelry category'
+ },
+ products: {
+ page_info: {
+ total_pages: 1
+ },
+ items: [
+ {
+ id: 1,
+ name: 'Ring'
+ },
+ {
+ id: 2,
+ name: 'Necklace'
+ }
+ ]
+ }
+ }
+};
+
+const mockProductFiltersByCategoryData = {
+ products: {
+ aggregations: [
+ {
+ label: 'Label'
+ }
+ ]
+ }
+};
+
+const mockGetFilters = jest.fn();
+
+useLazyQuery.mockReturnValue([
+ mockGetFilters,
+ { data: mockProductFiltersByCategoryData }
+]);
+
+it('returns the proper shape', () => {
+ const rendered = createTestInstance( );
+
+ const talonProps = rendered.root.findByType('i').props;
+
+ expect(mockGetFilters).toHaveBeenCalled();
+ expect(talonProps).toMatchSnapshot();
+});
+
+it('sets the filter loading state', () => {
+ const rendered = createTestInstance( );
+
+ const talonProps = rendered.root.findByType('i').props;
+
+ const { loadFilters, handleLoadFilters } = talonProps;
+
+ expect(loadFilters).toBeFalsy();
+
+ act(() => {
+ handleLoadFilters();
+ });
+
+ const updatedProps = rendered.root.findByType('i').props;
+
+ expect(updatedProps.loadFilters).toBeTruthy();
+});
+
+it('toggles drawer when opening filters', () => {
+ const mockToggleDrawer = jest.fn();
+ useAppContext.mockReturnValue([
+ {},
+ {
+ toggleDrawer: mockToggleDrawer
+ }
+ ]);
+
+ const rendered = createTestInstance( );
+
+ const talonProps = rendered.root.findByType('i').props;
+
+ const { loadFilters, handleOpenFilters } = talonProps;
+
+ expect(loadFilters).toBeFalsy();
+
+ act(() => {
+ handleOpenFilters();
+ });
+
+ const updatedProps = rendered.root.findByType('i').props;
+
+ expect(updatedProps.loadFilters).toBeTruthy();
+ expect(mockToggleDrawer).toHaveBeenCalled();
+});
+
+it('handles default category id', () => {
+ const testProps = Object.assign({}, mockProps, {
+ categoryId: 0
+ });
+
+ createTestInstance( );
+
+ expect(mockGetFilters).not.toHaveBeenCalled();
+});
+
+it('handles no filter data returned', () => {
+ useLazyQuery.mockReturnValue([mockGetFilters, {}]);
+ const rendered = createTestInstance( );
+
+ const talonProps = rendered.root.findByType('i').props;
+
+ expect(talonProps.filters).toBeNull();
+});
+
+it('handles no data prop', () => {
+ const testProps = {
+ categoryId: 0,
+ data: null,
+ pageSize: 9
+ };
+
+ const rendered = createTestInstance( );
+
+ const talonProps = rendered.root.findByType('i').props;
+
+ expect(talonProps).toMatchSnapshot();
+});
diff --git a/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useNoProductsFound.spec.js b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useNoProductsFound.spec.js
new file mode 100644
index 0000000000..b81fc97903
--- /dev/null
+++ b/packages/peregrine/lib/talons/RootComponents/Category/__tests__/useNoProductsFound.spec.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import { useCatalogContext } from '../../../../context/catalog';
+import { createTestInstance } from '@magento/peregrine';
+
+import { useNoProductsFound } from '../useNoProductsFound';
+
+jest.mock('../../../../context/catalog', () => ({
+ useCatalogContext: jest.fn()
+}));
+
+const Component = props => {
+ const talonProps = useNoProductsFound(props);
+
+ return ;
+};
+
+const props = {
+ categoryId: '3'
+};
+
+const mockCatalogContext = {
+ categories: [
+ {
+ parentId: 0,
+ id: 1
+ },
+ {
+ parentId: 1,
+ id: 2
+ },
+ {
+ parentId: 1,
+ id: 3
+ },
+ {
+ parentId: 1,
+ id: 4
+ },
+ {
+ parentId: 1,
+ id: 5
+ },
+ {
+ parentId: 1,
+ id: 6
+ }
+ ]
+};
+
+it('returns the proper shape', () => {
+ useCatalogContext.mockReturnValue([mockCatalogContext]);
+
+ const rendered = createTestInstance( );
+
+ const talonProps = rendered.root.findByType('i').props;
+
+ const { recommendedCategories } = talonProps;
+
+ expect(recommendedCategories).toHaveLength(3);
+});
+
+it('handles fewer categories than default amount to show', () => {
+ useCatalogContext.mockReturnValue([
+ {
+ categories: [
+ {
+ parentId: 0,
+ id: 1
+ },
+ {
+ parentId: 1,
+ id: 2
+ }
+ ]
+ }
+ ]);
+
+ const rendered = createTestInstance( );
+
+ const talonProps = rendered.root.findByType('i').props;
+
+ const { recommendedCategories } = talonProps;
+
+ expect(recommendedCategories).toHaveLength(1);
+});
diff --git a/packages/peregrine/lib/talons/RootComponents/Category/useCategory.js b/packages/peregrine/lib/talons/RootComponents/Category/useCategory.js
index e774e5dc42..ff420b33b9 100644
--- a/packages/peregrine/lib/talons/RootComponents/Category/useCategory.js
+++ b/packages/peregrine/lib/talons/RootComponents/Category/useCategory.js
@@ -170,10 +170,10 @@ export const useCategory = props => {
// If we get an error after loading we should try to reset to page 1.
// If we continue to have errors after that, render an error message.
useEffect(() => {
- if (error && !categoryLoading && currentPage !== 1) {
+ if (error && !categoryLoading && !data && currentPage !== 1) {
setCurrentPage(1);
}
- }, [currentPage, error, categoryLoading, setCurrentPage]);
+ }, [currentPage, error, categoryLoading, setCurrentPage, data]);
// Reset the current page back to one (1) when the search string, filters
// or sort criteria change.
diff --git a/packages/peregrine/lib/talons/RootComponents/Product/__tests__/useProduct.spec.js b/packages/peregrine/lib/talons/RootComponents/Product/__tests__/useProduct.spec.js
index b7b3964bb2..92838c36c4 100644
--- a/packages/peregrine/lib/talons/RootComponents/Product/__tests__/useProduct.spec.js
+++ b/packages/peregrine/lib/talons/RootComponents/Product/__tests__/useProduct.spec.js
@@ -13,16 +13,27 @@ jest.mock('@magento/peregrine/lib/context/app', () => {
});
jest.mock('@apollo/client', () => {
- const queryResult = {
- loading: false,
+ const apolloClient = jest.requireActual('@apollo/client');
+ const useQueryMock = jest.fn().mockReturnValue({
+ data: {
+ products: {
+ items: [
+ {
+ id: 1,
+ name: 'Karena Halter Dress',
+ url_key: 'karena-halter-dress'
+ }
+ ]
+ }
+ },
error: null,
- data: null
- };
- const useQuery = jest.fn(() => {
- queryResult;
+ loading: false
});
- return { useQuery };
+ return {
+ ...apolloClient,
+ useQuery: useQueryMock
+ };
});
const log = jest.fn();
@@ -38,9 +49,6 @@ const Component = props => {
const props = {
mapProduct: jest.fn(product => product),
- queries: {
- getProductQuery: 'getProductQuery'
- },
urlKey: 'unit_test'
};
diff --git a/packages/peregrine/lib/talons/RootComponents/Product/product.gql.js b/packages/peregrine/lib/talons/RootComponents/Product/product.gql.js
new file mode 100644
index 0000000000..8d7fe8eff3
--- /dev/null
+++ b/packages/peregrine/lib/talons/RootComponents/Product/product.gql.js
@@ -0,0 +1,19 @@
+import { gql } from '@apollo/client';
+
+import { ProductDetailsFragment } from './productDetailFragment.gql';
+
+export const GET_PRODUCT_DETAIL_QUERY = gql`
+ query getProductDetailForProductPage($urlKey: String!) {
+ products(filter: { url_key: { eq: $urlKey } }) {
+ items {
+ id
+ ...ProductDetailsFragment
+ }
+ }
+ }
+ ${ProductDetailsFragment}
+`;
+
+export default {
+ getProductDetailQuery: GET_PRODUCT_DETAIL_QUERY
+};
diff --git a/packages/peregrine/lib/talons/RootComponents/Product/productDetailFragment.gql.js b/packages/peregrine/lib/talons/RootComponents/Product/productDetailFragment.gql.js
new file mode 100644
index 0000000000..e7485690d9
--- /dev/null
+++ b/packages/peregrine/lib/talons/RootComponents/Product/productDetailFragment.gql.js
@@ -0,0 +1,86 @@
+import { gql } from '@apollo/client';
+
+export const ProductDetailsFragment = gql`
+ fragment ProductDetailsFragment on ProductInterface {
+ __typename
+ categories {
+ id
+ breadcrumbs {
+ category_id
+ }
+ }
+ description {
+ html
+ }
+ id
+ media_gallery_entries {
+ id
+ label
+ position
+ disabled
+ file
+ }
+ meta_description
+ name
+ price {
+ regularPrice {
+ amount {
+ currency
+ value
+ }
+ }
+ }
+ sku
+ small_image {
+ url
+ }
+ url_key
+ ... on ConfigurableProduct {
+ configurable_options {
+ attribute_code
+ attribute_id
+ id
+ label
+ values {
+ default_label
+ label
+ store_label
+ use_default_value
+ value_index
+ swatch_data {
+ ... on ImageSwatchData {
+ thumbnail
+ }
+ value
+ }
+ }
+ }
+ variants {
+ attributes {
+ code
+ value_index
+ }
+ product {
+ id
+ media_gallery_entries {
+ id
+ disabled
+ file
+ label
+ position
+ }
+ sku
+ stock_status
+ price {
+ regularPrice {
+ amount {
+ currency
+ value
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+`;
diff --git a/packages/peregrine/lib/talons/RootComponents/Product/useProduct.js b/packages/peregrine/lib/talons/RootComponents/Product/useProduct.js
index 472b947ca7..5294b6c4e8 100644
--- a/packages/peregrine/lib/talons/RootComponents/Product/useProduct.js
+++ b/packages/peregrine/lib/talons/RootComponents/Product/useProduct.js
@@ -2,6 +2,9 @@ import { useQuery } from '@apollo/client';
import { useEffect, useMemo } from 'react';
import { useAppContext } from '@magento/peregrine/lib/context/app';
+import mergeOperations from '../../../util/shallowMerge';
+import DEFAULT_OPERATIONS from './product.gql';
+
/**
* A [React Hook]{@link https://reactjs.org/docs/hooks-intro.html} that
* controls the logic for the Product Root Component.
@@ -19,7 +22,11 @@ import { useAppContext } from '@magento/peregrine/lib/context/app';
* @returns {Bool} result.product - The product's details.
*/
export const useProduct = props => {
- const { mapProduct, queries, urlKey } = props;
+ const { mapProduct, urlKey } = props;
+
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
+ const { getProductDetailQuery } = operations;
+
const [
,
{
@@ -27,7 +34,7 @@ export const useProduct = props => {
}
] = useAppContext();
- const { error, loading, data } = useQuery(queries.getProductQuery, {
+ const { error, loading, data } = useQuery(getProductDetailQuery, {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
variables: {
diff --git a/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/__snapshots__/useSavedPaymentsPage.spec.js.snap b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/__snapshots__/useSavedPaymentsPage.spec.js.snap
new file mode 100644
index 0000000000..4e4bb1f389
--- /dev/null
+++ b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/__snapshots__/useSavedPaymentsPage.spec.js.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`it returns the proper shape 1`] = `
+Object {
+ "isLoading": false,
+ "savedPayments": Array [],
+}
+`;
diff --git a/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js
index 09c3de03bb..b8e7b5ae54 100644
--- a/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js
+++ b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js
@@ -69,7 +69,7 @@ const Component = props => {
};
const props = {
- queries: {
+ operations: {
getSavedPaymentsQuery: 'getSavedPaymentsQuery'
}
};
@@ -80,9 +80,8 @@ test('it returns the proper shape', () => {
// Assert.
const talonProps = log.mock.calls[0][0];
- const actualKeys = Object.keys(talonProps);
- const expectedKeys = ['savedPayments', 'handleAddPayment', 'isLoading'];
- expect(actualKeys.sort()).toEqual(expectedKeys.sort());
+
+ expect(talonProps).toMatchSnapshot();
});
test('it returns the savedPayments correctly when present', () => {
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js b/packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js
similarity index 77%
rename from packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js
rename to packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js
index 1ef294a254..a7b2411570 100644
--- a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js
+++ b/packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js
@@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
export const GET_SAVED_PAYMENTS_QUERY = gql`
- query getSavedPayments {
+ query GetSavedPayments {
customerPaymentTokens {
items {
details
@@ -13,7 +13,5 @@ export const GET_SAVED_PAYMENTS_QUERY = gql`
`;
export default {
- queries: {
- GET_SAVED_PAYMENTS_QUERY
- }
+ getSavedPaymentsQuery: GET_SAVED_PAYMENTS_QUERY
};
diff --git a/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js b/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js
index b9d1aebe96..8552125597 100644
--- a/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js
+++ b/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js
@@ -1,4 +1,4 @@
-import { useCallback, useEffect } from 'react';
+import { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useQuery } from '@apollo/client';
@@ -6,6 +6,9 @@ import { useQuery } from '@apollo/client';
import { useAppContext } from '@magento/peregrine/lib/context/app';
import { useUserContext } from '@magento/peregrine/lib/context/user';
+import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
+import defaultOperations from './savedPaymentsPage.gql';
+
export const normalizeTokens = responseData => {
const paymentTokens =
(responseData && responseData.customerPaymentTokens.items) || [];
@@ -26,17 +29,16 @@ export const normalizeTokens = responseData => {
* @function
*
* @param {Object} props
- * @param {SavedPaymentsPageQueries} props.queries GraphQL queries
+ * @param {SavedPaymentsPageQueries} props.operations GraphQL queries
*
* @returns {SavedPaymentsPageTalonProps}
*
* @example Importing into your project
* import { useSavedPayments } from '@magento/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage';
*/
-export const useSavedPaymentsPage = props => {
- const {
- queries: { getSavedPaymentsQuery }
- } = props;
+export const useSavedPaymentsPage = (props = {}) => {
+ const operations = mergeOperations(defaultOperations, props.operations);
+ const { getSavedPaymentsQuery } = operations;
const [
,
@@ -68,14 +70,9 @@ export const useSavedPaymentsPage = props => {
setPageLoading(loading);
}, [loading, setPageLoading]);
- const handleAddPayment = useCallback(() => {
- // TODO in PWA-637
- }, []);
-
const savedPayments = normalizeTokens(savedPaymentsData);
return {
- handleAddPayment,
isLoading: loading,
savedPayments
};
@@ -90,7 +87,7 @@ export const useSavedPaymentsPage = props => {
*
* @property {GraphQLAST} getSavedPaymentsQuery Query for getting saved payments. See https://devdocs.magento.com/guides/v2.4/graphql/queries/customer-payment-tokens.html
*
- * @see [savedPaymentsPage.gql.js]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js}
+ * @see [savedPaymentsPage.gql.js]{@link https://github.com/magento/pwa-studio/blob/develop/packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js}
* for queries used in Venia
*/
diff --git a/packages/peregrine/lib/talons/SearchPage/searchPage.gql.js b/packages/peregrine/lib/talons/SearchPage/searchPage.gql.js
index 4c141b2421..83659889b2 100644
--- a/packages/peregrine/lib/talons/SearchPage/searchPage.gql.js
+++ b/packages/peregrine/lib/talons/SearchPage/searchPage.gql.js
@@ -1,5 +1,14 @@
import { gql } from '@apollo/client';
+export const GET_PAGE_SIZE = gql`
+ query getPageSize {
+ storeConfig {
+ id
+ grid_per_page
+ }
+ }
+`;
+
export const GET_PRODUCT_FILTERS_BY_SEARCH = gql`
query getProductFiltersBySearch($search: String!) {
products(search: $search) {
@@ -71,6 +80,7 @@ export const GET_FILTER_INPUTS = gql`
export default {
getFilterInputsQuery: GET_FILTER_INPUTS,
+ getPageSize: GET_PAGE_SIZE,
getProductFiltersBySearchQuery: GET_PRODUCT_FILTERS_BY_SEARCH,
productSearchQuery: PRODUCT_SEARCH
};
diff --git a/packages/peregrine/lib/talons/SearchPage/useSearchPage.js b/packages/peregrine/lib/talons/SearchPage/useSearchPage.js
index 9da4248ac0..5a8a53d524 100644
--- a/packages/peregrine/lib/talons/SearchPage/useSearchPage.js
+++ b/packages/peregrine/lib/talons/SearchPage/useSearchPage.js
@@ -19,13 +19,11 @@ import DEFAULT_OPERATIONS from './searchPage.gql';
* @param {String} props.query - graphql query used for executing search
*/
export const useSearchPage = (props = {}) => {
- const {
- queries: { getPageSize }
- } = props;
-
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
+
const {
getFilterInputsQuery,
+ getPageSize,
getProductFiltersBySearchQuery,
productSearchQuery
} = operations;
diff --git a/packages/peregrine/lib/targets/__tests__/peregrine-targets.spec.js b/packages/peregrine/lib/targets/__tests__/peregrine-targets.spec.js
index 5d0e492683..67eec03988 100644
--- a/packages/peregrine/lib/targets/__tests__/peregrine-targets.spec.js
+++ b/packages/peregrine/lib/targets/__tests__/peregrine-targets.spec.js
@@ -119,6 +119,7 @@ test('exposes all hooks and targets', async () => {
talons.Checkout.useShippingForm.wrapWith() wraps export "useShippingForm" from "Checkout/useShippingForm.js"
talons.CheckoutPage.AddressBook.useAddressBook.wrapWith() wraps export "useAddressBook" from "CheckoutPage/AddressBook/useAddressBook.js"
talons.CheckoutPage.AddressBook.useAddressCard.wrapWith() wraps export "useAddressCard" from "CheckoutPage/AddressBook/useAddressCard.js"
+ talons.CheckoutPage.GuestSignIn.useGuestSignIn.wrapWith() wraps export "useGuestSignIn" from "CheckoutPage/GuestSignIn/useGuestSignIn.js"
talons.CheckoutPage.ItemsReview.useItemsReview.wrapWith() wraps export "useItemsReview" from "CheckoutPage/ItemsReview/useItemsReview.js"
talons.CheckoutPage.OrderConfirmationPage.useCreateAccount.wrapWith() wraps export "useCreateAccount" from "CheckoutPage/OrderConfirmationPage/useCreateAccount.js"
talons.CheckoutPage.OrderConfirmationPage.useOrderConfirmationPage.wrapWith() wraps export "useOrderConfirmationPage" from "CheckoutPage/OrderConfirmationPage/useOrderConfirmationPage.js"
diff --git a/packages/peregrine/package.json b/packages/peregrine/package.json
index eef1881d89..dd80dc6ed6 100644
--- a/packages/peregrine/package.json
+++ b/packages/peregrine/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/peregrine",
- "version": "8.0.0",
+ "version": "9.0.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/pwa-buildpack/lib/BuildBus/declare-base.js b/packages/pwa-buildpack/lib/BuildBus/declare-base.js
index 617bd2c33f..76a578e100 100644
--- a/packages/pwa-buildpack/lib/BuildBus/declare-base.js
+++ b/packages/pwa-buildpack/lib/BuildBus/declare-base.js
@@ -153,7 +153,26 @@ module.exports = targets => {
* @member {tapable.AsyncSeriesHook}
* @param {transformUpwardIntercept} interceptor
*/
- transformUpward: new targets.types.AsyncSeries(['definitions'])
+ transformUpward: new targets.types.AsyncSeries(['definitions']),
+
+ /**
+ * Collect all ENV validation functions that will run against the
+ * project's ENV. The functions can be async and they will run in
+ * parallel. If a validation function wants to stop the whole process
+ * for instance in case of a serious security issue, it can do so
+ * by throwing an error. If it wants to report an error, it can do so
+ * by using the onFail callback provided as an argument. A validation
+ * function can submit multiple errors by calling the onFail function
+ * multiple times. All the errors will be queued into an array and
+ * displayed on the console at the end of the process.
+ *
+ * @example
+ * targets.of('@magento/pwa-buildpack').validateEnv.tapPromise(validateBackendUrl);
+ *
+ * @member {tapable.AsyncParallelHook}
+ * @param {envValidationInterceptor} validator
+ */
+ validateEnv: new targets.types.AsyncParallel(['validator'])
};
/**
@@ -282,3 +301,29 @@ module.exports = targets => {
* @param {object} definition - Parsed UPWARD definition object.
* @returns {Promise}
*/
+
+/** Type definitions related to: validateEnv */
+
+/**
+ * Intercept function signature for the validateEnv target.
+ *
+ * Interceptors of the `validateEnv` target receive a config object.
+ * The config object contains the project env, an onFail callback and
+ * the debug function to be used in case of the debug mode to log more
+ * inforamtion to the console.
+ *
+ * This Target can be used asynchronously in the parallel mode. If a
+ * validator needs to stop the process immediately, it can throw an error.
+ * If it needs to report an error but not stop the whole process, it can do
+ * so by calling the onFail function with the error message it wants to report.
+ * It can call the onFail multiple times if it wants to report multiple errors.
+ *
+ * All the errors will be queued and printed into the console at the end of the
+ * validation process and the build process will be stopeed.
+ *
+ * @callback envValidationInterceptor
+ * @param {Object} config.env - Project ENV
+ * @param {Function} config.onFail - On fail callback
+ * @param {Function} config.debug - Debug function to be used for additional reporting in debug mode
+ * @returns {Boolean}
+ */
diff --git a/packages/pwa-buildpack/lib/Utilities/__tests__/__snapshots__/loadEnvironment.spec.js.snap b/packages/pwa-buildpack/lib/Utilities/__tests__/__snapshots__/loadEnvironment.spec.js.snap
index 2c0ccbb7f0..e82219833e 100644
--- a/packages/pwa-buildpack/lib/Utilities/__tests__/__snapshots__/loadEnvironment.spec.js.snap
+++ b/packages/pwa-buildpack/lib/Utilities/__tests__/__snapshots__/loadEnvironment.spec.js.snap
@@ -19,3 +19,9 @@ Array [
],
]
`;
+
+exports[`throws on load if variable defs are invalid 1`] = `
+"Bad environment variable definition. Section inscrutable variable {
+ \\"type\\": \\"ineffable\\"
+} declares an unknown type ineffable"
+`;
diff --git a/packages/pwa-buildpack/lib/Utilities/__tests__/__snapshots__/runEnvValidators.spec.js.snap b/packages/pwa-buildpack/lib/Utilities/__tests__/__snapshots__/runEnvValidators.spec.js.snap
new file mode 100644
index 0000000000..06b824e36c
--- /dev/null
+++ b/packages/pwa-buildpack/lib/Utilities/__tests__/__snapshots__/runEnvValidators.spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should throw error if there are validation errors reported by interceptors 1`] = `
+"Environment has 2 validation errors:
+ (1) Danger,
+ (2) Another error"
+`;
diff --git a/packages/pwa-buildpack/lib/Utilities/__tests__/addImgOptMiddleware.spec.js b/packages/pwa-buildpack/lib/Utilities/__tests__/addImgOptMiddleware.spec.js
index 7efec62460..b5e817a63a 100644
--- a/packages/pwa-buildpack/lib/Utilities/__tests__/addImgOptMiddleware.spec.js
+++ b/packages/pwa-buildpack/lib/Utilities/__tests__/addImgOptMiddleware.spec.js
@@ -43,7 +43,7 @@ test('attaches middleware to app', () => {
expect.objectContaining({ force: false })
);
- expect(app.use).toHaveBeenCalledWith(mockCacheMiddleware, filterMiddleware);
+ expect(app.use).toHaveBeenCalled();
expect(filterMiddleware).toBeTruthy();
});
diff --git a/packages/pwa-buildpack/lib/Utilities/__tests__/createDotEnvFile.spec.js b/packages/pwa-buildpack/lib/Utilities/__tests__/createDotEnvFile.spec.js
index 53e922f0cc..d2fd284d9a 100644
--- a/packages/pwa-buildpack/lib/Utilities/__tests__/createDotEnvFile.spec.js
+++ b/packages/pwa-buildpack/lib/Utilities/__tests__/createDotEnvFile.spec.js
@@ -3,6 +3,7 @@ jest.mock('../../util/pretty-logger', () => ({
error: jest.fn()
}));
jest.mock('../getEnvVarDefinitions');
+jest.mock('../runEnvValidators', () => jest.fn().mockResolvedValue(true));
const dotenv = require('dotenv');
const getEnvVarDefinitions = require('../getEnvVarDefinitions');
const createDotEnvFile = require('../createDotEnvFile');
@@ -45,47 +46,47 @@ beforeEach(() => {
mockLog.error.mockClear();
});
-test('logs errors to default logger if env is not valid', () => {
+test('logs errors to default logger if env is not valid', async () => {
mockEnvVars.set({
MAGENTO_BACKEND_URL: mockEnvVars.UNSET
});
- createDotEnvFile('./');
+ await createDotEnvFile('./');
expect(prettyLogger.warn).toHaveBeenCalled();
});
-test('uses alternate logger', () => {
+test('uses alternate logger', async () => {
mockEnvVars.set({
MAGENTO_BACKEND_URL: mockEnvVars.UNSET
});
- createDotEnvFile('./', { logger: mockLog });
+ await createDotEnvFile('./', { logger: mockLog });
expect(mockLog.warn).toHaveBeenCalled();
});
-test('returns valid dotenv file if env is valid', () => {
+test('returns valid dotenv file if env is valid', async () => {
mockEnvVars.set(examples);
- const fileText = createDotEnvFile('./', { logger: mockLog });
+ const fileText = await createDotEnvFile('./', { logger: mockLog });
expect(snapshotEnvFile(fileText)).toMatchSnapshot();
expect(dotenv.parse(fileText)).toMatchObject(examples);
});
-test('populates with examples where available', () => {
+test('populates with examples where available', async () => {
const unsetExamples = {};
for (const key of Object.keys(examples)) {
unsetExamples[key] = mockEnvVars.UNSET;
}
mockEnvVars.set(unsetExamples);
- const fileText = createDotEnvFile('./', { useExamples: true });
+ const fileText = await createDotEnvFile('./', { useExamples: true });
expect(dotenv.parse(fileText)).toMatchObject(examples);
});
-test('does not print example comment if value is set custom', () => {
+test('does not print example comment if value is set custom', async () => {
const fakeEnv = {
...examples,
MAGENTO_BACKEND_URL: 'https://custom.url',
IMAGE_SERVICE_CACHE_EXPIRES: 'a million years'
};
mockEnvVars.set(fakeEnv);
- const fileText = createDotEnvFile(fakeEnv);
+ const fileText = await createDotEnvFile(fakeEnv);
expect(fileText).not.toMatch(MAGENTO_BACKEND_URL_EXAMPLE);
expect(fileText).not.toMatch(
`Example: ${examples.IMAGE_SERVICE_CACHE_EXPIRES}`
@@ -93,7 +94,7 @@ test('does not print example comment if value is set custom', () => {
expect(dotenv.parse(fileText)).not.toMatchObject(examples);
});
-test('passing an env object works, but warns deprecation and assumes cwd is context', () => {
+test('passing an env object works, but warns deprecation and assumes cwd is context', async () => {
getEnvVarDefinitions.mockReturnValue({
sections: [
{
@@ -117,7 +118,7 @@ test('passing an env object works, but warns deprecation and assumes cwd is cont
});
expect(
snapshotEnvFile(
- createDotEnvFile({
+ await createDotEnvFile({
TEST_ENV_VAR_NOTHING: 'foo'
})
)
diff --git a/packages/pwa-buildpack/lib/Utilities/__tests__/loadEnvironment.spec.js b/packages/pwa-buildpack/lib/Utilities/__tests__/loadEnvironment.spec.js
index 0a23bb0c94..33af00b375 100644
--- a/packages/pwa-buildpack/lib/Utilities/__tests__/loadEnvironment.spec.js
+++ b/packages/pwa-buildpack/lib/Utilities/__tests__/loadEnvironment.spec.js
@@ -43,6 +43,10 @@ getEnvVarDefinitions.mockReturnValue({
]
});
+jest.doMock('../runEnvValidators');
+const validateEnv = require('../runEnvValidators');
+validateEnv.mockResolvedValue(true);
+
jest.mock('../../../package.json', () => {
const packageJson = jest.requireActual('../../../package.json');
@@ -69,7 +73,7 @@ afterAll(() => {
const loadEnvironment = require('../loadEnvironment');
-test('throws on load if variable defs are invalid', () => {
+test('throws on load if variable defs are invalid', async () => {
getEnvVarDefinitions.mockReturnValueOnce({
sections: [
{
@@ -83,22 +87,21 @@ test('throws on load if variable defs are invalid', () => {
],
changes: []
});
- expect(() => loadEnvironment('./')).toThrow(
- 'Bad environment variable definition'
- );
+
+ await expect(loadEnvironment('./')).rejects.toThrowErrorMatchingSnapshot();
});
-test('parses dotenv file if argument is path string', () => {
+test('parses dotenv file if argument is path string', async () => {
dotenv.config.mockReturnValueOnce({
parsed: 'DOTENV PARSED'
});
- const { envFilePresent } = loadEnvironment('/path/to/dir');
+ const { envFilePresent } = await loadEnvironment('/path/to/dir');
expect(envFilePresent).toBe(true);
expect(dotenv.config).toHaveBeenCalledWith({ path: '/path/to/dir/.env' });
});
-test('warns on deprecated api use', () => {
- loadEnvironment({
+test('warns on deprecated api use', async () => {
+ await loadEnvironment({
MUST_BE_BOOLEAN_DUDE: false
});
expect(console.warn).toHaveBeenCalledWith(
@@ -106,8 +109,8 @@ test('warns on deprecated api use', () => {
);
});
-test('does not warn if deprecated API is okay', () => {
- loadEnvironment(
+test('does not warn if deprecated API is okay', async () => {
+ await loadEnvironment(
{
MUST_BE_BOOLEAN_DUDE: false
},
@@ -133,9 +136,9 @@ test('does not warn if deprecated API is okay', () => {
);
});
-test('debug logs environment in human readable way', () => {
+test('debug logs environment in human readable way', async () => {
debug.enabled = true;
- loadEnvironment({
+ await loadEnvironment({
MUST_BE_BOOLEAN_DUDE: false
});
expect(debug).toHaveBeenCalledWith(
@@ -145,29 +148,29 @@ test('debug logs environment in human readable way', () => {
debug.enabled = true;
});
-test('sets envFilePresent to false if .env is missing', () => {
+test('sets envFilePresent to false if .env is missing', async () => {
const enoent = new Error('ENOENT');
enoent.code = 'ENOENT';
dotenv.config.mockReturnValueOnce({
error: enoent
});
- const { envFilePresent } = loadEnvironment('/path/to/dir');
+ const { envFilePresent } = await loadEnvironment('/path/to/dir');
expect(envFilePresent).toBe(false);
});
-test('warns but continues if .env has errors', () => {
+test('warns but continues if .env has errors', async () => {
dotenv.config.mockReturnValueOnce({
error: new Error('blagh')
});
- loadEnvironment('/path/to/dir');
+ await loadEnvironment('/path/to/dir');
expect(console.warn).toHaveBeenCalledWith(
expect.stringMatching(/could not.*parse/i),
expect.any(Error)
);
});
-test('emits errors on type mismatch', () => {
- const { error } = loadEnvironment({
+test('emits errors on type mismatch', async () => {
+ const { error } = await loadEnvironment({
MUST_BE_BOOLEAN_DUDE: 'but it aint'
});
expect(error.message).toMatch(/MUST_BE_BOOLEAN/);
@@ -176,14 +179,19 @@ test('emits errors on type mismatch', () => {
);
});
-test('throws anything unexpected from validation', () => {
+test('throws anything unexpected from validation', async () => {
envalid.cleanEnv.mockImplementationOnce(() => {
throw new Error('invalid in a way i cannot even describe');
});
- expect(() => loadEnvironment({})).toThrow('cannot even');
+
+ try {
+ await loadEnvironment({});
+ } catch (e) {
+ expect(e.message).toBe('invalid in a way i cannot even describe');
+ }
});
-test('emits log messages on a custom logger', () => {
+test('emits log messages on a custom logger', async () => {
const mockLog = {
warn: jest.fn().mockName('mockLog.warn'),
error: jest.fn().mockName('mockLog.error')
@@ -202,7 +210,7 @@ test('emits log messages on a custom logger', () => {
],
changes: []
});
- loadEnvironment(
+ await loadEnvironment(
{
MUST_BE_BOOLEAN_DUDE: 'twelve'
},
@@ -213,7 +221,7 @@ test('emits log messages on a custom logger', () => {
);
});
-test('logs all types of change', () => {
+test('logs all types of change', async () => {
const defs = {
sections: [
{
@@ -329,7 +337,7 @@ test('logs all types of change', () => {
}
]
};
- loadEnvironment(
+ await loadEnvironment(
{
HAS_BEEN_REMOVED: 'motivation',
RON_ARTEST: 'hi',
@@ -350,7 +358,7 @@ test('logs all types of change', () => {
expect(consoleMessages).toMatchSnapshot();
});
-test('ignores invalid change defs', () => {
+test('ignores invalid change defs', async () => {
getEnvVarDefinitions.mockReturnValueOnce({
sections: [],
changes: [
@@ -363,7 +371,7 @@ test('ignores invalid change defs', () => {
expect(() => loadEnvironment({ OH_NOES: 'foo' })).not.toThrow();
});
-test('returns configuration object', () => {
+test('returns configuration object', async () => {
getEnvVarDefinitions.mockReturnValueOnce({
sections: [
{
@@ -401,7 +409,7 @@ test('returns configuration object', () => {
GEWGAW_PALADIN: 'level 3',
GEWGAW_ROGUE: 'level 4'
};
- const config = loadEnvironment({ ...party, NODE_ENV: 'test' });
+ const config = await loadEnvironment({ ...party, NODE_ENV: 'test' });
expect(config).toMatchObject({
isProd: false,
isProduction: false,
@@ -434,7 +442,7 @@ test('returns configuration object', () => {
expect(all).not.toHaveProperty('mustang');
});
-test('augments with interceptors of envVarDefinitions target', () => {
+test('augments with interceptors of envVarDefinitions target', async () => {
getEnvVarDefinitions.mockReset();
getEnvVarDefinitions.mockImplementationOnce(context =>
jest.requireActual('../getEnvVarDefinitions')(context)
@@ -471,7 +479,7 @@ test('augments with interceptors of envVarDefinitions target', () => {
),
{ virtual: true }
);
- loadEnvironment('./other/context');
+ await loadEnvironment('./other/context');
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('SIGNAL_INTENSITY')
);
diff --git a/packages/pwa-buildpack/lib/Utilities/__tests__/runEnvValidators.spec.js b/packages/pwa-buildpack/lib/Utilities/__tests__/runEnvValidators.spec.js
new file mode 100644
index 0000000000..cf1f55a657
--- /dev/null
+++ b/packages/pwa-buildpack/lib/Utilities/__tests__/runEnvValidators.spec.js
@@ -0,0 +1,62 @@
+jest.mock('../../BuildBus');
+const BuildBus = require('../../BuildBus');
+
+const runValidations = require('../runEnvValidators');
+
+const context = 'some/valid/path';
+const env = {};
+
+test('should run validator targets', async () => {
+ const initFn = jest.fn();
+ const runValidationTargets = jest.fn();
+ const getTargetsOfFn = jest.fn().mockReturnValueOnce({
+ validateEnv: { promise: runValidationTargets }
+ });
+ const bus = {
+ init: initFn,
+ getTargetsOf: getTargetsOfFn
+ };
+ const forFn = jest.fn().mockReturnValue(bus);
+ const enableTrackingFn = jest.fn();
+ BuildBus.for.mockImplementationOnce(forFn);
+ BuildBus.enableTracking.mockImplementationOnce(enableTrackingFn);
+
+ const returnValue = await runValidations(context, env);
+
+ expect(initFn).toHaveBeenCalled();
+ expect(forFn).toHaveBeenCalledWith(context);
+ expect(getTargetsOfFn).toHaveBeenCalledWith('@magento/pwa-buildpack');
+ expect(runValidationTargets).toHaveBeenCalledWith({
+ env: expect.any(Object),
+ onFail: expect.any(Function),
+ debug: expect.any(Function)
+ });
+ expect(returnValue).toBeTruthy();
+});
+
+test('should throw error if there are validation errors reported by interceptors', async () => {
+ const initFn = jest.fn();
+ const runValidationTargets = jest.fn().mockImplementation(({ onFail }) => {
+ onFail('Danger');
+ onFail(new Error('Another error'));
+ });
+ const getTargetsOfFn = jest.fn().mockReturnValueOnce({
+ validateEnv: { promise: runValidationTargets }
+ });
+ const bus = {
+ init: initFn,
+ getTargetsOf: getTargetsOfFn
+ };
+ const forFn = jest.fn().mockReturnValue(bus);
+ const enableTrackingFn = jest.fn();
+ BuildBus.for.mockImplementationOnce(forFn);
+ BuildBus.enableTracking.mockImplementationOnce(enableTrackingFn);
+
+ try {
+ const returnValue = await runValidations(context, env);
+
+ expect(returnValue).toBeUndefined();
+ } catch (e) {
+ expect(e.message).toMatchSnapshot();
+ }
+});
diff --git a/packages/pwa-buildpack/lib/Utilities/addImgOptMiddleware.js b/packages/pwa-buildpack/lib/Utilities/addImgOptMiddleware.js
index 80bb20e242..7fd755d3f2 100644
--- a/packages/pwa-buildpack/lib/Utilities/addImgOptMiddleware.js
+++ b/packages/pwa-buildpack/lib/Utilities/addImgOptMiddleware.js
@@ -1,5 +1,6 @@
const debug = require('../util/debug').makeFileLogger(__filename);
let cache;
+/** @type {import("hastily")} */
let hastily;
let missingDeps = '';
const markDepInvalid = (dep, e) => {
@@ -56,7 +57,11 @@ If possible, install additional tools to build NodeJS native dependencies:
https://github.com/nodejs/node-gyp#installation`
);
} else {
- app.use(cacheMiddleware, imageopto);
+ app.use(
+ hastily.HASTILY_STREAMABLE_PATH_REGEXP,
+ cacheMiddleware,
+ imageopto
+ );
}
}
diff --git a/packages/pwa-buildpack/lib/Utilities/createDotEnvFile.js b/packages/pwa-buildpack/lib/Utilities/createDotEnvFile.js
index 1f6008f788..31c0676266 100644
--- a/packages/pwa-buildpack/lib/Utilities/createDotEnvFile.js
+++ b/packages/pwa-buildpack/lib/Utilities/createDotEnvFile.js
@@ -23,7 +23,7 @@ const graf = txt =>
}) + '\n';
const paragraphs = (...grafs) => grafs.map(graf).join(blankline);
-module.exports = function printEnvFile(dirOrEnv, options = {}) {
+module.exports = async function printEnvFile(dirOrEnv, options = {}) {
const { logger = prettyLogger, useExamples } = options;
// All environment variables Buildpack and PWA Studio use should be defined
// in envVarDefinitions.json, along with recent changes to those vars for
@@ -37,7 +37,7 @@ module.exports = function printEnvFile(dirOrEnv, options = {}) {
}
const definitions = getEnvVarDefinitions(context);
- const { env, error } = loadEnvironment(dirOrEnv, logger, definitions);
+ const { env, error } = await loadEnvironment(dirOrEnv, logger, definitions);
if (error && !useExamples) {
logger.warn(
`The current environment is not yet valid; please set any missing variables to build the project before generating a .env file.`
diff --git a/packages/pwa-buildpack/lib/Utilities/createProject.js b/packages/pwa-buildpack/lib/Utilities/createProject.js
index 510b462694..54eb0b2efe 100644
--- a/packages/pwa-buildpack/lib/Utilities/createProject.js
+++ b/packages/pwa-buildpack/lib/Utilities/createProject.js
@@ -108,7 +108,7 @@ async function createProject(options) {
before = () => {},
ignores = getIgnores(packageRoot),
visitor
- } = instructions.create({
+ } = await instructions.create({
fs: fse,
tasks: makeCommonTasks(fse),
options
diff --git a/packages/pwa-buildpack/lib/Utilities/loadEnvironment.js b/packages/pwa-buildpack/lib/Utilities/loadEnvironment.js
index d6f3d0196b..49de625d3b 100644
--- a/packages/pwa-buildpack/lib/Utilities/loadEnvironment.js
+++ b/packages/pwa-buildpack/lib/Utilities/loadEnvironment.js
@@ -10,6 +10,7 @@ const envalid = require('envalid');
const camelspace = require('camelspace');
const prettyLogger = require('../util/pretty-logger');
const getEnvVarDefinitions = require('./getEnvVarDefinitions');
+const validateEnv = require('./runEnvValidators');
const CompatEnvAdapter = require('./CompatEnvAdapter');
/**
@@ -116,7 +117,7 @@ class ProjectConfiguration {
* retrieving definitions from the BuildBus. _Internal only._
* @returns {ProjectConfiguration}
*/
-function loadEnvironment(dirOrEnv, customLogger, providedDefs) {
+async function loadEnvironment(dirOrEnv, customLogger, providedDefs) {
const logger = customLogger || prettyLogger;
let incomingEnv = process.env;
let definitions;
@@ -207,6 +208,9 @@ This call to loadEnvironment() will assume that the working directory ${context}
strict: true
}
);
+ if (typeof dirOrEnv === 'string') {
+ await validateEnv(dirOrEnv, loadedEnv);
+ }
if (debug.enabled) {
// Only do this prettiness if we gotta
debug(
diff --git a/packages/pwa-buildpack/lib/Utilities/runEnvValidators.js b/packages/pwa-buildpack/lib/Utilities/runEnvValidators.js
new file mode 100644
index 0000000000..026f9cd884
--- /dev/null
+++ b/packages/pwa-buildpack/lib/Utilities/runEnvValidators.js
@@ -0,0 +1,73 @@
+/**
+ * @module Buildpack/Utilities
+ */
+const debug = require('debug')('pwa-buildpack:runEnvValidators');
+
+/**
+ * Validate the project ENV.
+ * Calling this function will invoke the `validateEnv` target of buildpack. All the intercepts
+ * will be provided the whole process.ENV, a callback function to call if the validation has failed
+ * and a debug function to be used in case of the debug mode.
+ *
+ * @public
+ * @memberof Buildpack/Utilities
+ * @param {string} context Project root directory.
+ * @param {object} env Project ENV.
+ * @returns {Boolean}
+ */
+async function validateEnv(context, env) {
+ debug('Running ENV Validations');
+
+ const BuildBus = require('../BuildBus');
+
+ if (process.env.DEBUG && process.env.DEBUG.includes('BuildBus')) {
+ BuildBus.enableTracking();
+ }
+
+ const bus = BuildBus.for(context);
+ bus.init();
+
+ const errorMessages = [];
+ const onFail = errorMessage => errorMessages.push(errorMessage);
+
+ const validationContext = { env, onFail, debug };
+
+ try {
+ await bus
+ .getTargetsOf('@magento/pwa-buildpack')
+ .validateEnv.promise(validationContext);
+ } catch {
+ /**
+ * While creating a new project using the create-pwa cli
+ * runEnvValidators will be invoked but the buildpack targets
+ * will be missing, and it is expected. Hence we are wrapping
+ * it in a try catch to avoid build failures. Anyways we wont be
+ * using env validations while creating project. It will be useful
+ * while building a project.
+ */
+ debug('Buildpack targets not found.');
+ }
+
+ if (errorMessages.length) {
+ debug('Found validation errors in ENV, stopping the build process');
+
+ const removeErrorPrefix = msg => msg.replace(/^Error:\s*/, '');
+ const printValidationMsg = (error, index) =>
+ `\n (${index + 1}) ${removeErrorPrefix(error.message || error)}`;
+ const prettyErrorList = errorMessages.map(printValidationMsg);
+ const validationError = new Error(
+ `Environment has ${
+ errorMessages.length
+ } validation errors: ${prettyErrorList}`
+ );
+ validationError.errorMessages = errorMessages;
+
+ throw validationError;
+ }
+
+ debug('No issues found in the ENV');
+
+ return true;
+}
+
+module.exports = validateEnv;
diff --git a/packages/pwa-buildpack/lib/Utilities/serve.js b/packages/pwa-buildpack/lib/Utilities/serve.js
index 19c1b25120..f065fbb08c 100644
--- a/packages/pwa-buildpack/lib/Utilities/serve.js
+++ b/packages/pwa-buildpack/lib/Utilities/serve.js
@@ -2,7 +2,7 @@ const loadEnvironment = require('../Utilities/loadEnvironment');
const path = require('path');
module.exports = async function serve(dirname) {
- const config = loadEnvironment(dirname);
+ const config = await loadEnvironment(dirname);
if (config.error) {
// loadEnvironment takes care of logging it
throw new Error('Can not load environment config!');
diff --git a/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/configureWebpack.js b/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/configureWebpack.js
index 3bf6a3d1f9..20384d80a1 100644
--- a/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/configureWebpack.js
+++ b/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/configureWebpack.js
@@ -131,7 +131,7 @@ async function configureWebpack(options) {
const babelRootMode = await getBabelRootMode(context);
- const projectConfig = loadEnvironment(context);
+ const projectConfig = await loadEnvironment(context);
if (projectConfig.error) {
throw projectConfig.error;
}
diff --git a/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getModuleRules.js b/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getModuleRules.js
index 7d20292ef0..6cadeb3432 100644
--- a/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getModuleRules.js
+++ b/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getModuleRules.js
@@ -1,6 +1,7 @@
/**
* @module Buildpack/WebpackTools
*/
+const path = require('path');
/**
* Create a Webpack
@@ -56,7 +57,10 @@ getModuleRules.js = async ({
const astLoaders = [
{
// Use custom loader to enable warning reporting from Babel plugins
- loader: 'buildbus-babel-loader',
+ loader: path.resolve(
+ __dirname,
+ '../loaders/buildbus-babel-loader.js'
+ ),
options: {
sourceMaps: mode === 'development' && 'inline',
envName: mode,
diff --git a/packages/pwa-buildpack/lib/__tests__/cli-create-env-file.spec.js b/packages/pwa-buildpack/lib/__tests__/cli-create-env-file.spec.js
index a8283407d7..f693641589 100644
--- a/packages/pwa-buildpack/lib/__tests__/cli-create-env-file.spec.js
+++ b/packages/pwa-buildpack/lib/__tests__/cli-create-env-file.spec.js
@@ -1,15 +1,29 @@
jest.mock('fs');
-const { resolve } = require('path');
+
const { writeFileSync } = require('fs');
-const dotenv = require('dotenv');
jest.mock('../Utilities/getEnvVarDefinitions', () => () =>
require('../../envVarDefinitions.json')
);
const createEnvCliBuilder = require('../cli/create-env-file');
+jest.mock('../Utilities/createDotEnvFile', () =>
+ jest.fn().mockResolvedValue('DOT ENV FILE CONTENTS')
+);
+const createDotEnvFile = require('../Utilities/createDotEnvFile');
+
+jest.mock('path', () => {
+ const path = jest.requireActual('path');
+
+ return {
+ ...path,
+ resolve: jest.fn().mockReturnValue('./pwa-studio/.env')
+ };
+});
+
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});
+
afterEach(() => {
jest.restoreAllMocks();
});
@@ -22,33 +36,37 @@ test('is a yargs builder', () => {
});
});
-test('creates and writes file', () => {
+test('creates and writes file', async () => {
process.env.MAGENTO_BACKEND_URL = 'https://example.com/';
- createEnvCliBuilder.handler({
- directory: process.cwd()
+ const directory = process.cwd();
+ await createEnvCliBuilder.handler({
+ directory
});
expect(writeFileSync).toHaveBeenCalledWith(
- resolve(process.cwd(), '.env'),
- expect.stringContaining('https://example.com/'),
+ './pwa-studio/.env',
+ 'DOT ENV FILE CONTENTS',
'utf8'
);
+ expect(createDotEnvFile).toHaveBeenCalledWith(directory, {
+ useExamples: undefined
+ });
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('wrote'));
});
-test('creates and writes file with examples', () => {
+test('creates and writes file with examples', async () => {
process.env.MAGENTO_BACKEND_URL = 'https://example.com/';
- createEnvCliBuilder.handler({
- directory: process.cwd(),
+ const directory = process.cwd();
+ await createEnvCliBuilder.handler({
+ directory,
useExamples: true
});
expect(writeFileSync).toHaveBeenCalledWith(
- resolve(process.cwd(), '.env'),
- expect.stringContaining('https://example.com/'),
+ './pwa-studio/.env',
+ 'DOT ENV FILE CONTENTS',
'utf8'
);
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('wrote'));
- const writtenFile = writeFileSync.mock.calls[0][1];
- expect(dotenv.parse(writtenFile)).toMatchObject({
- MAGENTO_BACKEND_URL: 'https://example.com/'
+ expect(createDotEnvFile).toHaveBeenCalledWith(directory, {
+ useExamples: true
});
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('wrote'));
});
diff --git a/packages/pwa-buildpack/lib/__tests__/cli-load-env.spec.js b/packages/pwa-buildpack/lib/__tests__/cli-load-env.spec.js
index b2163fc776..2ed44e6eea 100644
--- a/packages/pwa-buildpack/lib/__tests__/cli-load-env.spec.js
+++ b/packages/pwa-buildpack/lib/__tests__/cli-load-env.spec.js
@@ -7,6 +7,10 @@ const dotenv = require('dotenv');
const loadEnvCliBuilder = require('../cli/load-env');
const createEnv = require('../cli/create-env-file').handler;
+jest.mock('../Utilities/runEnvValidators', () =>
+ jest.fn().mockResolvedValue(true)
+);
+
const proc = {
exit: jest.fn()
};
@@ -39,18 +43,18 @@ test('is a yargs builder', () => {
});
});
-test('handler exits nonzero on missing required variables on errors', () => {
+test('handler exits nonzero on missing required variables on errors', async () => {
// missing required variables
dotenv.config.mockReturnValueOnce({
parsed: {}
});
- loadEnvCliBuilder.handler({ directory: '.' }, proc);
+ await loadEnvCliBuilder.handler({ directory: '.' }, proc);
expect(console.error).toHaveBeenCalled();
expect(proc.exit).toHaveBeenCalledTimes(1);
expect(proc.exit.mock.calls[0][0]).toBeGreaterThan(0);
});
-test('handler loads from dotenv file', () => {
+test('handler loads from dotenv file', async () => {
// Arrange.
process.env.MAGENTO_BACKEND_URL = 'https://glorp.zorp';
process.env.CHECKOUT_BRAINTREE_TOKEN = 'my_custom_value';
@@ -59,7 +63,7 @@ test('handler loads from dotenv file', () => {
});
// Act.
- const result = loadEnvCliBuilder.handler(
+ const result = await loadEnvCliBuilder.handler(
{
directory: '.'
},
@@ -70,7 +74,7 @@ test('handler loads from dotenv file', () => {
expect(result).toBeUndefined();
});
-test('warns if dotenv file does not exist', () => {
+test('warns if dotenv file does not exist', async () => {
// Arrange.
process.env.MAGENTO_BACKEND_URL = 'https://glorp.zorp';
process.env.CHECKOUT_BRAINTREE_TOKEN = 'my_custom_value';
@@ -84,7 +88,7 @@ test('warns if dotenv file does not exist', () => {
});
// Act.
- loadEnvCliBuilder.handler(
+ await loadEnvCliBuilder.handler(
{
directory: '.'
},
@@ -97,7 +101,7 @@ test('warns if dotenv file does not exist', () => {
);
});
-test('creates a .env file from example values if --core-dev-mode', () => {
+test('creates a .env file from example values if --core-dev-mode', async () => {
// Arrange.
process.env.MAGENTO_BACKEND_URL = 'https://glorp.zorp';
process.env.CHECKOUT_BRAINTREE_TOKEN = 'my_custom_value';
@@ -111,7 +115,7 @@ test('creates a .env file from example values if --core-dev-mode', () => {
});
// Act.
- loadEnvCliBuilder.handler(
+ await loadEnvCliBuilder.handler(
{
directory: '.',
coreDevMode: true
diff --git a/packages/pwa-buildpack/lib/cli/create-custom-origin.js b/packages/pwa-buildpack/lib/cli/create-custom-origin.js
index 9fe6b80619..39364ea8c3 100644
--- a/packages/pwa-buildpack/lib/cli/create-custom-origin.js
+++ b/packages/pwa-buildpack/lib/cli/create-custom-origin.js
@@ -19,7 +19,7 @@ module.exports.describe =
module.exports.handler = async function buildpackCli({ directory }) {
const projectRoot = resolve(directory);
try {
- const projectConfig = loadEnvironment(projectRoot, prettyLogger);
+ const projectConfig = await loadEnvironment(projectRoot, prettyLogger);
if (projectConfig.error) {
failExpected(projectConfig.error);
}
diff --git a/packages/pwa-buildpack/lib/cli/create-env-file.js b/packages/pwa-buildpack/lib/cli/create-env-file.js
index 43c7c11cd4..31d2a95758 100644
--- a/packages/pwa-buildpack/lib/cli/create-env-file.js
+++ b/packages/pwa-buildpack/lib/cli/create-env-file.js
@@ -1,5 +1,7 @@
const { writeFileSync } = require('fs');
const { resolve } = require('path');
+
+const createDotEnvFile = require('../Utilities/createDotEnvFile');
const prettyLogger = require('../util/pretty-logger');
module.exports.command = 'create-env-file ';
@@ -13,15 +15,15 @@ module.exports.builder = {
}
};
-module.exports.handler = function buildpackCli({ directory, useExamples }) {
+module.exports.handler = async function buildpackCli({
+ directory,
+ useExamples
+}) {
const envFilePath = resolve(directory, '.env');
- writeFileSync(
- envFilePath,
- require('../Utilities/createDotEnvFile')(directory, {
- useExamples
- }),
- 'utf8'
- );
+ const dotEnvFile = await createDotEnvFile(directory, {
+ useExamples
+ });
+ writeFileSync(envFilePath, dotEnvFile, 'utf8');
prettyLogger.info(
`Successfully wrote a fresh configuration file to ${envFilePath}`
);
diff --git a/packages/pwa-buildpack/lib/cli/load-env.js b/packages/pwa-buildpack/lib/cli/load-env.js
index e253fe65e4..e60a58f6b0 100644
--- a/packages/pwa-buildpack/lib/cli/load-env.js
+++ b/packages/pwa-buildpack/lib/cli/load-env.js
@@ -12,13 +12,14 @@ module.exports.builder = {
}
};
-module.exports.handler = function buildpackCli(
+module.exports.handler = async function buildpackCli(
{ directory, coreDevMode },
proc = process
) {
- const { error, envFilePresent } = require('../Utilities/loadEnvironment')(
- directory
- );
+ const {
+ error,
+ envFilePresent
+ } = await require('../Utilities/loadEnvironment')(directory);
if (!envFilePresent) {
if (coreDevMode) {
prettyLogger.warn(`Creating new .env file using example values`);
diff --git a/packages/pwa-buildpack/lib/queries/getStoreMediaUrl.graphql b/packages/pwa-buildpack/lib/queries/getStoreMediaUrl.graphql
index 40db702035..013db00ce1 100644
--- a/packages/pwa-buildpack/lib/queries/getStoreMediaUrl.graphql
+++ b/packages/pwa-buildpack/lib/queries/getStoreMediaUrl.graphql
@@ -1,6 +1,7 @@
# Get the media URL from the store configuration.
query {
storeConfig {
+ id
secure_base_media_url
}
}
diff --git a/packages/pwa-buildpack/package.json b/packages/pwa-buildpack/package.json
index 172adbc15b..1e5f97e6b2 100644
--- a/packages/pwa-buildpack/package.json
+++ b/packages/pwa-buildpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/pwa-buildpack",
- "version": "7.0.0",
+ "version": "8.0.0",
"publishConfig": {
"access": "public"
},
@@ -27,7 +27,7 @@
"homepage": "https://github.com/magento/pwa-studio/tree/master/packages/pwa-buildpack#readme",
"dependencies": {
"@magento/directive-parser": "~0.1.7",
- "@magento/upward-js": "~5.0.0",
+ "@magento/upward-js": "~5.0.1",
"@pmmmwh/react-refresh-webpack-plugin": "~0.4.1",
"apicache": "~1.4.0",
"boxen": "~3.0.0",
@@ -45,7 +45,7 @@
"fs-extra": "~7.0.1",
"gitignore-to-glob": "~0.3.0",
"graphql-playground-middleware-express": "~1.7.18",
- "hastily": "~0.4.7",
+ "hastily": "~0.5.0",
"js-yaml": "~3.13.1",
"jsdom": "~16.2.2",
"klaw": "~3.0.0",
diff --git a/packages/upward-js/lib/resolvers/DirectoryResolver.js b/packages/upward-js/lib/resolvers/DirectoryResolver.js
index 03138774de..12494a1d51 100644
--- a/packages/upward-js/lib/resolvers/DirectoryResolver.js
+++ b/packages/upward-js/lib/resolvers/DirectoryResolver.js
@@ -30,17 +30,20 @@ class DirectoryResolver extends AbstractResolver {
let server = DirectoryResolver.servers.get(directory);
if (!server) {
+ const staticOpts = {
+ fallthrough: false,
+ index: false,
+ maxAge: process.env.NODE_ENV === 'production' ? 604800000 : 0
+ };
debug(
- `creating new server for ${directory} relative to ${
- this.visitor.upwardPath
- }`
+ `creating new server for directory "%s" relative to "%s" with options %o`,
+ directory,
+ this.visitor.upwardPath,
+ staticOpts
);
server = serveStatic(
path.resolve(path.dirname(this.visitor.upwardPath), directory),
- {
- fallthrough: false,
- index: false
- }
+ staticOpts
);
DirectoryResolver.servers.set(directory, server);
}
diff --git a/packages/upward-js/package.json b/packages/upward-js/package.json
index 777c50cc21..3914482ce0 100644
--- a/packages/upward-js/package.json
+++ b/packages/upward-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/upward-js",
- "version": "5.0.0",
+ "version": "5.0.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/venia-concept/_buildpack/__tests__/create.spec.js b/packages/venia-concept/_buildpack/__tests__/create.spec.js
index 75f4a55edf..1236574b80 100644
--- a/packages/venia-concept/_buildpack/__tests__/create.spec.js
+++ b/packages/venia-concept/_buildpack/__tests__/create.spec.js
@@ -47,7 +47,7 @@ const runCreate = async (fs, opts) => {
...opts,
directory: '/target'
};
- const { after, before, visitor } = createVenia({
+ const { after, before, visitor } = await createVenia({
fs,
tasks: makeCommonTasks(fs),
options
diff --git a/packages/venia-concept/_buildpack/create.js b/packages/venia-concept/_buildpack/create.js
index 8af2f2a83a..309517ddff 100644
--- a/packages/venia-concept/_buildpack/create.js
+++ b/packages/venia-concept/_buildpack/create.js
@@ -1,7 +1,42 @@
const { resolve } = require('path');
+const {
+ sampleBackends: defaultSampleBackends
+} = require('@magento/pwa-buildpack/lib/cli/create-project');
-function createProjectFromVenia({ fs, tasks, options }) {
+const uniqBy = (array, property) => {
+ const map = new Map();
+
+ for (const element of array) {
+ if (element && element.hasOwnProperty(property)) {
+ map.set(element[property], element);
+ }
+ }
+
+ return Array.from(map.values());
+};
+
+const removeDuplicateBackends = backendEnvironments =>
+ uniqBy(backendEnvironments, 'url');
+
+const fetchSampleBackends = async () => {
+ try {
+ const res = await fetch(
+ 'https://fvp0esmt8f.execute-api.us-east-1.amazonaws.com/default/getSampleBackends'
+ );
+ const { sampleBackends } = await res.json();
+
+ return removeDuplicateBackends([
+ ...sampleBackends.environments,
+ ...defaultSampleBackends.environments
+ ]).map(({ url }) => url);
+ } catch {
+ return defaultSampleBackends.environments.map(({ url }) => url);
+ }
+};
+
+async function createProjectFromVenia({ fs, tasks, options }) {
const npmCli = options.npmClient;
+ const sampleBackendEnvironments = await fetchSampleBackends();
const toCopyFromPackageJson = [
'main',
@@ -63,7 +98,7 @@ function createProjectFromVenia({ fs, tasks, options }) {
'package.json': ({
path,
targetPath,
- options: { name, author }
+ options: { name, author, backendUrl }
}) => {
const pkgTpt = fs.readJsonSync(path);
const pkg = {
@@ -80,6 +115,14 @@ function createProjectFromVenia({ fs, tasks, options }) {
pkg[prop] = pkgTpt[prop];
});
+ // If the backend url is a sample backend, add the validator.
+ if (sampleBackendEnvironments.includes(backendUrl)) {
+ pkg.devDependencies = {
+ ...pkg.devDependencies,
+ '@magento/venia-sample-backends': '~0.0.1'
+ };
+ }
+
// The venia-concept template is part of the monorepo, which
// uses yarn for workspaces. But if the user wants to use
// npm, then the scripts which use `yarn` must change.
diff --git a/packages/venia-concept/package.json b/packages/venia-concept/package.json
index 5b70c915fc..7589ce0e62 100644
--- a/packages/venia-concept/package.json
+++ b/packages/venia-concept/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/venia-concept",
- "version": "8.0.0",
+ "version": "9.0.0",
"publishConfig": {
"access": "public"
},
@@ -36,7 +36,7 @@
},
"homepage": "https://github.com/magento/pwa-studio/tree/master/packages/venia-concept#readme",
"dependencies": {
- "@magento/pwa-buildpack": "~7.0.0"
+ "@magento/pwa-buildpack": "~8.0.0"
},
"devDependencies": {
"@adobe/apollo-link-mutation-queue": "~1.0.2",
@@ -52,10 +52,11 @@
"@babel/runtime": "~7.4.2",
"@magento/babel-preset-peregrine": "~1.1.0",
"@magento/eslint-config": "~1.5.0",
- "@magento/pagebuilder": "~3.0.0",
- "@magento/peregrine": "~8.0.0",
- "@magento/upward-security-headers": "~1.0.0",
- "@magento/venia-ui": "~5.0.0",
+ "@magento/pagebuilder": "~4.0.0",
+ "@magento/peregrine": "~9.0.0",
+ "@magento/upward-security-headers": "~1.0.1",
+ "@magento/venia-adobe-data-layer": "~0.0.1",
+ "@magento/venia-ui": "~6.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.4.1",
"@storybook/react": "~5.2.6",
"apollo-cache-persist": "~0.1.1",
@@ -88,6 +89,7 @@
"informed": "~2.11.17",
"jarallax": "~1.11.1",
"load-google-maps-api": "~2.0.1",
+ "lodash": "~4.17.20",
"lodash.escape": "~4.0.1",
"lodash.get": "~4.4.2",
"lodash.over": "~4.7.0",
diff --git a/packages/venia-concept/src/.storybook/webpack.config.js b/packages/venia-concept/src/.storybook/webpack.config.js
index 02cd641010..48359ecd19 100644
--- a/packages/venia-concept/src/.storybook/webpack.config.js
+++ b/packages/venia-concept/src/.storybook/webpack.config.js
@@ -15,7 +15,7 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
// defines in the docs.
// See https://storybook.js.org/docs/configurations/custom-webpack-config/#full-control-mode
module.exports = async ({ config: storybookBaseConfig, mode }) => {
- const projectConfig = loadEnvironment(
+ const projectConfig = await loadEnvironment(
// Load .env from root
path.resolve(__dirname, '../..')
);
diff --git a/packages/venia-concept/src/ServiceWorker/Utilities/__tests__/routeHandler.spec.js b/packages/venia-concept/src/ServiceWorker/Utilities/__tests__/routeHandler.spec.js
index 0a07ed8ec9..2517dd5844 100644
--- a/packages/venia-concept/src/ServiceWorker/Utilities/__tests__/routeHandler.spec.js
+++ b/packages/venia-concept/src/ServiceWorker/Utilities/__tests__/routeHandler.spec.js
@@ -5,30 +5,58 @@ const urlWithHTML = 'https://develop.pwa-venia.com/isodora-skirt.html';
const nonHTMLRoute =
'https://magento-venia-concept-7bnnn.local.pwadev:9914/media/';
-test('isHomeRoute should return boolean', () => {
- expect(typeof isHomeRoute(new URL(homePageUrl))).toBe('boolean');
+describe('isHomeRoute', () => {
+ test('returns a boolean', () => {
+ expect(typeof isHomeRoute(new URL(homePageUrl))).toBe('boolean');
+ });
+
+ test("returns true if route's pathname is /", () => {
+ expect(isHomeRoute(new URL(homePageUrl))).toBeTruthy();
+ });
+
+ test("returns false if route's pathname is not /", () => {
+ expect(isHomeRoute(new URL(urlWithHTML))).toBeFalsy();
+ });
+
+ test('returns true if pathname matches an available store view code', () => {
+ const previousEnv = process.env.USE_STORE_CODE_IN_URL;
+ process.env.USE_STORE_CODE_IN_URL = true;
+
+ expect(isHomeRoute(new URL(`${homePageUrl}default`))).toBeTruthy();
+ expect(isHomeRoute(new URL(`${homePageUrl}fr`))).toBeTruthy();
+ expect(isHomeRoute(new URL(`${homePageUrl}default/`))).toBeTruthy();
+ expect(isHomeRoute(new URL(`${homePageUrl}fr/`))).toBeTruthy();
+
+ // Reset.
+ process.env.USE_STORE_CODE_IN_URL = previousEnv;
+ });
+
+ test('returns false if pathname has does not end in store code', () => {
+ const previousEnv = process.env.USE_STORE_CODE_IN_URL;
+ process.env.USE_STORE_CODE_IN_URL = true;
+
+ expect(isHomeRoute(new URL(`${homePageUrl}default/foo`))).toBeFalsy();
+ expect(isHomeRoute(new URL(`${homePageUrl}fr/foo`))).toBeFalsy();
+
+ // Reset.
+ process.env.USE_STORE_CODE_IN_URL = previousEnv;
+ });
});
-test("isHomeRoute should return true if route's pathname is /", () => {
- expect(isHomeRoute(new URL(homePageUrl))).toBeTruthy();
-});
-
-test("isHomeRoute should return false if route's pathname is not /", () => {
- expect(isHomeRoute(new URL(urlWithHTML))).toBeFalsy();
-});
+describe('isHTMLRoute', () => {
+ test('returns a boolean', () => {
+ expect(typeof isHTMLRoute(new URL(urlWithHTML))).toBe('boolean');
+ });
-test('isHTMLRoute should return boolean', () => {
- expect(typeof isHTMLRoute(new URL(urlWithHTML))).toBe('boolean');
-});
+ test("returns true if route's pathname has .html", () => {
+ expect(isHTMLRoute(new URL(urlWithHTML))).toBeTruthy();
+ });
-test("isHTMLRoute should return true if route's pathname has .html", () => {
- expect(isHTMLRoute(new URL(urlWithHTML))).toBeTruthy();
-});
-
-test("isHTMLRoute should return false if route's pathname does not have .html", () => {
- expect(isHTMLRoute(new URL(nonHTMLRoute))).toBeFalsy();
-});
+ test("returns false if route's pathname does not have .html", () => {
+ expect(isHTMLRoute(new URL(nonHTMLRoute))).toBeFalsy();
+ });
-test('isHTMLRoute should return true for home route', () => {
- expect(isHTMLRoute(new URL(homePageUrl))).toBeTruthy();
+ test('returns true for home route', () => {
+ expect(isHTMLRoute(new URL(homePageUrl))).toBeTruthy();
+ });
});
diff --git a/packages/venia-concept/src/ServiceWorker/Utilities/routeHandler.js b/packages/venia-concept/src/ServiceWorker/Utilities/routeHandler.js
index 40605154aa..53d21d20bb 100644
--- a/packages/venia-concept/src/ServiceWorker/Utilities/routeHandler.js
+++ b/packages/venia-concept/src/ServiceWorker/Utilities/routeHandler.js
@@ -1,11 +1,24 @@
/**
- * Checks if the given URL object belongs to the home route `/`.
+ * Checks if the given URL object belongs to the home route.
*
* @param {URL} url
*
* @returns {boolean}
*/
-export const isHomeRoute = url => url.pathname === '/';
+export const isHomeRoute = url => {
+ if (url.pathname === '/') {
+ return true;
+ }
+
+ // If store code is in the url, the home route will be url.com/view_code.
+ // A trailing / may or may not follow.
+ if (process.env.USE_STORE_CODE_IN_URL === 'true') {
+ return AVAILABLE_STORE_VIEWS.some(
+ ({ code }) =>
+ url.pathname === `/${code}/` || url.pathname === `/${code}`
+ );
+ }
+};
/**
* Checks if the given URL object belongs to the home route `/`
@@ -16,4 +29,4 @@ export const isHomeRoute = url => url.pathname === '/';
* @returns {boolean}
*/
export const isHTMLRoute = url =>
- isHomeRoute(url) || new RegExp('.html$').test(url.pathname);
+ isHomeRoute(url) || new RegExp('\\.html$').test(url.pathname);
diff --git a/packages/venia-concept/webpack.config.js b/packages/venia-concept/webpack.config.js
index d69f99fd12..b3839886ab 100644
--- a/packages/venia-concept/webpack.config.js
+++ b/packages/venia-concept/webpack.config.js
@@ -11,16 +11,13 @@ const { DefinePlugin } = require('webpack');
const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = async env => {
- const mediaUrl = await getMediaURL();
- const storeConfigData = await getStoreConfigData();
- const { availableStores } = await getAvailableStoresConfigData();
-
- global.MAGENTO_MEDIA_BACKEND_URL = mediaUrl;
- global.LOCALE = storeConfigData.locale.replace('_', '-');
- global.AVAILABLE_STORE_VIEWS = availableStores;
-
- const possibleTypes = await getPossibleTypes();
-
+ /**
+ * configureWebpack() returns a regular Webpack configuration object.
+ * You can customize the build by mutating the object here, as in
+ * this example. Since it's a regular Webpack configuration, the object
+ * supports the `module.noParse` option in Webpack, documented here:
+ * https://webpack.js.org/configuration/module/#modulenoparse
+ */
const config = await configureWebpack({
context: __dirname,
vendor: [
@@ -44,14 +41,20 @@ module.exports = async env => {
env
});
- /**
- * configureWebpack() returns a regular Webpack configuration object.
- * You can customize the build by mutating the object here, as in
- * this example. Since it's a regular Webpack configuration, the object
- * supports the `module.noParse` option in Webpack, documented here:
- * https://webpack.js.org/configuration/module/#modulenoparse
- */
- config.module.noParse = [/braintree\-web\-drop\-in/];
+ const mediaUrl = await getMediaURL();
+ const storeConfigData = await getStoreConfigData();
+ const { availableStores } = await getAvailableStoresConfigData();
+
+ global.MAGENTO_MEDIA_BACKEND_URL = mediaUrl;
+ global.LOCALE = storeConfigData.locale.replace('_', '-');
+ global.AVAILABLE_STORE_VIEWS = availableStores;
+
+ const possibleTypes = await getPossibleTypes();
+
+ config.module.noParse = [
+ /@adobe\/adobe\-client\-data\-layer/,
+ /braintree\-web\-drop\-in/
+ ];
config.plugins = [
...config.plugins,
new DefinePlugin({
diff --git a/packages/venia-ui/.storybook/webpack.config.js b/packages/venia-ui/.storybook/webpack.config.js
index ae8161662c..c6daa5d97b 100644
--- a/packages/venia-ui/.storybook/webpack.config.js
+++ b/packages/venia-ui/.storybook/webpack.config.js
@@ -17,7 +17,7 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
module.exports = async ({ config: storybookBaseConfig, mode }) => {
// The .env for running most of this project comes from venia-concept.
// This is not resilient and will need to change if venia-concept is renamed.
- const projectConfig = loadEnvironment(
+ const projectConfig = await loadEnvironment(
path.resolve(__dirname, '../../venia-concept')
);
diff --git a/packages/venia-ui/__mocks__/react-intl.js b/packages/venia-ui/__mocks__/react-intl.js
index cde43f7db1..52e525d05c 100644
--- a/packages/venia-ui/__mocks__/react-intl.js
+++ b/packages/venia-ui/__mocks__/react-intl.js
@@ -1,3 +1,4 @@
+const React = require('react');
const reactIntl = jest.requireActual('react-intl');
const messages = require('../i18n/en_US.json');
const intl = reactIntl.createIntl({
@@ -7,6 +8,6 @@ const intl = reactIntl.createIntl({
module.exports = {
...reactIntl,
- FormattedMessage: jest.fn(({ defaultMessage }) => defaultMessage),
+ FormattedMessage: props => ,
useIntl: jest.fn(() => intl)
};
diff --git a/packages/venia-ui/i18n/en_US.json b/packages/venia-ui/i18n/en_US.json
index dd4481b99f..c048eaaf79 100644
--- a/packages/venia-ui/i18n/en_US.json
+++ b/packages/venia-ui/i18n/en_US.json
@@ -23,6 +23,10 @@
"addressBook.headerText": "Change Shipping Information",
"addressBookPage.addAddressText": "Add an Address",
"addressBookPage.addressBookText": "Address Book",
+ "addressBookPage.addDialogTitle": "New Address",
+ "addressBookPage.editDialogTitle": "Edit Address",
+ "addressBookPage.makeDefaultAddress": "Make this my default address",
+ "addressBookPage.telephone": "Phone {telephone}",
"addressCard.defaultText": "Default",
"app.errorOffline": "You are offline. Some features may be unavailable.",
"app.errorUnexpected": "Sorry! An unexpected error occurred.",
@@ -39,7 +43,6 @@
"cartPage.couponCode": "Coupon Code",
"cartPage.emptyCart": "There are no items in your cart.",
"cartPage.heading": "Cart",
- "cartPage.signIn": "Sign In",
"cartPage.title": "Cart - {name}",
"cartTrigger.ariaLabel": "Toggle mini cart. You have {count} items in your cart.",
"category.dataFetchError": "Data Fetch Error",
@@ -48,8 +51,8 @@
"categoryLeaf.allLabel": "All {name}",
"categoryList.errorFetch": "Data Fetch Error: ",
"categoryList.noResults": "No child categories found.",
- "checkoutPage.additionalText": "You will also receive an email with the details and we will let you know when your order has shipped.",
"checkoutPage.accountSuccessfullyCreated": "Account successfully created.",
+ "checkoutPage.additionalText": "You will also receive an email with the details and we will let you know when your order has shipped.",
"checkoutPage.billingAddressSame": "Billing address same as shipping address",
"checkoutPage.checkout": "Checkout",
"checkoutPage.couponCode": "Enter Coupon Code",
@@ -64,25 +67,30 @@
"checkoutPage.giftOptions": "See Gift Options",
"checkoutPage.greeting": "Welcome {firstname}!",
"checkoutPage.guestCheckout": "Guest Checkout",
+ "checkoutPage.guestSignIn.backToCheckout": "Back to Checkout",
+ "checkoutPage.guestSignIn.header": "Account Sign-in",
"checkoutPage.itemsInYourOrder": " items in your order",
"checkoutPage.loadingPayment": "Loading Payment",
"checkoutPage.loadingPaymentInformation": "Fetching Payment Information",
- "checkoutPage.loginAndCheckoutFaster": "Login and Checkout Faster",
"checkoutPage.orderNumber": "Order Number: {orderNumber}",
"checkoutPage.orderSummary": "Order Summary",
"checkoutPage.paymentInformation": "Payment Information",
"checkoutPage.paymentInformationStep": "3. Payment Information",
"checkoutPage.paymentMethodStatus": "{selectedPaymentMethod} is not supported for editing.",
"checkoutPage.paymentSummary": "{cardType} ending in {lastFour}",
+ "checkoutPage.paymentLoadingError": "There was an error loading payments.",
+ "checkoutPage.refreshOrTryAgainLater": "Please refresh or try again later.",
"checkoutPage.placeOrder": "Place Order",
"checkoutPage.quantity": "Qty : {quantity}",
"checkoutPage.quickCheckout": "Quick Checkout When You Return",
"checkoutPage.returnToCart": "Return to Cart",
- "checkoutPage.reviewOrder": "Review Order",
"checkoutPage.reviewAndPlaceOrder": "Review and Place Order",
+ "checkoutPage.reviewOrder": "Review Order",
"checkoutPage.setAPasswordAndSave": "Set a password and save your information for next time in one easy step!",
"checkoutPage.shippingMethodStep": "2. Shipping Method",
"checkoutPage.showAllItems": "SHOW ALL ITEMS",
+ "checkoutPage.signInButton": "Sign In",
+ "checkoutPage.signInLabel": "Sign in for Express Checkout",
"checkoutPage.step0": "Loading Payment",
"checkoutPage.step1": "Checking Credit Card Information",
"checkoutPage.step2": "Checking Credit Card Information",
@@ -165,6 +173,7 @@
"Give Back": "Give Back",
"global.addButton": "Add",
"global.cancelButton": "Cancel",
+ "global.deleteButton": "Delete",
"global.changePassword": "Change Password",
"global.city": "City",
"global.confirmButton": "Confirm",
@@ -176,6 +185,7 @@
"global.free": "Free",
"global.home": "Home",
"global.lastName": "Last Name",
+ "global.middleName": "Middle Name",
"global.name": "Name",
"global.newPassword": "New Password",
"global.password": "Password",
@@ -197,8 +207,8 @@
"Live Chat": "Live Chat",
"loadingIndicator.message": "Fetching Data...",
"logo.title": "Venia",
- "magentoRoute.routeError": "That page could not be found. Please try again.",
"magentoRoute.internalError": "Something went wrong. Please try again.",
+ "magentoRoute.routeError": "That page could not be found. Please try again.",
"miniCart.checkout": "CHECKOUT",
"miniCart.editCartButton": "Edit Shopping Bag",
"miniCart.emptyMessage": "There are no items in your cart.",
@@ -219,6 +229,7 @@
"orderDetails.billingInformationLabel": "Billing Information",
"orderDetails.buyAgain": "Buy Again",
"orderDetails.discount": "Discount",
+ "orderDetails.noShippingInformation": "No shipping information",
"orderDetails.orderTotal": "Order Total",
"orderDetails.paymentMethodLabel": "Payment Method",
"orderDetails.printLabel": "Print Receipt",
@@ -230,9 +241,15 @@
"orderDetails.subtotal": "Subtotal",
"orderDetails.tax": "Tax",
"orderDetails.total": "Total",
- "orderDetails.trackingInformation": "{carrier} Tracking: {number}",
+ "orderDetails.trackingInformation": "Tracking number: {number}",
+ "orderDetails.waitingOnTracking": "Waiting for tracking information",
"orderHistoryPage.emptyDataMessage": "You don't have any orders yet.",
+ "orderHistoryPage.invalidOrderNumber": "Order \"{number}\" was not found.",
+ "orderHistoryPage.loadMore": "Load More",
+ "orderHistoryPage.pageInfo": "Showing {current} of {total}",
"orderHistoryPage.pageTitleText": "Order History",
+ "orderHistoryPage.search": "Search by Order Number",
+ "orderItems.itemsHeading": "Items",
"orderProgressBar.deliveredText": "Delivered",
"orderProgressBar.processingText": "Processing",
"orderProgressBar.readyToShipText": "Ready to ship",
@@ -246,9 +263,10 @@
"orderRow.shippedText": "Shipped",
"Our Story": "Our Story",
"pagination.firstPage": "move to the first page",
- "pagination.prevPage": "move to the previous page",
- "pagination.nextPage": "move to the next page",
"pagination.lastPage": "move to the last page",
+ "pagination.nextPage": "move to the next page",
+ "pagination.prevPage": "move to the previous page",
+ "postcode.label": "ZIP / Postal Code",
"priceAdjustments.couponCode": "Enter Coupon Code",
"priceAdjustments.giftOptions": "See Gift Options",
"priceAdjustments.shippingMethod": "Estimate your Shipping",
@@ -286,7 +304,6 @@
"productOptions.selectedLabel": "Selected {label}:",
"productQuantity.label": "product's quantity",
"productSort.sortButton": "Sort",
- "postcode.label": "ZIP / Postal Code",
"quantity.buttonDecrement": "Decrease Quantity",
"quantity.buttonIncrement": "Increase Quantity",
"quantity.input": "Item Quantity",
@@ -296,13 +313,13 @@
"resetPassword.invalidTokenMessage": "Uh oh, something went wrong. Check the link or try again.",
"resetPassword.newPasswordText": "New Password",
"resetPassword.pageTitleText": "Reset Password",
- "resetPassword.savePassword": "Save Password",
"resetPassword.savedPasswordText": "Your new password has been saved.",
+ "resetPassword.savePassword": "Save Password",
"resetPassword.successMessage": "Your new password has been saved. Please use this password to sign into your Account.",
"Returns": "Returns",
"savedPaymentsPage.addButtonText": "Add a credit card",
- "savedPaymentsPage.subHeading": "Credit Cards saved here will be available during checkout.",
- "savedPaymentsPage.title": "Saved Payments - {store_name}",
+ "savedPaymentsPage.noSavedPayments": "You have no saved payments.",
+ "savedPaymentsPage.title": "Saved Payments",
"searchBar.heading": "Product Suggestions",
"searchBar.label": " in {label}",
"searchPage.filterButton": "Filter",
@@ -321,14 +338,14 @@
"shippingInformation.editTitle": "1. Shipping Information",
"shippingInformation.loading": "Fetching Shipping Information...",
"shippingMethod.continueToNextStep": "Continue to Payment Information",
- "shippingMethod.loading": "Loading shipping methods...",
"shippingMethod.heading": "Shipping Method",
+ "shippingMethod.loading": "Loading shipping methods...",
"shippingMethods.estimateButton": "I want to estimate my shipping",
"shippingMethods.message": "For shipping estimates before proceeding to checkout, please provide the Country, State, and ZIP for the destination of your order.",
"shippingMethods.prompt": "Shipping Methods",
+ "shippingRadios.errorLoading": "Error loading shipping methods. Please ensure a shipping address is set and try again.",
"shippingSummary.estimatedShipping": "Estimated Shipping",
"shippingSummary.shipping": "Shipping",
- "shippingRadios.errorLoading": "Error loading shipping methods. Please ensure a shipping address is set and try again.",
"Sign In": "Sign In",
"signIn.createAccountText": "Create an Account",
"signIn.emailAddressText": "Email address",
@@ -341,6 +358,8 @@
"sortItem.priceDesc": "Price: High to Low",
"sortItem.relevance": "Best Match",
"stockStatusMessage.message": "An item in your cart is currently out-of-stock and must be removed in order to Checkout.",
+ "storedPayments.creditCard": "Credit Card",
+ "storedPayments.delete": "Delete",
"taxSummary.estimatedTax": "Estimated Tax",
"taxSummary.tax": "Tax",
"validation.hasLengthAtLeast": "Must contain at least {value} character(s).",
@@ -357,16 +376,16 @@
"wishlist.emptyListText": "There are currently no items in this list",
"wishlist.privateText": "Private",
"wishlist.publicText": "Public",
+ "wishlistConfirmRemoveProductDialog.confirmationPrompt": "Are you sure you want to delete this product from the list?",
+ "wishlistConfirmRemoveProductDialog.confirmButton": "Delete",
+ "wishlistConfirmRemoveProductDialog.errorMessage": "There was an error deleting this product. Please try again later.",
+ "wishlistConfirmRemoveProductDialog.title": "Remove Product from Wishlist",
"wishlistItem.addToCart": "Add to Cart",
"wishlistItem.addToCartError": "Something went wrong. Please refresh and try again.",
"wishlistMoreActionsDialog.copy": "Copy to",
"wishlistMoreActionsDialog.delete": "Remove",
"wishlistMoreActionsDialog.move": "Move to",
"wishlistMoreActionsDialog.title": "Actions",
- "wishlistConfirmRemoveProductDialog.confirmButton": "Delete",
- "wishlistConfirmRemoveProductDialog.confirmationPrompt": "Are you sure you want to delete this product from the list?",
- "wishlistConfirmRemoveProductDialog.errorMessage": "There was an error deleting this product. Please try again later.",
- "wishlistConfirmRemoveProductDialog.title": "Remove Product from Wishlist",
"wishlistPage.disabledMessage": "Sorry, this feature has been disabled.",
"wishlistPage.fetchErrorMessage": "Something went wrong. Please refresh and try again.",
"wishlistPage.headingText": "Favorites Lists",
diff --git a/packages/venia-ui/lib/RootComponents/CMS/__tests__/cms.spec.js b/packages/venia-ui/lib/RootComponents/CMS/__tests__/cms.spec.js
index 3434ff93aa..b754d1e159 100644
--- a/packages/venia-ui/lib/RootComponents/CMS/__tests__/cms.spec.js
+++ b/packages/venia-ui/lib/RootComponents/CMS/__tests__/cms.spec.js
@@ -73,6 +73,9 @@ test('page is set to loading when checking the network for updates', () => {
cmsPage: {
url_key: 'cached_page',
content: 'Cached Page.'
+ },
+ storeConfig: {
+ root_category_id: 2
}
},
error: false,
@@ -103,6 +106,9 @@ test('render CategoryList when default content is present', () => {
cmsPage: {
url_key: 'homepage',
content: 'CMS homepage content goes here.'
+ },
+ storeConfig: {
+ root_category_id: 2
}
},
error: false,
@@ -126,6 +132,9 @@ test('render RichContent when default content is not present', () => {
content_heading: 'This is a rich content heading',
content:
'This is rich content
'
+ },
+ storeConfig: {
+ root_category_id: 2
}
},
error: false,
@@ -153,6 +162,9 @@ test('render meta information based on meta data from GraphQL', () => {
title: 'Test Title',
meta_title: 'Test Meta Title',
meta_description: 'Test Meta Description'
+ },
+ storeConfig: {
+ root_category_id: 2
}
},
error: false,
diff --git a/packages/venia-ui/lib/RootComponents/CMS/cms.js b/packages/venia-ui/lib/RootComponents/CMS/cms.js
index 7a63ad169a..326318778b 100644
--- a/packages/venia-ui/lib/RootComponents/CMS/cms.js
+++ b/packages/venia-ui/lib/RootComponents/CMS/cms.js
@@ -14,7 +14,12 @@ const CMSPage = props => {
const { id } = props;
const talonProps = useCmsPage({ id });
- const { cmsPage, hasContent, shouldShowLoadingIndicator } = talonProps;
+ const {
+ cmsPage,
+ hasContent,
+ rootCategoryId,
+ shouldShowLoadingIndicator
+ } = talonProps;
const { formatMessage } = useIntl();
if (shouldShowLoadingIndicator) {
@@ -50,13 +55,14 @@ const CMSPage = props => {
);
}
+ // Fallback to a category list if there is no cms content.
return (
);
};
diff --git a/packages/venia-ui/lib/RootComponents/Category/__tests__/__snapshots__/category.spec.js.snap b/packages/venia-ui/lib/RootComponents/Category/__tests__/__snapshots__/category.spec.js.snap
index 982d05a501..7eef349f0f 100644
--- a/packages/venia-ui/lib/RootComponents/Category/__tests__/__snapshots__/category.spec.js.snap
+++ b/packages/venia-ui/lib/RootComponents/Category/__tests__/__snapshots__/category.spec.js.snap
@@ -1,6 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`it renders a loading indicator when appropriate 1`] = `
+exports[`Category Root Component error view does not render when data is present 1`] = `
+Array [
+ "Meta",
+ ,
+]
+`;
+
+exports[`Category Root Component error view renders when data is not present and not loading 1`] = `
+
+
+
+`;
+
+exports[`Category Root Component loading indicator does not render when data is present 1`] = `
+Array [
+ "Meta",
+ ,
+]
+`;
+
+exports[`Category Root Component loading indicator renders when data is not present 1`] = `
- Fetching Data...
+
`;
-exports[`it shows error when appropriate 1`] = `
-
- Data Fetch Error
-
-`;
-
-exports[`renders the correct tree 1`] = `
+exports[`Category Root Component renders the correct tree 1`] = `
Array [
"Meta",
{
- useCategory.mockReturnValueOnce(talonProps);
- const tree = createTestInstance( );
- expect(tree.toJSON()).toMatchSnapshot();
-});
+describe('Category Root Component', () => {
+ test('renders the correct tree', () => {
+ useCategory.mockReturnValueOnce(talonProps);
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
-test('it renders a loading indicator when appropriate', () => {
- useCategory.mockReturnValueOnce({
- ...talonProps,
- loading: true
+ describe('loading indicator', () => {
+ test('does not render when data is present', () => {
+ useCategory.mockReturnValueOnce({
+ ...talonProps,
+ loading: true
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ test('renders when data is not present', () => {
+ useCategory.mockReturnValueOnce({
+ ...talonProps,
+ loading: true,
+ categoryData: undefined
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
});
- const tree = createTestInstance( );
- expect(tree.toJSON()).toMatchSnapshot();
-});
-test('it shows error when appropriate', () => {
- useCategory.mockReturnValueOnce({
- ...talonProps,
- error: true,
- loading: false
+ describe('error view', () => {
+ test('does not render when data is present', () => {
+ useCategory.mockReturnValueOnce({
+ ...talonProps,
+ error: true
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+
+ test('renders when data is not present and not loading', () => {
+ useCategory.mockReturnValueOnce({
+ ...talonProps,
+ error: true,
+ loading: false,
+ categoryData: undefined
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
});
- const tree = createTestInstance( );
- expect(tree.toJSON()).toMatchSnapshot();
});
diff --git a/packages/venia-ui/lib/RootComponents/Category/category.js b/packages/venia-ui/lib/RootComponents/Category/category.js
index e5f915c62b..d883ec8ade 100644
--- a/packages/venia-ui/lib/RootComponents/Category/category.js
+++ b/packages/venia-ui/lib/RootComponents/Category/category.js
@@ -32,24 +32,26 @@ const Category = props => {
const classes = mergeClasses(defaultClasses, props.classes);
- // Show the loading indicator until data has been fetched.
- if (loading) {
- return fullPageLoadingIndicator;
- }
-
- if (error && pageControl.currentPage === 1) {
- if (process.env.NODE_ENV !== 'production') {
- console.error(error);
+ if (!categoryData) {
+ // Show the loading indicator until data has been fetched.
+ if (loading) {
+ return fullPageLoadingIndicator;
}
- return (
-
-
-
- );
+ if (error && pageControl.currentPage === 1) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.error(error);
+ }
+
+ return (
+
+
+
+ );
+ }
}
return (
diff --git a/packages/venia-ui/lib/RootComponents/Product/__tests__/__snapshots__/product.spec.js.snap b/packages/venia-ui/lib/RootComponents/Product/__tests__/__snapshots__/product.spec.js.snap
index e04d2f1846..f7b2e1b884 100644
--- a/packages/venia-ui/lib/RootComponents/Product/__tests__/__snapshots__/product.spec.js.snap
+++ b/packages/venia-ui/lib/RootComponents/Product/__tests__/__snapshots__/product.spec.js.snap
@@ -196,7 +196,7 @@ exports[`renders product page correctly when error and product data exist 1`] =
exports[`renders product page failed when error data 1`] = `
-
diff --git a/packages/venia-ui/lib/RootComponents/Product/product.gql.js b/packages/venia-ui/lib/RootComponents/Product/product.gql.js
deleted file mode 100644
index 453bc95a97..0000000000
--- a/packages/venia-ui/lib/RootComponents/Product/product.gql.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import { gql } from '@apollo/client';
-
-export const GET_PRODUCT_DETAIL_QUERY = gql`
- query getProductDetailForProductPage($urlKey: String!) {
- products(filter: { url_key: { eq: $urlKey } }) {
- items {
- # Once graphql-ce/1027 is resolved, use a ProductDetails fragment
- # here instead.
- __typename
- categories {
- id
- breadcrumbs {
- category_id
- }
- }
- description {
- html
- }
- id
- media_gallery_entries {
- id
- label
- position
- disabled
- file
- }
- meta_description
- name
- price {
- regularPrice {
- amount {
- currency
- value
- }
- }
- }
- sku
- small_image {
- url
- }
- url_key
- ... on ConfigurableProduct {
- configurable_options {
- attribute_code
- attribute_id
- id
- label
- values {
- default_label
- label
- store_label
- use_default_value
- value_index
- swatch_data {
- ... on ImageSwatchData {
- thumbnail
- }
- value
- }
- }
- }
- variants {
- attributes {
- code
- value_index
- }
- product {
- id
- media_gallery_entries {
- id
- disabled
- file
- label
- position
- }
- sku
- stock_status
- price {
- regularPrice {
- amount {
- currency
- value
- }
- }
- }
- }
- }
- }
- }
- }
- }
-`;
diff --git a/packages/venia-ui/lib/RootComponents/Product/product.js b/packages/venia-ui/lib/RootComponents/Product/product.js
index e935931860..76686f8798 100644
--- a/packages/venia-ui/lib/RootComponents/Product/product.js
+++ b/packages/venia-ui/lib/RootComponents/Product/product.js
@@ -15,41 +15,38 @@ import mapProduct from '../../util/mapProduct';
* https://github.com/magento/graphql-ce/issues/86
* TODO: Replace with a single product query when possible.
*/
-import { GET_PRODUCT_DETAIL_QUERY } from './product.gql';
const Product = () => {
const talonProps = useProduct({
mapProduct,
- queries: {
- getProductQuery: GET_PRODUCT_DETAIL_QUERY
- },
urlKey: getUrlKey()
});
const { error, loading, product } = talonProps;
- if (loading && !product) return fullPageLoadingIndicator;
- if (error && !product)
- return (
-
-
-
- );
-
if (!product) {
- return (
-
-
-
- );
+ if (loading) return fullPageLoadingIndicator;
+ else if (error) {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
}
// Note: STORE_NAME is injected by Webpack at build time.
diff --git a/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/accountInformationPage.spec.js.snap b/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/accountInformationPage.spec.js.snap
index b5eb8f7b27..726f926bf5 100644
--- a/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/accountInformationPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/accountInformationPage.spec.js.snap
@@ -14,12 +14,18 @@ exports[`renders form error 1`] = `
- Account Information
+
- Something went wrong. Please refresh and try again.
+
`;
diff --git a/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editForm.spec.js.snap b/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editForm.spec.js.snap
index 4a7a1c86be..eb7336296d 100644
--- a/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editForm.spec.js.snap
+++ b/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editForm.spec.js.snap
@@ -228,7 +228,10 @@ Array [
- Change Password
+
,
diff --git a/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editModal.spec.js.snap b/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editModal.spec.js.snap
index 9dee6985ff..9172bdb036 100644
--- a/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editModal.spec.js.snap
+++ b/packages/venia-ui/lib/components/AccountInformationPage/__tests__/__snapshots__/editModal.spec.js.snap
@@ -85,7 +85,10 @@ exports[`it disables the submit button while loading 1`] = `
- Cancel
+
- Save
+
@@ -192,7 +198,10 @@ exports[`it renders correctly 1`] = `
- Cancel
+
- Save
+
diff --git a/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenuItems.spec.js.snap b/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenuItems.spec.js.snap
index c55f6fadd5..10c320e3e5 100644
--- a/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenuItems.spec.js.snap
+++ b/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenuItems.spec.js.snap
@@ -13,7 +13,10 @@ exports[`it renders correctly 1`] = `
onClick={[MockFunction handleSignOut]}
type="button"
>
- Sign Out
+
`;
diff --git a/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addEditDialog.spec.js.snap b/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addEditDialog.spec.js.snap
new file mode 100644
index 0000000000..6eb303a06e
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addEditDialog.spec.js.snap
@@ -0,0 +1,1707 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Edit Mode renders correctly 1`] = `
+
+
+
+
+
+
+ First Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Middle Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Last Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ City
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Phone Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Make this my default address
+
+
+
+
+
+
+`;
+
+exports[`Edit Mode renders correctly with errors 1`] = `
+
+
+
+
+
+
+ First Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Middle Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Last Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ City
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Phone Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Make this my default address
+
+
+
+
+
+
+`;
+
+exports[`renders correctly 1`] = `
+
+
+
+
+
+
+ First Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Middle Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Last Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ City
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Phone Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Make this my default address
+
+
+
+
+
+
+`;
+
+exports[`renders correctly with errors 1`] = `
+
+
+
+
+
+
+ First Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Middle Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Last Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Street Address 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ City
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Phone Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Make this my default address
+
+
+
+
+
+
+`;
diff --git a/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addressBookPage.spec.js.snap b/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addressBookPage.spec.js.snap
index 555dc19a9b..7abc274a47 100644
--- a/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addressBookPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addressBookPage.spec.js.snap
@@ -13,6 +13,51 @@ exports[`renders correctly when there are existing addresses 1`] = `
+
+
+
- Add an Address
+
-
-
-
+
`;
@@ -85,10 +133,145 @@ exports[`renders correctly when there are no existing addresses 1`] = `
- Add an Address
+
+
+
+`;
+
+exports[`renders delete confirmation on address that is being deleted 1`] = `
+
+ Title
+
+ Address Book
+
+
+
+
+`;
+
+exports[`renders loading indicator 1`] = `
+
+
+
+
+
`;
diff --git a/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addressCard.spec.js.snap b/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addressCard.spec.js.snap
new file mode 100644
index 0000000000..7f0944f9bd
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/__tests__/__snapshots__/addressCard.spec.js.snap
@@ -0,0 +1,610 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders a default address 1`] = `
+
+
+
+
+
+
+ Philip Fry
+
+
+ 111 57th Street
+
+
+ Suite 1000
+
+
+ New New York, New York 10019
+
+
+ United States
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders a non-default address 1`] = `
+
+
+
+ Philip Fry
+
+
+ 111 57th Street
+
+
+ Suite 1000
+
+
+ New New York, New York 10019
+
+
+ United States
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders an address with a middle name 1`] = `
+
+
+
+
+
+
+ Philip MIDDLE Fry
+
+
+ 111 57th Street
+
+
+ Suite 1000
+
+
+ New New York, New York 10019
+
+
+ United States
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders delete confirmation if isConfirmingDelete is true 1`] = `
+
+
+
+
+
+
+ Philip Fry
+
+
+ 111 57th Street
+
+
+ Suite 1000
+
+
+ New New York, New York 10019
+
+
+ United States
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders disabled delete confirmation if isConfirmingDelete and isDeletingCustomerAddress are true 1`] = `
+
+
+
+
+
+
+ Philip Fry
+
+
+ 111 57th Street
+
+
+ Suite 1000
+
+
+ New New York, New York 10019
+
+
+ United States
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/venia-ui/lib/components/AddressBookPage/__tests__/addEditDialog.spec.js b/packages/venia-ui/lib/components/AddressBookPage/__tests__/addEditDialog.spec.js
new file mode 100644
index 0000000000..17bb0bc26b
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/__tests__/addEditDialog.spec.js
@@ -0,0 +1,93 @@
+import React from 'react';
+import { createTestInstance } from '@magento/peregrine';
+
+import AddEditDialog from '../addEditDialog';
+
+jest.mock('@magento/venia-ui/lib/classify');
+jest.mock('../../Country', () => 'Country');
+jest.mock('../../Dialog', () => 'Dialog');
+jest.mock('../../FormError', () => 'FormError');
+jest.mock('../../Postcode', () => 'Postcode');
+jest.mock('../../Region', () => 'Region');
+
+const props = {
+ classes: {},
+ formErrors: new Map([]),
+ isBusy: false,
+ isEditMode: false,
+ isOpen: true,
+ onCancel: jest.fn().mockName('onCancel'),
+ onConfirm: jest.fn().mockName('onConfirm')
+};
+
+it('renders correctly', () => {
+ // Act.
+ const instance = createTestInstance( );
+
+ // Assert.
+ expect(instance.toJSON()).toMatchSnapshot();
+});
+
+it('renders correctly with errors', () => {
+ // Arrange.
+ const testProps = {
+ ...props,
+ formErrors: new Map([
+ ['createCustomerAddressMutation', 'Unit Test Error 1'],
+ ['updateCustomerAddressMutation', 'Unit Test Error 2']
+ ])
+ };
+
+ // Act.
+ const instance = createTestInstance( );
+
+ // Assert.
+ expect(instance.toJSON()).toMatchSnapshot();
+});
+
+describe('Edit Mode', () => {
+ const editModeProps = {
+ ...props,
+ isEditMode: true,
+ activeEditAddress: {
+ region: {
+ region: 'Arizona',
+ region_code: 'AZ'
+ },
+ country_code: 'US',
+ street: ['123 Main Street'],
+ telephone: '7777777777',
+ postcode: '77777',
+ city: 'Phoenix',
+ firstname: 'Bob',
+ lastname: 'Loblaw'
+ }
+ };
+
+ it('renders correctly', () => {
+ // Act.
+ const instance = createTestInstance(
+
+ );
+
+ // Assert.
+ expect(instance.toJSON()).toMatchSnapshot();
+ });
+
+ it('renders correctly with errors', () => {
+ // Arrange.
+ const testProps = {
+ ...editModeProps,
+ formErrors: new Map([
+ ['createCustomerAddressMutation', 'Unit Test Error 1'],
+ ['updateCustomerAddressMutation', 'Unit Test Error 2']
+ ])
+ };
+
+ // Act.
+ const instance = createTestInstance( );
+
+ // Assert.
+ expect(instance.toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/packages/venia-ui/lib/components/AddressBookPage/__tests__/addressBookPage.spec.js b/packages/venia-ui/lib/components/AddressBookPage/__tests__/addressBookPage.spec.js
index fe3e38feb0..041bdad374 100644
--- a/packages/venia-ui/lib/components/AddressBookPage/__tests__/addressBookPage.spec.js
+++ b/packages/venia-ui/lib/components/AddressBookPage/__tests__/addressBookPage.spec.js
@@ -7,7 +7,6 @@ import AddressBookPage from '../addressBookPage';
jest.mock('@magento/venia-ui/lib/classify');
jest.mock('../../Head', () => ({ Title: () => 'Title' }));
-jest.mock('../../CheckoutPage/AddressBook/addressCard', () => 'AddressCard');
jest.mock('../../Icon', () => 'Icon');
jest.mock(
'@magento/peregrine/lib/talons/AddressBookPage/useAddressBookPage',
@@ -17,11 +16,30 @@ jest.mock(
};
}
);
+jest.mock('../addEditDialog', () => 'AddEditDialog');
+jest.mock('../addressCard', () => 'AddressCard');
const props = {};
const talonProps = {
+ confirmDeleteAddressId: null,
+ countryDisplayNameMap: new Map([['US', 'United States']]),
customerAddresses: [],
- handleAddAddress: jest.fn().mockName('handleAddAddress')
+ formErrors: new Map([]),
+ formProps: null,
+ handleAddAddress: jest.fn().mockName('handleAddAddress'),
+ handleCancelDeleteAddress: jest.fn().mockName('handleCancelDeleteAddress'),
+ handleCancelDialog: jest.fn().mockName('handleCancelDialog'),
+ handleConfirmDeleteAddress: jest
+ .fn()
+ .mockName('handleConfirmDeleteAddress'),
+ handleConfirmDialog: jest.fn().mockName('handleConfirmDialog'),
+ handleDeleteAddress: jest.fn().mockName('handleDeleteAddress'),
+ handleEditAddress: jest.fn().mockName('handleEditAddress'),
+ isDeletingCustomerAddress: false,
+ isDialogBusy: false,
+ isDialogEditMode: false,
+ isDialogOpen: false,
+ isLoading: false
};
it('renders correctly when there are no existing addresses', () => {
@@ -35,11 +53,44 @@ it('renders correctly when there are no existing addresses', () => {
expect(instance.toJSON()).toMatchSnapshot();
});
+it('renders loading indicator', () => {
+ useAddressBookPage.mockReturnValueOnce({ ...talonProps, isLoading: true });
+
+ const instance = createTestInstance( );
+
+ expect(instance.toJSON()).toMatchSnapshot();
+});
+
it('renders correctly when there are existing addresses', () => {
// Arrange.
const myTalonProps = {
...talonProps,
- customerAddresses: ['a', 'b', 'c']
+ customerAddresses: [
+ { id: 'a', country_code: 'US' },
+ { id: 'b', country_code: 'US', default_shipping: true },
+ { id: 'c', country_code: 'FR' }
+ ]
+ };
+ useAddressBookPage.mockReturnValueOnce(myTalonProps);
+
+ // Act.
+ const instance = createTestInstance( );
+
+ // Assert.
+ expect(instance.toJSON()).toMatchSnapshot();
+});
+
+it('renders delete confirmation on address that is being deleted', () => {
+ // Arrange.
+ const myTalonProps = {
+ ...talonProps,
+ customerAddresses: [
+ { id: 'a', country_code: 'US' },
+ { id: 'b', country_code: 'US', default_shipping: true },
+ { id: 'c', country_code: 'FR' }
+ ],
+ isDeletingCustomerAddress: true,
+ confirmDeleteAddressId: 'a'
};
useAddressBookPage.mockReturnValueOnce(myTalonProps);
diff --git a/packages/venia-ui/lib/components/AddressBookPage/__tests__/addressCard.spec.js b/packages/venia-ui/lib/components/AddressBookPage/__tests__/addressCard.spec.js
new file mode 100644
index 0000000000..9f4ebca8e0
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/__tests__/addressCard.spec.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import { createTestInstance } from '@magento/peregrine';
+import AddressCard from '../addressCard';
+
+jest.mock('@magento/venia-ui/lib/classify');
+
+const mockAddress = {
+ city: 'New New York',
+ country_code: 'US',
+ default_shipping: true,
+ firstname: 'Philip',
+ lastname: 'Fry',
+ postcode: '10019',
+ region: { region: 'New York' },
+ street: ['111 57th Street', 'Suite 1000'],
+ telephone: '+12345678909'
+};
+
+const props = {
+ address: mockAddress,
+ countryName: 'United States',
+ isConfirmingDelete: false,
+ isDeletingCustomerAddress: false,
+ onCancelDelete: jest.fn().mockName('onCancelDelete'),
+ onConfirmDelete: jest.fn().mockName('onConfirmDelete'),
+ onEdit: jest.fn().mockName('onEdit'),
+ onDelete: jest.fn().mockName('onDelete')
+};
+
+test('renders a default address', () => {
+ const tree = createTestInstance( );
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders a non-default address', () => {
+ const tree = createTestInstance(
+
+ );
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders an address with a middle name', () => {
+ // Arrange.
+ const myAddress = {
+ ...mockAddress,
+ middlename: 'MIDDLE'
+ };
+
+ // Act.
+ const tree = createTestInstance(
+
+ );
+
+ // Assert.
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders delete confirmation if isConfirmingDelete is true', () => {
+ const tree = createTestInstance(
+
+ );
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders disabled delete confirmation if isConfirmingDelete and isDeletingCustomerAddress are true', () => {
+ const tree = createTestInstance(
+
+ );
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/AddressBookPage/addEditDialog.css b/packages/venia-ui/lib/components/AddressBookPage/addEditDialog.css
new file mode 100644
index 0000000000..c40c1c87f7
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/addEditDialog.css
@@ -0,0 +1,24 @@
+.root {
+ display: grid;
+ gap: 0.5rem 1.5rem;
+ grid-template-columns: 1fr;
+}
+
+/*
+ * Desktop-specific styles.
+ */
+@media (min-width: 961px) {
+ .root {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .fullWidthField {
+ grid-column-end: span 2;
+ }
+
+ .lastname,
+ .country,
+ .default_address_check {
+ composes: fullWidthField;
+ }
+}
diff --git a/packages/venia-ui/lib/components/AddressBookPage/addEditDialog.js b/packages/venia-ui/lib/components/AddressBookPage/addEditDialog.js
new file mode 100644
index 0000000000..5837fe26d6
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/addEditDialog.js
@@ -0,0 +1,185 @@
+import React from 'react';
+import { bool, func, object, shape, string } from 'prop-types';
+import { useIntl } from 'react-intl';
+
+import { mergeClasses } from '@magento/venia-ui/lib/classify';
+import { isRequired } from '@magento/venia-ui/lib/util/formValidators';
+
+import Checkbox from '@magento/venia-ui/lib/components/Checkbox';
+import Country from '@magento/venia-ui/lib/components/Country';
+import Dialog from '@magento/venia-ui/lib/components/Dialog';
+import Field from '@magento/venia-ui/lib/components/Field';
+import FormError from '@magento/venia-ui/lib/components/FormError';
+import Postcode from '@magento/venia-ui/lib/components/Postcode';
+import Region from '@magento/venia-ui/lib/components/Region';
+import TextInput from '@magento/venia-ui/lib/components/TextInput';
+import defaultClasses from './addEditDialog.css';
+
+const AddEditDialog = props => {
+ const {
+ formErrors,
+ formProps,
+ isBusy,
+ isEditMode,
+ isOpen,
+ onCancel,
+ onConfirm
+ } = props;
+
+ const { formatMessage } = useIntl();
+
+ const classes = mergeClasses(defaultClasses, props.classes);
+
+ let formatTitleArgs;
+ if (isEditMode) {
+ formatTitleArgs = {
+ id: 'addressBookPage.editDialogTitle',
+ defaultMessage: 'Edit Address'
+ };
+ } else {
+ formatTitleArgs = {
+ id: 'addressBookPage.addDialogTitle',
+ defaultMessage: 'New Address'
+ };
+ }
+ const title = formatMessage(formatTitleArgs);
+
+ const firstNameLabel = formatMessage({
+ id: 'global.firstName',
+ defaultMessage: 'First Name'
+ });
+ const middleNameLabel = formatMessage({
+ id: 'global.middleName',
+ defaultMessage: 'Middle Name'
+ });
+ const lastNameLabel = formatMessage({
+ id: 'global.lastName',
+ defaultMessage: 'Last Name'
+ });
+ const street1Label = formatMessage({
+ id: 'global.streetAddress',
+ defaultMessage: 'Street Address'
+ });
+ const street2Label = formatMessage({
+ id: 'global.streetAddress2',
+ defaultMessage: 'Street Address 2'
+ });
+ const cityLabel = formatMessage({
+ id: 'global.city',
+ defaultMessage: 'City'
+ });
+ const telephoneLabel = formatMessage({
+ id: 'global.phoneNumber',
+ defaultMessage: 'Phone Number'
+ });
+ const defaultAddressCheckLabel = formatMessage({
+ id: 'addressBookPage.makeDefaultAddress',
+ defaultMessage: 'Make this my default address'
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AddEditDialog;
+
+AddEditDialog.propTypes = {
+ classes: shape({
+ root: string,
+ city: string,
+ country: string,
+ default_address_check: string,
+ errorContainer: string,
+ firstname: string,
+ lastname: string,
+ middlename: string,
+ postcode: string,
+ region: string,
+ street1: string,
+ street2: string,
+ telephone: string
+ }),
+ formErrors: object,
+ isEditMode: bool,
+ isOpen: bool,
+ onCancel: func,
+ onConfirm: func
+};
diff --git a/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.css b/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.css
index b1fee85289..9802a4454a 100644
--- a/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.css
+++ b/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.css
@@ -28,20 +28,28 @@
transition: border-color 384ms var(--venia-global-anim-standard);
color: rgb(var(--venia-brand-color-1-700));
}
+
.addButton:focus {
outline: none;
box-shadow: -6px 6px rgb(var(--venia-global-color-blue-700) / 0.3);
}
+
.addButton:hover {
border-color: rgb(var(--venia-brand-color-1-600));
}
-@media (max-width: 960px) {
+@media (max-width: 1024px) {
.root {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
+ .content {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+@media (max-width: 640px) {
.content {
grid-template-columns: 1fr;
}
diff --git a/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.js b/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.js
index ab4dc2a16d..5a99ca3699 100644
--- a/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.js
+++ b/packages/venia-ui/lib/components/AddressBookPage/addressBookPage.js
@@ -4,21 +4,36 @@ import { PlusSquare } from 'react-feather';
import { useAddressBookPage } from '@magento/peregrine/lib/talons/AddressBookPage/useAddressBookPage';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
+import { Title } from '@magento/venia-ui/lib/components/Head';
+import Icon from '@magento/venia-ui/lib/components/Icon';
+import LinkButton from '@magento/venia-ui/lib/components/LinkButton';
+import { fullPageLoadingIndicator } from '@magento/venia-ui/lib/components/LoadingIndicator';
-import { GET_CUSTOMER_ADDRESSES } from '../CheckoutPage/AddressBook/addressBook.gql';
-import AddressCard from '../CheckoutPage/AddressBook/addressCard';
-import Icon from '../Icon';
-import LinkButton from '../LinkButton';
-import { Title } from '../Head';
+import AddressCard from './addressCard';
+import AddEditDialog from './addEditDialog';
import defaultClasses from './addressBookPage.css';
const AddressBookPage = props => {
- const talonProps = useAddressBookPage({
- queries: {
- getCustomerAddressesQuery: GET_CUSTOMER_ADDRESSES
- }
- });
- const { customerAddresses, handleAddAddress } = talonProps;
+ const talonProps = useAddressBookPage();
+ const {
+ confirmDeleteAddressId,
+ countryDisplayNameMap,
+ customerAddresses,
+ formErrors,
+ formProps,
+ handleAddAddress,
+ handleCancelDeleteAddress,
+ handleCancelDialog,
+ handleConfirmDeleteAddress,
+ handleConfirmDialog,
+ handleDeleteAddress,
+ handleEditAddress,
+ isDeletingCustomerAddress,
+ isDialogBusy,
+ isDialogEditMode,
+ isDialogOpen,
+ isLoading
+ } = talonProps;
const { formatMessage } = useIntl();
const classes = mergeClasses(defaultClasses, props.classes);
@@ -28,10 +43,52 @@ const AddressBookPage = props => {
defaultMessage: 'Address Book'
});
const addressBookElements = useMemo(() => {
- return customerAddresses.map(addressEntry => (
-
- ));
- }, [customerAddresses]);
+ const defaultToBeginning = (address1, address2) => {
+ if (address1.default_shipping) return -1;
+ if (address2.default_shipping) return 1;
+ return 0;
+ };
+
+ return Array.from(customerAddresses)
+ .sort(defaultToBeginning)
+ .map(addressEntry => {
+ const countryName = countryDisplayNameMap.get(
+ addressEntry.country_code
+ );
+
+ const boundEdit = () => handleEditAddress(addressEntry);
+ const boundDelete = () => handleDeleteAddress(addressEntry.id);
+ const isConfirmingDelete =
+ confirmDeleteAddressId === addressEntry.id;
+
+ return (
+
+ );
+ });
+ }, [
+ confirmDeleteAddressId,
+ countryDisplayNameMap,
+ customerAddresses,
+ handleCancelDeleteAddress,
+ handleConfirmDeleteAddress,
+ handleDeleteAddress,
+ handleEditAddress,
+ isDeletingCustomerAddress
+ ]);
+
+ if (isLoading) {
+ return fullPageLoadingIndicator;
+ }
// STORE_NAME is injected by Webpack at build time.
const title = `${PAGE_TITLE} - ${STORE_NAME}`;
@@ -40,6 +97,7 @@ const AddressBookPage = props => {
{title}
{PAGE_TITLE}
+ {addressBookElements}
{
/>
- {addressBookElements}
+
);
};
diff --git a/packages/venia-ui/lib/components/AddressBookPage/addressCard.css b/packages/venia-ui/lib/components/AddressBookPage/addressCard.css
new file mode 100644
index 0000000000..57f54d4383
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/addressCard.css
@@ -0,0 +1,121 @@
+.root {
+ align-content: flex-start;
+ border: 2px solid rgb(var(--venia-global-color-gray-400));
+ border-radius: 0.375rem;
+ display: grid;
+ grid-template-columns: 1fr max-content;
+ padding: 1.25rem 2rem;
+ position: relative;
+}
+
+.root_updated {
+ composes: root;
+ animation: flash var(--venia-global-anim-bounce) 640ms 2;
+}
+
+.confirmDeleteContainer {
+ align-items: center;
+ background-color: rgba(255, 255, 255, 0.9);
+ display: grid;
+ gap: 2rem;
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
+ height: 100%;
+ justify-items: center;
+ left: 0;
+ padding: 1.25rem 2rem;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+.confirmDeleteButton {
+ composes: root_normalPriorityNegative from '../Button/button.css';
+
+ background-color: white;
+ order: 1;
+}
+.cancelDeleteButton {
+ composes: root_lowPriority from '../Button/button.css';
+
+ background-color: white;
+}
+
+.contentContainer {
+ display: grid;
+ row-gap: 0.5rem;
+}
+
+.actionContainer {
+ align-content: start;
+ display: grid;
+ justify-items: start;
+ row-gap: 0.5rem;
+}
+
+.defaultBadge {
+ width: max-content;
+ padding: 0.375rem 1.5rem;
+ margin-bottom: 0.25rem;
+ border: 1px solid rgb(var(--venia-global-color-gray-400));
+ border-radius: 0.375rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.name {
+ font-weight: 600;
+}
+
+.telephone {
+ margin-top: 0.5rem;
+}
+
+.linkButton {
+ composes: root from '../LinkButton/linkButton.css';
+ text-decoration: none;
+}
+
+.editButton {
+ composes: linkButton;
+}
+
+.deleteButton {
+ composes: linkButton;
+}
+
+@media (max-width: 640px) {
+ .root {
+ padding: 1rem;
+ }
+
+ .confirmDeleteContainer {
+ padding: 1rem;
+ grid-template-columns: 1fr;
+ }
+
+ .confirmDeleteButton {
+ order: 0;
+ }
+}
+
+@media (max-width: 384px) {
+ .actionLabel {
+ display: none;
+ }
+
+ .linkButton {
+ padding: 0 0.25rem 0.25rem;
+ }
+}
+
+@keyframes flash {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/packages/venia-ui/lib/components/AddressBookPage/addressCard.js b/packages/venia-ui/lib/components/AddressBookPage/addressCard.js
new file mode 100644
index 0000000000..987f19bed5
--- /dev/null
+++ b/packages/venia-ui/lib/components/AddressBookPage/addressCard.js
@@ -0,0 +1,191 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { arrayOf, bool, func, shape, string } from 'prop-types';
+import { Trash2 as TrashIcon, Edit2 as EditIcon } from 'react-feather';
+
+import { mergeClasses } from '@magento/venia-ui/lib/classify';
+import Button from '@magento/venia-ui/lib/components/Button';
+import Icon from '@magento/venia-ui/lib/components/Icon';
+import defaultClasses from './addressCard.css';
+import LinkButton from '../LinkButton';
+
+const AddressCard = props => {
+ const {
+ address,
+ classes: propClasses,
+ countryName,
+ isConfirmingDelete,
+ isDeletingCustomerAddress,
+ onCancelDelete,
+ onConfirmDelete,
+ onEdit,
+ onDelete
+ } = props;
+
+ const {
+ city,
+ country_code,
+ default_shipping,
+ firstname,
+ middlename = '',
+ lastname,
+ postcode,
+ region: { region },
+ street,
+ telephone
+ } = address;
+
+ const classes = mergeClasses(defaultClasses, propClasses);
+ const confirmDeleteButtonClasses = {
+ root_normalPriorityNegative: classes.confirmDeleteButton
+ };
+ const cancelDeleteButtonClasses = {
+ root_lowPriority: classes.cancelDeleteButton
+ };
+
+ const streetRows = street.map((row, index) => {
+ return (
+
+ {row}
+
+ );
+ });
+
+ const defaultBadge = default_shipping ? (
+
+
+
+ ) : null;
+
+ const nameString = [firstname, middlename, lastname]
+ .filter(name => !!name)
+ .join(' ');
+ const additionalAddressString = `${city}, ${region} ${postcode}`;
+
+ const deleteButtonElement = !default_shipping ? (
+
+
+
+
+
+
+ ) : null;
+
+ const maybeConfirmingDeleteOverlay = isConfirmingDelete ? (
+
+
+
+
+
+
+
+
+ ) : null;
+
+ return (
+
+
+ {defaultBadge}
+ {nameString}
+ {streetRows}
+
+ {additionalAddressString}
+
+
+ {countryName || country_code}
+
+
+
+
+
+
+
+
+
+
+
+
+ {deleteButtonElement}
+ {maybeConfirmingDeleteOverlay}
+
+
+ );
+};
+
+export default AddressCard;
+
+AddressCard.propTypes = {
+ address: shape({
+ city: string,
+ country_code: string,
+ default_shipping: bool,
+ firstname: string,
+ lastname: string,
+ postcode: string,
+ region: shape({
+ region_code: string,
+ region: string
+ }),
+ street: arrayOf(string),
+ telephone: string
+ }).isRequired,
+ classes: shape({
+ actionContainer: string,
+ actionLabel: string,
+ additionalAddress: string,
+ contentContainer: string,
+ country: string,
+ defaultBadge: string,
+ defaultCard: string,
+ deleteButton: string,
+ editButton: string,
+ flash: string,
+ linkButton: string,
+ name: string,
+ root: string,
+ root_updated: string,
+ streetRow: string,
+ telephone: string
+ }),
+ countryName: string,
+ isConfirmingDelete: bool,
+ isDeletingCustomerAddress: bool,
+ onCancelDelete: func,
+ onConfirmDelete: func,
+ onDelete: func,
+ onEdit: func
+};
diff --git a/packages/venia-ui/lib/components/AuthModal/__tests__/__snapshots__/authModal.spec.js.snap b/packages/venia-ui/lib/components/AuthModal/__tests__/__snapshots__/authModal.spec.js.snap
index fa87ea0cda..239a2c2ec9 100644
--- a/packages/venia-ui/lib/components/AuthModal/__tests__/__snapshots__/authModal.spec.js.snap
+++ b/packages/venia-ui/lib/components/AuthModal/__tests__/__snapshots__/authModal.spec.js.snap
@@ -1,8 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render create account component if view is CREATE_ACCOUNT 1`] = `
-
+
+
+
@@ -37,7 +49,9 @@ exports[`should render my account component if view is MY_ACCOUNT 1`] = `
`;
exports[`should render properly 1`] = `
-
+
props => (
));
diff --git a/packages/venia-ui/lib/components/AuthModal/authModal.css b/packages/venia-ui/lib/components/AuthModal/authModal.css
index 8b98de946a..03c28c3383 100644
--- a/packages/venia-ui/lib/components/AuthModal/authModal.css
+++ b/packages/venia-ui/lib/components/AuthModal/authModal.css
@@ -1,3 +1,13 @@
.root {
display: block;
}
+
+.createAccountActions {
+ composes: actions from '../CreateAccount/createAccount.css';
+ grid-auto-flow: row;
+}
+
+.createAccountSubmitButton {
+ composes: root_highPriority from '../Button/button.css';
+ grid-column-start: auto;
+}
diff --git a/packages/venia-ui/lib/components/AuthModal/authModal.js b/packages/venia-ui/lib/components/AuthModal/authModal.js
index a9635eff94..25524ed2c5 100644
--- a/packages/venia-ui/lib/components/AuthModal/authModal.js
+++ b/packages/venia-ui/lib/components/AuthModal/authModal.js
@@ -21,11 +21,17 @@ const AuthModal = props => {
username
} = useAuthModal(props);
+ const classes = mergeClasses(defaultClasses, props.classes);
+
let child = null;
switch (props.view) {
case 'CREATE_ACCOUNT': {
child = (
{
}
}
- const classes = mergeClasses(defaultClasses, props.classes);
return {child}
;
};
diff --git a/packages/venia-ui/lib/components/Breadcrumbs/__tests__/__snapshots__/breadcrumbs.spec.js.snap b/packages/venia-ui/lib/components/Breadcrumbs/__tests__/__snapshots__/breadcrumbs.spec.js.snap
index 3fb158decb..d9e5ff5d16 100644
--- a/packages/venia-ui/lib/components/Breadcrumbs/__tests__/__snapshots__/breadcrumbs.spec.js.snap
+++ b/packages/venia-ui/lib/components/Breadcrumbs/__tests__/__snapshots__/breadcrumbs.spec.js.snap
@@ -16,7 +16,10 @@ exports[`renders breadcrumbs for a category view 1`] = `
- Home
+
@@ -34,7 +37,10 @@ exports[`renders breadcrumbs for a product view 1`] = `
- Home
+
@@ -58,7 +64,10 @@ exports[`renders breadcrumbs for intermediate links 1`] = `
- Home
+
diff --git a/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCard.spec.js.snap b/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCard.spec.js.snap
index c612c80b89..f872c3733f 100644
--- a/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCard.spec.js.snap
+++ b/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCard.spec.js.snap
@@ -7,7 +7,10 @@ Array [
unit test card code
- Balance:
+
$
@@ -28,7 +31,10 @@ Array [
type="button"
>
- Remove
+
,
]
@@ -41,7 +47,10 @@ Array [
unit test card code
- Balance:
+
$
@@ -62,7 +71,10 @@ Array [
type="button"
>
- Remove
+
,
]
diff --git a/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCards.spec.js.snap b/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCards.spec.js.snap
index 5ef10ab5a0..1fbd901972 100644
--- a/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCards.spec.js.snap
+++ b/packages/venia-ui/lib/components/CartPage/GiftCards/__tests__/__snapshots__/giftCards.spec.js.snap
@@ -77,7 +77,10 @@ exports[`it renders correctly when it has cards 1`] = `
- Apply
+
@@ -90,7 +93,10 @@ exports[`it renders correctly when it has cards 1`] = `
- Check balance
+
@@ -110,7 +116,10 @@ exports[`it renders correctly when it has cards 1`] = `
- Balance:
+
$
@@ -134,7 +143,10 @@ exports[`it renders correctly when it has cards 1`] = `
- Remove
+
- Balance:
+
$
@@ -172,7 +187,10 @@ exports[`it renders correctly when it has cards 1`] = `
- Remove
+
@@ -256,7 +274,10 @@ exports[`it renders correctly with no cards 1`] = `
- Apply
+
@@ -269,7 +290,10 @@ exports[`it renders correctly with no cards 1`] = `
- Check balance
+
diff --git a/packages/venia-ui/lib/components/CartPage/PriceAdjustments/CouponCode/__tests__/__snapshots__/couponCode.spec.js.snap b/packages/venia-ui/lib/components/CartPage/PriceAdjustments/CouponCode/__tests__/__snapshots__/couponCode.spec.js.snap
index 495e78a739..6973984b97 100644
--- a/packages/venia-ui/lib/components/CartPage/PriceAdjustments/CouponCode/__tests__/__snapshots__/couponCode.spec.js.snap
+++ b/packages/venia-ui/lib/components/CartPage/PriceAdjustments/CouponCode/__tests__/__snapshots__/couponCode.spec.js.snap
@@ -14,7 +14,10 @@ exports[`disables remove button on click 1`] = `
type="button"
>
- Remove
+
@@ -65,7 +68,10 @@ exports[`disables submit button on coupon entry 1`] = `
type="submit"
>
- Apply
+
@@ -117,7 +123,10 @@ exports[`renders CouponCode input and submit button 1`] = `
type="submit"
>
- Apply
+
@@ -168,7 +177,10 @@ exports[`renders an error message if an error occurs on code entry 1`] = `
type="submit"
>
- Apply
+
@@ -177,7 +189,10 @@ exports[`renders an error message if an error occurs on code entry 1`] = `
exports[`renders an error state if unable to fetch applied coupons 1`] = `
- Something went wrong. Please refresh and try again.
+
`;
@@ -197,7 +212,10 @@ exports[`renders the coupon code view if applied coupons has data 1`] = `
type="button"
>
- Remove
+
diff --git a/packages/venia-ui/lib/components/CartPage/PriceAdjustments/ShippingMethods/__tests__/__snapshots__/shippingForm.spec.js.snap b/packages/venia-ui/lib/components/CartPage/PriceAdjustments/ShippingMethods/__tests__/__snapshots__/shippingForm.spec.js.snap
index bca0b2a247..3b2fcc572d 100644
--- a/packages/venia-ui/lib/components/CartPage/PriceAdjustments/ShippingMethods/__tests__/__snapshots__/shippingForm.spec.js.snap
+++ b/packages/venia-ui/lib/components/CartPage/PriceAdjustments/ShippingMethods/__tests__/__snapshots__/shippingForm.spec.js.snap
@@ -5,7 +5,10 @@ Array [
- Destination
+
,
- Shipping Methods
+
+
+
`;
-exports[`renders form with data 1`] = `
-
-
-
- Quantity
-
-
-
-
-
- Update
-
-
-
-
-`;
-
-exports[`renders loading indicator while options are being fetched 1`] = `
-
- Fetching Product Options...
-
+
+ Edit Item
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/editModal.spec.js b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/editModal.spec.js
index ba4040e352..a5e64c829e 100644
--- a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/editModal.spec.js
+++ b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/editModal.spec.js
@@ -26,15 +26,46 @@ test('renders closed shell with no active item', () => {
expect(tree.toJSON()).toMatchSnapshot();
});
-test('renders open drawer with active item', () => {
+test('renders dialog form when active item is set up', () => {
const mockItem = {
id: '123',
- name: 'Simple Product'
+ quantity: 5,
+ configurable_options: ['option1', 'option2'],
+ prices: {
+ price: {
+ currency: 'EUR',
+ value: '456.78'
+ }
+ },
+ product: {
+ id: 123,
+ name: 'Juno Sweater',
+ sku: 'ABC',
+ small_image: {
+ url:
+ 'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/s/vsw02-pe_main_2.jpg'
+ },
+ stock_status: 'IN STOCK'
+ }
};
useEditModal.mockReturnValueOnce({
- handleClose: jest.fn(),
- isOpen: true
+ setVariantPrice: jest.fn(),
+ variantPrice: ''
+ });
+
+ const tree = createTestInstance(
+
+ );
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('if active edit item is not exist, dialog form is not visible', () => {
+ const mockItem = null;
+
+ useEditModal.mockReturnValueOnce({
+ setVariantPrice: jest.fn(),
+ variantPrice: ''
});
const tree = createTestInstance(
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/productForm.spec.js b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/productForm.spec.js
index a057500b69..505fb92c73 100644
--- a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/productForm.spec.js
+++ b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/__tests__/productForm.spec.js
@@ -10,11 +10,30 @@ jest.mock(
jest.mock('../../../../../classify');
jest.mock('../../../../LoadingIndicator', () => 'LoadingIndicator');
jest.mock('../../../../ProductOptions', () => 'Options');
+jest.mock('../../../../Portal', () => ({
+ Portal: props => {props.children}
+}));
const mockItem = {
id: '123',
quantity: 5,
- configurable_options: ['option1', 'option2']
+ configurable_options: ['option1', 'option2'],
+ prices: {
+ price: {
+ currency: 'EUR',
+ value: '456.78'
+ }
+ },
+ product: {
+ id: 123,
+ name: 'Juno Sweater',
+ sku: 'ABC',
+ small_image: {
+ url:
+ 'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/s/vsw02-pe_main_2.jpg'
+ },
+ stock_status: 'IN STOCK'
+ }
};
const mockTalonProps = {
@@ -28,6 +47,11 @@ const mockTalonProps = {
setFormApi: jest.fn()
};
+const variantPrice = {
+ currency: 'EUR',
+ value: '456.78'
+};
+
test('renders loading indicator while options are being fetched', () => {
useProductForm.mockReturnValueOnce({
isLoading: true
@@ -43,7 +67,12 @@ test('renders form with data', () => {
useProductForm.mockReturnValueOnce(mockTalonProps);
const tree = createTestInstance(
-
+
);
expect(tree.toJSON()).toMatchSnapshot();
});
@@ -55,7 +84,12 @@ test('renders form errors', () => {
});
const tree = createTestInstance(
-
+
);
expect(tree.toJSON()).toMatchSnapshot();
});
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/editModal.css b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/editModal.css
deleted file mode 100644
index 7f4edf1870..0000000000
--- a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/editModal.css
+++ /dev/null
@@ -1,48 +0,0 @@
-.root {
- background-color: white;
- bottom: 0;
- display: grid;
- grid-template-rows: auto 1fr;
- height: 100%;
- max-width: 360px;
- opacity: 0;
- overflow: hidden;
- position: fixed;
- right: 0;
- top: 0;
- transform: translate3d(100%, 0, 0);
- transition-duration: 192ms;
- transition-timing-function: var(--venia-global-anim-out);
- transition-property: opacity, transform, visibility;
- visibility: hidden;
- width: 100%;
- z-index: 3;
-}
-
-.root_open {
- composes: root;
- box-shadow: 1px 0 rgb(var(--venia-global-color-border));
- opacity: 1;
- transform: translate3d(0, 0, 0);
- transition-duration: 224ms;
- transition-timing-function: var(--venia-global-anim-in);
- visibility: visible;
-}
-
-.body {
- border-bottom: 1px solid rgb(var(--venia-global-color-border));
- overflow: auto;
- padding: 0.5rem 2rem;
-}
-
-.header {
- border-bottom: 1px solid rgb(var(--venia-global-color-border));
- display: flex;
- justify-content: space-between;
- padding: 0.875rem;
-}
-
-.headerText {
- align-self: center;
- font-weight: 600;
-}
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/editModal.js b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/editModal.js
index 689f822861..cc78d7f24b 100644
--- a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/editModal.js
+++ b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/editModal.js
@@ -1,13 +1,6 @@
import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { X as CloseIcon } from 'react-feather';
import { useEditModal } from '@magento/peregrine/lib/talons/CartPage/ProductListing/EditModal/useEditModal';
-import { mergeClasses } from '../../../../classify';
-import Icon from '../../../Icon';
-import { Portal } from '../../../Portal';
-import defaultClasses from './editModal.css';
-import ProductDetail from './productDetail';
import ProductForm from './productForm';
/**
@@ -16,6 +9,7 @@ import ProductForm from './productForm';
*
* @param {Object} props
* @param {Object} props.item Product to edit.
+ * @param {function} props.setActiveEditItem Function for setting the actively editing item
* See [productListingFragments.js]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/CartPage/ProductListing/productListingFragments.js}
* for a list of properties for this object.
* @param {Function} props.setIsCartUpdating Function for setting the updating state of the cart.
@@ -29,44 +23,18 @@ import ProductForm from './productForm';
* import EditModal from "@magento/venia-ui/lib/components/CartPage/ProductListing/EditModal";
*/
const EditModal = props => {
- const { item, setIsCartUpdating } = props;
+ const { item, setActiveEditItem, setIsCartUpdating } = props;
const talonProps = useEditModal();
- const { handleClose, isOpen, setVariantPrice, variantPrice } = talonProps;
-
- const classes = mergeClasses(defaultClasses, props.classes);
- const rootClass = isOpen ? classes.root_open : classes.root;
-
- const bodyComponent = item ? (
-
- ) : null;
+ const { setVariantPrice, variantPrice } = talonProps;
return (
-
-
-
-
-
-
-
-
-
-
- {bodyComponent}
-
-
+
);
};
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.css b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.css
index a478f6b096..3a6d7cc7a3 100644
--- a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.css
+++ b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.css
@@ -1,3 +1,8 @@
+.contents {
+ composes: contents from '../../../Dialog/dialog.css';
+ position: relative;
+}
+
.optionRoot {
border-bottom: 1px solid rgb(var(--venia-global-color-border));
padding: 1rem 0;
@@ -13,20 +18,20 @@
.quantityRoot {
composes: root from '../quantity.css';
- grid-template-columns: auto 1fr auto;
+ grid-template-columns: auto 4rem auto;
+ justify-content: start;
padding: 0 1rem;
}
-.submit {
- display: flex;
- justify-content: center;
- padding: 2rem;
-}
-
.loading {
composes: root from '../../../LoadingIndicator/indicator.css';
height: unset;
+ left: 0;
+ position: absolute;
text-align: center;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 10;
}
.dataError {
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.js b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.js
index d0732cb649..3d75e81a2c 100644
--- a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.js
+++ b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productForm.js
@@ -1,20 +1,27 @@
import React, { Fragment } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { gql } from '@apollo/client';
-import { Form } from 'informed';
import { useProductForm } from '@magento/peregrine/lib/talons/CartPage/ProductListing/EditModal/useProductForm';
import { mergeClasses } from '../../../../classify';
-import Button from '../../../Button';
import FormError from '../../../FormError';
import LoadingIndicator from '../../../LoadingIndicator';
import Options from '../../../ProductOptions';
import { QuantityFields } from '../quantity';
import defaultClasses from './productForm.css';
import { CartPageFragment } from '../../cartPageFragments.gql';
+import { ProductFormFragment } from './productFormFragment.gql';
+import Dialog from '../../../Dialog';
+import ProductDetail from './productDetail';
const ProductForm = props => {
- const { item: cartItem, setIsCartUpdating, setVariantPrice } = props;
+ const {
+ item: cartItem,
+ setIsCartUpdating,
+ variantPrice,
+ setVariantPrice,
+ setActiveEditItem
+ } = props;
const { formatMessage } = useIntl();
const talonProps = useProductForm({
cartItem,
@@ -22,7 +29,8 @@ const ProductForm = props => {
setIsCartUpdating,
setVariantPrice,
updateConfigurableOptionsMutation: UPDATE_CONFIGURABLE_OPTIONS_MUTATION,
- updateQuantityMutation: UPDATE_QUANTITY_MUTATION
+ updateQuantityMutation: UPDATE_QUANTITY_MUTATION,
+ setActiveEditItem
});
const {
configItem,
@@ -30,22 +38,32 @@ const ProductForm = props => {
handleOptionSelection,
handleSubmit,
isLoading,
- isSaving
+ isSaving,
+ isDialogOpen,
+ handleClose
} = talonProps;
const classes = mergeClasses(defaultClasses, props.classes);
+ const dialogButtonsDisabled = isLoading;
+ const dialogSubmitButtonDisabled = isSaving;
+ const dialogFormProps = {
+ initialValues: cartItem
+ };
- if (isLoading || isSaving) {
- const message = isLoading
- ? formatMessage({
- id: 'productForm.fetchingProductOptions',
- defaultMessage: 'Fetching Product Options...'
- })
+ const message = isLoading
+ ? formatMessage({
+ id: 'productForm.fetchingProductOptions',
+ defaultMessage: 'Fetching Product Options...'
+ })
+ ? isSaving
: formatMessage({
id: 'productForm.updatingCart',
defaultMessage: 'Updating Cart...'
- });
- return (
+ })
+ : null;
+
+ const maybeLoadingIndicator =
+ isLoading || isSaving ? (
{
>
{message}
- );
- }
+ ) : null;
- if (!configItem) {
+ if (cartItem && !isLoading && !configItem) {
return (
{
);
}
- return (
-
-
-
+ const dialogContent =
+ cartItem && configItem ? (
+
+
+
{
initialValue={cartItem.quantity}
itemId={cartItem.id}
/>
-
-
-
-
-
-
+
+ ) : null;
+
+ return (
+
+
+ {maybeLoadingIndicator}
+ {dialogContent}
+
);
};
@@ -120,49 +154,11 @@ export const GET_CONFIGURABLE_OPTIONS = gql`
products(filter: { sku: { eq: $sku } }) {
items {
id
- sku
- ... on ConfigurableProduct {
- configurable_options {
- attribute_code
- attribute_id
- id
- label
- values {
- default_label
- label
- store_label
- use_default_value
- value_index
- swatch_data {
- ... on ImageSwatchData {
- thumbnail
- }
- value
- }
- }
- }
- variants {
- attributes {
- code
- value_index
- }
- product {
- id
- price {
- regularPrice {
- amount {
- currency
- value
- }
- }
- }
- sku
- }
- }
- }
+ ...ProductFormFragment
}
}
}
+ ${ProductFormFragment}
`;
export const UPDATE_QUANTITY_MUTATION = gql`
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productFormFragment.gql.js b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productFormFragment.gql.js
new file mode 100644
index 0000000000..7ecb1239ad
--- /dev/null
+++ b/packages/venia-ui/lib/components/CartPage/ProductListing/EditModal/productFormFragment.gql.js
@@ -0,0 +1,47 @@
+import { gql } from '@apollo/client';
+
+export const ProductFormFragment = gql`
+ fragment ProductFormFragment on ProductInterface {
+ id
+ sku
+ ... on ConfigurableProduct {
+ configurable_options {
+ attribute_code
+ attribute_id
+ id
+ label
+ values {
+ default_label
+ label
+ store_label
+ use_default_value
+ value_index
+ swatch_data {
+ ... on ImageSwatchData {
+ thumbnail
+ }
+ value
+ }
+ }
+ }
+ variants {
+ attributes {
+ code
+ value_index
+ }
+ product {
+ id
+ price {
+ regularPrice {
+ amount {
+ currency
+ value
+ }
+ }
+ }
+ sku
+ }
+ }
+ }
+ }
+`;
diff --git a/packages/venia-ui/lib/components/CartPage/ProductListing/__tests__/__snapshots__/product.spec.js.snap b/packages/venia-ui/lib/components/CartPage/ProductListing/__tests__/__snapshots__/product.spec.js.snap
index 465e5f8542..b26c44f3f4 100644
--- a/packages/venia-ui/lib/components/CartPage/ProductListing/__tests__/__snapshots__/product.spec.js.snap
+++ b/packages/venia-ui/lib/components/CartPage/ProductListing/__tests__/__snapshots__/product.spec.js.snap
@@ -85,7 +85,10 @@ exports[`renders configurable product with options 1`] = `
00
- ea.
+
00
- ea.
+
00
- ea.
+
{
);
diff --git a/packages/venia-ui/lib/components/CartPage/__tests__/__snapshots__/cartPage.spec.js.snap b/packages/venia-ui/lib/components/CartPage/__tests__/__snapshots__/cartPage.spec.js.snap
index 410a7d130f..a8bcf80aa8 100644
--- a/packages/venia-ui/lib/components/CartPage/__tests__/__snapshots__/cartPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/CartPage/__tests__/__snapshots__/cartPage.spec.js.snap
@@ -72,7 +72,10 @@ exports[`renders a loading indicator when talon indicates 1`] = `
- Fetching Data...
+
`;
@@ -87,20 +90,11 @@ exports[`renders components if cart has items 1`] = `
- Cart
+
-
-
- Sign In
-
-
@@ -149,20 +143,11 @@ exports[`renders empty cart text (no adjustments, list or summary) if cart is em
- Cart
+
-
-
- Sign In
-
-
@@ -176,7 +161,10 @@ exports[`renders empty cart text (no adjustments, list or summary) if cart is em
className="items_container"
>
- There are no items in your cart.
+
{
const {
cartItems,
- handleSignIn,
hasItems,
- isSignedIn,
isCartUpdating,
setIsCartUpdating,
shouldShowLoadingIndicator
@@ -54,20 +51,6 @@ const CartPage = props => {
return fullPageLoadingIndicator;
}
- const signInDisplay = !isSignedIn ? (
-
-
-
- ) : null;
-
const productListing = hasItems ? (
) : (
@@ -104,7 +87,6 @@ const CartPage = props => {
defaultMessage={'Cart'}
/>
- {signInDisplay}
diff --git a/packages/venia-ui/lib/components/CategoryList/categoryList.js b/packages/venia-ui/lib/components/CategoryList/categoryList.js
index 14f9cc38d4..d69df3303b 100644
--- a/packages/venia-ui/lib/components/CategoryList/categoryList.js
+++ b/packages/venia-ui/lib/components/CategoryList/categoryList.js
@@ -88,7 +88,7 @@ const CategoryList = props => {
};
CategoryList.propTypes = {
- id: number,
+ id: number.isRequired,
title: string,
classes: shape({
root: string,
diff --git a/packages/venia-ui/lib/components/CategoryTree/categoryTree.js b/packages/venia-ui/lib/components/CategoryTree/categoryTree.js
index 87637a1650..460a31950d 100644
--- a/packages/venia-ui/lib/components/CategoryTree/categoryTree.js
+++ b/packages/venia-ui/lib/components/CategoryTree/categoryTree.js
@@ -45,7 +45,7 @@ const Tree = props => {
export default Tree;
Tree.propTypes = {
- categoryId: number.isRequired,
+ categoryId: number,
classes: shape({
root: string,
tree: string
diff --git a/packages/venia-ui/lib/components/Checkbox/checkbox.css b/packages/venia-ui/lib/components/Checkbox/checkbox.css
index 03843d8050..9e4ed6f599 100644
--- a/packages/venia-ui/lib/components/Checkbox/checkbox.css
+++ b/packages/venia-ui/lib/components/Checkbox/checkbox.css
@@ -44,12 +44,17 @@
cursor: default;
}
-.input:checked + .icon {
+/* When the input is disabled, update the cursor on the sibling label element. */
+.input:disabled ~ .label {
+ cursor: default;
+}
+
+.input:checked:enabled + .icon {
--stroke: var(--venia-brand-color-1-700);
}
-.input:active,
-.input:focus {
+.input:active:enabled,
+.input:focus:enabled {
box-shadow: -3px 3px rgb(var(--venia-brand-color-1-100));
outline: none;
}
diff --git a/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressBook.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressBook.spec.js.snap
index 88b062957a..9f302cc85c 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressBook.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressBook.spec.js.snap
@@ -25,7 +25,10 @@ Array [
- Change Shipping Information
+
- Cancel
+
- Apply
+
@@ -143,7 +152,10 @@ Array [
- Add New Address
+
@@ -163,7 +175,10 @@ Array [
- Change Shipping Information
+
- Cancel
+
- Apply
+
@@ -245,7 +266,10 @@ Array [
- Add New Address
+
diff --git a/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressCard.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressCard.spec.js.snap
index 88eff42981..077f9f319a 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressCard.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/AddressBook/__tests__/__snapshots__/addressCard.spec.js.snap
@@ -11,7 +11,10 @@ exports[`renders base card state 1`] = `
- Default
+
- Default
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders ForgotPassword component 1`] = `
+
+`;
+
+exports[`renders SignIn component 2`] = `
+
+`;
diff --git a/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/__tests__/guestSignIn.spec.js b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/__tests__/guestSignIn.spec.js
new file mode 100644
index 0000000000..f3a29fc96c
--- /dev/null
+++ b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/__tests__/guestSignIn.spec.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { createTestInstance } from '@magento/peregrine';
+import { useGuestSignIn } from '@magento/peregrine/lib/talons/CheckoutPage/GuestSignIn/useGuestSignIn';
+
+import GuestSignIn from '../guestSignIn';
+
+jest.mock(
+ '@magento/peregrine/lib/talons/CheckoutPage/GuestSignIn/useGuestSignIn'
+);
+jest.mock('@magento/venia-ui/lib/classify');
+jest.mock(
+ '@magento/venia-ui/lib/components/CreateAccount',
+ () => 'CreateAccount'
+);
+jest.mock(
+ '@magento/venia-ui/lib/components/ForgotPassword',
+ () => 'ForgotPassword'
+);
+jest.mock('@magento/venia-ui/lib/components/SignIn', () => 'SignIn');
+
+const defaultProps = {
+ isActive: true,
+ toggleActiveContent: jest.fn().mockName('toggleActiveContent')
+};
+
+const defaultTalonProps = {
+ handleBackToCheckout: jest.fn().mockName('handleBackToCheckout'),
+ toggleCreateAccountView: jest.fn().mockName('toggleCreateAccountView'),
+ toggleForgotPasswordView: jest.fn().mockName('toggleForgotPasswordView'),
+ view: 'SIGNIN'
+};
+
+test('renders SignIn component', () => {
+ useGuestSignIn.mockReturnValue(defaultTalonProps);
+ const tree = createTestInstance(
+
+ );
+
+ expect(useGuestSignIn.mock.calls[0][0]).toMatchInlineSnapshot(`
+ Object {
+ "toggleActiveContent": [MockFunction toggleActiveContent],
+ }
+ `);
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders ForgotPassword component', () => {
+ useGuestSignIn.mockReturnValue({
+ ...defaultTalonProps,
+ view: 'FORGOT_PASSWORD'
+ });
+
+ const tree = createTestInstance(
);
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders CreateAccount component', () => {
+ useGuestSignIn.mockReturnValue({
+ ...defaultTalonProps,
+ view: 'CREATE_ACCOUNT'
+ });
+
+ const tree = createTestInstance(
);
+ expect(tree.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/guestSignIn.css b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/guestSignIn.css
new file mode 100644
index 0000000000..640abca082
--- /dev/null
+++ b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/guestSignIn.css
@@ -0,0 +1,53 @@
+.root {
+ display: grid;
+ grid-template-columns: minmax(auto, 512px);
+ justify-content: center;
+ row-gap: 2rem;
+ text-align: center;
+}
+
+.root_hidden {
+ composes: root;
+ display: none;
+}
+
+.header {
+ font-family: var(--venia-global-fontFamily-serif);
+}
+
+.contentContainer {
+ border: 2px solid rgb(var(--venia-global-color-border));
+ border-radius: 0.375rem;
+ padding-bottom: 2rem;
+}
+
+.signInRoot {
+ composes: root from '../../SignIn/signIn.css';
+}
+
+.forgotPasswordRoot {
+ composes: root from '../../ForgotPassword/forgotPassword.css';
+}
+
+.createAccountRoot {
+ composes: root from '../../CreateAccount/createAccount.css';
+}
+
+@media (max-width: 960px) {
+ .contentContainer {
+ border: none;
+ padding: 0;
+ }
+
+ .signInRoot {
+ padding: 1rem 0;
+ }
+
+ .forgotPasswordRoot {
+ padding: 1rem 0;
+ }
+
+ .createAccountRoot {
+ padding: 1rem 0;
+ }
+}
diff --git a/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/guestSignIn.js b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/guestSignIn.js
new file mode 100644
index 0000000000..99212d702e
--- /dev/null
+++ b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/guestSignIn.js
@@ -0,0 +1,89 @@
+import React from 'react';
+import { bool, func, shape, string } from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { useGuestSignIn } from '@magento/peregrine/lib/talons/CheckoutPage/GuestSignIn/useGuestSignIn';
+
+import { mergeClasses } from '@magento/venia-ui/lib/classify';
+import CreateAccount from '@magento/venia-ui/lib/components/CreateAccount';
+import ForgotPassword from '@magento/venia-ui/lib/components/ForgotPassword';
+import LinkButton from '@magento/venia-ui/lib/components/LinkButton';
+import SignIn from '@magento/venia-ui/lib/components/SignIn';
+import defaultClasses from './guestSignIn.css';
+
+const GuestSignIn = props => {
+ const { isActive, toggleActiveContent } = props;
+
+ const talonProps = useGuestSignIn({ toggleActiveContent });
+ const {
+ handleBackToCheckout,
+ toggleCreateAccountView,
+ toggleForgotPasswordView,
+ view
+ } = talonProps;
+
+ const classes = mergeClasses(defaultClasses, props.classes);
+
+ const rootClass = isActive ? classes.root : classes.root_hidden;
+
+ let content;
+ if (view === 'SIGNIN') {
+ content = (
+
+ );
+ } else if (view === 'FORGOT_PASSWORD') {
+ content = (
+
+ );
+ } else if (view === 'CREATE_ACCOUNT') {
+ content = (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {content}
+
+
+
+
+
+ );
+};
+
+export default GuestSignIn;
+
+GuestSignIn.propTypes = {
+ classes: shape({
+ root: string,
+ root_hidden: string,
+ header: string,
+ contentContainer: string,
+ signInRoot: string,
+ forgotPasswordRoot: string,
+ createAccountRoot: string
+ }),
+ isActive: bool.isRequired,
+ toggleActiveContent: func.isRequired
+};
diff --git a/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/index.js b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/index.js
new file mode 100644
index 0000000000..0ccd75ee40
--- /dev/null
+++ b/packages/venia-ui/lib/components/CheckoutPage/GuestSignIn/index.js
@@ -0,0 +1 @@
+export { default } from './guestSignIn.js';
diff --git a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/item.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/item.spec.js.snap
index 2933e6f7e3..217a1ce202 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/item.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/item.spec.js.snap
@@ -23,7 +23,15 @@ exports[`Snapshot test 1`] = `
}
/>
- Qty :
+
`;
diff --git a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/itemsReview.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/itemsReview.spec.js.snap
index 7777f7fa85..c66c518466 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/itemsReview.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/itemsReview.spec.js.snap
@@ -7,7 +7,10 @@ exports[`Snapshot test 1`] = `
7
- items in your order
+
- Qty :
+
@@ -118,7 +129,15 @@ https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/media/catalog/produc
- Qty :
+
@@ -174,7 +193,15 @@ https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/media/catalog/produc
- Qty :
+
diff --git a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/showAllButton.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/showAllButton.spec.js.snap
index 13036bef66..1c5a9fc55a 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/showAllButton.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/__tests__/__snapshots__/showAllButton.spec.js.snap
@@ -6,7 +6,10 @@ exports[`Snapshot test 1`] = `
>
- SHOW ALL ITEMS
+
- Quick Checkout When You Return
+
- Set a password and save your information for next time in one easy step!
+
- Create Account
+
@@ -285,10 +294,16 @@ exports[`CreateAccount renders errors 1`] = `
className="root"
>
- Quick Checkout When You Return
+
- Set a password and save your information for next time in one easy step!
+
- Create Account
+
diff --git a/packages/venia-ui/lib/components/CheckoutPage/OrderConfirmationPage/__tests__/__snapshots__/orderConfirmationPage.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/OrderConfirmationPage/__tests__/__snapshots__/orderConfirmationPage.spec.js.snap
index f866ec2a27..5bc5620580 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/OrderConfirmationPage/__tests__/__snapshots__/orderConfirmationPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/OrderConfirmationPage/__tests__/__snapshots__/orderConfirmationPage.spec.js.snap
@@ -5,13 +5,27 @@ exports[`OrderConfirmationPage renders OrderConfirmationPage component 1`] = `
Title
- Thank you for your order!
+
- Order Number
+
- Shipping Information
+
@@ -28,7 +42,10 @@ exports[`OrderConfirmationPage renders OrderConfirmationPage component 1`] = `
- Shipping Method
+
Flat Rate - Fixed
@@ -37,7 +54,10 @@ exports[`OrderConfirmationPage renders OrderConfirmationPage component 1`] = `
- You will also receive an email with the details and we will let you know when your order has shipped.
+
diff --git a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/paymentMethods.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/paymentMethods.spec.js.snap
index 616d7d459b..44dfb32141 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/paymentMethods.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/paymentMethods.spec.js.snap
@@ -1,3 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders null when loading 1`] = `null`;
+
+exports[`should render error message if availablePaymentMethods is empty 1`] = `
+
+
+
+ There was an error loading payments.
+
+
+ Please refresh or try again later.
+
+
+
+`;
diff --git a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/summary.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/summary.spec.js.snap
index b619f38b0c..4254aed180 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/summary.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/__tests__/__snapshots__/summary.spec.js.snap
@@ -10,7 +10,10 @@ exports[`should render a braintree summary 1`] = `
- Payment Information
+
- Edit
+
@@ -55,7 +61,10 @@ exports[`should render a braintree summary 1`] = `
- Credit Card
+
- Fetching Payment Information
+
`;
@@ -203,7 +215,10 @@ exports[`should render a non-braintree summary 1`] = `
- Payment Information
+
{
tree.root.findByProps({ id: 'BraintreeMockId' });
}).not.toThrow();
});
+
+test('should render error message if availablePaymentMethods is empty', () => {
+ usePaymentMethods.mockReturnValueOnce({
+ ...defaultTalonProps,
+ availablePaymentMethods: []
+ });
+
+ const tree = createTestInstance(
);
+
+ expect(tree).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js
index ff47c24c3a..6250110945 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js
+++ b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js
@@ -1,6 +1,7 @@
/**
* This file is augmented at build time using the @magento/venia-ui build
- * target "payments", which allows third-party modules to add new payment component mappings.
+ * target "checkoutPagePaymentTypes", which allows third-party modules to
+ * add new payment component mappings for the checkout page.
*
* @see [Payment definition object]{@link PaymentDefinition}
*/
@@ -17,6 +18,6 @@ export default {};
* @example
A custom payment method
* const myCustomPayment = {
* paymentCode: 'cc',
- * importPath: '@partner/module/path_to_your_component'
+ * importPath: '@partner/module/path_to_your_component'
* }
*/
diff --git a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.css b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.css
index 44bc771129..a3c0ce87e3 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.css
+++ b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.css
@@ -1,9 +1,9 @@
.root {
display: grid;
+ padding: 2rem;
}
.payment_method {
- padding: 1.5rem;
border-bottom: 1px solid rgb(var(--venia-global-color-border));
}
@@ -15,3 +15,9 @@
font-weight: 600;
justify-self: start;
}
+
+.payment_errors {
+ display: grid;
+ gap: 0.5em;
+ color: rgb(var(--venia-global-color-error));
+}
diff --git a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.js b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.js
index 9c8f393773..646f49be79 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.js
+++ b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethods.js
@@ -1,6 +1,7 @@
import React from 'react';
import { shape, string, bool, func } from 'prop-types';
import { RadioGroup } from 'informed';
+import { useIntl } from 'react-intl';
import { usePaymentMethods } from '@magento/peregrine/lib/talons/CheckoutPage/PaymentInformation/usePaymentMethods';
@@ -19,6 +20,8 @@ const PaymentMethods = props => {
shouldSubmit
} = props;
+ const { formatMessage } = useIntl();
+
const classes = mergeClasses(defaultClasses, propClasses);
const talonProps = usePaymentMethods({
@@ -36,37 +39,56 @@ const PaymentMethods = props => {
return null;
}
- const radios = availablePaymentMethods.map(({ code, title }) => {
- // If we don't have an implementation for a method type, ignore it.
- if (!Object.keys(payments).includes(code)) {
- return;
- }
-
- const isSelected = currentSelectedPaymentMethod === code;
- const PaymentMethodComponent = payments[code];
- const renderedComponent = isSelected ? (
-
- ) : null;
+ const radios = availablePaymentMethods
+ .map(({ code, title }) => {
+ // If we don't have an implementation for a method type, ignore it.
+ if (!Object.keys(payments).includes(code)) {
+ return;
+ }
- return (
-
-
- {renderedComponent}
-
- );
- });
+ ) : null;
+
+ return (
+
+
+ {renderedComponent}
+
+ );
+ })
+ .filter(paymentMethod => !!paymentMethod);
+
+ const noPaymentMethodMessage = !radios.length ? (
+
+
+ {formatMessage({
+ id: 'checkoutPage.paymentLoadingError',
+ defaultMessage: 'There was an error loading payments.'
+ })}
+
+
+ {formatMessage({
+ id: 'checkoutPage.refreshOrTryAgainLater',
+ defaultMessage: 'Please refresh or try again later.'
+ })}
+
+
+ ) : null;
return (
@@ -76,6 +98,7 @@ const PaymentMethods = props => {
>
{radios}
+ {noPaymentMethodMessage}
);
};
diff --git a/packages/venia-ui/lib/components/CheckoutPage/ShippingInformation/AddressForm/__tests__/__snapshots__/customerForm.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/ShippingInformation/AddressForm/__tests__/__snapshots__/customerForm.spec.js.snap
index 0d8b572068..972e841597 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/ShippingInformation/AddressForm/__tests__/__snapshots__/customerForm.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/ShippingInformation/AddressForm/__tests__/__snapshots__/customerForm.spec.js.snap
@@ -72,7 +72,10 @@ exports[`renders loading indicator 1`] = `
- Fetching Customer Details...
+
`;
@@ -246,7 +249,10 @@ Array [
- Optional
+
- Cancel
+
- Optional
+
- Cancel
+
- The shipping address you enter will be saved to your address book and set as your default for future purchases.
+
- Optional
+
- Set a password at the end of guest checkout to create an account in one easy step.
+
@@ -217,7 +220,10 @@ Array [
- Optional
+
- Set a password at the end of guest checkout to create an account in one easy step.
+
@@ -635,7 +644,10 @@ Array [
- Optional
+
- Optional
+
- Cancel
+
- Optional
+
- Cancel
+
- Edit Shipping Information
+
- Edit Shipping Information
+
- Shipping Information
+
- Edit
+
@@ -65,7 +71,10 @@ exports[`renders card state with guest data 1`] = `
- Shipping Information
+
- Edit
+
@@ -120,7 +132,10 @@ exports[`renders form state without data 1`] = `
- 1. Shipping Information
+
- Fetching Shipping Information...
+
`;
diff --git a/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/completedView.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/completedView.spec.js.snap
index 840eb6568e..be2dacfe79 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/completedView.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/completedView.spec.js.snap
@@ -13,7 +13,10 @@ exports[`it renders an error when selectedShippingMethod is missing 1`] = `
- Shipping Method
+
- Edit
+
@@ -55,7 +61,10 @@ exports[`it renders an error when selectedShippingMethod is missing 1`] = `
- Error loading selected shipping method. Please select again.
+
@@ -74,7 +83,10 @@ exports[`it renders correctly 1`] = `
- Shipping Method
+
- Edit
+
diff --git a/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/shippingMethod.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/shippingMethod.spec.js.snap
index 72eb0f83be..706a5bf759 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/shippingMethod.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/shippingMethod.spec.js.snap
@@ -7,7 +7,10 @@ exports[`it disables inputs when the page is updating 1`] = `
- Shipping Method
+
- Continue to Payment Information
+
@@ -108,7 +114,10 @@ exports[`it renders correctly 1`] = `
- Shipping Method
+
- Continue to Payment Information
+
@@ -267,7 +279,10 @@ exports[`it renders correctly in initializing mode 1`] = `
- Shipping Method
+
- Loading shipping methods...
+
diff --git a/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/updateModal.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/updateModal.spec.js.snap
index 35bdd5c4bf..6d85a85a20 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/updateModal.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/ShippingMethod/__tests__/__snapshots__/updateModal.spec.js.snap
@@ -162,7 +162,10 @@ exports[`it disables the submit button while loading 1`] = `
- Cancel
+
- Update
+
@@ -340,7 +346,10 @@ exports[`it renders correctly 1`] = `
- Cancel
+
- Update
+
diff --git a/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap
index 66fbfb8cf4..0064fe9f22 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap
@@ -15,7 +15,7 @@ exports[`CheckoutPage renders address book for customer 1`] = `
cartItems={Array []}
message={
-
@@ -23,7 +23,7 @@ exports[`CheckoutPage renders address book for customer 1`] = `
className="cartLink"
to="/cart"
>
-
@@ -42,7 +42,7 @@ exports[`CheckoutPage renders address book for customer 1`] = `
>
- 2. Shipping Method
+
- 3. Payment Information
+
`;
@@ -93,7 +99,7 @@ exports[`CheckoutPage renders checkout content for customer - default address 1`
cartItems={Array []}
message={
-
@@ -101,7 +107,7 @@ exports[`CheckoutPage renders checkout content for customer - default address 1`
className="cartLink"
to="/cart"
>
-
@@ -120,7 +126,7 @@ exports[`CheckoutPage renders checkout content for customer - default address 1`
>
- 2. Shipping Method
+
- 3. Payment Information
+
`;
@@ -171,7 +183,7 @@ exports[`CheckoutPage renders checkout content for customer - no default address
cartItems={Array []}
message={
-
@@ -179,7 +191,7 @@ exports[`CheckoutPage renders checkout content for customer - no default address
className="cartLink"
to="/cart"
>
-
@@ -198,7 +210,7 @@ exports[`CheckoutPage renders checkout content for customer - no default address
>
- 2. Shipping Method
+
- 3. Payment Information
+
`;
@@ -242,22 +260,6 @@ exports[`CheckoutPage renders checkout content for guest 1`] = `
-
-
-
- Login and Checkout Faster
-
-
-
@@ -265,7 +267,7 @@ exports[`CheckoutPage renders checkout content for guest 1`] = `
cartItems={Array []}
message={
-
@@ -273,7 +275,7 @@ exports[`CheckoutPage renders checkout content for guest 1`] = `
className="cartLink"
to="/cart"
>
-
@@ -287,12 +289,39 @@ exports[`CheckoutPage renders checkout content for guest 1`] = `
Guest Checkout
+
+
+
+
+
+
+
+
+
+
- 2. Shipping Method
+
- 3. Payment Information
+
+
`;
@@ -396,7 +435,10 @@ exports[`CheckoutPage renders loading indicator 1`] = `
- Fetching Data...
+
`;
diff --git a/packages/venia-ui/lib/components/CheckoutPage/__tests__/checkoutPage.spec.js b/packages/venia-ui/lib/components/CheckoutPage/__tests__/checkoutPage.spec.js
index 03899f1f31..bbe18dcaa0 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/__tests__/checkoutPage.spec.js
+++ b/packages/venia-ui/lib/components/CheckoutPage/__tests__/checkoutPage.spec.js
@@ -38,6 +38,7 @@ jest.mock('../../../classify');
jest.mock('../../../components/Head', () => ({ Title: () => 'Title' }));
jest.mock('../../StockStatusMessage', () => 'StockStatusMessage');
jest.mock('../ItemsReview', () => 'ItemsReview');
+jest.mock('../GuestSignIn', () => 'GuestSignIn');
jest.mock('../OrderSummary', () => 'OrderSummary');
jest.mock('../OrderConfirmationPage', () => 'OrderConfirmationPage');
jest.mock('../ShippingInformation', () => 'ShippingInformation');
@@ -69,7 +70,8 @@ const defaultTalonProps = {
.mockName('setShippingInformationDone'),
setShippingMethodDone: jest.fn().mockName('setShippingMethodDone'),
setPaymentInformationDone: jest.fn().mockName('setPaymentInformationDone'),
- toggleActiveContent: jest.fn().mockName('toggleActiveContent')
+ toggleAddressBookContent: jest.fn().mockName('toggleAddressBookContent'),
+ toggleSignInContent: jest.fn().mockName('toggleSignInContent')
};
describe('CheckoutPage', () => {
test('throws a toast if there is an error', () => {
@@ -166,4 +168,17 @@ describe('CheckoutPage', () => {
const tree = createTestInstance( );
expect(tree.toJSON()).toMatchSnapshot();
});
+
+ test('renders sign in for guest', () => {
+ useCheckoutPage.mockReturnValueOnce({
+ ...defaultTalonProps,
+ activeContent: 'signIn'
+ });
+
+ const tree = createTestInstance( );
+ const { root } = tree;
+ const signInComponent = root.findByType('GuestSignIn');
+
+ expect(signInComponent.props.isActive).toBe(true);
+ });
});
diff --git a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css
index f37811ce18..c8387c44e2 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css
+++ b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css
@@ -17,7 +17,6 @@
.heading_container {
display: grid;
- grid-column-end: span 2;
row-gap: 1rem;
}
@@ -46,9 +45,25 @@
composes: stepper_heading;
}
-.signin_container {
- grid-column: 1 / span 1;
- border-bottom: 1px solid rgb(var(--venia-global-color-border));
+.signInContainer {
+ align-items: center;
+ border: 2px solid rgb(var(--venia-global-color-gray-400));
+ border-radius: 0.375rem;
+ display: grid;
+ gap: 1rem;
+ grid-auto-flow: column;
+ padding: 1rem;
+}
+
+.signInLabel {
+ font-weight: var(--venia-global-fontWeight-semibold);
+}
+
+.signInButton {
+ composes: root_normalPriority from '../Button/button.css';
+ min-height: auto;
+ min-width: auto;
+ padding: 0.25rem 2rem;
}
.empty_cart_container {
@@ -75,11 +90,6 @@
grid-column: 1 / span 1;
}
-.sign_in {
- composes: root from '../LinkButton/linkButton.css';
- margin-bottom: 1.5rem;
-}
-
.summaryContainer {
grid-column: 1 / span 1;
}
@@ -92,10 +102,6 @@
top: 5.5rem;
height: min-content;
}
-
- .signin_container ~ .summaryContainer {
- grid-row: 3 / span 3;
- }
}
.review_order_button {
@@ -126,6 +132,13 @@
gap: 1rem;
}
+ .signInContainer {
+ grid-row-start: 1;
+ grid-auto-flow: row;
+ justify-items: center;
+ margin-bottom: 1rem;
+ }
+
.stepper_heading {
padding-bottom: 1rem;
}
diff --git a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js
index 5a430851db..cff4161a63 100644
--- a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js
+++ b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js
@@ -13,10 +13,10 @@ import { mergeClasses } from '../../classify';
import Button from '../Button';
import { Title } from '../Head';
import Icon from '../Icon';
-import LinkButton from '../LinkButton';
import { fullPageLoadingIndicator } from '../LoadingIndicator';
import StockStatusMessage from '../StockStatusMessage';
import AddressBook from './AddressBook';
+import GuestSignIn from './GuestSignIn';
import OrderSummary from './OrderSummary';
import PaymentInformation from './PaymentInformation';
import PriceAdjustments from './PriceAdjustments';
@@ -46,7 +46,6 @@ const CheckoutPage = props => {
checkoutStep,
customer,
error,
- handleSignIn,
handlePlaceOrder,
hasError,
isCartEmpty,
@@ -65,7 +64,8 @@ const CheckoutPage = props => {
resetReviewOrderButtonClicked,
handleReviewOrder,
reviewOrderButtonClicked,
- toggleActiveContent
+ toggleAddressBookContent,
+ toggleSignInContent
} = talonProps;
const [, { addToast }] = useToasts();
@@ -111,7 +111,7 @@ const CheckoutPage = props => {
defaultMessage: 'Checkout'
});
- if (orderNumber) {
+ if (orderNumber && orderDetailsData) {
return (
{
);
} else {
- const loginButton = isGuestCheckout ? (
-
-
+ const signInContainerElement = isGuestCheckout ? (
+
+
-
+
+
+
+
) : null;
@@ -279,7 +289,6 @@ const CheckoutPage = props => {
);
checkoutContent = (
- {loginButton}
{
/>
{headerText}
+ {signInContainerElement}
@@ -311,7 +321,14 @@ const CheckoutPage = props => {
const addressBookElement = !isGuestCheckout ? (
+ ) : null;
+
+ const signInElement = isGuestCheckout ? (
+
) : null;
@@ -328,6 +345,7 @@ const CheckoutPage = props => {
{checkoutContent}
{addressBookElement}
+ {signInElement}
);
};
diff --git a/packages/venia-ui/lib/components/CommunicationsPage/__tests__/__snapshots__/communicationsPage.spec.js.snap b/packages/venia-ui/lib/components/CommunicationsPage/__tests__/__snapshots__/communicationsPage.spec.js.snap
index 3c4b324f9f..71d138b5a2 100644
--- a/packages/venia-ui/lib/components/CommunicationsPage/__tests__/__snapshots__/communicationsPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/CommunicationsPage/__tests__/__snapshots__/communicationsPage.spec.js.snap
@@ -14,10 +14,16 @@ exports[`renders empty form without data 1`] = `
- Communications
+
- We'd like to stay in touch. Please check the boxes next to the communications you'd like to receive.
+
- Communications
+
- We'd like to stay in touch. Please check the boxes next to the communications you'd like to receive.
+
- Communications
+
- We'd like to stay in touch. Please check the boxes next to the communications you'd like to receive.
+
- Communications
+
- We'd like to stay in touch. Please check the boxes next to the communications you'd like to receive.
+
+
+
+
First Name
@@ -184,7 +190,10 @@ exports[`renders the correct tree 1`] = `
type="submit"
>
- Create an Account
+
@@ -197,6 +206,12 @@ exports[`should not render cancel button if isCancelButtonHidden is true 1`] = `
onReset={[Function]}
onSubmit={[Function]}
>
+
+
+
First Name
@@ -375,7 +390,10 @@ exports[`should not render cancel button if isCancelButtonHidden is true 1`] = `
type="submit"
>
- Create an Account
+
diff --git a/packages/venia-ui/lib/components/CreateAccount/createAccount.css b/packages/venia-ui/lib/components/CreateAccount/createAccount.css
index bdb1b3c1e2..d74da407de 100644
--- a/packages/venia-ui/lib/components/CreateAccount/createAccount.css
+++ b/packages/venia-ui/lib/components/CreateAccount/createAccount.css
@@ -18,26 +18,33 @@
}
.actions {
- align-items: center;
display: grid;
gap: 1rem;
grid-auto-flow: column;
+ justify-content: center;
margin-top: 1rem;
text-align: center;
}
.cancelButton {
- composes: root_normalPriority from '../Button/button.css';
-
- min-width: 6rem;
+ composes: root_lowPriority from '../Button/button.css';
}
.submitButton {
composes: root_highPriority from '../Button/button.css';
-
- min-width: 11rem;
+ grid-column-start: 2;
}
.subscribe {
margin-left: -0.375rem;
}
+
+@media (max-width: 960px) {
+ .actions {
+ grid-auto-flow: row;
+ }
+
+ .submitButton {
+ grid-column-start: auto;
+ }
+}
diff --git a/packages/venia-ui/lib/components/CreateAccount/createAccount.js b/packages/venia-ui/lib/components/CreateAccount/createAccount.js
index f1c5e1ef68..04cbcfc4f5 100644
--- a/packages/venia-ui/lib/components/CreateAccount/createAccount.js
+++ b/packages/venia-ui/lib/components/CreateAccount/createAccount.js
@@ -48,7 +48,7 @@ const CreateAccount = props => {
className={classes.cancelButton}
disabled={isDisabled}
type="button"
- priority="normal"
+ priority="low"
onClick={handleCancel}
>
{
initialValues={initialValues}
onSubmit={handleSubmit}
>
+
+
+
{
/>
- {cancelButton}
{submitButton}
+ {cancelButton}
);
@@ -171,7 +177,7 @@ CreateAccount.propTypes = {
lastName: string
}),
isCancelButtonHidden: bool,
- onSubmit: func.isRequired,
+ onSubmit: func,
onCancel: func
};
diff --git a/packages/venia-ui/lib/components/Dialog/__tests__/__snapshots__/dialog.spec.js.snap b/packages/venia-ui/lib/components/Dialog/__tests__/__snapshots__/dialog.spec.js.snap
index d663a877eb..9b0b8a8a79 100644
--- a/packages/venia-ui/lib/components/Dialog/__tests__/__snapshots__/dialog.spec.js.snap
+++ b/packages/venia-ui/lib/components/Dialog/__tests__/__snapshots__/dialog.spec.js.snap
@@ -45,7 +45,10 @@ exports[`does not render a close X button in modal mode 1`] = `
- Cancel
+
- Confirm
+
@@ -147,7 +153,10 @@ exports[`renders a Dialog with disabled buttons 1`] = `
- Cancel
+
- Confirm
+
@@ -325,7 +337,10 @@ exports[`renders a basic Dialog 1`] = `
- Cancel
+
- Confirm
+
@@ -427,7 +445,10 @@ exports[`renders a dialog with only the confirm button disabled 1`] = `
- Cancel
+
- Confirm
+
@@ -533,7 +557,10 @@ exports[`should render children even if dialog is hidden and if shouldUnmountOnH
- Cancel
+
- Confirm
+
@@ -559,101 +589,7 @@ exports[`should unmount children if dialog is hidden and if shouldUnmountOnHide
-
-
-
-
-
- Unit Test Dialog Title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
- Confirm
-
-
-
-
-
-
-
+ />
`;
@@ -737,7 +673,10 @@ exports[`supports modifying title and button texts 1`] = `
- Unit Test Cancel
+
- Unit Test Confirm
+
diff --git a/packages/venia-ui/lib/components/Dialog/dialog.js b/packages/venia-ui/lib/components/Dialog/dialog.js
index cade1db062..81baa7c47d 100644
--- a/packages/venia-ui/lib/components/Dialog/dialog.js
+++ b/packages/venia-ui/lib/components/Dialog/dialog.js
@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
import { bool, func, shape, string, object } from 'prop-types';
import { Form } from 'informed';
@@ -112,46 +112,33 @@ const Dialog = props => {
) : null;
- const contents = useMemo(() => {
- if (isOpen) {
- return children;
- } else {
- if (shouldUnmountOnHide) {
- return null;
- } else {
- return children;
- }
- }
- }, [children, isOpen, shouldUnmountOnHide]);
+ const maybeForm =
+ isOpen || !shouldUnmountOnHide ? (
+
+ {/* The Mask. */}
+
+ {/* The Dialog. */}
+
+
+ {title}
+ {maybeCloseXButton}
+
+
+
{children}
+ {maybeButtons}
+
+
+
+ ) : null;
return (
-
-
- {/* The Mask. */}
-
- {/* The Dialog. */}
-
-
- {title}
- {maybeCloseXButton}
-
-
-
{contents}
- {maybeButtons}
-
-
-
-
+
);
};
diff --git a/packages/venia-ui/lib/components/Footer/__tests__/__snapshots__/footer.spec.js.snap b/packages/venia-ui/lib/components/Footer/__tests__/__snapshots__/footer.spec.js.snap
index 1745bd0e49..3a8c7ad8d7 100644
--- a/packages/venia-ui/lib/components/Footer/__tests__/__snapshots__/footer.spec.js.snap
+++ b/packages/venia-ui/lib/components/Footer/__tests__/__snapshots__/footer.spec.js.snap
@@ -18,7 +18,10 @@ exports[`footer renders copyright 1`] = `
href="/a"
onClick={[Function]}
>
- a
+
- b
+
@@ -44,7 +50,10 @@ exports[`footer renders copyright 1`] = `
href="/1"
onClick={[Function]}
>
- 1
+
- 2
+
@@ -65,12 +77,18 @@ exports[`footer renders copyright 1`] = `
- Follow Us!
+
- Lorem ipsum dolor sit amet, consectetur adipsicing elit, sed do eiusmod tempor incididunt ut labore et dolore.
+
- Terms of Use
+
- Privacy Policy
+
- Cancel
+
- Submit
+
diff --git a/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.css b/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.css
index 31ce814bc5..777a270058 100644
--- a/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.css
+++ b/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.css
@@ -14,7 +14,7 @@
}
.cancelButton {
- composes: root_normalPriority from '../../Button/button.css';
+ composes: root_lowPriority from '../../Button/button.css';
min-width: 9rem;
}
diff --git a/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.js b/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.js
index 55e142b592..6129c78552 100644
--- a/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.js
+++ b/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/forgotPasswordForm.js
@@ -39,7 +39,7 @@ const ForgotPasswordForm = props => {
className={classes.cancelButton}
disabled={isResettingPassword}
type="button"
- priority="normal"
+ priority="low"
onClick={onCancel}
>
- Recover Password
+
- Recover Password
+
Please enter the email address associated with this account.
diff --git a/packages/venia-ui/lib/components/LegacyMiniCart/editFormFragment.gql.js b/packages/venia-ui/lib/components/LegacyMiniCart/editFormFragment.gql.js
new file mode 100644
index 0000000000..7b6e6d38ca
--- /dev/null
+++ b/packages/venia-ui/lib/components/LegacyMiniCart/editFormFragment.gql.js
@@ -0,0 +1,50 @@
+import { gql } from '@apollo/client';
+
+export const EditFormFragment = gql`
+ fragment EditFormFragment on ProductInterface {
+ id
+ name
+ sku
+ url_key
+ __typename
+ ... on ConfigurableProduct {
+ configurable_options {
+ attribute_code
+ attribute_id
+ id
+ label
+ values {
+ default_label
+ label
+ store_label
+ use_default_value
+ value_index
+ swatch_data {
+ ... on ImageSwatchData {
+ thumbnail
+ }
+ value
+ }
+ }
+ }
+ variants {
+ attributes {
+ code
+ value_index
+ }
+ product {
+ id
+ media_gallery_entries {
+ id
+ disabled
+ file
+ label
+ position
+ }
+ sku
+ stock_status
+ }
+ }
+ }
+ }
+`;
diff --git a/packages/venia-ui/lib/components/LegacyMiniCart/editItem.js b/packages/venia-ui/lib/components/LegacyMiniCart/editItem.js
index 765e6bd436..024684d209 100644
--- a/packages/venia-ui/lib/components/LegacyMiniCart/editItem.js
+++ b/packages/venia-ui/lib/components/LegacyMiniCart/editItem.js
@@ -5,6 +5,7 @@ import { useEditItem } from '@magento/peregrine/lib/talons/LegacyMiniCart/useEdi
import LoadingIndicator from '../LoadingIndicator';
import CartOptions from './cartOptions';
+import { EditFormFragment } from './editFormFragment.gql';
const ERROR_TEXT = 'Unable to fetch item options.';
const LOADING_TEXT = 'Fetching Item Options...';
@@ -55,51 +56,10 @@ export const PRODUCT_DETAILS = gql`
query productDetailBySku($sku: String) {
products(filter: { sku: { eq: $sku } }) {
items {
- __typename
id
- name
- sku
- url_key
- ... on ConfigurableProduct {
- configurable_options {
- attribute_code
- attribute_id
- id
- label
- values {
- default_label
- label
- store_label
- use_default_value
- value_index
- swatch_data {
- ... on ImageSwatchData {
- thumbnail
- }
- value
- }
- }
- }
- variants {
- attributes {
- code
- value_index
- }
- product {
- id
- media_gallery_entries {
- id
- disabled
- file
- label
- position
- }
- sku
- stock_status
- }
- }
- }
+ ...EditFormFragment
}
}
}
+ ${EditFormFragment}
`;
diff --git a/packages/venia-ui/lib/components/MagentoRoute/magentoRoute.gql.js b/packages/venia-ui/lib/components/MagentoRoute/magentoRoute.gql.js
deleted file mode 100644
index bcb8a52fbe..0000000000
--- a/packages/venia-ui/lib/components/MagentoRoute/magentoRoute.gql.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { gql } from '@apollo/client';
-
-export const GET_STORE_CODE = gql`
- query getStoreCode {
- storeConfig {
- id
- code
- }
- }
-`;
-
-export default {
- queries: {
- getStoreCode: GET_STORE_CODE
- }
-};
diff --git a/packages/venia-ui/lib/components/MagentoRoute/magentoRoute.js b/packages/venia-ui/lib/components/MagentoRoute/magentoRoute.js
index 971c718d92..45f216e7e7 100644
--- a/packages/venia-ui/lib/components/MagentoRoute/magentoRoute.js
+++ b/packages/venia-ui/lib/components/MagentoRoute/magentoRoute.js
@@ -1,44 +1,36 @@
import React from 'react';
import { useIntl } from 'react-intl';
import ErrorView from '../ErrorView';
-import {
- INTERNAL_ERROR,
- NOT_FOUND,
- useMagentoRoute
-} from '@magento/peregrine/lib/talons/MagentoRoute';
+import { useMagentoRoute } from '@magento/peregrine/lib/talons/MagentoRoute';
import { fullPageLoadingIndicator } from '../LoadingIndicator';
-import { GET_STORE_CODE } from './magentoRoute.gql';
const MESSAGES = new Map()
- .set(NOT_FOUND, 'That page could not be found. Please try again.')
- .set(INTERNAL_ERROR, 'Something went wrong. Please try again.');
+ .set('NOT_FOUND', 'That page could not be found. Please try again.')
+ .set('INTERNAL_ERROR', 'Something went wrong. Please try again.');
const MagentoRoute = () => {
const { formatMessage } = useIntl();
- const magentoRouteProps = { getStoreCode: GET_STORE_CODE };
- // If we have a specific store view code configured pass it into the url resolver
-
- const talonProps = useMagentoRoute(magentoRouteProps);
+ const talonProps = useMagentoRoute();
const {
component: RootComponent,
id,
isLoading,
- isRedirect,
- routeError
+ isNotFound,
+ isRedirect
} = talonProps;
if (isLoading || isRedirect) {
return fullPageLoadingIndicator;
} else if (RootComponent) {
return ;
- } else if (routeError === NOT_FOUND) {
+ } else if (isNotFound) {
return (
{formatMessage({
id: 'magentoRoute.routeError',
- defaultMessage: MESSAGES.get(routeError)
+ defaultMessage: MESSAGES.get('NOT_FOUND')
})}
@@ -50,7 +42,7 @@ const MagentoRoute = () => {
{formatMessage({
id: 'magentoRoute.internalError',
- defaultMessage: MESSAGES.get(INTERNAL_ERROR)
+ defaultMessage: MESSAGES.get('INTERNAL_ERROR')
})}
diff --git a/packages/venia-ui/lib/components/MiniCart/ProductList/__tests__/__snapshots__/item.spec.js.snap b/packages/venia-ui/lib/components/MiniCart/ProductList/__tests__/__snapshots__/item.spec.js.snap
index fe0fb53461..4057a0897d 100644
--- a/packages/venia-ui/lib/components/MiniCart/ProductList/__tests__/__snapshots__/item.spec.js.snap
+++ b/packages/venia-ui/lib/components/MiniCart/ProductList/__tests__/__snapshots__/item.spec.js.snap
@@ -66,7 +66,15 @@ www.venia.com/p1 2560w"
- Qty :
+
00
- ea.
+
- Qty :
+
00
- ea.
+
- Qty :
+
00
- ea.
+
- Items
+
- Subtotal:
+
$420
@@ -102,7 +113,10 @@ exports[`it renders correctly 1`] = `
/>
- CHECKOUT
+
- Edit Shopping Bag
+
diff --git a/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/__snapshots__/resetPassword.spec.js.snap b/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/__snapshots__/resetPassword.spec.js.snap
index f9c9ef1c7b..b7239d9d0e 100644
--- a/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/__snapshots__/resetPassword.spec.js.snap
+++ b/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/__snapshots__/resetPassword.spec.js.snap
@@ -10,7 +10,10 @@ exports[`should render error message if token is falsy 1`] = `
- Uh oh, something went wrong. Check the link or try again.
+
@@ -30,7 +33,10 @@ exports[`should render formErrors 1`] = `
onSubmit={[Function]}
>
- Please enter your email address and new password.
+
@@ -118,7 +124,10 @@ exports[`should render formErrors 1`] = `
type="submit"
>
- Save Password
+
@@ -139,7 +148,10 @@ exports[`should render properly 1`] = `
onSubmit={[Function]}
>
- Please enter your email address and new password.
+
@@ -227,7 +239,10 @@ exports[`should render properly 1`] = `
type="submit"
>
- Save Password
+
@@ -244,7 +259,10 @@ exports[`should render success message if hasCompleted is true 1`] = `
- Your new password has been saved. Please use this password to sign into your Account.
+
diff --git a/packages/venia-ui/lib/components/Navigation/__tests__/navigation.spec.js b/packages/venia-ui/lib/components/Navigation/__tests__/navigation.spec.js
index d0188952df..8ce0abe0d5 100755
--- a/packages/venia-ui/lib/components/Navigation/__tests__/navigation.spec.js
+++ b/packages/venia-ui/lib/components/Navigation/__tests__/navigation.spec.js
@@ -1,8 +1,6 @@
import React from 'react';
import { createTestInstance } from '@magento/peregrine';
-import { useAppContext } from '@magento/peregrine/lib/context/app';
-import { useUserContext } from '@magento/peregrine/lib/context/user';
import { useNavigation } from '@magento/peregrine/lib/talons/Navigation/useNavigation';
import NavHeader from '../navHeader';
@@ -21,61 +19,7 @@ jest.mock('../navHeader', () => () => );
jest.mock('../../Header/storeSwitcher', () => () => 'StoreSwitcher');
jest.mock('../../Header/currencySwitcher', () => () => 'CurrencySwitcher');
-jest.mock('@magento/peregrine/lib/context/app', () => {
- const closeDrawer = jest.fn();
- const useAppContext = jest.fn(() => [
- { drawer: 'nav' },
- {
- actions: {},
- closeDrawer
- }
- ]);
-
- return { useAppContext };
-});
-
-jest.mock('@magento/peregrine/lib/context/catalog', () => {
- const updateCategories = jest.fn();
- const useCatalogContext = jest.fn(() => [
- {
- categories: {
- 1: { parentId: 0 },
- 2: { parentId: 1 }
- },
- rootCategoryId: 1
- },
- {
- actions: { updateCategories }
- }
- ]);
-
- return { useCatalogContext };
-});
-
-jest.mock('@magento/peregrine/lib/context/user', () => {
- const getUserDetails = jest.fn();
- const useUserContext = jest.fn(() => [{}, { getUserDetails }]);
-
- return { useUserContext };
-});
-
-jest.mock('@magento/peregrine/lib/hooks/useAwaitQuery', () => {
- const useAwaitQuery = jest
- .fn()
- .mockResolvedValue({ data: { customer: {} } });
-
- return { useAwaitQuery };
-});
-
-jest.mock('@magento/peregrine/lib/talons/Navigation/useNavigation', () => {
- const useNavigationTalon = jest.requireActual(
- '@magento/peregrine/lib/talons/Navigation/useNavigation'
- );
-
- const spy = jest.spyOn(useNavigationTalon, 'useNavigation');
-
- return Object.assign(useNavigationTalon, { useNavigation: spy });
-});
+jest.mock('@magento/peregrine/lib/talons/Navigation/useNavigation');
/*
* Tests.
@@ -87,6 +31,10 @@ const talonProps = {
};
test('renders correctly when open', () => {
+ useNavigation.mockReturnValueOnce({
+ ...talonProps,
+ isOpen: true
+ });
const instance = createTestInstance( );
expect(instance.toJSON()).toMatchSnapshot();
@@ -94,10 +42,10 @@ test('renders correctly when open', () => {
});
test('renders correctly when closed', () => {
- useAppContext.mockImplementationOnce(() => [
- { drawer: null },
- { closeDrawer: jest.fn() }
- ]);
+ useNavigation.mockReturnValueOnce({
+ ...talonProps,
+ isOpen: false
+ });
const instance = createTestInstance( );
@@ -120,14 +68,6 @@ test('authModal is rendered when hasModal is true', () => {
expect(instance.toJSON()).toMatchSnapshot();
});
-test('getUserDetails() is called on mount', () => {
- const { getUserDetails } = useUserContext()[1];
-
- createTestInstance( );
-
- expect(getUserDetails).toHaveBeenCalledTimes(1);
-});
-
test('view is passed to NavHeader', () => {
// Arrange.
const expected = 'UNIT_TEST';
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/billingInformation.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/billingInformation.spec.js.snap
index e7d288395a..f387ff88f7 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/billingInformation.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/billingInformation.spec.js.snap
@@ -9,22 +9,14 @@ exports[`should render properly 1`] = `
id="orderDetails.billingInformationLabel"
/>
-
-
- Gooseton
-
-
- Jr
-
-
-
+
+ Gooseton Jr
+
+
2134, Apt 123, Goose Drive
-
-
- Austin, TX, 78451
-
+
- USA
+ Austin, TX 78451 USA
`;
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/item.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/item.spec.js.snap
index e7768276df..7e5b589bf2 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/item.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/item.spec.js.snap
@@ -4,7 +4,7 @@ exports[`should render properly 1`] = `
-
- Product 1
+
@@ -45,8 +56,7 @@ exports[`should render properly 1`] = `
-
-
- $100.00
-
-
+
-
+ $
-
+
+ 100
+
+
+ .
+
+
+ 00
+
+
-
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/items.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/items.spec.js.snap
index 422672fdc0..8187aceb7e 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/items.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/items.spec.js.snap
@@ -2,77 +2,88 @@
exports[`should render properly 1`] = `
-
+
+
+
+
+
-
+
-
+ url_key="helena-cardigan"
+ url_suffix=".html"
+ />
+
`;
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderDetails.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderDetails.spec.js.snap
index a13a2c0d56..5bbb6126c2 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderDetails.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderDetails.spec.js.snap
@@ -225,8 +225,7 @@ exports[`should render properly 1`] = `
-
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderTotal.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderTotal.spec.js.snap
index 6e3b8ecd08..94890d5123 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderTotal.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/orderTotal.spec.js.snap
@@ -1,92 +1,244 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render properly 1`] = `
+exports[`should conditionally render discount row 1`] = `
-
-
+ $
+
+
+ 1
+
+
+ ,
+
+
+ 234
+
+
+ .
+
+
+ 00
+
+
+
+
+
+
+
+
+
+ $
+
+
+ 34
+
+
+ .
+
+
+ 00
+
+
+
+
+
+
+
+
+
+ $
+
+
+ 12
+
+
+ .
+
+
+ 00
+
+
+
+
+
+
+
+
+
+ $
+
+
+ 1
+
+
+ ,
+
+
+ 434
+
+
+ .
+
+
+ 00
+
+
+
+
+`;
+
+exports[`should render properly 1`] = `
+
+
+
+
+
+
+
+
+
+ $
+
+
+ 1
+
+
+ ,
+
+
+ 234
+
+
+ .
+
+
+ 00
+
+
-
-
+
+ $
+
+
+ 123
+
+
+ .
+
+
+ 00
+
-
-
+
+ $
+
+
+ 34
+
+
+ .
+
+
+ 00
+
-
-
+
+ $
+
+
+ 12
+
+
+ .
+
+
+ 00
+
-
-
+
+ $
+
+
+ 1
+
+
+ ,
+
+
+ 434
+
+
+ .
+
+
+ 00
+
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/paymentMethod.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/paymentMethod.spec.js.snap
index e4a39975ae..35cc376525 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/paymentMethod.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/paymentMethod.spec.js.snap
@@ -3,17 +3,13 @@
exports[`should render properly 1`] = `
- Credit Card - Visa
-
-
- 1234
+ Credit Card
`;
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingInformation.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingInformation.spec.js.snap
index d5135082e5..736a072150 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingInformation.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingInformation.spec.js.snap
@@ -1,30 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render properly 1`] = `
+exports[`should render placeholder label without data 1`] = `
+`;
+
+exports[`should render properly 1`] = `
+
-
- Gooseton
-
-
- Jr
-
+
-
+
+ Gooseton Jr
+
+
Goose Dr
-
-
- Austin, TX, 78759
-
+
- US
+ Austin, TX 78759 US
`;
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingMethod.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingMethod.spec.js.snap
index 83f2d4da21..6082c2f00a 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingMethod.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/__snapshots__/shippingMethod.spec.js.snap
@@ -1,26 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should render placeholder text without shipments 1`] = `
+
+`;
+
exports[`should render properly 1`] = `
`;
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/billingInformation.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/billingInformation.spec.js
index f3b2aa762a..ee5deb0d77 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/billingInformation.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/billingInformation.spec.js
@@ -15,8 +15,8 @@ const defaultData = {
firstname: 'Gooseton',
lastname: 'Jr',
postcode: '78451',
- region_id: 'TX',
- street: '2134, Apt 123, Goose Drive'
+ region: 'TX',
+ street: ['2134, Apt 123, Goose Drive']
};
test('should render properly', () => {
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/item.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/item.spec.js
index b81686ce34..7e1bb6c0bb 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/item.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/item.spec.js
@@ -2,21 +2,28 @@ import React from 'react';
import { createTestInstance } from '@magento/peregrine';
import Item from '../item';
+import PlaceholderImage from '../../../Image/placeholderImage';
-jest.mock('@magento/venia-drivers', () => ({
- Link: props =>
,
- resourceUrl: url => url
+jest.mock('react-router-dom', () => ({
+ Link: props =>
}));
-jest.mock('react-intl', () => ({
- FormattedMessage: props => (
-
- )
-}));
+jest.mock(
+ '@magento/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext',
+ () => ({
+ useOrderHistoryContext: jest
+ .fn()
+ .mockReturnValue({ productURLSuffix: '.html' })
+ })
+);
const defaultProps = {
product_name: 'Product 1',
- product_sale_price: '$100.00',
+ product_sale_price: {
+ currency: 'USD',
+ value: 100
+ },
+ product_url_key: 'carina-cardigan',
quantity_ordered: 3,
selected_options: [
{
@@ -24,9 +31,7 @@ const defaultProps = {
value: 'Black'
}
],
- thumbnail: 'www.venia.com/product1-thumbnail.jpg',
- url_key: 'carina-cardigan',
- url_suffix: '.html'
+ thumbnail: { url: 'https://www.venia.com/product1-thumbnail.jpg' }
};
test('should render properly', () => {
@@ -34,3 +39,15 @@ test('should render properly', () => {
expect(tree.toJSON()).toMatchSnapshot();
});
+
+test('should render placeholder without thumbnail', () => {
+ const props = {
+ ...defaultProps,
+ thumbnail: undefined
+ };
+ const tree = createTestInstance(
);
+ const { root } = tree;
+ const imagePlaceholderNode = root.findByType(PlaceholderImage);
+
+ expect(imagePlaceholderNode).toBeTruthy();
+});
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/items.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/items.spec.js
index 48b04b1ba6..7fed511b5e 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/items.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/items.spec.js
@@ -7,12 +7,6 @@ jest.mock('../item', () => props => (
));
-jest.mock('react-intl', () => ({
- FormattedMessage: props => (
-
- )
-}));
-
const defaultProps = {
data: {
imagesData: [
@@ -53,6 +47,7 @@ const defaultProps = {
product_name: 'Product 3',
product_sale_price: '$100.00',
product_sku: 'VA03',
+ product_url_key: 'valeria-two-layer-tank',
selected_options: [
{
label: 'Color',
@@ -66,6 +61,7 @@ const defaultProps = {
product_name: 'Product 4',
product_sale_price: '$100.00',
product_sku: 'VP08',
+ product_url_key: 'chloe-silk-shell',
selected_options: [
{
label: 'Color',
@@ -79,6 +75,7 @@ const defaultProps = {
product_name: 'Product 5',
product_sale_price: '$100.00',
product_sku: 'VSW09',
+ product_url_key: 'helena-cardigan',
selected_options: [
{
label: 'Color',
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderDetails.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderDetails.spec.js
index bc46895e42..247993a4a1 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderDetails.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderDetails.spec.js
@@ -19,11 +19,6 @@ jest.mock('../orderTotal', () => props => (
));
jest.mock('../items', () => props =>
);
-jest.mock('react-intl', () => ({
- FormattedMessage: props => (
-
- )
-}));
const defaultProps = {
imagesData: [
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderTotal.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderTotal.spec.js
index 22b7710680..f5fcbdcad3 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderTotal.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/orderTotal.spec.js
@@ -3,25 +3,19 @@ import { createTestInstance } from '@magento/peregrine';
import OrderTotal from '../orderTotal';
-jest.mock('@magento/peregrine', () => ({
- createTestInstance: jest.requireActual('@magento/peregrine')
- .createTestInstance,
- Price: props =>
-}));
-
-jest.mock('react-intl', () => ({
- FormattedMessage: props => (
-
- )
-}));
-
const defaultProps = {
data: {
discounts: [
{
amount: {
currency: 'USD',
- value: 123
+ value: 62
+ }
+ },
+ {
+ amount: {
+ currency: 'USD',
+ value: 61
}
}
],
@@ -49,3 +43,15 @@ test('should render properly', () => {
expect(tree.toJSON()).toMatchSnapshot();
});
+
+test('should conditionally render discount row', () => {
+ const props = {
+ data: {
+ ...defaultProps.data,
+ discounts: null
+ }
+ };
+ const tree = createTestInstance(
);
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/paymentMethod.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/paymentMethod.spec.js
index edb7d5e1f6..6dd4dd594d 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/paymentMethod.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/paymentMethod.spec.js
@@ -3,28 +3,8 @@ import { createTestInstance } from '@magento/peregrine';
import PaymentMethod from '../paymentMethod';
-jest.mock('react-intl', () => ({
- FormattedMessage: props => (
-
- )
-}));
-
const defaultProps = {
- data: [
- {
- type: 'Credit Card',
- additional_data: [
- {
- name: 'card_type',
- value: 'Visa'
- },
- {
- name: 'last_four',
- value: '1234'
- }
- ]
- }
- ]
+ data: [{ name: 'Credit Card' }]
};
test('should render properly', () => {
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingInformation.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingInformation.spec.js
index 1fa8d87f1e..0b577384de 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingInformation.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingInformation.spec.js
@@ -3,12 +3,6 @@ import { createTestInstance } from '@magento/peregrine';
import ShippingInformation from '../shippingInformation';
-jest.mock('react-intl', () => ({
- FormattedMessage: props => (
-
- )
-}));
-
const defaultProps = {
data: {
city: 'Austin',
@@ -16,8 +10,8 @@ const defaultProps = {
firstname: 'Gooseton',
lastname: 'Jr',
postcode: '78759',
- region_id: 'TX',
- street: 'Goose Dr',
+ region: 'TX',
+ street: ['Goose Dr'],
telephone: '9123456789'
}
};
@@ -27,3 +21,9 @@ test('should render properly', () => {
expect(tree.toJSON()).toMatchSnapshot();
});
+
+test('should render placeholder label without data', () => {
+ const tree = createTestInstance(
);
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingMethod.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingMethod.spec.js
index c872d259b2..791f74c3a3 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingMethod.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/__tests__/shippingMethod.spec.js
@@ -3,12 +3,6 @@ import { createTestInstance } from '@magento/peregrine';
import ShippingMethod from '../shippingMethod';
-jest.mock('react-intl', () => ({
- FormattedMessage: props => (
-
- )
-}));
-
const defaultProps = {
data: {
shipments: [
@@ -16,10 +10,20 @@ const defaultProps = {
id: '1',
tracking: [
{
- carrier: 'Fedex',
number: 'FEDEX5885541235452125'
}
]
+ },
+ {
+ id: '2',
+ tracking: [
+ {
+ number: 'USPS8645'
+ },
+ {
+ number: 'UPS0001'
+ }
+ ]
}
],
shipping_method: 'Free'
@@ -31,3 +35,11 @@ test('should render properly', () => {
expect(tree.toJSON()).toMatchSnapshot();
});
+
+test('should render placeholder text without shipments', () => {
+ const tree = createTestInstance(
+
+ );
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.css b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.css
index 17bc4aef6a..690a01b360 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.css
@@ -1,26 +1,9 @@
.root {
display: grid;
- row-gap: 0.35rem;
+ row-gap: 0.375rem;
}
.heading {
- grid-row: 1 / span 1;
font-weight: var(--venia-global-fontWeight-bold);
- padding-bottom: 0.35rem;
-}
-
-.name {
- grid-row: 2 / span 1;
-}
-
-.addressLine1 {
- grid-row: 3 / span 1;
-}
-
-.addressLine2 {
- grid-row: 4 / span 1;
-}
-
-.country {
- grid-row: 5 / span 1;
+ padding-bottom: 0.375rem;
}
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.js
index 33420fbb7b..2d9eaed61b 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/billingInformation.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { shape, string } from 'prop-types';
+import { arrayOf, shape, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
@@ -14,12 +14,20 @@ const BillingInformation = props => {
firstname,
lastname,
postcode,
- region_id,
+ region,
street
} = data;
const classes = mergeClasses(defaultClasses, propsClasses);
- const additionalAddressString = `${city}, ${region_id}, ${postcode}`;
+ const additionalAddressString = `${city}, ${region} ${postcode} ${country_code}`;
+ const fullName = `${firstname} ${lastname}`;
+ const streetRows = street.map((row, index) => {
+ return (
+
+ {row}
+
+ );
+ });
return (
@@ -29,15 +37,11 @@ const BillingInformation = props => {
defaultMessage="Billing Information"
/>
-
- {firstname}
- {lastname}
-
-
{street}
-
+
{fullName}
+ {streetRows}
+
{additionalAddressString}
-
{country_code}
);
};
@@ -49,9 +53,8 @@ BillingInformation.propTypes = {
root: string,
heading: string,
name: string,
- addressLine1: string,
- addressLine2: string,
- country: string
+ streetRow: string,
+ additionalAddress: string
}),
data: shape({
city: string,
@@ -59,7 +62,7 @@ BillingInformation.propTypes = {
firstname: string,
lastname: string,
postcode: string,
- region_id: string,
- street: string
+ region: string,
+ street: arrayOf(string)
})
};
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.css b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.css
index afff48fc89..58f96ec2bc 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.css
@@ -1,95 +1,39 @@
.root {
display: grid;
- grid-template-columns: repeat(3, auto) 1fr auto;
- row-gap: 0.35rem;
+ grid-template-columns: auto repeat(3, 1fr) auto;
+ grid-template-rows: auto 1fr;
+ row-gap: 0.375rem;
column-gap: 1rem;
}
.thumbnailContainer {
- grid-row: 1 / span 3;
- grid-column: 1 / span 1;
+ grid-row: 1 / -1;
}
-.thumbnail {
-}
-
-.name {
- grid-row: 1 / span 1;
- grid-column: 2 / span 1;
+.nameContainer {
+ grid-column: 2 / -1;
font-weight: var(--venia-global-fontWeight-bold);
}
-.options {
- grid-row: 2 / span 1;
- grid-column: 2 / span 1;
-}
-
-.quantity {
- grid-row: 2 / span 1;
- grid-column: 3 / span 1;
-}
-
-.price {
- grid-row: 2 / span 1;
- grid-column: 4 / span 1;
-}
-
.buyAgainButton {
- grid-row: 1 / span 2;
- grid-column: 5 / span 1;
- text-decoration: underline;
-}
-
-.returnThisButton {
- grid-row: 3 / span 1;
- grid-column: 5 / span 1;
+ align-self: start;
+ grid-column-end: -1;
text-decoration: underline;
+ /** Hide until PWA-979 is completed */
+ visibility: hidden;
}
@media (max-width: 960px) {
.root {
display: grid;
grid-template-columns: auto 1fr;
+ grid-template-rows: repeat(5, auto);
row-gap: 0.5rem;
column-gap: 1rem;
}
- .thumbnailContainer {
- grid-row: 1 / span 6;
- grid-column: 1 / span 1;
- }
-
- .name {
- grid-row: 1 / span 1;
- grid-column: 2 / span 1;
- }
-
- .options {
- grid-row: 2 / span 1;
- grid-column: 2 / span 1;
- }
-
- .quantity {
- grid-row: 3 / span 1;
- grid-column: 2 / span 1;
- }
-
- .price {
- grid-row: 4 / span 1;
- grid-column: 2 / span 1;
- }
-
.buyAgainButton {
- grid-row: 5 / span 1;
- grid-column: 2 / span 1;
- text-decoration: underline;
- width: fit-content;
- }
-
- .returnThisButton {
- grid-row: 6 / span 1;
- grid-column: 2 / span 1;
- text-decoration: underline;
- width: fit-content;
+ grid-column-end: auto;
+ justify-self: start;
}
}
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.js
index 1dcccfa26c..97744dd324 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/item.js
@@ -1,30 +1,31 @@
import React, { useMemo } from 'react';
import { shape, string, number, arrayOf } from 'prop-types';
import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router-dom';
+import { useOrderHistoryContext } from '@magento/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext';
-import { Link, resourceUrl } from '@magento/venia-drivers';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
-
import Button from '../../Button';
import ProductOptions from '../../LegacyMiniCart/productOptions';
import Image from '../../Image';
-
+import Price from '../../Price';
import defaultClasses from './item.css';
+import PlaceholderImage from '../../Image/placeholderImage';
const Item = props => {
const {
product_name,
product_sale_price,
+ product_url_key,
quantity_ordered,
selected_options,
- thumbnail,
- url_key,
- url_suffix
+ thumbnail
} = props;
- const itemLink = useMemo(() => resourceUrl(`/${url_key}${url_suffix}`), [
- url_key,
- url_suffix
- ]);
+ const { currency, value: unitPrice } = product_sale_price;
+
+ const orderHistoryState = useOrderHistoryContext();
+ const { productURLSuffix } = orderHistoryState;
+ const itemLink = `${product_url_key}${productURLSuffix}`;
const mappedOptions = useMemo(
() =>
selected_options.map(option => ({
@@ -35,19 +36,25 @@ const Item = props => {
);
const classes = mergeClasses(defaultClasses, props.classes);
+ const thumbnailProps = {
+ alt: product_name,
+ classes: { root: classes.thumbnail },
+ width: 50
+ };
+ const thumbnailElement = thumbnail ? (
+
+ ) : (
+
+ );
+
return (
-
-
-
- {product_name}
+ {thumbnailElement}
+
+ {product_name}
+
{
}}
/>
- {product_sale_price}
+
{
// TODO will be implemented in PWA-979
@@ -76,18 +85,6 @@ const Item = props => {
defaultMessage="Buy Again"
/>
- {
- // TODO will be implemented in PWA-979
- console.log('Returning the Item');
- }}
- className={classes.returnThisButton}
- >
-
-
);
};
@@ -103,11 +100,14 @@ Item.propTypes = {
options: string,
quantity: string,
price: string,
- buyAgainButton: string,
- returnThisButton: string
+ buyAgainButton: string
}),
product_name: string.isRequired,
- product_sale_price: string.isRequired,
+ product_sale_price: shape({
+ currency: string,
+ value: number
+ }).isRequired,
+ product_url_key: string.isRequired,
quantity_ordered: number.isRequired,
selected_options: arrayOf(
shape({
@@ -117,7 +117,5 @@ Item.propTypes = {
).isRequired,
thumbnail: shape({
url: string
- }).isRequired,
- url_key: string.isRequired,
- url_suffix: string.isRequired
+ })
};
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.css b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.css
index 1ed33c7437..5d7f351a3b 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.css
@@ -1,7 +1,15 @@
.root {
+ padding-right: 1.5rem;
+}
+
+.heading {
+ font-weight: var(--venia-global-fontWeight-bold);
+ padding-bottom: 0.75rem;
+}
+
+.itemsContainer {
display: grid;
row-gap: 2.5rem;
- padding-right: 1.5rem;
}
@media (max-width: 960px) {
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.js
index 0a0b222009..2f56b188b2 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/items.js
@@ -6,6 +6,7 @@ import { mergeClasses } from '@magento/venia-ui/lib/classify';
import Item from './item';
import defaultClasses from './items.css';
+import { FormattedMessage } from 'react-intl';
const Items = props => {
const { items, imagesData } = props.data;
@@ -15,17 +16,31 @@ const Items = props => {
const mappedImagesData = {};
imagesData.forEach(imageData => {
- mappedImagesData[imageData.sku] = imageData;
+ mappedImagesData[imageData.url_key] = imageData;
});
return mappedImagesData;
}, [imagesData]);
const itemsComponent = items.map(item => (
-
+
));
- return
{itemsComponent}
;
+ return (
+
+
+
+
+
{itemsComponent}
+
+ );
};
export default Items;
@@ -39,8 +54,12 @@ Items.propTypes = {
shape({
id: string,
product_name: string,
- product_sale_price: string,
+ product_sale_price: shape({
+ currency: string,
+ value: number
+ }),
product_sku: string,
+ product_url_key: string,
selected_options: arrayOf(
shape({
label: string,
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.css b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.css
index c8a8a5a1df..2f9b2da9d8 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.css
@@ -48,6 +48,8 @@
grid-template-columns: auto 1fr;
width: fit-content;
margin: auto;
+ /** Hide until PWA-978 is completed */
+ visibility: hidden;
}
.printLabel {
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.js
index 82877312a4..513912a217 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderDetails.js
@@ -106,13 +106,16 @@ OrderDetails.propTypes = {
lastname: string,
postcode: string,
region_id: string,
- street: string
+ street: arrayOf(string)
}),
items: arrayOf(
shape({
id: string,
product_name: string,
- product_sale_price: string,
+ product_sale_price: shape({
+ currency: string,
+ value: number
+ }),
product_sku: string,
selected_options: arrayOf(
shape({
@@ -141,7 +144,7 @@ OrderDetails.propTypes = {
lastname: string,
postcode: string,
region_id: string,
- street: string,
+ street: arrayOf(string),
telephone: string
}),
shipping_method: string,
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.css b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.css
index 49e2e30caf..b03659499c 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.css
@@ -7,7 +7,6 @@
}
.heading {
- grid-row: 1 / span 1;
font-weight: var(--venia-global-fontWeight-bold);
padding-bottom: 0.5rem;
}
@@ -16,35 +15,30 @@
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
- grid-row: 2 / span 1;
}
.discount {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
- grid-row: 3 / span 1;
}
.tax {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
- grid-row: 4 / span 1;
}
.shipping {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
- grid-row: 5 / span 1;
}
.total {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
- grid-row: 6 / span 1;
font-weight: var(--venia-global-fontWeight-bold);
}
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.js
index e20a3bdfd2..e6e3724199 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/orderTotal.js
@@ -1,37 +1,12 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { arrayOf, string, shape, number } from 'prop-types';
import { FormattedMessage } from 'react-intl';
+import Price from '@magento/venia-ui/lib/components/Price';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
-import { Price } from '@magento/peregrine';
import defaultClasses from './orderTotal.css';
-const DEFAULT_AMOUNT = {
- currency: 'USD',
- value: 0
-};
-
-/**
- * Reduces discounts array into a single amount.
- *
- * @param {Array} discounts
- */
-const getDiscount = (discounts = []) => {
- // discounts from data can be null
- if (!discounts || !discounts.length) {
- return DEFAULT_AMOUNT;
- } else {
- return {
- currency: discounts[0].amount.currency,
- value: discounts.reduce(
- (acc, discount) => acc + discount.amount.value,
- 0
- )
- };
- }
-};
-
const OrderTotal = props => {
const { classes: propClasses, data } = props;
const {
@@ -42,7 +17,37 @@ const OrderTotal = props => {
total_shipping
} = data;
const classes = mergeClasses(defaultClasses, propClasses);
- const totalDiscount = getDiscount(discounts);
+
+ const discountRowElement = useMemo(() => {
+ if (!discounts || !discounts.length) {
+ return null;
+ }
+
+ const discountTotal = {
+ currency: discounts[0].amount.currency,
+ value: discounts.reduce(
+ (acc, discount) => acc + discount.amount.value,
+ 0
+ )
+ };
+
+ return (
+
+ );
+ }, [classes.discount, discounts]);
return (
@@ -66,20 +71,7 @@ const OrderTotal = props => {
/>
-
+ {discountRowElement}
{
* since Venia does not support multiple payment methods yet
* we are picking the first method in the array.
*/
- const [{ type, additional_data }] = data;
- const { card_type, last_four } = useMemo(() => {
- const mappedAdditionalData = {};
-
- additional_data.forEach(additionalData => {
- mappedAdditionalData[additionalData.name] = additionalData.value;
- });
-
- return mappedAdditionalData;
- }, [additional_data]);
-
- const typeString = `${type} - ${card_type}`;
+ const [{ name }] = data;
return (
@@ -35,8 +24,7 @@ const PaymentMethod = props => {
defaultMessage="Payment Method"
/>
- {typeString}
- {last_four}
+ {name}
);
};
@@ -47,18 +35,11 @@ PaymentMethod.propTypes = {
classes: shape({
root: string,
heading: string,
- payment_type: string,
- payment_last_four_digits: string
+ payment_type: string
}),
data: arrayOf(
shape({
- type: string,
- additional_data: arrayOf(
- shape({
- name: string,
- value: string
- })
- )
+ name: string
})
)
};
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.css b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.css
index 17bc4aef6a..690a01b360 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.css
@@ -1,26 +1,9 @@
.root {
display: grid;
- row-gap: 0.35rem;
+ row-gap: 0.375rem;
}
.heading {
- grid-row: 1 / span 1;
font-weight: var(--venia-global-fontWeight-bold);
- padding-bottom: 0.35rem;
-}
-
-.name {
- grid-row: 2 / span 1;
-}
-
-.addressLine1 {
- grid-row: 3 / span 1;
-}
-
-.addressLine2 {
- grid-row: 4 / span 1;
-}
-
-.country {
- grid-row: 5 / span 1;
+ padding-bottom: 0.375rem;
}
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.js
index 9d140910ee..b0ff351fd4 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingInformation.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { shape, string } from 'prop-types';
+import React, { Fragment } from 'react';
+import { arrayOf, shape, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
@@ -8,18 +8,48 @@ import defaultClasses from './shippingInformation.css';
const ShippingInformation = props => {
const { data, classes: propsClasses } = props;
- const {
- city,
- country_code,
- firstname,
- lastname,
- postcode,
- region_id,
- street
- } = data;
const classes = mergeClasses(defaultClasses, propsClasses);
- const additionalAddressString = `${city}, ${region_id}, ${postcode}`;
+ let shippingContentElement;
+
+ if (data) {
+ const {
+ city,
+ country_code,
+ firstname,
+ lastname,
+ postcode,
+ region,
+ street
+ } = data;
+
+ const additionalAddressString = `${city}, ${region} ${postcode} ${country_code}`;
+ const fullName = `${firstname} ${lastname}`;
+ const streetRows = street.map((row, index) => {
+ return (
+
+ {row}
+
+ );
+ });
+
+ shippingContentElement = (
+
+ {fullName}
+ {streetRows}
+
+ {additionalAddressString}
+
+
+ );
+ } else {
+ shippingContentElement = (
+
+ );
+ }
return (
@@ -29,15 +59,7 @@ const ShippingInformation = props => {
defaultMessage="Shipping Information"
/>
-
- {firstname}
- {lastname}
-
-
{street}
-
- {additionalAddressString}
-
-
{country_code}
+ {shippingContentElement}
);
};
@@ -49,9 +71,8 @@ ShippingInformation.propTypes = {
root: string,
heading: string,
name: string,
- addressLine1: string,
- addressLine2: string,
- country: string
+ streetRow: string,
+ additionalAddress: string
}),
data: shape({
city: string,
@@ -59,8 +80,8 @@ ShippingInformation.propTypes = {
firstname: string,
lastname: string,
postcode: string,
- region_id: string,
- street: string,
+ region: string,
+ street: arrayOf(string),
telephone: string
})
};
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.css b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.css
index 67f274697d..4db071bf8d 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.css
@@ -1,18 +1,18 @@
.root {
display: grid;
- row-gap: 0.35rem;
+ row-gap: 0.375rem;
}
.heading {
- grid-row: 1 / span 1;
font-weight: var(--venia-global-fontWeight-bold);
- padding-bottom: 0.35rem;
+ padding-bottom: 0.375rem;
}
-.method {
- grid-row: 2 / span 1;
+.method:empty {
+ display: none;
}
.tracking {
- grid-row: 3 / span 1;
+ display: grid;
+ row-gap: 0.375rem;
}
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.js b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.js
index b490a8590b..5e1c3aea4b 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/OrderDetails/shippingMethod.js
@@ -10,13 +10,37 @@ const ShippingMethod = props => {
const { data, classes: propsClasses } = props;
const { shipments, shippingMethod } = data;
const classes = mergeClasses(defaultClasses, propsClasses);
- /**
- * Shipments and Tracking are arrays. Since Venia does not
- * support multiple shipping arrdresses in checkout we will
- * be picking the first value in those arrays for now.
- */
- const [{ tracking }] = shipments;
- const [{ carrier, number }] = tracking;
+ let trackingElement;
+
+ if (shipments.length) {
+ trackingElement = shipments.map(shipment => {
+ const { tracking: trackingCollection } = shipment;
+ if (trackingCollection.length) {
+ return trackingCollection.map(tracking => {
+ const { number } = tracking;
+
+ return (
+
+ {chunks}
+ }}
+ />
+
+ );
+ });
+ }
+ });
+ } else {
+ trackingElement = (
+
+ );
+ }
return (
@@ -27,15 +51,7 @@ const ShippingMethod = props => {
/>
{shippingMethod}
-
-
-
+
{trackingElement}
);
};
@@ -47,7 +63,8 @@ ShippingMethod.propTypes = {
root: string,
heading: string,
method: string,
- tracking: string
+ tracking: string,
+ trackingRow: string
}),
data: shape({
shippingMethod: string,
@@ -56,7 +73,6 @@ ShippingMethod.propTypes = {
id: string,
tracking: arrayOf(
shape({
- carrier: string,
number: string
})
)
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/__snapshots__/orderHistoryPage.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/__snapshots__/orderHistoryPage.spec.js.snap
index c3bf707ffd..a4dc872119 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/__snapshots__/orderHistoryPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/__snapshots__/orderHistoryPage.spec.js.snap
@@ -1,134 +1,840 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with data 1`] = `
-
- Title
-
- Order History
-
-
+
-
+ Order History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ />
+
+
+
+
+
+
+
+
`;
exports[`renders correctly without data 1`] = `
-
- Title
-
- Order History
-
-
+
- You don't have any orders yet.
-
-
+ Title
+
+ Order History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
-exports[`renders full page loading indicator 1`] = `
-
-
,
+ "message": "Some Error Message",
+ "timeout": 10000,
+ "type": "error",
+ },
+ ],
+]
+`;
+
+exports[`renders invalid order id message if order id is wrong 1`] = `
+
+
-
-
-
-
+ Order History
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+`;
+
+exports[`renders loading indicator 1`] = `
+
+
+ Title
+
+ Order History
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+`;
+
+exports[`renders no orders message is orders is empty 1`] = `
+
+
+ Title
+
+ Order History
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- Fetching Data...
-
-
+
+
+
`;
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/__snapshots__/resetButton.spec.js.snap b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/__snapshots__/resetButton.spec.js.snap
new file mode 100644
index 0000000000..c27b744ca7
--- /dev/null
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/__snapshots__/resetButton.spec.js.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders reset trigger 1`] = `
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/orderHistoryPage.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/orderHistoryPage.spec.js
index 761aa58331..251d6396a6 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/orderHistoryPage.spec.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/orderHistoryPage.spec.js
@@ -1,5 +1,6 @@
import React from 'react';
import { createTestInstance } from '@magento/peregrine';
+import { useToasts } from '@magento/peregrine/lib/Toasts';
import { useOrderHistoryPage } from '@magento/peregrine/lib/talons/OrderHistoryPage/useOrderHistoryPage';
import OrderHistoryPage from '../orderHistoryPage';
@@ -7,16 +8,66 @@ import OrderHistoryPage from '../orderHistoryPage';
jest.mock(
'@magento/peregrine/lib/talons/OrderHistoryPage/useOrderHistoryPage',
() => ({
- useOrderHistoryPage: jest.fn()
+ useOrderHistoryPage: jest
+ .fn()
+ .mockName('useOrderHistoryPage')
+ .mockReturnValue({
+ errorMessage: null,
+ handleReset: jest.fn().mockName('handleReset'),
+ handleSubmit: jest.fn().mockName('handleSubmit'),
+ isBackgroundLoading: false,
+ isLoadingWithoutData: false,
+ loadMoreOrders: null,
+ orders: [],
+ pageInfo: null,
+ searchText: ''
+ })
})
);
+jest.mock(
+ '@magento/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext',
+ () => ({
+ __esModule: true,
+ default: props => (
+
+ {props.children}
+
+ )
+ })
+);
+
+jest.mock('@magento/peregrine/lib/Toasts', () => ({
+ useToasts: jest
+ .fn()
+ .mockName('useToasts')
+ .mockReturnValue([
+ {},
+ {
+ addToast: jest.fn().mockName('addToast')
+ }
+ ])
+}));
+
jest.mock('../../../classify');
jest.mock('../../Head', () => ({ Title: () => 'Title' }));
jest.mock('../orderRow', () => 'OrderRow');
-test('renders full page loading indicator', () => {
+const talonProps = {
+ errorMessage: null,
+ handleReset: jest.fn().mockName('handleReset'),
+ handleSubmit: jest.fn().mockName('handleSubmit'),
+ isBackgroundLoading: false,
+ isLoadingWithoutData: false,
+ loadMoreOrders: null,
+ orders: [],
+ pageInfo: null,
+ searchText: ''
+};
+
+test('renders loading indicator', () => {
useOrderHistoryPage.mockReturnValueOnce({
+ ...talonProps,
isLoadingWithoutData: true,
orders: []
});
@@ -28,6 +79,7 @@ test('renders full page loading indicator', () => {
test('renders correctly without data', () => {
useOrderHistoryPage.mockReturnValueOnce({
+ ...talonProps,
isLoadingWithoutData: false,
orders: []
});
@@ -39,8 +91,51 @@ test('renders correctly without data', () => {
test('renders correctly with data', () => {
useOrderHistoryPage.mockReturnValueOnce({
+ ...talonProps,
isLoadingWithoutData: false,
- orders: [{ id: 1 }, { id: 2 }, { id: 3 }]
+ loadMoreOrders: jest.fn().mockName('loadMoreOrders'),
+ orders: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ pageInfo: { current: 3, total: 6 }
+ });
+
+ const tree = createTestInstance(
);
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders error messages if any', () => {
+ useOrderHistoryPage.mockReturnValueOnce({
+ ...talonProps,
+ errorMessage: 'Some Error Message'
+ });
+ const addToast = jest.fn();
+ useToasts.mockReturnValueOnce([{}, { addToast }]);
+
+ createTestInstance(
);
+
+ expect(addToast).toHaveBeenCalled();
+ expect(addToast.mock.calls).toMatchSnapshot();
+});
+
+test('renders invalid order id message if order id is wrong', () => {
+ useOrderHistoryPage.mockReturnValueOnce({
+ ...talonProps,
+ searchText: '********',
+ isBackgroundLoading: false,
+ orders: []
+ });
+
+ const tree = createTestInstance(
);
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('renders no orders message is orders is empty', () => {
+ useOrderHistoryPage.mockReturnValueOnce({
+ ...talonProps,
+ searchText: null,
+ isBackgroundLoading: false,
+ orders: []
});
const tree = createTestInstance(
);
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/resetButton.spec.js b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/resetButton.spec.js
new file mode 100644
index 0000000000..b60d98164c
--- /dev/null
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/__tests__/resetButton.spec.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { useFormApi } from 'informed';
+import { createTestInstance } from '@magento/peregrine';
+
+import Trigger from '../../Trigger';
+import ResetButton from '../resetButton';
+
+jest.mock('informed', () => ({
+ useFormApi: jest.fn()
+}));
+
+const props = { onReset: jest.fn() };
+
+test('renders reset trigger', () => {
+ const tree = createTestInstance(
);
+
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+test('calls reset handlers on click', () => {
+ const mockReset = jest.fn();
+ useFormApi.mockReturnValue({ reset: mockReset });
+
+ const { root } = createTestInstance(
);
+ const triggerNode = root.findByType(Trigger);
+ const { action } = triggerNode.props;
+
+ action();
+
+ expect(mockReset).toHaveBeenCalled();
+ expect(props.onReset).toHaveBeenCalled();
+});
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.css b/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.css
index 5e99c20474..5ee867e108 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.css
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.css
@@ -20,11 +20,38 @@
row-gap: 1rem;
}
-@media (max-width: 960px) {
- .root {
- padding-left: 1.5rem;
- padding-right: 1.5rem;
- }
+.filterRow {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+}
+
+.pageInfo {
+ font-size: var(--venia-typography-body-S-fontSize);
+}
+
+.search {
+ display: grid;
+ gap: 1rem;
+ grid-auto-flow: column;
+ width: 22rem;
+}
+
+.searchButton {
+ composes: root_highPriority from '../Button/button.css';
+
+ width: 5rem;
+ height: 2rem;
+ min-width: 5rem;
+}
+
+.submitIcon {
+ color: white;
+}
+
+.loadMoreButton {
+ composes: root_lowPriority from '../Button/button.css';
+ justify-self: center;
}
@media (max-width: 960px) {
@@ -32,4 +59,16 @@
padding-left: 1.5rem;
padding-right: 1.5rem;
}
+
+ .filterRow {
+ align-items: flex-start;
+ flex-direction: column;
+ row-gap: 1rem;
+ }
+
+ .search {
+ gap: 0.5rem;
+ width: 100%;
+ justify-self: center;
+ }
}
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.gql.js b/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.gql.js
deleted file mode 100644
index a9ed02e774..0000000000
--- a/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.gql.js
+++ /dev/null
@@ -1,394 +0,0 @@
-import { gql } from '@apollo/client';
-
-/*
- This feature is being built ahead of GraphQL coverage that is landing in 2.4.1 of Magento. We're going to mock
- the data based on the approved schema to make removing the mocking layer as seamless as possible.
-
- @see https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/customer/customer-orders.md
- */
-
-/* eslint-disable graphql/template-strings */
-export const GET_CUSTOMER_ORDERS = gql`
- query GetCustomerOrders {
- customer {
- id
- orders @client {
- items {
- billing_address {
- city
- country_code
- firstname
- lastname
- postcode
- region_id
- street
- telephone
- }
- id
- invoices {
- id
- }
- items {
- id
- product_name
- product_sale_price
- product_sku
- selected_options {
- label
- value
- }
- quantity_ordered
- }
- number
- order_date
- payment_methods {
- name
- type
- additional_data {
- name
- value
- }
- }
- shipments {
- id
- tracking {
- carrier
- number
- }
- }
- shipping_address {
- city
- country_code
- firstname
- lastname
- postcode
- region_id
- street
- telephone
- }
- shipping_method
- status
- total {
- discounts {
- amount {
- currency
- value
- }
- }
- grand_total {
- currency
- value
- }
- subtotal {
- currency
- value
- }
- total_shipping {
- currency
- value
- }
- total_tax {
- currency
- value
- }
- }
- }
- }
- }
- }
-`;
-/* eslint-enable graphql/template-strings */
-
-const MOCK_ORDERS = {
- items: [
- {
- billing_address: {
- city: 'Austin',
- country_code: 'US',
- firstname: 'Gooseton',
- lastname: 'Jr',
- postcode: '78759',
- region_id: 'TX',
- street: 'Goose Dr',
- telephone: '9123456789'
- },
- id: 1,
- invoices: [{ id: 1 }],
- items: [
- {
- id: '1',
- product_name: 'Product 1',
- product_sale_price: '$100.00',
- product_sku: 'VSW01',
- selected_options: [
- {
- label: 'Color',
- value: 'Green'
- }
- ],
- quantity_ordered: 1
- },
- {
- id: '2',
- product_name: 'Product 2',
- product_sale_price: '$100.00',
- product_sku: 'VD02',
- selected_options: [
- {
- label: 'Color',
- value: 'Red'
- }
- ],
- quantity_ordered: 10
- },
- {
- id: '3',
- product_name: 'Product 3',
- product_sale_price: '$100.00',
- product_sku: 'VT02',
- selected_options: [
- {
- label: 'Color',
- value: 'Black'
- }
- ],
- quantity_ordered: 1
- },
- {
- id: '4',
- product_name: 'Product 4',
- product_sale_price: '$100.00',
- product_sku: 'VT11',
- selected_options: [
- {
- label: 'Color',
- value: 'Pink'
- }
- ],
- quantity_ordered: 1
- },
- {
- id: '5',
- product_name: 'Product 5',
- product_sale_price: '$100.00',
- product_sku: 'VSW09',
- selected_options: [
- {
- label: 'Color',
- value: 'Grey'
- }
- ],
- quantity_ordered: 1
- }
- ],
- number: '000000002',
- order_date: '2020-08-26 18:22:35',
- payment_methods: [
- {
- name: 'Braintree',
- type: 'Credit Card',
- additional_data: [
- {
- name: 'card_type',
- value: 'Visa'
- },
- {
- name: 'last_four',
- value: '1234'
- }
- ]
- }
- ],
- shipments: [
- {
- id: '1',
- tracking: [
- {
- carrier: 'Fedex',
- number: 'FEDEX516351813216541'
- }
- ]
- }
- ],
- shipping_address: {
- city: 'Austin',
- country_code: 'US',
- firstname: 'Gooseton',
- lastname: 'Jr',
- postcode: '78759',
- region_id: 'TX',
- street: 'Goose Dr',
- telephone: '9123456789'
- },
- shipping_method: 'Free',
- status: 'Processing',
- total: {
- discounts: [
- {
- amount: {
- currency: 'USD',
- value: 123
- }
- }
- ],
- grand_total: {
- currency: 'USD',
- value: 1434
- },
- subtotal: {
- currency: 'USD',
- value: 1234
- },
- total_tax: {
- currency: 'USD',
- value: 34
- },
- total_shipping: {
- currency: 'USD',
- value: 12
- }
- }
- },
- {
- billing_address: {
- city: 'Austin',
- country_code: 'US',
- firstname: 'Gooseton',
- lastname: 'Jr',
- postcode: '78759',
- region_id: 'TX',
- street: 'Goose Dr',
- telephone: '9123456789'
- },
- id: 2,
- invoices: [{ id: 1 }],
- items: [
- {
- id: '3',
- product_name: 'Product 3',
- product_sale_price: '$100.00',
- product_sku: 'VA03',
- selected_options: [
- {
- label: 'Color',
- value: 'Blue'
- }
- ],
- quantity_ordered: 1
- },
- {
- id: '4',
- product_name: 'Product 4',
- product_sale_price: '$100.00',
- product_sku: 'VP08',
- selected_options: [
- {
- label: 'Color',
- value: 'Black'
- }
- ],
- quantity_ordered: 1
- },
- {
- id: '5',
- product_name: 'Product 5',
- product_sale_price: '$100.00',
- product_sku: 'VSW09',
- selected_options: [
- {
- label: 'Color',
- value: 'Orange'
- }
- ],
- quantity_ordered: 1
- }
- ],
- number: '000000005',
- order_date: '2020-05-26 18:22:35',
- payment_methods: [
- {
- name: 'Braintree',
- type: 'Credit Card',
- additional_data: [
- {
- name: 'card_type',
- value: 'Master Card'
- },
- {
- name: 'last_four',
- value: '7894'
- }
- ]
- }
- ],
- shipments: [
- {
- id: '1',
- tracking: [
- {
- carrier: 'Fedex',
- number: 'FEDEX5885541235452125'
- }
- ]
- }
- ],
- shipping_address: {
- city: 'Austin',
- country_code: 'US',
- firstname: 'Gooseton',
- lastname: 'Jr',
- postcode: '78759',
- region_id: 'TX',
- street: 'Goose Dr',
- telephone: '9123456789'
- },
- shipping_method: 'Free',
- status: 'Complete',
- total: {
- discounts: [
- {
- amount: {
- currency: 'USD',
- value: 123
- }
- }
- ],
- grand_total: {
- currency: 'USD',
- value: 1434
- },
- subtotal: {
- currency: 'USD',
- value: 1234
- },
- total_tax: {
- currency: 'USD',
- value: 34
- },
- total_shipping: {
- currency: 'USD',
- value: 12
- }
- }
- }
- ]
-};
-
-export const CUSTOM_TYPES = {
- Customer: {
- fields: {
- orders: {
- read(cached) {
- return cached || MOCK_ORDERS;
- }
- }
- }
- }
-};
-
-export default {
- queries: {
- getCustomerOrdersQuery: GET_CUSTOMER_ORDERS
- },
- types: CUSTOM_TYPES
-};
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.js b/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.js
index 59dcf35128..b7b024d1f1 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.js
@@ -1,26 +1,60 @@
-import React, { useMemo } from 'react';
-import { useIntl } from 'react-intl';
+import React, { useMemo, useEffect } from 'react';
+import { useIntl, FormattedMessage } from 'react-intl';
+import {
+ Search as SearchIcon,
+ AlertCircle as AlertCircleIcon,
+ ArrowRight as SubmitIcon
+} from 'react-feather';
import { shape, string } from 'prop-types';
+import { Form } from 'informed';
+
+import { useToasts } from '@magento/peregrine/lib/Toasts';
+import OrderHistoryContextProvider from '@magento/peregrine/lib/talons/OrderHistoryPage/orderHistoryContext';
import { useOrderHistoryPage } from '@magento/peregrine/lib/talons/OrderHistoryPage/useOrderHistoryPage';
import { mergeClasses } from '../../classify';
+import Button from '../Button';
import { Title } from '../Head';
-import { fullPageLoadingIndicator } from '../LoadingIndicator';
+import Icon from '../Icon';
+import LoadingIndicator from '../LoadingIndicator';
+import TextInput from '../TextInput';
+
import defaultClasses from './orderHistoryPage.css';
-import orderHistoryOperations from './orderHistoryPage.gql';
import OrderRow from './orderRow';
+import ResetButton from './resetButton';
+
+const errorIcon = (
+
+);
+const searchIcon =
;
const OrderHistoryPage = props => {
- const talonProps = useOrderHistoryPage({ ...orderHistoryOperations });
- const { isLoadingWithoutData, orders } = talonProps;
+ const talonProps = useOrderHistoryPage();
+ const {
+ errorMessage,
+ loadMoreOrders,
+ handleReset,
+ handleSubmit,
+ isBackgroundLoading,
+ isLoadingWithoutData,
+ orders,
+ pageInfo,
+ searchText
+ } = talonProps;
+ const [, { addToast }] = useToasts();
const { formatMessage } = useIntl();
const PAGE_TITLE = formatMessage({
id: 'orderHistoryPage.pageTitleText',
defaultMessage: 'Order History'
});
- const EMPTY_DATA_MESSAGE = formatMessage({
- id: 'orderHistoryPage.emptyDataMessage',
- defaultMessage: "You don't have any orders yet."
+ const SEARCH_PLACE_HOLDER = formatMessage({
+ id: 'orderHistoryPage.search',
+ defaultMessage: 'Search by Order Number'
});
const classes = mergeClasses(defaultClasses, props.classes);
@@ -30,32 +64,125 @@ const OrderHistoryPage = props => {
});
}, [orders]);
- if (isLoadingWithoutData) {
- return fullPageLoadingIndicator;
- }
-
- let pageContents;
- if (!orders.length) {
- pageContents = (
-
- {EMPTY_DATA_MESSAGE}
-
- );
- } else {
- pageContents = (
-
- );
- }
+ const pageContents = useMemo(() => {
+ if (isLoadingWithoutData) {
+ return
;
+ } else if (!isBackgroundLoading && searchText && !orders.length) {
+ return (
+
+
+
+ );
+ } else if (!isBackgroundLoading && !orders.length) {
+ return (
+
+
+
+ );
+ } else {
+ return
;
+ }
+ }, [
+ classes.emptyHistoryMessage,
+ classes.orderHistoryTable,
+ isBackgroundLoading,
+ isLoadingWithoutData,
+ orderRows,
+ orders.length,
+ searchText
+ ]);
// STORE_NAME is injected by Webpack at build time.
const title = `${PAGE_TITLE} - ${STORE_NAME}`;
+ const resetButtonElement = searchText ? (
+
+ ) : null;
+
+ const submitIcon = (
+
+ );
+
+ const pageInfoLabel = pageInfo ? (
+
+ ) : null;
+
+ const loadMoreButton = loadMoreOrders ? (
+
+
+
+ ) : null;
+
+ useEffect(() => {
+ if (errorMessage) {
+ addToast({
+ type: 'error',
+ icon: errorIcon,
+ message: errorMessage,
+ dismissable: true,
+ timeout: 10000
+ });
+ }
+ }, [addToast, errorMessage]);
+
return (
-
-
{title}
- {PAGE_TITLE}
- {pageContents}
-
+
+
+
{title}
+
{PAGE_TITLE}
+
+ {pageInfoLabel}
+
+
+
+ {submitIcon}
+
+
+
+ {pageContents}
+ {loadMoreButton}
+
+
);
};
@@ -66,6 +193,10 @@ OrderHistoryPage.propTypes = {
root: string,
heading: string,
emptyHistoryMessage: string,
- orderHistoryTable: string
+ orderHistoryTable: string,
+ search: string,
+ searchButton: string,
+ submitIcon: string,
+ loadMoreButton: string
})
};
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/orderRow.js b/packages/venia-ui/lib/components/OrderHistoryPage/orderRow.js
index 9704fbac4d..aedfb0bd05 100644
--- a/packages/venia-ui/lib/components/OrderHistoryPage/orderRow.js
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/orderRow.js
@@ -10,9 +10,6 @@ import Icon from '../Icon';
import CollapsedImageGallery from './collapsedImageGallery';
import OrderProgressBar from './orderProgressBar';
import OrderDetails from './OrderDetails';
-
-import orderRowOperations from './orderRow.gql';
-
import defaultClasses from './orderRow.css';
const OrderRow = props => {
@@ -66,10 +63,7 @@ const OrderRow = props => {
});
}
- const talonProps = useOrderRow({
- items,
- ...orderRowOperations
- });
+ const talonProps = useOrderRow({ items });
const { loading, isOpen, handleContentToggle, imagesData } = talonProps;
const classes = mergeClasses(defaultClasses, props.classes);
@@ -173,13 +167,16 @@ OrderRow.propTypes = {
lastname: string,
postcode: string,
region_id: string,
- street: string
+ street: arrayOf(string)
}),
items: arrayOf(
shape({
id: string,
product_name: string,
- product_sale_price: string,
+ product_sale_price: shape({
+ currency: string,
+ value: number
+ }),
product_sku: string,
selected_options: arrayOf(
shape({
@@ -192,7 +189,7 @@ OrderRow.propTypes = {
),
invoices: arrayOf(
shape({
- id: number
+ id: string
})
),
number: string,
@@ -215,7 +212,7 @@ OrderRow.propTypes = {
lastname: string,
postcode: string,
region_id: string,
- street: string,
+ street: arrayOf(string),
telephone: string
}),
shipping_method: string,
@@ -224,7 +221,6 @@ OrderRow.propTypes = {
id: string,
tracking: arrayOf(
shape({
- carrier: string,
number: string
})
)
diff --git a/packages/venia-ui/lib/components/OrderHistoryPage/resetButton.js b/packages/venia-ui/lib/components/OrderHistoryPage/resetButton.js
new file mode 100644
index 0000000000..364e41c146
--- /dev/null
+++ b/packages/venia-ui/lib/components/OrderHistoryPage/resetButton.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { useFormApi } from 'informed';
+import { func } from 'prop-types';
+import { X as ClearIcon } from 'react-feather';
+
+import Icon from '../Icon';
+import Trigger from '../Trigger';
+
+const clearIcon =
;
+
+const ResetButton = props => {
+ const { onReset } = props;
+ const formApi = useFormApi();
+
+ const handleReset = () => {
+ formApi.reset();
+
+ if (onReset) {
+ onReset();
+ }
+ };
+
+ return
{clearIcon} ;
+};
+
+export default ResetButton;
+
+ResetButton.propTypes = {
+ onReset: func
+};
diff --git a/packages/venia-ui/lib/components/ProductFullDetail/__tests__/__snapshots__/productFullDetail.spec.js.snap b/packages/venia-ui/lib/components/ProductFullDetail/__tests__/__snapshots__/productFullDetail.spec.js.snap
index 4bf024c28b..477d665a51 100644
--- a/packages/venia-ui/lib/components/ProductFullDetail/__tests__/__snapshots__/productFullDetail.spec.js.snap
+++ b/packages/venia-ui/lib/components/ProductFullDetail/__tests__/__snapshots__/productFullDetail.spec.js.snap
@@ -58,7 +58,10 @@ exports[`it disables the add to cart button when the talon indicates 1`] = `
- Quantity
+
QuantityFields
@@ -73,7 +76,10 @@ exports[`it disables the add to cart button when the talon indicates 1`] = `
- Add to Cart
+
@@ -83,7 +89,10 @@ exports[`it disables the add to cart button when the talon indicates 1`] = `
- Product Description
+
- SKU
+
BTTF123
@@ -160,7 +172,10 @@ exports[`it does not render options if the product is not a ConfigurableProduct
- Quantity
+
QuantityFields
@@ -175,7 +190,10 @@ exports[`it does not render options if the product is not a ConfigurableProduct
- Add to Cart
+
@@ -185,7 +203,10 @@ exports[`it does not render options if the product is not a ConfigurableProduct
- Product Description
+
- SKU
+
BTTF123
@@ -268,7 +292,10 @@ exports[`it renders an error for an invalid user token when adding to cart 1`] =
- Quantity
+
QuantityFields
@@ -283,7 +310,10 @@ exports[`it renders an error for an invalid user token when adding to cart 1`] =
- Add to Cart
+
@@ -293,7 +323,10 @@ exports[`it renders an error for an invalid user token when adding to cart 1`] =
- Product Description
+
- SKU
+
BTTF123
@@ -377,7 +413,10 @@ Array [
- Quantity
+
QuantityFields
@@ -392,7 +431,10 @@ Array [
- Add to Cart
+
@@ -402,7 +444,10 @@ Array [
- Product Description
+
- SKU
+
BTTF123
@@ -545,7 +593,10 @@ exports[`it renders correctly 1`] = `
- Fetching Data...
+
@@ -555,7 +606,10 @@ exports[`it renders correctly 1`] = `
- Quantity
+
QuantityFields
@@ -570,7 +624,10 @@ exports[`it renders correctly 1`] = `
- Add to Cart
+
@@ -580,7 +637,10 @@ exports[`it renders correctly 1`] = `
- Product Description
+
- SKU
+
BTTF123
@@ -659,7 +722,10 @@ exports[`it renders field level errors for quantity - message 1 1`] = `
- Quantity
+
QuantityFields
@@ -674,7 +740,10 @@ exports[`it renders field level errors for quantity - message 1 1`] = `
- Add to Cart
+
@@ -684,7 +753,10 @@ exports[`it renders field level errors for quantity - message 1 1`] = `
- Product Description
+
- SKU
+
BTTF123
@@ -763,7 +838,10 @@ exports[`it renders field level errors for quantity - message 2 1`] = `
- Quantity
+
QuantityFields
@@ -778,7 +856,10 @@ exports[`it renders field level errors for quantity - message 2 1`] = `
- Add to Cart
+
@@ -788,7 +869,10 @@ exports[`it renders field level errors for quantity - message 2 1`] = `
- Product Description
+
- SKU
+
BTTF123
@@ -867,7 +954,10 @@ exports[`it renders field level errors for quantity - message 3 1`] = `
- Quantity
+
QuantityFields
@@ -882,7 +972,10 @@ exports[`it renders field level errors for quantity - message 3 1`] = `
- Add to Cart
+
@@ -892,7 +985,10 @@ exports[`it renders field level errors for quantity - message 3 1`] = `
- Product Description
+
- SKU
+
BTTF123
@@ -975,7 +1074,10 @@ exports[`it renders form level errors 1`] = `
- Quantity
+
QuantityFields
@@ -990,7 +1092,10 @@ exports[`it renders form level errors 1`] = `
- Add to Cart
+
@@ -1000,7 +1105,10 @@ exports[`it renders form level errors 1`] = `
- Product Description
+
- SKU
+
BTTF123
diff --git a/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/option.spec.js.snap b/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/option.spec.js.snap
index 5b6a894633..a457ef56b3 100644
--- a/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/option.spec.js.snap
+++ b/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/option.spec.js.snap
@@ -18,7 +18,15 @@ exports[`renders Option component correctly 1`] = `
- Selected Foo:
+
None
diff --git a/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/options.spec.js.snap b/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/options.spec.js.snap
index 743c9dffe0..3596350dae 100644
--- a/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/options.spec.js.snap
+++ b/packages/venia-ui/lib/components/ProductOptions/__tests__/__snapshots__/options.spec.js.snap
@@ -21,7 +21,15 @@ Array [
- Selected option-1:
+
None
@@ -47,7 +55,15 @@ Array [
- Selected option-1:
+
None
diff --git a/packages/venia-ui/lib/components/ProductSort/__tests__/__snapshots__/productSort.spec.js.snap b/packages/venia-ui/lib/components/ProductSort/__tests__/__snapshots__/productSort.spec.js.snap
index 877d6a8f7b..0a4e3b37c5 100644
--- a/packages/venia-ui/lib/components/ProductSort/__tests__/__snapshots__/productSort.spec.js.snap
+++ b/packages/venia-ui/lib/components/ProductSort/__tests__/__snapshots__/productSort.spec.js.snap
@@ -8,7 +8,10 @@ exports[`renders correctly 1`] = `
type="button"
>
- Sort
+
diff --git a/packages/venia-ui/lib/components/Region/region.js b/packages/venia-ui/lib/components/Region/region.js
index 9e1556cd6f..da6c6a2f88 100644
--- a/packages/venia-ui/lib/components/Region/region.js
+++ b/packages/venia-ui/lib/components/Region/region.js
@@ -18,6 +18,7 @@ import { GET_REGIONS_QUERY } from './region.gql';
const Region = props => {
const {
classes: propClasses,
+ countryCodeField,
fieldInput,
fieldSelect,
label,
@@ -28,6 +29,7 @@ const Region = props => {
const { formatMessage } = useIntl();
const talonProps = useRegion({
+ countryCodeField,
fieldInput,
fieldSelect,
optionValueKey,
@@ -68,6 +70,7 @@ const Region = props => {
export default Region;
Region.defaultProps = {
+ countryCodeField: 'country',
fieldInput: 'region',
fieldSelect: 'region',
label: 'State',
@@ -79,6 +82,7 @@ Region.propTypes = {
classes: shape({
root: string
}),
+ countryCodeField: string,
fieldInput: string,
fieldSelect: string,
label: string,
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/creditCard.spec.js.snap b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/creditCard.spec.js.snap
new file mode 100644
index 0000000000..f14720c4df
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/creditCard.spec.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should render properly 1`] = `
+
+
+
+
+
+ **** 1234 Visa
+
+
+ Dec. 12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/paymentCard.spec.js.snap b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/paymentCard.spec.js.snap
new file mode 100644
index 0000000000..a274d240bc
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/paymentCard.spec.js.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should render properly 1`] = `
+
+`;
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap
index 002b0ac498..33fa304cf5 100644
--- a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap
@@ -8,54 +8,26 @@ exports[`renders correctly when there are existing saved payments 1`] = `
- Saved Payments - Venia
+ Saved Payments
-
- Credit Cards saved here will be available during checkout.
-
-
-
-
-
- Add a credit card
-
-
-
-
-
-
+
+
`;
@@ -67,41 +39,95 @@ exports[`renders correctly when there are no existing saved payments 1`] = `
- Saved Payments - Venia
+ Saved Payments
-
- Credit Cards saved here will be available during checkout.
-
+
-
-
-
-
- Add a credit card
-
-
-
+ You have no saved payments.
`;
+
+exports[`renders loading indicator when isLoading is true 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/creditCard.spec.js b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/creditCard.spec.js
new file mode 100644
index 0000000000..890ca23145
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/creditCard.spec.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import CreditCard from '../creditCard';
+
+import { createTestInstance } from '@magento/peregrine';
+
+const props = {
+ details: {
+ maskedCC: '1234',
+ type: 'VI',
+ expirationDate: '12/12/2022'
+ }
+};
+
+test('Should render properly', () => {
+ const instance = createTestInstance( );
+
+ expect(instance.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/paymentCard.spec.js b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/paymentCard.spec.js
new file mode 100644
index 0000000000..10773e5229
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/paymentCard.spec.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PaymentCard from '../paymentCard';
+
+import { createTestInstance } from '@magento/peregrine';
+
+jest.mock('../savedPaymentTypes', () => ({
+ braintree: props =>
+}));
+
+const props = {
+ payment_method_code: 'braintree',
+ details: {
+ maskedCC: '1234',
+ type: 'VI',
+ expirationDate: '12/12/2022'
+ }
+};
+
+test('Should render properly', () => {
+ const instance = createTestInstance( );
+
+ expect(instance.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js
index 435e5aa7cf..dff14ad0a9 100644
--- a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js
@@ -7,7 +7,6 @@ import SavedPaymentsPage from '../savedPaymentsPage';
jest.mock('@magento/venia-ui/lib/classify');
jest.mock('../../Head', () => ({ Title: () => 'Title' }));
-jest.mock('../../Icon', () => 'Icon');
jest.mock(
'@magento/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage',
() => {
@@ -16,14 +15,35 @@ jest.mock(
};
}
);
+jest.mock('../paymentCard', () => props => );
const props = {};
const talonProps = {
- savedPayments: [],
- handleAddPayment: jest.fn().mockName('handleAddPayment')
+ savedPayments: [
+ {
+ public_hash: '78asfg87ibafv',
+ payment_method_code: 'braintree',
+ details: {
+ maskedCC: '1234',
+ type: 'VI',
+ expirationDate: '12/12/2022'
+ }
+ }
+ ]
};
-it('renders correctly when there are no existing saved payments', () => {
+test('renders correctly when there are no existing saved payments', () => {
+ // Arrange.
+ useSavedPaymentsPage.mockReturnValueOnce({ savedPayments: [] });
+
+ // Act.
+ const instance = createTestInstance( );
+
+ // Assert.
+ expect(instance.toJSON()).toMatchSnapshot();
+});
+
+test('renders correctly when there are existing saved payments', () => {
// Arrange.
useSavedPaymentsPage.mockReturnValueOnce(talonProps);
@@ -34,13 +54,12 @@ it('renders correctly when there are no existing saved payments', () => {
expect(instance.toJSON()).toMatchSnapshot();
});
-it('renders correctly when there are existing saved payments', () => {
+test('renders loading indicator when isLoading is true', () => {
// Arrange.
- const myTalonProps = {
+ useSavedPaymentsPage.mockReturnValueOnce({
...talonProps,
- savedPayments: ['a', 'b', 'c']
- };
- useSavedPaymentsPage.mockReturnValueOnce(myTalonProps);
+ isLoading: true
+ });
// Act.
const instance = createTestInstance( );
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.css b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.css
new file mode 100644
index 0000000000..bf8fb00200
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.css
@@ -0,0 +1,48 @@
+.root {
+ border-radius: 0.375rem;
+ border: 2px solid rgb(var(--venia-global-color-gray-400));
+ column-gap: 1rem;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ min-height: 10rem;
+ min-width: 20rem;
+ padding: 1.5rem 2rem;
+}
+
+.root_selected {
+ composes: root;
+ border-color: rgb(var(--venia-brand-color-1-600));
+}
+
+.title {
+ font-weight: var(--venia-global-fontWeight-semibold);
+ grid-column: 1 / span 1;
+ grid-row: 1 / span 1;
+}
+
+.number {
+ grid-column: 1 / span 1;
+ grid-row: 2 / span 1;
+}
+
+.expiry_date {
+ grid-column: 1 / span 1;
+ grid-row: 3 / span 1;
+}
+
+.delete {
+ grid-column: 2 / span 1;
+ grid-row: 1 / span 3;
+}
+
+.deleteButton {
+ composes: root from '../LinkButton/linkButton.css';
+ text-decoration: none;
+ visibility: hidden;
+}
+
+@media (max-width: 960px) {
+ .deleteText {
+ display: none;
+ }
+}
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.js b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.js
new file mode 100644
index 0000000000..c8fd5f013b
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.js
@@ -0,0 +1,98 @@
+import React, { useCallback, useMemo } from 'react';
+import { shape, string } from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { Trash2 as DeleteIcon } from 'react-feather';
+
+import LinkButton from '../LinkButton';
+import Icon from '../Icon';
+import { mergeClasses } from '@magento/venia-ui/lib/classify';
+
+import defaultClasses from './creditCard.css';
+
+/**
+ * Enumerated list of supported credit card types from
+ *
+ * https://github.com/magento/magento2/blob/2.4-develop/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js
+ */
+const cardTypeMapper = {
+ AE: 'American Express',
+ AU: 'Aura',
+ DI: 'Discover',
+ DN: 'Diners',
+ ELO: 'Elo',
+ HC: 'Hipercard',
+ JCB: 'JCB',
+ MC: 'MasterCard',
+ MD: 'Maestro Domestic',
+ MI: 'Maestro International',
+ UN: 'UnionPay',
+ VI: 'Visa'
+};
+
+const CreditCard = props => {
+ const { classes: propClasses, details } = props;
+ const classes = mergeClasses(defaultClasses, propClasses);
+
+ const number = `**** ${details.maskedCC} \u00A0\u00A0 ${cardTypeMapper[
+ details.type
+ ] || ''}`;
+ const cardExpiryDate = useMemo(() => {
+ const [month, year] = details.expirationDate.split('/');
+ const shortMonth = new Date(+year, +month - 1).toLocaleString(
+ 'default',
+ { month: 'short' }
+ );
+
+ return `${shortMonth}. ${year}`;
+ }, [details.expirationDate]);
+
+ // Should be moved to a talon in the future
+ const handleDelete = useCallback(() => {}, []);
+ const deleteButton = (
+
+
+
+
+
+
+ );
+
+ return (
+
+
+
+
+
{number}
+
{cardExpiryDate}
+
{deleteButton}
+
+ );
+};
+
+export default CreditCard;
+
+CreditCard.propTypes = {
+ classes: shape({
+ delete: 'string',
+ deleteButton: 'string',
+ expiry_date: 'string',
+ number: 'string',
+ root_selected: 'string',
+ root: 'string',
+ title: 'string'
+ }),
+ details: shape({
+ expirationDate: string,
+ maskedCC: string,
+ type: string
+ })
+};
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/paymentCard.js b/packages/venia-ui/lib/components/SavedPaymentsPage/paymentCard.js
new file mode 100644
index 0000000000..cb38d1ddd0
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/paymentCard.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { shape, string } from 'prop-types';
+
+import paymentCardMapper from './savedPaymentTypes';
+
+const PaymentCard = props => {
+ const PaymentComponent = paymentCardMapper[props.payment_method_code];
+
+ if (!PaymentComponent) {
+ /**
+ * Will be handled in https://jira.corp.magento.com/browse/PWA-1202
+ */
+ }
+
+ return ;
+};
+
+export default PaymentCard;
+
+PaymentCard.propTypes = {
+ details: shape({
+ expirationDate: string,
+ maskedCC: string,
+ type: string
+ }),
+ payment_method_code: string.isRequired
+};
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentTypes.js b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentTypes.js
new file mode 100644
index 0000000000..b8d34b1e6b
--- /dev/null
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentTypes.js
@@ -0,0 +1,22 @@
+/**
+ * This file is augmented at build time using the @magento/venia-ui build
+ * target "savedPaymentTypes", which allows third-party modules to add new saved payment component mappings.
+ *
+ * @see [SavedPayment definition object]{@link SavedPaymentDefinition}
+ */
+export default {};
+
+/**
+ * A payment definition object that describes a saved payment in your storefront.
+ *
+ * @typedef {Object} SavedPaymentDefinition
+ * @property {string} paymentCode is use to map your payment
+ * @property {string} importPath Resolvable path to the component the
+ * Route component will render
+ *
+ * @example A custom payment method
+ * const myCustomSavedPayment = {
+ * paymentCode: 'cc',
+ * importPath: '@partner/module/path_to_your_component'
+ * }
+ */
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css
index fb952d812a..2fe729d5d1 100644
--- a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css
@@ -25,6 +25,10 @@
grid-template-columns: 1fr 1fr 1fr;
}
+.noPayments {
+ text-align: center;
+}
+
.addButton {
border: 2px dashed rgb(var(--venia-global-color-gray-400));
border-radius: 0.375rem;
diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js
index 8355c4af67..9e3b8a989a 100644
--- a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js
+++ b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js
@@ -1,72 +1,63 @@
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
-import { PlusSquare } from 'react-feather';
import { useSavedPaymentsPage } from '@magento/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
-import Icon from '../Icon';
-import LinkButton from '../LinkButton';
import { Title } from '../Head';
+import PaymentCard from './paymentCard';
+import { fullPageLoadingIndicator } from '../LoadingIndicator';
-import { GET_SAVED_PAYMENTS_QUERY } from './savedPaymentsPage.gql';
import defaultClasses from './savedPaymentsPage.css';
const SavedPaymentsPage = props => {
- const talonProps = useSavedPaymentsPage({
- queries: {
- getSavedPaymentsQuery: GET_SAVED_PAYMENTS_QUERY
- }
- });
+ const talonProps = useSavedPaymentsPage();
- const { handleAddPayment, savedPayments } = talonProps;
+ const { isLoading, savedPayments } = talonProps;
const classes = mergeClasses(defaultClasses, props.classes);
+ const { formatMessage } = useIntl();
+
const savedPaymentElements = useMemo(() => {
- return savedPayments.map(
- ({ details, public_hash, payment_method_code }) => (
- // TODO: Clean up in PWA-636
-
-
{payment_method_code}
-
{JSON.stringify(details, null, 2)}
-
- )
- );
+ if (savedPayments.length) {
+ return savedPayments.map(paymentDetails => (
+
+ ));
+ } else {
+ return null;
+ }
}, [savedPayments]);
- const { formatMessage } = useIntl();
+ const noSavedPayments = useMemo(() => {
+ if (!savedPayments.length) {
+ return formatMessage({
+ id: 'savedPaymentsPage.noSavedPayments',
+ defaultMessage: 'You have no saved payments.'
+ });
+ } else {
+ return null;
+ }
+ }, [savedPayments, formatMessage]);
- // STORE_NAME is injected by Webpack at build time.
- const title = formatMessage(
- { id: 'savedPaymentsPage.title' },
- { store_name: STORE_NAME }
- );
- const subHeading = formatMessage({ id: 'savedPaymentsPage.subHeading' });
- const addButtonText = formatMessage({
- id: 'savedPaymentsPage.addButtonText'
+ const title = formatMessage({
+ id: 'savedPaymentsPage.title',
+ defaultMessage: 'Saved Payments'
});
+ if (isLoading) {
+ return fullPageLoadingIndicator;
+ }
+
return (
{title}
{title}
-
{subHeading}
-
-
-
- {addButtonText}
-
- {savedPaymentElements}
-
+
{savedPaymentElements}
+
{noSavedPayments}
);
};
diff --git a/packages/venia-ui/lib/components/SearchBar/__tests__/__snapshots__/suggestions.spec.js.snap b/packages/venia-ui/lib/components/SearchBar/__tests__/__snapshots__/suggestions.spec.js.snap
index f2db72c34a..a0ceb9ea45 100644
--- a/packages/venia-ui/lib/components/SearchBar/__tests__/__snapshots__/suggestions.spec.js.snap
+++ b/packages/venia-ui/lib/components/SearchBar/__tests__/__snapshots__/suggestions.spec.js.snap
@@ -3,7 +3,10 @@
exports[`renders correctly 1`] = `
- Product Suggestions
+
`;
diff --git a/packages/venia-ui/lib/components/SearchPage/__tests__/__snapshots__/searchPage.spec.js.snap b/packages/venia-ui/lib/components/SearchPage/__tests__/__snapshots__/searchPage.spec.js.snap
new file mode 100644
index 0000000000..33eaab5fc2
--- /dev/null
+++ b/packages/venia-ui/lib/components/SearchPage/__tests__/__snapshots__/searchPage.spec.js.snap
@@ -0,0 +1,798 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Search Page Component error view does not render when data is present 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component error view renders when data is not present and not loading 1`] = `
+
+
+
+`;
+
+exports[`Search Page Component filter button/modal does not render if there are no filters 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component filter button/modal renders when there are filters 1`] = `
+
+
+
+ 0 items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component loading indicator does not render when data is present 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component loading indicator renders when data is not present 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component search results does not render if data returned has empty array 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component search results heading renders a generic message if no search term 1`] = `
+
+
+
+ 1 items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component search results heading renders a specific message if search term 1`] = `
+
+
+
+ 1 items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component search results renders if data has items 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component sort button/container does not render if total count is 0 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component sort button/container renders when total count > 0 1`] = `
+
+
+
+ 1 items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component total count renders 0 items if data.products.total_count is falsy 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Search Page Component total count renders results from data 1`] = `
+
+
+
+ 1 items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/venia-ui/lib/components/SearchPage/__tests__/searchPage.spec.js b/packages/venia-ui/lib/components/SearchPage/__tests__/searchPage.spec.js
new file mode 100644
index 0000000000..b074b9e4cd
--- /dev/null
+++ b/packages/venia-ui/lib/components/SearchPage/__tests__/searchPage.spec.js
@@ -0,0 +1,225 @@
+import React from 'react';
+import createTestInstance from '@magento/peregrine/lib/util/createTestInstance';
+import { useSearchPage } from '@magento/peregrine/lib/talons/SearchPage/useSearchPage';
+import SearchPage from '../searchPage';
+
+jest.mock('@magento/peregrine/lib/talons/SearchPage/useSearchPage', () => ({
+ useSearchPage: jest.fn()
+}));
+
+jest.mock('../../Gallery', () => 'Gallery');
+jest.mock('../../FilterModal', () => 'FilterModal');
+jest.mock('../../ProductSort', () => 'ProductSort');
+jest.mock('../../../components/Pagination', () => 'Pagination');
+jest.mock('@magento/venia-ui/lib/classify');
+
+const talonProps = {
+ data: {
+ products: {
+ items: [{}]
+ }
+ },
+ error: null,
+ filters: [],
+ loading: false,
+ openDrawer: jest.fn(),
+ pageControl: {
+ currentPage: 1,
+ setPage: jest.fn(),
+ totalPages: 6
+ },
+ sortProps: [
+ {
+ sortText: 'Best Match',
+ sortAttribute: 'relevance',
+ sortDirection: 'DESC'
+ },
+ jest.fn()
+ ]
+};
+
+describe('Search Page Component', () => {
+ describe('loading indicator', () => {
+ test('does not render when data is present', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ loading: true
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ test('renders when data is not present', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ loading: true,
+ data: undefined
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ });
+
+ describe('error view', () => {
+ test('does not render when data is present', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ error: true
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+
+ test('renders when data is not present and not loading', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ error: true,
+ loading: false,
+ data: undefined
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ });
+
+ describe('search results', () => {
+ test('does not render if data returned has empty array', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ loading: true,
+ data: {
+ products: {
+ items: []
+ }
+ }
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+
+ test('renders if data has items', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ loading: true,
+ data: {
+ products: {
+ items: [{}]
+ }
+ }
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ });
+
+ describe('total count', () => {
+ test('renders results from data', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ data: {
+ products: {
+ items: [{}],
+ total_count: 1
+ }
+ }
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ test('renders 0 items if data.products.total_count is falsy', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ data: {
+ products: {
+ items: [],
+ total_count: 0
+ }
+ }
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ });
+
+ describe('filter button/modal', () => {
+ test('does not render if there are no filters', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ filters: []
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+
+ test('renders when there are filters', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ filters: [{}]
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ });
+
+ describe('sort button/container', () => {
+ test('does not render if total count is 0', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ data: {
+ products: {
+ items: [],
+ total_count: 0
+ }
+ }
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+
+ test('renders when total count > 0', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ data: {
+ products: {
+ items: [{}],
+ total_count: 1
+ }
+ }
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ });
+
+ describe('search results heading', () => {
+ test('renders a generic message if no search term', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ data: {
+ products: {
+ items: [{}],
+ total_count: 1
+ }
+ },
+ searchTerm: false
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+
+ test('renders a specific message if search term', () => {
+ useSearchPage.mockReturnValueOnce({
+ ...talonProps,
+ data: {
+ products: {
+ items: [{}],
+ total_count: 1
+ }
+ },
+ searchTerm: 'Search Term',
+ searchCategory: 'Search Category'
+ });
+ const tree = createTestInstance( );
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/venia-ui/lib/components/SearchPage/searchPage.gql.js b/packages/venia-ui/lib/components/SearchPage/searchPage.gql.js
deleted file mode 100644
index 2243ad49e1..0000000000
--- a/packages/venia-ui/lib/components/SearchPage/searchPage.gql.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { gql } from '@apollo/client';
-
-export const GET_PAGE_SIZE = gql`
- query getPageSize {
- storeConfig {
- id
- grid_per_page
- }
- }
-`;
-
-export default {
- queries: {
- getPageSize: GET_PAGE_SIZE
- }
-};
diff --git a/packages/venia-ui/lib/components/SearchPage/searchPage.js b/packages/venia-ui/lib/components/SearchPage/searchPage.js
index 7976c0b65d..4aa0698e57 100644
--- a/packages/venia-ui/lib/components/SearchPage/searchPage.js
+++ b/packages/venia-ui/lib/components/SearchPage/searchPage.js
@@ -5,23 +5,19 @@ import { shape, string } from 'prop-types';
import { useSearchPage } from '@magento/peregrine/lib/talons/SearchPage/useSearchPage';
import { mergeClasses } from '../../classify';
-import Gallery from '../Gallery';
+import Pagination from '../../components/Pagination';
import FilterModal from '../FilterModal';
+import Gallery from '../Gallery';
import { fullPageLoadingIndicator } from '../LoadingIndicator';
-import Pagination from '../../components/Pagination';
-import defaultClasses from './searchPage.css';
-import { GET_PAGE_SIZE } from './searchPage.gql';
import ProductSort from '../ProductSort';
import Button from '../Button';
+import defaultClasses from './searchPage.css';
+
const SearchPage = props => {
const classes = mergeClasses(defaultClasses, props.classes);
- const talonProps = useSearchPage({
- queries: {
- getPageSize: GET_PAGE_SIZE
- }
- });
+ const talonProps = useSearchPage();
const {
data,
@@ -38,22 +34,24 @@ const SearchPage = props => {
const [currentSort] = sortProps;
- if (loading && !data) return fullPageLoadingIndicator;
- if (error) {
- return (
-
-
-
- );
+ if (!data) {
+ if (loading) return fullPageLoadingIndicator;
+ else if (error) {
+ return (
+
+
+
+ );
+ }
}
let content;
- if (!data || data.products.items.length === 0) {
+ if (data.products.items.length === 0) {
content = (
{
);
}
- const totalCount = data ? data.products.total_count : 0;
+ const totalCount = data.products.total_count || 0;
const maybeFilterButtons =
filters && filters.length ? (
diff --git a/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap b/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap
index a8b6258be5..963f97729b 100644
--- a/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap
+++ b/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap
@@ -7,7 +7,10 @@ exports[`displays an error message if there is a sign in error 1`] = `
- Sign-in to Your Account
+
- Sign-in to Your Account
+
- An item in your cart is currently out-of-stock and must be removed in order to Checkout.
+
`;
diff --git a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/createWishlist.ee.spec.js.snap b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/createWishlist.ee.spec.js.snap
index 83ed619730..5c3383eed7 100644
--- a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/createWishlist.ee.spec.js.snap
+++ b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/createWishlist.ee.spec.js.snap
@@ -50,7 +50,10 @@ exports[`renders correctly 1`] = `
- Create a list
+
diff --git a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlist.spec.js.snap b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlist.spec.js.snap
index 60f86304a3..9ce183b461 100644
--- a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlist.spec.js.snap
+++ b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlist.spec.js.snap
@@ -195,7 +195,10 @@ exports[`render open with no items 1`] = `
className="content"
>
- There are currently no items in this list
+
diff --git a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistConfirmRemoveProductDialog.spec.js.snap b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistConfirmRemoveProductDialog.spec.js.snap
index 13c993c996..75336b8fa4 100644
--- a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistConfirmRemoveProductDialog.spec.js.snap
+++ b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistConfirmRemoveProductDialog.spec.js.snap
@@ -21,7 +21,10 @@ exports[`renders correctly 1`] = `
- Are you sure you want to delete this product from the list?
+
@@ -48,7 +51,10 @@ exports[`renders correctly when closed 1`] = `
- Are you sure you want to delete this product from the list?
+
@@ -75,7 +81,10 @@ exports[`renders correctly when removal is in progress 1`] = `
- Are you sure you want to delete this product from the list?
+
@@ -102,12 +111,18 @@ exports[`renders correctly with error 1`] = `
- There was an error deleting this product. Please try again later.
+
- Are you sure you want to delete this product from the list?
+
diff --git a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistMoreActionsDialog.spec.js.snap b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistMoreActionsDialog.spec.js.snap
index 9a869bf474..d1c46aac75 100644
--- a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistMoreActionsDialog.spec.js.snap
+++ b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistMoreActionsDialog.spec.js.snap
@@ -60,7 +60,10 @@ exports[`renders correctly when closed 1`] = `
- Move to
+
- Copy to
+
- Remove
+
@@ -258,7 +267,10 @@ exports[`renders correctly when open 1`] = `
- Move to
+
- Copy to
+
- Remove
+
diff --git a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistPage.spec.js.snap b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistPage.spec.js.snap
index f84850b459..40235b8579 100644
--- a/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistPage.spec.js.snap
+++ b/packages/venia-ui/lib/components/WishlistPage/__tests__/__snapshots__/wishlistPage.spec.js.snap
@@ -7,13 +7,19 @@ exports[`renders disabled feature error 1`] = `
- Favorites Lists
+
- Sorry, this feature has been disabled.
+
@@ -26,7 +32,10 @@ exports[`renders general fetch error 1`] = `
- Favorites Lists
+
- Something went wrong. Please refresh and try again.
+
@@ -112,7 +124,10 @@ exports[`renders loading indicator 1`] = `
- Fetching Data...
+
`;
@@ -124,7 +139,10 @@ exports[`renders wishlist data 1`] = `
- Favorites Lists
+
{
+ const savedPaymentTypes = new SavedPaymentTypes(venia);
+
+ expect(savedPaymentTypes).toMatchSnapshot();
+});
+
+test('Should add new import when add is called', () => {
+ const savedPaymentTypes = new SavedPaymentTypes(venia);
+
+ const paymentCode = 'braintree';
+ const importPath = 'path/to/the/component.js';
+ savedPaymentTypes.add({
+ paymentCode,
+ importPath
+ });
+
+ expect(add).toHaveBeenCalledWith(
+ `import ${paymentCode} from '${importPath}'`
+ );
+});
diff --git a/packages/venia-ui/lib/targets/__tests__/__snapshots__/SavedPaymentTypes.spec.js.snap b/packages/venia-ui/lib/targets/__tests__/__snapshots__/SavedPaymentTypes.spec.js.snap
new file mode 100644
index 0000000000..374ab24583
--- /dev/null
+++ b/packages/venia-ui/lib/targets/__tests__/__snapshots__/SavedPaymentTypes.spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should return correct shape 1`] = `
+SavedPaymentTypes {
+ "_methods": Object {
+ "add": [MockFunction esModuleObject.add],
+ },
+}
+`;
diff --git a/packages/venia-ui/lib/targets/__tests__/makeRoutesTarget.spec.js b/packages/venia-ui/lib/targets/__tests__/makeRoutesTarget.spec.js
new file mode 100644
index 0000000000..205bd4acf5
--- /dev/null
+++ b/packages/venia-ui/lib/targets/__tests__/makeRoutesTarget.spec.js
@@ -0,0 +1,66 @@
+const {
+ mockTargetProvider
+} = require('@magento/pwa-buildpack/lib/TestHelpers');
+
+const makeRoutesTarget = require('../makeRoutesTarget');
+const TargetableSet = require('@magento/pwa-buildpack/lib/WebpackTools/targetables/TargetableSet');
+
+const FAKE_ADDED_ROUTE = 'ADDED_ROUTE';
+
+const targets = mockTargetProvider(
+ '@magento/venia-ui',
+ (_, dep) =>
+ ({
+ '@magento/pwa-buildpack': {
+ specialFeatures: {
+ tap: jest.fn()
+ },
+ transformModules: {
+ tapPromise: jest.fn()
+ }
+ }
+ }[dep])
+);
+const targetable = TargetableSet.using(targets);
+const mockPrependJSX = jest.fn();
+
+jest.mock(
+ '../../defaultRoutes.json',
+ () => [
+ {
+ name: 'Single path route',
+ pattern: '/simple',
+ exact: true,
+ path: '../AccountInformationPage'
+ },
+ {
+ name: 'Multiple path route',
+ pattern: ['/one', '/two'],
+ exact: true,
+ path: '../AccountInformationPage'
+ }
+ ],
+ { virtual: true }
+);
+
+beforeAll(() => {
+ jest.spyOn(targetable, 'reactComponent').mockImplementation(() => ({
+ addReactLazyImport: () => FAKE_ADDED_ROUTE,
+ prependJSX: mockPrependJSX
+ }));
+});
+
+test('Call prependJSX with the correct path patterns', async () => {
+ makeRoutesTarget(targetable);
+
+ expect(mockPrependJSX).toHaveBeenNthCalledWith(
+ 1,
+ 'Switch',
+ `<${FAKE_ADDED_ROUTE}/> `
+ );
+ expect(mockPrependJSX).toHaveBeenNthCalledWith(
+ 2,
+ 'Switch',
+ `<${FAKE_ADDED_ROUTE}/> `
+ );
+});
diff --git a/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js b/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js
index 9937e5016c..790ee034e7 100644
--- a/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js
+++ b/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js
@@ -99,18 +99,18 @@ test('uses routes to inject client-routed pages', async () => {
expect(built.bundle).toContain('DynamicCheckout');
});
-test('declares payments target', async () => {
+test('declares checkoutPagePaymentTypes target', async () => {
const bus = mockBuildBus({
context: __dirname,
dependencies: [thisDep]
});
bus.runPhase('declare');
- const { payments } = bus.getTargetsOf('@magento/venia-ui');
+ const { checkoutPagePaymentTypes } = bus.getTargetsOf('@magento/venia-ui');
const interceptor = jest.fn();
// no implementation testing in declare phase
- payments.tap('test', interceptor);
- payments.call('woah');
+ checkoutPagePaymentTypes.tap('test', interceptor);
+ checkoutPagePaymentTypes.call('woah');
expect(interceptor).toHaveBeenCalledWith('woah');
});
@@ -125,36 +125,21 @@ test('uses RichContentRenderers to default strategy Payment Method', async () =>
}
);
- const payments = built.run();
- expect(payments).toHaveProperty('braintree');
+ const checkoutPagePaymentTypes = built.run();
+ expect(checkoutPagePaymentTypes).toHaveProperty('braintree');
});
-test('declares payments target', async () => {
+test('declares savedPaymentTypes target', async () => {
const bus = mockBuildBus({
context: __dirname,
dependencies: [thisDep]
});
bus.runPhase('declare');
- const { payments } = bus.getTargetsOf('@magento/venia-ui');
+ const { savedPaymentTypes } = bus.getTargetsOf('@magento/venia-ui');
const interceptor = jest.fn();
// no implementation testing in declare phase
- payments.tap('test', interceptor);
- payments.call('woah');
+ savedPaymentTypes.tap('test', interceptor);
+ savedPaymentTypes.call('woah');
expect(interceptor).toHaveBeenCalledWith('woah');
});
-
-test('uses RichContentRenderers to default strategy Payment Method', async () => {
- jest.setTimeout(WEBPACK_BUILD_TIMEOUT);
-
- const built = await buildModuleWith(
- '../../components/CheckoutPage/PaymentInformation/paymentMethodCollection.js',
- {
- context: __dirname,
- dependencies: ['@magento/peregrine', thisDep]
- }
- );
-
- const payments = built.run();
- expect(payments).toHaveProperty('braintree');
-});
diff --git a/packages/venia-ui/lib/targets/makeRoutesTarget.js b/packages/venia-ui/lib/targets/makeRoutesTarget.js
index 834f6c8220..54b252246d 100644
--- a/packages/venia-ui/lib/targets/makeRoutesTarget.js
+++ b/packages/venia-ui/lib/targets/makeRoutesTarget.js
@@ -13,9 +13,9 @@ function addRoutes(routeList, routes) {
const AddedRoute = routeList.addReactLazyImport(route.path, route.name);
routeList.prependJSX(
'Switch',
- `<${AddedRoute}/> `
+ )}}><${AddedRoute}/>`
);
}
}
diff --git a/packages/venia-ui/lib/targets/venia-ui-declare.js b/packages/venia-ui/lib/targets/venia-ui-declare.js
index 19eadf58aa..fb85ba8ed0 100644
--- a/packages/venia-ui/lib/targets/venia-ui-declare.js
+++ b/packages/venia-ui/lib/targets/venia-ui-declare.js
@@ -66,25 +66,48 @@ module.exports = targets => {
routes: new targets.types.AsyncSeriesWaterfall(['routes']),
/**
- * Provides access to Venia's payment methods
+ * Provides access to Venia's checkout page payment methods
*
- * This target lets you add new payment to your storefronts.
+ * This target lets you add new checkout page payment to your storefronts.
*
* @member {tapable.SyncHook}
*
* @see [Intercept function signature]{@link paymentInterceptFunction}
- * @see [PaymentMethodList]{@link #PaymentMethodList}
- * @see [Payment definition object]{@link PaymentDefinition}
+ * @see [CheckoutPaymentTypes]{@link #CheckoutPaymentTypesDefinition}
+ * @see [CheckoutPayment definition object]{@link CheckoutPaymentDefinition}
*
* @example Add a payment
- * targets.of('@magento/venia-ui').payments.tap(
- * payments => payments.add({
+ * targets.of('@magento/venia-ui').checkoutPagePaymentTypes.tap(
+ * checkoutPagePaymentTypes => checkoutPagePaymentTypes.add({
* paymentCode: 'braintree',
* importPath: '@magento/braintree_payment'
* })
* );
*/
- payments: new targets.types.Sync(['payments'])
+ checkoutPagePaymentTypes: new targets.types.Sync([
+ 'checkoutPagePaymentTypes'
+ ]),
+
+ /**
+ * Provides access to Venia's saved payment methods
+ *
+ * This target lets you add new saved payment method to your storefronts.
+ *
+ * @member {tapable.SyncHook}
+ *
+ * @see [Intercept function signature]{@link savedPaymentInterceptFunction}
+ * @see [SavedPaymentTypes]{@link #SavedPaymentTypesDefinition}
+ * @see [SavedPayment definition object]{@link SavedPaymentDefinition}
+ *
+ * @example Add a payment
+ * targets.of('@magento/venia-ui').savedPaymentTypes.tap(
+ * savedPaymentTypes => savedPaymentTypes.add({
+ * paymentCode: 'braintree',
+ * importPath: '@magento/braintree_payment'
+ * })
+ * );
+ */
+ savedPaymentTypes: new targets.types.Sync(['savedPaymentTypes'])
});
};
@@ -181,24 +204,24 @@ module.exports = targets => {
* }
*/
-/** Type definition related to: payments */
+/** Type definition related to: checkoutPagePaymentTypes */
/**
- * Intercept function signature for the `payments` target.
+ * Intercept function signature for the `checkoutPagePaymentTypes` target.
*
- * Interceptors of `payments` should call `.add` on the provided [payment list]{@link #PaymentMethodList}.
+ * Interceptors of `checkoutPagePaymentTypes` should call `.add` on the provided [payment list]{@link #CheckoutPaymentTypesDefinition}.
*
* @callback paymentInterceptFunction
*
- * @param {PaymentMethodList} renderers The list of payments registered
+ * @param {CheckoutPaymentTypesDefinition} renderers The list of payments registered
* so far in the build.
*
*/
/**
- * A payment definition object that describes a payment in your storefront.
+ * A payment definition object that describes a checkout page payment in your storefront.
*
- * @typedef {Object} PaymentDefinition
+ * @typedef {Object} CheckoutPaymentDefinition
* @property {string} paymentCode is use to map your payment
* @property {string} importPath Resolvable path to the component the
* Route component will render
@@ -210,24 +233,24 @@ module.exports = targets => {
* }
*/
-/** Type definition related to: payments */
+/** Type definition related to: savedPaymentTypes */
/**
- * Intercept function signature for the `payments` target.
+ * Intercept function signature for the `savedPaymentTypes` target.
*
- * Interceptors of `payments` should call `.add` on the provided [payment list]{@link #PaymentMethodList}.
+ * Interceptors of `savedPaymentTypes` should call `.add` on the provided [payment list]{@link #SavedPaymentTypesDefinition}.
*
- * @callback paymentInterceptFunction
+ * @callback savedPaymentInterceptFunction
*
- * @param {PaymentMethodList} renderers The list of payments registered
+ * @param {SavedPaymentTypesDefinition} renderers The list of saved payments registered
* so far in the build.
*
*/
/**
- * A payment definition object that describes a payment in your storefront.
+ * A payment definition object that describes a saved payment in your storefront.
*
- * @typedef {Object} PaymentDefinition
+ * @typedef {Object} SavedPaymentDefinition
* @property {string} paymentCode is use to map your payment
* @property {string} importPath Resolvable path to the component the
* Route component will render
@@ -235,6 +258,6 @@ module.exports = targets => {
* @example A custom payment method
* const myCustomPayment = {
* paymentCode: 'cc',
- * importPath: '@partner/module/path_to_your_component'
+ * importPath: '@partner/module/path_to_your_component'
* }
*/
diff --git a/packages/venia-ui/lib/targets/venia-ui-intercept.js b/packages/venia-ui/lib/targets/venia-ui-intercept.js
index 4d41478a1c..cf2ea3f59a 100644
--- a/packages/venia-ui/lib/targets/venia-ui-intercept.js
+++ b/packages/venia-ui/lib/targets/venia-ui-intercept.js
@@ -4,7 +4,8 @@
const { Targetables } = require('@magento/pwa-buildpack');
const RichContentRendererList = require('./RichContentRendererList');
const makeRoutesTarget = require('./makeRoutesTarget');
-const PaymentMethodList = require('./PaymentMethodList');
+const CheckoutPagePaymentsList = require('./CheckoutPagePaymentsList');
+const SavedPaymentTypes = require('./SavedPaymentTypes');
module.exports = veniaTargets => {
const venia = Targetables.using(veniaTargets);
@@ -27,10 +28,17 @@ module.exports = veniaTargets => {
importPath: './plainHtmlRenderer'
});
- const paymentMethodList = new PaymentMethodList(venia);
- paymentMethodList.add({
+ const checkoutPagePaymentsList = new CheckoutPagePaymentsList(venia);
+ checkoutPagePaymentsList.add({
paymentCode: 'braintree',
importPath:
'@magento/venia-ui/lib/components/CheckoutPage/PaymentInformation/creditCard'
});
+
+ const savedPaymentTypes = new SavedPaymentTypes(venia);
+ savedPaymentTypes.add({
+ paymentCode: 'braintree',
+ importPath:
+ '@magento/venia-ui/lib/components/SavedPaymentsPage/creditCard'
+ });
};
diff --git a/packages/venia-ui/package.json b/packages/venia-ui/package.json
index 1cf8c1f49c..197025a30f 100644
--- a/packages/venia-ui/package.json
+++ b/packages/venia-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@magento/venia-ui",
- "version": "5.0.0",
+ "version": "6.0.0",
"publishConfig": {
"access": "public"
},
@@ -76,8 +76,8 @@
"peerDependencies": {
"@apollo/client": "~3.1.2",
"@magento/babel-preset-peregrine": "~1.1.0",
- "@magento/peregrine": "~8.0.0",
- "@magento/pwa-buildpack": "~7.0.0",
+ "@magento/peregrine": "~9.0.0",
+ "@magento/pwa-buildpack": "~8.0.0",
"apollo-cache-persist": "~0.1.1",
"braintree-web-drop-in": "~1.16.0",
"graphql": "~15.3.0",
diff --git a/packages/venia-ui/upward.yml b/packages/venia-ui/upward.yml
index a0d009723a..42ff06420b 100644
--- a/packages/venia-ui/upward.yml
+++ b/packages/venia-ui/upward.yml
@@ -31,6 +31,14 @@ staticFromRoot:
resolver: inline
inline:
content-type: contentTypeFromExtension
+ cache-control:
+ when:
+ - matches: env.NODE_ENV
+ pattern: 'production'
+ use:
+ inline: public, max-age=604800
+ default:
+ inline: no-cache, no-store, must-revalidate
body:
resolver: file
parse:
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/css-modules/index.md b/pwa-devdocs/_archived/css-modules/index.md
similarity index 100%
rename from pwa-devdocs/src/tutorials/pwa-studio-fundamentals/css-modules/index.md
rename to pwa-devdocs/_archived/css-modules/index.md
diff --git a/pwa-devdocs/src/tutorials/hello-upward/adding-react/index.md b/pwa-devdocs/_archived/hello-upward/adding-react/index.md
similarity index 100%
rename from pwa-devdocs/src/tutorials/hello-upward/adding-react/index.md
rename to pwa-devdocs/_archived/hello-upward/adding-react/index.md
diff --git a/pwa-devdocs/src/tutorials/hello-upward/simple-server/index.md b/pwa-devdocs/_archived/hello-upward/simple-server/index.md
similarity index 100%
rename from pwa-devdocs/src/tutorials/hello-upward/simple-server/index.md
rename to pwa-devdocs/_archived/hello-upward/simple-server/index.md
diff --git a/pwa-devdocs/src/tutorials/hello-upward/using-template-resolver/index.md b/pwa-devdocs/_archived/hello-upward/using-template-resolver/index.md
similarity index 100%
rename from pwa-devdocs/src/tutorials/hello-upward/using-template-resolver/index.md
rename to pwa-devdocs/_archived/hello-upward/using-template-resolver/index.md
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/manage-state/index.md b/pwa-devdocs/_archived/manage-state/index.md
similarity index 100%
rename from pwa-devdocs/src/tutorials/pwa-studio-fundamentals/manage-state/index.md
rename to pwa-devdocs/_archived/manage-state/index.md
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/using-component-props/images/prop-types-error.png b/pwa-devdocs/_archived/using-component-props/images/prop-types-error.png
similarity index 100%
rename from pwa-devdocs/src/tutorials/pwa-studio-fundamentals/using-component-props/images/prop-types-error.png
rename to pwa-devdocs/_archived/using-component-props/images/prop-types-error.png
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/using-component-props/index.md b/pwa-devdocs/_archived/using-component-props/index.md
similarity index 100%
rename from pwa-devdocs/src/tutorials/pwa-studio-fundamentals/using-component-props/index.md
rename to pwa-devdocs/_archived/using-component-props/index.md
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/work-with-graphql/index.md b/pwa-devdocs/_archived/work-with-graphql/index.md
similarity index 100%
rename from pwa-devdocs/src/tutorials/pwa-studio-fundamentals/work-with-graphql/index.md
rename to pwa-devdocs/_archived/work-with-graphql/index.md
diff --git a/pwa-devdocs/package.json b/pwa-devdocs/package.json
index 75ad6c817a..150fcdd829 100644
--- a/pwa-devdocs/package.json
+++ b/pwa-devdocs/package.json
@@ -1,7 +1,7 @@
{
"name": "pwa-devdocs",
"private": true,
- "version": "8.0.0",
+ "version": "9.0.0",
"description": "A documentation site for Magento PWA",
"main": "gulpfile.js",
"dependencies": {
diff --git a/pwa-devdocs/src/_data/getting-started.yml b/pwa-devdocs/src/_data/getting-started.yml
index a03f618a47..dc0397de2b 100644
--- a/pwa-devdocs/src/_data/getting-started.yml
+++ b/pwa-devdocs/src/_data/getting-started.yml
@@ -56,6 +56,9 @@ entries:
- label: Configuration management
url: /pwa-buildpack/configuration-management/
+ - label: Extensibility framework
+ url: /pwa-buildpack/extensibility-framework/
+
- label: Scaffolding
url: /pwa-buildpack/scaffolding/
diff --git a/pwa-devdocs/src/_data/home-sections.yml b/pwa-devdocs/src/_data/home-sections.yml
index ef21fea73c..0157830ed2 100644
--- a/pwa-devdocs/src/_data/home-sections.yml
+++ b/pwa-devdocs/src/_data/home-sections.yml
@@ -4,7 +4,7 @@
- title: Tutorials
content: Step-by-step instructions to help you become familiar with PWA Studio tools and concepts.
- url: /tutorials/pwa-studio-fundamentals/
+ url: /tutorials/
- title: Basic Concepts
content: Things you need to know to work with the Magento PWA Studio tools.
diff --git a/pwa-devdocs/src/_data/tutorials.yml b/pwa-devdocs/src/_data/tutorials.yml
index a628a98f2f..31b9c8a6db 100644
--- a/pwa-devdocs/src/_data/tutorials.yml
+++ b/pwa-devdocs/src/_data/tutorials.yml
@@ -1,63 +1,45 @@
title: Tutorials
+
entries:
- - label: Overview
+ - label: Getting Started
url: /tutorials/
+ entries:
+
+ - label: Setup your project
+ url: /tutorials/pwa-studio-fundamentals/project-setup/
- - label: Magento Cloud deployment
- url: /tutorials/cloud-deploy/
+ - label: Explore the project structure
+ url: /tutorials/pwa-studio-fundamentals/project-structure/
- - label: Hello UPWARD
+ - label: Basic modifications
entries:
- - label: Creating a simple server
- url: /tutorials/hello-upward/simple-server/
- - label: Using the TemplateResolver
- url: /tutorials/hello-upward/using-template-resolver/
+ - label: Enable SASS or LESS support
+ url: /tutorials/enable-sass-less-support/
- - label: Adding React
- url: /tutorials/hello-upward/adding-react/
+ - label: Add a static route
+ url: /tutorials/pwa-studio-fundamentals/add-a-static-route/
- - label: Create a TagList component
- url: /tutorials/create-taglist-component/
+ - label: Modify the site footer
+ url: /tutorials/pwa-studio-fundamentals/modify-site-footer/
- - label: Enable SASS or LESS support
- url: /tutorials/enable-sass-less-support/
+ - label: Create a TagList component
+ url: /tutorials/create-taglist-component/
- - label: Intercept a Target
+ - label: Target interception
url: /tutorials/intercept-a-target/
entries:
+
- label: Modify talon results
url: /tutorials/intercept-a-target/modify-talon-results/
- label: Add a new environment variable
url: /tutorials/intercept-a-target/add-new-environment-variable/
- - label: PWA Studio fundamentals
- url: /tutorials/pwa-studio-fundamentals/
+ - label: Production deployments
+ url: /tutorials/pwa-studio-fundamentals/production-launch-checklist/
entries:
- - label: Project setup
- url: /tutorials/pwa-studio-fundamentals/project-setup/
-
- - label: Project structure
- url: /tutorials/pwa-studio-fundamentals/project-structure/
-
- - label: Add a static route
- url: /tutorials/pwa-studio-fundamentals/add-a-static-route/
-
- - label: Modify the site footer
- url: /tutorials/pwa-studio-fundamentals/modify-site-footer/
-
- - label: Using component props
- url: /tutorials/pwa-studio-fundamentals/using-component-props/
-
- - label: CSS modules
- url: /tutorials/pwa-studio-fundamentals/css-modules/
-
- - label: Manage state
- url: /tutorials/pwa-studio-fundamentals/manage-state/
- - label: Work with GraphQL
- url: /tutorials/pwa-studio-fundamentals/work-with-graphql/
+ - label: Deploy to Magento Cloud
+ url: /tutorials/cloud-deploy/
- - label: Production launch checklist
- url: /tutorials/pwa-studio-fundamentals/production-launch-checklist/
diff --git a/pwa-devdocs/src/_includes/layout/toc.html b/pwa-devdocs/src/_includes/layout/toc.html
index 903133510e..435c6c5694 100644
--- a/pwa-devdocs/src/_includes/layout/toc.html
+++ b/pwa-devdocs/src/_includes/layout/toc.html
@@ -10,7 +10,7 @@ {{toc.title}}
{% if entry.url %}
{{ entry.label }}
{% else %}
- {{ entry.label }}
+ {{ entry.label }}
{% endif %}
{% if entry.entries.size > 0 %}
diff --git a/pwa-devdocs/src/frequently-asked-questions/index.md b/pwa-devdocs/src/frequently-asked-questions/index.md
index acc2b59465..e1f7f3e0f8 100644
--- a/pwa-devdocs/src/frequently-asked-questions/index.md
+++ b/pwa-devdocs/src/frequently-asked-questions/index.md
@@ -26,8 +26,6 @@ The Tutorials section of this site contains a number of tutorials which cover cu
- [How to add a static route][]
- [How to modify the site footer][]
-- [How to use component props][]
-- [How to use CSS modules][]
## Are there any live stores built using PWA Studio
@@ -101,10 +99,6 @@ _**Note:** For testing, resize the viewport manually instead of using the native
[how to modify the site footer]: <{%link tutorials/pwa-studio-fundamentals/modify-site-footer/index.md %}>
-[how to use component props]: <{%link tutorials/pwa-studio-fundamentals/using-component-props/index.md %}>
-
-[how to use css modules]: <{%link tutorials/pwa-studio-fundamentals/css-modules/index.md %}>
-
[magento cloud deployment]: <{% link tutorials/cloud-deploy/index.md %}>
[project repository]: https://github.com/magento/pwa-studio
diff --git a/pwa-devdocs/src/peregrine/routing/index.md b/pwa-devdocs/src/peregrine/routing/index.md
index ba9aab5332..46abb9b834 100644
--- a/pwa-devdocs/src/peregrine/routing/index.md
+++ b/pwa-devdocs/src/peregrine/routing/index.md
@@ -26,8 +26,6 @@ Early versions of the Venia storefront used this approach, but
in the current version, every page request now returns the same HTML with the application shell.
The application decides how the page should render based on the request.
-If you want a better idea of how UPWARD works, follow the [Hello UPWARD tutorial][].
-
### Client-side routing
Client-side routing happens inside the storefront application.
@@ -73,7 +71,6 @@ The [`resolveUnknownRoute()`][] function is a utility function for fetching page
The `getRouteComponent()` function uses the information from this query to get the correct root component from an object that maps page types to root components.
[upward]: {%link venia-pwa-concept/features/modular-components/index.md %}
-[hello upward tutorial]: {%link tutorials/hello-upward/simple-server/index.md %}
[peregrine talon]: {%link peregrine/talons/index.md %}
[app component]: https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/App/app.js
diff --git a/pwa-devdocs/src/pwa-buildpack/configuration-management/index.md b/pwa-devdocs/src/pwa-buildpack/configuration-management/index.md
index 1e6481838d..c6422f39c1 100644
--- a/pwa-devdocs/src/pwa-buildpack/configuration-management/index.md
+++ b/pwa-devdocs/src/pwa-buildpack/configuration-management/index.md
@@ -122,7 +122,7 @@ class MyWebpackPlugin {
**Good**: passing plain objects created by the Configuration object
```js
-const projectConfig = loadEnvironment(__dirname);
+const projectConfig = await loadEnvironment(__dirname);
await PWADevServer.configure({
publicPath: config.output.publicPath,
graphqlPlayground: true,
diff --git a/pwa-devdocs/src/pwa-buildpack/extensibility-framework/images/extensibility-overview.svg b/pwa-devdocs/src/pwa-buildpack/extensibility-framework/images/extensibility-overview.svg
new file mode 100644
index 0000000000..a4fc8e39d3
--- /dev/null
+++ b/pwa-devdocs/src/pwa-buildpack/extensibility-framework/images/extensibility-overview.svg
@@ -0,0 +1 @@
+Extended Storefront Package B Base Storefront Build Phase Install Phase Final Result Package A Project installs Package A as a dependency PWA Studio adds Package A code to the static code bundles The final application includes features provided by Package A Project installs Package A and Package B as dependencies Package A Package A declares its extension points as Targets Package B provides instructions for intercepting and extending Package A Targets PWA Studio modifies Target code in Package A with logic from Package B before adding it to the static code bundles The final application includes features provided by Package A modified by Package B declare.js intercept.js
\ No newline at end of file
diff --git a/pwa-devdocs/src/pwa-buildpack/extensibility-framework/images/interceptor-pattern.svg b/pwa-devdocs/src/pwa-buildpack/extensibility-framework/images/interceptor-pattern.svg
new file mode 100644
index 0000000000..4356a6b4ea
--- /dev/null
+++ b/pwa-devdocs/src/pwa-buildpack/extensibility-framework/images/interceptor-pattern.svg
@@ -0,0 +1 @@
+Module A Module C Module B Modified Logic Flow Normal Logic Flow Module A Targets
\ No newline at end of file
diff --git a/pwa-devdocs/src/pwa-buildpack/extensibility-framework/index.md b/pwa-devdocs/src/pwa-buildpack/extensibility-framework/index.md
new file mode 100644
index 0000000000..35ba489123
--- /dev/null
+++ b/pwa-devdocs/src/pwa-buildpack/extensibility-framework/index.md
@@ -0,0 +1,227 @@
+---
+title: Extensibility framework
+---
+
+PWA Studio's extensibility framework provides the tools you need to customize and extend your storefront without copying over code from the core PWA Studio project.
+The extensibility framework gives developers the ability to:
+
+- Extend the base Venia storefront with minimal core code duplication
+- Create and install extensions for PWA Studio storefronts
+- Create their own extendible modules and storefronts
+
+## How it works
+
+The extensibility framework uses a modular approach for modifying application behavior.
+It applies configurations and customizations defined inside extensions you install in the project.
+
+_Extensions_ for PWA Studio storefronts are normal NPM packages you install as a project dependency.
+These packages contain instructions that affect the build process and static code output for the generated application bundles.
+By modifying the output code during build time, there is no runtime performance cost associated with changing the storefront behavior.
+
+The following diagram illustrates the general build process for a basic storefront with no extensions and a storefront that installs an extension.
+
+![extensibility-overview][]
+
+This is different from a _plugin architecture_ where the application detects and dispatches plugins as the front end loads in the browser.
+The more plugins you install with this architecture, the slower the application gets as it becomes bloated with overhead processes.
+
+### Interceptor pattern
+
+The extensibility framework implements an [interceptor pattern][] to allow changes to the build process.
+The interceptor pattern lets a module **C** intercept the flow of logic between module **A** and module **B** and add its own logic.
+
+![interceptor-pattern-image][]
+
+In PWA Studio, the points where a module may intercept the normal flow of logic and add their own are [Targets][].
+The framework uses the [BuildBus][] to gather instructions from each extension's [Intercept File][intercept files].
+These files determine which Targets the extension intercepts and extends during the build process.
+
+## Targets
+
+_Targets_ are objects that represent areas in code you can access and intercept.
+These extension points are variants of a simple, common JavaScript pattern called the [Tapable hook][tapable] and share some functionality with NodeJS's [`EventEmitter` class][].
+
+Targets let you choose a code process's extension point _by name_ and let you run interceptor code when that process executes.
+Unlike `EventEmmitter` objects, Targets have defined behavior for how and in what order they run their interceptors.
+How those interceptors change the logic is also a defined behavior for Targets.
+
+Targets are part of a package's public API along with the modules it exports.
+They provide another layer of customization on top of using plain code composition techniques with modules.
+
+### TargetProviders
+
+A _TargetProvider_ is an object that manages the connections between modules and their Targets.
+This object is available to your module's [intercept file][intercept files] and is the API you use to access Targets or declare them.
+
+Each extension receives its own TargetProvider in its intercept and declare files.
+Use this object to declare a module's own targets, intercept its own targets, or intercept the targets of other extensions.
+
+### Example
+
+An example of a Target is the [`richContentRenderers` target][] declared by the Venia UI package.
+This Target lets you change the behavior of the `RichContent` component across your project by adding a rendering strategy.
+Tapping into this Target gives access to a `richContentRenders` list object in your intercept function.
+Calling the `add()` function on this object lets you add a custom rendering strategy for your storefront.
+
+The [Page Builder extension][] provides an example of how to intercept this Target.
+It taps the Target and provides an intercept function that adds a custom renderer for any detected Page Builder content.
+
+```js
+targets
+ .of('@magento/venia-ui')
+ .richContentRenderers.tap(richContentRenderers => {
+ richContentRenderers.add({
+ componentName: 'PageBuilder',
+ importPath: '@magento/pagebuilder'
+ });
+ });
+```
+
+## Declare files
+
+A _declare file_ lists the Targets available for interception in an extension or package.
+During build time, the framework looks for this file using the value set for `pwa-studio.targets.declare` in your project's `package.json` file.
+The framework registers the Targets in all the declare files it finds in the project and dependencies before it runs any [intercept files][].
+This guarantees Target availability to any dependent interceptor.
+
+Declare files must export a function that accepts a TargetProvider object as a parameter.
+It provides a `declare()` function to register your project's Targets by providing it a dictionary object,
+which maps a unique name to its Target.
+
+When you register your Target, the dictionary key is the named property a developer uses to access that Target.
+In the Page Builder example, the dictionary key for the rich content renderers Target that Venia UI declares is `richContentRenderers`.
+
+The value associated with the key is a variant of a Tapable hook created using one of the class constructors available under the TargetProvider object's `types` property.
+Each class constructor accepts an optional array of strings that represent the list of arguments passed to the [intercept function][].
+
+The TargetProvider object exposes the same classes as the [Tapable][] package except their names do not end with 'Hook'.
+This is intentional to avoid confusion with the concept of React Hooks.
+For example, the `SyncWaterfallHook` class from Tapable is `SyncWaterfall` under the TargetProvider object's `types` property.
+
+See the [Hook types][] section of the Tapable documentation to learn about the available hook types.
+
+### Example declare file content
+
+```js
+module.exports = targets => {
+ targets.declare({
+ perfReport: new targets.types.AsyncParallel(['report'])
+ socialIcons: new targets.types.SyncWaterfall(['iconlist'])
+ });
+}
+```
+
+The example provided declares two Targets that this project or other modules can intercept, a `perfReport` Target and a `socialIcons` Target.
+Intercept files can access these Targets using `targets.of().perfReport` and `targets.of().socialIcons` respectively.
+
+The `perfReport` key maps to a Target type that runs its interceptors asynchronously and in parallel.
+This is appropriate for logging and monitoring interceptors that do not affect functionality.
+
+The `socialIcons` key maps to a Target type that runs its interceptors synchronously and in subscription order, which passes its return values as arguments to the next interceptor.
+This is appropriate for customizations that must happen in a predictable order.
+
+Both targets passes in a single parameter to their intercept functions, a `report` argument and an `iconlist` argument.
+
+## Intercept files
+
+The _intercept file_ describes the targets you want to intercept and define or extend.
+During build time, the framework looks for this file using the value for `pwa-studio.targets.intercept` in your project's `package.json` file.
+These files execute after all the declare files register their Targets.
+
+Your intercept files must export a function that accepts a TargetProvider object as a parameter.
+This object gives you access to all available Targets in your project that let you make customizations, gather data, change the build itself, or develop and call your own declared targets.
+The TargetProvider object provides an `of()` function and an `own` property to access Targets in other packages or Targets within its own module respectively.
+
+For more information on how intercept files work, see the tutorial on how to [Intercept a Target][].
+
+### Example intercept file content
+
+```js
+module.exports = targets => {
+
+ const builtins = targets.of("@magento/pwa-buildpack");
+ builtins.specialFeatures.tap((featuresByModule) => {
+ featuresByModule["my-extension"] = {
+ esModules: true,
+ };
+ });
+};
+}
+```
+
+The example provided defines an intercept file that intercepts the `specialFeatures` target in the `@magento/pwa-buildpack` package.
+It adds a webpack configuration for the `my-extension` package that lets the build process know that the package uses ES modules.
+
+### Intercept functions
+
+When you call the `tap()` function on a Target, you supply it with an _intercept function_.
+An intercept function is a callback function that provides the interception logic for a specific Target.
+Calling the `tap()` function registers your intercept function with that Target.
+When the framework builds your project, it generates code that calls your intercept function when the project runs the Target code.
+
+The function signature for an intercept function depends on the tapped Target.
+In the Page Builder example, the intercept function signature when you tap the `richContentRenderers` target is a function that receives a list object, which lets you add custom rendering strategies.
+Other Targets may require you to return a modified value, use an object with a specific API, or provide a configuration.
+
+Read the reference API on this site or in doc blocks in the source code to learn about the intercept function signatures for each Target.
+
+## Writing intercept and declare files
+
+When writing intercept and declare files, keep in mind the following requirements:
+
+- Both files must export a function that accepts a TargetProvider object as a parameter.
+- Both files are CommonJS modules that run in Node.
+- You can create these files anywhere in your project, but you must specify their paths in your `package.json` file.
+
+As shown in the previous examples, a common practice when authoring these files involve assigning the TargetProvider object to a `targets` variable.
+
+## Targets in PWA Studio packages
+
+When you create a new storefront project using the scaffolding tool, you have access to all the same PWA Studio Targets as the Venia storefront.
+The following is a list of PWA Studio packages that contain Targets.
+
+[Buildpack][]
+: Targets in the Buildpack are low level and generic.
+They are often used as building blocks for more complicated feature Targets.
+You can also find Targets that let you add environment variables or change UPWARD behavior in this package.
+
+[Peregrine][]
+: Targets in the Peregrine package focus mainly on the set of talons it provides.
+The `talons` Target lets you [wrap a Talon][] with your own module.
+
+[Venia UI][]
+: Targets in the Venia UI provide access to the list of items used in the UI components.
+These Targets let you add new routes, rendering strategies, and payment methods.
+
+## Extension examples
+
+The PWA Studio scaffolding tool also installs extensions on all new storefront projects.
+These extensions use the framework to add useful features on top of the base application.
+They are also examples of what a PWA Studio storefront extension looks like.
+
+[@magento/upward-security-headers][]
+: This extension adds security headers to UPWARD by tapping into the `transformUpward` Target in Buildpack.
+
+[@magento/venia-sample-language-packs][]
+: This extension provides sample translations for PWA Studio's internationalization feature.
+
+[`richcontentrenderers` target]: <{% link venia-ui/reference/targets/index.md %}#richContentRenderers>
+[intercept a target]: <{% link tutorials/intercept-a-target/index.md %}>
+[buildpack]: <{% link pwa-buildpack/reference/targets/index.md %}>
+[peregrine]: <{% link peregrine/reference/targets/index.md %}>
+[wrap a talon]: <{% link tutorials/intercept-a-target/modify-talon-results/index.md %}>
+[venia ui]: <{% link venia-ui/reference/targets/index.md %}>
+
+[interceptor pattern]: https://en.wikipedia.org/wiki/Interceptor_pattern
+[interceptor-pattern-image]: ./images/interceptor-pattern.svg
+[extensibility-overview]: ./images/extensibility-overview.svg
+[buildbus]: https://github.com/magento/pwa-studio/blob/develop/packages/pwa-buildpack/lib/BuildBus/BuildBus.js
+[targets]: #targets
+[intercept files]: #intercept-files
+[intercept function]: #intercept-functions
+[tapable]: https://github.com/webpack/tapable
+[hook types]: https://github.com/webpack/tapable#hook-types
+[`eventemitter` class]: https://nodejs.org/api/events.html#events_class_eventemitter
+[page builder extension]: https://github.com/magento/pwa-studio/blob/develop/packages/pagebuilder/lib/intercept.js
+[@magento/upward-security-headers]: https://github.com/magento/pwa-studio/tree/develop/packages/extensions/upward-security-headers
+[@magento/venia-sample-language-packs]: https://github.com/magento/pwa-studio/tree/develop/packages/extensions/venia-sample-language-packs
diff --git a/pwa-devdocs/src/pwa-buildpack/reference/buildpack-cli/load-env/index.md b/pwa-devdocs/src/pwa-buildpack/reference/buildpack-cli/load-env/index.md
index 6e0ad5b889..f310e3779c 100644
--- a/pwa-devdocs/src/pwa-buildpack/reference/buildpack-cli/load-env/index.md
+++ b/pwa-devdocs/src/pwa-buildpack/reference/buildpack-cli/load-env/index.md
@@ -37,7 +37,7 @@ Loads a given directory's `.env` file and provides a [configuration object][].
```js
const { loadEnvironment } = require('@magento/pwa-buildpack');
-const configuration = loadEnvironment(process.cwd());
+const configuration = await loadEnvironment(process.cwd());
```
#### Parameters
@@ -91,7 +91,7 @@ import { loadEnvironment } from '@magento/pwa-buildpack';
// Give `loadEnvironment` the path to the project root.
// If the current file is in project root, use the Node builtin `__dirname`.
-const configuration = loadEnvironment('/Users/me/path/to/project');
+const configuration = await loadEnvironment('/Users/me/path/to/project');
// `loadEnvironment` has now read the contents of
// `/Users/me/path/to/project/.env` and merged it with any environment
diff --git a/pwa-devdocs/src/pwa-buildpack/troubleshooting/index.md b/pwa-devdocs/src/pwa-buildpack/troubleshooting/index.md
index 9f5ba25c7b..39d625e16e 100644
--- a/pwa-devdocs/src/pwa-buildpack/troubleshooting/index.md
+++ b/pwa-devdocs/src/pwa-buildpack/troubleshooting/index.md
@@ -22,6 +22,7 @@ Paste the result console output into the issue. Thank you!
* [Webpack hangs for a long time before beginning compilation](#webpack-hangs)
* [Browser cannot resolve the `.local.pwadev` site](#cannot-resolve-site)
* [Browser does not trust the generated SSL certificate](#untrusted-ssl-cert)
+* [Watcher runs out of memory](#watcher-out-of-memory)
## Resolutions
@@ -95,6 +96,11 @@ Some users have suggested deleting the `devcert` folder to trigger certificate r
${User}\AppData\Local\devcert
```
+**Watcher runs out of memory**{:#watcher-out-of-memory}
+
+If you leave the `yarn watch` process running for an extended amount of time, it runs out of memory and shows an error.
+The solution is to restart the watch process and make sure it does not run for an extended amount of time.
+
[create an issue]: https://github.com/magento/pwa-buildpack/issues
[Slack channel]: https://magentocommeng.slack.com/messages/C71HNKYS2/team/UAFV915FB/
[host file]: https://en.wikipedia.org/wiki/Hosts_(file)
diff --git a/pwa-devdocs/src/technologies/overview/index.md b/pwa-devdocs/src/technologies/overview/index.md
index 1876869653..fe41d3f8ab 100644
--- a/pwa-devdocs/src/technologies/overview/index.md
+++ b/pwa-devdocs/src/technologies/overview/index.md
@@ -18,6 +18,13 @@ The following features define a basic PWA website:
- **Mobile "Install"** - Mobile users can add PWA sites to their home screens and even receive Push notifications from the site.
- **Shareable content** - Each page in a PWA site has a unique URL that can be shared with other apps or social media.
+This strategy is known as the [RAIL](https://web.dev/rail/) model:
+
+- **Response** – An application is receptive to the user's request.
+- **Animation** – It shows a movement to keep the user pausing.
+- **Idle** – A PWA utilizes the "idle" second to cache content.
+- **Load** – It loads under a moment.
+
## What is the Magento PWA Studio project
![pwa studio overview][]
diff --git a/pwa-devdocs/src/tutorials/cloud-deploy/index.md b/pwa-devdocs/src/tutorials/cloud-deploy/index.md
index eebf29f48f..76817b5704 100644
--- a/pwa-devdocs/src/tutorials/cloud-deploy/index.md
+++ b/pwa-devdocs/src/tutorials/cloud-deploy/index.md
@@ -2,238 +2,227 @@
title: Magento Cloud deployment
---
-## Prerequisites
-
-- An existing Magento Cloud project
-- Composer
-- Yarn or NPM
-- A [Magento PWA Studio][] storefront managed by NPM or Yarn.
- You are not required to publish to npmjs.com, but
- NPM or Yarn should be able to access your project code through Git.
+[Magento Commerce Cloud][] is a managed, automated hosting platform for the Magento Commerce software.
+You can use this platform to host your storefront code by installing packages developed specifically to connect your storefront with Magento on the same server.
-For this tutorial, the [`@magento/venia-concept`][] package for the [Venia storefront][] is used as a template, but any PWA available though NPM with an UPWARD compatible YAML file is supported.
+This tutorial provides the general steps for adding your storefront onto your Magento Commerce Cloud project and setting it as the front end application.
+By the end of this tutorial, you will have a Cloud project setup that includes your storefront project's code bundles.
+You can use this setup to update and deploy your storefront project in Magento Commerce Cloud.
-These instructions provide a method for building your application bundle in the Magento Cloud, but
-you can get the same results by building locally with the same environment variables as your targeted production environment, and then checking your local build artifacts into source control.
-
-{: .bs-callout .bs-callout-warning}
-Magento Cloud does not support node processes, so you cannot use UPWARD-JS to serve your storefront project.
-You must use the [magento2-upward-connector][] module with [UPWARD-PHP][] to deploy your storefront in Magento Cloud.
+## Prerequisites
-## Add Composer dependencies
+Before you follow this tutorial, you should be familiar with Cloud's [Starter workflow][] or [Pro workflow][] depending on your plan.
+Make sure you complete the [Cloud onboarding tasks][] to avoid account or access issues.
-Use the `composer` CLI command to include the UPWARD module in the Magento installation:
+Verify that your Magento instance is [compatible][] with the PWA Studio version you use in your storefront project.
-```sh
-composer require magento/module-upward-connector
-```
+This tutorial requires the following tools:
-This command modifies the `composer.json` file and adds the package entry to the `require` section of the `composer.json` file.
+- [Magento Cloud CLI][]
+- Git
+- [Composer][]
+- Yarn or NPM (depends on your storefront project configuration)
-```json
-"magento/module-upward-connector": "^1.0.1"
-```
+If you need to do more advanced Cloud tasks, see the [Cloud technologies and requirements][] for the full list of tools you need to fully manage the rest of your Cloud project.
-## Add Venia sample data (optional)
+You also need an existing storefront project to do this tutorial.
+Follow the instructions on the [Project setup][] page to set up your storefront project using the scaffolding tool.
-The Venia storefront works best with the Venia sample data installed. There is an [automated script](https://pwastudio.io/venia-pwa-concept/install-sample-data/) in the `@magento/venia-concept` package, or you can follow the manual steps here.
+## Tasks overview
-If you are deploying your own custom storefront, you may skip this step and continue to the next section.
+1. Clone your Cloud project
+1. Add required Magento extensions
+1. Set environment variables
+1. Build your storefront application
+1. Add your storefront project code
+1. Deploy changes
-### Add sample data repository information
+## Clone your Cloud project
-Use the `composer` CLI to add the sample data repositories to the `composer.json` file:
+Use the Magento Cloud CLI tool to login and clone your Cloud project.
-```sh
-composer config repositories.catalog-venia vcs https://github.com/PMET-public/module-catalog-sample-data-venia
-```
+Run the following command:
```sh
-composer config repositories.configurable-venia vcs https://github.com/PMET-public/module-configurable-sample-data-venia
+magento-cloud
```
-```sh
-composer config repositories.customer-venia vcs https://github.com/PMET-public/module-customer-sample-data-venia
-```
+If this is your first time running this command, the tool takes you through the login process.
+After you login or when you run this command again, it presents a table of all the projects you have permission to access.
```sh
-composer config repositories.tax-venia vcs https://github.com/PMET-public/module-tax-sample-data-venia
+Your projects are:
++---------------+------------------+---------------------------------------------------+
+| ID | Title | URL |
++---------------+------------------+---------------------------------------------------+
+| yswqmrbknvqjz | My Magento Store | https://us-4.magento.cloud/projects/yswqmrbknvqjz |
++---------------+------------------+---------------------------------------------------+
```
-```sh
-composer config repositories.sales-venia vcs https://github.com/PMET-public/module-sales-sample-data-venia
-```
+Find the Cloud project you want to add your storefront to and use the Magento Cloud CLI to clone the project by specifying its ID.
```sh
-composer config repositories.media-venia vcs https://github.com/PMET-public/sample-data-media-venia
-```
-
-These commands add the following entries to the `composer.json` file:
-
-```json
-"catalog-venia": {
- "type": "vcs",
- "url": "https://github.com/PMET-public/module-catalog-sample-data-venia"
-
-},
- "configurable-venia": {
- "type": "vcs",
- "url": "https://github.com/PMET-public/module-configurable-sample-data-venia"
-
- },
- "customer-venia": {
- "type": "vcs",
- "url": "https://github.com/PMET-public/module-customer-sample-data-venia"
-
- },
- "tax-venia": {
- "type": "vcs",
- "url": "https://github.com/PMET-public/module-tax-sample-data-venia"
-
- },
- "sales-venia": {
- "type": "vcs",
- "url": "https://github.com/PMET-public/module-sales-sample-data-venia"
-
- },
- "media-venia": {
- "type": "vcs",
- "url": "https://github.com/PMET-public/sample-data-media-venia"
-
- },
+magento-cloud get yswqmrbknvqjz
```
-### Require the sample data modules
+This command creates a project directory and initializes the Git repository associated with your Cloud project.
+Depending on your access permissions for the default environment, this directory may appear empty.
-Run the following `composer` CLI commands to install the sample data modules into Magento:
+Navigate to the project directory and use the Magento Cloud CLI to list the environments for this project.
```sh
-composer require magento/sample-data-media-venia:dev-master
+magento-cloud environment:list
```
-```sh
-composer require magento/module-catalog-sample-data-venia:dev-master
-```
+This command displays a table of environments which lists their ID, Title, and Status.
```sh
-composer require magento/module-configurable-sample-data-venia:dev-master
+Your environments are:
++----------------------+----------------------+-------------+
+| ID | Title | Status |
++----------------------+----------------------+-------------+
+| production* | Production | Active |
+| staging | Staging | Active |
+| myStorefront-develop | myStorefront-develop | In progress |
+| myStorefront-cicd | myStorefront-cicd | Active |
++----------------------+----------------------+-------------+
+* - Indicates the current environment
```
-```sh
-composer require magento/module-customer-sample-data-venia:dev-master
-```
+Use the Magento Cloud CLI to checkout the environment where you want to add your storefront code, such as the `staging` environment.
```sh
-composer require magento/module-tax-sample-data-venia:dev-master
+magento-cloud checkout staging
```
-```sh
-composer require magento/module-sales-sample-data-venia:dev-master
-```
+## Add required Magento extensions
+Magento Cloud does not support node processes, so you cannot use UPWARD-JS to serve your storefront project.
+You must use the [magento2-upward-connector][] module with [UPWARD-PHP][] to deploy your storefront in Magento Cloud.
-These commands modify the `composer.json` file and adds the sample data modules to the `require` section:
+Use the `composer` CLI command to add the magento2-upward-connector module to the Magento installation:
-```json
-"magento/module-catalog-sample-data-venia": "dev-master",
-"magento/module-configurable-sample-data-venia": "dev-master",
-"magento/module-customer-sample-data-venia": "dev-master",
-"magento/module-tax-sample-data-venia": "dev-master",
-"magento/module-sales-sample-data-venia": "dev-master",
-"magento/sample-data-media-venia": "dev-master",
+```sh
+composer require magento/module-upward-connector
```
-## Install dependencies
+This command modifies the `composer.json` file and adds the package entry to the `require` section of the `composer.json` file.
-### Composer dependencies
+```text
+"magento/module-upward-connector": "^1.1.2"
+```
-_Optional_: If you manually modified the `composer.json` file to make the changes described in the previous steps, update your `composer.lock` with these new dependencies:
+{: .bs-callout .bs-callout-info}
+UPWARD-PHP is a dependency of the magento2-upward-connector, so
+you do not need to add it manually to your project.
-```sh
-composer update
-```
+## Set environment variables
-## Specify Cloud server environment
+PWA Studio storefronts require you to set the following [environment variables][] in your project for the build and runtime processes:
-{: .bs-callout .bs-callout-info}
-If you build your project locally and check the build artifacts into your Magento Cloud project, skip the **Install NPX and Yarn** and **Update build hooks** sections.
+| Name | Buildtime | Runtime | Description |
+| ------------------------------------ | --------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------- |
+| `CONFIG__DEFAULT__WEB__UPWARD__PATH` | | check_box | Absolute path to UPWARD YAML configuration |
+| `NODE_ENV` | check_box | check_box | Specifies the node environment type |
+| `MAGENTO_BACKEND_URL` | check_box | check_box | URL of your Magento backend |
+| `CHECKOUT_BRAINTREE_TOKEN` | check_box | | Braintree token associated with your Magento backend |
+| `MAGENTO_BACKEND_EDITION` | check_box | | Must be `EE` since Cloud only supports Magento Enterprise Edition |
+| `IMAGE_OPTIMIZING_ORIGIN` | check_box | | Origin to use for images in the UI |
-### Install NPX and Yarn
+### Set runtime variables
-If your project supports Yarn, which is the case for `venia-concept`, add the following entry to the `.magento.app.yaml` file.
+To set your Cloud project's runtime variables, edit the [`.magento.app.yaml`][] file and add entries to the `variables.env` section.
-```yaml
-dependencies:
- nodejs:
- yarn: "*"
+```text
+variables:
+ env:
+ CONFIG__DEFAULT__WEB__UPWARD__PATH: "/app/pmu35riuj7btw_stg/pwa/upward.yml"
+ NODE_ENV: "production"
+ MAGENTO_BACKEND_URL: "https://[your-cloud-url-here]/"
```
-### Update build hooks
+### Set buildtime variables
-The `venia-concept` storefront uses a newer version of Node than the default environment provides.
-It also uses Yarn to install its dependencies.
+To set your environment variables for buildtime, navigate or open a new terminal to _your storefront project_ and edit the `.env` file.
+Your `.env` file should have entries that look like the following:
-The following update to the `hooks.build` section of `.magento.app.yaml` installs NVM and the latest LTS release of Node.
+```text
+NODE_ENV=production
+MAGENTO_BACKEND_URL=https://[your-cloud-url-here]/
+CHECKOUT_BRAINTREE_TOKEN=
+MAGENTO_BACKEND_EDITION=EE
+IMAGE_OPTIMIZING_ORIGIN=backend
+```
+
+### Finding the correct UPWARD path value
-It also uses NPX to run the `buildpack` CLI tool with the `create-project` sub-command to scaffold the application using the `venia-concept` template.
-After scaffolding completes, it installs and builds the application and moves the build artifact to a persistent area on the file system.
+The `CONFIG_DEFAULT_WEB_UPWARD_PATH` variable specifies the _absolute_ path to the UPWARD configuration file in the deployed Cloud instance.
+If this value is incorrect or not set, the Magento 2 UPWARD connector extension cannot serve your storefront application and your frontend appears broken.
-```yaml
-hooks:
- # We run build hooks before your application has been packaged.
- build: |
- set -e
+In the previous example, `/app/pmu35riuj7btw_stg/` is the Magento application root directory on the deployed instance.
+This value is different for each environment in your Cloud project, so you must configure each of your project environments with the path specific to each instance.
+To find the correct root directory path for an environment, [SSH][] into the remote server and use the `pwd` command to find the Magento application root directory.
- unset NPM_CONFIG_PREFIX
- curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.1/install.sh | bash
- export NVM_DIR="$HOME/.nvm"
- [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
- nvm install --lts=dubnium
+## Build your storefront application
- npx @magento/pwa-buildpack create-project /tmp/pwa --template venia-concept --backend-url https://[your-cloud-url-here] --name Venia --author Magento --install false --npm-client yarn
- cd /tmp/pwa
- # Override this environment variable at runtime given all scaffolded dependencies are devDependencies
- NODE_ENV=development yarn install --ignore-optional
- yarn build
- cp -p -r dist/ "$HOME/pwa"
- cd $HOME
+In _your storefront project_ directory, use `yarn` or `npm` to install project dependencies and run the project's build command.
- php ./vendor/bin/ece-tools build:generate
- php ./vendor/bin/ece-tools build:transfer
+```sh
+yarn install &&
+yarn build
```
-### Add required environment variables
+These commands install project dependencies and runs the build process, which creates a `dist` folder that contains code bundles for your storefront application.
+It also copies over the static assets your application needs from your project into this folder.
-Use the Magento Cloud GUI or modify the `variables.env` entry in `.magento.app.yaml` to add any required environment variables.
+## Add your storefront project code
-The following table lists the required environment variables for the Venia storefront:
+*In your Cloud project*, create a `pwa` folder and copy into it the content inside your storefront project's `dist` folder.
-| Name | Value |
-| ------------------------------------ | ------------------------------------------------------------------------------------------------------- |
-| `CONFIG__DEFAULT__WEB__UPWARD__PATH` | `/app/pwa/upward.yml` (absolute path to UPWARD YAML configuration) |
-| `NODE_ENV` | `production` |
-| `MAGENTO_BACKEND_URL` | `https://[your-cloud-url-here]` |
-| `CHECKOUT_BRAINTREE_TOKEN` | `` |
-| `MAGENTO_BACKEND_EDITION` | `EE` (enable Magento Commerce features) |
-| `IMAGE_OPTIMIZING_ORIGIN` | `backend` (use Fastly for image optimization) |
+```sh
+mkdir pwa && cp -r /dist/* pwa
+```
+
+If you are updating your existing storefront code, you must delete the content inside the `pwa` directory before you copy the new `dist` content to avoid keeping the old bundles the application no longer uses.
-## Commit modified files
+## Deploy changes
-Commit all changes to the following files:
+At this point in the tutorial, your Cloud project should have changes in the following files and directories:
- `.magento.app.yaml`
- `composer.json`
- `composer.lock`
+- `pwa`
+
+Edit your Cloud project's `.gitignore` file and add the following entries to track the `pwa` directory in git:
-## Push changes
+```text
+!/pwa
+!/pwa/**
+```
+
+Use the Git CLI tool to stage, commit, and push these changes to your Cloud project.
-Push your local changes for your deployment and wait for it to complete.
+```sh
+git add . &&
+git commit -m "Added storefront file bundles" &&
+git push origin
+```
-## Congratulations
+After you push changes to your Cloud project, the remote build process runs and deploys a live instance of your site to the Magento Commerce Cloud service.
-You have installed a PWA storefront on the Cloud!
+### Merging environments
-You should be able to navigate to the frontend URL of your Cloud instance and see your PWA storefront.
+The Cloud topic on how to [Deploy your store][] provides more details on the deployment process.
+It also includes instructions for merging environment branches, such as integration to staging or staging to production.
+
+If your workflow involves merging environment branches,
+you must rebuild your application bundle with the correct environment variables before you push your changes to the Magento Cloud service because
+variables, such as `CONFIG__DEFAULT__WEB__UPWARD__PATH` and `MAGENTO_BACKEND_URL`, can vary between these environments.
+
+[compatible]: <{%link technologies/magento-compatibility/index.md %}>
+[environment variables]: <{%link pwa-buildpack/reference/environment-variables/core-definitions/index.md %}>
+[project setup]: <{%link tutorials/pwa-studio-fundamentals/project-setup/index.md %}>
[magento pwa studio]: http://pwastudio.io
[`@magento/venia-concept`]: https://www.npmjs.com/package/@magento/venia-concept
@@ -242,3 +231,15 @@ You should be able to navigate to the frontend URL of your Cloud instance and se
[magento2-upward-connector]: https://github.com/magento/magento2-upward-connector
[upward-php]: https://github.com/magento/upward-php
+
+[magento commerce cloud]: https://devdocs.magento.com/cloud/bk-cloud.html
+[features and workflows]: https://devdocs.magento.com/cloud/architecture/cloud-architecture.html
+[starter workflow]: https://devdocs.magento.com/cloud/architecture/starter-develop-deploy-workflow.html
+[pro workflow]: https://devdocs.magento.com/cloud/architecture/pro-develop-deploy-workflow.html
+[cloud onboarding tasks]: https://devdocs.magento.com/cloud/onboarding/onboarding-tasks.html
+[magento cloud cli]: https://devdocs.magento.com/cloud/reference/cli-ref-topic.html
+[`.magento.app.yaml`]: https://devdocs.magento.com/cloud/project/magento-env-yaml.html
+[deploy your store]: https://devdocs.magento.com/cloud/live/stage-prod-live.html
+[ssh]: https://devdocs.magento.com/cloud/env/environments-ssh.html
+[composer]: https://getcomposer.org/
+[cloud technologies and requirements]: https://devdocs.magento.com/cloud/requirements/cloud-requirements.html
diff --git a/pwa-devdocs/src/tutorials/index.md b/pwa-devdocs/src/tutorials/index.md
index bde89e5c52..578d196584 100644
--- a/pwa-devdocs/src/tutorials/index.md
+++ b/pwa-devdocs/src/tutorials/index.md
@@ -1,26 +1,32 @@
---
-title: Tutorials
+title: Getting started
---
-This section contains links to tutorials that will help you become familiar with the different tools provided by PWA Studio.
+PWA Studio is a library of tools and packages designed to help you create a Magento PWA storefront.
+In this section you will learn how to set up your storefront project using the scaffolding tool and learn about the different files and folders in your project.
-## Recommended tutorials
+## Prerequisites
-### UPWARD
+- A basic understanding of working with [React][]
+- Node >= 10.14.1
+- Yarn (recommended) or NPM
-This three part tutorial provides an introduction to the concepts introduced in the UPWARD spec.
-By the end of this tutorial, you should have a simple React application running on top of an UPWARD server.
+### Node 12 deprecation warning
-1. [Creating a simple UPWARD server][] - Teaches the very basics of reading and writing an UPWARD specification file for your projects
-2. [Using the TemplateResolver][] - Introduces the concept of using templates and the TemplateResolver to keep your UPWARD specification file lean
-3. [Adding React][] - Add React and Webpack into your UPWARD project
+If you are using Node 12, you may see the following deprecation warning in the log when you run `yarn watch:venia`.
-### Magento Cloud deployment
+```sh
+(node:89176) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
+```
-[Magento Cloud deployment][] - Provides steps for deploying a PWA Studio storefront into the Magento Cloud.
-This tutorial uses the Venia example storefront to illustrate the general process.
+This is caused by a project dependency used by PWA Studio and not by PWA Studio itself.
-[creating a simple upward server]: <{%link tutorials/hello-upward/simple-server/index.md%}>
-[using the templateresolver]: <{%link tutorials/hello-upward/using-template-resolver/index.md%}>
-[adding react]: <{%link tutorials/hello-upward/adding-react/index.md%}>
-[magento cloud deployment]: <{%link tutorials/cloud-deploy/index.md %}>
+## First steps
+
+- [Setup your project][]
+- [Explore the project structure][]
+
+[setup your project]: <{%link tutorials/pwa-studio-fundamentals/project-setup/index.md %}>
+[explore the project structure]: <{%link tutorials/pwa-studio-fundamentals/project-structure/index.md %}>
+
+[react]: https://reactjs.org/
\ No newline at end of file
diff --git a/pwa-devdocs/src/tutorials/intercept-a-target/index.md b/pwa-devdocs/src/tutorials/intercept-a-target/index.md
index c0b2613932..ac8b6801cb 100644
--- a/pwa-devdocs/src/tutorials/intercept-a-target/index.md
+++ b/pwa-devdocs/src/tutorials/intercept-a-target/index.md
@@ -1,5 +1,5 @@
---
-title: Intercept a Target
+title: Target interception
---
Target interception is a feature provided by PWA Studio's extensibility framework.
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/index.md b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/index.md
index 58feb5c73a..7f9ae22109 100644
--- a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/index.md
+++ b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/index.md
@@ -33,10 +33,6 @@ This is caused by a project dependency used by PWA Studio and not by PWA Studio
- [Project Structure][] - Learn about the project structure of your new project
- [Add a static route][] - Add a static route to your PWA Studio app
- [Modify the site footer][] - Learn how to modify existing components by modifying the site footer
-- [Using component props][] - Learn how to use props in your custom components
-- [CSS modules][] - Learn how to use CSS modules in you custom components
-- [Manage state][] - Learn how to manage internal and global state in your components
-- [Work with GraphQL][] - Learn how to work with GraphQL from Magento
- [Production launch checklist][] - Make sure your storefront is production ready with this checklist
## Related content
@@ -51,10 +47,6 @@ This is caused by a project dependency used by PWA Studio and not by PWA Studio
[project structure]: {%link tutorials/pwa-studio-fundamentals/project-structure/index.md %}
[add a static route]: {%link tutorials/pwa-studio-fundamentals/add-a-static-route/index.md %}
[modify the site footer]: {%link tutorials/pwa-studio-fundamentals/modify-site-footer/index.md %}
-[using component props]: {%link tutorials/pwa-studio-fundamentals/using-component-props/index.md %}
-[css modules]: {%link tutorials/pwa-studio-fundamentals/css-modules/index.md %}
-[manage state]: {%link tutorials/pwa-studio-fundamentals/manage-state/index.md %}
-[work with graphql]: {%link tutorials/pwa-studio-fundamentals/work-with-graphql/index.md %}
[production launch checklist]: {%link tutorials/pwa-studio-fundamentals/production-launch-checklist/index.md %}
[react]: https://reactjs.org/
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/production-launch-checklist/index.md b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/production-launch-checklist/index.md
index 6105432ed7..91540b29e9 100644
--- a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/production-launch-checklist/index.md
+++ b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/production-launch-checklist/index.md
@@ -46,6 +46,10 @@ yarn start
This command starts a production UPWARD-JS server that runs your production build.
+## Remove development packages
+
+Edit your project's `package.json` file and remove any packages you only use in your development environment. For example, the `venia-sample-backends` is an extension included in projects built using the scaffolding tool.
+
## Audit with Lighthouse
[Lighthouse][] is a web developer tool that audits your website.
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-setup/index.md b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-setup/index.md
index 265b9f3490..77685c18af 100644
--- a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-setup/index.md
+++ b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-setup/index.md
@@ -137,6 +137,10 @@ Apply these fixes to address common issues you may encounter during project setu
If you encounter any other issues, ask the Magento community in the [#PWA][] Slack channel.
+## Next
+
+[Explore the project structure][]
+
[scaffolding]: {%link pwa-buildpack/scaffolding/index.md %}
[peregrine]: {%link peregrine/index.md %}
[venia concept storefront]: {%link venia-pwa-concept/index.md %}
@@ -149,6 +153,7 @@ If you encounter any other issues, ask the Magento community in the [#PWA][] Sla
[compiled successfully screen-shot]: {%link tutorials/pwa-studio-fundamentals/project-setup/images/compiled-successfully.png %}
[clear storage]: {%link tutorials/pwa-studio-fundamentals/project-setup/images/clear-storage.png %}
[privacy error]: {%link tutorials/pwa-studio-fundamentals/project-setup/images/privacy-error.png %}
+[explore the project structure]: <{%link tutorials/pwa-studio-fundamentals/project-structure/index.md %}>
[venia.magento.com]: http://venia.magento.com/
[n]: https://github.com/tj/n
diff --git a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-structure/index.md b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-structure/index.md
index 67b3201cc2..a7b98cc274 100644
--- a/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-structure/index.md
+++ b/pwa-devdocs/src/tutorials/pwa-studio-fundamentals/project-structure/index.md
@@ -99,7 +99,7 @@ It uses [`configureWebpack`][] from PWA Studio's buildpack to create a configura
#### `upward.yml`
The `upward.yml` file is an [UPWARD][] configuration file.
-It provides provides instructions for how an UPWARD server implementation should respond to a request.
+It provides instructions for how an UPWARD server implementation should respond to a request.
The `status`, `headers`, and `body` values defined in the default `upward.yml` file uses values from the `veniaResponse` object.
This object is defined in the [venia-ui package's `upward.yml` file][].
@@ -129,6 +129,20 @@ Use the following values for `NODE_ENV`:
For more information on loading environment variables, see: [Load environment file][]
+## Next steps
+
+Now that you have a storefront project setup and understand its structure, lets learn about some basic customizations you can make to your project:
+
+- [Enable SASS or LESS support][]
+- [Add a static route][]
+- [Modify the site footer][]
+- [Create a TagList component][]
+
+
+[enable sass or less support]: <{%link tutorials/enable-sass-less-support/index.md %}>
+[add a static route]: <{%link tutorials/pwa-studio-fundamentals/add-a-static-route/index.md %}>
+[modify the site footer]: <{%link tutorials/pwa-studio-fundamentals/modify-site-footer/index.md %}>
+[create a taglist component]: <{%link tutorials/create-taglist-component/index.md %}>
[modular components]: <{%link venia-pwa-concept/features/modular-components/index.md %}>
[project setup]: <{%link tutorials/pwa-studio-fundamentals/index.md %}>
[`configurewebpack`]: <{%link pwa-buildpack/reference/configure-webpack/index.md %}>
diff --git a/scripts/monorepo-introduction.js b/scripts/monorepo-introduction.js
index 830b7c9053..7138a6fbf4 100644
--- a/scripts/monorepo-introduction.js
+++ b/scripts/monorepo-introduction.js
@@ -82,9 +82,10 @@ async function prepare() {
return () => {};
}
});
- const customOrigin = loadEnvironment(veniaPath, nullLogger).section(
- 'customOrigin'
- );
+ const customOrigin = await loadEnvironment(
+ veniaPath,
+ nullLogger
+ ).section('customOrigin');
if (customOrigin.enabled) {
const customOriginConfig = await configureHost(
Object.assign(customOrigin, {
diff --git a/yarn.lock b/yarn.lock
index b79daa1515..6e58fb9455 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,13 @@
# yarn lockfile v1
+"@adobe/adobe-client-data-layer@~1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@adobe/adobe-client-data-layer/-/adobe-client-data-layer-1.1.3.tgz#1b3746407d789a24858348c96ca32493933928fc"
+ integrity sha512-pEYEHprHaaIJROe6QRuTme7sUN46bn2/jpGUBeqfd8cTL+sNsOUTcg3TN7bCAnwunS4Y7NAMs6cXHEeksAgNPA==
+ dependencies:
+ lodash "^4.17.15"
+
"@adobe/apollo-link-mutation-queue@~1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@adobe/apollo-link-mutation-queue/-/apollo-link-mutation-queue-1.0.2.tgz#0c589ffb970b9917ba52c38812740c613c0a40da"
@@ -7500,6 +7507,11 @@ fast-redact@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-2.0.0.tgz#17bb8f5e1f56ecf4a38c8455985e5eab4c478431"
integrity sha512-zxpkULI9W9MNTK2sJ3BpPQrTEXFNESd2X6O1tXMFpK/XM0G5c5Rll2EVYZH2TqI3xRGK/VaJ+eEOt7pnENJpeA==
+fast-redact@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz#ac2f9e36c9f4976f5db9fb18c6ffbaf308cf316d"
+ integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w==
+
fast-safe-stringify@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
@@ -8675,19 +8687,19 @@ hast-util-parse-selector@^2.0.0:
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.3.tgz#57edd449103900c7f63fd9e6f694ffd7e4634719"
integrity sha512-nxbeqjQNxsvo/uYYAw9kij6td05YVUlf1qti09rVfbWSLT5H6wo3c+USIwX6nzXWk5kFZzXnEqO82856r0aM2Q==
-hastily@~0.4.7:
- version "0.4.7"
- resolved "https://registry.yarnpkg.com/hastily/-/hastily-0.4.7.tgz#3bb105586019708cd6c464d7c0fd710aae6b9bc3"
- integrity sha512-sHih6UGApmVJ6jGdqDLjfmwkVo3bkw6/NK/3tyM/OK8rRm+mucwv+Q8Fdl2bWQT5gowoVDh0FM3wETWaTbfIFQ==
+hastily@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/hastily/-/hastily-0.5.0.tgz#629583855ab9b082682249d978d278258b80dbad"
+ integrity sha512-y2zBaI3S0f2bfIPE9EO6f/BXe/y3poD11zYoHOMuPR9cswv1DKpSHGHiFnNb1wJMbd/hCEPv8Qn04Zjmh92GFw==
dependencies:
accepts "^1.3.7"
debug "^4.1.1"
mime "^2.4.6"
mime-types "^2.1.27"
on-headers "^1.0.2"
- pino "^6.3.2"
+ pino "^6.5.1"
pino-http "^5.2.0"
- pino-pretty "^4.0.0"
+ pino-pretty "^4.1.0"
vary "^1.1.2"
hastscript@^5.0.0:
@@ -11021,6 +11033,11 @@ lodash@^4.0.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.16.4, lodash@^4.17.10,
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+lodash@~4.17.20:
+ version "4.17.20"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+ integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
log-driver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8"
@@ -12681,14 +12698,14 @@ pino-http@^5.2.0:
pino "^6.0.0"
pino-std-serializers "^2.4.0"
-pino-pretty@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-4.0.0.tgz#afbff81f946342b9d6eabc434942fe490e02faa9"
- integrity sha512-YLy/n3dMXYWOodSm530gelkSAJGmEp29L9pqiycInlIae5FEJPWAkMRO3JFMbIFtjD2Ve4SH2aBcz2aRreGpBQ==
+pino-pretty@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-4.3.0.tgz#18695606fd4f1e21cd1585d18999cd84d429e1d8"
+ integrity sha512-uEc9SUCCGVEs0goZvyznKXBHtI1PNjGgqHviJHxOCEFEWZN6Z/IQKv5pO9gSdm/b+WfX+/dfheWhtZUyScqjlQ==
dependencies:
"@hapi/bourne" "^2.0.0"
args "^5.0.1"
- chalk "^3.0.0"
+ chalk "^4.0.0"
dateformat "^3.0.3"
fast-safe-stringify "^2.0.7"
jmespath "^0.15.0"
@@ -12696,14 +12713,14 @@ pino-pretty@^4.0.0:
pump "^3.0.0"
readable-stream "^3.6.0"
split2 "^3.1.1"
- strip-json-comments "^3.0.1"
+ strip-json-comments "^3.1.1"
pino-std-serializers@^2.4.0, pino-std-serializers@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.4.2.tgz#cb5e3e58c358b26f88969d7e619ae54bdfcc1ae1"
integrity sha512-WaL504dO8eGs+vrK+j4BuQQq6GLKeCCcHaMB2ItygzVURcL1CycwNEUHTD/lHFHs/NL5qAz2UKrjYWXKSf4aMQ==
-pino@^6.0.0, pino@^6.3.2:
+pino@^6.0.0:
version "6.3.2"
resolved "https://registry.yarnpkg.com/pino/-/pino-6.3.2.tgz#55f73aa61584774ca5984068ffb78e8d519ce19e"
integrity sha512-EiP3L1hoFw19KPocWimjnfXeysld0ne89ZRQ+bf8nAeA2TyuLoggNlibAi+Kla67GvQBopLdIZOsh1z/Lruo5Q==
@@ -12715,6 +12732,18 @@ pino@^6.0.0, pino@^6.3.2:
quick-format-unescaped "^4.0.1"
sonic-boom "^1.0.0"
+pino@^6.5.1:
+ version "6.7.0"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-6.7.0.tgz#d5d96b7004fed78816b5694fda3eab02b5ca6d23"
+ integrity sha512-vPXJ4P9rWCwzlTJt+f0Ni4THc3DWyt8iDDCO4edQ8narTu6hnpzdXu8FqeSJCGndl1W6lfbYQUQihUO54y66Lw==
+ dependencies:
+ fast-redact "^3.0.0"
+ fast-safe-stringify "^2.0.7"
+ flatstr "^1.0.12"
+ pino-std-serializers "^2.4.2"
+ quick-format-unescaped "^4.0.1"
+ sonic-boom "^1.0.2"
+
pinpoint@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pinpoint/-/pinpoint-1.1.0.tgz#0cf7757a6977f1bf7f6a32207b709e377388e874"
@@ -14987,6 +15016,14 @@ sonic-boom@^1.0.0:
atomic-sleep "^1.0.0"
flatstr "^1.0.12"
+sonic-boom@^1.0.2:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.3.0.tgz#5c77c846ce6c395dddf2eb8e8e65f9cc576f2e76"
+ integrity sha512-4nX6OYvOYr6R76xfQKi6cZpTO3YSWe/vd+QdIfoH0lBy0MnPkeAbb2rRWgmgADkXUeCKPwO1FZAKlAVWAadELw==
+ dependencies:
+ atomic-sleep "^1.0.0"
+ flatstr "^1.0.12"
+
sort-keys@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
@@ -15452,10 +15489,10 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
-strip-json-comments@^3.0.1:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180"
- integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-loader@^0.23.1, style-loader@~0.23.1:
version "0.23.1"