@vtex/faststore-plugin-buyer-portal 2.0.10 → 2.0.11
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 +15 -1
- package/package.json +1 -1
- package/src/features/addresses/clients/AddressesClient.ts +15 -11
- package/src/features/addresses/components/CreateAddressSettingsDrawer/CreateAddressSettingsDrawer.tsx +1 -0
- package/src/features/addresses/hooks/useDebouncedSearchAddressByUnitId.ts +2 -0
- package/src/features/addresses/hooks/useListAddresses.ts +15 -5
- package/src/features/addresses/layouts/AddressesLayout/AddressesLayout.tsx +1 -0
- package/src/features/addresses/services/{get-addresses-by-unit-id.service.ts → get-attached-addresses-by-customer-id.service.ts} +18 -14
- package/src/features/addresses/services/index.ts +3 -3
- package/src/features/addresses/types/AddressData.ts +3 -1
- package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +39 -5
- package/src/features/contracts/layouts/ContractListingLayout/contract-listing-layout.scss +2 -2
- package/src/features/org-units/layouts/OrgUnitDetailsLayout/OrgUnitDetailsLayout.tsx +4 -1
- package/src/features/shared/clients/Client.ts +1 -1
- package/src/features/shared/hooks/index.ts +1 -0
- package/src/features/shared/hooks/useReloadOnBfcacheRestore.ts +24 -0
- package/src/features/shared/layouts/BaseTabsLayout/SidebarMenu.tsx +8 -8
- package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +4 -1
- package/src/features/shared/layouts/OrgUnitTabsLayout/OrgUnitTabLayout.tsx +4 -1
- package/src/features/shared/layouts/SumaPageLayout/FullSidebarNav.tsx +17 -6
- package/src/features/shared/layouts/SumaPageLayout/SumaPageLayout.tsx +13 -3
- package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +3 -0
- package/src/features/shared/utils/__tests__/resolveContractId.test.ts +47 -0
- package/src/features/shared/utils/buyerPortalRoutes.ts +29 -7
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/shared/utils/getOrganizationSettingsLinks.ts +10 -4
- package/src/features/shared/utils/getSumaListingSidebarLinks.ts +1 -1
- package/src/features/shared/utils/index.ts +1 -0
- package/src/features/shared/utils/resolveContractId.ts +30 -0
- package/src/features/shared/utils/withAuthLoader.ts +11 -3
- package/src/pages/contracts.tsx +28 -26
- package/src/pages/org-units.tsx +20 -1
- package/src/pages/roles.tsx +32 -14
- package/src/pages/users.tsx +41 -18
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.0.11] - 2026-07-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Sidebar links on the Users, Roles, and Organizational Units pages now carry the currently viewed contract's id in the URL, instead of silently falling back to the org unit's first contract — this previously caused the Finance section (Budgets/Buying Policies) links and accounting fields on those pages to point at the wrong contract
|
|
15
|
+
- Removed a redundant `getBuyerPortalFeatureFlags` fetch on the Contract Listing page and a conflicting `no-cache`/`next.revalidate: 300` combination on the shared HTTP client, both of which could intermittently hide the "Add contracts" button (and serve stale per-contract data) for up to 5 minutes
|
|
16
|
+
- Removing the session's currently active contract from an organizational unit now switches the session to a remaining contract, instead of leaving the UI pointed at a contract that no longer exists
|
|
17
|
+
- Added a back-forward-cache guard to the contract switch flow to address a QA report of briefly seeing the previous contract's data after switching and going back
|
|
18
|
+
- `SumaPageLayout` (the layout actually rendering the Users/Roles/Organizational Units sidebar when the `suma` flag is on) now also resolves its contract id from the URL instead of falling back to the org unit's first contract; the Users/Roles/Organizational Units page loaders now fetch minimal contract details when the URL carries a contract id, so the sidebar identity block shows consistently everywhere, not just on Budgets/Buying Policies
|
|
19
|
+
- The sidebar's "Accounting fields" fetch is now skipped while its containing layout is in a transient loading state (e.g. during a route transition), instead of firing with route-parsed, possibly-stale ids; the link itself still renders, just without its child fields until loading finishes
|
|
20
|
+
- Widened the Contract Listing sidebar and adjusted its padding; the "Add contracts" button is no longer gated behind an admin role check
|
|
21
|
+
|
|
10
22
|
## [2.0.10] - 2026-07-17
|
|
11
23
|
|
|
12
24
|
### Changed
|
|
@@ -745,7 +757,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
745
757
|
- Add CHANGELOG file
|
|
746
758
|
- Add README file
|
|
747
759
|
|
|
748
|
-
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/
|
|
760
|
+
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/2.0.11...HEAD
|
|
749
761
|
[1.3.55]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...v1.3.55
|
|
750
762
|
[1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
|
|
751
763
|
[1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
|
|
@@ -842,3 +854,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
842
854
|
|
|
843
855
|
[2.0.10]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.9...v2.0.10
|
|
844
856
|
[2.0.9]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/2.0.9
|
|
857
|
+
|
|
858
|
+
[2.0.11]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/2.0.11
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from "../types";
|
|
10
10
|
import type {
|
|
11
11
|
EditLocationSettingsParams,
|
|
12
|
-
|
|
12
|
+
GetAttachedAddressesByCustomerIdParams,
|
|
13
13
|
} from "../types/AddressData";
|
|
14
14
|
|
|
15
15
|
type AddressResponse = {
|
|
@@ -33,21 +33,25 @@ export default class AddressesClient extends Client {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
async
|
|
36
|
+
async getAttachedAddressesByCustomerId({
|
|
37
37
|
orgUnitId,
|
|
38
|
+
customerId,
|
|
38
39
|
cookie,
|
|
39
40
|
name,
|
|
40
41
|
addressType,
|
|
41
42
|
page = 1,
|
|
42
|
-
}:
|
|
43
|
-
return this.get<AddressResponse>(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
}: GetAttachedAddressesByCustomerIdParams) {
|
|
44
|
+
return this.get<AddressResponse>(
|
|
45
|
+
`units/${orgUnitId}/customers/${customerId}/addresses/attached`,
|
|
46
|
+
{
|
|
47
|
+
headers: { Cookie: cookie },
|
|
48
|
+
params: {
|
|
49
|
+
page,
|
|
50
|
+
name: name ?? "",
|
|
51
|
+
addressType: addressType ?? "",
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
);
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
getAddressById(
|
|
@@ -91,6 +91,7 @@ export const CreateAddressSettingsDrawer = ({
|
|
|
91
91
|
|
|
92
92
|
const { searchedAddresses } = useDebouncedSearchAddressByUnitId({
|
|
93
93
|
orgUnitId: router.query.orgUnitId as string,
|
|
94
|
+
contractId: router.query.contractId as string,
|
|
94
95
|
search: searchValue,
|
|
95
96
|
addressType: activeField === "Shipping" ? "Shipping" : "Billing",
|
|
96
97
|
});
|
|
@@ -8,12 +8,14 @@ import type { UseDebouncedSearchAddressByUnitIdProps } from "../types/AddressDat
|
|
|
8
8
|
|
|
9
9
|
export const useDebouncedSearchAddressByUnitId = ({
|
|
10
10
|
orgUnitId,
|
|
11
|
+
contractId,
|
|
11
12
|
search = "",
|
|
12
13
|
addressType,
|
|
13
14
|
}: UseDebouncedSearchAddressByUnitIdProps) => {
|
|
14
15
|
const debouncedSearchTerm = useDebounce(search, DEBOUNCE_TIMEOUT);
|
|
15
16
|
const { listAddressesData, listAddressesLoading } = useListAddresses({
|
|
16
17
|
orgUnitId,
|
|
18
|
+
contractId,
|
|
17
19
|
search: debouncedSearchTerm,
|
|
18
20
|
addressType,
|
|
19
21
|
});
|
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
import { type QueryOptions, useQuery } from "../../shared/hooks";
|
|
2
|
-
import {
|
|
2
|
+
import { getAttachedAddressesByCustomerIdService } from "../services";
|
|
3
3
|
|
|
4
4
|
type UseListAddressesProps = {
|
|
5
5
|
orgUnitId: string;
|
|
6
|
+
contractId: string;
|
|
6
7
|
search: string;
|
|
7
8
|
page?: number;
|
|
8
9
|
addressType?: string;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
export const useListAddresses = (
|
|
12
|
-
{
|
|
13
|
-
|
|
13
|
+
{
|
|
14
|
+
orgUnitId,
|
|
15
|
+
contractId,
|
|
16
|
+
search,
|
|
17
|
+
page = 1,
|
|
18
|
+
addressType,
|
|
19
|
+
}: UseListAddressesProps,
|
|
20
|
+
options?: QueryOptions<
|
|
21
|
+
AwaitedType<typeof getAttachedAddressesByCustomerIdService>
|
|
22
|
+
>
|
|
14
23
|
) => {
|
|
15
24
|
const { data, error, isLoading, refetch } = useQuery(
|
|
16
|
-
`api/addresses/unitId=${orgUnitId}/page=${page}/search=${
|
|
25
|
+
`api/addresses/unitId=${orgUnitId}/contractId=${contractId}/page=${page}/search=${
|
|
17
26
|
search ?? ""
|
|
18
27
|
}/addressType=${addressType ?? ""}`,
|
|
19
28
|
({ cookie }) =>
|
|
20
|
-
|
|
29
|
+
getAttachedAddressesByCustomerIdService({
|
|
21
30
|
orgUnitId,
|
|
31
|
+
contractId,
|
|
22
32
|
search,
|
|
23
33
|
cookie,
|
|
24
34
|
addressType,
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "../../shared/utils/withClientErrorBoundary";
|
|
6
6
|
import { addressesClient } from "../clients/AddressesClient";
|
|
7
7
|
|
|
8
|
-
export type
|
|
8
|
+
export type GetAttachedAddressesByCustomerIdServiceProps = Partial<{
|
|
9
9
|
search: string;
|
|
10
10
|
orgUnitId: string;
|
|
11
11
|
status: string;
|
|
@@ -14,11 +14,13 @@ export type GetAddressesByUnitIdServiceProps = Partial<{
|
|
|
14
14
|
page?: number;
|
|
15
15
|
addressType?: string;
|
|
16
16
|
}> & {
|
|
17
|
+
contractId: string;
|
|
17
18
|
cookie: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
export const
|
|
21
|
+
export const getAttachedAddressesByCustomerIdServiceFn = async ({
|
|
21
22
|
orgUnitId,
|
|
23
|
+
contractId,
|
|
22
24
|
status,
|
|
23
25
|
type,
|
|
24
26
|
sort,
|
|
@@ -26,17 +28,19 @@ export const getAddressesByUnitIdServiceFn = async ({
|
|
|
26
28
|
search = "",
|
|
27
29
|
page = 1,
|
|
28
30
|
addressType,
|
|
29
|
-
}:
|
|
30
|
-
if (!orgUnitId) {
|
|
31
|
+
}: GetAttachedAddressesByCustomerIdServiceProps) => {
|
|
32
|
+
if (!orgUnitId || !contractId) {
|
|
31
33
|
return { data: [], total: 0 };
|
|
32
34
|
}
|
|
33
|
-
const { addresses, total } =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
const { addresses, total } =
|
|
36
|
+
await addressesClient.getAttachedAddressesByCustomerId({
|
|
37
|
+
orgUnitId,
|
|
38
|
+
customerId: contractId,
|
|
39
|
+
cookie,
|
|
40
|
+
name: search,
|
|
41
|
+
addressType,
|
|
42
|
+
page,
|
|
43
|
+
});
|
|
40
44
|
|
|
41
45
|
const formattedAddress = addresses
|
|
42
46
|
?.filter((address) => {
|
|
@@ -51,10 +55,10 @@ export const getAddressesByUnitIdServiceFn = async ({
|
|
|
51
55
|
return { data: formattedAddress, total };
|
|
52
56
|
};
|
|
53
57
|
|
|
54
|
-
export const
|
|
55
|
-
|
|
58
|
+
export const getAttachedAddressesByCustomerIdService = withClientErrorBoundary(
|
|
59
|
+
getAttachedAddressesByCustomerIdServiceFn,
|
|
56
60
|
{
|
|
57
|
-
componentName: "
|
|
61
|
+
componentName: "AttachedAddressesByCustomerId",
|
|
58
62
|
onError: handleUnauthorizedRedirect,
|
|
59
63
|
}
|
|
60
64
|
);
|
|
@@ -4,9 +4,9 @@ export {
|
|
|
4
4
|
type GetAddressesServiceProps,
|
|
5
5
|
} from "./get-addresses.service";
|
|
6
6
|
export {
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
} from "./get-addresses-by-
|
|
7
|
+
getAttachedAddressesByCustomerIdService,
|
|
8
|
+
type GetAttachedAddressesByCustomerIdServiceProps,
|
|
9
|
+
} from "./get-attached-addresses-by-customer-id.service";
|
|
10
10
|
export {
|
|
11
11
|
createNewAddressService,
|
|
12
12
|
type CreateNewAddressServiceProps,
|
|
@@ -102,8 +102,9 @@ export type AddressInput = {
|
|
|
102
102
|
neighborhood?: string;
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
export type
|
|
105
|
+
export type GetAttachedAddressesByCustomerIdParams = {
|
|
106
106
|
orgUnitId: string;
|
|
107
|
+
customerId: string;
|
|
107
108
|
cookie: string;
|
|
108
109
|
name?: string;
|
|
109
110
|
addressType?: string;
|
|
@@ -120,6 +121,7 @@ export type DefaultAddressParams = {
|
|
|
120
121
|
|
|
121
122
|
export type UseDebouncedSearchAddressByUnitIdProps = {
|
|
122
123
|
orgUnitId: string;
|
|
124
|
+
contractId: string;
|
|
123
125
|
search: string;
|
|
124
126
|
addressType?: string;
|
|
125
127
|
};
|
|
@@ -14,11 +14,14 @@ import {
|
|
|
14
14
|
VerticalNav,
|
|
15
15
|
LetterHighlight,
|
|
16
16
|
} from "../../../shared/components";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
useDrawerProps,
|
|
19
|
+
useBuyerPortal,
|
|
20
|
+
useReloadOnBfcacheRestore,
|
|
21
|
+
} from "../../../shared/hooks";
|
|
18
22
|
import { GlobalLayout } from "../../../shared/layouts";
|
|
19
23
|
import { SumaSidebar } from "../../../shared/layouts/SumaPageLayout/SumaSidebar";
|
|
20
24
|
import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
|
|
21
|
-
import { USER_ROLES } from "../../../users/types";
|
|
22
25
|
import { AddContractsDrawer } from "../../components/AddContractsDrawer/AddContractsDrawer";
|
|
23
26
|
import { ConfirmRemoveDrawer } from "../../components/ConfirmRemoveDrawer/ConfirmRemoveDrawer";
|
|
24
27
|
import { ContractSelectAllRow } from "../../components/ContractSelectAllRow";
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
useContractSwitchChannel,
|
|
33
36
|
useRemoveContracts,
|
|
34
37
|
useSetDefaultContract,
|
|
38
|
+
useSwitchContract,
|
|
35
39
|
} from "../../hooks";
|
|
36
40
|
|
|
37
41
|
import { buildContractSettingsNavItems } from "./ContractSettingsNav";
|
|
@@ -96,8 +100,8 @@ export const ContractListingLayout = ({
|
|
|
96
100
|
...addContractsDrawer
|
|
97
101
|
} = useDrawerProps();
|
|
98
102
|
|
|
99
|
-
const { featureFlags } = useBuyerPortal();
|
|
100
|
-
const
|
|
103
|
+
const { featureFlags, clientContext } = useBuyerPortal();
|
|
104
|
+
const { switchContract } = useSwitchContract();
|
|
101
105
|
|
|
102
106
|
const {
|
|
103
107
|
open: openConfirmRemoveDrawer,
|
|
@@ -119,6 +123,34 @@ export const ContractListingLayout = ({
|
|
|
119
123
|
onSuccess: () => {
|
|
120
124
|
confirmRemoveDrawer.close();
|
|
121
125
|
clearSelection();
|
|
126
|
+
|
|
127
|
+
const removedActiveContract = pendingRemoveIds.has(
|
|
128
|
+
clientContext.customerId
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (removedActiveContract) {
|
|
132
|
+
const remainingContracts = contracts.filter(
|
|
133
|
+
(c) => !pendingRemoveIds.has(c.id)
|
|
134
|
+
);
|
|
135
|
+
const fallbackContractId = getDefaultContractId(remainingContracts);
|
|
136
|
+
|
|
137
|
+
if (fallbackContractId) {
|
|
138
|
+
switchContract(fallbackContractId, { orgUnitId: orgUnit.id }).then(
|
|
139
|
+
(switched) => {
|
|
140
|
+
if (!switched) {
|
|
141
|
+
pushToast({
|
|
142
|
+
message:
|
|
143
|
+
"Contracts removed, but we couldn't refresh your active contract. Please reload the page.",
|
|
144
|
+
status: "ERROR",
|
|
145
|
+
});
|
|
146
|
+
router.replace(router.asPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
122
154
|
router.replace(router.asPath);
|
|
123
155
|
pushToast({
|
|
124
156
|
message: `${pendingRemoveIds.size} contract${
|
|
@@ -200,6 +232,8 @@ export const ContractListingLayout = ({
|
|
|
200
232
|
return cleanup;
|
|
201
233
|
}, []);
|
|
202
234
|
|
|
235
|
+
useReloadOnBfcacheRestore();
|
|
236
|
+
|
|
203
237
|
useEffect(() => {
|
|
204
238
|
if (router.query.error === "contract-load-failed") {
|
|
205
239
|
pushToast({
|
|
@@ -289,7 +323,7 @@ export const ContractListingLayout = ({
|
|
|
289
323
|
info={headerInfo}
|
|
290
324
|
data-fs-bp-contract-listing-header
|
|
291
325
|
>
|
|
292
|
-
{featureFlags?.suma &&
|
|
326
|
+
{featureFlags?.suma && (
|
|
293
327
|
<HeaderInside.Button
|
|
294
328
|
onClick={openAddContractsDrawer}
|
|
295
329
|
aria-label="Add contracts"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
margin: var(--fs-bp-margin-0) var(--fs-bp-margin-auto);
|
|
22
22
|
|
|
23
23
|
@include u.media(">=notebook") {
|
|
24
|
-
grid-template-columns:
|
|
24
|
+
grid-template-columns: 20rem 1fr;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
@import "../../../shared/components/LetterHighlight/letter-highlight.scss";
|
|
31
31
|
|
|
32
32
|
display: none;
|
|
33
|
-
padding: var(--fs-bp-padding-5) var(--fs-bp-padding-5) var(--fs-bp-padding-12) var(--fs-bp-padding-
|
|
33
|
+
padding: var(--fs-bp-padding-5) var(--fs-bp-padding-5) var(--fs-bp-padding-12) var(--fs-bp-padding-5);
|
|
34
34
|
border-right: 1px solid var(--fs-bp-color-neutral-2);
|
|
35
35
|
overflow-y: auto;
|
|
36
36
|
|
|
@@ -104,7 +104,10 @@ export const OrgUnitsDetailsLayout = ({
|
|
|
104
104
|
listAccountingFieldRefresh,
|
|
105
105
|
} = useListAccountingFields({ orgUnitId: orgUnit.id });
|
|
106
106
|
|
|
107
|
-
const organizationMenu = getOrganizationSettingsLinks(
|
|
107
|
+
const organizationMenu = getOrganizationSettingsLinks({
|
|
108
|
+
orgUnitId: orgUnit.id,
|
|
109
|
+
contractId: linkParams.contractId,
|
|
110
|
+
});
|
|
108
111
|
|
|
109
112
|
const financeMenu = getFinanceSettingsLinks(linkParams);
|
|
110
113
|
|
|
@@ -97,7 +97,7 @@ class Client {
|
|
|
97
97
|
...(headers.Cookie ? getAuthFromCookie(headers.Cookie ?? "") : {}),
|
|
98
98
|
...headers,
|
|
99
99
|
},
|
|
100
|
-
...(cache ? { cache
|
|
100
|
+
...(cache ? { cache } : {}),
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
const paramsString = toQueryParams(params);
|
|
@@ -19,3 +19,4 @@ export { ANALYTICS_FALLBACK_CORRELATION_ID } from "./analytics/constants";
|
|
|
19
19
|
export { useGetScopeConfig, SCOPE_KEYS } from "./useGetScopeConfig";
|
|
20
20
|
export { useSetScopeConfig } from "./useSetScopeConfig";
|
|
21
21
|
export { useUrlPaginatedSearch } from "./useUrlPaginatedSearch";
|
|
22
|
+
export { useReloadOnBfcacheRestore } from "./useReloadOnBfcacheRestore";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/router";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Forces a full reload when the page is restored from the browser's
|
|
7
|
+
* back-forward cache (e.g. after pressing Back), so contract-scoped layouts
|
|
8
|
+
* never resume with frozen JS state/closures from before a prior contract
|
|
9
|
+
* switch.
|
|
10
|
+
*/
|
|
11
|
+
export const useReloadOnBfcacheRestore = () => {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handlePageShow = (event: PageTransitionEvent) => {
|
|
16
|
+
if (event.persisted) {
|
|
17
|
+
router.reload();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
22
|
+
return () => window.removeEventListener("pageshow", handlePageShow);
|
|
23
|
+
}, []);
|
|
24
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import { useRouter } from "next/router";
|
|
4
4
|
|
|
5
5
|
import { useBuyerPortal } from "../../hooks";
|
|
6
|
+
import { resolveContractId } from "../../utils";
|
|
6
7
|
import { FullSidebarNav } from "../SumaPageLayout/FullSidebarNav";
|
|
7
8
|
|
|
8
9
|
export type SidebarMenuProps = {
|
|
@@ -16,21 +17,19 @@ export type SidebarMenuProps = {
|
|
|
16
17
|
loading?: boolean;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
export const SidebarMenu = ({ ...otherProps }: SidebarMenuProps) => {
|
|
20
|
+
export const SidebarMenu = ({ loading, ...otherProps }: SidebarMenuProps) => {
|
|
20
21
|
const { currentOrgUnit, currentContract } = useBuyerPortal();
|
|
21
22
|
const router = useRouter();
|
|
22
23
|
|
|
23
24
|
const orgUnitId = (router.query.orgUnitId as string) || "";
|
|
24
|
-
const customerId = (router.query.contractId as string) || "";
|
|
25
|
-
|
|
26
|
-
const idContract =
|
|
27
|
-
(currentContract?.id
|
|
28
|
-
? currentContract?.id
|
|
29
|
-
: currentOrgUnit?.customerGroup.customerIds[0]) ?? "";
|
|
30
25
|
|
|
31
26
|
const linkParams = {
|
|
32
27
|
orgUnitId: orgUnitId || currentOrgUnit?.id || "",
|
|
33
|
-
contractId:
|
|
28
|
+
contractId: resolveContractId({
|
|
29
|
+
queryContractId: router.query.contractId,
|
|
30
|
+
currentContract,
|
|
31
|
+
currentOrgUnit,
|
|
32
|
+
}),
|
|
34
33
|
};
|
|
35
34
|
|
|
36
35
|
return (
|
|
@@ -38,6 +37,7 @@ export const SidebarMenu = ({ ...otherProps }: SidebarMenuProps) => {
|
|
|
38
37
|
<FullSidebarNav
|
|
39
38
|
orgUnitId={linkParams.orgUnitId}
|
|
40
39
|
contractId={linkParams.contractId}
|
|
40
|
+
loading={loading}
|
|
41
41
|
/>
|
|
42
42
|
</aside>
|
|
43
43
|
);
|
|
@@ -4,7 +4,7 @@ import { DropdownItem } from "@faststore/ui";
|
|
|
4
4
|
|
|
5
5
|
import { OrgUnitDetailsNavbar } from "../../../org-units/components";
|
|
6
6
|
import { Icon } from "../../components";
|
|
7
|
-
import { useBuyerPortal } from "../../hooks";
|
|
7
|
+
import { useBuyerPortal, useReloadOnBfcacheRestore } from "../../hooks";
|
|
8
8
|
import { getContractSettingsLinks } from "../../utils";
|
|
9
9
|
import { BaseTabsLayout } from "../BaseTabsLayout/BaseTabsLayout";
|
|
10
10
|
import { SumaSidebar } from "../SumaPageLayout/SumaSidebar";
|
|
@@ -29,6 +29,8 @@ export const ContractTabsLayout = ({
|
|
|
29
29
|
const { currentOrgUnit, currentUser, currentContract, featureFlags } =
|
|
30
30
|
useBuyerPortal();
|
|
31
31
|
|
|
32
|
+
useReloadOnBfcacheRestore();
|
|
33
|
+
|
|
32
34
|
const resolvedOrgUnitId = currentOrgUnit?.id ?? orgUnitId ?? "";
|
|
33
35
|
const resolvedContractId =
|
|
34
36
|
currentContract?.id ??
|
|
@@ -57,6 +59,7 @@ export const ContractTabsLayout = ({
|
|
|
57
59
|
contractId={resolvedContractId}
|
|
58
60
|
variant="full"
|
|
59
61
|
showBackLink
|
|
62
|
+
loading={loading}
|
|
60
63
|
/>
|
|
61
64
|
<section data-fs-bp-contract-listing-section>{children}</section>
|
|
62
65
|
</div>
|
|
@@ -17,7 +17,10 @@ export const OrgUnitTabsLayout = ({
|
|
|
17
17
|
}: OrgUnitTabsLayoutProps) => {
|
|
18
18
|
const { currentOrgUnit, currentUser, currentContract } = useBuyerPortal();
|
|
19
19
|
|
|
20
|
-
const verticalLinks = getOrganizationSettingsLinks(
|
|
20
|
+
const verticalLinks = getOrganizationSettingsLinks({
|
|
21
|
+
orgUnitId: currentOrgUnit?.id ?? "",
|
|
22
|
+
contractId: currentContract?.id ?? "",
|
|
23
|
+
});
|
|
21
24
|
|
|
22
25
|
return (
|
|
23
26
|
<BaseTabsLayout data-fs-bp-org-unit-tabs-layout>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getContractSettingsLinks,
|
|
13
13
|
getFinanceSettingsLinks,
|
|
14
14
|
getOrganizationSettingsLinks,
|
|
15
|
+
resolveContractId,
|
|
15
16
|
} from "../../utils";
|
|
16
17
|
import { buyerPortalRoutes } from "../../utils/buyerPortalRoutes";
|
|
17
18
|
|
|
@@ -21,6 +22,7 @@ export type FullSidebarNavProps = {
|
|
|
21
22
|
orgUnitId: string;
|
|
22
23
|
contractId: string;
|
|
23
24
|
showContractTitle?: boolean;
|
|
25
|
+
loading?: boolean;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
const mapLinksHandler = ({
|
|
@@ -35,6 +37,7 @@ export const FullSidebarNav = ({
|
|
|
35
37
|
orgUnitId,
|
|
36
38
|
contractId,
|
|
37
39
|
showContractTitle = true,
|
|
40
|
+
loading = false,
|
|
38
41
|
}: FullSidebarNavProps) => {
|
|
39
42
|
const { currentOrgUnit, currentContract } = useBuyerPortal();
|
|
40
43
|
|
|
@@ -46,21 +49,26 @@ export const FullSidebarNav = ({
|
|
|
46
49
|
|
|
47
50
|
const resolvedOrgUnitId = orgUnitId || currentOrgUnit?.id || "";
|
|
48
51
|
const resolvedContractId =
|
|
49
|
-
contractId ||
|
|
50
|
-
currentContract?.id ||
|
|
51
|
-
currentOrgUnit?.customerGroup.customerIds[0] ||
|
|
52
|
-
"";
|
|
52
|
+
contractId || resolveContractId({ currentContract, currentOrgUnit });
|
|
53
53
|
|
|
54
54
|
const linkParams = {
|
|
55
55
|
orgUnitId: resolvedOrgUnitId,
|
|
56
56
|
contractId: resolvedContractId,
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
// Skip the fetch entirely while this sidebar is just a transient loading
|
|
60
|
+
// placeholder (e.g. LoadingTabsLayout during a route transition) — it would
|
|
61
|
+
// otherwise fire with route-parsed, possibly-stale ids. The item below still
|
|
62
|
+
// renders (just with no children yet), since data/loading naturally stay at
|
|
63
|
+
// their initial empty/false values while the fetch is skipped.
|
|
59
64
|
const {
|
|
60
65
|
listAccountingFieldLoading,
|
|
61
66
|
listAccountingFieldData,
|
|
62
67
|
listAccountingFieldRefresh,
|
|
63
|
-
} = useListAccountingFields(
|
|
68
|
+
} = useListAccountingFields(
|
|
69
|
+
{ orgUnitId: resolvedOrgUnitId },
|
|
70
|
+
{ lazy: loading }
|
|
71
|
+
);
|
|
64
72
|
|
|
65
73
|
const contractMenu =
|
|
66
74
|
getContractSettingsLinks(linkParams).map(mapLinksHandler);
|
|
@@ -95,7 +103,10 @@ export const FullSidebarNav = ({
|
|
|
95
103
|
),
|
|
96
104
|
});
|
|
97
105
|
|
|
98
|
-
const organizationMenu = getOrganizationSettingsLinks(
|
|
106
|
+
const organizationMenu = getOrganizationSettingsLinks({
|
|
107
|
+
orgUnitId: resolvedOrgUnitId,
|
|
108
|
+
contractId: resolvedContractId,
|
|
109
|
+
});
|
|
99
110
|
const financeMenu = getFinanceSettingsLinks(linkParams);
|
|
100
111
|
|
|
101
112
|
return (
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
import { useRouter } from "next/router";
|
|
4
|
+
|
|
3
5
|
import { OrgUnitDetailsNavbar } from "../../../org-units/components";
|
|
4
|
-
import { useBuyerPortal } from "../../hooks";
|
|
6
|
+
import { useBuyerPortal, useReloadOnBfcacheRestore } from "../../hooks";
|
|
7
|
+
import { resolveContractId } from "../../utils";
|
|
5
8
|
|
|
6
9
|
import { SumaSidebar } from "./SumaSidebar";
|
|
7
10
|
|
|
@@ -20,13 +23,19 @@ export const SumaPageLayout = ({
|
|
|
20
23
|
}: SumaPageLayoutProps) => {
|
|
21
24
|
const { featureFlags, currentOrgUnit, currentUser, currentContract } =
|
|
22
25
|
useBuyerPortal();
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
|
|
28
|
+
useReloadOnBfcacheRestore();
|
|
23
29
|
|
|
24
30
|
if (!featureFlags?.suma || !currentOrgUnit?.id) {
|
|
25
31
|
return <>{children}</>;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
const contractId =
|
|
29
|
-
|
|
34
|
+
const contractId = resolveContractId({
|
|
35
|
+
queryContractId: router.query.contractId,
|
|
36
|
+
currentContract,
|
|
37
|
+
currentOrgUnit,
|
|
38
|
+
});
|
|
30
39
|
|
|
31
40
|
return (
|
|
32
41
|
<>
|
|
@@ -48,6 +57,7 @@ export const SumaPageLayout = ({
|
|
|
48
57
|
contractId={contractId}
|
|
49
58
|
variant={sidebarVariant}
|
|
50
59
|
showBackLink={showBackLink}
|
|
60
|
+
loading={loading}
|
|
51
61
|
/>
|
|
52
62
|
<section data-fs-bp-contract-listing-section>{children}</section>
|
|
53
63
|
</div>
|
|
@@ -12,6 +12,7 @@ export type SumaSidebarProps = {
|
|
|
12
12
|
contractId: string;
|
|
13
13
|
variant?: "listing" | "full";
|
|
14
14
|
showBackLink?: boolean;
|
|
15
|
+
loading?: boolean;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export const SumaSidebar = ({
|
|
@@ -19,6 +20,7 @@ export const SumaSidebar = ({
|
|
|
19
20
|
contractId,
|
|
20
21
|
variant = "full",
|
|
21
22
|
showBackLink = false,
|
|
23
|
+
loading = false,
|
|
22
24
|
}: SumaSidebarProps) => {
|
|
23
25
|
const { featureFlags, currentContract } = useBuyerPortal();
|
|
24
26
|
const contractsUrl = buyerPortalRoutes.contracts({ orgUnitId });
|
|
@@ -73,6 +75,7 @@ export const SumaSidebar = ({
|
|
|
73
75
|
orgUnitId={orgUnitId}
|
|
74
76
|
contractId={contractId}
|
|
75
77
|
showContractTitle={!showIdentity}
|
|
78
|
+
loading={loading}
|
|
76
79
|
/>
|
|
77
80
|
)}
|
|
78
81
|
</aside>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { resolveContractId } from "../resolveContractId";
|
|
4
|
+
|
|
5
|
+
import type { ContractData } from "../../../contracts/types";
|
|
6
|
+
import type { OrgUnitBasicData } from "../../../org-units/types";
|
|
7
|
+
|
|
8
|
+
const contract = { id: "contract-b" } as ContractData;
|
|
9
|
+
const orgUnit = {
|
|
10
|
+
customerGroup: { customerIds: ["contract-a", "contract-b"] },
|
|
11
|
+
} as OrgUnitBasicData;
|
|
12
|
+
|
|
13
|
+
describe("resolveContractId", () => {
|
|
14
|
+
it("prefers the URL's contractId over context", () => {
|
|
15
|
+
expect(
|
|
16
|
+
resolveContractId({
|
|
17
|
+
queryContractId: "contract-c",
|
|
18
|
+
currentContract: contract,
|
|
19
|
+
currentOrgUnit: orgUnit,
|
|
20
|
+
})
|
|
21
|
+
).toBe("contract-c");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("takes the first value when the URL param is an array", () => {
|
|
25
|
+
expect(
|
|
26
|
+
resolveContractId({
|
|
27
|
+
queryContractId: ["contract-c", "contract-d"],
|
|
28
|
+
currentContract: contract,
|
|
29
|
+
currentOrgUnit: orgUnit,
|
|
30
|
+
})
|
|
31
|
+
).toBe("contract-c");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("falls back to currentContract when the URL has no contractId", () => {
|
|
35
|
+
expect(
|
|
36
|
+
resolveContractId({ currentContract: contract, currentOrgUnit: orgUnit })
|
|
37
|
+
).toBe("contract-b");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to the org unit's first customer id as a last resort", () => {
|
|
41
|
+
expect(resolveContractId({ currentOrgUnit: orgUnit })).toBe("contract-a");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns an empty string when nothing is known", () => {
|
|
45
|
+
expect(resolveContractId({})).toBe("");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -10,6 +10,12 @@ function replaceParams(template: string, params: Record<string, string>) {
|
|
|
10
10
|
.replace(/\/+/g, "/"); // Remove consecutive slashes
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function appendContractIdParam(url: string, contractId?: string): string {
|
|
14
|
+
return contractId
|
|
15
|
+
? `${url}?contractId=${encodeURIComponent(contractId)}`
|
|
16
|
+
: url;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export const buyerPortalRoutes = {
|
|
14
20
|
// orgUnitId + contractId
|
|
15
21
|
profileDetails: (params: { orgUnitId: string; contractId: string }) =>
|
|
@@ -66,18 +72,34 @@ export const buyerPortalRoutes = {
|
|
|
66
72
|
`${base}/buying-policy/[orgUnitId]/[contractId]/[buyingPolicyId]`,
|
|
67
73
|
params
|
|
68
74
|
),
|
|
69
|
-
// orgUnitId only
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// orgUnitId only (contractId is optional and only carried in the URL so
|
|
76
|
+
// the sidebar can keep linking to the right contract elsewhere)
|
|
77
|
+
users: (params: { orgUnitId: string; contractId?: string }) =>
|
|
78
|
+
appendContractIdParam(
|
|
79
|
+
replaceParams(`${base}/users/[orgUnitId]`, {
|
|
80
|
+
orgUnitId: params.orgUnitId,
|
|
81
|
+
}),
|
|
82
|
+
params.contractId
|
|
83
|
+
),
|
|
72
84
|
|
|
73
|
-
roles: (params: { orgUnitId: string }) =>
|
|
74
|
-
|
|
85
|
+
roles: (params: { orgUnitId: string; contractId?: string }) =>
|
|
86
|
+
appendContractIdParam(
|
|
87
|
+
replaceParams(`${base}/roles/[orgUnitId]`, {
|
|
88
|
+
orgUnitId: params.orgUnitId,
|
|
89
|
+
}),
|
|
90
|
+
params.contractId
|
|
91
|
+
),
|
|
75
92
|
|
|
76
93
|
roleDetails: (params: { orgUnitId: string; roleName: string }) =>
|
|
77
94
|
replaceParams(`${base}/roles/[orgUnitId]/[roleName]`, params),
|
|
78
95
|
|
|
79
|
-
orgUnits: (params: { orgUnitId: string }) =>
|
|
80
|
-
|
|
96
|
+
orgUnits: (params: { orgUnitId: string; contractId?: string }) =>
|
|
97
|
+
appendContractIdParam(
|
|
98
|
+
replaceParams(`${base}/org-units/[orgUnitId]`, {
|
|
99
|
+
orgUnitId: params.orgUnitId,
|
|
100
|
+
}),
|
|
101
|
+
params.contractId
|
|
102
|
+
),
|
|
81
103
|
|
|
82
104
|
// orgUnitId + specific ID
|
|
83
105
|
userDetails: (params: { orgUnitId: string; userId: string }) =>
|
|
@@ -22,7 +22,7 @@ export const SCOPE_KEYS = {
|
|
|
22
22
|
CREDIT_CARDS: "creditCards",
|
|
23
23
|
} as const;
|
|
24
24
|
|
|
25
|
-
export const CURRENT_VERSION = "2.0.
|
|
25
|
+
export const CURRENT_VERSION = "2.0.11";
|
|
26
26
|
|
|
27
27
|
export const CHANGES_TIMEOUT_MESSAGE =
|
|
28
28
|
"Changes may take up to 10 minutes to apply.";
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { buyerPortalRoutes } from "./buyerPortalRoutes";
|
|
2
2
|
|
|
3
|
-
export const getOrganizationSettingsLinks = (
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export const getOrganizationSettingsLinks = ({
|
|
4
|
+
orgUnitId,
|
|
5
|
+
contractId,
|
|
6
|
+
}: {
|
|
7
|
+
orgUnitId: string;
|
|
8
|
+
contractId?: string;
|
|
9
|
+
}) => [
|
|
10
|
+
{ name: "Users", link: buyerPortalRoutes.users({ orgUnitId, contractId }) },
|
|
11
|
+
{ name: "Roles", link: buyerPortalRoutes.roles({ orgUnitId, contractId }) },
|
|
6
12
|
{
|
|
7
13
|
name: "Organizational Units",
|
|
8
|
-
link: buyerPortalRoutes.orgUnits({ orgUnitId }),
|
|
14
|
+
link: buyerPortalRoutes.orgUnits({ orgUnitId, contractId }),
|
|
9
15
|
},
|
|
10
16
|
];
|
|
@@ -13,6 +13,6 @@ export const getSumaListingSidebarLinks = ({
|
|
|
13
13
|
name: "Contracts",
|
|
14
14
|
link: buyerPortalRoutes.contracts({ orgUnitId }),
|
|
15
15
|
},
|
|
16
|
-
...getOrganizationSettingsLinks(orgUnitId),
|
|
16
|
+
...getOrganizationSettingsLinks({ orgUnitId, contractId }),
|
|
17
17
|
...getFinanceSettingsLinks({ orgUnitId, contractId }),
|
|
18
18
|
];
|
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
} from "./getKeyByValue";
|
|
24
24
|
export { getOrganizationSettingsLinks } from "./getOrganizationSettingsLinks";
|
|
25
25
|
export { getSumaListingSidebarLinks } from "./getSumaListingSidebarLinks";
|
|
26
|
+
export { resolveContractId } from "./resolveContractId";
|
|
26
27
|
export { getTreeDepth } from "./getTreeDepth";
|
|
27
28
|
export { doLogout } from "./logout";
|
|
28
29
|
export { transformToDecimalMoney } from "./monetaryConverter";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ContractData } from "../../contracts/types";
|
|
2
|
+
import type { OrgUnitBasicData } from "../../org-units/types";
|
|
3
|
+
|
|
4
|
+
export type ResolveContractIdParams = {
|
|
5
|
+
queryContractId?: string | string[];
|
|
6
|
+
currentContract?: ContractData | null;
|
|
7
|
+
currentOrgUnit?: OrgUnitBasicData | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Single source of truth for which contract the sidebar should link to.
|
|
12
|
+
* The URL's `contractId` (when present) always wins; `customerIds[0]` is a
|
|
13
|
+
* last resort for direct/bookmarked visits where nothing else is known.
|
|
14
|
+
*/
|
|
15
|
+
export const resolveContractId = ({
|
|
16
|
+
queryContractId,
|
|
17
|
+
currentContract,
|
|
18
|
+
currentOrgUnit,
|
|
19
|
+
}: ResolveContractIdParams): string => {
|
|
20
|
+
const fromQuery = Array.isArray(queryContractId)
|
|
21
|
+
? queryContractId[0]
|
|
22
|
+
: queryContractId;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
fromQuery ||
|
|
26
|
+
currentContract?.id ||
|
|
27
|
+
currentOrgUnit?.customerGroup.customerIds[0] ||
|
|
28
|
+
""
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -3,6 +3,7 @@ import { getBuyerPortalFeatureFlags } from "../services/feature-flags/get-featur
|
|
|
3
3
|
|
|
4
4
|
import { ClientContext, getClientContext } from "./getClientContext";
|
|
5
5
|
|
|
6
|
+
import type { FeatureFlags } from "../components/BuyerPortalProvider/BuyerPortalProvider";
|
|
6
7
|
import type { AuthRouteProps } from "../types";
|
|
7
8
|
import type { LoaderData } from "../types";
|
|
8
9
|
|
|
@@ -10,13 +11,18 @@ export type WithAuthLoaderOptions = {
|
|
|
10
11
|
validateAccess?: (props: { cookie: string }) => Promise<boolean>;
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
export type WithAuthLoaderClientContext = ClientContext & {
|
|
15
|
+
/** Single in-flight fetch shared with the flags merge below — reuse this instead of calling getBuyerPortalFeatureFlags again. */
|
|
16
|
+
featureFlagsPromise: Promise<FeatureFlags>;
|
|
17
|
+
};
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Utility function to encapsulate access validation in loaders.
|
|
15
21
|
* Similar to withAuthProvider but for server-side usage.
|
|
16
22
|
*/
|
|
17
23
|
export const withAuthLoader = async <T>(
|
|
18
24
|
data: LoaderData,
|
|
19
|
-
dataLoader: (clientContext:
|
|
25
|
+
dataLoader: (clientContext: WithAuthLoaderClientContext) => Promise<T>,
|
|
20
26
|
options?: WithAuthLoaderOptions
|
|
21
27
|
): Promise<AuthRouteProps<T>> => {
|
|
22
28
|
try {
|
|
@@ -35,9 +41,11 @@ export const withAuthLoader = async <T>(
|
|
|
35
41
|
};
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
const featureFlagsPromise = getBuyerPortalFeatureFlags(cookie);
|
|
45
|
+
|
|
38
46
|
const [pageData, featureFlags] = await Promise.all([
|
|
39
|
-
dataLoader({ cookie, userId, ...clientContext }),
|
|
40
|
-
|
|
47
|
+
dataLoader({ cookie, userId, ...clientContext, featureFlagsPromise }),
|
|
48
|
+
featureFlagsPromise,
|
|
41
49
|
]);
|
|
42
50
|
|
|
43
51
|
const mergedPageData = (() => {
|
package/src/pages/contracts.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import { listAttachedContractsService } from "../features/contracts/services";
|
|
|
3
3
|
import { getOrgUnitBasicDataService } from "../features/org-units/services";
|
|
4
4
|
import { withErrorBoundary } from "../features/shared/components";
|
|
5
5
|
import { ErrorTabsLayout } from "../features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout";
|
|
6
|
-
import { getBuyerPortalFeatureFlags } from "../features/shared/services/feature-flags/get-feature-flags.service";
|
|
7
6
|
import {
|
|
8
7
|
type ClientContext,
|
|
9
8
|
withLoaderErrorBoundary,
|
|
@@ -46,35 +45,38 @@ const loaderFunction = async (
|
|
|
46
45
|
throw new Error(`Missing required query param: orgUnitId=${orgUnitId}`);
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
return withAuthLoader(
|
|
50
|
-
|
|
48
|
+
return withAuthLoader(
|
|
49
|
+
data,
|
|
50
|
+
async ({ cookie, userId, featureFlagsPromise, ...clientContext }) => {
|
|
51
|
+
const featureFlags = await featureFlagsPromise;
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (!featureFlags?.suma) {
|
|
54
|
+
data.res?.writeHead(302, {
|
|
55
|
+
Location: buyerPortalRoutes.orgUnitDetails({ orgUnitId }),
|
|
56
|
+
});
|
|
57
|
+
data.res?.end();
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
return { redirected: true } as unknown as ContractsPageData;
|
|
60
|
+
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
const [orgUnit, contracts, user] = await Promise.all([
|
|
63
|
+
getOrgUnitBasicDataService({ id: orgUnitId, cookie }),
|
|
64
|
+
listAttachedContractsService({ orgUnitId, cookie }),
|
|
65
|
+
getUserByIdService({ orgUnitId, userId, cookie }),
|
|
66
|
+
]);
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
return {
|
|
69
|
+
data: {
|
|
70
|
+
orgUnit,
|
|
71
|
+
contracts: contracts ?? [],
|
|
72
|
+
user,
|
|
73
|
+
},
|
|
74
|
+
context: {
|
|
75
|
+
clientContext: { cookie, userId, ...clientContext },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
);
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
export const loader = withLoaderErrorBoundary(loaderFunction, {
|
package/src/pages/org-units.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getContractDetailsService } from "../features/contracts/services";
|
|
1
2
|
import { OrgUnitsLayout } from "../features/org-units/layouts";
|
|
2
3
|
import {
|
|
3
4
|
getChildrenOrgUnitsService,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
} from "../features/shared/utils";
|
|
17
18
|
import { getUserByIdService } from "../features/users/services";
|
|
18
19
|
|
|
20
|
+
import type { ContractData } from "../features/contracts/types";
|
|
19
21
|
import type {
|
|
20
22
|
OrgUnitBasicData,
|
|
21
23
|
OrgUnitSearchResponse,
|
|
@@ -29,6 +31,7 @@ export type OrgUnitsPageData = {
|
|
|
29
31
|
context: {
|
|
30
32
|
clientContext: ClientContext;
|
|
31
33
|
currentOrgUnit: OrgUnitBasicData | null;
|
|
34
|
+
currentContract: ContractData | null;
|
|
32
35
|
currentUser: UserData | null;
|
|
33
36
|
};
|
|
34
37
|
hasError?: boolean;
|
|
@@ -37,13 +40,14 @@ export type OrgUnitsPageData = {
|
|
|
37
40
|
|
|
38
41
|
type OrgUnitsPageQuery = {
|
|
39
42
|
orgUnitId: string;
|
|
43
|
+
contractId?: string;
|
|
40
44
|
search?: string;
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
const loaderFunction = async (
|
|
44
48
|
data: LoaderData<OrgUnitsPageQuery>
|
|
45
49
|
): Promise<AuthRouteProps<OrgUnitsPageData>> => {
|
|
46
|
-
const { search, orgUnitId } = data.query;
|
|
50
|
+
const { search, orgUnitId, contractId } = data.query;
|
|
47
51
|
|
|
48
52
|
return withAuthLoader(
|
|
49
53
|
data,
|
|
@@ -55,6 +59,18 @@ const loaderFunction = async (
|
|
|
55
59
|
|
|
56
60
|
const user = await getUserByIdService({ orgUnitId, userId, cookie });
|
|
57
61
|
|
|
62
|
+
// Organizational Units don't need contract data, but the sidebar
|
|
63
|
+
// (rendered by shared layouts) uses `currentContract` for its identity
|
|
64
|
+
// block, so fetch it when the URL carries the contract the user was
|
|
65
|
+
// last viewing.
|
|
66
|
+
const currentContract = contractId
|
|
67
|
+
? await getContractDetailsService({
|
|
68
|
+
contractId,
|
|
69
|
+
unitId: orgUnitId,
|
|
70
|
+
cookie,
|
|
71
|
+
})
|
|
72
|
+
: null;
|
|
73
|
+
|
|
58
74
|
const clientContext = {
|
|
59
75
|
cookie,
|
|
60
76
|
customerId,
|
|
@@ -73,6 +89,7 @@ const loaderFunction = async (
|
|
|
73
89
|
context: {
|
|
74
90
|
clientContext,
|
|
75
91
|
currentOrgUnit,
|
|
92
|
+
currentContract,
|
|
76
93
|
currentUser: user,
|
|
77
94
|
},
|
|
78
95
|
};
|
|
@@ -99,6 +116,7 @@ const loaderFunction = async (
|
|
|
99
116
|
context: {
|
|
100
117
|
clientContext,
|
|
101
118
|
currentOrgUnit,
|
|
119
|
+
currentContract,
|
|
102
120
|
currentUser: user,
|
|
103
121
|
},
|
|
104
122
|
};
|
|
@@ -113,6 +131,7 @@ const loaderFunction = async (
|
|
|
113
131
|
context: {
|
|
114
132
|
clientContext,
|
|
115
133
|
currentOrgUnit,
|
|
134
|
+
currentContract,
|
|
116
135
|
currentUser: user,
|
|
117
136
|
},
|
|
118
137
|
};
|
package/src/pages/roles.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getContractDetailsService } from "../features/contracts/services";
|
|
1
2
|
import { getOrgUnitBasicDataService } from "../features/org-units/services";
|
|
2
3
|
import { RolesLayout } from "../features/roles/layout";
|
|
3
4
|
import { getRolesAndPermissionsService } from "../features/roles/services";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
} from "../features/shared/utils";
|
|
14
15
|
import { getUserByIdService } from "../features/users/services";
|
|
15
16
|
|
|
17
|
+
import type { ContractData } from "../features/contracts/types";
|
|
16
18
|
import type { OrgUnitBasicData } from "../features/org-units/types";
|
|
17
19
|
import type { AuthRouteProps, LoaderData } from "../features/shared/types";
|
|
18
20
|
import type { UserData } from "../features/users/types";
|
|
@@ -24,30 +26,45 @@ export type RolesPageData = {
|
|
|
24
26
|
context: {
|
|
25
27
|
clientContext: ClientContext;
|
|
26
28
|
currentOrgUnit: OrgUnitBasicData | null;
|
|
29
|
+
currentContract: ContractData | null;
|
|
27
30
|
currentUser: UserData | null;
|
|
28
31
|
};
|
|
29
32
|
hasError?: boolean;
|
|
30
33
|
error?: ErrorBoundaryProps;
|
|
31
34
|
};
|
|
32
35
|
|
|
36
|
+
export type RolesPageQuery = {
|
|
37
|
+
orgUnitId: string;
|
|
38
|
+
contractId?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
33
41
|
const loaderFunction = async (
|
|
34
|
-
data: LoaderData
|
|
42
|
+
data: LoaderData<RolesPageQuery>
|
|
35
43
|
): Promise<AuthRouteProps<RolesPageData>> => {
|
|
36
|
-
const { orgUnitId } = data.query;
|
|
44
|
+
const { orgUnitId, contractId } = data.query;
|
|
37
45
|
|
|
38
46
|
return withAuthLoader(data, async ({ cookie, userId, ...clientContext }) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
// Roles don't need contract data, but the sidebar (rendered by shared
|
|
48
|
+
// layouts) uses `currentContract` for its identity block, so fetch it
|
|
49
|
+
// when the URL carries the contract the user was last viewing.
|
|
50
|
+
const contractDetailsPromise = contractId
|
|
51
|
+
? getContractDetailsService({ contractId, unitId: orgUnitId, cookie })
|
|
52
|
+
: Promise.resolve(null);
|
|
53
|
+
|
|
54
|
+
const [rolesAndPermissions, currentOrgUnit, user, currentContract] =
|
|
55
|
+
await Promise.all([
|
|
56
|
+
getRolesAndPermissionsService({
|
|
57
|
+
unitId: orgUnitId,
|
|
58
|
+
customerId: clientContext.customerId,
|
|
59
|
+
cookie,
|
|
60
|
+
}),
|
|
61
|
+
getOrgUnitBasicDataService({
|
|
62
|
+
id: orgUnitId,
|
|
63
|
+
cookie,
|
|
64
|
+
}),
|
|
65
|
+
getUserByIdService({ orgUnitId, userId, cookie }),
|
|
66
|
+
contractDetailsPromise,
|
|
67
|
+
]);
|
|
51
68
|
|
|
52
69
|
return {
|
|
53
70
|
data: {
|
|
@@ -56,6 +73,7 @@ const loaderFunction = async (
|
|
|
56
73
|
context: {
|
|
57
74
|
clientContext: { cookie, userId, ...clientContext },
|
|
58
75
|
currentOrgUnit,
|
|
76
|
+
currentContract,
|
|
59
77
|
currentUser: user,
|
|
60
78
|
},
|
|
61
79
|
};
|
package/src/pages/users.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getContractDetailsService } from "../features/contracts/services";
|
|
1
2
|
import { getOrgUnitBasicDataService } from "../features/org-units/services";
|
|
2
3
|
import { getRolesIdsService } from "../features/roles/services";
|
|
3
4
|
import { RoleData } from "../features/roles/types";
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
getUsersByOrgUnitIdService,
|
|
18
19
|
} from "../features/users/services";
|
|
19
20
|
|
|
21
|
+
import type { ContractData } from "../features/contracts/types";
|
|
20
22
|
import type { OrgUnitBasicData } from "../features/org-units/types";
|
|
21
23
|
import type { AuthRouteProps, LoaderData } from "../features/shared/types";
|
|
22
24
|
import type {
|
|
@@ -35,6 +37,7 @@ export type UsersPageData = {
|
|
|
35
37
|
context: {
|
|
36
38
|
clientContext: ClientContext;
|
|
37
39
|
currentOrgUnit: OrgUnitBasicData | null;
|
|
40
|
+
currentContract: ContractData | null;
|
|
38
41
|
currentUser: UserData | null;
|
|
39
42
|
rolesOptions: RoleData[];
|
|
40
43
|
};
|
|
@@ -44,6 +47,7 @@ export type UsersPageData = {
|
|
|
44
47
|
|
|
45
48
|
export type UsersPageQuery = GetUsersByOrgUnitIdServiceProps & {
|
|
46
49
|
orgUnitId: string;
|
|
50
|
+
contractId?: string;
|
|
47
51
|
search?: string;
|
|
48
52
|
page?: string;
|
|
49
53
|
};
|
|
@@ -51,7 +55,7 @@ export type UsersPageQuery = GetUsersByOrgUnitIdServiceProps & {
|
|
|
51
55
|
const loaderFunction = async (
|
|
52
56
|
data: LoaderData<UsersPageQuery>
|
|
53
57
|
): Promise<AuthRouteProps<UsersPageData>> => {
|
|
54
|
-
const { orgUnitId, search = "", page = "1" } = data.query;
|
|
58
|
+
const { orgUnitId, contractId, search = "", page = "1" } = data.query;
|
|
55
59
|
|
|
56
60
|
return withAuthLoader(
|
|
57
61
|
data,
|
|
@@ -71,30 +75,48 @@ const loaderFunction = async (
|
|
|
71
75
|
clientContext: { cookie, userId, customerId, ...clientContext },
|
|
72
76
|
currentUser: null,
|
|
73
77
|
currentOrgUnit: null,
|
|
78
|
+
currentContract: null,
|
|
74
79
|
rolesOptions: [],
|
|
75
80
|
},
|
|
76
81
|
};
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
getUsersByOrgUnitIdService({
|
|
86
|
-
cookie,
|
|
87
|
-
orgUnitId,
|
|
88
|
-
search: search ?? "",
|
|
89
|
-
page: validPage ?? 1,
|
|
90
|
-
}),
|
|
91
|
-
getUserByIdService({ orgUnitId, userId, cookie }),
|
|
92
|
-
getRolesIdsService({
|
|
84
|
+
// Users don't need contract data, but the sidebar (rendered by shared
|
|
85
|
+
// layouts) uses `currentContract` for its identity block, so fetch it
|
|
86
|
+
// when the URL carries the contract the user was last viewing.
|
|
87
|
+
const contractDetailsPromise = contractId
|
|
88
|
+
? getContractDetailsService({
|
|
89
|
+
contractId,
|
|
93
90
|
unitId: orgUnitId,
|
|
94
91
|
cookie,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
})
|
|
93
|
+
: Promise.resolve(null);
|
|
94
|
+
|
|
95
|
+
const [
|
|
96
|
+
currentOrgUnit,
|
|
97
|
+
usersResponse,
|
|
98
|
+
user,
|
|
99
|
+
rolesOptions,
|
|
100
|
+
currentContract,
|
|
101
|
+
] = await Promise.all([
|
|
102
|
+
getOrgUnitBasicDataService({
|
|
103
|
+
id: orgUnitId,
|
|
104
|
+
cookie,
|
|
105
|
+
}),
|
|
106
|
+
getUsersByOrgUnitIdService({
|
|
107
|
+
cookie,
|
|
108
|
+
orgUnitId,
|
|
109
|
+
search: search ?? "",
|
|
110
|
+
page: validPage ?? 1,
|
|
111
|
+
}),
|
|
112
|
+
getUserByIdService({ orgUnitId, userId, cookie }),
|
|
113
|
+
getRolesIdsService({
|
|
114
|
+
unitId: orgUnitId,
|
|
115
|
+
cookie,
|
|
116
|
+
customerId,
|
|
117
|
+
}),
|
|
118
|
+
contractDetailsPromise,
|
|
119
|
+
]);
|
|
98
120
|
|
|
99
121
|
const { users, total } = usersResponse;
|
|
100
122
|
|
|
@@ -116,6 +138,7 @@ const loaderFunction = async (
|
|
|
116
138
|
context: {
|
|
117
139
|
clientContext: { cookie, userId, customerId, ...clientContext },
|
|
118
140
|
currentOrgUnit,
|
|
141
|
+
currentContract,
|
|
119
142
|
currentUser: user,
|
|
120
143
|
rolesOptions,
|
|
121
144
|
},
|