@okyrychenko-dev/react-action-guard 0.5.0 → 0.6.1
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 +147 -17
- package/dist/index.cjs +301 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1206 -62
- package/dist/index.d.ts +1206 -62
- package/dist/index.js +302 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -13,19 +13,131 @@ interface UseAsyncActionOptions {
|
|
|
13
13
|
onTimeout?: (blockerId: string) => void;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Wraps async functions with automatic UI blocking during execution.
|
|
17
17
|
*
|
|
18
|
-
* Creates a wrapper function that automatically
|
|
19
|
-
*
|
|
18
|
+
* Creates a wrapper function that automatically:
|
|
19
|
+
* 1. Adds a blocker before executing the async function
|
|
20
|
+
* 2. Executes the async function
|
|
21
|
+
* 3. Removes the blocker after completion (success or failure)
|
|
20
22
|
*
|
|
21
|
-
*
|
|
23
|
+
* Each invocation generates a unique blocker ID (actionId-counter), allowing
|
|
24
|
+
* multiple concurrent executions of the same action without ID conflicts.
|
|
25
|
+
* The blocker is removed even if the async function throws an error.
|
|
22
26
|
*
|
|
23
|
-
*
|
|
24
|
-
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
25
|
-
* @param scope - Scope(s) to block during execution (default: "global")
|
|
26
|
-
* @param options - Optional configuration including timeout
|
|
27
|
-
* @returns Function that wraps async functions with blocking behavior
|
|
27
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
28
28
|
*
|
|
29
|
+
* @template T - The return type of the async function being wrapped
|
|
30
|
+
* @param actionId - Base identifier for the action. Used in blocker ID and default reason.
|
|
31
|
+
* Example: "saveForm", "fetchData", "submitOrder"
|
|
32
|
+
* @param scope - Scope(s) to block during execution. Can be a single scope or array.
|
|
33
|
+
* Defaults to "global" if not specified.
|
|
34
|
+
* @param options - Optional configuration
|
|
35
|
+
* @param options.timeout - Timeout in milliseconds. Blocker auto-removes after this duration.
|
|
36
|
+
* @param options.onTimeout - Callback invoked when blocker times out before async completes
|
|
37
|
+
*
|
|
38
|
+
* @returns Wrapper function that accepts an async function and returns a Promise with the same type
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* Basic form submission
|
|
42
|
+
* ```tsx
|
|
43
|
+
* function MyForm() {
|
|
44
|
+
* const executeWithBlocking = useAsyncAction('saveForm', 'form');
|
|
45
|
+
*
|
|
46
|
+
* const handleSubmit = async (e) => {
|
|
47
|
+
* e.preventDefault();
|
|
48
|
+
*
|
|
49
|
+
* await executeWithBlocking(async () => {
|
|
50
|
+
* const response = await fetch('/api/save', {
|
|
51
|
+
* method: 'POST',
|
|
52
|
+
* body: JSON.stringify(formData)
|
|
53
|
+
* });
|
|
54
|
+
* return response.json();
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* alert('Form saved!');
|
|
58
|
+
* };
|
|
59
|
+
*
|
|
60
|
+
* return <form onSubmit={handleSubmit}>...</form>;
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* With timeout and error handling
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const executeWithBlocking = useAsyncAction(
|
|
68
|
+
* 'fetchData',
|
|
69
|
+
* ['global', 'data'],
|
|
70
|
+
* {
|
|
71
|
+
* timeout: 30000, // 30 seconds
|
|
72
|
+
* onTimeout: (blockerId) => {
|
|
73
|
+
* console.warn(`Operation ${blockerId} timed out`);
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* );
|
|
77
|
+
*
|
|
78
|
+
* const loadData = async () => {
|
|
79
|
+
* try {
|
|
80
|
+
* const data = await executeWithBlocking(async () => {
|
|
81
|
+
* return await fetchDataFromAPI();
|
|
82
|
+
* });
|
|
83
|
+
* setData(data);
|
|
84
|
+
* } catch (error) {
|
|
85
|
+
* console.error('Failed to load data:', error);
|
|
86
|
+
* // Blocker is automatically removed even on error
|
|
87
|
+
* }
|
|
88
|
+
* };
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* Multiple concurrent actions
|
|
93
|
+
* ```tsx
|
|
94
|
+
* function FileUploader() {
|
|
95
|
+
* const uploadWithBlocking = useAsyncAction('upload', 'uploads');
|
|
96
|
+
*
|
|
97
|
+
* const uploadFiles = async (files) => {
|
|
98
|
+
* // Each upload gets unique blocker ID (upload-1, upload-2, etc.)
|
|
99
|
+
* const promises = files.map(file =>
|
|
100
|
+
* uploadWithBlocking(() => uploadFile(file))
|
|
101
|
+
* );
|
|
102
|
+
*
|
|
103
|
+
* await Promise.all(promises);
|
|
104
|
+
* };
|
|
105
|
+
*
|
|
106
|
+
* return <...>;
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* Type-safe return value
|
|
112
|
+
* ```tsx
|
|
113
|
+
* interface UserData {
|
|
114
|
+
* id: string;
|
|
115
|
+
* name: string;
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* function UserProfile() {
|
|
119
|
+
* const executeWithBlocking = useAsyncAction<UserData>('fetchUser', 'user');
|
|
120
|
+
*
|
|
121
|
+
* const loadUser = async (userId: string) => {
|
|
122
|
+
* const userData = await executeWithBlocking(async () => {
|
|
123
|
+
* const response = await fetch(`/api/users/${userId}`);
|
|
124
|
+
* return response.json(); // TypeScript knows this is UserData
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* // userData is typed as UserData
|
|
128
|
+
* console.log(userData.name);
|
|
129
|
+
* };
|
|
130
|
+
*
|
|
131
|
+
* return <...>;
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* @see {@link useBlocker} for manual blocker management
|
|
136
|
+
* @see {@link useIsBlocked} to check blocking state
|
|
137
|
+
* @see {@link UseAsyncActionOptions} for available options
|
|
138
|
+
*
|
|
139
|
+
* @public
|
|
140
|
+
* @since 0.6.0
|
|
29
141
|
*/
|
|
30
142
|
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
31
143
|
|
|
@@ -45,7 +157,7 @@ interface CustomAnalyticsConfig {
|
|
|
45
157
|
}
|
|
46
158
|
type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
|
|
47
159
|
|
|
48
|
-
type BlockingAction = "add" | "remove" | "
|
|
160
|
+
type BlockingAction = "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
|
|
49
161
|
type MiddlewareBlockerConfig = BlockerConfig;
|
|
50
162
|
interface MiddlewareContext {
|
|
51
163
|
action: BlockingAction;
|
|
@@ -53,22 +165,200 @@ interface MiddlewareContext {
|
|
|
53
165
|
config?: MiddlewareBlockerConfig;
|
|
54
166
|
timestamp: number;
|
|
55
167
|
prevState?: MiddlewareBlockerConfig;
|
|
168
|
+
scope?: string;
|
|
169
|
+
count?: number;
|
|
56
170
|
}
|
|
57
171
|
type Middleware = (context: MiddlewareContext) => void | Promise<void>;
|
|
58
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Creates middleware for tracking blocker events to analytics platforms.
|
|
175
|
+
*
|
|
176
|
+
* Automatically tracks all blocker actions (add, remove, update, timeout, clear)
|
|
177
|
+
* to your analytics platform. Supports Google Analytics, Mixpanel, Amplitude,
|
|
178
|
+
* and custom analytics providers.
|
|
179
|
+
*
|
|
180
|
+
* Event data includes: blockerId, action, scope, reason, priority, timestamp,
|
|
181
|
+
* and duration (for remove events).
|
|
182
|
+
*
|
|
183
|
+
* @param config - Analytics configuration specifying provider and credentials
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* Google Analytics integration
|
|
187
|
+
* ```ts
|
|
188
|
+
* import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
189
|
+
*
|
|
190
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
191
|
+
* provider: 'ga',
|
|
192
|
+
* trackingId: 'GA-XXXXX-Y'
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* configureMiddleware([analyticsMiddleware]);
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* Mixpanel integration
|
|
200
|
+
* ```ts
|
|
201
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
202
|
+
* provider: 'mixpanel',
|
|
203
|
+
* token: 'your_mixpanel_token',
|
|
204
|
+
* options: {
|
|
205
|
+
* debug: true
|
|
206
|
+
* }
|
|
207
|
+
* });
|
|
208
|
+
*
|
|
209
|
+
* configureMiddleware([analyticsMiddleware]);
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* Custom analytics provider
|
|
214
|
+
* ```ts
|
|
215
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
216
|
+
* track: (eventName, eventData) => {
|
|
217
|
+
* // Send to your custom analytics
|
|
218
|
+
* myAnalytics.track(eventName, {
|
|
219
|
+
* ...eventData,
|
|
220
|
+
* userId: getCurrentUserId()
|
|
221
|
+
* });
|
|
222
|
+
* }
|
|
223
|
+
* });
|
|
224
|
+
* ```
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* Amplitude with user properties
|
|
228
|
+
* ```ts
|
|
229
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
230
|
+
* provider: 'amplitude',
|
|
231
|
+
* apiKey: 'your_amplitude_key',
|
|
232
|
+
* userId: 'user-123',
|
|
233
|
+
* userProperties: {
|
|
234
|
+
* plan: 'premium',
|
|
235
|
+
* role: 'admin'
|
|
236
|
+
* }
|
|
237
|
+
* });
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* @see {@link AnalyticsConfig} for configuration options
|
|
241
|
+
* @see {@link configureMiddleware} for registering middleware
|
|
242
|
+
*
|
|
243
|
+
* @public
|
|
244
|
+
* @since 0.6.0
|
|
245
|
+
*/
|
|
59
246
|
declare const createAnalyticsMiddleware: (config: AnalyticsConfig) => Middleware;
|
|
60
247
|
|
|
61
248
|
/**
|
|
62
|
-
*
|
|
249
|
+
* Configures and registers multiple middleware functions for the UI blocking store.
|
|
250
|
+
*
|
|
251
|
+
* Convenience function that registers an array of middleware at once. Each middleware
|
|
252
|
+
* is automatically assigned a unique name based on its array index ("middleware-0",
|
|
253
|
+
* "middleware-1", etc.). Middleware are executed in the order they are registered.
|
|
254
|
+
*
|
|
255
|
+
* All middleware receive {@link MiddlewareContext} containing action type, blocker ID,
|
|
256
|
+
* configuration, and timestamp for each blocking event.
|
|
63
257
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
258
|
+
* @param middlewares - Array of middleware functions to register. Functions are called
|
|
259
|
+
* in order for each blocking action (add, remove, update, etc.)
|
|
66
260
|
*
|
|
67
|
-
* @
|
|
261
|
+
* @example
|
|
262
|
+
* Basic usage with built-in middleware
|
|
263
|
+
* ```ts
|
|
264
|
+
* import { configureMiddleware, loggerMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
265
|
+
*
|
|
266
|
+
* configureMiddleware([
|
|
267
|
+
* loggerMiddleware,
|
|
268
|
+
* createPerformanceMiddleware({ threshold: 1000 })
|
|
269
|
+
* ]);
|
|
270
|
+
* ```
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* With custom middleware
|
|
274
|
+
* ```ts
|
|
275
|
+
* const customMiddleware: Middleware = (context) => {
|
|
276
|
+
* if (context.action === 'add') {
|
|
277
|
+
* console.log(`Blocker added: ${context.blockerId}`);
|
|
278
|
+
* }
|
|
279
|
+
* };
|
|
280
|
+
*
|
|
281
|
+
* configureMiddleware([
|
|
282
|
+
* loggerMiddleware,
|
|
283
|
+
* customMiddleware
|
|
284
|
+
* ]);
|
|
285
|
+
* ```
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* Analytics integration
|
|
289
|
+
* ```ts
|
|
290
|
+
* import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
68
291
|
*
|
|
292
|
+
* configureMiddleware([
|
|
293
|
+
* createAnalyticsMiddleware({
|
|
294
|
+
* provider: 'google-analytics',
|
|
295
|
+
* trackingId: 'GA-XXXXX'
|
|
296
|
+
* })
|
|
297
|
+
* ]);
|
|
298
|
+
* ```
|
|
299
|
+
*
|
|
300
|
+
* @see {@link Middleware} for middleware function signature
|
|
301
|
+
* @see {@link loggerMiddleware} for built-in logging middleware
|
|
302
|
+
* @see {@link createAnalyticsMiddleware} for analytics tracking
|
|
303
|
+
* @see {@link createPerformanceMiddleware} for performance monitoring
|
|
304
|
+
*
|
|
305
|
+
* @public
|
|
306
|
+
* @since 0.6.0
|
|
69
307
|
*/
|
|
70
308
|
declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): void;
|
|
71
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Built-in middleware for logging all blocking actions to the console.
|
|
312
|
+
*
|
|
313
|
+
* Logs every blocker action (add, remove, update, timeout, clear) with emoji
|
|
314
|
+
* indicators and formatted data. Useful for debugging blocking behavior during
|
|
315
|
+
* development. Automatically disabled in production builds for performance.
|
|
316
|
+
*
|
|
317
|
+
* Output format: `[UIBlocking] 🔒 blocker-id {scope: "form", reason: "..."}`
|
|
318
|
+
*
|
|
319
|
+
* Emoji indicators:
|
|
320
|
+
* - 🔒 Add
|
|
321
|
+
* - 🔓 Remove
|
|
322
|
+
* - 🔄 Update
|
|
323
|
+
* - ⏰ Timeout
|
|
324
|
+
* - 🧹 Clear/ClearScope
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* Register logger middleware
|
|
328
|
+
* ```ts
|
|
329
|
+
* import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
330
|
+
*
|
|
331
|
+
* // Enable logging for all blocking actions
|
|
332
|
+
* configureMiddleware([loggerMiddleware]);
|
|
333
|
+
* ```
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* Console output examples
|
|
337
|
+
* ```
|
|
338
|
+
* [UIBlocking] 🔒 save-form {scope: "form", reason: "Saving...", priority: 50}
|
|
339
|
+
* [UIBlocking] 🔄 save-form {scope: "form", reason: "Almost done..."}
|
|
340
|
+
* [UIBlocking] 🔓 save-form {reason: "Saving..."}
|
|
341
|
+
* [UIBlocking] ⏰ api-timeout {timeout: 30000}
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* Production check pattern
|
|
346
|
+
* ```ts
|
|
347
|
+
* import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
348
|
+
* const middlewares = [];
|
|
349
|
+
* if (process.env.NODE_ENV !== 'production') {
|
|
350
|
+
* middlewares.push(loggerMiddleware);
|
|
351
|
+
* }
|
|
352
|
+
* configureMiddleware(middlewares);
|
|
353
|
+
* ```
|
|
354
|
+
*
|
|
355
|
+
* @see {@link configureMiddleware} for registering middleware
|
|
356
|
+
* @see {@link Middleware} for middleware function signature
|
|
357
|
+
* @see {@link MiddlewareContext} for available context data
|
|
358
|
+
*
|
|
359
|
+
* @public
|
|
360
|
+
* @since 0.6.0
|
|
361
|
+
*/
|
|
72
362
|
declare const loggerMiddleware: Middleware;
|
|
73
363
|
|
|
74
364
|
interface PerformanceConfig {
|
|
@@ -76,14 +366,133 @@ interface PerformanceConfig {
|
|
|
76
366
|
onSlowBlock?: (blockerId: string, duration: number) => void;
|
|
77
367
|
}
|
|
78
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Creates middleware for monitoring blocker performance and detecting slow blocks.
|
|
371
|
+
*
|
|
372
|
+
* Tracks the duration of each blocker (from add to remove) and warns when a blocker
|
|
373
|
+
* exceeds the configured threshold. Useful for identifying performance issues,
|
|
374
|
+
* stuck blockers, or operations that take too long.
|
|
375
|
+
*
|
|
376
|
+
* Automatically logs warnings to console for slow blocks and optionally invokes
|
|
377
|
+
* a callback for custom handling (e.g., error reporting, analytics).
|
|
378
|
+
*
|
|
379
|
+
* @param config - Performance monitoring configuration
|
|
380
|
+
* @param config.slowBlockThreshold - Duration in ms before considering a block "slow".
|
|
381
|
+
* Defaults to 3000ms (3 seconds).
|
|
382
|
+
* @param config.onSlowBlock - Optional callback invoked when a slow block is detected.
|
|
383
|
+
* Receives blockerId and duration in milliseconds.
|
|
384
|
+
*
|
|
385
|
+
* @returns Middleware function for performance monitoring
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* Basic usage with default threshold
|
|
389
|
+
* ```ts
|
|
390
|
+
* import { configureMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
391
|
+
*
|
|
392
|
+
* const performanceMiddleware = createPerformanceMiddleware();
|
|
393
|
+
*
|
|
394
|
+
* configureMiddleware([performanceMiddleware]);
|
|
395
|
+
* // Warns if any blocker lasts > 3 seconds
|
|
396
|
+
* ```
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* Custom threshold and callback
|
|
400
|
+
* ```ts
|
|
401
|
+
* const performanceMiddleware = createPerformanceMiddleware({
|
|
402
|
+
* slowBlockThreshold: 5000, // 5 seconds
|
|
403
|
+
* onSlowBlock: (blockerId, duration) => {
|
|
404
|
+
* console.error(`Blocker ${blockerId} took ${duration}ms`);
|
|
405
|
+
*
|
|
406
|
+
* // Report to error tracking
|
|
407
|
+
* Sentry.captureMessage('Slow UI Blocker', {
|
|
408
|
+
* extra: { blockerId, duration }
|
|
409
|
+
* });
|
|
410
|
+
* }
|
|
411
|
+
* });
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* With analytics integration
|
|
416
|
+
* ```ts
|
|
417
|
+
* const performanceMiddleware = createPerformanceMiddleware({
|
|
418
|
+
* slowBlockThreshold: 2000,
|
|
419
|
+
* onSlowBlock: (blockerId, duration) => {
|
|
420
|
+
* // Track slow blocks in analytics
|
|
421
|
+
* analytics.track('slow_blocker_detected', {
|
|
422
|
+
* blocker_id: blockerId,
|
|
423
|
+
* duration_ms: duration,
|
|
424
|
+
* threshold_ms: 2000
|
|
425
|
+
* });
|
|
426
|
+
* }
|
|
427
|
+
* });
|
|
428
|
+
* ```
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* Development vs Production
|
|
432
|
+
* ```ts
|
|
433
|
+
* const performanceMiddleware = createPerformanceMiddleware({
|
|
434
|
+
* // Strict threshold in development
|
|
435
|
+
* slowBlockThreshold: process.env.NODE_ENV === 'production' ? 5000 : 1000,
|
|
436
|
+
* onSlowBlock: (blockerId, duration) => {
|
|
437
|
+
* if (process.env.NODE_ENV === 'production') {
|
|
438
|
+
* // Report to error service in production
|
|
439
|
+
* errorService.log({ blockerId, duration });
|
|
440
|
+
* } else {
|
|
441
|
+
* // Detailed logging in development
|
|
442
|
+
* console.table({ blockerId, duration });
|
|
443
|
+
* }
|
|
444
|
+
* }
|
|
445
|
+
* });
|
|
446
|
+
* ```
|
|
447
|
+
*
|
|
448
|
+
* @see {@link PerformanceConfig} for configuration options
|
|
449
|
+
* @see {@link configureMiddleware} for registering middleware
|
|
450
|
+
*
|
|
451
|
+
* @public
|
|
452
|
+
* @since 0.6.0
|
|
453
|
+
*/
|
|
79
454
|
declare const createPerformanceMiddleware: (config?: PerformanceConfig) => Middleware;
|
|
80
455
|
|
|
81
456
|
/**
|
|
82
|
-
* Configuration for a blocker
|
|
457
|
+
* Configuration options for creating or updating a blocker.
|
|
458
|
+
*
|
|
459
|
+
* All fields are optional. Defaults will be applied for any missing values:
|
|
460
|
+
* - `scope`: "global"
|
|
461
|
+
* - `reason`: "Blocking UI"
|
|
462
|
+
* - `priority`: 50
|
|
463
|
+
* - `timestamp`: Current time
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* Basic configuration
|
|
467
|
+
* ```ts
|
|
468
|
+
* const config: BlockerConfig = {
|
|
469
|
+
* scope: 'form',
|
|
470
|
+
* reason: 'Submitting data...',
|
|
471
|
+
* priority: 70
|
|
472
|
+
* };
|
|
473
|
+
* ```
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* With timeout and callback
|
|
477
|
+
* ```ts
|
|
478
|
+
* const config: BlockerConfig = {
|
|
479
|
+
* scope: ['form', 'navigation'],
|
|
480
|
+
* reason: 'Critical operation in progress',
|
|
481
|
+
* priority: 90,
|
|
482
|
+
* timeout: 30000, // 30 seconds
|
|
483
|
+
* onTimeout: (id) => {
|
|
484
|
+
* console.warn(`Blocker ${id} timed out`);
|
|
485
|
+
* }
|
|
486
|
+
* };
|
|
487
|
+
* ```
|
|
488
|
+
*
|
|
489
|
+
* @public
|
|
490
|
+
* @since 0.6.0
|
|
83
491
|
*/
|
|
84
492
|
interface BlockerConfig {
|
|
85
493
|
scope?: string | ReadonlyArray<string>;
|
|
86
494
|
reason?: string;
|
|
495
|
+
/** Priority level (negative values are normalized to 0) */
|
|
87
496
|
priority?: number;
|
|
88
497
|
timestamp?: number;
|
|
89
498
|
/** Automatically remove the blocker after N milliseconds */
|
|
@@ -92,7 +501,16 @@ interface BlockerConfig {
|
|
|
92
501
|
onTimeout?: (blockerId: string) => void;
|
|
93
502
|
}
|
|
94
503
|
/**
|
|
95
|
-
*
|
|
504
|
+
* Internal representation of a blocker with all fields populated.
|
|
505
|
+
*
|
|
506
|
+
* This is the stored format in the Map. All optional fields from {@link BlockerConfig}
|
|
507
|
+
* are converted to required fields with default values applied.
|
|
508
|
+
*
|
|
509
|
+
* @internal This type is primarily for internal use. Users should work with
|
|
510
|
+
* {@link BlockerConfig} for configuration and {@link BlockerInfo} for
|
|
511
|
+
* reading blocker data.
|
|
512
|
+
*
|
|
513
|
+
* @since 0.6.0
|
|
96
514
|
*/
|
|
97
515
|
interface StoredBlocker {
|
|
98
516
|
scope: string | ReadonlyArray<string>;
|
|
@@ -107,23 +525,74 @@ interface StoredBlocker {
|
|
|
107
525
|
onTimeout?: (blockerId: string) => void;
|
|
108
526
|
}
|
|
109
527
|
/**
|
|
110
|
-
*
|
|
528
|
+
* Complete blocker information including its unique identifier.
|
|
529
|
+
*
|
|
530
|
+
* This type extends {@link StoredBlocker} with the blocker's ID, making it
|
|
531
|
+
* suitable for reading and displaying blocker information. Returned by
|
|
532
|
+
* {@link UIBlockingStoreActions.getBlockingInfo} and used by {@link useBlockingInfo}.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* Accessing blocker info
|
|
536
|
+
* ```ts
|
|
537
|
+
* const blockers: BlockerInfo[] = store.getState().getBlockingInfo('form');
|
|
538
|
+
* blockers.forEach(blocker => {
|
|
539
|
+
* console.log(`${blocker.id}: ${blocker.reason} (priority: ${blocker.priority})`);
|
|
540
|
+
* });
|
|
541
|
+
* ```
|
|
542
|
+
*
|
|
543
|
+
* @public
|
|
544
|
+
* @since 0.6.0
|
|
111
545
|
*/
|
|
112
546
|
interface BlockerInfo extends StoredBlocker {
|
|
113
547
|
id: string;
|
|
114
548
|
}
|
|
115
549
|
/**
|
|
116
|
-
* Internal store
|
|
550
|
+
* Internal state structure of the UI blocking store.
|
|
551
|
+
*
|
|
552
|
+
* Contains the core data structures for managing blockers and middleware.
|
|
553
|
+
* This state is combined with {@link UIBlockingStoreActions} to create the
|
|
554
|
+
* complete {@link UIBlockingStore} type.
|
|
555
|
+
*
|
|
556
|
+
* @internal
|
|
557
|
+
* @since 0.6.0
|
|
117
558
|
*/
|
|
118
559
|
interface UIBlockingStoreState {
|
|
119
560
|
activeBlockers: Map<string, StoredBlocker>;
|
|
120
561
|
middlewares: Map<string, Middleware>;
|
|
121
562
|
}
|
|
122
563
|
/**
|
|
123
|
-
*
|
|
564
|
+
* All available actions for managing UI blocking state.
|
|
565
|
+
*
|
|
566
|
+
* These methods allow adding, removing, updating blockers, checking blocking status,
|
|
567
|
+
* and managing middleware. Combined with {@link UIBlockingStoreState} to form
|
|
568
|
+
* the complete {@link UIBlockingStore} type.
|
|
569
|
+
*
|
|
570
|
+
* All actions are safe to call multiple times and handle edge cases gracefully.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* Using store actions directly
|
|
574
|
+
* ```ts
|
|
575
|
+
* import { uiBlockingStoreApi } from '@okyrychenko-dev/react-action-guard';
|
|
576
|
+
*
|
|
577
|
+
* // Add a blocker
|
|
578
|
+
* uiBlockingStoreApi.getState().addBlocker('my-blocker', {
|
|
579
|
+
* scope: 'form',
|
|
580
|
+
* reason: 'Processing...'
|
|
581
|
+
* });
|
|
582
|
+
*
|
|
583
|
+
* // Check if blocked
|
|
584
|
+
* const isBlocked = uiBlockingStoreApi.getState().isBlocked('form');
|
|
585
|
+
*
|
|
586
|
+
* // Remove blocker
|
|
587
|
+
* uiBlockingStoreApi.getState().removeBlocker('my-blocker');
|
|
588
|
+
* ```
|
|
589
|
+
*
|
|
590
|
+
* @public
|
|
591
|
+
* @since 0.6.0
|
|
124
592
|
*/
|
|
125
593
|
interface UIBlockingStoreActions {
|
|
126
594
|
addBlocker: (id: string, config?: BlockerConfig) => void;
|
|
595
|
+
updateBlocker: (id: string, config?: Partial<BlockerConfig>) => void;
|
|
127
596
|
removeBlocker: (id: string) => void;
|
|
128
597
|
isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
129
598
|
getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
|
|
@@ -134,7 +603,32 @@ interface UIBlockingStoreActions {
|
|
|
134
603
|
runMiddlewares: (context: MiddlewareContext) => Promise<void>;
|
|
135
604
|
}
|
|
136
605
|
/**
|
|
137
|
-
* Complete store type
|
|
606
|
+
* Complete UI blocking store type combining state and actions.
|
|
607
|
+
*
|
|
608
|
+
* This is the full store interface used by hooks like {@link useUIBlockingStore}
|
|
609
|
+
* and accessible via {@link uiBlockingStoreApi}. It combines all state properties
|
|
610
|
+
* from {@link UIBlockingStoreState} with all action methods from {@link UIBlockingStoreActions}.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* Using the complete store
|
|
614
|
+
* ```ts
|
|
615
|
+
* import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
|
|
616
|
+
*
|
|
617
|
+
* function MyComponent() {
|
|
618
|
+
* const store = useUIBlockingStore();
|
|
619
|
+
*
|
|
620
|
+
* // Access state
|
|
621
|
+
* console.log(store.activeBlockers.size);
|
|
622
|
+
*
|
|
623
|
+
* // Call actions
|
|
624
|
+
* store.addBlocker('my-blocker', { scope: 'form' });
|
|
625
|
+
*
|
|
626
|
+
* return <div>...</div>;
|
|
627
|
+
* }
|
|
628
|
+
* ```
|
|
629
|
+
*
|
|
630
|
+
* @public
|
|
631
|
+
* @since 0.6.0
|
|
138
632
|
*/
|
|
139
633
|
type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
|
|
140
634
|
|
|
@@ -158,35 +652,157 @@ declare const useUIBlockingStore: {
|
|
|
158
652
|
declare const uiBlockingStoreApi: zustand.StoreApi<UIBlockingStore>;
|
|
159
653
|
|
|
160
654
|
/**
|
|
161
|
-
*
|
|
655
|
+
* Automatically manages a UI blocker based on component lifecycle.
|
|
656
|
+
*
|
|
657
|
+
* Creates a blocker that is automatically added when the condition is true
|
|
658
|
+
* and removed when the component unmounts or condition becomes false.
|
|
659
|
+
* Supports scoped blocking, priorities, timeouts, and works with both
|
|
660
|
+
* global store and isolated provider instances.
|
|
162
661
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
662
|
+
* @param blockerId - Unique identifier for this blocker. Must be unique within the scope.
|
|
663
|
+
* @param config - Configuration object for the blocker
|
|
664
|
+
* @param config.scope - Scope(s) to block. Can be a single scope string (e.g., "form", "navigation")
|
|
665
|
+
* or an array of scopes (e.g., ["form", "navigation"]). Defaults to "global".
|
|
666
|
+
* @param config.reason - Human-readable reason for blocking, displayed in DevTools and UI
|
|
667
|
+
* @param config.priority - Priority level (0-100). Higher priority blockers take precedence.
|
|
668
|
+
* Use 0-30 for low priority, 31-70 for medium, 71-100 for high priority.
|
|
669
|
+
* @param config.timeout - Auto-removal timeout in milliseconds. Blocker will be automatically
|
|
670
|
+
* removed after this duration. Use 0 or undefined for no timeout.
|
|
671
|
+
* @param config.onTimeout - Callback function invoked when the blocker times out
|
|
672
|
+
* @param isActive - When true, blocker is active; when false, blocker is removed.
|
|
673
|
+
* Defaults to true. Use this for conditional blocking.
|
|
165
674
|
*
|
|
166
|
-
*
|
|
675
|
+
* @example
|
|
676
|
+
* Basic form submission blocking
|
|
677
|
+
* ```tsx
|
|
678
|
+
* function MyForm() {
|
|
679
|
+
* const [isSubmitting, setIsSubmitting] = useState(false);
|
|
680
|
+
*
|
|
681
|
+
* useBlocker('form-submit', {
|
|
682
|
+
* scope: 'form',
|
|
683
|
+
* reason: 'Submitting form...',
|
|
684
|
+
* priority: 50,
|
|
685
|
+
* timeout: 30000, // Auto-remove after 30 seconds
|
|
686
|
+
* onTimeout: () => {
|
|
687
|
+
* console.warn('Form submission timed out');
|
|
688
|
+
* setIsSubmitting(false);
|
|
689
|
+
* }
|
|
690
|
+
* }, isSubmitting);
|
|
691
|
+
*
|
|
692
|
+
* return <form onSubmit={handleSubmit}>...</form>;
|
|
693
|
+
* }
|
|
694
|
+
* ```
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* Multiple scopes with high priority
|
|
698
|
+
* ```tsx
|
|
699
|
+
* useBlocker('critical-save', {
|
|
700
|
+
* scope: ['form', 'navigation', 'actions'],
|
|
701
|
+
* reason: 'Saving critical data...',
|
|
702
|
+
* priority: 90,
|
|
703
|
+
* }, isSaving);
|
|
704
|
+
* ```
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* Conditional blocking based on unsaved changes
|
|
708
|
+
* ```tsx
|
|
709
|
+
* function Editor() {
|
|
710
|
+
* const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
711
|
+
*
|
|
712
|
+
* useBlocker('unsaved-changes', {
|
|
713
|
+
* scope: 'navigation',
|
|
714
|
+
* reason: 'You have unsaved changes',
|
|
715
|
+
* priority: 80,
|
|
716
|
+
* }, hasUnsavedChanges);
|
|
717
|
+
*
|
|
718
|
+
* return <div>...</div>;
|
|
719
|
+
* }
|
|
720
|
+
* ```
|
|
167
721
|
*
|
|
168
|
-
* @
|
|
169
|
-
* @
|
|
170
|
-
* @
|
|
722
|
+
* @see {@link useIsBlocked} to check if a scope is currently blocked
|
|
723
|
+
* @see {@link useBlockingInfo} to get detailed blocker information
|
|
724
|
+
* @see {@link useAsyncAction} for async operation wrapping with automatic blocking
|
|
725
|
+
* @see {@link useConfirmableBlocker} for blockers that require user confirmation
|
|
171
726
|
*
|
|
727
|
+
* @public
|
|
728
|
+
* @since 0.6.0
|
|
172
729
|
*/
|
|
173
730
|
declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
|
|
174
731
|
|
|
175
732
|
/**
|
|
176
|
-
*
|
|
733
|
+
* Gets detailed information about all active blockers for a specific scope.
|
|
177
734
|
*
|
|
178
|
-
* Returns an array of blocker information objects
|
|
179
|
-
*
|
|
735
|
+
* Returns an array of blocker information objects, sorted by priority (highest first).
|
|
736
|
+
* Each blocker object contains id, reason, priority, and scope information.
|
|
737
|
+
* The hook automatically re-renders when blockers are added, removed, or updated.
|
|
180
738
|
*
|
|
181
|
-
* Uses a stable reference - only re-renders when the actual
|
|
182
|
-
* not on every store update.
|
|
739
|
+
* Uses memoization to provide a stable reference - only re-renders when the actual
|
|
740
|
+
* blocker list changes, not on every store update. This makes it efficient for
|
|
741
|
+
* rendering lists of blockers.
|
|
183
742
|
*
|
|
184
|
-
*
|
|
743
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
185
744
|
*
|
|
186
|
-
* @param scope - Scope to get blocking information for
|
|
187
|
-
* @returns Array of
|
|
745
|
+
* @param scope - Scope to get blocking information for. Defaults to "global" if not specified.
|
|
746
|
+
* @returns Array of {@link BlockerInfo} objects, sorted by priority (highest to lowest)
|
|
188
747
|
*
|
|
748
|
+
* @example
|
|
749
|
+
* Display list of active blockers
|
|
750
|
+
* ```tsx
|
|
751
|
+
* function BlockerList() {
|
|
752
|
+
* const blockers = useBlockingInfo('form');
|
|
753
|
+
*
|
|
754
|
+
* if (blockers.length === 0) {
|
|
755
|
+
* return <div>No active blockers</div>;
|
|
756
|
+
* }
|
|
757
|
+
*
|
|
758
|
+
* return (
|
|
759
|
+
* <ul>
|
|
760
|
+
* {blockers.map((blocker) => (
|
|
761
|
+
* <li key={blocker.id}>
|
|
762
|
+
* {blocker.reason} (Priority: {blocker.priority})
|
|
763
|
+
* </li>
|
|
764
|
+
* ))}
|
|
765
|
+
* </ul>
|
|
766
|
+
* );
|
|
767
|
+
* }
|
|
768
|
+
* ```
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* Show highest priority blocker reason
|
|
772
|
+
* ```tsx
|
|
773
|
+
* function BlockingBanner() {
|
|
774
|
+
* const blockers = useBlockingInfo('global');
|
|
775
|
+
*
|
|
776
|
+
* // blockers are already sorted by priority (highest first)
|
|
777
|
+
* const topBlocker = blockers[0];
|
|
778
|
+
*
|
|
779
|
+
* if (!topBlocker) {
|
|
780
|
+
* return null;
|
|
781
|
+
* }
|
|
782
|
+
*
|
|
783
|
+
* return <div className="banner">{topBlocker.reason}</div>;
|
|
784
|
+
* }
|
|
189
785
|
* ```
|
|
786
|
+
*
|
|
787
|
+
* @example
|
|
788
|
+
* Count active blockers
|
|
789
|
+
* ```tsx
|
|
790
|
+
* function BlockerCount() {
|
|
791
|
+
* const formBlockers = useBlockingInfo('form');
|
|
792
|
+
* const navBlockers = useBlockingInfo('navigation');
|
|
793
|
+
*
|
|
794
|
+
* const total = formBlockers.length + navBlockers.length;
|
|
795
|
+
*
|
|
796
|
+
* return <div>{total} active blockers</div>;
|
|
797
|
+
* }
|
|
798
|
+
* ```
|
|
799
|
+
*
|
|
800
|
+
* @see {@link useIsBlocked} for a simple boolean check
|
|
801
|
+
* @see {@link useBlocker} to create blockers
|
|
802
|
+
* @see {@link BlockerInfo} for the structure of blocker information objects
|
|
803
|
+
*
|
|
804
|
+
* @public
|
|
805
|
+
* @since 0.6.0
|
|
190
806
|
*/
|
|
191
807
|
declare const useBlockingInfo: (scope?: string) => ReadonlyArray<BlockerInfo>;
|
|
192
808
|
|
|
@@ -208,16 +824,150 @@ interface ConditionalBlockerConfig<TState = unknown> extends Omit<BlockerConfig,
|
|
|
208
824
|
}
|
|
209
825
|
|
|
210
826
|
/**
|
|
211
|
-
*
|
|
827
|
+
* Blocks UI based on a dynamic condition that is periodically evaluated.
|
|
828
|
+
*
|
|
829
|
+
* Periodically checks a condition function and automatically adds or removes
|
|
830
|
+
* a blocker based on the result. Useful for blocking based on application state,
|
|
831
|
+
* network status, feature flags, or any other dynamic condition.
|
|
212
832
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
833
|
+
* The condition function receives optional state and returns a boolean.
|
|
834
|
+
* It's evaluated immediately on mount and then at the specified interval.
|
|
835
|
+
* The blocker is added/removed automatically as the condition changes.
|
|
215
836
|
*
|
|
216
|
-
*
|
|
837
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
217
838
|
*
|
|
218
|
-
* @
|
|
219
|
-
* @param
|
|
839
|
+
* @template TState - Type of the state object passed to the condition function
|
|
840
|
+
* @param blockerId - Unique identifier for this conditional blocker
|
|
841
|
+
* @param config - Configuration for conditional blocking
|
|
842
|
+
* @param config.condition - Function that returns true to block, false to unblock.
|
|
843
|
+
* Receives the `state` object as parameter.
|
|
844
|
+
* @param config.state - Optional state object passed to the condition function
|
|
845
|
+
* @param config.checkInterval - How often to check the condition in milliseconds.
|
|
846
|
+
* Defaults to 1000ms (1 second). Lower values mean
|
|
847
|
+
* more responsive blocking but higher CPU usage.
|
|
848
|
+
* @param config.scope - Scope(s) to block when condition is true
|
|
849
|
+
* @param config.reason - Reason for blocking (displayed to users)
|
|
850
|
+
* @param config.priority - Optional blocker priority (0-100)
|
|
220
851
|
*
|
|
852
|
+
* @example
|
|
853
|
+
* Block based on network status
|
|
854
|
+
* ```tsx
|
|
855
|
+
* function OfflineBlocker() {
|
|
856
|
+
* const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
857
|
+
*
|
|
858
|
+
* useEffect(() => {
|
|
859
|
+
* const handleOnline = () => setIsOnline(true);
|
|
860
|
+
* const handleOffline = () => setIsOnline(false);
|
|
861
|
+
*
|
|
862
|
+
* window.addEventListener('online', handleOnline);
|
|
863
|
+
* window.addEventListener('offline', handleOffline);
|
|
864
|
+
*
|
|
865
|
+
* return () => {
|
|
866
|
+
* window.removeEventListener('online', handleOnline);
|
|
867
|
+
* window.removeEventListener('offline', handleOffline);
|
|
868
|
+
* };
|
|
869
|
+
* }, []);
|
|
870
|
+
*
|
|
871
|
+
* useConditionalBlocker('offline', {
|
|
872
|
+
* condition: () => !isOnline,
|
|
873
|
+
* state: { isOnline },
|
|
874
|
+
* checkInterval: 500, // Check every 500ms
|
|
875
|
+
* scope: 'global',
|
|
876
|
+
* reason: 'No internet connection. Please check your network.',
|
|
877
|
+
* priority: 95
|
|
878
|
+
* });
|
|
879
|
+
*
|
|
880
|
+
* return <App />;
|
|
881
|
+
* }
|
|
882
|
+
* ```
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* Block based on feature flag
|
|
886
|
+
* ```tsx
|
|
887
|
+
* function FeatureFlagBlocker() {
|
|
888
|
+
* const featureFlags = useFeatureFlags();
|
|
889
|
+
*
|
|
890
|
+
* useConditionalBlocker('feature-disabled', {
|
|
891
|
+
* condition: (state) => !state.featureFlags.newFeatureEnabled,
|
|
892
|
+
* state: { featureFlags },
|
|
893
|
+
* checkInterval: 5000, // Check every 5 seconds
|
|
894
|
+
* scope: 'new-feature',
|
|
895
|
+
* reason: 'This feature is currently disabled',
|
|
896
|
+
* priority: 70
|
|
897
|
+
* });
|
|
898
|
+
*
|
|
899
|
+
* return <NewFeature />;
|
|
900
|
+
* }
|
|
901
|
+
* ```
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* Block during API rate limit
|
|
905
|
+
* ```tsx
|
|
906
|
+
* function RateLimitBlocker() {
|
|
907
|
+
* const [requestCount, setRequestCount] = useState(0);
|
|
908
|
+
* const [resetTime, setResetTime] = useState<number | null>(null);
|
|
909
|
+
*
|
|
910
|
+
* useConditionalBlocker('rate-limit', {
|
|
911
|
+
* condition: (state) => {
|
|
912
|
+
* if (!state.resetTime) {
|
|
913
|
+
* return false;
|
|
914
|
+
* }
|
|
915
|
+
* return Date.now() < state.resetTime && state.requestCount >= 100;
|
|
916
|
+
* },
|
|
917
|
+
* state: { requestCount, resetTime },
|
|
918
|
+
* checkInterval: 1000,
|
|
919
|
+
* scope: 'api',
|
|
920
|
+
* reason: 'API rate limit reached. Please wait.',
|
|
921
|
+
* priority: 90
|
|
922
|
+
* });
|
|
923
|
+
*
|
|
924
|
+
* return <Dashboard />;
|
|
925
|
+
* }
|
|
926
|
+
* ```
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* Block based on form validation
|
|
930
|
+
* ```tsx
|
|
931
|
+
* function FormValidationBlocker() {
|
|
932
|
+
* const { formState } = useFormContext();
|
|
933
|
+
*
|
|
934
|
+
* useConditionalBlocker('invalid-form', {
|
|
935
|
+
* condition: (state) => !state.isValid && state.isDirty,
|
|
936
|
+
* state: formState,
|
|
937
|
+
* checkInterval: 500,
|
|
938
|
+
* scope: 'form-submit',
|
|
939
|
+
* reason: 'Please fix form errors before submitting',
|
|
940
|
+
* priority: 60
|
|
941
|
+
* });
|
|
942
|
+
*
|
|
943
|
+
* return <Form />;
|
|
944
|
+
* }
|
|
945
|
+
* ```
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* Without state parameter (simple condition)
|
|
949
|
+
* ```tsx
|
|
950
|
+
* useConditionalBlocker('low-battery', {
|
|
951
|
+
* condition: () => {
|
|
952
|
+
* // Check battery level if available
|
|
953
|
+
* if ('getBattery' in navigator) {
|
|
954
|
+
* return navigator.getBattery().then(b => b.level < 0.05);
|
|
955
|
+
* }
|
|
956
|
+
* return false;
|
|
957
|
+
* },
|
|
958
|
+
* checkInterval: 30000, // Check every 30 seconds
|
|
959
|
+
* scope: 'heavy-operations',
|
|
960
|
+
* reason: 'Battery level too low for this operation',
|
|
961
|
+
* priority: 80
|
|
962
|
+
* });
|
|
963
|
+
* ```
|
|
964
|
+
*
|
|
965
|
+
* @see {@link useBlocker} for simple conditional blocking with boolean
|
|
966
|
+
* @see {@link useScheduledBlocker} for time-based blocking
|
|
967
|
+
* @see {@link ConditionalBlockerConfig} for configuration options
|
|
968
|
+
*
|
|
969
|
+
* @public
|
|
970
|
+
* @since 0.6.0
|
|
221
971
|
*/
|
|
222
972
|
declare const useConditionalBlocker: <TState = unknown>(blockerId: string, config: ConditionalBlockerConfig<TState>) => void;
|
|
223
973
|
|
|
@@ -263,22 +1013,211 @@ interface UseConfirmableBlockerReturn {
|
|
|
263
1013
|
}
|
|
264
1014
|
|
|
265
1015
|
/**
|
|
266
|
-
*
|
|
1016
|
+
* Creates a blocker that requires user confirmation before executing an action.
|
|
1017
|
+
*
|
|
1018
|
+
* Manages a confirmation dialog and blocking state for actions that need user approval.
|
|
1019
|
+
* The blocker is active during both the confirmation dialog display and action execution.
|
|
1020
|
+
*
|
|
1021
|
+
* Workflow:
|
|
1022
|
+
* 1. Call `execute()` to show confirmation dialog (blocker activated)
|
|
1023
|
+
* 2. User confirms → `onConfirm` callback runs (blocker remains active)
|
|
1024
|
+
* 3. Action completes → blocker removed
|
|
1025
|
+
* 4. User cancels → blocker removed, optional `onCancel` callback runs
|
|
1026
|
+
*
|
|
1027
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1028
|
+
*
|
|
1029
|
+
* @param blockerId - Unique identifier for this confirmable blocker
|
|
1030
|
+
* @param config - Configuration for the confirmable action
|
|
1031
|
+
* @param config.confirmMessage - Message to display in the confirmation dialog
|
|
1032
|
+
* @param config.confirmTitle - Optional dialog title (defaults to "Confirm Action")
|
|
1033
|
+
* @param config.confirmButtonText - Optional confirm button text (defaults to "Confirm")
|
|
1034
|
+
* @param config.cancelButtonText - Optional cancel button text (defaults to "Cancel")
|
|
1035
|
+
* @param config.onConfirm - Async function to execute after user confirms
|
|
1036
|
+
* @param config.onCancel - Optional callback when user cancels
|
|
1037
|
+
* @param config.scope - Scope(s) to block during confirmation and execution
|
|
1038
|
+
* @param config.reason - Optional custom blocking reason (defaults to confirmMessage)
|
|
1039
|
+
* @param config.priority - Optional blocker priority (0-100)
|
|
1040
|
+
*
|
|
1041
|
+
* @returns Object containing:
|
|
1042
|
+
* - `execute`: Function to trigger the confirmation dialog
|
|
1043
|
+
* - `isDialogOpen`: Boolean indicating if confirmation dialog is visible
|
|
1044
|
+
* - `isExecuting`: Boolean indicating if the confirmed action is executing
|
|
1045
|
+
* - `confirmConfig`: Dialog configuration (title, message, button texts)
|
|
1046
|
+
* - `onConfirm`: Function to call when user confirms
|
|
1047
|
+
* - `onCancel`: Function to call when user cancels
|
|
1048
|
+
*
|
|
1049
|
+
* @example
|
|
1050
|
+
* Delete confirmation with custom dialog
|
|
1051
|
+
* ```tsx
|
|
1052
|
+
* function DeleteButton({ itemId, itemName }) {
|
|
1053
|
+
* const confirmableDelete = useConfirmableBlocker('delete-item', {
|
|
1054
|
+
* confirmMessage: `Are you sure you want to delete "${itemName}"?`,
|
|
1055
|
+
* confirmTitle: 'Delete Item',
|
|
1056
|
+
* confirmButtonText: 'Delete',
|
|
1057
|
+
* cancelButtonText: 'Keep',
|
|
1058
|
+
* scope: ['form', 'navigation'],
|
|
1059
|
+
* priority: 80,
|
|
1060
|
+
* onConfirm: async () => {
|
|
1061
|
+
* await deleteItem(itemId);
|
|
1062
|
+
* showToast('Item deleted successfully');
|
|
1063
|
+
* },
|
|
1064
|
+
* onCancel: () => {
|
|
1065
|
+
* console.log('Delete cancelled');
|
|
1066
|
+
* }
|
|
1067
|
+
* });
|
|
267
1068
|
*
|
|
1069
|
+
* return (
|
|
1070
|
+
* <>
|
|
1071
|
+
* <button onClick={confirmableDelete.execute}>
|
|
1072
|
+
* Delete
|
|
1073
|
+
* </button>
|
|
1074
|
+
*
|
|
1075
|
+
* {confirmableDelete.isDialogOpen && (
|
|
1076
|
+
* <ConfirmDialog
|
|
1077
|
+
* {...confirmableDelete.confirmConfig}
|
|
1078
|
+
* onConfirm={confirmableDelete.onConfirm}
|
|
1079
|
+
* onCancel={confirmableDelete.onCancel}
|
|
1080
|
+
* isLoading={confirmableDelete.isExecuting}
|
|
1081
|
+
* />
|
|
1082
|
+
* )}
|
|
1083
|
+
* </>
|
|
1084
|
+
* );
|
|
1085
|
+
* }
|
|
1086
|
+
* ```
|
|
1087
|
+
*
|
|
1088
|
+
* @example
|
|
1089
|
+
* Simple confirmation with default dialog
|
|
1090
|
+
* ```tsx
|
|
1091
|
+
* function LogoutButton() {
|
|
1092
|
+
* const confirmLogout = useConfirmableBlocker('logout', {
|
|
1093
|
+
* confirmMessage: 'Are you sure you want to log out?',
|
|
1094
|
+
* scope: 'global',
|
|
1095
|
+
* onConfirm: async () => {
|
|
1096
|
+
* await api.logout();
|
|
1097
|
+
* navigate('/login');
|
|
1098
|
+
* }
|
|
1099
|
+
* });
|
|
1100
|
+
*
|
|
1101
|
+
* return (
|
|
1102
|
+
* <>
|
|
1103
|
+
* <button onClick={confirmLogout.execute}>Logout</button>
|
|
1104
|
+
*
|
|
1105
|
+
* {confirmLogout.isDialogOpen && (
|
|
1106
|
+
* <Dialog
|
|
1107
|
+
* title={confirmLogout.confirmConfig.title}
|
|
1108
|
+
* message={confirmLogout.confirmConfig.message}
|
|
1109
|
+
* onConfirm={confirmLogout.onConfirm}
|
|
1110
|
+
* onCancel={confirmLogout.onCancel}
|
|
1111
|
+
* />
|
|
1112
|
+
* )}
|
|
1113
|
+
* </>
|
|
1114
|
+
* );
|
|
1115
|
+
* }
|
|
1116
|
+
* ```
|
|
1117
|
+
*
|
|
1118
|
+
* @example
|
|
1119
|
+
* With loading state during execution
|
|
1120
|
+
* ```tsx
|
|
1121
|
+
* function SaveButton() {
|
|
1122
|
+
* const confirmSave = useConfirmableBlocker('save-changes', {
|
|
1123
|
+
* confirmMessage: 'Save all changes?',
|
|
1124
|
+
* scope: 'form',
|
|
1125
|
+
* onConfirm: async () => {
|
|
1126
|
+
* await saveAllChanges();
|
|
1127
|
+
* }
|
|
1128
|
+
* });
|
|
1129
|
+
*
|
|
1130
|
+
* return (
|
|
1131
|
+
* <button
|
|
1132
|
+
* onClick={confirmSave.execute}
|
|
1133
|
+
* disabled={confirmSave.isExecuting}
|
|
1134
|
+
* >
|
|
1135
|
+
* {confirmSave.isExecuting ? 'Saving...' : 'Save All'}
|
|
1136
|
+
* </button>
|
|
1137
|
+
* );
|
|
1138
|
+
* }
|
|
1139
|
+
* ```
|
|
1140
|
+
*
|
|
1141
|
+
* @see {@link useBlocker} for simple blocking without confirmation
|
|
1142
|
+
* @see {@link ConfirmableBlockerConfig} for configuration options
|
|
1143
|
+
* @see {@link UseConfirmableBlockerReturn} for return value structure
|
|
1144
|
+
*
|
|
1145
|
+
* @public
|
|
1146
|
+
* @since 0.6.0
|
|
268
1147
|
*/
|
|
269
1148
|
declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBlockerConfig) => UseConfirmableBlockerReturn;
|
|
270
1149
|
|
|
271
1150
|
/**
|
|
272
|
-
*
|
|
1151
|
+
* Checks if one or more scopes are currently blocked.
|
|
1152
|
+
*
|
|
1153
|
+
* Returns a boolean indicating whether any of the specified scopes have active blockers.
|
|
1154
|
+
* The hook automatically re-renders the component when the blocking state changes,
|
|
1155
|
+
* ensuring your UI always reflects the current blocking state.
|
|
1156
|
+
*
|
|
1157
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1158
|
+
*
|
|
1159
|
+
* @param scope - Scope(s) to check for blocking. Can be:
|
|
1160
|
+
* - A single scope string (e.g., "form", "navigation", "global")
|
|
1161
|
+
* - An array of scopes (e.g., ["form", "navigation"])
|
|
1162
|
+
* - undefined to check the "global" scope
|
|
1163
|
+
* @returns true if ANY of the specified scopes are blocked, false otherwise
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* Basic single scope check
|
|
1167
|
+
* ```tsx
|
|
1168
|
+
* function SubmitButton() {
|
|
1169
|
+
* const isFormBlocked = useIsBlocked('form');
|
|
273
1170
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
1171
|
+
* return (
|
|
1172
|
+
* <button disabled={isFormBlocked}>
|
|
1173
|
+
* {isFormBlocked ? 'Processing...' : 'Submit'}
|
|
1174
|
+
* </button>
|
|
1175
|
+
* );
|
|
1176
|
+
* }
|
|
1177
|
+
* ```
|
|
276
1178
|
*
|
|
277
|
-
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* Multiple scopes check
|
|
1181
|
+
* ```tsx
|
|
1182
|
+
* function NavigationLink({ href, children }) {
|
|
1183
|
+
* const isBlocked = useIsBlocked(['navigation', 'global']);
|
|
1184
|
+
*
|
|
1185
|
+
* return (
|
|
1186
|
+
* <a
|
|
1187
|
+
* href={href}
|
|
1188
|
+
* onClick={(e) => {
|
|
1189
|
+
* if (isBlocked) {
|
|
1190
|
+
* e.preventDefault();
|
|
1191
|
+
* alert('Navigation is currently blocked');
|
|
1192
|
+
* }
|
|
1193
|
+
* }}
|
|
1194
|
+
* style={{ opacity: isBlocked ? 0.5 : 1 }}
|
|
1195
|
+
* >
|
|
1196
|
+
* {children}
|
|
1197
|
+
* </a>
|
|
1198
|
+
* );
|
|
1199
|
+
* }
|
|
1200
|
+
* ```
|
|
278
1201
|
*
|
|
279
|
-
* @
|
|
280
|
-
*
|
|
1202
|
+
* @example
|
|
1203
|
+
* Conditional rendering based on blocking state
|
|
1204
|
+
* ```tsx
|
|
1205
|
+
* function SaveIndicator() {
|
|
1206
|
+
* const isSaving = useIsBlocked('save');
|
|
1207
|
+
*
|
|
1208
|
+
* if (!isSaving) {
|
|
1209
|
+
* return null;
|
|
1210
|
+
* }
|
|
1211
|
+
*
|
|
1212
|
+
* return <div className="spinner">Saving...</div>;
|
|
1213
|
+
* }
|
|
1214
|
+
* ```
|
|
281
1215
|
*
|
|
1216
|
+
* @see {@link useBlocker} to create a blocker
|
|
1217
|
+
* @see {@link useBlockingInfo} to get detailed information about active blockers
|
|
1218
|
+
*
|
|
1219
|
+
* @public
|
|
1220
|
+
* @since 0.6.0
|
|
282
1221
|
*/
|
|
283
1222
|
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
284
1223
|
|
|
@@ -312,17 +1251,125 @@ interface ScheduledBlockerConfig extends BlockerConfig {
|
|
|
312
1251
|
}
|
|
313
1252
|
|
|
314
1253
|
/**
|
|
315
|
-
*
|
|
1254
|
+
* Schedules UI blocking for a specific time period (e.g., maintenance windows).
|
|
1255
|
+
*
|
|
1256
|
+
* Automatically blocks UI between specified start and end times. Useful for
|
|
1257
|
+
* scheduled maintenance, planned downtime, or time-based feature restrictions.
|
|
1258
|
+
* Supports callbacks when the blocking period starts and ends.
|
|
1259
|
+
*
|
|
1260
|
+
* Time specifications support:
|
|
1261
|
+
* - Date objects
|
|
1262
|
+
* - ISO 8601 strings (e.g., "2024-01-15T10:00:00Z")
|
|
1263
|
+
* - Unix timestamps (milliseconds since epoch)
|
|
1264
|
+
*
|
|
1265
|
+
* You can specify the end time either as an absolute time or as a duration
|
|
1266
|
+
* from the start time. If both are provided, duration takes precedence.
|
|
1267
|
+
*
|
|
1268
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1269
|
+
*
|
|
1270
|
+
* @param blockerId - Unique identifier for this scheduled blocker
|
|
1271
|
+
* @param config - Schedule configuration
|
|
1272
|
+
* @param config.schedule - Schedule timing configuration
|
|
1273
|
+
* @param config.schedule.start - When to start blocking (Date, ISO string, or timestamp)
|
|
1274
|
+
* @param config.schedule.end - Optional absolute end time (Date, ISO string, or timestamp)
|
|
1275
|
+
* @param config.schedule.duration - Optional duration in milliseconds from start time
|
|
1276
|
+
* @param config.scope - Scope(s) to block during the scheduled period
|
|
1277
|
+
* @param config.reason - Reason for blocking (displayed to users)
|
|
1278
|
+
* @param config.priority - Optional blocker priority (0-100)
|
|
1279
|
+
* @param config.onScheduleStart - Optional callback when blocking period starts
|
|
1280
|
+
* @param config.onScheduleEnd - Optional callback when blocking period ends
|
|
1281
|
+
*
|
|
1282
|
+
* @example
|
|
1283
|
+
* Maintenance window with absolute times
|
|
1284
|
+
* ```tsx
|
|
1285
|
+
* function MaintenanceWarning() {
|
|
1286
|
+
* useScheduledBlocker('maintenance', {
|
|
1287
|
+
* schedule: {
|
|
1288
|
+
* start: new Date('2024-01-15T02:00:00Z'),
|
|
1289
|
+
* end: new Date('2024-01-15T04:00:00Z'),
|
|
1290
|
+
* },
|
|
1291
|
+
* scope: 'global',
|
|
1292
|
+
* reason: 'System maintenance in progress',
|
|
1293
|
+
* priority: 95,
|
|
1294
|
+
* onScheduleStart: () => {
|
|
1295
|
+
* console.log('Maintenance started');
|
|
1296
|
+
* showNotification('System is now in maintenance mode');
|
|
1297
|
+
* },
|
|
1298
|
+
* onScheduleEnd: () => {
|
|
1299
|
+
* console.log('Maintenance completed');
|
|
1300
|
+
* showNotification('System is back online');
|
|
1301
|
+
* }
|
|
1302
|
+
* });
|
|
316
1303
|
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
1304
|
+
* return <div>...</div>;
|
|
1305
|
+
* }
|
|
1306
|
+
* ```
|
|
320
1307
|
*
|
|
321
|
-
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* Short maintenance with duration
|
|
1310
|
+
* ```tsx
|
|
1311
|
+
* useScheduledBlocker('quick-maintenance', {
|
|
1312
|
+
* schedule: {
|
|
1313
|
+
* start: new Date('2024-01-15T03:00:00'),
|
|
1314
|
+
* duration: 15 * 60 * 1000, // 15 minutes
|
|
1315
|
+
* },
|
|
1316
|
+
* scope: ['api', 'forms'],
|
|
1317
|
+
* reason: 'Brief system update',
|
|
1318
|
+
* priority: 90
|
|
1319
|
+
* });
|
|
1320
|
+
* ```
|
|
1321
|
+
*
|
|
1322
|
+
* @example
|
|
1323
|
+
* Business hours enforcement
|
|
1324
|
+
* ```tsx
|
|
1325
|
+
* function BusinessHoursBlocker() {
|
|
1326
|
+
* const now = new Date();
|
|
1327
|
+
* const tomorrow9AM = new Date(now);
|
|
1328
|
+
* tomorrow9AM.setDate(now.getDate() + 1);
|
|
1329
|
+
* tomorrow9AM.setHours(9, 0, 0, 0);
|
|
322
1330
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
1331
|
+
* useScheduledBlocker('after-hours', {
|
|
1332
|
+
* schedule: {
|
|
1333
|
+
* start: new Date(now.getTime() + 1000), // Start immediately
|
|
1334
|
+
* end: tomorrow9AM,
|
|
1335
|
+
* },
|
|
1336
|
+
* scope: 'trading',
|
|
1337
|
+
* reason: 'Trading is only available during business hours (9 AM - 5 PM)',
|
|
1338
|
+
* priority: 85
|
|
1339
|
+
* });
|
|
325
1340
|
*
|
|
1341
|
+
* return <TradingDashboard />;
|
|
1342
|
+
* }
|
|
1343
|
+
* ```
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* Using ISO strings for scheduling
|
|
1347
|
+
* ```tsx
|
|
1348
|
+
* useScheduledBlocker('deployment', {
|
|
1349
|
+
* schedule: {
|
|
1350
|
+
* start: '2024-01-20T00:00:00Z',
|
|
1351
|
+
* duration: 2 * 60 * 60 * 1000, // 2 hours
|
|
1352
|
+
* },
|
|
1353
|
+
* scope: 'global',
|
|
1354
|
+
* reason: 'Deploying new version',
|
|
1355
|
+
* priority: 100,
|
|
1356
|
+
* onScheduleStart: () => {
|
|
1357
|
+
* analytics.track('deployment_started');
|
|
1358
|
+
* },
|
|
1359
|
+
* onScheduleEnd: () => {
|
|
1360
|
+
* analytics.track('deployment_completed');
|
|
1361
|
+
* window.location.reload(); // Reload to get new version
|
|
1362
|
+
* }
|
|
1363
|
+
* });
|
|
1364
|
+
* ```
|
|
1365
|
+
*
|
|
1366
|
+
* @see {@link useBlocker} for immediate blocking without scheduling
|
|
1367
|
+
* @see {@link useConditionalBlocker} for condition-based blocking
|
|
1368
|
+
* @see {@link ScheduledBlockerConfig} for configuration options
|
|
1369
|
+
* @see {@link BlockingSchedule} for schedule specification details
|
|
1370
|
+
*
|
|
1371
|
+
* @public
|
|
1372
|
+
* @since 0.6.0
|
|
326
1373
|
*/
|
|
327
1374
|
declare const useScheduledBlocker: (blockerId: string, config: ScheduledBlockerConfig) => void;
|
|
328
1375
|
|
|
@@ -342,7 +1389,7 @@ interface BlockerConfigTyped<TScope extends string> {
|
|
|
342
1389
|
scope?: ScopeValue<TScope>;
|
|
343
1390
|
/** Human-readable reason for blocking */
|
|
344
1391
|
reason?: string;
|
|
345
|
-
/** Priority level (higher priority blockers take precedence) */
|
|
1392
|
+
/** Priority level (higher priority blockers take precedence, minimum value is 0) */
|
|
346
1393
|
priority?: number;
|
|
347
1394
|
/** Timestamp when the blocker was created */
|
|
348
1395
|
timestamp?: number;
|
|
@@ -395,7 +1442,14 @@ interface TypedHooks<TScope extends string> {
|
|
|
395
1442
|
declare function createTypedHooks<TScope extends string>(): TypedHooks<TScope>;
|
|
396
1443
|
|
|
397
1444
|
/**
|
|
398
|
-
* Props for UIBlockingProvider component
|
|
1445
|
+
* Props for the {@link UIBlockingProvider} component.
|
|
1446
|
+
*
|
|
1447
|
+
* Configures an isolated UI blocking store instance for a subtree of your app.
|
|
1448
|
+
* Useful for SSR, testing, micro-frontends, or any scenario where you need
|
|
1449
|
+
* independent blocking state.
|
|
1450
|
+
*
|
|
1451
|
+
* @public
|
|
1452
|
+
* @since 0.6.0
|
|
399
1453
|
*/
|
|
400
1454
|
interface UIBlockingProviderProps {
|
|
401
1455
|
children: ReactNode;
|
|
@@ -407,8 +1461,8 @@ interface UIBlockingProviderProps {
|
|
|
407
1461
|
middlewares?: ReadonlyArray<Middleware>;
|
|
408
1462
|
}
|
|
409
1463
|
/**
|
|
410
|
-
* Base provider created using toolkit's createStoreProvider
|
|
411
|
-
*
|
|
1464
|
+
* Base provider implementation created using toolkit's createStoreProvider.
|
|
1465
|
+
* @internal
|
|
412
1466
|
*/
|
|
413
1467
|
declare const useUIBlockingContext: () => StoreApi<UIBlockingStore>;
|
|
414
1468
|
declare const useUIBlockingStoreFromContext: {
|
|
@@ -417,10 +1471,100 @@ declare const useUIBlockingStoreFromContext: {
|
|
|
417
1471
|
};
|
|
418
1472
|
declare const useIsInsideUIBlockingProvider: () => boolean;
|
|
419
1473
|
/**
|
|
420
|
-
* Provider component for isolated UI blocking state
|
|
1474
|
+
* Provider component for isolated UI blocking state management.
|
|
1475
|
+
*
|
|
1476
|
+
* Creates an independent store instance for a subtree of your application.
|
|
1477
|
+
* Each provider has its own isolated blocking state, separate from the global
|
|
1478
|
+
* store and other provider instances.
|
|
1479
|
+
*
|
|
1480
|
+
* **Use cases:**
|
|
1481
|
+
* - Server-Side Rendering (SSR) - Avoid state sharing between requests
|
|
1482
|
+
* - Testing - Isolated state for each test
|
|
1483
|
+
* - Micro-frontends - Independent blocking state per micro-app
|
|
1484
|
+
* - Modals/Dialogs - Scoped blocking within a dialog
|
|
1485
|
+
*
|
|
1486
|
+
* Uses `createStoreProvider` from `@okyrychenko-dev/react-zustand-toolkit` with
|
|
1487
|
+
* built-in DevTools support and middleware registration.
|
|
1488
|
+
*
|
|
1489
|
+
* @param props - Provider configuration
|
|
1490
|
+
* @param props.children - React children to wrap with the provider
|
|
1491
|
+
* @param props.enableDevtools - Enable Redux DevTools integration. Defaults to true in development, false in production.
|
|
1492
|
+
* @param props.devtoolsName - Name displayed in Redux DevTools. Defaults to "UIBlocking".
|
|
1493
|
+
* @param props.middlewares - Array of middleware functions to register on store creation
|
|
1494
|
+
*
|
|
1495
|
+
* @example
|
|
1496
|
+
* Basic SSR setup
|
|
1497
|
+
* ```tsx
|
|
1498
|
+
* import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
|
|
1499
|
+
*
|
|
1500
|
+
* function App() {
|
|
1501
|
+
* return (
|
|
1502
|
+
* <UIBlockingProvider>
|
|
1503
|
+
* <MyApplication />
|
|
1504
|
+
* </UIBlockingProvider>
|
|
1505
|
+
* );
|
|
1506
|
+
* }
|
|
1507
|
+
* ```
|
|
1508
|
+
*
|
|
1509
|
+
* @example
|
|
1510
|
+
* With middleware and custom DevTools name
|
|
1511
|
+
* ```tsx
|
|
1512
|
+
* import { UIBlockingProvider, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
1513
|
+
*
|
|
1514
|
+
* function App() {
|
|
1515
|
+
* return (
|
|
1516
|
+
* <UIBlockingProvider
|
|
1517
|
+
* enableDevtools={true}
|
|
1518
|
+
* devtoolsName="MyApp-UIBlocking"
|
|
1519
|
+
* middlewares={[loggerMiddleware]}
|
|
1520
|
+
* >
|
|
1521
|
+
* <MyApplication />
|
|
1522
|
+
* </UIBlockingProvider>
|
|
1523
|
+
* );
|
|
1524
|
+
* }
|
|
1525
|
+
* ```
|
|
1526
|
+
*
|
|
1527
|
+
* @example
|
|
1528
|
+
* Multiple providers for micro-frontends
|
|
1529
|
+
* ```tsx
|
|
1530
|
+
* function MicroFrontendApp() {
|
|
1531
|
+
* return (
|
|
1532
|
+
* <div>
|
|
1533
|
+
* <UIBlockingProvider devtoolsName="MicroApp1">
|
|
1534
|
+
* <MicroApp1 />
|
|
1535
|
+
* </UIBlockingProvider>
|
|
1536
|
+
*
|
|
1537
|
+
* <UIBlockingProvider devtoolsName="MicroApp2">
|
|
1538
|
+
* <MicroApp2 />
|
|
1539
|
+
* </UIBlockingProvider>
|
|
1540
|
+
* </div>
|
|
1541
|
+
* );
|
|
1542
|
+
* }
|
|
1543
|
+
* ```
|
|
1544
|
+
*
|
|
1545
|
+
* @example
|
|
1546
|
+
* Testing with isolated state
|
|
1547
|
+
* ```tsx
|
|
1548
|
+
* // In your test file
|
|
1549
|
+
* import { render } from '@testing-library/react';
|
|
1550
|
+
* import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
|
|
1551
|
+
*
|
|
1552
|
+
* test('blocker behavior', () => {
|
|
1553
|
+
* render(
|
|
1554
|
+
* <UIBlockingProvider enableDevtools={false}>
|
|
1555
|
+
* <ComponentUnderTest />
|
|
1556
|
+
* </UIBlockingProvider>
|
|
1557
|
+
* );
|
|
1558
|
+
* // Each test gets isolated state
|
|
1559
|
+
* });
|
|
1560
|
+
* ```
|
|
1561
|
+
*
|
|
1562
|
+
* @see {@link useUIBlockingContext} to access the store from context
|
|
1563
|
+
* @see {@link useIsInsideUIBlockingProvider} to check if inside a provider
|
|
1564
|
+
* @see {@link UIBlockingProviderProps} for prop details
|
|
421
1565
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
1566
|
+
* @public
|
|
1567
|
+
* @since 0.6.0
|
|
424
1568
|
*/
|
|
425
1569
|
declare function UIBlockingProvider({ children, enableDevtools, devtoolsName, middlewares, }: UIBlockingProviderProps): ReactNode;
|
|
426
1570
|
|