@vtex/faststore-plugin-buyer-portal 2.0.19 → 2.0.21
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/CHANGELOG.md +14 -1
- package/docs/i18n/locales.json +39 -0
- package/docs/i18n/migration-inventory.md +78 -0
- package/package.json +1 -1
- package/specs/analytics-actor-type.md +243 -0
- package/src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx +28 -15
- package/src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx +15 -12
- package/src/features/contracts/components/ContractListItem/ContractListItem.tsx +6 -4
- package/src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx +5 -1
- package/src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx +5 -2
- package/src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx +4 -1
- package/src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx +5 -2
- package/src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx +4 -1
- package/src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx +12 -15
- package/src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx +11 -8
- package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx +27 -20
- package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +28 -26
- package/src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx +9 -6
- package/src/features/shared/hooks/analytics/useAnalytics.ts +1 -1
- package/src/features/shared/services/logger/analytics/__tests__/deriveActorType.test.ts +30 -0
- package/src/features/shared/services/logger/analytics/__tests__/mergeActorTypeIntoEvent.test.ts +83 -0
- package/src/features/shared/services/logger/analytics/analytics.ts +2 -24
- package/src/features/shared/services/logger/analytics/deriveActorType.ts +24 -0
- package/src/features/shared/services/logger/analytics/mergeActorTypeIntoEvent.ts +29 -0
- package/src/features/shared/services/logger/analytics/types.ts +5 -0
- package/src/features/shared/services/logger/analytics/useDataIngestionApi.ts +32 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx +9 -10
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
|
+
## [2.0.21] - 2026-08-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Replaced hardcoded, user-facing strings in the `contracts` feature (multi-contract/SUMA drawers, list, selection bar, listing/information layouts) and in `RolesMultiSelect` with `useLocalization()`/`t()` calls, adding the corresponding keys to the CMS message catalog
|
|
14
|
+
|
|
15
|
+
## [2.0.20] - 2026-08-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Analytics events now include a derived `metadata.actor_type` (`internal` | `external`) based on the logged-in user's email domain (`@vtex.com` → internal), with no email or identity fields in the payload ([B2BTEAM-3639](https://vtex-dev.atlassian.net/browse/B2BTEAM-3639))
|
|
20
|
+
|
|
10
21
|
## [2.0.19] - 2026-08-04
|
|
11
22
|
|
|
12
23
|
### Changed
|
|
@@ -821,7 +832,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
821
832
|
- Add CHANGELOG file
|
|
822
833
|
- Add README file
|
|
823
834
|
|
|
824
|
-
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.
|
|
835
|
+
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.21...HEAD
|
|
825
836
|
[1.3.55]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...v1.3.55
|
|
826
837
|
[1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
|
|
827
838
|
[1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
|
|
@@ -919,6 +930,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
919
930
|
[2.0.10]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.9...v2.0.10
|
|
920
931
|
[2.0.9]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/2.0.9
|
|
921
932
|
|
|
933
|
+
[2.0.21]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.20...v2.0.21
|
|
934
|
+
[2.0.20]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.19...v2.0.20
|
|
922
935
|
[2.0.19]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.18...v2.0.19
|
|
923
936
|
[2.0.18]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.17...v2.0.18
|
|
924
937
|
[2.0.17]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.16...v2.0.17
|
package/docs/i18n/locales.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"shared.buttons.create": "Create",
|
|
15
15
|
"shared.buttons.tryAgain": "Try again",
|
|
16
16
|
"shared.buttons.loadMore": "Load More",
|
|
17
|
+
"shared.buttons.gotIt": "Got it",
|
|
17
18
|
"shared.labels.loading": "Loading...",
|
|
18
19
|
"shared.labels.name": "Name",
|
|
19
20
|
"shared.labels.email": "Email",
|
|
@@ -377,6 +378,8 @@
|
|
|
377
378
|
"users.dropdown.resetPassword": "Reset password",
|
|
378
379
|
"users.dropdown.changeOrganizationalUnit": "Change organizational unit",
|
|
379
380
|
"users.components.UsersCard.manageUsers": "Manage users",
|
|
381
|
+
"users.rolesMultiSelect.customTag": "Custom",
|
|
382
|
+
"users.rolesMultiSelect.noMatchingRoles": "No matching roles",
|
|
380
383
|
"budgets.titles.budgets": "Budgets",
|
|
381
384
|
"budgets.titles.financeAndCompliance": "Finance and Compliance",
|
|
382
385
|
"budgets.labels.remainingBalance": "Remaining balance",
|
|
@@ -692,6 +695,42 @@
|
|
|
692
695
|
"contracts.actions.viewContracts": "View contracts",
|
|
693
696
|
"contracts.dropdown.open": "Open",
|
|
694
697
|
"contracts.dropdown.edit": "Edit",
|
|
698
|
+
"contracts.labels.contract": "contract",
|
|
699
|
+
"contracts.labels.contracts": "contracts",
|
|
700
|
+
"contracts.labels.selectAllCount": "Select all ({0})",
|
|
701
|
+
"contracts.labels.selectedCount": "{0} selected",
|
|
702
|
+
"contracts.actions.setAsDefault": "Set as default",
|
|
703
|
+
"contracts.addDrawer.title": "Add contracts",
|
|
704
|
+
"contracts.addDrawer.description": "Add available contracts to {0}",
|
|
705
|
+
"contracts.addDrawer.paginationRange": "{0}–{1} of {2}",
|
|
706
|
+
"contracts.addDrawer.loading": "Loading contracts…",
|
|
707
|
+
"contracts.addDrawer.noSearchResults": "No contracts match your search.",
|
|
708
|
+
"contracts.addDrawer.noneAvailable": "No contracts available to add.",
|
|
709
|
+
"contracts.removeDrawer.title": "Remove {0}",
|
|
710
|
+
"contracts.removeDrawer.question": "Are you sure you want to remove {0} {1} from {2}?",
|
|
711
|
+
"contracts.removeDrawer.warning": "Removed contracts can be added again later, but any specific configurations for these contracts within this organizational unit will be lost. This will prevent users in this organization unit from accessing these contracts during store navigation and checkout.",
|
|
712
|
+
"contracts.removeDrawer.unableTitle": "Unable to remove contracts",
|
|
713
|
+
"contracts.removeDrawer.error": "An error occurred while removing the contract. Please try again.",
|
|
714
|
+
"contracts.removeDrawer.minimumContractWarning": "Every organizational unit must have at least one contract available. This ensures that users within {0} can always access store features like navigation and checkout.",
|
|
715
|
+
"contracts.removeDrawer.deselectHint": "To proceed, deselect at least one contract to keep it in this organizational unit.",
|
|
716
|
+
"contracts.removeDrawer.unableDeselectHint": "To proceed, deselect at least one contract to keep it in this {0}. When an organizational unit has only one contract, it is automatically set as the default.",
|
|
717
|
+
"contracts.listing.emptyState": "No contracts found for this organizational unit.",
|
|
718
|
+
"contracts.listing.interval": "{0} — {1} of {2}",
|
|
719
|
+
"contracts.listing.infoTooltip": "Contracts define the commercial rules applied during purchasing, such as addresses, payment methods, credit cards and other checkout settings.",
|
|
720
|
+
"contracts.nav.basicInformation": "Basic information",
|
|
721
|
+
"contracts.nav.assortment": "Assortment",
|
|
722
|
+
"contracts.nav.costCenters": "Cost centers",
|
|
723
|
+
"contracts.nav.departments": "Departments",
|
|
724
|
+
"contracts.nav.poNumbers": "PO numbers",
|
|
725
|
+
"contracts.details.createdDate": "Created date",
|
|
726
|
+
"contracts.toasts.addError": "Failed to add contracts. Please try again.",
|
|
727
|
+
"contracts.toasts.switchError": "Failed to switch contract. Please try again.",
|
|
728
|
+
"contracts.toasts.setDefaultSuccess": "Default contract updated successfully",
|
|
729
|
+
"contracts.toasts.setDefaultError": "Failed to set default contract. Please try again.",
|
|
730
|
+
"contracts.toasts.removeSuccess": "{0} contract{1} removed successfully",
|
|
731
|
+
"contracts.toasts.removeError": "Failed to remove contracts. Please try again.",
|
|
732
|
+
"contracts.toasts.refreshError": "Contracts removed, but we couldn't refresh your active contract. Please reload the page.",
|
|
733
|
+
"contracts.toasts.loadError": "Failed to load contract information.",
|
|
695
734
|
"profile.titles.profile": "Profile",
|
|
696
735
|
"profile.titles.details": "Details",
|
|
697
736
|
"profile.labels.creationDate": "Creation Date",
|
|
@@ -487,6 +487,9 @@ Strings defined in `src/features/shared/` — canonical source for all `shared.*
|
|
|
487
487
|
| `shared.buttons.delete` | "Delete" | `src/features/users/components/UserDropdownMenu/UserDropdownMenu.tsx` | 100 |
|
|
488
488
|
| `users.components.UsersCard.manageUsers` | "Manage users" | `src/features/users/components/UsersCard/UsersCard.tsx` | 32 |
|
|
489
489
|
| `users.titles.users` | "Users" | `src/features/users/components/UsersCard/UsersCard.tsx` | 35 |
|
|
490
|
+
| `users.forms.roles` | "Roles" | `src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx` | 244 |
|
|
491
|
+
| `users.rolesMultiSelect.customTag` | "Custom" | `src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx` | 302 |
|
|
492
|
+
| `users.rolesMultiSelect.noMatchingRoles` | "No matching roles" | `src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx` | 258 |
|
|
490
493
|
|
|
491
494
|
---
|
|
492
495
|
|
|
@@ -914,6 +917,81 @@ Strings defined in `src/features/shared/` — canonical source for all `shared.*
|
|
|
914
917
|
| `contracts.actions.viewContracts` | "View contracts" | `src/features/contracts/components/ContractsCard/ContractsCard.tsx` | 36 |
|
|
915
918
|
| `contracts.dropdown.open` | "Open" | `src/features/contracts/components/ContractsCard/ContractsCard.tsx` | 81 |
|
|
916
919
|
| `contracts.dropdown.edit` | "Edit" | `src/features/contracts/components/ContractsCard/ContractsCard.tsx` | 90 |
|
|
920
|
+
| `contracts.toasts.addError` | "Failed to add contracts. Please try again." | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 53 |
|
|
921
|
+
| `contracts.addDrawer.title` | "Add contracts" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 145 |
|
|
922
|
+
| `contracts.addDrawer.description` | "Add available contracts to {0}" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 151 |
|
|
923
|
+
| `shared.placeholders.search` | "Search" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 161 |
|
|
924
|
+
| `contracts.addDrawer.paginationRange` | "{0}–{1} of {2}" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 190 |
|
|
925
|
+
| `contracts.addDrawer.loading` | "Loading contracts…" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 218 |
|
|
926
|
+
| `contracts.addDrawer.noSearchResults` | "No contracts match your search." | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 224 |
|
|
927
|
+
| `contracts.addDrawer.noneAvailable` | "No contracts available to add." | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 225 |
|
|
928
|
+
| `contracts.labels.selectAllCount` | "Select all ({0})" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 251 |
|
|
929
|
+
| `contracts.labels.selectedCount` | "{0} selected" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 299 |
|
|
930
|
+
| `shared.buttons.cancel` | "Cancel" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 309 |
|
|
931
|
+
| `shared.buttons.add` | "Add" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 318 |
|
|
932
|
+
| `contracts.labels.contract` | "contract" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 17 |
|
|
933
|
+
| `contracts.labels.contracts` | "contracts" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 17 |
|
|
934
|
+
| `contracts.removeDrawer.title` | "Remove {0}" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 22 |
|
|
935
|
+
| `contracts.removeDrawer.question` | "Are you sure you want to remove {0} {1} from {2}?" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 27 |
|
|
936
|
+
| `contracts.removeDrawer.warning` | "Removed contracts can be added again later, but any specific configurations for these contracts within this organizational unit will be lost..." | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 31 |
|
|
937
|
+
| `shared.buttons.cancel` | "Cancel" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 44 |
|
|
938
|
+
| `shared.buttons.remove` | "Remove" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 53 |
|
|
939
|
+
| `contracts.toasts.switchError` | "Failed to switch contract. Please try again." | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 42 |
|
|
940
|
+
| `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 95 |
|
|
941
|
+
| `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 100 |
|
|
942
|
+
| `shared.buttons.remove` | "Remove" | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 109 |
|
|
943
|
+
| `contracts.labels.selectAllCount` | "Select all ({0})" | `src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx` | 34 |
|
|
944
|
+
| `contracts.labels.selectedCount` | "{0} selected" | `src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx` | 32 |
|
|
945
|
+
| `shared.buttons.remove` | "Remove" | `src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx` | 42 |
|
|
946
|
+
| `contracts.listing.emptyState` | "No contracts found for this organizational unit." | `src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx` | 27 |
|
|
947
|
+
| `layouts.tabs.contract` | "Contract" | `src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx` | 29 |
|
|
948
|
+
| `layouts.titles.profile` | "Profile" | `src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx` | 32 |
|
|
949
|
+
| `contracts.listing.interval` | "{0} — {1} of {2}" | `src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx` | 35 |
|
|
950
|
+
| `contracts.removeDrawer.unableTitle` | "Unable to remove contracts" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 16 |
|
|
951
|
+
| `contracts.removeDrawer.error` | "An error occurred while removing the contract. Please try again." | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 22 |
|
|
952
|
+
| `contracts.removeDrawer.minimumContractWarning` | "Every organizational unit must have at least one contract available..." | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 27 |
|
|
953
|
+
| `contracts.removeDrawer.deselectHint` | "To proceed, deselect at least one contract to keep it in this organizational unit." | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 33 |
|
|
954
|
+
| `shared.buttons.cancel` | "Cancel" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 47 |
|
|
955
|
+
| `shared.buttons.tryAgain` | "Try again" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 54 |
|
|
956
|
+
| `shared.buttons.gotIt` | "Got it" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 63 |
|
|
957
|
+
| `contracts.removeDrawer.unableTitle` | "Unable to remove contracts" | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 23 |
|
|
958
|
+
| `contracts.removeDrawer.minimumContractWarning` | "Every organizational unit must have at least one contract available..." | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 31 |
|
|
959
|
+
| `contracts.removeDrawer.unableDeselectHint` | "To proceed, deselect at least one contract to keep it in this {0}..." | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 36 |
|
|
960
|
+
| `shared.buttons.gotIt` | "Got it" | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 47 |
|
|
961
|
+
| `contracts.nav.basicInformation` | "Basic information" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 46 |
|
|
962
|
+
| `layouts.navigation.addresses` | "Addresses" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 52 |
|
|
963
|
+
| `layouts.navigation.paymentMethods` | "Payment methods" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 56 |
|
|
964
|
+
| `layouts.navigation.creditCards` | "Credit cards" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 61 |
|
|
965
|
+
| `contracts.nav.assortment` | "Assortment" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 66 |
|
|
966
|
+
| `layouts.navigation.accountingFields` | "Accounting fields" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 72 |
|
|
967
|
+
| `contracts.nav.costCenters` | "Cost centers" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 82 |
|
|
968
|
+
| `contracts.nav.departments` | "Departments" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 90 |
|
|
969
|
+
| `contracts.nav.poNumbers` | "PO numbers" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 100 |
|
|
970
|
+
| `contracts.toasts.setDefaultSuccess` | "Default contract updated successfully" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 168 |
|
|
971
|
+
| `contracts.toasts.setDefaultError` | "Failed to set default contract. Please try again." | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 182 |
|
|
972
|
+
| `layouts.navigation.contracts` | "Contracts" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 217 |
|
|
973
|
+
| `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 235 |
|
|
974
|
+
| `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 243 |
|
|
975
|
+
| `shared.buttons.remove` | "Remove" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 251 |
|
|
976
|
+
| `contracts.nav.basicInformation` | "Basic information" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 267 |
|
|
977
|
+
| `shared.labels.name` | "Name" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 271 |
|
|
978
|
+
| `shared.labels.email` | "Email" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 277 |
|
|
979
|
+
| `contracts.details.createdDate` | "Created date" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 283 |
|
|
980
|
+
| `contracts.listing.infoTooltip` | "Contracts define the commercial rules applied during purchasing..." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 59 |
|
|
981
|
+
| `contracts.toasts.refreshError` | "Contracts removed, but we couldn't refresh your active contract. Please reload the page." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 149 |
|
|
982
|
+
| `contracts.toasts.removeSuccess` | "{0} contract{1} removed successfully" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 162 |
|
|
983
|
+
| `contracts.toasts.removeError` | "Failed to remove contracts. Please try again." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 178 |
|
|
984
|
+
| `contracts.toasts.setDefaultSuccess` | "Default contract updated successfully" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 209 |
|
|
985
|
+
| `contracts.toasts.setDefaultError` | "Failed to set default contract. Please try again." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 224 |
|
|
986
|
+
| `contracts.toasts.loadError` | "Failed to load contract information." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 246 |
|
|
987
|
+
| `contracts.titles.contracts` | "Contracts" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 328 |
|
|
988
|
+
| `orgUnitDetails.links.manageContractSettings` | "Manage contract settings" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 343 |
|
|
989
|
+
| `contracts.nav.basicInformation` | "Basic information" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 34 |
|
|
990
|
+
| `layouts.navigation.addresses` | "Addresses" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 38 |
|
|
991
|
+
| `layouts.navigation.paymentMethods` | "Payment methods" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 42 |
|
|
992
|
+
| `layouts.navigation.creditCards` | "Credit cards" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 46 |
|
|
993
|
+
| `contracts.nav.assortment` | "Assortment" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 50 |
|
|
994
|
+
| `layouts.navigation.accountingFields` | "Accounting fields" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 54 |
|
|
917
995
|
|
|
918
996
|
---
|
|
919
997
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Analytics Actor Type (Internal vs External)
|
|
2
|
+
|
|
3
|
+
> **Status**: Done
|
|
4
|
+
> **Created**: 2026-07-28
|
|
5
|
+
> **Jira**: [B2BTEAM-3639](https://vtex-dev.atlassian.net/browse/B2BTEAM-3639)
|
|
6
|
+
> **Epic**: [B2BTEAM-3596](https://vtex-dev.atlassian.net/browse/B2BTEAM-3596) — Melhoria 1
|
|
7
|
+
> **Sibling task**: [B2BTEAM-3640](https://vtex-dev.atlassian.net/browse/B2BTEAM-3640) (Redshift column + QuickSight segmentation)
|
|
8
|
+
|
|
9
|
+
## 1. Business Context
|
|
10
|
+
|
|
11
|
+
### Problem Statement
|
|
12
|
+
|
|
13
|
+
Organization Account already emits usage analytics (create/edit/delete, timing, errors) via `useAnalytics` into the VTEX Data Ingestion API, Redshift, and QuickSight. Those numbers mix real customer usage with traffic from VTEX employees testing and supporting accounts.
|
|
14
|
+
|
|
15
|
+
PMs cannot filter “real usage” without a reliable, PII-free signal on each event that says whether the actor was internal VTEX or external.
|
|
16
|
+
|
|
17
|
+
### Goals
|
|
18
|
+
|
|
19
|
+
1. Every Org Account analytics event includes a derived `actor_type` of `internal` or `external`.
|
|
20
|
+
2. Usage metrics can exclude VTEX traffic without dropping error analysis options (filtering stays at the consumer; emission always sends the field).
|
|
21
|
+
3. Zero PII in the analytics envelope: never send email, login, user id, or other identity — only the derived enum.
|
|
22
|
+
|
|
23
|
+
### User Stories
|
|
24
|
+
|
|
25
|
+
#### US-1: Emit actor_type on every analytics event
|
|
26
|
+
|
|
27
|
+
- **Story**: As a PM analyzing Org Account usage, I want every event tagged as internal or external, so I can segment real customer usage from VTEX traffic.
|
|
28
|
+
- **Acceptance Criteria**:
|
|
29
|
+
- **Given** a logged-in user whose email domain is `vtex.com`, **when** any analytics event is emitted through `useAnalytics` / `useDataIngestionApi`, **then** the payload includes `actor_type: "internal"`.
|
|
30
|
+
- **Given** a logged-in user whose email domain is not `vtex.com`, **when** any analytics event is emitted, **then** the payload includes `actor_type: "external"`.
|
|
31
|
+
- **Given** any successful emission path (`trackEvent`, `trackError`, `trackEntityCreated`, `trackEntityEdited`, `trackEntityCreateError`, `trackEntityEditError`), **when** the event is sent, **then** `actor_type` is present on the envelope (not only inside caller-supplied metadata).
|
|
32
|
+
|
|
33
|
+
#### US-2: Never send identity or email in the event
|
|
34
|
+
|
|
35
|
+
- **Story**: As a platform owner, I want the analytics envelope to stay free of PII, so we do not leak user identity into Redshift / QuickSight / Slack summaries.
|
|
36
|
+
- **Acceptance Criteria**:
|
|
37
|
+
- **Given** a user with email `alice@vtex.com`, **when** an event is emitted, **then** the JSON body does not contain that email, login, `userId`, or session `sub`.
|
|
38
|
+
- **Given** derivation needs the email in memory, **when** building the payload, **then** only the enum `actor_type` is attached to the outgoing event.
|
|
39
|
+
|
|
40
|
+
#### US-3: Safe default when identity is unavailable
|
|
41
|
+
|
|
42
|
+
- **Story**: As an engineer, I want a deterministic fallback when email is missing, so every event still carries `actor_type` and the pipeline never breaks.
|
|
43
|
+
- **Acceptance Criteria**:
|
|
44
|
+
- **Given** `currentUser` is null or `email` is empty/missing, **when** an event is emitted, **then** `actor_type` is `"external"`.
|
|
45
|
+
- **Given** derivation throws or email is malformed (no `@`), **when** an event is emitted, **then** `actor_type` is `"external"` and emission still succeeds.
|
|
46
|
+
|
|
47
|
+
### Key Scenarios
|
|
48
|
+
|
|
49
|
+
| Scenario | Pre-conditions | Steps | Expected Result |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| Happy path — VTEX employee | `currentUser.email` is `rodrigo.tavares@vtex.com` | User creates a budget; `trackEntityCreated` runs | Event has `actor_type: "internal"`; no email in payload |
|
|
52
|
+
| Happy path — customer | `currentUser.email` is `buyer@acme.com` | User edits an address; `trackEntityEdited` runs | Event has `actor_type: "external"` |
|
|
53
|
+
| Error case — Users API failed in loader | `currentUser` is `null` (e.g. `getUserByIdService` returned null) | User still triggers an analytics call from a mounted component | Event has `actor_type: "external"`; send does not throw |
|
|
54
|
+
| Edge case — username login, email from API | Session `sub` is `everton.ataidetestdev` (not an email); Users API returns `email: "everton@vtex.com"` | Any event is emitted | Classification uses API email → `internal`; session `sub` is never sent |
|
|
55
|
+
| Edge case — case / plus addressing | Email is `Name+qa@VTEX.COM` | Event emitted | Domain match is case-insensitive → `internal` |
|
|
56
|
+
|
|
57
|
+
### Functional Requirements
|
|
58
|
+
|
|
59
|
+
1. Derive `actor_type` at emission time from the logged-in user's email domain.
|
|
60
|
+
2. Classification rule: domain equals `vtex.com` (case-insensitive) → `internal`; otherwise → `external`.
|
|
61
|
+
3. Attach `actor_type` on every event sent through the shared analytics send path so callers do not need to pass it.
|
|
62
|
+
4. Do not attach email, login, userId, session `sub`, or other identity fields to the event.
|
|
63
|
+
5. Remain backward compatible with the existing schemaless ingestion pipeline (additive field only).
|
|
64
|
+
|
|
65
|
+
### Non-Functional Requirements
|
|
66
|
+
|
|
67
|
+
1. **Privacy**: envelope stays zero-PII for identity; derivation is local/ephemeral.
|
|
68
|
+
2. **Reliability**: missing user context must not block or fail analytics emission.
|
|
69
|
+
3. **Performance**: derivation is O(1) string work; no extra network calls at emit time.
|
|
70
|
+
4. **Testability**: pure domain classifier is unit-tested; send path asserts field presence and absence of PII.
|
|
71
|
+
|
|
72
|
+
### Out of Scope
|
|
73
|
+
|
|
74
|
+
- Exposing `actor_type` as a Redshift column or QuickSight filter (sibling [B2BTEAM-3640](https://vtex-dev.atlassian.net/browse/B2BTEAM-3640)).
|
|
75
|
+
- Melhoria 2 (`error_type` / stripping raw `error_message` / `error_stack`).
|
|
76
|
+
- Rewriting the telemetry pipeline, DAG, or Slack weekly summary.
|
|
77
|
+
- Classifying by session `sub` alone, account name, IP, or workspace.
|
|
78
|
+
- Matching sibling domains (e.g. `vtex.com.br`) or subdomains (e.g. `corp.vtex.com`) — only exact `vtex.com`.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 2. Arch Decisions
|
|
83
|
+
|
|
84
|
+
### Proposed Solution
|
|
85
|
+
|
|
86
|
+
Enrich the shared analytics send path so every event automatically receives `actor_type`, derived from `currentUser.email` already available in `BuyerPortalContext`.
|
|
87
|
+
|
|
88
|
+
**Identity source (important):** the auth session JWT does **not** carry email. Example session claims:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"sub": "everton.ataidetestdev",
|
|
93
|
+
"userId": "dd801513-ae2f-4ff8-b6de-dd3a2159a895",
|
|
94
|
+
"customerId": "…",
|
|
95
|
+
"unitId": "…",
|
|
96
|
+
"account": "b2bfaststoredev",
|
|
97
|
+
"type": "user"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`sub` is a login/username, not an email. Email is loaded server-side in page loaders via `getUserByIdService` → Users API (`usersClient.getUserById`), then passed into `BuyerPortalProvider` as `currentUser.email`. Classification must use that email, never the session `sub`.
|
|
102
|
+
|
|
103
|
+
```mermaid
|
|
104
|
+
flowchart LR
|
|
105
|
+
Cookie["Auth cookie JWT\nuserId, sub, …"] --> Loader["Page loader\nwithAuthLoader"]
|
|
106
|
+
Loader --> UsersAPI["getUserByIdService\nUsers API"]
|
|
107
|
+
UsersAPI --> Ctx["BuyerPortalContext\ncurrentUser.email"]
|
|
108
|
+
Ctx --> Derive["deriveActorType(email)\ninternal | external"]
|
|
109
|
+
Derive --> Send["useDataIngestionApi.sendEvent\n+ actor_type"]
|
|
110
|
+
Send --> Ingest["VTEX schemaless-events"]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Architecture Overview
|
|
114
|
+
|
|
115
|
+
| Layer | Responsibility |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `deriveActorType(email?)` | Pure helper: parse domain, return `internal` \| `external` |
|
|
118
|
+
| `useDataIngestionApi` | Read `currentUser?.email` from `BuyerPortalContext` (optional context, like `useLogger`); merge `actor_type` into every `sendEvent` payload at top level alongside `account`, `locale`, `device`, `production` |
|
|
119
|
+
| `useAnalytics` | Unchanged call sites; inherits enrichment via `sendEvent` |
|
|
120
|
+
| Callers | No changes required to pass `actor_type` |
|
|
121
|
+
|
|
122
|
+
### Alternatives Considered
|
|
123
|
+
|
|
124
|
+
| Alternative | Pros | Cons | Verdict |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| Derive in each feature caller | Explicit per event | Easy to miss; duplicates logic | Rejected |
|
|
127
|
+
| Derive only inside `useAnalytics.withDefaults` | Covers most UI tracking | Anything calling `useDataIngestionApi` / `dataIngestionApi` directly could skip it; harder to guarantee “every event” | Rejected as sole approach |
|
|
128
|
+
| Classify from session `sub` | No dependency on Users API | `sub` is often not an email (e.g. `everton.ataidetestdev`); false `external` for VTEX staff | Rejected |
|
|
129
|
+
| Put `actor_type` only inside `metadata` | Minimal type change | Sibling Redshift task wants a first-class column; top-level matches existing common fields | Rejected |
|
|
130
|
+
| Centralize in `useDataIngestionApi` + pure helper | Single choke point; additive; testable | Hook must tolerate missing React context | **Accepted** |
|
|
131
|
+
|
|
132
|
+
### Risks & Mitigations
|
|
133
|
+
|
|
134
|
+
| Risk | Impact | Likelihood | Mitigation |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| `currentUser` null when analytics fires | Med (VTEX traffic mislabeled as external) | Med | Default `external`; ensure loaders keep loading user where events fire; document gap |
|
|
137
|
+
| VTEX staff using non-`@vtex.com` email | Med (internal traffic still pollutes usage) | Low | Document rule; extend domain list only via explicit follow-up |
|
|
138
|
+
| Customer email on `vtex.com` (unlikely) | Low | Low | Accept false `internal`; rule is domain-based by product decision |
|
|
139
|
+
| Accidental PII leak via spread of user object | High | Low | Unit tests asserting outbound payload keys; helper never returns email |
|
|
140
|
+
|
|
141
|
+
### Key Decisions
|
|
142
|
+
|
|
143
|
+
#### Decision 1: Classification rule = exact `@vtex.com` domain
|
|
144
|
+
|
|
145
|
+
- **Status**: Accepted
|
|
146
|
+
- **Context**: Need a simple, privacy-safe heuristic for “VTEX employee”.
|
|
147
|
+
- **Decision**: `internal` iff the email’s domain equals `vtex.com` (case-insensitive). Plus-tags (`user+tag@vtex.com`) still match. Domains like `vtex.com.br` or `*.vtex.com` are **not** internal in this iteration.
|
|
148
|
+
- **Consequences**: Simple to implement and document; may miss regional VTEX domains until explicitly expanded.
|
|
149
|
+
|
|
150
|
+
#### Decision 2: Fallback = `external` when email unavailable
|
|
151
|
+
|
|
152
|
+
- **Status**: Accepted
|
|
153
|
+
- **Context**: Ticket enum is only `internal \| external`; every event must carry the field.
|
|
154
|
+
- **Decision**: Missing/invalid email → `external`.
|
|
155
|
+
- **Consequences**: Some VTEX traffic without a loaded user profile remains in “external” usage until context is present; safer than inventing a third enum value for Melhoria 1.
|
|
156
|
+
|
|
157
|
+
#### Decision 3: Identity source = `currentUser.email` from BuyerPortalContext
|
|
158
|
+
|
|
159
|
+
- **Status**: Accepted
|
|
160
|
+
- **Context**: Session JWT has `userId` / `sub` but not email; email already exists on `UserData` after loader fetch.
|
|
161
|
+
- **Decision**: Read `currentUser?.email` from context at send time. Do not decode the auth cookie on the client for classification. Do not use `login` / `sub` unless it is later proven to be an email and product asks for it — out of scope for v1 (email-only).
|
|
162
|
+
- **Consequences**: Correct for the common path where pages load the user; no extra network at emit time.
|
|
163
|
+
|
|
164
|
+
#### Decision 4: Place `actor_type` at top level of the event envelope
|
|
165
|
+
|
|
166
|
+
- **Status**: Accepted
|
|
167
|
+
- **Context**: Sibling task needs a Redshift column; common context fields (`account`, `locale`, `device`, `production`) are already top-level.
|
|
168
|
+
- **Decision**: Set `actor_type` as a top-level field on the object passed to `dataIngestionApi`, not buried only under `metadata`.
|
|
169
|
+
- **Consequences**: Easier DAG mapping; additive and backward compatible for schemaless ingestion.
|
|
170
|
+
|
|
171
|
+
### Implementation Plan
|
|
172
|
+
|
|
173
|
+
1. Add `ActorType` type and pure `deriveActorType(email?: string | null): ActorType` under shared analytics (or shared utils) with unit tests.
|
|
174
|
+
2. Extend `DataIngestionApiFields` / send path types with optional/required `actor_type`.
|
|
175
|
+
3. Update `useDataIngestionApi` to read `BuyerPortalContext` (gracefully if undefined) and always set `actor_type` on `sendEvent`.
|
|
176
|
+
4. Add tests that mock context email and assert outbound payload contains `actor_type` and does not contain email/login/userId.
|
|
177
|
+
5. Manual smoke on a host store: one `@vtex.com` user and one external user; confirm events in the analytics pipeline / network tab.
|
|
178
|
+
6. Hand off to [B2BTEAM-3640](https://vtex-dev.atlassian.net/browse/B2BTEAM-3640) for Redshift + QuickSight.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 3. Technical Contract
|
|
183
|
+
|
|
184
|
+
### Data Models
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
type ActorType = "internal" | "external";
|
|
188
|
+
|
|
189
|
+
/** Pure classifier — never logs or returns the input email. */
|
|
190
|
+
function deriveActorType(email?: string | null): ActorType;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Rules for `deriveActorType`:**
|
|
194
|
+
|
|
195
|
+
1. Trim and lowercase the email.
|
|
196
|
+
2. If empty or no `@`, return `"external"`.
|
|
197
|
+
3. Take the substring after the last `@` as the domain.
|
|
198
|
+
4. If domain === `"vtex.com"`, return `"internal"`; else `"external"`.
|
|
199
|
+
|
|
200
|
+
**Event envelope (additive):**
|
|
201
|
+
|
|
202
|
+
| Field | Type | Required on emit | Notes |
|
|
203
|
+
|---|---|---|---|
|
|
204
|
+
| `actor_type` | `"internal" \| "external"` | Yes | Derived; top-level |
|
|
205
|
+
| `email` / `login` / `userId` / `sub` | — | **Must not appear** | PII / identity ban |
|
|
206
|
+
|
|
207
|
+
Existing fields (`name`, `account`, `locale`, `production`, `device`, `event_name`, `event_category`, `metadata`, …) unchanged.
|
|
208
|
+
|
|
209
|
+
### Interfaces
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// analytics types — additive
|
|
213
|
+
interface DataIngestionApiFields {
|
|
214
|
+
// …existing fields…
|
|
215
|
+
actor_type?: ActorType; // set by send path; callers need not pass
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// useDataIngestionApi sendEvent behavior
|
|
219
|
+
sendEvent(fields: DataIngestionApiFields): void
|
|
220
|
+
// always merges: { ...commonFields, ...fields, actor_type: deriveActorType(currentUser?.email) }
|
|
221
|
+
// caller-supplied actor_type must not override the derived value (server of truth = derivation)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`useAnalytics` public API stays the same; no new required options on `UseAnalyticsConfig`.
|
|
225
|
+
|
|
226
|
+
### Integration Points
|
|
227
|
+
|
|
228
|
+
| System | Interaction |
|
|
229
|
+
|---|---|
|
|
230
|
+
| Auth cookie / session JWT | Provides `userId` only (indirectly); **not** used for domain classification |
|
|
231
|
+
| Users API via `getUserByIdService` | Source of `currentUser.email` in loaders |
|
|
232
|
+
| `BuyerPortalContext` | Runtime source of email for derivation in the client send hook |
|
|
233
|
+
| VTEX Data Ingestion (`schemaless-events`) | Receives additive `actor_type`; no schema migration in this task |
|
|
234
|
+
| Redshift / QuickSight | Consumer of the field in sibling task B2BTEAM-3640 |
|
|
235
|
+
|
|
236
|
+
### Invariants & Constraints
|
|
237
|
+
|
|
238
|
+
1. Every event leaving `useDataIngestionApi.sendEvent` includes `actor_type`.
|
|
239
|
+
2. Outgoing analytics JSON must never include email, login, userId, or session `sub` as a result of this feature.
|
|
240
|
+
3. Derivation does not perform network I/O.
|
|
241
|
+
4. Callers cannot force a conflicting `actor_type` that bypasses derivation (derived value wins).
|
|
242
|
+
5. Domains other than exact `vtex.com` are always `external` in this version.
|
|
243
|
+
6. This feature does not change event names, categories, or existing metadata shapes beyond the additive field.
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Icon,
|
|
9
9
|
LetterHighlight,
|
|
10
10
|
} from "../../../shared/components";
|
|
11
|
+
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
11
12
|
import { useAddContracts, useListAvailableContracts } from "../../hooks";
|
|
12
13
|
import {
|
|
13
14
|
buildRangeSet,
|
|
@@ -30,6 +31,7 @@ export const AddContractsDrawer = ({
|
|
|
30
31
|
onSuccess,
|
|
31
32
|
...drawerProps
|
|
32
33
|
}: AddContractsDrawerProps) => {
|
|
34
|
+
const { t } = useLocalization();
|
|
33
35
|
const { pushToast } = useUI();
|
|
34
36
|
|
|
35
37
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
@@ -50,7 +52,7 @@ export const AddContractsDrawer = ({
|
|
|
50
52
|
onError: () => {
|
|
51
53
|
drawerProps.onDismiss?.();
|
|
52
54
|
pushToast({
|
|
53
|
-
message: "
|
|
55
|
+
message: t("contracts.toasts.addError"),
|
|
54
56
|
status: "ERROR",
|
|
55
57
|
});
|
|
56
58
|
},
|
|
@@ -142,23 +144,24 @@ export const AddContractsDrawer = ({
|
|
|
142
144
|
return (
|
|
143
145
|
<BasicDrawer size="large" {...drawerProps}>
|
|
144
146
|
<BasicDrawer.Heading
|
|
145
|
-
title="
|
|
147
|
+
title={t("contracts.addDrawer.title")}
|
|
146
148
|
onClose={isAddContractsLoading ? undefined : drawerProps.onDismiss}
|
|
147
149
|
/>
|
|
148
150
|
|
|
149
151
|
<BasicDrawer.Body data-fs-bp-add-contracts-drawer-body>
|
|
150
152
|
<p data-fs-bp-add-contracts-drawer-description>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
{t("contracts.addDrawer.description", [
|
|
154
|
+
<strong data-fs-bp-add-contracts-drawer-unit-name key="orgUnitName">
|
|
155
|
+
{orgUnitName}
|
|
156
|
+
</strong>,
|
|
157
|
+
])}
|
|
155
158
|
</p>
|
|
156
159
|
|
|
157
160
|
<div data-fs-bp-add-contracts-drawer-toolbar>
|
|
158
161
|
<div data-fs-bp-add-contracts-drawer-search>
|
|
159
162
|
<input
|
|
160
163
|
type="text"
|
|
161
|
-
placeholder="
|
|
164
|
+
placeholder={t("shared.placeholders.search")}
|
|
162
165
|
value={searchQuery}
|
|
163
166
|
onChange={handleSearchChange}
|
|
164
167
|
disabled={isDisabled}
|
|
@@ -187,7 +190,11 @@ export const AddContractsDrawer = ({
|
|
|
187
190
|
aria-label="Pagination"
|
|
188
191
|
>
|
|
189
192
|
<span data-fs-bp-add-contracts-drawer-pagination-range>
|
|
190
|
-
{
|
|
193
|
+
{t("contracts.addDrawer.paginationRange", [
|
|
194
|
+
rangeStart,
|
|
195
|
+
rangeEnd,
|
|
196
|
+
filteredContracts.length,
|
|
197
|
+
])}
|
|
191
198
|
</span>
|
|
192
199
|
<button
|
|
193
200
|
type="button"
|
|
@@ -215,14 +222,14 @@ export const AddContractsDrawer = ({
|
|
|
215
222
|
|
|
216
223
|
{availableContractsLoading ? (
|
|
217
224
|
<div data-fs-bp-add-contracts-drawer-loading aria-busy="true">
|
|
218
|
-
|
|
225
|
+
{t("contracts.addDrawer.loading")}
|
|
219
226
|
</div>
|
|
220
227
|
) : filteredContracts.length === 0 ? (
|
|
221
228
|
<div data-fs-bp-add-contracts-drawer-empty>
|
|
222
229
|
<p>
|
|
223
230
|
{searchQuery
|
|
224
|
-
? "
|
|
225
|
-
: "
|
|
231
|
+
? t("contracts.addDrawer.noSearchResults")
|
|
232
|
+
: t("contracts.addDrawer.noneAvailable")}
|
|
226
233
|
</p>
|
|
227
234
|
</div>
|
|
228
235
|
) : (
|
|
@@ -248,7 +255,11 @@ export const AddContractsDrawer = ({
|
|
|
248
255
|
<Icon name="Check" width={12} height={12} />
|
|
249
256
|
)}
|
|
250
257
|
</div>
|
|
251
|
-
<span>
|
|
258
|
+
<span>
|
|
259
|
+
{t("contracts.labels.selectAllCount", [
|
|
260
|
+
filteredContracts.length,
|
|
261
|
+
])}
|
|
262
|
+
</span>
|
|
252
263
|
</div>
|
|
253
264
|
|
|
254
265
|
{displayedContracts.map((contract) => {
|
|
@@ -296,7 +307,9 @@ export const AddContractsDrawer = ({
|
|
|
296
307
|
>
|
|
297
308
|
<Icon name="X" width={14} height={14} />
|
|
298
309
|
</button>
|
|
299
|
-
<span>
|
|
310
|
+
<span>
|
|
311
|
+
{t("contracts.labels.selectedCount", [selectedIds.size])}
|
|
312
|
+
</span>
|
|
300
313
|
</>
|
|
301
314
|
)}
|
|
302
315
|
</div>
|
|
@@ -306,7 +319,7 @@ export const AddContractsDrawer = ({
|
|
|
306
319
|
type="button"
|
|
307
320
|
disabled={isDisabled}
|
|
308
321
|
>
|
|
309
|
-
|
|
322
|
+
{t("shared.buttons.cancel")}
|
|
310
323
|
</BasicDrawer.Button>
|
|
311
324
|
<BasicDrawer.Button
|
|
312
325
|
variant="confirm"
|
|
@@ -315,7 +328,7 @@ export const AddContractsDrawer = ({
|
|
|
315
328
|
disabled={selectedIds.size === 0 || isDisabled}
|
|
316
329
|
isLoading={isAddContractsLoading}
|
|
317
330
|
>
|
|
318
|
-
|
|
331
|
+
{t("shared.buttons.add")}
|
|
319
332
|
</BasicDrawer.Button>
|
|
320
333
|
</BasicDrawer.Footer>
|
|
321
334
|
</BasicDrawer>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BasicDrawer, type BasicDrawerProps } from "../../../shared/components";
|
|
2
|
+
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
2
3
|
|
|
3
4
|
export type ConfirmRemoveDrawerProps = Omit<BasicDrawerProps, "children"> & {
|
|
4
5
|
orgUnitName: string;
|
|
@@ -14,25 +15,27 @@ export const ConfirmRemoveDrawer = ({
|
|
|
14
15
|
isLoading = false,
|
|
15
16
|
...drawerProps
|
|
16
17
|
}: ConfirmRemoveDrawerProps) => {
|
|
17
|
-
const
|
|
18
|
+
const { t } = useLocalization();
|
|
19
|
+
const label =
|
|
20
|
+
count === 1
|
|
21
|
+
? t("contracts.labels.contract")
|
|
22
|
+
: t("contracts.labels.contracts");
|
|
18
23
|
|
|
19
24
|
return (
|
|
20
25
|
<BasicDrawer {...drawerProps}>
|
|
21
26
|
<BasicDrawer.Heading
|
|
22
|
-
title={
|
|
27
|
+
title={t("contracts.removeDrawer.title", [label])}
|
|
23
28
|
onClose={drawerProps.onDismiss}
|
|
24
29
|
/>
|
|
25
30
|
<BasicDrawer.Body data-fs-bp-confirm-remove-drawer-body>
|
|
26
31
|
<p>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
configurations for these contracts within this organizational unit
|
|
33
|
-
will be lost. This will prevent users in this organization unit from
|
|
34
|
-
accessing these contracts during store navigation and checkout.
|
|
32
|
+
{t("contracts.removeDrawer.question", [
|
|
33
|
+
<strong key="count">{count}</strong>,
|
|
34
|
+
label,
|
|
35
|
+
<strong key="orgUnitName">{orgUnitName}</strong>,
|
|
36
|
+
])}
|
|
35
37
|
</p>
|
|
38
|
+
<p>{t("contracts.removeDrawer.warning")}</p>
|
|
36
39
|
</BasicDrawer.Body>
|
|
37
40
|
<BasicDrawer.Footer>
|
|
38
41
|
<BasicDrawer.Button
|
|
@@ -41,7 +44,7 @@ export const ConfirmRemoveDrawer = ({
|
|
|
41
44
|
type="button"
|
|
42
45
|
disabled={isLoading}
|
|
43
46
|
>
|
|
44
|
-
|
|
47
|
+
{t("shared.buttons.cancel")}
|
|
45
48
|
</BasicDrawer.Button>
|
|
46
49
|
<BasicDrawer.Button
|
|
47
50
|
variant="danger"
|
|
@@ -50,7 +53,7 @@ export const ConfirmRemoveDrawer = ({
|
|
|
50
53
|
isLoading={isLoading}
|
|
51
54
|
disabled={isLoading}
|
|
52
55
|
>
|
|
53
|
-
|
|
56
|
+
{t("shared.buttons.remove")}
|
|
54
57
|
</BasicDrawer.Button>
|
|
55
58
|
</BasicDrawer.Footer>
|
|
56
59
|
</BasicDrawer>
|