@okyrychenko-dev/react-action-guard 1.0.5 → 2.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/CHANGELOG.md +451 -0
- package/README.md +171 -1036
- package/dist/index.cjs +760 -618
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -1205
- package/dist/index.d.ts +152 -1205
- package/dist/index.js +756 -621
- package/dist/index.js.map +1 -1
- package/package.json +47 -53
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import * as zustand from 'zustand';
|
|
3
3
|
import { StoreApi } from 'zustand';
|
|
4
|
+
import { Optional } from '@okyrychenko-dev/type-utils';
|
|
4
5
|
|
|
5
|
-
type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string
|
|
6
|
+
type AnalyticsEventData = Record<string, Optional<string | number | ReadonlyArray<string>>>;
|
|
6
7
|
type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
|
|
7
8
|
interface GoogleAnalyticsConfig {
|
|
8
9
|
provider: "ga";
|
|
@@ -18,39 +19,32 @@ interface CustomAnalyticsConfig {
|
|
|
18
19
|
}
|
|
19
20
|
type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
|
|
20
21
|
|
|
22
|
+
/** An immutable point-in-time blocker projection. */
|
|
23
|
+
type BlockingLifecycleSnapshot = ReadonlyArray<Readonly<BlockerInfo>>;
|
|
24
|
+
/** Read snapshots and observe lifecycle changes without transition authority. */
|
|
25
|
+
interface BlockingLifecycleObservation {
|
|
26
|
+
getSnapshot: () => BlockingLifecycleSnapshot;
|
|
27
|
+
subscribe: (listener: (snapshot: BlockingLifecycleSnapshot) => void) => VoidFunction;
|
|
28
|
+
observe: (observer: Middleware) => VoidFunction;
|
|
29
|
+
}
|
|
30
|
+
interface BlockingLifecycle extends BlockingLifecycleObservation {
|
|
31
|
+
add: (id: string, config?: BlockerConfig) => void;
|
|
32
|
+
update: (id: string, config?: Partial<BlockerConfig>) => void;
|
|
33
|
+
replace: (id: string, config: BlockerConfig) => void;
|
|
34
|
+
remove: (id: string) => void;
|
|
35
|
+
clear: () => void;
|
|
36
|
+
clearScope: (scope: string) => void;
|
|
37
|
+
isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
38
|
+
getBlockingInfo: (scope: string) => BlockingLifecycleSnapshot;
|
|
39
|
+
}
|
|
40
|
+
type BlockingEvent = MiddlewareContext;
|
|
41
|
+
|
|
42
|
+
/** Owns one independent store's blockers, timers, snapshots, and transition events. */
|
|
43
|
+
declare function createBlockingLifecycle(): BlockingLifecycle;
|
|
44
|
+
|
|
21
45
|
/**
|
|
22
46
|
* Configuration options for creating or updating a blocker.
|
|
23
47
|
*
|
|
24
|
-
* All fields are optional. Defaults will be applied for any missing values:
|
|
25
|
-
* - `scope`: "global"
|
|
26
|
-
* - `reason`: "Blocking UI"
|
|
27
|
-
* - `priority`: 50
|
|
28
|
-
* - `timestamp`: Current time
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* Basic configuration
|
|
32
|
-
* ```ts
|
|
33
|
-
* const config: BlockerConfig = {
|
|
34
|
-
* scope: 'form',
|
|
35
|
-
* reason: 'Submitting data...',
|
|
36
|
-
* priority: 70
|
|
37
|
-
* };
|
|
38
|
-
* ```
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* With timeout and callback
|
|
42
|
-
* ```ts
|
|
43
|
-
* const config: BlockerConfig = {
|
|
44
|
-
* scope: ['form', 'navigation'],
|
|
45
|
-
* reason: 'Critical operation in progress',
|
|
46
|
-
* priority: 90,
|
|
47
|
-
* timeout: 30000, // 30 seconds
|
|
48
|
-
* onTimeout: (id) => {
|
|
49
|
-
* console.warn(`Blocker ${id} timed out`);
|
|
50
|
-
* }
|
|
51
|
-
* };
|
|
52
|
-
* ```
|
|
53
|
-
*
|
|
54
48
|
* @public
|
|
55
49
|
* @since 0.6.0
|
|
56
50
|
*/
|
|
@@ -60,153 +54,65 @@ interface BlockerConfig {
|
|
|
60
54
|
/** Priority level (negative values are normalized to 0) */
|
|
61
55
|
priority?: number;
|
|
62
56
|
timestamp?: number;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Internal representation of a blocker with all fields populated.
|
|
70
|
-
*
|
|
71
|
-
* This is the stored format in the Map. All optional fields from {@link BlockerConfig}
|
|
72
|
-
* are converted to required fields with default values applied.
|
|
73
|
-
*
|
|
74
|
-
* @internal This type is primarily for internal use. Users should work with
|
|
75
|
-
* {@link BlockerConfig} for configuration and {@link BlockerInfo} for
|
|
76
|
-
* reading blocker data.
|
|
77
|
-
*
|
|
78
|
-
* @since 0.6.0
|
|
79
|
-
*/
|
|
80
|
-
interface StoredBlocker {
|
|
81
|
-
scope: string | ReadonlyArray<string>;
|
|
82
|
-
reason: string;
|
|
83
|
-
priority: number;
|
|
84
|
-
timestamp: number;
|
|
85
|
-
/** Timeout duration in milliseconds (if set) */
|
|
57
|
+
/**
|
|
58
|
+
* Remove after a positive finite delay in milliseconds, using bounded timer chunks.
|
|
59
|
+
* Omitted, zero and finite negative values disable expiration. Invalid/nonfinite values
|
|
60
|
+
* or a deadline beyond Number.MAX_SAFE_INTEGER throw RangeError before mutation.
|
|
61
|
+
*/
|
|
86
62
|
timeout?: number;
|
|
87
|
-
/** Internal timeout ID for cleanup */
|
|
88
|
-
timeoutId?: ReturnType<typeof setTimeout>;
|
|
89
63
|
/** Callback invoked when the blocker is automatically removed due to timeout */
|
|
90
64
|
onTimeout?: (blockerId: string) => void;
|
|
91
65
|
}
|
|
92
66
|
/**
|
|
93
67
|
* Complete blocker information including its unique identifier.
|
|
94
68
|
*
|
|
95
|
-
* This type extends {@link StoredBlocker} with the blocker's ID, making it
|
|
96
|
-
* suitable for reading and displaying blocker information. Returned by
|
|
97
|
-
* {@link UIBlockingStoreActions.getBlockingInfo} and used by {@link useBlockingInfo}.
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* Accessing blocker info
|
|
101
|
-
* ```ts
|
|
102
|
-
* const blockers: BlockerInfo[] = store.getState().getBlockingInfo('form');
|
|
103
|
-
* blockers.forEach(blocker => {
|
|
104
|
-
* console.log(`${blocker.id}: ${blocker.reason} (priority: ${blocker.priority})`);
|
|
105
|
-
* });
|
|
106
|
-
* ```
|
|
107
|
-
*
|
|
108
69
|
* @public
|
|
109
70
|
* @since 0.6.0
|
|
110
71
|
*/
|
|
111
|
-
interface BlockerInfo
|
|
112
|
-
id: string;
|
|
72
|
+
interface BlockerInfo {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly scope: string | ReadonlyArray<string>;
|
|
75
|
+
readonly reason: string;
|
|
76
|
+
readonly priority: number;
|
|
77
|
+
readonly timestamp: number;
|
|
78
|
+
readonly timeout?: number;
|
|
79
|
+
readonly onTimeout?: (blockerId: string) => void;
|
|
113
80
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Internal state structure of the UI blocking store.
|
|
116
|
-
*
|
|
117
|
-
* Contains the core data structures for managing blockers and middleware.
|
|
118
|
-
* This state is combined with {@link UIBlockingStoreActions} to create the
|
|
119
|
-
* complete {@link UIBlockingStore} type.
|
|
120
|
-
*
|
|
121
|
-
* @internal
|
|
122
|
-
* @since 0.6.0
|
|
123
|
-
*/
|
|
81
|
+
/** Immutable projection published by the Zustand adapter. */
|
|
124
82
|
interface UIBlockingStoreState {
|
|
125
|
-
|
|
126
|
-
middlewares: Map<string, Middleware>;
|
|
83
|
+
readonly blockingSnapshot: BlockingLifecycleSnapshot;
|
|
127
84
|
}
|
|
128
85
|
/**
|
|
129
86
|
* All available actions for managing UI blocking state.
|
|
130
87
|
*
|
|
131
|
-
* These methods allow adding, removing, updating blockers, checking blocking status,
|
|
132
|
-
* and managing middleware. Combined with {@link UIBlockingStoreState} to form
|
|
133
|
-
* the complete {@link UIBlockingStore} type.
|
|
134
|
-
*
|
|
135
|
-
* All actions are safe to call multiple times and handle edge cases gracefully.
|
|
136
|
-
*
|
|
137
|
-
* @example
|
|
138
|
-
* Using store actions directly
|
|
139
|
-
* ```ts
|
|
140
|
-
* import { uiBlockingStoreApi } from '@okyrychenko-dev/react-action-guard';
|
|
141
|
-
*
|
|
142
|
-
* // Add a blocker
|
|
143
|
-
* uiBlockingStoreApi.getState().addBlocker('my-blocker', {
|
|
144
|
-
* scope: 'form',
|
|
145
|
-
* reason: 'Processing...'
|
|
146
|
-
* });
|
|
147
|
-
*
|
|
148
|
-
* // Check if blocked
|
|
149
|
-
* const isBlocked = uiBlockingStoreApi.getState().isBlocked('form');
|
|
150
|
-
*
|
|
151
|
-
* // Remove blocker
|
|
152
|
-
* uiBlockingStoreApi.getState().removeBlocker('my-blocker');
|
|
153
|
-
* ```
|
|
154
|
-
*
|
|
155
88
|
* @public
|
|
156
89
|
* @since 0.6.0
|
|
157
90
|
*/
|
|
158
91
|
interface UIBlockingStoreActions {
|
|
159
92
|
addBlocker: (id: string, config?: BlockerConfig) => void;
|
|
160
93
|
updateBlocker: (id: string, config?: Partial<BlockerConfig>) => void;
|
|
94
|
+
/** @internal Apply current reactive configuration, clearing omitted optional fields. */
|
|
95
|
+
replaceBlocker: (id: string, config: BlockerConfig) => void;
|
|
161
96
|
removeBlocker: (id: string) => void;
|
|
162
97
|
isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
163
|
-
getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo
|
|
98
|
+
getBlockingInfo: (scope: string) => ReadonlyArray<Readonly<BlockerInfo>>;
|
|
164
99
|
clearAllBlockers: VoidFunction;
|
|
165
100
|
clearBlockersForScope: (scope: string) => void;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
runMiddlewares: (context: MiddlewareContext) => Promise<void>;
|
|
101
|
+
/** Observe lifecycle transitions through an anonymous, ownership-safe lease. */
|
|
102
|
+
observeBlockingEvents: (observer: Middleware) => VoidFunction;
|
|
169
103
|
}
|
|
170
104
|
/**
|
|
171
105
|
* Complete UI blocking store type combining state and actions.
|
|
172
106
|
*
|
|
173
|
-
* This is the full store interface used by hooks like {@link useUIBlockingStore}
|
|
174
|
-
* and accessible via {@link uiBlockingStoreApi}. It combines all state properties
|
|
175
|
-
* from {@link UIBlockingStoreState} with all action methods from {@link UIBlockingStoreActions}.
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* Using the complete store
|
|
179
|
-
* ```ts
|
|
180
|
-
* import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
|
|
181
|
-
*
|
|
182
|
-
* function MyComponent() {
|
|
183
|
-
* const store = useUIBlockingStore();
|
|
184
|
-
*
|
|
185
|
-
* // Access state
|
|
186
|
-
* console.log(store.activeBlockers.size);
|
|
187
|
-
*
|
|
188
|
-
* // Call actions
|
|
189
|
-
* store.addBlocker('my-blocker', { scope: 'form' });
|
|
190
|
-
*
|
|
191
|
-
* return <div>...</div>;
|
|
192
|
-
* }
|
|
193
|
-
* ```
|
|
194
|
-
*
|
|
195
107
|
* @public
|
|
196
108
|
* @since 0.6.0
|
|
197
109
|
*/
|
|
198
110
|
type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
|
|
199
|
-
interface ShallowStoreBindings<StoreState> {
|
|
200
|
-
useStore: {
|
|
201
|
-
(): StoreState;
|
|
202
|
-
<T>(selector: (state: StoreState) => T): T;
|
|
203
|
-
};
|
|
204
|
-
useStoreApi: StoreApi<StoreState>;
|
|
205
|
-
}
|
|
206
111
|
|
|
207
112
|
type BlockingAction = "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
|
|
208
113
|
type MiddlewareBlockerConfig = BlockerConfig;
|
|
209
114
|
interface MiddlewareContext {
|
|
115
|
+
registration?: object;
|
|
210
116
|
action: BlockingAction;
|
|
211
117
|
blockerId: string;
|
|
212
118
|
config?: MiddlewareBlockerConfig;
|
|
@@ -220,281 +126,68 @@ type Middleware = (context: MiddlewareContext) => void | Promise<void>;
|
|
|
220
126
|
/**
|
|
221
127
|
* Creates middleware for tracking blocker events to analytics platforms.
|
|
222
128
|
*
|
|
223
|
-
* Automatically tracks all blocker actions (add, remove, update, timeout, clear)
|
|
224
|
-
* to your analytics platform. Supports Google Analytics, Mixpanel, Amplitude,
|
|
225
|
-
* and custom analytics providers.
|
|
226
|
-
*
|
|
227
|
-
* Event data includes: blockerId, action, scope, reason, priority, timestamp,
|
|
228
|
-
* and duration (for remove events).
|
|
229
|
-
*
|
|
230
|
-
* @param config - Analytics configuration specifying provider and credentials
|
|
231
|
-
*
|
|
232
|
-
* @example
|
|
233
|
-
* Google Analytics integration
|
|
234
|
-
* ```ts
|
|
235
|
-
* import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
236
|
-
*
|
|
237
|
-
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
238
|
-
* provider: 'ga',
|
|
239
|
-
* trackingId: 'GA-XXXXX-Y'
|
|
240
|
-
* });
|
|
241
|
-
*
|
|
242
|
-
* configureMiddleware([analyticsMiddleware]);
|
|
243
|
-
* ```
|
|
244
|
-
*
|
|
245
|
-
* @example
|
|
246
|
-
* Mixpanel integration
|
|
247
|
-
* ```ts
|
|
248
|
-
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
249
|
-
* provider: 'mixpanel',
|
|
250
|
-
* token: 'your_mixpanel_token',
|
|
251
|
-
* options: {
|
|
252
|
-
* debug: true
|
|
253
|
-
* }
|
|
254
|
-
* });
|
|
255
|
-
*
|
|
256
|
-
* configureMiddleware([analyticsMiddleware]);
|
|
257
|
-
* ```
|
|
258
|
-
*
|
|
259
|
-
* @example
|
|
260
|
-
* Custom analytics provider
|
|
261
|
-
* ```ts
|
|
262
|
-
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
263
|
-
* track: (eventName, eventData) => {
|
|
264
|
-
* // Send to your custom analytics
|
|
265
|
-
* myAnalytics.track(eventName, {
|
|
266
|
-
* ...eventData,
|
|
267
|
-
* userId: getCurrentUserId()
|
|
268
|
-
* });
|
|
269
|
-
* }
|
|
270
|
-
* });
|
|
271
|
-
* ```
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* Amplitude with user properties
|
|
275
|
-
* ```ts
|
|
276
|
-
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
277
|
-
* provider: 'amplitude',
|
|
278
|
-
* apiKey: 'your_amplitude_key',
|
|
279
|
-
* userId: 'user-123',
|
|
280
|
-
* userProperties: {
|
|
281
|
-
* plan: 'premium',
|
|
282
|
-
* role: 'admin'
|
|
283
|
-
* }
|
|
284
|
-
* });
|
|
285
|
-
* ```
|
|
286
|
-
*
|
|
287
|
-
* @see {@link AnalyticsConfig} for configuration options
|
|
288
|
-
* @see {@link configureMiddleware} for registering middleware
|
|
289
|
-
*
|
|
290
129
|
* @public
|
|
291
130
|
* @since 0.6.0
|
|
131
|
+
* @see {@link AnalyticsConfig} for configuration options
|
|
132
|
+
* @see {@link configureMiddleware} for registering middleware
|
|
292
133
|
*/
|
|
293
134
|
declare function createAnalyticsMiddleware(config: AnalyticsConfig): Middleware;
|
|
294
135
|
|
|
295
|
-
/**
|
|
296
|
-
* Configures and registers multiple middleware functions for the UI blocking store.
|
|
297
|
-
*
|
|
298
|
-
* Convenience function that registers an array of middleware at once. Each middleware
|
|
299
|
-
* is automatically assigned a unique name based on its array index ("middleware-0",
|
|
300
|
-
* "middleware-1", etc.). Middleware are executed in the order they are registered.
|
|
301
|
-
*
|
|
302
|
-
* All middleware receive {@link MiddlewareContext} containing action type, blocker ID,
|
|
303
|
-
* configuration, and timestamp for each blocking event.
|
|
304
|
-
*
|
|
305
|
-
* @param middlewares - Array of middleware functions to register. Functions are called
|
|
306
|
-
* in order for each blocking action (add, remove, update, etc.)
|
|
307
|
-
*
|
|
308
|
-
* @example
|
|
309
|
-
* Basic usage with built-in middleware
|
|
310
|
-
* ```ts
|
|
311
|
-
* import { configureMiddleware, loggerMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
312
|
-
*
|
|
313
|
-
* configureMiddleware([
|
|
314
|
-
* loggerMiddleware,
|
|
315
|
-
* createPerformanceMiddleware({ threshold: 1000 })
|
|
316
|
-
* ]);
|
|
317
|
-
* ```
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* With custom middleware
|
|
321
|
-
* ```ts
|
|
322
|
-
* const customMiddleware: Middleware = (context) => {
|
|
323
|
-
* if (context.action === 'add') {
|
|
324
|
-
* console.log(`Blocker added: ${context.blockerId}`);
|
|
325
|
-
* }
|
|
326
|
-
* };
|
|
327
|
-
*
|
|
328
|
-
* configureMiddleware([
|
|
329
|
-
* loggerMiddleware,
|
|
330
|
-
* customMiddleware
|
|
331
|
-
* ]);
|
|
332
|
-
* ```
|
|
333
|
-
*
|
|
334
|
-
* @example
|
|
335
|
-
* Analytics integration
|
|
336
|
-
* ```ts
|
|
337
|
-
* import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
338
|
-
*
|
|
339
|
-
* configureMiddleware([
|
|
340
|
-
* createAnalyticsMiddleware({
|
|
341
|
-
* provider: 'google-analytics',
|
|
342
|
-
* trackingId: 'GA-XXXXX'
|
|
343
|
-
* })
|
|
344
|
-
* ]);
|
|
345
|
-
* ```
|
|
346
|
-
*
|
|
347
|
-
* @see {@link Middleware} for middleware function signature
|
|
348
|
-
* @see {@link loggerMiddleware} for built-in logging middleware
|
|
349
|
-
* @see {@link createAnalyticsMiddleware} for analytics tracking
|
|
350
|
-
* @see {@link createPerformanceMiddleware} for performance monitoring
|
|
351
|
-
*
|
|
352
|
-
* @public
|
|
353
|
-
* @since 0.6.0
|
|
354
|
-
*/
|
|
136
|
+
/** Replace configured global observations without releasing independently owned observations. */
|
|
355
137
|
declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): void;
|
|
356
138
|
|
|
357
139
|
/**
|
|
358
140
|
* Built-in middleware for logging all blocking actions to the console.
|
|
359
141
|
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* development. Register it conditionally when production logging is not desired.
|
|
363
|
-
*
|
|
364
|
-
* Output format: `[UIBlocking] 🔒 blocker-id {scope: "form", reason: "..."}`
|
|
365
|
-
*
|
|
366
|
-
* Emoji indicators:
|
|
367
|
-
* - 🔒 Add
|
|
368
|
-
* - 🔓 Remove
|
|
369
|
-
* - 🔄 Update
|
|
370
|
-
* - ⏰ Timeout
|
|
371
|
-
* - 🧹 Clear/ClearScope
|
|
372
|
-
*
|
|
373
|
-
* @example
|
|
374
|
-
* Register logger middleware
|
|
375
|
-
* ```ts
|
|
376
|
-
* import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
377
|
-
*
|
|
378
|
-
* // Enable logging for all blocking actions
|
|
379
|
-
* configureMiddleware([loggerMiddleware]);
|
|
380
|
-
* ```
|
|
381
|
-
*
|
|
382
|
-
* @example
|
|
383
|
-
* Console output examples
|
|
384
|
-
* ```
|
|
385
|
-
* [UIBlocking] 🔒 save-form {scope: "form", reason: "Saving...", priority: 50}
|
|
386
|
-
* [UIBlocking] 🔄 save-form {scope: "form", reason: "Almost done..."}
|
|
387
|
-
* [UIBlocking] 🔓 save-form {reason: "Saving..."}
|
|
388
|
-
* [UIBlocking] ⏰ api-timeout {timeout: 30000}
|
|
389
|
-
* ```
|
|
390
|
-
*
|
|
391
|
-
* @example
|
|
392
|
-
* Production check pattern
|
|
393
|
-
* ```ts
|
|
394
|
-
* import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
395
|
-
* const middlewares = [];
|
|
396
|
-
* if (process.env.NODE_ENV !== 'production') {
|
|
397
|
-
* middlewares.push(loggerMiddleware);
|
|
398
|
-
* }
|
|
399
|
-
* configureMiddleware(middlewares);
|
|
400
|
-
* ```
|
|
401
|
-
*
|
|
142
|
+
* @public
|
|
143
|
+
* @since 0.6.0
|
|
402
144
|
* @see {@link configureMiddleware} for registering middleware
|
|
403
145
|
* @see {@link Middleware} for middleware function signature
|
|
404
146
|
* @see {@link MiddlewareContext} for available context data
|
|
405
|
-
*
|
|
406
|
-
* @public
|
|
407
|
-
* @since 0.6.0
|
|
408
147
|
*/
|
|
409
148
|
declare function loggerMiddleware(context: MiddlewareContext): void;
|
|
410
149
|
|
|
150
|
+
declare const DEFAULT_SCOPE: "global";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A blocking scope is either one name or a list of names.
|
|
154
|
+
*
|
|
155
|
+
* Lists are interpreted as a set: order and duplicate entries have no semantic meaning.
|
|
156
|
+
* An empty list observes no scope. Guarded controls additionally treat an empty explicit list as
|
|
157
|
+
* an instruction to inherit their provider scope through {@link resolveScope}.
|
|
158
|
+
*/
|
|
159
|
+
type Scope = string | ReadonlyArray<string>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns a frozen, stable, deduplicated, sorted scope list.
|
|
163
|
+
* An omitted scope means global; an empty list remains empty.
|
|
164
|
+
*/
|
|
165
|
+
declare function normalizeScope(scope?: Scope): ReadonlyArray<string>;
|
|
166
|
+
/**
|
|
167
|
+
* Resolves guarded-control inheritance.
|
|
168
|
+
* An omitted or empty explicit scope inherits; any non-empty explicit scope wins.
|
|
169
|
+
*/
|
|
170
|
+
declare function resolveScope(explicitScope?: Scope, inheritedScope?: Scope): Optional<Scope>;
|
|
171
|
+
/**
|
|
172
|
+
* Checks ordinary observation semantics.
|
|
173
|
+
* A global blocker affects every observed scope and otherwise any shared scope is a match.
|
|
174
|
+
*/
|
|
175
|
+
declare function scopeAffectsObservation(blockerScope: Scope, observedScope: Scope): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Checks targeted-clear semantics.
|
|
178
|
+
* Global blockers never match a target, including the global target; otherwise any shared scope
|
|
179
|
+
* is a match.
|
|
180
|
+
*/
|
|
181
|
+
declare function scopeMatchesTarget(blockerScope: Scope, targetScope: Scope): boolean;
|
|
182
|
+
|
|
411
183
|
interface PerformanceConfig {
|
|
412
184
|
slowBlockThreshold?: number;
|
|
413
185
|
onSlowBlock?: (blockerId: string, duration: number) => void;
|
|
414
186
|
}
|
|
415
187
|
|
|
416
188
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* Tracks the duration of each blocker (from add to remove) and invokes an optional
|
|
420
|
-
* callback when a blocker exceeds the configured threshold. Useful for identifying
|
|
421
|
-
* performance issues, stuck blockers, or operations that take too long.
|
|
422
|
-
*
|
|
423
|
-
* The middleware is side-effect free by default; use `onSlowBlock` for custom
|
|
424
|
-
* reporting, logging, or analytics integration.
|
|
425
|
-
*
|
|
426
|
-
* @param config - Performance monitoring configuration
|
|
427
|
-
* @param config.slowBlockThreshold - Duration in ms before considering a block "slow".
|
|
428
|
-
* Defaults to 3000ms (3 seconds).
|
|
429
|
-
* @param config.onSlowBlock - Optional callback invoked when a slow block is detected.
|
|
430
|
-
* Receives blockerId and duration in milliseconds.
|
|
431
|
-
*
|
|
432
|
-
* @returns Middleware function for performance monitoring
|
|
433
|
-
*
|
|
434
|
-
* @example
|
|
435
|
-
* Basic usage with default threshold
|
|
436
|
-
* ```ts
|
|
437
|
-
* import { configureMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
438
|
-
*
|
|
439
|
-
* const performanceMiddleware = createPerformanceMiddleware();
|
|
440
|
-
*
|
|
441
|
-
* configureMiddleware([performanceMiddleware]);
|
|
442
|
-
* // Track slow blocks via onSlowBlock if any blocker lasts > 3 seconds
|
|
443
|
-
* ```
|
|
444
|
-
*
|
|
445
|
-
* @example
|
|
446
|
-
* Custom threshold and callback
|
|
447
|
-
* ```ts
|
|
448
|
-
* const performanceMiddleware = createPerformanceMiddleware({
|
|
449
|
-
* slowBlockThreshold: 5000, // 5 seconds
|
|
450
|
-
* onSlowBlock: (blockerId, duration) => {
|
|
451
|
-
* console.error(`Blocker ${blockerId} took ${duration}ms`);
|
|
452
|
-
*
|
|
453
|
-
* // Report to error tracking
|
|
454
|
-
* Sentry.captureMessage('Slow UI Blocker', {
|
|
455
|
-
* extra: { blockerId, duration }
|
|
456
|
-
* });
|
|
457
|
-
* }
|
|
458
|
-
* });
|
|
459
|
-
* ```
|
|
460
|
-
*
|
|
461
|
-
* @example
|
|
462
|
-
* With analytics integration
|
|
463
|
-
* ```ts
|
|
464
|
-
* const performanceMiddleware = createPerformanceMiddleware({
|
|
465
|
-
* slowBlockThreshold: 2000,
|
|
466
|
-
* onSlowBlock: (blockerId, duration) => {
|
|
467
|
-
* // Track slow blocks in analytics
|
|
468
|
-
* analytics.track('slow_blocker_detected', {
|
|
469
|
-
* blocker_id: blockerId,
|
|
470
|
-
* duration_ms: duration,
|
|
471
|
-
* threshold_ms: 2000
|
|
472
|
-
* });
|
|
473
|
-
* }
|
|
474
|
-
* });
|
|
475
|
-
* ```
|
|
476
|
-
*
|
|
477
|
-
* @example
|
|
478
|
-
* Development vs Production
|
|
479
|
-
* ```ts
|
|
480
|
-
* const performanceMiddleware = createPerformanceMiddleware({
|
|
481
|
-
* // Strict threshold in development
|
|
482
|
-
* slowBlockThreshold: process.env.NODE_ENV === 'production' ? 5000 : 1000,
|
|
483
|
-
* onSlowBlock: (blockerId, duration) => {
|
|
484
|
-
* if (process.env.NODE_ENV === 'production') {
|
|
485
|
-
* // Report to error service in production
|
|
486
|
-
* errorService.log({ blockerId, duration });
|
|
487
|
-
* } else {
|
|
488
|
-
* // Detailed logging in development
|
|
489
|
-
* console.table({ blockerId, duration });
|
|
490
|
-
* }
|
|
491
|
-
* }
|
|
492
|
-
* });
|
|
493
|
-
* ```
|
|
494
|
-
*
|
|
495
|
-
* @see {@link PerformanceConfig} for configuration options
|
|
496
|
-
* @see {@link configureMiddleware} for registering middleware
|
|
497
|
-
*
|
|
189
|
+
* Measures registration lifetimes from add until removal, timeout or bulk clear.
|
|
190
|
+
* Updates preserve the start; replacement adds restart it. Callback failures are isolated.
|
|
498
191
|
* @public
|
|
499
192
|
* @since 0.6.0
|
|
500
193
|
*/
|
|
@@ -527,94 +220,11 @@ declare function useUIBlockingStoreFromContext<T>(selector: (state: UIBlockingSt
|
|
|
527
220
|
/**
|
|
528
221
|
* Provider component for isolated UI blocking state management.
|
|
529
222
|
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* store and other provider instances.
|
|
533
|
-
*
|
|
534
|
-
* **Use cases:**
|
|
535
|
-
* - Server-Side Rendering (SSR) - Avoid state sharing between requests
|
|
536
|
-
* - Testing - Isolated state for each test
|
|
537
|
-
* - Micro-frontends - Independent blocking state per micro-app
|
|
538
|
-
* - Modals/Dialogs - Scoped blocking within a dialog
|
|
539
|
-
*
|
|
540
|
-
* Uses `createStoreProvider` from `@okyrychenko-dev/react-zustand-toolkit` with
|
|
541
|
-
* provider-scoped store creation and middleware registration.
|
|
542
|
-
*
|
|
543
|
-
* @param props - Provider configuration
|
|
544
|
-
* @param props.children - React children to wrap with the provider
|
|
545
|
-
* @param props.middlewares - Array of middleware functions to register on store creation
|
|
546
|
-
*
|
|
547
|
-
* @example
|
|
548
|
-
* Basic SSR setup
|
|
549
|
-
* ```tsx
|
|
550
|
-
* import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
|
|
551
|
-
*
|
|
552
|
-
* function App() {
|
|
553
|
-
* return (
|
|
554
|
-
* <UIBlockingProvider>
|
|
555
|
-
* <MyApplication />
|
|
556
|
-
* </UIBlockingProvider>
|
|
557
|
-
* );
|
|
558
|
-
* }
|
|
559
|
-
* ```
|
|
560
|
-
*
|
|
561
|
-
* @example
|
|
562
|
-
* With middleware
|
|
563
|
-
* ```tsx
|
|
564
|
-
* import { UIBlockingProvider, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
565
|
-
*
|
|
566
|
-
* function App() {
|
|
567
|
-
* return (
|
|
568
|
-
* <UIBlockingProvider
|
|
569
|
-
* middlewares={[loggerMiddleware]}
|
|
570
|
-
* >
|
|
571
|
-
* <MyApplication />
|
|
572
|
-
* </UIBlockingProvider>
|
|
573
|
-
* );
|
|
574
|
-
* }
|
|
575
|
-
* ```
|
|
576
|
-
*
|
|
577
|
-
* @example
|
|
578
|
-
* Multiple providers for micro-frontends
|
|
579
|
-
* ```tsx
|
|
580
|
-
* function MicroFrontendApp() {
|
|
581
|
-
* return (
|
|
582
|
-
* <div>
|
|
583
|
-
* <UIBlockingProvider>
|
|
584
|
-
* <MicroApp1 />
|
|
585
|
-
* </UIBlockingProvider>
|
|
586
|
-
*
|
|
587
|
-
* <UIBlockingProvider>
|
|
588
|
-
* <MicroApp2 />
|
|
589
|
-
* </UIBlockingProvider>
|
|
590
|
-
* </div>
|
|
591
|
-
* );
|
|
592
|
-
* }
|
|
593
|
-
* ```
|
|
594
|
-
*
|
|
595
|
-
* @example
|
|
596
|
-
* Testing with isolated state
|
|
597
|
-
* ```tsx
|
|
598
|
-
* // In your test file
|
|
599
|
-
* import { render } from '@testing-library/react';
|
|
600
|
-
* import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
|
|
601
|
-
*
|
|
602
|
-
* test('blocker behavior', () => {
|
|
603
|
-
* render(
|
|
604
|
-
* <UIBlockingProvider>
|
|
605
|
-
* <ComponentUnderTest />
|
|
606
|
-
* </UIBlockingProvider>
|
|
607
|
-
* );
|
|
608
|
-
* // Each test gets isolated state
|
|
609
|
-
* });
|
|
610
|
-
* ```
|
|
611
|
-
*
|
|
223
|
+
* @public
|
|
224
|
+
* @since 0.6.0
|
|
612
225
|
* @see {@link useUIBlockingContext} to access the store from context
|
|
613
226
|
* @see {@link useIsInsideUIBlockingProvider} to check if inside a provider
|
|
614
227
|
* @see {@link UIBlockingProviderProps} for prop details
|
|
615
|
-
*
|
|
616
|
-
* @public
|
|
617
|
-
* @since 0.6.0
|
|
618
228
|
*/
|
|
619
229
|
declare function UIBlockingProvider({ children, middlewares, }: UIBlockingProviderProps): ReactNode;
|
|
620
230
|
|
|
@@ -633,148 +243,46 @@ declare function useResolvedStoreApi(): StoreApi<UIBlockingStore>;
|
|
|
633
243
|
/**
|
|
634
244
|
* Hook to use the resolved store with a selector
|
|
635
245
|
*
|
|
636
|
-
*
|
|
246
|
+
* Uses toolkit shallow comparison by default, or the supplied domain equality.
|
|
637
247
|
*
|
|
638
248
|
* @param selector - Selector function to pick state from the store
|
|
249
|
+
* @param equalityFn - Optional comparison retaining equivalent selected values
|
|
639
250
|
* @returns Selected state value
|
|
640
251
|
*/
|
|
641
|
-
declare function useResolvedValue<T>(selector: (state: UIBlockingStore) => T): T;
|
|
252
|
+
declare function useResolvedValue<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (left: T, right: T) => boolean): T;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @deprecated Use `useOptionalUIBlockingContext`.
|
|
256
|
+
*/
|
|
257
|
+
declare function useOptionalContext(): ReturnType<typeof useOptionalUIBlockingContext>;
|
|
258
|
+
/**
|
|
259
|
+
* @deprecated Use `useResolvedStoreApi`.
|
|
260
|
+
*/
|
|
261
|
+
declare function useResolvedStore(): StoreApi<UIBlockingStore>;
|
|
262
|
+
/**
|
|
263
|
+
* @deprecated Use `useResolvedValue`.
|
|
264
|
+
*/
|
|
265
|
+
declare function useResolvedStoreWithSelector<T>(selector: (state: UIBlockingStore) => T): T;
|
|
642
266
|
|
|
643
267
|
/**
|
|
644
268
|
* Options for useAsyncAction hook
|
|
645
269
|
*/
|
|
646
270
|
interface UseAsyncActionOptions {
|
|
647
|
-
/**
|
|
271
|
+
/** Blocker lifetime in milliseconds; does not cancel or settle the operation */
|
|
648
272
|
timeout?: number;
|
|
649
273
|
/** Callback invoked when the blocker is automatically removed due to timeout */
|
|
650
274
|
onTimeout?: (blockerId: string) => void;
|
|
651
275
|
}
|
|
652
276
|
/**
|
|
653
|
-
* Wraps async functions with
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
* 1. Adds a blocker before executing the async function
|
|
657
|
-
* 2. Executes the async function
|
|
658
|
-
* 3. Removes the blocker after completion (success or failure)
|
|
659
|
-
*
|
|
660
|
-
* Each invocation generates a unique blocker ID (actionId-counter), allowing
|
|
661
|
-
* multiple concurrent executions of the same action without ID conflicts.
|
|
662
|
-
* The blocker is removed even if the async function throws an error.
|
|
663
|
-
*
|
|
664
|
-
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
665
|
-
*
|
|
666
|
-
* @template T - The return type of the async function being wrapped
|
|
667
|
-
* @param actionId - Base identifier for the action. Used in blocker ID and default reason.
|
|
668
|
-
* Example: "saveForm", "fetchData", "submitOrder"
|
|
669
|
-
* @param scope - Scope(s) to block during execution. Can be a single scope or array.
|
|
670
|
-
* Defaults to "global" if not specified.
|
|
671
|
-
* @param options - Optional configuration
|
|
672
|
-
* @param options.timeout - Timeout in milliseconds. Blocker auto-removes after this duration.
|
|
673
|
-
* @param options.onTimeout - Callback invoked when blocker times out before async completes
|
|
674
|
-
*
|
|
675
|
-
* @returns Wrapper function that accepts an async function and returns a Promise with the same type
|
|
676
|
-
*
|
|
677
|
-
* @example
|
|
678
|
-
* Basic form submission
|
|
679
|
-
* ```tsx
|
|
680
|
-
* function MyForm() {
|
|
681
|
-
* const executeWithBlocking = useAsyncAction('saveForm', 'form');
|
|
682
|
-
*
|
|
683
|
-
* const handleSubmit = async (e) => {
|
|
684
|
-
* e.preventDefault();
|
|
685
|
-
*
|
|
686
|
-
* await executeWithBlocking(async () => {
|
|
687
|
-
* const response = await fetch('/api/save', {
|
|
688
|
-
* method: 'POST',
|
|
689
|
-
* body: JSON.stringify(formData)
|
|
690
|
-
* });
|
|
691
|
-
* return response.json();
|
|
692
|
-
* });
|
|
693
|
-
*
|
|
694
|
-
* alert('Form saved!');
|
|
695
|
-
* };
|
|
696
|
-
*
|
|
697
|
-
* return <form onSubmit={handleSubmit}>...</form>;
|
|
698
|
-
* }
|
|
699
|
-
* ```
|
|
700
|
-
*
|
|
701
|
-
* @example
|
|
702
|
-
* With timeout and error handling
|
|
703
|
-
* ```tsx
|
|
704
|
-
* const executeWithBlocking = useAsyncAction(
|
|
705
|
-
* 'fetchData',
|
|
706
|
-
* ['global', 'data'],
|
|
707
|
-
* {
|
|
708
|
-
* timeout: 30000, // 30 seconds
|
|
709
|
-
* onTimeout: (blockerId) => {
|
|
710
|
-
* console.warn(`Operation ${blockerId} timed out`);
|
|
711
|
-
* }
|
|
712
|
-
* }
|
|
713
|
-
* );
|
|
714
|
-
*
|
|
715
|
-
* const loadData = async () => {
|
|
716
|
-
* try {
|
|
717
|
-
* const data = await executeWithBlocking(async () => {
|
|
718
|
-
* return await fetchDataFromAPI();
|
|
719
|
-
* });
|
|
720
|
-
* setData(data);
|
|
721
|
-
* } catch (error) {
|
|
722
|
-
* console.error('Failed to load data:', error);
|
|
723
|
-
* // Blocker is automatically removed even on error
|
|
724
|
-
* }
|
|
725
|
-
* };
|
|
726
|
-
* ```
|
|
727
|
-
*
|
|
728
|
-
* @example
|
|
729
|
-
* Multiple concurrent actions
|
|
730
|
-
* ```tsx
|
|
731
|
-
* function FileUploader() {
|
|
732
|
-
* const uploadWithBlocking = useAsyncAction('upload', 'uploads');
|
|
733
|
-
*
|
|
734
|
-
* const uploadFiles = async (files) => {
|
|
735
|
-
* // Each upload gets unique blocker ID (upload-1, upload-2, etc.)
|
|
736
|
-
* const promises = files.map(file =>
|
|
737
|
-
* uploadWithBlocking(() => uploadFile(file))
|
|
738
|
-
* );
|
|
739
|
-
*
|
|
740
|
-
* await Promise.all(promises);
|
|
741
|
-
* };
|
|
742
|
-
*
|
|
743
|
-
* return <...>;
|
|
744
|
-
* }
|
|
745
|
-
* ```
|
|
746
|
-
*
|
|
747
|
-
* @example
|
|
748
|
-
* Type-safe return value
|
|
749
|
-
* ```tsx
|
|
750
|
-
* interface UserData {
|
|
751
|
-
* id: string;
|
|
752
|
-
* name: string;
|
|
753
|
-
* }
|
|
754
|
-
*
|
|
755
|
-
* function UserProfile() {
|
|
756
|
-
* const executeWithBlocking = useAsyncAction<UserData>('fetchUser', 'user');
|
|
757
|
-
*
|
|
758
|
-
* const loadUser = async (userId: string) => {
|
|
759
|
-
* const userData = await executeWithBlocking(async () => {
|
|
760
|
-
* const response = await fetch(`/api/users/${userId}`);
|
|
761
|
-
* return response.json(); // TypeScript knows this is UserData
|
|
762
|
-
* });
|
|
763
|
-
*
|
|
764
|
-
* // userData is typed as UserData
|
|
765
|
-
* console.log(userData.name);
|
|
766
|
-
* };
|
|
767
|
-
*
|
|
768
|
-
* return <...>;
|
|
769
|
-
* }
|
|
770
|
-
* ```
|
|
771
|
-
*
|
|
772
|
-
* @see {@link useBlocker} for manual blocker management
|
|
773
|
-
* @see {@link useIsBlocked} to check blocking state
|
|
774
|
-
* @see {@link UseAsyncActionOptions} for available options
|
|
277
|
+
* Wraps async functions with UI blocking. Concurrent calls get unique blocker IDs;
|
|
278
|
+
* each blocker is removed in finally on success or failure, even after caller unmount.
|
|
279
|
+
* Tracking does not exclude concurrent execution. Timeout releases only the blocker.
|
|
775
280
|
*
|
|
776
281
|
* @public
|
|
777
282
|
* @since 0.6.0
|
|
283
|
+
* @see {@link useActionBlocker} for manual blocker management
|
|
284
|
+
* @see {@link useIsBlocked} to check blocking state
|
|
285
|
+
* @see {@link UseAsyncActionOptions} for available options
|
|
778
286
|
*/
|
|
779
287
|
declare function useAsyncAction<T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions): (asyncFn: () => Promise<T>) => Promise<T>;
|
|
780
288
|
|
|
@@ -801,157 +309,30 @@ declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T,
|
|
|
801
309
|
/**
|
|
802
310
|
* Automatically manages a UI blocker based on component lifecycle.
|
|
803
311
|
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
* Supports scoped blocking, priorities, timeouts, and works with both
|
|
807
|
-
* global store and isolated provider instances.
|
|
808
|
-
*
|
|
809
|
-
* @param blockerId - Unique identifier for this blocker. Must be unique within the scope.
|
|
810
|
-
* @param config - Configuration object for the blocker
|
|
811
|
-
* @param config.scope - Scope(s) to block. Can be a single scope string (e.g., "form", "navigation")
|
|
812
|
-
* or an array of scopes (e.g., ["form", "navigation"]). Defaults to "global".
|
|
813
|
-
* @param config.reason - Human-readable reason for blocking, displayed in DevTools and UI
|
|
814
|
-
* @param config.priority - Priority level (0-100). Higher priority blockers take precedence.
|
|
815
|
-
* Use 0-30 for low priority, 31-70 for medium, 71-100 for high priority.
|
|
816
|
-
* @param config.timeout - Auto-removal timeout in milliseconds. Blocker will be automatically
|
|
817
|
-
* removed after this duration. Use 0 or undefined for no timeout.
|
|
818
|
-
* @param config.onTimeout - Callback function invoked when the blocker times out
|
|
819
|
-
* @param isActive - When true, blocker is active; when false, blocker is removed.
|
|
820
|
-
* Defaults to true. Use this for conditional blocking.
|
|
821
|
-
*
|
|
822
|
-
* @example
|
|
823
|
-
* Basic form submission blocking
|
|
824
|
-
* ```tsx
|
|
825
|
-
* function MyForm() {
|
|
826
|
-
* const [isSubmitting, setIsSubmitting] = useState(false);
|
|
827
|
-
*
|
|
828
|
-
* useBlocker('form-submit', {
|
|
829
|
-
* scope: 'form',
|
|
830
|
-
* reason: 'Submitting form...',
|
|
831
|
-
* priority: 50,
|
|
832
|
-
* timeout: 30000, // Auto-remove after 30 seconds
|
|
833
|
-
* onTimeout: () => {
|
|
834
|
-
* console.warn('Form submission timed out');
|
|
835
|
-
* setIsSubmitting(false);
|
|
836
|
-
* }
|
|
837
|
-
* }, isSubmitting);
|
|
838
|
-
*
|
|
839
|
-
* return <form onSubmit={handleSubmit}>...</form>;
|
|
840
|
-
* }
|
|
841
|
-
* ```
|
|
842
|
-
*
|
|
843
|
-
* @example
|
|
844
|
-
* Multiple scopes with high priority
|
|
845
|
-
* ```tsx
|
|
846
|
-
* useBlocker('critical-save', {
|
|
847
|
-
* scope: ['form', 'navigation', 'actions'],
|
|
848
|
-
* reason: 'Saving critical data...',
|
|
849
|
-
* priority: 90,
|
|
850
|
-
* }, isSaving);
|
|
851
|
-
* ```
|
|
852
|
-
*
|
|
853
|
-
* @example
|
|
854
|
-
* Conditional blocking based on unsaved changes
|
|
855
|
-
* ```tsx
|
|
856
|
-
* function Editor() {
|
|
857
|
-
* const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
858
|
-
*
|
|
859
|
-
* useBlocker('unsaved-changes', {
|
|
860
|
-
* scope: 'navigation',
|
|
861
|
-
* reason: 'You have unsaved changes',
|
|
862
|
-
* priority: 80,
|
|
863
|
-
* }, hasUnsavedChanges);
|
|
864
|
-
*
|
|
865
|
-
* return <div>...</div>;
|
|
866
|
-
* }
|
|
867
|
-
* ```
|
|
868
|
-
*
|
|
312
|
+
* @public
|
|
313
|
+
* @since 0.6.0
|
|
869
314
|
* @see {@link useIsBlocked} to check if a scope is currently blocked
|
|
870
315
|
* @see {@link useBlockingInfo} to get detailed blocker information
|
|
871
316
|
* @see {@link useAsyncAction} for async operation wrapping with automatic blocking
|
|
872
317
|
* @see {@link useConfirmableBlocker} for blockers that require user confirmation
|
|
873
|
-
*
|
|
874
|
-
* @public
|
|
875
|
-
* @since 0.6.0
|
|
876
318
|
*/
|
|
877
|
-
declare function
|
|
319
|
+
declare function useActionBlocker(blockerId: string, config: BlockerConfig, isActive?: boolean): void;
|
|
320
|
+
/**
|
|
321
|
+
* @deprecated Use {@link useActionBlocker} to avoid confusion with React Router's `useBlocker`.
|
|
322
|
+
*/
|
|
323
|
+
declare const useBlocker: typeof useActionBlocker;
|
|
878
324
|
|
|
879
325
|
/**
|
|
880
|
-
* Gets
|
|
881
|
-
*
|
|
882
|
-
* Returns an array of blocker information objects, sorted by priority (highest first).
|
|
883
|
-
* Each blocker object contains id, reason, priority, and scope information.
|
|
884
|
-
* The hook automatically re-renders when blockers are added, removed, or updated.
|
|
885
|
-
*
|
|
886
|
-
* Uses memoization to provide a stable reference - only re-renders when the actual
|
|
887
|
-
* blocker list changes, not on every store update. This makes it efficient for
|
|
888
|
-
* rendering lists of blockers.
|
|
889
|
-
*
|
|
890
|
-
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
891
|
-
*
|
|
892
|
-
* @param scope - Scope to get blocking information for. Defaults to "global" if not specified.
|
|
893
|
-
* @returns Array of {@link BlockerInfo} objects, sorted by priority (highest to lowest)
|
|
894
|
-
*
|
|
895
|
-
* @example
|
|
896
|
-
* Display list of active blockers
|
|
897
|
-
* ```tsx
|
|
898
|
-
* function BlockerList() {
|
|
899
|
-
* const blockers = useBlockingInfo('form');
|
|
900
|
-
*
|
|
901
|
-
* if (blockers.length === 0) {
|
|
902
|
-
* return <div>No active blockers</div>;
|
|
903
|
-
* }
|
|
904
|
-
*
|
|
905
|
-
* return (
|
|
906
|
-
* <ul>
|
|
907
|
-
* {blockers.map((blocker) => (
|
|
908
|
-
* <li key={blocker.id}>
|
|
909
|
-
* {blocker.reason} (Priority: {blocker.priority})
|
|
910
|
-
* </li>
|
|
911
|
-
* ))}
|
|
912
|
-
* </ul>
|
|
913
|
-
* );
|
|
914
|
-
* }
|
|
915
|
-
* ```
|
|
916
|
-
*
|
|
917
|
-
* @example
|
|
918
|
-
* Show highest priority blocker reason
|
|
919
|
-
* ```tsx
|
|
920
|
-
* function BlockingBanner() {
|
|
921
|
-
* const blockers = useBlockingInfo('global');
|
|
922
|
-
*
|
|
923
|
-
* // blockers are already sorted by priority (highest first)
|
|
924
|
-
* const topBlocker = blockers[0];
|
|
925
|
-
*
|
|
926
|
-
* if (!topBlocker) {
|
|
927
|
-
* return null;
|
|
928
|
-
* }
|
|
929
|
-
*
|
|
930
|
-
* return <div className="banner">{topBlocker.reason}</div>;
|
|
931
|
-
* }
|
|
932
|
-
* ```
|
|
933
|
-
*
|
|
934
|
-
* @example
|
|
935
|
-
* Count active blockers
|
|
936
|
-
* ```tsx
|
|
937
|
-
* function BlockerCount() {
|
|
938
|
-
* const formBlockers = useBlockingInfo('form');
|
|
939
|
-
* const navBlockers = useBlockingInfo('navigation');
|
|
940
|
-
*
|
|
941
|
-
* const total = formBlockers.length + navBlockers.length;
|
|
942
|
-
*
|
|
943
|
-
* return <div>{total} active blockers</div>;
|
|
944
|
-
* }
|
|
945
|
-
* ```
|
|
946
|
-
*
|
|
947
|
-
* @see {@link useIsBlocked} for a simple boolean check
|
|
948
|
-
* @see {@link useBlocker} to create blockers
|
|
949
|
-
* @see {@link BlockerInfo} for the structure of blocker information objects
|
|
326
|
+
* Gets immutable, priority-ordered information about blockers affecting the observed scopes.
|
|
327
|
+
* Unrelated lifecycle changes preserve the result and do not trigger a render.
|
|
950
328
|
*
|
|
951
329
|
* @public
|
|
952
330
|
* @since 0.6.0
|
|
331
|
+
* @see {@link useIsBlocked} for a simple boolean check
|
|
332
|
+
* @see {@link useActionBlocker} to create blockers
|
|
333
|
+
* @see {@link BlockerInfo} for the structure of blocker information objects
|
|
953
334
|
*/
|
|
954
|
-
declare function useBlockingInfo(scope?:
|
|
335
|
+
declare function useBlockingInfo(scope?: Scope): ReadonlyArray<Readonly<BlockerInfo>>;
|
|
955
336
|
|
|
956
337
|
/**
|
|
957
338
|
* Configuration for conditional blocker hook.
|
|
@@ -973,142 +354,11 @@ interface ConditionalBlockerConfig<TState = unknown> extends Omit<BlockerConfig,
|
|
|
973
354
|
/**
|
|
974
355
|
* Blocks UI based on a dynamic condition that is periodically evaluated.
|
|
975
356
|
*
|
|
976
|
-
* Periodically checks a condition function and automatically adds or removes
|
|
977
|
-
* a blocker based on the result. Useful for blocking based on application state,
|
|
978
|
-
* network status, feature flags, or any other dynamic condition.
|
|
979
|
-
*
|
|
980
|
-
* The condition function receives optional state and returns a boolean.
|
|
981
|
-
* It's evaluated immediately on mount and then at the specified interval.
|
|
982
|
-
* The blocker is added/removed automatically as the condition changes.
|
|
983
|
-
*
|
|
984
|
-
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
985
|
-
*
|
|
986
|
-
* @template TState - Type of the state object passed to the condition function
|
|
987
|
-
* @param blockerId - Unique identifier for this conditional blocker
|
|
988
|
-
* @param config - Configuration for conditional blocking
|
|
989
|
-
* @param config.condition - Function that returns true to block, false to unblock.
|
|
990
|
-
* Receives the `state` object as parameter.
|
|
991
|
-
* @param config.state - Optional state object passed to the condition function
|
|
992
|
-
* @param config.checkInterval - How often to check the condition in milliseconds.
|
|
993
|
-
* Defaults to 1000ms (1 second). Lower values mean
|
|
994
|
-
* more responsive blocking but higher CPU usage.
|
|
995
|
-
* @param config.scope - Scope(s) to block when condition is true
|
|
996
|
-
* @param config.reason - Reason for blocking (displayed to users)
|
|
997
|
-
* @param config.priority - Optional blocker priority (0-100)
|
|
998
|
-
*
|
|
999
|
-
* @example
|
|
1000
|
-
* Block based on network status
|
|
1001
|
-
* ```tsx
|
|
1002
|
-
* function OfflineBlocker() {
|
|
1003
|
-
* const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
1004
|
-
*
|
|
1005
|
-
* useEffect(() => {
|
|
1006
|
-
* const handleOnline = () => setIsOnline(true);
|
|
1007
|
-
* const handleOffline = () => setIsOnline(false);
|
|
1008
|
-
*
|
|
1009
|
-
* window.addEventListener('online', handleOnline);
|
|
1010
|
-
* window.addEventListener('offline', handleOffline);
|
|
1011
|
-
*
|
|
1012
|
-
* return () => {
|
|
1013
|
-
* window.removeEventListener('online', handleOnline);
|
|
1014
|
-
* window.removeEventListener('offline', handleOffline);
|
|
1015
|
-
* };
|
|
1016
|
-
* }, []);
|
|
1017
|
-
*
|
|
1018
|
-
* useConditionalBlocker('offline', {
|
|
1019
|
-
* condition: () => !isOnline,
|
|
1020
|
-
* state: { isOnline },
|
|
1021
|
-
* checkInterval: 500, // Check every 500ms
|
|
1022
|
-
* scope: 'global',
|
|
1023
|
-
* reason: 'No internet connection. Please check your network.',
|
|
1024
|
-
* priority: 95
|
|
1025
|
-
* });
|
|
1026
|
-
*
|
|
1027
|
-
* return <App />;
|
|
1028
|
-
* }
|
|
1029
|
-
* ```
|
|
1030
|
-
*
|
|
1031
|
-
* @example
|
|
1032
|
-
* Block based on feature flag
|
|
1033
|
-
* ```tsx
|
|
1034
|
-
* function FeatureFlagBlocker() {
|
|
1035
|
-
* const featureFlags = useFeatureFlags();
|
|
1036
|
-
*
|
|
1037
|
-
* useConditionalBlocker('feature-disabled', {
|
|
1038
|
-
* condition: (state) => !state.featureFlags.newFeatureEnabled,
|
|
1039
|
-
* state: { featureFlags },
|
|
1040
|
-
* checkInterval: 5000, // Check every 5 seconds
|
|
1041
|
-
* scope: 'new-feature',
|
|
1042
|
-
* reason: 'This feature is currently disabled',
|
|
1043
|
-
* priority: 70
|
|
1044
|
-
* });
|
|
1045
|
-
*
|
|
1046
|
-
* return <NewFeature />;
|
|
1047
|
-
* }
|
|
1048
|
-
* ```
|
|
1049
|
-
*
|
|
1050
|
-
* @example
|
|
1051
|
-
* Block during API rate limit
|
|
1052
|
-
* ```tsx
|
|
1053
|
-
* function RateLimitBlocker() {
|
|
1054
|
-
* const [requestCount, setRequestCount] = useState(0);
|
|
1055
|
-
* const [resetTime, setResetTime] = useState<number | null>(null);
|
|
1056
|
-
*
|
|
1057
|
-
* useConditionalBlocker('rate-limit', {
|
|
1058
|
-
* condition: (state) => {
|
|
1059
|
-
* if (!state.resetTime) {
|
|
1060
|
-
* return false;
|
|
1061
|
-
* }
|
|
1062
|
-
* return Date.now() < state.resetTime && state.requestCount >= 100;
|
|
1063
|
-
* },
|
|
1064
|
-
* state: { requestCount, resetTime },
|
|
1065
|
-
* checkInterval: 1000,
|
|
1066
|
-
* scope: 'api',
|
|
1067
|
-
* reason: 'API rate limit reached. Please wait.',
|
|
1068
|
-
* priority: 90
|
|
1069
|
-
* });
|
|
1070
|
-
*
|
|
1071
|
-
* return <Dashboard />;
|
|
1072
|
-
* }
|
|
1073
|
-
* ```
|
|
1074
|
-
*
|
|
1075
|
-
* @example
|
|
1076
|
-
* Block based on form validation
|
|
1077
|
-
* ```tsx
|
|
1078
|
-
* function FormValidationBlocker() {
|
|
1079
|
-
* const { formState } = useFormContext();
|
|
1080
|
-
*
|
|
1081
|
-
* useConditionalBlocker('invalid-form', {
|
|
1082
|
-
* condition: (state) => !state.isValid && state.isDirty,
|
|
1083
|
-
* state: formState,
|
|
1084
|
-
* checkInterval: 500,
|
|
1085
|
-
* scope: 'form-submit',
|
|
1086
|
-
* reason: 'Please fix form errors before submitting',
|
|
1087
|
-
* priority: 60
|
|
1088
|
-
* });
|
|
1089
|
-
*
|
|
1090
|
-
* return <Form />;
|
|
1091
|
-
* }
|
|
1092
|
-
* ```
|
|
1093
|
-
*
|
|
1094
|
-
* @example
|
|
1095
|
-
* Without state parameter (simple condition)
|
|
1096
|
-
* ```tsx
|
|
1097
|
-
* useConditionalBlocker('low-battery', {
|
|
1098
|
-
* condition: () => !navigator.onLine,
|
|
1099
|
-
* checkInterval: 30000, // Check every 30 seconds
|
|
1100
|
-
* scope: 'heavy-operations',
|
|
1101
|
-
* reason: 'Internet connection is required for this operation',
|
|
1102
|
-
* priority: 80
|
|
1103
|
-
* });
|
|
1104
|
-
* ```
|
|
1105
|
-
*
|
|
1106
|
-
* @see {@link useBlocker} for simple conditional blocking with boolean
|
|
1107
|
-
* @see {@link useScheduledBlocker} for time-based blocking
|
|
1108
|
-
* @see {@link ConditionalBlockerConfig} for configuration options
|
|
1109
|
-
*
|
|
1110
357
|
* @public
|
|
1111
358
|
* @since 0.6.0
|
|
359
|
+
* @see {@link useActionBlocker} for simple conditional blocking with boolean
|
|
360
|
+
* @see {@link useScheduledBlocker} for time-based blocking
|
|
361
|
+
* @see {@link ConditionalBlockerConfig} for configuration options
|
|
1112
362
|
*/
|
|
1113
363
|
declare function useConditionalBlocker<TState = unknown>(blockerId: string, config: ConditionalBlockerConfig<TState>): void;
|
|
1114
364
|
|
|
@@ -1156,209 +406,21 @@ interface UseConfirmableBlockerReturn {
|
|
|
1156
406
|
/**
|
|
1157
407
|
* Creates a blocker that requires user confirmation before executing an action.
|
|
1158
408
|
*
|
|
1159
|
-
* Manages a confirmation dialog and blocking state for actions that need user approval.
|
|
1160
|
-
* The blocker is active during both the confirmation dialog display and action execution.
|
|
1161
|
-
*
|
|
1162
|
-
* Workflow:
|
|
1163
|
-
* 1. Call `execute()` to show confirmation dialog (blocker activated)
|
|
1164
|
-
* 2. User confirms → `onConfirm` callback runs (blocker remains active)
|
|
1165
|
-
* 3. Action completes → blocker removed
|
|
1166
|
-
* 4. User cancels → blocker removed, optional `onCancel` callback runs
|
|
1167
|
-
*
|
|
1168
|
-
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1169
|
-
*
|
|
1170
|
-
* @param blockerId - Unique identifier for this confirmable blocker
|
|
1171
|
-
* @param config - Configuration for the confirmable action
|
|
1172
|
-
* @param config.confirmMessage - Message to display in the confirmation dialog
|
|
1173
|
-
* @param config.confirmTitle - Optional dialog title (defaults to "Confirm Action")
|
|
1174
|
-
* @param config.confirmButtonText - Optional confirm button text (defaults to "Confirm")
|
|
1175
|
-
* @param config.cancelButtonText - Optional cancel button text (defaults to "Cancel")
|
|
1176
|
-
* @param config.onConfirm - Async function to execute after user confirms
|
|
1177
|
-
* @param config.onCancel - Optional callback when user cancels
|
|
1178
|
-
* @param config.scope - Scope(s) to block during confirmation and execution
|
|
1179
|
-
* @param config.reason - Optional custom blocking reason (defaults to confirmMessage)
|
|
1180
|
-
* @param config.priority - Optional blocker priority (0-100)
|
|
1181
|
-
*
|
|
1182
|
-
* @returns Object containing:
|
|
1183
|
-
* - `execute`: Function to trigger the confirmation dialog
|
|
1184
|
-
* - `isDialogOpen`: Boolean indicating if confirmation dialog is visible
|
|
1185
|
-
* - `isExecuting`: Boolean indicating if the confirmed action is executing
|
|
1186
|
-
* - `confirmConfig`: Dialog configuration (title, message, button texts)
|
|
1187
|
-
* - `onConfirm`: Function to call when user confirms
|
|
1188
|
-
* - `onCancel`: Function to call when user cancels
|
|
1189
|
-
*
|
|
1190
|
-
* @example
|
|
1191
|
-
* Delete confirmation with custom dialog
|
|
1192
|
-
* ```tsx
|
|
1193
|
-
* function DeleteButton({ itemId, itemName }) {
|
|
1194
|
-
* const confirmableDelete = useConfirmableBlocker('delete-item', {
|
|
1195
|
-
* confirmMessage: `Are you sure you want to delete "${itemName}"?`,
|
|
1196
|
-
* confirmTitle: 'Delete Item',
|
|
1197
|
-
* confirmButtonText: 'Delete',
|
|
1198
|
-
* cancelButtonText: 'Keep',
|
|
1199
|
-
* scope: ['form', 'navigation'],
|
|
1200
|
-
* priority: 80,
|
|
1201
|
-
* onConfirm: async () => {
|
|
1202
|
-
* await deleteItem(itemId);
|
|
1203
|
-
* showToast('Item deleted successfully');
|
|
1204
|
-
* },
|
|
1205
|
-
* onCancel: () => {
|
|
1206
|
-
* console.log('Delete cancelled');
|
|
1207
|
-
* }
|
|
1208
|
-
* });
|
|
1209
|
-
*
|
|
1210
|
-
* return (
|
|
1211
|
-
* <>
|
|
1212
|
-
* <button onClick={confirmableDelete.execute}>
|
|
1213
|
-
* Delete
|
|
1214
|
-
* </button>
|
|
1215
|
-
*
|
|
1216
|
-
* {confirmableDelete.isDialogOpen && (
|
|
1217
|
-
* <ConfirmDialog
|
|
1218
|
-
* {...confirmableDelete.confirmConfig}
|
|
1219
|
-
* onConfirm={confirmableDelete.onConfirm}
|
|
1220
|
-
* onCancel={confirmableDelete.onCancel}
|
|
1221
|
-
* isLoading={confirmableDelete.isExecuting}
|
|
1222
|
-
* />
|
|
1223
|
-
* )}
|
|
1224
|
-
* </>
|
|
1225
|
-
* );
|
|
1226
|
-
* }
|
|
1227
|
-
* ```
|
|
1228
|
-
*
|
|
1229
|
-
* @example
|
|
1230
|
-
* Simple confirmation with default dialog
|
|
1231
|
-
* ```tsx
|
|
1232
|
-
* function LogoutButton() {
|
|
1233
|
-
* const confirmLogout = useConfirmableBlocker('logout', {
|
|
1234
|
-
* confirmMessage: 'Are you sure you want to log out?',
|
|
1235
|
-
* scope: 'global',
|
|
1236
|
-
* onConfirm: async () => {
|
|
1237
|
-
* await api.logout();
|
|
1238
|
-
* navigate('/login');
|
|
1239
|
-
* }
|
|
1240
|
-
* });
|
|
1241
|
-
*
|
|
1242
|
-
* return (
|
|
1243
|
-
* <>
|
|
1244
|
-
* <button onClick={confirmLogout.execute}>Logout</button>
|
|
1245
|
-
*
|
|
1246
|
-
* {confirmLogout.isDialogOpen && (
|
|
1247
|
-
* <Dialog
|
|
1248
|
-
* title={confirmLogout.confirmConfig.title}
|
|
1249
|
-
* message={confirmLogout.confirmConfig.message}
|
|
1250
|
-
* onConfirm={confirmLogout.onConfirm}
|
|
1251
|
-
* onCancel={confirmLogout.onCancel}
|
|
1252
|
-
* />
|
|
1253
|
-
* )}
|
|
1254
|
-
* </>
|
|
1255
|
-
* );
|
|
1256
|
-
* }
|
|
1257
|
-
* ```
|
|
1258
|
-
*
|
|
1259
|
-
* @example
|
|
1260
|
-
* With loading state during execution
|
|
1261
|
-
* ```tsx
|
|
1262
|
-
* function SaveButton() {
|
|
1263
|
-
* const confirmSave = useConfirmableBlocker('save-changes', {
|
|
1264
|
-
* confirmMessage: 'Save all changes?',
|
|
1265
|
-
* scope: 'form',
|
|
1266
|
-
* onConfirm: async () => {
|
|
1267
|
-
* await saveAllChanges();
|
|
1268
|
-
* }
|
|
1269
|
-
* });
|
|
1270
|
-
*
|
|
1271
|
-
* return (
|
|
1272
|
-
* <button
|
|
1273
|
-
* onClick={confirmSave.execute}
|
|
1274
|
-
* disabled={confirmSave.isExecuting}
|
|
1275
|
-
* >
|
|
1276
|
-
* {confirmSave.isExecuting ? 'Saving...' : 'Save All'}
|
|
1277
|
-
* </button>
|
|
1278
|
-
* );
|
|
1279
|
-
* }
|
|
1280
|
-
* ```
|
|
1281
|
-
*
|
|
1282
|
-
* @see {@link useBlocker} for simple blocking without confirmation
|
|
1283
|
-
* @see {@link ConfirmableBlockerConfig} for configuration options
|
|
1284
|
-
* @see {@link UseConfirmableBlockerReturn} for return value structure
|
|
1285
|
-
*
|
|
1286
409
|
* @public
|
|
1287
410
|
* @since 0.6.0
|
|
411
|
+
* @see {@link useActionBlocker} for simple blocking without confirmation
|
|
412
|
+
* @see {@link ConfirmableBlockerConfig} for configuration options
|
|
413
|
+
* @see {@link UseConfirmableBlockerReturn} for return value structure
|
|
1288
414
|
*/
|
|
1289
415
|
declare function useConfirmableBlocker(blockerId: string, config: ConfirmableBlockerConfig): UseConfirmableBlockerReturn;
|
|
1290
416
|
|
|
1291
417
|
/**
|
|
1292
418
|
* Checks if one or more scopes are currently blocked.
|
|
1293
419
|
*
|
|
1294
|
-
* Returns a boolean indicating whether any of the specified scopes have active blockers.
|
|
1295
|
-
* The hook automatically re-renders the component when the blocking state changes,
|
|
1296
|
-
* ensuring your UI always reflects the current blocking state.
|
|
1297
|
-
*
|
|
1298
|
-
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1299
|
-
*
|
|
1300
|
-
* @param scope - Scope(s) to check for blocking. Can be:
|
|
1301
|
-
* - A single scope string (e.g., "form", "navigation", "global")
|
|
1302
|
-
* - An array of scopes (e.g., ["form", "navigation"])
|
|
1303
|
-
* - undefined to check the "global" scope
|
|
1304
|
-
* @returns true if ANY of the specified scopes are blocked, false otherwise
|
|
1305
|
-
*
|
|
1306
|
-
* @example
|
|
1307
|
-
* Basic single scope check
|
|
1308
|
-
* ```tsx
|
|
1309
|
-
* function SubmitButton() {
|
|
1310
|
-
* const isFormBlocked = useIsBlocked('form');
|
|
1311
|
-
*
|
|
1312
|
-
* return (
|
|
1313
|
-
* <button disabled={isFormBlocked}>
|
|
1314
|
-
* {isFormBlocked ? 'Processing...' : 'Submit'}
|
|
1315
|
-
* </button>
|
|
1316
|
-
* );
|
|
1317
|
-
* }
|
|
1318
|
-
* ```
|
|
1319
|
-
*
|
|
1320
|
-
* @example
|
|
1321
|
-
* Multiple scopes check
|
|
1322
|
-
* ```tsx
|
|
1323
|
-
* function NavigationLink({ href, children }) {
|
|
1324
|
-
* const isBlocked = useIsBlocked(['navigation', 'global']);
|
|
1325
|
-
*
|
|
1326
|
-
* return (
|
|
1327
|
-
* <a
|
|
1328
|
-
* href={href}
|
|
1329
|
-
* onClick={(e) => {
|
|
1330
|
-
* if (isBlocked) {
|
|
1331
|
-
* e.preventDefault();
|
|
1332
|
-
* alert('Navigation is currently blocked');
|
|
1333
|
-
* }
|
|
1334
|
-
* }}
|
|
1335
|
-
* style={{ opacity: isBlocked ? 0.5 : 1 }}
|
|
1336
|
-
* >
|
|
1337
|
-
* {children}
|
|
1338
|
-
* </a>
|
|
1339
|
-
* );
|
|
1340
|
-
* }
|
|
1341
|
-
* ```
|
|
1342
|
-
*
|
|
1343
|
-
* @example
|
|
1344
|
-
* Conditional rendering based on blocking state
|
|
1345
|
-
* ```tsx
|
|
1346
|
-
* function SaveIndicator() {
|
|
1347
|
-
* const isSaving = useIsBlocked('save');
|
|
1348
|
-
*
|
|
1349
|
-
* if (!isSaving) {
|
|
1350
|
-
* return null;
|
|
1351
|
-
* }
|
|
1352
|
-
*
|
|
1353
|
-
* return <div className="spinner">Saving...</div>;
|
|
1354
|
-
* }
|
|
1355
|
-
* ```
|
|
1356
|
-
*
|
|
1357
|
-
* @see {@link useBlocker} to create a blocker
|
|
1358
|
-
* @see {@link useBlockingInfo} to get detailed information about active blockers
|
|
1359
|
-
*
|
|
1360
420
|
* @public
|
|
1361
421
|
* @since 0.6.0
|
|
422
|
+
* @see {@link useActionBlocker} to create a blocker
|
|
423
|
+
* @see {@link useBlockingInfo} to get detailed information about active blockers
|
|
1362
424
|
*/
|
|
1363
425
|
declare function useIsBlocked(scope?: string | ReadonlyArray<string>): boolean;
|
|
1364
426
|
|
|
@@ -1369,7 +431,11 @@ declare function useIsBlocked(scope?: string | ReadonlyArray<string>): boolean;
|
|
|
1369
431
|
* 1. start + duration: Block for a specific duration after start time
|
|
1370
432
|
* 2. start + end: Block between start and end times
|
|
1371
433
|
*
|
|
1372
|
-
*
|
|
434
|
+
* Windows are half-open [start, end). Omit both duration and end for indefinite blocking.
|
|
435
|
+
* Zero duration or equal bounds are empty and emit no schedule notifications.
|
|
436
|
+
* Every supplied bound must be valid, even when duration takes precedence over end.
|
|
437
|
+
* Negative/nonfinite duration, invalid dates, end before start and calculated end
|
|
438
|
+
* outside the JavaScript Date range cancel the schedule with a development warning.
|
|
1373
439
|
*/
|
|
1374
440
|
interface BlockingSchedule {
|
|
1375
441
|
/** Start time as ISO string, Date object, or timestamp */
|
|
@@ -1394,123 +460,12 @@ interface ScheduledBlockerConfig extends BlockerConfig {
|
|
|
1394
460
|
/**
|
|
1395
461
|
* Schedules UI blocking for a specific time period (e.g., maintenance windows).
|
|
1396
462
|
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
* Time specifications support:
|
|
1402
|
-
* - Date objects
|
|
1403
|
-
* - ISO 8601 strings (e.g., "2024-01-15T10:00:00Z")
|
|
1404
|
-
* - Unix timestamps (milliseconds since epoch)
|
|
1405
|
-
*
|
|
1406
|
-
* You can specify the end time either as an absolute time or as a duration
|
|
1407
|
-
* from the start time. If both are provided, duration takes precedence.
|
|
1408
|
-
*
|
|
1409
|
-
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1410
|
-
*
|
|
1411
|
-
* @param blockerId - Unique identifier for this scheduled blocker
|
|
1412
|
-
* @param config - Schedule configuration
|
|
1413
|
-
* @param config.schedule - Schedule timing configuration
|
|
1414
|
-
* @param config.schedule.start - When to start blocking (Date, ISO string, or timestamp)
|
|
1415
|
-
* @param config.schedule.end - Optional absolute end time (Date, ISO string, or timestamp)
|
|
1416
|
-
* @param config.schedule.duration - Optional duration in milliseconds from start time
|
|
1417
|
-
* @param config.scope - Scope(s) to block during the scheduled period
|
|
1418
|
-
* @param config.reason - Reason for blocking (displayed to users)
|
|
1419
|
-
* @param config.priority - Optional blocker priority (0-100)
|
|
1420
|
-
* @param config.onScheduleStart - Optional callback when blocking period starts
|
|
1421
|
-
* @param config.onScheduleEnd - Optional callback when blocking period ends
|
|
1422
|
-
*
|
|
1423
|
-
* @example
|
|
1424
|
-
* Maintenance window with absolute times
|
|
1425
|
-
* ```tsx
|
|
1426
|
-
* function MaintenanceWarning() {
|
|
1427
|
-
* useScheduledBlocker('maintenance', {
|
|
1428
|
-
* schedule: {
|
|
1429
|
-
* start: new Date('2024-01-15T02:00:00Z'),
|
|
1430
|
-
* end: new Date('2024-01-15T04:00:00Z'),
|
|
1431
|
-
* },
|
|
1432
|
-
* scope: 'global',
|
|
1433
|
-
* reason: 'System maintenance in progress',
|
|
1434
|
-
* priority: 95,
|
|
1435
|
-
* onScheduleStart: () => {
|
|
1436
|
-
* console.log('Maintenance started');
|
|
1437
|
-
* showNotification('System is now in maintenance mode');
|
|
1438
|
-
* },
|
|
1439
|
-
* onScheduleEnd: () => {
|
|
1440
|
-
* console.log('Maintenance completed');
|
|
1441
|
-
* showNotification('System is back online');
|
|
1442
|
-
* }
|
|
1443
|
-
* });
|
|
1444
|
-
*
|
|
1445
|
-
* return <div>...</div>;
|
|
1446
|
-
* }
|
|
1447
|
-
* ```
|
|
1448
|
-
*
|
|
1449
|
-
* @example
|
|
1450
|
-
* Short maintenance with duration
|
|
1451
|
-
* ```tsx
|
|
1452
|
-
* useScheduledBlocker('quick-maintenance', {
|
|
1453
|
-
* schedule: {
|
|
1454
|
-
* start: new Date('2024-01-15T03:00:00'),
|
|
1455
|
-
* duration: 15 * 60 * 1000, // 15 minutes
|
|
1456
|
-
* },
|
|
1457
|
-
* scope: ['api', 'forms'],
|
|
1458
|
-
* reason: 'Brief system update',
|
|
1459
|
-
* priority: 90
|
|
1460
|
-
* });
|
|
1461
|
-
* ```
|
|
1462
|
-
*
|
|
1463
|
-
* @example
|
|
1464
|
-
* Business hours enforcement
|
|
1465
|
-
* ```tsx
|
|
1466
|
-
* function BusinessHoursBlocker() {
|
|
1467
|
-
* const now = new Date();
|
|
1468
|
-
* const tomorrow9AM = new Date(now);
|
|
1469
|
-
* tomorrow9AM.setDate(now.getDate() + 1);
|
|
1470
|
-
* tomorrow9AM.setHours(9, 0, 0, 0);
|
|
1471
|
-
*
|
|
1472
|
-
* useScheduledBlocker('after-hours', {
|
|
1473
|
-
* schedule: {
|
|
1474
|
-
* start: new Date(now.getTime() + 1000), // Start immediately
|
|
1475
|
-
* end: tomorrow9AM,
|
|
1476
|
-
* },
|
|
1477
|
-
* scope: 'trading',
|
|
1478
|
-
* reason: 'Trading is only available during business hours (9 AM - 5 PM)',
|
|
1479
|
-
* priority: 85
|
|
1480
|
-
* });
|
|
1481
|
-
*
|
|
1482
|
-
* return <TradingDashboard />;
|
|
1483
|
-
* }
|
|
1484
|
-
* ```
|
|
1485
|
-
*
|
|
1486
|
-
* @example
|
|
1487
|
-
* Using ISO strings for scheduling
|
|
1488
|
-
* ```tsx
|
|
1489
|
-
* useScheduledBlocker('deployment', {
|
|
1490
|
-
* schedule: {
|
|
1491
|
-
* start: '2024-01-20T00:00:00Z',
|
|
1492
|
-
* duration: 2 * 60 * 60 * 1000, // 2 hours
|
|
1493
|
-
* },
|
|
1494
|
-
* scope: 'global',
|
|
1495
|
-
* reason: 'Deploying new version',
|
|
1496
|
-
* priority: 100,
|
|
1497
|
-
* onScheduleStart: () => {
|
|
1498
|
-
* analytics.track('deployment_started');
|
|
1499
|
-
* },
|
|
1500
|
-
* onScheduleEnd: () => {
|
|
1501
|
-
* analytics.track('deployment_completed');
|
|
1502
|
-
* window.location.reload(); // Reload to get new version
|
|
1503
|
-
* }
|
|
1504
|
-
* });
|
|
1505
|
-
* ```
|
|
1506
|
-
*
|
|
1507
|
-
* @see {@link useBlocker} for immediate blocking without scheduling
|
|
463
|
+
* @public
|
|
464
|
+
* @since 0.6.0
|
|
465
|
+
* @see {@link useActionBlocker} for immediate blocking without scheduling
|
|
1508
466
|
* @see {@link useConditionalBlocker} for condition-based blocking
|
|
1509
467
|
* @see {@link ScheduledBlockerConfig} for configuration options
|
|
1510
468
|
* @see {@link BlockingSchedule} for schedule specification details
|
|
1511
|
-
*
|
|
1512
|
-
* @public
|
|
1513
|
-
* @since 0.6.0
|
|
1514
469
|
*/
|
|
1515
470
|
declare function useScheduledBlocker(blockerId: string, config: ScheduledBlockerConfig): void;
|
|
1516
471
|
|
|
@@ -1530,7 +485,7 @@ interface BlockerConfigTyped<TScope extends string> {
|
|
|
1530
485
|
scope?: ScopeValue<TScope>;
|
|
1531
486
|
/** Human-readable reason for blocking */
|
|
1532
487
|
reason?: string;
|
|
1533
|
-
/**
|
|
488
|
+
/** Reason ordering priority (minimum 0). Every matching active blocker retains protection, including lower-priority ones. */
|
|
1534
489
|
priority?: number;
|
|
1535
490
|
/** Timestamp when the blocker was created */
|
|
1536
491
|
timestamp?: number;
|
|
@@ -1545,7 +500,12 @@ interface BlockerConfigTyped<TScope extends string> {
|
|
|
1545
500
|
*/
|
|
1546
501
|
interface TypedHooks<TScope extends string> {
|
|
1547
502
|
/**
|
|
1548
|
-
* Type-safe
|
|
503
|
+
* Type-safe action blocker hook
|
|
504
|
+
*/
|
|
505
|
+
useActionBlocker: (blockerId: string, config: BlockerConfigTyped<TScope>, isActive?: boolean) => void;
|
|
506
|
+
/**
|
|
507
|
+
* Backward-compatible name for useActionBlocker
|
|
508
|
+
* @deprecated Use `useActionBlocker`.
|
|
1549
509
|
*/
|
|
1550
510
|
useBlocker: (blockerId: string, config: BlockerConfigTyped<TScope>, isActive?: boolean) => void;
|
|
1551
511
|
/**
|
|
@@ -1559,7 +519,7 @@ interface TypedHooks<TScope extends string> {
|
|
|
1559
519
|
/**
|
|
1560
520
|
* Type-safe version of useBlockingInfo hook
|
|
1561
521
|
*/
|
|
1562
|
-
useBlockingInfo: (scope: TScope) => ReturnType<typeof useBlockingInfo>;
|
|
522
|
+
useBlockingInfo: (scope: ScopeValue<TScope>) => ReturnType<typeof useBlockingInfo>;
|
|
1563
523
|
}
|
|
1564
524
|
/**
|
|
1565
525
|
* Creates type-safe versions of all blocking hooks with your custom scope types
|
|
@@ -1573,26 +533,13 @@ interface TypedHooks<TScope extends string> {
|
|
|
1573
533
|
* type AppScopes = "global" | "form" | "navigation" | "checkout";
|
|
1574
534
|
*
|
|
1575
535
|
* // Create typed hooks
|
|
1576
|
-
* const {
|
|
536
|
+
* const { useActionBlocker, useIsBlocked, useAsyncAction } = createTypedHooks<AppScopes>();
|
|
1577
537
|
*
|
|
1578
538
|
* // Now TypeScript will catch typos
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
539
|
+
* useActionBlocker("id", { scope: "form" }); // ✓ OK
|
|
540
|
+
* useActionBlocker("id", { scope: "typo" }); // ✗ Type error!
|
|
1581
541
|
* ```
|
|
1582
542
|
*/
|
|
1583
543
|
declare function createTypedHooks<TScope extends string>(): TypedHooks<TScope>;
|
|
1584
544
|
|
|
1585
|
-
|
|
1586
|
-
* @deprecated Use `useOptionalUIBlockingContext`.
|
|
1587
|
-
*/
|
|
1588
|
-
declare function useOptionalContext(): ReturnType<typeof useOptionalUIBlockingContext>;
|
|
1589
|
-
/**
|
|
1590
|
-
* @deprecated Use `useResolvedStoreApi`.
|
|
1591
|
-
*/
|
|
1592
|
-
declare function useResolvedStore(): StoreApi<UIBlockingStore>;
|
|
1593
|
-
/**
|
|
1594
|
-
* @deprecated Use `useResolvedValue`.
|
|
1595
|
-
*/
|
|
1596
|
-
declare function useResolvedStoreWithSelector<T>(selector: (state: UIBlockingStore) => T): T;
|
|
1597
|
-
|
|
1598
|
-
export { type AmplitudeConfig, type AnalyticsConfig, type AnalyticsEventData, type AnalyticsProvider, type BlockerConfig, type BlockerConfigTyped, type BlockerInfo, type BlockingAction, type BlockingSchedule, type ConditionalBlockerConfig, type ConfirmDialogConfig, type ConfirmableBlockerConfig, type CustomAnalyticsConfig, type DefaultScopes, type GoogleAnalyticsConfig, type Middleware, type MiddlewareContext, type MixpanelConfig, type PerformanceConfig, type ScheduledBlockerConfig, type ScopeValue, type ShallowStoreBindings, type StoredBlocker, type TypedHooks, UIBlockingProvider, type UIBlockingProviderProps, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, type UseAsyncActionOptions, type UseConfirmableBlockerReturn, configureMiddleware, createAnalyticsMiddleware, createPerformanceMiddleware, createTypedHooks, loggerMiddleware, uiBlockingStoreApi, useAsyncAction, useBlocker, useBlockingInfo, useConditionalBlocker, useConfirmableBlocker, useIsBlocked, useIsInsideUIBlockingProvider, useOptionalContext, useOptionalUIBlockingContext, useResolvedStore, useResolvedStoreApi, useResolvedStoreWithSelector, useResolvedValue, useScheduledBlocker, useUIBlockingContext, useUIBlockingStore, useUIBlockingStoreFromContext };
|
|
545
|
+
export { type AmplitudeConfig, type AnalyticsConfig, type AnalyticsEventData, type AnalyticsProvider, type BlockerConfig, type BlockerConfigTyped, type BlockerInfo, type BlockingAction, type BlockingEvent, type BlockingLifecycle, type BlockingLifecycleObservation, type BlockingLifecycleSnapshot, type BlockingSchedule, type ConditionalBlockerConfig, type ConfirmDialogConfig, type ConfirmableBlockerConfig, type CustomAnalyticsConfig, DEFAULT_SCOPE, type DefaultScopes, type GoogleAnalyticsConfig, type Middleware, type MiddlewareContext, type MixpanelConfig, type PerformanceConfig, type ScheduledBlockerConfig, type Scope, type ScopeValue, type TypedHooks, UIBlockingProvider, type UIBlockingProviderProps, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, type UseAsyncActionOptions, type UseConfirmableBlockerReturn, configureMiddleware, createAnalyticsMiddleware, createBlockingLifecycle, createPerformanceMiddleware, createTypedHooks, loggerMiddleware, normalizeScope, resolveScope, scopeAffectsObservation, scopeMatchesTarget, uiBlockingStoreApi, useActionBlocker, useAsyncAction, useBlocker, useBlockingInfo, useConditionalBlocker, useConfirmableBlocker, useIsBlocked, useIsInsideUIBlockingProvider, useOptionalContext, useOptionalUIBlockingContext, useResolvedStore, useResolvedStoreApi, useResolvedStoreWithSelector, useResolvedValue, useScheduledBlocker, useUIBlockingContext, useUIBlockingStore, useUIBlockingStoreFromContext };
|