@okyrychenko-dev/react-action-guard 0.2.0 → 0.2.2
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 +496 -37
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
- Priority-based blocking system
|
|
8
8
|
- Scoped blocking (global, specific areas, or multiple scopes)
|
|
9
9
|
- Automatic cleanup on unmount
|
|
10
|
-
-
|
|
10
|
+
- Advanced hooks for different use cases:
|
|
11
|
+
- Confirmable blockers with custom dialogs
|
|
12
|
+
- Scheduled blocking for maintenance windows
|
|
13
|
+
- Conditional blocking based on application state
|
|
14
|
+
- Async action wrapping
|
|
15
|
+
- Advanced middleware system for analytics, logging, and performance monitoring
|
|
16
|
+
- TypeScript support with full type safety
|
|
11
17
|
- Built on Zustand for efficient state management
|
|
18
|
+
- Tree-shakeable - import only what you need
|
|
12
19
|
- Hooks-based API
|
|
13
20
|
|
|
14
21
|
## Installation
|
|
@@ -20,16 +27,12 @@ npm install @okyrychenko-dev/react-action-guard zustand
|
|
|
20
27
|
## Quick Start
|
|
21
28
|
|
|
22
29
|
```jsx
|
|
23
|
-
import { useBlocker, useIsBlocked } from
|
|
30
|
+
import { useBlocker, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
|
|
24
31
|
|
|
25
32
|
function MyComponent() {
|
|
26
|
-
const isBlocked = useIsBlocked(
|
|
33
|
+
const isBlocked = useIsBlocked("my-scope");
|
|
27
34
|
|
|
28
|
-
return
|
|
29
|
-
<button disabled={isBlocked}>
|
|
30
|
-
Click me
|
|
31
|
-
</button>
|
|
32
|
-
);
|
|
35
|
+
return <button disabled={isBlocked}>Click me</button>;
|
|
33
36
|
}
|
|
34
37
|
```
|
|
35
38
|
|
|
@@ -42,6 +45,7 @@ function MyComponent() {
|
|
|
42
45
|
Automatically adds a blocker when the component mounts and removes it on unmount.
|
|
43
46
|
|
|
44
47
|
**Parameters:**
|
|
48
|
+
|
|
45
49
|
- `blockerId: string` - Unique identifier for the blocker
|
|
46
50
|
- `config: BlockerConfig` - Configuration object
|
|
47
51
|
- `scope?: string | string[]` - Scope(s) to block (default: "global")
|
|
@@ -53,10 +57,10 @@ Automatically adds a blocker when the component mounts and removes it on unmount
|
|
|
53
57
|
|
|
54
58
|
```jsx
|
|
55
59
|
function MyComponent() {
|
|
56
|
-
useBlocker(
|
|
57
|
-
scope:
|
|
58
|
-
reason:
|
|
59
|
-
priority: 10
|
|
60
|
+
useBlocker("my-blocker", {
|
|
61
|
+
scope: "form",
|
|
62
|
+
reason: "Form is saving",
|
|
63
|
+
priority: 10,
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
return <div>Content</div>;
|
|
@@ -68,6 +72,7 @@ function MyComponent() {
|
|
|
68
72
|
Checks if a specific scope (or scopes) is currently blocked.
|
|
69
73
|
|
|
70
74
|
**Parameters:**
|
|
75
|
+
|
|
71
76
|
- `scope?: string | string[]` - Scope(s) to check (default: "global")
|
|
72
77
|
|
|
73
78
|
**Returns:** `boolean` - Whether the scope is blocked
|
|
@@ -76,13 +81,9 @@ Checks if a specific scope (or scopes) is currently blocked.
|
|
|
76
81
|
|
|
77
82
|
```jsx
|
|
78
83
|
function SubmitButton() {
|
|
79
|
-
const isFormBlocked = useIsBlocked(
|
|
84
|
+
const isFormBlocked = useIsBlocked("form");
|
|
80
85
|
|
|
81
|
-
return
|
|
82
|
-
<button disabled={isFormBlocked}>
|
|
83
|
-
Submit
|
|
84
|
-
</button>
|
|
85
|
-
);
|
|
86
|
+
return <button disabled={isFormBlocked}>Submit</button>;
|
|
86
87
|
}
|
|
87
88
|
```
|
|
88
89
|
|
|
@@ -91,6 +92,7 @@ function SubmitButton() {
|
|
|
91
92
|
Wraps an async function with automatic blocking/unblocking.
|
|
92
93
|
|
|
93
94
|
**Parameters:**
|
|
95
|
+
|
|
94
96
|
- `actionId: string` - Identifier for the action
|
|
95
97
|
- `scope?: string | string[]` - Scope(s) to block during execution
|
|
96
98
|
|
|
@@ -100,7 +102,7 @@ Wraps an async function with automatic blocking/unblocking.
|
|
|
100
102
|
|
|
101
103
|
```jsx
|
|
102
104
|
function MyComponent() {
|
|
103
|
-
const executeWithBlocking = useAsyncAction(
|
|
105
|
+
const executeWithBlocking = useAsyncAction("save-data", "form");
|
|
104
106
|
|
|
105
107
|
const handleSave = async () => {
|
|
106
108
|
await executeWithBlocking(async () => {
|
|
@@ -112,6 +114,138 @@ function MyComponent() {
|
|
|
112
114
|
}
|
|
113
115
|
```
|
|
114
116
|
|
|
117
|
+
#### `useConfirmableBlocker(blockerId, config, options)`
|
|
118
|
+
|
|
119
|
+
Creates a blocker that requires user confirmation before being removed.
|
|
120
|
+
|
|
121
|
+
**Parameters:**
|
|
122
|
+
|
|
123
|
+
- `blockerId: string` - Unique identifier for the blocker
|
|
124
|
+
- `config: BlockerConfig` - Configuration object
|
|
125
|
+
- `options: ConfirmableBlockerOptions`
|
|
126
|
+
- `enabled?: boolean` - Whether the blocker is active (default: true)
|
|
127
|
+
- `confirmMessage?: string` - Message to show in confirmation dialog
|
|
128
|
+
- `onConfirm?: () => void | Promise<void>` - Callback when user confirms
|
|
129
|
+
|
|
130
|
+
**Returns:**
|
|
131
|
+
|
|
132
|
+
- `isBlocking: boolean` - Whether the blocker is currently active
|
|
133
|
+
- `cancel: () => void` - Manually remove the blocker without confirmation
|
|
134
|
+
- `requestRemoval: () => Promise<boolean>` - Request removal with confirmation
|
|
135
|
+
|
|
136
|
+
**Example:**
|
|
137
|
+
|
|
138
|
+
```jsx
|
|
139
|
+
function UnsavedChangesGuard() {
|
|
140
|
+
const { isBlocking, requestRemoval } = useConfirmableBlocker(
|
|
141
|
+
"unsaved-changes",
|
|
142
|
+
{
|
|
143
|
+
scope: "navigation",
|
|
144
|
+
reason: "Unsaved changes",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
enabled: hasUnsavedChanges,
|
|
148
|
+
confirmMessage: "You have unsaved changes. Are you sure you want to leave?",
|
|
149
|
+
onConfirm: async () => {
|
|
150
|
+
await discardChanges();
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div>
|
|
157
|
+
{isBlocking && <UnsavedIndicator />}
|
|
158
|
+
<button onClick={requestRemoval}>Leave</button>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `useScheduledBlocker(blockerId, config)`
|
|
165
|
+
|
|
166
|
+
Blocks UI during a scheduled time period or maintenance window.
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
|
|
170
|
+
- `blockerId: string` - Unique identifier for the blocker
|
|
171
|
+
- `config: ScheduledBlockerConfig`
|
|
172
|
+
- `schedule: BlockingSchedule`
|
|
173
|
+
- `start: string | Date | number` - Start time (ISO string, Date, or timestamp)
|
|
174
|
+
- `end?: string | Date | number` - End time (optional)
|
|
175
|
+
- `duration?: number` - Duration in milliseconds (takes precedence over end)
|
|
176
|
+
- `onScheduleStart?: () => void` - Callback when blocking starts
|
|
177
|
+
- `onScheduleEnd?: () => void` - Callback when blocking ends
|
|
178
|
+
- Plus all `BlockerConfig` properties (scope, reason, priority)
|
|
179
|
+
|
|
180
|
+
**Example:**
|
|
181
|
+
|
|
182
|
+
```jsx
|
|
183
|
+
function MaintenanceWindow() {
|
|
184
|
+
useScheduledBlocker("maintenance", {
|
|
185
|
+
scope: "global",
|
|
186
|
+
reason: "Scheduled maintenance",
|
|
187
|
+
priority: 1000,
|
|
188
|
+
schedule: {
|
|
189
|
+
start: "2024-01-15T02:00:00Z",
|
|
190
|
+
duration: 3600000, // 1 hour in milliseconds
|
|
191
|
+
},
|
|
192
|
+
onScheduleStart: () => {
|
|
193
|
+
console.log("Maintenance started");
|
|
194
|
+
},
|
|
195
|
+
onScheduleEnd: () => {
|
|
196
|
+
console.log("Maintenance completed");
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return <div>App content</div>;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### `useConditionalBlocker(blockerId, config)`
|
|
205
|
+
|
|
206
|
+
Periodically checks a condition and blocks/unblocks based on the result.
|
|
207
|
+
|
|
208
|
+
**Parameters:**
|
|
209
|
+
|
|
210
|
+
- `blockerId: string` - Unique identifier for the blocker
|
|
211
|
+
- `config: ConditionalBlockerConfig<TState>`
|
|
212
|
+
- `scope: string | string[]` - Required scope(s) to block
|
|
213
|
+
- `condition: (state?: TState) => boolean` - Function that determines if blocking should be active
|
|
214
|
+
- `checkInterval?: number` - How often to check the condition in ms (default: 1000)
|
|
215
|
+
- `state?: TState` - Optional state to pass to the condition function
|
|
216
|
+
- Plus all other `BlockerConfig` properties (reason, priority)
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
|
|
220
|
+
```jsx
|
|
221
|
+
function NetworkStatusBlocker() {
|
|
222
|
+
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const handleOnline = () => setIsOnline(true);
|
|
226
|
+
const handleOffline = () => setIsOnline(false);
|
|
227
|
+
|
|
228
|
+
window.addEventListener("online", handleOnline);
|
|
229
|
+
window.addEventListener("offline", handleOffline);
|
|
230
|
+
|
|
231
|
+
return () => {
|
|
232
|
+
window.removeEventListener("online", handleOnline);
|
|
233
|
+
window.removeEventListener("offline", handleOffline);
|
|
234
|
+
};
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
useConditionalBlocker("network-check", {
|
|
238
|
+
scope: ["form", "navigation"],
|
|
239
|
+
reason: "No network connection",
|
|
240
|
+
priority: 100,
|
|
241
|
+
condition: () => !isOnline,
|
|
242
|
+
checkInterval: 2000,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return <div>App content</div>;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
115
249
|
### Store
|
|
116
250
|
|
|
117
251
|
#### `useUIBlockingStore`
|
|
@@ -119,8 +253,10 @@ function MyComponent() {
|
|
|
119
253
|
Direct access to the Zustand store for advanced use cases.
|
|
120
254
|
|
|
121
255
|
**Methods:**
|
|
256
|
+
|
|
122
257
|
- `addBlocker(id, config)` - Manually add a blocker
|
|
123
258
|
- `removeBlocker(id)` - Manually remove a blocker
|
|
259
|
+
- `updateBlocker(id, config)` - Update an existing blocker
|
|
124
260
|
- `isBlocked(scope)` - Check if scope is blocked
|
|
125
261
|
- `getBlockingInfo(scope)` - Get detailed blocking information
|
|
126
262
|
- `clearAllBlockers()` - Remove all blockers
|
|
@@ -129,42 +265,185 @@ Direct access to the Zustand store for advanced use cases.
|
|
|
129
265
|
**Example:**
|
|
130
266
|
|
|
131
267
|
```jsx
|
|
132
|
-
import { useUIBlockingStore } from
|
|
268
|
+
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
|
|
133
269
|
|
|
134
270
|
function AdvancedComponent() {
|
|
135
|
-
const { addBlocker, removeBlocker } = useUIBlockingStore();
|
|
271
|
+
const { addBlocker, removeBlocker, updateBlocker } = useUIBlockingStore();
|
|
136
272
|
|
|
137
273
|
const startBlocking = () => {
|
|
138
|
-
addBlocker(
|
|
139
|
-
scope: [
|
|
140
|
-
reason:
|
|
141
|
-
priority: 100
|
|
274
|
+
addBlocker("custom-blocker", {
|
|
275
|
+
scope: ["form", "navigation"],
|
|
276
|
+
reason: "Critical operation in progress",
|
|
277
|
+
priority: 100,
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const updatePriority = () => {
|
|
282
|
+
updateBlocker("custom-blocker", {
|
|
283
|
+
priority: 200,
|
|
142
284
|
});
|
|
143
285
|
};
|
|
144
286
|
|
|
145
287
|
const stopBlocking = () => {
|
|
146
|
-
removeBlocker(
|
|
288
|
+
removeBlocker("custom-blocker");
|
|
147
289
|
};
|
|
148
290
|
|
|
149
291
|
return (
|
|
150
292
|
<div>
|
|
151
293
|
<button onClick={startBlocking}>Start</button>
|
|
294
|
+
<button onClick={updatePriority}>Increase Priority</button>
|
|
152
295
|
<button onClick={stopBlocking}>Stop</button>
|
|
153
296
|
</div>
|
|
154
297
|
);
|
|
155
298
|
}
|
|
156
299
|
```
|
|
157
300
|
|
|
301
|
+
## Middleware System
|
|
302
|
+
|
|
303
|
+
The library includes a powerful middleware system that allows you to hook into blocker lifecycle events for analytics, logging, and performance monitoring.
|
|
304
|
+
|
|
305
|
+
### Built-in Middleware
|
|
306
|
+
|
|
307
|
+
#### Analytics Middleware
|
|
308
|
+
|
|
309
|
+
Track blocker events with your analytics provider (Google Analytics, Mixpanel, Amplitude, or custom).
|
|
310
|
+
|
|
311
|
+
```jsx
|
|
312
|
+
import { configureMiddleware, analyticsMiddleware } from "@okyrychenko-dev/react-action-guard";
|
|
313
|
+
|
|
314
|
+
// Google Analytics
|
|
315
|
+
configureMiddleware([analyticsMiddleware({ provider: "ga" })]);
|
|
316
|
+
|
|
317
|
+
// Mixpanel
|
|
318
|
+
configureMiddleware([analyticsMiddleware({ provider: "mixpanel" })]);
|
|
319
|
+
|
|
320
|
+
// Amplitude
|
|
321
|
+
configureMiddleware([analyticsMiddleware({ provider: "amplitude" })]);
|
|
322
|
+
|
|
323
|
+
// Custom analytics
|
|
324
|
+
configureMiddleware([
|
|
325
|
+
analyticsMiddleware({
|
|
326
|
+
track: (event, data) => {
|
|
327
|
+
myAnalytics.track(event, data);
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
]);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### Logger Middleware
|
|
334
|
+
|
|
335
|
+
Log blocker lifecycle events to the console for debugging.
|
|
336
|
+
|
|
337
|
+
```jsx
|
|
338
|
+
import { configureMiddleware, loggerMiddleware } from "@okyrychenko-dev/react-action-guard";
|
|
339
|
+
|
|
340
|
+
configureMiddleware([loggerMiddleware]);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### Performance Middleware
|
|
344
|
+
|
|
345
|
+
Monitor blocker performance and detect slow operations.
|
|
346
|
+
|
|
347
|
+
```jsx
|
|
348
|
+
import { configureMiddleware, performanceMiddleware } from "@okyrychenko-dev/react-action-guard";
|
|
349
|
+
|
|
350
|
+
configureMiddleware([
|
|
351
|
+
performanceMiddleware({
|
|
352
|
+
onSlowBlock: (blockerId, duration) => {
|
|
353
|
+
console.warn(`Blocker ${blockerId} was active for ${duration}ms`);
|
|
354
|
+
},
|
|
355
|
+
slowBlockThreshold: 5000, // 5 seconds
|
|
356
|
+
}),
|
|
357
|
+
]);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Custom Middleware
|
|
361
|
+
|
|
362
|
+
Create your own middleware to handle blocker events:
|
|
363
|
+
|
|
364
|
+
```jsx
|
|
365
|
+
import { configureMiddleware } from "@okyrychenko-dev/react-action-guard";
|
|
366
|
+
|
|
367
|
+
const myCustomMiddleware = (context) => {
|
|
368
|
+
const { action, blockerId, config, timestamp } = context;
|
|
369
|
+
|
|
370
|
+
if (action === "add") {
|
|
371
|
+
console.log(`Blocker added: ${blockerId}`, config);
|
|
372
|
+
} else if (action === "remove") {
|
|
373
|
+
console.log(`Blocker removed: ${blockerId}`);
|
|
374
|
+
} else if (action === "update") {
|
|
375
|
+
console.log(`Blocker updated: ${blockerId}`, config);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
configureMiddleware([myCustomMiddleware]);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Combining Middleware
|
|
383
|
+
|
|
384
|
+
You can combine multiple middleware for comprehensive monitoring:
|
|
385
|
+
|
|
386
|
+
```jsx
|
|
387
|
+
import {
|
|
388
|
+
configureMiddleware,
|
|
389
|
+
analyticsMiddleware,
|
|
390
|
+
loggerMiddleware,
|
|
391
|
+
performanceMiddleware,
|
|
392
|
+
} from "@okyrychenko-dev/react-action-guard";
|
|
393
|
+
|
|
394
|
+
configureMiddleware([
|
|
395
|
+
loggerMiddleware,
|
|
396
|
+
analyticsMiddleware({ provider: "ga" }),
|
|
397
|
+
performanceMiddleware({
|
|
398
|
+
slowBlockThreshold: 3000,
|
|
399
|
+
onSlowBlock: (blockerId, duration) => {
|
|
400
|
+
// Send to error tracking service
|
|
401
|
+
errorTracker.captureMessage(`Slow blocker: ${blockerId}`, {
|
|
402
|
+
duration,
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
406
|
+
]);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Tree Shaking
|
|
410
|
+
|
|
411
|
+
The library is fully tree-shakeable. Import only the features you need to keep your bundle size small:
|
|
412
|
+
|
|
413
|
+
```jsx
|
|
414
|
+
// Only imports the hook you need
|
|
415
|
+
import { useBlocker } from "@okyrychenko-dev/react-action-guard";
|
|
416
|
+
|
|
417
|
+
// Middleware is not included unless you import it
|
|
418
|
+
import { configureMiddleware, analyticsMiddleware } from "@okyrychenko-dev/react-action-guard";
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The package is configured with `"sideEffects": false`, allowing modern bundlers (Webpack, Rollup, Vite) to eliminate unused code automatically.
|
|
422
|
+
|
|
158
423
|
## TypeScript
|
|
159
424
|
|
|
160
425
|
The package is written in TypeScript and includes full type definitions.
|
|
161
426
|
|
|
162
427
|
```typescript
|
|
163
428
|
import type {
|
|
429
|
+
// Core types
|
|
164
430
|
BlockerConfig,
|
|
165
431
|
BlockerInfo,
|
|
166
|
-
UIBlockingState
|
|
167
|
-
|
|
432
|
+
UIBlockingState,
|
|
433
|
+
|
|
434
|
+
// Hook types
|
|
435
|
+
ConfirmableBlockerOptions,
|
|
436
|
+
ScheduledBlockerConfig,
|
|
437
|
+
ConditionalBlockerConfig,
|
|
438
|
+
BlockingSchedule,
|
|
439
|
+
|
|
440
|
+
// Middleware types
|
|
441
|
+
Middleware,
|
|
442
|
+
MiddlewareContext,
|
|
443
|
+
AnalyticsConfig,
|
|
444
|
+
AnalyticsProvider,
|
|
445
|
+
PerformanceMiddlewareConfig,
|
|
446
|
+
} from "@okyrychenko-dev/react-action-guard";
|
|
168
447
|
```
|
|
169
448
|
|
|
170
449
|
## Use Cases
|
|
@@ -175,21 +454,27 @@ import type {
|
|
|
175
454
|
function DataLoader() {
|
|
176
455
|
const [isLoading, setIsLoading] = useState(false);
|
|
177
456
|
|
|
178
|
-
useBlocker(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
457
|
+
useBlocker(
|
|
458
|
+
"data-loader",
|
|
459
|
+
{
|
|
460
|
+
scope: "content",
|
|
461
|
+
reason: "Loading data",
|
|
462
|
+
},
|
|
463
|
+
isLoading
|
|
464
|
+
);
|
|
182
465
|
|
|
183
466
|
// ... rest of component
|
|
184
467
|
}
|
|
185
468
|
```
|
|
186
469
|
|
|
187
|
-
### Form Submission
|
|
470
|
+
### Form Submission with Analytics
|
|
188
471
|
|
|
189
472
|
```jsx
|
|
473
|
+
import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
|
|
474
|
+
|
|
190
475
|
function UserForm() {
|
|
191
|
-
const executeWithBlocking = useAsyncAction(
|
|
192
|
-
const isBlocked = useIsBlocked(
|
|
476
|
+
const executeWithBlocking = useAsyncAction("submit-form", "form");
|
|
477
|
+
const isBlocked = useIsBlocked("form");
|
|
193
478
|
|
|
194
479
|
const handleSubmit = async (data) => {
|
|
195
480
|
await executeWithBlocking(async () => {
|
|
@@ -206,11 +491,51 @@ function UserForm() {
|
|
|
206
491
|
}
|
|
207
492
|
```
|
|
208
493
|
|
|
494
|
+
### Unsaved Changes Protection
|
|
495
|
+
|
|
496
|
+
```jsx
|
|
497
|
+
import { useConfirmableBlocker } from "@okyrychenko-dev/react-action-guard";
|
|
498
|
+
|
|
499
|
+
function FormWithUnsavedWarning() {
|
|
500
|
+
const [formData, setFormData] = useState({});
|
|
501
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
502
|
+
|
|
503
|
+
const { requestRemoval } = useConfirmableBlocker(
|
|
504
|
+
"unsaved-form",
|
|
505
|
+
{
|
|
506
|
+
scope: "navigation",
|
|
507
|
+
reason: "Unsaved form data",
|
|
508
|
+
priority: 100,
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
enabled: hasChanges,
|
|
512
|
+
confirmMessage: "You have unsaved changes. Discard them?",
|
|
513
|
+
onConfirm: () => {
|
|
514
|
+
setFormData({});
|
|
515
|
+
setHasChanges(false);
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<form>
|
|
522
|
+
<input
|
|
523
|
+
onChange={(e) => {
|
|
524
|
+
setFormData({ ...formData, name: e.target.value });
|
|
525
|
+
setHasChanges(true);
|
|
526
|
+
}}
|
|
527
|
+
/>
|
|
528
|
+
<button onClick={requestRemoval}>Cancel</button>
|
|
529
|
+
</form>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
209
534
|
### Global Loading Overlay
|
|
210
535
|
|
|
211
536
|
```jsx
|
|
212
537
|
function App() {
|
|
213
|
-
const isGloballyBlocked = useIsBlocked(
|
|
538
|
+
const isGloballyBlocked = useIsBlocked("global");
|
|
214
539
|
|
|
215
540
|
return (
|
|
216
541
|
<div>
|
|
@@ -221,22 +546,156 @@ function App() {
|
|
|
221
546
|
}
|
|
222
547
|
```
|
|
223
548
|
|
|
549
|
+
### Multi-Step Process with Priority
|
|
550
|
+
|
|
551
|
+
```jsx
|
|
552
|
+
function MultiStepWizard() {
|
|
553
|
+
const [step, setStep] = useState(1);
|
|
554
|
+
|
|
555
|
+
// Higher priority for payment step
|
|
556
|
+
useBlocker(
|
|
557
|
+
"payment-step",
|
|
558
|
+
{
|
|
559
|
+
scope: ["navigation", "form"],
|
|
560
|
+
reason: "Processing payment",
|
|
561
|
+
priority: 100,
|
|
562
|
+
},
|
|
563
|
+
step === 3
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
// Lower priority for other steps
|
|
567
|
+
useBlocker(
|
|
568
|
+
"wizard-step",
|
|
569
|
+
{
|
|
570
|
+
scope: "navigation",
|
|
571
|
+
reason: "Wizard in progress",
|
|
572
|
+
priority: 50,
|
|
573
|
+
},
|
|
574
|
+
step < 3
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
return <div>Step {step}</div>;
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Scheduled Maintenance Window
|
|
582
|
+
|
|
583
|
+
```jsx
|
|
584
|
+
import { useScheduledBlocker } from "@okyrychenko-dev/react-action-guard";
|
|
585
|
+
|
|
586
|
+
function App() {
|
|
587
|
+
useScheduledBlocker("weekly-maintenance", {
|
|
588
|
+
scope: "global",
|
|
589
|
+
reason: "Weekly system maintenance",
|
|
590
|
+
priority: 500,
|
|
591
|
+
schedule: {
|
|
592
|
+
start: new Date("2024-01-21T03:00:00Z"),
|
|
593
|
+
duration: 1800000, // 30 minutes
|
|
594
|
+
},
|
|
595
|
+
onScheduleStart: () => {
|
|
596
|
+
showNotification("System maintenance in progress");
|
|
597
|
+
},
|
|
598
|
+
onScheduleEnd: () => {
|
|
599
|
+
showNotification("Maintenance completed");
|
|
600
|
+
window.location.reload();
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return <YourApp />;
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Dynamic Blocking Based on State
|
|
609
|
+
|
|
610
|
+
```jsx
|
|
611
|
+
import { useConditionalBlocker } from "@okyrychenko-dev/react-action-guard";
|
|
612
|
+
|
|
613
|
+
function StorageQuotaGuard() {
|
|
614
|
+
const [storageUsed, setStorageUsed] = useState(0);
|
|
615
|
+
const STORAGE_LIMIT = 1000000; // 1MB
|
|
616
|
+
|
|
617
|
+
useConditionalBlocker("storage-limit", {
|
|
618
|
+
scope: ["upload", "save"],
|
|
619
|
+
reason: "Storage quota exceeded",
|
|
620
|
+
priority: 200,
|
|
621
|
+
condition: () => storageUsed > STORAGE_LIMIT,
|
|
622
|
+
state: storageUsed,
|
|
623
|
+
checkInterval: 5000, // Check every 5 seconds
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return (
|
|
627
|
+
<div>
|
|
628
|
+
<p>
|
|
629
|
+
Storage used: {storageUsed} / {STORAGE_LIMIT} bytes
|
|
630
|
+
</p>
|
|
631
|
+
<UploadButton />
|
|
632
|
+
</div>
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
224
637
|
## Development
|
|
225
638
|
|
|
226
639
|
```bash
|
|
227
640
|
# Install dependencies
|
|
228
641
|
npm install
|
|
229
642
|
|
|
643
|
+
# Run tests
|
|
644
|
+
npm run test
|
|
645
|
+
|
|
646
|
+
# Run tests with coverage
|
|
647
|
+
npm run test:coverage
|
|
648
|
+
|
|
230
649
|
# Build the package
|
|
231
650
|
npm run build
|
|
232
651
|
|
|
233
652
|
# Type checking
|
|
234
653
|
npm run typecheck
|
|
235
654
|
|
|
655
|
+
# Lint code
|
|
656
|
+
npm run lint
|
|
657
|
+
|
|
658
|
+
# Fix lint errors
|
|
659
|
+
npm run lint:fix
|
|
660
|
+
|
|
661
|
+
# Format code
|
|
662
|
+
npm run format
|
|
663
|
+
|
|
236
664
|
# Watch mode for development
|
|
237
665
|
npm run dev
|
|
238
666
|
```
|
|
239
667
|
|
|
668
|
+
## Contributing
|
|
669
|
+
|
|
670
|
+
Contributions are welcome! Please ensure:
|
|
671
|
+
|
|
672
|
+
1. All tests pass (`npm run test`)
|
|
673
|
+
2. Code is properly typed (`npm run typecheck`)
|
|
674
|
+
3. Linting passes (`npm run lint`)
|
|
675
|
+
4. Code is formatted (`npm run format`)
|
|
676
|
+
|
|
677
|
+
## Changelog
|
|
678
|
+
|
|
679
|
+
### v0.2.0
|
|
680
|
+
|
|
681
|
+
- Added middleware system (analytics, logging, performance)
|
|
682
|
+
- Added advanced hooks:
|
|
683
|
+
- `useConfirmableBlocker` - blockers with user confirmation
|
|
684
|
+
- `useScheduledBlocker` - time-based blocking for maintenance windows
|
|
685
|
+
- `useConditionalBlocker` - dynamic blocking based on conditions
|
|
686
|
+
- Added `updateBlocker` method to store
|
|
687
|
+
- Improved TypeScript types with discriminated unions
|
|
688
|
+
- Added tree-shaking support
|
|
689
|
+
- Added support for Amplitude analytics provider
|
|
690
|
+
- Performance improvements and code refactoring
|
|
691
|
+
|
|
692
|
+
### v0.1.0
|
|
693
|
+
|
|
694
|
+
- Initial release
|
|
695
|
+
- Basic blocking system with priorities and scopes
|
|
696
|
+
- `useBlocker`, `useIsBlocked`, `useAsyncAction` hooks
|
|
697
|
+
- Zustand-based state management
|
|
698
|
+
|
|
240
699
|
## License
|
|
241
700
|
|
|
242
|
-
MIT ©
|
|
701
|
+
MIT © Olexii Kyrychenko
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okyrychenko-dev/react-action-guard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"loading",
|
|
45
45
|
"async-operations"
|
|
46
46
|
],
|
|
47
|
-
"author": "
|
|
47
|
+
"author": "Olexii Kyrychenko alexey.kirichenko@gmail.com",
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"repository": {
|
|
50
50
|
"type": "git",
|