@faststore/core 4.5.0-dev.0 → 4.5.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-generate.log +4 -4
- package/.turbo/turbo-test.log +132 -60
- package/@generated/cached-operations.json +2 -0
- package/@generated/gql.ts +18 -0
- package/@generated/graphql.ts +68 -0
- package/@generated/persisted-documents.json +3 -0
- package/@generated/schema.graphql +41 -0
- package/CHANGELOG.md +20 -0
- package/cms/faststore/components/cms_component__recommendationshelf.jsonc +86 -0
- package/cms/faststore/schema.json +89 -0
- package/discovery.config.default.js +3 -0
- package/discovery.config.js +10 -1
- package/discovery.config.local.example.js +62 -0
- package/package.json +6 -6
- package/public/icons.svg +4 -0
- package/src/Layout.tsx +6 -0
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +376 -0
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/index.ts +2 -0
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +51 -26
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +55 -0
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +45 -5
- package/src/components/account/Drawer/OrganizationDrawer/index.ts +5 -0
- package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +452 -54
- package/src/components/cms/global/Components.ts +10 -0
- package/src/components/sections/ProductGallery/ProductGallery.tsx +1 -1
- package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +223 -0
- package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +111 -0
- package/src/components/sections/RecommendationShelf/index.ts +1 -0
- package/src/components/sections/RecommendationShelf/section.module.scss +49 -0
- package/src/components/sections/RecommendationShelf/useRecommendationUserId.ts +49 -0
- package/src/components/sections/RecommendationShelf/useRecommendations.ts +55 -0
- package/src/components/sections/RecommendationShelf/vrn.ts +55 -0
- package/src/pages/[slug]/p.tsx +45 -0
- package/src/sdk/account/changeContractToken.ts +119 -0
- package/src/sdk/account/clearPersistedSessionState.ts +26 -0
- package/src/sdk/account/refreshToken.ts +21 -8
- package/src/sdk/account/useAvailableContracts.ts +49 -0
- package/src/sdk/account/useSwitchContract.ts +60 -0
- package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +96 -0
- package/src/sdk/analytics/utils.ts +9 -0
- package/src/utils/retry.ts +69 -0
- package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +235 -0
- package/test/components/RecommendationShelf/useRecommendationUserId.test.ts +69 -0
- package/test/components/RecommendationShelf/useRecommendations.test.ts +52 -0
- package/test/components/RecommendationShelf/vrn.test.ts +56 -0
- package/test/components/account/ContractSwitcher.browser.test.tsx +236 -0
- package/test/components/account/OrganizationDrawer.browser.test.tsx +197 -0
- package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +54 -0
- package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +55 -0
- package/test/components/account/doLogout.browser.test.ts +102 -0
- package/test/sdk/account/changeContractToken.browser.test.ts +157 -0
- package/test/sdk/account/clearPersistedSessionState.browser.test.ts +41 -0
- package/test/sdk/account/clearPersistedSessionState.test.ts +20 -0
- package/test/sdk/account/refreshToken.browser.test.ts +34 -0
- package/test/sdk/account/refreshToken.test.ts +87 -0
- package/test/sdk/account/useAvailableContracts.browser.test.ts +124 -0
- package/test/sdk/account/useSwitchContract.browser.test.ts +111 -0
- package/test/sdk/analytics/useStartRecommendationSession.test.ts +69 -0
- package/test/sdk/analytics/utils.test.ts +23 -0
- package/test/server/index.test.ts +3 -0
- package/test/utils/retry.test.ts +59 -0
package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { Button, Icon, IconButton, Loader } from '@faststore/ui'
|
|
2
|
+
import { useState, type ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useAvailableContracts } from 'src/sdk/account/useAvailableContracts'
|
|
5
|
+
import { useSwitchContract } from 'src/sdk/account/useSwitchContract'
|
|
6
|
+
import { useSession } from 'src/sdk/session'
|
|
7
|
+
|
|
8
|
+
export type ContractSwitcherProps = {
|
|
9
|
+
/** Returns to the drawer menu view (also used by Cancel). */
|
|
10
|
+
onBack: () => void
|
|
11
|
+
/** Closes the whole drawer. */
|
|
12
|
+
onClose: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Contract that can be displayed by the switcher. `isDefault` is optional so the
|
|
17
|
+
* component compiles against both the local mock and the real
|
|
18
|
+
* `useAvailableContracts` hook (which does not expose a default yet).
|
|
19
|
+
*/
|
|
20
|
+
type SwitcherContract = {
|
|
21
|
+
id: string
|
|
22
|
+
corporateName: string
|
|
23
|
+
isActive: boolean
|
|
24
|
+
isDefault?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initialOf = (name: string) => name.trim().charAt(0).toUpperCase()
|
|
28
|
+
|
|
29
|
+
type ContractSwitcherContentProps = {
|
|
30
|
+
loading: boolean
|
|
31
|
+
error: Error | null
|
|
32
|
+
currentContract: SwitcherContract | null
|
|
33
|
+
alternatives: SwitcherContract[]
|
|
34
|
+
ordered: SwitcherContract[]
|
|
35
|
+
search: string
|
|
36
|
+
selectedId: string | null
|
|
37
|
+
switchError: Error | null
|
|
38
|
+
onSearchChange: (value: string) => void
|
|
39
|
+
onSearchClear: () => void
|
|
40
|
+
onSelectContract: (contractId: string) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ContractSwitcherLoading = () => (
|
|
44
|
+
<div data-fs-contract-switcher-loading aria-busy="true">
|
|
45
|
+
<Loader />
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const ContractSwitcherLoadError = () => (
|
|
50
|
+
<div data-fs-contract-switcher-message role="alert">
|
|
51
|
+
<p>We couldn't load your contracts. Please try again.</p>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const ContractSwitcherCurrentSession = ({
|
|
56
|
+
currentContract,
|
|
57
|
+
}: {
|
|
58
|
+
currentContract: SwitcherContract | null
|
|
59
|
+
}) => (
|
|
60
|
+
<div data-fs-contract-switcher-current>
|
|
61
|
+
<span data-fs-contract-switcher-label>Current session</span>
|
|
62
|
+
{currentContract && (
|
|
63
|
+
<div data-fs-contract-switcher-current-card>
|
|
64
|
+
<span data-fs-contract-switcher-avatar aria-hidden="true">
|
|
65
|
+
{initialOf(currentContract.corporateName)}
|
|
66
|
+
</span>
|
|
67
|
+
<span data-fs-contract-switcher-option-name>
|
|
68
|
+
{currentContract.corporateName}
|
|
69
|
+
</span>
|
|
70
|
+
{currentContract.isDefault && (
|
|
71
|
+
<Icon
|
|
72
|
+
data-fs-contract-switcher-default
|
|
73
|
+
name="Star"
|
|
74
|
+
width={20}
|
|
75
|
+
height={20}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const ContractSwitcherEmptyAlternatives = () => (
|
|
84
|
+
<div data-fs-contract-switcher-message data-fs-contract-switcher-empty>
|
|
85
|
+
<p>No other contracts are available for your organization.</p>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
type ContractSwitcherAlternativesProps = {
|
|
90
|
+
alternatives: SwitcherContract[]
|
|
91
|
+
ordered: SwitcherContract[]
|
|
92
|
+
search: string
|
|
93
|
+
selectedId: string | null
|
|
94
|
+
onSearchChange: (value: string) => void
|
|
95
|
+
onSearchClear: () => void
|
|
96
|
+
onSelectContract: (contractId: string) => void
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ContractSwitcherSearch = ({
|
|
100
|
+
search,
|
|
101
|
+
onSearchChange,
|
|
102
|
+
onSearchClear,
|
|
103
|
+
}: Pick<
|
|
104
|
+
ContractSwitcherAlternativesProps,
|
|
105
|
+
'search' | 'onSearchChange' | 'onSearchClear'
|
|
106
|
+
>) => (
|
|
107
|
+
<div data-fs-contract-switcher-search>
|
|
108
|
+
<Icon name="MagnifyingGlass" width={20} height={20} />
|
|
109
|
+
<input
|
|
110
|
+
type="text"
|
|
111
|
+
value={search}
|
|
112
|
+
placeholder="Search"
|
|
113
|
+
aria-label="Search contracts"
|
|
114
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
115
|
+
/>
|
|
116
|
+
{search && (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
data-fs-contract-switcher-search-clear
|
|
120
|
+
aria-label="Clear search"
|
|
121
|
+
onClick={onSearchClear}
|
|
122
|
+
>
|
|
123
|
+
<Icon name="X" width={18} height={18} />
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const ContractSwitcherOptionList = ({
|
|
130
|
+
ordered,
|
|
131
|
+
selectedId,
|
|
132
|
+
onSelectContract,
|
|
133
|
+
}: Pick<
|
|
134
|
+
ContractSwitcherAlternativesProps,
|
|
135
|
+
'ordered' | 'selectedId' | 'onSelectContract'
|
|
136
|
+
>) => (
|
|
137
|
+
<ul data-fs-contract-switcher-list>
|
|
138
|
+
{ordered.map((contract) => {
|
|
139
|
+
const isSelected = selectedId === contract.id
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<li key={contract.id} data-fs-contract-switcher-item>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
data-fs-contract-switcher-option
|
|
146
|
+
data-fs-contract-switcher-option-selected={isSelected}
|
|
147
|
+
aria-pressed={isSelected}
|
|
148
|
+
onClick={() => onSelectContract(contract.id)}
|
|
149
|
+
>
|
|
150
|
+
<span data-fs-contract-switcher-avatar aria-hidden="true">
|
|
151
|
+
{initialOf(contract.corporateName)}
|
|
152
|
+
</span>
|
|
153
|
+
<span data-fs-contract-switcher-option-name>
|
|
154
|
+
{contract.corporateName}
|
|
155
|
+
</span>
|
|
156
|
+
{contract.isDefault && (
|
|
157
|
+
<Icon
|
|
158
|
+
data-fs-contract-switcher-default
|
|
159
|
+
name="Star"
|
|
160
|
+
width={20}
|
|
161
|
+
height={20}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</button>
|
|
165
|
+
</li>
|
|
166
|
+
)
|
|
167
|
+
})}
|
|
168
|
+
</ul>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const ContractSwitcherAlternatives = ({
|
|
172
|
+
alternatives,
|
|
173
|
+
ordered,
|
|
174
|
+
search,
|
|
175
|
+
selectedId,
|
|
176
|
+
onSearchChange,
|
|
177
|
+
onSearchClear,
|
|
178
|
+
onSelectContract,
|
|
179
|
+
}: ContractSwitcherAlternativesProps) => {
|
|
180
|
+
if (alternatives.length === 0) {
|
|
181
|
+
return <ContractSwitcherEmptyAlternatives />
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let listContent: ReactNode
|
|
185
|
+
if (ordered.length === 0) {
|
|
186
|
+
listContent = (
|
|
187
|
+
<div data-fs-contract-switcher-message>
|
|
188
|
+
<p>No contracts match your search.</p>
|
|
189
|
+
</div>
|
|
190
|
+
)
|
|
191
|
+
} else {
|
|
192
|
+
listContent = (
|
|
193
|
+
<ContractSwitcherOptionList
|
|
194
|
+
ordered={ordered}
|
|
195
|
+
selectedId={selectedId}
|
|
196
|
+
onSelectContract={onSelectContract}
|
|
197
|
+
/>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
<p data-fs-contract-switcher-count>
|
|
204
|
+
Select one of <strong>{alternatives.length}</strong> available
|
|
205
|
+
contracts:
|
|
206
|
+
</p>
|
|
207
|
+
<ContractSwitcherSearch
|
|
208
|
+
search={search}
|
|
209
|
+
onSearchChange={onSearchChange}
|
|
210
|
+
onSearchClear={onSearchClear}
|
|
211
|
+
/>
|
|
212
|
+
{listContent}
|
|
213
|
+
</>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const ContractSwitcherSwitchError = () => (
|
|
218
|
+
<div data-fs-contract-switcher-message role="alert">
|
|
219
|
+
<p>
|
|
220
|
+
We couldn't switch your contract. The previous contract is still active.
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const ContractSwitcherContent = ({
|
|
226
|
+
loading,
|
|
227
|
+
error,
|
|
228
|
+
currentContract,
|
|
229
|
+
alternatives,
|
|
230
|
+
ordered,
|
|
231
|
+
search,
|
|
232
|
+
selectedId,
|
|
233
|
+
switchError,
|
|
234
|
+
onSearchChange,
|
|
235
|
+
onSearchClear,
|
|
236
|
+
onSelectContract,
|
|
237
|
+
}: ContractSwitcherContentProps) => {
|
|
238
|
+
if (loading) {
|
|
239
|
+
return <ContractSwitcherLoading />
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (error) {
|
|
243
|
+
return <ContractSwitcherLoadError />
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<>
|
|
248
|
+
<ContractSwitcherCurrentSession currentContract={currentContract} />
|
|
249
|
+
<ContractSwitcherAlternatives
|
|
250
|
+
alternatives={alternatives}
|
|
251
|
+
ordered={ordered}
|
|
252
|
+
search={search}
|
|
253
|
+
selectedId={selectedId}
|
|
254
|
+
onSearchChange={onSearchChange}
|
|
255
|
+
onSearchClear={onSearchClear}
|
|
256
|
+
onSelectContract={onSelectContract}
|
|
257
|
+
/>
|
|
258
|
+
{switchError && <ContractSwitcherSwitchError />}
|
|
259
|
+
</>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Sub-view of the OrganizationDrawer that lets a B2B buyer change the active
|
|
265
|
+
* contract within their Organization Unit (REQ-03..06). Replaces the whole
|
|
266
|
+
* drawer body while open: own header (back / title / close), the current
|
|
267
|
+
* session, a searchable list of alternatives, and a Cancel / Confirm footer.
|
|
268
|
+
*/
|
|
269
|
+
export const ContractSwitcher = ({
|
|
270
|
+
onBack,
|
|
271
|
+
onClose,
|
|
272
|
+
}: ContractSwitcherProps) => {
|
|
273
|
+
const { contracts, loading, error } = useAvailableContracts(true)
|
|
274
|
+
const { b2b } = useSession()
|
|
275
|
+
const {
|
|
276
|
+
switchContract,
|
|
277
|
+
loading: switching,
|
|
278
|
+
error: switchError,
|
|
279
|
+
enabled: switchEnabled,
|
|
280
|
+
} = useSwitchContract()
|
|
281
|
+
|
|
282
|
+
const [search, setSearch] = useState('')
|
|
283
|
+
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
284
|
+
|
|
285
|
+
const allContracts: SwitcherContract[] = contracts
|
|
286
|
+
const activeContractId = b2b?.customerId?.trim() ?? ''
|
|
287
|
+
const currentContract =
|
|
288
|
+
(activeContractId
|
|
289
|
+
? allContracts.find((contract) => contract.id === activeContractId)
|
|
290
|
+
: null) ??
|
|
291
|
+
allContracts.find((contract) => contract.isActive) ??
|
|
292
|
+
allContracts[0] ??
|
|
293
|
+
null
|
|
294
|
+
|
|
295
|
+
const alternatives = allContracts.filter(
|
|
296
|
+
(contract) => contract.id !== currentContract?.id
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const query = search.toLowerCase().trim()
|
|
300
|
+
const filtered = query
|
|
301
|
+
? alternatives.filter((contract) =>
|
|
302
|
+
contract.corporateName.toLowerCase().includes(query)
|
|
303
|
+
)
|
|
304
|
+
: alternatives
|
|
305
|
+
|
|
306
|
+
const ordered = [...filtered].sort((a, b) => {
|
|
307
|
+
if (a.isDefault && !b.isDefault) return -1
|
|
308
|
+
if (b.isDefault && !a.isDefault) return 1
|
|
309
|
+
return a.corporateName.localeCompare(b.corporateName)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const canConfirm =
|
|
313
|
+
switchEnabled &&
|
|
314
|
+
selectedId !== null &&
|
|
315
|
+
selectedId !== currentContract?.id &&
|
|
316
|
+
ordered.some((contract) => contract.id === selectedId) &&
|
|
317
|
+
!switching
|
|
318
|
+
|
|
319
|
+
const handleConfirm = async () => {
|
|
320
|
+
if (!selectedId) {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await switchContract(selectedId)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<section data-fs-contract-switcher>
|
|
329
|
+
<header data-fs-contract-switcher-header>
|
|
330
|
+
<IconButton
|
|
331
|
+
data-fs-contract-switcher-back
|
|
332
|
+
aria-label="Back to account menu"
|
|
333
|
+
icon={<Icon name="ArrowLeft" width={20} height={20} />}
|
|
334
|
+
onClick={onBack}
|
|
335
|
+
/>
|
|
336
|
+
<h2 data-fs-contract-switcher-title>Change contract</h2>
|
|
337
|
+
<IconButton
|
|
338
|
+
data-fs-contract-switcher-close
|
|
339
|
+
aria-label="Close"
|
|
340
|
+
icon={<Icon name="X" width={20} height={20} />}
|
|
341
|
+
onClick={onClose}
|
|
342
|
+
/>
|
|
343
|
+
</header>
|
|
344
|
+
|
|
345
|
+
<div data-fs-contract-switcher-content>
|
|
346
|
+
<ContractSwitcherContent
|
|
347
|
+
loading={loading}
|
|
348
|
+
error={error}
|
|
349
|
+
currentContract={currentContract}
|
|
350
|
+
alternatives={alternatives}
|
|
351
|
+
ordered={ordered}
|
|
352
|
+
search={search}
|
|
353
|
+
selectedId={selectedId}
|
|
354
|
+
switchError={switchError}
|
|
355
|
+
onSearchChange={setSearch}
|
|
356
|
+
onSearchClear={() => setSearch('')}
|
|
357
|
+
onSelectContract={setSelectedId}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<footer data-fs-contract-switcher-footer>
|
|
362
|
+
<Button variant="tertiary" onClick={onBack}>
|
|
363
|
+
Cancel
|
|
364
|
+
</Button>
|
|
365
|
+
<Button
|
|
366
|
+
variant="primary"
|
|
367
|
+
disabled={!canConfirm}
|
|
368
|
+
loading={switching}
|
|
369
|
+
onClick={handleConfirm}
|
|
370
|
+
>
|
|
371
|
+
Confirm
|
|
372
|
+
</Button>
|
|
373
|
+
</footer>
|
|
374
|
+
</section>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SlideOver, useFadeEffect } from '@faststore/ui'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
2
3
|
|
|
3
4
|
import { getStoreURL } from 'src/sdk/localization/useLocalizationConfig'
|
|
4
5
|
import { useSession } from 'src/sdk/session'
|
|
@@ -9,12 +10,15 @@ import {
|
|
|
9
10
|
getVtexCookieNames,
|
|
10
11
|
} from 'src/utils/clearCookies'
|
|
11
12
|
import storeConfig from '../../../../../discovery.config'
|
|
12
|
-
import {
|
|
13
|
+
import { ContractSwitcher } from './ContractSwitcher'
|
|
13
14
|
import { OrganizationDrawerBody } from './OrganizationDrawerBody'
|
|
15
|
+
import { OrganizationDrawerFooter } from './OrganizationDrawerFooter'
|
|
14
16
|
import { OrganizationDrawerHeader } from './OrganizationDrawerHeader'
|
|
15
17
|
import { setReloadAfterLogoutReturn } from './useReloadAfterLogoutReturn'
|
|
16
18
|
import styles from './section.module.scss'
|
|
17
19
|
|
|
20
|
+
type OrganizationDrawerView = 'menu' | 'switch'
|
|
21
|
+
|
|
18
22
|
type OrganizationDrawerProps = {
|
|
19
23
|
isOpen: boolean
|
|
20
24
|
closeDrawer: () => void
|
|
@@ -125,7 +129,9 @@ export const doLogout = async (_event?: unknown) => {
|
|
|
125
129
|
} finally {
|
|
126
130
|
setReloadAfterLogoutReturn()
|
|
127
131
|
window.location.assign(
|
|
128
|
-
`${storeConfig.secureSubdomain}/api/vtexid/pub/logout?scope=${
|
|
132
|
+
`${storeConfig.secureSubdomain}/api/vtexid/pub/logout?scope=${
|
|
133
|
+
storeConfig.api.storeId
|
|
134
|
+
}&returnUrl=${getStoreURL()}`
|
|
129
135
|
)
|
|
130
136
|
}
|
|
131
137
|
}
|
|
@@ -137,17 +143,29 @@ export const OrganizationDrawer = ({
|
|
|
137
143
|
}: OrganizationDrawerProps) => {
|
|
138
144
|
const { fade, fadeOut } = useFadeEffect()
|
|
139
145
|
const { b2b, person } = useSession()
|
|
146
|
+
const [view, setView] = useState<OrganizationDrawerView>('menu')
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!isOpen) {
|
|
150
|
+
setView('menu')
|
|
151
|
+
}
|
|
152
|
+
}, [isOpen])
|
|
140
153
|
|
|
141
154
|
const contractName =
|
|
142
|
-
b2b?.contractName
|
|
143
|
-
|
|
144
|
-
''
|
|
155
|
+
b2b?.contractName?.trim() ||
|
|
156
|
+
b2b?.unitName?.trim() ||
|
|
157
|
+
`${(person?.givenName ?? '').trim()}` ||
|
|
158
|
+
'Organization'
|
|
145
159
|
|
|
146
160
|
const contractUrl = b2b?.unitId
|
|
147
161
|
? `/pvt/organization-account/org-unit/${b2b?.unitId}`
|
|
148
162
|
: null
|
|
149
163
|
|
|
150
164
|
const isOrganizationManager = b2b?.organizationManager || false
|
|
165
|
+
// The switcher is only meaningful for B2B buyers tied to an Organization Unit.
|
|
166
|
+
// Buyers with a single (or no alternative) contract see the empty state inside
|
|
167
|
+
// the switcher; we don't fetch the contract count eagerly to protect TTFB.
|
|
168
|
+
const canSwitchContract = Boolean(b2b?.unitId)
|
|
151
169
|
const isOrgMember = Boolean(b2b?.unitId)
|
|
152
170
|
|
|
153
171
|
return (
|
|
@@ -163,27 +181,34 @@ export const OrganizationDrawer = ({
|
|
|
163
181
|
className: `section ${styles.section} section-organization-drawer`,
|
|
164
182
|
}}
|
|
165
183
|
>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
{view === 'switch' ? (
|
|
185
|
+
<ContractSwitcher onBack={() => setView('menu')} onClose={fadeOut} />
|
|
186
|
+
) : (
|
|
187
|
+
<div data-fs-organization-drawer-menu>
|
|
188
|
+
<div data-fs-organization-drawer-menu-scroll>
|
|
189
|
+
<OrganizationDrawerHeader
|
|
190
|
+
onCloseDrawer={closeDrawer}
|
|
191
|
+
contractName={contractName}
|
|
192
|
+
contractUrl={contractUrl}
|
|
193
|
+
onChangeContract={
|
|
194
|
+
canSwitchContract ? () => setView('switch') : undefined
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
<OrganizationDrawerBody
|
|
198
|
+
isRepresentative={isRepresentative}
|
|
199
|
+
isOrgMember={isOrgMember}
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
<OrganizationDrawerFooter
|
|
203
|
+
orgName={b2b?.unitName ?? ''}
|
|
204
|
+
userName={b2b?.userName ?? ''}
|
|
205
|
+
userEmail={b2b?.userEmail ?? ''}
|
|
206
|
+
showManageLink={isOrganizationManager}
|
|
207
|
+
manageUrl={contractUrl ?? undefined}
|
|
208
|
+
onLogoutClick={doLogout}
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
187
212
|
</SlideOver>
|
|
188
213
|
)
|
|
189
214
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Icon, IconButton, Link } from '@faststore/ui'
|
|
2
|
+
|
|
3
|
+
export interface OrganizationDrawerFooterProps {
|
|
4
|
+
orgName: string
|
|
5
|
+
userName: string
|
|
6
|
+
userEmail?: string
|
|
7
|
+
showManageLink?: boolean
|
|
8
|
+
manageUrl?: string
|
|
9
|
+
onLogoutClick?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const OrganizationDrawerFooter = ({
|
|
13
|
+
orgName,
|
|
14
|
+
userName,
|
|
15
|
+
userEmail,
|
|
16
|
+
showManageLink,
|
|
17
|
+
manageUrl,
|
|
18
|
+
onLogoutClick,
|
|
19
|
+
}: OrganizationDrawerFooterProps) => {
|
|
20
|
+
return (
|
|
21
|
+
<div data-fs-organization-drawer-footer>
|
|
22
|
+
<div data-fs-organization-drawer-footer-org>
|
|
23
|
+
<div data-fs-organization-drawer-footer-org-info>
|
|
24
|
+
<Icon
|
|
25
|
+
data-fs-organization-drawer-footer-org-icon
|
|
26
|
+
name="FolderOpen"
|
|
27
|
+
width={18}
|
|
28
|
+
height={18}
|
|
29
|
+
/>
|
|
30
|
+
<span data-fs-organization-drawer-footer-org-name>{orgName}</span>
|
|
31
|
+
</div>
|
|
32
|
+
{showManageLink && manageUrl && (
|
|
33
|
+
<Link href={manageUrl} data-fs-organization-drawer-footer-manage>
|
|
34
|
+
Manage
|
|
35
|
+
</Link>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div data-fs-organization-drawer-footer-user>
|
|
40
|
+
<div data-fs-organization-drawer-footer-user-info>
|
|
41
|
+
<p data-fs-organization-drawer-footer-user-name>{userName}</p>
|
|
42
|
+
{userEmail && (
|
|
43
|
+
<p data-fs-organization-drawer-footer-user-email>{userEmail}</p>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
<IconButton
|
|
47
|
+
data-fs-organization-drawer-footer-logout
|
|
48
|
+
aria-label="Log out"
|
|
49
|
+
icon={<Icon name="SignOut" width={20} height={20} />}
|
|
50
|
+
onClick={onLogoutClick}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import { Link, SlideOverHeader } from '@faststore/ui'
|
|
1
|
+
import { Button, Icon, Link, SlideOverHeader } from '@faststore/ui'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
|
|
4
4
|
export type OrganizationDrawerHeaderProps = {
|
|
5
5
|
onCloseDrawer?: () => void
|
|
6
6
|
contractImage?: ReactNode
|
|
7
7
|
contractName: string
|
|
8
|
-
contractUrl: string
|
|
8
|
+
contractUrl: string | null
|
|
9
|
+
/**
|
|
10
|
+
* Marks the active contract as the Organization Unit's default (star).
|
|
11
|
+
*/
|
|
12
|
+
isDefault?: boolean
|
|
13
|
+
/**
|
|
14
|
+
* When provided, renders the "Change" CTA next to the active contract,
|
|
15
|
+
* opening the contract switcher (Figma node 103-5434).
|
|
16
|
+
*/
|
|
17
|
+
onChangeContract?: () => void
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
export const OrganizationDrawerHeader = ({
|
|
12
21
|
contractUrl,
|
|
13
22
|
contractName,
|
|
14
23
|
contractImage,
|
|
24
|
+
isDefault,
|
|
15
25
|
onCloseDrawer,
|
|
26
|
+
onChangeContract,
|
|
16
27
|
}: OrganizationDrawerHeaderProps) => {
|
|
17
28
|
return (
|
|
18
29
|
<>
|
|
@@ -20,6 +31,7 @@ export const OrganizationDrawerHeader = ({
|
|
|
20
31
|
<div data-fs-organization-drawer-header>
|
|
21
32
|
<Link
|
|
22
33
|
data-fs-organization-drawer-header-contract-link
|
|
34
|
+
aria-label={contractName}
|
|
23
35
|
{...(contractUrl && { href: contractUrl })}
|
|
24
36
|
>
|
|
25
37
|
<div data-fs-organization-drawer-header-contract-image>
|
|
@@ -29,10 +41,38 @@ export const OrganizationDrawerHeader = ({
|
|
|
29
41
|
</span>
|
|
30
42
|
)}
|
|
31
43
|
</div>
|
|
32
|
-
<h1 data-fs-organization-drawer-header-contract-name>
|
|
33
|
-
{contractName}
|
|
34
|
-
</h1>
|
|
35
44
|
</Link>
|
|
45
|
+
<div data-fs-organization-drawer-header-row>
|
|
46
|
+
<Link
|
|
47
|
+
data-fs-organization-drawer-header-contract-name-link
|
|
48
|
+
{...(contractUrl && { href: contractUrl })}
|
|
49
|
+
>
|
|
50
|
+
<h1 data-fs-organization-drawer-header-contract-name>
|
|
51
|
+
{contractName}
|
|
52
|
+
</h1>
|
|
53
|
+
</Link>
|
|
54
|
+
{(isDefault || onChangeContract) && (
|
|
55
|
+
<div data-fs-organization-drawer-header-actions>
|
|
56
|
+
{isDefault && (
|
|
57
|
+
<Icon
|
|
58
|
+
data-fs-organization-drawer-header-default
|
|
59
|
+
name="Star"
|
|
60
|
+
width={20}
|
|
61
|
+
height={20}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
{onChangeContract && (
|
|
65
|
+
<Button
|
|
66
|
+
data-fs-organization-drawer-header-change
|
|
67
|
+
variant="secondary"
|
|
68
|
+
onClick={onChangeContract}
|
|
69
|
+
>
|
|
70
|
+
Change
|
|
71
|
+
</Button>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
36
76
|
</div>
|
|
37
77
|
</>
|
|
38
78
|
)
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { OrganizationDrawer } from './OrganizationDrawer'
|
|
2
2
|
export { OrganizationDrawerBody } from './OrganizationDrawerBody'
|
|
3
|
+
export { OrganizationDrawerFooter } from './OrganizationDrawerFooter'
|
|
4
|
+
export type { OrganizationDrawerFooterProps } from './OrganizationDrawerFooter'
|
|
3
5
|
export { OrganizationDrawerHeader } from './OrganizationDrawerHeader'
|
|
6
|
+
export type { OrganizationDrawerHeaderProps } from './OrganizationDrawerHeader'
|
|
7
|
+
export { ContractSwitcher } from './ContractSwitcher'
|
|
8
|
+
export type { ContractSwitcherProps } from './ContractSwitcher'
|