@ibdop/platform-kit 1.0.0
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/README.md +63 -0
- package/dist/components/ErrorBoundary.d.ts +68 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/Notification.d.ts +46 -0
- package/dist/components/Notification.d.ts.map +1 -0
- package/dist/components/VersionInfo.d.ts +18 -0
- package/dist/components/VersionInfo.d.ts.map +1 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useApi.d.ts +65 -0
- package/dist/hooks/useApi.d.ts.map +1 -0
- package/dist/hooks/useInfoData.d.ts +35 -0
- package/dist/hooks/useInfoData.d.ts.map +1 -0
- package/dist/hooks/usePermissions.d.ts +23 -0
- package/dist/hooks/usePermissions.d.ts.map +1 -0
- package/dist/hooks/useShellAuth.d.ts +23 -0
- package/dist/hooks/useShellAuth.d.ts.map +1 -0
- package/dist/hooks/useV1Config.d.ts +27 -0
- package/dist/hooks/useV1Config.d.ts.map +1 -0
- package/dist/index.cjs.js +18 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +1500 -0
- package/dist/index.umd.js +18 -0
- package/dist/services/api.d.ts +59 -0
- package/dist/services/api.d.ts.map +1 -0
- package/dist/services/index.d.ts +8 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/logger.d.ts +41 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/types/index.d.ts +178 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +90 -0
- package/src/components/ErrorBoundary.tsx +292 -0
- package/src/components/Notification.tsx +297 -0
- package/src/components/VersionInfo.tsx +271 -0
- package/src/components/index.ts +14 -0
- package/src/global.d.ts +40 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/useApi.ts +314 -0
- package/src/hooks/useInfoData.ts +124 -0
- package/src/hooks/usePermissions.ts +88 -0
- package/src/hooks/useShellAuth.ts +145 -0
- package/src/hooks/useV1Config.ts +112 -0
- package/src/index.ts +17 -0
- package/src/services/api.ts +290 -0
- package/src/services/index.ts +9 -0
- package/src/services/logger.ts +71 -0
- package/src/types/index.ts +215 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorBoundary - Компонент для обработки ошибок React
|
|
3
|
+
*
|
|
4
|
+
* Перехватывает ошибки и dispatch'ит событие в shell для централизованной обработки
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { Component, ReactNode, ErrorInfo } from 'react'
|
|
8
|
+
|
|
9
|
+
// MF Name for logging
|
|
10
|
+
const MF_NAME = 'platform-kit'
|
|
11
|
+
|
|
12
|
+
// Development mode flag
|
|
13
|
+
const isDev = import.meta.env?.DEV === true || import.meta.env?.MODE === 'development'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Logger for ErrorBoundary
|
|
17
|
+
*/
|
|
18
|
+
const logger = {
|
|
19
|
+
log: (...args: unknown[]) => {
|
|
20
|
+
if (isDev) console.log(`[${MF_NAME}]`, ...args)
|
|
21
|
+
},
|
|
22
|
+
warn: (...args: unknown[]) => {
|
|
23
|
+
if (isDev) console.warn(`[${MF_NAME}]`, ...args)
|
|
24
|
+
},
|
|
25
|
+
error: (...args: unknown[]) => {
|
|
26
|
+
console.error(`[${MF_NAME}]`, ...args)
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Props для ErrorBoundary
|
|
32
|
+
*/
|
|
33
|
+
interface ErrorBoundaryProps {
|
|
34
|
+
children: ReactNode
|
|
35
|
+
mfeName?: string
|
|
36
|
+
showDetails?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* State для ErrorBoundary
|
|
41
|
+
*/
|
|
42
|
+
interface ErrorBoundaryState {
|
|
43
|
+
hasError: boolean
|
|
44
|
+
error?: Error
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ErrorBoundary - компонент для перехвата и обработки ошибок
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <ErrorBoundary mfeName="@ib-dop/mf-example">
|
|
53
|
+
* <MyComponent />
|
|
54
|
+
* </ErrorBoundary>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
58
|
+
private hasDispatched = false
|
|
59
|
+
|
|
60
|
+
constructor(props: ErrorBoundaryProps) {
|
|
61
|
+
super(props)
|
|
62
|
+
this.state = { hasError: false }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Получить имя MF из props или window
|
|
67
|
+
*/
|
|
68
|
+
private getMfeName(): string {
|
|
69
|
+
if (this.props.mfeName) return this.props.mfeName
|
|
70
|
+
|
|
71
|
+
if (typeof window !== 'undefined') {
|
|
72
|
+
const win = window as unknown as { __MF_NAME__?: string }
|
|
73
|
+
if (win.__MF_NAME__) return win.__MF_NAME__
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return MF_NAME
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Определить нужно ли показывать детали ошибки
|
|
81
|
+
*/
|
|
82
|
+
private shouldShowDetails(): boolean {
|
|
83
|
+
// Priority: props > sessionStorage > NODE_ENV
|
|
84
|
+
if (this.props.showDetails !== undefined) {
|
|
85
|
+
return this.props.showDetails
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check sessionStorage config
|
|
89
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
90
|
+
try {
|
|
91
|
+
const storedConfig = sessionStorage.getItem('config')
|
|
92
|
+
if (storedConfig) {
|
|
93
|
+
const parsed = JSON.parse(storedConfig)
|
|
94
|
+
if (parsed.showErrorDetails !== undefined) {
|
|
95
|
+
return parsed.showErrorDetails
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore parse errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fallback to NODE_ENV
|
|
104
|
+
return isDev
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Dispatch ошибки в shell
|
|
109
|
+
*/
|
|
110
|
+
private dispatchError(error: Error, errorInfo?: ErrorInfo): void {
|
|
111
|
+
if (this.hasDispatched || typeof window === 'undefined') return
|
|
112
|
+
|
|
113
|
+
this.hasDispatched = true
|
|
114
|
+
const mfeName = this.getMfeName()
|
|
115
|
+
|
|
116
|
+
logger.error('ErrorBoundary caught:', error)
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
window.dispatchEvent(new CustomEvent('mfe-error', {
|
|
120
|
+
detail: {
|
|
121
|
+
mfeName,
|
|
122
|
+
error: error.message || String(error),
|
|
123
|
+
stack: error.stack,
|
|
124
|
+
componentStack: errorInfo?.componentStack,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
},
|
|
127
|
+
bubbles: true,
|
|
128
|
+
}))
|
|
129
|
+
} catch (dispatchError) {
|
|
130
|
+
logger.error('Failed to dispatch mfe-error event:', dispatchError)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Получить derived state из ошибки
|
|
136
|
+
*/
|
|
137
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
138
|
+
return { hasError: true, error }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Обработать ошибку
|
|
143
|
+
*/
|
|
144
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
145
|
+
this.dispatchError(error, errorInfo)
|
|
146
|
+
logger.error('Error info:', errorInfo.componentStack)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Копировать ошибку в буфер обмена
|
|
151
|
+
*/
|
|
152
|
+
private handleCopy = (): void => {
|
|
153
|
+
const errorText = `Error in ${this.getMfeName()}:\n${this.state.error?.message}\n${this.state.error?.stack}`
|
|
154
|
+
|
|
155
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
156
|
+
navigator.clipboard.writeText(errorText).then(() => {
|
|
157
|
+
alert('Ошибка скопирована в буфер обмена')
|
|
158
|
+
}).catch(() => {
|
|
159
|
+
prompt('Скопируйте ошибку:', errorText)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Повторить рендер
|
|
166
|
+
*/
|
|
167
|
+
private handleRetry = (): void => {
|
|
168
|
+
this.setState({ hasError: false, error: undefined })
|
|
169
|
+
this.hasDispatched = false
|
|
170
|
+
|
|
171
|
+
if (typeof window !== 'undefined') {
|
|
172
|
+
window.location.reload()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Перейти на главную
|
|
178
|
+
*/
|
|
179
|
+
private handleGoHome = (): void => {
|
|
180
|
+
if (typeof window !== 'undefined') {
|
|
181
|
+
window.location.href = '/'
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Рендер
|
|
187
|
+
*/
|
|
188
|
+
render(): ReactNode {
|
|
189
|
+
if (this.state.hasError) {
|
|
190
|
+
const errorMessage = this.state.error?.message || 'Unknown error'
|
|
191
|
+
const errorStack = this.state.error?.stack || ''
|
|
192
|
+
const showDetails = this.shouldShowDetails()
|
|
193
|
+
const mfeName = this.getMfeName()
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div style={{
|
|
197
|
+
padding: '20px',
|
|
198
|
+
textAlign: 'center',
|
|
199
|
+
color: '#d32f2f',
|
|
200
|
+
fontFamily: 'monospace',
|
|
201
|
+
background: '#ffebee',
|
|
202
|
+
border: '1px solid #ef5350',
|
|
203
|
+
borderRadius: '4px',
|
|
204
|
+
margin: '10px',
|
|
205
|
+
}}>
|
|
206
|
+
<h2 style={{ fontSize: '16px', margin: '0 0 8px 0' }}>
|
|
207
|
+
⚠️ Ошибка в {mfeName}
|
|
208
|
+
</h2>
|
|
209
|
+
<p style={{ fontSize: '12px', margin: 0 }}>
|
|
210
|
+
Произошла ошибка в микрофронтенде. Сообщение отправлено в shell.
|
|
211
|
+
</p>
|
|
212
|
+
|
|
213
|
+
{/* Error Details */}
|
|
214
|
+
{showDetails && (
|
|
215
|
+
<details style={{
|
|
216
|
+
whiteSpace: 'pre-wrap',
|
|
217
|
+
textAlign: 'left',
|
|
218
|
+
marginTop: '10px',
|
|
219
|
+
background: '#fff',
|
|
220
|
+
padding: '8px',
|
|
221
|
+
borderRadius: '4px',
|
|
222
|
+
}}>
|
|
223
|
+
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>Детали ошибки</summary>
|
|
224
|
+
<pre style={{
|
|
225
|
+
fontSize: '11px',
|
|
226
|
+
overflow: 'auto',
|
|
227
|
+
maxHeight: '150px',
|
|
228
|
+
margin: '8px 0 0 0',
|
|
229
|
+
padding: '8px',
|
|
230
|
+
background: '#f5f5f5',
|
|
231
|
+
borderRadius: '4px',
|
|
232
|
+
}}>
|
|
233
|
+
{errorMessage}
|
|
234
|
+
{errorStack && `\n\n${errorStack}`}
|
|
235
|
+
</pre>
|
|
236
|
+
</details>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Actions */}
|
|
240
|
+
<div style={{ marginTop: '12px', display: 'flex', gap: '8px', justifyContent: 'center' }}>
|
|
241
|
+
<button
|
|
242
|
+
onClick={this.handleCopy}
|
|
243
|
+
style={{
|
|
244
|
+
padding: '8px 12px',
|
|
245
|
+
background: '#666',
|
|
246
|
+
color: 'white',
|
|
247
|
+
border: 'none',
|
|
248
|
+
borderRadius: '4px',
|
|
249
|
+
cursor: 'pointer',
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
📋 Копировать
|
|
253
|
+
</button>
|
|
254
|
+
<button
|
|
255
|
+
onClick={this.handleRetry}
|
|
256
|
+
style={{
|
|
257
|
+
padding: '8px 12px',
|
|
258
|
+
background: '#d32f2f',
|
|
259
|
+
color: 'white',
|
|
260
|
+
border: 'none',
|
|
261
|
+
borderRadius: '4px',
|
|
262
|
+
cursor: 'pointer',
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
🔄 Обновить
|
|
266
|
+
</button>
|
|
267
|
+
<button
|
|
268
|
+
onClick={this.handleGoHome}
|
|
269
|
+
style={{
|
|
270
|
+
padding: '8px 12px',
|
|
271
|
+
background: '#1976d2',
|
|
272
|
+
color: 'white',
|
|
273
|
+
border: 'none',
|
|
274
|
+
borderRadius: '4px',
|
|
275
|
+
cursor: 'pointer',
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
🏠 На главную
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Null-check для children
|
|
286
|
+
if (this.props.children == null) {
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return this.props.children
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification - Компонент и хук для отображения уведомлений
|
|
3
|
+
*
|
|
4
|
+
* Dispatch уведомления в shell через CustomEvent
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { createContext, useContext, useCallback, useState, useEffect, ReactNode } from 'react'
|
|
8
|
+
import type { NotificationPayload, NotificationType } from '../types'
|
|
9
|
+
|
|
10
|
+
// MF Name for logging
|
|
11
|
+
const MF_NAME = 'platform-kit'
|
|
12
|
+
|
|
13
|
+
// Development mode flag
|
|
14
|
+
const isDev = import.meta.env?.DEV === true || import.meta.env?.MODE === 'development'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Logger for Notification
|
|
18
|
+
*/
|
|
19
|
+
const logger = {
|
|
20
|
+
log: (...args: unknown[]) => {
|
|
21
|
+
if (isDev) console.log(`[${MF_NAME}]`, ...args)
|
|
22
|
+
},
|
|
23
|
+
warn: (...args: unknown[]) => {
|
|
24
|
+
if (isDev) console.warn(`[${MF_NAME}]`, ...args)
|
|
25
|
+
},
|
|
26
|
+
error: (...args: unknown[]) => {
|
|
27
|
+
console.error(`[${MF_NAME}]`, ...args)
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Notification item
|
|
33
|
+
*/
|
|
34
|
+
interface NotificationItem extends NotificationPayload {
|
|
35
|
+
id: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Context for notifications
|
|
40
|
+
*/
|
|
41
|
+
interface NotificationContextValue {
|
|
42
|
+
notify: (payload: Omit<NotificationPayload, 'timestamp' | 'mfeName'>) => void
|
|
43
|
+
notifySuccess: (message: string, title?: string) => void
|
|
44
|
+
notifyError: (message: string, title?: string) => void
|
|
45
|
+
notifyWarning: (message: string, title?: string) => void
|
|
46
|
+
notifyInfo: (message: string, title?: string) => void
|
|
47
|
+
removeNotification: (id: string) => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Context for notifications
|
|
52
|
+
*/
|
|
53
|
+
const NotificationContext = createContext<NotificationContextValue | null>(null)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Provider for notifications
|
|
57
|
+
*/
|
|
58
|
+
interface NotificationProviderProps {
|
|
59
|
+
children: ReactNode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function NotificationProvider({ children }: NotificationProviderProps): React.ReactElement {
|
|
63
|
+
const [notifications, setNotifications] = useState<NotificationItem[]>([])
|
|
64
|
+
|
|
65
|
+
// Get mfeName
|
|
66
|
+
const mfeName = typeof window !== 'undefined'
|
|
67
|
+
? ((window as unknown as { __MF_NAME__?: string }).__MF_NAME__ || MF_NAME)
|
|
68
|
+
: MF_NAME
|
|
69
|
+
|
|
70
|
+
// Create notification
|
|
71
|
+
const createNotification = useCallback((
|
|
72
|
+
type: NotificationType,
|
|
73
|
+
title: string,
|
|
74
|
+
message: string,
|
|
75
|
+
duration?: number
|
|
76
|
+
): NotificationItem => ({
|
|
77
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
78
|
+
type,
|
|
79
|
+
title,
|
|
80
|
+
message,
|
|
81
|
+
mfeName,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
duration,
|
|
84
|
+
}), [mfeName])
|
|
85
|
+
|
|
86
|
+
// Add notification
|
|
87
|
+
const addNotification = useCallback((notification: NotificationItem) => {
|
|
88
|
+
setNotifications(prev => [...prev, notification].slice(0, 5))
|
|
89
|
+
|
|
90
|
+
// Auto remove after duration or 5 seconds
|
|
91
|
+
const duration = notification.duration || 5000
|
|
92
|
+
if (duration > 0) {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
setNotifications(prev => prev.filter(n => n.id !== notification.id))
|
|
95
|
+
}, duration)
|
|
96
|
+
}
|
|
97
|
+
}, [])
|
|
98
|
+
|
|
99
|
+
// Notify
|
|
100
|
+
const notify = useCallback((payload: Omit<NotificationPayload, 'timestamp' | 'mfeName'>) => {
|
|
101
|
+
const notification = createNotification(payload.type, payload.title, payload.message, payload.duration)
|
|
102
|
+
addNotification(notification)
|
|
103
|
+
|
|
104
|
+
// Dispatch to shell
|
|
105
|
+
if (typeof window !== 'undefined') {
|
|
106
|
+
window.dispatchEvent(new CustomEvent('mfe-notification', {
|
|
107
|
+
detail: notification,
|
|
108
|
+
bubbles: true,
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logger.log('Notification:', notification)
|
|
113
|
+
}, [createNotification, addNotification])
|
|
114
|
+
|
|
115
|
+
// Convenience methods
|
|
116
|
+
const notifySuccess = useCallback((message: string, title = 'Успешно') => {
|
|
117
|
+
notify({ type: 'success', title, message })
|
|
118
|
+
}, [notify])
|
|
119
|
+
|
|
120
|
+
const notifyError = useCallback((message: string, title = 'Ошибка') => {
|
|
121
|
+
notify({ type: 'error', title, message })
|
|
122
|
+
}, [notify])
|
|
123
|
+
|
|
124
|
+
const notifyWarning = useCallback((message: string, title = 'Предупреждение') => {
|
|
125
|
+
notify({ type: 'warning', title, message })
|
|
126
|
+
}, [notify])
|
|
127
|
+
|
|
128
|
+
const notifyInfo = useCallback((message: string, title = 'Информация') => {
|
|
129
|
+
notify({ type: 'info', title, message })
|
|
130
|
+
}, [notify])
|
|
131
|
+
|
|
132
|
+
// Remove notification
|
|
133
|
+
const removeNotification = useCallback((id: string) => {
|
|
134
|
+
setNotifications(prev => prev.filter(n => n.id !== id))
|
|
135
|
+
}, [])
|
|
136
|
+
|
|
137
|
+
// Handle incoming notifications from other MFE
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (typeof window === 'undefined') return
|
|
140
|
+
|
|
141
|
+
const handleNotification = (event: Event) => {
|
|
142
|
+
const detail = (event as CustomEvent).detail as NotificationPayload | undefined
|
|
143
|
+
if (detail && detail.type && detail.title && detail.message) {
|
|
144
|
+
const notification: NotificationItem = {
|
|
145
|
+
...detail,
|
|
146
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
147
|
+
}
|
|
148
|
+
addNotification(notification)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
window.addEventListener('mfe-notification', handleNotification)
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
window.removeEventListener('mfe-notification', handleNotification)
|
|
156
|
+
}
|
|
157
|
+
}, [addNotification])
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<NotificationContext.Provider value={{
|
|
161
|
+
notify,
|
|
162
|
+
notifySuccess,
|
|
163
|
+
notifyError,
|
|
164
|
+
notifyWarning,
|
|
165
|
+
notifyInfo,
|
|
166
|
+
removeNotification,
|
|
167
|
+
}}>
|
|
168
|
+
{children}
|
|
169
|
+
|
|
170
|
+
{/* Render notifications */}
|
|
171
|
+
{notifications.length > 0 && (
|
|
172
|
+
<div
|
|
173
|
+
style={{
|
|
174
|
+
position: 'fixed',
|
|
175
|
+
top: '80px',
|
|
176
|
+
right: '20px',
|
|
177
|
+
zIndex: 9998,
|
|
178
|
+
maxWidth: '400px',
|
|
179
|
+
width: '100%',
|
|
180
|
+
display: 'flex',
|
|
181
|
+
flexDirection: 'column',
|
|
182
|
+
gap: '8px',
|
|
183
|
+
pointerEvents: 'none',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{notifications.map(notification => (
|
|
187
|
+
<div
|
|
188
|
+
key={notification.id}
|
|
189
|
+
className={`notification notification-${notification.type}`}
|
|
190
|
+
style={{
|
|
191
|
+
pointerEvents: 'auto',
|
|
192
|
+
padding: '12px 16px',
|
|
193
|
+
borderRadius: '8px',
|
|
194
|
+
background: notification.type === 'success' ? '#d4edda' :
|
|
195
|
+
notification.type === 'error' ? '#f8d7da' :
|
|
196
|
+
notification.type === 'warning' ? '#fff3cd' : '#d1ecf1',
|
|
197
|
+
color: notification.type === 'success' ? '#155724' :
|
|
198
|
+
notification.type === 'error' ? '#721c24' :
|
|
199
|
+
notification.type === 'warning' ? '#856404' : '#0c5460',
|
|
200
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
201
|
+
display: 'flex',
|
|
202
|
+
alignItems: 'flex-start',
|
|
203
|
+
gap: '12px',
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<div style={{ flex: 1 }}>
|
|
207
|
+
<strong style={{ display: 'block', marginBottom: '4px' }}>
|
|
208
|
+
{notification.title}
|
|
209
|
+
</strong>
|
|
210
|
+
<p style={{ margin: 0, fontSize: '14px' }}>
|
|
211
|
+
{notification.message}
|
|
212
|
+
</p>
|
|
213
|
+
<small style={{ opacity: 0.7, fontSize: '12px' }}>
|
|
214
|
+
{notification.mfeName}
|
|
215
|
+
</small>
|
|
216
|
+
</div>
|
|
217
|
+
<button
|
|
218
|
+
onClick={() => removeNotification(notification.id)}
|
|
219
|
+
style={{
|
|
220
|
+
background: 'transparent',
|
|
221
|
+
border: 'none',
|
|
222
|
+
cursor: 'pointer',
|
|
223
|
+
fontSize: '18px',
|
|
224
|
+
lineHeight: 1,
|
|
225
|
+
opacity: 0.5,
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
×
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</NotificationContext.Provider>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Хук для использования уведомлений
|
|
240
|
+
*
|
|
241
|
+
* @returns NotificationContextValue
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```tsx
|
|
245
|
+
* const { notifySuccess, notifyError } = useNotification()
|
|
246
|
+
*
|
|
247
|
+
* const handleSave = async () => {
|
|
248
|
+
* try {
|
|
249
|
+
* await saveData()
|
|
250
|
+
* notifySuccess('Данные сохранены')
|
|
251
|
+
* } catch (error) {
|
|
252
|
+
* notifyError('Ошибка сохранения')
|
|
253
|
+
* }
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export function useNotification(): NotificationContextValue {
|
|
258
|
+
const context = useContext(NotificationContext)
|
|
259
|
+
|
|
260
|
+
if (!context) {
|
|
261
|
+
logger.warn('useNotification called outside NotificationProvider')
|
|
262
|
+
|
|
263
|
+
// Return no-op functions
|
|
264
|
+
return {
|
|
265
|
+
notify: () => {},
|
|
266
|
+
notifySuccess: () => {},
|
|
267
|
+
notifyError: () => {},
|
|
268
|
+
notifyWarning: () => {},
|
|
269
|
+
notifyInfo: () => {},
|
|
270
|
+
removeNotification: () => {},
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return context
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Dispatch notification to shell (standalone function)
|
|
279
|
+
*/
|
|
280
|
+
export function dispatchNotification(payload: Omit<NotificationPayload, 'timestamp' | 'mfeName'>): void {
|
|
281
|
+
if (typeof window === 'undefined') return
|
|
282
|
+
|
|
283
|
+
const mfeName = ((window as unknown) as { __MF_NAME__?: string }).__MF_NAME__ || MF_NAME
|
|
284
|
+
|
|
285
|
+
const fullPayload: NotificationPayload = {
|
|
286
|
+
...payload,
|
|
287
|
+
mfeName,
|
|
288
|
+
timestamp: Date.now(),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
window.dispatchEvent(new CustomEvent('mfe-notification', {
|
|
292
|
+
detail: fullPayload,
|
|
293
|
+
bubbles: true,
|
|
294
|
+
}))
|
|
295
|
+
|
|
296
|
+
logger.log('Dispatched notification:', fullPayload)
|
|
297
|
+
}
|