@okyrychenko-dev/react-action-guard 0.3.3 → 0.5.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/LICENSE +1 -1
- package/README.md +255 -62
- package/dist/index.cjs +131 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -3
- package/dist/index.d.ts +139 -3
- package/dist/index.js +124 -23
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
- Priority-based blocking system
|
|
12
12
|
- Scoped blocking (global, specific areas, or multiple scopes)
|
|
13
13
|
- Automatic cleanup on unmount
|
|
14
|
+
- **Timeout mechanism** - auto-remove blockers after specified time
|
|
15
|
+
- **Provider pattern** - isolated stores for SSR, testing, and micro-frontends
|
|
14
16
|
- Advanced hooks for different use cases:
|
|
15
17
|
- Confirmable blockers with custom dialogs
|
|
16
18
|
- Scheduled blocking for maintenance windows
|
|
17
19
|
- Conditional blocking based on application state
|
|
18
|
-
- Async action wrapping
|
|
20
|
+
- Async action wrapping with timeout support
|
|
19
21
|
- Advanced middleware system for analytics, logging, and performance monitoring
|
|
20
|
-
- TypeScript support with full type safety
|
|
22
|
+
- TypeScript support with full type safety (including optional type-safe scopes)
|
|
21
23
|
- Built on Zustand for efficient state management
|
|
22
24
|
- Tree-shakeable - import only what you need
|
|
23
25
|
- Hooks-based API
|
|
@@ -34,8 +36,8 @@ pnpm add @okyrychenko-dev/react-action-guard zustand
|
|
|
34
36
|
|
|
35
37
|
This package requires the following peer dependencies:
|
|
36
38
|
|
|
37
|
-
- [React](https://react.dev/) ^17.0.0 || ^18.0.0
|
|
38
|
-
- [Zustand](https://zustand-demo.pmnd.rs/) - State management library
|
|
39
|
+
- [React](https://react.dev/) ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
40
|
+
- [Zustand](https://zustand-demo.pmnd.rs/) ^4.5.7 || ^5.0.0 - State management library
|
|
39
41
|
|
|
40
42
|
## Quick Start
|
|
41
43
|
|
|
@@ -51,7 +53,7 @@ function MyComponent() {
|
|
|
51
53
|
|
|
52
54
|
## Documentation
|
|
53
55
|
|
|
54
|
-
📚 **
|
|
56
|
+
📚 **Interactive Storybook Documentation** - Run locally to explore live examples and detailed guides for all hooks
|
|
55
57
|
|
|
56
58
|
To run Storybook locally:
|
|
57
59
|
|
|
@@ -74,6 +76,8 @@ Automatically adds a blocker when the component mounts and removes it on unmount
|
|
|
74
76
|
- `scope?: string | string[]` - Scope(s) to block (default: "global")
|
|
75
77
|
- `reason?: string` - Reason for blocking (for debugging)
|
|
76
78
|
- `priority?: number` - Priority level (higher = more important)
|
|
79
|
+
- `timeout?: number` - Auto-remove after N milliseconds
|
|
80
|
+
- `onTimeout?: (blockerId: string) => void` - Callback when auto-removed
|
|
77
81
|
- `isActive?: boolean` - Whether the blocker is active (default: true)
|
|
78
82
|
|
|
79
83
|
**Example:**
|
|
@@ -88,6 +92,33 @@ function MyComponent() {
|
|
|
88
92
|
|
|
89
93
|
return <div>Content</div>;
|
|
90
94
|
}
|
|
95
|
+
|
|
96
|
+
// With timeout - auto-removes after 30 seconds
|
|
97
|
+
function SaveButton() {
|
|
98
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
99
|
+
|
|
100
|
+
useBlocker("save-operation", {
|
|
101
|
+
scope: "form",
|
|
102
|
+
reason: "Saving...",
|
|
103
|
+
timeout: 30000,
|
|
104
|
+
onTimeout: (id) => {
|
|
105
|
+
console.warn(`Operation ${id} timed out`);
|
|
106
|
+
showNotification("Operation timed out");
|
|
107
|
+
setIsSaving(false);
|
|
108
|
+
},
|
|
109
|
+
}, isSaving);
|
|
110
|
+
|
|
111
|
+
const handleSave = async () => {
|
|
112
|
+
setIsSaving(true);
|
|
113
|
+
try {
|
|
114
|
+
await saveData();
|
|
115
|
+
} finally {
|
|
116
|
+
setIsSaving(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return <button onClick={handleSave} disabled={isSaving}>Save</button>;
|
|
121
|
+
}
|
|
91
122
|
```
|
|
92
123
|
|
|
93
124
|
#### `useIsBlocked(scope)`
|
|
@@ -147,7 +178,7 @@ function CheckoutButton() {
|
|
|
147
178
|
}
|
|
148
179
|
```
|
|
149
180
|
|
|
150
|
-
#### `useAsyncAction(actionId, scope)`
|
|
181
|
+
#### `useAsyncAction(actionId, scope, options)`
|
|
151
182
|
|
|
152
183
|
Wraps an async function with automatic blocking/unblocking.
|
|
153
184
|
|
|
@@ -155,6 +186,9 @@ Wraps an async function with automatic blocking/unblocking.
|
|
|
155
186
|
|
|
156
187
|
- `actionId: string` - Identifier for the action
|
|
157
188
|
- `scope?: string | string[]` - Scope(s) to block during execution
|
|
189
|
+
- `options?: UseAsyncActionOptions` - Optional configuration
|
|
190
|
+
- `timeout?: number` - Auto-remove blocker after N milliseconds
|
|
191
|
+
- `onTimeout?: (blockerId: string) => void` - Callback when timed out
|
|
158
192
|
|
|
159
193
|
**Returns:** `(asyncFn: () => Promise<T>) => Promise<T>` - Function wrapper
|
|
160
194
|
|
|
@@ -172,51 +206,85 @@ function MyComponent() {
|
|
|
172
206
|
|
|
173
207
|
return <button onClick={handleSave}>Save</button>;
|
|
174
208
|
}
|
|
209
|
+
|
|
210
|
+
// With timeout - prevents infinite blocking if operation hangs
|
|
211
|
+
function ApiComponent() {
|
|
212
|
+
const execute = useAsyncAction("api-call", "global", {
|
|
213
|
+
timeout: 60000, // 1 minute timeout
|
|
214
|
+
onTimeout: (id) => {
|
|
215
|
+
showError("Request timed out. Please try again.");
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const fetchData = () => execute(async () => {
|
|
220
|
+
const response = await fetch("/api/data");
|
|
221
|
+
return response.json();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return <button onClick={fetchData}>Fetch Data</button>;
|
|
225
|
+
}
|
|
175
226
|
```
|
|
176
227
|
|
|
177
|
-
#### `useConfirmableBlocker(blockerId, config
|
|
228
|
+
#### `useConfirmableBlocker(blockerId, config)`
|
|
178
229
|
|
|
179
|
-
Creates a
|
|
230
|
+
Creates a confirmable action with UI blocking while the dialog is open or the action is running.
|
|
180
231
|
|
|
181
232
|
**Parameters:**
|
|
182
233
|
|
|
183
234
|
- `blockerId: string` - Unique identifier for the blocker
|
|
184
|
-
- `config:
|
|
185
|
-
- `
|
|
186
|
-
- `
|
|
187
|
-
- `
|
|
188
|
-
- `
|
|
235
|
+
- `config: ConfirmableBlockerConfig` - Configuration object
|
|
236
|
+
- `confirmMessage: string` - Message to show in confirmation dialog
|
|
237
|
+
- `confirmTitle?: string` - Dialog title (default: "Confirm Action")
|
|
238
|
+
- `confirmButtonText?: string` - Confirm button label (default: "Confirm")
|
|
239
|
+
- `cancelButtonText?: string` - Cancel button label (default: "Cancel")
|
|
240
|
+
- `onConfirm: () => void | Promise<void>` - Callback when user confirms
|
|
241
|
+
- `onCancel?: () => void` - Callback when user cancels
|
|
242
|
+
- Plus all `BlockerConfig` properties (scope, reason, priority, timeout)
|
|
189
243
|
|
|
190
244
|
**Returns:**
|
|
191
245
|
|
|
192
|
-
- `
|
|
193
|
-
- `
|
|
194
|
-
- `
|
|
246
|
+
- `execute: () => void` - Opens the confirmation dialog
|
|
247
|
+
- `isDialogOpen: boolean` - Whether the dialog is open
|
|
248
|
+
- `isExecuting: boolean` - Whether the confirm action is running
|
|
249
|
+
- `confirmConfig: { title, message, confirmText, cancelText }` - UI-ready dialog config
|
|
250
|
+
- `onConfirm: () => Promise<void>` - Confirm handler to wire to your dialog
|
|
251
|
+
- `onCancel: () => void` - Cancel handler to wire to your dialog
|
|
195
252
|
|
|
196
253
|
**Example:**
|
|
197
254
|
|
|
198
255
|
```jsx
|
|
199
|
-
function UnsavedChangesGuard() {
|
|
200
|
-
const {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
256
|
+
function UnsavedChangesGuard({ discardChanges }) {
|
|
257
|
+
const {
|
|
258
|
+
execute,
|
|
259
|
+
isDialogOpen,
|
|
260
|
+
isExecuting,
|
|
261
|
+
confirmConfig,
|
|
262
|
+
onConfirm,
|
|
263
|
+
onCancel,
|
|
264
|
+
} = useConfirmableBlocker("unsaved-changes", {
|
|
265
|
+
scope: "navigation",
|
|
266
|
+
reason: "Unsaved changes",
|
|
267
|
+
confirmMessage: "You have unsaved changes. Are you sure you want to leave?",
|
|
268
|
+
onConfirm: async () => {
|
|
269
|
+
await discardChanges();
|
|
205
270
|
},
|
|
206
|
-
|
|
207
|
-
enabled: hasUnsavedChanges,
|
|
208
|
-
confirmMessage: "You have unsaved changes. Are you sure you want to leave?",
|
|
209
|
-
onConfirm: async () => {
|
|
210
|
-
await discardChanges();
|
|
211
|
-
},
|
|
212
|
-
}
|
|
213
|
-
);
|
|
271
|
+
});
|
|
214
272
|
|
|
215
273
|
return (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
274
|
+
<>
|
|
275
|
+
<button onClick={execute}>Leave</button>
|
|
276
|
+
{isDialogOpen && (
|
|
277
|
+
<ConfirmDialog
|
|
278
|
+
title={confirmConfig.title}
|
|
279
|
+
message={confirmConfig.message}
|
|
280
|
+
confirmText={confirmConfig.confirmText}
|
|
281
|
+
cancelText={confirmConfig.cancelText}
|
|
282
|
+
onConfirm={onConfirm}
|
|
283
|
+
onCancel={onCancel}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
{isExecuting && <LoadingOverlay message="Processing..." />}
|
|
287
|
+
</>
|
|
220
288
|
);
|
|
221
289
|
}
|
|
222
290
|
```
|
|
@@ -306,21 +374,72 @@ function NetworkStatusBlocker() {
|
|
|
306
374
|
}
|
|
307
375
|
```
|
|
308
376
|
|
|
377
|
+
### Provider (Optional)
|
|
378
|
+
|
|
379
|
+
#### `UIBlockingProvider`
|
|
380
|
+
|
|
381
|
+
Provides an isolated store instance for SSR, testing, or micro-frontends. Without the provider, hooks use a global store.
|
|
382
|
+
|
|
383
|
+
**Props:**
|
|
384
|
+
|
|
385
|
+
- `children: ReactNode` - Child components
|
|
386
|
+
- `enableDevtools?: boolean` - Enable Redux DevTools (default: true in development)
|
|
387
|
+
- `devtoolsName?: string` - Name for DevTools (default: "UIBlocking")
|
|
388
|
+
- `middlewares?: Middleware[]` - Initial middlewares to register
|
|
389
|
+
|
|
390
|
+
**Example:**
|
|
391
|
+
|
|
392
|
+
```jsx
|
|
393
|
+
import { UIBlockingProvider } from "@okyrychenko-dev/react-action-guard";
|
|
394
|
+
|
|
395
|
+
// SSR - each request gets isolated state
|
|
396
|
+
function App() {
|
|
397
|
+
return (
|
|
398
|
+
<UIBlockingProvider>
|
|
399
|
+
<MyApp />
|
|
400
|
+
</UIBlockingProvider>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Testing - isolated state per test, no cleanup needed
|
|
405
|
+
function renderWithProvider(ui) {
|
|
406
|
+
return render(
|
|
407
|
+
<UIBlockingProvider>{ui}</UIBlockingProvider>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Micro-frontends - each app has its own blocking state
|
|
412
|
+
function MicroFrontend() {
|
|
413
|
+
return (
|
|
414
|
+
<UIBlockingProvider devtoolsName="MicroApp-Blocking">
|
|
415
|
+
<MicroApp />
|
|
416
|
+
</UIBlockingProvider>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Context Hooks
|
|
422
|
+
|
|
423
|
+
- `useUIBlockingContext()` - Get store from context (throws if outside provider)
|
|
424
|
+
- `useIsInsideUIBlockingProvider()` - Check if inside a provider
|
|
425
|
+
- `useUIBlockingStoreFromContext(selector)` - Select state from context store
|
|
426
|
+
|
|
309
427
|
### Store
|
|
310
428
|
|
|
311
429
|
#### `useUIBlockingStore`
|
|
312
430
|
|
|
313
|
-
Direct access to the Zustand store for advanced use cases.
|
|
431
|
+
Direct access to the Zustand store for advanced use cases (requires a selector).
|
|
314
432
|
|
|
315
433
|
**Methods:**
|
|
316
434
|
|
|
317
|
-
- `addBlocker(id, config)` - Manually add a blocker
|
|
435
|
+
- `addBlocker(id, config)` - Manually add a blocker (re-adding with the same ID replaces config)
|
|
318
436
|
- `removeBlocker(id)` - Manually remove a blocker
|
|
319
|
-
- `updateBlocker(id, config)` - Update an existing blocker
|
|
320
437
|
- `isBlocked(scope)` - Check if scope is blocked
|
|
321
438
|
- `getBlockingInfo(scope)` - Get detailed blocking information
|
|
322
439
|
- `clearAllBlockers()` - Remove all blockers
|
|
323
440
|
- `clearBlockersForScope(scope)` - Remove blockers for specific scope
|
|
441
|
+
- `registerMiddleware(name, middleware)` - Register middleware manually
|
|
442
|
+
- `unregisterMiddleware(name)` - Unregister middleware manually
|
|
324
443
|
|
|
325
444
|
**Example:**
|
|
326
445
|
|
|
@@ -328,7 +447,10 @@ Direct access to the Zustand store for advanced use cases.
|
|
|
328
447
|
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
|
|
329
448
|
|
|
330
449
|
function AdvancedComponent() {
|
|
331
|
-
const { addBlocker, removeBlocker
|
|
450
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore((state) => ({
|
|
451
|
+
addBlocker: state.addBlocker,
|
|
452
|
+
removeBlocker: state.removeBlocker,
|
|
453
|
+
}));
|
|
332
454
|
|
|
333
455
|
const startBlocking = () => {
|
|
334
456
|
addBlocker("custom-blocker", {
|
|
@@ -339,7 +461,10 @@ function AdvancedComponent() {
|
|
|
339
461
|
};
|
|
340
462
|
|
|
341
463
|
const updatePriority = () => {
|
|
342
|
-
|
|
464
|
+
// Re-add with the same ID to update the config
|
|
465
|
+
addBlocker("custom-blocker", {
|
|
466
|
+
scope: ["form", "navigation"],
|
|
467
|
+
reason: "Critical operation in progress",
|
|
343
468
|
priority: 200,
|
|
344
469
|
});
|
|
345
470
|
};
|
|
@@ -358,6 +483,26 @@ function AdvancedComponent() {
|
|
|
358
483
|
}
|
|
359
484
|
```
|
|
360
485
|
|
|
486
|
+
For non-hook contexts (tests, utilities, event handlers), use `uiBlockingStoreApi`:
|
|
487
|
+
|
|
488
|
+
```jsx
|
|
489
|
+
import { uiBlockingStoreApi } from "@okyrychenko-dev/react-action-guard";
|
|
490
|
+
|
|
491
|
+
uiBlockingStoreApi.getState().addBlocker("server-call", {
|
|
492
|
+
scope: "global",
|
|
493
|
+
reason: "Server call running",
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Store Toolkit Helpers
|
|
498
|
+
|
|
499
|
+
Advanced store helpers are re-exported from the internal toolkit for custom setups:
|
|
500
|
+
|
|
501
|
+
- `createShallowStore`
|
|
502
|
+
- `createStoreToolkit`
|
|
503
|
+
- `createStoreProvider`
|
|
504
|
+
- `createResolvedStoreHooks`
|
|
505
|
+
|
|
361
506
|
## Middleware System
|
|
362
507
|
|
|
363
508
|
The library includes a powerful middleware system that allows you to hook into blocker lifecycle events for analytics, logging, and performance monitoring.
|
|
@@ -369,7 +514,10 @@ The library includes a powerful middleware system that allows you to hook into b
|
|
|
369
514
|
Track blocker events with your analytics provider (Google Analytics, Mixpanel, Amplitude, or custom).
|
|
370
515
|
|
|
371
516
|
```jsx
|
|
372
|
-
import {
|
|
517
|
+
import {
|
|
518
|
+
configureMiddleware,
|
|
519
|
+
createAnalyticsMiddleware,
|
|
520
|
+
} from "@okyrychenko-dev/react-action-guard";
|
|
373
521
|
|
|
374
522
|
// Google Analytics
|
|
375
523
|
configureMiddleware([createAnalyticsMiddleware({ provider: "ga" })]);
|
|
@@ -405,7 +553,10 @@ configureMiddleware([loggerMiddleware]);
|
|
|
405
553
|
Monitor blocker performance and detect slow operations.
|
|
406
554
|
|
|
407
555
|
```jsx
|
|
408
|
-
import {
|
|
556
|
+
import {
|
|
557
|
+
configureMiddleware,
|
|
558
|
+
createPerformanceMiddleware,
|
|
559
|
+
} from "@okyrychenko-dev/react-action-guard";
|
|
409
560
|
|
|
410
561
|
configureMiddleware([
|
|
411
562
|
createPerformanceMiddleware({
|
|
@@ -417,6 +568,10 @@ configureMiddleware([
|
|
|
417
568
|
]);
|
|
418
569
|
```
|
|
419
570
|
|
|
571
|
+
Note: `configureMiddleware` registers middleware on the global store. If you use `UIBlockingProvider`, register middleware via the provider's `middlewares` prop instead.
|
|
572
|
+
|
|
573
|
+
Note: Middleware action types include `update` and `cancel`, but the store currently emits `add`, `remove`, and `timeout`.
|
|
574
|
+
|
|
420
575
|
### Custom Middleware
|
|
421
576
|
|
|
422
577
|
Create your own middleware to handle blocker events:
|
|
@@ -475,7 +630,10 @@ The library is fully tree-shakeable. Import only the features you need to keep y
|
|
|
475
630
|
import { useBlocker } from "@okyrychenko-dev/react-action-guard";
|
|
476
631
|
|
|
477
632
|
// Middleware is not included unless you import it
|
|
478
|
-
import {
|
|
633
|
+
import {
|
|
634
|
+
configureMiddleware,
|
|
635
|
+
createAnalyticsMiddleware,
|
|
636
|
+
} from "@okyrychenko-dev/react-action-guard";
|
|
479
637
|
```
|
|
480
638
|
|
|
481
639
|
The package is configured with `"sideEffects": false`, allowing modern bundlers (Webpack, Rollup, Vite) to eliminate unused code automatically.
|
|
@@ -489,23 +647,48 @@ import type {
|
|
|
489
647
|
// Core types
|
|
490
648
|
BlockerConfig,
|
|
491
649
|
BlockerInfo,
|
|
492
|
-
|
|
650
|
+
UIBlockingStore,
|
|
651
|
+
UIBlockingStoreState,
|
|
493
652
|
|
|
494
653
|
// Hook types
|
|
495
|
-
|
|
654
|
+
ConfirmableBlockerConfig,
|
|
655
|
+
ConfirmDialogConfig,
|
|
656
|
+
UseConfirmableBlockerReturn,
|
|
496
657
|
ScheduledBlockerConfig,
|
|
497
658
|
ConditionalBlockerConfig,
|
|
498
659
|
BlockingSchedule,
|
|
660
|
+
UseAsyncActionOptions,
|
|
499
661
|
|
|
500
662
|
// Middleware types
|
|
501
663
|
Middleware,
|
|
502
664
|
MiddlewareContext,
|
|
503
665
|
AnalyticsConfig,
|
|
504
666
|
AnalyticsProvider,
|
|
505
|
-
|
|
667
|
+
PerformanceConfig,
|
|
668
|
+
|
|
669
|
+
// Type-safe scopes
|
|
670
|
+
BlockerConfigTyped,
|
|
671
|
+
DefaultScopes,
|
|
672
|
+
ScopeValue,
|
|
506
673
|
} from "@okyrychenko-dev/react-action-guard";
|
|
507
674
|
```
|
|
508
675
|
|
|
676
|
+
## Type-Safe Scopes
|
|
677
|
+
|
|
678
|
+
Create typed versions of the hooks to prevent scope typos at compile time:
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
import { createTypedHooks } from "@okyrychenko-dev/react-action-guard";
|
|
682
|
+
|
|
683
|
+
type AppScopes = "global" | "form" | "navigation" | "checkout";
|
|
684
|
+
|
|
685
|
+
const { useBlocker, useIsBlocked, useAsyncAction, useBlockingInfo } =
|
|
686
|
+
createTypedHooks<AppScopes>();
|
|
687
|
+
|
|
688
|
+
useBlocker("save", { scope: "form" }); // OK
|
|
689
|
+
useBlocker("save", { scope: "typo" }); // Type error
|
|
690
|
+
```
|
|
691
|
+
|
|
509
692
|
## Use Cases
|
|
510
693
|
|
|
511
694
|
### Loading States
|
|
@@ -560,22 +743,22 @@ function FormWithUnsavedWarning() {
|
|
|
560
743
|
const [formData, setFormData] = useState({});
|
|
561
744
|
const [hasChanges, setHasChanges] = useState(false);
|
|
562
745
|
|
|
563
|
-
const {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
746
|
+
const {
|
|
747
|
+
execute,
|
|
748
|
+
isDialogOpen,
|
|
749
|
+
confirmConfig,
|
|
750
|
+
onConfirm,
|
|
751
|
+
onCancel,
|
|
752
|
+
} = useConfirmableBlocker("unsaved-form", {
|
|
753
|
+
scope: "navigation",
|
|
754
|
+
reason: "Unsaved form data",
|
|
755
|
+
priority: 100,
|
|
756
|
+
confirmMessage: "You have unsaved changes. Discard them?",
|
|
757
|
+
onConfirm: () => {
|
|
758
|
+
setFormData({});
|
|
759
|
+
setHasChanges(false);
|
|
569
760
|
},
|
|
570
|
-
|
|
571
|
-
enabled: hasChanges,
|
|
572
|
-
confirmMessage: "You have unsaved changes. Discard them?",
|
|
573
|
-
onConfirm: () => {
|
|
574
|
-
setFormData({});
|
|
575
|
-
setHasChanges(false);
|
|
576
|
-
},
|
|
577
|
-
}
|
|
578
|
-
);
|
|
761
|
+
});
|
|
579
762
|
|
|
580
763
|
return (
|
|
581
764
|
<form>
|
|
@@ -585,7 +768,17 @@ function FormWithUnsavedWarning() {
|
|
|
585
768
|
setHasChanges(true);
|
|
586
769
|
}}
|
|
587
770
|
/>
|
|
588
|
-
<button onClick={
|
|
771
|
+
<button type="button" onClick={execute}>Cancel</button>
|
|
772
|
+
{isDialogOpen && (
|
|
773
|
+
<ConfirmDialog
|
|
774
|
+
title={confirmConfig.title}
|
|
775
|
+
message={confirmConfig.message}
|
|
776
|
+
confirmText={confirmConfig.confirmText}
|
|
777
|
+
cancelText={confirmConfig.cancelText}
|
|
778
|
+
onConfirm={onConfirm}
|
|
779
|
+
onCancel={onCancel}
|
|
780
|
+
/>
|
|
781
|
+
)}
|
|
589
782
|
</form>
|
|
590
783
|
);
|
|
591
784
|
}
|
|
@@ -740,4 +933,4 @@ See [CHANGELOG.md](./CHANGELOG.md) for a detailed list of changes in each versio
|
|
|
740
933
|
|
|
741
934
|
## License
|
|
742
935
|
|
|
743
|
-
MIT ©
|
|
936
|
+
MIT © Oleksii Kyrychenko
|