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.
- package/dist/components/Counter.d.ts +9 -0
- package/dist/components/DetailsPage.d.ts +7 -0
- package/dist/components/ErrorBoundary.d.ts +16 -0
- package/dist/components/ErrorComponent.d.ts +4 -0
- package/dist/components/LoadingScreen.d.ts +2 -0
- package/dist/components/Login.d.ts +13 -0
- package/dist/components/Panel.d.ts +1 -3
- package/dist/components/components/Checkbox.d.ts +3 -2
- package/dist/components/components/FormField.d.ts +5 -1
- package/dist/components/components/InnerForm.d.ts +8 -3
- package/dist/components/components/Label.d.ts +3 -2
- package/dist/components/components/Uploader.d.ts +8 -0
- package/dist/components/components/index.d.ts +1 -1
- package/dist/components/components/list/ListPage.d.ts +1 -1
- package/dist/components/form/Checkbox.d.ts +7 -0
- package/dist/components/form/FormField.d.ts +17 -0
- package/dist/components/form/FormPage.d.ts +6 -0
- package/dist/components/form/InnerForm.d.ts +10 -0
- package/dist/components/form/Label.d.ts +9 -0
- package/dist/components/form/Uploader.d.ts +8 -0
- package/dist/components/layout/Layout.d.ts +3 -4
- package/dist/components/layout/SideBar.d.ts +2 -3
- package/dist/components/list/CellField.d.ts +9 -0
- package/dist/components/list/Datagrid.d.ts +6 -8
- package/dist/components/list/FilterPopup.d.ts +7 -5
- package/dist/components/list/ListHeader.d.ts +11 -0
- package/dist/components/list/ListPage.d.ts +6 -0
- package/dist/components/pages/FormPage.d.ts +8 -2
- package/dist/decorators/details/Details.d.ts +11 -0
- package/dist/decorators/details/DetailsItem.d.ts +11 -0
- package/dist/decorators/details/getDetailsPageMeta.d.ts +8 -0
- package/dist/decorators/form/Form.d.ts +21 -5
- package/dist/decorators/form/Input.d.ts +7 -3
- package/dist/decorators/form/getFormPageMeta.d.ts +10 -0
- package/dist/decorators/list/Cell.d.ts +13 -1
- package/dist/decorators/list/List.d.ts +18 -1
- package/dist/decorators/list/cells/ImageCell.d.ts +9 -0
- package/dist/decorators/list/getListPageMeta.d.ts +8 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +12 -17
- package/dist/index.esm.js +1 -1
- package/dist/initPanel.d.ts +1 -1
- package/dist/store/store.d.ts +0 -3
- package/dist/types/AnyClass.d.ts +2 -1
- package/dist/types/getDetailsData.d.ts +1 -0
- package/dist/types/initPanelOptions.d.ts +0 -1
- package/package.json +1 -1
- package/src/assets/icons/svg/check.svg +4 -0
- package/src/assets/icons/svg/cross.svg +4 -0
- package/src/components/DetailsPage.tsx +55 -0
- package/src/components/{components/ErrorComponent.tsx → ErrorComponent.tsx} +1 -1
- package/src/components/{pages/Login.tsx → Login.tsx} +2 -2
- package/src/components/Panel.tsx +4 -5
- package/src/components/form/Checkbox.tsx +21 -0
- package/src/components/{components → form}/FormField.tsx +30 -22
- package/src/components/form/FormPage.tsx +32 -0
- package/src/components/form/InnerForm.tsx +85 -0
- package/src/components/form/Label.tsx +21 -0
- package/src/components/form/Uploader.tsx +66 -0
- package/src/components/layout/Layout.tsx +29 -32
- package/src/components/layout/SideBar.tsx +4 -13
- package/src/components/list/CellField.tsx +63 -0
- package/src/components/list/Datagrid.tsx +106 -0
- package/src/components/{components/list → list}/FilterPopup.tsx +13 -9
- package/src/components/list/ListHeader.tsx +47 -0
- package/src/components/{components/list → list}/ListPage.tsx +20 -82
- package/src/decorators/details/Details.ts +31 -0
- package/src/decorators/details/DetailsItem.ts +40 -0
- package/src/decorators/details/getDetailsPageMeta.ts +15 -0
- package/src/decorators/form/Form.ts +37 -12
- package/src/decorators/form/Input.ts +11 -4
- package/src/decorators/form/getFormPageMeta.ts +21 -0
- package/src/decorators/list/Cell.ts +41 -1
- package/src/decorators/list/List.ts +30 -6
- package/src/decorators/list/cells/ImageCell.ts +17 -0
- package/src/decorators/list/getListPageMeta.ts +16 -0
- package/src/index.ts +32 -24
- package/src/initPanel.ts +1 -4
- package/src/store/store.ts +0 -5
- package/src/styles/components/checkbox.scss +42 -0
- package/src/styles/components/uploader.scss +86 -0
- package/src/styles/details.scss +62 -0
- package/src/styles/form.scss +9 -11
- package/src/styles/index.scss +26 -12
- package/src/styles/list.scss +3 -1
- package/src/types/AnyClass.ts +2 -1
- package/src/types/initPanelOptions.ts +1 -3
- package/src/components/components/Checkbox.tsx +0 -9
- package/src/components/components/ImageUploader.tsx +0 -301
- package/src/components/components/InnerForm.tsx +0 -74
- package/src/components/components/Label.tsx +0 -15
- package/src/components/components/index.ts +0 -8
- package/src/components/components/list/Datagrid.tsx +0 -127
- package/src/components/pages/ControllerDetails.tsx +0 -37
- package/src/components/pages/FormPage.tsx +0 -34
- package/src/decorators/Crud.ts +0 -20
- package/src/decorators/form/FormOptions.ts +0 -8
- package/src/decorators/form/getFormFields.ts +0 -13
- package/src/decorators/list/GetCellFields.ts +0 -13
- package/src/decorators/list/ImageCell.ts +0 -13
- package/src/decorators/list/ListData.ts +0 -7
- package/src/decorators/list/getListFields.ts +0 -10
- package/src/styles/image-uploader.scss +0 -94
- package/src/types/Screen.ts +0 -4
- package/src/types/ScreenCreatorData.ts +0 -14
- package/src/utils/createScreens.ts +0 -5
- package/src/utils/getFields.ts +0 -22
- /package/src/components/{components/Counter.tsx → Counter.tsx} +0 -0
- /package/src/components/{components/ErrorBoundary.tsx → ErrorBoundary.tsx} +0 -0
- /package/src/components/{components/LoadingScreen.tsx → LoadingScreen.tsx} +0 -0
- /package/src/components/{components/list → list}/EmptyList.tsx +0 -0
- /package/src/components/{components/list → list}/Pagination.tsx +0 -0
- /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 {
|
4
|
+
import { Uploader } from './Uploader';
|
6
5
|
import { Checkbox } from './Checkbox';
|
6
|
+
import { Label } from './Label';
|
7
7
|
|
8
8
|
interface FormFieldProps {
|
9
|
-
input:
|
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:
|
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:
|
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}
|
63
|
+
return <textarea {...register(fieldName)} placeholder={input.placeholder} />;
|
53
64
|
case 'select':
|
54
65
|
return (
|
55
|
-
<select {...register(fieldName)}
|
66
|
+
<select {...register(fieldName)}>
|
56
67
|
<option value="">Select {fieldName}</option>
|
57
|
-
{
|
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 <
|
81
|
+
return <Uploader input={input} />;
|
76
82
|
case 'checkbox':
|
77
|
-
return <Checkbox
|
83
|
+
return <Checkbox input={input} />;
|
78
84
|
case 'hidden':
|
79
|
-
return <input type="hidden" {...register(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
|
-
|
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
|
2
|
-
import { SideBar } from
|
3
|
-
import {
|
4
|
-
import {
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
children,
|
8
|
+
menu,
|
9
|
+
getIcons,
|
10
|
+
logout,
|
12
11
|
}: {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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?.(
|
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 {
|
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
|
-
|
11
|
+
listPageMeta: ListPageMeta<T>;
|
11
12
|
activeFilters?: Record<string, string>;
|
12
13
|
}
|
13
14
|
|
14
15
|
interface FilterFieldProps {
|
15
|
-
field:
|
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
|
-
|
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(
|
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:
|
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
|