@vtex/faststore-plugin-buyer-portal 2.0.28 → 2.0.30
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/package.json +3 -1
- package/src/features/addresses/components/AddressDropdownMenu/AddressDropdownMenu.tsx +57 -47
- package/src/features/addresses/components/AddressLocationsList/AddressLocationsList.tsx +45 -30
- package/src/features/addresses/components/AddressesCard/AddressesCard.tsx +1 -1
- package/src/features/addresses/components/CreateAddressSettingsDrawer/CreateAddressSettingsDrawer.tsx +28 -17
- package/src/features/addresses/layouts/AddressDetailsLayout/AddressDetailsLayout.tsx +62 -43
- package/src/features/addresses/layouts/AddressesLayout/AddressesLayout.tsx +10 -2
- package/src/features/buying-policies/components/AccountingFieldCriteriaSelector/AccountingFieldCriteriaSelector.tsx +5 -2
- package/src/features/buying-policies/components/AccountingFieldCriteriaSelector/AccountingValueCriteriaSelector.tsx +8 -3
- package/src/features/buying-policies/components/BudgetCriteriaSelector/BudgetCriteriaSelector.tsx +5 -2
- package/src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx +1 -0
- package/src/features/org-units/components/AddAllToOrgUnitDropdown/AddAllToOrgUnitDropdown.tsx +12 -6
- package/src/features/recipients/components/RecipientsList/RecipientsList.tsx +7 -1
- package/src/features/shared/clients/Auth.ts +14 -0
- package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +6 -0
- package/src/features/shared/components/LocalizationEslintIgnore/LocalizationEslintIgnore.tsx +12 -0
- package/src/features/shared/components/index.ts +5 -0
- package/src/features/shared/hooks/index.ts +1 -0
- package/src/features/shared/hooks/useIsAddressEditingRestricted.ts +12 -0
- package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +1 -1
- package/src/features/shared/services/__tests__/has-storefront-resource.service.test.ts +69 -0
- package/src/features/shared/services/feature-flags/__tests__/get-feature-flags.service.test.ts +6 -1
- package/src/features/shared/services/feature-flags/get-feature-flags.service.ts +1 -0
- package/src/features/shared/services/has-storefront-resource.service.ts +27 -0
- package/src/features/shared/services/index.ts +4 -0
- package/src/features/shared/utils/__tests__/isAddressEditingRestricted.test.ts +44 -0
- package/src/features/shared/utils/__tests__/withAuthLoader.test.ts +49 -1
- package/src/features/shared/utils/constants.ts +10 -2
- package/src/features/shared/utils/index.ts +4 -0
- package/src/features/shared/utils/isAddressEditingRestricted.ts +10 -0
- package/src/features/shared/utils/withAuthLoader.ts +28 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtex/faststore-plugin-buyer-portal",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
4
4
|
"description": "A plugin for faststore with buyer portal",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"test": "vitest run",
|
|
17
17
|
"test:watch": "vitest",
|
|
18
18
|
"test:coverage": "vitest run --coverage",
|
|
19
|
+
"i18n:check": "node scripts/check-localization-keys.mjs check --base=main",
|
|
20
|
+
"i18n:refresh": "node scripts/check-localization-keys.mjs refresh",
|
|
19
21
|
"prepare": "husky",
|
|
20
22
|
"release": "yarn release:stable",
|
|
21
23
|
"release:stable": "yarn check --integrity && yarn release-it -c .release-it.json --ci --no-npm",
|
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
} from "..";
|
|
13
13
|
import { AddRecipientsDrawer } from "../../../recipients/components";
|
|
14
14
|
import { BasicDropdownMenu, Icon } from "../../../shared/components";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
useBuyerPortal,
|
|
17
|
+
useDrawerProps,
|
|
18
|
+
useIsAddressEditingRestricted,
|
|
19
|
+
} from "../../../shared/hooks";
|
|
16
20
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
17
21
|
import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
|
|
18
22
|
import { useAddRecipientsToAddress } from "../../hooks/useAddRecipientsToAddress";
|
|
@@ -42,6 +46,7 @@ export const AddressDropdownMenu = ({
|
|
|
42
46
|
const router = useRouter();
|
|
43
47
|
const { pushToast } = useUI();
|
|
44
48
|
const { t } = useLocalization();
|
|
49
|
+
const isAddressEditingRestricted = useIsAddressEditingRestricted();
|
|
45
50
|
const { featureFlags } = useBuyerPortal();
|
|
46
51
|
const isRecipientsContractLevelEnabled = Boolean(
|
|
47
52
|
featureFlags?.enableRecipientsContractLevel
|
|
@@ -187,27 +192,31 @@ ${t("shared.messages.changesTimeout")}`,
|
|
|
187
192
|
<BasicDropdownMenu data-fs-bp-dropdown-menu>
|
|
188
193
|
<DropdownItem onClick={handleOpenNewTab}>
|
|
189
194
|
<Icon name="OpenNewTab" {...sizeProps} />
|
|
190
|
-
|
|
191
|
-
</DropdownItem>
|
|
192
|
-
<DropdownItem onClick={openEditDrawerProps}>
|
|
193
|
-
<Icon name="Edit" {...sizeProps} />
|
|
194
|
-
{t("addresses.dropdown.editDetails")}
|
|
195
|
-
</DropdownItem>
|
|
196
|
-
<DropdownItem
|
|
197
|
-
data-fs-bp-testid="dropdown-item-add-location"
|
|
198
|
-
onClick={openLocationDrawer}
|
|
199
|
-
>
|
|
200
|
-
<UIIcon name="PlusCircle" {...sizeProps} />
|
|
201
|
-
{t("addresses.actions.addLocations", [resolvedLocationsLabel])}
|
|
195
|
+
{t("shared.buttons.open")}
|
|
202
196
|
</DropdownItem>
|
|
203
|
-
{!
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
197
|
+
{!isAddressEditingRestricted && (
|
|
198
|
+
<>
|
|
199
|
+
<DropdownItem onClick={openEditDrawerProps}>
|
|
200
|
+
<Icon name="Edit" {...sizeProps} />
|
|
201
|
+
{t("addresses.dropdown.editDetails")}
|
|
202
|
+
</DropdownItem>
|
|
203
|
+
<DropdownItem
|
|
204
|
+
data-fs-bp-testid="dropdown-item-add-location"
|
|
205
|
+
onClick={openLocationDrawer}
|
|
206
|
+
>
|
|
207
|
+
<UIIcon name="PlusCircle" {...sizeProps} />
|
|
208
|
+
{t("addresses.actions.addLocations", [resolvedLocationsLabel])}
|
|
209
|
+
</DropdownItem>
|
|
210
|
+
{!isRecipientsContractLevelEnabled && (
|
|
211
|
+
<DropdownItem
|
|
212
|
+
data-fs-bp-testid="dropdown-item-add-recipient"
|
|
213
|
+
onClick={openRecipientsDrawer}
|
|
214
|
+
>
|
|
215
|
+
<UIIcon name="PlusCircle" {...sizeProps} />
|
|
216
|
+
{t("addresses.actions.addRecipients")}
|
|
217
|
+
</DropdownItem>
|
|
218
|
+
)}
|
|
219
|
+
</>
|
|
211
220
|
)}
|
|
212
221
|
{currentAddress.isDefault ? (
|
|
213
222
|
<DropdownItem onClick={handleUnsetAsDefault}>
|
|
@@ -229,16 +238,15 @@ ${t("shared.messages.changesTimeout")}`,
|
|
|
229
238
|
<UIIcon name="MinusCircle" {...sizeProps} />
|
|
230
239
|
{t("addresses.actions.removeFromUnit")}
|
|
231
240
|
</DropdownItem>
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
{/* )} */}
|
|
241
|
+
{!isAddressEditingRestricted && (
|
|
242
|
+
<DropdownItem
|
|
243
|
+
onClick={openDeleteAddressDrawer}
|
|
244
|
+
data-fs-bp-dropdown-menu-item-mode="danger"
|
|
245
|
+
>
|
|
246
|
+
<Icon name="Delete" {...sizeProps} data-fs-bp-delete-address />
|
|
247
|
+
<span data-fs-bp-delete-address>{t("shared.buttons.delete")}</span>
|
|
248
|
+
</DropdownItem>
|
|
249
|
+
)}
|
|
242
250
|
</BasicDropdownMenu>
|
|
243
251
|
{isEditDrawerOpen && (
|
|
244
252
|
<EditAddressDrawer
|
|
@@ -257,22 +265,24 @@ ${t("shared.messages.changesTimeout")}`,
|
|
|
257
265
|
locationsLabel={resolvedLocationsLabel}
|
|
258
266
|
/>
|
|
259
267
|
)}
|
|
260
|
-
{!
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
268
|
+
{!isAddressEditingRestricted &&
|
|
269
|
+
!isRecipientsContractLevelEnabled &&
|
|
270
|
+
isOpenRecipientsDrawer && (
|
|
271
|
+
<AddRecipientsDrawer
|
|
272
|
+
readonly
|
|
273
|
+
{...recipientDrawerProps}
|
|
274
|
+
isOpen={isOpenRecipientsDrawer}
|
|
275
|
+
isSubmitting={isAddRecipientToAddressLoading}
|
|
276
|
+
onSubmit={handleAddRecipientsSubmit}
|
|
277
|
+
onSubmitted={() => router.reload()}
|
|
278
|
+
addressId={currentAddress.id}
|
|
279
|
+
viewLink={`${buyerPortalRoutes.addressDetails({
|
|
280
|
+
orgUnitId,
|
|
281
|
+
contractId: customerId,
|
|
282
|
+
addressId: currentAddress.id,
|
|
283
|
+
})}?tab=recipient-tab`}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
276
286
|
{isOpenRemoveAddressDrawer && (
|
|
277
287
|
<RemoveAddressDrawer
|
|
278
288
|
readonly
|
|
@@ -15,6 +15,7 @@ import { getTableColumns } from "../../../shared/components/Table/utils/tableCol
|
|
|
15
15
|
import {
|
|
16
16
|
useBuyerPortal,
|
|
17
17
|
useDrawerProps,
|
|
18
|
+
useIsAddressEditingRestricted,
|
|
18
19
|
usePageItems,
|
|
19
20
|
} from "../../../shared/hooks";
|
|
20
21
|
import { PAGE_PARAMS, SEARCH_PARAMS } from "../../../shared/hooks/usePageItems";
|
|
@@ -78,6 +79,7 @@ export const AddressLocationsList = forwardRef<
|
|
|
78
79
|
const [totalLocations, setTotalLocations] = useState(total);
|
|
79
80
|
const { currentOrgUnit } = useBuyerPortal();
|
|
80
81
|
const router = useRouter();
|
|
82
|
+
const isAddressEditingRestricted = useIsAddressEditingRestricted();
|
|
81
83
|
const { listLocationData, listLocationRefresh } = useAddressLocationList({
|
|
82
84
|
addressId: (router.query.addressId as string) || "",
|
|
83
85
|
page,
|
|
@@ -118,8 +120,40 @@ export const AddressLocationsList = forwardRef<
|
|
|
118
120
|
refreshLocations: handleRefreshLocations,
|
|
119
121
|
}));
|
|
120
122
|
|
|
123
|
+
const locationActions = isAddressEditingRestricted
|
|
124
|
+
? []
|
|
125
|
+
: [
|
|
126
|
+
{
|
|
127
|
+
testId: "location-action-delete",
|
|
128
|
+
iconName: "Trash",
|
|
129
|
+
ariaLabel: "remove location action",
|
|
130
|
+
onSelect: openDeleteLocation,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
testId: "location-action-edit",
|
|
134
|
+
iconName: "Edit",
|
|
135
|
+
ariaLabel: "edit location action",
|
|
136
|
+
onSelect: openEditLocation,
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const renderActions = (item: LocationData) =>
|
|
141
|
+
locationActions.map(({ testId, iconName, ariaLabel, onSelect }) => (
|
|
142
|
+
<IconButton
|
|
143
|
+
key={testId}
|
|
144
|
+
data-fs-bp-address-tabs-actions-icon
|
|
145
|
+
data-fs-bp-testid={testId}
|
|
146
|
+
icon={<Icon name={iconName} width={20} height={20} />}
|
|
147
|
+
aria-label={ariaLabel}
|
|
148
|
+
onClick={() => {
|
|
149
|
+
selectLocation(item);
|
|
150
|
+
onSelect();
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
));
|
|
154
|
+
|
|
121
155
|
const columns = getTableColumns({
|
|
122
|
-
actionsLength:
|
|
156
|
+
actionsLength: locationActions.length,
|
|
123
157
|
nameColumnLabel: t("shared.labels.name"),
|
|
124
158
|
});
|
|
125
159
|
|
|
@@ -141,13 +175,15 @@ export const AddressLocationsList = forwardRef<
|
|
|
141
175
|
iconSize={56}
|
|
142
176
|
title={t("addresses.empty.noResults")}
|
|
143
177
|
>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
{!isAddressEditingRestricted && (
|
|
179
|
+
<Button
|
|
180
|
+
variant="primary"
|
|
181
|
+
onClick={() => onOpenAddDrawer?.()}
|
|
182
|
+
data-fs-bp-address-tabs-empty-state
|
|
183
|
+
>
|
|
184
|
+
{t("addresses.buttons.addLocations")}
|
|
185
|
+
</Button>
|
|
186
|
+
)}
|
|
151
187
|
</EmptyState>
|
|
152
188
|
) : (
|
|
153
189
|
<div data-fs-bp-address-tabs-table>
|
|
@@ -162,28 +198,7 @@ export const AddressLocationsList = forwardRef<
|
|
|
162
198
|
key={item.id}
|
|
163
199
|
title={item.value}
|
|
164
200
|
searchTerm={search}
|
|
165
|
-
actionIcons={
|
|
166
|
-
<IconButton
|
|
167
|
-
data-fs-bp-address-tabs-actions-icon
|
|
168
|
-
data-fs-bp-testid="location-action-delete"
|
|
169
|
-
icon={<Icon name="Trash" width={20} height={20} />}
|
|
170
|
-
aria-label={"remove location action"}
|
|
171
|
-
onClick={() => {
|
|
172
|
-
selectLocation(item);
|
|
173
|
-
openDeleteLocation();
|
|
174
|
-
}}
|
|
175
|
-
/>,
|
|
176
|
-
<IconButton
|
|
177
|
-
data-fs-bp-address-tabs-actions-icon
|
|
178
|
-
data-fs-bp-testid="location-action-edit"
|
|
179
|
-
icon={<Icon name="Edit" width={20} height={20} />}
|
|
180
|
-
aria-label={"edit location action"}
|
|
181
|
-
onClick={() => {
|
|
182
|
-
selectLocation(item);
|
|
183
|
-
openEditLocation();
|
|
184
|
-
}}
|
|
185
|
-
/>,
|
|
186
|
-
]}
|
|
201
|
+
actionIcons={renderActions(item)}
|
|
187
202
|
/>
|
|
188
203
|
))
|
|
189
204
|
)}
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
} from "../../../shared/components";
|
|
15
15
|
import { OptionSelected } from "../../../shared/components/OptionSelected/OptionSelected";
|
|
16
16
|
import { SearchHighlight } from "../../../shared/components/SearchHighlight/SearchHighlight";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
SCOPE_KEYS,
|
|
19
|
+
useIsAddressEditingRestricted,
|
|
20
|
+
useSetScopeConfig,
|
|
21
|
+
} from "../../../shared/hooks";
|
|
18
22
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
19
23
|
import { useEditAddressLocationSettings } from "../../hooks";
|
|
20
24
|
import { useDebouncedSearchAddressByUnitId } from "../../hooks/useDebouncedSearchAddressByUnitId";
|
|
@@ -41,6 +45,7 @@ export const CreateAddressSettingsDrawer = ({
|
|
|
41
45
|
}: CreateAddressSettingsDrawerProps) => {
|
|
42
46
|
const { pushToast } = useUI();
|
|
43
47
|
const { t } = useLocalization();
|
|
48
|
+
const isAddressEditingRestricted = useIsAddressEditingRestricted();
|
|
44
49
|
const addressListTypeOptions = createListTypeOptions("addresses", t);
|
|
45
50
|
const router = useRouter();
|
|
46
51
|
const locationLabelText = initialLocationsLabel;
|
|
@@ -172,7 +177,9 @@ export const CreateAddressSettingsDrawer = ({
|
|
|
172
177
|
});
|
|
173
178
|
const formData = getFormData();
|
|
174
179
|
const locationLabelChanged =
|
|
175
|
-
|
|
180
|
+
!isAddressEditingRestricted &&
|
|
181
|
+
!!locationsLabel &&
|
|
182
|
+
locationsLabel !== initialLocationsLabel;
|
|
176
183
|
|
|
177
184
|
if (formData.length === 0 && !locationLabelChanged) {
|
|
178
185
|
// Close drawer and trigger update when only list type changed
|
|
@@ -337,21 +344,25 @@ export const CreateAddressSettingsDrawer = ({
|
|
|
337
344
|
/>
|
|
338
345
|
)}
|
|
339
346
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
347
|
+
{!isAddressEditingRestricted && (
|
|
348
|
+
<>
|
|
349
|
+
<h4
|
|
350
|
+
data-fs-bp-default-addresses-title
|
|
351
|
+
data-fs-bp-default-addresses-locations-field
|
|
352
|
+
>
|
|
353
|
+
{t("addresses.labels.locations")}
|
|
354
|
+
</h4>
|
|
355
|
+
<h4 data-fs-bp-default-addresses-subtitle>
|
|
356
|
+
{t("addresses.forms.setLocationsFieldName", [locationLabelText])}
|
|
357
|
+
</h4>
|
|
358
|
+
<InputText
|
|
359
|
+
label={t("addresses.forms.fieldLabel")}
|
|
360
|
+
value={locationsLabel}
|
|
361
|
+
onChange={(event) => setLocationsLabel(event.target.value)}
|
|
362
|
+
name="locations-label"
|
|
363
|
+
/>
|
|
364
|
+
</>
|
|
365
|
+
)}
|
|
355
366
|
</div>
|
|
356
367
|
</SettingsDrawer>
|
|
357
368
|
);
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
import { TabBar } from "../../../shared/components/Tab/TabBar";
|
|
22
22
|
import { TabContent } from "../../../shared/components/Tab/TabContent";
|
|
23
23
|
import { TabOption } from "../../../shared/components/Tab/TabOption";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
useBuyerPortal,
|
|
26
|
+
useDrawerProps,
|
|
27
|
+
useIsAddressEditingRestricted,
|
|
28
|
+
} from "../../../shared/hooks";
|
|
25
29
|
import {
|
|
26
30
|
PAGE_PARAMS,
|
|
27
31
|
SEARCH_PARAMS,
|
|
@@ -81,6 +85,7 @@ export const AddressDetailsLayout = ({
|
|
|
81
85
|
currentContract: contract,
|
|
82
86
|
featureFlags,
|
|
83
87
|
} = useBuyerPortal();
|
|
88
|
+
const isAddressEditingRestricted = useIsAddressEditingRestricted();
|
|
84
89
|
const isRecipientsContractLevelEnabled = Boolean(
|
|
85
90
|
featureFlags?.enableRecipientsContractLevel
|
|
86
91
|
);
|
|
@@ -273,8 +278,12 @@ export const AddressDetailsLayout = ({
|
|
|
273
278
|
if (deleteRecipientErrorRef.current) throw deleteRecipientErrorRef.current;
|
|
274
279
|
};
|
|
275
280
|
|
|
276
|
-
const renderActionButton = () =>
|
|
277
|
-
({
|
|
281
|
+
const renderActionButton = () => {
|
|
282
|
+
if (isAddressEditingRestricted) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
278
287
|
["address-tab"]: (
|
|
279
288
|
<button
|
|
280
289
|
type="button"
|
|
@@ -302,7 +311,8 @@ export const AddressDetailsLayout = ({
|
|
|
302
311
|
{t("shared.buttons.add")}
|
|
303
312
|
</button>
|
|
304
313
|
),
|
|
305
|
-
}[activeTabId]
|
|
314
|
+
}[activeTabId];
|
|
315
|
+
};
|
|
306
316
|
|
|
307
317
|
return (
|
|
308
318
|
<GlobalLayout>
|
|
@@ -389,11 +399,13 @@ export const AddressDetailsLayout = ({
|
|
|
389
399
|
);
|
|
390
400
|
})}
|
|
391
401
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
402
|
+
{!isAddressEditingRestricted && (
|
|
403
|
+
<EditAddressDrawer
|
|
404
|
+
readonly
|
|
405
|
+
{...createDrawerProps}
|
|
406
|
+
currentAddress={addressDetails}
|
|
407
|
+
/>
|
|
408
|
+
)}
|
|
397
409
|
</TabContent>
|
|
398
410
|
|
|
399
411
|
<TabContent id="location-tab">
|
|
@@ -406,15 +418,17 @@ export const AddressDetailsLayout = ({
|
|
|
406
418
|
onOpenAddDrawer={openLocationsDrawer}
|
|
407
419
|
locationsLabel={locationSettingsData?.name}
|
|
408
420
|
/>
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
421
|
+
{!isAddressEditingRestricted && (
|
|
422
|
+
<LocationDrawer
|
|
423
|
+
{...locationsDrawerProps}
|
|
424
|
+
isOpen={isOpenLocationsDrawer}
|
|
425
|
+
addressId={addressDetails.id}
|
|
426
|
+
locationsLabel={locationSettingsData?.name}
|
|
427
|
+
onAddLocationSuccess={() =>
|
|
428
|
+
addressLocationsListRef.current?.refreshLocations()
|
|
429
|
+
}
|
|
430
|
+
/>
|
|
431
|
+
)}
|
|
418
432
|
</TabContent>
|
|
419
433
|
|
|
420
434
|
{!isRecipientsContractLevelEnabled && (
|
|
@@ -446,6 +460,7 @@ export const AddressDetailsLayout = ({
|
|
|
446
460
|
isLoading={isRecipientsLoading}
|
|
447
461
|
searchTerm={recipientsSearchTerm}
|
|
448
462
|
countryCode="USA"
|
|
463
|
+
hideActions={isAddressEditingRestricted}
|
|
449
464
|
onEditRecipient={(recipient) => {
|
|
450
465
|
setCurrentRecipient(recipient);
|
|
451
466
|
openEditRecipientDrawer();
|
|
@@ -477,7 +492,7 @@ export const AddressDetailsLayout = ({
|
|
|
477
492
|
)}
|
|
478
493
|
</div>
|
|
479
494
|
|
|
480
|
-
{isOpenRecipientsDrawer && (
|
|
495
|
+
{isOpenRecipientsDrawer && !isAddressEditingRestricted && (
|
|
481
496
|
<AddRecipientsDrawer
|
|
482
497
|
readonly
|
|
483
498
|
{...recipientDrawerProps}
|
|
@@ -489,31 +504,35 @@ export const AddressDetailsLayout = ({
|
|
|
489
504
|
/>
|
|
490
505
|
)}
|
|
491
506
|
|
|
492
|
-
{isOpenEditRecipientDrawer &&
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
507
|
+
{isOpenEditRecipientDrawer &&
|
|
508
|
+
currentRecipient &&
|
|
509
|
+
!isAddressEditingRestricted && (
|
|
510
|
+
<EditRecipientDrawer
|
|
511
|
+
readonly
|
|
512
|
+
recipient={currentRecipient}
|
|
513
|
+
{...editRecipientDrawerProps}
|
|
514
|
+
isOpen={isOpenEditRecipientDrawer}
|
|
515
|
+
isSubmitting={isEditRecipientToAddressLoading}
|
|
516
|
+
onSubmit={handleEditRecipientSubmit}
|
|
517
|
+
onSubmitted={handleRefetchRecipients}
|
|
518
|
+
addressId={addressDetails.id}
|
|
519
|
+
/>
|
|
520
|
+
)}
|
|
504
521
|
|
|
505
|
-
{isOpenDeleteRecipientDrawer &&
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
522
|
+
{isOpenDeleteRecipientDrawer &&
|
|
523
|
+
currentRecipient &&
|
|
524
|
+
!isAddressEditingRestricted && (
|
|
525
|
+
<DeleteRecipientDrawer
|
|
526
|
+
readonly
|
|
527
|
+
recipient={currentRecipient}
|
|
528
|
+
{...deleteRecipientDrawerProps}
|
|
529
|
+
isOpen={isOpenDeleteRecipientDrawer}
|
|
530
|
+
isSubmitting={isDeleteRecipientAddressLoading}
|
|
531
|
+
onSubmit={handleDeleteRecipientSubmit}
|
|
532
|
+
onSubmitted={handleRefetchRecipients}
|
|
533
|
+
addressId={addressDetails.id}
|
|
534
|
+
/>
|
|
535
|
+
)}
|
|
517
536
|
</TabContent>
|
|
518
537
|
)}
|
|
519
538
|
</Tab>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
useBuyerPortal,
|
|
18
18
|
useDrawerProps,
|
|
19
19
|
useGetScopeConfig,
|
|
20
|
+
useIsAddressEditingRestricted,
|
|
20
21
|
useUrlPaginatedSearch,
|
|
21
22
|
SCOPE_KEYS,
|
|
22
23
|
} from "../../../shared/hooks";
|
|
@@ -34,6 +35,7 @@ export const AddressLayout = () => {
|
|
|
34
35
|
const { currentOrgUnit } = useBuyerPortal();
|
|
35
36
|
const { pushToast } = useUI();
|
|
36
37
|
const { t } = useLocalization();
|
|
38
|
+
const isAddressEditingRestricted = useIsAddressEditingRestricted();
|
|
37
39
|
|
|
38
40
|
const {
|
|
39
41
|
page,
|
|
@@ -165,7 +167,9 @@ export const AddressLayout = () => {
|
|
|
165
167
|
style={{ backgroundColor: "transparent", color: "#1f1f1f" }}
|
|
166
168
|
onClick={openCreateAddressSettingsDrawer}
|
|
167
169
|
/>
|
|
168
|
-
|
|
170
|
+
{!isAddressEditingRestricted && (
|
|
171
|
+
<HeaderInside.Button onClick={openCreateDrawer} />
|
|
172
|
+
)}
|
|
169
173
|
</>
|
|
170
174
|
)}
|
|
171
175
|
</HeaderInside>
|
|
@@ -180,11 +184,15 @@ export const AddressLayout = () => {
|
|
|
180
184
|
title={
|
|
181
185
|
committedSearch.length > 0
|
|
182
186
|
? t("addresses.empty.noResults")
|
|
187
|
+
: isAddressEditingRestricted
|
|
188
|
+
? t("addresses.empty.managedByMerchant")
|
|
183
189
|
: t("addresses.empty.noAddressesYet")
|
|
184
190
|
}
|
|
185
191
|
description={
|
|
186
192
|
committedSearch.length > 0
|
|
187
193
|
? t("addresses.empty.tryDifferentTerms")
|
|
194
|
+
: isAddressEditingRestricted
|
|
195
|
+
? t("addresses.empty.managedByMerchantDescription")
|
|
188
196
|
: undefined
|
|
189
197
|
}
|
|
190
198
|
iconName={
|
|
@@ -263,7 +271,7 @@ export const AddressLayout = () => {
|
|
|
263
271
|
</div>
|
|
264
272
|
)}
|
|
265
273
|
|
|
266
|
-
{isCreateAddressDrawerOpen && (
|
|
274
|
+
{isCreateAddressDrawerOpen && !isAddressEditingRestricted && (
|
|
267
275
|
<CreateAddressDrawer
|
|
268
276
|
readonly
|
|
269
277
|
isOpen={isCreateAddressDrawerOpen}
|
|
@@ -4,6 +4,7 @@ import { useUI } from "@faststore/ui";
|
|
|
4
4
|
|
|
5
5
|
import { useListAccountingFields } from "../../../accounting-fields/hooks";
|
|
6
6
|
import { CustomDropdown } from "../../../shared/components/CustomDropdown/CustomDropdown";
|
|
7
|
+
import { LocalizationEslintIgnore } from "../../../shared/components/LocalizationEslintIgnore/LocalizationEslintIgnore";
|
|
7
8
|
import { useBuyerPortal } from "../../../shared/hooks";
|
|
8
9
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
9
10
|
import {
|
|
@@ -91,7 +92,9 @@ export const AccountingFieldCriteriaSelector = ({
|
|
|
91
92
|
<div data-fs-bp-accounting-field-criteria-selector>
|
|
92
93
|
<div data-fs-bp-accounting-fields-criteria-string>
|
|
93
94
|
<span onClick={handleEditablePartClick} data-fs-bp-editable-part>
|
|
94
|
-
|
|
95
|
+
<LocalizationEslintIgnore>
|
|
96
|
+
$exists(customData.customFields.fields[customFieldId="
|
|
97
|
+
</LocalizationEslintIgnore>
|
|
95
98
|
</span>
|
|
96
99
|
|
|
97
100
|
<div onClick={handleFieldClick} style={{ display: "inline-block" }}>
|
|
@@ -115,7 +118,7 @@ export const AccountingFieldCriteriaSelector = ({
|
|
|
115
118
|
</div>
|
|
116
119
|
|
|
117
120
|
<span onClick={handleEditablePartClick} data-fs-bp-editable-part>
|
|
118
|
-
"].value)
|
|
121
|
+
<LocalizationEslintIgnore>"].value)</LocalizationEslintIgnore>
|
|
119
122
|
</span>
|
|
120
123
|
</div>
|
|
121
124
|
</div>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useListAccountingValues,
|
|
9
9
|
} from "../../../accounting-fields/hooks";
|
|
10
10
|
import { CustomDropdown } from "../../../shared/components/CustomDropdown/CustomDropdown";
|
|
11
|
+
import { LocalizationEslintIgnore } from "../../../shared/components/LocalizationEslintIgnore/LocalizationEslintIgnore";
|
|
11
12
|
import { useBuyerPortal, useDebounce } from "../../../shared/hooks";
|
|
12
13
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
13
14
|
import {
|
|
@@ -216,7 +217,9 @@ export const AccountingValueCriteriaSelector = ({
|
|
|
216
217
|
<div data-fs-bp-accounting-field-criteria-selector>
|
|
217
218
|
<div data-fs-bp-accounting-fields-criteria-string>
|
|
218
219
|
<span onClick={handleEditablePartClick} data-fs-bp-editable-part>
|
|
219
|
-
|
|
220
|
+
<LocalizationEslintIgnore>
|
|
221
|
+
customData.customFields.fields[customFieldId="
|
|
222
|
+
</LocalizationEslintIgnore>
|
|
220
223
|
</span>
|
|
221
224
|
|
|
222
225
|
<div onClick={handleFieldClick} style={{ display: "inline-block" }}>
|
|
@@ -244,7 +247,9 @@ export const AccountingValueCriteriaSelector = ({
|
|
|
244
247
|
</div>
|
|
245
248
|
|
|
246
249
|
<span onClick={handleEditablePartClick} data-fs-bp-editable-part>
|
|
247
|
-
|
|
250
|
+
<LocalizationEslintIgnore>
|
|
251
|
+
"].customFieldValueId="
|
|
252
|
+
</LocalizationEslintIgnore>
|
|
248
253
|
</span>
|
|
249
254
|
|
|
250
255
|
<div onClick={handleValueClick} style={{ display: "inline-block" }}>
|
|
@@ -275,7 +280,7 @@ export const AccountingValueCriteriaSelector = ({
|
|
|
275
280
|
</div>
|
|
276
281
|
|
|
277
282
|
<span onClick={handleEditablePartClick} data-fs-bp-editable-part>
|
|
278
|
-
"
|
|
283
|
+
<LocalizationEslintIgnore>"</LocalizationEslintIgnore>
|
|
279
284
|
</span>
|
|
280
285
|
</div>
|
|
281
286
|
</div>
|
package/src/features/buying-policies/components/BudgetCriteriaSelector/BudgetCriteriaSelector.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
|
|
3
3
|
import { CustomDropdown } from "../../../shared/components/CustomDropdown/CustomDropdown";
|
|
4
|
+
import { LocalizationEslintIgnore } from "../../../shared/components/LocalizationEslintIgnore/LocalizationEslintIgnore";
|
|
4
5
|
import { useBuyerPortal } from "../../../shared/hooks";
|
|
5
6
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
6
7
|
import { useGetBudgets } from "../../hooks/useGetBudgets";
|
|
@@ -90,7 +91,9 @@ export const BudgetCriteriaSelector = ({
|
|
|
90
91
|
<div data-fs-bp-budget-criteria-selector>
|
|
91
92
|
<div data-fs-bp-budget-criteria-string>
|
|
92
93
|
<span onClick={handleEditablePartClick} data-fs-bp-editable-part>
|
|
93
|
-
|
|
94
|
+
<LocalizationEslintIgnore>
|
|
95
|
+
$exists(budgetData.budgets[id="
|
|
96
|
+
</LocalizationEslintIgnore>
|
|
94
97
|
</span>
|
|
95
98
|
|
|
96
99
|
<div onClick={handleBudgetValueClick}>
|
|
@@ -115,7 +118,7 @@ export const BudgetCriteriaSelector = ({
|
|
|
115
118
|
</div>
|
|
116
119
|
|
|
117
120
|
<span onClick={handleEditablePartClick} data-fs-bp-editable-part>
|
|
118
|
-
"])
|
|
121
|
+
<LocalizationEslintIgnore>"])</LocalizationEslintIgnore>
|
|
119
122
|
</span>
|
|
120
123
|
</div>
|
|
121
124
|
</div>
|
|
@@ -37,6 +37,7 @@ export const UnableToRemoveContractsDrawer = ({
|
|
|
37
37
|
</p>
|
|
38
38
|
<p>
|
|
39
39
|
{t("contracts.removeDrawer.unableDeselectHint", [
|
|
40
|
+
// eslint-disable-next-line no-restricted-syntax -- legacy hardcoded string, pending migration (see specs/custom-localization-system.md#out-of-scope)
|
|
40
41
|
<strong key="organizationalUnit">organizational unit</strong>,
|
|
41
42
|
])}
|
|
42
43
|
</p>
|
package/src/features/org-units/components/AddAllToOrgUnitDropdown/AddAllToOrgUnitDropdown.tsx
CHANGED
|
@@ -12,7 +12,10 @@ import { AddBuyingPolicyDrawer } from "../../../buying-policies/components";
|
|
|
12
12
|
import { CreateCreditCardDrawer } from "../../../credit-cards/components";
|
|
13
13
|
import { useGetRolesOptions } from "../../../roles/hooks";
|
|
14
14
|
import { BasicDropdownMenu, Icon } from "../../../shared/components";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
useDrawerProps,
|
|
17
|
+
useIsAddressEditingRestricted,
|
|
18
|
+
} from "../../../shared/hooks";
|
|
16
19
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
17
20
|
import { CreateUserDrawerSelector } from "../../../users/components";
|
|
18
21
|
|
|
@@ -38,6 +41,7 @@ export const AddAllToOrgUnitDropdown = ({
|
|
|
38
41
|
const { t } = useLocalization();
|
|
39
42
|
const sizeProps = { width: 20, height: 20 };
|
|
40
43
|
const { rolesOptions } = useGetRolesOptions(unitId);
|
|
44
|
+
const isAddressEditingRestricted = useIsAddressEditingRestricted();
|
|
41
45
|
|
|
42
46
|
const {
|
|
43
47
|
open: openCreditCardDrawer,
|
|
@@ -119,10 +123,12 @@ export const AddAllToOrgUnitDropdown = ({
|
|
|
119
123
|
|
|
120
124
|
{isSingleContract && (
|
|
121
125
|
<>
|
|
122
|
-
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
{!isAddressEditingRestricted && (
|
|
127
|
+
<DropdownItem onClick={openAddressDrawer}>
|
|
128
|
+
<Icon name="LocalPostOffice" {...sizeProps} />
|
|
129
|
+
{t("shared.orgUnitDropdown.addAddress")}
|
|
130
|
+
</DropdownItem>
|
|
131
|
+
)}
|
|
126
132
|
<DropdownItem onClick={openCreditCardDrawer}>
|
|
127
133
|
<Icon name="CreditCard" {...sizeProps} />
|
|
128
134
|
{t("shared.orgUnitDropdown.addCreditCard")}
|
|
@@ -181,7 +187,7 @@ export const AddAllToOrgUnitDropdown = ({
|
|
|
181
187
|
/>
|
|
182
188
|
)}
|
|
183
189
|
|
|
184
|
-
{isOpenAddressDrawer && (
|
|
190
|
+
{isOpenAddressDrawer && !isAddressEditingRestricted && (
|
|
185
191
|
<CreateAddressDrawer
|
|
186
192
|
{...addressDrawerProps}
|
|
187
193
|
isOpen={isOpenAddressDrawer}
|
|
@@ -18,6 +18,7 @@ export type RecipientsListProps = {
|
|
|
18
18
|
countryCode?: CountryCodes;
|
|
19
19
|
onEditRecipient: (recipient: RecipientData) => void;
|
|
20
20
|
onDeleteRecipient: (recipient: RecipientData) => void;
|
|
21
|
+
hideActions?: boolean;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export const RecipientsList = ({
|
|
@@ -27,11 +28,12 @@ export const RecipientsList = ({
|
|
|
27
28
|
countryCode,
|
|
28
29
|
onEditRecipient,
|
|
29
30
|
onDeleteRecipient,
|
|
31
|
+
hideActions = false,
|
|
30
32
|
}: RecipientsListProps) => {
|
|
31
33
|
const { t } = useLocalization();
|
|
32
34
|
|
|
33
35
|
const columns = getTableColumns({
|
|
34
|
-
actionsLength: 2,
|
|
36
|
+
actionsLength: hideActions ? 0 : 2,
|
|
35
37
|
nameColumnSize: "11.125rem",
|
|
36
38
|
nameColumnLabel: t("shared.labels.name"),
|
|
37
39
|
extraColumns: [
|
|
@@ -46,6 +48,10 @@ export const RecipientsList = ({
|
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
const renderActions = (recipient: RecipientData) => {
|
|
51
|
+
if (hideActions) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
return [
|
|
50
56
|
<IconButton
|
|
51
57
|
data-fs-bp-recipients-action-icon
|
|
@@ -2,6 +2,8 @@ import { getApiUrl } from "../../shared/utils";
|
|
|
2
2
|
|
|
3
3
|
import { Client } from "./Client";
|
|
4
4
|
|
|
5
|
+
import type { StorefrontResourceName } from "../utils/constants";
|
|
6
|
+
|
|
5
7
|
type AuthContextProps = {
|
|
6
8
|
cookie: string;
|
|
7
9
|
};
|
|
@@ -28,6 +30,18 @@ export default class AuthClient extends Client {
|
|
|
28
30
|
},
|
|
29
31
|
});
|
|
30
32
|
}
|
|
33
|
+
|
|
34
|
+
hasStorefrontResource(
|
|
35
|
+
resourceName: StorefrontResourceName,
|
|
36
|
+
context: AuthContextProps
|
|
37
|
+
) {
|
|
38
|
+
return this.get<boolean | string>(`permissions/granted/${resourceName}`, {
|
|
39
|
+
headers: {
|
|
40
|
+
Cookie: context.cookie,
|
|
41
|
+
"Content-Type": "text/plain",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
const authClient = new AuthClient();
|
|
@@ -20,6 +20,11 @@ export type FeatureFlags = {
|
|
|
20
20
|
authSettings?: AuthSettingsFeatureFlags;
|
|
21
21
|
suma?: boolean;
|
|
22
22
|
enableRecipientsContractLevel?: boolean;
|
|
23
|
+
enableAddressRestriction?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type StorefrontPermissions = {
|
|
27
|
+
canManageAddresses: boolean;
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
export type BuyerPortalContextType = {
|
|
@@ -28,6 +33,7 @@ export type BuyerPortalContextType = {
|
|
|
28
33
|
currentContract?: ContractData | null;
|
|
29
34
|
currentUser?: UserData | null;
|
|
30
35
|
featureFlags?: FeatureFlags;
|
|
36
|
+
permissions?: StorefrontPermissions;
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
export const BuyerPortalContext = createContext<
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type LocalizationEslintIgnoreProps = {
|
|
2
|
+
children: React.ReactNode;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps non-translatable literals (e.g. technical/DSL syntax) so the
|
|
7
|
+
* localization guardrail ignores them. `eslint.config.mjs` exempts this
|
|
8
|
+
* component by name. See docs/conventions.md#localization.
|
|
9
|
+
*/
|
|
10
|
+
export const LocalizationEslintIgnore = ({
|
|
11
|
+
children,
|
|
12
|
+
}: LocalizationEslintIgnoreProps) => <>{children}</>;
|
|
@@ -35,6 +35,7 @@ export {
|
|
|
35
35
|
type BuyerPortalContextType,
|
|
36
36
|
type BuyerPortalProviderProps,
|
|
37
37
|
type FeatureFlags,
|
|
38
|
+
type StorefrontPermissions,
|
|
38
39
|
} from "./BuyerPortalProvider/BuyerPortalProvider";
|
|
39
40
|
export { Card, CardBody, CardFooter, CardHeader } from "./Card";
|
|
40
41
|
export {
|
|
@@ -73,6 +74,10 @@ export {
|
|
|
73
74
|
LevelDivider,
|
|
74
75
|
type LevelDividerProps,
|
|
75
76
|
} from "./LevelDivider/LevelDivider";
|
|
77
|
+
export {
|
|
78
|
+
LocalizationEslintIgnore,
|
|
79
|
+
type LocalizationEslintIgnoreProps,
|
|
80
|
+
} from "./LocalizationEslintIgnore/LocalizationEslintIgnore";
|
|
76
81
|
export { Logo } from "./Logo/Logo";
|
|
77
82
|
export { MainLinksDropdownMenu } from "./MainLinksDropdownMenu/MainLinksDropdownMenu";
|
|
78
83
|
export { Pagination } from "./Pagination/Pagination";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isAddressEditingRestricted } from "../utils/isAddressEditingRestricted";
|
|
2
|
+
|
|
3
|
+
import { useBuyerPortal } from "./useBuyerPortal";
|
|
4
|
+
|
|
5
|
+
export const useIsAddressEditingRestricted = (): boolean => {
|
|
6
|
+
const { featureFlags, permissions } = useBuyerPortal();
|
|
7
|
+
|
|
8
|
+
return isAddressEditingRestricted({
|
|
9
|
+
enableAddressRestriction: featureFlags?.enableAddressRestriction,
|
|
10
|
+
canManageAddresses: permissions?.canManageAddresses,
|
|
11
|
+
});
|
|
12
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("discovery.config", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
api: { storeId: "test-store" },
|
|
6
|
+
secureSubdomain: "https://test.vtex.com",
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const hasStorefrontResourceMock = vi.fn();
|
|
11
|
+
|
|
12
|
+
vi.mock("../../clients/Auth", () => ({
|
|
13
|
+
authClient: {
|
|
14
|
+
hasStorefrontResource: (...args: unknown[]) =>
|
|
15
|
+
hasStorefrontResourceMock(...args),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { hasStorefrontResourceService } from "../has-storefront-resource.service";
|
|
20
|
+
|
|
21
|
+
describe("hasStorefrontResourceService", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
hasStorefrontResourceMock.mockReset();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns true when the BFF answers true", async () => {
|
|
27
|
+
hasStorefrontResourceMock.mockResolvedValue(true);
|
|
28
|
+
|
|
29
|
+
await expect(
|
|
30
|
+
hasStorefrontResourceService({
|
|
31
|
+
cookie: "cookie",
|
|
32
|
+
resourceName: "manage-addresses",
|
|
33
|
+
})
|
|
34
|
+
).resolves.toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns true when the BFF answers the string true", async () => {
|
|
38
|
+
hasStorefrontResourceMock.mockResolvedValue("true");
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
hasStorefrontResourceService({
|
|
42
|
+
cookie: "cookie",
|
|
43
|
+
resourceName: "manage-addresses",
|
|
44
|
+
})
|
|
45
|
+
).resolves.toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns false when the BFF answers false", async () => {
|
|
49
|
+
hasStorefrontResourceMock.mockResolvedValue(false);
|
|
50
|
+
|
|
51
|
+
await expect(
|
|
52
|
+
hasStorefrontResourceService({
|
|
53
|
+
cookie: "cookie",
|
|
54
|
+
resourceName: "manage-addresses",
|
|
55
|
+
})
|
|
56
|
+
).resolves.toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("fails closed when the BFF throws", async () => {
|
|
60
|
+
hasStorefrontResourceMock.mockRejectedValue(new Error("network"));
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
hasStorefrontResourceService({
|
|
64
|
+
cookie: "cookie",
|
|
65
|
+
resourceName: "manage-addresses",
|
|
66
|
+
})
|
|
67
|
+
).resolves.toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/features/shared/services/feature-flags/__tests__/get-feature-flags.service.test.ts
CHANGED
|
@@ -51,11 +51,16 @@ describe("normalizeFlagsEndpointBooleans", () => {
|
|
|
51
51
|
|
|
52
52
|
it("preserves other flags alongside suma", () => {
|
|
53
53
|
const result = normalizeFlagsEndpointBooleans({
|
|
54
|
-
flags: {
|
|
54
|
+
flags: {
|
|
55
|
+
suma: true,
|
|
56
|
+
enable_alternative_login: true,
|
|
57
|
+
enable_address_restriction: true,
|
|
58
|
+
},
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
expect(result["suma"]).toBe(true);
|
|
58
62
|
expect(result["enable_alternative_login"]).toBe(true);
|
|
63
|
+
expect(result["enable_address_restriction"]).toBe(true);
|
|
59
64
|
});
|
|
60
65
|
});
|
|
61
66
|
|
|
@@ -100,5 +100,6 @@ export async function getBuyerPortalFeatureFlags(
|
|
|
100
100
|
enableRecipientsContractLevel:
|
|
101
101
|
normalized["enable_recipients_contract_level"] ?? false,
|
|
102
102
|
suma: normalized["suma"] ?? false,
|
|
103
|
+
enableAddressRestriction: normalized["enable_address_restriction"] ?? false,
|
|
103
104
|
};
|
|
104
105
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { authClient } from "../clients/Auth";
|
|
2
|
+
|
|
3
|
+
import type { StorefrontResourceName } from "../utils/constants";
|
|
4
|
+
|
|
5
|
+
export type HasStorefrontResourceServiceProps = {
|
|
6
|
+
cookie: string;
|
|
7
|
+
resourceName: StorefrontResourceName;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function isGranted(value: unknown): boolean {
|
|
11
|
+
return value === true || value === "true";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const hasStorefrontResourceService = async ({
|
|
15
|
+
cookie,
|
|
16
|
+
resourceName,
|
|
17
|
+
}: HasStorefrontResourceServiceProps): Promise<boolean> => {
|
|
18
|
+
try {
|
|
19
|
+
const result = await authClient.hasStorefrontResource(resourceName, {
|
|
20
|
+
cookie,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return isGranted(result);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -14,6 +14,10 @@ export {
|
|
|
14
14
|
validateOrderEntryAccessService,
|
|
15
15
|
type ValidateOrderEntryAccessServiceProps,
|
|
16
16
|
} from "./validate-order-entry-access.service";
|
|
17
|
+
export {
|
|
18
|
+
hasStorefrontResourceService,
|
|
19
|
+
type HasStorefrontResourceServiceProps,
|
|
20
|
+
} from "./has-storefront-resource.service";
|
|
17
21
|
export * from "./logger";
|
|
18
22
|
export {
|
|
19
23
|
getDependenciesVersionService,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { isAddressEditingRestricted } from "../isAddressEditingRestricted";
|
|
4
|
+
|
|
5
|
+
describe("isAddressEditingRestricted", () => {
|
|
6
|
+
it("is false when the flag is off", () => {
|
|
7
|
+
expect(
|
|
8
|
+
isAddressEditingRestricted({
|
|
9
|
+
enableAddressRestriction: false,
|
|
10
|
+
canManageAddresses: false,
|
|
11
|
+
})
|
|
12
|
+
).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("is false when the flag is on and the user can manage addresses", () => {
|
|
16
|
+
expect(
|
|
17
|
+
isAddressEditingRestricted({
|
|
18
|
+
enableAddressRestriction: true,
|
|
19
|
+
canManageAddresses: true,
|
|
20
|
+
})
|
|
21
|
+
).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("is true when the flag is on and the user cannot manage addresses", () => {
|
|
25
|
+
expect(
|
|
26
|
+
isAddressEditingRestricted({
|
|
27
|
+
enableAddressRestriction: true,
|
|
28
|
+
canManageAddresses: false,
|
|
29
|
+
})
|
|
30
|
+
).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("fails closed when the flag is on and the permission is unknown", () => {
|
|
34
|
+
expect(
|
|
35
|
+
isAddressEditingRestricted({
|
|
36
|
+
enableAddressRestriction: true,
|
|
37
|
+
})
|
|
38
|
+
).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("is false when both values are omitted", () => {
|
|
42
|
+
expect(isAddressEditingRestricted({})).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -5,6 +5,8 @@ import { withAuthLoader } from "../withAuthLoader";
|
|
|
5
5
|
import type { LoaderData } from "../../types";
|
|
6
6
|
|
|
7
7
|
const validateAccessServiceMock = vi.fn();
|
|
8
|
+
const hasStorefrontResourceServiceMock = vi.fn();
|
|
9
|
+
const getBuyerPortalFeatureFlagsMock = vi.fn().mockResolvedValue({});
|
|
8
10
|
|
|
9
11
|
vi.mock("../../services", () => ({
|
|
10
12
|
validateAccessService: (...args: unknown[]) =>
|
|
@@ -12,7 +14,13 @@ vi.mock("../../services", () => ({
|
|
|
12
14
|
}));
|
|
13
15
|
|
|
14
16
|
vi.mock("../../services/feature-flags/get-feature-flags.service", () => ({
|
|
15
|
-
getBuyerPortalFeatureFlags:
|
|
17
|
+
getBuyerPortalFeatureFlags: (...args: unknown[]) =>
|
|
18
|
+
getBuyerPortalFeatureFlagsMock(...args),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../../services/has-storefront-resource.service", () => ({
|
|
22
|
+
hasStorefrontResourceService: (...args: unknown[]) =>
|
|
23
|
+
hasStorefrontResourceServiceMock(...args),
|
|
16
24
|
}));
|
|
17
25
|
|
|
18
26
|
vi.mock("../getClientContext", () => ({
|
|
@@ -35,6 +43,9 @@ const createLoaderData = (): LoaderData => ({
|
|
|
35
43
|
describe("withAuthLoader", () => {
|
|
36
44
|
beforeEach(() => {
|
|
37
45
|
validateAccessServiceMock.mockReset();
|
|
46
|
+
hasStorefrontResourceServiceMock.mockReset();
|
|
47
|
+
getBuyerPortalFeatureFlagsMock.mockReset();
|
|
48
|
+
getBuyerPortalFeatureFlagsMock.mockResolvedValue({});
|
|
38
49
|
});
|
|
39
50
|
|
|
40
51
|
it("uses the custom validateAccess option instead of the default when granted", async () => {
|
|
@@ -81,4 +92,41 @@ describe("withAuthLoader", () => {
|
|
|
81
92
|
});
|
|
82
93
|
expect(result.authorized).toBe(true);
|
|
83
94
|
});
|
|
95
|
+
|
|
96
|
+
it("does not call the permission API when address restriction is off", async () => {
|
|
97
|
+
validateAccessServiceMock.mockResolvedValue(true);
|
|
98
|
+
getBuyerPortalFeatureFlagsMock.mockResolvedValue({
|
|
99
|
+
enableAddressRestriction: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await withAuthLoader(createLoaderData(), async () => ({ context: {} }));
|
|
103
|
+
|
|
104
|
+
expect(hasStorefrontResourceServiceMock).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("calls the permission API and merges the result when address restriction is on", async () => {
|
|
108
|
+
validateAccessServiceMock.mockResolvedValue(true);
|
|
109
|
+
getBuyerPortalFeatureFlagsMock.mockResolvedValue({
|
|
110
|
+
enableAddressRestriction: true,
|
|
111
|
+
});
|
|
112
|
+
hasStorefrontResourceServiceMock.mockResolvedValue(false);
|
|
113
|
+
|
|
114
|
+
const result = await withAuthLoader(createLoaderData(), async () => ({
|
|
115
|
+
context: {},
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
expect(hasStorefrontResourceServiceMock).toHaveBeenCalledWith({
|
|
119
|
+
cookie: "cookie-value",
|
|
120
|
+
resourceName: "manage-addresses",
|
|
121
|
+
});
|
|
122
|
+
expect(result).toMatchObject({
|
|
123
|
+
authorized: true,
|
|
124
|
+
data: {
|
|
125
|
+
context: {
|
|
126
|
+
featureFlags: { enableAddressRestriction: true },
|
|
127
|
+
permissions: { canManageAddresses: false },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
});
|
|
84
132
|
});
|
|
@@ -4,7 +4,7 @@ export const API_URL = (checkoutUrl: string, operation?: string) =>
|
|
|
4
4
|
`${checkoutUrl}/_v/store-front/${operation}`;
|
|
5
5
|
// DEV URL - CHANGE BEFORE MERGE
|
|
6
6
|
// export const API_URL = (checkoutUrl?: string, operation?: string) =>
|
|
7
|
-
// `https://
|
|
7
|
+
// `https://nicholasb2b--b2bfaststoreqa.myvtex.com/_v/store-front/${operation}`;
|
|
8
8
|
|
|
9
9
|
export const DEBOUNCE_TIMEOUT = 500;
|
|
10
10
|
|
|
@@ -25,4 +25,12 @@ export const SCOPE_KEYS = {
|
|
|
25
25
|
CREDIT_CARDS: "creditCards",
|
|
26
26
|
} as const;
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/** Public names for GET /_v/store-front/permissions/granted/:resourceName */
|
|
29
|
+
export const STOREFRONT_RESOURCE_NAMES = {
|
|
30
|
+
MANAGE_ADDRESSES: "manage-addresses",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export type StorefrontResourceName =
|
|
34
|
+
(typeof STOREFRONT_RESOURCE_NAMES)[keyof typeof STOREFRONT_RESOURCE_NAMES];
|
|
35
|
+
|
|
36
|
+
export const CURRENT_VERSION = "2.0.30";
|
|
@@ -13,7 +13,11 @@ export {
|
|
|
13
13
|
DEBOUNCE_TIMEOUT,
|
|
14
14
|
DEFAULT_PAGE_SIZE,
|
|
15
15
|
SCOPE_KEYS,
|
|
16
|
+
STOREFRONT_RESOURCE_NAMES,
|
|
17
|
+
type StorefrontResourceName,
|
|
16
18
|
} from "./constants";
|
|
19
|
+
export { isAddressEditingRestricted } from "./isAddressEditingRestricted";
|
|
20
|
+
export type { AddressRestrictionInput } from "./isAddressEditingRestricted";
|
|
17
21
|
export {
|
|
18
22
|
getAuthCookie,
|
|
19
23
|
getCookieAsString,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type AddressRestrictionInput = {
|
|
2
|
+
enableAddressRestriction?: boolean;
|
|
3
|
+
canManageAddresses?: boolean;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const isAddressEditingRestricted = ({
|
|
7
|
+
enableAddressRestriction,
|
|
8
|
+
canManageAddresses,
|
|
9
|
+
}: AddressRestrictionInput): boolean =>
|
|
10
|
+
enableAddressRestriction === true && canManageAddresses !== true;
|
|
@@ -2,7 +2,9 @@ import { LOCALIZATION_ENTRY_IDS } from "../localization/constants";
|
|
|
2
2
|
import { getLocalizationLabelsService } from "../localization/services/get-localization-labels.service";
|
|
3
3
|
import { validateAccessService } from "../services";
|
|
4
4
|
import { getBuyerPortalFeatureFlags } from "../services/feature-flags/get-feature-flags.service";
|
|
5
|
+
import { hasStorefrontResourceService } from "../services/has-storefront-resource.service";
|
|
5
6
|
|
|
7
|
+
import { STOREFRONT_RESOURCE_NAMES } from "./constants";
|
|
6
8
|
import { ClientContext, getClientContext } from "./getClientContext";
|
|
7
9
|
|
|
8
10
|
import type { FeatureFlags } from "../components/BuyerPortalProvider/BuyerPortalProvider";
|
|
@@ -46,10 +48,23 @@ export const withAuthLoader = async <T>(
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
const featureFlagsPromise = getBuyerPortalFeatureFlags(cookie);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const canManageAddressesPromise = featureFlagsPromise.then(
|
|
52
|
+
async (flags) => {
|
|
53
|
+
if (!flags.enableAddressRestriction) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return hasStorefrontResourceService({
|
|
58
|
+
cookie,
|
|
59
|
+
resourceName: STOREFRONT_RESOURCE_NAMES.MANAGE_ADDRESSES,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const [pageData, featureFlags, canManageAddresses] = await Promise.all([
|
|
51
65
|
dataLoader({ cookie, userId, ...clientContext, featureFlagsPromise }),
|
|
52
66
|
featureFlagsPromise,
|
|
67
|
+
canManageAddressesPromise,
|
|
53
68
|
]);
|
|
54
69
|
|
|
55
70
|
const mergedPageData = (() => {
|
|
@@ -67,9 +82,12 @@ export const withAuthLoader = async <T>(
|
|
|
67
82
|
const currentFeatureFlags =
|
|
68
83
|
(currentContext as { featureFlags?: Record<string, unknown> })
|
|
69
84
|
.featureFlags ?? {};
|
|
85
|
+
const currentPermissions =
|
|
86
|
+
(currentContext as { permissions?: Record<string, unknown> })
|
|
87
|
+
.permissions ?? {};
|
|
70
88
|
|
|
71
89
|
return {
|
|
72
|
-
...(pageData as Record<string, unknown>),
|
|
90
|
+
...(pageData as unknown as Record<string, unknown>),
|
|
73
91
|
context: {
|
|
74
92
|
...currentContext,
|
|
75
93
|
featureFlags: {
|
|
@@ -78,8 +96,14 @@ export const withAuthLoader = async <T>(
|
|
|
78
96
|
? currentFeatureFlags
|
|
79
97
|
: {}),
|
|
80
98
|
},
|
|
99
|
+
permissions: {
|
|
100
|
+
canManageAddresses,
|
|
101
|
+
...(typeof currentPermissions === "object"
|
|
102
|
+
? currentPermissions
|
|
103
|
+
: {}),
|
|
104
|
+
},
|
|
81
105
|
},
|
|
82
|
-
} as T;
|
|
106
|
+
} as unknown as T;
|
|
83
107
|
})();
|
|
84
108
|
|
|
85
109
|
return {
|