Compare commits

...

40 Commits

Author SHA1 Message Date
9bac48d5dd Form prototype 2026-01-12 23:20:34 +02:00
fbb65afc94 Merge branch 'main' of git.warky.dev:wdevs/oranguru into rw 2026-01-12 23:20:02 +02:00
095ddf6162 refactor(former): 🔄 restructure form components and stores
* Remove unused FormLayout and SuperForm stores.
* Consolidate form logic into Former component.
* Implement new Former layout and types.
* Update stories for new Former component.
* Clean up unused styles and types across the project.
2026-01-12 23:19:25 +02:00
Hein
0d9511df77 fix(adaptor): 🐛 Handle undefined filter IDs in search 2026-01-12 11:00:58 +02:00
b2817f4233 Form prototype 2026-01-11 09:45:03 +02:00
Hein
71403289c2 Updated exports 2025-12-01 12:20:13 +02:00
Hein
7025f316de RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.23

[skip ci]
2025-12-01 11:48:08 +02:00
Hein
32054118de docs(changeset): Using the effect of array as feature. 2025-12-01 11:48:05 +02:00
Hein
017b6445fb RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.22

[skip ci]
2025-12-01 11:42:28 +02:00
Hein
7c1d47819a docs(changeset): Possible selection fixes 2025-12-01 11:42:26 +02:00
Hein
b514c906c8 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.21

[skip ci]
2025-12-01 11:14:25 +02:00
Hein
6664c988b7 docs(changeset): Calls onchange on cell click since selection does not change. 2025-12-01 11:14:22 +02:00
Hein
249c283819 Update buffer when onCellClick 2025-12-01 10:39:28 +02:00
Hein
1ce5c25098 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.20

[skip ci]
2025-10-30 14:54:52 +02:00
Hein
30581de17e docs(changeset): Version bump 2025-10-30 14:54:48 +02:00
Hein
8784a28a30 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.19

[skip ci]
2025-10-30 14:54:02 +02:00
Hein
1b2bf6282d docs(changeset): Fixed refresh bug 2025-10-30 14:54:00 +02:00
Hein
abcf08f98e Fixed the refresh bug 2025-10-30 14:53:42 +02:00
Hein
0ba8dca0b4 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.18

[skip ci]
2025-10-30 14:38:13 +02:00
Hein
7cfefa9e6d docs(changeset): API props change bug 2025-10-30 14:38:11 +02:00
Hein
e879abb43f Fixed api bug 2025-10-30 14:37:53 +02:00
Hein
9f04b36e7e RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.17

[skip ci]
2025-10-30 14:23:32 +02:00
Hein
03210a3a7a docs(changeset): Updated selected cols bug 2025-10-30 14:23:30 +02:00
Hein
e6560aa990 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.16

[skip ci]
2025-10-30 13:04:30 +02:00
Hein
5e922df97a docs(changeset): A Few fixes 2025-10-30 13:04:26 +02:00
Hein
abf9433c10 A few fixes 2025-10-30 13:04:13 +02:00
Hein
1284f46aa9 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.15

[skip ci]
2025-10-30 11:45:30 +02:00
Hein
a1202f9b6d docs(changeset): Hopefully fix the options not always loading 2025-10-30 11:45:26 +02:00
Hein
a8a172cbfe Hopefully fix the options not always loading 2025-10-30 11:45:08 +02:00
Hein
9f960a6729 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.14

[skip ci]
2025-10-30 10:47:21 +02:00
Hein
864188c599 docs(changeset): Added searchfields 2025-10-30 10:47:19 +02:00
Hein
5fc02f9671 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.13

[skip ci]
2025-10-30 10:42:11 +02:00
Hein
b4058f1ef3 docs(changeset): Fixed search and allow row selection for only rows with keys 2025-10-30 10:42:07 +02:00
Hein
0943ffc483 Search working 2025-10-30 10:20:25 +02:00
Hein
bd47e9d0ab RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.12

[skip ci]
2025-10-29 16:26:40 +02:00
Hein
57c72e656f docs(changeset): Search String and Better GoAPI functionality 2025-10-29 16:26:38 +02:00
Hein
b977308e54 RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.11

[skip ci]
2025-10-29 14:57:03 +02:00
Hein
54deac6ccc docs(changeset): Added refs and exports, isEmpty 2025-10-29 14:56:59 +02:00
Hein
615b89360a RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.10

[skip ci]
2025-10-24 16:52:20 +02:00
Hein
64cfed8a67 docs(changeset): Scroll to and forwarded ref 2025-10-24 16:52:18 +02:00
55 changed files with 3365 additions and 321 deletions

View File

@@ -1,13 +1,16 @@
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
export function PreviewDecorator(Story: any, { parameters }: any) { export function PreviewDecorator(Story: any, { parameters }: any) {
console.log('Rendering decorator', parameters); console.log('Rendering decorator', parameters);
return ( return (
<MantineProvider> <MantineProvider>
<div style={{ height: 'calc(100vh - 64px)', width: 'calc(100vw - 64px)' }}> <ModalsProvider>
<Story key={'mainStory'} /> <div style={{ height: 'calc(100vh - 64px)', width: 'calc(100vw - 64px)' }}>
</div> <Story key={'mainStory'} />
</div>
</ModalsProvider>
</MantineProvider> </MantineProvider>
); );
} }

View File

@@ -1,5 +1,89 @@
# @warkypublic/zustandsyncstore # @warkypublic/zustandsyncstore
## 0.0.23
### Patch Changes
- 3205411: Using the effect of array as feature.
## 0.0.22
### Patch Changes
- 7c1d478: Possible selection fixes
## 0.0.21
### Patch Changes
- 6664c98: Calls onchange on cell click since selection does not change.
## 0.0.20
### Patch Changes
- 30581de: Version bump
## 0.0.19
### Patch Changes
- 1b2bf62: Fixed refresh bug
## 0.0.18
### Patch Changes
- 7cfefa9: API props change bug
## 0.0.17
### Patch Changes
- 03210a3: Updated selected cols bug
## 0.0.16
### Patch Changes
- 5e922df: A Few fixes
## 0.0.15
### Patch Changes
- a1202f9: Hopefully fix the options not always loading
## 0.0.14
### Patch Changes
- 864188c: Added searchfields
## 0.0.13
### Patch Changes
- b4058f1: Fixed search and allow row selection for only rows with keys
## 0.0.12
### Patch Changes
- 57c72e6: Search String and Better GoAPI functionality
## 0.0.11
### Patch Changes
- 54deac6: Added refs and exports, isEmpty
## 0.0.10
### Patch Changes
- 64cfed8: Scroll to and forwarded ref
## 0.0.9 ## 0.0.9
### Patch Changes ### Patch Changes

View File

@@ -16,19 +16,22 @@ const config = defineConfig([
plugins: { js }, plugins: { js },
}, },
// reactHooks.configs['recommended-latest'], // reactHooks.configs['recommended-latest'],
{...reactRefresh.configs.vite, ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],}, { ...reactRefresh.configs.vite, ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] },
tseslint.configs.recommended, tseslint.configs.recommended,
{ {
...pluginReact.configs.flat.recommended, ...pluginReact.configs.flat.recommended,
ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', '*stories.tsx'],
rules: {...pluginReact.configs.flat.recommended.rules, rules: {
...pluginReact.configs.flat.recommended.rules,
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
} 'react-refresh/only-export-components': 'warn',
},
}, },
perfectionist.configs['recommended-alphabetical'], perfectionist.configs['recommended-alphabetical'],
{ {
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/ban-ts-comment': 'off',
}, },
}, },
]); ]);

View File

@@ -1,7 +1,7 @@
{ {
"name": "@warkypublic/oranguru", "name": "@warkypublic/oranguru",
"author": "Warky Devs", "author": "Warky Devs",
"version": "0.0.9", "version": "0.0.23",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -49,12 +49,14 @@
"./oranguru.css": "./src/oranguru.css" "./oranguru.css": "./src/oranguru.css"
}, },
"dependencies": { "dependencies": {
"moment": "^2.30.1" "moment": "^2.30.1"
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.29.7", "@changesets/cli": "^2.29.7",
"@eslint/js": "^9.38.0", "@eslint/js": "^9.38.0",
"@storybook/react-vite": "^9.1.13", "@storybook/react-vite": "^9.1.15",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
@@ -62,14 +64,14 @@
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@typescript-eslint/parser": "^8.46.2", "@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.2.0",
"eslint": "^9.38.0", "eslint": "^9.38.0",
"eslint-config-mantine": "^4.0.3", "eslint-config-mantine": "^4.0.3",
"eslint-plugin-perfectionist": "^4.15.1", "eslint-plugin-perfectionist": "^4.15.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-storybook": "^9.1.13", "eslint-plugin-storybook": "^9.1.15",
"global": "^4.4.0", "global": "^4.4.0",
"globals": "^16.4.0", "globals": "^16.4.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
@@ -81,23 +83,26 @@
"prettier-eslint": "^16.4.2", "prettier-eslint": "^16.4.2",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"storybook": "^9.1.13", "storybook": "^9.1.15",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.46.2", "typescript-eslint": "^8.46.2",
"vite": "^7.1.11", "vite": "^7.1.12",
"vite-plugin-dts": "^4.5.4", "vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^4.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"@glideapps/glide-data-grid": "^6.0.3", "@glideapps/glide-data-grid": "^6.0.3",
"@mantine/core": "^8.3.1", "@mantine/core": "^8.3.1",
"@mantine/hooks": "^8.3.1", "@mantine/hooks": "^8.3.1",
"@mantine/notifications": "^8.3.5", "@mantine/notifications": "^8.3.5",
"@mantine/modals": "^8.3.5",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.5", "@tanstack/react-query": "^5.90.5",
"@warkypublic/artemis-kit": "^1.0.10", "@warkypublic/artemis-kit": "^1.0.10",
"@warkypublic/zustandsyncstore": "^0.0.4", "@warkypublic/zustandsyncstore": "^0.0.4",
"react-hook-form": "^7.71.0",
"immer": "^10.1.3", "immer": "^10.1.3",
"react": ">= 19.0.0", "react": ">= 19.0.0",
"react-dom": ">= 19.0.0", "react-dom": ">= 19.0.0",

377
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
'@mantine/hooks': '@mantine/hooks':
specifier: ^8.3.1 specifier: ^8.3.1
version: 8.3.1(react@19.2.0) version: 8.3.1(react@19.2.0)
'@mantine/modals':
specifier: ^8.3.5
version: 8.3.12(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mantine/notifications': '@mantine/notifications':
specifier: ^8.3.5 specifier: ^8.3.5
version: 8.3.5(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 8.3.5(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -38,6 +41,9 @@ importers:
moment: moment:
specifier: ^2.30.1 specifier: ^2.30.1
version: 2.30.1 version: 2.30.1
react-hook-form:
specifier: ^7.71.0
version: 7.71.0(react@19.2.0)
use-sync-external-store: use-sync-external-store:
specifier: '>= 1.4.0' specifier: '>= 1.4.0'
version: 1.5.0(react@19.2.0) version: 1.5.0(react@19.2.0)
@@ -52,8 +58,8 @@ importers:
specifier: ^9.38.0 specifier: ^9.38.0
version: 9.38.0 version: 9.38.0
'@storybook/react-vite': '@storybook/react-vite':
specifier: ^9.1.13 specifier: ^9.1.15
version: 9.1.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.50.2)(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) version: 9.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.50.2)(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
'@testing-library/jest-dom': '@testing-library/jest-dom':
specifier: ^6.9.1 specifier: ^6.9.1
version: 6.9.1 version: 6.9.1
@@ -76,8 +82,8 @@ importers:
specifier: ^8.46.2 specifier: ^8.46.2
version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
'@vitejs/plugin-react-swc': '@vitejs/plugin-react-swc':
specifier: ^4.1.0 specifier: ^4.2.0
version: 4.1.0(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) version: 4.2.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
eslint: eslint:
specifier: ^9.38.0 specifier: ^9.38.0
version: 9.38.0(jiti@2.6.1) version: 9.38.0(jiti@2.6.1)
@@ -91,14 +97,14 @@ importers:
specifier: ^7.37.5 specifier: ^7.37.5
version: 7.37.5(eslint@9.38.0(jiti@2.6.1)) version: 7.37.5(eslint@9.38.0(jiti@2.6.1))
eslint-plugin-react-hooks: eslint-plugin-react-hooks:
specifier: ^7.0.0 specifier: ^7.0.1
version: 7.0.0(eslint@9.38.0(jiti@2.6.1)) version: 7.0.1(eslint@9.38.0(jiti@2.6.1))
eslint-plugin-react-refresh: eslint-plugin-react-refresh:
specifier: ^0.4.24 specifier: ^0.4.24
version: 0.4.24(eslint@9.38.0(jiti@2.6.1)) version: 0.4.24(eslint@9.38.0(jiti@2.6.1))
eslint-plugin-storybook: eslint-plugin-storybook:
specifier: ^9.1.13 specifier: ^9.1.15
version: 9.1.13(eslint@9.38.0(jiti@2.6.1))(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3) version: 9.1.15(eslint@9.38.0(jiti@2.6.1))(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)
global: global:
specifier: ^4.4.0 specifier: ^4.4.0
version: 4.4.0 version: 4.4.0
@@ -133,8 +139,8 @@ importers:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.0(react@19.2.0) version: 19.2.0(react@19.2.0)
storybook: storybook:
specifier: ^9.1.13 specifier: ^9.1.15
version: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) version: 9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
typescript: typescript:
specifier: ~5.9.3 specifier: ~5.9.3
version: 5.9.3 version: 5.9.3
@@ -142,17 +148,17 @@ importers:
specifier: ^8.46.2 specifier: ^8.46.2
version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
vite: vite:
specifier: ^7.1.11 specifier: ^7.1.12
version: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)) version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
vite-plugin-dts: vite-plugin-dts:
specifier: ^4.5.4 specifier: ^4.5.4
version: 4.5.4(@types/node@24.9.1)(rollup@4.50.2)(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) version: 4.5.4(@types/node@24.9.1)(rollup@4.50.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
vite-tsconfig-paths: vite-tsconfig-paths:
specifier: ^5.1.4 specifier: ^5.1.4
version: 5.1.4(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
vitest: vitest:
specifier: ^3.2.4 specifier: ^4.0.3
version: 3.2.4(@types/node@24.9.1)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(sugarss@5.0.1(postcss@8.5.6)) version: 4.0.3(@types/node@24.9.1)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(sugarss@5.0.1(postcss@8.5.6))
packages: packages:
@@ -702,6 +708,14 @@ packages:
peerDependencies: peerDependencies:
react: ^18.x || ^19.x react: ^18.x || ^19.x
'@mantine/modals@8.3.12':
resolution: {integrity: sha512-+uRyGe2lLy601qlMk+8aR9d/Aibu+dZi6Jcmvm5z8Gw4ocviyMMlnd8BLSQ/Jvib2OX8fWj+yUQN7FMQ4Rbwjw==}
peerDependencies:
'@mantine/core': 8.3.12
'@mantine/hooks': 8.3.12
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
'@mantine/notifications@8.3.5': '@mantine/notifications@8.3.5':
resolution: {integrity: sha512-8TvzrPxfdtOLGTalv7Ei1hy2F6KbR3P7/V73yw3AOKhrf1ydS89sqV2ShbsucHGJk9Pto0wjdTPd8Q7pm5MAYw==} resolution: {integrity: sha512-8TvzrPxfdtOLGTalv7Ei1hy2F6KbR3P7/V73yw3AOKhrf1ydS89sqV2ShbsucHGJk9Pto0wjdTPd8Q7pm5MAYw==}
peerDependencies: peerDependencies:
@@ -750,8 +764,8 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@rolldown/pluginutils@1.0.0-beta.35': '@rolldown/pluginutils@1.0.0-beta.43':
resolution: {integrity: sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==} resolution: {integrity: sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==}
'@rollup/pluginutils@5.3.0': '@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
@@ -892,43 +906,46 @@ packages:
'@sinclair/typebox@0.27.8': '@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@storybook/builder-vite@9.1.13': '@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-pmtIjU02ASJOZKdL8DoxWXJgZnpTDgD5WmMnjKJh9FaWmc2YiCW2Y6VRxPox96OM655jYHQe5+UIbk3Cwtwb4A==} resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@storybook/builder-vite@9.1.15':
resolution: {integrity: sha512-GZkx72cBnCuTL/cVOIWIicB4GCmZWx52zFGSC/qHOT/sKcUkrIoQSpVljqyPa66woHyUeSZX4mu7aGj5A27QVg==}
peerDependencies: peerDependencies:
storybook: ^9.1.13 storybook: ^9.1.15
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0
'@storybook/csf-plugin@9.1.13': '@storybook/csf-plugin@9.1.15':
resolution: {integrity: sha512-EMpzYuyt9FDcxxfBChWzfId50y8QMpdenviEQ8m+pa6c+ANx3pC5J6t7y0khD8TQu815sTy+nc6cc8PC45dPUA==} resolution: {integrity: sha512-UThWh7V3+zd+71XdIsNFkrNslkjyaD/HQPnjWGWBCU4ZWNWRSPd3r0r02nH0zzo+NBi0V4vzNDg/PmfD51iaOg==}
peerDependencies: peerDependencies:
storybook: ^9.1.13 storybook: ^9.1.15
'@storybook/global@5.0.0': '@storybook/global@5.0.0':
resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
'@storybook/react-dom-shim@9.1.13': '@storybook/react-dom-shim@9.1.15':
resolution: {integrity: sha512-/tMr9TmV3+98GEQO0S03k4gtKHGCpv9+k9Dmnv+TJK3TBz7QsaFEzMwe3gCgoTaebLACyVveDiZkWnCYAWB6NA==} resolution: {integrity: sha512-l6smvNwxh6kp2U/BupzQ4/NSraTWysZcAe2x+GO5CiIIB8Jbi41XLu5XIHI/GQRnNqpXXE3uMImiHGOORmHEXA==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.13 storybook: ^9.1.15
'@storybook/react-vite@9.1.13': '@storybook/react-vite@9.1.15':
resolution: {integrity: sha512-mV1bZ1bpkNQygnuDo1xMGAS5ZXuoXFF0WGmr/BzNDGmRhZ1K1HQh42kC0w3PklckFBUwCFxmP58ZwTFzf+/dJA==} resolution: {integrity: sha512-ORD3swzehAx6o7Sw/wmXr/L5Q4FANPsc1+G5s6OTfmbWNDy/e803h7lsUUFwokh7PeYgylJzbPMQ8HnkNghN0A==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.13 storybook: ^9.1.15
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0
'@storybook/react@9.1.13': '@storybook/react@9.1.15':
resolution: {integrity: sha512-B0UpYikKf29t8QGcdmumWojSQQ0phSDy/Ne2HYdrpNIxnUvHHUVOlGpq4lFcIDt52Ip5YG5GuAwJg3+eR4LCRg==} resolution: {integrity: sha512-tdd1Od3roaEQ2rjqk1L15yR/N2y/SLNcpPNp3n9AQT1eDPdbKzIzue7u1eW9KUFdwSF9S8xG35roDUkKwBJogQ==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.13 storybook: ^9.1.15
typescript: '>= 4.9.x' typescript: '>= 4.9.x'
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
@@ -1234,8 +1251,8 @@ packages:
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@vitejs/plugin-react-swc@4.1.0': '@vitejs/plugin-react-swc@4.2.0':
resolution: {integrity: sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==} resolution: {integrity: sha512-/tesahXD1qpkGC6FzMoFOJj0RyZdw9xLELOL+6jbElwmWfwOnIVy+IfpY+o9JfD9PKaR/Eyb6DNrvbXpuvA+8Q==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies: peerDependencies:
vite: ^4 || ^5 || ^6 || ^7 vite: ^4 || ^5 || ^6 || ^7
@@ -1243,6 +1260,9 @@ packages:
'@vitest/expect@3.2.4': '@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/expect@4.0.3':
resolution: {integrity: sha512-v3eSDx/bF25pzar6aEJrrdTXJduEBU3uSGXHslIdGIpJVP8tQQHV6x1ZfzbFQ/bLIomLSbR/2ZCfnaEGkWkiVQ==}
'@vitest/mocker@3.2.4': '@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies: peerDependencies:
@@ -1254,21 +1274,41 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/mocker@4.0.3':
resolution: {integrity: sha512-evZcRspIPbbiJEe748zI2BRu94ThCBE+RkjCpVF8yoVYuTV7hMe+4wLF/7K86r8GwJHSmAPnPbZhpXWWrg1qbA==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/runner@3.2.4': '@vitest/pretty-format@4.0.3':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} resolution: {integrity: sha512-N7gly/DRXzxa9w9sbDXwD9QNFYP2hw90LLLGDobPNwiWgyW95GMxsCt29/COIKKh3P7XJICR38PSDePenMBtsw==}
'@vitest/snapshot@3.2.4': '@vitest/runner@4.0.3':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} resolution: {integrity: sha512-1/aK6fPM0lYXWyGKwop2Gbvz1plyTps/HDbIIJXYtJtspHjpXIeB3If07eWpVH4HW7Rmd3Rl+IS/+zEAXrRtXA==}
'@vitest/snapshot@4.0.3':
resolution: {integrity: sha512-amnYmvZ5MTjNCP1HZmdeczAPLRD6iOm9+2nMRUGxbe/6sQ0Ymur0NnR9LIrWS8JA3wKE71X25D6ya/3LN9YytA==}
'@vitest/spy@3.2.4': '@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/spy@4.0.3':
resolution: {integrity: sha512-82vVL8Cqz7rbXaNUl35V2G7xeNMAjBdNOVaHbrzznT9BmiCiPOzhf0FhU3eP41nP1bLDm/5wWKZqkG4nyU95DQ==}
'@vitest/utils@3.2.4': '@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
'@vitest/utils@4.0.3':
resolution: {integrity: sha512-qV6KJkq8W3piW6MDIbGOmn1xhvcW4DuA07alqaQ+vdx7YA49J85pnwnxigZVQFQw3tWnQNRKWwhz5wbP6iv/GQ==}
'@volar/language-core@2.4.23': '@volar/language-core@2.4.23':
resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==}
@@ -1494,10 +1534,6 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1528,6 +1564,10 @@ packages:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'} engines: {node: '>=18'}
chai@6.2.0:
resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==}
engines: {node: '>=18'}
chalk@1.1.3: chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1804,8 +1844,8 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=8.45.0' eslint: '>=8.45.0'
eslint-plugin-react-hooks@7.0.0: eslint-plugin-react-hooks@7.0.1:
resolution: {integrity: sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==} resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
@@ -1821,12 +1861,12 @@ packages:
peerDependencies: peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
eslint-plugin-storybook@9.1.13: eslint-plugin-storybook@9.1.15:
resolution: {integrity: sha512-kPuhbtGDiJLB5OLZuwFZAxgzWakNDw64sJtXUPN8g0+VAeXfHyZEmsE28qIIETHxtal71lPKVm8QNnERaJHPJQ==} resolution: {integrity: sha512-c7eHGNoKmLkltdrR+KTwTiXDvXWOm8y9tqwcveN/wV10PeqDKyxLxn/dzVpMUwXd1gvEBdIjEjJfTATf6c53jQ==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
peerDependencies: peerDependencies:
eslint: '>=8' eslint: '>=8'
storybook: ^9.1.13 storybook: ^9.1.15
eslint-scope@7.2.2: eslint-scope@7.2.2:
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
@@ -2344,9 +2384,6 @@ packages:
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@3.14.1: js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true hasBin: true
@@ -2820,6 +2857,12 @@ packages:
resolution: {integrity: sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==} resolution: {integrity: sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
react-hook-form@7.71.0:
resolution: {integrity: sha512-oFDt/iIFMV9ZfV52waONXzg4xuSlbwKUPvXVH2jumL1me5qFhBMc4knZxuXiZ2+j6h546sYe3ZKJcg/900/iHw==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-html-attributes@1.4.6: react-html-attributes@1.4.6:
resolution: {integrity: sha512-uS3MmThNKFH2EZUQQw4k5pIcU7XIr208UE5dktrj/GOH1CMagqxDl4DCLpt3o2l9x+IB5nVYBeN3Cr4IutBXAg==} resolution: {integrity: sha512-uS3MmThNKFH2EZUQQw4k5pIcU7XIr208UE5dktrj/GOH1CMagqxDl4DCLpt3o2l9x+IB5nVYBeN3Cr4IutBXAg==}
@@ -3058,8 +3101,8 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
storybook@9.1.13: storybook@9.1.15:
resolution: {integrity: sha512-G3KZ36EVzXyHds72B/qtWiJnhUpM0xOUeYlDcO9DSHL1bDTv15cW4+upBl+mcBZrDvU838cn7Bv4GpF+O5MCfw==} resolution: {integrity: sha512-es7uDdEwRVVUAt7XLAZZ1hicOq9r4ov5NFeFPpa2YEyAsyHYOCr0CTlHBfslWG6D5EVNWK3kVIIuW8GHB6hEig==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
prettier: ^2 || ^3 prettier: ^2 || ^3
@@ -3130,9 +3173,6 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
sugarss@5.0.1: sugarss@5.0.1:
resolution: {integrity: sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==} resolution: {integrity: sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==}
engines: {node: '>=18.0'} engines: {node: '>=18.0'}
@@ -3181,14 +3221,14 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0: tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
tinyrainbow@3.0.3:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
tinyspy@4.0.4: tinyspy@4.0.4:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -3388,11 +3428,6 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true hasBin: true
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite-plugin-dts@4.5.4: vite-plugin-dts@4.5.4:
resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==}
peerDependencies: peerDependencies:
@@ -3410,8 +3445,8 @@ packages:
vite: vite:
optional: true optional: true
vite@7.1.11: vite@7.1.12:
resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -3450,16 +3485,18 @@ packages:
yaml: yaml:
optional: true optional: true
vitest@3.2.4: vitest@4.0.3:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} resolution: {integrity: sha512-IUSop8jgaT7w0g1yOM/35qVtKjr/8Va4PrjzH1OUb0YH4c3OXB2lCZDkMAB6glA8T5w8S164oJGsbcmAecr4sA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@edge-runtime/vm': '*' '@edge-runtime/vm': '*'
'@types/debug': ^4.1.12 '@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser': 3.2.4 '@vitest/browser-playwright': 4.0.3
'@vitest/ui': 3.2.4 '@vitest/browser-preview': 4.0.3
'@vitest/browser-webdriverio': 4.0.3
'@vitest/ui': 4.0.3
happy-dom: '*' happy-dom: '*'
jsdom: '*' jsdom: '*'
peerDependenciesMeta: peerDependenciesMeta:
@@ -3469,7 +3506,11 @@ packages:
optional: true optional: true
'@types/node': '@types/node':
optional: true optional: true
'@vitest/browser': '@vitest/browser-playwright':
optional: true
'@vitest/browser-preview':
optional: true
'@vitest/browser-webdriverio':
optional: true optional: true
'@vitest/ui': '@vitest/ui':
optional: true optional: true
@@ -4165,12 +4206,12 @@ snapshots:
dependencies: dependencies:
'@sinclair/typebox': 0.27.8 '@sinclair/typebox': 0.27.8
'@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))': '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))':
dependencies: dependencies:
glob: 10.4.5 glob: 10.4.5
magic-string: 0.30.19 magic-string: 0.30.19
react-docgen-typescript: 2.4.0(typescript@5.9.3) react-docgen-typescript: 2.4.0(typescript@5.9.3)
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)) vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
@@ -4264,6 +4305,13 @@ snapshots:
dependencies: dependencies:
react: 19.2.0 react: 19.2.0
'@mantine/modals@8.3.12(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@mantine/core': 8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mantine/hooks': 8.3.1(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
'@mantine/notifications@8.3.5(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@mantine/notifications@8.3.5(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@mantine/core': 8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mantine/core': 8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -4343,7 +4391,7 @@ snapshots:
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
'@rolldown/pluginutils@1.0.0-beta.35': {} '@rolldown/pluginutils@1.0.0-beta.43': {}
'@rollup/pluginutils@5.3.0(rollup@4.50.2)': '@rollup/pluginutils@5.3.0(rollup@4.50.2)':
dependencies: dependencies:
@@ -4452,53 +4500,55 @@ snapshots:
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
'@storybook/builder-vite@9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))': '@standard-schema/spec@1.0.0': {}
dependencies:
'@storybook/csf-plugin': 9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))
storybook: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
ts-dedent: 2.2.0
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
'@storybook/csf-plugin@9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))': '@storybook/builder-vite@9.1.15(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))':
dependencies: dependencies:
storybook: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) '@storybook/csf-plugin': 9.1.15(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))
storybook: 9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
ts-dedent: 2.2.0
vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
'@storybook/csf-plugin@9.1.15(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))':
dependencies:
storybook: 9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
unplugin: 1.16.1 unplugin: 1.16.1
'@storybook/global@5.0.0': {} '@storybook/global@5.0.0': {}
'@storybook/react-dom-shim@9.1.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))': '@storybook/react-dom-shim@9.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))':
dependencies: dependencies:
react: 19.2.0 react: 19.2.0
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
storybook: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) storybook: 9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
'@storybook/react-vite@9.1.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.50.2)(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))': '@storybook/react-vite@9.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.50.2)(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))':
dependencies: dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
'@rollup/pluginutils': 5.3.0(rollup@4.50.2) '@rollup/pluginutils': 5.3.0(rollup@4.50.2)
'@storybook/builder-vite': 9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) '@storybook/builder-vite': 9.1.15(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
'@storybook/react': 9.1.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3) '@storybook/react': 9.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)
find-up: 7.0.0 find-up: 7.0.0
magic-string: 0.30.19 magic-string: 0.30.19
react: 19.2.0 react: 19.2.0
react-docgen: 8.0.1 react-docgen: 8.0.1
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
resolve: 1.22.10 resolve: 1.22.10
storybook: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) storybook: 9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
tsconfig-paths: 4.2.0 tsconfig-paths: 4.2.0
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)) vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
- typescript - typescript
'@storybook/react@9.1.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)': '@storybook/react@9.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3)':
dependencies: dependencies:
'@storybook/global': 5.0.0 '@storybook/global': 5.0.0
'@storybook/react-dom-shim': 9.1.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))) '@storybook/react-dom-shim': 9.1.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))
react: 19.2.0 react: 19.2.0
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
storybook: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) storybook: 9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
@@ -4842,11 +4892,11 @@ snapshots:
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react-swc@4.1.0(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))': '@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.35 '@rolldown/pluginutils': 1.0.0-beta.43
'@swc/core': 1.13.5 '@swc/core': 1.13.5
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)) vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
transitivePeerDependencies: transitivePeerDependencies:
- '@swc/helpers' - '@swc/helpers'
@@ -4858,27 +4908,47 @@ snapshots:
chai: 5.3.3 chai: 5.3.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))': '@vitest/expect@4.0.3':
dependencies:
'@standard-schema/spec': 1.0.0
'@types/chai': 5.2.2
'@vitest/spy': 4.0.3
'@vitest/utils': 4.0.3
chai: 6.2.0
tinyrainbow: 3.0.3
'@vitest/mocker@3.2.4(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))':
dependencies: dependencies:
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.19 magic-string: 0.30.19
optionalDependencies: optionalDependencies:
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)) vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
'@vitest/mocker@4.0.3(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))':
dependencies:
'@vitest/spy': 4.0.3
estree-walker: 3.0.3
magic-string: 0.30.19
optionalDependencies:
vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/runner@3.2.4': '@vitest/pretty-format@4.0.3':
dependencies: dependencies:
'@vitest/utils': 3.2.4 tinyrainbow: 3.0.3
pathe: 2.0.3
strip-literal: 3.1.0
'@vitest/snapshot@3.2.4': '@vitest/runner@4.0.3':
dependencies: dependencies:
'@vitest/pretty-format': 3.2.4 '@vitest/utils': 4.0.3
pathe: 2.0.3
'@vitest/snapshot@4.0.3':
dependencies:
'@vitest/pretty-format': 4.0.3
magic-string: 0.30.19 magic-string: 0.30.19
pathe: 2.0.3 pathe: 2.0.3
@@ -4886,12 +4956,19 @@ snapshots:
dependencies: dependencies:
tinyspy: 4.0.4 tinyspy: 4.0.4
'@vitest/spy@4.0.3': {}
'@vitest/utils@3.2.4': '@vitest/utils@3.2.4':
dependencies: dependencies:
'@vitest/pretty-format': 3.2.4 '@vitest/pretty-format': 3.2.4
loupe: 3.2.1 loupe: 3.2.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/utils@4.0.3':
dependencies:
'@vitest/pretty-format': 4.0.3
tinyrainbow: 3.0.3
'@volar/language-core@2.4.23': '@volar/language-core@2.4.23':
dependencies: dependencies:
'@volar/source-map': 2.4.23 '@volar/source-map': 2.4.23
@@ -5138,8 +5215,6 @@ snapshots:
node-releases: 2.0.21 node-releases: 2.0.21
update-browserslist-db: 1.1.3(browserslist@4.26.2) update-browserslist-db: 1.1.3(browserslist@4.26.2)
cac@6.7.14: {}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -5173,6 +5248,8 @@ snapshots:
loupe: 3.2.1 loupe: 3.2.1
pathval: 2.0.1 pathval: 2.0.1
chai@6.2.0: {}
chalk@1.1.3: chalk@1.1.3:
dependencies: dependencies:
ansi-styles: 2.2.1 ansi-styles: 2.2.1
@@ -5528,7 +5605,7 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
eslint-plugin-react-hooks@7.0.0(eslint@9.38.0(jiti@2.6.1)): eslint-plugin-react-hooks@7.0.1(eslint@9.38.0(jiti@2.6.1)):
dependencies: dependencies:
'@babel/core': 7.28.4 '@babel/core': 7.28.4
'@babel/parser': 7.28.4 '@babel/parser': 7.28.4
@@ -5565,11 +5642,11 @@ snapshots:
string.prototype.matchall: 4.0.12 string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0 string.prototype.repeat: 1.0.0
eslint-plugin-storybook@9.1.13(eslint@9.38.0(jiti@2.6.1))(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3): eslint-plugin-storybook@9.1.15(eslint@9.38.0(jiti@2.6.1))(storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))))(typescript@5.9.3):
dependencies: dependencies:
'@typescript-eslint/utils': 8.43.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.38.0(jiti@2.6.1) eslint: 9.38.0(jiti@2.6.1)
storybook: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) storybook: 9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
@@ -6161,8 +6238,6 @@ snapshots:
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
js-yaml@3.14.1: js-yaml@3.14.1:
dependencies: dependencies:
argparse: 1.0.10 argparse: 1.0.10
@@ -6638,6 +6713,10 @@ snapshots:
dependencies: dependencies:
prop-types: 15.8.1 prop-types: 15.8.1
react-hook-form@7.71.0(react@19.2.0):
dependencies:
react: 19.2.0
react-html-attributes@1.4.6: react-html-attributes@1.4.6:
dependencies: dependencies:
html-element-attributes: 1.3.1 html-element-attributes: 1.3.1
@@ -6922,13 +7001,13 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
internal-slot: 1.1.0 internal-slot: 1.1.0
storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))): storybook@9.1.15(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))):
dependencies: dependencies:
'@storybook/global': 5.0.0 '@storybook/global': 5.0.0
'@testing-library/jest-dom': 6.9.1 '@testing-library/jest-dom': 6.9.1
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
better-opn: 3.0.2 better-opn: 3.0.2
esbuild: 0.25.9 esbuild: 0.25.9
@@ -7032,10 +7111,6 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
sugarss@5.0.1(postcss@8.5.6): sugarss@5.0.1(postcss@8.5.6):
dependencies: dependencies:
postcss: 8.5.6 postcss: 8.5.6
@@ -7071,10 +7146,10 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
tinypool@1.1.1: {}
tinyrainbow@2.0.0: {} tinyrainbow@2.0.0: {}
tinyrainbow@3.0.3: {}
tinyspy@4.0.4: {} tinyspy@4.0.4: {}
tldts-core@7.0.17: {} tldts-core@7.0.17: {}
@@ -7251,28 +7326,7 @@ snapshots:
uuid@11.1.0: {} uuid@11.1.0: {}
vite-node@3.2.4(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)): vite-plugin-dts@4.5.4(@types/node@24.9.1)(rollup@4.50.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-plugin-dts@4.5.4(@types/node@24.9.1)(rollup@4.50.2)(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))):
dependencies: dependencies:
'@microsoft/api-extractor': 7.52.13(@types/node@24.9.1) '@microsoft/api-extractor': 7.52.13(@types/node@24.9.1)
'@rollup/pluginutils': 5.3.0(rollup@4.50.2) '@rollup/pluginutils': 5.3.0(rollup@4.50.2)
@@ -7285,24 +7339,24 @@ snapshots:
magic-string: 0.30.19 magic-string: 0.30.19
typescript: 5.9.3 typescript: 5.9.3
optionalDependencies: optionalDependencies:
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)) vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- rollup - rollup
- supports-color - supports-color
vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))): vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))):
dependencies: dependencies:
debug: 4.4.3 debug: 4.4.3
globrex: 0.1.2 globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.9.3) tsconfck: 3.1.6(typescript@5.9.3)
optionalDependencies: optionalDependencies:
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)) vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)): vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)):
dependencies: dependencies:
esbuild: 0.25.9 esbuild: 0.25.9
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -7316,18 +7370,17 @@ snapshots:
jiti: 2.6.1 jiti: 2.6.1
sugarss: 5.0.1(postcss@8.5.6) sugarss: 5.0.1(postcss@8.5.6)
vitest@3.2.4(@types/node@24.9.1)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(sugarss@5.0.1(postcss@8.5.6)): vitest@4.0.3(@types/node@24.9.1)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(sugarss@5.0.1(postcss@8.5.6)):
dependencies: dependencies:
'@types/chai': 5.2.2 '@vitest/expect': 4.0.3
'@vitest/expect': 3.2.4 '@vitest/mocker': 4.0.3(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6)))
'@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))) '@vitest/pretty-format': 4.0.3
'@vitest/pretty-format': 3.2.4 '@vitest/runner': 4.0.3
'@vitest/runner': 3.2.4 '@vitest/snapshot': 4.0.3
'@vitest/snapshot': 3.2.4 '@vitest/spy': 4.0.3
'@vitest/spy': 3.2.4 '@vitest/utils': 4.0.3
'@vitest/utils': 3.2.4
chai: 5.3.3
debug: 4.4.3 debug: 4.4.3
es-module-lexer: 1.7.0
expect-type: 1.2.2 expect-type: 1.2.2
magic-string: 0.30.19 magic-string: 0.30.19
pathe: 2.0.3 pathe: 2.0.3
@@ -7336,10 +7389,8 @@ snapshots:
tinybench: 2.9.0 tinybench: 2.9.0
tinyexec: 0.3.2 tinyexec: 0.3.2
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinypool: 1.1.1 tinyrainbow: 3.0.3
tinyrainbow: 2.0.0 vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
vite-node: 3.2.4(@types/node@24.9.1)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.9.1 '@types/node': 24.9.1

View File

@@ -0,0 +1,38 @@
import React, { type ReactNode } from 'react'
import { Card, Stack, LoadingOverlay } from '@mantine/core'
import { FormSection } from './FormSection'
interface FormProps {
children: ReactNode
loading?: boolean
[key: string]: any
}
export const Form: React.FC<FormProps> & {
Section: typeof FormSection
} = ({ children, loading, ...others }) => {
return (
<Card
withBorder
component={Stack}
w="100%"
h="100%"
padding={0}
styles={{
root: {
height: '100%',
display: 'flex',
flexDirection: 'column',
},
}}
shadow="sm"
radius="md"
{...others}
>
<LoadingOverlay visible={loading || false} />
{children}
</Card>
)
}
Form.Section = FormSection

View File

@@ -0,0 +1,55 @@
import React, { type ReactNode } from 'react'
import { Modal } from '@mantine/core'
import { Form } from './Form'
import { FormLayoutStoreProvider, useFormLayoutStore } from '../store/FormLayout.store'
import type { RequestType } from '../types'
interface FormLayoutProps {
children: ReactNode
dirty?: boolean
loading?: boolean
onCancel?: () => void
onSubmit?: () => void
request?: RequestType
modal?: boolean
modalProps?: any
nested?: boolean
deleteFormProps?: any
[key: string]: any
}
const LayoutComponent: React.FC<FormLayoutProps> = ({
children,
modal,
modalProps,
...others
}) => {
const { request } = useFormLayoutStore((state) => ({
request: state.request,
}))
const modalWidth = request === 'delete' ? 400 : modalProps?.width
return modal === true ? (
<Modal
onClose={() => modalProps?.onClose?.()}
opened={modalProps?.opened || false}
size="auto"
withCloseButton={false}
centered={request !== 'delete'}
{...modalProps}
>
<div style={{ height: modalProps?.height, width: modalWidth }}>
<Form {...others}>{children}</Form>
</div>
</Modal>
) : (
<Form {...others}>{children}</Form>
)
}
export const FormLayout: React.FC<FormLayoutProps> = (props) => (
<FormLayoutStoreProvider {...props}>
<LayoutComponent {...props} />
</FormLayoutStoreProvider>
)

View File

@@ -0,0 +1,117 @@
import React, { type ReactNode } from 'react'
import { Stack, Group, Paper, Button, Title, Box } from '@mantine/core'
import { useFormLayoutStore } from '../store/FormLayout.store'
interface FormSectionProps {
type: 'header' | 'body' | 'footer' | 'error'
title?: string
rightSection?: ReactNode
children?: ReactNode
buttonTitles?: { submit?: string; cancel?: string }
className?: string
[key: string]: any
}
export const FormSection: React.FC<FormSectionProps> = ({
type,
title,
rightSection,
children,
buttonTitles,
className,
...others
}) => {
const { onCancel, onSubmit, request, loading } = useFormLayoutStore((state) => ({
onCancel: state.onCancel,
onSubmit: state.onSubmit,
request: state.request,
loading: state.loading,
}))
if (type === 'header') {
return (
<Group
justify="space-between"
p="md"
style={{
borderBottom: '1px solid var(--mantine-color-gray-3)',
}}
className={className}
{...others}
>
<Title order={4} size="h5">
{title}
</Title>
{rightSection && <Box>{rightSection}</Box>}
</Group>
)
}
if (type === 'body') {
return (
<Stack
gap="md"
p="md"
style={{ flex: 1, overflow: 'auto' }}
className={className}
{...others}
>
{children}
</Stack>
)
}
if (type === 'footer') {
return (
<Group
justify="flex-end"
gap="xs"
p="md"
style={{
borderTop: '1px solid var(--mantine-color-gray-3)',
}}
className={className}
{...others}
>
{children}
{request !== 'view' && (
<>
<Button
variant="default"
onClick={onCancel}
disabled={loading}
>
{buttonTitles?.cancel ?? 'Cancel'}
</Button>
<Button
type="submit"
onClick={onSubmit}
loading={loading}
>
{buttonTitles?.submit ?? (request === 'delete' ? 'Delete' : 'Save')}
</Button>
</>
)}
</Group>
)
}
if (type === 'error') {
return (
<Paper
p="sm"
m="md"
style={{
backgroundColor: 'var(--mantine-color-red-0)',
border: '1px solid var(--mantine-color-red-3)',
}}
className={className}
{...others}
>
{children}
</Paper>
)
}
return null
}

View File

@@ -0,0 +1,34 @@
import React, { forwardRef, type ReactElement, type Ref } from 'react'
import { FormProvider, useForm, type FieldValues } from 'react-hook-form'
import { Provider } from '../store/SuperForm.store'
import type { SuperFormProps, SuperFormRef } from '../types'
import Layout from './SuperFormLayout'
import SuperFormPersist from './SuperFormPersist'
const SuperForm = <T extends FieldValues>(
{ useFormProps, gridRef, children, persist, ...others }: SuperFormProps<T>,
ref
) => {
const form = useForm<T>({ ...useFormProps })
return (
<Provider {...others}>
<FormProvider {...form}>
{persist && (
<SuperFormPersist storageKey={typeof persist === 'object' ? persist.storageKey : null} />
)}
<Layout<T> gridRef={gridRef} ref={ref}>
{children}
</Layout>
</FormProvider>
</Provider>
)
}
const FRSuperForm = forwardRef(SuperForm) as <T extends FieldValues>(
props: SuperFormProps<T> & {
ref?: Ref<SuperFormRef<T>>
}
) => ReactElement
export default FRSuperForm

View File

@@ -0,0 +1,364 @@
import React, {
forwardRef,
RefObject,
useEffect,
useImperativeHandle,
useMemo,
type MutableRefObject,
type ReactElement,
type ReactNode,
type Ref,
} from 'react'
import { useFormContext, useFormState, type FieldValues, type UseFormReturn } from 'react-hook-form'
import { v4 as uuid } from 'uuid'
import {
ActionIcon,
Group,
List,
LoadingOverlay,
Paper,
Spoiler,
Stack,
Title,
Tooltip,
Transition,
} from '@mantine/core'
import { IconChevronsLeft, IconChevronsRight } from '@tabler/icons-react'
import { useUncontrolled } from '@mantine/hooks'
import useRemote from '../hooks/useRemote'
import { useStore } from '../store/SuperForm.store'
import classes from '../styles/Form.module.css'
import { Form } from './Form'
import { FormLayout } from './FormLayout'
import type { GridRef, SuperFormRef } from '../types'
const SuperFormLayout = <T extends FieldValues>(
{
children,
gridRef,
}: {
children: React.ReactNode | ((props: UseFormReturn<T, any, undefined>) => React.ReactNode)
gridRef?: MutableRefObject<GridRef<any> | null>
},
ref
) => {
// Component store State
const {
layoutProps,
meta,
nested,
onBeforeSubmit,
onCancel,
onLayoutMounted,
onLayoutUnMounted,
onResetForm,
onSubmit,
primeData,
request,
tableName,
value,
} = useStore((state) => ({
extraButtons: state.extraButtons,
layoutProps: state.layoutProps,
meta: state.meta,
nested: state.nested,
onBeforeSubmit: state.onBeforeSubmit,
onCancel: state.onCancel,
onLayoutMounted: state.onLayoutMounted,
onLayoutUnMounted: state.onLayoutUnMounted,
onResetForm: state.onResetForm,
onSubmit: state.onSubmit,
primeData: state.primeData,
request: state.request,
tableName: state.remote?.tableName,
value: state.value,
}))
const [_opened, _setOpened] = useUncontrolled({
value: layoutProps?.bodyRightSection?.opened,
defaultValue: false,
onChange: layoutProps?.bodyRightSection?.setOpened,
})
// Component Hooks
const form = useFormContext<T, any, undefined>()
const formState = useFormState({ control: form.control })
const { isFetching, mutateAsync, error, queryKey } = useRemote(gridRef)
// Component variables
const formUID = useMemo(() => {
return meta?.id ?? uuid()
}, [])
const requestString = request?.charAt(0).toUpperCase() + request?.slice(1)
const renderRightSection = (
<>
<Tooltip label={`${_opened ? 'Close' : 'Open'} Right Section`} withArrow>
<ActionIcon
style={{
position: 'absolute',
right: 12,
zIndex: 5,
display: layoutProps?.bodyRightSection?.hideToggleButton ? 'none' : 'block',
}}
variant='filled'
size='sm'
onClick={() => _setOpened(!_opened)}
radius='6'
m={2}
>
{_opened ? <IconChevronsRight /> : <IconChevronsLeft />}
</ActionIcon>
</Tooltip>
<Group wrap='nowrap' h='100%' align='flex-start' gap={2} w={'100%'}>
<Stack gap={0} h='100%' style={{ flex: 1 }}>
{typeof children === 'function' ? children({ ...form }) : children}
</Stack>
<Transition transition='slide-left' mounted={_opened}>
{(transitionStyles) => (
<Paper
style={transitionStyles}
h='100%'
w={layoutProps?.bodyRightSection?.w}
shadow='xs'
radius='xs'
mr='xs'
mt='xs'
ml={0}
{...layoutProps?.bodyRightSection?.paperProps}
>
{layoutProps?.bodyRightSection?.render?.({
form,
formValue: form.getValues(),
isFetching,
opened: _opened,
queryKey,
setOpened: _setOpened,
})}
</Paper>
)}
</Transition>
</Group>
</>
)
// Component Callback Functions
const onFormSubmit = async (data: T | any, closeForm: boolean = true) => {
const res: any =
typeof onBeforeSubmit === 'function'
? await mutateAsync?.(await onBeforeSubmit(data, request, form))
: await mutateAsync?.(data)
if ((tableName?.length ?? 0) > 0) {
if (res?.ok || (res?.status >= 200 && res?.status < 300)) {
onSubmit?.(res?.data, request, data, form, closeForm)
} else {
form.setError('root', {
message: res.status === 401 ? 'Username or password is incorrect' : res?.error,
})
}
} else {
onSubmit?.(data, request, data, form, closeForm)
}
}
// Component use Effects
useEffect(() => {
if (request === 'insert') {
if (onResetForm) {
onResetForm(primeData, form).then((resetData) => {
form.reset(resetData)
})
} else {
form.reset(primeData)
}
} else if ((request === 'change' || request === 'delete') && (tableName?.length ?? 0) === 0) {
if (onResetForm) {
onResetForm(value, form).then((resetData) => {
form.reset(resetData)
})
} else {
form.reset(value)
}
}
onLayoutMounted?.()
return onLayoutUnMounted
}, [
request,
primeData,
tableName,
value,
form.reset,
onResetForm,
onLayoutMounted,
onLayoutUnMounted,
])
useEffect(() => {
if (
(Object.keys(formState.errors)?.length > 0 || error) &&
_opened === false &&
layoutProps?.showErrorList !== false
) {
_setOpened(true)
}
}, [Object.keys(formState.errors)?.length > 0, error, layoutProps?.showErrorList])
useImperativeHandle<SuperFormRef<T>, SuperFormRef<T>>(ref, () => ({
form,
mutation: { isFetching, mutateAsync, error },
submit: (closeForm: boolean = true, afterSubmit?: (data: T | any) => void) => {
return form.handleSubmit(async (data: T | any) => {
await onFormSubmit(data, closeForm)
afterSubmit?.(data)
})()
},
queryKey,
getFormState: () => formState,
}))
return (
<form
name={formUID}
onSubmit={(e) => {
e.stopPropagation()
e.preventDefault()
form.handleSubmit((data: T | any) => {
onFormSubmit(data)
})(e)
}}
style={{ height: '100%' }}
className={request === 'view' ? classes.disabled : ''}
>
{/* <LoadingOverlay
visible={isFetching}
overlayProps={{
backgroundOpacity: 0.5,
}}
/> */}
{layoutProps?.noLayout ? (
typeof layoutProps?.bodyRightSection?.render === 'function' ? (
renderRightSection
) : typeof children === 'function' ? (
<>
<LoadingOverlay
visible={isFetching}
overlayProps={{
backgroundOpacity: 0.5,
}}
/>
{children({ ...form })}
</>
) : (
<>
<LoadingOverlay
visible={isFetching}
overlayProps={{
backgroundOpacity: 0.5,
}}
/>
{children}
</>
)
) : (
<FormLayout
dirty={formState.isDirty}
loading={isFetching}
onCancel={() => onCancel?.(request)}
onSubmit={form.handleSubmit((data: T | any) => {
onFormSubmit(data)
})}
request={request}
modal={false}
nested={nested}
>
{!layoutProps?.noHeader && (
<Form.Section
type='header'
title={`${layoutProps?.title || requestString}`}
rightSection={layoutProps?.rightSection}
/>
)}
{(Object.keys(formState.errors)?.length > 0 || error) && (
<Form.Section
className={classes.sticky}
buttonTitles={layoutProps?.buttonTitles}
type='error'
>
<Title order={6} size='sm' c='red'>
{(error?.message?.length ?? 0) > 0
? 'Server Error'
: 'Required information is incomplete*'}
</Title>
{(error as any)?.response?.data?.msg ||
(error as any)?.response?.data?._error ||
error?.message}
{layoutProps?.showErrorList !== false && (
<Spoiler maxHeight={50} showLabel='Show more' hideLabel='Hide'>
<List
size='xs'
style={{
color: 'light-dark(var(--mantine-color-dark-7), var(--mantine-color-gray-2))',
}}
>
{getErrorMessages(formState.errors)}
</List>
</Spoiler>
)}
</Form.Section>
)}
{typeof layoutProps?.bodyRightSection?.render === 'function' ? (
<Form.Section type='body' {...layoutProps?.bodySectionProps}>
{renderRightSection}
</Form.Section>
) : (
<Form.Section type='body' {...layoutProps?.bodySectionProps}>
{typeof children === 'function' ? children({ ...form }) : children}
</Form.Section>
)}
{!layoutProps?.noFooter && (
<Form.Section
className={classes.sticky}
buttonTitles={layoutProps?.buttonTitles}
type='footer'
{...(typeof layoutProps?.footerSectionProps === 'function'
? layoutProps?.footerSectionProps(ref as RefObject<SuperFormRef<T>>)
: layoutProps?.footerSectionProps)}
>
{typeof layoutProps?.extraButtons === 'function'
? layoutProps?.extraButtons(form)
: layoutProps?.extraButtons}
</Form.Section>
)}
</FormLayout>
)}
</form>
)
}
const getErrorMessages = (errors: any): ReactNode | null => {
return Object.keys(errors ?? {}).map((key) => {
if (typeof errors[key] === 'object' && key !== 'ref') {
return getErrorMessages(errors[key])
}
if (key !== 'message') {
return null
}
return <List.Item key={key}>{errors[key]}</List.Item>
})
}
const FRSuperFormLayout = forwardRef(SuperFormLayout) as <T extends FieldValues>(
props: {
children: React.ReactNode | ((props: UseFormReturn<T, any, undefined>) => React.ReactNode)
gridRef?: MutableRefObject<GridRef | null>
} & {
ref?: Ref<SuperFormRef<T>>
}
) => ReactElement
export default FRSuperFormLayout

View File

@@ -0,0 +1,66 @@
import { useEffect, useState } from 'react'
import { useFormContext, useFormState } from 'react-hook-form'
import { useDebouncedCallback } from '@mantine/hooks'
import useSubscribe from '../hooks/use-subscribe'
import { useSuperFormStore } from '../store/SuperForm.store'
import { openConfirmModal } from '../utils/openConfirmModal'
const SuperFormPersist = ({ storageKey }: { storageKey?: string | null }) => {
// Component store State
const [persistKey, setPersistKey] = useState<string>('')
const { isDirty, isReady, isSubmitted } = useFormState()
const { remote, request } = useSuperFormStore((state) => ({
request: state.request,
remote: state.remote,
}))
// Component Hooks
const { reset, setValue } = useFormContext()
const handleFormChange = useDebouncedCallback(({ values }) => {
setPersistKey(() => {
const key = `superform-persist-${storageKey?.length > 0 ? storageKey : `${remote?.tableName || 'local'}-${request}-${values[remote?.primaryKey] ?? ''}`}`
if (!isDirty) {
return key
}
window.localStorage.setItem(key, JSON.stringify(values))
return key
})
}, 250)
useSubscribe('', handleFormChange)
// Component use Effects
useEffect(() => {
if (isReady && persistKey) {
const data = window.localStorage.getItem(persistKey)
if (!data) {
return
}
if (isSubmitted) {
window.localStorage.removeItem(persistKey)
return
}
openConfirmModal(
() => {
reset(JSON.parse(data))
setValue('_dirty', true, { shouldDirty: true })
},
() => {
window.localStorage.removeItem(persistKey)
},
'Do you want to restore the previous data that was not submitted?'
)
}
}, [isReady, isSubmitted, persistKey])
return null
}
export default SuperFormPersist

View File

@@ -0,0 +1,43 @@
import React, { createContext, useContext, ReactNode, useState } from 'react'
interface ApiConfigContextValue {
apiURL: string
setApiURL: (url: string) => void
}
const ApiConfigContext = createContext<ApiConfigContextValue | null>(null)
interface ApiConfigProviderProps {
children: ReactNode
defaultApiURL?: string
}
export const ApiConfigProvider: React.FC<ApiConfigProviderProps> = ({
children,
defaultApiURL = '',
}) => {
const [apiURL, setApiURL] = useState(defaultApiURL)
return (
<ApiConfigContext.Provider value={{ apiURL, setApiURL }}>
{children}
</ApiConfigContext.Provider>
)
}
export const useApiConfig = (): ApiConfigContextValue => {
const context = useContext(ApiConfigContext)
if (!context) {
throw new Error('useApiConfig must be used within ApiConfigProvider')
}
return context
}
/**
* Hook to get API URL with optional override
* @param overrideURL - Optional URL to use instead of context value
*/
export const useApiURL = (overrideURL?: string): string => {
const { apiURL } = useApiConfig()
return overrideURL ?? apiURL
}

View File

@@ -0,0 +1,146 @@
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
type ReactElement,
type Ref,
} from 'react'
import { IconX } from '@tabler/icons-react'
import type { FieldValues } from 'react-hook-form'
import { ActionIcon, Drawer } from '@mantine/core'
import type { SuperFormDrawerProps, SuperFormDrawerRef, SuperFormRef } from '../../types'
import SuperForm from '../../components/SuperForm'
import { openConfirmModal } from '../../utils/openConfirmModal'
const SuperFormDrawer = <T extends FieldValues>(
{ drawerProps, noCloseOnSubmit, ...formProps }: SuperFormDrawerProps<T>,
ref: Ref<SuperFormDrawerRef<T>>
) => {
// Component Refs
const formRef = useRef<SuperFormRef<T>>(null)
const drawerRef = useRef<HTMLDivElement>(null)
// Component store State
// Tell drawer that form layout mounted to fix refs
const [layoutMounted, setLayoutMounted] = useState(false)
// Component Callback Functions
const onSubmit = (data: T, request, formData, form, closeForm: boolean = true) => {
formProps?.onSubmit?.(data, request, formData, form, closeForm)
if (request === 'delete') {
drawerProps?.onClose()
}
if (!noCloseOnSubmit) {
if (closeForm) {
drawerProps?.onClose()
}
}
}
const onCancel = (request) => {
if (formRef?.current?.getFormState().isDirty) {
openConfirmModal(() => {
drawerProps?.onClose()
formProps?.onCancel?.(request)
})
} else {
drawerProps?.onClose()
formProps?.onCancel?.(request)
}
}
const onLayoutMounted = useCallback(() => {
setLayoutMounted(true)
formProps?.onLayoutMounted?.()
}, [formProps?.onLayoutMounted])
const onLayoutUnMounted = useCallback(() => {
setLayoutMounted(false)
formProps?.onLayoutUnMounted?.()
}, [formProps?.onLayoutUnMounted])
// Component use Effects
useImperativeHandle<SuperFormDrawerRef<T>, SuperFormDrawerRef<T>>(
ref,
() => ({
...formRef.current,
drawer: drawerRef.current,
} as SuperFormDrawerRef<T>),
[layoutMounted]
)
return (
<Drawer
ref={drawerRef}
onClose={onCancel}
closeOnClickOutside={false}
onKeyDown={(e) => {
if (e.key === 'Escape' && drawerProps.closeOnEscape !== false) {
e.stopPropagation()
onCancel(formProps.request)
}
}}
overlayProps={{ backgroundOpacity: 0.5, blur: 0.5 }}
padding={6}
position='right'
transitionProps={{
transition: 'slide-left',
duration: 150,
timingFunction: 'linear',
}}
size={500}
styles={{
content: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'stretch',
},
body: {
minHeight: '100px',
flexGrow: 1,
},
}}
keepMounted={false}
{...drawerProps}
closeOnEscape={false}
withCloseButton={false}
title={null}
>
<SuperForm<T>
{...formProps}
onCancel={onCancel}
onSubmit={onSubmit}
onLayoutMounted={onLayoutMounted}
onLayoutUnMounted={onLayoutUnMounted}
ref={formRef}
layoutProps={{
...formProps?.layoutProps,
rightSection: (
<ActionIcon
size='xs'
onClick={() => {
onCancel(formProps?.request)
}}
>
<IconX size={18} />
</ActionIcon>
),
title:
(drawerProps.title as string) ??
formProps?.layoutProps?.title ??
(formProps?.request as string),
}}
/>
</Drawer>
)
}
const FRSuperFormDrawer = forwardRef(SuperFormDrawer) as <T extends FieldValues>(
props: SuperFormDrawerProps<T> & { ref?: Ref<SuperFormDrawerRef<T>> }
) => ReactElement
export default FRSuperFormDrawer

View File

@@ -0,0 +1,105 @@
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
type ReactElement,
type Ref,
} from 'react'
import type { FieldValues } from 'react-hook-form'
import { Modal, ScrollArea } from '@mantine/core'
import type { SuperFormModalProps, SuperFormModalRef, SuperFormRef } from '../../types'
import SuperForm from '../../components/SuperForm'
import { openConfirmModal } from '../../utils/openConfirmModal'
const SuperFormModal = <T extends FieldValues>(
{ modalProps, noCloseOnSubmit, ...formProps }: SuperFormModalProps<T>,
ref: Ref<SuperFormModalRef<T>>
) => {
// Component Refs
const modalRef = useRef<HTMLDivElement>(null)
const formRef = useRef<SuperFormRef<T>>(null)
// Component store State
// Tell drawer that form layout mounted to fix refs
const [layoutMounted, setLayoutMounted] = useState(false)
// Component Callback Functions
const onSubmit = (data: T, request, formData, form, closeForm: boolean = true) => {
formProps?.onSubmit?.(data, request, formData, form, closeForm)
if (request === 'delete') {
modalProps?.onClose()
}
if (!noCloseOnSubmit) {
if (closeForm) {
modalProps?.onClose()
}
}
}
const onCancel = (request) => {
if (formRef?.current?.getFormState().isDirty) {
openConfirmModal(() => {
modalProps?.onClose()
formProps?.onCancel?.(request)
})
} else {
modalProps?.onClose()
formProps?.onCancel?.(request)
}
}
const onLayoutMounted = useCallback(() => {
setLayoutMounted(true)
formProps?.onLayoutMounted?.()
}, [formProps?.onLayoutMounted])
const onLayoutUnMounted = useCallback(() => {
setLayoutMounted(false)
formProps?.onLayoutUnMounted?.()
}, [formProps?.onLayoutUnMounted])
// Component use Effects
useImperativeHandle<SuperFormModalRef<T>, SuperFormModalRef<T>>(
ref,
() => ({
...formRef.current,
modal: modalRef.current,
} as SuperFormModalRef<T>),
[layoutMounted]
)
return (
<Modal
ref={modalRef}
closeOnClickOutside={false}
overlayProps={{
backgroundOpacity: 0.5,
blur: 4,
}}
padding='sm'
scrollAreaComponent={ScrollArea.Autosize}
size={500}
keepMounted={false}
{...modalProps}
>
<SuperForm<T>
{...formProps}
onCancel={onCancel}
onSubmit={onSubmit}
onLayoutMounted={onLayoutMounted}
onLayoutUnMounted={onLayoutUnMounted}
ref={formRef}
/>
</Modal>
)
}
const FRSuperFormModal = forwardRef(SuperFormModal) as <T extends FieldValues>(
props: SuperFormModalProps<T> & { ref?: Ref<SuperFormModalRef<T>> }
) => ReactElement
export default FRSuperFormModal

View File

@@ -0,0 +1,116 @@
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
type ReactElement,
type Ref,
} from 'react'
import type { FieldValues } from 'react-hook-form'
import { Box, Popover } from '@mantine/core'
import { useUncontrolled } from '@mantine/hooks'
import type { SuperFormPopoverProps, SuperFormPopoverRef, SuperFormRef } from '../../types'
import SuperForm from '../../components/SuperForm'
import { openConfirmModal } from '../../utils/openConfirmModal'
const SuperFormPopover = <T extends FieldValues>(
{ popoverProps, target, noCloseOnSubmit, ...formProps }: SuperFormPopoverProps<T>,
ref: Ref<SuperFormPopoverRef<T>>
) => {
// Component Refs
const popoverRef = useRef<HTMLDivElement>(null)
const formRef = useRef<SuperFormRef<T>>(null)
// Component store State
// Tell drawer that form layout mounted to fix refs
const [layoutMounted, setLayoutMounted] = useState(false)
// Component Hooks
const [_value, _onChange] = useUncontrolled({
value: popoverProps?.opened,
onChange: popoverProps?.onChange,
})
// Component Callback Functions
const onSubmit = (data: T, request, formData, form, closeForm: boolean = true) => {
formProps?.onSubmit?.(data, request, formData, form, closeForm)
if (request === 'delete') {
_onChange(false)
}
if (!noCloseOnSubmit) {
if (closeForm) {
_onChange(false)
}
}
}
const onCancel = (request) => {
if (formRef?.current?.getFormState().isDirty) {
openConfirmModal(() => {
_onChange(false)
formProps?.onCancel?.(request)
})
} else {
_onChange(false)
formProps?.onCancel?.(request)
}
}
const onLayoutMounted = useCallback(() => {
setLayoutMounted(true)
formProps?.onLayoutMounted?.()
}, [formProps?.onLayoutMounted])
const onLayoutUnMounted = useCallback(() => {
setLayoutMounted(false)
formProps?.onLayoutUnMounted?.()
}, [formProps?.onLayoutUnMounted])
// Component use Effects
useImperativeHandle<SuperFormPopoverRef<T>, SuperFormPopoverRef<T>>(
ref,
() => ({
...formRef.current,
popover: popoverRef.current,
} as SuperFormPopoverRef<T>),
[layoutMounted]
)
return (
<Popover
closeOnClickOutside={false}
onClose={() => _onChange(false)}
opened={_value}
position='left'
radius='md'
withArrow
withinPortal
zIndex={200}
keepMounted={false}
{...popoverProps}
>
<Popover.Target>
<Box onClick={() => _onChange(true)}>{target}</Box>
</Popover.Target>
<Popover.Dropdown p={0} m={0} ref={popoverRef}>
<SuperForm
{...formProps}
onCancel={onCancel}
onSubmit={onSubmit}
onLayoutMounted={onLayoutMounted}
onLayoutUnMounted={onLayoutUnMounted}
ref={formRef}
/>
</Popover.Dropdown>
</Popover>
)
}
const FRSuperFormPopover = forwardRef(SuperFormPopover) as <T extends FieldValues>(
props: SuperFormPopoverProps<T> & { ref?: Ref<SuperFormPopoverRef<T>> }
) => ReactElement
export default FRSuperFormPopover

View File

@@ -0,0 +1,96 @@
import { useState } from 'react'
import { FieldValues } from 'react-hook-form'
import { SuperFormProps, RequestType, ExtendedDrawerProps } from '../types'
interface UseDrawerFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
drawerProps: Partial<ExtendedDrawerProps>
opened?: boolean
onClose?: () => void
request: RequestType
[key: string]: any
}
type AskFunction = (request: RequestType, buffer: any) => void
const useDrawerFormState = <T extends FieldValues>(
props?: Partial<UseDrawerFormState<T>>
): {
formProps: UseDrawerFormState<T>
setFormProps: React.Dispatch<React.SetStateAction<UseDrawerFormState<T>>>
open: (props: Partial<UseDrawerFormState<T>>) => void
close: () => void
ask: AskFunction
} => {
const [formProps, setFormProps] = useState<UseDrawerFormState<T>>({
opened: false,
request: 'insert',
...props,
onClose: () =>
setFormProps((curr) => ({
...curr,
opened: false,
drawerProps: { ...curr.drawerProps, opened: false },
})),
drawerProps: { opened: false, onClose: () => {}, ...props?.drawerProps },
})
return {
formProps,
setFormProps,
open: (props?: Partial<UseDrawerFormState<T>>) => {
setFormProps((curr) => {
return {
...curr,
...props,
request: props.request ?? curr.request,
opened: true,
drawerProps: {
...curr.drawerProps,
...props?.drawerProps,
opened: true,
onClose: curr.onClose,
},
primeData: props?.primeData,
useFormProps: {
...curr.useFormProps,
...props?.useFormProps,
},
layoutProps: {
...curr.layoutProps,
...props?.layoutProps,
},
useQueryOptions: {
...curr.useQueryOptions,
...props?.useQueryOptions,
},
meta: {
...curr.meta,
...props?.meta,
},
useMutationOptions: {
...curr.useMutationOptions,
...props?.useMutationOptions,
},
}
})
},
close: () =>
setFormProps((curr) => ({
...curr,
opened: false,
drawerProps: { ...curr.drawerProps, opened: false, onClose: curr.onClose },
})),
ask: (request: RequestType, buffer: any) => {
setFormProps((curr) => ({
...curr,
request,
value: buffer,
opened: true,
drawerProps: { ...curr.drawerProps, opened: true, onClose: curr.onClose },
}))
},
}
}
export default useDrawerFormState
export type { UseDrawerFormState }

View File

@@ -0,0 +1,97 @@
import { useState } from 'react'
import { FieldValues } from 'react-hook-form'
import { ModalProps } from '@mantine/core'
import { SuperFormProps, RequestType } from '../types'
interface UseModalFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
modalProps: ModalProps
opened?: boolean
onClose?: () => void
request: RequestType
[key: string]: any
}
type AskFunction = (request: RequestType, buffer: any) => void
const useModalFormState = <T extends FieldValues>(
props?: Partial<UseModalFormState<T>>
): {
formProps: UseModalFormState<T>
setFormProps: React.Dispatch<React.SetStateAction<UseModalFormState<T>>>
open: (props: Partial<UseModalFormState<T>>) => void
close: () => void
ask: AskFunction
} => {
const [formProps, setFormProps] = useState<UseModalFormState<T>>({
opened: false,
request: 'insert',
...props,
onClose: () =>
setFormProps((curr) => ({
...curr,
opened: false,
modalProps: { ...curr.modalProps, opened: false },
})),
modalProps: { opened: false, onClose: () => {}, ...props?.modalProps },
})
return {
formProps,
setFormProps,
open: (props?: Partial<UseModalFormState<T>>) => {
setFormProps((curr) => {
return {
...curr,
...props,
request: props.request ?? curr.request,
opened: true,
modalProps: {
...curr.modalProps,
...props?.modalProps,
opened: true,
onClose: curr.onClose,
},
primeData: props?.primeData,
useFormProps: {
...curr.useFormProps,
...props?.useFormProps,
},
layoutProps: {
...curr.layoutProps,
...props?.layoutProps,
},
useQueryOptions: {
...curr.useQueryOptions,
...props?.useQueryOptions,
},
meta: {
...curr.meta,
...props?.meta,
},
useMutationOptions: {
...curr.useMutationOptions,
...props?.useMutationOptions,
},
}
})
},
close: () =>
setFormProps((curr) => ({
...curr,
opened: false,
modalProps: { ...curr.modalProps, opened: false, onClose: curr.onClose },
})),
ask: (request: RequestType, buffer: any) => {
setFormProps((curr) => ({
...curr,
request,
value: buffer,
opened: true,
modalProps: { ...curr.modalProps, opened: true, onClose: curr.onClose },
}))
},
}
}
export default useModalFormState
export type { UseModalFormState }

View File

@@ -0,0 +1,97 @@
import { useState } from 'react'
import { FieldValues } from 'react-hook-form'
import { PopoverProps } from '@mantine/core'
import { SuperFormProps, RequestType } from '../types'
interface UsePopoverFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
popoverProps: Omit<PopoverProps, 'children'>
opened?: boolean
onClose?: () => void
request: RequestType
[key: string]: any
}
type AskFunction = (request: RequestType, buffer: any) => void
const usePopoverFormState = <T extends FieldValues>(
props?: Partial<UsePopoverFormState<T>>
): {
formProps: UsePopoverFormState<T>
setFormProps: React.Dispatch<React.SetStateAction<UsePopoverFormState<T>>>
open: (props: Partial<UsePopoverFormState<T>>) => void
close: () => void
ask: AskFunction
} => {
const [formProps, setFormProps] = useState<UsePopoverFormState<T>>({
opened: false,
request: 'insert',
...props,
popoverProps: { opened: false, onClose: () => {}, ...props?.popoverProps },
onClose: () =>
setFormProps((curr) => ({
...curr,
opened: false,
popoverProps: { ...curr.popoverProps, opened: false },
})),
})
return {
formProps,
setFormProps,
open: (props?: Partial<UsePopoverFormState<T>>) => {
setFormProps((curr) => {
return {
...curr,
...props,
request: props.request ?? curr.request,
opened: true,
popoverProps: {
...curr.popoverProps,
...props?.popoverProps,
opened: true,
onClose: curr.onClose,
},
primeData: props?.primeData,
useFormProps: {
...curr.useFormProps,
...props?.useFormProps,
},
layoutProps: {
...curr.layoutProps,
...props?.layoutProps,
},
useQueryOptions: {
...curr.useQueryOptions,
...props?.useQueryOptions,
},
meta: {
...curr.meta,
...props?.meta,
},
useMutationOptions: {
...curr.useMutationOptions,
...props?.useMutationOptions,
},
}
})
},
close: () =>
setFormProps((curr) => ({
...curr,
opened: false,
popoverProps: { ...curr.popoverProps, opened: false, onClose: curr.onClose },
})),
ask: (request: RequestType, buffer: any) => {
setFormProps((curr) => ({
...curr,
request,
value: buffer,
opened: true,
popoverProps: { ...curr.popoverProps, opened: true, onClose: curr.onClose },
}))
},
}
}
export default usePopoverFormState
export type { UsePopoverFormState }

View File

@@ -0,0 +1,40 @@
import { useEffect } from 'react'
import {
EventType,
FieldValues,
FormState,
InternalFieldName,
Path,
ReadFormState,
useFormContext,
UseFormReturn,
} from 'react-hook-form'
const useSubscribe = <T extends FieldValues>(
name: Path<T> | readonly Path<T>[] | undefined,
callback: (
data: Partial<FormState<T>> & {
values: FieldValues
name?: InternalFieldName
type?: EventType
},
form?: UseFormReturn<T, any, T>
) => void,
formState?: ReadFormState,
deps?: unknown[]
) => {
const form = useFormContext<T>()
return useEffect(() => {
const unsubscribe = form.subscribe({
name,
callback: (data) => callback(data, form),
formState: { values: true, ...formState },
exact: true,
})
return unsubscribe
}, [form.subscribe, ...(deps || [])])
}
export default useSubscribe

View File

@@ -0,0 +1,38 @@
import { useState } from 'react'
import { FieldValues } from 'react-hook-form'
import { SuperFormProps, RequestType } from '../types'
interface UseSuperFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
request: RequestType
[key: string]: any
}
type AskFunction = (request: RequestType, buffer: any) => void
const useSuperFormState = <T extends FieldValues>(
props?: UseSuperFormState<T>
): {
formProps: UseSuperFormState<any>
setFormProps: React.Dispatch<React.SetStateAction<UseSuperFormState<T>>>
ask: AskFunction
} => {
const [formProps, setFormProps] = useState<UseSuperFormState<T>>({
request: 'insert',
...props,
})
return {
formProps,
setFormProps,
ask: (request: RequestType, buffer: any) => {
setFormProps((curr) => ({
...curr,
request,
value: buffer,
}))
},
}
}
export default useSuperFormState
export type { UseSuperFormState }

View File

@@ -0,0 +1,123 @@
import { useEffect, type MutableRefObject } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useFormContext, useFormState, type FieldValues } from 'react-hook-form'
import { useStore } from '../store/SuperForm.store'
import { useApiURL } from '../config/ApiConfig'
import { getNestedValue } from '../utils/getNestedValue'
import { fetchClient, type FetchResponse, FetchError } from '../utils/fetchClient'
import type { GridRef } from '../types'
const useRemote = <T extends FieldValues>(gridRef?: MutableRefObject<GridRef<any> | null>) => {
// Component store State
const { onResetForm, remote, request, useMutationOptions, useQueryOptions, value } = useStore(
(state) => ({
onResetForm: state.onResetForm,
remote: state.remote,
request: state.request,
useMutationOptions: state.useMutationOptions,
useQueryOptions: state.useQueryOptions,
value: state.value,
})
)
// Component Hooks
const form = useFormContext<T>()
const { isDirty } = useFormState({ control: form.control })
// Component use Effects
const qc = useQueryClient()
// Get API URL from context or override
const contextApiURL = useApiURL()
const id = remote?.primaryKey?.includes('.')
? getNestedValue(remote?.primaryKey, value)
: value?.[remote?.primaryKey ?? '']
const queryKey = useQueryOptions?.queryKey || [remote?.tableName, id]
const enabled =
useQueryOptions?.enabled ||
!!(
(remote?.enabled ?? true) &&
(remote?.tableName ?? '').length > 0 &&
(request === 'change' || request === 'delete') &&
String(id) !== 'undefined' &&
(String(id)?.length ?? 0) > 0
)
let url = remote?.apiURL ?? `${contextApiURL}/${remote?.tableName}`
url = url?.endsWith('/') ? url.substring(0, url.length - 1) : url
const { isSuccess, status, data, isFetching } = useQuery<FetchResponse<T>>({
queryKey,
queryFn: () => fetchClient.get<T>(`${url}/${id}`, remote?.apiOptions),
enabled,
refetchOnMount: 'always',
refetchOnReconnect: !isDirty,
refetchOnWindowFocus: !isDirty,
staleTime: 0,
gcTime: 0,
...useQueryOptions,
})
const changeMut = useMutation({
// @ts-ignore
mutationFn: (mutVal: T) => {
if (!remote?.tableName || !remote?.primaryKey) {
return Promise.resolve(null)
}
return request === 'insert'
? fetchClient.post(url, mutVal, remote?.apiOptions)
: request === 'change'
? fetchClient.post(`${url}/${id}`, mutVal, remote?.apiOptions)
: request === 'delete'
? fetchClient.delete(`${url}/${id}`, remote?.apiOptions)
: Promise.resolve(null)
},
onSettled: (response: FetchResponse | null) => {
qc?.invalidateQueries({ queryKey: [remote?.tableName] })
if (request !== 'delete' && response) {
if (onResetForm) {
onResetForm(response?.data, form).then(() => {
form.reset(response?.data, { keepDirty: false })
})
} else {
form.reset(response?.data, { keepDirty: false })
}
}
gridRef?.current?.refresh?.()
// @ts-ignore
gridRef?.current?.selectRow?.(response?.data?.[remote?.primaryKey ?? ''])
},
...useMutationOptions,
})
useEffect(() => {
if (isSuccess && status === 'success' && enabled && !isFetching) {
if (!Object.keys(data?.data ?? {}).includes(remote?.primaryKey ?? '')) {
throw new Error('Primary key not found in remote data')
}
if (onResetForm) {
onResetForm(data?.data, form).then((resetData) => {
form.reset(resetData)
})
} else {
form.reset(data?.data)
}
}
}, [isSuccess, status, enabled, isFetching])
return {
error: changeMut.error as FetchError,
isFetching: (enabled ? isFetching : false) || changeMut?.isPending,
mutateAsync: changeMut.mutateAsync,
queryKey,
}
}
export default useRemote

View File

@@ -0,0 +1,48 @@
import React, { createContext, useContext, type ReactNode } from 'react'
import { create } from 'zustand'
import type { RequestType } from '../types'
interface FormLayoutState {
request: RequestType
loading: boolean
dirty: boolean
onCancel?: () => void
onSubmit?: () => void
setState: (key: string, value: any) => void
}
const createFormLayoutStore = (initialProps: any) =>
create<FormLayoutState>((set) => ({
request: initialProps.request || 'insert',
loading: initialProps.loading || false,
dirty: initialProps.dirty || false,
onCancel: initialProps.onCancel,
onSubmit: initialProps.onSubmit,
setState: (key, value) => set({ [key]: value }),
}))
const FormLayoutStoreContext = createContext<ReturnType<typeof createFormLayoutStore> | null>(null)
export const FormLayoutStoreProvider: React.FC<{ children: ReactNode; [key: string]: any }> = ({
children,
...props
}) => {
const storeRef = React.useRef<ReturnType<typeof createFormLayoutStore>>()
if (!storeRef.current) {
storeRef.current = createFormLayoutStore(props)
}
return (
<FormLayoutStoreContext.Provider value={storeRef.current}>
{children}
</FormLayoutStoreContext.Provider>
)
}
export const useFormLayoutStore = <T,>(selector: (state: FormLayoutState) => T): T => {
const store = useContext(FormLayoutStoreContext)
if (!store) {
throw new Error('useFormLayoutStore must be used within FormLayoutStoreProvider')
}
return store(selector)
}

View File

@@ -0,0 +1,22 @@
import type { SuperFormProviderProps } from '../types'
import { createSyncStore } from '@warkypublic/zustandsyncstore'
const { Provider, useStore } = createSyncStore<any, SuperFormProviderProps>((set) => ({
request: 'insert',
setRequest: (request) => {
set({ request })
},
value: undefined,
setValue: (value) => {
set({ value })
},
noCloseOnSubmit: false,
setNoCloseOnSubmit: (noCloseOnSubmit) => {
set({ noCloseOnSubmit })
},
}))
export { Provider, useStore }
export const useSuperFormStore = useStore

View File

@@ -0,0 +1,10 @@
.disabled {
pointer-events: none;
opacity: 0.9;
}
.sticky {
position: -webkit-sticky;
position: sticky;
bottom: 0;
}

View File

@@ -0,0 +1,135 @@
import type { UseMutationOptions, UseMutationResult, UseQueryOptions } from '@tanstack/react-query'
import type { FieldValues, UseFormProps, UseFormReturn, UseFormStateReturn } from 'react-hook-form'
import type { ModalProps, PaperProps, PopoverProps, DrawerProps } from '@mantine/core'
import type { RemoteConfig } from './remote.types'
export type RequestType = 'insert' | 'change' | 'view' | 'select' | 'delete' | 'get' | 'set'
// Grid integration types (simplified - removes BTGlideRef dependency)
export interface GridRef<T = any> {
refresh?: () => void
selectRow?: (id: any) => void
}
export interface FormSectionBodyProps {
// Add properties as needed from original FormLayout
[key: string]: any
}
export interface FormSectionFooterProps {
// Add properties as needed from original FormLayout
[key: string]: any
}
export interface BodyRightSection<T extends FieldValues> {
opened?: boolean
setOpened?: (opened: boolean) => void
w: number | string
hideToggleButton?: boolean
paperProps?: PaperProps
render: (props: {
form: UseFormReturn<any, any, undefined>
formValue: T
isFetching: boolean
opened: boolean
queryKey: any
setOpened: (opened: boolean) => void
}) => React.ReactNode
}
export interface SuperFormLayoutProps<T extends FieldValues> {
buttonTitles?: { submit?: string; cancel?: string }
extraButtons?: React.ReactNode | ((form: UseFormReturn<any, any, undefined>) => React.ReactNode)
noFooter?: boolean
noHeader?: boolean
noLayout?: boolean
bodySectionProps?: Partial<FormSectionBodyProps>
footerSectionProps?:
| Partial<FormSectionFooterProps>
| ((ref: React.RefObject<SuperFormRef<T>>) => Partial<FormSectionFooterProps>)
rightSection?: React.ReactNode
bodyRightSection?: BodyRightSection<T>
title?: string
showErrorList?: boolean
}
export interface CommonFormProps<T extends FieldValues> {
gridRef?: React.MutableRefObject<GridRef<any> | null>
layoutProps?: SuperFormLayoutProps<T>
meta?: { [key: string]: any }
nested?: boolean
onCancel?: (request: RequestType) => void
onLayoutMounted?: () => void
onLayoutUnMounted?: () => void
onResetForm?: (data: T, form?: UseFormReturn<any, any, undefined>) => Promise<T>
onBeforeSubmit?: (
data: T,
request: RequestType,
form?: UseFormReturn<any, any, undefined>
) => Promise<T>
onSubmit?: (
data: T,
request: RequestType,
formData?: T,
form?: UseFormReturn<any, any, undefined>,
closeForm?: boolean
) => void
primeData?: any
readonly?: boolean
remote?: RemoteConfig
request: RequestType
persist?: boolean | { storageKey?: string }
useMutationOptions?: UseMutationOptions<any, Error, T, unknown>
useQueryOptions?: Partial<UseQueryOptions<any, Error, T>>
value?: T | null
}
export interface SuperFormProps<T extends FieldValues> extends CommonFormProps<T> {
children: React.ReactNode | ((props: UseFormReturn<T, any, undefined>) => React.ReactNode)
useFormProps?: UseFormProps<T>
}
export interface SuperFormProviderProps extends Omit<SuperFormProps<any>, 'children'> {
children?: React.ReactNode
}
export interface SuperFormModalProps<T extends FieldValues> extends SuperFormProps<T> {
modalProps: ModalProps
noCloseOnSubmit?: boolean
}
export interface SuperFormPopoverProps<T extends FieldValues> extends SuperFormProps<T> {
popoverProps?: Omit<PopoverProps, 'children'>
target: any
noCloseOnSubmit?: boolean
}
export interface ExtendedDrawerProps extends DrawerProps {
// Add any extended drawer props needed
[key: string]: any
}
export interface SuperFormDrawerProps<T extends FieldValues> extends SuperFormProps<T> {
drawerProps: ExtendedDrawerProps
noCloseOnSubmit?: boolean
}
export interface SuperFormRef<T extends FieldValues> {
form: UseFormReturn<T, any, undefined>
mutation: Partial<UseMutationResult<any, Error, any, unknown>>
submit: (closeForm?: boolean, afterSubmit?: (data: T | any) => void) => Promise<void>
queryKey?: any
getFormState: () => UseFormStateReturn<T>
}
export interface SuperFormDrawerRef<T extends FieldValues> extends SuperFormRef<T> {
drawer: HTMLDivElement | null
}
export interface SuperFormModalRef<T extends FieldValues> extends SuperFormRef<T> {
modal: HTMLDivElement | null
}
export interface SuperFormPopoverRef<T extends FieldValues> extends SuperFormRef<T> {
popover: HTMLDivElement | null
}

2
src/Form/types/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './form.types'
export * from './remote.types'

View File

@@ -0,0 +1,11 @@
export interface RemoteConfig {
apiOptions?: RequestInit
apiURL?: string
enabled?: boolean
fetchSize?: number
hotFields?: string[]
primaryKey?: string
sqlFilter?: string
tableName: string
uniqueKeys?: string[]
}

View File

@@ -0,0 +1,161 @@
export interface FetchOptions extends RequestInit {
params?: Record<string, any>
timeout?: number
}
export interface FetchResponse<T = any> {
data: T
status: number
statusText: string
ok: boolean
error?: string
}
export class FetchError extends Error {
constructor(
public message: string,
public status?: number,
public response?: any
) {
super(message)
this.name = 'FetchError'
}
}
/**
* Fetch wrapper with timeout support and axios-like interface
*/
async function fetchWithTimeout(
url: string,
options: FetchOptions = {}
): Promise<Response> {
const { timeout = 30000, ...fetchOptions } = options
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
})
clearTimeout(timeoutId)
return response
} catch (error) {
clearTimeout(timeoutId)
throw error
}
}
/**
* GET request
*/
export async function get<T = any>(
url: string,
options?: FetchOptions
): Promise<FetchResponse<T>> {
try {
const response = await fetchWithTimeout(url, {
...options,
method: 'GET',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
const data = await response.json()
return {
data,
status: response.status,
statusText: response.statusText,
ok: response.ok,
error: response.ok ? undefined : data?.message || data?.error || response.statusText,
}
} catch (error) {
throw new FetchError(
error instanceof Error ? error.message : 'Network request failed',
undefined,
error
)
}
}
/**
* POST request
*/
export async function post<T = any>(
url: string,
data?: any,
options?: FetchOptions
): Promise<FetchResponse<T>> {
try {
const response = await fetchWithTimeout(url, {
...options,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(data),
})
const responseData = await response.json()
return {
data: responseData,
status: response.status,
statusText: response.statusText,
ok: response.ok,
error: response.ok ? undefined : responseData?.message || responseData?.error || response.statusText,
}
} catch (error) {
throw new FetchError(
error instanceof Error ? error.message : 'Network request failed',
undefined,
error
)
}
}
/**
* DELETE request
*/
export async function del<T = any>(
url: string,
options?: FetchOptions
): Promise<FetchResponse<T>> {
try {
const response = await fetchWithTimeout(url, {
...options,
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
const data = await response.json().catch(() => ({}))
return {
data,
status: response.status,
statusText: response.statusText,
ok: response.ok,
error: response.ok ? undefined : data?.message || data?.error || response.statusText,
}
} catch (error) {
throw new FetchError(
error instanceof Error ? error.message : 'Network request failed',
undefined,
error
)
}
}
export const fetchClient = {
get,
post,
delete: del,
}

View File

@@ -0,0 +1,9 @@
/**
* Retrieves a nested value from an object using dot notation path
* @param path - Dot-separated path (e.g., "user.address.city")
* @param obj - Object to extract value from
* @returns The value at the specified path, or undefined if not found
*/
export const getNestedValue = (path: string, obj: any): any => {
return path.split('.').reduce((prev, curr) => prev?.[curr], obj)
}

View File

@@ -0,0 +1,30 @@
import React from 'react'
import { Stack, Text } from '@mantine/core'
import { modals } from '@mantine/modals'
export const openConfirmModal = (
onConfirm: () => void,
onCancel?: (() => void) | null,
description?: string | null
) =>
modals.openConfirmModal({
size: 'xs',
children: (
<Stack gap={4}>
<Text size='xs' c={description ? 'blue' : 'red'} fw='bold'>
You have unsaved changes in this form.
</Text>
<Text size='xs'>
{description ??
'Closing now will discard any modifications you have made. Are you sure you want to continue?'}
</Text>
</Stack>
),
labels: { confirm: description ? 'Restore' : 'Confirm', cancel: 'Cancel' },
confirmProps: { color: description ? 'blue' : 'red', size: 'compact-xs' },
cancelProps: { size: 'compact-xs' },
groupProps: { gap: 'xs' },
withCloseButton: false,
onConfirm,
onCancel,
})

188
src/Former/Former.store.tsx Normal file
View File

@@ -0,0 +1,188 @@
import { createSyncStore } from '@warkypublic/zustandsyncstore';
import { produce } from 'immer';
import type { FormerProps, FormerState } from './Former.types';
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
FormerState<any> & Partial<FormerProps<any>>,
FormerProps<any>
>(
(set, get) => ({
getState: (key) => {
const current = get();
return current?.[key];
},
load: async (reset?: boolean) => {
try {
set({ loading: true });
const keyName = get()?.apiKeyField || 'id';
const keyValue = (get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
if (get().onAPICall && keyValue !== undefined) {
let data = await get().onAPICall!(
'read',
get().request || 'insert',
get().values,
keyValue
);
if (get().afterGet) {
data = await get().afterGet!({ ...data });
}
set({ loading: false, values: data });
get().onChange?.(data);
}
if (reset && get().getFormMethods) {
const formMethods = get().getFormMethods!();
formMethods.reset();
}
} catch (e) {
set({ error: (e as Error)?.message ?? e, loading: false });
}
set({ loading: false });
},
onChange: (values) => {
set({ values });
},
request: 'insert',
reset: async () => {
const state = get();
if (state.getFormMethods) {
if (state.request !== 'insert') {
await state.load(true);
}
const formMethods = state.getFormMethods!();
formMethods.reset({ ...state.values, ...state.primeData });
}
},
save: async (e?: React.BaseSyntheticEvent<object, any, any> | undefined) => {
try {
const keepOpen = get().keepOpen ?? false;
set({ loading: true });
if (get().getFormMethods) {
const formMethods = get().getFormMethods!();
let data = formMethods.getValues();
if (get().beforeSave) {
const newData = await get().beforeSave!(data);
data = newData;
}
let exit = false;
const handler = formMethods.handleSubmit(
(newdata) => {
data = newdata;
},
(errors) => {
set({ error: errors.root?.message || 'Validation errors', loading: false });
exit = true;
}
);
await handler(e);
//console.log('Former.store.tsx save called', success, e, data, get().getFormMethods);
if (exit) {
set({ loading: false });
return undefined;
}
if (get().request === 'delete' && !get().deleteConfirmed) {
const confirmed = (await get().onConfirmDelete?.(data)) ?? false;
if (!confirmed) {
set({ loading: false });
return undefined;
}
}
if (get().onAPICall) {
const keyName = get()?.apiKeyField || 'id';
const keyValue =
(get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
const savedData = await get().onAPICall!(
'mutate',
get().request || 'insert',
data,
keyValue
);
if (get().afterSave) {
await get().afterSave!(savedData);
}
set({ loading: false, values: savedData });
get().onChange?.(savedData);
if (!keepOpen) {
get().onClose?.(savedData);
}
return savedData;
}
set({ loading: false, values: data });
get().onChange?.(data);
if (!keepOpen) {
get().onClose?.(data);
}
return data;
}
} catch (e) {
set({ error: (e as Error)?.message ?? e, loading: false });
}
return undefined;
},
setRequest: (request) => {
set({ request });
},
setState: (key, value) => {
set(
produce((state) => {
state[key] = value;
})
);
},
setStateFN: (key, value) => {
const p = new Promise<void>((resolve, reject) => {
set(
produce((state) => {
if (typeof value === 'function') {
state[key] = (value as (value: unknown) => unknown)(state[key]);
} else {
reject(new Error(`Not a function ${value}`));
throw Error(`Not a function ${value}`);
}
})
);
resolve();
});
return p;
},
validate: async () => {
if (get().getFormMethods) {
const formMethods = get().getFormMethods!();
const isValid = await formMethods.trigger();
return isValid;
}
return true;
},
values: undefined,
}),
({ onConfirmDelete, primeData, request, values }) => {
let _onConfirmDelete = onConfirmDelete;
if (!onConfirmDelete) {
_onConfirmDelete = async () => {
return confirm('Are you sure you want to delete this item?');
};
}
return {
onConfirmDelete: _onConfirmDelete,
primeData,
request: request || 'insert',
values: { ...primeData, ...values },
};
}
);
export { FormerProvider };
export { useFormerStore };

115
src/Former/Former.tsx Normal file
View File

@@ -0,0 +1,115 @@
import { forwardRef, type PropsWithChildren, useEffect, useImperativeHandle } from 'react';
import { type FieldValues, FormProvider, useForm } from 'react-hook-form';
import type { FormerProps, FormerRef } from './Former.types';
import { FormerProvider, useFormerStore } from './Former.store';
import { FormerLayout } from './FormerLayout';
const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & PropsWithChildren>(
function FormerInner<T extends FieldValues>(
props: Partial<FormerProps<T>> & PropsWithChildren<T>,
ref: any
) {
const {
getState,
onChange,
onClose,
onOpen,
opened,
primeData,
reset,
save,
setState,
useFormProps,
validate,
values,
wrapper,
} = useFormerStore((state) => ({
getState: state.getState,
onChange: state.onChange,
onClose: state.onClose,
onOpen: state.onOpen,
opened: state.opened,
primeData: state.primeData,
reset: state.reset,
save: state.save,
setState: state.setState,
useFormProps: state.useFormProps,
validate: state.validate,
values: state.values,
wrapper: state.wrapper,
}));
const formMethods = useForm<T>({
defaultValues: primeData,
mode: 'all',
shouldUseNativeValidation: true,
values: values,
...useFormProps,
});
useImperativeHandle(
ref,
() => ({
close: async () => {
//console.log('close called');
onClose?.();
setState('opened', false);
},
getValue: () => {
return getState('values');
},
reset: () => {
reset();
},
save: async () => {
return await save();
},
setValue: (value: T) => {
onChange?.(value);
},
show: async () => {
//console.log('show called');
setState('opened', true);
onOpen?.();
},
validate: async () => {
return await validate();
},
}),
[getState, onChange]
);
useEffect(() => {
setState('getFormMethods', () => formMethods);
}, [formMethods]);
return (
<FormProvider {...formMethods}>
{typeof wrapper === 'function' ? (
wrapper(<FormerLayout>{props.children}</FormerLayout>, opened, onClose, onOpen, getState)
) : (
<FormerLayout>{props.children || null}</FormerLayout>
)}
</FormProvider>
);
}
);
export const Former = forwardRef<FormerRef<any>, FormerProps<any> & PropsWithChildren>(
function Former<T extends FieldValues = any>(
props: FormerProps<T> & PropsWithChildren<T>,
ref: any
) {
//if opened is false and wrapper is defined as function, do not render anything
if (!props.opened && typeof props.wrapper === 'function') {
return null;
}
return (
<FormerProvider {...props}>
<FormerInner ref={ref}>{props.children}</FormerInner>
</FormerProvider>
);
}
);

View File

@@ -0,0 +1,73 @@
import type { LoadingOverlayProps, ScrollAreaAutosizeProps } from '@mantine/core';
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
export interface FormerProps<T extends FieldValues = any> {
afterGet?: (data: T) => Promise<T> | void;
afterSave?: (data: T) => Promise<void> | void;
apiKeyField?: string;
beforeSave?: (data: T) => Promise<T> | T;
disableHTMlForm?: boolean;
keepOpen?: boolean;
onAPICall?: (
mode: 'mutate' | 'read',
request: RequestType,
value?: T,
key?: number | string
) => Promise<T>;
onCancel?: () => void;
onChange?: (value: T) => void;
onClose?: (data?: T) => void;
onConfirmDelete?: (values?: T) => Promise<boolean>;
onOpen?: (data?: T) => void;
opened?: boolean;
primeData?: T;
request: RequestType;
useFormProps?: UseFormProps<T>;
values?: T;
wrapper?: (
children: React.ReactNode,
opened: boolean | undefined,
onClose: ((data?: T) => void) | undefined,
onOpen: ((data?: T) => void) | undefined,
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K]
) => React.ReactNode;
}
export interface FormerRef<T extends FieldValues = any> {
close: () => Promise<void>;
getValue: () => T | undefined;
reset: () => void;
save: () => Promise<T | undefined>;
setValue: (value: T) => void;
show: () => Promise<void>;
validate: () => Promise<boolean>;
}
export interface FormerState<T extends FieldValues = any> {
deleteConfirmed?: boolean;
error?: string;
getFormMethods?: () => UseFormReturn<any, any>;
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K];
load: (reset?: boolean) => Promise<void>;
loading?: boolean;
loadingOverlayProps?: LoadingOverlayProps;
reset: (e?: React.BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>;
save: (e?: React.BaseSyntheticEvent<object, any, any> | undefined) => Promise<T | undefined>;
scrollAreaProps?: ScrollAreaAutosizeProps;
setRequest: (request: RequestType) => void;
setState: <K extends keyof FormStateAndProps<T>>(
key: K,
value: Partial<FormStateAndProps<T>>[K]
) => void;
setStateFN: <K extends keyof FormStateAndProps<T>>(
key: K,
value: (current: FormStateAndProps<T>[K]) => Partial<FormStateAndProps<T>[K]>
) => Promise<void>;
validate: () => Promise<boolean>;
}
export type FormStateAndProps<T extends FieldValues = any> = FormerProps<T> &
Partial<FormerState<T>>;
export type RequestType = 'delete' | 'insert' | 'select' | 'update' | 'view';

View File

@@ -0,0 +1,93 @@
import { LoadingOverlay, ScrollAreaAutosize } from '@mantine/core';
import { type PropsWithChildren, useEffect } from 'react';
import { useFormerStore } from './Former.store';
export const FormerLayout = (props: PropsWithChildren) => {
const {
disableHTMlForm,
getFormMethods,
load,
loading,
loadingOverlayProps,
request,
reset,
save,
scrollAreaProps,
} = useFormerStore((state) => ({
disableHTMlForm: state.disableHTMlForm,
getFormMethods: state.getFormMethods,
load: state.load,
loading: state.loading,
loadingOverlayProps: state.loadingOverlayProps,
request: state.request,
reset: state.reset,
save: state.save,
scrollAreaProps: state.scrollAreaProps,
}));
useEffect(() => {
if (getFormMethods) {
const formMethods = getFormMethods();
if (formMethods && request !== 'insert') {
load(true);
}
}
}, [getFormMethods, request]);
if (disableHTMlForm) {
return (
<ScrollAreaAutosize
offsetScrollbars
scrollbarSize={4}
type="auto"
{...scrollAreaProps}
style={{
height: '100%',
maxHeight: '89vh',
padding: '0.25rem',
width: '100%',
...scrollAreaProps?.style,
}}
>
{props.children}
<LoadingOverlay
loaderProps={{ type: 'bars' }}
overlayProps={{
backgroundOpacity: 0.5,
}}
{...loadingOverlayProps}
visible={loading}
/>
</ScrollAreaAutosize>
);
}
return (
<ScrollAreaAutosize
offsetScrollbars
scrollbarSize={4}
type="auto"
{...scrollAreaProps}
style={{
height: '100%',
maxHeight: '89vh',
padding: '0.25rem',
width: '100%',
...scrollAreaProps?.style,
}}
>
<form onReset={(e) => reset(e)} onSubmit={(e) => save(e)}>
{props.children}
<LoadingOverlay
loaderProps={{ type: 'bars' }}
overlayProps={{
backgroundOpacity: 0.5,
}}
{...loadingOverlayProps}
visible={loading}
/>
</form>
</ScrollAreaAutosize>
);
};

0
src/Former/index.ts Normal file
View File

View File

@@ -0,0 +1,42 @@
//@ts-nocheck
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Box } from '@mantine/core';
import { fn } from 'storybook/test';
import { FormTest } from './example';
const Renderable = (props: any) => {
return (
<Box h="100%" mih="400px" miw="400px" w="100%">
<FormTest {...props} />
</Box>
);
};
const meta = {
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
component: Renderable,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
//layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
title: 'Former/Former Basic',
} satisfies Meta<typeof Renderable>;
export default meta;
type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const BasicExample: Story = {
args: {
label: 'Test',
},
};

View File

@@ -0,0 +1,118 @@
import { Button, Drawer, Group, Paper, Select, Stack, Switch } from '@mantine/core';
import { useRef, useState } from 'react';
import { Controller } from 'react-hook-form';
import type { FormerRef } from '../Former.types';
import { Former } from '../Former';
export const FormTest = () => {
const [request, setRequest] = useState<null | string>('insert');
const [wrapped, setWrapped] = useState(false);
const [open, setOpen] = useState(false);
const [formData, setFormData] = useState({ a: 99 });
console.log('formData', formData);
const ref = useRef<FormerRef>(null);
return (
<Stack h="100%" mih="400px" w="90%">
<Select
data={['insert', 'update', 'delete', 'select', 'view']}
onChange={setRequest}
value={request}
/>
<Switch
checked={wrapped}
label="Wrapped in Drawer"
onChange={(event) => setWrapped(event.currentTarget.checked)}
/>
<Button onClick={() => setOpen(true)}>Open Former Drawer</Button>
<Group>
<Button
onClick={async () => {
const valid = await ref.current?.validate();
console.log('validate -> ', valid, ref.current);
}}
>
Test Ref Values. See console
</Button>
<Button
onClick={async () => {
setTimeout(() => {
ref.current?.close?.();
}, 3000);
}}
>
Test Show/Hide
</Button>
</Group>
<Former
//wrapper={(children, getState) => <div>{children}</div>}
//opened={true}
apiKeyField="a"
onAPICall={(mode, request, value) => {
console.log('API Call', mode, request, value);
if (mode === 'read') {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ a: 'Another Value', test: 'Loaded Value' });
}, 1000);
});
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(value || {});
}, 1000);
});
}}
onChange={setFormData}
onClose={() => setOpen(false)}
opened={open}
primeData={{ a: '66', test: 'primed' }}
ref={ref}
request={request as any}
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
values={formData}
wrapper={
wrapped
? (children, opened, onClose, onOpen, getState) => {
const values = getState('values');
return (
<Drawer
h={'100%'}
onClose={() => onClose?.()}
opened={opened ?? false}
title={`Drawer Former - Current A Value: ${values?.a}`}
w={'50%'}
>
<Paper h="100%" shadow="sm" w="100%" withBorder>
{children}
<Button>Test Save</Button>
</Paper>
</Drawer>
);
}
: undefined
}
>
<Stack h="1200px">
<Stack>
<Controller
name="test"
render={({ field }) => <input type="text" {...field} placeholder="A" />}
/>
<Controller
name="a"
render={({ field }) => <input type="text" {...field} placeholder="B" />}
rules={{ required: 'Field is required' }}
/>
</Stack>
<Stack>
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</Stack>
</Stack>
</Former>
</Stack>
);
};

11
src/Former/todo.md Normal file
View File

@@ -0,0 +1,11 @@
- [ ] Headerspec API
- [ ] Relspec API
- [ ] SocketSpec API
- [ ] Layout Tool
- [ ] Header Section
- [ ] Button Section
- [ ] Footer Section
- [ ] Different Loaded for saving vs loading
- [ ] Better Confirm Dialog
- [ ] Reset Confirm Dialog
- [ ] Request insert and save but keep open (must clear key from API, also add callback)

View File

@@ -50,6 +50,7 @@ export const GridlerDataGrid = () => {
headerHeight, headerHeight,
heightProp, heightProp,
mounted, mounted,
onCellActivated,
onCellClicked, onCellClicked,
onCellEdited, onCellEdited,
onColumnMoved, onColumnMoved,
@@ -80,6 +81,7 @@ export const GridlerDataGrid = () => {
headerHeight: s.headerHeight, headerHeight: s.headerHeight,
heightProp: s.height, heightProp: s.height,
mounted: s.mounted, mounted: s.mounted,
onCellActivated: s.onCellActivated,
onCellClicked: s.onCellClicked, onCellClicked: s.onCellClicked,
onCellEdited: s.onCellEdited, onCellEdited: s.onCellEdited,
onColumnMoved: s.onColumnMoved, onColumnMoved: s.onColumnMoved,
@@ -178,6 +180,7 @@ export const GridlerDataGrid = () => {
gridSelection={_gridSelection} gridSelection={_gridSelection}
headerHeight={headerHeight ?? 32} headerHeight={headerHeight ?? 32}
headerIcons={{ sort: SortSprite, sortdown: SortDownSprite, sortup: SortUpSprite }} headerIcons={{ sort: SortSprite, sortdown: SortDownSprite, sortup: SortUpSprite }}
onCellActivated={onCellActivated}
onCellClicked={onCellClicked} onCellClicked={onCellClicked}
onCellContextMenu={(cell, event) => { onCellContextMenu={(cell, event) => {
event.preventDefault(); event.preventDefault();
@@ -197,7 +200,13 @@ export const GridlerDataGrid = () => {
onGridSelectionChange={(selection) => { onGridSelectionChange={(selection) => {
let rows = CompactSelection.empty(); let rows = CompactSelection.empty();
const currentSelection = getState('_gridSelection'); const currentSelection = getState('_gridSelection');
const keyField = getState('keyField') ?? 'id';
const getRowBuffer = getState('getRowBuffer');
for (const r of selection.rows) { for (const r of selection.rows) {
const validRowID = getRowBuffer ? getRowBuffer(r)?.[keyField] : null;
if (!validRowID) {
continue;
}
rows = rows.hasIndex(r) ? rows : rows.add(r); rows = rows.hasIndex(r) ? rows : rows.add(r);
} }
if (selectMode === 'row' && selection.current?.range) { if (selectMode === 'row' && selection.current?.range) {
@@ -206,19 +215,32 @@ export const GridlerDataGrid = () => {
y < selection.current.range.y + selection.current.range.height; y < selection.current.range.y + selection.current.range.height;
y++ y++
) { ) {
const validRowID = getRowBuffer ? getRowBuffer(y)?.[keyField] : null;
if (!validRowID) {
continue;
}
rows = rows.hasIndex(y) ? rows : rows.add(y); rows = rows.hasIndex(y) ? rows : rows.add(y);
} }
} }
if (rows.length === 0) {
for (const r of currentSelection?.rows ?? []) {
const validRowID = getRowBuffer ? getRowBuffer(r)?.[keyField] : null;
if (!validRowID) {
continue;
}
rows = rows.hasIndex(r) ? rows : rows.add(r);
}
}
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) ||
JSON.stringify(currentSelection?.current) !== JSON.stringify(selection.current) JSON.stringify(currentSelection?.current) !== JSON.stringify(selection.current)
) { ) {
setState('_gridSelection', { ...selection, rows }); setState('_gridSelection', { ...selection, rows });
if (JSON.stringify(currentSelection?.rows) !== JSON.stringify(rows)) { //if (JSON.stringify(currentSelection?.rows) !== JSON.stringify(rows)) {
setState('_gridSelectionRows', rows); setState('_gridSelectionRows', rows);
} //}
} }
//console.log('Selection', selection); //console.log('Selection', selection);

View File

@@ -1,4 +1,4 @@
/* eslint-disable react/react-in-jsx-scope */
import { useGridlerStore } from './GridlerStore'; import { useGridlerStore } from './GridlerStore';
export function BottomBar() { export function BottomBar() {

View File

@@ -23,6 +23,7 @@ export interface GridlerColumn extends Partial<BaseGridColumn> {
disableFilter?: boolean; disableFilter?: boolean;
disableMove?: boolean; disableMove?: boolean;
disableResize?: boolean; disableResize?: boolean;
disableSearch?: boolean;
disableSort?: boolean; disableSort?: boolean;
getMenuItems?: ( getMenuItems?: (
id: string, id: string,
@@ -35,6 +36,7 @@ export interface GridlerColumn extends Partial<BaseGridColumn> {
maxWidth?: number; maxWidth?: number;
minWidth?: number; minWidth?: number;
tooltip?: ((buffer: any, row: number, col: number) => ReactNode) | string; tooltip?: ((buffer: any, row: number, col: number) => ReactNode) | string;
virtual?: boolean;
width?: number; width?: number;
} }

View File

@@ -1,4 +1,5 @@
import { CompactSelection } from '@glideapps/glide-data-grid'; import { CompactSelection } from '@glideapps/glide-data-grid';
import { useDebouncedCallback } from '@mantine/hooks';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { useGridlerStore } from './GridlerStore'; import { useGridlerStore } from './GridlerStore';
@@ -6,6 +7,7 @@ import { useGridlerStore } from './GridlerStore';
//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 Computer = React.memo(() => { export const Computer = React.memo(() => {
const refFirstRun = useRef(0); const refFirstRun = useRef(0);
const refLastSearch = useRef('');
const refLastFilters = useRef<unknown>(null); const refLastFilters = useRef<unknown>(null);
const { const {
_glideref, _glideref,
@@ -21,6 +23,7 @@ export const Computer = React.memo(() => {
ready, ready,
scrollToRowKey, scrollToRowKey,
searchStr,
selectedRowKey, selectedRowKey,
setState, setState,
setStateFN, setStateFN,
@@ -39,6 +42,7 @@ export const Computer = React.memo(() => {
ready: s.ready, ready: s.ready,
scrollToRowKey: s.scrollToRowKey, scrollToRowKey: s.scrollToRowKey,
searchStr: s.searchStr,
selectedRowKey: s.selectedRowKey, selectedRowKey: s.selectedRowKey,
setState: s.setState, setState: s.setState,
setStateFN: s.setStateFN, setStateFN: s.setStateFN,
@@ -46,6 +50,24 @@ export const Computer = React.memo(() => {
values: s.values, values: s.values,
})); }));
const debouncedDoSearch = useDebouncedCallback(
(searchStr: string) => {
loadPage(0, 'all').then(() => {
getState('refreshCells')?.();
getState('_events')?.dispatchEvent?.(
new CustomEvent('onSearched', {
detail: { search: searchStr },
})
);
});
},
{
delay: 300,
leading: false,
}
);
//When values change, update selection
useEffect(() => { useEffect(() => {
const searchSelection = async () => { const searchSelection = async () => {
const page_data = getState('_page_data'); const page_data = getState('_page_data');
@@ -107,40 +129,12 @@ export const Computer = React.memo(() => {
} }
}, [values]); }, [values]);
//Fire onChange when selection changes
useEffect(() => { useEffect(() => {
const onChange = getState('onChange'); const onChange = getState('onChange');
if (onChange && typeof onChange === 'function') { if (onChange && typeof onChange === 'function') {
const page_data = getState('_page_data'); const getGridSelectedRows = getState('getGridSelectedRows');
const pageSize = getState('pageSize'); const buffers = getGridSelectedRows();
const buffers = [];
if (_gridSelectionRows) {
for (const range of _gridSelectionRows) {
let buffer = undefined;
for (const p in page_data) {
for (const r in page_data[p]) {
const idx = Number(p) * pageSize + Number(r);
if (isNaN(idx)) {
continue;
}
if (Number(page_data[p][r]?._rownumber) === range + 1) {
buffer = page_data[p][r];
//console.log('Found row', range, idx, page_data[p][r]?._rownumber);
break;
} else if (idx === range + 1) {
buffer = page_data[p][r];
//console.log('Found row 2', range, idx, page_data[p][r]?._rownumber);
break;
}
}
}
if (buffer !== undefined) {
buffers.push(buffer);
}
}
}
const _values = getState('values'); const _values = getState('values');
@@ -148,7 +142,7 @@ export const Computer = React.memo(() => {
onChange(buffers); onChange(buffers);
} }
} }
}, [JSON.stringify(_gridSelectionRows), getState]); }, [_gridSelectionRows, _gridSelectionRows?.length, getState]);
useEffect(() => { useEffect(() => {
setState( setState(
@@ -161,6 +155,17 @@ export const Computer = React.memo(() => {
); );
}, [columns]); }, [columns]);
useEffect(() => {
if (searchStr === undefined || searchStr === null) {
refLastSearch.current = '';
return;
}
if (refLastSearch.current !== searchStr) {
debouncedDoSearch(searchStr);
refLastSearch.current = searchStr;
}
}, [searchStr]);
useEffect(() => { useEffect(() => {
if (!colSort) { if (!colSort) {
return; return;
@@ -246,6 +251,7 @@ export const Computer = React.memo(() => {
}); });
}, [colOrder]); }, [colOrder]);
//Initial Load
useEffect(() => { useEffect(() => {
if (!_glideref) { if (!_glideref) {
return; return;
@@ -259,11 +265,12 @@ export const Computer = React.memo(() => {
}); });
}, [ready, loadPage]); }, [ready, loadPage]);
//Logic to select first row on mount
useEffect(() => { useEffect(() => {
const _events = getState('_events'); const _events = getState('_events');
const loadPage = () => { const loadPage = () => {
const selectFirstRowOnMount = getState('selectFirstRowOnMount'); const selectFirstRowOnMount = getState('selectFirstRowOnMount');
if (selectFirstRowOnMount) { if (ready && selectFirstRowOnMount) {
const scrollToRowKey = getState('scrollToRowKey'); const scrollToRowKey = getState('scrollToRowKey');
if (scrollToRowKey && scrollToRowKey >= 0) { if (scrollToRowKey && scrollToRowKey >= 0) {
return; return;
@@ -275,7 +282,12 @@ export const Computer = React.memo(() => {
const firstRow = firstBuffer?.[keyField]; const firstRow = firstBuffer?.[keyField];
const currentValues = getState('values') ?? []; const currentValues = getState('values') ?? [];
if (firstRow && firstRow > 0 && (currentValues.length ?? 0) === 0) { if (
!(values && values.length > 0) &&
firstRow &&
firstRow > 0 &&
(currentValues.length ?? 0) === 0
) {
const values = [firstBuffer, ...(currentValues as Array<Record<string, unknown>>)]; const values = [firstBuffer, ...(currentValues as Array<Record<string, unknown>>)];
const onChange = getState('onChange'); const onChange = getState('onChange');
@@ -296,28 +308,28 @@ export const Computer = React.memo(() => {
return () => { return () => {
_events?.removeEventListener('loadPage', loadPage); _events?.removeEventListener('loadPage', loadPage);
}; };
}, []); }, [ready]);
/// logic to apply the selected row. /// logic to apply the selected row.
useEffect(() => { // useEffect(() => {
const ready = getState('ready'); // const ready = getState('ready');
const ref = getState('_glideref'); // const ref = getState('_glideref');
const getRowIndexByKey = getState('getRowIndexByKey'); // const getRowIndexByKey = getState('getRowIndexByKey');
if (scrollToRowKey && ref && ready) { // if (scrollToRowKey && ref && ready) {
getRowIndexByKey?.(scrollToRowKey).then((r) => { // getRowIndexByKey?.(scrollToRowKey).then((r) => {
if (r !== undefined) { // if (r !== undefined) {
console.log('Scrolling to selected row:', scrollToRowKey, r); // //console.log('Scrolling to selected row:', scrollToRowKey, r);
ref.scrollTo(0, r); // ref.scrollTo(0, r);
getState('_events').dispatchEvent( // getState('_events').dispatchEvent(
new CustomEvent('scrollToRowKeyFound', { // new CustomEvent('scrollToRowKeyFound', {
detail: { rowNumber: r, scrollToRowKey: scrollToRowKey }, // detail: { rowNumber: r, scrollToRowKey: scrollToRowKey },
}) // })
); // );
} // }
}); // });
} // }
}, [scrollToRowKey]); // }, [scrollToRowKey]);
useEffect(() => { useEffect(() => {
const ready = getState('ready'); const ready = getState('ready');
@@ -328,15 +340,17 @@ export const Computer = React.memo(() => {
if (key && ref && ready) { if (key && ref && ready) {
getRowIndexByKey?.(key).then((r) => { getRowIndexByKey?.(key).then((r) => {
if (r !== undefined) { if (r !== undefined) {
console.log('Scrolling to selected row:', r, selectedRowKey, scrollToRowKey); //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 }];
if (onChange) { if (JSON.stringify(getState('values')) !== JSON.stringify(selected)) {
onChange(selected); if (onChange) {
} else { onChange(selected);
setState('values', selected); } else {
setState('values', selected);
}
} }
} }

View File

@@ -90,6 +90,8 @@ export interface GridlerProps extends PropsWithChildren {
rowHeight?: number; rowHeight?: number;
scrollToRowKey?: number; scrollToRowKey?: number;
searchFields?: Array<string>;
searchStr?: string;
sections?: { sections?: {
bottom?: React.ReactNode; bottom?: React.ReactNode;
left?: React.ReactNode; left?: React.ReactNode;
@@ -106,8 +108,8 @@ export interface GridlerProps extends PropsWithChildren {
title?: string; title?: string;
tooltipBarProps?: React.HTMLAttributes<HTMLDivElement>; tooltipBarProps?: React.HTMLAttributes<HTMLDivElement>;
total_rows?: number; total_rows?: number;
uniqueid: string;
uniqueid: string;
values?: Array<Record<string, any>>; values?: Array<Record<string, any>>;
width?: number | string; width?: number | string;
} }
@@ -115,6 +117,7 @@ export interface GridlerProps extends PropsWithChildren {
export interface GridlerRef { export interface GridlerRef {
getGlideRef: () => DataEditorRef | undefined; getGlideRef: () => DataEditorRef | undefined;
getState: GridlerState['getState']; getState: GridlerState['getState'];
isEmpty: () => boolean;
refresh: (parms?: any) => Promise<void>; refresh: (parms?: any) => Promise<void>;
reload: (parms?: any) => Promise<void>; reload: (parms?: any) => Promise<void>;
reloadRow: (key: number | string) => Promise<void>; reloadRow: (key: number | string) => Promise<void>;
@@ -132,10 +135,10 @@ export interface GridlerState {
_gridSelectionRows?: GridSelection['rows']; _gridSelectionRows?: GridSelection['rows'];
_loadingList: CompactSelection; _loadingList: CompactSelection;
_page_data: Record<number, Array<any>>; _page_data: Record<number, Array<any>>;
_refresh: () => Promise<void>;
_scrollTimeout?: any | number; _scrollTimeout?: any | number;
_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<number>;
colFilters?: Array<FilterOption>; colFilters?: Array<FilterOption>;
@@ -144,22 +147,25 @@ export interface GridlerState {
colSort?: Array<SortOption>; colSort?: Array<SortOption>;
data?: Array<any>; data?: Array<any>;
errors: Array<string>; errors: Array<string>;
focused?: boolean; focused?: boolean;
get: () => GridlerState; get: () => GridlerState;
getCellContent: (cell: Item) => GridCell; getCellContent: (cell: Item) => GridCell;
getCellsForSelection: ( getCellsForSelection: (
selection: Rectangle, selection: Rectangle,
abortSignal: AbortSignal abortSignal: AbortSignal
) => CellArray | GetCellsThunk; ) => CellArray | GetCellsThunk;
getGridSelectedRows: () => Array<any>;
getRowBuffer: (row: number) => Record<string, any>; getRowBuffer: (row: number) => Record<string, any>;
getRowIndexByKey: (key: number | string) => Promise<number | undefined>; getRowIndexByKey: (key: number | string) => Promise<number | undefined>;
getState: <K extends keyof GridlerStoreState>(key: K) => GridlerStoreState[K]; getState: <K extends keyof GridlerStoreState>(key: K) => GridlerStoreState[K];
hasLocalData: boolean; hasLocalData: boolean;
isEmpty: 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;
onCellActivated: (cell: Item) => void;
onCellClicked: (cell: Item, event: CellClickedEventArgs) => void; onCellClicked: (cell: Item, event: CellClickedEventArgs) => void;
onCellEdited: (cell: Item, newVal: EditableGridCell) => void; onCellEdited: (cell: Item, newVal: EditableGridCell) => void;
onColumnMoved: (from: number, to: number) => void; onColumnMoved: (from: number, to: number) => void;
@@ -213,6 +219,12 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
_events: new EventTarget(), _events: new EventTarget(),
_loadingList: CompactSelection.empty(), _loadingList: CompactSelection.empty(),
_page_data: {}, _page_data: {},
_refresh: async () => {
const s = get();
await s.loadPage(0, 'all');
await s.refreshCells();
await s.reload?.();
},
_visibleArea: { height: 10000, width: 1000, x: 0, y: 0 }, _visibleArea: { height: 10000, width: 1000, x: 0, y: 0 },
_visiblePages: { height: 0, width: 0, x: 0, y: 0 }, _visiblePages: { height: 0, width: 0, x: 0, y: 0 },
addError: (err: string, ...args: Array<unknown>) => { addError: (err: string, ...args: Array<unknown>) => {
@@ -266,6 +278,41 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
return result as CellArray; return result as CellArray;
}; };
}, },
getGridSelectedRows: () => {
const state = get();
const buffers: Array<any> = [];
const page_data = state._page_data;
const pageSize = state.pageSize;
if (state._gridSelectionRows) {
for (const range of state._gridSelectionRows) {
let buffer = undefined;
for (const p in page_data) {
for (const r in page_data[p]) {
const idx = Number(p) * pageSize + Number(r);
if (isNaN(idx)) {
continue;
}
if (Number(page_data[p][r]?._rownumber) === range + 1) {
buffer = page_data[p][r];
//console.log('Found row', range, idx, page_data[p][r]?._rownumber);
break;
} else if (idx === range + 1) {
buffer = page_data[p][r];
//console.log('Found row 2', range, idx, page_data[p][r]?._rownumber);
break;
}
}
}
if (buffer !== undefined) {
buffers.push(buffer);
}
}
}
return buffers;
},
getRowBuffer: (row: number) => { getRowBuffer: (row: number) => {
const state = get(); const state = get();
@@ -330,6 +377,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
return get()[key]; return get()[key];
}, },
hasLocalData: false, hasLocalData: false,
isEmpty: true,
keyField: 'id', keyField: 'id',
loadPage: async (pPage: number, clearMode?: 'all' | 'page') => { loadPage: async (pPage: number, clearMode?: 'all' | 'page') => {
const state = get(); const state = get();
@@ -388,7 +436,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
); );
}) })
.catch((e) => { .catch((e) => {
console.warn('loadPage Error: ', page, e); console.error('loadPage Error: ', page, e);
state._events.dispatchEvent( state._events.dispatchEvent(
new CustomEvent('loadPage_error', { new CustomEvent('loadPage_error', {
detail: { clearMode, error: e, page: pPage, state }, detail: { clearMode, error: e, page: pPage, state },
@@ -399,10 +447,29 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
}, },
maxConcurrency: 1, maxConcurrency: 1,
mounted: false, mounted: false,
onCellActivated: (cell: Item): void => {
const state = get();
const [col, row] = cell;
state._events.dispatchEvent(
new CustomEvent('onCellActivated', {
detail: { cell, col, row, state },
})
);
state.glideProps?.onCellActivated?.(cell);
},
onCellClicked: (cell: Item, event: CellClickedEventArgs) => { onCellClicked: (cell: Item, event: CellClickedEventArgs) => {
const state = get(); const state = get();
const [col, row] = cell; const [col, row] = cell;
state.glideProps?.onCellClicked?.(cell, event); if (state.glideProps?.onCellClicked) {
state.glideProps?.onCellClicked?.(cell, event);
}
if (state.values?.length) {
if (state.onChange) {
state.onChange(state.values);
}
}
state._events.dispatchEvent( state._events.dispatchEvent(
new CustomEvent('onCellClicked', { new CustomEvent('onCellClicked', {
detail: { cell, col, row, state }, detail: { cell, col, row, state },
@@ -453,6 +520,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
} }
return true; return true;
}, },
onColumnResize: ( onColumnResize: (
column: GridColumn, column: GridColumn,
newSize: number, newSize: number,
@@ -477,7 +545,6 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
}); });
} }
}, },
onContextClick: (area: string, event: CellClickedEventArgs, col?: number, row?: number) => { onContextClick: (area: string, event: CellClickedEventArgs, col?: number, row?: number) => {
const s = get(); const s = get();
const coldef = s.renderColumns?.[col ?? -1]; const coldef = s.renderColumns?.[col ?? -1];
@@ -657,9 +724,11 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
isDivider: true, isDivider: true,
}, },
{ {
id: 'refesh',
label: `Refresh`, label: `Refresh`,
onClickAsync: async () => { onClick: () => {
await s.reload?.(); const s = get();
s._refresh?.();
}, },
}, },
]; ];
@@ -836,8 +905,8 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
return { return {
allowOverlay: true, allowOverlay: true,
data: val, data: val ?? '',
displayData: String(val), displayData: String(val ?? ''),
kind: GridCellKind.Text, kind: GridCellKind.Text,
}; };
} catch (e) { } catch (e) {
@@ -895,8 +964,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
}, [setState, getState]); }, [setState, getState]);
getState('_events').addEventListener('reload', (_e: Event) => { getState('_events').addEventListener('reload', (_e: Event) => {
getState('reload')?.(); getState('_refresh')?.();
getState('refreshCells')?.();
}); });
return { return {
@@ -906,7 +974,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
hideMenu: props.hideMenu ?? menus.hide, hideMenu: props.hideMenu ?? menus.hide,
scrollToRowKey: props.scrollToRowKey ?? props.selectedRowKey ?? getState('scrollToRowKey'), scrollToRowKey: props.scrollToRowKey ?? props.selectedRowKey ?? getState('scrollToRowKey'),
showMenu: props.showMenu ?? menus.show, showMenu: props.showMenu ?? menus.show,
total_rows: props.total_rows ?? getState('total_rows') ?? 0, total_rows: getState('total_rows') ?? props.total_rows,
}; };
} }
); );

View File

@@ -7,6 +7,7 @@ import { useGridlerStore } from './GridlerStore';
export const Pager = React.memo(() => { export const Pager = React.memo(() => {
const [ const [
setState, setState,
getState,
glideref, glideref,
visiblePages, visiblePages,
//_visibleArea, //_visibleArea,
@@ -16,6 +17,7 @@ export const Pager = React.memo(() => {
hasLocalData, hasLocalData,
] = useGridlerStore((s) => [ ] = useGridlerStore((s) => [
s.setState, s.setState,
s.getState,
s._glideref, s._glideref,
s._visiblePages, s._visiblePages,
//s._visibleArea, //s._visibleArea,
@@ -38,10 +40,10 @@ export const Pager = React.memo(() => {
if (!glideref) { if (!glideref) {
return; return;
} }
if (hasLocalData) { // if (hasLocalData) {
//using local data, no need to load pages // //using local data, no need to load pages
return; // return;
} // }
const firstPage = Math.max(0, Math.floor(visiblePages.y / pageSize)); const firstPage = Math.max(0, Math.floor(visiblePages.y / pageSize));
const lastPage = Math.floor((visiblePages.y + visiblePages.height) / pageSize); const lastPage = Math.floor((visiblePages.y + visiblePages.height) / pageSize);
//const upperPage = pageSize * firstPage; //const upperPage = pageSize * firstPage;
@@ -57,7 +59,10 @@ export const Pager = React.memo(() => {
// ); // );
for (const page of range(firstPage, lastPage + 1, 1)) { for (const page of range(firstPage, lastPage + 1, 1)) {
loadPage(page); loadPage(page).then(() => {
const pg = getState('_page_data')?.[0] ?? {};
setState('isEmpty', pg && pg.length > 0);
});
} }
}, [loadPage, pageSize, visiblePages, glideref, _loadingList, hasLocalData]); }, [loadPage, pageSize, visiblePages, glideref, _loadingList, hasLocalData]);

View File

@@ -11,17 +11,20 @@ function _GridlerRefHandler(props: PropsWithChildren, ref: Ref<GridlerRef> | und
return getstate('_glideref'); return getstate('_glideref');
}, },
getState: getstate, getState: getstate,
isEmpty: () => getstate('isEmpty'),
refresh: async (parms?: any) => { refresh: async (parms?: any) => {
const refreshCells = getstate('refreshCells'); const refreshCells = getstate('refreshCells');
const loadPage = getstate('loadPage'); const loadPage = getstate('loadPage');
loadPage?.(parms?.pageIndex ?? 0, 'all'); loadPage?.(parms?.pageIndex ?? 0, 'all').then(() => {
refreshCells?.(); refreshCells?.();
});
}, },
reload: async (parms?: any) => { reload: async (parms?: any) => {
const refreshCells = getstate('refreshCells'); const refreshCells = getstate('refreshCells');
const loadPage = getstate('loadPage'); const loadPage = getstate('loadPage');
loadPage?.(parms?.pageIndex ?? 0, 'all'); loadPage?.(parms?.pageIndex ?? 0, 'all').then(() => {
refreshCells?.(); refreshCells?.();
});
}, },
reloadRow: async (key: number | string) => { reloadRow: async (key: number | string) => {
const refreshCells = getstate('refreshCells'); const refreshCells = getstate('refreshCells');

View File

@@ -2,66 +2,127 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import type { APIOptions } from '../../utils/types'; import type { APIOptions } from '../../utils/types';
import type { GridlerColumn } from '../Column';
import { GoAPIHeaders, type GoAPIOperation } from '../../utils/golang-restapi-v2'; import {
type FetchAPIOperation,
GoAPIHeaders,
type GoAPIOperation,
} from '../../utils/golang-restapi-v2';
import { useGridlerStore } from '../GridlerStore'; import { useGridlerStore } from '../GridlerStore';
export interface GlidlerAPIAdaptorForGoLangv2Props<T = unknown> extends APIOptions { export interface GlidlerAPIAdaptorForGoLangv2Props<T = unknown> extends APIOptions {
filter?: string; filter?: string;
hotfields?: Array<string>;
initialData?: Array<T>; initialData?: Array<T>;
options?: Array<GoAPIOperation>; options?: Array<GoAPIOperation>;
} }
function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForGoLangv2Props<T>) { function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForGoLangv2Props<T>) {
const [setStateFN, setState, getState, addError, mounted, loadPage] = useGridlerStore((s) => [ const [setStateFN, setState, getState, addError, mounted] = useGridlerStore((s) => [
s.setStateFN, s.setStateFN,
s.setState, s.setState,
s.getState, s.getState,
s.addError, s.addError,
s.mounted, s.mounted,
s.loadPage,
]); ]);
const useAPIQuery: (index: number) => Promise<any> = useCallback( const useAPIQuery: (index: number) => Promise<any> = useCallback(
async (index: number) => { async (index: number) => {
const columns = getState('columns');
const colSort = getState('colSort'); const colSort = getState('colSort');
const pageSize = getState('pageSize'); const pageSize = getState('pageSize');
const colFilters = getState('colFilters'); const colFilters = getState('colFilters');
const searchStr = getState('searchStr');
const searchFields = getState('searchFields');
const _active_requests = getState('_active_requests'); const _active_requests = getState('_active_requests');
setState('loadingData', true); setState('loadingData', true);
try { try {
//console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props }); //console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props });
if (props && props.url) { if (props && props.url) {
const head = new Headers(); const head = new Headers();
head.set('x-limit', String(pageSize ?? 50));
head.set('x-offset', String((pageSize ?? 50) * index));
head.set('Authorization', `Token ${props.authtoken}`); head.set('Authorization', `Token ${props.authtoken}`);
const ops: FetchAPIOperation[] = [
{ type: 'limit', value: String(pageSize ?? 50) },
{ type: 'offset', value: String((pageSize ?? 50) * index) },
];
if (colSort?.length && colSort.length > 0) { if (colSort?.length && colSort.length > 0) {
head.set( ops.push({
'x-sort', type: 'sort',
colSort value: colSort
?.map((sort: any) => `${sort.id} ${sort.direction}`) ?.map((sort: any) => `${sort.id} ${sort.direction}`)
.reduce((acc: any, val: any) => `${acc},${val}`) .reduce((acc: any, val: any) => `${acc},${val}`),
); });
} }
if (colFilters?.length && colFilters.length > 0) { colFilters
colFilters ?.filter((f) => f.value?.length > 0)
?.filter((f) => f.value?.length > 0) ?.forEach((filter: any) => {
if (filter.value && filter.value !== '') {
ops.push({
name: `${filter.id}`,
op: filter.operator,
type: 'searchop',
value: filter.value,
});
}
});
if (searchStr && searchStr !== '') {
columns
?.filter(
(f) =>
!f.disableFilter &&
!f.disableSearch &&
!f.virtual &&
f.id &&
((searchFields ?? []).length == 0 || searchFields?.includes(f.id))
)
?.forEach((filter: any) => { ?.forEach((filter: any) => {
if (filter.value && filter.value !== '') { ops.push({
head.set(`x-searchop-${filter.operator}-${filter.id}`, `${filter.value}`); name: `${filter.id ?? ""}`,
} op: 'contains',
type: 'searchor',
value: searchStr,
});
}); });
} }
if (props.filter && props.filter !== '') { if (props.filter && props.filter !== '') {
head.set('x-custom-sql-w-buildin-filter', props.filter); ops.push({
name: 'sql_filter',
type: 'custom-sql-w',
value: props.filter,
});
} }
if (props.options && props.options.length > 0) {
const optionHeaders = GoAPIHeaders(props.options); if ((props.options ?? []).length > 0) {
for (const oh in optionHeaders) { ops.push(...(props.options ?? []));
}
const col_ids =
columns
?.filter((col) => !col.virtual)
?.map((col: GridlerColumn) => {
return col.id;
}) ?? [];
if (props.hotfields && props.hotfields.length > 0) {
col_ids?.push(props.hotfields.join(','));
}
if (col_ids && col_ids.length > 0) {
ops.push({
type: 'select-fields',
value: col_ids.join(','),
});
}
if (ops && ops.length > 0) {
const optionHeaders = GoAPIHeaders(ops);
for (const oh in GoAPIHeaders(ops)) {
head.set(oh, optionHeaders[oh]); head.set(oh, optionHeaders[oh]);
} }
} }
@@ -72,6 +133,7 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
r.controller?.abort?.(); r.controller?.abort?.();
} }
}); });
if ( if (
_active_requests && _active_requests &&
currentRequestIndex >= 0 && currentRequestIndex >= 0 &&
@@ -113,9 +175,10 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
...(cv ?? []).filter((f) => f.page !== index), ...(cv ?? []).filter((f) => f.page !== index),
]); ]);
} }
} catch (e) { // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_e) {
//console.log('APIAdaptorGoLangv2 error', e); //console.log('APIAdaptorGoLangv2 error', e);
addError(`Error: ${e}`, 'api', props.url); //addError(`Error: ${e}`, 'api', props.url);
} }
setState('loadingData', false); setState('loadingData', false);
return []; return [];
@@ -125,7 +188,7 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
props.authtoken, props.authtoken,
props.url, props.url,
props.filter, props.filter,
props.options, JSON.stringify(props.options),
setState, setState,
setStateFN, setStateFN,
addError, addError,
@@ -139,8 +202,10 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
//console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props }); //console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props });
if (props && props.url) { if (props && props.url) {
const head = new Headers(); const head = new Headers();
head.set('x-limit', '10'); const ops: FetchAPIOperation[] = [
head.set('x-fetch-rownumber', String(key)); { type: 'limit', value: String(10) },
{ type: 'fetch-rownumber', value: key },
];
head.set('Authorization', `Token ${props.authtoken}`); head.set('Authorization', `Token ${props.authtoken}`);
@@ -155,7 +220,11 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
} }
if (props.filter && props.filter !== '') { if (props.filter && props.filter !== '') {
head.set('x-custom-sql-w-buildin-filter', props.filter); ops.push({
name: 'sql_filter',
type: 'custom-sql-w',
value: props.filter,
});
} }
if (props.options && props.options.length > 0) { if (props.options && props.options.length > 0) {
@@ -165,6 +234,13 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
} }
} }
if (ops && ops.length > 0) {
const optionHeaders = GoAPIHeaders(ops);
for (const oh in GoAPIHeaders(ops)) {
head.set(oh, optionHeaders[oh]);
}
}
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}}`, {
@@ -185,16 +261,23 @@ function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForG
[props.url, props.authtoken, props.filter, props.options, getState, addError] [props.url, props.authtoken, props.filter, props.options, getState, addError]
); );
//Reset the loaded pages to new rules
useEffect(() => {
loadPage(0, 'all');
}, [JSON.stringify(props.options), props.filter, props.url, props.authtoken]);
//Reset the function in the store. //Reset the function in the store.
useEffect(() => { useEffect(() => {
setState('useAPIQuery', useAPIQuery); setState('useAPIQuery', useAPIQuery);
setState('askAPIRowNumber', askAPIRowNumber); setState('askAPIRowNumber', askAPIRowNumber);
}, [props.url, props.authtoken, props.filter, props.options, mounted, setState]);
const _refresh = getState('_refresh');
//Reset the loaded pages to new rules
_refresh?.().then(() => {
const onChange = getState('onChange');
const getGridSelectedRows = getState('getGridSelectedRows');
if (onChange && typeof onChange === 'function') {
const buffers = getGridSelectedRows?.();
onChange(buffers);
}
});
}, [props.url, props.authtoken, props.filter, JSON.stringify(props.options), mounted, setState]);
return <></>; return <></>;
} }

View File

@@ -14,6 +14,7 @@ import type { GridlerColumn } from '../Column';
import { type GridlerProps, type GridlerState, useGridlerStore } from '../GridlerStore'; import { type GridlerProps, type GridlerState, useGridlerStore } from '../GridlerStore';
export function GlidlerFormAdaptor(props: { export function GlidlerFormAdaptor(props: {
changeOnActiveClick?: boolean;
descriptionField?: ((data: Record<string, unknown>) => string) | string; descriptionField?: ((data: Record<string, unknown>) => string) | string;
getMenuItems?: GridlerProps['getMenuItems']; getMenuItems?: GridlerProps['getMenuItems'];
onReload?: () => void; onReload?: () => void;
@@ -23,13 +24,35 @@ export function GlidlerFormAdaptor(props: {
) => void; ) => void;
showDescriptionInMenu?: boolean; showDescriptionInMenu?: boolean;
}) { }) {
const [getState, mounted, setState, reload] = useGridlerStore((s) => [ const [getState, mounted, setState, _events] = useGridlerStore((s) => [
s.getState, s.getState,
s.mounted, s.mounted,
s.setState, s.setState,
s.reload, s._events,
]); ]);
useEffect(() => {
if (mounted && props.changeOnActiveClick) {
const evf = (event: CustomEvent<any>) => {
const { row, state } = event.detail;
const getRowBuffer = state.getRowBuffer as (row: number) => Record<string, unknown>;
if (getRowBuffer) {
const rowData = getRowBuffer(row);
if (!rowData) {
return;
}
props.onRequestForm('change', rowData);
}
};
_events?.addEventListener('onCellActivated', evf as any);
return () => {
if (evf) {
_events?.removeEventListener('onCellActivated', evf as any);
}
};
}
}, [props.changeOnActiveClick, mounted, _events]);
const getMenuItems = useCallback( const getMenuItems = useCallback(
( (
id: string, id: string,
@@ -140,8 +163,9 @@ export function GlidlerFormAdaptor(props: {
c: 'orange', c: 'orange',
label: 'Refresh', label: 'Refresh',
leftSection: <IconRefresh color="orange" size={16} />, leftSection: <IconRefresh color="orange" size={16} />,
onClick: () => { onClickAsync: async () => {
reload?.(); const _refresh = getState('_refresh');
await _refresh?.();
}, },
}); });

View File

@@ -16,23 +16,30 @@ export interface GlidlerLocalDataAdaptorProps<T = unknown> {
cols: GridlerColumns | undefined, cols: GridlerColumns | undefined,
data: Array<T> data: Array<T>
) => Array<T>; ) => Array<T>;
onSearch?: (
searchField: string | undefined,
cols: GridlerColumns | undefined,
data: Array<T>
) => Array<T>;
} }
//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.
function _GlidlerLocalDataAdaptor<T = unknown>(props: GlidlerLocalDataAdaptorProps<T>) { function _GlidlerLocalDataAdaptor<T = unknown>(props: GlidlerLocalDataAdaptorProps<T>) {
const [setState, getState, mounted] = useGridlerStore((s) => [s.setState, s.getState, s.mounted]); const [setState, getState, mounted] = useGridlerStore((s) => [s.setState, s.getState, s.mounted]);
const { colFilters, colSort, columns } = useGridlerStore((s) => ({ const { colFilters, colSort, columns, searchStr } = useGridlerStore((s) => ({
colFilters: s.colFilters, colFilters: s.colFilters,
colOrder: s.colOrder, colOrder: s.colOrder,
colSize: s.colSize, colSize: s.colSize,
colSort: s.colSort, colSort: s.colSort,
columns: s.columns, columns: s.columns,
searchStr: s.searchStr,
})); }));
const refChanged = React.useRef({ const refChanged = React.useRef({
colFilters: colFilters, colFilters: colFilters,
colSort: colSort, colSort: colSort,
searchStr: searchStr,
}); });
const useAPIQuery: (index: number) => Promise<any> = async (index: number) => { const useAPIQuery: (index: number) => Promise<any> = async (index: number) => {
@@ -70,6 +77,16 @@ function _GlidlerLocalDataAdaptor<T = unknown>(props: GlidlerLocalDataAdaptorPro
} }
}, [colFilters, props.onColumnFilter]); }, [colFilters, props.onColumnFilter]);
useEffect(() => {
if (props.onSearch && searchStr !== refChanged?.current?.searchStr) {
const filteredData = props.onSearch(searchStr, columns, props.data as Array<T>);
setState('total_rows', filteredData.length);
setState('data', filteredData);
refChanged.current.colFilters = colFilters;
getState('refreshCells')?.();
}
}, [searchStr, props.onSearch]);
return <></>; return <></>;
} }
export const GlidlerLocalDataAdaptor = React.memo(_GlidlerLocalDataAdaptor); export const GlidlerLocalDataAdaptor = React.memo(_GlidlerLocalDataAdaptor);

View File

@@ -2,7 +2,9 @@ export {GlidlerAPIAdaptorForGoLangv2 } from './components/adaptors/GlidlerAPIAda
export {GlidlerFormAdaptor } from './components/adaptors/GlidlerFormAdaptor' export {GlidlerFormAdaptor } from './components/adaptors/GlidlerFormAdaptor'
export {GlidlerLocalDataAdaptor } from './components/adaptors/GlidlerLocalDataAdaptor' export {GlidlerLocalDataAdaptor } from './components/adaptors/GlidlerLocalDataAdaptor'
export * from './components/Column' export * from './components/Column'
export {useGridlerStore } from './components/GridlerStore' export {type GridlerProps,type GridlerRef,type GridlerState, useGridlerStore } from './components/GridlerStore'
export { GridlerRightMenuIcon } from './components/RightMenuIcon' export { GridlerRightMenuIcon } from './components/RightMenuIcon'
export {Gridler} from './Gridler' export {Gridler} from './Gridler'
export * from './utils' export {GoAPIHeaders} from './utils'
export type {FetchAPIOperation} from './utils'
export type {APIOptions} from './utils/types'

View File

@@ -17,6 +17,7 @@ export const GridlerGoAPIExampleEventlog = () => {
const [apiKey, setApiKey] = useLocalStorage({ defaultValue: '', key: 'apikey' }); const [apiKey, setApiKey] = useLocalStorage({ defaultValue: '', key: 'apikey' });
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 [sections, setSections] = useState<Record<string, unknown> | undefined>(undefined); const [sections, setSections] = useState<Record<string, unknown> | undefined>(undefined);
const columns: GridlerColumns = [ const columns: GridlerColumns = [
{ {
@@ -110,6 +111,7 @@ export const GridlerGoAPIExampleEventlog = () => {
}} }}
ref={ref} ref={ref}
scrollToRowKey={selectRow ? parseInt(selectRow, 10) : undefined} scrollToRowKey={selectRow ? parseInt(selectRow, 10) : undefined}
searchStr={search}
sections={{ ...sections, rightElementDisabled: false }} sections={{ ...sections, rightElementDisabled: false }}
selectFirstRowOnMount={true} selectFirstRowOnMount={true}
selectMode="row" selectMode="row"
@@ -119,10 +121,12 @@ export const GridlerGoAPIExampleEventlog = () => {
> >
<GlidlerAPIAdaptorForGoLangv2 <GlidlerAPIAdaptorForGoLangv2
authtoken={apiKey} authtoken={apiKey}
options={[{ type: 'preload', value: 'PRO' }]}
//options={[{ type: 'fieldfilter', name: 'process', value: 'test' }]} //options={[{ type: 'fieldfilter', name: 'process', value: 'test' }]}
url={`${apiUrl}/public/process`} url={`${apiUrl}/public/process`}
/> />
<Gridler.FormAdaptor <Gridler.FormAdaptor
changeOnActiveClick={true}
descriptionField={'process'} descriptionField={'process'}
onRequestForm={(request, data) => { onRequestForm={(request, data) => {
console.log('Form requested', request, data); console.log('Form requested', request, data);
@@ -131,6 +135,13 @@ export const GridlerGoAPIExampleEventlog = () => {
</Gridler> </Gridler>
<Divider /> <Divider />
<Group> <Group>
<TextInput
leftSection={<>S</>}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
value={search}
w="190px"
/>
<TextInput <TextInput
onChange={(e) => setSelectRow(e.target.value)} onChange={(e) => setSelectRow(e.target.value)}
placeholder="row" placeholder="row"

View File

@@ -1,3 +1,4 @@
//@ts-nocheck
import type { Meta, StoryObj } from '@storybook/react-vite'; import type { Meta, StoryObj } from '@storybook/react-vite';
import { Box } from '@mantine/core'; import { Box } from '@mantine/core';
@@ -6,7 +7,12 @@ import { fn } from 'storybook/test';
import { GridlerGoAPIExampleEventlog } from './Examples.goapi'; import { GridlerGoAPIExampleEventlog } from './Examples.goapi';
const Renderable = (props: any) => { const Renderable = (props: any) => {
return <Box h="100%" mih="400px" miw="400px" w='100%' > <GridlerGoAPIExampleEventlog {...props} /></Box>; return (
<Box h="100%" mih="400px" miw="400px" w="100%">
{' '}
<GridlerGoAPIExampleEventlog {...props} />
</Box>
);
}; };
const meta = { const meta = {
@@ -19,7 +25,7 @@ const meta = {
component: Renderable, component: Renderable,
parameters: { parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered', //layout: 'centered',
}, },
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'], tags: ['autodocs'],

View File

@@ -1,3 +1,4 @@
//@ts-nocheck
import type { Meta, StoryObj } from '@storybook/react-vite'; import type { Meta, StoryObj } from '@storybook/react-vite';
import { Box } from '@mantine/core'; import { Box } from '@mantine/core';
@@ -24,7 +25,7 @@ const meta = {
component: Renderable, component: Renderable,
parameters: { parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered', // layout: 'centered',
}, },
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'], tags: ['autodocs'],

View File

@@ -5,21 +5,20 @@ import { fn } from 'storybook/test';
import { MantineBetterMenusProvider, useMantineBetterMenus } from './'; import { MantineBetterMenusProvider, useMantineBetterMenus } from './';
const Renderable = (props: Record<string, unknown>) => {
const Renderable = (props: Record<string,unknown>) => {
return ( return (
<MantineBetterMenusProvider providerID='test' {...props} > <MantineBetterMenusProvider providerID="test" {...props}>
<Menu/> <Menu />
</MantineBetterMenusProvider> </MantineBetterMenusProvider>
); );
} };
const Menu = () => { const Menu = () => {
const menus = useMantineBetterMenus(); const menus = useMantineBetterMenus();
//menus.setState("menus",[{id:"test",items:[{id:"1",label:"Test",onClick:()=>{console.log("Clicked")}}]}]) //menus.setState("menus",[{id:"test",items:[{id:"1",label:"Test",onClick:()=>{console.log("Clicked")}}]}])
return <Button onClick={()=> menus.show("test",{})}>Menu</Button>; return <Button onClick={() => menus.show('test', {})}>Menu</Button>;
} };
const meta = { const meta = {
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
@@ -31,7 +30,7 @@ const meta = {
component: Renderable, component: Renderable,
parameters: { parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered', //layout: 'centered',
}, },
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'], tags: ['autodocs'],
@@ -44,8 +43,6 @@ type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const BasicExample: Story = { export const BasicExample: Story = {
args: { args: {
label: 'Test', label: 'Test',
}, },
}; };

View File

@@ -80,7 +80,7 @@ const MenuItemRenderer = ({ children, label, ...props }: MantineBetterMenuInstan
props.onClick?.(e); props.onClick?.(e);
if (props.onClickAsync) { if (props.onClickAsync) {
setLoading(true); setLoading(true);
props.onClickAsync().finally(() => setLoading(false)); props.onClickAsync(e).finally(() => setLoading(false));
} }
}} }}
styles={{ styles={{
@@ -120,7 +120,7 @@ const MenuItemRenderer = ({ children, label, ...props }: MantineBetterMenuInstan
props.onClick?.(e); props.onClick?.(e);
if (props.onClickAsync) { if (props.onClickAsync) {
setLoading(true); setLoading(true);
props.onClickAsync().finally(() => setLoading(false)); props.onClickAsync(e).finally(() => setLoading(false));
} }
}} }}
styles={{ styles={{

View File

@@ -22,7 +22,7 @@ export interface MantineBetterMenuInstanceItem extends Partial<MenuItemProps> {
items?: Array<MantineBetterMenuInstanceItem>; items?: Array<MantineBetterMenuInstanceItem>;
label?: string; label?: string;
onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onClickAsync?: () => Promise<void>; onClickAsync?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>;
renderer?: renderer?:
| ((props: MantineBetterMenuInstanceItem & Record<string, unknown>) => ReactNode) | ((props: MantineBetterMenuInstanceItem & Record<string, unknown>) => ReactNode)
| ReactNode; | ReactNode;