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.
Files changed (68) hide show
  1. package/.cursor/rules.md +21 -95
  2. package/.vscode/settings.json +6 -1
  3. package/PTD.md +124 -24
  4. package/dist/components/DetailsPage.d.ts +2 -2
  5. package/dist/components/ErrorComponent.d.ts +3 -2
  6. package/dist/components/LoadingScreen.d.ts +3 -1
  7. package/dist/components/Login.d.ts +2 -2
  8. package/dist/components/Panel.d.ts +2 -2
  9. package/dist/components/form/FormField.d.ts +1 -5
  10. package/dist/components/form/FormHeader.d.ts +10 -0
  11. package/dist/components/form/FormPage.d.ts +12 -1
  12. package/dist/components/form/Select.d.ts +1 -1
  13. package/dist/components/form/SelectStyles.d.ts +3 -3
  14. package/dist/components/list/CellField.d.ts +2 -3
  15. package/dist/components/list/EmptyList.d.ts +1 -1
  16. package/dist/decorators/details/Details.d.ts +1 -2
  17. package/dist/decorators/form/Form.d.ts +6 -4
  18. package/dist/decorators/form/Input.d.ts +10 -2
  19. package/dist/decorators/form/inputs/SelectInput.d.ts +8 -7
  20. package/dist/decorators/list/Cell.d.ts +3 -3
  21. package/dist/decorators/list/List.d.ts +3 -4
  22. package/dist/decorators/list/getListPageMeta.d.ts +2 -2
  23. package/dist/index.cjs.js +1 -1
  24. package/dist/index.esm.js +1 -1
  25. package/dist/store/store.d.ts +1 -0
  26. package/dist/utils/decerators.d.ts +3 -3
  27. package/eslint.config.mjs +60 -0
  28. package/package.json +7 -3
  29. package/src/api/CrudApi.ts +59 -53
  30. package/src/components/DetailsPage.tsx +12 -9
  31. package/src/components/ErrorComponent.tsx +36 -33
  32. package/src/components/LoadingScreen.tsx +4 -3
  33. package/src/components/Login.tsx +11 -4
  34. package/src/components/Panel.tsx +10 -6
  35. package/src/components/form/Checkbox.tsx +1 -1
  36. package/src/components/form/FormField.tsx +13 -9
  37. package/src/components/form/FormHeader.tsx +18 -0
  38. package/src/components/form/FormPage.tsx +146 -5
  39. package/src/components/form/InnerForm.tsx +24 -8
  40. package/src/components/form/Select.tsx +14 -12
  41. package/src/components/form/SelectStyles.ts +4 -4
  42. package/src/components/layout/Layout.tsx +1 -7
  43. package/src/components/layout/SideBar.tsx +1 -2
  44. package/src/components/list/CellField.tsx +2 -5
  45. package/src/components/list/Datagrid.tsx +0 -1
  46. package/src/components/list/EmptyList.tsx +2 -2
  47. package/src/components/list/FilterPopup.tsx +4 -2
  48. package/src/components/list/ListPage.tsx +11 -8
  49. package/src/components/list/cells/DefaultCell.tsx +2 -0
  50. package/src/decorators/details/Details.ts +2 -2
  51. package/src/decorators/details/DetailsItem.ts +2 -0
  52. package/src/decorators/form/Form.ts +13 -10
  53. package/src/decorators/form/Input.ts +18 -9
  54. package/src/decorators/form/inputs/SelectInput.ts +8 -7
  55. package/src/decorators/list/Cell.ts +6 -4
  56. package/src/decorators/list/ExtendedCell.ts +1 -9
  57. package/src/decorators/list/List.ts +8 -4
  58. package/src/decorators/list/cells/ImageCell.ts +1 -1
  59. package/src/decorators/list/getListPageMeta.ts +4 -3
  60. package/src/store/store.ts +3 -1
  61. package/src/styles/components/form-header.scss +75 -0
  62. package/src/styles/index.scss +1 -0
  63. package/src/types/AnyClass.ts +3 -0
  64. package/src/utils/decerators.ts +3 -2
  65. package/.eslintrc.js +0 -23
  66. package/.eslintrc.json +0 -26
  67. package/src/initPanel.ts +0 -3
  68. package/src/types/initPanelOptions.ts +0 -1
@@ -1,59 +1,65 @@
1
1
  interface FetchOptions {
2
- token: string;
3
- baseUrl: string;
2
+ token: string;
3
+ baseUrl: string;
4
4
  }
5
5
 
6
6
  export const CrudApi = {
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
- create: (options: FetchOptions, api: string, data: any) => {
19
- const headers: HeadersInit = { Authorization: `Bearer ${options.token}` };
20
- // Don't set Content-Type for FormData
21
- if (!(data instanceof FormData)) {
22
- headers["Content-Type"] = "application/json";
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
- return fetch(`${options?.baseUrl ?? ""}/${api}`, {
26
- method: "POST",
27
- headers,
28
- body: data instanceof FormData ? data : JSON.stringify(data),
29
- }).then((res) => res.json());
30
- },
31
- details: (options: FetchOptions, api: string, id: any) => {
32
- return fetch(`${options?.baseUrl ?? ""}/${api}/${id}`, {
33
- method: "GET",
34
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.token}` },
35
- }).then((res) => {
36
- return res.json();
37
- });
38
- },
39
- edit: (options: FetchOptions, api: string, data: any) => {
40
- const headers: HeadersInit = { Authorization: `Bearer ${options.token}` };
41
- // Don't set Content-Type for FormData
42
- if (!(data instanceof FormData)) {
43
- headers["Content-Type"] = "application/json";
44
- }
45
- return fetch(`${options?.baseUrl ?? ""}/${api}/${data.id}`, {
46
- method: "PUT",
47
- headers,
48
- body: data instanceof FormData ? data : JSON.stringify(data),
49
- }).then((res) => res.json());
50
- },
51
- delete: (options: FetchOptions, api: string, id: string) => {
52
- return fetch(`${options?.baseUrl ?? ""}/${api}/${id}`, {
53
- method: "DELETE",
54
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.token}` },
55
- }).then((res) => {
56
- return res.clone().json();
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: new (...args: any[]) => T;
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<any>(null);
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
- setData(data);
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?.getDetailsData]);
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 "react";
1
+ import React from 'react';
2
2
  //TODO: create, edit, details
3
- export function ErrorComponent({ error }: { error: unknown | Response }) {
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 (errorInner as { message?: string })?.message || "Something went wrong. Please try again later.";
22
- };
23
-
24
- return (
25
- <div className="error-container">
26
- <div className="error-icon">
27
- <i className="fa fa-exclamation-circle" />
28
- </div>
29
- <div className="error-content">
30
- <h3>Error Occurred</h3>
31
- <p>{getErrorMessage(error)}</p>
32
- </div>
33
- </div>
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
+ }
@@ -9,12 +9,14 @@ interface LoginFormData {
9
9
  password: string;
10
10
  }
11
11
 
12
- export type OnLogin = {
12
+ export interface OnLogin {
13
13
  login: (username: string, password: string) => Promise<LoginResponse>;
14
- };
14
+ }
15
15
 
16
16
  interface LoginResponse {
17
- user: any; // Replace with proper user type if available
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.setState({ user });
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}
@@ -1,11 +1,10 @@
1
- import React, { useEffect } from '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
- type AppProps = {
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 <ErrorBoundary>{children}</ErrorBoundary>;
15
+ return (
16
+ <ErrorBoundary>
17
+ {children}
18
+ <ToastContainer />
19
+ </ErrorBoundary>
20
+ );
17
21
  }
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import { InputConfiguration } from '../../decorators/form/Input';
3
3
  import { useFormContext } from 'react-hook-form';
4
4
  import { Label } from './Label';
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { InputConfiguration, InputOptions } from '../../decorators/form/Input';
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 any)?.message }
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, onSelectPreloader }: FormFieldProps) {
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
- export function FormPage<T extends AnyClass>({ model }: FormPageProps<T>) {
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 any);
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
  );