@orsetra/shared-ui 1.0.38 → 1.0.40
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.
|
@@ -10,10 +10,10 @@ const alertBannerVariants = cva(
|
|
|
10
10
|
{
|
|
11
11
|
variants: {
|
|
12
12
|
variant: {
|
|
13
|
-
success: "bg-green-50 text-green-800
|
|
14
|
-
error: "bg-red-50 text-red-800
|
|
15
|
-
warning: "bg-amber-50 text-amber-800
|
|
16
|
-
info: "bg-blue-50 text-blue-800
|
|
13
|
+
success: "bg-green-50 text-green-800",
|
|
14
|
+
error: "bg-red-50 text-red-800",
|
|
15
|
+
warning: "bg-amber-50 text-amber-800",
|
|
16
|
+
info: "bg-blue-50 text-blue-800",
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
defaultVariants: {
|
|
@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|
|
18
18
|
>(({ className, ...props }, ref) => (
|
|
19
19
|
<AlertDialogPrimitive.Overlay
|
|
20
20
|
className={cn(
|
|
21
|
-
"fixed inset-0 z-50 bg-
|
|
21
|
+
"fixed inset-0 z-50 bg-ibm-gray-100/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
22
22
|
className
|
|
23
23
|
)}
|
|
24
24
|
{...props}
|
|
@@ -32,11 +32,10 @@ const AlertDialogContent = React.forwardRef<
|
|
|
32
32
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
33
33
|
>(({ className, ...props }, ref) => (
|
|
34
34
|
<AlertDialogPortal>
|
|
35
|
-
<AlertDialogOverlay />
|
|
36
35
|
<AlertDialogPrimitive.Content
|
|
37
36
|
ref={ref}
|
|
38
37
|
className={cn(
|
|
39
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-
|
|
38
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-ibm-gray-20 bg-white p-6 shadow-xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
40
39
|
className
|
|
41
40
|
)}
|
|
42
41
|
{...props}
|
|
@@ -79,7 +78,7 @@ const AlertDialogTitle = React.forwardRef<
|
|
|
79
78
|
>(({ className, ...props }, ref) => (
|
|
80
79
|
<AlertDialogPrimitive.Title
|
|
81
80
|
ref={ref}
|
|
82
|
-
className={cn("text-lg font-semibold", className)}
|
|
81
|
+
className={cn("text-lg font-semibold text-ibm-gray-100", className)}
|
|
83
82
|
{...props}
|
|
84
83
|
/>
|
|
85
84
|
))
|
|
@@ -91,7 +90,7 @@ const AlertDialogDescription = React.forwardRef<
|
|
|
91
90
|
>(({ className, ...props }, ref) => (
|
|
92
91
|
<AlertDialogPrimitive.Description
|
|
93
92
|
ref={ref}
|
|
94
|
-
className={cn("text-sm text-
|
|
93
|
+
className={cn("text-sm text-ibm-gray-70", className)}
|
|
95
94
|
{...props}
|
|
96
95
|
/>
|
|
97
96
|
))
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
AlertDialogTrigger,
|
|
14
|
+
} from "./alert-dialog"
|
|
15
|
+
import { AlertBanner, useAlertBanner } from "./alert-banner"
|
|
16
|
+
import { Input } from "./input"
|
|
17
|
+
import { Label } from "./label"
|
|
18
|
+
import type { ReactNode } from "react"
|
|
19
|
+
|
|
20
|
+
export interface ConfirmationDialogProps {
|
|
21
|
+
trigger: ReactNode
|
|
22
|
+
title: string
|
|
23
|
+
description: string | ReactNode
|
|
24
|
+
confirmText?: string
|
|
25
|
+
cancelText?: string
|
|
26
|
+
confirmVariant?: "default" | "destructive"
|
|
27
|
+
onConfirm: () => void | Promise<void>
|
|
28
|
+
onCancel?: () => void
|
|
29
|
+
requireTextConfirmation?: {
|
|
30
|
+
expectedText: string
|
|
31
|
+
label?: string
|
|
32
|
+
placeholder?: string
|
|
33
|
+
}
|
|
34
|
+
open?: boolean
|
|
35
|
+
onOpenChange?: (open: boolean) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ConfirmationDialog({
|
|
39
|
+
trigger,
|
|
40
|
+
title,
|
|
41
|
+
description,
|
|
42
|
+
confirmText = "Confirm",
|
|
43
|
+
cancelText = "Cancel",
|
|
44
|
+
confirmVariant = "default",
|
|
45
|
+
onConfirm,
|
|
46
|
+
onCancel,
|
|
47
|
+
requireTextConfirmation,
|
|
48
|
+
open: controlledOpen,
|
|
49
|
+
onOpenChange: controlledOnOpenChange,
|
|
50
|
+
}: ConfirmationDialogProps) {
|
|
51
|
+
const { alert, showSuccess, showError, hideAlert } = useAlertBanner()
|
|
52
|
+
const [loading, setLoading] = useState(false)
|
|
53
|
+
const [internalOpen, setInternalOpen] = useState(false)
|
|
54
|
+
const [confirmationText, setConfirmationText] = useState("")
|
|
55
|
+
|
|
56
|
+
const isControlled = controlledOpen !== undefined
|
|
57
|
+
const open = isControlled ? controlledOpen : internalOpen
|
|
58
|
+
const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen
|
|
59
|
+
|
|
60
|
+
const isConfirmDisabled =
|
|
61
|
+
loading ||
|
|
62
|
+
(requireTextConfirmation &&
|
|
63
|
+
confirmationText !== requireTextConfirmation.expectedText)
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!open) {
|
|
67
|
+
setConfirmationText("")
|
|
68
|
+
hideAlert()
|
|
69
|
+
}
|
|
70
|
+
}, [open])
|
|
71
|
+
|
|
72
|
+
const handleConfirm = async () => {
|
|
73
|
+
setLoading(true)
|
|
74
|
+
hideAlert()
|
|
75
|
+
try {
|
|
76
|
+
await onConfirm()
|
|
77
|
+
setOpen(false)
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
showError(error?.message || "Operation failed")
|
|
80
|
+
} finally {
|
|
81
|
+
setLoading(false)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleCancel = () => {
|
|
86
|
+
onCancel?.()
|
|
87
|
+
setOpen(false)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const confirmButtonClass =
|
|
91
|
+
confirmVariant === "destructive"
|
|
92
|
+
? "bg-red-600 hover:bg-red-700"
|
|
93
|
+
: undefined
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
97
|
+
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
|
|
98
|
+
<AlertDialogContent>
|
|
99
|
+
<AlertDialogHeader>
|
|
100
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
101
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
102
|
+
</AlertDialogHeader>
|
|
103
|
+
|
|
104
|
+
{alert.show && (
|
|
105
|
+
<AlertBanner
|
|
106
|
+
variant={alert.variant}
|
|
107
|
+
message={alert.message}
|
|
108
|
+
onClose={hideAlert}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{requireTextConfirmation && (
|
|
113
|
+
<div className="space-y-2">
|
|
114
|
+
<Label htmlFor="confirmation-input">
|
|
115
|
+
{requireTextConfirmation.label ||
|
|
116
|
+
`Type "${requireTextConfirmation.expectedText}" to confirm`}
|
|
117
|
+
</Label>
|
|
118
|
+
<Input
|
|
119
|
+
id="confirmation-input"
|
|
120
|
+
value={confirmationText}
|
|
121
|
+
onChange={(e) => setConfirmationText(e.target.value)}
|
|
122
|
+
placeholder={
|
|
123
|
+
requireTextConfirmation.placeholder ||
|
|
124
|
+
requireTextConfirmation.expectedText
|
|
125
|
+
}
|
|
126
|
+
disabled={loading}
|
|
127
|
+
autoComplete="off"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<AlertDialogFooter className="mt-6">
|
|
133
|
+
<AlertDialogCancel disabled={loading} onClick={handleCancel}>
|
|
134
|
+
{cancelText}
|
|
135
|
+
</AlertDialogCancel>
|
|
136
|
+
<AlertDialogAction
|
|
137
|
+
className={confirmButtonClass}
|
|
138
|
+
onClick={handleConfirm}
|
|
139
|
+
disabled={isConfirmDisabled}
|
|
140
|
+
>
|
|
141
|
+
{confirmText}
|
|
142
|
+
</AlertDialogAction>
|
|
143
|
+
</AlertDialogFooter>
|
|
144
|
+
</AlertDialogContent>
|
|
145
|
+
</AlertDialog>
|
|
146
|
+
)
|
|
147
|
+
}
|
package/components/ui/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './
|
|
|
23
23
|
export { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from './alert-dialog'
|
|
24
24
|
export { Alert, AlertTitle, AlertDescription } from './alert'
|
|
25
25
|
export { AlertBanner, useAlertBanner, type AlertBannerProps, type AlertState } from './alert-banner'
|
|
26
|
+
export { ConfirmationDialog, type ConfirmationDialogProps } from './confirmation-dialog'
|
|
26
27
|
export { AspectRatio } from './aspect-ratio'
|
|
27
28
|
export { Badge } from './badge'
|
|
28
29
|
export { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis } from './breadcrumb'
|
package/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Utilities
|
|
2
2
|
export * from './lib/utils'
|
|
3
3
|
export * from './lib/menu-utils'
|
|
4
|
+
export * from './lib/error-utils'
|
|
4
5
|
export { BaseService } from './lib/base-service'
|
|
5
|
-
export { default as HttpClient, useHttpClient } from './lib/http-client'
|
|
6
|
+
export { default as HttpClient, useHttpClient, ApiError } from './lib/http-client'
|
|
6
7
|
|
|
7
8
|
// UI Components
|
|
8
9
|
export * from './components/ui'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for parsing API errors
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ApiError } from './http-client'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Standard API error response format
|
|
9
|
+
*/
|
|
10
|
+
export interface ApiErrorResponse {
|
|
11
|
+
BusinessCode?: number
|
|
12
|
+
Message?: string
|
|
13
|
+
message?: string
|
|
14
|
+
error?: string
|
|
15
|
+
code?: number
|
|
16
|
+
details?: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracts a user-friendly error message from various error formats
|
|
21
|
+
*
|
|
22
|
+
* Supports:
|
|
23
|
+
* - ApiError from http-client
|
|
24
|
+
* - Standard Error objects
|
|
25
|
+
* - API responses with BusinessCode/Message format
|
|
26
|
+
* - API responses with message/error format
|
|
27
|
+
*
|
|
28
|
+
* @param error - The error object to parse
|
|
29
|
+
* @param fallbackMessage - Default message if no error message can be extracted
|
|
30
|
+
* @returns A user-friendly error message
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* try {
|
|
35
|
+
* await apiCall()
|
|
36
|
+
* } catch (error) {
|
|
37
|
+
* const message = parseApiError(error)
|
|
38
|
+
* showError(message)
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function parseApiError(error: unknown, fallbackMessage = "An unexpected error occurred"): string {
|
|
43
|
+
// Handle ApiError from http-client
|
|
44
|
+
if (error instanceof ApiError) {
|
|
45
|
+
// Check raw response for BusinessCode/Message format
|
|
46
|
+
if (error.raw && typeof error.raw === 'object') {
|
|
47
|
+
const rawError = error.raw as ApiErrorResponse
|
|
48
|
+
if (rawError.Message) {
|
|
49
|
+
return rawError.Message
|
|
50
|
+
}
|
|
51
|
+
if (rawError.message) {
|
|
52
|
+
return rawError.message
|
|
53
|
+
}
|
|
54
|
+
if (rawError.error) {
|
|
55
|
+
return rawError.error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Use ApiError message
|
|
59
|
+
return error.message
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle standard Error objects
|
|
63
|
+
if (error instanceof Error) {
|
|
64
|
+
return error.message
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle plain objects with error properties
|
|
68
|
+
if (error && typeof error === 'object') {
|
|
69
|
+
const errorObj = error as ApiErrorResponse
|
|
70
|
+
|
|
71
|
+
// Check for BusinessCode/Message format (custom API format)
|
|
72
|
+
if (errorObj.Message) {
|
|
73
|
+
return errorObj.Message
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for standard message/error properties
|
|
77
|
+
if (errorObj.message) {
|
|
78
|
+
return errorObj.message
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (errorObj.error) {
|
|
82
|
+
return errorObj.error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle string errors
|
|
87
|
+
if (typeof error === 'string') {
|
|
88
|
+
return error
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fallback
|
|
92
|
+
return fallbackMessage
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if an error is an ApiError with a specific status code
|
|
97
|
+
*
|
|
98
|
+
* @param error - The error to check
|
|
99
|
+
* @param status - The status code to match
|
|
100
|
+
* @returns true if the error is an ApiError with the specified status
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* if (isApiErrorWithStatus(error, 404)) {
|
|
105
|
+
* showError("Resource not found")
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function isApiErrorWithStatus(error: unknown, status: number): boolean {
|
|
110
|
+
return error instanceof ApiError && error.status === status
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Checks if an error is a network error (no response from server)
|
|
115
|
+
*
|
|
116
|
+
* @param error - The error to check
|
|
117
|
+
* @returns true if the error is a network error
|
|
118
|
+
*/
|
|
119
|
+
export function isNetworkError(error: unknown): boolean {
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
const message = error.message.toLowerCase()
|
|
122
|
+
return message.includes('network') ||
|
|
123
|
+
message.includes('fetch') ||
|
|
124
|
+
message.includes('connection')
|
|
125
|
+
}
|
|
126
|
+
return false
|
|
127
|
+
}
|