proje-react-panel 1.2.0 → 1.3.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/__tests__/utils/PreloadCacheHelper.test.d.ts +1 -0
- package/dist/components/Login.d.ts +3 -12
- package/dist/components/Panel.d.ts +4 -1
- package/dist/components/form/Checkbox.d.ts +2 -1
- package/dist/components/form/FormPage.d.ts +2 -1
- package/dist/components/form/Uploader.d.ts +2 -1
- package/dist/decorators/form/Form.d.ts +9 -7
- package/dist/decorators/form/Input.d.ts +1 -1
- package/dist/decorators/form/inputs/SelectInput.d.ts +6 -8
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +1 -1
- package/dist/store/store.d.ts +2 -3
- package/dist/types/User.d.ts +3 -0
- package/dist/utils/PreloadCacheHelper.d.ts +15 -0
- package/dist/utils/login.d.ts +2 -0
- package/dist/utils/logout.d.ts +1 -0
- package/package.json +1 -1
- package/src/__tests__/utils/PreloadCacheHelper.test.ts +9 -20
- package/src/components/Login.tsx +4 -83
- package/src/components/Panel.tsx +13 -6
- package/src/components/form/Checkbox.tsx +6 -5
- package/src/components/form/FormField.tsx +16 -32
- package/src/components/form/FormPage.tsx +24 -5
- package/src/components/form/InnerForm.tsx +73 -72
- package/src/components/form/Select.tsx +4 -3
- package/src/components/form/Uploader.tsx +5 -4
- package/src/decorators/form/Form.ts +13 -10
- package/src/decorators/form/Input.ts +7 -3
- package/src/index.ts +2 -2
- package/src/store/store.ts +3 -4
- package/src/styles/form.scss +17 -6
- package/src/styles/login.scss +24 -93
- package/src/types/User.ts +3 -0
- package/src/utils/PreloadCacheHelper.ts +5 -12
- package/src/utils/login.ts +8 -0
- package/src/utils/logout.ts +7 -0
@@ -1,13 +1,10 @@
|
|
1
|
-
import {
|
1
|
+
import { OnResult, preloadCacheHelper } from '../../utils/PreloadCacheHelper';
|
2
2
|
import { SelectPreloader } from '../../decorators/form/inputs/SelectInput';
|
3
3
|
import { describe, expect, it, beforeEach, jest } from '@jest/globals';
|
4
4
|
|
5
5
|
interface CacheMap {
|
6
6
|
cache: Map<SelectPreloader<unknown>, { label: string; value: unknown }[]>;
|
7
|
-
asyncQueue: Map<
|
8
|
-
SelectPreloader<unknown>,
|
9
|
-
((result: { label: string; value: unknown }[]) => Promise<void>)[]
|
10
|
-
>;
|
7
|
+
asyncQueue: Map<SelectPreloader<unknown>, OnResult<unknown>[]>;
|
11
8
|
}
|
12
9
|
|
13
10
|
describe('PreloadCacheHelper', () => {
|
@@ -20,33 +17,28 @@ describe('PreloadCacheHelper', () => {
|
|
20
17
|
// Generated by AI
|
21
18
|
it('should cache and return results for the same key', async () => {
|
22
19
|
const mockSelectPreloaderKey = jest.fn() as SelectPreloader<number>;
|
23
|
-
const mockOnPreload = jest
|
24
|
-
.fn<OnPreload<number>>()
|
25
|
-
.mockResolvedValue([{ label: 'Test', value: 1 }]);
|
26
20
|
const mockOnResult = jest.fn<OnResult<number>>().mockImplementation(() => {
|
27
21
|
return Promise.resolve();
|
28
22
|
});
|
29
23
|
|
30
|
-
await preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult
|
31
|
-
await preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult
|
24
|
+
await preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult);
|
25
|
+
await preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult);
|
32
26
|
|
33
|
-
expect(mockSelectPreloaderKey).toHaveBeenCalledTimes(
|
34
|
-
expect(mockOnPreload).toHaveBeenCalledTimes(1);
|
27
|
+
expect(mockSelectPreloaderKey).toHaveBeenCalledTimes(1);
|
35
28
|
expect(mockOnResult).toHaveBeenCalledTimes(2);
|
36
29
|
});
|
37
30
|
|
38
31
|
// Generated by AI
|
39
32
|
it('should handle preload errors gracefully', async () => {
|
40
|
-
const mockSelectPreloaderKey = jest
|
41
|
-
|
42
|
-
.fn<OnPreload<number>>()
|
33
|
+
const mockSelectPreloaderKey = jest
|
34
|
+
.fn<SelectPreloader<number>>()
|
43
35
|
.mockRejectedValue(new Error('Preload failed'));
|
44
36
|
const mockOnResult = jest
|
45
37
|
.fn<OnResult<number>>()
|
46
38
|
.mockImplementation(async () => Promise.resolve());
|
47
39
|
|
48
40
|
await expect(
|
49
|
-
preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult
|
41
|
+
preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult)
|
50
42
|
).rejects.toThrow('Preload failed');
|
51
43
|
|
52
44
|
expect(mockOnResult).not.toHaveBeenCalled();
|
@@ -55,14 +47,11 @@ describe('PreloadCacheHelper', () => {
|
|
55
47
|
// Generated by AI
|
56
48
|
it('should clear async queue after successful preload', async () => {
|
57
49
|
const mockSelectPreloaderKey = jest.fn() as SelectPreloader<number>;
|
58
|
-
const mockOnPreload = jest
|
59
|
-
.fn<OnPreload<number>>()
|
60
|
-
.mockResolvedValue([{ label: 'Test', value: 1 }]);
|
61
50
|
const mockOnResult = jest
|
62
51
|
.fn<OnResult<number>>()
|
63
52
|
.mockImplementation(async () => Promise.resolve());
|
64
53
|
|
65
|
-
await preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult
|
54
|
+
await preloadCacheHelper.setOrGetCache(mockSelectPreloaderKey, mockOnResult);
|
66
55
|
|
67
56
|
expect((preloadCacheHelper as unknown as CacheMap).asyncQueue.has(mockSelectPreloaderKey)).toBe(
|
68
57
|
false
|
package/src/components/Login.tsx
CHANGED
@@ -1,86 +1,7 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
import { useNavigate } from 'react-router';
|
5
|
-
import { useAppStore } from '../store/store';
|
2
|
+
import { FormPage, FormPageProps } from './form/FormPage';
|
3
|
+
import { AnyClass } from '../types/AnyClass';
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
password: string;
|
10
|
-
}
|
11
|
-
|
12
|
-
export interface OnLogin {
|
13
|
-
login: (username: string, password: string) => Promise<LoginResponse>;
|
14
|
-
}
|
15
|
-
|
16
|
-
interface LoginResponse {
|
17
|
-
//TODO: any is not a good solution, we need to find a better way to do this
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
19
|
-
user: any;
|
20
|
-
token: string;
|
21
|
-
}
|
22
|
-
|
23
|
-
interface LoginProps {
|
24
|
-
onLogin: OnLogin;
|
25
|
-
}
|
26
|
-
|
27
|
-
export function Login({ onLogin }: LoginProps) {
|
28
|
-
const {
|
29
|
-
register,
|
30
|
-
handleSubmit,
|
31
|
-
formState: { errors },
|
32
|
-
} = useForm<LoginFormData>();
|
33
|
-
const navigate = useNavigate();
|
34
|
-
const onSubmit = async (data: LoginFormData) => {
|
35
|
-
onLogin.login(data.username, data.password).then((dataInner: LoginResponse) => {
|
36
|
-
const { user, token } = dataInner;
|
37
|
-
localStorage.setItem('token', token);
|
38
|
-
useAppStore.getState().login(user);
|
39
|
-
navigate('/');
|
40
|
-
});
|
41
|
-
};
|
42
|
-
|
43
|
-
return (
|
44
|
-
<div className="login-container">
|
45
|
-
<div className="login-panel">
|
46
|
-
<div className="login-header">
|
47
|
-
<h1>Welcome Back</h1>
|
48
|
-
<p>Please sign in to continue</p>
|
49
|
-
</div>
|
50
|
-
<form onSubmit={handleSubmit(onSubmit)} className="login-form">
|
51
|
-
<FormField
|
52
|
-
input={{
|
53
|
-
name: 'username',
|
54
|
-
label: 'Username',
|
55
|
-
placeholder: 'Enter your username',
|
56
|
-
type: 'input',
|
57
|
-
inputType: 'text',
|
58
|
-
includeInCSV: true,
|
59
|
-
includeInJSON: true,
|
60
|
-
}}
|
61
|
-
register={register}
|
62
|
-
error={errors.username}
|
63
|
-
/>
|
64
|
-
<FormField
|
65
|
-
input={{
|
66
|
-
name: 'password',
|
67
|
-
label: 'Password',
|
68
|
-
inputType: 'password',
|
69
|
-
placeholder: 'Enter your password',
|
70
|
-
type: 'input',
|
71
|
-
includeInCSV: true,
|
72
|
-
includeInJSON: true,
|
73
|
-
}}
|
74
|
-
register={register}
|
75
|
-
error={errors.password}
|
76
|
-
/>
|
77
|
-
<div className="form-actions">
|
78
|
-
<button type="submit" className="submit-button">
|
79
|
-
Sign In
|
80
|
-
</button>
|
81
|
-
</div>
|
82
|
-
</form>
|
83
|
-
</div>
|
84
|
-
</div>
|
85
|
-
);
|
5
|
+
export function Login<T extends AnyClass>(props: FormPageProps<T>) {
|
6
|
+
return <FormPage className="login-form" {...props} />;
|
86
7
|
}
|
package/src/components/Panel.tsx
CHANGED
@@ -1,16 +1,23 @@
|
|
1
|
-
import React from 'react';
|
1
|
+
import React, { useEffect } from 'react';
|
2
2
|
import { ErrorBoundary } from './ErrorBoundary';
|
3
3
|
import { ToastContainer } from 'react-toastify';
|
4
4
|
|
5
5
|
interface AppProps {
|
6
|
+
onInit?: (appData: { token?: string }) => void;
|
6
7
|
children: React.ReactNode;
|
7
8
|
}
|
8
9
|
|
9
|
-
export function Panel({ children }: AppProps) {
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
export function Panel({ onInit, children }: AppProps) {
|
11
|
+
useEffect(() => {
|
12
|
+
if (onInit) {
|
13
|
+
const token = localStorage.getItem('token');
|
14
|
+
const params: { token?: string } = { token: '' };
|
15
|
+
if (token) {
|
16
|
+
params.token = token;
|
17
|
+
}
|
18
|
+
onInit(params);
|
19
|
+
}
|
20
|
+
}, [onInit]);
|
14
21
|
|
15
22
|
return (
|
16
23
|
<ErrorBoundary>
|
@@ -5,22 +5,23 @@ import { Label } from './Label';
|
|
5
5
|
|
6
6
|
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
7
7
|
input: InputConfiguration;
|
8
|
+
fieldName: string;
|
8
9
|
}
|
9
10
|
|
10
11
|
//TODO2:
|
11
|
-
export function Checkbox({ input, ...props }: CheckboxProps) {
|
12
|
-
const { label
|
12
|
+
export function Checkbox({ input, fieldName, ...props }: CheckboxProps) {
|
13
|
+
const { label } = input;
|
13
14
|
const form = useFormContext();
|
14
15
|
const { register } = form;
|
15
16
|
|
16
17
|
return (
|
17
|
-
<Label className="checkbox-label" htmlFor={
|
18
|
+
<Label className="checkbox-label" htmlFor={fieldName} label={label} fieldName={fieldName}>
|
18
19
|
<input
|
19
20
|
type="checkbox"
|
20
|
-
id={
|
21
|
+
id={fieldName}
|
21
22
|
className="apple-switch"
|
22
23
|
{...props}
|
23
|
-
{...register(
|
24
|
+
{...register(fieldName, {
|
24
25
|
setValueAs: (value: string) => value === 'on',
|
25
26
|
})}
|
26
27
|
/>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React from 'react';
|
1
|
+
import React, { useMemo } from 'react';
|
2
2
|
import { InputConfiguration } from '../../decorators/form/Input';
|
3
3
|
import { useFormContext, UseFormRegister } from 'react-hook-form';
|
4
4
|
import { Uploader } from './Uploader';
|
@@ -20,13 +20,13 @@ interface NestedFormFieldsProps {
|
|
20
20
|
//TODO: any is not a good solution, we need to find a better way to do this
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
22
22
|
register: UseFormRegister<any>;
|
23
|
+
fieldName: string;
|
23
24
|
}
|
24
25
|
|
25
|
-
function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
26
|
+
function NestedFormFields({ input, register, fieldName }: NestedFormFieldsProps) {
|
26
27
|
const form = useFormContext();
|
27
28
|
//TODO: inputOptions İnputResult seperate
|
28
|
-
const data = form.getValues(
|
29
|
-
console.log('--_>', data, input, input.nestedFields);
|
29
|
+
const data = form.getValues(fieldName);
|
30
30
|
return (
|
31
31
|
<div>
|
32
32
|
{/* TODO: any is not a good solution, we need to find a better way to do this */}
|
@@ -36,12 +36,12 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
|
36
36
|
{input.nestedFields?.map((nestedInput: InputConfiguration) => (
|
37
37
|
<FormField
|
38
38
|
key={nestedInput.name?.toString() ?? ''}
|
39
|
-
baseName={
|
39
|
+
baseName={fieldName + '[' + index + ']'}
|
40
40
|
input={nestedInput}
|
41
41
|
register={register}
|
42
42
|
error={
|
43
43
|
input.name
|
44
|
-
? { message: (form.formState.errors[
|
44
|
+
? { message: (form.formState.errors[fieldName] as { message: string })?.message }
|
45
45
|
: undefined
|
46
46
|
}
|
47
47
|
/>
|
@@ -54,53 +54,37 @@ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
|
|
54
54
|
|
55
55
|
export function FormField({ input, register, error, baseName }: FormFieldProps) {
|
56
56
|
const fieldName: string = (baseName ? baseName.toString() + '.' : '') + input.name || '';
|
57
|
-
|
58
|
-
const renderField = () => {
|
57
|
+
const renderedField = useMemo(() => {
|
59
58
|
switch (input.type) {
|
60
59
|
case 'textarea':
|
61
|
-
return (
|
62
|
-
<textarea
|
63
|
-
defaultValue={input.defaultValue}
|
64
|
-
{...register(fieldName, {
|
65
|
-
value: input.defaultValue,
|
66
|
-
})}
|
67
|
-
placeholder={input.placeholder}
|
68
|
-
/>
|
69
|
-
);
|
60
|
+
return <textarea {...register(fieldName)} placeholder={input.placeholder} />;
|
70
61
|
case 'select':
|
71
62
|
return <Select input={input} fieldName={fieldName} />;
|
72
63
|
case 'input': {
|
73
64
|
return (
|
74
|
-
<input
|
75
|
-
type={input.inputType}
|
76
|
-
defaultValue={input.defaultValue}
|
77
|
-
{...register(fieldName, {
|
78
|
-
value: input.defaultValue,
|
79
|
-
})}
|
80
|
-
placeholder={input.placeholder}
|
81
|
-
/>
|
65
|
+
<input type={input.inputType} {...register(fieldName)} placeholder={input.placeholder} />
|
82
66
|
);
|
83
67
|
}
|
84
68
|
case 'file-upload':
|
85
|
-
return <Uploader input={input} />;
|
69
|
+
return <Uploader fieldName={fieldName} input={input} />;
|
86
70
|
case 'checkbox':
|
87
|
-
return <Checkbox input={input} />;
|
71
|
+
return <Checkbox fieldName={fieldName} input={input} />;
|
88
72
|
case 'hidden':
|
89
73
|
return <input type="hidden" {...register(fieldName)} />;
|
90
74
|
case 'nested':
|
91
|
-
return <NestedFormFields input={input} register={register} />;
|
75
|
+
return <NestedFormFields fieldName={fieldName} input={input} register={register} />;
|
92
76
|
default:
|
93
77
|
return null;
|
94
78
|
}
|
95
|
-
};
|
79
|
+
}, [input, register, fieldName]);
|
96
80
|
|
97
81
|
return (
|
98
82
|
<div className="form-field">
|
99
|
-
{input.type !== 'checkbox' && (
|
83
|
+
{input.type !== 'hidden' && input.type !== 'checkbox' && (
|
100
84
|
<Label htmlFor={fieldName} label={input.label} fieldName={fieldName} />
|
101
85
|
)}
|
102
|
-
{
|
103
|
-
{error && <span className="error-message">{error.message}</span>}
|
86
|
+
{renderedField}
|
87
|
+
{error && input.type !== 'hidden' && <span className="error-message">{error.message}</span>}
|
104
88
|
</div>
|
105
89
|
);
|
106
90
|
}
|
@@ -2,7 +2,15 @@ import React, { useEffect, useMemo } from 'react';
|
|
2
2
|
import { InnerForm } from './InnerForm';
|
3
3
|
import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
|
4
4
|
import { useParams } from 'react-router';
|
5
|
-
import {
|
5
|
+
import {
|
6
|
+
FormProvider,
|
7
|
+
Resolver,
|
8
|
+
useForm,
|
9
|
+
UseFormReturn,
|
10
|
+
Path,
|
11
|
+
PathValue,
|
12
|
+
DefaultValues,
|
13
|
+
} from 'react-hook-form';
|
6
14
|
import { getFormPageMeta } from '../../decorators/form/getFormPageMeta';
|
7
15
|
import { FormHeader } from './FormHeader';
|
8
16
|
import { InputConfiguration } from '../../decorators/form/Input';
|
@@ -21,6 +29,7 @@ export interface FormPageProps<T extends AnyClass> {
|
|
21
29
|
title?: string;
|
22
30
|
documentTitle?: string;
|
23
31
|
header?: (utils: FormUtils<T>) => React.ReactNode;
|
32
|
+
className?: string;
|
24
33
|
}
|
25
34
|
|
26
35
|
function useCreateFormUtils<T extends AnyClass>(
|
@@ -142,11 +151,19 @@ export function FormPage<T extends AnyClass>({
|
|
142
151
|
title,
|
143
152
|
documentTitle,
|
144
153
|
header,
|
154
|
+
className,
|
145
155
|
}: FormPageProps<T>) {
|
146
156
|
const { class: formClass, inputs, resolver } = useMemo(() => getFormPageMeta(model), [model]);
|
147
157
|
const params = useParams();
|
148
158
|
const form = useForm<T>({
|
149
159
|
resolver: resolver as Resolver<T>,
|
160
|
+
defaultValues: inputs.reduce(
|
161
|
+
(acc, input) => {
|
162
|
+
acc[input.name] = input.defaultValue;
|
163
|
+
return acc;
|
164
|
+
},
|
165
|
+
{} as Record<string, unknown>
|
166
|
+
) as DefaultValues<T>,
|
150
167
|
});
|
151
168
|
const utils = useCreateFormUtils(inputs, form);
|
152
169
|
|
@@ -165,9 +182,11 @@ export function FormPage<T extends AnyClass>({
|
|
165
182
|
}, [params, form.reset, formClass.getDetailsData, formClass, form]);
|
166
183
|
|
167
184
|
return (
|
168
|
-
<
|
169
|
-
<
|
170
|
-
|
171
|
-
|
185
|
+
<div className={`form-wrapper ${className ?? ''}`}>
|
186
|
+
<FormProvider {...form}>
|
187
|
+
<FormHeader title={title} utils={utils} header={header} />
|
188
|
+
<InnerForm inputs={inputs} formClass={formClass} />
|
189
|
+
</FormProvider>
|
190
|
+
</div>
|
172
191
|
);
|
173
192
|
}
|
@@ -21,81 +21,82 @@ export function InnerForm<T extends AnyClass>({ inputs, formClass }: InnerFormPr
|
|
21
21
|
const loadingRef = useRef(false);
|
22
22
|
|
23
23
|
return (
|
24
|
-
<
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
formData.append(key, dataForm[key]);
|
40
|
-
}
|
24
|
+
<form
|
25
|
+
ref={formRef}
|
26
|
+
onSubmit={form.handleSubmit(
|
27
|
+
async (dataForm: T) => {
|
28
|
+
if (loadingRef.current) return;
|
29
|
+
loadingRef.current = true;
|
30
|
+
try {
|
31
|
+
const data =
|
32
|
+
formClass.type === 'json'
|
33
|
+
? dataForm
|
34
|
+
: (() => {
|
35
|
+
const formData = new FormData(formRef.current!);
|
36
|
+
for (const key in dataForm) {
|
37
|
+
if (!formData.get(key)) {
|
38
|
+
formData.append(key, dataForm[key]);
|
41
39
|
}
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
}
|
53
|
-
|
54
|
-
|
55
|
-
errorResponse?.response?.data?.message ||
|
56
|
-
(error instanceof Error ? error.message : 'An error occurred');
|
57
|
-
toast.error('Something went wrong');
|
58
|
-
setErrorMessage(message);
|
59
|
-
console.error(error);
|
60
|
-
} finally {
|
61
|
-
loadingRef.current = false;
|
40
|
+
}
|
41
|
+
return formData;
|
42
|
+
})();
|
43
|
+
const resut = await formClass.onSubmit(data);
|
44
|
+
form.reset(resut);
|
45
|
+
setErrorMessage(null);
|
46
|
+
toast.success('Form submitted successfully');
|
47
|
+
//TODO: https path or relative path
|
48
|
+
if (formClass.redirectSuccessUrl) {
|
49
|
+
navigate(formClass.redirectSuccessUrl);
|
50
|
+
}
|
51
|
+
if (formClass.onSubmitSuccess) {
|
52
|
+
formClass.onSubmitSuccess(resut);
|
62
53
|
}
|
63
|
-
}
|
64
|
-
|
65
|
-
|
66
|
-
|
54
|
+
} catch (error: unknown) {
|
55
|
+
const errorResponse = error as { response?: { data?: { message?: string } } };
|
56
|
+
const message =
|
57
|
+
errorResponse?.response?.data?.message ||
|
58
|
+
(error instanceof Error ? error.message : 'An error occurred');
|
59
|
+
toast.error('Something went wrong');
|
60
|
+
setErrorMessage(message);
|
61
|
+
console.error(error);
|
62
|
+
} finally {
|
63
|
+
loadingRef.current = false;
|
67
64
|
}
|
65
|
+
},
|
66
|
+
(errors, event) => {
|
67
|
+
//TOOD: put error if useer choose global error
|
68
|
+
console.error('error creating creation', errors, event);
|
69
|
+
}
|
70
|
+
)}
|
71
|
+
>
|
72
|
+
<div>
|
73
|
+
{errorMessage && (
|
74
|
+
<div className="error-message" style={{ color: 'red', marginBottom: '1rem' }}>
|
75
|
+
{errorMessage}
|
76
|
+
</div>
|
68
77
|
)}
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
/>
|
93
|
-
))}
|
94
|
-
<button type="submit" className="submit-button">
|
95
|
-
Submit
|
96
|
-
</button>
|
97
|
-
</div>
|
98
|
-
</form>
|
99
|
-
</div>
|
78
|
+
{inputs?.map((input: InputConfiguration) => (
|
79
|
+
<FormField
|
80
|
+
key={input.name || ''}
|
81
|
+
input={input}
|
82
|
+
register={form.register}
|
83
|
+
error={
|
84
|
+
input.name
|
85
|
+
? {
|
86
|
+
message: (
|
87
|
+
form.formState.errors[input.name as keyof T] as {
|
88
|
+
message: string;
|
89
|
+
}
|
90
|
+
)?.message,
|
91
|
+
}
|
92
|
+
: undefined
|
93
|
+
}
|
94
|
+
/>
|
95
|
+
))}
|
96
|
+
<button type="submit" className="submit-button">
|
97
|
+
Submit
|
98
|
+
</button>
|
99
|
+
</div>
|
100
|
+
</form>
|
100
101
|
);
|
101
102
|
}
|
@@ -4,6 +4,7 @@ import { useFormContext, Controller } from 'react-hook-form';
|
|
4
4
|
import { SelectInputConfiguration } from '../../decorators/form/inputs/SelectInput';
|
5
5
|
import ReactSelect from 'react-select';
|
6
6
|
import { darkSelectStyles } from './SelectStyles';
|
7
|
+
import { preloadCacheHelper } from '../../utils/PreloadCacheHelper';
|
7
8
|
|
8
9
|
interface SelectProps {
|
9
10
|
input: InputConfiguration;
|
@@ -23,9 +24,9 @@ export function Select<TValue>({ input, fieldName }: SelectProps) {
|
|
23
24
|
const styles = useMemo(() => darkSelectStyles<TValue>(), []);
|
24
25
|
useEffect(() => {
|
25
26
|
if (inputSelect.onSelectPreloader) {
|
26
|
-
const
|
27
|
-
|
28
|
-
setOptions(
|
27
|
+
const onSelectPreloader = inputSelect.onSelectPreloader;
|
28
|
+
preloadCacheHelper.setOrGetCache(onSelectPreloader, (options: OptionType<TValue>[]) => {
|
29
|
+
setOptions(options);
|
29
30
|
});
|
30
31
|
}
|
31
32
|
}, [inputSelect, inputSelect.onSelectPreloader]);
|
@@ -5,17 +5,18 @@ import { InputConfiguration } from '../../decorators/form/Input';
|
|
5
5
|
interface UploaderProps {
|
6
6
|
input: InputConfiguration;
|
7
7
|
maxLength?: number;
|
8
|
+
fieldName: string;
|
8
9
|
}
|
9
10
|
|
10
|
-
export function Uploader({ input, maxLength = 1 }: UploaderProps) {
|
11
|
+
export function Uploader({ input, maxLength = 1, fieldName }: UploaderProps) {
|
11
12
|
const form = useFormContext();
|
12
13
|
const [files, setFiles] = useState<File[]>([]);
|
13
|
-
const id =
|
14
|
+
const id = fieldName;
|
14
15
|
|
15
16
|
useEffect(() => {
|
16
17
|
// Update form value whenever files change
|
17
|
-
form.setValue(
|
18
|
-
}, [files, form,
|
18
|
+
form.setValue(fieldName + '_files', files.length > 0);
|
19
|
+
}, [files, form, fieldName]);
|
19
20
|
|
20
21
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
21
22
|
if (maxLength > 1) {
|
@@ -3,23 +3,25 @@ import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
|
|
3
3
|
import { GetDetailsDataFN } from '../details/Details';
|
4
4
|
|
5
5
|
const DETAILS_METADATA_KEY = 'DetailsMetaData';
|
6
|
-
export type OnSubmitFN<T> = (data: T | FormData) => Promise<
|
6
|
+
export type OnSubmitFN<T, L = T> = (data: T | FormData) => Promise<L>;
|
7
7
|
|
8
|
-
interface FormOptions<T extends AnyClass> {
|
9
|
-
onSubmit: OnSubmitFN<T>;
|
8
|
+
interface FormOptions<T extends AnyClass, L = T> {
|
9
|
+
onSubmit: OnSubmitFN<T, L>;
|
10
|
+
onSubmitSuccess?: (data: L) => void;
|
10
11
|
getDetailsData?: GetDetailsDataFN<T>;
|
11
12
|
type?: 'json' | 'formData';
|
12
13
|
redirectSuccessUrl?: string;
|
13
14
|
}
|
14
15
|
|
15
|
-
export interface FormConfiguration<T extends AnyClass> {
|
16
|
-
onSubmit: OnSubmitFN<T>;
|
16
|
+
export interface FormConfiguration<T extends AnyClass, L = T> {
|
17
|
+
onSubmit: OnSubmitFN<T, L>;
|
18
|
+
onSubmitSuccess?: (data: L) => void;
|
17
19
|
getDetailsData?: GetDetailsDataFN<T>;
|
18
20
|
type: 'json' | 'formData';
|
19
21
|
redirectSuccessUrl?: string;
|
20
22
|
}
|
21
23
|
|
22
|
-
export function Form<T extends AnyClass>(options?: FormOptions<T>): ClassDecorator {
|
24
|
+
export function Form<T extends AnyClass, L = T>(options?: FormOptions<T, L>): ClassDecorator {
|
23
25
|
return (target: object) => {
|
24
26
|
if (options) {
|
25
27
|
Reflect.defineMetadata(DETAILS_METADATA_KEY, options, target);
|
@@ -27,18 +29,19 @@ export function Form<T extends AnyClass>(options?: FormOptions<T>): ClassDecorat
|
|
27
29
|
};
|
28
30
|
}
|
29
31
|
|
30
|
-
export function getFormConfiguration<T extends AnyClass, K extends AnyClassConstructor<T
|
32
|
+
export function getFormConfiguration<T extends AnyClass, K extends AnyClassConstructor<T>, L = T>(
|
31
33
|
entityClass: K
|
32
|
-
): FormConfiguration<T> {
|
33
|
-
const formOptions: FormOptions<T> = Reflect.getMetadata(
|
34
|
+
): FormConfiguration<T, L> {
|
35
|
+
const formOptions: FormOptions<T, L> = Reflect.getMetadata(
|
34
36
|
DETAILS_METADATA_KEY,
|
35
37
|
entityClass as object
|
36
38
|
);
|
37
39
|
if (!formOptions) {
|
38
40
|
throw new Error('Form decerator should be used on class');
|
39
41
|
}
|
40
|
-
const formConfiguration: FormConfiguration<T> = {
|
42
|
+
const formConfiguration: FormConfiguration<T, L> = {
|
41
43
|
onSubmit: formOptions.onSubmit,
|
44
|
+
onSubmitSuccess: formOptions.onSubmitSuccess,
|
42
45
|
getDetailsData: formOptions.getDetailsData,
|
43
46
|
type: formOptions.type ?? 'json',
|
44
47
|
redirectSuccessUrl: formOptions.redirectSuccessUrl,
|