Skip to content

Commit 1c9b557

Browse files
feat: order list placed by filter-SFS-2702 (#2988)
## What’s the purpose of this pull request? - Add a new Placed by filter to My Account → Orders, enabling users to search and select a shopper before applying filters. - Implement the feature with mock data, preparing for a future API integration. ## How it works? - New facet in the Filters panel (between Status and Order date): - Autocomplete input using mock shoppers. - Type to filter by name/email (case-insensitive). - Selecting a shopper makes the input read-only and shows a clear X inside the field. - Clear X removes the selection and restores search mode. - If the user types but doesn’t select and blurs, the input auto-clears. - Selected shopper appears as a tag inside the filter panel; Clear All removes it. - Clicking View Results rebuilds the URL and reloads the page with: - purchaseAgentId=<id> when selected ## How to test it? <!--- Describe the steps with bullet points. Is there any external link that can be used to better test it or an example? ---> ### Starters Deploy Preview <!--- Add a link to a deploy preview from `starter.store` with this branch being used. ---> <!--- Tip: You can get an installable version of this branch from the CodeSandbox generated when this PR is created. ---> ## References [SFS-2702](https://vtex-dev.atlassian.net/browse/SFS-2702?atlOrigin=eyJpIjoiZmNjZmUwNWQxMjdjNDlhNDk0YTU5OTY4Y2NkMDBmYzciLCJwIjoiaiJ9) [Figma](https://www.figma.com/design/JppevzwPSV58ud0JDCFE7o/My-Account-%E2%80%93-ODP?node-id=2975-182090&p=f&t=TUnyb1nRi7t0WYVV-0) | Desktop | Mobile | |--------|--------| | <img width="1300" height="605" alt="image" src="https://github.com/user-attachments/assets/eda90b7a-4a35-4411-8e5f-26088893c404" /> | | | <img width="1300" height="605" alt="image" src="https://github.com/user-attachments/assets/bd9501e9-f69a-409c-8873-e841e3d1c6fb" /> | | | <img width="1300" height="605" alt="image" src="https://github.com/user-attachments/assets/04dc6fdc-312a-436a-9906-4836b95e2024" /> | <img width="437" height="902" alt="image" src="https://github.com/user-attachments/assets/67f0e795-0fd1-42c5-90d7-aaad7bb56d35" /> | | <img width="1300" height="605" alt="image" src="https://github.com/user-attachments/assets/b3170f57-71e2-4cac-927d-a68cf85c63cc" /> | <img width="437" height="902" alt="image" src="https://github.com/user-attachments/assets/48a7aac7-4fb7-4adb-b9b6-3a775a3dd793" /> | [SFS-2702]: https://vtex-dev.atlassian.net/browse/SFS-2702?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 7214b54 commit 1c9b557

File tree

10 files changed

+480
-3
lines changed

10 files changed

+480
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { useEffect, useMemo, useRef, useState } from 'react'
2+
import { Input, IconButton, Icon, Loader } from '@faststore/ui'
3+
import type { SelectedFacet } from 'src/sdk/search/useMyAccountFilter'
4+
import useShopperSuggestions from 'src/sdk/account/useShopperSuggestions'
5+
import type { Shopper } from 'src/sdk/account/useShopperSuggestions'
6+
7+
export interface MyAccountFilterFacetPlacedByProps {
8+
/**
9+
* Current selected facets from filter context
10+
*/
11+
selected: SelectedFacet[]
12+
/**
13+
* Dispatch from filter context
14+
*/
15+
dispatch: (action: { type: 'toggleFacet' | 'setFacet'; payload: any }) => void
16+
}
17+
18+
function MyAccountFilterFacetPlacedBy({
19+
selected,
20+
dispatch,
21+
}: MyAccountFilterFacetPlacedByProps) {
22+
const inputRef = useRef<HTMLInputElement>(null)
23+
const [query, setQuery] = useState('')
24+
const [selectedShopper, setSelectedShopper] = useState<Shopper | null>(null)
25+
const [isOpen, setIsOpen] = useState(false)
26+
27+
// Use the new hook for shoppers suggestions
28+
const { data, isLoading, findShopperById } = useShopperSuggestions(query)
29+
30+
// Get the filtered shoppers from hook data
31+
const filteredShoppers = data?.shoppers || []
32+
33+
const selectedId = useMemo(
34+
() => selected.find((f) => f.key === 'purchaseAgentId')?.value,
35+
[selected]
36+
)
37+
38+
const clearAll = () => {
39+
setQuery('')
40+
setSelectedShopper(null)
41+
if (inputRef.current) inputRef.current.value = ''
42+
}
43+
44+
useEffect(() => {
45+
if (selectedId && !selectedShopper) {
46+
const found = findShopperById(selectedId)
47+
if (found) setSelectedShopper(found)
48+
} else if (!selectedId) {
49+
clearAll()
50+
}
51+
}, [selectedId, selectedShopper])
52+
53+
function handleSearchOnChange(value: string) {
54+
setQuery(value)
55+
setIsOpen(true)
56+
}
57+
58+
const isSearchEmpty = useMemo(
59+
() => !isLoading && query && filteredShoppers.length === 0,
60+
[isLoading, query, filteredShoppers]
61+
)
62+
63+
function handleSelect(shopper: Shopper) {
64+
setSelectedShopper(shopper)
65+
setIsOpen(false)
66+
dispatch({
67+
type: 'setFacet',
68+
payload: {
69+
facet: { key: 'purchaseAgentId', value: shopper.purchase_agent_id },
70+
unique: true,
71+
},
72+
})
73+
}
74+
75+
function handleClearTag() {
76+
if (selectedShopper) {
77+
// Using toggleFacet here removes the purchaseAgentId from selected facets
78+
// because toggleFacet will remove the facet if it already exists in the selected facets
79+
dispatch({
80+
type: 'toggleFacet',
81+
payload: {
82+
key: 'purchaseAgentId',
83+
value: selectedShopper.purchase_agent_id,
84+
},
85+
})
86+
}
87+
clearAll()
88+
}
89+
90+
return (
91+
<div data-fs-list-orders-filters-placed-by>
92+
<div data-fs-list-orders-filters-placed-by-input>
93+
<Input
94+
id="placed-by-input"
95+
placeholder="Enter the shopper's name..."
96+
ref={inputRef}
97+
value={selectedShopper ? selectedShopper.name : query}
98+
readOnly={Boolean(selectedShopper)}
99+
onFocus={() => {
100+
if (!selectedShopper) setIsOpen(true)
101+
}}
102+
onChange={(e) => {
103+
if (selectedShopper) return
104+
handleSearchOnChange(e.target.value)
105+
}}
106+
onBlur={() => {
107+
// delay close to allow click selection
108+
setTimeout(() => {
109+
setIsOpen(false)
110+
if (!selectedShopper) {
111+
setQuery('')
112+
if (inputRef.current) inputRef.current.value = ''
113+
}
114+
}, 100)
115+
}}
116+
type="text"
117+
inputMode="text"
118+
/>
119+
{isLoading && (
120+
<div data-fs-list-orders-filters-placed-by-loader>
121+
<Loader />
122+
</div>
123+
)}
124+
{selectedShopper && (
125+
<IconButton
126+
size="small"
127+
aria-label="Clear shopper"
128+
data-fs-list-orders-filters-placed-by-clear
129+
icon={<Icon name="X" />}
130+
onClick={handleClearTag}
131+
/>
132+
)}
133+
</div>
134+
135+
{isOpen && isSearchEmpty && (
136+
<div
137+
data-fs-list-orders-filters-placed-by-dropdown
138+
data-fs-list-orders-filters-placed-by-empty
139+
aria-label="No shoppers found with query"
140+
>
141+
<p>No shoppers found with "{query}"</p>
142+
</div>
143+
)}
144+
145+
{isOpen && filteredShoppers.length > 0 && (
146+
<div
147+
data-fs-list-orders-filters-placed-by-dropdown
148+
aria-label="Shopper selection dropdown"
149+
>
150+
<ul>
151+
{filteredShoppers.map((s) => (
152+
<li key={s.purchase_agent_id}>
153+
<button
154+
type="button"
155+
onMouseDown={(e) => e.preventDefault()}
156+
onClick={() => handleSelect(s)}
157+
data-fs-list-orders-filters-placed-by-option
158+
>
159+
<span data-fs-list-orders-filters-placed-by-option-name>
160+
{s.name}
161+
</span>
162+
</button>
163+
</li>
164+
))}
165+
</ul>
166+
</div>
167+
)}
168+
</div>
169+
)
170+
}
171+
172+
export default MyAccountFilterFacetPlacedBy
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './MyAccountFilterFacetPlacedBy'
2+
export type { MyAccountFilterFacetPlacedByProps } from './MyAccountFilterFacetPlacedBy'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
[data-fs-list-orders-filters-placed-by] {
2+
--fs-list-orders-filters-placed-by-dropdown-bkg : var(--fs-color-neutral-0);
3+
--fs-list-orders-filters-placed-by-dropdown-border : 1px solid var(--fs-border-color-light);
4+
--fs-list-orders-filters-placed-by-dropdown-radius : var(--fs-border-radius);
5+
--fs-list-orders-filters-placed-by-option-padding : var(--fs-spacing-2) var(--fs-spacing-3);
6+
--fs-list-orders-filters-placed-by-selected-height : var(--fs-control-height);
7+
--fs-list-orders-filters-placed-by-selected-padding : 0 var(--fs-spacing-3);
8+
--fs-list-orders-filters-placed-by-selected-border : var(--fs-input-border-width) solid var(--fs-input-border-color);
9+
--fs-list-orders-filters-placed-by-selected-radius : var(--fs-input-border-radius);
10+
--fs-list-orders-filters-placed-by-empty-text-color : var(--fs-color-text-light);
11+
--fs-list-orders-filters-placed-by-loader-size : 1.25rem;
12+
--fs-list-orders-filters-placed-by-dropdown-shadow : 0 2px 8px rgb(0 0 0 / 10%);
13+
14+
position: relative;
15+
16+
// Clear icon inside input when actionable
17+
[data-fs-list-orders-filters-placed-by-input] {
18+
position: relative;
19+
20+
[data-fs-input] {
21+
width: 100%;
22+
}
23+
24+
[data-fs-icon-button] {
25+
position: absolute;
26+
top: 50%;
27+
right: var(--fs-spacing-2);
28+
transform: translateY(-50%);
29+
}
30+
31+
[data-fs-list-orders-filters-placed-by-loader] {
32+
position: absolute;
33+
top: 50%;
34+
right: var(--fs-spacing-2);
35+
display: flex;
36+
align-items: center;
37+
justify-content: center;
38+
transform: translateY(-50%);
39+
40+
[data-fs-loader] {
41+
width: var(--fs-list-orders-filters-placed-by-loader-size);
42+
height: var(--fs-list-orders-filters-placed-by-loader-size);
43+
}
44+
}
45+
}
46+
47+
[data-fs-list-orders-filters-placed-by-dropdown] {
48+
position: absolute;
49+
z-index: 10;
50+
width: 100%;
51+
margin-top: var(--fs-spacing-1);
52+
background: var(--fs-list-orders-filters-placed-by-dropdown-bkg);
53+
border: var(--fs-list-orders-filters-placed-by-dropdown-border);
54+
border-radius: var(--fs-list-orders-filters-placed-by-dropdown-radius);
55+
box-shadow: var(--fs-list-orders-filters-placed-by-dropdown-shadow);
56+
57+
&[data-fs-list-orders-filters-placed-by-empty] {
58+
padding: var(--fs-spacing-3);
59+
60+
p {
61+
margin: 0;
62+
font-size: var(--fs-text-size-body);
63+
font-style: italic;
64+
color: var(--fs-list-orders-filters-placed-by-empty-text-color);
65+
text-align: center;
66+
}
67+
}
68+
69+
ul {
70+
max-height: 220px;
71+
padding: 0;
72+
margin: 0;
73+
overflow: auto;
74+
list-style: none;
75+
}
76+
77+
[data-fs-list-orders-filters-placed-by-option] {
78+
display: flex;
79+
justify-content: flex-start;
80+
width: 100%;
81+
padding: var(--fs-list-orders-filters-placed-by-option-padding);
82+
text-align: left;
83+
cursor: pointer;
84+
background: transparent;
85+
border: 0;
86+
87+
&:hover, &:focus {
88+
background: var(--fs-color-neutral-bkg);
89+
}
90+
91+
[data-fs-list-orders-filters-placed-by-option-name] {
92+
font-weight: var(--fs-text-weight-regular);
93+
}
94+
}
95+
}
96+
}

packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterSlider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
useMyAccountFilter,
1414
} from 'src/sdk/search/useMyAccountFilter'
1515
import FilterFacetDateRange from './MyAccountFilterFacetDateRange'
16+
import FilterFacetPlacedBy from './MyAccountFilterFacetPlacedBy'
1617
import styles from './section.module.scss'
1718

1819
export interface FilterSliderProps {
@@ -91,6 +92,10 @@ function MyAccountFilterSlider({
9192
: [value]
9293
}
9394

95+
if (key === 'purchaseAgentId') {
96+
acc['purchaseAgentId'] = value
97+
}
98+
9499
return acc
95100
},
96101
{} as Record<string, string | string[]>
@@ -197,6 +202,9 @@ function MyAccountFilterSlider({
197202
))}
198203
</UIFilterFacetBoolean>
199204
)}
205+
{type === 'StoreFacetPlacedBy' && isExpanded && (
206+
<FilterFacetPlacedBy selected={selected} dispatch={dispatch} />
207+
)}
200208
{type === 'StoreFacetRange' && isExpanded && (
201209
<FilterFacetDateRange
202210
ref={dateRangeInputRef}

packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/section.module.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.section {
22
@import "@faststore/ui/src/components/atoms/Button/styles.scss";
3+
@import "@faststore/ui/src/components/atoms/Loader/styles.scss";
34
@import "@faststore/ui/src/components/atoms/Badge/styles.scss";
45
@import "@faststore/ui/src/components/atoms/Checkbox/styles.scss";
56
@import "@faststore/ui/src/components/atoms/Icon/styles.scss";
@@ -16,6 +17,7 @@
1617
@import "@faststore/ui/src/components/organisms/FilterSlider/styles.scss";
1718
@import "@faststore/ui/src/components/organisms/SlideOver/styles.scss";
1819
@import "./MyAccountFilterFacetDateRange/styles.scss";
20+
@import "./MyAccountFilterFacetPlacedBy/styles.scss";
1921

2022
[data-fs-badge] {
2123
display: none;

packages/core/src/components/account/orders/MyAccountListOrders/MyAccountListOrders.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type MyAccountListOrdersProps = {
3939
dateFinal: string
4040
text: string
4141
clientEmail: string
42+
purchaseAgentId?: string
4243
}
4344
}
4445

@@ -73,6 +74,11 @@ function getSelectedFacets({
7374
key: 'dateFinal',
7475
value: String(value),
7576
})
77+
} else if (filter === 'purchaseAgentId' && value) {
78+
acc.push({
79+
key: 'purchaseAgentId',
80+
value: String(value),
81+
})
7682
}
7783

7884
return acc
@@ -96,6 +102,11 @@ function getAllFacets({
96102
value: status.toLowerCase(),
97103
})),
98104
},
105+
{
106+
__typename: 'StoreFacetPlacedBy',
107+
key: 'purchaseAgentId',
108+
label: 'Placed by',
109+
} as any,
99110
{
100111
__typename: 'StoreFacetRange',
101112
key: 'dateRange',
@@ -233,6 +244,7 @@ export default function MyAccountListOrders({
233244
status: filters.status,
234245
dateInitial: filters.dateInitial,
235246
dateFinal: filters.dateFinal,
247+
purchaseAgentId: filters.purchaseAgentId,
236248
}}
237249
onClearAll={() => {
238250
window.location.href = '/account/orders'
@@ -247,6 +259,8 @@ export default function MyAccountListOrders({
247259
} else if (key === 'dateInitial' || key === 'dateFinal') {
248260
delete updatedFilters.dateInitial
249261
delete updatedFilters.dateFinal
262+
} else if (key === 'purchaseAgentId') {
263+
delete updatedFilters.purchaseAgentId
250264
} else {
251265
delete updatedFilters[key]
252266
}
@@ -258,6 +272,8 @@ export default function MyAccountListOrders({
258272
} else if (key === 'dateInitial' || key === 'dateFinal') {
259273
delete updatedFilters.dateInitial
260274
delete updatedFilters.dateFinal
275+
} else if (key === 'purchaseAgentId') {
276+
delete updatedFilters.purchaseAgentId
261277
} else {
262278
delete updatedFilters[key]
263279
}

0 commit comments

Comments
 (0)