@okyrychenko-dev/react-action-guard 0.4.0 → 0.6.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 +276 -63
- package/dist/index.cjs +178 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -53
- package/dist/index.d.ts +23 -53
- package/dist/index.js +176 -78
- 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
|
|
|
@@ -154,10 +154,12 @@ Gets detailed information about all active blockers for a specific scope.
|
|
|
154
154
|
**BlockerInfo:**
|
|
155
155
|
|
|
156
156
|
- `id: string` - Unique identifier of the blocker
|
|
157
|
-
- `reason
|
|
158
|
-
- `priority: number` - Priority level (0
|
|
157
|
+
- `reason: string` - Reason for blocking (defaults to "Unknown")
|
|
158
|
+
- `priority: number` - Priority level (higher = higher priority, minimum value is 0)
|
|
159
159
|
- `scope: string | string[]` - Scope(s) being blocked
|
|
160
|
-
- `timestamp: number` - When the blocker was added
|
|
160
|
+
- `timestamp: number` - When the blocker was added (milliseconds since epoch)
|
|
161
|
+
- `timeout?: number` - Optional timeout duration in milliseconds
|
|
162
|
+
- `onTimeout?: (blockerId: string) => void` - Optional callback when timeout expires
|
|
161
163
|
|
|
162
164
|
**Example:**
|
|
163
165
|
|
|
@@ -225,49 +227,66 @@ function ApiComponent() {
|
|
|
225
227
|
}
|
|
226
228
|
```
|
|
227
229
|
|
|
228
|
-
#### `useConfirmableBlocker(blockerId, config
|
|
230
|
+
#### `useConfirmableBlocker(blockerId, config)`
|
|
229
231
|
|
|
230
|
-
Creates a
|
|
232
|
+
Creates a confirmable action with UI blocking while the dialog is open or the action is running.
|
|
231
233
|
|
|
232
234
|
**Parameters:**
|
|
233
235
|
|
|
234
236
|
- `blockerId: string` - Unique identifier for the blocker
|
|
235
|
-
- `config:
|
|
236
|
-
- `
|
|
237
|
-
- `
|
|
238
|
-
- `
|
|
239
|
-
- `
|
|
237
|
+
- `config: ConfirmableBlockerConfig` - Configuration object
|
|
238
|
+
- `confirmMessage: string` - Message to show in confirmation dialog
|
|
239
|
+
- `confirmTitle?: string` - Dialog title (default: "Confirm Action")
|
|
240
|
+
- `confirmButtonText?: string` - Confirm button label (default: "Confirm")
|
|
241
|
+
- `cancelButtonText?: string` - Cancel button label (default: "Cancel")
|
|
242
|
+
- `onConfirm: () => void | Promise<void>` - Callback when user confirms
|
|
243
|
+
- `onCancel?: () => void` - Callback when user cancels
|
|
244
|
+
- Plus all `BlockerConfig` properties (scope, reason, priority, timeout)
|
|
240
245
|
|
|
241
246
|
**Returns:**
|
|
242
247
|
|
|
243
|
-
- `
|
|
244
|
-
- `
|
|
245
|
-
- `
|
|
248
|
+
- `execute: () => void` - Opens the confirmation dialog
|
|
249
|
+
- `isDialogOpen: boolean` - Whether the dialog is open
|
|
250
|
+
- `isExecuting: boolean` - Whether the confirm action is running
|
|
251
|
+
- `confirmConfig: { title, message, confirmText, cancelText }` - UI-ready dialog config
|
|
252
|
+
- `onConfirm: () => Promise<void>` - Confirm handler to wire to your dialog
|
|
253
|
+
- `onCancel: () => void` - Cancel handler to wire to your dialog
|
|
246
254
|
|
|
247
255
|
**Example:**
|
|
248
256
|
|
|
249
257
|
```jsx
|
|
250
|
-
function UnsavedChangesGuard({
|
|
251
|
-
const {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
function UnsavedChangesGuard({ discardChanges }) {
|
|
259
|
+
const {
|
|
260
|
+
execute,
|
|
261
|
+
isDialogOpen,
|
|
262
|
+
isExecuting,
|
|
263
|
+
confirmConfig,
|
|
264
|
+
onConfirm,
|
|
265
|
+
onCancel,
|
|
266
|
+
} = useConfirmableBlocker("unsaved-changes", {
|
|
267
|
+
scope: "navigation",
|
|
268
|
+
reason: "Unsaved changes",
|
|
269
|
+
confirmMessage: "You have unsaved changes. Are you sure you want to leave?",
|
|
270
|
+
onConfirm: async () => {
|
|
271
|
+
await discardChanges();
|
|
256
272
|
},
|
|
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
|
-
);
|
|
273
|
+
});
|
|
265
274
|
|
|
266
275
|
return (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
276
|
+
<>
|
|
277
|
+
<button onClick={execute}>Leave</button>
|
|
278
|
+
{isDialogOpen && (
|
|
279
|
+
<ConfirmDialog
|
|
280
|
+
title={confirmConfig.title}
|
|
281
|
+
message={confirmConfig.message}
|
|
282
|
+
confirmText={confirmConfig.confirmText}
|
|
283
|
+
cancelText={confirmConfig.cancelText}
|
|
284
|
+
onConfirm={onConfirm}
|
|
285
|
+
onCancel={onCancel}
|
|
286
|
+
/>
|
|
287
|
+
)}
|
|
288
|
+
{isExecuting && <LoadingOverlay message="Processing..." />}
|
|
289
|
+
</>
|
|
271
290
|
);
|
|
272
291
|
}
|
|
273
292
|
```
|
|
@@ -411,17 +430,25 @@ function MicroFrontend() {
|
|
|
411
430
|
|
|
412
431
|
#### `useUIBlockingStore`
|
|
413
432
|
|
|
414
|
-
Direct access to the Zustand store for advanced use cases.
|
|
433
|
+
Direct access to the Zustand store for advanced use cases (requires a selector).
|
|
415
434
|
|
|
416
435
|
**Methods:**
|
|
417
436
|
|
|
418
|
-
- `addBlocker(id, config)` - Manually add a blocker
|
|
437
|
+
- `addBlocker(id, config)` - Manually add a blocker (re-adding with the same ID replaces config)
|
|
438
|
+
- `updateBlocker(id, config)` - Update blocker metadata (timeout restarts if new `timeout` value provided)
|
|
419
439
|
- `removeBlocker(id)` - Manually remove a blocker
|
|
420
|
-
- `updateBlocker(id, config)` - Update an existing blocker
|
|
421
440
|
- `isBlocked(scope)` - Check if scope is blocked
|
|
422
441
|
- `getBlockingInfo(scope)` - Get detailed blocking information
|
|
423
|
-
- `clearAllBlockers()` - Remove all blockers
|
|
424
|
-
- `clearBlockersForScope(scope)` - Remove blockers for specific scope
|
|
442
|
+
- `clearAllBlockers()` - Remove all blockers (emits `"clear"` middleware event)
|
|
443
|
+
- `clearBlockersForScope(scope)` - Remove blockers for specific scope (emits `"clear_scope"` middleware event)
|
|
444
|
+
- `registerMiddleware(name, middleware)` - Register middleware manually
|
|
445
|
+
- `unregisterMiddleware(name)` - Unregister middleware manually
|
|
446
|
+
|
|
447
|
+
**Note about `updateBlocker` and timeouts:**
|
|
448
|
+
- If you pass a **new** `timeout` value, the timer will be **restarted**
|
|
449
|
+
- If you **don't** pass `timeout`, the existing timer continues unchanged
|
|
450
|
+
- Set `timeout: 0` to **clear** an existing timeout
|
|
451
|
+
- `addBlocker` always resets/creates a new timeout timer when called with the same ID
|
|
425
452
|
|
|
426
453
|
**Example:**
|
|
427
454
|
|
|
@@ -429,7 +456,11 @@ Direct access to the Zustand store for advanced use cases.
|
|
|
429
456
|
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
|
|
430
457
|
|
|
431
458
|
function AdvancedComponent() {
|
|
432
|
-
const { addBlocker,
|
|
459
|
+
const { addBlocker, updateBlocker, removeBlocker } = useUIBlockingStore((state) => ({
|
|
460
|
+
addBlocker: state.addBlocker,
|
|
461
|
+
updateBlocker: state.updateBlocker,
|
|
462
|
+
removeBlocker: state.removeBlocker,
|
|
463
|
+
}));
|
|
433
464
|
|
|
434
465
|
const startBlocking = () => {
|
|
435
466
|
addBlocker("custom-blocker", {
|
|
@@ -440,9 +471,13 @@ function AdvancedComponent() {
|
|
|
440
471
|
};
|
|
441
472
|
|
|
442
473
|
const updatePriority = () => {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
474
|
+
// Update metadata - timeout continues if not changed
|
|
475
|
+
updateBlocker("custom-blocker", { priority: 200 });
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const extendTimeout = () => {
|
|
479
|
+
// Update timeout - timer restarts with new value
|
|
480
|
+
updateBlocker("custom-blocker", { timeout: 60000 });
|
|
446
481
|
};
|
|
447
482
|
|
|
448
483
|
const stopBlocking = () => {
|
|
@@ -459,10 +494,54 @@ function AdvancedComponent() {
|
|
|
459
494
|
}
|
|
460
495
|
```
|
|
461
496
|
|
|
497
|
+
For non-hook contexts (tests, utilities, event handlers), use `uiBlockingStoreApi`:
|
|
498
|
+
|
|
499
|
+
```jsx
|
|
500
|
+
import { uiBlockingStoreApi } from "@okyrychenko-dev/react-action-guard";
|
|
501
|
+
|
|
502
|
+
uiBlockingStoreApi.getState().addBlocker("server-call", {
|
|
503
|
+
scope: "global",
|
|
504
|
+
reason: "Server call running",
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Store Toolkit Helpers
|
|
509
|
+
|
|
510
|
+
Advanced store helpers are re-exported from the internal toolkit for custom setups:
|
|
511
|
+
|
|
512
|
+
- `createShallowStore`
|
|
513
|
+
- `createStoreToolkit`
|
|
514
|
+
- `createStoreProvider`
|
|
515
|
+
- `createResolvedStoreHooks`
|
|
516
|
+
|
|
462
517
|
## Middleware System
|
|
463
518
|
|
|
464
519
|
The library includes a powerful middleware system that allows you to hook into blocker lifecycle events for analytics, logging, and performance monitoring.
|
|
465
520
|
|
|
521
|
+
### Middleware Actions
|
|
522
|
+
|
|
523
|
+
Middleware receives events for the following actions:
|
|
524
|
+
|
|
525
|
+
- `"add"` - Blocker was added
|
|
526
|
+
- `"update"` - Blocker metadata was updated
|
|
527
|
+
- `"remove"` - Blocker was removed
|
|
528
|
+
- `"timeout"` - Blocker was auto-removed due to timeout
|
|
529
|
+
- `"clear"` - All blockers were cleared (includes `count` field)
|
|
530
|
+
- `"clear_scope"` - Blockers for specific scope were cleared (includes `scope` and `count` fields)
|
|
531
|
+
|
|
532
|
+
**MiddlewareContext type:**
|
|
533
|
+
```typescript
|
|
534
|
+
{
|
|
535
|
+
action: "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
|
|
536
|
+
blockerId: string;
|
|
537
|
+
config?: BlockerConfig;
|
|
538
|
+
timestamp: number;
|
|
539
|
+
prevState?: BlockerConfig; // Available for "update" and "remove"
|
|
540
|
+
scope?: string; // Available for "clear_scope"
|
|
541
|
+
count?: number; // Available for "clear" and "clear_scope"
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
466
545
|
### Built-in Middleware
|
|
467
546
|
|
|
468
547
|
#### Analytics Middleware
|
|
@@ -524,6 +603,8 @@ configureMiddleware([
|
|
|
524
603
|
]);
|
|
525
604
|
```
|
|
526
605
|
|
|
606
|
+
Note: `configureMiddleware` registers middleware on the global store. If you use `UIBlockingProvider`, register middleware via the provider's `middlewares` prop instead.
|
|
607
|
+
|
|
527
608
|
### Custom Middleware
|
|
528
609
|
|
|
529
610
|
Create your own middleware to handle blocker events:
|
|
@@ -532,14 +613,20 @@ Create your own middleware to handle blocker events:
|
|
|
532
613
|
import { configureMiddleware } from "@okyrychenko-dev/react-action-guard";
|
|
533
614
|
|
|
534
615
|
const myCustomMiddleware = (context) => {
|
|
535
|
-
const { action, blockerId, config, timestamp } = context;
|
|
616
|
+
const { action, blockerId, config, timestamp, scope, count } = context;
|
|
536
617
|
|
|
537
618
|
if (action === "add") {
|
|
538
619
|
console.log(`Blocker added: ${blockerId}`, config);
|
|
539
|
-
} else if (action === "remove") {
|
|
540
|
-
console.log(`Blocker removed: ${blockerId}`);
|
|
541
620
|
} else if (action === "update") {
|
|
542
621
|
console.log(`Blocker updated: ${blockerId}`, config);
|
|
622
|
+
} else if (action === "remove") {
|
|
623
|
+
console.log(`Blocker removed: ${blockerId}`);
|
|
624
|
+
} else if (action === "timeout") {
|
|
625
|
+
console.log(`Blocker timed out: ${blockerId}`);
|
|
626
|
+
} else if (action === "clear") {
|
|
627
|
+
console.log(`All blockers cleared (${count} total)`);
|
|
628
|
+
} else if (action === "clear_scope") {
|
|
629
|
+
console.log(`Cleared ${count} blocker(s) for scope: ${scope}`);
|
|
543
630
|
}
|
|
544
631
|
};
|
|
545
632
|
|
|
@@ -599,23 +686,48 @@ import type {
|
|
|
599
686
|
// Core types
|
|
600
687
|
BlockerConfig,
|
|
601
688
|
BlockerInfo,
|
|
602
|
-
|
|
689
|
+
UIBlockingStore,
|
|
690
|
+
UIBlockingStoreState,
|
|
603
691
|
|
|
604
692
|
// Hook types
|
|
605
|
-
|
|
693
|
+
ConfirmableBlockerConfig,
|
|
694
|
+
ConfirmDialogConfig,
|
|
695
|
+
UseConfirmableBlockerReturn,
|
|
606
696
|
ScheduledBlockerConfig,
|
|
607
697
|
ConditionalBlockerConfig,
|
|
608
698
|
BlockingSchedule,
|
|
699
|
+
UseAsyncActionOptions,
|
|
609
700
|
|
|
610
701
|
// Middleware types
|
|
611
702
|
Middleware,
|
|
612
703
|
MiddlewareContext,
|
|
613
704
|
AnalyticsConfig,
|
|
614
705
|
AnalyticsProvider,
|
|
615
|
-
|
|
706
|
+
PerformanceConfig,
|
|
707
|
+
|
|
708
|
+
// Type-safe scopes
|
|
709
|
+
BlockerConfigTyped,
|
|
710
|
+
DefaultScopes,
|
|
711
|
+
ScopeValue,
|
|
616
712
|
} from "@okyrychenko-dev/react-action-guard";
|
|
617
713
|
```
|
|
618
714
|
|
|
715
|
+
## Type-Safe Scopes
|
|
716
|
+
|
|
717
|
+
Create typed versions of the hooks to prevent scope typos at compile time:
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
import { createTypedHooks } from "@okyrychenko-dev/react-action-guard";
|
|
721
|
+
|
|
722
|
+
type AppScopes = "global" | "form" | "navigation" | "checkout";
|
|
723
|
+
|
|
724
|
+
const { useBlocker, useIsBlocked, useAsyncAction, useBlockingInfo } =
|
|
725
|
+
createTypedHooks<AppScopes>();
|
|
726
|
+
|
|
727
|
+
useBlocker("save", { scope: "form" }); // OK
|
|
728
|
+
useBlocker("save", { scope: "typo" }); // Type error
|
|
729
|
+
```
|
|
730
|
+
|
|
619
731
|
## Use Cases
|
|
620
732
|
|
|
621
733
|
### Loading States
|
|
@@ -670,22 +782,22 @@ function FormWithUnsavedWarning() {
|
|
|
670
782
|
const [formData, setFormData] = useState({});
|
|
671
783
|
const [hasChanges, setHasChanges] = useState(false);
|
|
672
784
|
|
|
673
|
-
const {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
785
|
+
const {
|
|
786
|
+
execute,
|
|
787
|
+
isDialogOpen,
|
|
788
|
+
confirmConfig,
|
|
789
|
+
onConfirm,
|
|
790
|
+
onCancel,
|
|
791
|
+
} = useConfirmableBlocker("unsaved-form", {
|
|
792
|
+
scope: "navigation",
|
|
793
|
+
reason: "Unsaved form data",
|
|
794
|
+
priority: 100,
|
|
795
|
+
confirmMessage: "You have unsaved changes. Discard them?",
|
|
796
|
+
onConfirm: () => {
|
|
797
|
+
setFormData({});
|
|
798
|
+
setHasChanges(false);
|
|
679
799
|
},
|
|
680
|
-
|
|
681
|
-
enabled: hasChanges,
|
|
682
|
-
confirmMessage: "You have unsaved changes. Discard them?",
|
|
683
|
-
onConfirm: () => {
|
|
684
|
-
setFormData({});
|
|
685
|
-
setHasChanges(false);
|
|
686
|
-
},
|
|
687
|
-
}
|
|
688
|
-
);
|
|
800
|
+
});
|
|
689
801
|
|
|
690
802
|
return (
|
|
691
803
|
<form>
|
|
@@ -695,7 +807,17 @@ function FormWithUnsavedWarning() {
|
|
|
695
807
|
setHasChanges(true);
|
|
696
808
|
}}
|
|
697
809
|
/>
|
|
698
|
-
<button onClick={
|
|
810
|
+
<button type="button" onClick={execute}>Cancel</button>
|
|
811
|
+
{isDialogOpen && (
|
|
812
|
+
<ConfirmDialog
|
|
813
|
+
title={confirmConfig.title}
|
|
814
|
+
message={confirmConfig.message}
|
|
815
|
+
confirmText={confirmConfig.confirmText}
|
|
816
|
+
cancelText={confirmConfig.cancelText}
|
|
817
|
+
onConfirm={onConfirm}
|
|
818
|
+
onCancel={onCancel}
|
|
819
|
+
/>
|
|
820
|
+
)}
|
|
699
821
|
</form>
|
|
700
822
|
);
|
|
701
823
|
}
|
|
@@ -804,6 +926,97 @@ function StorageQuotaGuard() {
|
|
|
804
926
|
}
|
|
805
927
|
```
|
|
806
928
|
|
|
929
|
+
### Clearing Blockers on Navigation
|
|
930
|
+
|
|
931
|
+
Clear blockers when navigating away or on specific events:
|
|
932
|
+
|
|
933
|
+
```jsx
|
|
934
|
+
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
|
|
935
|
+
import { useEffect } from "react";
|
|
936
|
+
|
|
937
|
+
function CheckoutPage() {
|
|
938
|
+
const { clearBlockersForScope, clearAllBlockers } = useUIBlockingStore(
|
|
939
|
+
(state) => ({
|
|
940
|
+
clearBlockersForScope: state.clearBlockersForScope,
|
|
941
|
+
clearAllBlockers: state.clearAllBlockers,
|
|
942
|
+
})
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
// Clear checkout-specific blockers when leaving the page
|
|
946
|
+
useEffect(() => {
|
|
947
|
+
return () => {
|
|
948
|
+
// Clean up checkout blockers on unmount
|
|
949
|
+
clearBlockersForScope("checkout");
|
|
950
|
+
};
|
|
951
|
+
}, [clearBlockersForScope]);
|
|
952
|
+
|
|
953
|
+
const handleCancelOrder = () => {
|
|
954
|
+
// Clear all blockers when user explicitly cancels
|
|
955
|
+
clearAllBlockers();
|
|
956
|
+
navigate("/");
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
return (
|
|
960
|
+
<div>
|
|
961
|
+
<CheckoutForm />
|
|
962
|
+
<button onClick={handleCancelOrder}>Cancel Order</button>
|
|
963
|
+
</div>
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### Managing Session Timeouts with Dynamic Updates
|
|
969
|
+
|
|
970
|
+
Extend or update blocker timeouts dynamically:
|
|
971
|
+
|
|
972
|
+
```jsx
|
|
973
|
+
import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
|
|
974
|
+
|
|
975
|
+
function SessionManager() {
|
|
976
|
+
const { addBlocker, updateBlocker, removeBlocker } = useUIBlockingStore(
|
|
977
|
+
(state) => ({
|
|
978
|
+
addBlocker: state.addBlocker,
|
|
979
|
+
updateBlocker: state.updateBlocker,
|
|
980
|
+
removeBlocker: state.removeBlocker,
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
const startSession = () => {
|
|
985
|
+
addBlocker("session-timeout", {
|
|
986
|
+
scope: "global",
|
|
987
|
+
reason: "Session expiring soon",
|
|
988
|
+
priority: 50,
|
|
989
|
+
timeout: 300000, // 5 minutes
|
|
990
|
+
onTimeout: () => {
|
|
991
|
+
logout();
|
|
992
|
+
showNotification("Session expired");
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
const extendSession = () => {
|
|
998
|
+
// Extend timeout - timer restarts with new value
|
|
999
|
+
updateBlocker("session-timeout", {
|
|
1000
|
+
timeout: 600000, // 10 minutes
|
|
1001
|
+
reason: "Session extended",
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const endSession = () => {
|
|
1006
|
+
removeBlocker("session-timeout");
|
|
1007
|
+
logout();
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
return (
|
|
1011
|
+
<div>
|
|
1012
|
+
<button onClick={startSession}>Start Session</button>
|
|
1013
|
+
<button onClick={extendSession}>Extend Session</button>
|
|
1014
|
+
<button onClick={endSession}>Logout</button>
|
|
1015
|
+
</div>
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
807
1020
|
## Development
|
|
808
1021
|
|
|
809
1022
|
```bash
|