@vtex/faststore-plugin-buyer-portal 1.3.48 → 1.3.50

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.
Files changed (20) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/package.json +1 -1
  3. package/src/features/budgets/components/BudgetEditNotificationDrawer/BudgetEditNotificationDrawer.tsx +1 -0
  4. package/src/features/budgets/components/BudgetNotificationForm/BudgetNotificationForm.tsx +64 -26
  5. package/src/features/budgets/components/BudgetNotificationForm/budget-notification-form.scss +26 -15
  6. package/src/features/budgets/components/BudgetUsersTable/BudgetUsersTable.tsx +2 -13
  7. package/src/features/budgets/components/CreateBudgetDrawer/create-budget-drawer.scss +0 -9
  8. package/src/features/budgets/components/EditBudgetDrawer/edit-budget-drawer.scss +0 -6
  9. package/src/features/buying-policies/components/BasicBuyingPolicyDrawer/BasicBuyingPolicyDrawer.tsx +43 -0
  10. package/src/features/buying-policies/components/BasicBuyingPolicyDrawer/basic-buying-policy-drawer.scss +1 -1
  11. package/src/features/buying-policies/components/CustomFieldCriteriaSelector/CustomFieldCriteriaSelector.tsx +154 -0
  12. package/src/features/buying-policies/components/CustomFieldCriteriaSelector/custom-field-criteria-selector.scss +139 -0
  13. package/src/features/buying-policies/utils/index.ts +2 -0
  14. package/src/features/buying-policies/utils/orderFieldsCriteriaOptions.ts +6 -4
  15. package/src/features/shared/components/AutocompleteDropdown/AutocompleteDropdown.tsx +23 -6
  16. package/src/features/shared/components/AutocompleteDropdown/autocomplete-dropdown.scss +21 -7
  17. package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +1 -0
  18. package/src/features/shared/components/QuantitySelectorWithPercentage/QuantitySelectorWithPercentage.tsx +6 -3
  19. package/src/features/shared/components/Table/TableRow/table-row.scss +4 -0
  20. package/src/features/shared/utils/constants.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.50] - 2025-12-19
11
+
12
+ ### Changed
13
+ - Introduces several improvements and refactorings to the budget notification drawer, focusing on user experience.
14
+
15
+ ## [1.3.49] - 2025-12-19
16
+
17
+ ### Added
18
+
19
+ - Add component of Criteria Selection of Custom Fields on Buying Policies
20
+
10
21
  ## [1.3.48] - 2025-12-19
11
22
 
12
23
  - Adjustment from merge to Collections to Products Assortment
@@ -411,7 +422,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
411
422
  - Add CHANGELOG file
412
423
  - Add README file
413
424
 
414
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.48...HEAD
425
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.50...HEAD
415
426
  [1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.2.2...1.2.3
416
427
  [1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.3
417
428
  [1.2.4]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.4
@@ -461,6 +472,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
461
472
  [1.3.36]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.35...v1.3.36
462
473
  [1.3.35]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.35
463
474
 
475
+ [1.3.50]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.49...v1.3.50
476
+ [1.3.49]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.48...v1.3.49
464
477
  [1.3.48]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.47...v1.3.48
465
478
  [1.3.47]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.46...v1.3.47
466
479
  [1.3.46]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.45...v1.3.46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.3.48",
3
+ "version": "1.3.50",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -104,6 +104,7 @@ export function BudgetEditNotificationDrawer({
104
104
  <BasicDrawer
105
105
  data-fs-bp-budget-notifications-drawer
106
106
  close={close}
107
+ size="large"
107
108
  {...props}
108
109
  >
109
110
  <BasicDrawer.Heading title="Edit notifications" onClose={close} />
@@ -7,6 +7,7 @@ import {
7
7
  QuantitySelectorWithPercentage,
8
8
  Table,
9
9
  } from "../../../shared/components";
10
+ import { useBuyerPortal } from "../../../shared/hooks";
10
11
  import { CurrencyType, LocaleType } from "../../../shared/types";
11
12
  import { useDebouncedSearchBudgetNotification } from "../../hooks/useDebouncedSearchBudgetNotification";
12
13
  import BudgetUsersTable from "../BudgetUsersTable/BudgetUsersTable";
@@ -36,6 +37,8 @@ type BudgetNotificationFormProps = {
36
37
 
37
38
  type UserOption = { id: string; name: string; email?: string };
38
39
 
40
+ const MAX_THRESHOLDS = 5;
41
+
39
42
  export const BudgetNotificationForm = ({
40
43
  budget,
41
44
  contractId,
@@ -47,6 +50,8 @@ export const BudgetNotificationForm = ({
47
50
  locale = "en-US",
48
51
  showUsersError = false,
49
52
  }: BudgetNotificationFormProps) => {
53
+ const { currentUser } = useBuyerPortal();
54
+
50
55
  const [enabled, setEnabled] = useState<boolean>(
51
56
  Boolean(budget?.notifications?.hasNotification)
52
57
  );
@@ -124,7 +129,7 @@ export const BudgetNotificationForm = ({
124
129
  const addThreshold = () => {
125
130
  if (readonly || parsedTotal <= 0) return;
126
131
  setThresholds((prev) =>
127
- prev.length >= 5 ? prev : [...prev, { value: 50 }]
132
+ prev.length >= MAX_THRESHOLDS ? prev : [...prev, { value: 50 }]
128
133
  );
129
134
  };
130
135
 
@@ -154,8 +159,20 @@ export const BudgetNotificationForm = ({
154
159
  name: String(u.name ?? "Usuário"),
155
160
  email: u.email,
156
161
  }));
157
- setOptions(mapped);
158
- }, [usersResponse]);
162
+
163
+ setOptions(
164
+ currentUser?.id
165
+ ? [
166
+ {
167
+ id: String(currentUser.id),
168
+ email: currentUser?.email ?? "",
169
+ name: currentUser?.name ? `${currentUser.name} (You)` : "You",
170
+ },
171
+ ...mapped,
172
+ ]
173
+ : mapped
174
+ );
175
+ }, [usersResponse, currentUser]);
159
176
 
160
177
  const addUserIfNotExists = (opt: UserOption) => {
161
178
  const exists = selectedUsers.some((u) => u.userId === opt.id);
@@ -178,8 +195,21 @@ export const BudgetNotificationForm = ({
178
195
  setSelectedUsers(next);
179
196
  };
180
197
 
198
+ const handleThresholdsChange = (value: number, idx: number) => {
199
+ setThresholds((prev) =>
200
+ prev.map((th, i) =>
201
+ i === idx
202
+ ? {
203
+ value: Math.min(100, Math.max(1, value)),
204
+ }
205
+ : th
206
+ )
207
+ );
208
+ };
209
+
181
210
  const sizeProps = { width: 20, height: 20 };
182
211
  const disableRows = readonly || !enabled;
212
+ const isLoading = isLoadingUsers || isDebouncing;
183
213
 
184
214
  return (
185
215
  <div data-fs-bp-budget-notification>
@@ -200,8 +230,8 @@ export const BudgetNotificationForm = ({
200
230
 
201
231
  <div data-fs-bp-budget-notification-section-label>
202
232
  <span>
203
- Set up to 5 thresholds and notify users when the total amount reaches
204
- specific percentages
233
+ Set up to {MAX_THRESHOLDS} thresholds and notify users when the total
234
+ amount reaches specific percentages
205
235
  </span>
206
236
  </div>
207
237
 
@@ -240,37 +270,35 @@ export const BudgetNotificationForm = ({
240
270
  title={
241
271
  <div data-fs-bp-budget-notification-stepper>
242
272
  <QuantitySelectorWithPercentage
243
- min={1}
273
+ min={5}
244
274
  max={100}
245
275
  initial={t.value}
246
276
  formatAsPercent
247
277
  disabled={disableRows || parsedTotal <= 0}
248
- onChange={(val: number) =>
249
- setThresholds((prev) =>
250
- prev.map((th, i) =>
251
- i === idx
252
- ? {
253
- value: Math.min(
254
- 100,
255
- Math.max(1, val)
256
- ),
257
- }
258
- : th
259
- )
260
- )
278
+ onChange={(value) =>
279
+ handleThresholdsChange(value, idx)
261
280
  }
281
+ increaseQuantityBy={5}
282
+ decreaseQuantityBy={5}
262
283
  />
263
284
  </div>
264
285
  }
265
286
  enabled={!disableRows}
266
287
  actionIcons={
267
- <Icon
268
- name="MinusCircle"
269
- {...sizeProps}
270
- data-fs-bp-budget-notification-remove
288
+ <button
289
+ data-fs-bp-budget-notification-remove-button
290
+ type="button"
271
291
  onClick={() => removeThreshold(idx)}
272
292
  aria-label="Remove threshold"
273
- />
293
+ title="Remove threshold"
294
+ disabled={disableRows}
295
+ >
296
+ <Icon
297
+ name="MinusCircle"
298
+ {...sizeProps}
299
+ data-fs-bp-budget-notification-remove
300
+ />
301
+ </button>
274
302
  }
275
303
  >
276
304
  <Table.Cell>
@@ -294,7 +322,11 @@ export const BudgetNotificationForm = ({
294
322
  <button
295
323
  type="button"
296
324
  onClick={addThreshold}
297
- disabled={disableRows || thresholds.length >= 5}
325
+ disabled={
326
+ disableRows ||
327
+ parsedTotal <= 0 ||
328
+ thresholds.length >= MAX_THRESHOLDS
329
+ }
298
330
  >
299
331
  Add threshold
300
332
  </button>
@@ -315,11 +347,17 @@ export const BudgetNotificationForm = ({
315
347
  shouldOpenOnFocus={false}
316
348
  autoComplete="off"
317
349
  shouldShowArrowDown={false}
350
+ isLoading={isLoading}
351
+ disabled={isLoading}
318
352
  onChange={(e: ChangeEvent<HTMLInputElement>) => {
319
353
  setInputValue(e.target.value);
320
354
  }}
321
355
  options={
322
- isLoadingUsers || isDebouncing ? [loadingObject] : options
356
+ isLoading
357
+ ? [loadingObject]
358
+ : options.filter(
359
+ (opt) => !selectedUsers.some((u) => u.userId === opt.id)
360
+ )
323
361
  }
324
362
  hasError={showUsersError && enabled && selectedUsers.length === 0}
325
363
  helperLabel={
@@ -1,11 +1,11 @@
1
1
  [data-fs-bp-budget-notification] {
2
- @import "@faststore/ui/src/components/molecules/Toggle/styles.scss";
3
- @import "@faststore/ui/src/components/molecules/Tooltip/styles.scss";
4
- @import "@faststore/ui/src/components/molecules/QuantitySelector/styles.scss";
5
- @import "../../../shared/components/InputText/input-text.scss";
6
- @import "../../../shared/components/Table/table.scss";
7
- @import "../../../shared/components/ErrorMessage/error-message.scss";
8
- @import "../BudgetUsersTable/budget-users-table.scss";
2
+ @import '@faststore/ui/src/components/molecules/Toggle/styles.scss';
3
+ @import '@faststore/ui/src/components/molecules/Tooltip/styles.scss';
4
+ @import '@faststore/ui/src/components/molecules/QuantitySelector/styles.scss';
5
+ @import '../../../shared/components/InputText/input-text.scss';
6
+ @import '../../../shared/components/Table/table.scss';
7
+ @import '../../../shared/components/ErrorMessage/error-message.scss';
8
+ @import '../BudgetUsersTable/budget-users-table.scss';
9
9
 
10
10
  padding-bottom: var(--fs-spacing-7);
11
11
 
@@ -29,7 +29,7 @@
29
29
  [data-fs-bp-budget-notification-section-label] {
30
30
  font-weight: var(--fs-text-weight-regular);
31
31
  font-size: var(--fs-text-size-1);
32
- color: #5c5c5c;
32
+ color: var(--fs-bp-color-neutral-7);
33
33
  line-height: calc(var(--fs-spacing-3) + var(--fs-spacing-0));
34
34
  }
35
35
 
@@ -38,7 +38,7 @@
38
38
  flex-direction: column;
39
39
  width: 100%;
40
40
 
41
- @include media(">=notebook") {
41
+ @include media('>=notebook') {
42
42
  align-items: center;
43
43
  flex-direction: row;
44
44
  gap: var(--fs-spacing-3);
@@ -62,9 +62,9 @@
62
62
  }
63
63
  }
64
64
 
65
- [data-fs-bp-autocomplete-dropdown]{
66
- [data-fs-bp-autocomplete-dropdown-menu]{
67
- z-index:2;
65
+ [data-fs-bp-autocomplete-dropdown] {
66
+ [data-fs-bp-autocomplete-dropdown-menu] {
67
+ z-index: 2;
68
68
  }
69
69
  }
70
70
 
@@ -99,7 +99,7 @@
99
99
  text-align: start;
100
100
  font-weight: var(--fs-text-weight-medium);
101
101
  font-size: var(--fs-text-size-1);
102
- color: #5c5c5c;
102
+ color: var(--fs-bp-color-neutral-7);
103
103
  padding: var(--fs-spacing-2) 0;
104
104
  }
105
105
 
@@ -165,7 +165,7 @@
165
165
  }
166
166
  }
167
167
 
168
- div[data-fs-quantity-selector="disabled"] {
168
+ div[data-fs-quantity-selector='disabled'] {
169
169
  [data-fs-input] {
170
170
  color: #c3c3c3;
171
171
  }
@@ -185,18 +185,23 @@
185
185
  [data-fs-bp-budget-notification-money] {
186
186
  font-weight: var(--fs-text-weight-regular);
187
187
  font-size: var(--fs-text-size-1);
188
- color: #5C5C5C;
188
+ color: var(--fs-bp-color-neutral-7);
189
189
  }
190
190
 
191
191
  [data-fs-bp-budget-notification-remove] {
192
192
  color: #000000;
193
193
  stroke-width: 1.3rem;
194
+ cursor: pointer;
194
195
  &[disabled] {
195
196
  color: #9ca3af; /* gray when disabled */
196
197
  cursor: not-allowed;
197
198
  }
198
199
  }
199
200
 
201
+ [data-fs-bp-budget-notification-remove-button] {
202
+ cursor: pointer;
203
+ }
204
+
200
205
  [data-fs-bp-budget-notification-add] {
201
206
  margin-top: var(--fs-spacing-3);
202
207
  display: flex;
@@ -204,6 +209,7 @@
204
209
  gap: var(--fs-spacing-2);
205
210
 
206
211
  button {
212
+ cursor: pointer;
207
213
  height: var(--fs-spacing-6);
208
214
  width: 8.4375rem;
209
215
  border-radius: var(--fs-border-radius-pill);
@@ -214,6 +220,11 @@
214
220
  color: #0068d7;
215
221
  font-size: var(--fs-text-size-1);
216
222
  font-weight: var(--fs-text-weight-semibold);
223
+
224
+ &:disabled {
225
+ cursor: not-allowed;
226
+ color: #9ca3af; /* gray when disabled */
227
+ }
217
228
  }
218
229
  }
219
230
  }
@@ -4,7 +4,6 @@ import { Icon, Tooltip } from "@faststore/ui";
4
4
 
5
5
  import { Table } from "../../../shared/components";
6
6
  import { getTableColumns } from "../../../shared/components/Table/utils/tableColumns";
7
- import { useBuyerPortal } from "../../../shared/hooks";
8
7
 
9
8
  type UserRow = {
10
9
  userId: string;
@@ -28,8 +27,6 @@ function BudgetUsersTableBase({
28
27
  className,
29
28
  disabled = false,
30
29
  }: BudgetUsersTableProps) {
31
- const { clientContext } = useBuyerPortal();
32
- const currClientId = clientContext.userId;
33
30
  const columns = getTableColumns({
34
31
  actionsLength: onRemove ? 1 : 0,
35
32
  nameColumnLabel: "User name",
@@ -64,16 +61,7 @@ function BudgetUsersTableBase({
64
61
  users.map((u) => (
65
62
  <Table.Row
66
63
  key={u.userId}
67
- title={
68
- currClientId === u.userId ? (
69
- <span>
70
- {u.userName}
71
- <span data-fs-bp-users-you> (you)</span>
72
- </span>
73
- ) : (
74
- u.userName
75
- )
76
- }
64
+ title={u.userName}
77
65
  enabled={!disabled}
78
66
  actionIcons={(() => {
79
67
  const rowDisabled =
@@ -85,6 +73,7 @@ function BudgetUsersTableBase({
85
73
  aria-label={`Remove ${u.userName}`}
86
74
  disabled={rowDisabled}
87
75
  onClick={() => onRemove?.(u.userId)}
76
+ title={rowDisabled ? undefined : `Remove ${u.userName}`}
88
77
  >
89
78
  <Icon name="MinusCircle" width={20} height={20} />
90
79
  </button>
@@ -21,15 +21,6 @@
21
21
  width: 100%;
22
22
  max-width: none;
23
23
 
24
- @include media(">=tablet") {
25
- max-width: 30rem;
26
- min-width: 30rem;
27
- }
28
-
29
- @include media(">=notebook") {
30
- max-width: 40rem;
31
- }
32
-
33
24
  [data-fs-divider] {
34
25
  border: var(--fs-border-radius-small) solid #e0e0e0;
35
26
  margin: var(--fs-spacing-4) 0rem;
@@ -23,12 +23,6 @@
23
23
  }
24
24
  }
25
25
 
26
- @include media(">=notebook") {
27
- &[data-fs-modal-content] {
28
- max-width: 40rem;
29
- width: 100%;
30
- }
31
- }
32
26
  [data-fs-divider] {
33
27
  border: var(--fs-border-radius-small) solid #e0e0e0;
34
28
  margin: var(--fs-spacing-4) 0rem;
@@ -24,12 +24,15 @@ import {
24
24
  BUYING_POLICIES_WORKFLOW_TYPES,
25
25
  buyingPolicyDefault,
26
26
  BUDGET_CRITERIA,
27
+ COST_CENTER_CRITERIA,
28
+ PO_NUMBER_CRITERIA,
27
29
  orderFieldsCriteriaOptions,
28
30
  spendingLimitsCriteriaOptions,
29
31
  } from "../../utils";
30
32
  import { BUYING_POLICIES_WORKFLOW_LABELS } from "../../utils/buyingPoliciesWorkflowTypes";
31
33
  import { getHighlightedText } from "../../utils/criteriaHighlightSyntax";
32
34
  import { BudgetCriteriaSelector } from "../BudgetCriteriaSelector";
35
+ import { CustomFieldCriteriaSelector } from "../CustomFieldCriteriaSelector/CustomFieldCriteriaSelector";
33
36
 
34
37
  const LIMIT_OF_LEVELS = 5;
35
38
 
@@ -173,6 +176,18 @@ export const BasicBuyingPolicyDrawer = ({
173
176
  criteria === BUDGET_CRITERIA ||
174
177
  criteria.includes('$exists(budgetData.budgets[id="');
175
178
 
179
+ const isPONumberCriteria =
180
+ criteria === PO_NUMBER_CRITERIA ||
181
+ criteria.includes(
182
+ 'customData.customFields.fields[name="PO Number"].value ='
183
+ );
184
+
185
+ const isCostCenterCriteria =
186
+ criteria === COST_CENTER_CRITERIA ||
187
+ criteria.includes(
188
+ 'customData.customFields.fields[name="Cost Center"].value ='
189
+ );
190
+
176
191
  const renderCriteria = (criteria: string) => {
177
192
  if (isBudgetCriteria) {
178
193
  return (
@@ -186,6 +201,34 @@ export const BasicBuyingPolicyDrawer = ({
186
201
  );
187
202
  }
188
203
 
204
+ if (isPONumberCriteria) {
205
+ return (
206
+ <CustomFieldCriteriaSelector
207
+ key={"PO Number"}
208
+ fieldName="PO Number"
209
+ currentCriteria={criteria}
210
+ onCriteriaChange={(newCriteria) =>
211
+ updateField("criteria", newCriteria)
212
+ }
213
+ onEditablePartClick={() => setCriteriaFocused(true)}
214
+ />
215
+ );
216
+ }
217
+
218
+ if (isCostCenterCriteria) {
219
+ return (
220
+ <CustomFieldCriteriaSelector
221
+ key={"Cost Center"}
222
+ fieldName="Cost Center"
223
+ currentCriteria={criteria}
224
+ onCriteriaChange={(newCriteria) =>
225
+ updateField("criteria", newCriteria)
226
+ }
227
+ onEditablePartClick={() => setCriteriaFocused(true)}
228
+ />
229
+ );
230
+ }
231
+
189
232
  return getHighlightedText(criteria);
190
233
  };
191
234
 
@@ -2,6 +2,7 @@
2
2
  @import '../../../shared/components/OrgUnitInputSearch/org-unit-input-search.scss';
3
3
  @import '../../../shared/components/CustomDropdown/custom-dropdown.scss';
4
4
  @import '../BudgetCriteriaSelector/budget-criteria-selector.scss';
5
+ @import '../CustomFieldCriteriaSelector/custom-field-criteria-selector.scss';
5
6
 
6
7
  [data-fs-bp-basic-buying-policy-drawer] {
7
8
  @import '../../../shared/components/InputText/input-text.scss';
@@ -108,7 +109,6 @@
108
109
  text-wrap: nowrap;
109
110
  text-overflow: ellipsis;
110
111
  white-space: nowrap;
111
- overflow: hidden;
112
112
  max-width: 840px;
113
113
  cursor: pointer;
114
114
 
@@ -0,0 +1,154 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ import { CustomDropdown } from "../../../shared/components/CustomDropdown/CustomDropdown";
4
+ import { useBuyerPortal } from "../../../shared/hooks";
5
+ import { useCustomFieldValues } from "../../../shared/hooks/custom-field";
6
+
7
+ export type CustomFieldCriteriaSelectorProps = {
8
+ fieldName: "PO Number" | "Cost Center";
9
+ onCriteriaChange: (criteria: string) => void;
10
+ currentCriteria: string;
11
+ onEditablePartClick?: () => void;
12
+ };
13
+
14
+ export const CustomFieldCriteriaSelector = ({
15
+ fieldName,
16
+ onCriteriaChange,
17
+ currentCriteria,
18
+ onEditablePartClick,
19
+ }: CustomFieldCriteriaSelectorProps) => {
20
+ const {
21
+ currentOrgUnit: orgUnit,
22
+ currentContract: contract,
23
+ clientContext,
24
+ } = useBuyerPortal();
25
+
26
+ const cookie = clientContext?.cookie ?? "";
27
+
28
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
29
+
30
+ const { data, isLoading, refetch } = useCustomFieldValues({
31
+ data: {
32
+ contractId: contract?.id || "",
33
+ customField: fieldName,
34
+ unitId: orgUnit?.id || "",
35
+ cookie,
36
+ params: {
37
+ value: "",
38
+ page: 1,
39
+ filterByUnit: true,
40
+ },
41
+ },
42
+ options: { lazy: true },
43
+ });
44
+
45
+ const extractValueFromCriteria = (criteria: string): string => {
46
+ const match = criteria.match(/value\s*=\s*"([^"]+)"/);
47
+ return match
48
+ ? match[1]
49
+ : `${fieldName.toLowerCase().replace(/\s+/g, "-")}-001`;
50
+ };
51
+
52
+ const [currentFieldValue, setCurrentFieldValue] = useState<string>(
53
+ extractValueFromCriteria(currentCriteria)
54
+ );
55
+
56
+ useEffect(() => {
57
+ setIsDropdownOpen(false);
58
+ const extractedValue = extractValueFromCriteria(currentCriteria);
59
+ setCurrentFieldValue(extractedValue);
60
+ }, [currentCriteria, fieldName]);
61
+
62
+ const generateCriteriaString = (value: string) => {
63
+ return `customData.customFields.fields[name="${fieldName}"].value = "${value}"`;
64
+ };
65
+
66
+ const handleFieldSelect = (selectedIdOrValue: string) => {
67
+ if (selectedIdOrValue === "loading") return;
68
+
69
+ const items = data?.data || [];
70
+
71
+ const selectedItem = items.find(
72
+ (item) =>
73
+ item.value === selectedIdOrValue || item.id === selectedIdOrValue
74
+ );
75
+
76
+ if (!selectedItem) return;
77
+
78
+ const valueToUse = selectedItem.value || selectedIdOrValue;
79
+ setCurrentFieldValue(valueToUse);
80
+ onCriteriaChange(generateCriteriaString(valueToUse));
81
+ setIsDropdownOpen(false);
82
+ };
83
+
84
+ const handleFieldValueClick = (e: React.MouseEvent) => {
85
+ e.stopPropagation();
86
+ setIsDropdownOpen(true);
87
+
88
+ if ((!data?.data || data.data.length === 0) && !isLoading) {
89
+ refetch();
90
+ }
91
+ };
92
+
93
+ const handleEditablePartClick = (e: React.MouseEvent) => {
94
+ e.stopPropagation();
95
+ onEditablePartClick?.();
96
+ };
97
+
98
+ const loadingOptions = [
99
+ {
100
+ label: `Loading ${fieldName.toLowerCase()}...`,
101
+ value: "loading",
102
+ },
103
+ ];
104
+
105
+ const fieldOptions = (data?.data || []).map((item) => ({
106
+ label: item.name || item.value || `${fieldName}-${item.id?.slice(0, 8)}`,
107
+ value: item.value || item.id,
108
+ }));
109
+
110
+ const options =
111
+ isLoading || !data?.data || data.data.length === 0
112
+ ? loadingOptions
113
+ : fieldOptions;
114
+
115
+ const triggerLabel = isLoading ? "Loading..." : currentFieldValue;
116
+
117
+ return (
118
+ <div data-fs-bp-custom-field-criteria-selector data-field-name={fieldName}>
119
+ <div data-fs-bp-custom-field-criteria-string>
120
+ <span onClick={handleEditablePartClick} data-fs-bp-editable-part>
121
+ customData.customFields.fields[name="{fieldName}"].value = "
122
+ </span>
123
+
124
+ <div
125
+ onClick={handleFieldValueClick}
126
+ style={{ display: "inline-block" }}
127
+ >
128
+ <CustomDropdown
129
+ options={options}
130
+ onSelect={handleFieldSelect}
131
+ triggerLabel={triggerLabel}
132
+ isOpen={isDropdownOpen}
133
+ highlightText={(text) => (
134
+ <div data-fs-bp-dropdown-option>
135
+ <div data-fs-bp-dropdown-option-label>{text}</div>
136
+ {!isLoading && data?.data && (
137
+ <div data-fs-bp-dropdown-option-uuid>
138
+ {data.data.find(
139
+ (item) => (item.name || item.value) === text
140
+ )?.value || text}
141
+ </div>
142
+ )}
143
+ </div>
144
+ )}
145
+ />
146
+ </div>
147
+
148
+ <span onClick={handleEditablePartClick} data-fs-bp-editable-part>
149
+ "
150
+ </span>
151
+ </div>
152
+ </div>
153
+ );
154
+ };
@@ -0,0 +1,139 @@
1
+ [data-fs-bp-custom-field-criteria-selector] {
2
+ position: absolute;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100%;
6
+ height: 100%;
7
+ margin: 0;
8
+ background-color: var(--fs-bp-color-neutral-1);
9
+ border: var(--fs-border-width) solid var(--fs-bp-color-neutral-4);
10
+ border-radius: calc(var(--fs-border-radius) * 2);
11
+ display: flex;
12
+ flex-direction: column;
13
+ align-items: flex-start;
14
+
15
+ [data-fs-bp-custom-field-criteria-string] {
16
+ font-family: "Roboto", monospace;
17
+ font-size: var(--fs-text-size-1);
18
+ color: var(--fs-bp-color-neutral-7);
19
+ display: flex;
20
+ align-items: center;
21
+ padding: var(--fs-spacing-3) var(--fs-spacing-3);
22
+ gap: 0;
23
+ flex-wrap: wrap;
24
+ width: 100%;
25
+
26
+ > span {
27
+ white-space: nowrap;
28
+ }
29
+
30
+ [data-fs-bp-editable-part] {
31
+ cursor: pointer;
32
+ transition: background-color 0.2s ease;
33
+ padding: 0.125rem 0 0.25rem;
34
+ border-radius: calc(var(--fs-border-radius) * 0.5);
35
+
36
+ &:hover {
37
+ background-color: var(--fs-bp-color-neutral-1);
38
+ color: var(--fs-bp-color-neutral-8);
39
+ }
40
+ }
41
+
42
+ [data-fs-bp-custom-dropdown] {
43
+ display: inline-flex;
44
+
45
+ [data-fs-bp-custom-dropdown-trigger] {
46
+ color: var(--fs-color-highlight);
47
+ border: none;
48
+ min-width: 0;
49
+ border-radius: calc(var(--fs-border-radius) * 1.5);
50
+ font-family: "Roboto", monospace;
51
+ font-size: calc(var(--fs-text-size-1) * 0.9);
52
+ padding: 0;
53
+ cursor: pointer;
54
+ transition: background-color 0.2s ease;
55
+ background-color: transparent;
56
+
57
+ &:hover {
58
+ background-color: var(--fs-bp-color-neutral-1);
59
+ }
60
+ }
61
+
62
+ [data-fs-bp-custom-dropdown-menu] {
63
+ position: absolute;
64
+ top: 100%;
65
+ left: 0;
66
+ margin-top: var(--fs-spacing-1);
67
+ padding: var(--fs-spacing-1) 0;
68
+ background-color: var(--fs-bp-color-neutral-0);
69
+ box-shadow: 0rem 0.5rem 0.625rem 0rem rgba(0, 0, 0, 0.0784313725);
70
+ max-height: 15rem;
71
+ min-width: 20rem;
72
+ overflow-y: auto;
73
+ z-index: 1;
74
+ border-radius: calc(var(--fs-border-radius) * 2);
75
+
76
+ &::-webkit-scrollbar {
77
+ width: 0.75rem;
78
+ height: 0.75rem;
79
+ background-color: var(--fs-bp-color-neutral-0);
80
+ }
81
+
82
+ &::-webkit-scrollbar-thumb {
83
+ background-color: #adadad;
84
+ border: 0.25rem solid transparent;
85
+ border-radius: 0.375rem;
86
+ background-clip: content-box;
87
+ }
88
+
89
+ [data-fs-bp-custom-dropdown-item] {
90
+ padding: var(--fs-spacing-2) var(--fs-spacing-3);
91
+ cursor: pointer;
92
+ border-bottom: var(--fs-border-width) solid #f0f0f0;
93
+
94
+ &:last-child {
95
+ border-bottom: none;
96
+ }
97
+
98
+ &:hover,
99
+ &[data-focused="true"] {
100
+ background-color: var(--fs-bp-color-neutral-2);
101
+ }
102
+
103
+ [data-fs-bp-dropdown-option] {
104
+ font-family: Inter;
105
+
106
+ [data-fs-bp-dropdown-option-label] {
107
+ font-weight: var(--fs-text-weight-semibold);
108
+ color: var(--fs-bp-color-neutral-8);
109
+ font-size: var(--fs-text-size-1);
110
+ margin-bottom: var(--fs-spacing-0);
111
+ }
112
+
113
+ [data-fs-bp-dropdown-option-uuid] {
114
+ font-size: var(--fs-text-size-0);
115
+ color: var(--fs-bp-color-neutral-7);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ [data-fs-bp-custom-value-indicator] {
124
+ font-size: calc(var(--fs-text-size-0) * 0.85);
125
+ color: #666666;
126
+ font-style: italic;
127
+ padding: 0 var(--fs-spacing-3) calc(var(--fs-spacing-1) * 0.5);
128
+ border-top: 0.0625rem solid var(--fs-bp-color-neutral-3);
129
+ margin-top: auto;
130
+ width: 100%;
131
+ background-color: #fafafa;
132
+ border-radius: 0 0 calc(var(--fs-border-radius) * 2)
133
+ calc(var(--fs-border-radius) * 2);
134
+ }
135
+
136
+ [data-fs-bp-custom-field-value] {
137
+ color: var(--fs-color-highlight);
138
+ }
139
+ }
@@ -1,6 +1,8 @@
1
1
  export { buyingPolicyDefault } from "./buyingPolicyDefault";
2
2
  export {
3
3
  BUDGET_CRITERIA,
4
+ PO_NUMBER_CRITERIA,
5
+ COST_CENTER_CRITERIA,
4
6
  orderFieldsCriteriaOptions,
5
7
  } from "./orderFieldsCriteriaOptions";
6
8
  export { spendingLimitsCriteriaOptions } from "./spendingLimitsCriteriaOptions";
@@ -1,4 +1,8 @@
1
1
  export const BUDGET_CRITERIA = '$exists(budgetData.budgets[id="${budget-id}"])';
2
+ export const PO_NUMBER_CRITERIA =
3
+ 'customData.customFields.fields[name="PO Number"].value = "${po-number}"';
4
+ export const COST_CENTER_CRITERIA =
5
+ 'customData.customFields.fields[name="Cost Center"].value = "${cost-center}"';
2
6
 
3
7
  export const orderFieldsCriteriaOptions = [
4
8
  {
@@ -25,8 +29,7 @@ export const orderFieldsCriteriaOptions = [
25
29
  },
26
30
  {
27
31
  label: "If the order contains the PO numbers X, Y, Z",
28
- criteria:
29
- 'customData.customFields.fields[name="PO Number"].value = "1112223334444" or customData.customFields.fields[name="PO Number"].value = "1234567890"',
32
+ criteria: PO_NUMBER_CRITERIA,
30
33
  },
31
34
  {
32
35
  label: "If the order contains a cost center",
@@ -35,8 +38,7 @@ export const orderFieldsCriteriaOptions = [
35
38
  },
36
39
  {
37
40
  label: "If the order contains the cost centers X, Y, Z",
38
- criteria:
39
- 'customData.customFields.fields[name="Cost Center"].value = "CC1" or customData.customFields.fields[name="Cost Center"].value = "CC2"',
41
+ criteria: COST_CENTER_CRITERIA,
40
42
  },
41
43
  {
42
44
  label: "If all order items are negotiated items",
@@ -43,6 +43,7 @@ export type AutocompleteDropdownProps<T> = ComponentProps<typeof InputText> & {
43
43
  renderOption?: (option: T, index: number) => ReactNode;
44
44
  onConfirmKeyPress?: (option: T) => void;
45
45
  hasError?: boolean;
46
+ isLoading?: boolean;
46
47
  shouldOpenOnFocus?: boolean;
47
48
  shouldShowArrowDown?: boolean;
48
49
  shouldCloseOnSelect?: boolean;
@@ -57,6 +58,7 @@ export const AutocompleteDropdown = <T,>({
57
58
  disabled,
58
59
  onConfirmKeyPress,
59
60
  hasError,
61
+ isLoading = false,
60
62
  value,
61
63
  helperLabel,
62
64
  shouldOpenOnFocus = true,
@@ -138,6 +140,25 @@ export const AutocompleteDropdown = <T,>({
138
140
  setIsOpened(false);
139
141
  }, []);
140
142
 
143
+ const getIconElement = () => {
144
+ if (shouldShowArrowDown) {
145
+ return <Icon name="ArrowDropDown" width={20} height={20} />;
146
+ }
147
+
148
+ if (isLoading) {
149
+ return (
150
+ <Icon
151
+ name="LoadingIndicator"
152
+ width={20}
153
+ height={20}
154
+ data-fs-icon-loading="true"
155
+ />
156
+ );
157
+ }
158
+
159
+ return undefined;
160
+ };
161
+
141
162
  useEffect(() => {
142
163
  const event = (e: MouseEvent) => {
143
164
  const target = e.target as Node;
@@ -215,11 +236,7 @@ export const AutocompleteDropdown = <T,>({
215
236
  <InputText
216
237
  label={label}
217
238
  onKeyDown={handleBackdropKeyDown}
218
- icon={
219
- shouldShowArrowDown ? (
220
- <Icon name="ArrowDropDown" width={20} height={20} />
221
- ) : null
222
- }
239
+ icon={getIconElement()}
223
240
  onChange={onChange}
224
241
  disabled={disabled}
225
242
  hasError={hasError}
@@ -229,7 +246,7 @@ export const AutocompleteDropdown = <T,>({
229
246
  {...props}
230
247
  />
231
248
 
232
- {isOpened && options.length > 0 ? (
249
+ {!isLoading && isOpened && options.length > 0 ? (
233
250
  <div
234
251
  style={{ width: positionStyle.width }}
235
252
  data-fs-bp-autocomplete-dropdown-menu={position}
@@ -1,9 +1,9 @@
1
- @import "@faststore/ui/src/components/molecules/Dropdown/styles.scss";
1
+ @import '@faststore/ui/src/components/molecules/Dropdown/styles.scss';
2
2
 
3
3
  [data-fs-bp-autocomplete-dropdown] {
4
4
  position: relative;
5
-
6
- &[data-fs-bp-autocomplete-dropdown-only-select="true"] {
5
+
6
+ &[data-fs-bp-autocomplete-dropdown-only-select='true'] {
7
7
  [data-fs-bp-input-text-input] {
8
8
  caret-color: transparent;
9
9
  background-color: #ffffff;
@@ -11,6 +11,20 @@
11
11
  }
12
12
  }
13
13
 
14
+ [data-fs-icon-loading] {
15
+ &[data-fs-icon-loading='true'] {
16
+ @keyframes rotate {
17
+ from {
18
+ transform: rotate(0deg);
19
+ }
20
+ to {
21
+ transform: rotate(360deg);
22
+ }
23
+ }
24
+ animation: rotate 2s linear infinite;
25
+ }
26
+ }
27
+
14
28
  [data-fs-bp-autocomplete-dropdown-menu] {
15
29
  padding: var(--fs-spacing-1) 0;
16
30
  background-color: #fff;
@@ -22,11 +36,11 @@
22
36
 
23
37
  border-radius: calc(var(--fs-border-radius) * 2);
24
38
 
25
- &[data-fs-bp-autocomplete-dropdown-menu="bottom"] {
39
+ &[data-fs-bp-autocomplete-dropdown-menu='bottom'] {
26
40
  top: calc(100% + var(--fs-spacing-0));
27
41
  }
28
42
 
29
- &[data-fs-bp-autocomplete-dropdown-menu="top"] {
43
+ &[data-fs-bp-autocomplete-dropdown-menu='top'] {
30
44
  bottom: calc(100% + var(--fs-spacing-0));
31
45
  }
32
46
 
@@ -59,12 +73,12 @@
59
73
  color: var(--fs-color-neutral-7);
60
74
  }
61
75
 
62
- &[data-fs-bp-autocomplete-dropdown-option-focused="true"] {
76
+ &[data-fs-bp-autocomplete-dropdown-option-focused='true'] {
63
77
  background-color: #f5f5f5;
64
78
  color: #3d3d3d;
65
79
  }
66
80
 
67
- &[data-fs-bp-autocomplete-dropdown-option-selected="true"] {
81
+ &[data-fs-bp-autocomplete-dropdown-option-selected='true'] {
68
82
  color: #0366dd;
69
83
  }
70
84
 
@@ -19,6 +19,7 @@ export type BuyerPortalContextType = {
19
19
  id: string;
20
20
  name: string;
21
21
  role?: string;
22
+ email?: string;
22
23
  } | null;
23
24
  featureFlags?: FeatureFlags;
24
25
  };
@@ -12,10 +12,11 @@ interface QuantitySelectorWithPercentageParams {
12
12
  disabled?: boolean;
13
13
  onChange?: (value: number) => void;
14
14
  onValidateBlur?: (min: number, maxValue: number, quantity: number) => void;
15
-
16
15
  formatAsPercent?: boolean;
17
16
  allowPercentToggle?: boolean;
18
17
  onFormatToggle?: (formatAsPercent: boolean) => void;
18
+ increaseQuantityBy?: number;
19
+ decreaseQuantityBy?: number;
19
20
  }
20
21
 
21
22
  const QuantitySelectorWithPercentage = ({
@@ -29,6 +30,8 @@ const QuantitySelectorWithPercentage = ({
29
30
  onValidateBlur,
30
31
  testId = "fs-quantity-selector",
31
32
  formatAsPercent = true,
33
+ increaseQuantityBy = 1,
34
+ decreaseQuantityBy = 1,
32
35
  ...otherProps
33
36
  }: QuantitySelectorWithPercentageParams) => {
34
37
  const [quantity, setQuantity] = useState<number>(initial ?? min);
@@ -61,8 +64,8 @@ const QuantitySelectorWithPercentage = ({
61
64
  : maxValue;
62
65
  };
63
66
 
64
- const increase = () => changeQuantity(1);
65
- const decrease = () => changeQuantity(-1);
67
+ const increase = () => changeQuantity(increaseQuantityBy);
68
+ const decrease = () => changeQuantity(-decreaseQuantityBy);
66
69
 
67
70
  const changeQuantity = (delta: number) => {
68
71
  const next = validateQuantityBounds(quantity + delta);
@@ -98,6 +98,10 @@
98
98
  align-items: center;
99
99
  gap: var(--fs-spacing-1);
100
100
  justify-content: flex-end;
101
+
102
+ svg {
103
+ cursor: pointer;
104
+ }
101
105
 
102
106
  &:empty {
103
107
  padding: 0;
@@ -22,4 +22,4 @@ export const SCOPE_KEYS = {
22
22
  CREDIT_CARDS: "creditCards",
23
23
  } as const;
24
24
 
25
- export const CURRENT_VERSION = "1.3.48";
25
+ export const CURRENT_VERSION = "1.3.50";