@vtex/faststore-plugin-buyer-portal 2.0.30 → 2.0.31
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/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
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { useUI } from "@faststore/ui";
|
|
4
4
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
buildRangeSet,
|
|
16
16
|
buildToggleSet,
|
|
17
17
|
} from "../../hooks/contractSelectionHelpers";
|
|
18
|
+
import { pluralSuffixArgs } from "../../layouts/ContractListingLayout/utils";
|
|
18
19
|
|
|
19
20
|
export type AddContractsDrawerProps = Omit<BasicDrawerProps, "children"> & {
|
|
20
21
|
orgUnitId: string;
|
|
@@ -43,10 +44,19 @@ export const AddContractsDrawer = ({
|
|
|
43
44
|
fetchAvailableContracts,
|
|
44
45
|
} = useListAvailableContracts(orgUnitId);
|
|
45
46
|
|
|
47
|
+
const pendingAddCountRef = useRef(0);
|
|
48
|
+
|
|
46
49
|
const { addContracts, isAddContractsLoading } = useAddContracts({
|
|
47
50
|
onSuccess: () => {
|
|
48
51
|
onSuccess();
|
|
49
52
|
drawerProps.onDismiss?.();
|
|
53
|
+
pushToast({
|
|
54
|
+
message: t(
|
|
55
|
+
"contracts.toasts.addSuccess",
|
|
56
|
+
pluralSuffixArgs(pendingAddCountRef.current)
|
|
57
|
+
),
|
|
58
|
+
status: "INFO",
|
|
59
|
+
});
|
|
50
60
|
},
|
|
51
61
|
onError: () => {
|
|
52
62
|
drawerProps.onDismiss?.();
|
|
@@ -129,6 +139,7 @@ export const AddContractsDrawer = ({
|
|
|
129
139
|
};
|
|
130
140
|
|
|
131
141
|
const handleConfirm = () => {
|
|
142
|
+
pendingAddCountRef.current = selectedIds.size;
|
|
132
143
|
addContracts({
|
|
133
144
|
orgUnitId,
|
|
134
145
|
ids: Array.from(selectedIds),
|
|
@@ -6,6 +6,8 @@ import { useLocalization } from "../../../shared/localization/LocalizationContex
|
|
|
6
6
|
import { useSwitchContract } from "../../hooks";
|
|
7
7
|
import { ContractSwitchOverlay } from "../ContractSwitchOverlay/ContractSwitchOverlay";
|
|
8
8
|
|
|
9
|
+
import { getContractMenuState } from "./contractListItemMenu";
|
|
10
|
+
|
|
9
11
|
import type { ContractData } from "../../types";
|
|
10
12
|
|
|
11
13
|
export type ContractListItemProps = {
|
|
@@ -30,6 +32,10 @@ export const ContractListItem = ({
|
|
|
30
32
|
const { pushToast } = useUI();
|
|
31
33
|
const { switchContract, loading } = useSwitchContract();
|
|
32
34
|
const isRemovable = contract.isAttached !== false;
|
|
35
|
+
const menuState = getContractMenuState({
|
|
36
|
+
isDefault: contract.isDefault,
|
|
37
|
+
isRemovable,
|
|
38
|
+
});
|
|
33
39
|
|
|
34
40
|
const handleCheckboxClick = (event: React.MouseEvent) => {
|
|
35
41
|
event.stopPropagation();
|
|
@@ -97,18 +103,20 @@ export const ContractListItem = ({
|
|
|
97
103
|
<Dropdown>
|
|
98
104
|
<BasicDropdownMenu.Trigger />
|
|
99
105
|
<BasicDropdownMenu>
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
{menuState.showSetDefault && (
|
|
107
|
+
<>
|
|
108
|
+
<DropdownItem
|
|
109
|
+
onClick={
|
|
110
|
+
menuState.setDefaultEnabled ? onSetDefault : undefined
|
|
111
|
+
}
|
|
112
|
+
disabled={!menuState.setDefaultEnabled}
|
|
113
|
+
>
|
|
114
|
+
<Icon name="StarOutline" width={16} height={16} />
|
|
115
|
+
{t("contracts.actions.setAsDefault")}
|
|
116
|
+
</DropdownItem>
|
|
117
|
+
<BasicDropdownMenu.Separator />
|
|
118
|
+
</>
|
|
110
119
|
)}
|
|
111
|
-
<BasicDropdownMenu.Separator />
|
|
112
120
|
<DropdownItem
|
|
113
121
|
data-fs-bp-dropdown-menu-item-mode="danger"
|
|
114
122
|
disabled={!isRemovable}
|
package/src/features/contracts/components/ContractListItem/__tests__/contractListItemMenu.test.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { getContractMenuState } from "../contractListItemMenu";
|
|
4
|
+
|
|
5
|
+
describe("getContractMenuState", () => {
|
|
6
|
+
it("hides 'Set as default' when the contract is already the default", () => {
|
|
7
|
+
expect(
|
|
8
|
+
getContractMenuState({ isDefault: true, isRemovable: true })
|
|
9
|
+
).toEqual({
|
|
10
|
+
showSetDefault: false,
|
|
11
|
+
setDefaultEnabled: false,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("shows and enables 'Set as default' for a removable non-default contract", () => {
|
|
16
|
+
expect(
|
|
17
|
+
getContractMenuState({ isDefault: false, isRemovable: true })
|
|
18
|
+
).toEqual({
|
|
19
|
+
showSetDefault: true,
|
|
20
|
+
setDefaultEnabled: true,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("shows but disables 'Set as default' for a legacy (non-attached) contract", () => {
|
|
25
|
+
expect(
|
|
26
|
+
getContractMenuState({ isDefault: undefined, isRemovable: false })
|
|
27
|
+
).toEqual({
|
|
28
|
+
showSetDefault: true,
|
|
29
|
+
setDefaultEnabled: false,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ContractMenuState = {
|
|
2
|
+
/** The default contract never offers "Set as default" (AS-8). */
|
|
3
|
+
showSetDefault: boolean;
|
|
4
|
+
/** Legacy non-attached contracts keep the item visible but disabled. */
|
|
5
|
+
setDefaultEnabled: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getContractMenuState = ({
|
|
9
|
+
isDefault,
|
|
10
|
+
isRemovable,
|
|
11
|
+
}: {
|
|
12
|
+
isDefault?: boolean;
|
|
13
|
+
isRemovable: boolean;
|
|
14
|
+
}): ContractMenuState => ({
|
|
15
|
+
showSetDefault: !isDefault,
|
|
16
|
+
setDefaultEnabled: !isDefault && isRemovable,
|
|
17
|
+
});
|
package/src/features/contracts/components/ContractSelectionList/ContractSelectionListSkeleton.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Skeleton } from "@faststore/ui";
|
|
2
|
+
|
|
3
|
+
export type ContractSelectionListSkeletonProps = {
|
|
4
|
+
/** Number of placeholder rows; defaults to a typical page. */
|
|
5
|
+
rows?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Placeholder rows shaped like `ContractListItem`, shown while the listing
|
|
10
|
+
* loads (route transition) or refreshes after adding/removing contracts.
|
|
11
|
+
*/
|
|
12
|
+
export const ContractSelectionListSkeleton = ({
|
|
13
|
+
rows = 4,
|
|
14
|
+
}: ContractSelectionListSkeletonProps) => (
|
|
15
|
+
<div
|
|
16
|
+
data-fs-bp-contract-selection-list
|
|
17
|
+
data-fs-bp-contract-selection-list-skeleton
|
|
18
|
+
aria-busy="true"
|
|
19
|
+
aria-live="polite"
|
|
20
|
+
>
|
|
21
|
+
<div data-fs-bp-contract-selection-list-rows>
|
|
22
|
+
{Array.from({ length: Math.max(rows, 1) }, (_, index) => (
|
|
23
|
+
<div
|
|
24
|
+
data-fs-bp-contract-list-item
|
|
25
|
+
data-fs-bp-contract-list-item-skeleton
|
|
26
|
+
key={`contract-skeleton-${index}`}
|
|
27
|
+
>
|
|
28
|
+
<Skeleton size={{ width: "1.25rem", height: "1.25rem" }} />
|
|
29
|
+
<Skeleton size={{ width: "2rem", height: "2rem" }} />
|
|
30
|
+
<Skeleton size={{ width: "14rem", height: "1.25rem" }} />
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useBuyerPortal } from "../../../shared/hooks";
|
|
1
2
|
import { ContractTabsLayout } from "../../../shared/layouts/ContractTabsLayout/ContractTabsLayout";
|
|
2
3
|
import { LoadingTabsLayoutContent } from "../../../shared/layouts/LoadingTabsLayout/LoadingTabsLayout";
|
|
3
4
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
@@ -21,6 +22,7 @@ export const ContractSwitchOverlay = ({
|
|
|
21
22
|
contractId,
|
|
22
23
|
}: ContractSwitchOverlayProps) => {
|
|
23
24
|
const { t } = useLocalization();
|
|
25
|
+
const { featureFlags } = useBuyerPortal();
|
|
24
26
|
|
|
25
27
|
return (
|
|
26
28
|
<div data-fs-bp-contract-switch-overlay role="alert" aria-busy="true">
|
|
@@ -32,7 +34,13 @@ export const ContractSwitchOverlay = ({
|
|
|
32
34
|
pageName={t("layouts.tabs.contract")}
|
|
33
35
|
loading
|
|
34
36
|
>
|
|
35
|
-
<LoadingTabsLayoutContent
|
|
37
|
+
<LoadingTabsLayoutContent
|
|
38
|
+
title={t(
|
|
39
|
+
featureFlags?.suma
|
|
40
|
+
? "layouts.navigation.basicInformation"
|
|
41
|
+
: "layouts.titles.profile"
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
36
44
|
</ContractTabsLayout>
|
|
37
45
|
</div>
|
|
38
46
|
);
|
|
@@ -3,6 +3,8 @@ export { ContractListItem } from "./ContractListItem/ContractListItem";
|
|
|
3
3
|
export type { ContractListItemProps } from "./ContractListItem/ContractListItem";
|
|
4
4
|
export { ContractSelectionList } from "./ContractSelectionList/ContractSelectionList";
|
|
5
5
|
export type { ContractSelectionListProps } from "./ContractSelectionList/ContractSelectionList";
|
|
6
|
+
export { ContractSelectionListSkeleton } from "./ContractSelectionList/ContractSelectionListSkeleton";
|
|
7
|
+
export type { ContractSelectionListSkeletonProps } from "./ContractSelectionList/ContractSelectionListSkeleton";
|
|
6
8
|
export { ConfirmRemoveDrawer } from "./ConfirmRemoveDrawer/ConfirmRemoveDrawer";
|
|
7
9
|
export type { ConfirmRemoveDrawerProps } from "./ConfirmRemoveDrawer/ConfirmRemoveDrawer";
|
|
8
10
|
export { ErrorRemoveDrawer } from "./ErrorRemoveDrawer/ErrorRemoveDrawer";
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
|
|
3
|
-
import { buyerPortalRoutes } from "../../shared/utils/buyerPortalRoutes";
|
|
4
3
|
import {
|
|
5
4
|
ContractSwitchError,
|
|
6
5
|
changeContractToken,
|
|
7
6
|
} from "../utils/changeContractToken";
|
|
8
7
|
import { clearPersistedSessionState } from "../utils/clearPersistedSessionState";
|
|
8
|
+
import { resolveSwitchRedirect } from "../utils/resolveSwitchRedirect";
|
|
9
9
|
|
|
10
10
|
import { useContractSwitchChannel } from "./useContractSwitchChannel";
|
|
11
11
|
|
|
12
12
|
export type SwitchContractParams = {
|
|
13
13
|
orgUnitId: string;
|
|
14
|
+
/** Where to land after the switch. Defaults to the contract's Profile page. */
|
|
15
|
+
redirectTo?: string;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
export type UseSwitchContractReturn = {
|
|
@@ -28,7 +30,7 @@ export type UseSwitchContractReturn = {
|
|
|
28
30
|
* 1. POST switch-properties with the target contract id and apply the returned auth cookie.
|
|
29
31
|
* 2. Clear persisted host session state.
|
|
30
32
|
* 3. Broadcast to other tabs so they reload under the new context.
|
|
31
|
-
* 4. Full-navigate to the contract's profile page so SSR re-runs with the new cookie.
|
|
33
|
+
* 4. Full-navigate to `redirectTo` (default: the contract's profile page) so SSR re-runs with the new cookie.
|
|
32
34
|
*
|
|
33
35
|
* On failure the previous contract stays active and `switchContract` resolves to `false`.
|
|
34
36
|
*/
|
|
@@ -39,7 +41,7 @@ export function useSwitchContract(): UseSwitchContractReturn {
|
|
|
39
41
|
|
|
40
42
|
const switchContract = async (
|
|
41
43
|
contractId: string,
|
|
42
|
-
{ orgUnitId }: SwitchContractParams
|
|
44
|
+
{ orgUnitId, redirectTo }: SwitchContractParams
|
|
43
45
|
): Promise<boolean> => {
|
|
44
46
|
setLoading(true);
|
|
45
47
|
setError(null);
|
|
@@ -52,7 +54,7 @@ export function useSwitchContract(): UseSwitchContractReturn {
|
|
|
52
54
|
|
|
53
55
|
if (typeof window !== "undefined") {
|
|
54
56
|
window.location.assign(
|
|
55
|
-
|
|
57
|
+
resolveSwitchRedirect({ orgUnitId, contractId, redirectTo })
|
|
56
58
|
);
|
|
57
59
|
}
|
|
58
60
|
|
package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import { ConfirmRemoveDrawer } from "../../components/ConfirmRemoveDrawer/Confir
|
|
|
21
21
|
import { ErrorRemoveDrawer } from "../../components/ErrorRemoveDrawer/ErrorRemoveDrawer";
|
|
22
22
|
import { useContractSwitchChannel, useSetDefaultContract } from "../../hooks";
|
|
23
23
|
import { removeContractService } from "../../services";
|
|
24
|
+
import { pluralSuffixArgs } from "../ContractListingLayout/utils";
|
|
24
25
|
|
|
25
26
|
import type { OrgUnitBasicData } from "../../../org-units/types";
|
|
26
27
|
import type { FeatureFlags } from "../../../shared/components/BuyerPortalProvider/BuyerPortalProvider";
|
|
@@ -125,6 +126,7 @@ export const ContractInformationLayout = ({
|
|
|
125
126
|
const router = useRouter();
|
|
126
127
|
const { broadcast } = useContractSwitchChannel();
|
|
127
128
|
const { featureFlags } = useBuyerPortal();
|
|
129
|
+
const { pushToast } = useUI();
|
|
128
130
|
|
|
129
131
|
const contract = contracts.find((c) => c.id === contractId);
|
|
130
132
|
const isSingleContract = contracts.length <= 1;
|
|
@@ -144,8 +146,12 @@ export const ContractInformationLayout = ({
|
|
|
144
146
|
|
|
145
147
|
const handleRemoveSuccess = useCallback(() => {
|
|
146
148
|
broadcast({ type: "contract-removed", contractId });
|
|
149
|
+
pushToast({
|
|
150
|
+
message: t("contracts.toasts.removeSuccess", pluralSuffixArgs(1)),
|
|
151
|
+
status: "INFO",
|
|
152
|
+
});
|
|
147
153
|
router.push(buyerPortalRoutes.contracts({ orgUnitId: orgUnit.id }));
|
|
148
|
-
}, [broadcast, contractId, orgUnit.id, router]);
|
|
154
|
+
}, [broadcast, contractId, orgUnit.id, router, pushToast, t]);
|
|
149
155
|
|
|
150
156
|
const handleRemoveError = useCallback(() => {
|
|
151
157
|
confirmDrawerProps.close();
|
|
@@ -173,7 +179,6 @@ export const ContractInformationLayout = ({
|
|
|
173
179
|
openConfirmDrawer();
|
|
174
180
|
};
|
|
175
181
|
|
|
176
|
-
const { pushToast } = useUI();
|
|
177
182
|
const { setDefaultContract, isSetDefaultContractLoading } =
|
|
178
183
|
useSetDefaultContract({
|
|
179
184
|
onSuccess: () => {
|
|
@@ -224,7 +229,7 @@ export const ContractInformationLayout = ({
|
|
|
224
229
|
<Link
|
|
225
230
|
href={contractsUrl}
|
|
226
231
|
data-fs-bp-contract-information-back-link
|
|
227
|
-
aria-label="
|
|
232
|
+
aria-label={t("layouts.navigation.contracts")}
|
|
228
233
|
>
|
|
229
234
|
<Icon name="ArrowPointLeft" width={16} height={16} />
|
|
230
235
|
{t("layouts.navigation.contracts")}
|
|
@@ -239,15 +244,7 @@ export const ContractInformationLayout = ({
|
|
|
239
244
|
<Dropdown>
|
|
240
245
|
<BasicDropdownMenu.Trigger />
|
|
241
246
|
<BasicDropdownMenu>
|
|
242
|
-
{contract
|
|
243
|
-
<DropdownItem
|
|
244
|
-
data-fs-bp-contract-information-more-menu-set-default
|
|
245
|
-
disabled
|
|
246
|
-
>
|
|
247
|
-
<Icon name="Star" width={16} height={16} />
|
|
248
|
-
{t("contracts.actions.setAsDefault")}
|
|
249
|
-
</DropdownItem>
|
|
250
|
-
) : (
|
|
247
|
+
{contract && !contract.isDefault && (
|
|
251
248
|
<DropdownItem
|
|
252
249
|
data-fs-bp-contract-information-more-menu-set-default
|
|
253
250
|
onClick={handleSetDefault}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
useDrawerProps,
|
|
19
19
|
useBuyerPortal,
|
|
20
|
+
useRefreshPageData,
|
|
20
21
|
useReloadOnBfcacheRestore,
|
|
21
22
|
} from "../../../shared/hooks";
|
|
22
23
|
import { GlobalLayout } from "../../../shared/layouts";
|
|
@@ -29,6 +30,7 @@ import { ConfirmRemoveDrawer } from "../../components/ConfirmRemoveDrawer/Confir
|
|
|
29
30
|
import { ContractSelectAllRow } from "../../components/ContractSelectAllRow";
|
|
30
31
|
import { ContractSelectionBar } from "../../components/ContractSelectionBar";
|
|
31
32
|
import { ContractSelectionList } from "../../components/ContractSelectionList/ContractSelectionList";
|
|
33
|
+
import { ContractSelectionListSkeleton } from "../../components/ContractSelectionList/ContractSelectionListSkeleton";
|
|
32
34
|
import { ContractsListHeader } from "../../components/ContractsListHeader";
|
|
33
35
|
import { UnableToRemoveContractsDrawer } from "../../components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer";
|
|
34
36
|
import {
|
|
@@ -41,7 +43,14 @@ import {
|
|
|
41
43
|
} from "../../hooks";
|
|
42
44
|
|
|
43
45
|
import { buildContractSettingsNavItems } from "./ContractSettingsNav";
|
|
44
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
REMOVED_QUERY_PARAM,
|
|
48
|
+
buildRemovedRedirectUrl,
|
|
49
|
+
getDefaultContractId,
|
|
50
|
+
parseRemovedCount,
|
|
51
|
+
pluralSuffixArgs,
|
|
52
|
+
resolveViewedContractId,
|
|
53
|
+
} from "./utils";
|
|
45
54
|
|
|
46
55
|
import type { OrgUnitBasicData } from "../../../org-units/types";
|
|
47
56
|
import type { UserData } from "../../../users/types";
|
|
@@ -65,6 +74,10 @@ export const ContractListingLayout = ({
|
|
|
65
74
|
const router = useRouter();
|
|
66
75
|
const { pushToast } = useUI();
|
|
67
76
|
const { subscribe } = useContractSwitchChannel();
|
|
77
|
+
// Re-runs the loader after add/remove/set-default; `isRefreshing` drives the
|
|
78
|
+
// inline skeleton (a same-URL replace shows no route-change skeleton).
|
|
79
|
+
const { refresh, isRefreshing } = useRefreshPageData();
|
|
80
|
+
const isBusy = loading || isRefreshing;
|
|
68
81
|
|
|
69
82
|
const queryContractId = Array.isArray(router.query.contractId)
|
|
70
83
|
? router.query.contractId[0]
|
|
@@ -78,10 +91,14 @@ export const ContractListingLayout = ({
|
|
|
78
91
|
listAccountingFieldData,
|
|
79
92
|
listAccountingFieldLoading,
|
|
80
93
|
listAccountingFieldRefresh,
|
|
81
|
-
} = useListAccountingFields(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
} = useListAccountingFields(
|
|
95
|
+
{
|
|
96
|
+
orgUnitId: orgUnit.id,
|
|
97
|
+
contractId: defaultContractId,
|
|
98
|
+
},
|
|
99
|
+
// No contract to scope by (route-transition skeleton) → nothing to fetch.
|
|
100
|
+
{ lazy: !defaultContractId }
|
|
101
|
+
);
|
|
85
102
|
|
|
86
103
|
const {
|
|
87
104
|
open: openAccountingFieldDrawer,
|
|
@@ -130,27 +147,31 @@ export const ContractListingLayout = ({
|
|
|
130
147
|
const fallbackContractId = getDefaultContractId(remainingContracts);
|
|
131
148
|
|
|
132
149
|
if (fallbackContractId) {
|
|
133
|
-
switchContract(fallbackContractId, {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
switchContract(fallbackContractId, {
|
|
151
|
+
orgUnitId: orgUnit.id,
|
|
152
|
+
redirectTo: buildRemovedRedirectUrl(
|
|
153
|
+
orgUnit.id,
|
|
154
|
+
pendingRemoveIds.size
|
|
155
|
+
),
|
|
156
|
+
}).then((switched) => {
|
|
157
|
+
if (!switched) {
|
|
158
|
+
pushToast({
|
|
159
|
+
message: t("contracts.toasts.refreshError"),
|
|
160
|
+
status: "ERROR",
|
|
161
|
+
});
|
|
162
|
+
void refresh();
|
|
142
163
|
}
|
|
143
|
-
);
|
|
164
|
+
});
|
|
144
165
|
return;
|
|
145
166
|
}
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
|
|
169
|
+
void refresh();
|
|
149
170
|
pushToast({
|
|
150
|
-
message: t(
|
|
151
|
-
|
|
152
|
-
pendingRemoveIds.size
|
|
153
|
-
|
|
171
|
+
message: t(
|
|
172
|
+
"contracts.toasts.removeSuccess",
|
|
173
|
+
pluralSuffixArgs(pendingRemoveIds.size)
|
|
174
|
+
),
|
|
154
175
|
status: "INFO",
|
|
155
176
|
});
|
|
156
177
|
},
|
|
@@ -199,7 +220,7 @@ export const ContractListingLayout = ({
|
|
|
199
220
|
|
|
200
221
|
const { setDefaultContract } = useSetDefaultContract({
|
|
201
222
|
onSuccess: () => {
|
|
202
|
-
|
|
223
|
+
void refresh();
|
|
203
224
|
pushToast({
|
|
204
225
|
message: t("contracts.toasts.setDefaultSuccess"),
|
|
205
226
|
status: "INFO",
|
|
@@ -213,7 +234,7 @@ export const ContractListingLayout = ({
|
|
|
213
234
|
} catch {
|
|
214
235
|
// message is not JSON
|
|
215
236
|
}
|
|
216
|
-
void
|
|
237
|
+
void refresh();
|
|
217
238
|
pushToast({
|
|
218
239
|
message: apiMessage ?? t("contracts.toasts.setDefaultError"),
|
|
219
240
|
status: "ERROR",
|
|
@@ -249,6 +270,24 @@ export const ContractListingLayout = ({
|
|
|
249
270
|
}
|
|
250
271
|
}, [router.query.error]);
|
|
251
272
|
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
const removedCount = parseRemovedCount(router.query[REMOVED_QUERY_PARAM]);
|
|
275
|
+
if (removedCount === null) return;
|
|
276
|
+
|
|
277
|
+
pushToast({
|
|
278
|
+
message: t(
|
|
279
|
+
"contracts.toasts.removeSuccess",
|
|
280
|
+
pluralSuffixArgs(removedCount)
|
|
281
|
+
),
|
|
282
|
+
status: "INFO",
|
|
283
|
+
});
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
285
|
+
const { [REMOVED_QUERY_PARAM]: _removed, ...rest } = router.query;
|
|
286
|
+
router.replace({ pathname: router.pathname, query: rest }, undefined, {
|
|
287
|
+
shallow: true,
|
|
288
|
+
});
|
|
289
|
+
}, [router.query[REMOVED_QUERY_PARAM]]);
|
|
290
|
+
|
|
252
291
|
const {
|
|
253
292
|
selectedIds,
|
|
254
293
|
handleModifierClick,
|
|
@@ -328,22 +367,22 @@ export const ContractListingLayout = ({
|
|
|
328
367
|
email: user?.email ?? "",
|
|
329
368
|
}}
|
|
330
369
|
loading={loading}
|
|
331
|
-
sidebarVariant="
|
|
370
|
+
sidebarVariant="unit"
|
|
332
371
|
/>
|
|
333
372
|
<div data-fs-bp-contract-listing-body>
|
|
334
373
|
<SumaSidebar
|
|
335
374
|
orgUnitId={orgUnit.id}
|
|
336
375
|
contractId={defaultContractId}
|
|
337
|
-
variant="
|
|
376
|
+
variant="unit"
|
|
338
377
|
/>
|
|
339
378
|
|
|
340
|
-
<section data-fs-bp-contract-listing-section>
|
|
379
|
+
<section data-fs-bp-contract-listing-section aria-busy={isBusy}>
|
|
341
380
|
<HeaderInside
|
|
342
|
-
title={t("contracts.titles.contracts")}
|
|
343
|
-
info={headerInfo}
|
|
381
|
+
title={loading ? undefined : t("contracts.titles.contracts")}
|
|
382
|
+
info={loading ? undefined : headerInfo}
|
|
344
383
|
data-fs-bp-contract-listing-header
|
|
345
384
|
>
|
|
346
|
-
{featureFlags?.suma && (
|
|
385
|
+
{featureFlags?.suma && !loading && (
|
|
347
386
|
<HeaderInside.Button
|
|
348
387
|
onClick={openAddContractsDrawer}
|
|
349
388
|
aria-label="Add contracts"
|
|
@@ -351,7 +390,11 @@ export const ContractListingLayout = ({
|
|
|
351
390
|
)}
|
|
352
391
|
</HeaderInside>
|
|
353
392
|
|
|
354
|
-
{
|
|
393
|
+
{loading ? (
|
|
394
|
+
// Route-transition skeleton (LoadingTabsLayout): the destination
|
|
395
|
+
// is the multi-contract listing, so mirror its rows.
|
|
396
|
+
<ContractSelectionListSkeleton />
|
|
397
|
+
) : isSingleContract ? (
|
|
355
398
|
<BasicCard
|
|
356
399
|
data-fs-bp-contracts-settings-card
|
|
357
400
|
footerMessage={t("orgUnitDetails.links.manageContractSettings")}
|
|
@@ -362,8 +405,8 @@ export const ContractListingLayout = ({
|
|
|
362
405
|
enableFooter
|
|
363
406
|
>
|
|
364
407
|
<LetterHighlight letter={displayName} />
|
|
365
|
-
<VerticalNav.Menu title={displayName} loading={
|
|
366
|
-
{
|
|
408
|
+
<VerticalNav.Menu title={displayName} loading={isBusy}>
|
|
409
|
+
{isBusy ? (
|
|
367
410
|
[...new Array(8)].map((_, index) => (
|
|
368
411
|
<li style={{ padding: "8px 0" }} key={`skeleton-${index}`}>
|
|
369
412
|
<Skeleton
|
|
@@ -408,15 +451,21 @@ export const ContractListingLayout = ({
|
|
|
408
451
|
selectedCount={selectedInFiltered}
|
|
409
452
|
onToggle={handleToggleSelectAll}
|
|
410
453
|
/>
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
454
|
+
{isRefreshing ? (
|
|
455
|
+
<ContractSelectionListSkeleton
|
|
456
|
+
rows={Math.max(pageItems.length, 3)}
|
|
457
|
+
/>
|
|
458
|
+
) : (
|
|
459
|
+
<ContractSelectionList
|
|
460
|
+
contracts={pageItems}
|
|
461
|
+
orgUnitId={orgUnit.id}
|
|
462
|
+
selectedIds={selectedIds}
|
|
463
|
+
onKeyDown={handleKeyDown}
|
|
464
|
+
onSelect={handleSelect}
|
|
465
|
+
onItemRemove={handleItemRemove}
|
|
466
|
+
onSetDefault={handleSetDefault}
|
|
467
|
+
/>
|
|
468
|
+
)}
|
|
420
469
|
<ContractSelectionBar
|
|
421
470
|
selectedCount={selectedIds.size}
|
|
422
471
|
onClear={clearSelection}
|
|
@@ -443,7 +492,7 @@ export const ContractListingLayout = ({
|
|
|
443
492
|
orgUnitId={orgUnit.id}
|
|
444
493
|
orgUnitName={orgUnit.name}
|
|
445
494
|
existingContractIds={contracts.map((c) => c.id)}
|
|
446
|
-
onSuccess={() =>
|
|
495
|
+
onSuccess={() => void refresh()}
|
|
447
496
|
{...addContractsDrawer}
|
|
448
497
|
/>
|
|
449
498
|
)}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildRemovedRedirectUrl,
|
|
5
|
+
getDefaultContractId,
|
|
6
|
+
parseRemovedCount,
|
|
7
|
+
pluralSuffixArgs,
|
|
8
|
+
resolveViewedContractId,
|
|
9
|
+
} from "../utils";
|
|
4
10
|
|
|
5
11
|
import type { ContractData } from "../../../types";
|
|
6
12
|
|
|
@@ -38,3 +44,36 @@ describe("resolveViewedContractId", () => {
|
|
|
38
44
|
expect(resolveViewedContractId(contracts, undefined)).toBe("contract-b");
|
|
39
45
|
});
|
|
40
46
|
});
|
|
47
|
+
|
|
48
|
+
describe("pluralSuffixArgs", () => {
|
|
49
|
+
it("returns no suffix for one item", () => {
|
|
50
|
+
expect(pluralSuffixArgs(1)).toEqual([1, ""]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns an 's' suffix for zero or many items", () => {
|
|
54
|
+
expect(pluralSuffixArgs(0)).toEqual([0, "s"]);
|
|
55
|
+
expect(pluralSuffixArgs(3)).toEqual([3, "s"]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("buildRemovedRedirectUrl", () => {
|
|
60
|
+
it("points at the unit's listing with the removed count", () => {
|
|
61
|
+
expect(buildRemovedRedirectUrl("unit-1", 2)).toBe(
|
|
62
|
+
"/pvt/organization-account/contracts/unit-1?removed=2"
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("parseRemovedCount", () => {
|
|
68
|
+
it("parses a positive integer", () => {
|
|
69
|
+
expect(parseRemovedCount("3")).toBe(3);
|
|
70
|
+
expect(parseRemovedCount(["2", "9"])).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rejects missing, zero, negative or non-numeric values", () => {
|
|
74
|
+
expect(parseRemovedCount(undefined)).toBeNull();
|
|
75
|
+
expect(parseRemovedCount("0")).toBeNull();
|
|
76
|
+
expect(parseRemovedCount("-1")).toBeNull();
|
|
77
|
+
expect(parseRemovedCount("abc")).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Match old [data-fs-bp-sidebar-menu] typography and colors
|
|
104
|
-
&[data-fs-bp-suma-sidebar-variant="
|
|
104
|
+
&[data-fs-bp-suma-sidebar-variant="contract"] {
|
|
105
105
|
[data-fs-vertical-nav-menu] {
|
|
106
106
|
margin-bottom: var(--fs-bp-padding-5);
|
|
107
107
|
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
&[data-fs-bp-suma-sidebar-variant="
|
|
140
|
+
&[data-fs-bp-suma-sidebar-variant="unit"] {
|
|
141
141
|
[data-fs-vertical-nav-menu-list] {
|
|
142
142
|
margin-top: var(--fs-bp-margin-0);
|
|
143
143
|
@include text-style("emphasis");
|
|
@@ -420,6 +420,14 @@
|
|
|
420
420
|
background-color: #f5f5f5;
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
&[data-fs-bp-contract-list-item-skeleton] {
|
|
424
|
+
cursor: default;
|
|
425
|
+
|
|
426
|
+
&:hover {
|
|
427
|
+
background-color: transparent;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
423
431
|
&[data-fs-bp-contract-list-item-selected] {
|
|
424
432
|
background-color: #f0f7ff;
|
|
425
433
|
|