proje-react-panel 1.0.15 → 1.0.17-test-8

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 (62) hide show
  1. package/.vscode/launch.json +10 -0
  2. package/dist/components/components/FormField.d.ts +6 -1
  3. package/dist/components/components/InnerForm.d.ts +9 -4
  4. package/dist/components/components/Uploader.d.ts +8 -0
  5. package/dist/components/components/index.d.ts +1 -1
  6. package/dist/components/components/list/Datagrid.d.ts +9 -0
  7. package/dist/components/components/list/EmptyList.d.ts +2 -0
  8. package/dist/components/components/list/FilterPopup.d.ts +11 -0
  9. package/dist/components/components/list/ListPage.d.ts +20 -0
  10. package/dist/components/components/list/Pagination.d.ts +11 -0
  11. package/dist/components/components/list/index.d.ts +0 -0
  12. package/dist/components/list/Datagrid.d.ts +8 -4
  13. package/dist/components/list/EmptyList.d.ts +2 -0
  14. package/dist/components/list/FilterPopup.d.ts +10 -0
  15. package/dist/components/pages/FormPage.d.ts +10 -3
  16. package/dist/components/pages/ListPage.d.ts +2 -1
  17. package/dist/decorators/form/Input.d.ts +8 -3
  18. package/dist/decorators/list/Cell.d.ts +14 -2
  19. package/dist/decorators/list/List.d.ts +26 -4
  20. package/dist/decorators/list/ListData.d.ts +4 -4
  21. package/dist/decorators/list/getListFields.d.ts +2 -2
  22. package/dist/index.cjs.js +1 -1
  23. package/dist/index.d.ts +4 -3
  24. package/dist/index.esm.js +1 -1
  25. package/dist/types/AnyClass.d.ts +1 -1
  26. package/dist/types/ScreenCreatorData.d.ts +5 -5
  27. package/package.json +9 -3
  28. package/src/assets/icons/svg/create.svg +9 -0
  29. package/src/assets/icons/svg/filter.svg +3 -0
  30. package/src/assets/icons/svg/pencil.svg +8 -0
  31. package/src/assets/icons/svg/search.svg +8 -0
  32. package/src/assets/icons/svg/trash.svg +8 -0
  33. package/src/components/components/FormField.tsx +52 -9
  34. package/src/components/components/InnerForm.tsx +53 -15
  35. package/src/components/components/Uploader.tsx +66 -0
  36. package/src/components/components/index.ts +2 -2
  37. package/src/components/components/list/Datagrid.tsx +135 -0
  38. package/src/components/components/list/EmptyList.tsx +26 -0
  39. package/src/components/components/list/FilterPopup.tsx +202 -0
  40. package/src/components/components/list/ListPage.tsx +176 -0
  41. package/src/components/pages/FormPage.tsx +12 -3
  42. package/src/decorators/form/Input.ts +7 -4
  43. package/src/decorators/form/getFormFields.ts +1 -0
  44. package/src/decorators/list/Cell.ts +24 -14
  45. package/src/decorators/list/List.ts +26 -11
  46. package/src/decorators/list/ListData.ts +5 -5
  47. package/src/decorators/list/getListFields.ts +8 -8
  48. package/src/index.ts +8 -3
  49. package/src/styles/filter-popup.scss +134 -0
  50. package/src/styles/form.scss +2 -4
  51. package/src/styles/index.scss +18 -22
  52. package/src/styles/list.scss +151 -8
  53. package/src/styles/uploader.scss +86 -0
  54. package/src/types/AnyClass.ts +3 -1
  55. package/src/types/ScreenCreatorData.ts +12 -12
  56. package/src/types/svg.d.ts +5 -0
  57. package/src/components/components/ImageUploader.tsx +0 -301
  58. package/src/components/list/Datagrid.tsx +0 -101
  59. package/src/components/pages/ListPage.tsx +0 -85
  60. /package/src/components/{list → components/list}/Pagination.tsx +0 -0
  61. /package/src/components/{list → components/list}/index.ts +0 -0
  62. /package/src/styles/{_scrollbar.scss → utils/scrollbar.scss} +0 -0
@@ -1,62 +1,100 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import { FormProvider, useForm } from 'react-hook-form';
3
3
  import { InputOptions } from '../../decorators/form/Input';
4
4
  import { FormField } from './FormField';
5
5
  import { FormOptions } from '../../decorators/form/FormOptions';
6
6
  import { AnyClass } from '../../types/AnyClass';
7
7
  import { OnSubmitFN, GetDetailsDataFN } from '../pages/FormPage';
8
- import { DefaultValues } from 'react-hook-form';
9
- import { useParams } from 'react-router';
8
+ import { useParams, useNavigate } from 'react-router';
10
9
 
11
- interface InnerFormProps<T extends AnyClass> {
10
+ interface InnerFormProps<T> {
12
11
  formOptions: FormOptions;
13
12
  onSubmit: OnSubmitFN<T>;
14
- redirect?: string;
15
13
  getDetailsData?: GetDetailsDataFN<T>;
14
+ redirectBackOnSuccess?: boolean;
15
+ onSelectPreloader?: (inputOptions: InputOptions) => Promise<{ label: string; value: string }[]>;
16
+ type?: 'json' | 'formData';
16
17
  }
17
18
 
18
- export function InnerForm<T extends AnyClass>({
19
+ export function InnerForm<T>({
19
20
  formOptions,
20
21
  onSubmit,
21
- redirect,
22
22
  getDetailsData,
23
+ redirectBackOnSuccess,
24
+ onSelectPreloader,
25
+ type,
23
26
  }: InnerFormProps<T>) {
24
27
  const params = useParams();
25
- const form = useForm<T>({
28
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
29
+ //TODO: any is not a good solution, we need to find a better way to do this
30
+ const form = useForm<any>({
26
31
  resolver: formOptions.resolver,
27
32
  });
28
-
33
+ const formRef = useRef<HTMLFormElement>(null);
34
+ const navigate = useNavigate();
29
35
  const inputs = formOptions.inputs;
30
36
  useEffect(() => {
31
37
  if (getDetailsData) {
32
- getDetailsData(params.id as string).then(data => {
38
+ getDetailsData(params as Record<string, string>).then(data => {
33
39
  form.reset({ ...data });
34
- });
40
+ });
35
41
  }
36
- }, [, form.reset]);
42
+ }, [params, form.reset]);
37
43
 
38
44
  return (
39
45
  <div className="form-wrapper">
40
46
  <FormProvider {...form}>
41
47
  <form
48
+ ref={formRef}
42
49
  onSubmit={form.handleSubmit(
43
50
  async dataForm => {
44
- await onSubmit(dataForm);
45
- if (redirect) {
46
- window.location.href = redirect;
51
+ try {
52
+ console.log('dataForm', dataForm);
53
+ const data =
54
+ type === 'json'
55
+ ? dataForm
56
+ : (() => {
57
+ const formData = new FormData(formRef.current!);
58
+ for (const key in dataForm) {
59
+ if (!formData.get(key)) {
60
+ formData.append(key, dataForm[key]);
61
+ }
62
+ }
63
+ console.log('formData', formData);
64
+ return formData;
65
+ })();
66
+ console.log('data', data);
67
+ await onSubmit(data);
68
+ setErrorMessage(null);
69
+ if (redirectBackOnSuccess) {
70
+ navigate(-1);
71
+ }
72
+ } catch (error: any) {
73
+ const message =
74
+ error?.response?.data?.message ||
75
+ (error instanceof Error ? error.message : 'An error occurred');
76
+ setErrorMessage(message);
77
+ console.error(error);
47
78
  }
48
79
  },
49
80
  (errors, event) => {
81
+ //TOOD: put error if useer choose global error
50
82
  console.log('error creating creation', errors, event);
51
83
  }
52
84
  )}
53
85
  >
54
86
  <div>
87
+ {errorMessage && (
88
+ <div className="error-message" style={{ color: 'red', marginBottom: '1rem' }}>
89
+ {errorMessage}
90
+ </div>
91
+ )}
55
92
  {inputs?.map((input: InputOptions) => (
56
93
  <FormField
57
94
  key={input.name || ''}
58
95
  input={input}
59
96
  register={form.register}
97
+ onSelectPreloader={onSelectPreloader}
60
98
  error={
61
99
  input.name
62
100
  ? { message: (form.formState.errors[input.name as keyof T] as any)?.message }
@@ -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
+ }
@@ -2,7 +2,7 @@ export { InnerForm } from './InnerForm';
2
2
  export { FormField } from './FormField';
3
3
  export { LoadingScreen } from './LoadingScreen';
4
4
  export { Counter } from './Counter';
5
- export { ImageUploader } from './ImageUploader';
5
+ export { Uploader } from './Uploader';
6
6
  export { ErrorComponent } from './ErrorComponent';
7
7
  export { Label } from './Label';
8
- export { ErrorBoundary } from './ErrorBoundary';
8
+ export { ErrorBoundary } from './ErrorBoundary';
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import { CellOptions } from '../../../decorators/list/Cell';
3
+ import { Link } from 'react-router';
4
+ import { useAppStore } from '../../../store/store';
5
+ import { ImageCellOptions } from '../../../decorators/list/ImageCell';
6
+ import { ListData } from '../../../decorators/list/ListData';
7
+ import { EmptyList } from './EmptyList';
8
+ import SearchIcon from '../../../assets/icons/svg/search.svg';
9
+ import PencilIcon from '../../../assets/icons/svg/pencil.svg';
10
+ import TrashIcon from '../../../assets/icons/svg/trash.svg';
11
+
12
+ interface DatagridProps<T> {
13
+ data: T[];
14
+ listData: ListData<T>;
15
+ onRemoveItem?: (item: T) => Promise<void>;
16
+ }
17
+
18
+ export function Datagrid<T>({ data, listData, onRemoveItem }: DatagridProps<T>) {
19
+ const cells = listData.cells;
20
+ const listGeneralCells = data?.[0]
21
+ ? typeof listData.list?.cells === 'function'
22
+ ? listData.list?.cells?.(data[0])
23
+ : listData.list?.cells
24
+ : null;
25
+
26
+ return (
27
+ <div className="datagrid">
28
+ {!data || data.length === 0 ? (
29
+ <EmptyList />
30
+ ) : (
31
+ <table className="datagrid-table">
32
+ <thead>
33
+ <tr>
34
+ {cells.map(cellOptions => (
35
+ <th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
36
+ ))}
37
+ {listGeneralCells?.details && <th>Details</th>}
38
+ {listGeneralCells?.edit && <th>Edit</th>}
39
+ {listGeneralCells?.delete && <th>Delete</th>}
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ {data.map((item, index) => {
44
+ const listCells = item
45
+ ? typeof listData.list?.cells === 'function'
46
+ ? listData.list?.cells?.(item)
47
+ : listData.list?.cells
48
+ : null;
49
+ return (
50
+ <tr key={index}>
51
+ {cells.map(cellOptions => {
52
+ // @ts-ignore
53
+ const value = item[cellOptions.name];
54
+ let render = value ?? '-'; // Default value if the field is undefined or null
55
+
56
+ switch (cellOptions.type) {
57
+ case 'date':
58
+ if (value) {
59
+ const date = new Date(value);
60
+ render = `${date.getDate().toString().padStart(2, '0')}/${(
61
+ date.getMonth() + 1
62
+ )
63
+ .toString()
64
+ .padStart(
65
+ 2,
66
+ '0'
67
+ )}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date
68
+ .getMinutes()
69
+ .toString()
70
+ .padStart(2, '0')}`;
71
+ }
72
+ break;
73
+
74
+ case 'image': {
75
+ const imageCellOptions = cellOptions as ImageCellOptions;
76
+ render = (
77
+ <img
78
+ width={100}
79
+ height={100}
80
+ src={imageCellOptions.baseUrl + value}
81
+ style={{ objectFit: 'contain' }}
82
+ />
83
+ );
84
+ break;
85
+ }
86
+ case 'string':
87
+ default:
88
+ render = value ? value.toString() : (cellOptions?.placeHolder ?? '-'); // Handles string type or default fallback
89
+ break;
90
+ }
91
+ /*
92
+ if (cellOptions.linkTo) {
93
+ render = <Link to={cellOptions.linkTo(item)}>{formattedValue}</Link>;
94
+ }
95
+ */
96
+ return <td key={cellOptions.name}>{render}</td>;
97
+ })}
98
+ {listCells?.details && (
99
+ <td>
100
+ <Link to={listCells.details.path} className="util-cell-link">
101
+ <SearchIcon className="icon icon-search" />
102
+ <span className="util-cell-label">{listCells.details.label}</span>
103
+ </Link>
104
+ </td>
105
+ )}
106
+ {listCells?.edit && (
107
+ <td>
108
+ <Link to={listCells.edit.path} className="util-cell-link">
109
+ <PencilIcon className="icon icon-pencil" />
110
+ <span className="util-cell-label">{listCells.edit.label}</span>
111
+ </Link>
112
+ </td>
113
+ )}
114
+ {listCells?.delete && (
115
+ <td>
116
+ <a
117
+ onClick={() => {
118
+ onRemoveItem?.(item);
119
+ }}
120
+ className="util-cell-link util-cell-link-remove"
121
+ >
122
+ <TrashIcon className="icon icon-trash" />
123
+ <span className="util-cell-label">{listCells.delete.label}</span>
124
+ </a>
125
+ </td>
126
+ )}
127
+ </tr>
128
+ );
129
+ })}
130
+ </tbody>
131
+ </table>
132
+ )}
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ export const EmptyList: React.FC = () => {
4
+ return (
5
+ <div className="empty-list">
6
+ <div className="empty-list-content">
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ width="64"
10
+ height="64"
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeWidth="2"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ >
18
+ <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
19
+ <polyline points="13 2 13 9 20 9" />
20
+ </svg>
21
+ <h3>No Data Found</h3>
22
+ <p>There are no items to display at the moment.</p>
23
+ </div>
24
+ </div>
25
+ );
26
+ };
@@ -0,0 +1,202 @@
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { ListData } from '../../../decorators/list/ListData';
3
+ import { CellOptions, StaticSelectFilter } from '../../../decorators/list/Cell';
4
+ import Select from 'react-select';
5
+
6
+ interface FilterPopupProps<T> {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ onApplyFilters: (filters: Record<string, string>) => void;
10
+ listData: ListData<T>;
11
+ activeFilters?: Record<string, string>;
12
+ }
13
+
14
+ interface FilterFieldProps {
15
+ field: CellOptions;
16
+ value: string;
17
+ onChange: (value: string) => void;
18
+ }
19
+
20
+ function FilterField({ field, value, onChange }: FilterFieldProps): React.ReactElement {
21
+ switch (field.filter?.type) {
22
+ case 'static-select': {
23
+ const filter = field.filter as StaticSelectFilter;
24
+ return (
25
+ <Select
26
+ id={field.name}
27
+ menuPortalTarget={document.body}
28
+ styles={{
29
+ control: (baseStyles, state) => ({
30
+ ...baseStyles,
31
+ backgroundColor: '#1f2937',
32
+ borderColor: state.isFocused ? '#6366f1' : '#374151',
33
+ boxShadow: state.isFocused ? '0 0 0 1px #6366f1' : 'none',
34
+ '&:hover': {
35
+ borderColor: '#6366f1',
36
+ },
37
+ borderRadius: '6px',
38
+ padding: '2px',
39
+ color: 'white',
40
+ }),
41
+ option: (baseStyles, state) => ({
42
+ ...baseStyles,
43
+ backgroundColor: state.isSelected
44
+ ? '#6366f1'
45
+ : state.isFocused
46
+ ? '#374151'
47
+ : '#1f2937',
48
+ color: 'white',
49
+ '&:active': {
50
+ backgroundColor: '#6366f1',
51
+ },
52
+ '&:hover': {
53
+ backgroundColor: '#374151',
54
+ },
55
+ cursor: 'pointer',
56
+ }),
57
+ input: baseStyles => ({
58
+ ...baseStyles,
59
+ color: 'white',
60
+ }),
61
+ placeholder: baseStyles => ({
62
+ ...baseStyles,
63
+ color: '#9ca3af',
64
+ }),
65
+ singleValue: baseStyles => ({
66
+ ...baseStyles,
67
+ color: 'white',
68
+ }),
69
+ menuPortal: baseStyles => ({
70
+ ...baseStyles,
71
+ zIndex: 9999,
72
+ }),
73
+ menu: baseStyles => ({
74
+ ...baseStyles,
75
+ backgroundColor: '#1f2937',
76
+ border: '1px solid #374151',
77
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
78
+ }),
79
+ menuList: baseStyles => ({
80
+ ...baseStyles,
81
+ padding: '4px',
82
+ }),
83
+ dropdownIndicator: baseStyles => ({
84
+ ...baseStyles,
85
+ color: '#9ca3af',
86
+ '&:hover': {
87
+ color: '#6366f1',
88
+ },
89
+ }),
90
+ clearIndicator: baseStyles => ({
91
+ ...baseStyles,
92
+ color: '#9ca3af',
93
+ '&:hover': {
94
+ color: '#6366f1',
95
+ },
96
+ }),
97
+ }}
98
+ value={
99
+ value
100
+ ? {
101
+ value: value,
102
+ label: filter.options.find(opt => opt.value === value)?.label || value,
103
+ }
104
+ : null
105
+ }
106
+ onChange={option => onChange(option?.value || '')}
107
+ options={filter.options.map(opt => ({
108
+ value: opt.value,
109
+ label: opt.label,
110
+ }))}
111
+ placeholder={`Filter by ${field.title || field.name}`}
112
+ isClearable
113
+ />
114
+ );
115
+ }
116
+ default:
117
+ return (
118
+ <input
119
+ type={field.type === 'number' ? 'number' : 'text'}
120
+ id={field.name}
121
+ value={value || ''}
122
+ onChange={e => onChange(e.target.value)}
123
+ placeholder={`Filter by ${field.title || field.name}`}
124
+ />
125
+ );
126
+ }
127
+ }
128
+
129
+ export function FilterPopup<T>({
130
+ isOpen,
131
+ onClose,
132
+ onApplyFilters,
133
+ listData,
134
+ activeFilters,
135
+ }: FilterPopupProps<T>): React.ReactElement | null {
136
+ const [filters, setFilters] = React.useState<Record<string, any>>(activeFilters ?? {});
137
+ const popupRef = useRef<HTMLDivElement>(null);
138
+ const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
139
+
140
+ useEffect(() => {
141
+ const handleClickOutside = (event: MouseEvent) => {
142
+ if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
143
+ onClose();
144
+ }
145
+ };
146
+
147
+ if (isOpen) {
148
+ document.addEventListener('mousedown', handleClickOutside);
149
+ }
150
+
151
+ return () => {
152
+ document.removeEventListener('mousedown', handleClickOutside);
153
+ };
154
+ }, [isOpen, onClose]);
155
+
156
+ if (!isOpen) return null;
157
+
158
+ const handleFilterChange = (fieldName: string, value: any) => {
159
+ setFilters(prev => ({
160
+ ...prev,
161
+ [fieldName]: value,
162
+ }));
163
+ };
164
+
165
+ const handleApply = () => {
166
+ onApplyFilters(filters);
167
+ onClose();
168
+ };
169
+
170
+ return (
171
+ <div className="filter-popup-overlay">
172
+ <div ref={popupRef} className="filter-popup">
173
+ <div className="filter-popup-header">
174
+ <h3>Filter</h3>
175
+ <button onClick={onClose} className="close-button">
176
+ ×
177
+ </button>
178
+ </div>
179
+ <div className="filter-popup-content">
180
+ {fields.map((field: CellOptions) => (
181
+ <div key={field.name} className="filter-field">
182
+ <label htmlFor={field.name}>{field.title || field.name}</label>
183
+ <FilterField
184
+ field={field}
185
+ value={filters[field.name || '']}
186
+ onChange={value => handleFilterChange(field.name || '', value)}
187
+ />
188
+ </div>
189
+ ))}
190
+ </div>
191
+ <div className="filter-popup-footer">
192
+ <button onClick={onClose} className="cancel-button">
193
+ Cancel
194
+ </button>
195
+ <button onClick={handleApply} className="apply-button">
196
+ Apply Filters
197
+ </button>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ );
202
+ }