Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Cart v2] ProductListing Component #2094

Merged
merged 18 commits into from
Jan 21, 2020
Merged

Conversation

tjwiebell
Copy link
Contributor

@tjwiebell tjwiebell commented Jan 10, 2020

Description

Within the Cart Page component, if a product has been added to cart it will display with necessary information and actions for a shopper to view/edit/delete.

Requirements:

Adhere to design specs/flows provided
Empty cart should display appropriate messaging
"Sign in" button opens sign in modal that returns back to cart once user is auth'ed
User can update quantity using increment/decrement functionality
Kebab menu should reuse options from existing Venia mini-cart
Open Questions:

What product attributes should be listed in the view? For now, it's just size/color in the mockup. As discussed in grooming, all product options should be displayed for now.

Related Issue

Acceptance

Verification Stakeholders

Specification

Verification Steps

Data fetch policy I used for ease of development has been removed so its not accidentally left in once all cart features are completed. You'll need to clear your Apollo cache if you make any cart mutations outside of this view (ie. Add to Cart).

  1. Navigate to /cart route, verify empty cart message is shown
  2. Add Products to your cart, both simple and configurable
  3. Go back to /cart route, verify details are displayed and aligned with mocks
  4. Switch to mobile view and verify layout collapses and adheres to mocks

Screenshots / Screen Captures (if appropriate)

Checklist

  • I have updated the documentation accordingly, if necessary.
  • I have added tests to cover my changes, if necessary.

@tjwiebell tjwiebell added in progress version: Minor This changeset includes functionality added in a backwards compatible manner. labels Jan 10, 2020
@PWAStudioBot
Copy link
Contributor

PWAStudioBot commented Jan 10, 2020

Messages
📖

Access a deployed version of this PR here. Make sure to wait for the "pwa-pull-request-deploy" job to complete.

📖 DangerCI Failures related to missing labels/description/linked issues/etc will persist until the next push or next nightly build run (assuming they are fixed).
📖

Associated JIRA tickets: PWA-238.

Generated by 🚫 dangerJS against 11576c7

const handleRemoveFromCart = useCallback(() => {
setIsRemoving(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to set this to false in a catch incase removeItem fails for whatever reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0ac27f8. I need to do some validation, but I'm banking on Apollo running the refetch on error, which would make this unnecessary; but if not, would put this in an odd state. Will validate that assumption, and manually trigger a refetch onError if needed.

variables: {
cartId,
itemId: item.id
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the order of the merges someone should add refetchQueries here and use this exported query so the order summary gets updated.

OR you could use manually update. Both should work, but the refetchQueries is probably easiest.

import PriceSummary from './priceSummary;

removeItem({
  variables, 
  refetchQueries: [{
      query: PriceSummary.queries.GET_PRICE_SUMMARY,
      variables: { cartId },
   }],
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 0ac27f8 (and I'll fix again once your PR is merged 😉 )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I believe price summary was merged :D

@@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { useCartPage } from '@magento/peregrine/lib/talons/CartPage/useCartPage';

import Button from '../Button';
import ProductListing from '../ProductListing';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I colocated the PriceSummary component with cartPage. Do you think it is likely that something else will need to use the component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has a public/private API implication, but I'm okay moving if we're not considering these components public API (suppose we could always export in index to denote they are public).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's CartPage that should actually just be a RootComponent and these should all just be top-level components. Hmm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's CartPage that should actually just be a RootComponent and these should all just be top-level components. Hmm.

We're moving away from RootComponents being anything but shells bound to a route. I would prefer that CartPage and all pages that actually have DOM, style, talons, etc., be in components. See #1953.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested under CartPage in 0ac27f8

import { createTestInstance } from '@magento/peregrine';

import Product from '../product';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also mock classify so the classnames are not undefined in the snapshots.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mocked in 0ac27f8

<span className={classes.name}>{name}</span>
<ProductOptions
options={options}
classes={{ options: classes.options, optionLabel: {} }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty object is stringified to [object Object] in the html. You should just create an empty .optionLabel class in the css and pass it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this API isn't pretty, but props for using it. At some point we'll make a better solution for this use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this change in 0ac27f8, but vscode linting is complaining about the empty rule now. Is that something we enforce?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, VSC's linter is not something we care about, so an empty rule is fine. That said, you may safely delete empty rules. The component should not throw an error if you try to apply classes.foo but there is no .foo {} defined in the CSS module.

text="Move to favorites"
onClick={handleToggleFavorites}
icon="Heart"
isFilled={false}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Favorites doesn't do anything right now but I think this should use a local state variable at least.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe say "wishlist" in code (if not labels too) since that lines up to Magento's terminology.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for now Tommy is using the existing options kebab which hardcodes the label.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to stick with the copy in the mocks, as I think this functionality is intended to be different from the auth'd wishlist feature. I'll make it usable client-side for @sirugh though 🤗

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this functional client-side in 0ac27f8

// Export query to be used by other components that may need to trigger
// a full re-fetch.
export const GET_PRODUCT_LISTING = gql`
query getProductListing($cartId: String!) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have queries exported as a static member like fragments. Thoughts on doing that instead of exporting a const here?

ProductListing.queries = {
  getProductListing: gql`...`
}
``

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed IRL, this is now obsolete.

@brendanfalkowski
Copy link
Contributor

brendanfalkowski commented Jan 14, 2020

Just a note on the ProductListing naming. This seems a little confusing to me coming from a Magento background where "product grid" and "product list" are general terms applicable to a view mode of a category/search listing of products.

Since this component is mostly useful within cart (err, maybe within a wishlist too?) I would probably rename it CartList. We have all four of those to support, and each has different UI structure so just thinking ahead that this naming doesn't stretch too cleanly (for us). But maybe it could for simpler sites.

return {
handleEditItem,
handleRemoveFromCart,
handleToggleFavorites: () => {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extremely minor, but inline functions like this are recreated on every call/render, so if something does use it, it'll re-render all the time. Might as well define const noop = () => {} outside of the component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By request of @sirugh, I made this functional client-side in 0ac27f8, which resolves this render problem. Good to know though 😄

} = item;

const { price } = prices;
const { value: unitPrice, currency } = price;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good rename. 👍

@@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { useCartPage } from '@magento/peregrine/lib/talons/CartPage/useCartPage';

import Button from '../Button';
import ProductListing from '../ProductListing';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's CartPage that should actually just be a RootComponent and these should all just be top-level components. Hmm.

We're moving away from RootComponents being anything but shells bound to a route. I would prefer that CartPage and all pages that actually have DOM, style, talons, etc., be in components. See #1953.

'image name quantity kebab'
'image options quantity kebab'
'image price quantity kebab';
grid-template-columns: 100px 2fr max-content min-content;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
grid-template-columns: 100px 2fr max-content min-content;
grid-template-columns: 100px 1fr max-content min-content;

If you only have one flexible column, 1fr is enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0ac27f8

background-color: rgb(var(--venia-grey));
border: solid 1px rgb(var(--venia-border));
border-radius: 2px;
height: 100px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's for an image, I think it's fine.

<span className={classes.name}>{name}</span>
<ProductOptions
options={options}
classes={{ options: classes.options, optionLabel: {} }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this API isn't pretty, but props for using it. At some point we'll make a better solution for this use case.

@jimbo
Copy link
Contributor

jimbo commented Jan 15, 2020

Just a note on the ProductListing naming. This seems a little confusing to me coming from a Magento background where "product grid" and "product list" are general terms applicable to a view mode of a category/search listing of products.

Since this component is mostly useful within cart (err, maybe within a wishlist too?) I would probably rename it CartList. We have all four of those to support, and each has different UI structure so just thinking ahead that this naming doesn't stretch too cleanly (for us). But maybe it could for simpler sites.

Agree on all points. 👍

@tjwiebell
Copy link
Contributor Author

Just a note on the ProductListing naming. This seems a little confusing to me coming from a Magento background where "product grid" and "product list" are general terms applicable to a view mode of a category/search listing of products.

Since this component is mostly useful within cart (err, maybe within a wishlist too?) I would probably rename it CartList. We have all four of those to support, and each has different UI structure so just thinking ahead that this naming doesn't stretch too cleanly (for us). But maybe it could for simpler sites.

@brendanfalkowski - I've got two suggestions related to naming and location that conflict. If I take @sirugh's advice and nest as a sub-component under CartPage, would that remove the overlap with existing Magento jargon? eg. CartPage > ProductListing.

@brendanfalkowski
Copy link
Contributor

brendanfalkowski commented Jan 15, 2020

@tjwiebell — That's nicer from my POV. I still prefer even more explicit naming all the way down like CartPage > CartList. I've been on BEM naming for 9 years so I'm guilty as charged.

It's nice in an IDE being able to fuzzy search when the file name, component name, and outer context all have the same meta cues in naming. Said another way, I'd never search "productlist" to pick from product lists in multiple contexts when I'm really thinking "the product list in the cart" — hence "cartlist" is what I'd type and name it.

That "seems" to work better when we don't have generically named sub-components (ex: Thing/button, Thing/form, etc) vs (Thing/thingForm, Thing/thingButton, etc). Maybe the fuzzy sort puts more weight on "file name matches" than "in the path" matches. It seems to always find the exact file I meant without confusing paths that happen to match.

- Replace fragment usage in mutation to refetchQueries
- Make a sub-component of CartPage
- Fixup tests
@@ -25,10 +25,7 @@ export const usePriceSummary = props => {
const [{ cartId }] = useCartContext();

const [fetchPriceSummary, { error, loading, data }] = useLazyQuery(
props.query,
{
fetchPolicy: 'no-cache'
Copy link
Contributor

@sirugh sirugh Jan 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You removed this cause refetch refreshes this data right? Something is wrong -- I'm not seeing any items or a price summary after adding items to the cart.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Since I didn't know where to track the remaining work for making sure aggressive fetch policies are removed on completion, I just removed them entirely. You'll need to delete the apollo-cache entry in local storage and refresh, and you should see live data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is on a fresh start, cleared cache and everything.

If you start from the /cart page I believe it never is updated after future operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion we have opened a ticket to track the ideal state work.


const [{ cartId }] = useCartContext();

const { data, error, loading } = useQuery(query, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're going to want to change this to a lazy query since cartId can change. Will this work if you sign out/sign in?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I'm not sure if that's necessary as cartId should update and the useQuery hook should update appropriately...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so this will work correctly IF cartId is present, but you do need to make sure you aren't calling the query when cartId is undefined. After adding an item, go to the page and sign in/out. You should see a console error for graphql related to missing cartId.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was comfortable having error state === empty cart state, but agree its best to save the network request we know will fail. Added in f622664.

refetchQueries: [
{
query: refetchCartQuery,
variables: { cartId }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked offline about this -- this kinda sucks because this is sort of a magical coupling between query/variables. This component doesn't actually know what variables these queries expect. 🤷‍♂

Copy link
Contributor

@sirugh sirugh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few last comments.

const classes = mergeClasses(defaultClasses, props.classes);

if (isLoading) {
return <LoadingIndicator>{`Fetching Cart...`}</LoadingIndicator>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a loading indicator here? I thought we were trying to get rid of them -- I just display nothing in summary when loading.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really a case-by-case thing. You're right, we don't want loading indicators everywhere, but we also don't want to have a whole view suddenly just pop in. Ultimately, the way for us to avoid having spinners everywhere will have to be something like art direction but for loading states; until we have that, having an indicator here is fine.

sirugh
sirugh previously approved these changes Jan 16, 2020
Copy link
Contributor

@sirugh sirugh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. There's a lot of good stuff in here, specifically around the interaction between components/graphql/cache. Lets get this merged!

@soumya-ashok, @schensley please give this a once-over with Tommy, or use the link in the PR to check it out yourself.

@dpatil-magento you can QA this but wait to merge until @soumya-ashok or @schensley gives this a thumbs up.

}, []);

const handleRemoveFromCart = useCallback(async () => {
setIsRemoving(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You . may not have to use a local state. The removeItem mutation returns an object with a loading prop. I'm not sure if it waits for refetchQueries though.

- Update test snapshots with new element
@dpatil-magento
Copy link
Contributor

QA Pass, waiting for reviewer approval on latest commits.

const classes = mergeClasses(defaultClasses, props.classes);

if (isLoading) {
return <LoadingIndicator>{`Fetching Cart...`}</LoadingIndicator>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really a case-by-case thing. You're right, we don't want loading indicators everywhere, but we also don't want to have a whole view suddenly just pop in. Ultimately, the way for us to avoid having spinners everywhere will have to be something like art direction but for loading states; until we have that, having an indicator here is fine.

@schensley
Copy link

@tjwiebell the UI looks good. The 'adjust quantity control' is yet to be implemented at this time, but the Product Listing itself is approved by UX.

@dpatil-magento dpatil-magento merged commit cacde9c into develop Jan 21, 2020
@dpatil-magento dpatil-magento deleted the tommy/product-listing-v2 branch January 21, 2020 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg:peregrine pkg:venia-ui version: Minor This changeset includes functionality added in a backwards compatible manner.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants