@okyrychenko-dev/react-action-guard 0.1.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 +21 -0
- package/README.md +242 -0
- package/dist/index.cjs +182 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +177 -0
- package/dist/index.js.map +1 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Olexii Kyrychenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# @okyrychenko-dev/react-action-guard
|
|
2
|
+
|
|
3
|
+
> Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Priority-based blocking system
|
|
8
|
+
- Scoped blocking (global, specific areas, or multiple scopes)
|
|
9
|
+
- Automatic cleanup on unmount
|
|
10
|
+
- TypeScript support
|
|
11
|
+
- Built on Zustand for efficient state management
|
|
12
|
+
- Hooks-based API
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @okyrychenko-dev/react-action-guard zustand
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
import { useBlocker, useIsBlocked } from '@okyrychenko-dev/react-action-guard';
|
|
24
|
+
|
|
25
|
+
function MyComponent() {
|
|
26
|
+
const isBlocked = useIsBlocked('my-scope');
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<button disabled={isBlocked}>
|
|
30
|
+
Click me
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API Reference
|
|
37
|
+
|
|
38
|
+
### Hooks
|
|
39
|
+
|
|
40
|
+
#### `useBlocker(blockerId, config, isActive)`
|
|
41
|
+
|
|
42
|
+
Automatically adds a blocker when the component mounts and removes it on unmount.
|
|
43
|
+
|
|
44
|
+
**Parameters:**
|
|
45
|
+
- `blockerId: string` - Unique identifier for the blocker
|
|
46
|
+
- `config: BlockerConfig` - Configuration object
|
|
47
|
+
- `scope?: string | string[]` - Scope(s) to block (default: "global")
|
|
48
|
+
- `reason?: string` - Reason for blocking (for debugging)
|
|
49
|
+
- `priority?: number` - Priority level (higher = more important)
|
|
50
|
+
- `isActive?: boolean` - Whether the blocker is active (default: true)
|
|
51
|
+
|
|
52
|
+
**Example:**
|
|
53
|
+
|
|
54
|
+
```jsx
|
|
55
|
+
function MyComponent() {
|
|
56
|
+
useBlocker('my-blocker', {
|
|
57
|
+
scope: 'form',
|
|
58
|
+
reason: 'Form is saving',
|
|
59
|
+
priority: 10
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return <div>Content</div>;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### `useIsBlocked(scope)`
|
|
67
|
+
|
|
68
|
+
Checks if a specific scope (or scopes) is currently blocked.
|
|
69
|
+
|
|
70
|
+
**Parameters:**
|
|
71
|
+
- `scope?: string | string[]` - Scope(s) to check (default: "global")
|
|
72
|
+
|
|
73
|
+
**Returns:** `boolean` - Whether the scope is blocked
|
|
74
|
+
|
|
75
|
+
**Example:**
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
function SubmitButton() {
|
|
79
|
+
const isFormBlocked = useIsBlocked('form');
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<button disabled={isFormBlocked}>
|
|
83
|
+
Submit
|
|
84
|
+
</button>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `useAsyncAction(actionId, scope)`
|
|
90
|
+
|
|
91
|
+
Wraps an async function with automatic blocking/unblocking.
|
|
92
|
+
|
|
93
|
+
**Parameters:**
|
|
94
|
+
- `actionId: string` - Identifier for the action
|
|
95
|
+
- `scope?: string | string[]` - Scope(s) to block during execution
|
|
96
|
+
|
|
97
|
+
**Returns:** `(asyncFn: () => Promise<T>) => Promise<T>` - Function wrapper
|
|
98
|
+
|
|
99
|
+
**Example:**
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
function MyComponent() {
|
|
103
|
+
const executeWithBlocking = useAsyncAction('save-data', 'form');
|
|
104
|
+
|
|
105
|
+
const handleSave = async () => {
|
|
106
|
+
await executeWithBlocking(async () => {
|
|
107
|
+
await saveData();
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return <button onClick={handleSave}>Save</button>;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Store
|
|
116
|
+
|
|
117
|
+
#### `useUIBlockingStore`
|
|
118
|
+
|
|
119
|
+
Direct access to the Zustand store for advanced use cases.
|
|
120
|
+
|
|
121
|
+
**Methods:**
|
|
122
|
+
- `addBlocker(id, config)` - Manually add a blocker
|
|
123
|
+
- `removeBlocker(id)` - Manually remove a blocker
|
|
124
|
+
- `isBlocked(scope)` - Check if scope is blocked
|
|
125
|
+
- `getBlockingInfo(scope)` - Get detailed blocking information
|
|
126
|
+
- `clearAllBlockers()` - Remove all blockers
|
|
127
|
+
- `clearBlockersForScope(scope)` - Remove blockers for specific scope
|
|
128
|
+
|
|
129
|
+
**Example:**
|
|
130
|
+
|
|
131
|
+
```jsx
|
|
132
|
+
import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
|
|
133
|
+
|
|
134
|
+
function AdvancedComponent() {
|
|
135
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore();
|
|
136
|
+
|
|
137
|
+
const startBlocking = () => {
|
|
138
|
+
addBlocker('custom-blocker', {
|
|
139
|
+
scope: ['form', 'navigation'],
|
|
140
|
+
reason: 'Critical operation in progress',
|
|
141
|
+
priority: 100
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const stopBlocking = () => {
|
|
146
|
+
removeBlocker('custom-blocker');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div>
|
|
151
|
+
<button onClick={startBlocking}>Start</button>
|
|
152
|
+
<button onClick={stopBlocking}>Stop</button>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## TypeScript
|
|
159
|
+
|
|
160
|
+
The package is written in TypeScript and includes full type definitions.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import type {
|
|
164
|
+
BlockerConfig,
|
|
165
|
+
BlockerInfo,
|
|
166
|
+
UIBlockingState
|
|
167
|
+
} from '@okyrychenko-dev/react-action-guard';
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Use Cases
|
|
171
|
+
|
|
172
|
+
### Loading States
|
|
173
|
+
|
|
174
|
+
```jsx
|
|
175
|
+
function DataLoader() {
|
|
176
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
177
|
+
|
|
178
|
+
useBlocker('data-loader', {
|
|
179
|
+
scope: 'content',
|
|
180
|
+
reason: 'Loading data'
|
|
181
|
+
}, isLoading);
|
|
182
|
+
|
|
183
|
+
// ... rest of component
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Form Submission
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
function UserForm() {
|
|
191
|
+
const executeWithBlocking = useAsyncAction('submit-form', 'form');
|
|
192
|
+
const isBlocked = useIsBlocked('form');
|
|
193
|
+
|
|
194
|
+
const handleSubmit = async (data) => {
|
|
195
|
+
await executeWithBlocking(async () => {
|
|
196
|
+
await submitForm(data);
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<form onSubmit={handleSubmit}>
|
|
202
|
+
<input disabled={isBlocked} />
|
|
203
|
+
<button disabled={isBlocked}>Submit</button>
|
|
204
|
+
</form>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Global Loading Overlay
|
|
210
|
+
|
|
211
|
+
```jsx
|
|
212
|
+
function App() {
|
|
213
|
+
const isGloballyBlocked = useIsBlocked('global');
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div>
|
|
217
|
+
{isGloballyBlocked && <LoadingOverlay />}
|
|
218
|
+
<YourApp />
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Install dependencies
|
|
228
|
+
npm install
|
|
229
|
+
|
|
230
|
+
# Build the package
|
|
231
|
+
npm run build
|
|
232
|
+
|
|
233
|
+
# Type checking
|
|
234
|
+
npm run typecheck
|
|
235
|
+
|
|
236
|
+
# Watch mode for development
|
|
237
|
+
npm run dev
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
MIT © Oleksandr Kyrychenko
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var zustand = require('zustand');
|
|
5
|
+
var middleware = require('zustand/middleware');
|
|
6
|
+
|
|
7
|
+
// src/hooks/useBlocker.ts
|
|
8
|
+
|
|
9
|
+
// src/store/uiBlockingStore.constants.ts
|
|
10
|
+
var DEFAULT_SCOPE = "global";
|
|
11
|
+
var DEFAULT_REASON = "Unknown";
|
|
12
|
+
var DEFAULT_PRIORITY = 0;
|
|
13
|
+
var ASYNC_ACTION_PRIORITY = 20;
|
|
14
|
+
var DEVTOOLS_NAME = "UIBlocking";
|
|
15
|
+
|
|
16
|
+
// src/store/uiBlockingStore.utils.ts
|
|
17
|
+
function normalizeScopeToArray(scope) {
|
|
18
|
+
if (typeof scope === "string") {
|
|
19
|
+
return [scope];
|
|
20
|
+
}
|
|
21
|
+
return Array.from(scope);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/store/uiBlockingStore.actions.ts
|
|
25
|
+
var createUIBlockingActions = (set, get) => ({
|
|
26
|
+
// State
|
|
27
|
+
activeBlockers: /* @__PURE__ */ new Map(),
|
|
28
|
+
// Actions
|
|
29
|
+
/**
|
|
30
|
+
* Add a blocker to the store
|
|
31
|
+
*
|
|
32
|
+
* @param id - Unique blocker identifier
|
|
33
|
+
* @param config - Optional blocker configuration
|
|
34
|
+
*
|
|
35
|
+
*/
|
|
36
|
+
addBlocker: (id, config = {}) => {
|
|
37
|
+
set((state) => {
|
|
38
|
+
const newBlockers = new Map(state.activeBlockers);
|
|
39
|
+
const storedBlocker = {
|
|
40
|
+
scope: config.scope ?? DEFAULT_SCOPE,
|
|
41
|
+
reason: config.reason ?? DEFAULT_REASON,
|
|
42
|
+
priority: config.priority ?? DEFAULT_PRIORITY,
|
|
43
|
+
timestamp: config.timestamp ?? Date.now()
|
|
44
|
+
};
|
|
45
|
+
newBlockers.set(id, storedBlocker);
|
|
46
|
+
return { activeBlockers: newBlockers };
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
/**
|
|
50
|
+
* Remove a blocker from the store
|
|
51
|
+
*
|
|
52
|
+
* @param id - Blocker identifier to remove
|
|
53
|
+
*
|
|
54
|
+
*/
|
|
55
|
+
removeBlocker: (id) => {
|
|
56
|
+
set((state) => {
|
|
57
|
+
const newBlockers = new Map(state.activeBlockers);
|
|
58
|
+
newBlockers.delete(id);
|
|
59
|
+
return { activeBlockers: newBlockers };
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Check if a scope is currently blocked
|
|
64
|
+
*
|
|
65
|
+
* @param scope - Scope or array of scopes to check (default: "global")
|
|
66
|
+
* @returns True if any of the scopes are blocked
|
|
67
|
+
*
|
|
68
|
+
*/
|
|
69
|
+
isBlocked: (scope = DEFAULT_SCOPE) => {
|
|
70
|
+
const { activeBlockers } = get();
|
|
71
|
+
const scopes = normalizeScopeToArray(scope);
|
|
72
|
+
for (const [, blocker] of activeBlockers) {
|
|
73
|
+
if (blocker.scope === DEFAULT_SCOPE) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
77
|
+
if (scopes.some((s) => blockerScopes.includes(s))) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
},
|
|
83
|
+
/**
|
|
84
|
+
* Get detailed blocking information for a specific scope
|
|
85
|
+
*
|
|
86
|
+
* @param scope - Scope to get blocking information for
|
|
87
|
+
* @returns Array of blockers affecting the scope, sorted by priority (highest first)
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
getBlockingInfo: (scope) => {
|
|
91
|
+
const { activeBlockers } = get();
|
|
92
|
+
const blockers = [];
|
|
93
|
+
for (const [id, blocker] of activeBlockers) {
|
|
94
|
+
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
95
|
+
if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {
|
|
96
|
+
blockers.push({ id, ...blocker });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return blockers.sort((a, b) => b.priority - a.priority);
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Clear all blockers from the store
|
|
103
|
+
*/
|
|
104
|
+
clearAllBlockers: () => {
|
|
105
|
+
set({ activeBlockers: /* @__PURE__ */ new Map() });
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Clear all blockers for a specific scope
|
|
109
|
+
*
|
|
110
|
+
* @param scope - Scope to clear blockers for
|
|
111
|
+
*
|
|
112
|
+
*/
|
|
113
|
+
clearBlockersForScope: (scope) => {
|
|
114
|
+
set((state) => {
|
|
115
|
+
const newBlockers = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const [id, blocker] of state.activeBlockers) {
|
|
117
|
+
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
118
|
+
if (!blockerScopes.includes(scope)) {
|
|
119
|
+
newBlockers.set(id, blocker);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { activeBlockers: newBlockers };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/store/uiBlockingStore.config.ts
|
|
128
|
+
var devtoolsConfig = {
|
|
129
|
+
name: DEVTOOLS_NAME,
|
|
130
|
+
enabled: process.env.NODE_ENV !== "production"
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/store/uiBlockingStore.store.ts
|
|
134
|
+
var useUIBlockingStore = zustand.create()(
|
|
135
|
+
middleware.devtools(createUIBlockingActions, devtoolsConfig)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// src/hooks/useBlocker.ts
|
|
139
|
+
var useBlocker = (blockerId, config, isActive = true) => {
|
|
140
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore();
|
|
141
|
+
react.useEffect(() => {
|
|
142
|
+
if (!isActive || !blockerId) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
addBlocker(blockerId, config);
|
|
146
|
+
return () => {
|
|
147
|
+
removeBlocker(blockerId);
|
|
148
|
+
};
|
|
149
|
+
}, [blockerId, isActive, addBlocker, removeBlocker]);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// src/hooks/useIsBlocked.ts
|
|
153
|
+
var useIsBlocked = (scope) => {
|
|
154
|
+
return useUIBlockingStore((state) => state.isBlocked(scope));
|
|
155
|
+
};
|
|
156
|
+
var useAsyncAction = (actionId, scope) => {
|
|
157
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore();
|
|
158
|
+
const executeWithBlocking = react.useCallback(
|
|
159
|
+
async (asyncFn) => {
|
|
160
|
+
const blockerId = `${actionId}-${Date.now().toString()}`;
|
|
161
|
+
try {
|
|
162
|
+
addBlocker(blockerId, {
|
|
163
|
+
scope,
|
|
164
|
+
reason: `Executing ${actionId}`,
|
|
165
|
+
priority: ASYNC_ACTION_PRIORITY
|
|
166
|
+
});
|
|
167
|
+
return await asyncFn();
|
|
168
|
+
} finally {
|
|
169
|
+
removeBlocker(blockerId);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
[actionId, scope, addBlocker, removeBlocker]
|
|
173
|
+
);
|
|
174
|
+
return executeWithBlocking;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
exports.useAsyncAction = useAsyncAction;
|
|
178
|
+
exports.useBlocker = useBlocker;
|
|
179
|
+
exports.useIsBlocked = useIsBlocked;
|
|
180
|
+
exports.useUIBlockingStore = useUIBlockingStore;
|
|
181
|
+
//# sourceMappingURL=index.cjs.map
|
|
182
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/uiBlockingStore.constants.ts","../src/store/uiBlockingStore.utils.ts","../src/store/uiBlockingStore.actions.ts","../src/store/uiBlockingStore.config.ts","../src/store/uiBlockingStore.store.ts","../src/hooks/useBlocker.ts","../src/hooks/useIsBlocked.ts","../src/hooks/useAsyncAction.ts"],"names":["create","devtools","useEffect","useCallback"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Default values and constants for UI Blocking Store\n */\n\n/**\n * Default scope for blockers\n */\nexport const DEFAULT_SCOPE = \"global\" as const;\n\n/**\n * Default reason when none is provided\n */\nexport const DEFAULT_REASON = \"Unknown\" as const;\n\n/**\n * Default priority for blockers\n */\nexport const DEFAULT_PRIORITY = 0 as const;\n\n/**\n * Default priority for async actions\n */\nexport const ASYNC_ACTION_PRIORITY = 20 as const;\n\n/**\n * DevTools store name\n */\nexport const DEVTOOLS_NAME = \"UIBlocking\" as const;\n","/**\n * Normalize scope to array format\n *\n * @param scope - Single scope or array of scopes\n * @returns Array of scopes\n *\n * @internal\n */\nexport function normalizeScopeToArray(scope: string | ReadonlyArray<string>): Array<string> {\n if (typeof scope === \"string\") {\n return [scope];\n }\n return Array.from(scope);\n}\n","import type { StateCreator } from \"zustand\";\nimport type {\n BlockerConfig,\n BlockerInfo,\n StoredBlocker,\n UIBlockingStore,\n} from \"./uiBlockingStore.types\";\nimport { DEFAULT_SCOPE, DEFAULT_REASON, DEFAULT_PRIORITY } from \"./uiBlockingStore.constants\";\nimport { normalizeScopeToArray } from \"./uiBlockingStore.utils\";\n\n/**\n * UI Blocking Store Slice\n *\n * Implements the state and actions for UI blocking management.\n * This slice follows the Zustand slice pattern for better code organization.\n */\nexport const createUIBlockingActions: StateCreator<UIBlockingStore, [], [], UIBlockingStore> = (\n set,\n get\n) => ({\n // State\n activeBlockers: new Map(),\n\n // Actions\n /**\n * Add a blocker to the store\n *\n * @param id - Unique blocker identifier\n * @param config - Optional blocker configuration\n *\n */\n addBlocker: (id: string, config: BlockerConfig = {}): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n const storedBlocker: StoredBlocker = {\n scope: config.scope ?? DEFAULT_SCOPE,\n reason: config.reason ?? DEFAULT_REASON,\n priority: config.priority ?? DEFAULT_PRIORITY,\n timestamp: config.timestamp ?? Date.now(),\n };\n newBlockers.set(id, storedBlocker);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Remove a blocker from the store\n *\n * @param id - Blocker identifier to remove\n *\n */\n removeBlocker: (id: string): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n newBlockers.delete(id);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Check if a scope is currently blocked\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the scopes are blocked\n *\n */\n isBlocked: (scope: string | ReadonlyArray<string> = DEFAULT_SCOPE): boolean => {\n const { activeBlockers } = get();\n const scopes = normalizeScopeToArray(scope);\n\n for (const [, blocker] of activeBlockers) {\n // Global blocking blocks everything\n if (blocker.scope === DEFAULT_SCOPE) {\n return true;\n }\n\n // Check scope intersection\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (scopes.some((s) => blockerScopes.includes(s))) {\n return true;\n }\n }\n\n return false;\n },\n\n /**\n * Get detailed blocking information for a specific scope\n *\n * @param scope - Scope to get blocking information for\n * @returns Array of blockers affecting the scope, sorted by priority (highest first)\n *\n */\n getBlockingInfo: (scope: string): ReadonlyArray<BlockerInfo> => {\n const { activeBlockers } = get();\n const blockers: Array<BlockerInfo> = [];\n\n for (const [id, blocker] of activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {\n blockers.push({ id, ...blocker });\n }\n }\n\n return blockers.sort((a, b) => b.priority - a.priority);\n },\n\n /**\n * Clear all blockers from the store\n */\n clearAllBlockers: (): void => {\n set({ activeBlockers: new Map() });\n },\n\n /**\n * Clear all blockers for a specific scope\n *\n * @param scope - Scope to clear blockers for\n *\n */\n clearBlockersForScope: (scope: string): void => {\n set((state) => {\n const newBlockers = new Map();\n\n for (const [id, blocker] of state.activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (!blockerScopes.includes(scope)) {\n newBlockers.set(id, blocker);\n }\n }\n\n return { activeBlockers: newBlockers };\n });\n },\n});\n","import type { DevtoolsOptions } from \"zustand/middleware\";\nimport { DEVTOOLS_NAME } from \"./uiBlockingStore.constants\";\n\n/**\n * DevTools configuration for UI Blocking Store\n *\n * Enables Redux DevTools integration only in development mode\n * for better debugging experience without production overhead.\n */\nexport const devtoolsConfig: DevtoolsOptions = {\n name: DEVTOOLS_NAME,\n enabled: process.env.NODE_ENV !== \"production\",\n};\n","import { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { createUIBlockingActions } from \"./uiBlockingStore.actions\";\nimport { devtoolsConfig } from \"./uiBlockingStore.config\";\nimport type { UIBlockingStore } from \"./uiBlockingStore.types\";\n\n/**\n * UI Blocking Store\n *\n * Global Zustand store for managing UI blocking state across the application.\n *\n * Features:\n * - Priority-based blocking system\n * - Scope-based blocking (global, specific areas, or multiple scopes)\n * - Automatic cleanup on unmount (when using hooks)\n * - DevTools integration for debugging\n *\n */\nexport const useUIBlockingStore = create<UIBlockingStore>()(\n devtools(createUIBlockingActions, devtoolsConfig)\n);\n","import { useEffect } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport type { BlockerConfig } from \"../store\";\n\n/**\n * Hook to automatically add and remove a blocker based on component lifecycle\n *\n * Automatically adds a blocker when the component mounts (or when activated)\n * and removes it when the component unmounts (or when deactivated).\n *\n * @param blockerId - Unique identifier for the blocker\n * @param config - Blocker configuration (scope, reason, priority)\n * @param isActive - Whether the blocker is currently active (default: true)\n *\n */\nexport const useBlocker = (blockerId: string, config: BlockerConfig, isActive = true): void => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n useEffect(() => {\n if (!isActive || !blockerId) {\n return;\n }\n\n addBlocker(blockerId, config);\n\n return () => {\n removeBlocker(blockerId);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [blockerId, isActive, addBlocker, removeBlocker]);\n};\n","import { useUIBlockingStore } from \"../store\";\n\n/**\n * Hook to check if a scope is currently blocked\n *\n * Returns a boolean indicating whether the specified scope(s) are blocked.\n * Automatically re-renders when the blocking state changes.\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the specified scopes are blocked\n *\n */\nexport const useIsBlocked = (scope?: string | ReadonlyArray<string>): boolean => {\n return useUIBlockingStore((state) => state.isBlocked(scope));\n};\n","import { useCallback } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport { ASYNC_ACTION_PRIORITY } from \"../store\";\n\n/**\n * Hook to wrap async functions with automatic blocking/unblocking\n *\n * Creates a wrapper function that automatically adds a blocker before executing\n * the async function and removes it after completion (success or failure).\n *\n * @template T - The return type of the async function\n * @param actionId - Unique identifier for the action (used in blocker ID and reason)\n * @param scope - Scope(s) to block during execution (default: \"global\")\n * @returns Function that wraps async functions with blocking behavior\n *\n */\nexport const useAsyncAction = <T = unknown>(\n actionId: string,\n scope?: string | ReadonlyArray<string>\n): ((asyncFn: () => Promise<T>) => Promise<T>) => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n const executeWithBlocking = useCallback(\n async (asyncFn: () => Promise<T>): Promise<T> => {\n const blockerId = `${actionId}-${Date.now().toString()}`;\n\n try {\n addBlocker(blockerId, {\n scope,\n reason: `Executing ${actionId}`,\n priority: ASYNC_ACTION_PRIORITY,\n });\n\n return await asyncFn();\n } finally {\n removeBlocker(blockerId);\n }\n },\n [actionId, scope, addBlocker, removeBlocker]\n );\n\n return executeWithBlocking;\n};\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as zustand from 'zustand';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for a blocker
|
|
5
|
+
*/
|
|
6
|
+
interface BlockerConfig {
|
|
7
|
+
scope?: string | ReadonlyArray<string>;
|
|
8
|
+
reason?: string;
|
|
9
|
+
priority?: number;
|
|
10
|
+
timestamp?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Stored blocker with all required fields
|
|
14
|
+
*/
|
|
15
|
+
interface StoredBlocker {
|
|
16
|
+
scope: string | ReadonlyArray<string>;
|
|
17
|
+
reason: string;
|
|
18
|
+
priority: number;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Blocker information including its ID
|
|
23
|
+
*/
|
|
24
|
+
interface BlockerInfo extends StoredBlocker {
|
|
25
|
+
id: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Internal store state
|
|
29
|
+
*/
|
|
30
|
+
interface UIBlockingStoreState {
|
|
31
|
+
activeBlockers: Map<string, StoredBlocker>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Store actions
|
|
35
|
+
*/
|
|
36
|
+
interface UIBlockingStoreActions {
|
|
37
|
+
addBlocker: (id: string, config?: BlockerConfig) => void;
|
|
38
|
+
removeBlocker: (id: string) => void;
|
|
39
|
+
isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
40
|
+
getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
|
|
41
|
+
clearAllBlockers: () => void;
|
|
42
|
+
clearBlockersForScope: (scope: string) => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Complete store type (state + actions)
|
|
46
|
+
*/
|
|
47
|
+
type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* UI Blocking Store
|
|
51
|
+
*
|
|
52
|
+
* Global Zustand store for managing UI blocking state across the application.
|
|
53
|
+
*
|
|
54
|
+
* Features:
|
|
55
|
+
* - Priority-based blocking system
|
|
56
|
+
* - Scope-based blocking (global, specific areas, or multiple scopes)
|
|
57
|
+
* - Automatic cleanup on unmount (when using hooks)
|
|
58
|
+
* - DevTools integration for debugging
|
|
59
|
+
*
|
|
60
|
+
*/
|
|
61
|
+
declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
|
|
62
|
+
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
63
|
+
[x: string]: unknown;
|
|
64
|
+
[x: number]: unknown;
|
|
65
|
+
[x: symbol]: unknown;
|
|
66
|
+
type: string;
|
|
67
|
+
}) | undefined): void;
|
|
68
|
+
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
69
|
+
[x: string]: unknown;
|
|
70
|
+
[x: number]: unknown;
|
|
71
|
+
[x: symbol]: unknown;
|
|
72
|
+
type: string;
|
|
73
|
+
}) | undefined): void;
|
|
74
|
+
devtools: {
|
|
75
|
+
cleanup: () => void;
|
|
76
|
+
};
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Hook to automatically add and remove a blocker based on component lifecycle
|
|
81
|
+
*
|
|
82
|
+
* Automatically adds a blocker when the component mounts (or when activated)
|
|
83
|
+
* and removes it when the component unmounts (or when deactivated).
|
|
84
|
+
*
|
|
85
|
+
* @param blockerId - Unique identifier for the blocker
|
|
86
|
+
* @param config - Blocker configuration (scope, reason, priority)
|
|
87
|
+
* @param isActive - Whether the blocker is currently active (default: true)
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Hook to check if a scope is currently blocked
|
|
94
|
+
*
|
|
95
|
+
* Returns a boolean indicating whether the specified scope(s) are blocked.
|
|
96
|
+
* Automatically re-renders when the blocking state changes.
|
|
97
|
+
*
|
|
98
|
+
* @param scope - Scope or array of scopes to check (default: "global")
|
|
99
|
+
* @returns True if any of the specified scopes are blocked
|
|
100
|
+
*
|
|
101
|
+
*/
|
|
102
|
+
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hook to wrap async functions with automatic blocking/unblocking
|
|
106
|
+
*
|
|
107
|
+
* Creates a wrapper function that automatically adds a blocker before executing
|
|
108
|
+
* the async function and removes it after completion (success or failure).
|
|
109
|
+
*
|
|
110
|
+
* @template T - The return type of the async function
|
|
111
|
+
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
112
|
+
* @param scope - Scope(s) to block during execution (default: "global")
|
|
113
|
+
* @returns Function that wraps async functions with blocking behavior
|
|
114
|
+
*
|
|
115
|
+
*/
|
|
116
|
+
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
117
|
+
|
|
118
|
+
export { type BlockerConfig, type BlockerInfo, type StoredBlocker, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, useAsyncAction, useBlocker, useIsBlocked, useUIBlockingStore };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as zustand from 'zustand';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for a blocker
|
|
5
|
+
*/
|
|
6
|
+
interface BlockerConfig {
|
|
7
|
+
scope?: string | ReadonlyArray<string>;
|
|
8
|
+
reason?: string;
|
|
9
|
+
priority?: number;
|
|
10
|
+
timestamp?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Stored blocker with all required fields
|
|
14
|
+
*/
|
|
15
|
+
interface StoredBlocker {
|
|
16
|
+
scope: string | ReadonlyArray<string>;
|
|
17
|
+
reason: string;
|
|
18
|
+
priority: number;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Blocker information including its ID
|
|
23
|
+
*/
|
|
24
|
+
interface BlockerInfo extends StoredBlocker {
|
|
25
|
+
id: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Internal store state
|
|
29
|
+
*/
|
|
30
|
+
interface UIBlockingStoreState {
|
|
31
|
+
activeBlockers: Map<string, StoredBlocker>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Store actions
|
|
35
|
+
*/
|
|
36
|
+
interface UIBlockingStoreActions {
|
|
37
|
+
addBlocker: (id: string, config?: BlockerConfig) => void;
|
|
38
|
+
removeBlocker: (id: string) => void;
|
|
39
|
+
isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
40
|
+
getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
|
|
41
|
+
clearAllBlockers: () => void;
|
|
42
|
+
clearBlockersForScope: (scope: string) => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Complete store type (state + actions)
|
|
46
|
+
*/
|
|
47
|
+
type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* UI Blocking Store
|
|
51
|
+
*
|
|
52
|
+
* Global Zustand store for managing UI blocking state across the application.
|
|
53
|
+
*
|
|
54
|
+
* Features:
|
|
55
|
+
* - Priority-based blocking system
|
|
56
|
+
* - Scope-based blocking (global, specific areas, or multiple scopes)
|
|
57
|
+
* - Automatic cleanup on unmount (when using hooks)
|
|
58
|
+
* - DevTools integration for debugging
|
|
59
|
+
*
|
|
60
|
+
*/
|
|
61
|
+
declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
|
|
62
|
+
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
63
|
+
[x: string]: unknown;
|
|
64
|
+
[x: number]: unknown;
|
|
65
|
+
[x: symbol]: unknown;
|
|
66
|
+
type: string;
|
|
67
|
+
}) | undefined): void;
|
|
68
|
+
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
69
|
+
[x: string]: unknown;
|
|
70
|
+
[x: number]: unknown;
|
|
71
|
+
[x: symbol]: unknown;
|
|
72
|
+
type: string;
|
|
73
|
+
}) | undefined): void;
|
|
74
|
+
devtools: {
|
|
75
|
+
cleanup: () => void;
|
|
76
|
+
};
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Hook to automatically add and remove a blocker based on component lifecycle
|
|
81
|
+
*
|
|
82
|
+
* Automatically adds a blocker when the component mounts (or when activated)
|
|
83
|
+
* and removes it when the component unmounts (or when deactivated).
|
|
84
|
+
*
|
|
85
|
+
* @param blockerId - Unique identifier for the blocker
|
|
86
|
+
* @param config - Blocker configuration (scope, reason, priority)
|
|
87
|
+
* @param isActive - Whether the blocker is currently active (default: true)
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Hook to check if a scope is currently blocked
|
|
94
|
+
*
|
|
95
|
+
* Returns a boolean indicating whether the specified scope(s) are blocked.
|
|
96
|
+
* Automatically re-renders when the blocking state changes.
|
|
97
|
+
*
|
|
98
|
+
* @param scope - Scope or array of scopes to check (default: "global")
|
|
99
|
+
* @returns True if any of the specified scopes are blocked
|
|
100
|
+
*
|
|
101
|
+
*/
|
|
102
|
+
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hook to wrap async functions with automatic blocking/unblocking
|
|
106
|
+
*
|
|
107
|
+
* Creates a wrapper function that automatically adds a blocker before executing
|
|
108
|
+
* the async function and removes it after completion (success or failure).
|
|
109
|
+
*
|
|
110
|
+
* @template T - The return type of the async function
|
|
111
|
+
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
112
|
+
* @param scope - Scope(s) to block during execution (default: "global")
|
|
113
|
+
* @returns Function that wraps async functions with blocking behavior
|
|
114
|
+
*
|
|
115
|
+
*/
|
|
116
|
+
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
117
|
+
|
|
118
|
+
export { type BlockerConfig, type BlockerInfo, type StoredBlocker, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, useAsyncAction, useBlocker, useIsBlocked, useUIBlockingStore };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { devtools } from 'zustand/middleware';
|
|
4
|
+
|
|
5
|
+
// src/hooks/useBlocker.ts
|
|
6
|
+
|
|
7
|
+
// src/store/uiBlockingStore.constants.ts
|
|
8
|
+
var DEFAULT_SCOPE = "global";
|
|
9
|
+
var DEFAULT_REASON = "Unknown";
|
|
10
|
+
var DEFAULT_PRIORITY = 0;
|
|
11
|
+
var ASYNC_ACTION_PRIORITY = 20;
|
|
12
|
+
var DEVTOOLS_NAME = "UIBlocking";
|
|
13
|
+
|
|
14
|
+
// src/store/uiBlockingStore.utils.ts
|
|
15
|
+
function normalizeScopeToArray(scope) {
|
|
16
|
+
if (typeof scope === "string") {
|
|
17
|
+
return [scope];
|
|
18
|
+
}
|
|
19
|
+
return Array.from(scope);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/store/uiBlockingStore.actions.ts
|
|
23
|
+
var createUIBlockingActions = (set, get) => ({
|
|
24
|
+
// State
|
|
25
|
+
activeBlockers: /* @__PURE__ */ new Map(),
|
|
26
|
+
// Actions
|
|
27
|
+
/**
|
|
28
|
+
* Add a blocker to the store
|
|
29
|
+
*
|
|
30
|
+
* @param id - Unique blocker identifier
|
|
31
|
+
* @param config - Optional blocker configuration
|
|
32
|
+
*
|
|
33
|
+
*/
|
|
34
|
+
addBlocker: (id, config = {}) => {
|
|
35
|
+
set((state) => {
|
|
36
|
+
const newBlockers = new Map(state.activeBlockers);
|
|
37
|
+
const storedBlocker = {
|
|
38
|
+
scope: config.scope ?? DEFAULT_SCOPE,
|
|
39
|
+
reason: config.reason ?? DEFAULT_REASON,
|
|
40
|
+
priority: config.priority ?? DEFAULT_PRIORITY,
|
|
41
|
+
timestamp: config.timestamp ?? Date.now()
|
|
42
|
+
};
|
|
43
|
+
newBlockers.set(id, storedBlocker);
|
|
44
|
+
return { activeBlockers: newBlockers };
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Remove a blocker from the store
|
|
49
|
+
*
|
|
50
|
+
* @param id - Blocker identifier to remove
|
|
51
|
+
*
|
|
52
|
+
*/
|
|
53
|
+
removeBlocker: (id) => {
|
|
54
|
+
set((state) => {
|
|
55
|
+
const newBlockers = new Map(state.activeBlockers);
|
|
56
|
+
newBlockers.delete(id);
|
|
57
|
+
return { activeBlockers: newBlockers };
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Check if a scope is currently blocked
|
|
62
|
+
*
|
|
63
|
+
* @param scope - Scope or array of scopes to check (default: "global")
|
|
64
|
+
* @returns True if any of the scopes are blocked
|
|
65
|
+
*
|
|
66
|
+
*/
|
|
67
|
+
isBlocked: (scope = DEFAULT_SCOPE) => {
|
|
68
|
+
const { activeBlockers } = get();
|
|
69
|
+
const scopes = normalizeScopeToArray(scope);
|
|
70
|
+
for (const [, blocker] of activeBlockers) {
|
|
71
|
+
if (blocker.scope === DEFAULT_SCOPE) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
75
|
+
if (scopes.some((s) => blockerScopes.includes(s))) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
},
|
|
81
|
+
/**
|
|
82
|
+
* Get detailed blocking information for a specific scope
|
|
83
|
+
*
|
|
84
|
+
* @param scope - Scope to get blocking information for
|
|
85
|
+
* @returns Array of blockers affecting the scope, sorted by priority (highest first)
|
|
86
|
+
*
|
|
87
|
+
*/
|
|
88
|
+
getBlockingInfo: (scope) => {
|
|
89
|
+
const { activeBlockers } = get();
|
|
90
|
+
const blockers = [];
|
|
91
|
+
for (const [id, blocker] of activeBlockers) {
|
|
92
|
+
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
93
|
+
if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {
|
|
94
|
+
blockers.push({ id, ...blocker });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return blockers.sort((a, b) => b.priority - a.priority);
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* Clear all blockers from the store
|
|
101
|
+
*/
|
|
102
|
+
clearAllBlockers: () => {
|
|
103
|
+
set({ activeBlockers: /* @__PURE__ */ new Map() });
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* Clear all blockers for a specific scope
|
|
107
|
+
*
|
|
108
|
+
* @param scope - Scope to clear blockers for
|
|
109
|
+
*
|
|
110
|
+
*/
|
|
111
|
+
clearBlockersForScope: (scope) => {
|
|
112
|
+
set((state) => {
|
|
113
|
+
const newBlockers = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const [id, blocker] of state.activeBlockers) {
|
|
115
|
+
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
116
|
+
if (!blockerScopes.includes(scope)) {
|
|
117
|
+
newBlockers.set(id, blocker);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { activeBlockers: newBlockers };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// src/store/uiBlockingStore.config.ts
|
|
126
|
+
var devtoolsConfig = {
|
|
127
|
+
name: DEVTOOLS_NAME,
|
|
128
|
+
enabled: process.env.NODE_ENV !== "production"
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/store/uiBlockingStore.store.ts
|
|
132
|
+
var useUIBlockingStore = create()(
|
|
133
|
+
devtools(createUIBlockingActions, devtoolsConfig)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// src/hooks/useBlocker.ts
|
|
137
|
+
var useBlocker = (blockerId, config, isActive = true) => {
|
|
138
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore();
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!isActive || !blockerId) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
addBlocker(blockerId, config);
|
|
144
|
+
return () => {
|
|
145
|
+
removeBlocker(blockerId);
|
|
146
|
+
};
|
|
147
|
+
}, [blockerId, isActive, addBlocker, removeBlocker]);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/hooks/useIsBlocked.ts
|
|
151
|
+
var useIsBlocked = (scope) => {
|
|
152
|
+
return useUIBlockingStore((state) => state.isBlocked(scope));
|
|
153
|
+
};
|
|
154
|
+
var useAsyncAction = (actionId, scope) => {
|
|
155
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore();
|
|
156
|
+
const executeWithBlocking = useCallback(
|
|
157
|
+
async (asyncFn) => {
|
|
158
|
+
const blockerId = `${actionId}-${Date.now().toString()}`;
|
|
159
|
+
try {
|
|
160
|
+
addBlocker(blockerId, {
|
|
161
|
+
scope,
|
|
162
|
+
reason: `Executing ${actionId}`,
|
|
163
|
+
priority: ASYNC_ACTION_PRIORITY
|
|
164
|
+
});
|
|
165
|
+
return await asyncFn();
|
|
166
|
+
} finally {
|
|
167
|
+
removeBlocker(blockerId);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
[actionId, scope, addBlocker, removeBlocker]
|
|
171
|
+
);
|
|
172
|
+
return executeWithBlocking;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export { useAsyncAction, useBlocker, useIsBlocked, useUIBlockingStore };
|
|
176
|
+
//# sourceMappingURL=index.js.map
|
|
177
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/uiBlockingStore.constants.ts","../src/store/uiBlockingStore.utils.ts","../src/store/uiBlockingStore.actions.ts","../src/store/uiBlockingStore.config.ts","../src/store/uiBlockingStore.store.ts","../src/hooks/useBlocker.ts","../src/hooks/useIsBlocked.ts","../src/hooks/useAsyncAction.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Default values and constants for UI Blocking Store\n */\n\n/**\n * Default scope for blockers\n */\nexport const DEFAULT_SCOPE = \"global\" as const;\n\n/**\n * Default reason when none is provided\n */\nexport const DEFAULT_REASON = \"Unknown\" as const;\n\n/**\n * Default priority for blockers\n */\nexport const DEFAULT_PRIORITY = 0 as const;\n\n/**\n * Default priority for async actions\n */\nexport const ASYNC_ACTION_PRIORITY = 20 as const;\n\n/**\n * DevTools store name\n */\nexport const DEVTOOLS_NAME = \"UIBlocking\" as const;\n","/**\n * Normalize scope to array format\n *\n * @param scope - Single scope or array of scopes\n * @returns Array of scopes\n *\n * @internal\n */\nexport function normalizeScopeToArray(scope: string | ReadonlyArray<string>): Array<string> {\n if (typeof scope === \"string\") {\n return [scope];\n }\n return Array.from(scope);\n}\n","import type { StateCreator } from \"zustand\";\nimport type {\n BlockerConfig,\n BlockerInfo,\n StoredBlocker,\n UIBlockingStore,\n} from \"./uiBlockingStore.types\";\nimport { DEFAULT_SCOPE, DEFAULT_REASON, DEFAULT_PRIORITY } from \"./uiBlockingStore.constants\";\nimport { normalizeScopeToArray } from \"./uiBlockingStore.utils\";\n\n/**\n * UI Blocking Store Slice\n *\n * Implements the state and actions for UI blocking management.\n * This slice follows the Zustand slice pattern for better code organization.\n */\nexport const createUIBlockingActions: StateCreator<UIBlockingStore, [], [], UIBlockingStore> = (\n set,\n get\n) => ({\n // State\n activeBlockers: new Map(),\n\n // Actions\n /**\n * Add a blocker to the store\n *\n * @param id - Unique blocker identifier\n * @param config - Optional blocker configuration\n *\n */\n addBlocker: (id: string, config: BlockerConfig = {}): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n const storedBlocker: StoredBlocker = {\n scope: config.scope ?? DEFAULT_SCOPE,\n reason: config.reason ?? DEFAULT_REASON,\n priority: config.priority ?? DEFAULT_PRIORITY,\n timestamp: config.timestamp ?? Date.now(),\n };\n newBlockers.set(id, storedBlocker);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Remove a blocker from the store\n *\n * @param id - Blocker identifier to remove\n *\n */\n removeBlocker: (id: string): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n newBlockers.delete(id);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Check if a scope is currently blocked\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the scopes are blocked\n *\n */\n isBlocked: (scope: string | ReadonlyArray<string> = DEFAULT_SCOPE): boolean => {\n const { activeBlockers } = get();\n const scopes = normalizeScopeToArray(scope);\n\n for (const [, blocker] of activeBlockers) {\n // Global blocking blocks everything\n if (blocker.scope === DEFAULT_SCOPE) {\n return true;\n }\n\n // Check scope intersection\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (scopes.some((s) => blockerScopes.includes(s))) {\n return true;\n }\n }\n\n return false;\n },\n\n /**\n * Get detailed blocking information for a specific scope\n *\n * @param scope - Scope to get blocking information for\n * @returns Array of blockers affecting the scope, sorted by priority (highest first)\n *\n */\n getBlockingInfo: (scope: string): ReadonlyArray<BlockerInfo> => {\n const { activeBlockers } = get();\n const blockers: Array<BlockerInfo> = [];\n\n for (const [id, blocker] of activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {\n blockers.push({ id, ...blocker });\n }\n }\n\n return blockers.sort((a, b) => b.priority - a.priority);\n },\n\n /**\n * Clear all blockers from the store\n */\n clearAllBlockers: (): void => {\n set({ activeBlockers: new Map() });\n },\n\n /**\n * Clear all blockers for a specific scope\n *\n * @param scope - Scope to clear blockers for\n *\n */\n clearBlockersForScope: (scope: string): void => {\n set((state) => {\n const newBlockers = new Map();\n\n for (const [id, blocker] of state.activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (!blockerScopes.includes(scope)) {\n newBlockers.set(id, blocker);\n }\n }\n\n return { activeBlockers: newBlockers };\n });\n },\n});\n","import type { DevtoolsOptions } from \"zustand/middleware\";\nimport { DEVTOOLS_NAME } from \"./uiBlockingStore.constants\";\n\n/**\n * DevTools configuration for UI Blocking Store\n *\n * Enables Redux DevTools integration only in development mode\n * for better debugging experience without production overhead.\n */\nexport const devtoolsConfig: DevtoolsOptions = {\n name: DEVTOOLS_NAME,\n enabled: process.env.NODE_ENV !== \"production\",\n};\n","import { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { createUIBlockingActions } from \"./uiBlockingStore.actions\";\nimport { devtoolsConfig } from \"./uiBlockingStore.config\";\nimport type { UIBlockingStore } from \"./uiBlockingStore.types\";\n\n/**\n * UI Blocking Store\n *\n * Global Zustand store for managing UI blocking state across the application.\n *\n * Features:\n * - Priority-based blocking system\n * - Scope-based blocking (global, specific areas, or multiple scopes)\n * - Automatic cleanup on unmount (when using hooks)\n * - DevTools integration for debugging\n *\n */\nexport const useUIBlockingStore = create<UIBlockingStore>()(\n devtools(createUIBlockingActions, devtoolsConfig)\n);\n","import { useEffect } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport type { BlockerConfig } from \"../store\";\n\n/**\n * Hook to automatically add and remove a blocker based on component lifecycle\n *\n * Automatically adds a blocker when the component mounts (or when activated)\n * and removes it when the component unmounts (or when deactivated).\n *\n * @param blockerId - Unique identifier for the blocker\n * @param config - Blocker configuration (scope, reason, priority)\n * @param isActive - Whether the blocker is currently active (default: true)\n *\n */\nexport const useBlocker = (blockerId: string, config: BlockerConfig, isActive = true): void => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n useEffect(() => {\n if (!isActive || !blockerId) {\n return;\n }\n\n addBlocker(blockerId, config);\n\n return () => {\n removeBlocker(blockerId);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [blockerId, isActive, addBlocker, removeBlocker]);\n};\n","import { useUIBlockingStore } from \"../store\";\n\n/**\n * Hook to check if a scope is currently blocked\n *\n * Returns a boolean indicating whether the specified scope(s) are blocked.\n * Automatically re-renders when the blocking state changes.\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the specified scopes are blocked\n *\n */\nexport const useIsBlocked = (scope?: string | ReadonlyArray<string>): boolean => {\n return useUIBlockingStore((state) => state.isBlocked(scope));\n};\n","import { useCallback } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport { ASYNC_ACTION_PRIORITY } from \"../store\";\n\n/**\n * Hook to wrap async functions with automatic blocking/unblocking\n *\n * Creates a wrapper function that automatically adds a blocker before executing\n * the async function and removes it after completion (success or failure).\n *\n * @template T - The return type of the async function\n * @param actionId - Unique identifier for the action (used in blocker ID and reason)\n * @param scope - Scope(s) to block during execution (default: \"global\")\n * @returns Function that wraps async functions with blocking behavior\n *\n */\nexport const useAsyncAction = <T = unknown>(\n actionId: string,\n scope?: string | ReadonlyArray<string>\n): ((asyncFn: () => Promise<T>) => Promise<T>) => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n const executeWithBlocking = useCallback(\n async (asyncFn: () => Promise<T>): Promise<T> => {\n const blockerId = `${actionId}-${Date.now().toString()}`;\n\n try {\n addBlocker(blockerId, {\n scope,\n reason: `Executing ${actionId}`,\n priority: ASYNC_ACTION_PRIORITY,\n });\n\n return await asyncFn();\n } finally {\n removeBlocker(blockerId);\n }\n },\n [actionId, scope, addBlocker, removeBlocker]\n );\n\n return executeWithBlocking;\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@okyrychenko-dev/react-action-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"clean": "rm -rf dist",
|
|
25
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
28
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
29
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
30
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
31
|
+
"test": "vitest",
|
|
32
|
+
"test:ui": "vitest --ui",
|
|
33
|
+
"test:run": "vitest run",
|
|
34
|
+
"test:coverage": "vitest run --coverage",
|
|
35
|
+
"prepare": "husky"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"react",
|
|
39
|
+
"ui-blocking",
|
|
40
|
+
"state-management",
|
|
41
|
+
"zustand",
|
|
42
|
+
"typescript",
|
|
43
|
+
"loading",
|
|
44
|
+
"async-operations"
|
|
45
|
+
],
|
|
46
|
+
"author": "Oleksandr Kyrychenko <your@email.com>",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/okyrychenko-dev/react-action-guard"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/okyrychenko-dev/react-action-guard/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/okyrychenko-dev/react-action-guard#readme",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
61
|
+
"zustand": "^4.5.7"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"immer": "^10.0.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@eslint/js": "^9.39.1",
|
|
68
|
+
"@testing-library/dom": "^10.4.1",
|
|
69
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
70
|
+
"@testing-library/react": "^16.3.0",
|
|
71
|
+
"@testing-library/user-event": "^14.6.1",
|
|
72
|
+
"@types/node": "^24.10.1",
|
|
73
|
+
"@types/react": "^19.2.5",
|
|
74
|
+
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
|
75
|
+
"@typescript-eslint/parser": "^8.46.4",
|
|
76
|
+
"@vitest/ui": "^4.0.9",
|
|
77
|
+
"eslint": "^9.39.1",
|
|
78
|
+
"eslint-config-prettier": "^10.1.8",
|
|
79
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
80
|
+
"happy-dom": "^20.0.10",
|
|
81
|
+
"husky": "^9.1.7",
|
|
82
|
+
"jsdom": "^27.2.0",
|
|
83
|
+
"prettier": "^3.6.2",
|
|
84
|
+
"react": "^19.2.0",
|
|
85
|
+
"react-dom": "^19.2.0",
|
|
86
|
+
"tsup": "^8.5.1",
|
|
87
|
+
"typescript": "^5.3.3",
|
|
88
|
+
"typescript-eslint": "^8.46.4",
|
|
89
|
+
"vitest": "^4.0.9",
|
|
90
|
+
"zustand": "^5.0.8"
|
|
91
|
+
}
|
|
92
|
+
}
|