@faststore/core 4.4.1-dev.0 → 4.5.0-dev.0
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 +66 -52
- package/@generated/cached-operations.json +2 -0
- package/@generated/gql.ts +12 -0
- package/@generated/graphql.ts +73 -0
- package/@generated/persisted-documents.json +2 -0
- package/@generated/schema.graphql +59 -0
- package/CHANGELOG.md +6 -0
- package/package.json +6 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +5 -1
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +16 -6
- package/src/components/account/Layout/Layout.tsx +14 -5
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +170 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +194 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +15 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +162 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/section.module.scss +49 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +125 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +107 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/styles.module.scss +144 -0
- package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
- package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +89 -0
- package/src/components/account/quotes/MyAccountListQuotes/styles.module.scss +174 -0
- package/src/customizations/src/myAccount/extensions/quotes/after.tsx +5 -0
- package/src/customizations/src/myAccount/extensions/quotes/before.tsx +5 -0
- package/src/pages/pvt/account/quotes/index.tsx +264 -0
- package/src/sdk/account/getMyAccountRoutes.ts +7 -0
- package/src/utils/quoteStatus.ts +27 -0
- package/test/components/account/Layout.test.tsx +62 -0
- package/test/components/account/OrganizationDrawerBody.test.tsx +52 -0
- package/test/components/quotes/MyAccountListQuotes.test.tsx +232 -0
- package/test/components/quotes/MyAccountListQuotesTable.test.tsx +240 -0
- package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +28 -0
- package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +152 -0
- package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +122 -0
- package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +85 -0
- package/test/components/quotes/quoteFilters.test.ts +185 -0
- package/test/sdk/account/getMyAccountRoutes.test.ts +90 -0
- package/test/server/index.test.ts +2 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { Locator } from '@vtex/client-cms'
|
|
2
|
+
import type { GetServerSideProps } from 'next'
|
|
3
|
+
import { NextSeo } from 'next-seo'
|
|
4
|
+
import type { ComponentType } from 'react'
|
|
5
|
+
import { Layout } from 'src/components/account'
|
|
6
|
+
import RenderSections from 'src/components/cms/RenderSections'
|
|
7
|
+
import GLOBAL_COMPONENTS from 'src/components/cms/global/Components'
|
|
8
|
+
import CUSTOM_COMPONENTS from 'src/customizations/src/components'
|
|
9
|
+
|
|
10
|
+
import { getGlobalSectionsData } from 'src/components/cms/GlobalSections'
|
|
11
|
+
|
|
12
|
+
import { gql } from '@generated/gql'
|
|
13
|
+
import type {
|
|
14
|
+
ServerAccountPageQueryQuery,
|
|
15
|
+
ServerAccountPageQueryQueryVariables,
|
|
16
|
+
ServerListQuotesQueryQuery,
|
|
17
|
+
ServerListQuotesQueryQueryVariables,
|
|
18
|
+
ServerQuotesOrganizationMemberQueryQuery,
|
|
19
|
+
ServerQuotesOrganizationMemberQueryQueryVariables,
|
|
20
|
+
} from '@generated/graphql'
|
|
21
|
+
import { ServerAccountPageQueryDocument } from '@generated/graphql'
|
|
22
|
+
import AfterSection from 'src/customizations/src/myAccount/extensions/quotes/after'
|
|
23
|
+
import BeforeSection from 'src/customizations/src/myAccount/extensions/quotes/before'
|
|
24
|
+
import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
|
|
25
|
+
import { execute } from 'src/server'
|
|
26
|
+
import { injectGlobalSections } from 'src/server/cms/global'
|
|
27
|
+
import { withLocaleValidationSSR } from 'src/utils/localization/withLocaleValidation'
|
|
28
|
+
import { getMyAccountRedirect } from 'src/utils/myAccountRedirect'
|
|
29
|
+
|
|
30
|
+
import storeConfig from 'discovery.config'
|
|
31
|
+
import { MyAccountListQuotes } from 'src/components/account/quotes/MyAccountListQuotes'
|
|
32
|
+
import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
|
|
33
|
+
import PageProvider from 'src/sdk/overrides/PageProvider'
|
|
34
|
+
import { extractStatusFromError } from 'src/utils/utilities'
|
|
35
|
+
|
|
36
|
+
const COMPONENTS: Record<string, ComponentType<any>> = {
|
|
37
|
+
...GLOBAL_COMPONENTS,
|
|
38
|
+
...CUSTOM_COMPONENTS,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ListQuotesPageProps = {
|
|
42
|
+
listQuotes: ServerListQuotesQueryQuery['listUserQuotes']
|
|
43
|
+
total: number
|
|
44
|
+
perPage: number
|
|
45
|
+
filters: {
|
|
46
|
+
page: number
|
|
47
|
+
status: string[]
|
|
48
|
+
createdAtFrom: string
|
|
49
|
+
createdAtTo: string
|
|
50
|
+
expiresAtFrom: string
|
|
51
|
+
expiresAtTo: string
|
|
52
|
+
label: string
|
|
53
|
+
}
|
|
54
|
+
} & MyAccountProps
|
|
55
|
+
|
|
56
|
+
export default function ListQuotesPage({
|
|
57
|
+
globalSections: globalSectionsProp,
|
|
58
|
+
accountName,
|
|
59
|
+
listQuotes,
|
|
60
|
+
total,
|
|
61
|
+
perPage,
|
|
62
|
+
filters,
|
|
63
|
+
isRepresentative,
|
|
64
|
+
}: ListQuotesPageProps) {
|
|
65
|
+
const { sections: globalSections, settings: globalSettings } =
|
|
66
|
+
globalSectionsProp ?? {}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<PageProvider context={{ globalSettings }}>
|
|
70
|
+
<RenderSections globalSections={globalSections} components={COMPONENTS}>
|
|
71
|
+
<NextSeo noindex nofollow />
|
|
72
|
+
|
|
73
|
+
<Layout isRepresentative={isRepresentative} accountName={accountName}>
|
|
74
|
+
<BeforeSection />
|
|
75
|
+
<MyAccountListQuotes
|
|
76
|
+
listQuotes={listQuotes}
|
|
77
|
+
filters={filters}
|
|
78
|
+
perPage={perPage}
|
|
79
|
+
total={total}
|
|
80
|
+
/>
|
|
81
|
+
<AfterSection />
|
|
82
|
+
</Layout>
|
|
83
|
+
</RenderSections>
|
|
84
|
+
</PageProvider>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const query = gql(`
|
|
89
|
+
query ServerListQuotesQuery ($page: Int, $perPage: Int, $status: [String], $createdAtFrom: String, $createdAtTo: String, $expiresAtFrom: String, $expiresAtTo: String, $label: String) {
|
|
90
|
+
listUserQuotes (page: $page, perPage: $perPage, status: $status, createdAtFrom: $createdAtFrom, createdAtTo: $createdAtTo, expiresAtFrom: $expiresAtFrom, expiresAtTo: $expiresAtTo, label: $label) {
|
|
91
|
+
list {
|
|
92
|
+
id
|
|
93
|
+
status
|
|
94
|
+
label
|
|
95
|
+
createdAt
|
|
96
|
+
expiresAt
|
|
97
|
+
amount
|
|
98
|
+
createdBy
|
|
99
|
+
}
|
|
100
|
+
paging {
|
|
101
|
+
total
|
|
102
|
+
currentPage
|
|
103
|
+
perPage
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`)
|
|
108
|
+
|
|
109
|
+
const organizationMemberQuery = gql(`
|
|
110
|
+
query ServerQuotesOrganizationMemberQuery {
|
|
111
|
+
isOrganizationMember
|
|
112
|
+
}
|
|
113
|
+
`)
|
|
114
|
+
|
|
115
|
+
const getServerSidePropsBase: GetServerSideProps<
|
|
116
|
+
MyAccountProps,
|
|
117
|
+
Record<string, string>,
|
|
118
|
+
Locator
|
|
119
|
+
> = async (context) => {
|
|
120
|
+
const isRepresentative = getIsRepresentative({
|
|
121
|
+
headers: context.req.headers as Record<string, string>,
|
|
122
|
+
account: storeConfig.api.storeId,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const contentContext = {
|
|
126
|
+
previewData: context.previewData,
|
|
127
|
+
locale: context.locale,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { isFaststoreMyAccountEnabled, redirect } = getMyAccountRedirect({
|
|
131
|
+
query: context.query,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (!isFaststoreMyAccountEnabled) {
|
|
135
|
+
return { redirect }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const [
|
|
139
|
+
globalSectionsPromise,
|
|
140
|
+
globalSectionsHeaderPromise,
|
|
141
|
+
globalSectionsFooterPromise,
|
|
142
|
+
] = getGlobalSectionsData(contentContext)
|
|
143
|
+
|
|
144
|
+
const page = Number(context.query.page as string | undefined) || 1
|
|
145
|
+
const perPage = 25
|
|
146
|
+
const status = (
|
|
147
|
+
Array.isArray(context.query.status)
|
|
148
|
+
? context.query.status
|
|
149
|
+
: [context.query.status]
|
|
150
|
+
).filter(Boolean) as string[]
|
|
151
|
+
const createdAtFrom =
|
|
152
|
+
(context.query.createdAtFrom as string | undefined) || ''
|
|
153
|
+
const createdAtTo = (context.query.createdAtTo as string | undefined) || ''
|
|
154
|
+
const expiresAtFrom =
|
|
155
|
+
(context.query.expiresAtFrom as string | undefined) || ''
|
|
156
|
+
const expiresAtTo = (context.query.expiresAtTo as string | undefined) || ''
|
|
157
|
+
const label = (context.query.label as string | undefined) || ''
|
|
158
|
+
|
|
159
|
+
const [
|
|
160
|
+
listQuotesResult,
|
|
161
|
+
accountProfileResult,
|
|
162
|
+
organizationMemberResult,
|
|
163
|
+
globalSections,
|
|
164
|
+
globalSectionsHeader,
|
|
165
|
+
globalSectionsFooter,
|
|
166
|
+
] = await Promise.all([
|
|
167
|
+
execute<ServerListQuotesQueryQueryVariables, ServerListQuotesQueryQuery>(
|
|
168
|
+
{
|
|
169
|
+
variables: {
|
|
170
|
+
page,
|
|
171
|
+
perPage,
|
|
172
|
+
status,
|
|
173
|
+
createdAtFrom: createdAtFrom || undefined,
|
|
174
|
+
createdAtTo: createdAtTo || undefined,
|
|
175
|
+
expiresAtFrom: expiresAtFrom || undefined,
|
|
176
|
+
expiresAtTo: expiresAtTo || undefined,
|
|
177
|
+
label: label || undefined,
|
|
178
|
+
},
|
|
179
|
+
operation: query,
|
|
180
|
+
},
|
|
181
|
+
{ headers: { ...context.req.headers } }
|
|
182
|
+
),
|
|
183
|
+
execute<ServerAccountPageQueryQueryVariables, ServerAccountPageQueryQuery>(
|
|
184
|
+
{ variables: {}, operation: ServerAccountPageQueryDocument },
|
|
185
|
+
{ headers: { ...context.req.headers } }
|
|
186
|
+
).catch(() => null),
|
|
187
|
+
execute<
|
|
188
|
+
ServerQuotesOrganizationMemberQueryQueryVariables,
|
|
189
|
+
ServerQuotesOrganizationMemberQueryQuery
|
|
190
|
+
>(
|
|
191
|
+
{ variables: {}, operation: organizationMemberQuery },
|
|
192
|
+
{ headers: { ...context.req.headers } }
|
|
193
|
+
).catch(() => null),
|
|
194
|
+
globalSectionsPromise,
|
|
195
|
+
globalSectionsHeaderPromise,
|
|
196
|
+
globalSectionsFooterPromise,
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
if (!organizationMemberResult?.data?.isOrganizationMember) {
|
|
200
|
+
return {
|
|
201
|
+
redirect: {
|
|
202
|
+
destination: `/pvt/account/403?from=${encodeURIComponent('/pvt/account/quotes')}`,
|
|
203
|
+
permanent: false,
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (listQuotesResult.errors) {
|
|
209
|
+
console.error(...listQuotesResult.errors)
|
|
210
|
+
|
|
211
|
+
const errorStatus = extractStatusFromError(listQuotesResult.errors[0])
|
|
212
|
+
|
|
213
|
+
const destination =
|
|
214
|
+
errorStatus === 403 || errorStatus === 401
|
|
215
|
+
? `/pvt/account/403?from=${encodeURIComponent('/pvt/account/quotes')}`
|
|
216
|
+
: '/pvt/account/404'
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
redirect: {
|
|
220
|
+
destination,
|
|
221
|
+
permanent: false,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!listQuotesResult.data.listUserQuotes) {
|
|
227
|
+
return {
|
|
228
|
+
redirect: {
|
|
229
|
+
destination: '/pvt/account/404',
|
|
230
|
+
permanent: false,
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const globalSectionsResult = injectGlobalSections({
|
|
236
|
+
globalSections,
|
|
237
|
+
globalSectionsHeader,
|
|
238
|
+
globalSectionsFooter,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
props: {
|
|
243
|
+
globalSections: globalSectionsResult,
|
|
244
|
+
accountName: accountProfileResult?.data?.accountProfile?.name ?? '',
|
|
245
|
+
listQuotes: listQuotesResult.data.listUserQuotes,
|
|
246
|
+
total: listQuotesResult.data.listUserQuotes.paging.total,
|
|
247
|
+
perPage: listQuotesResult.data.listUserQuotes.paging.perPage,
|
|
248
|
+
filters: {
|
|
249
|
+
page: listQuotesResult.data.listUserQuotes.paging.currentPage ?? page,
|
|
250
|
+
status,
|
|
251
|
+
createdAtFrom,
|
|
252
|
+
createdAtTo,
|
|
253
|
+
expiresAtFrom,
|
|
254
|
+
expiresAtTo,
|
|
255
|
+
label,
|
|
256
|
+
},
|
|
257
|
+
isRepresentative,
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const getServerSideProps = withLocaleValidationSSR(
|
|
263
|
+
getServerSidePropsBase
|
|
264
|
+
)
|
|
@@ -27,6 +27,9 @@ export const PROFILE_ROUTE = '/pvt/account/profile'
|
|
|
27
27
|
export const ORDERS_ROUTE = '/pvt/account/orders'
|
|
28
28
|
export const USER_DETAILS_ROUTE = '/pvt/account/user-details'
|
|
29
29
|
export const SECURITY_ROUTE = '/pvt/account/security'
|
|
30
|
+
export const QUOTES_ROUTE = '/pvt/account/quotes'
|
|
31
|
+
|
|
32
|
+
export const ROUTES_ONLY_FOR_B2B_MEMBERS = [QUOTES_ROUTE]
|
|
30
33
|
|
|
31
34
|
const ROUTE_LABEL_KEYS: Record<string, keyof AccountNavigationLabels> = {
|
|
32
35
|
[PROFILE_ROUTE]: 'profileLabel',
|
|
@@ -45,6 +48,10 @@ const DEFAULT_ROUTES: Route[] = [
|
|
|
45
48
|
title: 'Orders',
|
|
46
49
|
route: ORDERS_ROUTE,
|
|
47
50
|
},
|
|
51
|
+
{
|
|
52
|
+
title: 'Quotes',
|
|
53
|
+
route: QUOTES_ROUTE,
|
|
54
|
+
},
|
|
48
55
|
{
|
|
49
56
|
title: 'User Details',
|
|
50
57
|
route: USER_DETAILS_ROUTE,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type QuoteStatusKey =
|
|
2
|
+
| 'Draft'
|
|
3
|
+
| 'Requested'
|
|
4
|
+
| 'InReview'
|
|
5
|
+
| 'Reviewed'
|
|
6
|
+
| 'Approved'
|
|
7
|
+
| 'Declined'
|
|
8
|
+
| 'Expired'
|
|
9
|
+
| 'ConvertedToCart'
|
|
10
|
+
| 'ConvertedToOrder'
|
|
11
|
+
|
|
12
|
+
export type QuoteStatusMapValue = {
|
|
13
|
+
variant: 'neutral' | 'warning' | 'info' | 'success' | 'danger'
|
|
14
|
+
label: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const quoteStatusMap: Record<QuoteStatusKey, QuoteStatusMapValue> = {
|
|
18
|
+
Draft: { variant: 'neutral', label: 'Draft' },
|
|
19
|
+
Requested: { variant: 'warning', label: 'Requested' },
|
|
20
|
+
InReview: { variant: 'warning', label: 'In Review' },
|
|
21
|
+
Reviewed: { variant: 'info', label: 'Revised' },
|
|
22
|
+
Approved: { variant: 'success', label: 'Approved' },
|
|
23
|
+
Declined: { variant: 'danger', label: 'Declined' },
|
|
24
|
+
Expired: { variant: 'neutral', label: 'Expired' },
|
|
25
|
+
ConvertedToCart: { variant: 'success', label: 'Converted' },
|
|
26
|
+
ConvertedToOrder: { variant: 'success', label: 'Converted to Order' },
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, screen } from '@testing-library/react'
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
9
|
+
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
10
|
+
|
|
11
|
+
vi.mock('../../../src/components/account/Menu', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: ({ items }: { items: Array<{ title: string; route: string }> }) => (
|
|
14
|
+
<ul>
|
|
15
|
+
{items.map((item) => (
|
|
16
|
+
<li key={item.route}>{item.title}</li>
|
|
17
|
+
))}
|
|
18
|
+
</ul>
|
|
19
|
+
),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
import Layout from '../../../src/components/account/Layout/Layout'
|
|
23
|
+
|
|
24
|
+
describe('Layout', () => {
|
|
25
|
+
it('hides the Quotes route when the user is not an org member', () => {
|
|
26
|
+
mockUseSession.mockReturnValue({ b2b: null })
|
|
27
|
+
|
|
28
|
+
render(<Layout accountName="Jane Doe">content</Layout>)
|
|
29
|
+
|
|
30
|
+
expect(screen.queryByText('Quotes')).toBeNull()
|
|
31
|
+
expect(screen.getByText('Profile')).toBeTruthy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('shows the Quotes route when the user belongs to an org unit', () => {
|
|
35
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
36
|
+
|
|
37
|
+
render(<Layout accountName="Jane Doe">content</Layout>)
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('hides User Details when not a representative, regardless of org membership', () => {
|
|
43
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
44
|
+
|
|
45
|
+
render(
|
|
46
|
+
<Layout accountName="Jane Doe" isRepresentative={false}>
|
|
47
|
+
content
|
|
48
|
+
</Layout>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(screen.queryByText('User Details')).toBeNull()
|
|
52
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders children inside the layout content area', () => {
|
|
56
|
+
mockUseSession.mockReturnValue({ b2b: null })
|
|
57
|
+
|
|
58
|
+
render(<Layout accountName="Jane Doe">child content</Layout>)
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText('child content')).toBeTruthy()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, screen } from '@testing-library/react'
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockUseAccountNavigationLabels = vi.hoisted(() => vi.fn())
|
|
9
|
+
vi.mock('src/sdk/account/accountPageContext', () => ({
|
|
10
|
+
useAccountNavigationLabels: mockUseAccountNavigationLabels,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('src/components/ui/Link', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: ({
|
|
16
|
+
href,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
href: string
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
}) => <a href={href}>{children}</a>,
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
import { OrganizationDrawerBody } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody'
|
|
25
|
+
|
|
26
|
+
describe('OrganizationDrawerBody', () => {
|
|
27
|
+
it('hides the Quotes route when the user is not an org member', () => {
|
|
28
|
+
mockUseAccountNavigationLabels.mockReturnValue(undefined)
|
|
29
|
+
|
|
30
|
+
render(<OrganizationDrawerBody isRepresentative isOrgMember={false} />)
|
|
31
|
+
|
|
32
|
+
expect(screen.queryByText('Quotes')).toBeNull()
|
|
33
|
+
expect(screen.getByText('Profile')).toBeTruthy()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('shows the Quotes route when the user belongs to an org unit', () => {
|
|
37
|
+
mockUseAccountNavigationLabels.mockReturnValue(undefined)
|
|
38
|
+
|
|
39
|
+
render(<OrganizationDrawerBody isRepresentative isOrgMember />)
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('hides User Details when not a representative, regardless of org membership', () => {
|
|
45
|
+
mockUseAccountNavigationLabels.mockReturnValue(undefined)
|
|
46
|
+
|
|
47
|
+
render(<OrganizationDrawerBody isRepresentative={false} isOrgMember />)
|
|
48
|
+
|
|
49
|
+
expect(screen.queryByText('User Details')).toBeNull()
|
|
50
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
6
|
+
import React from 'react'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
const mockUseScreenResize = vi.hoisted(() => vi.fn())
|
|
10
|
+
vi.mock('src/sdk/ui/useScreenResize', () => ({
|
|
11
|
+
default: mockUseScreenResize,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock(
|
|
15
|
+
'../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable',
|
|
16
|
+
() => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: () => <div data-testid="quotes-table" />,
|
|
19
|
+
Pagination: ({ page }: { page: number }) => (
|
|
20
|
+
<div data-testid="pagination">page {page}</div>
|
|
21
|
+
),
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
vi.mock(
|
|
26
|
+
'../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider',
|
|
27
|
+
() => ({
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: () => <div data-testid="filter-slider" />,
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const mockUseUI = vi.hoisted(() => vi.fn())
|
|
34
|
+
vi.mock('@faststore/ui', async () => {
|
|
35
|
+
const actual =
|
|
36
|
+
await vi.importActual<typeof import('@faststore/ui')>('@faststore/ui')
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
useUI: mockUseUI,
|
|
40
|
+
SearchInputField: React.forwardRef(function MockSearchInputField(
|
|
41
|
+
{ onBlur, onKeyDown }: any,
|
|
42
|
+
ref: any
|
|
43
|
+
) {
|
|
44
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
45
|
+
React.useImperativeHandle(ref, () => ({ inputRef: inputRef.current }))
|
|
46
|
+
return (
|
|
47
|
+
<input
|
|
48
|
+
ref={inputRef}
|
|
49
|
+
placeholder="Search"
|
|
50
|
+
onBlur={onBlur}
|
|
51
|
+
onKeyDown={onKeyDown}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}),
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
import MyAccountListQuotes from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes'
|
|
59
|
+
|
|
60
|
+
const originalLocation = window.location
|
|
61
|
+
|
|
62
|
+
function stubLocation(search = '') {
|
|
63
|
+
Object.defineProperty(window, 'location', {
|
|
64
|
+
configurable: true,
|
|
65
|
+
value: { ...originalLocation, search, href: '' },
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function baseFilters(overrides: Record<string, unknown> = {}) {
|
|
70
|
+
return {
|
|
71
|
+
page: 1,
|
|
72
|
+
status: [],
|
|
73
|
+
createdAtFrom: '',
|
|
74
|
+
createdAtTo: '',
|
|
75
|
+
expiresAtFrom: '',
|
|
76
|
+
expiresAtTo: '',
|
|
77
|
+
label: '',
|
|
78
|
+
...overrides,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('MyAccountListQuotes', () => {
|
|
83
|
+
const openFilter = vi.fn()
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
stubLocation()
|
|
87
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
88
|
+
mockUseUI.mockReturnValue({ openFilter, filter: false })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
vi.clearAllMocks()
|
|
93
|
+
Object.defineProperty(window, 'location', {
|
|
94
|
+
configurable: true,
|
|
95
|
+
value: originalLocation,
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('renders the quotes table when there are results', () => {
|
|
100
|
+
render(
|
|
101
|
+
<MyAccountListQuotes
|
|
102
|
+
listQuotes={{ list: [{ id: 'q-1' }] } as any}
|
|
103
|
+
total={1}
|
|
104
|
+
perPage={25}
|
|
105
|
+
filters={baseFilters()}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(screen.getByTestId('quotes-table')).toBeTruthy()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('renders the "no quotes" empty state when there are no results and no filters', () => {
|
|
113
|
+
render(
|
|
114
|
+
<MyAccountListQuotes
|
|
115
|
+
listQuotes={{ list: [] } as any}
|
|
116
|
+
total={0}
|
|
117
|
+
perPage={25}
|
|
118
|
+
filters={baseFilters()}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
expect(screen.getByText("You don't have any quotes")).toBeTruthy()
|
|
123
|
+
expect(screen.queryByTestId('quotes-table')).toBeNull()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('renders the "no results" empty state when filters are active but nothing matches', () => {
|
|
127
|
+
render(
|
|
128
|
+
<MyAccountListQuotes
|
|
129
|
+
listQuotes={{ list: [] } as any}
|
|
130
|
+
total={0}
|
|
131
|
+
perPage={25}
|
|
132
|
+
filters={baseFilters({ status: ['Draft'] })}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(screen.getByText('No results found')).toBeTruthy()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('opens the filter slider and dispatches the current selected facets when Filters is clicked', () => {
|
|
140
|
+
render(
|
|
141
|
+
<MyAccountListQuotes
|
|
142
|
+
listQuotes={{ list: [] } as any}
|
|
143
|
+
total={0}
|
|
144
|
+
perPage={25}
|
|
145
|
+
filters={baseFilters({ status: ['Draft'] })}
|
|
146
|
+
/>
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
fireEvent.click(screen.getByText('Filters'))
|
|
150
|
+
|
|
151
|
+
expect(openFilter).toHaveBeenCalledTimes(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('shows the active filter count badge', () => {
|
|
155
|
+
render(
|
|
156
|
+
<MyAccountListQuotes
|
|
157
|
+
listQuotes={{ list: [] } as any}
|
|
158
|
+
total={0}
|
|
159
|
+
perPage={25}
|
|
160
|
+
filters={baseFilters({ status: ['Draft'] })}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
expect(screen.getByText('1')).toBeTruthy()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('renders the filter slider when the UI filter drawer is open', () => {
|
|
168
|
+
mockUseUI.mockReturnValue({ openFilter, filter: true })
|
|
169
|
+
|
|
170
|
+
render(
|
|
171
|
+
<MyAccountListQuotes
|
|
172
|
+
listQuotes={{ list: [] } as any}
|
|
173
|
+
total={0}
|
|
174
|
+
perPage={25}
|
|
175
|
+
filters={baseFilters()}
|
|
176
|
+
/>
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
expect(screen.getByTestId('filter-slider')).toBeTruthy()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('shows pagination on desktop when there are results', () => {
|
|
183
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
184
|
+
|
|
185
|
+
render(
|
|
186
|
+
<MyAccountListQuotes
|
|
187
|
+
listQuotes={{ list: [{ id: 'q-1' }] } as any}
|
|
188
|
+
total={30}
|
|
189
|
+
perPage={25}
|
|
190
|
+
filters={baseFilters({ page: 2 })}
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
expect(screen.getAllByTestId('pagination').length).toBeGreaterThan(0)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('navigates with the search term on blur', async () => {
|
|
198
|
+
render(
|
|
199
|
+
<MyAccountListQuotes
|
|
200
|
+
listQuotes={{ list: [] } as any}
|
|
201
|
+
total={0}
|
|
202
|
+
perPage={25}
|
|
203
|
+
filters={baseFilters()}
|
|
204
|
+
/>
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const input = screen.getByPlaceholderText('Search')
|
|
208
|
+
fireEvent.change(input, { target: { value: 'ACME' } })
|
|
209
|
+
fireEvent.blur(input)
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, 350))
|
|
211
|
+
|
|
212
|
+
expect(window.location.href).toBe('/pvt/account/quotes?label=ACME')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('navigates without a label param when the search is cleared', async () => {
|
|
216
|
+
render(
|
|
217
|
+
<MyAccountListQuotes
|
|
218
|
+
listQuotes={{ list: [] } as any}
|
|
219
|
+
total={0}
|
|
220
|
+
perPage={25}
|
|
221
|
+
filters={baseFilters({ label: 'ACME' })}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const input = screen.getByPlaceholderText('Search')
|
|
226
|
+
fireEvent.change(input, { target: { value: '' } })
|
|
227
|
+
fireEvent.blur(input)
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 350))
|
|
229
|
+
|
|
230
|
+
expect(window.location.href).toBe('/pvt/account/quotes?')
|
|
231
|
+
})
|
|
232
|
+
})
|