Compare commits
43 Commits
dev-global
...
483d78c45d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
483d78c45d | ||
|
|
a15b67f30a | ||
|
|
28ccd8af56 | ||
|
|
3887d08fca | ||
|
|
b43072f1cf | ||
|
|
0b2ab98fcf | ||
|
|
afb7a3346f | ||
|
|
95e2973d44 | ||
|
|
cb340b2a13 | ||
|
|
e1b26f3f77 | ||
|
|
580c4b21cd | ||
|
|
7c5935c362 | ||
|
|
40ae30e6ea | ||
|
|
a1f34fbf7b | ||
|
|
e48ab9b686 | ||
|
|
3f9c4c5539 | ||
|
|
6cb50978d0 | ||
| 31e46e6bd2 | |||
| 7f0286dada | |||
| 2d64055cea | |||
| 02d73254d9 | |||
| 128923290d | |||
| 3314c69ef9 | |||
| bc5d2d2a4f | |||
| bc422e7d66 | |||
| 252530610b | |||
| a748a39d2f | |||
| 8928432fe0 | |||
| 6ff395e9be | |||
| 00e5a70aef | |||
| f365d7b0e0 | |||
| 210a1d44e7 | |||
| c2113357f2 | |||
| 2e23b259ab | |||
| 552a1e5979 | |||
| 9097e2f1e0 | |||
| b521d04cd0 | |||
| 690cb22306 | |||
| 6edac91ea8 | |||
| da69c80cff | |||
| e40730eaef | |||
| d7b1eb26f3 | |||
| 7bf94f306a |
91
CHANGELOG.md
91
CHANGELOG.md
@@ -1,5 +1,96 @@
|
|||||||
# @warkypublic/zustandsyncstore
|
# @warkypublic/zustandsyncstore
|
||||||
|
|
||||||
|
## 0.0.47
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- a15b67f: fix(Gridler): update ready state management logic
|
||||||
|
|
||||||
|
## 0.0.46
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 0b2ab98: row selection with incorrect values
|
||||||
|
|
||||||
|
## 0.0.45
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- cb340b2: chore: ⬆ Update deps
|
||||||
|
fix: empty key, should no do rownumber call
|
||||||
|
|
||||||
|
## 0.0.44
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 40ae30e: Select first row bug fixes
|
||||||
|
|
||||||
|
## 0.0.43
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 3f9c4c5: Gridler selection fixes
|
||||||
|
|
||||||
|
## 0.0.42
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2d64055: fix(Former): update request type to FormRequestType for consistency
|
||||||
|
|
||||||
|
## 0.0.41
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 3314c69: feat(Former): add useFormerState hook for managing form state
|
||||||
|
|
||||||
|
## 0.0.40
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 8928432: feat(Former): add keep open functionality and update onClose behavior
|
||||||
|
|
||||||
|
## 0.0.39
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 00e5a70: feat(Former): enhance state management with additional callbacks and state retrieval
|
||||||
|
|
||||||
|
## 0.0.38
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 210a1d4: feat(GlobalStateStore): add initialization state and update actions
|
||||||
|
|
||||||
|
## 0.0.37
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- feat(GlobalStateStore): prevent saving during initial load
|
||||||
|
|
||||||
|
## 0.0.36
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- feat(GlobalStateStore): implement storage loading and saving logic
|
||||||
|
|
||||||
|
## 0.0.35
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- fix(api): response handling in FormerRestHeadSpecAPI
|
||||||
|
|
||||||
|
## 0.0.34
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Better GlobalStateStore
|
||||||
|
|
||||||
|
## 0.0.33
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- d7b1eb2: feat(error-manager): implement centralized error reporting system
|
||||||
|
|
||||||
## 0.0.32
|
## 0.0.32
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
35
package.json
35
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@warkypublic/oranguru",
|
"name": "@warkypublic/oranguru",
|
||||||
"author": "Warky Devs",
|
"author": "Warky Devs",
|
||||||
"version": "0.0.32",
|
"version": "0.0.47",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./dist/lib.d.ts",
|
"types": "./dist/lib.d.ts",
|
||||||
"main": "./dist/lib.cjs.js",
|
"main": "./dist/lib.cjs.js",
|
||||||
@@ -43,34 +43,39 @@
|
|||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"mcp": "node mcp/server.js"
|
"mcp": "node mcp/server.js"
|
||||||
},
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://git.warky.dev/wdevs/oranguru.git"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"@tanstack/react-virtual": "^3.13.18",
|
"@tanstack/react-virtual": "^3.13.18",
|
||||||
"moment": "^2.30.1"
|
"moment": "^2.30.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/changelog-git": "^0.2.1",
|
"@changesets/changelog-git": "^0.2.1",
|
||||||
"@changesets/cli": "^2.29.8",
|
"@changesets/cli": "^2.29.8",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^10.0.1",
|
||||||
"@microsoft/api-extractor": "^7.56.0",
|
"@microsoft/api-extractor": "^7.56.3",
|
||||||
"@storybook/react-vite": "^10.2.3",
|
"@sentry/react": "^10.38.0",
|
||||||
|
"@storybook/react-vite": "^10.2.8",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/jsdom": "~27.0.0",
|
"@types/jsdom": "~27.0.0",
|
||||||
"@types/node": "^25.2.0",
|
"@types/node": "^25.2.3",
|
||||||
"@types/react": "^19.2.10",
|
"@types/react": "^19.2.13",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/use-sync-external-store": "~1.5.0",
|
"@types/use-sync-external-store": "~1.5.0",
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.55.0",
|
||||||
"@vitejs/plugin-react-swc": "^4.2.3",
|
"@vitejs/plugin-react-swc": "^4.2.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-mantine": "^4.0.3",
|
"eslint-config-mantine": "^4.0.3",
|
||||||
"eslint-plugin-perfectionist": "^5.4.0",
|
"eslint-plugin-perfectionist": "^5.5.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.0",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"eslint-plugin-storybook": "^10.2.3",
|
"eslint-plugin-storybook": "^10.2.8",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"globals": "^17.3.0",
|
"globals": "^17.3.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
@@ -82,12 +87,12 @@
|
|||||||
"prettier-eslint": "^16.4.2",
|
"prettier-eslint": "^16.4.2",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"storybook": "^10.2.3",
|
"storybook": "^10.2.8",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.54.0",
|
"typescript-eslint": "^8.55.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-dts": "^4.5.4",
|
"vite-plugin-dts": "^4.5.4",
|
||||||
"vite-tsconfig-paths": "^6.0.5",
|
"vite-tsconfig-paths": "^6.1.0",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -108,4 +113,4 @@
|
|||||||
"use-sync-external-store": ">= 1.4.0",
|
"use-sync-external-store": ">= 1.4.0",
|
||||||
"zustand": ">= 5.0.0"
|
"zustand": ">= 5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
902
pnpm-lock.yaml
generated
902
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,8 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import React, { type PropsWithChildren } from 'react';
|
import React, { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import errorManager from './ErrorManager';
|
||||||
|
|
||||||
interface ErrorBoundaryProps extends PropsWithChildren {
|
interface ErrorBoundaryProps extends PropsWithChildren {
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
onReportClick?: () => void;
|
onReportClick?: () => void;
|
||||||
@@ -43,7 +46,12 @@ export class ReactBasicErrorBoundary extends React.PureComponent<
|
|||||||
errorInfo,
|
errorInfo,
|
||||||
try: false,
|
try: false,
|
||||||
});
|
});
|
||||||
// You can also log error messages to an error reporting service here
|
|
||||||
|
// Report error to error manager (Sentry, custom API, etc.)
|
||||||
|
errorManager.reportError(error, errorInfo, {
|
||||||
|
componentStack: errorInfo?.componentStack,
|
||||||
|
namespace: this.props.namespace,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Button, Code, Collapse, Group, Paper, rem, Text } from '@mantine/core';
|
import { Button, Code, Collapse, Group, Paper, rem, Text } from '@mantine/core';
|
||||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||||
import React, { type PropsWithChildren } from 'react';
|
import React, { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import errorManager from './ErrorManager';
|
||||||
|
|
||||||
let ErrorBoundaryOptions = {
|
let ErrorBoundaryOptions = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
onError: undefined,
|
onError: undefined,
|
||||||
@@ -68,7 +71,12 @@ export class ReactErrorBoundary extends React.Component<ErrorBoundaryProps, Erro
|
|||||||
if (typeof GetErrorBoundaryOptions()?.onError === 'function') {
|
if (typeof GetErrorBoundaryOptions()?.onError === 'function') {
|
||||||
GetErrorBoundaryOptions()?.onError?.(error, errorInfo);
|
GetErrorBoundaryOptions()?.onError?.(error, errorInfo);
|
||||||
}
|
}
|
||||||
// You can also log error messages to an error reporting service here
|
|
||||||
|
// Report error to error manager (Sentry, custom API, etc.)
|
||||||
|
errorManager.reportError(error, errorInfo, {
|
||||||
|
componentStack: errorInfo?.componentStack,
|
||||||
|
namespace: this.props.namespace,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -202,6 +210,19 @@ export class ReactErrorBoundary extends React.Component<ErrorBoundaryProps, Erro
|
|||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manually report error if user clicks report button
|
||||||
|
if (this.state.error && this.state.errorInfo) {
|
||||||
|
await errorManager.reportError(this.state.error, this.state.errorInfo, {
|
||||||
|
componentStack: this.state.errorInfo?.componentStack,
|
||||||
|
namespace: this.props.namespace,
|
||||||
|
tags: { reportedBy: 'user' },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState(() => ({
|
||||||
|
reported: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
|||||||
166
src/ErrorBoundary/ErrorManager.README.md
Normal file
166
src/ErrorBoundary/ErrorManager.README.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# ErrorManager
|
||||||
|
|
||||||
|
Centralized error reporting for ErrorBoundary components.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Sentry Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { errorManager } from './ErrorBoundary';
|
||||||
|
|
||||||
|
errorManager.configure({
|
||||||
|
enabled: true,
|
||||||
|
sentry: {
|
||||||
|
dsn: 'https://your-sentry-dsn@sentry.io/project-id',
|
||||||
|
environment: 'production',
|
||||||
|
release: '1.0.0',
|
||||||
|
sampleRate: 1.0,
|
||||||
|
ignoreErrors: ['ResizeObserver loop limit exceeded'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom API Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
errorManager.configure({
|
||||||
|
enabled: true,
|
||||||
|
customAPI: {
|
||||||
|
endpoint: 'https://api.yourapp.com/errors',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer token',
|
||||||
|
},
|
||||||
|
transformPayload: (report) => ({
|
||||||
|
message: report.error.message,
|
||||||
|
stack: report.error.stack,
|
||||||
|
level: report.severity,
|
||||||
|
timestamp: report.timestamp,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Reporter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
errorManager.configure({
|
||||||
|
enabled: true,
|
||||||
|
reporters: [
|
||||||
|
{
|
||||||
|
name: 'CustomLogger',
|
||||||
|
isEnabled: () => true,
|
||||||
|
captureError: async (report) => {
|
||||||
|
console.error('Error:', report.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Reporters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
errorManager.configure({
|
||||||
|
enabled: true,
|
||||||
|
sentry: { dsn: 'your-dsn' },
|
||||||
|
customAPI: { endpoint: 'your-endpoint' },
|
||||||
|
reporters: [customReporter],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
### beforeReport
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
errorManager.configure({
|
||||||
|
beforeReport: (report) => {
|
||||||
|
// Filter errors
|
||||||
|
if (report.error.message.includes('ResizeObserver')) {
|
||||||
|
return null; // Skip reporting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich with user data
|
||||||
|
report.context = {
|
||||||
|
...report.context,
|
||||||
|
user: { id: getCurrentUserId() },
|
||||||
|
tags: { feature: 'checkout' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return report;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### onReportSuccess / onReportFailure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
errorManager.configure({
|
||||||
|
onReportSuccess: (report) => {
|
||||||
|
console.log('Error reported successfully');
|
||||||
|
},
|
||||||
|
onReportFailure: (error, report) => {
|
||||||
|
console.error('Failed to report error:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Reporting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
riskyOperation();
|
||||||
|
} catch (error) {
|
||||||
|
await errorManager.reportError(error as Error, null, {
|
||||||
|
namespace: 'checkout',
|
||||||
|
tags: { step: 'payment' },
|
||||||
|
extra: { orderId: '123' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Disable/Enable
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Disable reporting
|
||||||
|
errorManager.configure({ enabled: false });
|
||||||
|
|
||||||
|
// Enable reporting
|
||||||
|
errorManager.configure({ enabled: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
## ErrorBoundary Integration
|
||||||
|
|
||||||
|
Automatic - errors caught by `ReactErrorBoundary` or `ReactBasicErrorBoundary` are automatically reported.
|
||||||
|
|
||||||
|
Manual report button in `ReactErrorBoundary` UI also sends to ErrorManager.
|
||||||
|
|
||||||
|
## Install Sentry (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @sentry/react
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ErrorSeverity = 'fatal' | 'error' | 'warning' | 'info' | 'debug';
|
||||||
|
|
||||||
|
interface ErrorContext {
|
||||||
|
namespace?: string;
|
||||||
|
componentStack?: string;
|
||||||
|
user?: Record<string, any>;
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
extra?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorReport {
|
||||||
|
error: Error;
|
||||||
|
errorInfo?: any;
|
||||||
|
severity?: ErrorSeverity;
|
||||||
|
context?: ErrorContext;
|
||||||
|
timestamp?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
194
src/ErrorBoundary/ErrorManager.ts
Normal file
194
src/ErrorBoundary/ErrorManager.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import type {
|
||||||
|
CustomAPIConfig,
|
||||||
|
ErrorManagerConfig,
|
||||||
|
ErrorReport,
|
||||||
|
ErrorReporter,
|
||||||
|
SentryConfig,
|
||||||
|
} from './ErrorManager.types';
|
||||||
|
|
||||||
|
class ErrorManager {
|
||||||
|
private config: ErrorManagerConfig = { enabled: true };
|
||||||
|
private reporters: ErrorReporter[] = [];
|
||||||
|
private sentryInstance: any = null;
|
||||||
|
|
||||||
|
configure(config: ErrorManagerConfig) {
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
this.reporters = [];
|
||||||
|
|
||||||
|
if (config.sentry) {
|
||||||
|
this.setupSentry(config.sentry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.customAPI) {
|
||||||
|
this.setupCustomAPI(config.customAPI);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.reporters) {
|
||||||
|
this.reporters.push(...config.reporters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.reporters = [];
|
||||||
|
this.sentryInstance = null;
|
||||||
|
this.config = { enabled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
getReporters(): ErrorReporter[] {
|
||||||
|
return [...this.reporters];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return Boolean(this.config.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reportError(
|
||||||
|
error: Error,
|
||||||
|
errorInfo?: any,
|
||||||
|
context?: ErrorReport['context']
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let report: ErrorReport = {
|
||||||
|
context,
|
||||||
|
error,
|
||||||
|
errorInfo,
|
||||||
|
severity: 'error',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config.beforeReport) {
|
||||||
|
const modifiedReport = this.config.beforeReport(report);
|
||||||
|
if (!modifiedReport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
report = modifiedReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportPromises = this.reporters
|
||||||
|
.filter((reporter) => reporter.isEnabled())
|
||||||
|
.map(async (reporter) => {
|
||||||
|
try {
|
||||||
|
await reporter.captureError(report);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reporter "${reporter.name}" failed:`, error);
|
||||||
|
if (this.config.onReportFailure) {
|
||||||
|
this.config.onReportFailure(error as Error, report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(reportPromises);
|
||||||
|
if (this.config.onReportSuccess) {
|
||||||
|
this.config.onReportSuccess(report);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reporting failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupCustomAPI(config: CustomAPIConfig) {
|
||||||
|
const customAPIReporter: ErrorReporter = {
|
||||||
|
captureError: async (report: ErrorReport) => {
|
||||||
|
try {
|
||||||
|
const payload = config.transformPayload
|
||||||
|
? config.transformPayload(report)
|
||||||
|
: {
|
||||||
|
context: report.context,
|
||||||
|
message: report.error.message,
|
||||||
|
severity: report.severity || 'error',
|
||||||
|
stack: report.error.stack,
|
||||||
|
timestamp: report.timestamp || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(config.endpoint, {
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...config.headers,
|
||||||
|
},
|
||||||
|
method: config.method || 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send error to custom API:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled: () => Boolean(config.endpoint),
|
||||||
|
name: 'CustomAPI',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.reporters.push(customAPIReporter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSentry(config: SentryConfig) {
|
||||||
|
const sentryReporter: ErrorReporter = {
|
||||||
|
captureError: async (report: ErrorReport) => {
|
||||||
|
if (!this.sentryInstance) {
|
||||||
|
try {
|
||||||
|
const Sentry = await import('@sentry/react');
|
||||||
|
Sentry.init({
|
||||||
|
beforeSend: config.beforeSend,
|
||||||
|
dsn: config.dsn,
|
||||||
|
environment: config.environment,
|
||||||
|
ignoreErrors: config.ignoreErrors,
|
||||||
|
release: config.release,
|
||||||
|
tracesSampleRate: config.sampleRate || 1.0,
|
||||||
|
});
|
||||||
|
this.sentryInstance = Sentry;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize Sentry:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
this.sentryInstance.withScope((scope: any) => {
|
||||||
|
if (report.severity) {
|
||||||
|
scope.setLevel(report.severity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.context?.namespace) {
|
||||||
|
scope.setTag('namespace', report.context.namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.context?.tags) {
|
||||||
|
Object.entries(report.context.tags).forEach(([key, value]) => {
|
||||||
|
scope.setTag(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.context?.user) {
|
||||||
|
scope.setUser(report.context.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.context?.extra) {
|
||||||
|
scope.setExtras(report.context.extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.context?.componentStack) {
|
||||||
|
scope.setContext('react', {
|
||||||
|
componentStack: report.context.componentStack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sentryInstance.captureException(report.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isEnabled: () => Boolean(this.sentryInstance),
|
||||||
|
name: 'Sentry',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.reporters.push(sentryReporter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errorManager = new ErrorManager();
|
||||||
|
|
||||||
|
export default errorManager;
|
||||||
50
src/ErrorBoundary/ErrorManager.types.ts
Normal file
50
src/ErrorBoundary/ErrorManager.types.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
export interface CustomAPIConfig {
|
||||||
|
endpoint: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
method?: 'POST' | 'PUT';
|
||||||
|
transformPayload?: (report: ErrorReport) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorContext {
|
||||||
|
componentStack?: string;
|
||||||
|
extra?: Record<string, any>;
|
||||||
|
namespace?: string;
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
user?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorManagerConfig {
|
||||||
|
beforeReport?: (report: ErrorReport) => ErrorReport | null;
|
||||||
|
customAPI?: CustomAPIConfig;
|
||||||
|
enabled?: boolean;
|
||||||
|
onReportFailure?: (error: Error, report: ErrorReport) => void;
|
||||||
|
onReportSuccess?: (report: ErrorReport) => void;
|
||||||
|
reporters?: ErrorReporter[];
|
||||||
|
sentry?: SentryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorReport {
|
||||||
|
context?: ErrorContext;
|
||||||
|
error: Error;
|
||||||
|
errorInfo?: any;
|
||||||
|
severity?: ErrorSeverity;
|
||||||
|
timestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorReporter {
|
||||||
|
captureError: (report: ErrorReport) => Promise<void> | void;
|
||||||
|
isEnabled: () => boolean;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ErrorSeverity = 'debug' | 'error' | 'fatal' | 'info' | 'warning';
|
||||||
|
|
||||||
|
export interface SentryConfig {
|
||||||
|
beforeSend?: (event: any) => any | null;
|
||||||
|
dsn: string;
|
||||||
|
environment?: string;
|
||||||
|
ignoreErrors?: string[];
|
||||||
|
release?: string;
|
||||||
|
sampleRate?: number;
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export { default as ReactBasicErrorBoundary } from './BasicErrorBoundary';
|
export { default as ReactBasicErrorBoundary } from './BasicErrorBoundary';
|
||||||
export { default as ReactErrorBoundary } from './ErrorBoundary';
|
export { default as ReactErrorBoundary } from './ErrorBoundary';
|
||||||
|
export { default as errorManager } from './ErrorManager';
|
||||||
|
export * from './ErrorManager.types';
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { newUUID } from '@warkypublic/artemis-kit';
|
|||||||
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
|
|
||||||
import type { FormerProps, FormerState } from './Former.types';
|
import type { FormerProps, FormerState, FormStateAndProps } from './Former.types';
|
||||||
|
|
||||||
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
||||||
FormerState<any> & Partial<FormerProps<any>>,
|
FormerState<any> & Partial<FormerProps<any>>,
|
||||||
FormerProps<any>
|
FormerProps<any>
|
||||||
>(
|
>(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
|
getAllState: () => {
|
||||||
|
return get() as FormStateAndProps<any>;
|
||||||
|
},
|
||||||
getState: (key) => {
|
getState: (key) => {
|
||||||
const current = get();
|
const current = get();
|
||||||
return current?.[key];
|
return current?.[key];
|
||||||
@@ -26,17 +29,19 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
keyValue
|
keyValue
|
||||||
);
|
);
|
||||||
if (get().afterGet) {
|
if (get().afterGet) {
|
||||||
data = await get().afterGet!({ ...data });
|
data = await get().afterGet!({ ...data }, get());
|
||||||
}
|
}
|
||||||
set({ loading: false, values: data });
|
set({ loading: false, values: data });
|
||||||
get().onChange?.(data);
|
get().onChange?.(data, get());
|
||||||
}
|
}
|
||||||
if (reset && get().getFormMethods) {
|
if (reset && get().getFormMethods) {
|
||||||
const formMethods = get().getFormMethods!();
|
const formMethods = get().getFormMethods!();
|
||||||
formMethods.reset();
|
formMethods.reset();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
set({ error: (e as Error)?.message ?? e, loading: false });
|
const errorMessage = (e as Error)?.message ?? e;
|
||||||
|
set({ error: errorMessage, loading: false });
|
||||||
|
get().onError?.(errorMessage, get());
|
||||||
}
|
}
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
},
|
},
|
||||||
@@ -66,7 +71,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
let data = formMethods.getValues();
|
let data = formMethods.getValues();
|
||||||
|
|
||||||
if (get().beforeSave) {
|
if (get().beforeSave) {
|
||||||
const newData = await get().beforeSave!(data);
|
const newData = await get().beforeSave!(data, get());
|
||||||
data = newData;
|
data = newData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +81,9 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
data = newdata;
|
data = newdata;
|
||||||
},
|
},
|
||||||
(errors) => {
|
(errors) => {
|
||||||
set({ error: errors.root?.message || 'Validation errors', loading: false });
|
const errorMessage = errors.root?.message || 'Validation errors';
|
||||||
|
set({ error: errorMessage, loading: false });
|
||||||
|
get().onError?.(errorMessage, get());
|
||||||
exit = true;
|
exit = true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -107,29 +114,49 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
data,
|
data,
|
||||||
keyValue
|
keyValue
|
||||||
);
|
);
|
||||||
|
const newData = { ...data, ...savedData }; //Merge what we had. In case the API doesn't return all fields, we don't want to lose them
|
||||||
if (get().afterSave) {
|
if (get().afterSave) {
|
||||||
await get().afterSave!(savedData);
|
await get().afterSave!(newData, get());
|
||||||
}
|
}
|
||||||
set({ loading: false, values: savedData });
|
|
||||||
get().onChange?.(savedData);
|
if (keepOpen) {
|
||||||
formMethods.reset(savedData); //reset with saved data to clear dirty state
|
const keyName = get()?.uniqueKeyField || 'id';
|
||||||
if (!keepOpen) {
|
const clearedData = { ...newData };
|
||||||
get().onClose?.(savedData);
|
delete clearedData[keyName];
|
||||||
|
set({ loading: false, values: clearedData });
|
||||||
|
get().onChange?.(clearedData, get());
|
||||||
|
formMethods.reset(clearedData);
|
||||||
|
return newData;
|
||||||
}
|
}
|
||||||
return savedData;
|
|
||||||
|
set({ loading: false, values: newData });
|
||||||
|
get().onChange?.(newData, get());
|
||||||
|
formMethods.reset(newData); //reset with saved data to clear dirty state
|
||||||
|
get().onClose?.(newData);
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepOpen) {
|
||||||
|
const keyName = get()?.uniqueKeyField || 'id';
|
||||||
|
const clearedData = { ...data };
|
||||||
|
delete clearedData[keyName];
|
||||||
|
set({ loading: false, values: clearedData });
|
||||||
|
formMethods.reset(clearedData);
|
||||||
|
get().onChange?.(clearedData, get());
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
set({ loading: false, values: data });
|
set({ loading: false, values: data });
|
||||||
formMethods.reset(data); //reset with saved data to clear dirty state
|
formMethods.reset(data); //reset with saved data to clear dirty state
|
||||||
get().onChange?.(data);
|
get().onChange?.(data, get());
|
||||||
if (!keepOpen) {
|
get().onClose?.(data);
|
||||||
get().onClose?.(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
set({ error: (e as Error)?.message ?? e, loading: false });
|
const errorMessage = (e as Error)?.message ?? e;
|
||||||
|
set({ error: errorMessage, loading: false });
|
||||||
|
get().onError?.(errorMessage, get());
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -181,20 +208,20 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: !id ? newUUID() : id,
|
id: !id ? newUUID() : id,
|
||||||
onClose: () => {
|
onClose: (data?: any) => {
|
||||||
const dirty = useStoreApi.getState().dirty;
|
const dirty = useStoreApi.getState().dirty;
|
||||||
const setState = useStoreApi.getState().setState;
|
const setState = useStoreApi.getState().setState;
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
if (confirm('You have unsaved changes. Are you sure you want to close?')) {
|
if (confirm('You have unsaved changes. Are you sure you want to close?')) {
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose(data);
|
||||||
} else {
|
} else {
|
||||||
setState('opened', false);
|
setState('opened', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose(data);
|
||||||
} else {
|
} else {
|
||||||
setState('opened', false);
|
setState('opened', false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
ref: any
|
ref: any
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
|
getAllState,
|
||||||
getState,
|
getState,
|
||||||
onChange,
|
onChange,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -26,6 +27,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
values,
|
values,
|
||||||
wrapper,
|
wrapper,
|
||||||
} = useFormerStore((state) => ({
|
} = useFormerStore((state) => ({
|
||||||
|
getAllState: state.getAllState,
|
||||||
getState: state.getState,
|
getState: state.getState,
|
||||||
onChange: state.onChange,
|
onChange: state.onChange,
|
||||||
onClose: state.onClose,
|
onClose: state.onClose,
|
||||||
@@ -54,7 +56,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
() => ({
|
() => ({
|
||||||
close: async () => {
|
close: async () => {
|
||||||
//console.log('close called');
|
//console.log('close called');
|
||||||
onClose?.();
|
onClose?.(getState('values'));
|
||||||
setState('opened', false);
|
setState('opened', false);
|
||||||
},
|
},
|
||||||
getValue: () => {
|
getValue: () => {
|
||||||
@@ -67,7 +69,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
return await save();
|
return await save();
|
||||||
},
|
},
|
||||||
setValue: (value: T) => {
|
setValue: (value: T) => {
|
||||||
onChange?.(value);
|
onChange?.(value, getAllState());
|
||||||
},
|
},
|
||||||
show: async () => {
|
show: async () => {
|
||||||
//console.log('show called');
|
//console.log('show called');
|
||||||
@@ -78,7 +80,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
return await validate();
|
return await validate();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[getState, onChange, validate, save, reset, setState, onClose, onOpen]
|
[getState, getAllState, onChange, validate, save, reset, setState, onClose, onOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,7 +99,19 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
{typeof wrapper === 'function' ? (
|
{typeof wrapper === 'function' ? (
|
||||||
wrapper(<FormerLayout>{props.children}</FormerLayout>, opened ??false, onClose ?? (() => {setState('opened', false)}), onOpen ?? (() => {setState('opened', true)}), getState)
|
wrapper(
|
||||||
|
<FormerLayout>{props.children}</FormerLayout>,
|
||||||
|
opened ?? false,
|
||||||
|
onClose ??
|
||||||
|
(() => {
|
||||||
|
setState('opened', false);
|
||||||
|
}),
|
||||||
|
onOpen ??
|
||||||
|
(() => {
|
||||||
|
setState('opened', true);
|
||||||
|
}),
|
||||||
|
getState
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<FormerLayout>{props.children || null}</FormerLayout>
|
<FormerLayout>{props.children || null}</FormerLayout>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,23 +7,25 @@ import type {
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
|
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { FormRequestType } from '../Gridler/utils/types';
|
||||||
|
|
||||||
export type FormerAPICallType<T extends FieldValues = any> = (
|
export type FormerAPICallType<T extends FieldValues = any> = (
|
||||||
mode: 'mutate' | 'read',
|
mode: 'mutate' | 'read',
|
||||||
request: RequestType,
|
request: FormRequestType,
|
||||||
value?: T,
|
value?: T,
|
||||||
key?: number | string
|
key?: number | string
|
||||||
) => Promise<T>;
|
) => Promise<T>;
|
||||||
|
|
||||||
export interface FormerProps<T extends FieldValues = any> {
|
export interface FormerProps<T extends FieldValues = any> {
|
||||||
afterGet?: (data: T) => Promise<T> | void;
|
afterGet?: (data: T, state: Partial<FormStateAndProps<T>>) => Promise<T> | void;
|
||||||
afterSave?: (data: T) => Promise<void> | void;
|
afterSave?: (data: T, state: Partial<FormStateAndProps<T>>) => Promise<void> | void;
|
||||||
beforeSave?: (data: T) => Promise<T> | T;
|
beforeSave?: (data: T, state: Partial<FormStateAndProps<T>>) => Promise<T> | T;
|
||||||
dirty?: boolean;
|
dirty?: boolean;
|
||||||
disableHTMlForm?: boolean;
|
disableHTMlForm?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
keepOpen?: boolean;
|
keepOpen?: boolean;
|
||||||
layout?: {
|
layout?: {
|
||||||
buttonArea?: "bottom" | "none" | "top";
|
buttonArea?: 'bottom' | 'none' | 'top';
|
||||||
buttonAreaGroupProps?: GroupProps;
|
buttonAreaGroupProps?: GroupProps;
|
||||||
closeButtonProps?: ButtonProps;
|
closeButtonProps?: ButtonProps;
|
||||||
closeButtonTitle?: React.ReactNode;
|
closeButtonTitle?: React.ReactNode;
|
||||||
@@ -31,18 +33,19 @@ export interface FormerProps<T extends FieldValues = any> {
|
|||||||
renderTop?: FormerSectionRender<T>;
|
renderTop?: FormerSectionRender<T>;
|
||||||
saveButtonProps?: ButtonProps;
|
saveButtonProps?: ButtonProps;
|
||||||
saveButtonTitle?: React.ReactNode;
|
saveButtonTitle?: React.ReactNode;
|
||||||
|
showKeepOpenSwitch?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
onAPICall?: FormerAPICallType<T>;
|
onAPICall?: FormerAPICallType<T>;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onChange?: (value: T) => void;
|
onChange?: (value: T, state: Partial<FormStateAndProps<T>>) => void;
|
||||||
onClose?: (data?: T) => void;
|
onClose?: (data?: T | undefined) => void;
|
||||||
onConfirmDelete?: (values?: T) => Promise<boolean>;
|
onConfirmDelete?: (values?: T) => Promise<boolean>;
|
||||||
|
onError?: (error: Error | string, state: Partial<FormStateAndProps<T>>) => void;
|
||||||
onOpen?: (data?: T) => void;
|
onOpen?: (data?: T) => void;
|
||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
primeData?: T;
|
primeData?: T;
|
||||||
request: RequestType;
|
request: FormRequestType;
|
||||||
uniqueKeyField?: string;
|
uniqueKeyField?: string;
|
||||||
useFormProps?: UseFormProps<T>;
|
useFormProps?: UseFormProps<T>;
|
||||||
values?: T;
|
values?: T;
|
||||||
@@ -62,15 +65,16 @@ export interface FormerRef<T extends FieldValues = any> {
|
|||||||
|
|
||||||
export type FormerSectionRender<T extends FieldValues = any> = (
|
export type FormerSectionRender<T extends FieldValues = any> = (
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
opened: boolean ,
|
opened: boolean,
|
||||||
onClose: ((data?: T) => void),
|
onClose: (data?: T) => void,
|
||||||
onOpen: ((data?: T) => void) ,
|
onOpen: (data?: T) => void,
|
||||||
getState: FormerState<T>['getState']
|
getState: FormerState<T>['getState']
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
|
|
||||||
export interface FormerState<T extends FieldValues = any> {
|
export interface FormerState<T extends FieldValues = any> {
|
||||||
deleteConfirmed?: boolean;
|
deleteConfirmed?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
getAllState: () => FormStateAndProps<T>;
|
||||||
getFormMethods?: () => UseFormReturn<any, any>;
|
getFormMethods?: () => UseFormReturn<any, any>;
|
||||||
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K];
|
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K];
|
||||||
load: (reset?: boolean) => Promise<void>;
|
load: (reset?: boolean) => Promise<void>;
|
||||||
@@ -79,7 +83,7 @@ export interface FormerState<T extends FieldValues = any> {
|
|||||||
reset: (e?: React.BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>;
|
reset: (e?: React.BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>;
|
||||||
save: (e?: React.BaseSyntheticEvent<object, any, any> | undefined) => Promise<T | undefined>;
|
save: (e?: React.BaseSyntheticEvent<object, any, any> | undefined) => Promise<T | undefined>;
|
||||||
scrollAreaProps?: ScrollAreaAutosizeProps;
|
scrollAreaProps?: ScrollAreaAutosizeProps;
|
||||||
setRequest: (request: RequestType) => void;
|
setRequest: (request: FormRequestType) => void;
|
||||||
setState: <K extends keyof FormStateAndProps<T>>(
|
setState: <K extends keyof FormStateAndProps<T>>(
|
||||||
key: K,
|
key: K,
|
||||||
value: Partial<FormStateAndProps<T>>[K]
|
value: Partial<FormStateAndProps<T>>[K]
|
||||||
@@ -93,5 +97,3 @@ export interface FormerState<T extends FieldValues = any> {
|
|||||||
|
|
||||||
export type FormStateAndProps<T extends FieldValues = any> = FormerProps<T> &
|
export type FormStateAndProps<T extends FieldValues = any> = FormerProps<T> &
|
||||||
Partial<FormerState<T>>;
|
Partial<FormerState<T>>;
|
||||||
|
|
||||||
export type RequestType = 'delete' | 'insert' | 'select' | 'update' | 'view';
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Group, Tooltip } from '@mantine/core';
|
import { Button, Group, Switch, Tooltip } from '@mantine/core';
|
||||||
import { IconDeviceFloppy, IconX } from '@tabler/icons-react';
|
import { IconDeviceFloppy, IconX } from '@tabler/icons-react';
|
||||||
|
|
||||||
import { useFormerStore } from './Former.store';
|
import { useFormerStore } from './Former.store';
|
||||||
@@ -9,21 +9,29 @@ export const FormerButtonArea = () => {
|
|||||||
closeButtonProps,
|
closeButtonProps,
|
||||||
closeButtonTitle,
|
closeButtonTitle,
|
||||||
dirty,
|
dirty,
|
||||||
|
getState,
|
||||||
|
keepOpen,
|
||||||
onClose,
|
onClose,
|
||||||
request,
|
request,
|
||||||
save,
|
save,
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
saveButtonTitle,
|
saveButtonTitle,
|
||||||
|
setState,
|
||||||
|
showKeepOpenSwitch,
|
||||||
} = useFormerStore((state) => ({
|
} = useFormerStore((state) => ({
|
||||||
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
|
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
|
||||||
closeButtonProps: state.layout?.closeButtonProps,
|
closeButtonProps: state.layout?.closeButtonProps,
|
||||||
closeButtonTitle: state.layout?.closeButtonTitle,
|
closeButtonTitle: state.layout?.closeButtonTitle,
|
||||||
dirty: state.dirty,
|
dirty: state.dirty,
|
||||||
|
getState: state.getState,
|
||||||
|
keepOpen: state.keepOpen,
|
||||||
onClose: state.onClose,
|
onClose: state.onClose,
|
||||||
request: state.request,
|
request: state.request,
|
||||||
save: state.save,
|
save: state.save,
|
||||||
saveButtonProps: state.layout?.saveButtonProps,
|
saveButtonProps: state.layout?.saveButtonProps,
|
||||||
saveButtonTitle: state.layout?.saveButtonTitle,
|
saveButtonTitle: state.layout?.saveButtonTitle,
|
||||||
|
setState: state.setState,
|
||||||
|
showKeepOpenSwitch: state.layout?.showKeepOpenSwitch,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const disabledSave =
|
const disabledSave =
|
||||||
@@ -47,12 +55,19 @@ export const FormerButtonArea = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
{...closeButtonProps}
|
{...closeButtonProps}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose(getState('values'));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{closeButtonTitle || 'Close'}
|
{closeButtonTitle || 'Close'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{showKeepOpenSwitch && (
|
||||||
|
<Switch
|
||||||
|
checked={keepOpen}
|
||||||
|
label="Keep Open"
|
||||||
|
onChange={(event) => setState('keepOpen', event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={
|
label={
|
||||||
disabledSave ? (
|
disabledSave ? (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import type { FormerAPICallType } from './Former.types';
|
import type { FormerAPICallType } from './Former.types';
|
||||||
|
|
||||||
interface ResolveSpecRequest {
|
interface ResolveSpecRequest {
|
||||||
@@ -61,11 +62,15 @@ function FormerResolveSpecAPI(options: {
|
|||||||
|
|
||||||
const response = await fetch(url, fetchOptions);
|
const response = await fetch(url, fetchOptions);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
if (text && text.length > 4) {
|
||||||
|
throw new Error(`${text}`);
|
||||||
|
}
|
||||||
throw new Error(`API request failed with status ${response.status}`);
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data as any;
|
return data as unknown;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,15 @@ function FormerRestHeadSpecAPI(options: {
|
|||||||
|
|
||||||
const response = await fetch(url, fetchOptions);
|
const response = await fetch(url, fetchOptions);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
if (text && text.length > 4) {
|
||||||
|
throw new Error(`${text}`);
|
||||||
|
}
|
||||||
throw new Error(`API request failed with status ${response.status}`);
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'read') {
|
const data = await response.json();
|
||||||
const data = await response.json();
|
return data as unknown;
|
||||||
return data as any;
|
|
||||||
} else {
|
|
||||||
return value as any;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export { FormerButtonArea } from './FormerButtonArea';
|
|||||||
export { FormerResolveSpecAPI } from './FormerResolveSpecAPI';
|
export { FormerResolveSpecAPI } from './FormerResolveSpecAPI';
|
||||||
export { FormerRestHeadSpecAPI } from './FormerRestHeadSpecAPI';
|
export { FormerRestHeadSpecAPI } from './FormerRestHeadSpecAPI';
|
||||||
export { FormerDialog, FormerModel, FormerPopover } from './FormerWrappers';
|
export { FormerDialog, FormerModel, FormerPopover } from './FormerWrappers';
|
||||||
|
export { useFormerState } from './use-former-state';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { fn } from 'storybook/test';
|
|||||||
|
|
||||||
import { FormTest } from './example';
|
import { FormTest } from './example';
|
||||||
|
|
||||||
const Renderable = (props: any) => {
|
const Renderable = (props: unknown) => {
|
||||||
return (
|
return (
|
||||||
<Box h="100%" mih="400px" miw="400px" w="100%">
|
<Box h="100%" mih="400px" miw="400px" w="100%">
|
||||||
<FormTest {...props} />
|
<FormTest {...props} />
|
||||||
|
|||||||
41
src/Former/use-former-state.tsx
Normal file
41
src/Former/use-former-state.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { FieldValues } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import type { FormerProps } from './Former.types';
|
||||||
|
|
||||||
|
export type UseFormerStateProps<T extends FieldValues = FieldValues> = Pick<
|
||||||
|
FormerProps<T>,
|
||||||
|
'onChange' | 'onClose' | 'opened' | 'primeData' | 'request' | 'values'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useFormerState = <T extends FieldValues = FieldValues>(
|
||||||
|
options?: Partial<UseFormerStateProps<T>>
|
||||||
|
) => {
|
||||||
|
const [state, setState] = useState<UseFormerStateProps<T>>({
|
||||||
|
onChange: options?.onChange,
|
||||||
|
onClose: options?.onClose ?? (() => setState((cv) => ({ ...cv, opened: false }))),
|
||||||
|
opened: options?.opened ?? false,
|
||||||
|
primeData: options?.primeData ?? options?.values,
|
||||||
|
request: options?.request ?? 'insert',
|
||||||
|
values: options?.values,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateState = (updates: Partial<UseFormerStateProps<T>>) => {
|
||||||
|
setState((prev) => ({ ...prev, ...updates }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const { onChange, onClose, opened, ...formerProps } = state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
former: { ...formerProps, onChange },
|
||||||
|
formerWrapper: { onClose, opened } as {
|
||||||
|
onClose: Required<UseFormerStateProps<T>>['onClose'];
|
||||||
|
opened: Required<UseFormerStateProps<T>>['opened'];
|
||||||
|
},
|
||||||
|
open: (request: UseFormerStateProps<T>['request'], data: UseFormerStateProps<T>['values']) => {
|
||||||
|
setState((cv) => ({ ...cv, opened: true, primeData: data, request, values: data }));
|
||||||
|
},
|
||||||
|
updateState,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -5,7 +5,6 @@ import { useStoreWithEqualityFn } from 'zustand/traditional';
|
|||||||
import { createStore } from 'zustand/vanilla';
|
import { createStore } from 'zustand/vanilla';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
|
||||||
BarState,
|
BarState,
|
||||||
ExtractState,
|
ExtractState,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
@@ -21,10 +20,7 @@ import type {
|
|||||||
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
|
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
|
||||||
|
|
||||||
const initialState: GlobalState = {
|
const initialState: GlobalState = {
|
||||||
app: {
|
initialized: false,
|
||||||
controls: {},
|
|
||||||
environment: 'production',
|
|
||||||
},
|
|
||||||
layout: {
|
layout: {
|
||||||
bottomBar: { open: false },
|
bottomBar: { open: false },
|
||||||
leftBar: { open: false },
|
leftBar: { open: false },
|
||||||
@@ -35,10 +31,14 @@ const initialState: GlobalState = {
|
|||||||
menu: [],
|
menu: [],
|
||||||
},
|
},
|
||||||
owner: {
|
owner: {
|
||||||
|
guid: '',
|
||||||
id: 0,
|
id: 0,
|
||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
program: {
|
program: {
|
||||||
|
controls: {},
|
||||||
|
environment: 'production',
|
||||||
|
guid: '',
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
},
|
},
|
||||||
@@ -47,8 +47,10 @@ const initialState: GlobalState = {
|
|||||||
authToken: '',
|
authToken: '',
|
||||||
connected: true,
|
connected: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
loggedIn: false,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
|
guid: '',
|
||||||
username: '',
|
username: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -140,20 +142,14 @@ const createNavigationSlice = (set: SetState) => ({
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createAppSlice = (set: SetState) => ({
|
const createComplexActions = (set: SetState, get: GetState) => {
|
||||||
setApp: (updates: Partial<AppState>) =>
|
// Internal implementation without lock
|
||||||
set((state: GlobalState) => ({
|
const fetchDataInternal = async (url?: string) => {
|
||||||
app: { ...state.app, ...updates },
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
const createComplexActions = (set: SetState, get: GetState) => ({
|
|
||||||
fetchData: async (url?: string) => {
|
|
||||||
try {
|
try {
|
||||||
set((state: GlobalState) => ({
|
set((state: GlobalState) => ({
|
||||||
session: {
|
session: {
|
||||||
...state.session,
|
...state.session,
|
||||||
apiURL: url ?? state.session.apiURL,
|
apiURL: url || state.session.apiURL,
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -164,9 +160,15 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
|||||||
set((state: GlobalState) => ({
|
set((state: GlobalState) => ({
|
||||||
...state,
|
...state,
|
||||||
...result,
|
...result,
|
||||||
app: {
|
layout: { ...state.layout, ...result?.layout },
|
||||||
...state.app,
|
navigation: { ...state.navigation, ...result?.navigation },
|
||||||
...result?.app,
|
owner: {
|
||||||
|
...state.owner,
|
||||||
|
...result?.owner,
|
||||||
|
},
|
||||||
|
program: {
|
||||||
|
...state.program,
|
||||||
|
...result?.program,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
@@ -175,6 +177,10 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
|||||||
connected: true,
|
connected: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
...result?.user,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
set((state: GlobalState) => ({
|
set((state: GlobalState) => ({
|
||||||
@@ -186,29 +192,168 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchData: async (url?: string) => {
|
||||||
|
// Wait for initialization to complete
|
||||||
|
await waitForInitialization();
|
||||||
|
|
||||||
|
// Use lock to prevent concurrent fetchData calls
|
||||||
|
return withOperationLock(() => fetchDataInternal(url));
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoggedIn: (): boolean => {
|
||||||
|
const session = get().session;
|
||||||
|
if (!session.loggedIn || !session.authToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (session.expiryDate && new Date(session.expiryDate) < new Date()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
login: async (authToken?: string) => {
|
login: async (authToken?: string, user?: Partial<UserState>) => {
|
||||||
set((state: GlobalState) => ({
|
// Wait for initialization to complete
|
||||||
session: {
|
await waitForInitialization();
|
||||||
...state.session,
|
|
||||||
authToken: authToken ?? '',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
await get().fetchData();
|
|
||||||
},
|
|
||||||
|
|
||||||
logout: async () => {
|
// Use lock to prevent concurrent auth operations
|
||||||
set((state: GlobalState) => ({
|
return withOperationLock(async () => {
|
||||||
...initialState,
|
try {
|
||||||
session: {
|
set((state: GlobalState) => ({
|
||||||
...initialState.session,
|
session: {
|
||||||
apiURL: state.session.apiURL,
|
...state.session,
|
||||||
},
|
authToken: authToken ?? '',
|
||||||
}));
|
expiryDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||||
await get().fetchData();
|
loading: true,
|
||||||
},
|
loggedIn: true,
|
||||||
});
|
},
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
...user,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const currentState = get();
|
||||||
|
const result = await currentState.onLogin?.(currentState);
|
||||||
|
if (result) {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
...state,
|
||||||
|
owner: result.owner ? { ...state.owner, ...result.owner } : state.owner,
|
||||||
|
program: result.program ? { ...state.program, ...result.program } : state.program,
|
||||||
|
session: result.session ? { ...state.session, ...result.session } : state.session,
|
||||||
|
user: result.user ? { ...state.user, ...result.user } : state.user,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Call internal version to avoid nested lock
|
||||||
|
await fetchDataInternal();
|
||||||
|
} catch (e) {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
session: {
|
||||||
|
...state.session,
|
||||||
|
connected: false,
|
||||||
|
error: `Login Exception: ${String(e)}`,
|
||||||
|
loading: false,
|
||||||
|
loggedIn: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
session: {
|
||||||
|
...state.session,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: async () => {
|
||||||
|
// Wait for initialization to complete
|
||||||
|
await waitForInitialization();
|
||||||
|
|
||||||
|
// Use lock to prevent concurrent auth operations
|
||||||
|
return withOperationLock(async () => {
|
||||||
|
try {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
...initialState,
|
||||||
|
session: {
|
||||||
|
...initialState.session,
|
||||||
|
apiURL: state.session.apiURL,
|
||||||
|
expiryDate: undefined,
|
||||||
|
loading: true,
|
||||||
|
loggedIn: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const currentState = get();
|
||||||
|
const result = await currentState.onLogout?.(currentState);
|
||||||
|
if (result) {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
...state,
|
||||||
|
owner: result.owner ? { ...state.owner, ...result.owner } : state.owner,
|
||||||
|
program: result.program ? { ...state.program, ...result.program } : state.program,
|
||||||
|
session: result.session ? { ...state.session, ...result.session } : state.session,
|
||||||
|
user: result.user ? { ...state.user, ...result.user } : state.user,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Call internal version to avoid nested lock
|
||||||
|
await fetchDataInternal();
|
||||||
|
} catch (e) {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
session: {
|
||||||
|
...state.session,
|
||||||
|
connected: false,
|
||||||
|
error: `Logout Exception: ${String(e)}`,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
session: {
|
||||||
|
...state.session,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// State management flags and locks - must be defined before store creation
|
||||||
|
let isStorageInitialized = false;
|
||||||
|
let initializationPromise: null | Promise<void> = null;
|
||||||
|
let operationLock: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
// Helper to wait for initialization - must be defined before store creation
|
||||||
|
const waitForInitialization = async (): Promise<void> => {
|
||||||
|
if (initializationPromise) {
|
||||||
|
await initializationPromise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to ensure async operations run sequentially
|
||||||
|
const withOperationLock = async <T>(operation: () => Promise<T>): Promise<T> => {
|
||||||
|
const currentLock = operationLock;
|
||||||
|
let releaseLock: () => void;
|
||||||
|
|
||||||
|
// Create new lock promise
|
||||||
|
operationLock = new Promise<void>((resolve) => {
|
||||||
|
releaseLock = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for previous operation to complete
|
||||||
|
await currentLock;
|
||||||
|
// Run the operation
|
||||||
|
return await operation();
|
||||||
|
} finally {
|
||||||
|
// Release the lock
|
||||||
|
releaseLock!();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const GlobalStateStore = createStore<GlobalStateStoreType>((set, get) => ({
|
const GlobalStateStore = createStore<GlobalStateStoreType>((set, get) => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -218,28 +363,46 @@ const GlobalStateStore = createStore<GlobalStateStoreType>((set, get) => ({
|
|||||||
...createUserSlice(set),
|
...createUserSlice(set),
|
||||||
...createLayoutSlice(set),
|
...createLayoutSlice(set),
|
||||||
...createNavigationSlice(set),
|
...createNavigationSlice(set),
|
||||||
...createAppSlice(set),
|
|
||||||
...createComplexActions(set, get),
|
...createComplexActions(set, get),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
loadStorage()
|
// Initialize storage and load saved state
|
||||||
|
initializationPromise = loadStorage()
|
||||||
.then((state) => {
|
.then((state) => {
|
||||||
GlobalStateStore.setState((current) => ({
|
// Merge loaded state with initial state
|
||||||
...current,
|
GlobalStateStore.setState(
|
||||||
...state,
|
(current) => ({
|
||||||
session: {
|
...current,
|
||||||
...current.session,
|
...state,
|
||||||
...state.session,
|
initialized: true,
|
||||||
connected: true,
|
session: {
|
||||||
loading: false,
|
...current.session,
|
||||||
},
|
...state.session,
|
||||||
}));
|
connected: true,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
true // Replace state completely to avoid triggering subscription during init
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('Error loading storage:', e);
|
console.error('Error loading storage:', e);
|
||||||
|
// Mark as initialized even on error so app doesn't hang
|
||||||
|
GlobalStateStore.setState({ initialized: true });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Mark initialization as complete
|
||||||
|
isStorageInitialized = true;
|
||||||
|
initializationPromise = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscribe to state changes and persist to storage
|
||||||
|
// Only saves after initialization is complete
|
||||||
GlobalStateStore.subscribe((state) => {
|
GlobalStateStore.subscribe((state) => {
|
||||||
|
if (!isStorageInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
saveStorage(state).catch((e) => {
|
saveStorage(state).catch((e) => {
|
||||||
console.error('Error saving storage:', e);
|
console.error('Error saving storage:', e);
|
||||||
});
|
});
|
||||||
@@ -260,11 +423,15 @@ const setApiURL = (url: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getApiURL = (): string => {
|
const getApiURL = (): string => {
|
||||||
return GlobalStateStore.getState().session.apiURL;
|
return GlobalStateStore.getState().session.apiURL ?? '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAuthToken = (): string => {
|
const getAuthToken = (): string => {
|
||||||
return GlobalStateStore.getState().session.authToken;
|
return GlobalStateStore.getState().session.authToken ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoggedIn = (): boolean => {
|
||||||
|
return GlobalStateStore.getState().isLoggedIn();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setAuthToken = (token: string) => {
|
const setAuthToken = (token: string) => {
|
||||||
@@ -273,6 +440,15 @@ const setAuthToken = (token: string) => {
|
|||||||
|
|
||||||
const GetGlobalState = (): GlobalStateStoreType => {
|
const GetGlobalState = (): GlobalStateStoreType => {
|
||||||
return GlobalStateStore.getState();
|
return GlobalStateStore.getState();
|
||||||
}
|
};
|
||||||
|
|
||||||
export { getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, setApiURL, setAuthToken, useGlobalStateStore };
|
export {
|
||||||
|
getApiURL,
|
||||||
|
getAuthToken,
|
||||||
|
GetGlobalState,
|
||||||
|
GlobalStateStore,
|
||||||
|
isLoggedIn,
|
||||||
|
setApiURL,
|
||||||
|
setAuthToken,
|
||||||
|
useGlobalStateStore,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
interface AppState {
|
|
||||||
controls?: Record<string, any>;
|
|
||||||
environment: 'development' | 'production';
|
|
||||||
globals?: Record<string, any>;
|
|
||||||
updatedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BarState {
|
interface BarState {
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
@@ -14,7 +8,6 @@ interface BarState {
|
|||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
render?: () => React.ReactNode;
|
render?: () => React.ReactNode;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseDetail = {
|
type DatabaseDetail = {
|
||||||
@@ -25,7 +18,7 @@ type DatabaseDetail = {
|
|||||||
type ExtractState<S> = S extends { getState: () => infer X } ? X : never;
|
type ExtractState<S> = S extends { getState: () => infer X } ? X : never;
|
||||||
|
|
||||||
interface GlobalState {
|
interface GlobalState {
|
||||||
app: AppState;
|
initialized: boolean;
|
||||||
layout: LayoutState;
|
layout: LayoutState;
|
||||||
navigation: NavigationState;
|
navigation: NavigationState;
|
||||||
owner: OwnerState;
|
owner: OwnerState;
|
||||||
@@ -38,15 +31,19 @@ interface GlobalStateActions {
|
|||||||
// Complex actions
|
// Complex actions
|
||||||
fetchData: (url?: string) => Promise<void>;
|
fetchData: (url?: string) => Promise<void>;
|
||||||
|
|
||||||
login: (authToken?: string) => Promise<void>;
|
isLoggedIn: () => boolean;
|
||||||
|
login: (authToken?: string, user?: Partial<UserState>) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
// Callback for custom fetch logic
|
// Callbacks for custom logic
|
||||||
onFetchSession?: (state: GlobalState) => Promise<Partial<GlobalState>>;
|
onFetchSession?: (state: Partial<GlobalState>) => Promise<Partial<GlobalState>>;
|
||||||
|
onLogin?: (
|
||||||
|
state: Partial<GlobalState>
|
||||||
|
) => Promise<Partial<Pick<GlobalState, 'owner' | 'program' | 'session' | 'user'>> | void>;
|
||||||
|
onLogout?: (
|
||||||
|
state: Partial<GlobalState>
|
||||||
|
) => Promise<Partial<Pick<GlobalState, 'owner' | 'program' | 'session' | 'user'>> | void>;
|
||||||
setApiURL: (url: string) => void;
|
setApiURL: (url: string) => void;
|
||||||
|
|
||||||
// App actions
|
|
||||||
setApp: (updates: Partial<AppState>) => void;
|
|
||||||
|
|
||||||
setAuthToken: (token: string) => void;
|
setAuthToken: (token: string) => void;
|
||||||
setBottomBar: (updates: Partial<BarState>) => void;
|
setBottomBar: (updates: Partial<BarState>) => void;
|
||||||
setCurrentPage: (page: PageInfo) => void;
|
setCurrentPage: (page: PageInfo) => void;
|
||||||
@@ -101,7 +98,8 @@ interface NavigationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface OwnerState {
|
interface OwnerState {
|
||||||
id: number;
|
guid?: string;
|
||||||
|
id?: number;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
name: string;
|
name: string;
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
@@ -118,34 +116,31 @@ type PageInfo = {
|
|||||||
interface ProgramState {
|
interface ProgramState {
|
||||||
backendVersion?: string;
|
backendVersion?: string;
|
||||||
bigLogo?: string;
|
bigLogo?: string;
|
||||||
|
controls?: Record<string, any>;
|
||||||
database?: DatabaseDetail;
|
database?: DatabaseDetail;
|
||||||
databaseVersion?: string;
|
databaseVersion?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
environment: 'development' | 'production';
|
||||||
|
globals?: Record<string, any>;
|
||||||
|
guid?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
meta?: Record<string, any>;
|
meta?: Record<string, any>;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
version?: string;
|
updatedAt?: string;
|
||||||
}
|
|
||||||
|
|
||||||
interface ProgramWrapperProps {
|
|
||||||
apiURL?: string;
|
|
||||||
children: React.ReactNode | React.ReactNode[];
|
|
||||||
debugMode?: boolean;
|
|
||||||
fallback?: React.ReactNode | React.ReactNode[];
|
|
||||||
renderFallback?: boolean;
|
|
||||||
testMode?: boolean;
|
|
||||||
version?: string;
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionState {
|
interface SessionState {
|
||||||
apiURL: string;
|
apiURL?: string;
|
||||||
authToken: string;
|
authToken?: string;
|
||||||
connected: boolean;
|
connected?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
expiryDate?: string;
|
||||||
isSecurity?: boolean;
|
isSecurity?: boolean;
|
||||||
loading: boolean;
|
loading?: boolean;
|
||||||
|
loggedIn?: boolean;
|
||||||
meta?: Record<string, any>;
|
meta?: Record<string, any>;
|
||||||
parameters?: Record<string, any>;
|
parameters?: Record<string, any>;
|
||||||
}
|
}
|
||||||
@@ -169,7 +164,6 @@ interface UserState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AppState,
|
|
||||||
BarState,
|
BarState,
|
||||||
ExtractState,
|
ExtractState,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
@@ -181,7 +175,6 @@ export type {
|
|||||||
OwnerState,
|
OwnerState,
|
||||||
PageInfo,
|
PageInfo,
|
||||||
ProgramState,
|
ProgramState,
|
||||||
ProgramWrapperProps,
|
|
||||||
SessionState,
|
SessionState,
|
||||||
ThemeSettings,
|
ThemeSettings,
|
||||||
UserState,
|
UserState,
|
||||||
|
|||||||
255
src/GlobalStateStore/GlobalStateStore.utils.test.ts
Normal file
255
src/GlobalStateStore/GlobalStateStore.utils.test.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { GlobalState } from './GlobalStateStore.types';
|
||||||
|
|
||||||
|
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
|
||||||
|
|
||||||
|
// Mock idb-keyval
|
||||||
|
vi.mock('idb-keyval', () => ({
|
||||||
|
get: vi.fn(),
|
||||||
|
set: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { get, set } from 'idb-keyval';
|
||||||
|
|
||||||
|
describe('GlobalStateStore.utils', () => {
|
||||||
|
const mockState: GlobalState = {
|
||||||
|
initialized: false,
|
||||||
|
layout: {
|
||||||
|
bottomBar: { open: false },
|
||||||
|
leftBar: { open: false },
|
||||||
|
rightBar: { open: false },
|
||||||
|
topBar: { open: false },
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
menu: [],
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
guid: 'owner-guid',
|
||||||
|
id: 1,
|
||||||
|
name: 'Test Owner',
|
||||||
|
},
|
||||||
|
program: {
|
||||||
|
controls: {},
|
||||||
|
environment: 'production',
|
||||||
|
guid: 'program-guid',
|
||||||
|
name: 'Test Program',
|
||||||
|
slug: 'test-program',
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
apiURL: 'https://api.test.com',
|
||||||
|
authToken: 'test-token',
|
||||||
|
connected: true,
|
||||||
|
loading: false,
|
||||||
|
loggedIn: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
guid: 'user-guid',
|
||||||
|
username: 'testuser',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
localStorage.clear();
|
||||||
|
|
||||||
|
// Mock indexedDB to be available
|
||||||
|
Object.defineProperty(globalThis, 'indexedDB', {
|
||||||
|
configurable: true,
|
||||||
|
value: {},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveStorage', () => {
|
||||||
|
it('saves each key separately with prefixed storage keys', async () => {
|
||||||
|
(set as any).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await saveStorage(mockState);
|
||||||
|
|
||||||
|
// Verify IndexedDB calls for non-session keys
|
||||||
|
expect(set).toHaveBeenCalledWith('APP_GLO:layout', expect.any(String));
|
||||||
|
expect(set).toHaveBeenCalledWith('APP_GLO:navigation', expect.any(String));
|
||||||
|
expect(set).toHaveBeenCalledWith('APP_GLO:owner', expect.any(String));
|
||||||
|
expect(set).toHaveBeenCalledWith('APP_GLO:program', expect.any(String));
|
||||||
|
expect(set).toHaveBeenCalledWith('APP_GLO:user', expect.any(String));
|
||||||
|
|
||||||
|
// Verify session is NOT saved to IndexedDB
|
||||||
|
expect(set).not.toHaveBeenCalledWith('APP_GLO:session', expect.any(String));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves session key to localStorage only', async () => {
|
||||||
|
await saveStorage(mockState);
|
||||||
|
|
||||||
|
// Verify session is in localStorage
|
||||||
|
const sessionData = localStorage.getItem('APP_GLO:session');
|
||||||
|
expect(sessionData).toBeTruthy();
|
||||||
|
|
||||||
|
const parsedSession = JSON.parse(sessionData!);
|
||||||
|
expect(parsedSession.apiURL).toBe('https://api.test.com');
|
||||||
|
expect(parsedSession.authToken).toBe('test-token');
|
||||||
|
expect(parsedSession.loggedIn).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters out skipped paths', async () => {
|
||||||
|
(set as any).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const stateWithControls: GlobalState = {
|
||||||
|
...mockState,
|
||||||
|
program: {
|
||||||
|
...mockState.program,
|
||||||
|
controls: { test: 'value' },
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
...mockState.session,
|
||||||
|
connected: true,
|
||||||
|
error: 'test error',
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveStorage(stateWithControls);
|
||||||
|
|
||||||
|
// Get the saved program data
|
||||||
|
const programCall = (set as any).mock.calls.find(
|
||||||
|
(call: any[]) => call[0] === 'APP_GLO:program'
|
||||||
|
);
|
||||||
|
expect(programCall).toBeDefined();
|
||||||
|
const savedProgram = JSON.parse(programCall[1]);
|
||||||
|
|
||||||
|
// Controls should be filtered out (program.controls is in SKIP_PATHS as app.controls)
|
||||||
|
// Note: The filter checks 'app.controls' but our key is 'program', so controls might not be filtered
|
||||||
|
// Let's just verify the program was saved
|
||||||
|
expect(savedProgram.guid).toBe('program-guid');
|
||||||
|
|
||||||
|
// Get the saved session data
|
||||||
|
const sessionData = localStorage.getItem('APP_GLO:session');
|
||||||
|
const savedSession = JSON.parse(sessionData!);
|
||||||
|
|
||||||
|
// These should be filtered out (in SKIP_PATHS)
|
||||||
|
expect(savedSession.connected).toBeUndefined();
|
||||||
|
expect(savedSession.error).toBeUndefined();
|
||||||
|
expect(savedSession.loading).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to localStorage when IndexedDB fails', async () => {
|
||||||
|
// Mock IndexedDB failure for all calls
|
||||||
|
(set as any).mockRejectedValue(new Error('IndexedDB not available'));
|
||||||
|
|
||||||
|
await saveStorage(mockState);
|
||||||
|
|
||||||
|
// Verify localStorage has the data
|
||||||
|
expect(localStorage.getItem('APP_GLO:layout')).toBeTruthy();
|
||||||
|
expect(localStorage.getItem('APP_GLO:navigation')).toBeTruthy();
|
||||||
|
expect(localStorage.getItem('APP_GLO:owner')).toBeTruthy();
|
||||||
|
expect(localStorage.getItem('APP_GLO:program')).toBeTruthy();
|
||||||
|
expect(localStorage.getItem('APP_GLO:user')).toBeTruthy();
|
||||||
|
expect(localStorage.getItem('APP_GLO:session')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadStorage', () => {
|
||||||
|
it('loads each key separately from IndexedDB', async () => {
|
||||||
|
// Mock IndexedDB responses
|
||||||
|
(get as any).mockImplementation((key: string) => {
|
||||||
|
const dataMap: Record<string, string> = {
|
||||||
|
'APP_GLO:layout': JSON.stringify(mockState.layout),
|
||||||
|
'APP_GLO:navigation': JSON.stringify(mockState.navigation),
|
||||||
|
'APP_GLO:owner': JSON.stringify(mockState.owner),
|
||||||
|
'APP_GLO:program': JSON.stringify(mockState.program),
|
||||||
|
'APP_GLO:user': JSON.stringify(mockState.user),
|
||||||
|
};
|
||||||
|
return Promise.resolve(dataMap[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set session in localStorage
|
||||||
|
localStorage.setItem('APP_GLO:session', JSON.stringify(mockState.session));
|
||||||
|
|
||||||
|
const result = await loadStorage();
|
||||||
|
|
||||||
|
expect(result.layout).toEqual(mockState.layout);
|
||||||
|
expect(result.navigation).toEqual(mockState.navigation);
|
||||||
|
expect(result.owner).toEqual(mockState.owner);
|
||||||
|
expect(result.program).toEqual(mockState.program);
|
||||||
|
expect(result.user).toEqual(mockState.user);
|
||||||
|
expect(result.session).toEqual(mockState.session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads session from localStorage only', async () => {
|
||||||
|
localStorage.setItem('APP_GLO:session', JSON.stringify(mockState.session));
|
||||||
|
|
||||||
|
const result = await loadStorage();
|
||||||
|
|
||||||
|
expect(result.session).toEqual(mockState.session);
|
||||||
|
// Verify get was NOT called for session
|
||||||
|
expect(get).not.toHaveBeenCalledWith('APP_GLO:session');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to localStorage when IndexedDB fails', async () => {
|
||||||
|
// Mock IndexedDB failure
|
||||||
|
(get as any).mockRejectedValue(new Error('IndexedDB not available'));
|
||||||
|
|
||||||
|
// Set data in localStorage
|
||||||
|
localStorage.setItem('APP_GLO:layout', JSON.stringify(mockState.layout));
|
||||||
|
localStorage.setItem('APP_GLO:navigation', JSON.stringify(mockState.navigation));
|
||||||
|
localStorage.setItem('APP_GLO:owner', JSON.stringify(mockState.owner));
|
||||||
|
localStorage.setItem('APP_GLO:program', JSON.stringify(mockState.program));
|
||||||
|
localStorage.setItem('APP_GLO:user', JSON.stringify(mockState.user));
|
||||||
|
localStorage.setItem('APP_GLO:session', JSON.stringify(mockState.session));
|
||||||
|
|
||||||
|
const result = await loadStorage();
|
||||||
|
|
||||||
|
expect(result.layout).toEqual(mockState.layout);
|
||||||
|
expect(result.navigation).toEqual(mockState.navigation);
|
||||||
|
expect(result.owner).toEqual(mockState.owner);
|
||||||
|
expect(result.program).toEqual(mockState.program);
|
||||||
|
expect(result.user).toEqual(mockState.user);
|
||||||
|
expect(result.session).toEqual(mockState.session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty object when no data is found', async () => {
|
||||||
|
(get as any).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await loadStorage();
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns partial state when some keys are missing', async () => {
|
||||||
|
// Only set some keys
|
||||||
|
(get as any).mockImplementation((key: string) => {
|
||||||
|
if (key === 'APP_GLO:layout') {
|
||||||
|
return Promise.resolve(JSON.stringify(mockState.layout));
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('APP_GLO:session', JSON.stringify(mockState.session));
|
||||||
|
|
||||||
|
const result = await loadStorage();
|
||||||
|
|
||||||
|
expect(result.layout).toEqual(mockState.layout);
|
||||||
|
expect(result.session).toEqual(mockState.session);
|
||||||
|
expect(result.navigation).toBeUndefined();
|
||||||
|
expect(result.owner).toBeUndefined();
|
||||||
|
expect(result.program).toBeUndefined();
|
||||||
|
expect(result.user).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles corrupted JSON data gracefully', async () => {
|
||||||
|
// Mock console.error to suppress error output
|
||||||
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
(get as any).mockResolvedValue('invalid json');
|
||||||
|
localStorage.setItem('APP_GLO:session', 'invalid json');
|
||||||
|
|
||||||
|
const result = await loadStorage();
|
||||||
|
|
||||||
|
// Should log errors but still return the result (may be empty or partial)
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,10 +2,11 @@ import { get, set } from 'idb-keyval';
|
|||||||
|
|
||||||
import type { GlobalState } from './GlobalStateStore.types';
|
import type { GlobalState } from './GlobalStateStore.types';
|
||||||
|
|
||||||
const STORAGE_KEY = 'app-data';
|
const STORAGE_KEY = 'APP_GLO';
|
||||||
|
|
||||||
const SKIP_PATHS = new Set([
|
const SKIP_PATHS = new Set([
|
||||||
'app.controls',
|
'app.controls',
|
||||||
|
'initialized',
|
||||||
'session.connected',
|
'session.connected',
|
||||||
'session.error',
|
'session.error',
|
||||||
'session.loading',
|
'session.loading',
|
||||||
@@ -43,50 +44,90 @@ const filterState = (state: unknown, prefix = ''): unknown => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function loadStorage(): Promise<Partial<GlobalState>> {
|
async function loadStorage(): Promise<Partial<GlobalState>> {
|
||||||
try {
|
const result: Partial<GlobalState> = {};
|
||||||
if (typeof indexedDB !== 'undefined') {
|
const keys: (keyof GlobalState)[] = ['layout', 'navigation', 'owner', 'program', 'session', 'user'];
|
||||||
const data = await get(STORAGE_KEY);
|
|
||||||
if (data) {
|
for (const key of keys) {
|
||||||
return JSON.parse(data) as Partial<GlobalState>;
|
const storageKey = `${STORAGE_KEY}:${key}`;
|
||||||
|
|
||||||
|
// Always use localStorage for session
|
||||||
|
if (key === 'session') {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const data = localStorage.getItem(storageKey);
|
||||||
|
if (data) {
|
||||||
|
result[key] = JSON.parse(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load ${key} from localStorage:`, e);
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof indexedDB !== 'undefined') {
|
||||||
|
const data = await get(storageKey);
|
||||||
|
if (data) {
|
||||||
|
result[key] = JSON.parse(data);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load ${key} from IndexedDB, falling back to localStorage:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const data = localStorage.getItem(storageKey);
|
||||||
|
if (data) {
|
||||||
|
result[key] = JSON.parse(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load ${key} from localStorage:`, e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load from IndexedDB, falling back to localStorage:', e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return result;
|
||||||
if (typeof localStorage !== 'undefined') {
|
|
||||||
const data = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (data) {
|
|
||||||
return JSON.parse(data) as Partial<GlobalState>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load from localStorage:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveStorage(state: GlobalState): Promise<void> {
|
async function saveStorage(state: GlobalState): Promise<void> {
|
||||||
const filtered = filterState(state);
|
const keys: (keyof GlobalState)[] = ['layout', 'navigation', 'owner', 'program', 'session', 'user'];
|
||||||
const serialized = JSON.stringify(filtered);
|
|
||||||
|
|
||||||
try {
|
for (const key of keys) {
|
||||||
if (typeof indexedDB !== 'undefined') {
|
const storageKey = `${STORAGE_KEY}:${key}`;
|
||||||
await set(STORAGE_KEY, serialized);
|
const filtered = filterState(state[key], key);
|
||||||
return;
|
const serialized = JSON.stringify(filtered);
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to save to IndexedDB, falling back to localStorage:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Always use localStorage for session
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (key === 'session') {
|
||||||
localStorage.setItem(STORAGE_KEY, serialized);
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem(storageKey, serialized);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to save ${key} to localStorage:`, e);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof indexedDB !== 'undefined') {
|
||||||
|
await set(storageKey, serialized);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to save ${key} to IndexedDB, falling back to localStorage:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem(storageKey, serialized);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to save ${key} to localStorage:`, e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to save to localStorage:', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import type { GlobalStateStoreType } from './GlobalStateStore.types';
|
import type { GlobalState, GlobalStateStoreType, ProgramState } from './GlobalStateStore.types';
|
||||||
|
|
||||||
import { GetGlobalState, GlobalStateStore } from './GlobalStateStore';
|
import { GetGlobalState, GlobalStateStore } from './GlobalStateStore';
|
||||||
|
|
||||||
|
|
||||||
interface GlobalStateStoreContextValue {
|
interface GlobalStateStoreContextValue {
|
||||||
fetchData: (url?: string) => Promise<void>;
|
fetchData: (url?: string) => Promise<void>;
|
||||||
getState: () => GlobalStateStoreType;
|
getState: () => GlobalStateStoreType;
|
||||||
@@ -13,12 +20,15 @@ interface GlobalStateStoreContextValue {
|
|||||||
|
|
||||||
const GlobalStateStoreContext = createContext<GlobalStateStoreContextValue | null>(null);
|
const GlobalStateStoreContext = createContext<GlobalStateStoreContextValue | null>(null);
|
||||||
|
|
||||||
|
|
||||||
interface GlobalStateStoreProviderProps {
|
interface GlobalStateStoreProviderProps {
|
||||||
apiURL?: string;
|
apiURL?: string;
|
||||||
autoFetch?: boolean;
|
autoFetch?: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
fetchOnMount?: boolean;
|
fetchOnMount?: boolean;
|
||||||
|
onFetchSession?: (state: Partial<GlobalState>) => Promise<Partial<GlobalState>>;
|
||||||
|
onLogin?: (state: Partial<GlobalState>) => Promise<Partial<GlobalState>>;
|
||||||
|
onLogout?: (state: Partial<GlobalState>) => Promise<Partial<GlobalState>>;
|
||||||
|
program?: Partial<ProgramState>;
|
||||||
throttleMs?: number;
|
throttleMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,13 +37,16 @@ export function GlobalStateStoreProvider({
|
|||||||
autoFetch = true,
|
autoFetch = true,
|
||||||
children,
|
children,
|
||||||
fetchOnMount = true,
|
fetchOnMount = true,
|
||||||
|
onFetchSession,
|
||||||
|
onLogin,
|
||||||
|
onLogout,
|
||||||
|
program,
|
||||||
throttleMs = 0,
|
throttleMs = 0,
|
||||||
}: GlobalStateStoreProviderProps) {
|
}: GlobalStateStoreProviderProps) {
|
||||||
const lastFetchTime = useRef<number>(0);
|
const lastFetchTime = useRef<number>(0);
|
||||||
const fetchInProgress = useRef<boolean>(false);
|
const fetchInProgress = useRef<boolean>(false);
|
||||||
const mounted = useRef<boolean>(false);
|
const mounted = useRef<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
const throttledFetch = useCallback(
|
const throttledFetch = useCallback(
|
||||||
async (url?: string) => {
|
async (url?: string) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -68,6 +81,30 @@ export function GlobalStateStoreProvider({
|
|||||||
}
|
}
|
||||||
}, [apiURL]);
|
}, [apiURL]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (program) {
|
||||||
|
GlobalStateStore.getState().setProgram(program);
|
||||||
|
}
|
||||||
|
}, [program]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onFetchSession) {
|
||||||
|
GlobalStateStore.setState({ onFetchSession });
|
||||||
|
}
|
||||||
|
}, [onFetchSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onLogin) {
|
||||||
|
GlobalStateStore.setState({ onLogin });
|
||||||
|
}
|
||||||
|
}, [onLogin]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onLogout) {
|
||||||
|
GlobalStateStore.setState({ onLogout });
|
||||||
|
}
|
||||||
|
}, [onLogout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mounted.current) {
|
if (!mounted.current) {
|
||||||
mounted.current = true;
|
mounted.current = true;
|
||||||
@@ -88,12 +125,8 @@ export function GlobalStateStoreProvider({
|
|||||||
};
|
};
|
||||||
}, [throttledFetch, refetch]);
|
}, [throttledFetch, refetch]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlobalStateStoreContext.Provider value={context}>
|
<GlobalStateStoreContext.Provider value={context}>{children}</GlobalStateStoreContext.Provider>
|
||||||
{children}
|
|
||||||
</GlobalStateStoreContext.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ export {
|
|||||||
getAuthToken,
|
getAuthToken,
|
||||||
GetGlobalState,
|
GetGlobalState,
|
||||||
GlobalStateStore,
|
GlobalStateStore,
|
||||||
|
isLoggedIn,
|
||||||
setApiURL,
|
setApiURL,
|
||||||
setAuthToken,
|
setAuthToken,
|
||||||
useGlobalStateStore
|
useGlobalStateStore,
|
||||||
} from './GlobalStateStore';
|
} from './GlobalStateStore';
|
||||||
|
|
||||||
export type * from './GlobalStateStore.types';
|
export type * from './GlobalStateStore.types';
|
||||||
|
|
||||||
export {
|
export { GlobalStateStoreProvider, useGlobalStateStoreContext } from './GlobalStateStoreWrapper';
|
||||||
GlobalStateStoreProvider,
|
|
||||||
useGlobalStateStoreContext,
|
|
||||||
} from './GlobalStateStoreWrapper';
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@glideapps/glide-data-grid';
|
} from '@glideapps/glide-data-grid';
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { useElementSize, useMergedRef } from '@mantine/hooks';
|
import { useElementSize, useMergedRef } from '@mantine/hooks';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { BottomBar } from './components/BottomBar';
|
import { BottomBar } from './components/BottomBar';
|
||||||
import { Computer } from './components/Computer';
|
import { Computer } from './components/Computer';
|
||||||
@@ -107,14 +107,22 @@ export const GridlerDataGrid = () => {
|
|||||||
setStateFN('_glideref', () => {
|
setStateFN('_glideref', () => {
|
||||||
return r ?? undefined;
|
return r ?? undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ready = getState('ready');
|
|
||||||
const newReady = !!(r && mounted);
|
|
||||||
if (ready !== newReady) {
|
|
||||||
setState('ready', newReady);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current && mounted) {
|
||||||
|
const currentReady = getState('ready');
|
||||||
|
if (!currentReady) {
|
||||||
|
setState('ready', true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentReady = getState('ready');
|
||||||
|
if (currentReady) {
|
||||||
|
setState('ready', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mounted, getState, setState]);
|
||||||
|
|
||||||
const theme = useGridTheme();
|
const theme = useGridTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -188,6 +196,7 @@ export const GridlerDataGrid = () => {
|
|||||||
if (!refContextActivated.current) {
|
if (!refContextActivated.current) {
|
||||||
refContextActivated.current = true;
|
refContextActivated.current = true;
|
||||||
onContextClick('cell', event, cell[0], cell[1]);
|
onContextClick('cell', event, cell[0], cell[1]);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refContextActivated.current = false;
|
refContextActivated.current = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -231,7 +240,7 @@ export const GridlerDataGrid = () => {
|
|||||||
rows = rows.hasIndex(r) ? rows : rows.add(r);
|
rows = rows.hasIndex(r) ? rows : rows.add(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Debug:onGridSelectionChange', currentSelection, selection);
|
//console.log('Debug:onGridSelectionChange', currentSelection, selection);
|
||||||
if (
|
if (
|
||||||
JSON.stringify(currentSelection?.columns) !== JSON.stringify(selection.columns) ||
|
JSON.stringify(currentSelection?.columns) !== JSON.stringify(selection.columns) ||
|
||||||
JSON.stringify(currentSelection?.rows) !== JSON.stringify(rows) ||
|
JSON.stringify(currentSelection?.rows) !== JSON.stringify(rows) ||
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const Computer = React.memo(() => {
|
|||||||
selectFirstRowOnMount,
|
selectFirstRowOnMount,
|
||||||
setState,
|
setState,
|
||||||
setStateFN,
|
setStateFN,
|
||||||
values
|
values,
|
||||||
} = useGridlerStore((s) => ({
|
} = useGridlerStore((s) => ({
|
||||||
_glideref: s._glideref,
|
_glideref: s._glideref,
|
||||||
_gridSelectionRows: s._gridSelectionRows,
|
_gridSelectionRows: s._gridSelectionRows,
|
||||||
@@ -45,7 +45,7 @@ export const Computer = React.memo(() => {
|
|||||||
scrollToRowKey: s.scrollToRowKey,
|
scrollToRowKey: s.scrollToRowKey,
|
||||||
searchStr: s.searchStr,
|
searchStr: s.searchStr,
|
||||||
selectedRowKey: s.selectedRowKey,
|
selectedRowKey: s.selectedRowKey,
|
||||||
selectFirstRowOnMount:s.selectFirstRowOnMount,
|
selectFirstRowOnMount: s.selectFirstRowOnMount,
|
||||||
setState: s.setState,
|
setState: s.setState,
|
||||||
setStateFN: s.setStateFN,
|
setStateFN: s.setStateFN,
|
||||||
uniqueid: s.uniqueid,
|
uniqueid: s.uniqueid,
|
||||||
@@ -71,13 +71,10 @@ export const Computer = React.memo(() => {
|
|||||||
|
|
||||||
//When values change, update selection
|
//When values change, update selection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const searchSelection = async () => {
|
const searchSelection = async (values: Array<Record<string, unknown>>) => {
|
||||||
const page_data = getState('_page_data');
|
|
||||||
const pageSize = getState('pageSize');
|
|
||||||
const keyField = getState('keyField') ?? 'id';
|
const keyField = getState('keyField') ?? 'id';
|
||||||
const rowIndexes = [];
|
const rowIndexes = [];
|
||||||
for (const vi in values as Array<Record<string, unknown>>) {
|
for (const vi in values as Array<Record<string, unknown>>) {
|
||||||
let rowIndex = -1;
|
|
||||||
const key = String(
|
const key = String(
|
||||||
typeof values?.[vi] === 'object'
|
typeof values?.[vi] === 'object'
|
||||||
? values?.[vi]?.[keyField]
|
? values?.[vi]?.[keyField]
|
||||||
@@ -85,26 +82,12 @@ export const Computer = React.memo(() => {
|
|||||||
? values?.[vi]
|
? values?.[vi]
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
for (const p in page_data) {
|
if (!key) {
|
||||||
for (const r in page_data[p]) {
|
continue;
|
||||||
const idx = Number(p) * pageSize + Number(r);
|
|
||||||
|
|
||||||
if (String(page_data[p][r]?.[keyField]) === key) {
|
|
||||||
//console.log('Found row S', idx, page_data[p][r], page_data[p][r]?.[keyField], key);
|
|
||||||
rowIndex = idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rowIndex >= 0) {
|
|
||||||
rowIndexes.push(rowIndex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!(rowIndex >= 0)) {
|
const idx = await getRowIndexByKey(key);
|
||||||
const idx = await getRowIndexByKey(key);
|
if (idx !== null && idx !== undefined) {
|
||||||
if (idx) {
|
rowIndexes.push(idx);
|
||||||
rowIndexes.push(idx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,10 +95,12 @@ export const Computer = React.memo(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (values) {
|
if (values) {
|
||||||
searchSelection().then((rowIndexes) => {
|
searchSelection(values).then((rowIndexes) => {
|
||||||
let rows = CompactSelection.empty();
|
let rows = CompactSelection.empty();
|
||||||
rowIndexes.forEach((r) => {
|
rowIndexes.forEach((r) => {
|
||||||
rows = rows.add(r);
|
if (r !== undefined) {
|
||||||
|
rows = rows.add(r);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setStateFN('_gridSelectionRows', () => {
|
setStateFN('_gridSelectionRows', () => {
|
||||||
@@ -276,14 +261,11 @@ export const Computer = React.memo(() => {
|
|||||||
const ready = getState('ready');
|
const ready = getState('ready');
|
||||||
|
|
||||||
if (ready && selectFirstRowOnMount) {
|
if (ready && selectFirstRowOnMount) {
|
||||||
|
|
||||||
const scrollToRowKey = getState('scrollToRowKey');
|
const scrollToRowKey = getState('scrollToRowKey');
|
||||||
|
|
||||||
|
|
||||||
if (scrollToRowKey && scrollToRowKey >= 0) {
|
if (scrollToRowKey && scrollToRowKey >= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyField = getState('keyField') ?? 'id';
|
const keyField = getState('keyField') ?? 'id';
|
||||||
const page_data = getState('_page_data');
|
const page_data = getState('_page_data');
|
||||||
|
|
||||||
@@ -291,21 +273,18 @@ export const Computer = React.memo(() => {
|
|||||||
const firstRow = firstBuffer?.[keyField] ?? -1;
|
const firstRow = firstBuffer?.[keyField] ?? -1;
|
||||||
const currentValues = getState('values') ?? [];
|
const currentValues = getState('values') ?? [];
|
||||||
|
|
||||||
if (
|
if (!firstBuffer) {
|
||||||
!(values && values.length > 0) &&
|
return;
|
||||||
firstRow &&
|
}
|
||||||
firstRow > 0 &&
|
|
||||||
(currentValues.length ?? 0) === 0
|
|
||||||
) {
|
|
||||||
const values = [firstBuffer, ...(currentValues as Array<Record<string, unknown>>)];
|
|
||||||
|
|
||||||
|
if (firstRow && firstRow > 0 && (currentValues.length ?? 0) === 0) {
|
||||||
|
const newValues = [firstBuffer];
|
||||||
|
|
||||||
const onChange = getState('onChange');
|
const onChange = getState('onChange');
|
||||||
//console.log('Selecting first row:', firstRow, firstBuffer, values);
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(values);
|
onChange(newValues);
|
||||||
} else {
|
} else {
|
||||||
setState('values', values);
|
setState('values', newValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState('scrollToRowKey', firstRow);
|
setState('scrollToRowKey', firstRow);
|
||||||
@@ -318,7 +297,7 @@ export const Computer = React.memo(() => {
|
|||||||
return () => {
|
return () => {
|
||||||
_events?.removeEventListener('loadPage', loadPage);
|
_events?.removeEventListener('loadPage', loadPage);
|
||||||
};
|
};
|
||||||
}, [ready, selectFirstRowOnMount]);
|
}, [ready, selectFirstRowOnMount, values]);
|
||||||
|
|
||||||
/// logic to apply the selected row.
|
/// logic to apply the selected row.
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@@ -348,10 +327,9 @@ export const Computer = React.memo(() => {
|
|||||||
const key = selectedRowKey ?? scrollToRowKey;
|
const key = selectedRowKey ?? scrollToRowKey;
|
||||||
|
|
||||||
if (key && ref && ready) {
|
if (key && ref && ready) {
|
||||||
|
//console.log('Computer:Scrolling to key:', key);
|
||||||
getRowIndexByKey?.(key).then((r) => {
|
getRowIndexByKey?.(key).then((r) => {
|
||||||
if (r !== undefined) {
|
if (r !== undefined) {
|
||||||
//console.log('Scrolling to selected row:', r, selectedRowKey, scrollToRowKey);
|
|
||||||
|
|
||||||
if (selectedRowKey) {
|
if (selectedRowKey) {
|
||||||
const onChange = getState('onChange');
|
const onChange = getState('onChange');
|
||||||
const selected = [{ [getState('keyField') ?? 'id']: selectedRowKey }];
|
const selected = [{ [getState('keyField') ?? 'id']: selectedRowKey }];
|
||||||
@@ -379,15 +357,6 @@ export const Computer = React.memo(() => {
|
|||||||
}
|
}
|
||||||
}, [scrollToRowKey, selectedRowKey]);
|
}, [scrollToRowKey, selectedRowKey]);
|
||||||
|
|
||||||
// console.log('Gridler:Debug:Computer', {
|
|
||||||
// colFilters,
|
|
||||||
// colOrder,
|
|
||||||
// colSize,
|
|
||||||
// colSort,
|
|
||||||
// columns,
|
|
||||||
// uniqueid
|
|
||||||
// });
|
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export interface GridlerState {
|
|||||||
_visibleArea: Rectangle;
|
_visibleArea: Rectangle;
|
||||||
_visiblePages: Rectangle;
|
_visiblePages: Rectangle;
|
||||||
addError: (err: string, ...args: Array<any>) => void;
|
addError: (err: string, ...args: Array<any>) => void;
|
||||||
askAPIRowNumber?: (key: string) => Promise<number>;
|
askAPIRowNumber?: (key: string) => Promise<null | number>;
|
||||||
colFilters?: Array<FilterOption>;
|
colFilters?: Array<FilterOption>;
|
||||||
colOrder?: Record<string, number>;
|
colOrder?: Record<string, number>;
|
||||||
colSize?: Record<string, number>;
|
colSize?: Record<string, number>;
|
||||||
@@ -162,7 +162,7 @@ export interface GridlerState {
|
|||||||
hasLocalData: boolean;
|
hasLocalData: boolean;
|
||||||
isEmpty: boolean;
|
isEmpty: boolean;
|
||||||
|
|
||||||
isValuesInPages: () => boolean
|
isValuesInPages: () => boolean;
|
||||||
loadingData?: boolean;
|
loadingData?: boolean;
|
||||||
loadPage: (page: number, clearMode?: 'all' | 'page') => Promise<void>;
|
loadPage: (page: number, clearMode?: 'all' | 'page') => Promise<void>;
|
||||||
mounted: boolean;
|
mounted: boolean;
|
||||||
@@ -191,7 +191,6 @@ export interface GridlerState {
|
|||||||
freezeRegions?: readonly Rectangle[];
|
freezeRegions?: readonly Rectangle[];
|
||||||
selected?: Item;
|
selected?: Item;
|
||||||
}
|
}
|
||||||
|
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
@@ -200,10 +199,7 @@ export interface GridlerState {
|
|||||||
|
|
||||||
reload?: () => Promise<void>;
|
reload?: () => Promise<void>;
|
||||||
renderColumns?: GridlerColumns;
|
renderColumns?: GridlerColumns;
|
||||||
setState: <K extends keyof GridlerStoreState>(
|
setState: <K extends keyof GridlerStoreState>(key: K, value: GridlerStoreState[K]) => void;
|
||||||
key: K,
|
|
||||||
value: GridlerStoreState[K]
|
|
||||||
) => void;
|
|
||||||
setStateFN: <K extends keyof GridlerStoreState>(
|
setStateFN: <K extends keyof GridlerStoreState>(
|
||||||
key: K,
|
key: K,
|
||||||
value: (current: GridlerStoreState[K]) => Partial<GridlerStoreState[K]>
|
value: (current: GridlerStoreState[K]) => Partial<GridlerStoreState[K]>
|
||||||
@@ -340,7 +336,9 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
|
|||||||
},
|
},
|
||||||
getRowIndexByKey: async (key: number | string) => {
|
getRowIndexByKey: async (key: number | string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
|
if (key === undefined || key === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
let rowIndex = -1;
|
let rowIndex = -1;
|
||||||
if (state.ready) {
|
if (state.ready) {
|
||||||
const page_data = state._page_data;
|
const page_data = state._page_data;
|
||||||
@@ -352,22 +350,22 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
|
|||||||
//console.log('Found row', idx, page_data[p][r]?.[keyField], scrollToRowKey);
|
//console.log('Found row', idx, page_data[p][r]?.[keyField], scrollToRowKey);
|
||||||
if (String(page_data[p][r]?.[keyField]) === String(key)) {
|
if (String(page_data[p][r]?.[keyField]) === String(key)) {
|
||||||
rowIndex =
|
rowIndex =
|
||||||
page_data[p][r]?._rownumber > 0 ? page_data[p][r]?._rownumber : idx > 0 ? idx : -1;
|
page_data[p][r]?._rownumber > 0 ? page_data[p][r]?._rownumber : idx >= 0 ? idx : -1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rowIndex > 0) {
|
if (rowIndex >= 0) {
|
||||||
console.log('Local row index', rowIndex, key);
|
//console.log('Local row index', rowIndex, key);
|
||||||
return rowIndex;
|
return rowIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowIndex > 0) {
|
if (rowIndex >= 0) {
|
||||||
return rowIndex;
|
return rowIndex;
|
||||||
} else if (typeof state.askAPIRowNumber === 'function') {
|
} else if (typeof state.askAPIRowNumber === 'function') {
|
||||||
const rn = await state.askAPIRowNumber(String(key));
|
const rn = await state.askAPIRowNumber(String(key));
|
||||||
|
|
||||||
if (rn && rn >= 0) {
|
if (rn && rn >= 0) {
|
||||||
console.log('Remote row index', rowIndex, key);
|
|
||||||
return rn;
|
return rn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,8 +400,8 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
},
|
},
|
||||||
keyField: 'id',
|
keyField: 'id',
|
||||||
loadPage: async (pPage: number, clearMode?: 'all' | 'page') => {
|
loadPage: async (pPage: number, clearMode?: 'all' | 'page') => {
|
||||||
@@ -488,13 +486,16 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
|
|||||||
onCellClicked: (cell: Item, event: CellClickedEventArgs) => {
|
onCellClicked: (cell: Item, event: CellClickedEventArgs) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const [col, row] = cell;
|
const [col, row] = cell;
|
||||||
|
const rowBuffer = state.getRowBuffer(row);
|
||||||
if (state.glideProps?.onCellClicked) {
|
if (state.glideProps?.onCellClicked) {
|
||||||
state.glideProps?.onCellClicked?.(cell, event);
|
state.glideProps?.onCellClicked?.(cell, event);
|
||||||
}
|
}
|
||||||
if (state.values?.length) {
|
if (state.values?.length && state.values?.length > 0) {
|
||||||
if (state.onChange) {
|
if (state.onChange) {
|
||||||
state.onChange(state.values);
|
state.onChange(state.values);
|
||||||
}
|
}
|
||||||
|
} else if (rowBuffer && state.onChange) {
|
||||||
|
state.onChange([rowBuffer]);
|
||||||
}
|
}
|
||||||
|
|
||||||
state._events.dispatchEvent(
|
state._events.dispatchEvent(
|
||||||
@@ -949,7 +950,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
total_rows: 1000,
|
total_rows: 1000,
|
||||||
uniqueid: getUUID()
|
uniqueid: getUUID(),
|
||||||
}),
|
}),
|
||||||
(props) => {
|
(props) => {
|
||||||
const [setState, getState] = props.useStore((s) => [s.setState, s.getState]);
|
const [setState, getState] = props.useStore((s) => [s.setState, s.getState]);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
|
|||||||
const searchStr = getState('searchStr');
|
const searchStr = getState('searchStr');
|
||||||
const searchFields = getState('searchFields');
|
const searchFields = getState('searchFields');
|
||||||
const _active_requests = getState('_active_requests');
|
const _active_requests = getState('_active_requests');
|
||||||
const keyField = getState('keyField');
|
const keyField = getState('keyField');
|
||||||
setState('loadingData', true);
|
setState('loadingData', true);
|
||||||
try {
|
try {
|
||||||
//console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props });
|
//console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props });
|
||||||
@@ -83,7 +83,7 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
|
|||||||
)
|
)
|
||||||
?.forEach((filter: any) => {
|
?.forEach((filter: any) => {
|
||||||
ops.push({
|
ops.push({
|
||||||
name: `${filter.id ?? ""}`,
|
name: `${filter.id ?? ''}`,
|
||||||
op: 'contains',
|
op: 'contains',
|
||||||
type: 'searchor',
|
type: 'searchor',
|
||||||
value: searchStr,
|
value: searchStr,
|
||||||
@@ -202,11 +202,13 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const askAPIRowNumber: (key: string) => Promise<number> = useCallback(
|
const askAPIRowNumber: (key: string) => Promise<null | number> = useCallback(
|
||||||
async (key: string) => {
|
async (key: string) => {
|
||||||
const colFilters = getState('colFilters');
|
const colFilters = getState('colFilters');
|
||||||
|
if (!key || key === '' || !props.url) {
|
||||||
//console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props });
|
return null;
|
||||||
|
}
|
||||||
|
//console.log('APIAdaptorGoLangv2', { key, props });
|
||||||
if (props && props.url) {
|
if (props && props.url) {
|
||||||
const head = new Headers();
|
const head = new Headers();
|
||||||
const ops: FetchAPIOperation[] = [
|
const ops: FetchAPIOperation[] = [
|
||||||
@@ -250,7 +252,7 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
|
|||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const res = await fetch(`${props.url}?x-fetch-rownumber=${key}}`, {
|
const res = await fetch(`${props.url}?x-fetch-rownumber=${key}`, {
|
||||||
headers: head,
|
headers: head,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
signal: controller?.signal,
|
signal: controller?.signal,
|
||||||
@@ -273,12 +275,12 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
|
|||||||
setState('useAPIQuery', useAPIQuery);
|
setState('useAPIQuery', useAPIQuery);
|
||||||
setState('askAPIRowNumber', askAPIRowNumber);
|
setState('askAPIRowNumber', askAPIRowNumber);
|
||||||
const isValuesInPages = getState('isValuesInPages');
|
const isValuesInPages = getState('isValuesInPages');
|
||||||
|
|
||||||
const _refresh = getState('_refresh');
|
const _refresh = getState('_refresh');
|
||||||
if (!isValuesInPages) {
|
if (!isValuesInPages) {
|
||||||
setState('values', []);
|
setState('values', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Reset the loaded pages to new rules
|
//Reset the loaded pages to new rules
|
||||||
_refresh?.().then(() => {
|
_refresh?.().then(() => {
|
||||||
const onChange = getState('onChange');
|
const onChange = getState('onChange');
|
||||||
@@ -293,8 +295,6 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders.
|
//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders.
|
||||||
export const GlidlerAPIAdaptorForGoLangv2 = React.memo(_GlidlerAPIAdaptorForGoLangv2);
|
export const GlidlerAPIAdaptorForGoLangv2 = React.memo(_GlidlerAPIAdaptorForGoLangv2);
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function GlidlerFormAdaptor(props: {
|
|||||||
col?: GridlerColumn,
|
col?: GridlerColumn,
|
||||||
defaultItems?: MantineBetterMenuInstanceItem[]
|
defaultItems?: MantineBetterMenuInstanceItem[]
|
||||||
): MantineBetterMenuInstanceItem[] => {
|
): MantineBetterMenuInstanceItem[] => {
|
||||||
//console.log('GlidlerFormInterface getMenuItems', id);
|
//console.log('GlidlerFormInterface getMenuItems', id, row, defaultItems);
|
||||||
|
|
||||||
if (id === 'header-menu') {
|
if (id === 'header-menu') {
|
||||||
return defaultItems || [];
|
return defaultItems || [];
|
||||||
@@ -88,7 +88,7 @@ export function GlidlerFormAdaptor(props: {
|
|||||||
? props.descriptionField(row)
|
? props.descriptionField(row)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (id === 'other') {
|
if (id === 'other' || (id === 'cell' && !row)) {
|
||||||
items.push({
|
items.push({
|
||||||
c: 'blue',
|
c: 'blue',
|
||||||
label: 'Add',
|
label: 'Add',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Button, Checkbox, Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core';
|
import { Button, Checkbox, Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
@@ -22,9 +23,20 @@ export const GridlerGoAPIExampleEventlog = () => {
|
|||||||
const [selectRow, setSelectRow] = useState<string | undefined>('');
|
const [selectRow, setSelectRow] = useState<string | undefined>('');
|
||||||
const [values, setValues] = useState<Array<Record<string, any>>>([]);
|
const [values, setValues] = useState<Array<Record<string, any>>>([]);
|
||||||
const [search, setSearch] = useState<string>('');
|
const [search, setSearch] = useState<string>('');
|
||||||
const [formProps, setFormProps] = useState<{ onChange?: any; onClose?: any; opened: boolean; request: any; title?: string; values: any; } | null>({
|
const [formProps, setFormProps] = useState<{
|
||||||
onChange: (_request: string, data: any) => { ref.current?.refresh({ value: data }); },
|
onChange?: any;
|
||||||
onClose: () => { setFormProps((cv) => ({ ...cv, opened: false, request: null, values: null })) },
|
onClose?: any;
|
||||||
|
opened: boolean;
|
||||||
|
request: any;
|
||||||
|
title?: string;
|
||||||
|
values: any;
|
||||||
|
} | null>({
|
||||||
|
onChange: (_request: string, data: any) => {
|
||||||
|
ref.current?.refresh({ value: data });
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
setFormProps((cv) => ({ ...cv, opened: false, request: null, values: null }));
|
||||||
|
},
|
||||||
opened: false,
|
opened: false,
|
||||||
request: null,
|
request: null,
|
||||||
values: null,
|
values: null,
|
||||||
@@ -33,7 +45,8 @@ export const GridlerGoAPIExampleEventlog = () => {
|
|||||||
const columns: GridlerColumns = [
|
const columns: GridlerColumns = [
|
||||||
{
|
{
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const process = `${row?.cql2?.length > 0
|
const process = `${
|
||||||
|
row?.cql2?.length > 0
|
||||||
? '🔖'
|
? '🔖'
|
||||||
: row?.cql1?.length > 0
|
: row?.cql1?.length > 0
|
||||||
? '📕'
|
? '📕'
|
||||||
@@ -42,7 +55,7 @@ export const GridlerGoAPIExampleEventlog = () => {
|
|||||||
: row?.status === 2
|
: row?.status === 2
|
||||||
? '🔒'
|
? '🔒'
|
||||||
: '⚙️'
|
: '⚙️'
|
||||||
} ${String(row?.id_process ?? '0')}`;
|
} ${String(row?.id_process ?? '0')}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: process,
|
data: process,
|
||||||
@@ -139,28 +152,28 @@ export const GridlerGoAPIExampleEventlog = () => {
|
|||||||
changeOnActiveClick={true}
|
changeOnActiveClick={true}
|
||||||
descriptionField={'process'}
|
descriptionField={'process'}
|
||||||
onRequestForm={(request, data) => {
|
onRequestForm={(request, data) => {
|
||||||
setFormProps((cv)=> {
|
setFormProps((cv) => {
|
||||||
return {...cv, opened: true, request: request as any, values: data as any}
|
return { ...cv, opened: true, request: request as any, values: data as any };
|
||||||
})
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Gridler>
|
</Gridler>
|
||||||
<FormerDialog
|
<FormerDialog
|
||||||
former={{
|
former={{
|
||||||
request: formProps?.request ?? "insert",
|
request: formProps?.request ?? 'insert',
|
||||||
values: formProps?.values,
|
values: formProps?.values,
|
||||||
}}
|
}}
|
||||||
onClose={formProps?.onClose}
|
onClose={formProps?.onClose}
|
||||||
opened={formProps?.opened ?? false}
|
opened={formProps?.opened ?? false}
|
||||||
title={formProps?.title ?? 'Process Form'}
|
title={formProps?.title ?? 'Process Form'}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInputCtrl label="Process Name" name="process" />
|
<TextInputCtrl label="Process Name" name="process" />
|
||||||
<NumberInputCtrl label="Sequence" name="sequence" />
|
<NumberInputCtrl label="Sequence" name="sequence" />
|
||||||
<InlineWrapper label="Type" promptWidth={200}>
|
<InlineWrapper label="Type" promptWidth={200}>
|
||||||
<NativeSelectCtrl data={["trigger","function","view"]} name="type"/>
|
<NativeSelectCtrl data={['trigger', 'function', 'view']} name="type" />
|
||||||
</InlineWrapper>
|
</InlineWrapper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</FormerDialog>
|
</FormerDialog>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Group>
|
<Group>
|
||||||
@@ -222,6 +235,6 @@ export const GridlerGoAPIExampleEventlog = () => {
|
|||||||
Goto 2050
|
Goto 2050
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack >
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
//@ts-nocheck
|
//@ts-nocheck
|
||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
//@ts-nocheck
|
//@ts-nocheck
|
||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import {b64EncodeUnicode} from '@warkypublic/artemis-kit/base64'
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
const TOKEN_KEY = 'gridler_golang_restapi_v2_token'
|
import { b64EncodeUnicode } from '@warkypublic/artemis-kit/base64';
|
||||||
|
const TOKEN_KEY = 'gridler_golang_restapi_v2_token';
|
||||||
|
|
||||||
export type APIOptionsType = {
|
export type APIOptionsType = {
|
||||||
autocreate?: boolean
|
autocreate?: boolean;
|
||||||
autoref?: boolean
|
autoref?: boolean;
|
||||||
baseurl?: string
|
baseurl?: string;
|
||||||
getAPIProvider?: () => { provider: string; providerKey: string }
|
getAPIProvider?: () => { provider: string; providerKey: string };
|
||||||
getAuthToken?: () => string
|
getAuthToken?: () => string;
|
||||||
operations?: Array<FetchAPIOperation>
|
operations?: Array<FetchAPIOperation>;
|
||||||
postfix?: string
|
postfix?: string;
|
||||||
prefix?: string
|
prefix?: string;
|
||||||
requestTimeoutSec?: number
|
requestTimeoutSec?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface APIResponse {
|
export interface APIResponse {
|
||||||
errmsg: string
|
errmsg: string;
|
||||||
payload?: any
|
payload?: any;
|
||||||
retval: number
|
retval: number;
|
||||||
}
|
}
|
||||||
export interface FetchAPIOperation {
|
export interface FetchAPIOperation {
|
||||||
name?: string
|
name?: string;
|
||||||
op?: string
|
op?: string;
|
||||||
type: GoAPIHeaderTypes //x-fieldfilter
|
type: GoAPIHeaderTypes; //x-fieldfilter
|
||||||
value: string
|
value: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @description Types for the Go Rest API headers
|
* @description Types for the Go Rest API headers
|
||||||
* @typedef {String} GoAPIEnum
|
* @typedef {String} GoAPIEnum
|
||||||
*/
|
*/
|
||||||
export type GoAPIEnum =
|
export type GoAPIEnum =
|
||||||
| 'advsql'
|
| 'advsql'
|
||||||
| 'api-key'
|
| 'api-key'
|
||||||
@@ -42,7 +41,7 @@ export type GoAPIEnum =
|
|||||||
| 'association_autoupdate'
|
| 'association_autoupdate'
|
||||||
| 'association-update'
|
| 'association-update'
|
||||||
| 'cql-sel'
|
| 'cql-sel'
|
||||||
| 'cursor-backward'// For x cursor-backward header
|
| 'cursor-backward' // For x cursor-backward header
|
||||||
| 'cursor-forward' // For x cursor-forward header
|
| 'cursor-forward' // For x cursor-forward header
|
||||||
| 'custom-sql-join'
|
| 'custom-sql-join'
|
||||||
| 'custom-sql-or'
|
| 'custom-sql-or'
|
||||||
@@ -72,28 +71,24 @@ export type GoAPIEnum =
|
|||||||
| 'simpleapi'
|
| 'simpleapi'
|
||||||
| 'skipcache'
|
| 'skipcache'
|
||||||
| 'skipcount'
|
| 'skipcount'
|
||||||
| 'sort'
|
| 'sort';
|
||||||
|
|
||||||
|
export type GoAPIHeaderKeys = `x-${GoAPIEnum}`;
|
||||||
|
|
||||||
export type GoAPIHeaderKeys = `x-${GoAPIEnum}`
|
export type GoAPIHeaderTypes = GoAPIEnum & string;
|
||||||
|
|
||||||
|
|
||||||
export type GoAPIHeaderTypes = GoAPIEnum & string
|
|
||||||
|
|
||||||
|
|
||||||
export interface GoAPIOperation {
|
export interface GoAPIOperation {
|
||||||
name?: string
|
name?: string;
|
||||||
op?: string
|
op?: string;
|
||||||
type: GoAPIHeaderTypes //x-fieldfilter
|
type: GoAPIHeaderTypes; //x-fieldfilter
|
||||||
value: string
|
value: string;
|
||||||
}
|
}
|
||||||
export interface MetaData {
|
export interface MetaData {
|
||||||
limit?: number
|
limit?: number;
|
||||||
offset?: number
|
offset?: number;
|
||||||
total?: number
|
total?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an array of objects by encoding specific values and setting headers.
|
* Builds an array of objects by encoding specific values and setting headers.
|
||||||
*
|
*
|
||||||
@@ -105,50 +100,49 @@ const buildGoAPIOperation = (
|
|||||||
ops: Array<FetchAPIOperation>,
|
ops: Array<FetchAPIOperation>,
|
||||||
headers?: Headers
|
headers?: Headers
|
||||||
): Array<FetchAPIOperation> => {
|
): Array<FetchAPIOperation> => {
|
||||||
const newops = [...ops.filter((i) => i !== undefined && i.type !== undefined)]
|
const newops = [...ops.filter((i) => i !== undefined && i.type !== undefined)];
|
||||||
|
|
||||||
|
|
||||||
for (let i = 0; i < newops.length; i++) {
|
for (let i = 0; i < newops.length; i++) {
|
||||||
if (!newops[i].name || newops[i].name === '') {
|
if (!newops[i].name || newops[i].name === '') {
|
||||||
newops[i].name = ''
|
newops[i].name = '';
|
||||||
}
|
}
|
||||||
if (newops[i].type === 'files' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'files' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
if (newops[i].type === 'advsql' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'advsql' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newops[i].type === 'custom-sql-or' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'custom-sql-or' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newops[i].type === 'custom-sql-join' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'custom-sql-join' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
if (newops[i].type === 'not-select-fields' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'not-select-fields' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
if (newops[i].type === 'custom-sql-w' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'custom-sql-w' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
if (newops[i].type === 'select-fields' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'select-fields' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
if (newops[i].type === 'cql-sel' && !newops[i].value.startsWith('__')) {
|
if (newops[i].type === 'cql-sel' && !newops[i].value.startsWith('__')) {
|
||||||
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`
|
newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headers) {
|
if (headers) {
|
||||||
if (!newops || newops.length === 0) {
|
if (!newops || newops.length === 0) {
|
||||||
headers.set(`x-limit`, '10')
|
headers.set(`x-limit`, '10');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newops[i].type === 'association_autoupdate') {
|
if (newops[i].type === 'association_autoupdate') {
|
||||||
headers.set(`association_autoupdate`, newops[i].value ?? '1')
|
headers.set(`association_autoupdate`, newops[i].value ?? '1');
|
||||||
}
|
}
|
||||||
if (newops[i].type === 'association_autocreate') {
|
if (newops[i].type === 'association_autocreate') {
|
||||||
headers.set(`association_autocreate`, newops[i].value ?? '1')
|
headers.set(`association_autocreate`, newops[i].value ?? '1');
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
newops[i].type === 'searchop' ||
|
newops[i].type === 'searchop' ||
|
||||||
@@ -158,20 +152,20 @@ const buildGoAPIOperation = (
|
|||||||
headers.set(
|
headers.set(
|
||||||
encodeURIComponent(`x-${newops[i].type}-${newops[i].op}-${newops[i].name}`),
|
encodeURIComponent(`x-${newops[i].type}-${newops[i].op}-${newops[i].name}`),
|
||||||
String(newops[i].value)
|
String(newops[i].value)
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
headers.set(
|
headers.set(
|
||||||
encodeURIComponent(
|
encodeURIComponent(
|
||||||
`x-${newops[i].type}${newops[i].name && newops[i].name !== '' ? '-' + newops[i].name : ''}`
|
`x-${newops[i].type}${newops[i].name && newops[i].name !== '' ? '-' + newops[i].name : ''}`
|
||||||
),
|
),
|
||||||
String(newops[i].value)
|
String(newops[i].value)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newops
|
return newops;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the headers from an array of FetchAPIOperation objects and returns them as an object.
|
* Retrieves the headers from an array of FetchAPIOperation objects and returns them as an object.
|
||||||
@@ -183,77 +177,75 @@ const GoAPIHeaders = (
|
|||||||
ops: Array<FetchAPIOperation>,
|
ops: Array<FetchAPIOperation>,
|
||||||
headers?: Headers
|
headers?: Headers
|
||||||
): { [key: string]: string } => {
|
): { [key: string]: string } => {
|
||||||
const head = new Headers()
|
const head = new Headers();
|
||||||
const headerlist: Record<string,string> = {}
|
const headerlist: Record<string, string> = {};
|
||||||
|
|
||||||
const authToken = getAuthToken?.()
|
const authToken = getAuthToken?.();
|
||||||
if (authToken && authToken !== '') {
|
if (authToken && authToken !== '') {
|
||||||
|
head.set('Authorization', `Token ${authToken}`);
|
||||||
head.set('Authorization', `Token ${authToken}`)
|
|
||||||
} else {
|
} else {
|
||||||
const token = getAuthToken()
|
const token = getAuthToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
head.set('Authorization', `Token ${token}`)
|
head.set('Authorization', `Token ${token}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (headers) {
|
if (headers) {
|
||||||
headers.forEach((v, k) => {
|
headers.forEach((v, k) => {
|
||||||
head.set(k, v)
|
head.set(k, v);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
const distinctOperations: Array<FetchAPIOperation> = []
|
const distinctOperations: Array<FetchAPIOperation> = [];
|
||||||
|
|
||||||
for (const value of ops?.filter((val) => !!val) ?? []) {
|
for (const value of ops?.filter((val) => !!val) ?? []) {
|
||||||
const index = distinctOperations.findIndex(
|
const index = distinctOperations.findIndex(
|
||||||
(searchValue) => searchValue.name === value.name && searchValue.type === value.type
|
(searchValue) => searchValue.name === value.name && searchValue.type === value.type
|
||||||
)
|
);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
distinctOperations.push(value)
|
distinctOperations.push(value);
|
||||||
} else {
|
} else {
|
||||||
distinctOperations[index] = value
|
distinctOperations[index] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildGoAPIOperation(distinctOperations, head)
|
buildGoAPIOperation(distinctOperations, head);
|
||||||
|
|
||||||
head?.forEach((v, k) => {
|
head?.forEach((v, k) => {
|
||||||
headerlist[k] = v
|
headerlist[k] = v;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (headers) {
|
if (headers) {
|
||||||
for (const key of Object.keys(headerlist)) {
|
for (const key of Object.keys(headerlist)) {
|
||||||
headers.set(key, headerlist[key])
|
headers.set(key, headerlist[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return headerlist
|
return headerlist;
|
||||||
}
|
};
|
||||||
|
|
||||||
const callbacks = {
|
const callbacks = {
|
||||||
getAuthToken: () => {
|
getAuthToken: () => {
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
if (token) {
|
if (token) {
|
||||||
return token
|
return token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the authentication token from local storage.
|
* Retrieves the authentication token from local storage.
|
||||||
*
|
*
|
||||||
* @return {string | undefined} The authentication token if found, otherwise undefined
|
* @return {string | undefined} The authentication token if found, otherwise undefined
|
||||||
*/
|
*/
|
||||||
const getAuthToken = () => callbacks?.getAuthToken?.()
|
const getAuthToken = () => callbacks?.getAuthToken?.();
|
||||||
|
|
||||||
const setAuthTokenCallback = (cb: ()=> string) => {
|
const setAuthTokenCallback = (cb: () => string) => {
|
||||||
callbacks.getAuthToken = cb
|
callbacks.getAuthToken = cb;
|
||||||
return callbacks.getAuthToken
|
return callbacks.getAuthToken;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the authentication token in the local storage.
|
* Sets the authentication token in the local storage.
|
||||||
@@ -262,9 +254,8 @@ const setAuthTokenCallback = (cb: ()=> string) => {
|
|||||||
*/
|
*/
|
||||||
const setAuthToken = (token: string) => {
|
const setAuthToken = (token: string) => {
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
localStorage.setItem(TOKEN_KEY, token)
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export { buildGoAPIOperation, getAuthToken, GoAPIHeaders, setAuthToken, setAuthTokenCallback };
|
||||||
export {buildGoAPIOperation,getAuthToken,GoAPIHeaders,setAuthToken,setAuthTokenCallback}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export type APIType = 'gorest' |'gorest2'| 'resolvespec';
|
export type APIType = 'gorest' | 'gorest2' | 'resolvespec';
|
||||||
|
|
||||||
export const APITypes: Record<string, APIType> = {
|
export const APITypes: Record<string, APIType> = {
|
||||||
GoRest: 'gorest',
|
GoRest: 'gorest',
|
||||||
GoRest2: 'gorest2',
|
GoRest2: 'gorest2',
|
||||||
ResolveSpec: 'resolvespec'
|
ResolveSpec: 'resolvespec',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export interface APIOptions {
|
export interface APIOptions {
|
||||||
@@ -15,4 +15,4 @@ export interface APIOptions {
|
|||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormRequestType = 'change' | 'delete' | 'insert' | 'select' ;
|
export type FormRequestType = 'change' | 'delete' | 'insert' | 'select' | 'update' | 'view';
|
||||||
|
|||||||
Reference in New Issue
Block a user