proje-react-panel 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules.md +21 -95
- package/.vscode/settings.json +6 -1
- package/PTD.md +124 -24
- package/dist/components/DetailsPage.d.ts +2 -2
- package/dist/components/Login.d.ts +2 -2
- package/dist/components/Panel.d.ts +2 -2
- package/dist/components/form/FormField.d.ts +1 -5
- package/dist/components/form/FormHeader.d.ts +10 -0
- package/dist/components/form/FormPage.d.ts +12 -1
- package/dist/components/form/Select.d.ts +1 -1
- package/dist/components/form/SelectStyles.d.ts +3 -3
- package/dist/components/list/CellField.d.ts +2 -3
- package/dist/components/list/EmptyList.d.ts +1 -1
- package/dist/decorators/details/Details.d.ts +1 -2
- package/dist/decorators/form/Form.d.ts +3 -5
- package/dist/decorators/form/Input.d.ts +10 -2
- package/dist/decorators/form/inputs/SelectInput.d.ts +8 -7
- package/dist/decorators/list/Cell.d.ts +3 -3
- package/dist/decorators/list/List.d.ts +3 -4
- package/dist/decorators/list/getListPageMeta.d.ts +2 -2
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/store/store.d.ts +1 -0
- package/dist/utils/decerators.d.ts +3 -3
- package/eslint.config.mjs +60 -0
- package/package.json +6 -3
- package/src/api/CrudApi.ts +59 -53
- package/src/components/DetailsPage.tsx +8 -6
- package/src/components/Login.tsx +11 -4
- package/src/components/Panel.tsx +3 -3
- package/src/components/form/Checkbox.tsx +1 -1
- package/src/components/form/FormField.tsx +13 -9
- package/src/components/form/FormHeader.tsx +18 -0
- package/src/components/form/FormPage.tsx +146 -5
- package/src/components/form/InnerForm.tsx +13 -8
- package/src/components/form/Select.tsx +14 -15
- package/src/components/form/SelectStyles.ts +4 -4
- package/src/components/layout/Layout.tsx +1 -7
- package/src/components/layout/SideBar.tsx +1 -2
- package/src/components/list/CellField.tsx +2 -5
- package/src/components/list/Datagrid.tsx +0 -1
- package/src/components/list/EmptyList.tsx +2 -2
- package/src/components/list/FilterPopup.tsx +4 -2
- package/src/components/list/ListPage.tsx +7 -5
- package/src/components/list/cells/DefaultCell.tsx +2 -0
- package/src/decorators/details/Details.ts +2 -2
- package/src/decorators/details/DetailsItem.ts +2 -0
- package/src/decorators/form/Form.ts +10 -11
- package/src/decorators/form/Input.ts +19 -10
- package/src/decorators/form/inputs/SelectInput.ts +8 -7
- package/src/decorators/list/Cell.ts +6 -4
- package/src/decorators/list/ExtendedCell.ts +1 -9
- package/src/decorators/list/List.ts +8 -4
- package/src/decorators/list/cells/ImageCell.ts +1 -1
- package/src/decorators/list/getListPageMeta.ts +4 -3
- package/src/store/store.ts +3 -1
- package/src/styles/components/form-header.scss +75 -0
- package/src/styles/index.scss +1 -0
- package/src/types/AnyClass.ts +3 -0
- package/src/utils/decerators.ts +3 -2
- package/.eslintrc.js +0 -23
- package/.eslintrc.json +0 -26
- package/src/initPanel.ts +0 -3
- package/src/types/initPanelOptions.ts +0 -1
@@ -0,0 +1,60 @@
|
|
1
|
+
import { defineConfig } from 'eslint/config';
|
2
|
+
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
|
3
|
+
import react from 'eslint-plugin-react';
|
4
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
5
|
+
import path from 'node:path';
|
6
|
+
import { fileURLToPath } from 'node:url';
|
7
|
+
import js from '@eslint/js';
|
8
|
+
import { FlatCompat } from '@eslint/eslintrc';
|
9
|
+
import globals from 'globals';
|
10
|
+
import eslint from '@eslint/js';
|
11
|
+
import tseslint from 'typescript-eslint';
|
12
|
+
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
14
|
+
const __dirname = path.dirname(__filename);
|
15
|
+
const compat = new FlatCompat({
|
16
|
+
baseDirectory: __dirname,
|
17
|
+
recommendedConfig: js.configs.recommended,
|
18
|
+
allConfig: js.configs.all,
|
19
|
+
});
|
20
|
+
|
21
|
+
export default defineConfig(
|
22
|
+
eslint.configs.recommended,
|
23
|
+
tseslint.configs.recommended,
|
24
|
+
tseslint.configs.stylistic,
|
25
|
+
[
|
26
|
+
{
|
27
|
+
languageOptions: {
|
28
|
+
globals: {
|
29
|
+
...globals.browser,
|
30
|
+
...globals.nodeBuiltin,
|
31
|
+
},
|
32
|
+
},
|
33
|
+
extends: fixupConfigRules(
|
34
|
+
compat.extends(
|
35
|
+
'plugin:react/recommended',
|
36
|
+
'plugin:react-hooks/recommended'
|
37
|
+
)
|
38
|
+
),
|
39
|
+
|
40
|
+
plugins: {
|
41
|
+
react: fixupPluginRules(react),
|
42
|
+
'react-hooks': fixupPluginRules(reactHooks),
|
43
|
+
},
|
44
|
+
|
45
|
+
settings: {
|
46
|
+
react: {
|
47
|
+
version: 'detect',
|
48
|
+
},
|
49
|
+
},
|
50
|
+
|
51
|
+
rules: {
|
52
|
+
'react/function-component-definition': ['error'],
|
53
|
+
'import/prefer-default-export': 'off',
|
54
|
+
'react/prefer-stateless-function': 'error',
|
55
|
+
'react/no-this-in-sfc': 'error',
|
56
|
+
'react-hooks/exhaustive-deps': 'warn',
|
57
|
+
},
|
58
|
+
},
|
59
|
+
]
|
60
|
+
);
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "proje-react-panel",
|
3
|
-
"version": "1.1.
|
3
|
+
"version": "1.1.6",
|
4
4
|
"type": "module",
|
5
5
|
"description": "",
|
6
6
|
"author": "SEFA DEMİR",
|
@@ -12,8 +12,7 @@
|
|
12
12
|
"scripts": {
|
13
13
|
"test": "echo \"Error: no test specified\" && exit 1",
|
14
14
|
"build": "rollup -c",
|
15
|
-
"lint": "eslint src --ext .ts,.tsx"
|
16
|
-
"generate-font": "node src/scripts/svg-to-font.js"
|
15
|
+
"lint": "eslint src --ext .ts,.tsx"
|
17
16
|
},
|
18
17
|
"repository": {
|
19
18
|
"type": "git",
|
@@ -26,6 +25,7 @@
|
|
26
25
|
"homepage": "https://github.com/demirsefa/proje-react-panel#readme",
|
27
26
|
"dependencies": {},
|
28
27
|
"devDependencies": {
|
28
|
+
"@eslint/compat": "^1.2.9",
|
29
29
|
"@hookform/resolvers": "^4.1.3",
|
30
30
|
"@rollup/plugin-commonjs": "^28.0.3",
|
31
31
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
@@ -37,6 +37,8 @@
|
|
37
37
|
"class-validator": "^0.14.1",
|
38
38
|
"eslint": "^9.24.0",
|
39
39
|
"eslint-plugin-react": "^7.37.4",
|
40
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
41
|
+
"globals": "^16.0.0",
|
40
42
|
"prettier": "^3.5.3",
|
41
43
|
"prettier-eslint": "^16.3.0",
|
42
44
|
"react": "^19.0.0",
|
@@ -52,6 +54,7 @@
|
|
52
54
|
"rollup-plugin-typescript2": "^0.36.0",
|
53
55
|
"svgicons2svgfont": "^15.0.1",
|
54
56
|
"typescript": "^5.8.3",
|
57
|
+
"typescript-eslint": "^8.31.1",
|
55
58
|
"use-sync-external-store": "^1.4.0",
|
56
59
|
"zustand": "^5.0.3"
|
57
60
|
},
|
package/src/api/CrudApi.ts
CHANGED
@@ -1,59 +1,65 @@
|
|
1
1
|
interface FetchOptions {
|
2
|
-
|
3
|
-
|
2
|
+
token: string;
|
3
|
+
baseUrl: string;
|
4
4
|
}
|
5
5
|
|
6
6
|
export const CrudApi = {
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
7
|
+
getList: (options: FetchOptions, api: string) => {
|
8
|
+
return fetch(`${options.baseUrl}/${api}`, {
|
9
|
+
method: 'GET',
|
10
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${options.token}` },
|
11
|
+
}).then(res => {
|
12
|
+
if (res.ok) {
|
13
|
+
return res.json();
|
14
|
+
}
|
15
|
+
throw res;
|
16
|
+
});
|
17
|
+
},
|
18
|
+
//TODO: fix this
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
20
|
+
create: (options: FetchOptions, api: string, data: any) => {
|
21
|
+
const headers: HeadersInit = { Authorization: `Bearer ${options.token}` };
|
22
|
+
// Don't set Content-Type for FormData
|
23
|
+
if (!(data instanceof FormData)) {
|
24
|
+
headers['Content-Type'] = 'application/json';
|
25
|
+
}
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
27
|
+
return fetch(`${options?.baseUrl ?? ''}/${api}`, {
|
28
|
+
method: 'POST',
|
29
|
+
headers,
|
30
|
+
body: data instanceof FormData ? data : JSON.stringify(data),
|
31
|
+
}).then(res => res.json());
|
32
|
+
},
|
33
|
+
//TODO: fix this
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
35
|
+
details: (options: FetchOptions, api: string, id: any) => {
|
36
|
+
return fetch(`${options?.baseUrl ?? ''}/${api}/${id}`, {
|
37
|
+
method: 'GET',
|
38
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${options.token}` },
|
39
|
+
}).then(res => {
|
40
|
+
return res.json();
|
41
|
+
});
|
42
|
+
},
|
43
|
+
//TODO: fix this
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
45
|
+
edit: (options: FetchOptions, api: string, data: any) => {
|
46
|
+
const headers: HeadersInit = { Authorization: `Bearer ${options.token}` };
|
47
|
+
// Don't set Content-Type for FormData
|
48
|
+
if (!(data instanceof FormData)) {
|
49
|
+
headers['Content-Type'] = 'application/json';
|
50
|
+
}
|
51
|
+
return fetch(`${options?.baseUrl ?? ''}/${api}/${data.id}`, {
|
52
|
+
method: 'PUT',
|
53
|
+
headers,
|
54
|
+
body: data instanceof FormData ? data : JSON.stringify(data),
|
55
|
+
}).then(res => res.json());
|
56
|
+
},
|
57
|
+
delete: (options: FetchOptions, api: string, id: string) => {
|
58
|
+
return fetch(`${options?.baseUrl ?? ''}/${api}/${id}`, {
|
59
|
+
method: 'DELETE',
|
60
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${options.token}` },
|
61
|
+
}).then(res => {
|
62
|
+
return res.clone().json();
|
63
|
+
});
|
64
|
+
},
|
59
65
|
};
|
@@ -1,19 +1,19 @@
|
|
1
1
|
import { useParams } from 'react-router';
|
2
2
|
import React, { useEffect, useId, useMemo, useState } from 'react';
|
3
3
|
import { ErrorComponent } from './ErrorComponent';
|
4
|
-
import { AnyClass } from '../types/AnyClass';
|
4
|
+
import { AnyClass, AnyClassConstructor } from '../types/AnyClass';
|
5
5
|
import { getDetailsPageMeta } from '../decorators/details/getDetailsPageMeta';
|
6
6
|
import { LoadingScreen } from './LoadingScreen';
|
7
7
|
|
8
8
|
interface DetailsPageProps<T extends AnyClass> {
|
9
|
-
model:
|
9
|
+
model: AnyClassConstructor<T>;
|
10
10
|
}
|
11
11
|
|
12
12
|
export function DetailsPage<T extends AnyClass>({ model }: DetailsPageProps<T>) {
|
13
13
|
const id = useId();
|
14
14
|
const { class: detailsClass, items } = useMemo(() => getDetailsPageMeta(model), [model]);
|
15
15
|
const params = useParams();
|
16
|
-
const [data, setData] = useState<
|
16
|
+
const [data, setData] = useState<T | null>(null);
|
17
17
|
const [error, setError] = useState(null);
|
18
18
|
const [loading, setLoading] = useState(true);
|
19
19
|
|
@@ -21,11 +21,13 @@ export function DetailsPage<T extends AnyClass>({ model }: DetailsPageProps<T>)
|
|
21
21
|
detailsClass
|
22
22
|
.getDetailsData(params as Record<string, string>)
|
23
23
|
.then(data => {
|
24
|
-
|
24
|
+
//TODO: any is not a good solution, we need to find a better way to do this
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
26
|
+
setData(data as any);
|
25
27
|
})
|
26
28
|
.catch(setError)
|
27
29
|
.finally(() => setLoading(false));
|
28
|
-
}, [params, detailsClass
|
30
|
+
}, [params, detailsClass.getDetailsData, detailsClass]);
|
29
31
|
|
30
32
|
if (error) {
|
31
33
|
return (
|
@@ -48,7 +50,7 @@ export function DetailsPage<T extends AnyClass>({ model }: DetailsPageProps<T>)
|
|
48
50
|
{items.map(item => (
|
49
51
|
<div key={item.name} className="details-item">
|
50
52
|
<div className="item-label">{item.name}</div>
|
51
|
-
<div className="item-value">{data[item.name]}</div>
|
53
|
+
<div className="item-value">{data?.[item.name]}</div>
|
52
54
|
</div>
|
53
55
|
))}
|
54
56
|
</div>
|
package/src/components/Login.tsx
CHANGED
@@ -9,12 +9,14 @@ interface LoginFormData {
|
|
9
9
|
password: string;
|
10
10
|
}
|
11
11
|
|
12
|
-
export
|
12
|
+
export interface OnLogin {
|
13
13
|
login: (username: string, password: string) => Promise<LoginResponse>;
|
14
|
-
}
|
14
|
+
}
|
15
15
|
|
16
16
|
interface LoginResponse {
|
17
|
-
|
17
|
+
//TODO: any is not a good solution, we need to find a better way to do this
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
19
|
+
user: any;
|
18
20
|
token: string;
|
19
21
|
}
|
20
22
|
|
@@ -33,7 +35,7 @@ export function Login({ onLogin }: LoginProps) {
|
|
33
35
|
onLogin.login(data.username, data.password).then((dataInner: LoginResponse) => {
|
34
36
|
const { user, token } = dataInner;
|
35
37
|
localStorage.setItem('token', token);
|
36
|
-
useAppStore.
|
38
|
+
useAppStore.getState().login(user);
|
37
39
|
navigate('/');
|
38
40
|
});
|
39
41
|
};
|
@@ -52,6 +54,9 @@ export function Login({ onLogin }: LoginProps) {
|
|
52
54
|
label: 'Username',
|
53
55
|
placeholder: 'Enter your username',
|
54
56
|
type: 'input',
|
57
|
+
inputType: 'text',
|
58
|
+
includeInCSV: true,
|
59
|
+
includeInJSON: true,
|
55
60
|
}}
|
56
61
|
register={register}
|
57
62
|
error={errors.username}
|
@@ -63,6 +68,8 @@ export function Login({ onLogin }: LoginProps) {
|
|
63
68
|
inputType: 'password',
|
64
69
|
placeholder: 'Enter your password',
|
65
70
|
type: 'input',
|
71
|
+
includeInCSV: true,
|
72
|
+
includeInJSON: true,
|
66
73
|
}}
|
67
74
|
register={register}
|
68
75
|
error={errors.password}
|
package/src/components/Panel.tsx
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
import React
|
1
|
+
import React from 'react';
|
2
2
|
import { ErrorBoundary } from './ErrorBoundary';
|
3
3
|
import { ToastContainer } from 'react-toastify';
|
4
4
|
|
5
|
-
|
5
|
+
interface AppProps {
|
6
6
|
children: React.ReactNode;
|
7
|
-
}
|
7
|
+
}
|
8
8
|
|
9
9
|
export function Panel({ children }: AppProps) {
|
10
10
|
/*useEffect(() => {
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import React
|
2
|
-
import { InputConfiguration
|
1
|
+
import React from 'react';
|
2
|
+
import { InputConfiguration } from '../../decorators/form/Input';
|
3
3
|
import { useFormContext, UseFormRegister } from 'react-hook-form';
|
4
4
|
import { Uploader } from './Uploader';
|
5
5
|
import { Checkbox } from './Checkbox';
|
@@ -8,16 +8,17 @@ import { Select } from './Select';
|
|
8
8
|
|
9
9
|
interface FormFieldProps {
|
10
10
|
input: InputConfiguration;
|
11
|
+
//TODO: any is not a good solution, we need to find a better way to do this
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
11
13
|
register: UseFormRegister<any>;
|
12
14
|
error?: { message?: string };
|
13
15
|
baseName?: string;
|
14
|
-
onSelectPreloader?: (
|
15
|
-
inputOptions: InputConfiguration
|
16
|
-
) => Promise<{ label: string; value: string }[]>;
|
17
16
|
}
|
18
17
|
|
19
18
|
interface NestedFormFieldsProps {
|
20
19
|
input: InputConfiguration;
|
20
|
+
//TODO: any is not a good solution, we need to find a better way to do this
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
21
22
|
register: UseFormRegister<any>;
|
22
23
|
}
|
23
24
|
|
@@ -25,8 +26,11 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
|
25
26
|
const form = useFormContext();
|
26
27
|
//TODO: inputOptions İnputResult seperate
|
27
28
|
const data = form.getValues(input.name!);
|
29
|
+
console.log('--_>', data, input, input.nestedFields);
|
28
30
|
return (
|
29
31
|
<div>
|
32
|
+
{/* TODO: any is not a good solution, we need to find a better way to do this */}
|
33
|
+
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
30
34
|
{data?.map((value: any, index: number) => (
|
31
35
|
<div key={index}>
|
32
36
|
{input.nestedFields?.map((nestedInput: InputConfiguration) => (
|
@@ -37,7 +41,7 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
|
37
41
|
register={register}
|
38
42
|
error={
|
39
43
|
input.name
|
40
|
-
? { message: (form.formState.errors[input.name] as
|
44
|
+
? { message: (form.formState.errors[input.name] as { message: string })?.message }
|
41
45
|
: undefined
|
42
46
|
}
|
43
47
|
/>
|
@@ -48,8 +52,8 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
|
48
52
|
);
|
49
53
|
}
|
50
54
|
|
51
|
-
export function FormField({ input, register, error, baseName
|
52
|
-
const fieldName = (baseName ? baseName.toString() + '.' : '') + input.name || '';
|
55
|
+
export function FormField({ input, register, error, baseName }: FormFieldProps) {
|
56
|
+
const fieldName: string = (baseName ? baseName.toString() + '.' : '') + input.name || '';
|
53
57
|
const renderField = () => {
|
54
58
|
switch (input.type) {
|
55
59
|
case 'textarea':
|
@@ -70,7 +74,7 @@ export function FormField({ input, register, error, baseName, onSelectPreloader
|
|
70
74
|
case 'nested':
|
71
75
|
return <NestedFormFields input={input} register={register} />;
|
72
76
|
default:
|
73
|
-
null;
|
77
|
+
return null;
|
74
78
|
}
|
75
79
|
};
|
76
80
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { AnyClass } from '../../types/AnyClass';
|
3
|
+
import { FormUtils } from './FormPage';
|
4
|
+
|
5
|
+
interface FormHeaderProps<T extends AnyClass> {
|
6
|
+
title?: string;
|
7
|
+
header?: (utils: FormUtils<T>) => React.ReactNode;
|
8
|
+
utils: FormUtils<T>;
|
9
|
+
}
|
10
|
+
|
11
|
+
export function FormHeader<T extends AnyClass>({ utils, header, title }: FormHeaderProps<T>) {
|
12
|
+
return (
|
13
|
+
<div className="form-header">
|
14
|
+
{title && <h2 className="form-title">{title}</h2>}
|
15
|
+
<div className="form-actions">{header ? header(utils) : null}</div>
|
16
|
+
</div>
|
17
|
+
);
|
18
|
+
}
|
@@ -2,30 +2,171 @@ import React, { useEffect, useMemo } from 'react';
|
|
2
2
|
import { InnerForm } from './InnerForm';
|
3
3
|
import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
|
4
4
|
import { useParams } from 'react-router';
|
5
|
-
import { FormProvider, Resolver, useForm } from 'react-hook-form';
|
5
|
+
import { FormProvider, Resolver, useForm, UseFormReturn, Path, PathValue } from 'react-hook-form';
|
6
6
|
import { getFormPageMeta } from '../../decorators/form/getFormPageMeta';
|
7
|
+
import { FormHeader } from './FormHeader';
|
8
|
+
import { InputConfiguration } from '../../decorators/form/Input';
|
9
|
+
|
10
|
+
export interface FormUtils<T extends AnyClass> {
|
11
|
+
getValues: () => T;
|
12
|
+
setValues: (values: Partial<T>) => void;
|
13
|
+
toJSON: (values: Partial<T>) => string;
|
14
|
+
fromJSON: (json: string) => Partial<T>;
|
15
|
+
export: (data: string, extension: 'txt' | 'json' | 'csv') => void;
|
16
|
+
import: () => Promise<string>;
|
17
|
+
}
|
7
18
|
|
8
19
|
export interface FormPageProps<T extends AnyClass> {
|
9
20
|
model: AnyClassConstructor<T>;
|
21
|
+
title?: string;
|
22
|
+
documentTitle?: string;
|
23
|
+
header?: (utils: FormUtils<T>) => React.ReactNode;
|
10
24
|
}
|
11
25
|
|
12
|
-
|
26
|
+
function useCreateFormUtils<T extends AnyClass>(
|
27
|
+
inputs: InputConfiguration[],
|
28
|
+
form: UseFormReturn<T>
|
29
|
+
): FormUtils<T> {
|
30
|
+
return {
|
31
|
+
getValues: form.getValues,
|
32
|
+
setValues: (values: Partial<T>) => {
|
33
|
+
Object.entries(values).forEach(([key, value]) => {
|
34
|
+
const input = inputs.find(input => input.name === key);
|
35
|
+
//form.reset(); //TODO: if there is default fix it??
|
36
|
+
if (input) {
|
37
|
+
if (input.nestedFields) {
|
38
|
+
if (Array.isArray(value)) {
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
40
|
+
const innerArray: Record<string, any>[] = [];
|
41
|
+
value.forEach(nestedValue => {
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
43
|
+
const innerObject: Record<string, any> = {};
|
44
|
+
input.nestedFields!.forEach(nestedInput => {
|
45
|
+
if (nestedInput.includeInJSON) {
|
46
|
+
innerObject[nestedInput.name] = nestedValue[nestedInput.name];
|
47
|
+
}
|
48
|
+
});
|
49
|
+
innerArray.push(innerObject);
|
50
|
+
});
|
51
|
+
form.setValue(key as Path<T>, innerArray as PathValue<T, Path<T>>);
|
52
|
+
} else {
|
53
|
+
console.warn('Non-array nested fields are not supported for json conversion', value);
|
54
|
+
}
|
55
|
+
} else {
|
56
|
+
form.setValue(key as Path<T>, value);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
});
|
60
|
+
},
|
61
|
+
toJSON: (values: Partial<T>) => {
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
63
|
+
const jsonData: Record<string, any> = {};
|
64
|
+
Object.entries(values).forEach(([key, value]) => {
|
65
|
+
const input = inputs.find(input => input.name === key);
|
66
|
+
//form.reset(); //TODO: if there is default fix it??
|
67
|
+
if (input && input.includeInJSON) {
|
68
|
+
if (input.nestedFields) {
|
69
|
+
jsonData[key] = [];
|
70
|
+
if (Array.isArray(value)) {
|
71
|
+
//TODO: fix this
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
73
|
+
value.forEach((innerValue: any) => {
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
75
|
+
const nestedObject: Record<string, any> = {};
|
76
|
+
jsonData[key].push(nestedObject);
|
77
|
+
Object.entries(innerValue).forEach(([nestedKey, nestedValue]) => {
|
78
|
+
const nestedInput = input.nestedFields!.find(
|
79
|
+
nestedInput => nestedInput.name === nestedKey
|
80
|
+
);
|
81
|
+
if (nestedInput && nestedInput.includeInJSON) {
|
82
|
+
nestedObject[nestedKey] = nestedValue;
|
83
|
+
}
|
84
|
+
});
|
85
|
+
});
|
86
|
+
} else {
|
87
|
+
console.warn('Non-array nested fields are not supported for json conversion', value);
|
88
|
+
}
|
89
|
+
} else {
|
90
|
+
jsonData[key] = value;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
});
|
94
|
+
return JSON.stringify(jsonData, null, 2);
|
95
|
+
},
|
96
|
+
fromJSON: (json: string): Partial<T> => {
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
98
|
+
const jsonObject: Record<string, any> = {};
|
99
|
+
const jsonData = JSON.parse(json);
|
100
|
+
Object.entries(jsonData).forEach(([key, value]) => {
|
101
|
+
const input = inputs.find(input => input.name === key);
|
102
|
+
if (input && input.includeInJSON) {
|
103
|
+
jsonObject[key] = value;
|
104
|
+
}
|
105
|
+
});
|
106
|
+
return jsonObject as Partial<T>;
|
107
|
+
},
|
108
|
+
export: (data: string, extension: 'txt' | 'json' | 'csv') => {
|
109
|
+
const blob = new Blob([data], { type: 'text/plain' });
|
110
|
+
const url = URL.createObjectURL(blob);
|
111
|
+
const a = document.createElement('a');
|
112
|
+
a.href = url;
|
113
|
+
a.download = `form-data.${extension}`;
|
114
|
+
document.body.appendChild(a);
|
115
|
+
a.click();
|
116
|
+
document.body.removeChild(a);
|
117
|
+
URL.revokeObjectURL(url);
|
118
|
+
},
|
119
|
+
import: () => {
|
120
|
+
return new Promise<string>(resolve => {
|
121
|
+
const input = document.createElement('input');
|
122
|
+
input.type = 'file';
|
123
|
+
input.accept = '.txt,.json,.csv';
|
124
|
+
input.onchange = e => {
|
125
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
126
|
+
if (file) {
|
127
|
+
const reader = new FileReader();
|
128
|
+
reader.onload = event => {
|
129
|
+
resolve(event.target?.result as string);
|
130
|
+
};
|
131
|
+
reader.readAsText(file);
|
132
|
+
}
|
133
|
+
};
|
134
|
+
input.click();
|
135
|
+
});
|
136
|
+
},
|
137
|
+
};
|
138
|
+
}
|
139
|
+
|
140
|
+
export function FormPage<T extends AnyClass>({
|
141
|
+
model,
|
142
|
+
title,
|
143
|
+
documentTitle,
|
144
|
+
header,
|
145
|
+
}: FormPageProps<T>) {
|
13
146
|
const { class: formClass, inputs, resolver } = useMemo(() => getFormPageMeta(model), [model]);
|
147
|
+
const params = useParams();
|
14
148
|
const form = useForm<T>({
|
15
149
|
resolver: resolver as Resolver<T>,
|
16
150
|
});
|
151
|
+
const utils = useCreateFormUtils(inputs, form);
|
152
|
+
|
153
|
+
useEffect(() => {
|
154
|
+
if (documentTitle) {
|
155
|
+
document.title = documentTitle;
|
156
|
+
}
|
157
|
+
}, [documentTitle]);
|
17
158
|
|
18
|
-
const params = useParams();
|
19
159
|
useEffect(() => {
|
20
160
|
if (formClass.getDetailsData) {
|
21
161
|
formClass.getDetailsData(params as Record<string, string>).then(data => {
|
22
|
-
form.reset(data as
|
162
|
+
form.reset(data as T);
|
23
163
|
});
|
24
164
|
}
|
25
|
-
}, [params, form.reset, formClass.getDetailsData]);
|
165
|
+
}, [params, form.reset, formClass.getDetailsData, formClass, form]);
|
26
166
|
|
27
167
|
return (
|
28
168
|
<FormProvider {...form}>
|
169
|
+
<FormHeader title={title} utils={utils} header={header} />
|
29
170
|
<InnerForm inputs={inputs} formClass={formClass} />
|
30
171
|
</FormProvider>
|
31
172
|
);
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import React, {
|
2
|
-
import {
|
1
|
+
import React, { useRef, useState } from 'react';
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
3
3
|
import { InputConfiguration } from '../../decorators/form/Input';
|
4
4
|
import { FormField } from './FormField';
|
5
5
|
import { useNavigate } from 'react-router';
|
@@ -45,15 +45,14 @@ export function InnerForm<T extends AnyClass>({ inputs, formClass }: InnerFormPr
|
|
45
45
|
form.reset(resut);
|
46
46
|
setErrorMessage(null);
|
47
47
|
toast.success('Form submitted successfully');
|
48
|
-
|
49
|
-
navigate(-1);
|
50
|
-
}
|
48
|
+
//TODO: https path or relative path
|
51
49
|
if (formClass.redirectSuccessUrl) {
|
52
50
|
navigate(formClass.redirectSuccessUrl);
|
53
51
|
}
|
54
|
-
} catch (error:
|
52
|
+
} catch (error: unknown) {
|
53
|
+
const errorResponse = error as { response?: { data?: { message?: string } } };
|
55
54
|
const message =
|
56
|
-
|
55
|
+
errorResponse?.response?.data?.message ||
|
57
56
|
(error instanceof Error ? error.message : 'An error occurred');
|
58
57
|
toast.error('Something went wrong');
|
59
58
|
setErrorMessage(message);
|
@@ -81,7 +80,13 @@ export function InnerForm<T extends AnyClass>({ inputs, formClass }: InnerFormPr
|
|
81
80
|
register={form.register}
|
82
81
|
error={
|
83
82
|
input.name
|
84
|
-
? {
|
83
|
+
? {
|
84
|
+
message: (
|
85
|
+
form.formState.errors[input.name as keyof T] as {
|
86
|
+
message: string;
|
87
|
+
}
|
88
|
+
)?.message,
|
89
|
+
}
|
85
90
|
: undefined
|
86
91
|
}
|
87
92
|
/>
|