@vtex/faststore-plugin-buyer-portal 1.1.91 → 1.1.93
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/public/buyer-portal-icons.svg +1 -1
- package/src/features/addresses/services/get-addresses-by-unit-id.service.ts +13 -1
- package/src/features/addresses/services/get-addresses.service.ts +15 -2
- package/src/features/buying-policies/services/get-buying-policies.service.ts +19 -21
- package/src/features/contracts/services/get-contract-details.service.ts +13 -1
- package/src/features/contracts/services/get-contracts-org-by-unit-id.service.ts +18 -11
- package/src/features/contracts/services/update-contract-status.service.ts +18 -11
- package/src/features/org-units/components/AddAllToOrgUnitDropdown/AddAllToOrgUnitDropdown.tsx +3 -1
- package/src/features/org-units/components/OrgUnitBreadcrumb/OrgUnitBreadcrumb.tsx +2 -0
- package/src/features/org-units/components/OrgUnitBreadcrumb/OrgUnitBreadcrumbPath.tsx +12 -12
- package/src/features/org-units/components/OrgUnitBreadcrumb/org-unit-breadcrumb.scss +47 -36
- package/src/features/org-units/types/OrgUnitBreadcrumbTypes.ts +2 -0
- package/src/features/profile/layouts/ProfileLayout/profile-layout.scss +1 -0
- package/src/features/shared/clients/Client.ts +84 -8
- package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +4 -1
- package/src/features/shared/components/Error/Error.tsx +31 -0
- package/src/features/shared/components/Error/error.scss +71 -0
- package/src/features/shared/components/ErrorBoundary/ErrorBoundary.tsx +63 -0
- package/src/features/shared/components/ErrorBoundary/types.ts +14 -0
- package/src/features/shared/components/index.ts +3 -0
- package/src/features/shared/components/withErrorBoundary/withErrorBoundary.tsx +35 -0
- package/src/features/shared/layouts/BaseTabsLayout/Navbar.tsx +1 -1
- package/src/features/shared/layouts/BaseTabsLayout/SidebarMenu.tsx +9 -2
- package/src/features/shared/layouts/BaseTabsLayout/base-tabs-layout.scss +9 -6
- package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +4 -2
- package/src/features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout.tsx +119 -0
- package/src/features/shared/layouts/ErrorTabsLayout/error-tabs-layout.scss +1 -0
- package/src/features/shared/utils/environment.ts +41 -0
- package/src/features/shared/utils/extractErrorMessage.ts +22 -0
- package/src/features/shared/utils/getHome.tsx +5 -0
- package/src/features/shared/utils/index.ts +2 -0
- package/src/features/shared/utils/withClientErrorBoundary.ts +61 -0
- package/src/features/shared/utils/withLoaderErrorBoundary.ts +46 -0
- package/src/features/users/clients/UsersClient.ts +0 -1
- package/src/pages/address-details.tsx +38 -11
- package/src/pages/addresses.tsx +35 -6
- package/src/pages/budgets-details.tsx +38 -8
- package/src/pages/budgets.tsx +33 -8
- package/src/pages/buying-policies.tsx +36 -12
- package/src/pages/buying-policy-details.tsx +38 -8
- package/src/pages/collections.tsx +36 -12
- package/src/pages/cost-centers.tsx +38 -8
- package/src/pages/credit-cards.tsx +38 -8
- package/src/pages/home.tsx +22 -5
- package/src/pages/org-unit-details.tsx +43 -7
- package/src/pages/org-units.tsx +39 -8
- package/src/pages/payment-methods.tsx +38 -8
- package/src/pages/po-numbers.tsx +38 -8
- package/src/pages/profile.tsx +31 -6
- package/src/pages/releases.tsx +33 -8
- package/src/pages/role-details.tsx +39 -7
- package/src/pages/roles.tsx +28 -7
- package/src/pages/user-details.tsx +39 -8
- package/src/pages/users.tsx +25 -7
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[data-fs-bp-error] {
|
|
2
|
+
@import "@faststore/ui/src/components/atoms/Icon/styles.scss";
|
|
3
|
+
|
|
4
|
+
flex: 1;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
gap: var(--fs-spacing-4);
|
|
10
|
+
height: 100%;
|
|
11
|
+
min-height: calc(100vh / 2);
|
|
12
|
+
|
|
13
|
+
[data-fs-bp-error-title] {
|
|
14
|
+
font-size: var(--fs-text-size-3);
|
|
15
|
+
font-weight: var(--fs-text-weight-semibold);
|
|
16
|
+
line-height: calc(var(--fs-text-size-4) + var(--fs-scale));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-fs-bp-error-button] {
|
|
20
|
+
font-size: var(--fs-text-size-1);
|
|
21
|
+
font-weight: var(--fs-text-weight-semibold);
|
|
22
|
+
color: #1f1f1f;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[data-fs-icon="true"] {
|
|
27
|
+
color: #d31a15;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[data-fs-bp-error-details] {
|
|
31
|
+
background-color: #1f1f1f;
|
|
32
|
+
border-top: calc(var(--fs-border-width) * 5) solid #d31a15;
|
|
33
|
+
padding: var(--fs-spacing-4);
|
|
34
|
+
border-radius: var(--fs-border-radius-medium);
|
|
35
|
+
position: relative;
|
|
36
|
+
|
|
37
|
+
[data-fs-bp-error-details-type] {
|
|
38
|
+
position: absolute;
|
|
39
|
+
right: var(--fs-spacing-4);
|
|
40
|
+
top: var(--fs-spacing-4);
|
|
41
|
+
background: #f1f1f1;
|
|
42
|
+
padding: var(--fs-spacing-1);
|
|
43
|
+
border-radius: var(--fs-border-radius-medium);
|
|
44
|
+
font-size: var(--fs-text-size-0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
[data-fs-bp-error-details-title] {
|
|
48
|
+
font-size: var(--fs-text-size-3);
|
|
49
|
+
color: #e2e2e2;
|
|
50
|
+
font-weight: var(--fs-text-weight-semibold);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-fs-bp-error-details-message] {
|
|
54
|
+
margin: var(--fs-spacing-4) 0;
|
|
55
|
+
font-size: var(--fs-text-size-2);
|
|
56
|
+
font-weight: var(--fs-text-weight-medium);
|
|
57
|
+
color: #e2e2e2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[data-fs-bp-error-details-stack] {
|
|
61
|
+
font-size: var(--fs-text-size-2);
|
|
62
|
+
margin: var(--fs-spacing-4) 0;
|
|
63
|
+
font-weight: var(--fs-text-weight-medium);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[data-fs-bp-error-details-component] {
|
|
67
|
+
font-size: var(--fs-text-size-2);
|
|
68
|
+
font-weight: var(--fs-text-weight-medium);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { Component, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { ErrorTabsLayout } from "../../layouts/ErrorTabsLayout/ErrorTabsLayout";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
fallback?: ReactNode;
|
|
10
|
+
tags?: {
|
|
11
|
+
component: string;
|
|
12
|
+
errorType: string;
|
|
13
|
+
};
|
|
14
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface State {
|
|
18
|
+
hasError: boolean;
|
|
19
|
+
error?: Error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
23
|
+
constructor(props: Props) {
|
|
24
|
+
super(props);
|
|
25
|
+
this.state = { hasError: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static getDerivedStateFromError(error: Error): State {
|
|
29
|
+
return { hasError: true, error };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
33
|
+
if (this.props.onError) {
|
|
34
|
+
this.props.onError(error, errorInfo);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render() {
|
|
39
|
+
if (this.state.hasError) {
|
|
40
|
+
if (this.props.fallback) {
|
|
41
|
+
return this.props.fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ErrorTabsLayout
|
|
46
|
+
error={{
|
|
47
|
+
error: this.state.error ?? {
|
|
48
|
+
message: "Unknown error",
|
|
49
|
+
name: "Unknown error",
|
|
50
|
+
stack: "Unknown error",
|
|
51
|
+
},
|
|
52
|
+
tags: this.props.tags ?? {
|
|
53
|
+
component: "Unknown",
|
|
54
|
+
errorType: "Unknown",
|
|
55
|
+
},
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this.props.children;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -72,3 +72,6 @@ export type { CounterProps as PaginatorCounterProps } from "./Paginator/Counter"
|
|
|
72
72
|
export type { NextPageButtonProps as PaginatorNextPageButtonProps } from "./Paginator/NextPageButton";
|
|
73
73
|
|
|
74
74
|
export { EmptyState, type EmptyStateProps } from "./EmptyState/EmptyState";
|
|
75
|
+
export { ErrorBoundary } from "./ErrorBoundary/ErrorBoundary";
|
|
76
|
+
export { withErrorBoundary } from "./withErrorBoundary/withErrorBoundary";
|
|
77
|
+
export { default as Error } from "./Error/Error";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import { ErrorBoundary } from "../ErrorBoundary/ErrorBoundary";
|
|
6
|
+
|
|
7
|
+
type WithErrorBoundaryOptions = {
|
|
8
|
+
fallback?: React.ReactNode;
|
|
9
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
10
|
+
tags?: {
|
|
11
|
+
component: string;
|
|
12
|
+
errorType: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function withErrorBoundary<P extends Record<string, unknown>>(
|
|
17
|
+
WrappedComponent: React.ComponentType<P>,
|
|
18
|
+
options: WithErrorBoundaryOptions = {}
|
|
19
|
+
) {
|
|
20
|
+
const { onError, tags } = options;
|
|
21
|
+
|
|
22
|
+
const ComponentWithErrorBoundary = (props: P) => {
|
|
23
|
+
return (
|
|
24
|
+
<ErrorBoundary onError={onError} tags={tags}>
|
|
25
|
+
<WrappedComponent {...props} />
|
|
26
|
+
</ErrorBoundary>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
ComponentWithErrorBoundary.displayName = `WithErrorBoundary(${
|
|
31
|
+
WrappedComponent.displayName || WrappedComponent.name || "Component"
|
|
32
|
+
})`;
|
|
33
|
+
|
|
34
|
+
return ComponentWithErrorBoundary;
|
|
35
|
+
}
|
|
@@ -73,7 +73,7 @@ export const Navbar = ({
|
|
|
73
73
|
{loading ? (
|
|
74
74
|
<Skeleton size={{ width: "22rem", height: "2.5rem" }} />
|
|
75
75
|
) : (
|
|
76
|
-
<OrgUnitBreadcrumb items={breadcrumbList} maxItems={1} />
|
|
76
|
+
<OrgUnitBreadcrumb items={breadcrumbList} maxItems={1} hasDropdown />
|
|
77
77
|
)}
|
|
78
78
|
|
|
79
79
|
<div data-fs-bp-nav-menu-actions>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
import { useRouter } from "next/router";
|
|
4
|
+
|
|
3
5
|
import { VerticalNav } from "../../components";
|
|
4
6
|
import { useBuyerPortal } from "../../hooks";
|
|
5
7
|
import {
|
|
@@ -22,6 +24,11 @@ export type SidebarMenuProps = {
|
|
|
22
24
|
export const SidebarMenu = ({ ...otherProps }: SidebarMenuProps) => {
|
|
23
25
|
const { currentOrgUnit, currentContract } = useBuyerPortal();
|
|
24
26
|
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
|
|
29
|
+
const orgUnitId = router.query.orgUnitId as string;
|
|
30
|
+
const customerId = router.query.contractId as string;
|
|
31
|
+
|
|
25
32
|
const idContract =
|
|
26
33
|
(currentContract?.id
|
|
27
34
|
? currentContract?.id
|
|
@@ -31,8 +38,8 @@ export const SidebarMenu = ({ ...otherProps }: SidebarMenuProps) => {
|
|
|
31
38
|
<aside data-fs-bp-sidebar-menu {...otherProps}>
|
|
32
39
|
<VerticalNav.Menu title="Contract">
|
|
33
40
|
{getContractSettingsLinks({
|
|
34
|
-
orgUnitId: currentOrgUnit?.id ?? "",
|
|
35
|
-
contractId: idContract,
|
|
41
|
+
orgUnitId: orgUnitId ?? currentOrgUnit?.id ?? "",
|
|
42
|
+
contractId: customerId ?? idContract,
|
|
36
43
|
}).map((option) => (
|
|
37
44
|
<VerticalNav.Link key={option.name} link={option.link}>
|
|
38
45
|
{option.name}
|
|
@@ -20,12 +20,15 @@
|
|
|
20
20
|
calc(var(--fs-spacing-0) + var(--fs-spacing-3));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
[data-fs-bp-breadcrumb-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
[data-fs-bp-breadcrumb-nav] {
|
|
24
|
+
[data-fs-bp-breadcrumb-current],
|
|
25
|
+
[data-fs-bp-breadcrumb-dropdown-trigger] {
|
|
26
|
+
font-size: var(--fs-text-size-2);
|
|
27
|
+
|
|
28
|
+
@include media("<=phonemid") {
|
|
29
|
+
font-size: var(--fs-text-size-1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
[data-fs-bp-nav-menu-btn-container] {
|
|
@@ -27,12 +27,14 @@ export const ContractTabsLayout = ({
|
|
|
27
27
|
pageName,
|
|
28
28
|
loading = false,
|
|
29
29
|
children,
|
|
30
|
+
orgUnitId,
|
|
31
|
+
contractId,
|
|
30
32
|
}: ContractTabsLayoutProps) => {
|
|
31
33
|
const { currentOrgUnit, currentUser, currentContract } = useBuyerPortal();
|
|
32
34
|
|
|
33
35
|
const verticalLinks = getContractSettingsLinks({
|
|
34
|
-
contractId: currentContract?.id ?? "",
|
|
35
|
-
orgUnitId: currentOrgUnit?.id ?? "",
|
|
36
|
+
contractId: currentContract?.id ?? contractId ?? "",
|
|
37
|
+
orgUnitId: currentOrgUnit?.id ?? orgUnitId ?? "",
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
return (
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/router";
|
|
4
|
+
|
|
5
|
+
import { OrgUnitDetailsNavbar } from "../../../org-units/components";
|
|
6
|
+
import { HeaderInside } from "../../components";
|
|
7
|
+
import Error from "../../components/Error/Error";
|
|
8
|
+
import { useBuyerPortal } from "../../hooks";
|
|
9
|
+
import { getTabsLayoutConfigFromRoute } from "../../utils/routeLayoutMapping";
|
|
10
|
+
import { ContractTabsLayout } from "../ContractTabsLayout/ContractTabsLayout";
|
|
11
|
+
import { FinanceTabsLayout } from "../FinanceTabsLayout/FinanceTabsLayout";
|
|
12
|
+
import { OrgUnitTabsLayout } from "../OrgUnitTabsLayout/OrgUnitTabLayout";
|
|
13
|
+
|
|
14
|
+
export type ErrorTabsLayoutContentProps = {
|
|
15
|
+
title?: string;
|
|
16
|
+
error?: {
|
|
17
|
+
error: Error;
|
|
18
|
+
tags: {
|
|
19
|
+
component: string;
|
|
20
|
+
errorType: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ErrorTabsLayoutProps = {
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
error?: {
|
|
28
|
+
error: Error;
|
|
29
|
+
tags: {
|
|
30
|
+
component: string;
|
|
31
|
+
errorType: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const ErrorTabsLayoutContent = ({
|
|
37
|
+
title,
|
|
38
|
+
error,
|
|
39
|
+
}: ErrorTabsLayoutContentProps) => (
|
|
40
|
+
<div>
|
|
41
|
+
<section data-fs-bp-profile>
|
|
42
|
+
<HeaderInside title={title} />
|
|
43
|
+
<Error error={error?.error} tags={error?.tags} />
|
|
44
|
+
</section>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const ErrorTabsLayout = ({ children, error }: ErrorTabsLayoutProps) => {
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
currentOrgUnit: orgUnit,
|
|
53
|
+
currentUser: user,
|
|
54
|
+
currentContract: contract,
|
|
55
|
+
} = useBuyerPortal();
|
|
56
|
+
|
|
57
|
+
const orgUnitId = router.query.orgUnitId as string;
|
|
58
|
+
const customerId = router.query.contractId as string;
|
|
59
|
+
|
|
60
|
+
const layoutConfig = getTabsLayoutConfigFromRoute(router.route);
|
|
61
|
+
|
|
62
|
+
if (!layoutConfig) {
|
|
63
|
+
return <>{children}</>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const pageTitle = layoutConfig.pageTitle;
|
|
67
|
+
|
|
68
|
+
switch (layoutConfig.layout) {
|
|
69
|
+
case "ContractTabsLayout":
|
|
70
|
+
return (
|
|
71
|
+
<ContractTabsLayout
|
|
72
|
+
orgUnitName={orgUnit?.name ?? ""}
|
|
73
|
+
orgUnitId={orgUnitId}
|
|
74
|
+
contractId={customerId}
|
|
75
|
+
contractName={contract?.name ?? ""}
|
|
76
|
+
pageName="Contract"
|
|
77
|
+
person={{
|
|
78
|
+
image: undefined,
|
|
79
|
+
name: user?.name ?? "",
|
|
80
|
+
role: user?.role ?? "",
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<ErrorTabsLayoutContent title={pageTitle} error={error} />
|
|
84
|
+
</ContractTabsLayout>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
case "FinanceTabsLayout":
|
|
88
|
+
return (
|
|
89
|
+
<FinanceTabsLayout pageName={layoutConfig.pageName}>
|
|
90
|
+
<ErrorTabsLayoutContent title={pageTitle} error={error} />
|
|
91
|
+
</FinanceTabsLayout>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
case "OrgUnitTabsLayout":
|
|
95
|
+
return (
|
|
96
|
+
<OrgUnitTabsLayout pageName={layoutConfig.pageName}>
|
|
97
|
+
<ErrorTabsLayoutContent title={pageTitle} error={error} />
|
|
98
|
+
</OrgUnitTabsLayout>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
case "OrgUnitDetailsLayout":
|
|
102
|
+
return (
|
|
103
|
+
<div data-fs-bp-create-user-error-state>
|
|
104
|
+
<OrgUnitDetailsNavbar
|
|
105
|
+
orgName={orgUnit?.name ?? ""}
|
|
106
|
+
person={{
|
|
107
|
+
name: user?.name ?? "",
|
|
108
|
+
role: user?.role ?? "",
|
|
109
|
+
}}
|
|
110
|
+
loading={false}
|
|
111
|
+
/>
|
|
112
|
+
<ErrorTabsLayoutContent title={pageTitle} error={error} />
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
return <ErrorTabsLayoutContent error={error} />;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "../../components//Error/error.scss";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for environment detection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if the application is running on localhost
|
|
7
|
+
* @returns {boolean} True if running on localhost, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
export const isLocalhost = (): boolean => {
|
|
10
|
+
if (typeof window === "undefined") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hostname = window.location.hostname;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
hostname === "localhost" ||
|
|
18
|
+
hostname === "127.0.0.1" ||
|
|
19
|
+
hostname === "0.0.0.0" ||
|
|
20
|
+
hostname.startsWith("192.168.") ||
|
|
21
|
+
hostname.startsWith("10.") ||
|
|
22
|
+
hostname.startsWith("172.") ||
|
|
23
|
+
hostname.endsWith(".local")
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks if the application is running in development mode
|
|
29
|
+
* @returns {boolean} True if in development mode, false otherwise
|
|
30
|
+
*/
|
|
31
|
+
export const isDevelopment = (): boolean => {
|
|
32
|
+
return process.env.NODE_ENV === "development";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if detailed error information should be shown
|
|
37
|
+
* @returns {boolean} True if error details should be shown, false otherwise
|
|
38
|
+
*/
|
|
39
|
+
export const shouldShowErrorDetails = (): boolean => {
|
|
40
|
+
return isLocalhost() && isDevelopment();
|
|
41
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const extractErrorMessage = (
|
|
2
|
+
response: Response,
|
|
3
|
+
responseData: unknown
|
|
4
|
+
): string => {
|
|
5
|
+
if (
|
|
6
|
+
responseData &&
|
|
7
|
+
typeof responseData === "object" &&
|
|
8
|
+
"message" in responseData
|
|
9
|
+
) {
|
|
10
|
+
return String((responseData as { message: unknown }).message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (responseData) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(responseData);
|
|
16
|
+
} catch {
|
|
17
|
+
return String(responseData);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return `HTTP ${response.status}: ${response.statusText}`;
|
|
22
|
+
};
|
|
@@ -33,3 +33,5 @@ export {
|
|
|
33
33
|
} from "./search";
|
|
34
34
|
export { toQueryParams } from "./toQueryParams";
|
|
35
35
|
export { getValidPage } from "./getValidPage";
|
|
36
|
+
export { withLoaderErrorBoundary } from "./withLoaderErrorBoundary";
|
|
37
|
+
export { withClientErrorBoundary } from "./withClientErrorBoundary";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { goHome } from "./getHome";
|
|
2
|
+
|
|
3
|
+
import type { ClientError } from "../clients/Client";
|
|
4
|
+
|
|
5
|
+
interface WithClientErrorBoundaryOptions {
|
|
6
|
+
componentName?: string;
|
|
7
|
+
onError?: (
|
|
8
|
+
error: ClientError,
|
|
9
|
+
requestInfo: {
|
|
10
|
+
url: string;
|
|
11
|
+
method: string;
|
|
12
|
+
data?: unknown;
|
|
13
|
+
}
|
|
14
|
+
) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const handleUnauthorizedRedirect = (error: ClientError) => {
|
|
18
|
+
if (error.status === 401) {
|
|
19
|
+
if (typeof window !== "undefined") {
|
|
20
|
+
goHome();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function withClientErrorBoundary<TArgs extends unknown[], TReturn>(
|
|
26
|
+
clientFunction: (...args: TArgs) => Promise<TReturn>,
|
|
27
|
+
options: WithClientErrorBoundaryOptions = {}
|
|
28
|
+
) {
|
|
29
|
+
const { componentName, onError } = options;
|
|
30
|
+
|
|
31
|
+
return async (...args: TArgs): Promise<TReturn> => {
|
|
32
|
+
try {
|
|
33
|
+
return await clientFunction(...args);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (onError && error instanceof Error) {
|
|
36
|
+
const clientError = error as Partial<ClientError>;
|
|
37
|
+
const requestInfo = {
|
|
38
|
+
url:
|
|
39
|
+
typeof clientError.url === "string" ? clientError.url : "unknown",
|
|
40
|
+
method:
|
|
41
|
+
typeof clientError.method === "string"
|
|
42
|
+
? clientError.method
|
|
43
|
+
: "unknown",
|
|
44
|
+
data:
|
|
45
|
+
clientError.responseData !== undefined
|
|
46
|
+
? clientError.responseData
|
|
47
|
+
: undefined,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
onError(error as ClientError, requestInfo);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (error instanceof Error && (error as ClientError).status === 401) {
|
|
54
|
+
return null as TReturn;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.error(`Error in ${componentName || "client function"}:`, error);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LoaderData } from "../types";
|
|
2
|
+
|
|
3
|
+
interface WithLoaderErrorBoundaryOptions {
|
|
4
|
+
componentName?: string;
|
|
5
|
+
onError?: (error: Error, query: unknown) => void;
|
|
6
|
+
redirectToError?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function withLoaderErrorBoundary<TQuery, TReturn>(
|
|
10
|
+
loaderFn: (data: LoaderData<TQuery>) => Promise<TReturn>,
|
|
11
|
+
options: WithLoaderErrorBoundaryOptions = {}
|
|
12
|
+
) {
|
|
13
|
+
const { componentName, onError, redirectToError = true } = options;
|
|
14
|
+
|
|
15
|
+
return async (data: LoaderData<TQuery>): Promise<TReturn> => {
|
|
16
|
+
try {
|
|
17
|
+
return await loaderFn(data);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (onError) {
|
|
20
|
+
onError(error as Error, data.query);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (redirectToError) {
|
|
24
|
+
return {
|
|
25
|
+
error: {
|
|
26
|
+
error: {
|
|
27
|
+
message: (error as Error).message,
|
|
28
|
+
name: (error as Error).name,
|
|
29
|
+
stack: (error as Error).stack,
|
|
30
|
+
},
|
|
31
|
+
tags: {
|
|
32
|
+
component: componentName || "Unknown",
|
|
33
|
+
errorType: "loader_error",
|
|
34
|
+
},
|
|
35
|
+
extra: {
|
|
36
|
+
query: data.query,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
hasError: true,
|
|
40
|
+
} as TReturn;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -4,15 +4,21 @@ import { getAddressLocationService } from "../features/addresses/services/locati
|
|
|
4
4
|
import { getAddressRecipientsService } from "../features/addresses/services/recipients/get-address-recipients.service";
|
|
5
5
|
import { getContractDetailsService } from "../features/contracts/services";
|
|
6
6
|
import { getOrgUnitBasicDataService } from "../features/org-units/services";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
BuyerPortalProvider,
|
|
9
|
+
withErrorBoundary,
|
|
10
|
+
} from "../features/shared/components";
|
|
11
|
+
import { ErrorBoundaryProps } from "../features/shared/components/ErrorBoundary/types";
|
|
8
12
|
import {
|
|
9
13
|
PAGE_PARAMS,
|
|
10
14
|
SEARCH_PARAMS,
|
|
11
15
|
} from "../features/shared/hooks/usePageItems";
|
|
16
|
+
import { ErrorTabsLayout } from "../features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout";
|
|
12
17
|
import {
|
|
13
18
|
type ClientContext,
|
|
14
19
|
getClientContext,
|
|
15
20
|
getValidPage,
|
|
21
|
+
withLoaderErrorBoundary,
|
|
16
22
|
} from "../features/shared/utils";
|
|
17
23
|
import { getUserByIdService } from "../features/users/services";
|
|
18
24
|
|
|
@@ -57,11 +63,13 @@ export type AddressDetailsPageData = {
|
|
|
57
63
|
currentContract: ContractData | null;
|
|
58
64
|
currentUser: UserData | null;
|
|
59
65
|
};
|
|
66
|
+
hasError?: boolean;
|
|
67
|
+
error?: ErrorBoundaryProps;
|
|
60
68
|
};
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
const loaderFunction = async (
|
|
63
71
|
data: LoaderData<AddressDetailsPageQuery>
|
|
64
|
-
): Promise<AddressDetailsPageData> {
|
|
72
|
+
): Promise<AddressDetailsPageData> => {
|
|
65
73
|
const {
|
|
66
74
|
contractId,
|
|
67
75
|
orgUnitId,
|
|
@@ -135,16 +143,35 @@ export async function loader(
|
|
|
135
143
|
currentUser: user,
|
|
136
144
|
},
|
|
137
145
|
};
|
|
138
|
-
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const loader = withLoaderErrorBoundary(loaderFunction, {
|
|
149
|
+
componentName: "AddressDetailsPage",
|
|
150
|
+
redirectToError: true,
|
|
151
|
+
});
|
|
139
152
|
|
|
140
|
-
const AddressDetailsPage = ({
|
|
153
|
+
const AddressDetailsPage = ({
|
|
154
|
+
data,
|
|
155
|
+
context,
|
|
156
|
+
hasError,
|
|
157
|
+
error,
|
|
158
|
+
}: AddressDetailsPageData) => (
|
|
141
159
|
<BuyerPortalProvider {...context}>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
{hasError ? (
|
|
161
|
+
<ErrorTabsLayout error={error} />
|
|
162
|
+
) : (
|
|
163
|
+
<AddressDetailsLayout
|
|
164
|
+
addressDetails={data.addressDetails}
|
|
165
|
+
recipientsData={data.recipientsData ?? DEFAULT_ADDRESS_PAGINATED_DATA}
|
|
166
|
+
locationsData={data.locationsData ?? DEFAULT_ADDRESS_PAGINATED_DATA}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
147
169
|
</BuyerPortalProvider>
|
|
148
170
|
);
|
|
149
171
|
|
|
150
|
-
export default AddressDetailsPage
|
|
172
|
+
export default withErrorBoundary(AddressDetailsPage, {
|
|
173
|
+
tags: {
|
|
174
|
+
component: "AddressDetailsPage",
|
|
175
|
+
errorType: "address_details_error",
|
|
176
|
+
},
|
|
177
|
+
});
|