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.
Files changed (64) 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/Login.d.ts +2 -2
  6. package/dist/components/Panel.d.ts +2 -2
  7. package/dist/components/form/FormField.d.ts +1 -5
  8. package/dist/components/form/FormHeader.d.ts +10 -0
  9. package/dist/components/form/FormPage.d.ts +12 -1
  10. package/dist/components/form/Select.d.ts +1 -1
  11. package/dist/components/form/SelectStyles.d.ts +3 -3
  12. package/dist/components/list/CellField.d.ts +2 -3
  13. package/dist/components/list/EmptyList.d.ts +1 -1
  14. package/dist/decorators/details/Details.d.ts +1 -2
  15. package/dist/decorators/form/Form.d.ts +3 -5
  16. package/dist/decorators/form/Input.d.ts +10 -2
  17. package/dist/decorators/form/inputs/SelectInput.d.ts +8 -7
  18. package/dist/decorators/list/Cell.d.ts +3 -3
  19. package/dist/decorators/list/List.d.ts +3 -4
  20. package/dist/decorators/list/getListPageMeta.d.ts +2 -2
  21. package/dist/index.cjs.js +1 -1
  22. package/dist/index.esm.js +1 -1
  23. package/dist/store/store.d.ts +1 -0
  24. package/dist/utils/decerators.d.ts +3 -3
  25. package/eslint.config.mjs +60 -0
  26. package/package.json +6 -3
  27. package/src/api/CrudApi.ts +59 -53
  28. package/src/components/DetailsPage.tsx +8 -6
  29. package/src/components/Login.tsx +11 -4
  30. package/src/components/Panel.tsx +3 -3
  31. package/src/components/form/Checkbox.tsx +1 -1
  32. package/src/components/form/FormField.tsx +13 -9
  33. package/src/components/form/FormHeader.tsx +18 -0
  34. package/src/components/form/FormPage.tsx +146 -5
  35. package/src/components/form/InnerForm.tsx +13 -8
  36. package/src/components/form/Select.tsx +14 -15
  37. package/src/components/form/SelectStyles.ts +4 -4
  38. package/src/components/layout/Layout.tsx +1 -7
  39. package/src/components/layout/SideBar.tsx +1 -2
  40. package/src/components/list/CellField.tsx +2 -5
  41. package/src/components/list/Datagrid.tsx +0 -1
  42. package/src/components/list/EmptyList.tsx +2 -2
  43. package/src/components/list/FilterPopup.tsx +4 -2
  44. package/src/components/list/ListPage.tsx +7 -5
  45. package/src/components/list/cells/DefaultCell.tsx +2 -0
  46. package/src/decorators/details/Details.ts +2 -2
  47. package/src/decorators/details/DetailsItem.ts +2 -0
  48. package/src/decorators/form/Form.ts +10 -11
  49. package/src/decorators/form/Input.ts +19 -10
  50. package/src/decorators/form/inputs/SelectInput.ts +8 -7
  51. package/src/decorators/list/Cell.ts +6 -4
  52. package/src/decorators/list/ExtendedCell.ts +1 -9
  53. package/src/decorators/list/List.ts +8 -4
  54. package/src/decorators/list/cells/ImageCell.ts +1 -1
  55. package/src/decorators/list/getListPageMeta.ts +4 -3
  56. package/src/store/store.ts +3 -1
  57. package/src/styles/components/form-header.scss +75 -0
  58. package/src/styles/index.scss +1 -0
  59. package/src/types/AnyClass.ts +3 -0
  60. package/src/utils/decerators.ts +3 -2
  61. package/.eslintrc.js +0 -23
  62. package/.eslintrc.json +0 -26
  63. package/src/initPanel.ts +0 -3
  64. 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.4",
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
  },
@@ -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,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: new (...args: any[]) => T;
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<any>(null);
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
- 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);
25
27
  })
26
28
  .catch(setError)
27
29
  .finally(() => setLoading(false));
28
- }, [params, detailsClass?.getDetailsData]);
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>
@@ -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,10 +1,10 @@
1
- import React, { useEffect } from 'react';
1
+ import React from 'react';
2
2
  import { ErrorBoundary } from './ErrorBoundary';
3
3
  import { ToastContainer } from 'react-toastify';
4
4
 
5
- type AppProps = {
5
+ interface AppProps {
6
6
  children: React.ReactNode;
7
- };
7
+ }
8
8
 
9
9
  export function Panel({ children }: AppProps) {
10
10
  /*useEffect(() => {
@@ -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
  );
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { FormProvider, useForm, useFormContext, UseFormReturn } from 'react-hook-form';
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
- if (formClass.redirectBackOnSuccess) {
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: any) {
52
+ } catch (error: unknown) {
53
+ const errorResponse = error as { response?: { data?: { message?: string } } };
55
54
  const message =
56
- error?.response?.data?.message ||
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
- ? { message: (form.formState.errors[input.name as keyof T] as any)?.message }
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
  />