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.
- package/.vscode/launch.json +10 -0
- package/dist/components/components/FormField.d.ts +6 -1
- package/dist/components/components/InnerForm.d.ts +9 -4
- package/dist/components/components/Uploader.d.ts +8 -0
- package/dist/components/components/index.d.ts +1 -1
- package/dist/components/components/list/Datagrid.d.ts +9 -0
- package/dist/components/components/list/EmptyList.d.ts +2 -0
- package/dist/components/components/list/FilterPopup.d.ts +11 -0
- package/dist/components/components/list/ListPage.d.ts +20 -0
- package/dist/components/components/list/Pagination.d.ts +11 -0
- package/dist/components/components/list/index.d.ts +0 -0
- package/dist/components/list/Datagrid.d.ts +8 -4
- package/dist/components/list/EmptyList.d.ts +2 -0
- package/dist/components/list/FilterPopup.d.ts +10 -0
- package/dist/components/pages/FormPage.d.ts +10 -3
- package/dist/components/pages/ListPage.d.ts +2 -1
- package/dist/decorators/form/Input.d.ts +8 -3
- package/dist/decorators/list/Cell.d.ts +14 -2
- package/dist/decorators/list/List.d.ts +26 -4
- package/dist/decorators/list/ListData.d.ts +4 -4
- package/dist/decorators/list/getListFields.d.ts +2 -2
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.esm.js +1 -1
- package/dist/types/AnyClass.d.ts +1 -1
- package/dist/types/ScreenCreatorData.d.ts +5 -5
- package/package.json +9 -3
- package/src/assets/icons/svg/create.svg +9 -0
- package/src/assets/icons/svg/filter.svg +3 -0
- package/src/assets/icons/svg/pencil.svg +8 -0
- package/src/assets/icons/svg/search.svg +8 -0
- package/src/assets/icons/svg/trash.svg +8 -0
- package/src/components/components/FormField.tsx +52 -9
- package/src/components/components/InnerForm.tsx +53 -15
- package/src/components/components/Uploader.tsx +66 -0
- package/src/components/components/index.ts +2 -2
- package/src/components/components/list/Datagrid.tsx +135 -0
- package/src/components/components/list/EmptyList.tsx +26 -0
- package/src/components/components/list/FilterPopup.tsx +202 -0
- package/src/components/components/list/ListPage.tsx +176 -0
- package/src/components/pages/FormPage.tsx +12 -3
- package/src/decorators/form/Input.ts +7 -4
- package/src/decorators/form/getFormFields.ts +1 -0
- package/src/decorators/list/Cell.ts +24 -14
- package/src/decorators/list/List.ts +26 -11
- package/src/decorators/list/ListData.ts +5 -5
- package/src/decorators/list/getListFields.ts +8 -8
- package/src/index.ts +8 -3
- package/src/styles/filter-popup.scss +134 -0
- package/src/styles/form.scss +2 -4
- package/src/styles/index.scss +18 -22
- package/src/styles/list.scss +151 -8
- package/src/styles/uploader.scss +86 -0
- package/src/types/AnyClass.ts +3 -1
- package/src/types/ScreenCreatorData.ts +12 -12
- package/src/types/svg.d.ts +5 -0
- package/src/components/components/ImageUploader.tsx +0 -301
- package/src/components/list/Datagrid.tsx +0 -101
- package/src/components/pages/ListPage.tsx +0 -85
- /package/src/components/{list → components/list}/Pagination.tsx +0 -0
- /package/src/components/{list → components/list}/index.ts +0 -0
- /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 {
|
9
|
-
import { useParams } from 'react-router';
|
8
|
+
import { useParams, useNavigate } from 'react-router';
|
10
9
|
|
11
|
-
interface InnerFormProps<T
|
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
|
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
|
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
|
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
|
-
|
45
|
-
|
46
|
-
|
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 {
|
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
|
+
}
|