-
Notifications
You must be signed in to change notification settings - Fork 214
@W-18669892 - Add Inventory Filter to PLP #2546
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
Changes from 10 commits
96a3994
9ff7e03
b66584f
de6d3f3
da1603a
9ca9ed7
5fe6ca1
dd6e286
ecdee88
b9d368f
d2896bf
1e727ea
a5e34fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| /* | ||
| * Copyright (c) 2025, Salesforce, Inc. | ||
| * All rights reserved. | ||
| * SPDX-License-Identifier: BSD-3-Clause | ||
| * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
|
|
||
| import React, {useEffect, useState} from 'react' | ||
| import {useIntl, FormattedMessage} from 'react-intl' | ||
| import PropTypes from 'prop-types' | ||
| import { | ||
| Heading, | ||
| Checkbox, | ||
| Stack, | ||
| Text, | ||
| useDisclosure | ||
| } from '@salesforce/retail-react-app/app/components/shared/ui' | ||
| import StoreLocatorModal from '@salesforce/retail-react-app/app/components/store-locator-modal' | ||
| import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site' | ||
| import {getSelectedStoreData} from '@salesforce/retail-react-app/app/utils/store-locator-utils' | ||
|
|
||
| const StoreInventoryFilter = ({toggleFilter, selectedFilters}) => { | ||
| const [selectedStore, setSelectedStore] = useState(null) | ||
| const {isOpen, onOpen, onClose} = useDisclosure() | ||
| const {site} = useMultiSite() | ||
| const {formatMessage} = useIntl() | ||
|
|
||
| const isChecked = selectedFilters?.ilids !== undefined | ||
|
|
||
| useEffect(() => { | ||
| const storeInfo = getSelectedStoreData(site?.id) | ||
|
|
||
| if (storeInfo?.name && storeInfo?.inventoryId) { | ||
| setSelectedStore(storeInfo) | ||
| } | ||
| }, [site?.id]) | ||
|
|
||
| const handleCheckboxChange = (e) => { | ||
| // If no store is selected or no inventoryId, open store locator | ||
| if (!selectedStore?.inventoryId) { | ||
| e.preventDefault() | ||
| onOpen() | ||
| return | ||
| } | ||
|
|
||
| // Normal checkbox behavior when store is selected | ||
| const checked = e.target.checked | ||
| toggleFilter({value: selectedStore.inventoryId}, 'ilids', !checked, false) | ||
| } | ||
|
|
||
| // Always open store locator when store name text is clicked | ||
| const handleStoreNameClick = (e) => { | ||
| e.stopPropagation() | ||
| e.preventDefault() | ||
| onOpen() | ||
| } | ||
|
|
||
| const handleStoreLocatorClose = () => { | ||
| const storeInfo = getSelectedStoreData(site?.id) | ||
|
|
||
| if (storeInfo?.name && storeInfo?.inventoryId) { | ||
| setSelectedStore(storeInfo) | ||
|
|
||
| // Apply the filter when a store is selected from the locator | ||
| toggleFilter({value: storeInfo.inventoryId}, 'ilids', false, false) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have a future WI, where we dont add the ilids refinement in the URL params?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea, added the WI here: https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00002G0eEcYAJ/view |
||
| } | ||
|
|
||
| onClose() | ||
| } | ||
|
|
||
| const storeLinkText = | ||
| selectedStore?.name || | ||
| formatMessage({ | ||
| defaultMessage: 'Select Store', | ||
| id: 'store_inventory_filter.action.select_store' | ||
| }) | ||
|
|
||
| return ( | ||
| <> | ||
| <Stack | ||
| spacing={4} | ||
| paddingTop={0} | ||
| paddingBottom={6} | ||
| borderBottom="1px solid gray.200" | ||
| data-testid="sf-store-inventory-filter" | ||
| > | ||
| <Heading as="h2" fontSize="md" fontWeight={600}> | ||
| <FormattedMessage | ||
| defaultMessage="Shop by Availability" | ||
| id="store_inventory_filter.heading.shop_by_availability" | ||
| /> | ||
| </Heading> | ||
| <Checkbox | ||
| isChecked={isChecked} | ||
| onChange={handleCheckboxChange} | ||
| aria-label={formatMessage( | ||
| { | ||
| defaultMessage: 'Filter products by store availability at {storeName}', | ||
| id: 'store_inventory_filter.checkbox.assistive_msg' | ||
| }, | ||
| { | ||
| storeName: storeLinkText | ||
| } | ||
| )} | ||
| > | ||
| <FormattedMessage | ||
| defaultMessage="In stock at {storeLink}" | ||
| id="store_inventory_filter.checkbox.label" | ||
| values={{ | ||
| storeLink: ( | ||
| <Text | ||
| as="span" | ||
| textDecoration="underline" | ||
| cursor="pointer" | ||
| onClick={handleStoreNameClick} | ||
| _hover={{color: 'blue.500'}} | ||
| aria-label={formatMessage( | ||
| { | ||
| defaultMessage: 'Open store locator to {action}', | ||
| id: 'store_inventory_filter.link.assistive_msg' | ||
| }, | ||
| { | ||
| action: selectedStore?.name | ||
| ? formatMessage({ | ||
| defaultMessage: 'change store', | ||
| id: 'store_inventory_filter.action.change_store' | ||
| }) | ||
| : formatMessage({ | ||
| defaultMessage: 'select a store', | ||
| id: 'store_inventory_filter.action.select_store_link' | ||
| }) | ||
| } | ||
| )} | ||
| > | ||
| {storeLinkText} | ||
| </Text> | ||
| ) | ||
| }} | ||
| /> | ||
| </Checkbox> | ||
| </Stack> | ||
|
|
||
| <StoreLocatorModal isOpen={isOpen} onClose={handleStoreLocatorClose} /> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| StoreInventoryFilter.propTypes = { | ||
| toggleFilter: PropTypes.func.isRequired, | ||
| selectedFilters: PropTypes.object | ||
| } | ||
|
|
||
| export default StoreInventoryFilter | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| /* | ||
| * Copyright (c) 2025, Salesforce, Inc. | ||
| * All rights reserved. | ||
| * SPDX-License-Identifier: BSD-3-Clause | ||
| * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
|
|
||
| import React from 'react' | ||
| import {screen, waitFor} from '@testing-library/react' | ||
| import userEvent from '@testing-library/user-event' | ||
| import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' | ||
| import StoreInventoryFilter from '@salesforce/retail-react-app/app/pages/product-list/partials/inventory-filter' | ||
| import {getSelectedStoreData} from '@salesforce/retail-react-app/app/utils/store-locator-utils' | ||
|
|
||
| jest.mock('@salesforce/retail-react-app/app/utils/store-locator-utils', () => ({ | ||
| getSelectedStoreData: jest.fn() | ||
| })) | ||
|
|
||
| jest.mock('@salesforce/retail-react-app/app/components/store-locator-modal', () => { | ||
| // eslint-disable-next-line react/prop-types | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was there a need to disable the linting check?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without this we get linting errors around prop validation, but we don't need to define props here since its just a unit test |
||
| function MockStoreLocatorModal({isOpen, onClose}) { | ||
| return isOpen ? ( | ||
| <div data-testid="store-locator-modal"> | ||
| <button onClick={onClose}>Close Modal</button> | ||
| </div> | ||
| ) : null | ||
| } | ||
|
|
||
| return MockStoreLocatorModal | ||
| }) | ||
|
|
||
| const mockToggleFilter = jest.fn() | ||
|
|
||
| const defaultProps = { | ||
| toggleFilter: mockToggleFilter, | ||
| selectedFilters: {} | ||
| } | ||
|
|
||
| const mockStoreData = { | ||
| id: 'store-123', | ||
| name: 'Test Store Location', | ||
| inventoryId: 'inv-456' | ||
| } | ||
|
|
||
| describe('StoreInventoryFilter', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| localStorage.clear() | ||
| getSelectedStoreData.mockReturnValue(null) | ||
| }) | ||
|
|
||
| test('renders component with default state', async () => { | ||
| renderWithProviders(<StoreInventoryFilter {...defaultProps} />) | ||
|
|
||
| expect(screen.getByTestId('sf-store-inventory-filter')).toBeInTheDocument() | ||
| expect(screen.getByText('Shop by Availability')).toBeInTheDocument() | ||
| expect(screen.getByText('Select Store')).toBeInTheDocument() | ||
| expect(screen.getByRole('checkbox')).not.toBeChecked() | ||
| }) | ||
|
|
||
| test('displays selected store name when store data exists', async () => { | ||
| getSelectedStoreData.mockReturnValue(mockStoreData) | ||
|
|
||
| renderWithProviders(<StoreInventoryFilter {...defaultProps} />) | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('Test Store Location')).toBeInTheDocument() | ||
| }) | ||
| }) | ||
|
|
||
| test('shows checkbox as checked when ilids filter is selected', () => { | ||
| const propsWithFilter = { | ||
| ...defaultProps, | ||
| selectedFilters: {ilids: 'inv-456'} | ||
| } | ||
|
|
||
| renderWithProviders(<StoreInventoryFilter {...propsWithFilter} />) | ||
|
|
||
| expect(screen.getByRole('checkbox')).toBeChecked() | ||
| }) | ||
|
|
||
| test('opens store locator modal when checkbox clicked without selected store', async () => { | ||
| const user = userEvent.setup() | ||
| renderWithProviders(<StoreInventoryFilter {...defaultProps} />) | ||
|
|
||
| const checkbox = screen.getByRole('checkbox') | ||
| await user.click(checkbox) | ||
|
|
||
| expect(screen.getByTestId('store-locator-modal')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('opens store locator modal when store name is clicked', async () => { | ||
| const user = userEvent.setup() | ||
| getSelectedStoreData.mockReturnValue(mockStoreData) | ||
|
|
||
| renderWithProviders(<StoreInventoryFilter {...defaultProps} />) | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('Test Store Location')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| await user.click(screen.getByText('Test Store Location')) | ||
|
|
||
| expect(screen.getByTestId('store-locator-modal')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('calls toggleFilter when checkbox is changed with selected store', async () => { | ||
| const user = userEvent.setup() | ||
| getSelectedStoreData.mockReturnValue(mockStoreData) | ||
|
|
||
| renderWithProviders(<StoreInventoryFilter {...defaultProps} />) | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('Test Store Location')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| const checkbox = screen.getByRole('checkbox') | ||
| await user.click(checkbox) | ||
|
|
||
| expect(mockToggleFilter).toHaveBeenCalledWith({value: 'inv-456'}, 'ilids', false, false) | ||
| }) | ||
|
|
||
| test('calls toggleFilter to remove filter when checkbox is unchecked', async () => { | ||
| const user = userEvent.setup() | ||
| getSelectedStoreData.mockReturnValue(mockStoreData) | ||
|
|
||
| const propsWithFilter = { | ||
| ...defaultProps, | ||
| selectedFilters: {ilids: 'inv-456'} | ||
| } | ||
|
|
||
| renderWithProviders(<StoreInventoryFilter {...propsWithFilter} />) | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('Test Store Location')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| const checkbox = screen.getByRole('checkbox') | ||
| expect(checkbox).toBeChecked() | ||
|
|
||
| await user.click(checkbox) | ||
|
|
||
| expect(mockToggleFilter).toHaveBeenCalledWith({value: 'inv-456'}, 'ilids', true, false) | ||
| }) | ||
|
|
||
| test('applies filter when store is selected from locator modal', async () => { | ||
| const user = userEvent.setup() | ||
| // Initially no store | ||
| getSelectedStoreData.mockReturnValue(null) | ||
|
|
||
| renderWithProviders(<StoreInventoryFilter {...defaultProps} />) | ||
|
|
||
| // Click checkbox to open modal | ||
| await user.click(screen.getByRole('checkbox')) | ||
| expect(screen.getByTestId('store-locator-modal')).toBeInTheDocument() | ||
|
|
||
| // Simulate store selection by changing the mock return value | ||
| getSelectedStoreData.mockReturnValue(mockStoreData) | ||
|
|
||
| // Close modal | ||
| await user.click(screen.getByText('Close Modal')) | ||
|
|
||
| // Should have called toggleFilter to apply the filter | ||
| expect(mockToggleFilter).toHaveBeenCalledWith({value: 'inv-456'}, 'ilids', false, 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.
I believe this is the common piece, which we will handle in refactor story