proje-react-panel 1.0.17 → 1.1.0

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 (113) hide show
  1. package/dist/components/Counter.d.ts +9 -0
  2. package/dist/components/DetailsPage.d.ts +7 -0
  3. package/dist/components/ErrorBoundary.d.ts +16 -0
  4. package/dist/components/ErrorComponent.d.ts +4 -0
  5. package/dist/components/LoadingScreen.d.ts +2 -0
  6. package/dist/components/Login.d.ts +13 -0
  7. package/dist/components/Panel.d.ts +1 -3
  8. package/dist/components/components/Checkbox.d.ts +3 -2
  9. package/dist/components/components/FormField.d.ts +5 -1
  10. package/dist/components/components/InnerForm.d.ts +8 -3
  11. package/dist/components/components/Label.d.ts +3 -2
  12. package/dist/components/components/Uploader.d.ts +8 -0
  13. package/dist/components/components/index.d.ts +1 -1
  14. package/dist/components/components/list/ListPage.d.ts +1 -1
  15. package/dist/components/form/Checkbox.d.ts +7 -0
  16. package/dist/components/form/FormField.d.ts +17 -0
  17. package/dist/components/form/FormPage.d.ts +6 -0
  18. package/dist/components/form/InnerForm.d.ts +10 -0
  19. package/dist/components/form/Label.d.ts +9 -0
  20. package/dist/components/form/Uploader.d.ts +8 -0
  21. package/dist/components/layout/Layout.d.ts +3 -4
  22. package/dist/components/layout/SideBar.d.ts +2 -3
  23. package/dist/components/list/CellField.d.ts +9 -0
  24. package/dist/components/list/Datagrid.d.ts +6 -8
  25. package/dist/components/list/FilterPopup.d.ts +7 -5
  26. package/dist/components/list/ListHeader.d.ts +11 -0
  27. package/dist/components/list/ListPage.d.ts +6 -0
  28. package/dist/components/pages/FormPage.d.ts +8 -2
  29. package/dist/decorators/details/Details.d.ts +11 -0
  30. package/dist/decorators/details/DetailsItem.d.ts +11 -0
  31. package/dist/decorators/details/getDetailsPageMeta.d.ts +8 -0
  32. package/dist/decorators/form/Form.d.ts +21 -5
  33. package/dist/decorators/form/Input.d.ts +7 -3
  34. package/dist/decorators/form/getFormPageMeta.d.ts +10 -0
  35. package/dist/decorators/list/Cell.d.ts +13 -1
  36. package/dist/decorators/list/List.d.ts +18 -1
  37. package/dist/decorators/list/cells/ImageCell.d.ts +9 -0
  38. package/dist/decorators/list/getListPageMeta.d.ts +8 -0
  39. package/dist/index.cjs.js +1 -1
  40. package/dist/index.d.ts +12 -17
  41. package/dist/index.esm.js +1 -1
  42. package/dist/initPanel.d.ts +1 -1
  43. package/dist/store/store.d.ts +0 -3
  44. package/dist/types/AnyClass.d.ts +2 -1
  45. package/dist/types/getDetailsData.d.ts +1 -0
  46. package/dist/types/initPanelOptions.d.ts +0 -1
  47. package/package.json +1 -1
  48. package/src/assets/icons/svg/check.svg +4 -0
  49. package/src/assets/icons/svg/cross.svg +4 -0
  50. package/src/components/DetailsPage.tsx +55 -0
  51. package/src/components/{components/ErrorComponent.tsx → ErrorComponent.tsx} +1 -1
  52. package/src/components/{pages/Login.tsx → Login.tsx} +2 -2
  53. package/src/components/Panel.tsx +4 -5
  54. package/src/components/form/Checkbox.tsx +21 -0
  55. package/src/components/{components → form}/FormField.tsx +30 -22
  56. package/src/components/form/FormPage.tsx +32 -0
  57. package/src/components/form/InnerForm.tsx +85 -0
  58. package/src/components/form/Label.tsx +21 -0
  59. package/src/components/form/Uploader.tsx +66 -0
  60. package/src/components/layout/Layout.tsx +29 -32
  61. package/src/components/layout/SideBar.tsx +4 -13
  62. package/src/components/list/CellField.tsx +63 -0
  63. package/src/components/list/Datagrid.tsx +106 -0
  64. package/src/components/{components/list → list}/FilterPopup.tsx +13 -9
  65. package/src/components/list/ListHeader.tsx +47 -0
  66. package/src/components/{components/list → list}/ListPage.tsx +20 -82
  67. package/src/decorators/details/Details.ts +31 -0
  68. package/src/decorators/details/DetailsItem.ts +40 -0
  69. package/src/decorators/details/getDetailsPageMeta.ts +15 -0
  70. package/src/decorators/form/Form.ts +37 -12
  71. package/src/decorators/form/Input.ts +11 -4
  72. package/src/decorators/form/getFormPageMeta.ts +21 -0
  73. package/src/decorators/list/Cell.ts +41 -1
  74. package/src/decorators/list/List.ts +30 -6
  75. package/src/decorators/list/cells/ImageCell.ts +17 -0
  76. package/src/decorators/list/getListPageMeta.ts +16 -0
  77. package/src/index.ts +32 -24
  78. package/src/initPanel.ts +1 -4
  79. package/src/store/store.ts +0 -5
  80. package/src/styles/components/checkbox.scss +42 -0
  81. package/src/styles/components/uploader.scss +86 -0
  82. package/src/styles/details.scss +62 -0
  83. package/src/styles/form.scss +9 -11
  84. package/src/styles/index.scss +26 -12
  85. package/src/styles/list.scss +3 -1
  86. package/src/types/AnyClass.ts +2 -1
  87. package/src/types/initPanelOptions.ts +1 -3
  88. package/src/components/components/Checkbox.tsx +0 -9
  89. package/src/components/components/ImageUploader.tsx +0 -301
  90. package/src/components/components/InnerForm.tsx +0 -74
  91. package/src/components/components/Label.tsx +0 -15
  92. package/src/components/components/index.ts +0 -8
  93. package/src/components/components/list/Datagrid.tsx +0 -127
  94. package/src/components/pages/ControllerDetails.tsx +0 -37
  95. package/src/components/pages/FormPage.tsx +0 -34
  96. package/src/decorators/Crud.ts +0 -20
  97. package/src/decorators/form/FormOptions.ts +0 -8
  98. package/src/decorators/form/getFormFields.ts +0 -13
  99. package/src/decorators/list/GetCellFields.ts +0 -13
  100. package/src/decorators/list/ImageCell.ts +0 -13
  101. package/src/decorators/list/ListData.ts +0 -7
  102. package/src/decorators/list/getListFields.ts +0 -10
  103. package/src/styles/image-uploader.scss +0 -94
  104. package/src/types/Screen.ts +0 -4
  105. package/src/types/ScreenCreatorData.ts +0 -14
  106. package/src/utils/createScreens.ts +0 -5
  107. package/src/utils/getFields.ts +0 -22
  108. /package/src/components/{components/Counter.tsx → Counter.tsx} +0 -0
  109. /package/src/components/{components/ErrorBoundary.tsx → ErrorBoundary.tsx} +0 -0
  110. /package/src/components/{components/LoadingScreen.tsx → LoadingScreen.tsx} +0 -0
  111. /package/src/components/{components/list → list}/EmptyList.tsx +0 -0
  112. /package/src/components/{components/list → list}/Pagination.tsx +0 -0
  113. /package/src/components/{components/list → list}/index.ts +0 -0
@@ -1,19 +1,22 @@
1
- import React from 'react';
2
- import { InputOptions } from '../../decorators/form/Input';
3
- import { Label } from './Label';
1
+ import React, { useEffect, useState } from 'react';
2
+ import { InputConfiguration, InputOptions } from '../../decorators/form/Input';
4
3
  import { useFormContext, UseFormRegister } from 'react-hook-form';
5
- import { ImageUploader } from './ImageUploader';
4
+ import { Uploader } from './Uploader';
6
5
  import { Checkbox } from './Checkbox';
6
+ import { Label } from './Label';
7
7
 
8
8
  interface FormFieldProps {
9
- input: InputOptions;
9
+ input: InputConfiguration;
10
10
  register: UseFormRegister<any>;
11
11
  error?: { message?: string };
12
12
  baseName?: string;
13
+ onSelectPreloader?: (
14
+ inputOptions: InputConfiguration
15
+ ) => Promise<{ label: string; value: string }[]>;
13
16
  }
14
17
 
15
18
  interface NestedFormFieldsProps {
16
- input: InputOptions;
19
+ input: InputConfiguration;
17
20
  register: UseFormRegister<any>;
18
21
  }
19
22
 
@@ -25,7 +28,7 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
25
28
  <div>
26
29
  {data?.map((value: any, index: number) => (
27
30
  <div key={index}>
28
- {input.nestedFields?.map((nestedInput: InputOptions) => (
31
+ {input.nestedFields?.map((nestedInput: InputConfiguration) => (
29
32
  <FormField
30
33
  key={nestedInput.name?.toString() ?? ''}
31
34
  baseName={input.name + '[' + index + ']'}
@@ -44,39 +47,42 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
44
47
  );
45
48
  }
46
49
 
47
- export function FormField({ input, register, error, baseName }: FormFieldProps) {
50
+ export function FormField({ input, register, error, baseName, onSelectPreloader }: FormFieldProps) {
48
51
  const fieldName = (baseName ? baseName.toString() + '.' : '') + input.name || '';
52
+ const [options, setOptions] = useState<{ label: string; value: string }[]>(input.options || []);
53
+ useEffect(() => {
54
+ if (input.optionsPreload && onSelectPreloader) {
55
+ onSelectPreloader(input).then(option => {
56
+ setOptions(option);
57
+ });
58
+ }
59
+ }, [input]);
49
60
  const renderField = () => {
50
61
  switch (input.type) {
51
62
  case 'textarea':
52
- return <textarea {...register(fieldName)} placeholder={input.placeholder} id={fieldName} />;
63
+ return <textarea {...register(fieldName)} placeholder={input.placeholder} />;
53
64
  case 'select':
54
65
  return (
55
- <select {...register(fieldName)} id={fieldName}>
66
+ <select {...register(fieldName)}>
56
67
  <option value="">Select {fieldName}</option>
57
- {input.options?.map(option => (
68
+ {options?.map(option => (
58
69
  <option key={option.value} value={option.value}>
59
70
  {option.label}
60
71
  </option>
61
72
  ))}
62
73
  </select>
63
74
  );
64
- case 'input': {
75
+ case 'input': {
65
76
  return (
66
- <input
67
- type={input.inputType}
68
- {...register(fieldName)}
69
- placeholder={input.placeholder}
70
- id={fieldName}
71
- />
77
+ <input type={input.inputType} {...register(fieldName)} placeholder={input.placeholder} />
72
78
  );
73
79
  }
74
80
  case 'file-upload':
75
- return <ImageUploader />;
81
+ return <Uploader input={input} />;
76
82
  case 'checkbox':
77
- return <Checkbox {...register(fieldName)} id={fieldName} />;
83
+ return <Checkbox input={input} />;
78
84
  case 'hidden':
79
- return <input type="hidden" {...register(fieldName)} id={fieldName} />;
85
+ return <input type="hidden" {...register(fieldName)} />;
80
86
  case 'nested':
81
87
  return <NestedFormFields input={input} register={register} />;
82
88
  default:
@@ -86,7 +92,9 @@ export function FormField({ input, register, error, baseName }: FormFieldProps)
86
92
 
87
93
  return (
88
94
  <div className="form-field">
89
- <Label htmlFor={fieldName} label={input.label} fieldName={fieldName} />
95
+ {input.type !== 'checkbox' && (
96
+ <Label htmlFor={fieldName} label={input.label} fieldName={fieldName} />
97
+ )}
90
98
  {renderField()}
91
99
  {error && <span className="error-message">{error.message}</span>}
92
100
  </div>
@@ -0,0 +1,32 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { InnerForm } from './InnerForm';
3
+ import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
4
+ import { useParams } from 'react-router';
5
+ import { FormProvider, Resolver, useForm } from 'react-hook-form';
6
+ import { getFormPageMeta } from '../../decorators/form/getFormPageMeta';
7
+
8
+ export interface FormPageProps<T extends AnyClass> {
9
+ model: AnyClassConstructor<T>;
10
+ }
11
+
12
+ export function FormPage<T extends AnyClass>({ model }: FormPageProps<T>) {
13
+ const { class: formClass, inputs, resolver } = useMemo(() => getFormPageMeta(model), [model]);
14
+ const form = useForm<T>({
15
+ resolver: resolver as Resolver<T>,
16
+ });
17
+
18
+ const params = useParams();
19
+ useEffect(() => {
20
+ if (formClass.getDetailsData) {
21
+ formClass.getDetailsData(params as Record<string, string>).then(data => {
22
+ form.reset(data as any);
23
+ });
24
+ }
25
+ }, [params, form.reset, formClass.getDetailsData]);
26
+
27
+ return (
28
+ <FormProvider {...form}>
29
+ <InnerForm inputs={inputs} formClass={formClass} />
30
+ </FormProvider>
31
+ );
32
+ }
@@ -0,0 +1,85 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { FormProvider, useForm, useFormContext, UseFormReturn } from 'react-hook-form';
3
+ import { InputConfiguration } from '../../decorators/form/Input';
4
+ import { FormField } from './FormField';
5
+ import { useNavigate } from 'react-router';
6
+ import { FormConfiguration } from '../../decorators/form/Form';
7
+ import { AnyClass } from '../../types/AnyClass';
8
+
9
+ interface InnerFormProps<T extends AnyClass> {
10
+ inputs: InputConfiguration[];
11
+ formClass: FormConfiguration<T>;
12
+ }
13
+
14
+ export function InnerForm<T extends AnyClass>({ inputs, formClass }: InnerFormProps<T>) {
15
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
16
+ //TODO: any is not a good solution, we need to find a better way to do this
17
+ const formRef = useRef<HTMLFormElement>(null);
18
+ const navigate = useNavigate();
19
+ const form = useFormContext<T>();
20
+
21
+ return (
22
+ <div className="form-wrapper">
23
+ <form
24
+ ref={formRef}
25
+ onSubmit={form.handleSubmit(
26
+ async (dataForm: T) => {
27
+ try {
28
+ const data =
29
+ formClass.type === 'json'
30
+ ? dataForm
31
+ : (() => {
32
+ const formData = new FormData(formRef.current!);
33
+ for (const key in dataForm) {
34
+ if (!formData.get(key)) {
35
+ formData.append(key, dataForm[key]);
36
+ }
37
+ }
38
+ return formData;
39
+ })();
40
+ await formClass.onSubmit(data);
41
+ setErrorMessage(null);
42
+ if (formClass.redirectBackOnSuccess) {
43
+ navigate(-1);
44
+ }
45
+ } catch (error: any) {
46
+ const message =
47
+ error?.response?.data?.message ||
48
+ (error instanceof Error ? error.message : 'An error occurred');
49
+ setErrorMessage(message);
50
+ console.error(error);
51
+ }
52
+ },
53
+ (errors, event) => {
54
+ //TOOD: put error if useer choose global error
55
+ console.log('error creating creation', errors, event);
56
+ }
57
+ )}
58
+ >
59
+ <div>
60
+ {errorMessage && (
61
+ <div className="error-message" style={{ color: 'red', marginBottom: '1rem' }}>
62
+ {errorMessage}
63
+ </div>
64
+ )}
65
+ {inputs?.map((input: InputConfiguration) => (
66
+ <FormField
67
+ key={input.name || ''}
68
+ input={input}
69
+ register={form.register}
70
+ onSelectPreloader={formClass.onSelectPreloader}
71
+ error={
72
+ input.name
73
+ ? { message: (form.formState.errors[input.name as keyof T] as any)?.message }
74
+ : undefined
75
+ }
76
+ />
77
+ ))}
78
+ <button type="submit" className="submit-button">
79
+ Submit
80
+ </button>
81
+ </div>
82
+ </form>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+
3
+ interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
4
+ htmlFor: string;
5
+ label?: string;
6
+ fieldName: string;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export function Label({ label, fieldName, children, ...props }: LabelProps) {
11
+ return (
12
+ <label {...props} className={'label ' + props.className}>
13
+ {
14
+ <span>
15
+ {label ? label + ':' : fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ':'}
16
+ </span>
17
+ }
18
+ {children}
19
+ </label>
20
+ );
21
+ }
@@ -0,0 +1,66 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useForm, Controller, useFormContext } from 'react-hook-form';
3
+ import { InputOptions } from '../../decorators/form/Input';
4
+
5
+ interface UploaderProps {
6
+ input: InputOptions;
7
+ maxLength?: number;
8
+ }
9
+
10
+ export function Uploader({ input, maxLength = 1 }: UploaderProps) {
11
+ const form = useFormContext();
12
+ const [files, setFiles] = useState<File[]>([]);
13
+ const id = input.name!;
14
+
15
+ useEffect(() => {
16
+ // Update form value whenever files change
17
+ form.setValue(input.name + '_files', files.length > 0);
18
+ }, [files, form, input.name]);
19
+
20
+ const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
21
+ if (maxLength > 1) {
22
+ throw new Error('TODO: Multiple file upload is not implemented yet');
23
+ }
24
+
25
+ const fileList = e.target.files;
26
+ if (fileList) {
27
+ const filesArray = Array.from(fileList);
28
+ setFiles(prevFiles => [...prevFiles, ...filesArray]);
29
+ }
30
+ };
31
+
32
+ const removeFile = (index: number) => {
33
+ setFiles(prevFiles => prevFiles.filter((_, i) => i !== index));
34
+ };
35
+
36
+ return (
37
+ <div className="uploader-container">
38
+ <button
39
+ type="button"
40
+ className="uploader-button"
41
+ onClick={() => document.getElementById(id)?.click()}
42
+ >
43
+ Upload Files
44
+ </button>
45
+ <input id={id} hidden name={input.name} onChange={onChange} type="file" multiple />
46
+ {files.length > 0 && (
47
+ <div className="uploader-files">
48
+ {files.map((file, index) => (
49
+ <div key={`${file.name}-${index}`} className="uploader-file">
50
+ <p>{file.name}</p>
51
+ <p>{(file.size / 1024 / 1024).toFixed(2)} MB</p>
52
+ <p>{file.type || 'Unknown type'}</p>
53
+ <button
54
+ onClick={() => removeFile(index)}
55
+ className="remove-file-button"
56
+ type="button"
57
+ >
58
+ Remove
59
+ </button>
60
+ </div>
61
+ ))}
62
+ </div>
63
+ )}
64
+ </div>
65
+ );
66
+ }
@@ -1,38 +1,35 @@
1
- import React from "react";
2
- import { SideBar } from "./SideBar";
3
- import { ScreenCreatorData } from "../../types/ScreenCreatorData";
4
- import { useAppStore } from "../../store/store";
5
- import { useNavigate } from "react-router";
1
+ import React from 'react';
2
+ import { SideBar } from './SideBar';
3
+ import { useAppStore } from '../../store/store';
4
+ import { useNavigate } from 'react-router';
6
5
 
7
6
  export function Layout<IconType>({
8
- children,
9
- menu,
10
- getIcons,
11
- logout,
7
+ children,
8
+ menu,
9
+ getIcons,
10
+ logout,
12
11
  }: {
13
- children?: React.ReactNode;
14
- menu?: (screens: Record<string, ScreenCreatorData>) => { name: string; path: string; iconType: IconType }[];
15
- getIcons?: (iconType: IconType) => React.ReactNode;
16
- logout?: () => void;
12
+ children?: React.ReactNode;
13
+ menu?: () => { name: string; path: string; iconType: IconType }[];
14
+ getIcons?: (iconType: IconType) => React.ReactNode;
15
+ logout?: (type: 'redirect' | 'logout') => void;
17
16
  }) {
18
- const { user, screenPaths } = useAppStore((s) => ({
19
- user: s.user,
20
- screenPaths: s.screenPaths,
21
- }));
22
- const data = useAppStore();
23
- const navigate = useNavigate();
24
- if (!user) {
25
- navigate(screenPaths.login);
26
- }
17
+ const { user } = useAppStore(s => ({
18
+ user: s.user,
19
+ }));
20
+ const navigate = useNavigate();
21
+ if (!user) {
22
+ logout?.('redirect');
23
+ }
27
24
 
28
- return (
29
- <div className="layout">
30
- <SideBar onLogout={() => {
31
- if (logout) {
32
- logout();
33
- }
34
- }} menu={menu} getIcons={getIcons} />
35
- <main className="content">{children}</main>
36
- </div>
37
- );
25
+ return (
26
+ <div className="layout">
27
+ <SideBar
28
+ onLogout={logout}
29
+ menu={menu}
30
+ getIcons={getIcons}
31
+ />
32
+ <main className="content">{children}</main>
33
+ </div>
34
+ );
38
35
  }
@@ -1,11 +1,7 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Link, useLocation, useNavigate } from 'react-router';
3
- import { ScreenCreatorData } from '../../types/ScreenCreatorData';
4
- import { useAppStore } from '../../store/store';
5
3
 
6
- type GetMenuFunction<IconType> = (
7
- screens: Record<string, ScreenCreatorData>
8
- ) => { name: string; path: string; iconType: IconType }[];
4
+ type GetMenuFunction<IconType> = () => { name: string; path: string; iconType: IconType }[];
9
5
 
10
6
  type GetIconsFunction<IconType> = (iconType: IconType) => React.ReactNode;
11
7
 
@@ -16,12 +12,8 @@ export function SideBar<IconType>({
16
12
  }: {
17
13
  menu?: GetMenuFunction<IconType>;
18
14
  getIcons?: GetIconsFunction<IconType>;
19
- onLogout?: () => void;
15
+ onLogout?: (type: 'redirect' | 'logout') => void;
20
16
  }) {
21
- const { screens, screenPaths } = useAppStore(s => ({
22
- screens: s.screens ?? {},
23
- screenPaths: s.screenPaths ?? {},
24
- }));
25
17
  const [isOpen, setIsOpen] = useState(true);
26
18
  const location = useLocation();
27
19
  const navigate = useNavigate();
@@ -54,7 +46,7 @@ export function SideBar<IconType>({
54
46
  {isOpen ? '<' : '>'}
55
47
  </button>
56
48
  <nav className="nav-links">
57
- {menu?.(screens).map((item, index) => (
49
+ {menu?.().map((item, index) => (
58
50
  <Link
59
51
  key={index}
60
52
  to={item.path}
@@ -72,8 +64,7 @@ export function SideBar<IconType>({
72
64
  className="logout-button"
73
65
  onClick={() => {
74
66
  if (onLogout) {
75
- onLogout();
76
- navigate(screenPaths.login);
67
+ onLogout('logout');
77
68
  }
78
69
  }}
79
70
  aria-label="Logout"
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { ImageCellOptions } from '../../decorators/list/cells/ImageCell';
3
+ import { AnyClass } from '../../types/AnyClass';
4
+ import { ExtendedCellTypes } from '../../decorators/list/Cell';
5
+ import CheckIcon from '../../assets/icons/svg/check.svg';
6
+ import CrossIcon from '../../assets/icons/svg/cross.svg';
7
+
8
+ interface CellFieldProps<T extends AnyClass> {
9
+ cellOptions: any; // TODO: Create proper type for cellOptions
10
+ item: T;
11
+ value: any;
12
+ }
13
+
14
+ export function CellField<T extends AnyClass>({ cellOptions, item, value }: CellFieldProps<T>) {
15
+ let render = value ?? '-'; // Default value if the field is undefined or null
16
+
17
+ switch (cellOptions.type as ExtendedCellTypes) {
18
+ case 'boolean': {
19
+ render = value ? <CheckIcon className="icon icon-true" /> : <CrossIcon className="icon icon-false" />;
20
+ break;
21
+ }
22
+ case 'date':
23
+ if (value) {
24
+ const date = new Date(value);
25
+ render = `${date.getDate().toString().padStart(2, '0')}/${(date.getMonth() + 1)
26
+ .toString()
27
+ .padStart(2, '0')}/${date.getFullYear()} ${date
28
+ .getHours()
29
+ .toString()
30
+ .padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
31
+ }
32
+ break;
33
+
34
+ case 'image': {
35
+ const imageCellOptions = cellOptions as ImageCellOptions;
36
+ render = (
37
+ <img
38
+ width={100}
39
+ height={100}
40
+ src={imageCellOptions.baseUrl + value}
41
+ style={{ objectFit: 'contain' }}
42
+ />
43
+ );
44
+ break;
45
+ }
46
+ case 'uuid':
47
+ if (value && typeof value === 'string' && value.length >= 6) {
48
+ render = `${value.slice(0, 3)}...${value.slice(-3)}`;
49
+ }
50
+ break;
51
+ default:
52
+ render = value ? value.toString() : (cellOptions?.placeHolder ?? '-');
53
+ break;
54
+ }
55
+
56
+ /*
57
+ if (cellOptions.linkTo) {
58
+ render = <Link to={cellOptions.linkTo(item)}>{formattedValue}</Link>;
59
+ }
60
+ */
61
+
62
+ return <td key={cellOptions.name}>{render}</td>;
63
+ }
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router';
3
+ import { EmptyList } from './EmptyList';
4
+ import SearchIcon from '../../assets/icons/svg/search.svg';
5
+ import PencilIcon from '../../assets/icons/svg/pencil.svg';
6
+ import TrashIcon from '../../assets/icons/svg/trash.svg';
7
+ import { ListPageMeta } from '../../decorators/list/getListPageMeta';
8
+ import { ImageCellOptions } from '../../decorators/list/cells/ImageCell';
9
+ import { AnyClass } from '../../types/AnyClass';
10
+ import { CellField } from './CellField';
11
+
12
+ interface DatagridProps<T extends AnyClass> {
13
+ data: T[];
14
+ listPageMeta: ListPageMeta<T>;
15
+ onRemoveItem?: (item: T) => Promise<void>;
16
+ }
17
+
18
+ export function Datagrid<T extends AnyClass>({
19
+ data,
20
+ listPageMeta,
21
+ onRemoveItem,
22
+ }: DatagridProps<T>) {
23
+ const cells = listPageMeta.cells;
24
+ const listGeneralCells = data?.[0]
25
+ ? typeof listPageMeta.class.cells === 'function'
26
+ ? listPageMeta.class.cells?.(data[0])
27
+ : listPageMeta.class.cells
28
+ : null;
29
+
30
+ return (
31
+ <div className="datagrid">
32
+ {!data || data.length === 0 ? (
33
+ <EmptyList />
34
+ ) : (
35
+ <table className="datagrid-table">
36
+ <thead>
37
+ <tr>
38
+ {cells.map(cellOptions => (
39
+ <th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
40
+ ))}
41
+ {listGeneralCells?.details && <th>Details</th>}
42
+ {listGeneralCells?.edit && <th>Edit</th>}
43
+ {listGeneralCells?.delete && <th>Delete</th>}
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ {data.map((item, index) => {
48
+ const listCells = item
49
+ ? typeof listPageMeta.class.cells === 'function'
50
+ ? listPageMeta.class.cells?.(item)
51
+ : listPageMeta.class.cells
52
+ : null;
53
+ return (
54
+ <tr key={index}>
55
+ {cells.map(cellOptions => {
56
+ // @ts-ignore
57
+ const value = item[cellOptions.name];
58
+ return (
59
+ <CellField
60
+ key={cellOptions.name}
61
+ cellOptions={cellOptions}
62
+ item={item}
63
+ value={value}
64
+ />
65
+ );
66
+ })}
67
+ {listCells?.details && (
68
+ <td>
69
+ <Link to={listCells.details.path} className="util-cell-link">
70
+ <SearchIcon className="icon icon-search" />
71
+ <span className="util-cell-label">{listCells.details.label}</span>
72
+ </Link>
73
+ </td>
74
+ )}
75
+ {listCells?.edit && (
76
+ <td>
77
+ <Link to={listCells.edit.path} className="util-cell-link">
78
+ <PencilIcon className="icon icon-pencil" />
79
+ <span className="util-cell-label">{listCells.edit.label}</span>
80
+ </Link>
81
+ </td>
82
+ )}
83
+ {listCells?.delete && (
84
+ <td>
85
+ <a
86
+ onClick={() => {
87
+ listCells.delete?.onRemoveItem?.(item).then(() => {
88
+ onRemoveItem?.(item);
89
+ });
90
+ }}
91
+ className="util-cell-link util-cell-link-remove"
92
+ >
93
+ <TrashIcon className="icon icon-trash" />
94
+ <span className="util-cell-label">{listCells.delete.label}</span>
95
+ </a>
96
+ </td>
97
+ )}
98
+ </tr>
99
+ );
100
+ })}
101
+ </tbody>
102
+ </table>
103
+ )}
104
+ </div>
105
+ );
106
+ }
@@ -1,18 +1,19 @@
1
1
  import React, { useEffect, useMemo, useRef } from 'react';
2
- import { ListData } from '../../../decorators/list/ListData';
3
- import { CellOptions, StaticSelectFilter } from '../../../decorators/list/Cell';
2
+ import { CellConfiguration, StaticSelectFilter } from '../../decorators/list/Cell';
4
3
  import Select from 'react-select';
4
+ import { ListPageMeta } from '../../decorators/list/getListPageMeta';
5
+ import { AnyClass } from '../../types/AnyClass';
5
6
 
6
- interface FilterPopupProps<T> {
7
+ interface FilterPopupProps<T extends AnyClass> {
7
8
  isOpen: boolean;
8
9
  onClose: () => void;
9
10
  onApplyFilters: (filters: Record<string, string>) => void;
10
- listData: ListData<T>;
11
+ listPageMeta: ListPageMeta<T>;
11
12
  activeFilters?: Record<string, string>;
12
13
  }
13
14
 
14
15
  interface FilterFieldProps {
15
- field: CellOptions;
16
+ field: CellConfiguration;
16
17
  value: string;
17
18
  onChange: (value: string) => void;
18
19
  }
@@ -126,16 +127,19 @@ function FilterField({ field, value, onChange }: FilterFieldProps): React.ReactE
126
127
  }
127
128
  }
128
129
 
129
- export function FilterPopup<T>({
130
+ export function FilterPopup<T extends AnyClass>({
130
131
  isOpen,
131
132
  onClose,
132
133
  onApplyFilters,
133
- listData,
134
+ listPageMeta,
134
135
  activeFilters,
135
136
  }: FilterPopupProps<T>): React.ReactElement | null {
136
137
  const [filters, setFilters] = React.useState<Record<string, any>>(activeFilters ?? {});
137
138
  const popupRef = useRef<HTMLDivElement>(null);
138
- const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
139
+ const fields = useMemo(
140
+ () => listPageMeta.cells.filter(cell => !!cell.filter),
141
+ [listPageMeta.cells]
142
+ );
139
143
 
140
144
  useEffect(() => {
141
145
  const handleClickOutside = (event: MouseEvent) => {
@@ -177,7 +181,7 @@ export function FilterPopup<T>({
177
181
  </button>
178
182
  </div>
179
183
  <div className="filter-popup-content">
180
- {fields.map((field: CellOptions) => (
184
+ {fields.map((field: CellConfiguration) => (
181
185
  <div key={field.name} className="filter-field">
182
186
  <label htmlFor={field.name}>{field.title || field.name}</label>
183
187
  <FilterField