@vtex/faststore-plugin-buyer-portal 2.0.30 → 2.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx +12 -1
- package/src/features/contracts/components/ContractListItem/ContractListItem.tsx +19 -11
- package/src/features/contracts/components/ContractListItem/__tests__/contractListItemMenu.test.ts +32 -0
- package/src/features/contracts/components/ContractListItem/contractListItemMenu.ts +17 -0
- package/src/features/contracts/components/ContractSelectionList/ContractSelectionListSkeleton.tsx +35 -0
- package/src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx +9 -1
- package/src/features/contracts/components/index.ts +2 -0
- package/src/features/contracts/hooks/useSwitchContract.ts +6 -4
- package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx +9 -12
- package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +90 -41
- package/src/features/contracts/layouts/ContractListingLayout/__tests__/utils.test.ts +40 -1
- package/src/features/contracts/layouts/ContractListingLayout/contract-listing-layout.scss +10 -2
- package/src/features/contracts/layouts/ContractListingLayout/utils.ts +25 -0
- package/src/features/contracts/utils/__tests__/clearPersistedSessionState.test.ts +32 -0
- package/src/features/contracts/utils/__tests__/resolveSwitchRedirect.test.ts +21 -0
- package/src/features/contracts/utils/clearPersistedSessionState.ts +43 -5
- package/src/features/contracts/utils/resolveSwitchRedirect.ts +15 -0
- package/src/features/credit-cards/services/tokenize-credit-card.service.ts +3 -1
- package/src/features/org-units/components/OrgUnitBreadcrumb/OrgUnitBreadcrumb.tsx +21 -31
- package/src/features/org-units/components/OrgUnitBreadcrumb/__tests__/buildBreadcrumbItems.test.ts +64 -0
- package/src/features/org-units/components/OrgUnitBreadcrumb/buildBreadcrumbItems.ts +36 -0
- package/src/features/org-units/components/OrgUnitDetailsNavbar/OrgUnitDetailsNavbar.tsx +67 -27
- package/src/features/org-units/components/OrgUnitDetailsNavbar/org-unit-details-navbar.scss +22 -1
- package/src/features/org-units/components/OrgUnitHeaderNav/OrgUnitHeaderNav.tsx +112 -0
- package/src/features/org-units/components/OrgUnitHeaderNav/__tests__/getOrgUnitHeaderNavItems.test.ts +83 -0
- package/src/features/org-units/components/OrgUnitHeaderNav/getOrgUnitHeaderNavItems.ts +36 -0
- package/src/features/org-units/components/OrgUnitHeaderNav/org-unit-header-nav.scss +129 -0
- package/src/features/org-units/components/index.ts +4 -0
- package/src/features/org-units/layouts/OrgUnitDetailsLayout/org-units-details.scss +1 -0
- package/src/features/org-units/types/OrgUnitBreadcrumbTypes.ts +8 -0
- package/src/features/profile/layouts/ProfileLayout/ProfileLayout.tsx +8 -31
- package/src/features/shared/hooks/index.ts +1 -0
- package/src/features/shared/hooks/useRefreshPageData.ts +35 -0
- package/src/features/shared/layouts/BaseTabsLayout/about-drawer/__tests__/aboutQueryParam.test.ts +23 -0
- package/src/features/shared/layouts/BaseTabsLayout/about-drawer/aboutQueryParam.ts +13 -0
- package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +2 -2
- package/src/features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout.tsx +6 -1
- package/src/features/shared/layouts/LoadingTabsLayout/LoadingTabsLayout.tsx +35 -2
- package/src/features/shared/layouts/SumaPageLayout/FullSidebarNav.tsx +33 -12
- package/src/features/shared/layouts/SumaPageLayout/SumaPageLayout.tsx +4 -2
- package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +26 -17
- package/src/features/shared/layouts/SumaPageLayout/__tests__/resolveOrgLinkContractId.test.ts +16 -0
- package/src/features/shared/layouts/SumaPageLayout/__tests__/sumaSidebarConfig.test.ts +27 -0
- package/src/features/shared/layouts/SumaPageLayout/resolveOrgLinkContractId.ts +13 -0
- package/src/features/shared/layouts/SumaPageLayout/sumaSidebarConfig.ts +37 -0
- package/src/features/shared/utils/__tests__/getContractSettingsLinks.test.ts +18 -0
- package/src/features/shared/utils/__tests__/getSumaListingSidebarLinks.test.ts +43 -0
- package/src/features/shared/utils/__tests__/routeLayoutMapping.test.ts +34 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/shared/utils/getContractSettingsLinks.ts +6 -1
- package/src/features/shared/utils/routeLayoutMapping.ts +9 -1
- package/src/pages/contract-information.tsx +9 -1
- package/src/pages/contracts.tsx +5 -0
- package/src/pages/home.tsx +25 -19
- package/src/pages/org-unit-details.tsx +10 -24
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
|
|
2
|
+
|
|
1
3
|
import type { ContractData } from "../../types";
|
|
2
4
|
|
|
3
5
|
export const getDefaultContractId = (contracts: ContractData[]) =>
|
|
@@ -15,3 +17,26 @@ export const resolveViewedContractId = (
|
|
|
15
17
|
contracts.some((c) => c.id === queryContractId)
|
|
16
18
|
? (queryContractId as string)
|
|
17
19
|
: getDefaultContractId(contracts);
|
|
20
|
+
|
|
21
|
+
/** Args for the `{0} contract{1} …` toast messages. */
|
|
22
|
+
export const pluralSuffixArgs = (count: number): [number, string] => [
|
|
23
|
+
count,
|
|
24
|
+
count === 1 ? "" : "s",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Query param carrying the removed-contracts count across the post-switch hard reload (AS-6). */
|
|
28
|
+
export const REMOVED_QUERY_PARAM = "removed";
|
|
29
|
+
|
|
30
|
+
export const buildRemovedRedirectUrl = (orgUnitId: string, count: number) =>
|
|
31
|
+
`${buyerPortalRoutes.contracts({
|
|
32
|
+
orgUnitId,
|
|
33
|
+
})}?${REMOVED_QUERY_PARAM}=${count}`;
|
|
34
|
+
|
|
35
|
+
export const parseRemovedCount = (
|
|
36
|
+
value: string | string[] | undefined
|
|
37
|
+
): number | null => {
|
|
38
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
39
|
+
if (!raw) return null;
|
|
40
|
+
const count = Number(raw);
|
|
41
|
+
return Number.isInteger(count) && count > 0 ? count : null;
|
|
42
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CHECKOUT_ORDER_FORM_COOKIE,
|
|
5
|
+
PERSISTED_STORE_KEYS,
|
|
6
|
+
getCookieVariants,
|
|
7
|
+
} from "../clearPersistedSessionState";
|
|
8
|
+
|
|
9
|
+
describe("clearPersistedSessionState constants", () => {
|
|
10
|
+
it("clears both the host session and the host cart", () => {
|
|
11
|
+
expect(PERSISTED_STORE_KEYS).toEqual(["fs::session", "fs::cart"]);
|
|
12
|
+
expect(CHECKOUT_ORDER_FORM_COOKIE).toBe("checkout.vtex.com");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("getCookieVariants", () => {
|
|
17
|
+
it("covers host-only, host and dot-host domains for every parent path", () => {
|
|
18
|
+
expect(
|
|
19
|
+
getCookieVariants("shop.example.com", "/pvt/organization-account")
|
|
20
|
+
).toEqual([
|
|
21
|
+
{ domain: undefined, path: "/" },
|
|
22
|
+
{ domain: undefined, path: "/pvt" },
|
|
23
|
+
{ domain: undefined, path: "/pvt/organization-account" },
|
|
24
|
+
{ domain: "shop.example.com", path: "/" },
|
|
25
|
+
{ domain: "shop.example.com", path: "/pvt" },
|
|
26
|
+
{ domain: "shop.example.com", path: "/pvt/organization-account" },
|
|
27
|
+
{ domain: ".shop.example.com", path: "/" },
|
|
28
|
+
{ domain: ".shop.example.com", path: "/pvt" },
|
|
29
|
+
{ domain: ".shop.example.com", path: "/pvt/organization-account" },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { resolveSwitchRedirect } from "../resolveSwitchRedirect";
|
|
4
|
+
|
|
5
|
+
describe("resolveSwitchRedirect", () => {
|
|
6
|
+
it("defaults to the contract's profile page", () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolveSwitchRedirect({ orgUnitId: "unit-1", contractId: "contract-2" })
|
|
9
|
+
).toBe("/pvt/organization-account/profile/unit-1/contract-2");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("honors an explicit redirect target", () => {
|
|
13
|
+
expect(
|
|
14
|
+
resolveSwitchRedirect({
|
|
15
|
+
orgUnitId: "unit-1",
|
|
16
|
+
contractId: "contract-2",
|
|
17
|
+
redirectTo: "/pvt/organization-account/contracts/unit-1?removed=2",
|
|
18
|
+
})
|
|
19
|
+
).toBe("/pvt/organization-account/contracts/unit-1?removed=2");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
const IDB_NAME = "keyval-store";
|
|
2
2
|
const IDB_STORE = "keyval";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
/** Host FastStore persisted stores (`packages/sdk`): session and cart. */
|
|
5
|
+
export const PERSISTED_STORE_KEYS = ["fs::session", "fs::cart"] as const;
|
|
6
|
+
/** Cookie holding the checkout orderForm id read by `validateCart`. */
|
|
7
|
+
export const CHECKOUT_ORDER_FORM_COOKIE = "checkout.vtex.com";
|
|
8
|
+
|
|
9
|
+
export const getCookieVariants = (hostname: string, pathname: string) => {
|
|
10
|
+
const domains: Array<string | undefined> = [
|
|
11
|
+
undefined,
|
|
12
|
+
hostname,
|
|
13
|
+
hostname.startsWith(".") ? hostname : `.${hostname}`,
|
|
14
|
+
];
|
|
15
|
+
const paths = ["/"];
|
|
16
|
+
let current = "";
|
|
17
|
+
for (const part of pathname.split("/").filter(Boolean)) {
|
|
18
|
+
current += `/${part}`;
|
|
19
|
+
if (!paths.includes(current)) paths.push(current);
|
|
20
|
+
}
|
|
21
|
+
return domains.flatMap((domain) => paths.map((path) => ({ domain, path })));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const expireCheckoutOrderFormCookie = () => {
|
|
25
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
26
|
+
try {
|
|
27
|
+
const secure = window.location.protocol === "https:" ? "; secure" : "";
|
|
28
|
+
for (const { domain, path } of getCookieVariants(
|
|
29
|
+
window.location.hostname,
|
|
30
|
+
window.location.pathname
|
|
31
|
+
)) {
|
|
32
|
+
const domainAttr = domain ? `; domain=${domain}` : "";
|
|
33
|
+
document.cookie = `${CHECKOUT_ORDER_FORM_COOKIE}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; path=${path}${domainAttr}; samesite=lax${secure}`;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// no-op: cookie expiry is best-effort and must never throw
|
|
37
|
+
}
|
|
38
|
+
};
|
|
4
39
|
|
|
5
40
|
const SESSION_STORAGE_KEYS = [
|
|
6
41
|
"faststore_session_ready",
|
|
@@ -26,7 +61,8 @@ function deletePersistedSession(): Promise<void> {
|
|
|
26
61
|
return;
|
|
27
62
|
}
|
|
28
63
|
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
29
|
-
tx.objectStore(IDB_STORE)
|
|
64
|
+
const store = tx.objectStore(IDB_STORE);
|
|
65
|
+
for (const key of PERSISTED_STORE_KEYS) store.delete(key);
|
|
30
66
|
tx.oncomplete = () => {
|
|
31
67
|
db.close();
|
|
32
68
|
resolve();
|
|
@@ -53,12 +89,14 @@ function deletePersistedSession(): Promise<void> {
|
|
|
53
89
|
}
|
|
54
90
|
|
|
55
91
|
/**
|
|
56
|
-
* Clears client-side session persistence (host FastStore session cache)
|
|
57
|
-
*
|
|
58
|
-
*
|
|
92
|
+
* Clears client-side session persistence (host FastStore session cache), the
|
|
93
|
+
* persisted cart, and the checkout orderForm cookie, so the next hard navigation
|
|
94
|
+
* creates a fresh orderForm under the new contract (B2BTEAM-3827, Product review
|
|
95
|
+
* items 1 and 3).
|
|
59
96
|
*/
|
|
60
97
|
export async function clearPersistedSessionState(): Promise<void> {
|
|
61
98
|
await deletePersistedSession();
|
|
99
|
+
expireCheckoutOrderFormCookie();
|
|
62
100
|
|
|
63
101
|
if (typeof window === "undefined" || typeof sessionStorage === "undefined") {
|
|
64
102
|
return;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buyerPortalRoutes } from "../../shared/utils/buyerPortalRoutes";
|
|
2
|
+
|
|
3
|
+
export type ResolveSwitchRedirectParams = {
|
|
4
|
+
orgUnitId: string;
|
|
5
|
+
contractId: string;
|
|
6
|
+
/** Overrides the default Profile destination (e.g. stay on the listing). */
|
|
7
|
+
redirectTo?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const resolveSwitchRedirect = ({
|
|
11
|
+
orgUnitId,
|
|
12
|
+
contractId,
|
|
13
|
+
redirectTo,
|
|
14
|
+
}: ResolveSwitchRedirectParams): string =>
|
|
15
|
+
redirectTo ?? buyerPortalRoutes.profileDetails({ orgUnitId, contractId });
|
|
@@ -27,7 +27,9 @@ export const tokenizeCreditCardService = async ({
|
|
|
27
27
|
complement: creditCard.address.streetAddress2,
|
|
28
28
|
coutry: creditCard.address.countryCode,
|
|
29
29
|
neighborhood: creditCard.address.neighborhood ?? "",
|
|
30
|
-
geoCoordinates: creditCard.address.geoCoordinates
|
|
30
|
+
geoCoordinates: creditCard.address.geoCoordinates
|
|
31
|
+
? creditCard.address.geoCoordinates.split(",")
|
|
32
|
+
: [],
|
|
31
33
|
postalCode: creditCard.address.zip,
|
|
32
34
|
number: creditCard.address.streetNumber ?? "",
|
|
33
35
|
receiverName: "NOT EMPTY",
|
|
@@ -4,16 +4,13 @@ import { Icon } from "../../../shared/components";
|
|
|
4
4
|
import { useBuyerPortal } from "../../../shared/hooks";
|
|
5
5
|
import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
|
|
6
6
|
import { resolveContractId } from "../../../shared/utils/resolveContractId";
|
|
7
|
-
import { USER_ROLES } from "../../../users/types";
|
|
8
7
|
import { useOrgUnitByUser } from "../../hooks";
|
|
9
8
|
|
|
9
|
+
import { buildOrgUnitBreadcrumbItems } from "./buildBreadcrumbItems";
|
|
10
10
|
import { OrgUnitBreadcrumbDropdown } from "./OrgUnitBreadcrumbDropdown";
|
|
11
11
|
import { OrgUnitBreadcrumbPath } from "./OrgUnitBreadcrumbPath";
|
|
12
12
|
|
|
13
|
-
import type {
|
|
14
|
-
OrgUnitBreadcrumbItem,
|
|
15
|
-
OrgUnitBreadcrumbProps,
|
|
16
|
-
} from "../../types";
|
|
13
|
+
import type { OrgUnitBreadcrumbProps } from "../../types";
|
|
17
14
|
|
|
18
15
|
export const OrgUnitBreadcrumb = ({
|
|
19
16
|
maxItems,
|
|
@@ -21,9 +18,17 @@ export const OrgUnitBreadcrumb = ({
|
|
|
21
18
|
orgUnit,
|
|
22
19
|
userId,
|
|
23
20
|
userRoles,
|
|
21
|
+
userUnitId,
|
|
24
22
|
}: OrgUnitBreadcrumbProps) => {
|
|
25
|
-
const { currentContract, currentOrgUnit } = useBuyerPortal();
|
|
26
|
-
|
|
23
|
+
const { currentContract, currentOrgUnit, featureFlags } = useBuyerPortal();
|
|
24
|
+
// Skip the fetch entirely when the caller already knows the user's unit
|
|
25
|
+
// (e.g. `currentUser.orgUnit.id`, already loaded by the page) — otherwise
|
|
26
|
+
// this fires an uncached request on every render and the breadcrumb shows
|
|
27
|
+
// no unit name for non-super-users until it resolves.
|
|
28
|
+
const { orgUnit: fetchedUserUnit } = useOrgUnitByUser(userId, {
|
|
29
|
+
lazy: Boolean(userUnitId),
|
|
30
|
+
});
|
|
31
|
+
const resolvedUserUnitId = userUnitId ?? fetchedUserUnit?.id;
|
|
27
32
|
const router = useRouter();
|
|
28
33
|
|
|
29
34
|
const contractId = resolveContractId({
|
|
@@ -32,30 +37,15 @@ export const OrgUnitBreadcrumb = ({
|
|
|
32
37
|
currentOrgUnit,
|
|
33
38
|
});
|
|
34
39
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
userRoles.includes(USER_ROLES.SUPER_USER) ||
|
|
45
|
-
(userUnitIndex > -1 && index >= userUnitIndex)
|
|
46
|
-
) {
|
|
47
|
-
acc.push({
|
|
48
|
-
unitId: orgUnitId,
|
|
49
|
-
item: buyerPortalRoutes.orgUnitDetails({ orgUnitId, contractId }),
|
|
50
|
-
name: orgUnit?.path.names.split("/")[index] ?? "",
|
|
51
|
-
position: index + 1,
|
|
52
|
-
enabled: true,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return acc;
|
|
56
|
-
},
|
|
57
|
-
[]
|
|
58
|
-
);
|
|
40
|
+
const items = buildOrgUnitBreadcrumbItems({
|
|
41
|
+
orgUnit,
|
|
42
|
+
userUnitId: resolvedUserUnitId,
|
|
43
|
+
userRoles,
|
|
44
|
+
buildLink: (orgUnitId) =>
|
|
45
|
+
featureFlags?.suma
|
|
46
|
+
? buyerPortalRoutes.contracts({ orgUnitId })
|
|
47
|
+
: buyerPortalRoutes.orgUnitDetails({ orgUnitId, contractId }),
|
|
48
|
+
});
|
|
59
49
|
|
|
60
50
|
const sortedByPosition = [...items].sort((a, b) => a.position - b.position);
|
|
61
51
|
|
package/src/features/org-units/components/OrgUnitBreadcrumb/__tests__/buildBreadcrumbItems.test.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { USER_ROLES } from "../../../../users/types";
|
|
4
|
+
import { buildOrgUnitBreadcrumbItems } from "../buildBreadcrumbItems";
|
|
5
|
+
|
|
6
|
+
import type { OrgUnitBasicData } from "../../../types";
|
|
7
|
+
|
|
8
|
+
const orgUnit = {
|
|
9
|
+
id: "child",
|
|
10
|
+
name: "Child",
|
|
11
|
+
path: { ids: "/root/mid/child", names: "Root/Mid/Child" },
|
|
12
|
+
} as OrgUnitBasicData;
|
|
13
|
+
|
|
14
|
+
const buildLink = (id: string) => `/contracts/${id}`;
|
|
15
|
+
|
|
16
|
+
describe("buildOrgUnitBreadcrumbItems", () => {
|
|
17
|
+
it("lists the whole hierarchy for a super user", () => {
|
|
18
|
+
const items = buildOrgUnitBreadcrumbItems({
|
|
19
|
+
orgUnit,
|
|
20
|
+
userUnitId: "child",
|
|
21
|
+
userRoles: [USER_ROLES.SUPER_USER],
|
|
22
|
+
buildLink,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(items.map((i) => [i.name, i.item, i.position])).toEqual([
|
|
26
|
+
["Root", "/contracts/root", 1],
|
|
27
|
+
["Mid", "/contracts/mid", 2],
|
|
28
|
+
["Child", "/contracts/child", 3],
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("starts at the user's own unit for a regular admin", () => {
|
|
33
|
+
const items = buildOrgUnitBreadcrumbItems({
|
|
34
|
+
orgUnit,
|
|
35
|
+
userUnitId: "mid",
|
|
36
|
+
userRoles: [],
|
|
37
|
+
buildLink,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(items.map((i) => i.unitId)).toEqual(["mid", "child"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns nothing when the user's unit is not in the path", () => {
|
|
44
|
+
expect(
|
|
45
|
+
buildOrgUnitBreadcrumbItems({
|
|
46
|
+
orgUnit,
|
|
47
|
+
userUnitId: "other",
|
|
48
|
+
userRoles: [],
|
|
49
|
+
buildLink,
|
|
50
|
+
})
|
|
51
|
+
).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns nothing without an org unit", () => {
|
|
55
|
+
expect(
|
|
56
|
+
buildOrgUnitBreadcrumbItems({
|
|
57
|
+
orgUnit: null,
|
|
58
|
+
userUnitId: "x",
|
|
59
|
+
userRoles: [],
|
|
60
|
+
buildLink,
|
|
61
|
+
})
|
|
62
|
+
).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { USER_ROLES } from "../../../users/types";
|
|
2
|
+
|
|
3
|
+
import type { OrgUnitBasicData, OrgUnitBreadcrumbItem } from "../../types";
|
|
4
|
+
|
|
5
|
+
export type BuildOrgUnitBreadcrumbItemsParams = {
|
|
6
|
+
orgUnit?: OrgUnitBasicData | null;
|
|
7
|
+
userUnitId?: string;
|
|
8
|
+
userRoles: string[];
|
|
9
|
+
buildLink: (orgUnitId: string) => string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const buildOrgUnitBreadcrumbItems = ({
|
|
13
|
+
orgUnit,
|
|
14
|
+
userUnitId,
|
|
15
|
+
userRoles,
|
|
16
|
+
buildLink,
|
|
17
|
+
}: BuildOrgUnitBreadcrumbItemsParams): OrgUnitBreadcrumbItem[] => {
|
|
18
|
+
const ids = orgUnit?.path.ids.split("/").slice(1) ?? [];
|
|
19
|
+
const names = orgUnit?.path.names.split("/") ?? [];
|
|
20
|
+
const userUnitIndex = ids.findIndex((id) => id === userUnitId);
|
|
21
|
+
const isSuperUser = userRoles.includes(USER_ROLES.SUPER_USER);
|
|
22
|
+
|
|
23
|
+
return ids.flatMap((unitId, index) =>
|
|
24
|
+
isSuperUser || (userUnitIndex > -1 && index >= userUnitIndex)
|
|
25
|
+
? [
|
|
26
|
+
{
|
|
27
|
+
unitId,
|
|
28
|
+
item: buildLink(unitId),
|
|
29
|
+
name: names[index] ?? "",
|
|
30
|
+
position: index + 1,
|
|
31
|
+
enabled: true,
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
: []
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
1
3
|
import storeConfig from "discovery.config";
|
|
2
4
|
|
|
3
5
|
import dynamic from "next/dynamic";
|
|
6
|
+
import { useRouter } from "next/router";
|
|
4
7
|
|
|
5
8
|
import { useLink_unstable as useLink } from "@faststore/core/experimental";
|
|
6
9
|
import { Dropdown, DropdownMenu, IconButton, Link } from "@faststore/ui";
|
|
@@ -15,11 +18,19 @@ const LocalizationButton = dynamic(
|
|
|
15
18
|
|
|
16
19
|
import { ProfileSummary } from "../../../profile/components";
|
|
17
20
|
import { BasicDropdownMenu, Icon } from "../../../shared/components";
|
|
18
|
-
import { useDrawerProps } from "../../../shared/hooks";
|
|
21
|
+
import { useBuyerPortal, useDrawerProps } from "../../../shared/hooks";
|
|
19
22
|
import { AboutDrawer } from "../../../shared/layouts/BaseTabsLayout/about-drawer/AboutDrawer";
|
|
23
|
+
import {
|
|
24
|
+
ABOUT_QUERY_PARAM,
|
|
25
|
+
shouldOpenAboutDrawer,
|
|
26
|
+
} from "../../../shared/layouts/BaseTabsLayout/about-drawer/aboutQueryParam";
|
|
20
27
|
import { SumaSidebarDrawer } from "../../../shared/layouts/SumaPageLayout/SumaSidebarDrawer";
|
|
21
28
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
22
29
|
import { doLogout } from "../../../shared/utils";
|
|
30
|
+
import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
|
|
31
|
+
import { OrgUnitHeaderNav } from "../OrgUnitHeaderNav/OrgUnitHeaderNav";
|
|
32
|
+
|
|
33
|
+
import type { SumaSidebarVariant } from "../../../shared/layouts/SumaPageLayout/sumaSidebarConfig";
|
|
23
34
|
|
|
24
35
|
export type OrgUnitDetailsNavbarProps = {
|
|
25
36
|
orgId: string;
|
|
@@ -32,7 +43,7 @@ export type OrgUnitDetailsNavbarProps = {
|
|
|
32
43
|
email?: string;
|
|
33
44
|
};
|
|
34
45
|
loading?: boolean;
|
|
35
|
-
sidebarVariant?:
|
|
46
|
+
sidebarVariant?: SumaSidebarVariant;
|
|
36
47
|
showBackLink?: boolean;
|
|
37
48
|
};
|
|
38
49
|
|
|
@@ -46,6 +57,9 @@ export const OrgUnitDetailsNavbar = ({
|
|
|
46
57
|
}: OrgUnitDetailsNavbarProps) => {
|
|
47
58
|
const { t } = useLocalization();
|
|
48
59
|
const { resolveLink } = useLink();
|
|
60
|
+
const router = useRouter();
|
|
61
|
+
const { currentOrgUnit, currentUser, featureFlags } = useBuyerPortal();
|
|
62
|
+
const isSuma = Boolean(featureFlags?.suma);
|
|
49
63
|
const {
|
|
50
64
|
open: openAboutMenu,
|
|
51
65
|
isOpen: isOpenAboutMenu,
|
|
@@ -58,6 +72,22 @@ export const OrgUnitDetailsNavbar = ({
|
|
|
58
72
|
...sidebarMenuDrawerProps
|
|
59
73
|
} = useDrawerProps();
|
|
60
74
|
|
|
75
|
+
// `?about=1` opens the About/version drawer and is stripped from the URL.
|
|
76
|
+
// Under SUMA there is no visible trigger (QA-11), so this is the way to
|
|
77
|
+
// read the installed versions on any Organization Account page.
|
|
78
|
+
const aboutParam = router.query[ABOUT_QUERY_PARAM];
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!shouldOpenAboutDrawer(aboutParam)) return;
|
|
81
|
+
openAboutMenu();
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
83
|
+
const { [ABOUT_QUERY_PARAM]: _about, ...rest } = router.query;
|
|
84
|
+
void router
|
|
85
|
+
.replace({ pathname: router.pathname, query: rest }, undefined, {
|
|
86
|
+
shallow: true,
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {});
|
|
89
|
+
}, [aboutParam]);
|
|
90
|
+
|
|
61
91
|
return (
|
|
62
92
|
<header data-fs-bp-org-unit-details-navbar>
|
|
63
93
|
<div data-fs-bp-org-unit-details-navbar-heading>
|
|
@@ -73,17 +103,24 @@ export const OrgUnitDetailsNavbar = ({
|
|
|
73
103
|
{storeConfig.seo.title}
|
|
74
104
|
</Link>
|
|
75
105
|
</div>
|
|
106
|
+
{isSuma && currentOrgUnit && (
|
|
107
|
+
<div data-fs-bp-org-unit-details-navbar-unit>
|
|
108
|
+
<OrgUnitHeaderNav
|
|
109
|
+
orgUnit={currentOrgUnit}
|
|
110
|
+
userId={currentUser?.id ?? ""}
|
|
111
|
+
userUnitId={currentUser?.orgUnit?.id}
|
|
112
|
+
userRoles={currentUser?.roles ?? []}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
76
116
|
<div data-fs-bp-org-unit-details-navbar-actions>
|
|
77
|
-
{/* TO-DO: Reenable button when agent is made ready for users */}
|
|
78
|
-
{/*
|
|
79
117
|
<Link
|
|
80
118
|
href={buyerPortalRoutes.b2bAgent}
|
|
81
119
|
data-fs-bp-org-unit-details-navbar-agent-link
|
|
82
120
|
>
|
|
83
121
|
<Icon name="Sparkle" width={20} height={20} weight="regular" />
|
|
84
|
-
|
|
122
|
+
{t("shared.navigation.organizationManager")}
|
|
85
123
|
</Link>
|
|
86
|
-
*/}
|
|
87
124
|
|
|
88
125
|
<LocalizationButton
|
|
89
126
|
saveLabel={t("shared.buttons.save")}
|
|
@@ -91,27 +128,30 @@ export const OrgUnitDetailsNavbar = ({
|
|
|
91
128
|
languageLabel={t("layouts.labels.language")}
|
|
92
129
|
icon="Globe"
|
|
93
130
|
/>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
{!isSuma && (
|
|
132
|
+
<Link
|
|
133
|
+
target="_blank"
|
|
134
|
+
href={resolveLink("/")}
|
|
135
|
+
data-fs-bp-org-unit-details-navbar-start-shopping-link
|
|
136
|
+
>
|
|
137
|
+
{t("layouts.navigation.startShopping")}
|
|
138
|
+
</Link>
|
|
139
|
+
)}
|
|
140
|
+
{!isSuma && (
|
|
141
|
+
<IconButton
|
|
142
|
+
data-fs-bp-nav-about-trigger
|
|
143
|
+
icon={
|
|
144
|
+
<Icon
|
|
145
|
+
name="CircleQuestionMark"
|
|
146
|
+
width={24}
|
|
147
|
+
height={24}
|
|
148
|
+
weight="regular"
|
|
149
|
+
/>
|
|
150
|
+
}
|
|
151
|
+
aria-label={"open about menu"}
|
|
152
|
+
onClick={openAboutMenu}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
115
155
|
<Dropdown>
|
|
116
156
|
<BasicDropdownMenu.Trigger iconSize={20} iconName="AccountCircle" />
|
|
117
157
|
<DropdownMenu align="right" data-fs-bp-org-unit-details-navbar-menu>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@use "~@faststore/ui/src/styles/base/utilities" as u;
|
|
2
2
|
|
|
3
3
|
[data-fs-bp-org-unit-details-navbar] {
|
|
4
|
-
|
|
4
|
+
position: relative;
|
|
5
5
|
display: flex;
|
|
6
6
|
justify-content: space-between;
|
|
7
7
|
align-items: center;
|
|
@@ -21,6 +21,27 @@
|
|
|
21
21
|
gap: var(--fs-bp-gap-2);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Current unit control: sits between the store name and the actions on
|
|
25
|
+
// small screens and is centered on the header (prototype) from notebook up.
|
|
26
|
+
[data-fs-bp-org-unit-details-navbar-unit] {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex: 1 1 auto;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
padding: 0 var(--fs-bp-padding-3);
|
|
33
|
+
|
|
34
|
+
@include u.media(">=notebook") {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: 50%;
|
|
37
|
+
left: 50%;
|
|
38
|
+
flex: none;
|
|
39
|
+
max-width: 40%;
|
|
40
|
+
padding: 0;
|
|
41
|
+
transform: translate(-50%, -50%);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
[data-fs-localization-button] {
|
|
25
46
|
[data-fs-button-wrapper] {
|
|
26
47
|
span {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useRouter } from "next/router";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Dropdown,
|
|
5
|
+
DropdownButton,
|
|
6
|
+
DropdownItem,
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
} from "@faststore/ui";
|
|
9
|
+
|
|
10
|
+
import { Icon } from "../../../shared/components";
|
|
11
|
+
import { useBuyerPortal } from "../../../shared/hooks";
|
|
12
|
+
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
13
|
+
import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
|
|
14
|
+
import { useOrgUnitByUser } from "../../hooks";
|
|
15
|
+
|
|
16
|
+
import { getOrgUnitHeaderNavItems } from "./getOrgUnitHeaderNavItems";
|
|
17
|
+
|
|
18
|
+
import type { OrgUnitBasicData } from "../../types";
|
|
19
|
+
|
|
20
|
+
export type OrgUnitHeaderNavProps = {
|
|
21
|
+
orgUnit: OrgUnitBasicData;
|
|
22
|
+
userId: string;
|
|
23
|
+
userRoles: string[];
|
|
24
|
+
/** The user's own unit id when already known; skips the lookup. */
|
|
25
|
+
userUnitId?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Header control of the SUMA shell: a pill with the current Organization Unit
|
|
30
|
+
* and, when the user may go up the hierarchy, a dropdown listing the parent
|
|
31
|
+
* units (root first) with the current unit highlighted at the bottom.
|
|
32
|
+
* Selecting a unit lands on its Contracts listing (unit view).
|
|
33
|
+
* Mirrors `Header.tsx` in the `multiple-contracts` prototype.
|
|
34
|
+
*/
|
|
35
|
+
export const OrgUnitHeaderNav = ({
|
|
36
|
+
orgUnit,
|
|
37
|
+
userId,
|
|
38
|
+
userRoles,
|
|
39
|
+
userUnitId,
|
|
40
|
+
}: OrgUnitHeaderNavProps) => {
|
|
41
|
+
const { t } = useLocalization();
|
|
42
|
+
const { push } = useRouter();
|
|
43
|
+
const { featureFlags } = useBuyerPortal();
|
|
44
|
+
const { orgUnit: fetchedUserUnit } = useOrgUnitByUser(userId, {
|
|
45
|
+
lazy: Boolean(userUnitId),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { ancestors, canNavigate } = getOrgUnitHeaderNavItems({
|
|
49
|
+
orgUnit,
|
|
50
|
+
userUnitId: userUnitId ?? fetchedUserUnit?.id,
|
|
51
|
+
userRoles,
|
|
52
|
+
buildLink: (orgUnitId) =>
|
|
53
|
+
featureFlags?.suma
|
|
54
|
+
? buyerPortalRoutes.contracts({ orgUnitId })
|
|
55
|
+
: buyerPortalRoutes.orgUnitDetails({ orgUnitId }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div data-fs-bp-org-unit-header-nav>
|
|
60
|
+
<Dropdown>
|
|
61
|
+
<DropdownButton asChild>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
data-fs-bp-org-unit-header-nav-trigger
|
|
65
|
+
disabled={!canNavigate}
|
|
66
|
+
aria-label={t("layouts.navigation.organizationalUnits")}
|
|
67
|
+
>
|
|
68
|
+
<Icon
|
|
69
|
+
name="FilledFolder"
|
|
70
|
+
width={20}
|
|
71
|
+
height={20}
|
|
72
|
+
data-fs-bp-org-unit-header-nav-icon
|
|
73
|
+
/>
|
|
74
|
+
<span data-fs-bp-org-unit-header-nav-name>{orgUnit.name}</span>
|
|
75
|
+
{canNavigate && (
|
|
76
|
+
<Icon
|
|
77
|
+
name="ArrowDropDown"
|
|
78
|
+
width={20}
|
|
79
|
+
height={20}
|
|
80
|
+
data-fs-bp-org-unit-header-nav-caret
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
</button>
|
|
84
|
+
</DropdownButton>
|
|
85
|
+
|
|
86
|
+
<DropdownMenu align="center" data-fs-bp-org-unit-header-nav-menu>
|
|
87
|
+
{ancestors.map((ancestor) => (
|
|
88
|
+
<DropdownItem
|
|
89
|
+
key={ancestor.unitId}
|
|
90
|
+
data-fs-bp-org-unit-header-nav-item
|
|
91
|
+
onClick={() => push(ancestor.item)}
|
|
92
|
+
>
|
|
93
|
+
<Icon name="Folder" width={20} height={20} />
|
|
94
|
+
<span data-fs-bp-org-unit-header-nav-item-name>
|
|
95
|
+
{ancestor.name}
|
|
96
|
+
</span>
|
|
97
|
+
</DropdownItem>
|
|
98
|
+
))}
|
|
99
|
+
<DropdownItem
|
|
100
|
+
data-fs-bp-org-unit-header-nav-item
|
|
101
|
+
data-fs-bp-org-unit-header-nav-item-current
|
|
102
|
+
aria-current="page"
|
|
103
|
+
dismissOnClick
|
|
104
|
+
>
|
|
105
|
+
<Icon name="FilledFolder" width={20} height={20} />
|
|
106
|
+
<span data-fs-bp-org-unit-header-nav-item-name>{orgUnit.name}</span>
|
|
107
|
+
</DropdownItem>
|
|
108
|
+
</DropdownMenu>
|
|
109
|
+
</Dropdown>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|