@okyrychenko-dev/react-action-guard 0.4.0 → 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/README.md +136 -53
- package/dist/index.cjs +33 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -51
- package/dist/index.d.ts +17 -51
- package/dist/index.js +31 -43
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ function MyComponent() {
|
|
|
53
53
|
|
|
54
54
|
## Documentation
|
|
55
55
|
|
|
56
|
-
📚 **
|
|
56
|
+
📚 **Interactive Storybook Documentation** - Run locally to explore live examples and detailed guides for all hooks
|
|
57
57
|
|
|
58
58
|
To run Storybook locally:
|
|
59
59
|
|
|
@@ -225,49 +225,66 @@ function ApiComponent() {
|
|
|
225
225
|
}
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
#### `useConfirmableBlocker(blockerId, config
|
|
228
|
+
#### `useConfirmableBlocker(blockerId, config)`
|
|
229
229
|
|
|
230
|
-
Creates a
|
|
230
|
+
Creates a confirmable action with UI blocking while the dialog is open or the action is running.
|
|
231
231
|
|
|
232
232
|
**Parameters:**
|
|
233
233
|
|
|
234
234
|
- `blockerId: string` - Unique identifier for the blocker
|
|
235
|
-
- `config:
|
|
236
|
-
- `
|
|
237
|
-
- `
|
|
238
|
-
- `
|
|
239
|
-
- `
|
|
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)
|
|
240
243
|
|
|
241
244
|
**Returns:**
|
|
242
245
|
|
|
243
|
-
- `
|
|
244
|
-
- `
|
|
245
|
-
- `
|
|
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
|
|
246
252
|
|
|
247
253
|
**Example:**
|
|
248
254
|
|
|
249
255
|
```jsx
|
|
250
|
-
function UnsavedChangesGuard({
|
|
251
|
-
const {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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();
|
|
256
270
|
},
|
|
257
|
-
|
|
258
|
-
enabled: hasUnsavedChanges,
|
|
259
|
-
confirmMessage: "You have unsaved changes. Are you sure you want to leave?",
|
|
260
|
-
onConfirm: async () => {
|
|
261
|
-
await discardChanges();
|
|
262
|
-
},
|
|
263
|
-
}
|
|
264
|
-
);
|
|
271
|
+
});
|
|
265
272
|
|
|
266
273
|
return (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
</>
|
|
271
288
|
);
|
|
272
289
|
}
|
|
273
290
|
```
|
|
@@ -411,17 +428,18 @@ function MicroFrontend() {
|
|
|
411
428
|
|
|
412
429
|
#### `useUIBlockingStore`
|
|
413
430
|
|
|
414
|
-
Direct access to the Zustand store for advanced use cases.
|
|
431
|
+
Direct access to the Zustand store for advanced use cases (requires a selector).
|
|
415
432
|
|
|
416
433
|
**Methods:**
|
|
417
434
|
|
|
418
|
-
- `addBlocker(id, config)` - Manually add a blocker
|
|
435
|
+
- `addBlocker(id, config)` - Manually add a blocker (re-adding with the same ID replaces config)
|
|
419
436
|
- `removeBlocker(id)` - Manually remove a blocker
|
|
420
|
-
- `updateBlocker(id, config)` - Update an existing blocker
|
|
421
437
|
- `isBlocked(scope)` - Check if scope is blocked
|
|
422
438
|
- `getBlockingInfo(scope)` - Get detailed blocking information
|
|
423
439
|
- `clearAllBlockers()` - Remove all blockers
|
|
424
440
|
- `clearBlockersForScope(scope)` - Remove blockers for specific scope
|
|
441
|
+
- `registerMiddleware(name, middleware)` - Register middleware manually
|
|
442
|
+
- `unregisterMiddleware(name)` - Unregister middleware manually
|
|
425
443
|
|
|
426
444
|
**Example:**
|
|
427
445
|
|
|
@@ -429,7 +447,10 @@ Direct access to the Zustand store for advanced use cases.
|
|
|
429
447
|
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
|
|
430
448
|
|
|
431
449
|
function AdvancedComponent() {
|
|
432
|
-
const { addBlocker, removeBlocker
|
|
450
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore((state) => ({
|
|
451
|
+
addBlocker: state.addBlocker,
|
|
452
|
+
removeBlocker: state.removeBlocker,
|
|
453
|
+
}));
|
|
433
454
|
|
|
434
455
|
const startBlocking = () => {
|
|
435
456
|
addBlocker("custom-blocker", {
|
|
@@ -440,7 +461,10 @@ function AdvancedComponent() {
|
|
|
440
461
|
};
|
|
441
462
|
|
|
442
463
|
const updatePriority = () => {
|
|
443
|
-
|
|
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",
|
|
444
468
|
priority: 200,
|
|
445
469
|
});
|
|
446
470
|
};
|
|
@@ -459,6 +483,26 @@ function AdvancedComponent() {
|
|
|
459
483
|
}
|
|
460
484
|
```
|
|
461
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
|
+
|
|
462
506
|
## Middleware System
|
|
463
507
|
|
|
464
508
|
The library includes a powerful middleware system that allows you to hook into blocker lifecycle events for analytics, logging, and performance monitoring.
|
|
@@ -524,6 +568,10 @@ configureMiddleware([
|
|
|
524
568
|
]);
|
|
525
569
|
```
|
|
526
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
|
+
|
|
527
575
|
### Custom Middleware
|
|
528
576
|
|
|
529
577
|
Create your own middleware to handle blocker events:
|
|
@@ -599,23 +647,48 @@ import type {
|
|
|
599
647
|
// Core types
|
|
600
648
|
BlockerConfig,
|
|
601
649
|
BlockerInfo,
|
|
602
|
-
|
|
650
|
+
UIBlockingStore,
|
|
651
|
+
UIBlockingStoreState,
|
|
603
652
|
|
|
604
653
|
// Hook types
|
|
605
|
-
|
|
654
|
+
ConfirmableBlockerConfig,
|
|
655
|
+
ConfirmDialogConfig,
|
|
656
|
+
UseConfirmableBlockerReturn,
|
|
606
657
|
ScheduledBlockerConfig,
|
|
607
658
|
ConditionalBlockerConfig,
|
|
608
659
|
BlockingSchedule,
|
|
660
|
+
UseAsyncActionOptions,
|
|
609
661
|
|
|
610
662
|
// Middleware types
|
|
611
663
|
Middleware,
|
|
612
664
|
MiddlewareContext,
|
|
613
665
|
AnalyticsConfig,
|
|
614
666
|
AnalyticsProvider,
|
|
615
|
-
|
|
667
|
+
PerformanceConfig,
|
|
668
|
+
|
|
669
|
+
// Type-safe scopes
|
|
670
|
+
BlockerConfigTyped,
|
|
671
|
+
DefaultScopes,
|
|
672
|
+
ScopeValue,
|
|
616
673
|
} from "@okyrychenko-dev/react-action-guard";
|
|
617
674
|
```
|
|
618
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
|
+
|
|
619
692
|
## Use Cases
|
|
620
693
|
|
|
621
694
|
### Loading States
|
|
@@ -670,22 +743,22 @@ function FormWithUnsavedWarning() {
|
|
|
670
743
|
const [formData, setFormData] = useState({});
|
|
671
744
|
const [hasChanges, setHasChanges] = useState(false);
|
|
672
745
|
|
|
673
|
-
const {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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);
|
|
679
760
|
},
|
|
680
|
-
|
|
681
|
-
enabled: hasChanges,
|
|
682
|
-
confirmMessage: "You have unsaved changes. Discard them?",
|
|
683
|
-
onConfirm: () => {
|
|
684
|
-
setFormData({});
|
|
685
|
-
setHasChanges(false);
|
|
686
|
-
},
|
|
687
|
-
}
|
|
688
|
-
);
|
|
761
|
+
});
|
|
689
762
|
|
|
690
763
|
return (
|
|
691
764
|
<form>
|
|
@@ -695,7 +768,17 @@ function FormWithUnsavedWarning() {
|
|
|
695
768
|
setHasChanges(true);
|
|
696
769
|
}}
|
|
697
770
|
/>
|
|
698
|
-
<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
|
+
)}
|
|
699
782
|
</form>
|
|
700
783
|
);
|
|
701
784
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var react = require('react');
|
|
4
|
+
var reactZustandToolkit = require('@okyrychenko-dev/react-zustand-toolkit');
|
|
4
5
|
var zustand = require('zustand');
|
|
5
|
-
|
|
6
|
-
var shallow = require('zustand/shallow');
|
|
6
|
+
require('zustand/react/shallow');
|
|
7
7
|
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
var shallow = require('zustand/shallow');
|
|
9
|
+
var middleware = require('zustand/middleware');
|
|
8
10
|
|
|
9
11
|
// src/hooks/useAsyncAction/useAsyncAction.ts
|
|
10
12
|
|
|
@@ -14,13 +16,6 @@ var DEFAULT_REASON = "Unknown";
|
|
|
14
16
|
var DEFAULT_PRIORITY = 0;
|
|
15
17
|
var ASYNC_ACTION_PRIORITY = 20;
|
|
16
18
|
var DEVTOOLS_NAME = "UIBlocking";
|
|
17
|
-
function createShallowStore(storeCreator) {
|
|
18
|
-
const storeApi = zustand.createStore(storeCreator);
|
|
19
|
-
const useScopedStore = (selector) => {
|
|
20
|
-
return zustand.useStore(storeApi, shallow.useShallow(selector));
|
|
21
|
-
};
|
|
22
|
-
return { useStore: useScopedStore, useStoreApi: storeApi };
|
|
23
|
-
}
|
|
24
19
|
function normalizeScopeToArray(scope) {
|
|
25
20
|
if (typeof scope === "string") {
|
|
26
21
|
return [scope];
|
|
@@ -221,44 +216,33 @@ var createUIBlockingActions = (set, get) => ({
|
|
|
221
216
|
});
|
|
222
217
|
}
|
|
223
218
|
});
|
|
224
|
-
var
|
|
219
|
+
var {
|
|
220
|
+
Provider: BaseUIBlockingProvider,
|
|
221
|
+
useContext: useUIBlockingContext,
|
|
222
|
+
useContextStore: useUIBlockingStoreFromContext,
|
|
223
|
+
useIsInsideProvider: useIsInsideUIBlockingProvider,
|
|
224
|
+
useOptionalContext
|
|
225
|
+
} = reactZustandToolkit.createStoreProvider(createUIBlockingActions, "UIBlocking");
|
|
225
226
|
function UIBlockingProvider({
|
|
226
227
|
children,
|
|
227
228
|
enableDevtools = process.env.NODE_ENV === "development",
|
|
228
229
|
devtoolsName = "UIBlocking",
|
|
229
230
|
middlewares = []
|
|
230
231
|
}) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
middleware.devtools(createUIBlockingActions, {
|
|
235
|
-
name: devtoolsName,
|
|
236
|
-
enabled: enableDevtools
|
|
237
|
-
})
|
|
238
|
-
) : zustand.createStore(createUIBlockingActions);
|
|
239
|
-
middlewaresRef.current.forEach((mw, index) => {
|
|
240
|
-
baseStore.getState().registerMiddleware(`provider-middleware-${index.toString()}`, mw);
|
|
232
|
+
const handleStoreCreate = (store) => {
|
|
233
|
+
middlewares.forEach((mw, index) => {
|
|
234
|
+
store.getState().registerMiddleware(`provider-middleware-${index.toString()}`, mw);
|
|
241
235
|
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
return store;
|
|
254
|
-
}
|
|
255
|
-
function useIsInsideUIBlockingProvider() {
|
|
256
|
-
const store = react.useContext(UIBlockingContext);
|
|
257
|
-
return store !== null;
|
|
258
|
-
}
|
|
259
|
-
function useUIBlockingStoreFromContext(selector) {
|
|
260
|
-
const store = useUIBlockingContext();
|
|
261
|
-
return zustand.useStore(store, shallow.useShallow(selector));
|
|
236
|
+
};
|
|
237
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
238
|
+
BaseUIBlockingProvider,
|
|
239
|
+
{
|
|
240
|
+
enableDevtools,
|
|
241
|
+
devtoolsName,
|
|
242
|
+
onStoreCreate: handleStoreCreate,
|
|
243
|
+
children
|
|
244
|
+
}
|
|
245
|
+
);
|
|
262
246
|
}
|
|
263
247
|
|
|
264
248
|
// src/store/uiBlockingStore.config.ts
|
|
@@ -268,11 +252,14 @@ var devtoolsConfig = {
|
|
|
268
252
|
};
|
|
269
253
|
|
|
270
254
|
// src/store/uiBlockingStore.store.ts
|
|
271
|
-
var { useStore: useUIBlockingStore, useStoreApi: uiBlockingStoreApi } = createShallowStore(middleware.devtools(createUIBlockingActions, devtoolsConfig));
|
|
255
|
+
var { useStore: useUIBlockingStore, useStoreApi: uiBlockingStoreApi } = reactZustandToolkit.createShallowStore(middleware.devtools(createUIBlockingActions, devtoolsConfig));
|
|
272
256
|
|
|
273
257
|
// src/context/useResolvedStore.ts
|
|
258
|
+
function useOptionalContextStore() {
|
|
259
|
+
return useOptionalContext();
|
|
260
|
+
}
|
|
274
261
|
function useResolvedStore() {
|
|
275
|
-
const contextStore =
|
|
262
|
+
const contextStore = useOptionalContextStore();
|
|
276
263
|
return contextStore ?? uiBlockingStoreApi;
|
|
277
264
|
}
|
|
278
265
|
function useResolvedStoreWithSelector(selector) {
|
|
@@ -719,6 +706,10 @@ function createTypedHooks() {
|
|
|
719
706
|
};
|
|
720
707
|
}
|
|
721
708
|
|
|
709
|
+
Object.defineProperty(exports, "createShallowStore", {
|
|
710
|
+
enumerable: true,
|
|
711
|
+
get: function () { return reactZustandToolkit.createShallowStore; }
|
|
712
|
+
});
|
|
722
713
|
exports.UIBlockingProvider = UIBlockingProvider;
|
|
723
714
|
exports.configureMiddleware = configureMiddleware;
|
|
724
715
|
exports.createAnalyticsMiddleware = createAnalyticsMiddleware;
|