proje-react-panel 1.0.17 → 1.1.1
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/Select.d.ts +8 -0
- package/dist/components/form/SelectStyles.d.ts +7 -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 +16 -5
- package/dist/decorators/form/Input.d.ts +14 -9
- package/dist/decorators/form/getFormPageMeta.d.ts +10 -0
- package/dist/decorators/form/inputs/SelectInput.d.ts +23 -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 +13 -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 +22 -30
- package/src/components/form/FormPage.tsx +32 -0
- package/src/components/form/InnerForm.tsx +84 -0
- package/src/components/form/Label.tsx +21 -0
- package/src/components/form/Select.tsx +51 -0
- package/src/components/form/SelectStyles.ts +73 -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 +38 -12
- package/src/decorators/form/Input.ts +31 -9
- package/src/decorators/form/getFormPageMeta.ts +21 -0
- package/src/decorators/form/inputs/SelectInput.ts +19 -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 +33 -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,23 @@
|
|
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
|
+
import { Select } from './Select';
|
7
8
|
|
8
9
|
interface FormFieldProps {
|
9
|
-
input:
|
10
|
+
input: InputConfiguration;
|
10
11
|
register: UseFormRegister<any>;
|
11
12
|
error?: { message?: string };
|
12
13
|
baseName?: string;
|
14
|
+
onSelectPreloader?: (
|
15
|
+
inputOptions: InputConfiguration
|
16
|
+
) => Promise<{ label: string; value: string }[]>;
|
13
17
|
}
|
14
18
|
|
15
19
|
interface NestedFormFieldsProps {
|
16
|
-
input:
|
20
|
+
input: InputConfiguration;
|
17
21
|
register: UseFormRegister<any>;
|
18
22
|
}
|
19
23
|
|
@@ -25,7 +29,7 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
|
25
29
|
<div>
|
26
30
|
{data?.map((value: any, index: number) => (
|
27
31
|
<div key={index}>
|
28
|
-
{input.nestedFields?.map((nestedInput:
|
32
|
+
{input.nestedFields?.map((nestedInput: InputConfiguration) => (
|
29
33
|
<FormField
|
30
34
|
key={nestedInput.name?.toString() ?? ''}
|
31
35
|
baseName={input.name + '[' + index + ']'}
|
@@ -44,39 +48,25 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
|
44
48
|
);
|
45
49
|
}
|
46
50
|
|
47
|
-
export function FormField({ input, register, error, baseName }: FormFieldProps) {
|
51
|
+
export function FormField({ input, register, error, baseName, onSelectPreloader }: FormFieldProps) {
|
48
52
|
const fieldName = (baseName ? baseName.toString() + '.' : '') + input.name || '';
|
49
53
|
const renderField = () => {
|
50
54
|
switch (input.type) {
|
51
55
|
case 'textarea':
|
52
|
-
return <textarea {...register(fieldName)} placeholder={input.placeholder}
|
56
|
+
return <textarea {...register(fieldName)} placeholder={input.placeholder} />;
|
53
57
|
case 'select':
|
58
|
+
return <Select input={input} fieldName={fieldName} />;
|
59
|
+
case 'input': {
|
54
60
|
return (
|
55
|
-
<
|
56
|
-
<option value="">Select {fieldName}</option>
|
57
|
-
{input.options?.map(option => (
|
58
|
-
<option key={option.value} value={option.value}>
|
59
|
-
{option.label}
|
60
|
-
</option>
|
61
|
-
))}
|
62
|
-
</select>
|
63
|
-
);
|
64
|
-
case 'input': {
|
65
|
-
return (
|
66
|
-
<input
|
67
|
-
type={input.inputType}
|
68
|
-
{...register(fieldName)}
|
69
|
-
placeholder={input.placeholder}
|
70
|
-
id={fieldName}
|
71
|
-
/>
|
61
|
+
<input type={input.inputType} {...register(fieldName)} placeholder={input.placeholder} />
|
72
62
|
);
|
73
63
|
}
|
74
64
|
case 'file-upload':
|
75
|
-
return <
|
65
|
+
return <Uploader input={input} />;
|
76
66
|
case 'checkbox':
|
77
|
-
return <Checkbox
|
67
|
+
return <Checkbox input={input} />;
|
78
68
|
case 'hidden':
|
79
|
-
return <input type="hidden" {...register(fieldName)}
|
69
|
+
return <input type="hidden" {...register(fieldName)} />;
|
80
70
|
case 'nested':
|
81
71
|
return <NestedFormFields input={input} register={register} />;
|
82
72
|
default:
|
@@ -86,7 +76,9 @@ export function FormField({ input, register, error, baseName }: FormFieldProps)
|
|
86
76
|
|
87
77
|
return (
|
88
78
|
<div className="form-field">
|
89
|
-
|
79
|
+
{input.type !== 'checkbox' && (
|
80
|
+
<Label htmlFor={fieldName} label={input.label} fieldName={fieldName} />
|
81
|
+
)}
|
90
82
|
{renderField()}
|
91
83
|
{error && <span className="error-message">{error.message}</span>}
|
92
84
|
</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,84 @@
|
|
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
|
+
error={
|
71
|
+
input.name
|
72
|
+
? { message: (form.formState.errors[input.name as keyof T] as any)?.message }
|
73
|
+
: undefined
|
74
|
+
}
|
75
|
+
/>
|
76
|
+
))}
|
77
|
+
<button type="submit" className="submit-button">
|
78
|
+
Submit
|
79
|
+
</button>
|
80
|
+
</div>
|
81
|
+
</form>
|
82
|
+
</div>
|
83
|
+
);
|
84
|
+
}
|
@@ -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,51 @@
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
2
|
+
import { InputConfiguration } from '../../decorators/form/Input';
|
3
|
+
import { useFormContext, Controller, useWatch } from 'react-hook-form';
|
4
|
+
import { SelectInputConfiguration } from '../../decorators/form/inputs/SelectInput';
|
5
|
+
import ReactSelect, { SingleValue } from 'react-select';
|
6
|
+
import { darkSelectStyles } from './SelectStyles';
|
7
|
+
|
8
|
+
interface SelectProps {
|
9
|
+
input: InputConfiguration;
|
10
|
+
fieldName: string;
|
11
|
+
}
|
12
|
+
|
13
|
+
interface OptionType {
|
14
|
+
label: string;
|
15
|
+
value: string;
|
16
|
+
}
|
17
|
+
|
18
|
+
export function Select({ input, fieldName }: SelectProps) {
|
19
|
+
const inputSelect = input as SelectInputConfiguration;
|
20
|
+
const { control } = useFormContext();
|
21
|
+
const [options, setOptions] = useState<OptionType[]>(inputSelect.defaultOptions || []);
|
22
|
+
const [key, setKey] = useState(0);
|
23
|
+
useEffect(() => {
|
24
|
+
if (inputSelect.onSelectPreloader) {
|
25
|
+
inputSelect.onSelectPreloader().then(option => {
|
26
|
+
setOptions(option);
|
27
|
+
setKey(key + 1);
|
28
|
+
});
|
29
|
+
}
|
30
|
+
}, [input]);
|
31
|
+
|
32
|
+
return (
|
33
|
+
<Controller
|
34
|
+
name={fieldName}
|
35
|
+
control={control}
|
36
|
+
render={({ field }) => {
|
37
|
+
return (
|
38
|
+
<ReactSelect
|
39
|
+
key={key}
|
40
|
+
options={options}
|
41
|
+
styles={darkSelectStyles}
|
42
|
+
value={options.find(option => option.value === field.value) || null}
|
43
|
+
onChange={(selectedOption: OptionType | null) => {
|
44
|
+
field.onChange(selectedOption?.value);
|
45
|
+
}}
|
46
|
+
/>
|
47
|
+
);
|
48
|
+
}}
|
49
|
+
/>
|
50
|
+
);
|
51
|
+
}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import { StylesConfig } from 'react-select';
|
2
|
+
|
3
|
+
interface OptionType {
|
4
|
+
label: string;
|
5
|
+
value: string;
|
6
|
+
}
|
7
|
+
|
8
|
+
export const darkSelectStyles: StylesConfig<OptionType, false> = {
|
9
|
+
control: (baseStyles, state) => ({
|
10
|
+
...baseStyles,
|
11
|
+
backgroundColor: '#1f2937',
|
12
|
+
borderColor: state.isFocused ? '#6366f1' : '#374151',
|
13
|
+
boxShadow: state.isFocused ? '0 0 0 1px #6366f1' : 'none',
|
14
|
+
'&:hover': {
|
15
|
+
borderColor: '#6366f1',
|
16
|
+
},
|
17
|
+
borderRadius: '6px',
|
18
|
+
padding: '2px',
|
19
|
+
color: 'white',
|
20
|
+
}),
|
21
|
+
option: (baseStyles, state) => ({
|
22
|
+
...baseStyles,
|
23
|
+
backgroundColor: state.isSelected ? '#6366f1' : state.isFocused ? '#374151' : '#1f2937',
|
24
|
+
color: 'white',
|
25
|
+
'&:active': {
|
26
|
+
backgroundColor: '#6366f1',
|
27
|
+
},
|
28
|
+
'&:hover': {
|
29
|
+
backgroundColor: '#374151',
|
30
|
+
},
|
31
|
+
cursor: 'pointer',
|
32
|
+
}),
|
33
|
+
input: baseStyles => ({
|
34
|
+
...baseStyles,
|
35
|
+
color: 'white',
|
36
|
+
}),
|
37
|
+
placeholder: baseStyles => ({
|
38
|
+
...baseStyles,
|
39
|
+
color: '#9ca3af',
|
40
|
+
}),
|
41
|
+
singleValue: baseStyles => ({
|
42
|
+
...baseStyles,
|
43
|
+
color: 'white',
|
44
|
+
}),
|
45
|
+
menuPortal: baseStyles => ({
|
46
|
+
...baseStyles,
|
47
|
+
zIndex: 9999,
|
48
|
+
}),
|
49
|
+
menu: baseStyles => ({
|
50
|
+
...baseStyles,
|
51
|
+
backgroundColor: '#1f2937',
|
52
|
+
border: '1px solid #374151',
|
53
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
54
|
+
}),
|
55
|
+
menuList: baseStyles => ({
|
56
|
+
...baseStyles,
|
57
|
+
padding: '4px',
|
58
|
+
}),
|
59
|
+
dropdownIndicator: baseStyles => ({
|
60
|
+
...baseStyles,
|
61
|
+
color: '#9ca3af',
|
62
|
+
'&:hover': {
|
63
|
+
color: '#6366f1',
|
64
|
+
},
|
65
|
+
}),
|
66
|
+
clearIndicator: baseStyles => ({
|
67
|
+
...baseStyles,
|
68
|
+
color: '#9ca3af',
|
69
|
+
'&:hover': {
|
70
|
+
color: '#6366f1',
|
71
|
+
},
|
72
|
+
}),
|
73
|
+
};
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
3
|
+
import { InputConfiguration } from '../../decorators/form/Input';
|
4
|
+
|
5
|
+
interface UploaderProps {
|
6
|
+
input: InputConfiguration;
|
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
|
+
}
|