-
Notifications
You must be signed in to change notification settings - Fork 683
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
Conversation
|
- Leverage fragments to auto update the cache
const handleRemoveFromCart = useCallback(() => { | ||
setIsRemoving(true); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | ||
} |
There was a problem hiding this comment.
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 },
}],
});
There was a problem hiding this comment.
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 😉 )
There was a problem hiding this comment.
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
packages/peregrine/lib/talons/ProductListing/useProductListing.js
Outdated
Show resolved
Hide resolved
packages/peregrine/lib/talons/ProductListing/useProductListing.js
Outdated
Show resolved
Hide resolved
@@ -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'; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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-levelcomponents
. 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.
There was a problem hiding this comment.
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'; | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: {} }} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🤗
There was a problem hiding this comment.
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
packages/venia-ui/lib/components/ProductListing/productListing.js
Outdated
Show resolved
Hide resolved
// 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!) { |
There was a problem hiding this comment.
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`...`
}
``
There was a problem hiding this comment.
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.
Just a note on the Since this component is mostly useful within cart (err, maybe within a wishlist too?) I would probably rename it |
return { | ||
handleEditItem, | ||
handleRemoveFromCart, | ||
handleToggleFavorites: () => {}, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} = item; | ||
|
||
const { price } = prices; | ||
const { value: unitPrice, currency } = price; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good rename. 👍
packages/peregrine/lib/talons/ProductListing/useProductListing.js
Outdated
Show resolved
Hide resolved
@@ -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'; |
There was a problem hiding this comment.
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-levelcomponents
. 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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: {} }} |
There was a problem hiding this comment.
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.
Agree on all points. 👍 |
@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 |
@tjwiebell — That's nicer from my POV. I still prefer even more explicit naming all the way down like 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' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
packages/peregrine/lib/talons/CartPage/ProductListing/useProductListing.js
Show resolved
Hide resolved
refetchQueries: [ | ||
{ | ||
query: refetchCartQuery, | ||
variables: { cartId } |
There was a problem hiding this comment.
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. 🤷♂
There was a problem hiding this 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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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); |
There was a problem hiding this comment.
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
QA Pass, waiting for reviewer approval on latest commits. |
const classes = mergeClasses(defaultClasses, props.classes); | ||
|
||
if (isLoading) { | ||
return <LoadingIndicator>{`Fetching Cart...`}</LoadingIndicator>; |
There was a problem hiding this comment.
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.
@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. |
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).
/cart
route, verify empty cart message is shown/cart
route, verify details are displayed and aligned with mocksScreenshots / Screen Captures (if appropriate)
Checklist