proje-react-panel 1.1.3 → 1.1.5
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/ErrorComponent.d.ts +3 -2
- package/dist/components/LoadingScreen.d.ts +3 -1
- 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 +6 -4
- 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 +7 -3
- package/src/api/CrudApi.ts +59 -53
- package/src/components/DetailsPage.tsx +12 -9
- package/src/components/ErrorComponent.tsx +36 -33
- package/src/components/LoadingScreen.tsx +4 -3
- package/src/components/Login.tsx +11 -4
- package/src/components/Panel.tsx +10 -6
- 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 +24 -8
- package/src/components/form/Select.tsx +14 -12
- 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 +11 -8
- 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 +13 -10
- package/src/decorators/form/Input.ts +18 -9
- 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
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,18 +1,19 @@
|
|
1
1
|
import { useParams } from 'react-router';
|
2
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
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
|
+
const id = useId();
|
13
14
|
const { class: detailsClass, items } = useMemo(() => getDetailsPageMeta(model), [model]);
|
14
15
|
const params = useParams();
|
15
|
-
const [data, setData] = useState<
|
16
|
+
const [data, setData] = useState<T | null>(null);
|
16
17
|
const [error, setError] = useState(null);
|
17
18
|
const [loading, setLoading] = useState(true);
|
18
19
|
|
@@ -20,16 +21,18 @@ export function DetailsPage<T extends AnyClass>({ model }: DetailsPageProps<T>)
|
|
20
21
|
detailsClass
|
21
22
|
.getDetailsData(params as Record<string, string>)
|
22
23
|
.then(data => {
|
23
|
-
|
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);
|
24
27
|
})
|
25
28
|
.catch(setError)
|
26
29
|
.finally(() => setLoading(false));
|
27
|
-
}, [params, detailsClass
|
30
|
+
}, [params, detailsClass.getDetailsData, detailsClass]);
|
28
31
|
|
29
32
|
if (error) {
|
30
33
|
return (
|
31
34
|
<div className="error-container">
|
32
|
-
<ErrorComponent error={error} />
|
35
|
+
<ErrorComponent error={error} id={id} />
|
33
36
|
</div>
|
34
37
|
);
|
35
38
|
}
|
@@ -37,7 +40,7 @@ export function DetailsPage<T extends AnyClass>({ model }: DetailsPageProps<T>)
|
|
37
40
|
if (loading) {
|
38
41
|
return (
|
39
42
|
<div className="loading-container">
|
40
|
-
<LoadingScreen />
|
43
|
+
<LoadingScreen id={id} />
|
41
44
|
</div>
|
42
45
|
);
|
43
46
|
}
|
@@ -47,7 +50,7 @@ export function DetailsPage<T extends AnyClass>({ model }: DetailsPageProps<T>)
|
|
47
50
|
{items.map(item => (
|
48
51
|
<div key={item.name} className="details-item">
|
49
52
|
<div className="item-label">{item.name}</div>
|
50
|
-
<div className="item-value">{data[item.name]}</div>
|
53
|
+
<div className="item-value">{data?.[item.name]}</div>
|
51
54
|
</div>
|
52
55
|
))}
|
53
56
|
</div>
|
@@ -1,35 +1,38 @@
|
|
1
|
-
import React from
|
1
|
+
import React from 'react';
|
2
2
|
//TODO: create, edit, details
|
3
|
-
export function ErrorComponent({ error }: { error: unknown | Response }) {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
3
|
+
export function ErrorComponent({ error, id }: { error: unknown | Response; id: string }) {
|
4
|
+
const getErrorMessage = (errorInner: unknown | Response) => {
|
5
|
+
if (errorInner instanceof Response) {
|
6
|
+
switch (errorInner.status) {
|
7
|
+
case 400:
|
8
|
+
return 'Bad Request: The request was invalid or malformed.';
|
9
|
+
case 401:
|
10
|
+
return 'Unauthorized: Please log in to access this resource.';
|
11
|
+
case 404:
|
12
|
+
return 'Not Found: The requested resource could not be found.';
|
13
|
+
case 403:
|
14
|
+
return "Forbidden: You don't have permission to access this resource.";
|
15
|
+
case 500:
|
16
|
+
return 'Internal Server Error: Something went wrong on our end.';
|
17
|
+
default:
|
18
|
+
return `Error ${errorInner.status}: ${errorInner.statusText || 'Something went wrong.'}`;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
return (
|
22
|
+
(errorInner as { message?: string })?.message ||
|
23
|
+
'Something went wrong. Please try again later.'
|
24
|
+
);
|
25
|
+
};
|
26
|
+
//Note: key added for react-router bug. Page is not reload
|
27
|
+
return (
|
28
|
+
<div key={id} className="error-container">
|
29
|
+
<div className="error-icon">
|
30
|
+
<i className="fa fa-exclamation-circle" />
|
31
|
+
</div>
|
32
|
+
<div className="error-content">
|
33
|
+
<h3>Error Occurred</h3>
|
34
|
+
<p>{getErrorMessage(error)}</p>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
);
|
35
38
|
}
|
@@ -1,12 +1,13 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
|
3
|
-
export function LoadingScreen() {
|
3
|
+
export function LoadingScreen({ id }: { id: string }) {
|
4
|
+
//Note: key added for react-router bug. Page is not reload
|
4
5
|
return (
|
5
|
-
<div className="loading-screen">
|
6
|
+
<div key={id} className="loading-screen">
|
6
7
|
<div className="loading-container">
|
7
8
|
<div className="loading-spinner"></div>
|
8
9
|
<div className="loading-text">Loading...</div>
|
9
10
|
</div>
|
10
11
|
</div>
|
11
12
|
);
|
12
|
-
}
|
13
|
+
}
|
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,11 +1,10 @@
|
|
1
|
-
import React
|
2
|
-
import { initPanel } from '../initPanel';
|
3
|
-
import { InitPanelOptions } from '../types/initPanelOptions';
|
1
|
+
import React from 'react';
|
4
2
|
import { ErrorBoundary } from './ErrorBoundary';
|
3
|
+
import { ToastContainer } from 'react-toastify';
|
5
4
|
|
6
|
-
|
5
|
+
interface AppProps {
|
7
6
|
children: React.ReactNode;
|
8
|
-
}
|
7
|
+
}
|
9
8
|
|
10
9
|
export function Panel({ children }: AppProps) {
|
11
10
|
/*useEffect(() => {
|
@@ -13,5 +12,10 @@ export function Panel({ children }: AppProps) {
|
|
13
12
|
initPanel(options);
|
14
13
|
}, [init]);*/
|
15
14
|
|
16
|
-
return
|
15
|
+
return (
|
16
|
+
<ErrorBoundary>
|
17
|
+
{children}
|
18
|
+
<ToastContainer />
|
19
|
+
</ErrorBoundary>
|
20
|
+
);
|
17
21
|
}
|
@@ -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
|
);
|