@jasperoosthoek/react-toolbox 0.8.1 → 0.9.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/change-log.md +330 -312
- package/dist/components/buttons/ConfirmButton.d.ts +2 -2
- package/dist/components/buttons/DeleteConfirmButton.d.ts +2 -2
- package/dist/components/buttons/IconButtons.d.ts +40 -41
- package/dist/components/errors/Errors.d.ts +1 -2
- package/dist/components/forms/FormField.d.ts +22 -0
- package/dist/components/forms/FormFields.d.ts +1 -56
- package/dist/components/forms/FormModal.d.ts +7 -34
- package/dist/components/forms/FormModalProvider.d.ts +25 -15
- package/dist/components/forms/FormProvider.d.ts +66 -0
- package/dist/components/forms/fields/FormBadgesSelection.d.ts +26 -0
- package/dist/components/forms/fields/FormCheckbox.d.ts +7 -0
- package/dist/components/forms/fields/FormDropdown.d.ts +19 -0
- package/dist/components/forms/fields/FormInput.d.ts +17 -0
- package/dist/components/forms/fields/FormSelect.d.ts +12 -0
- package/dist/components/forms/fields/index.d.ts +5 -0
- package/dist/components/indicators/CheckIndicator.d.ts +1 -2
- package/dist/components/indicators/LoadingIndicator.d.ts +4 -4
- package/dist/components/login/LoginPage.d.ts +1 -1
- package/dist/components/tables/DataTable.d.ts +2 -2
- package/dist/components/tables/DragAndDropList.d.ts +2 -2
- package/dist/components/tables/SearchBox.d.ts +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -2
- package/dist/index.js.LICENSE.txt +0 -4
- package/dist/localization/LocalizationContext.d.ts +1 -1
- package/dist/utils/hooks.d.ts +1 -1
- package/dist/utils/timeAndDate.d.ts +5 -2
- package/dist/utils/utils.d.ts +3 -3
- package/package.json +10 -11
- package/src/__tests__/buttons.test.tsx +545 -0
- package/src/__tests__/errors.test.tsx +339 -0
- package/src/__tests__/forms.test.tsx +3021 -0
- package/src/__tests__/hooks.test.tsx +413 -0
- package/src/__tests__/indicators.test.tsx +284 -0
- package/src/__tests__/localization.test.tsx +462 -0
- package/src/__tests__/login.test.tsx +417 -0
- package/src/__tests__/setupTests.ts +328 -0
- package/src/__tests__/tables.test.tsx +609 -0
- package/src/__tests__/timeAndDate.test.tsx +308 -0
- package/src/__tests__/utils.test.tsx +422 -0
- package/src/components/forms/FormField.tsx +92 -0
- package/src/components/forms/FormFields.tsx +3 -423
- package/src/components/forms/FormModal.tsx +168 -243
- package/src/components/forms/FormModalProvider.tsx +164 -85
- package/src/components/forms/FormProvider.tsx +218 -0
- package/src/components/forms/fields/FormBadgesSelection.tsx +108 -0
- package/src/components/forms/fields/FormCheckbox.tsx +76 -0
- package/src/components/forms/fields/FormDropdown.tsx +123 -0
- package/src/components/forms/fields/FormInput.tsx +114 -0
- package/src/components/forms/fields/FormSelect.tsx +47 -0
- package/src/components/forms/fields/index.ts +6 -0
- package/src/index.ts +32 -29
- package/src/localization/LocalizationContext.tsx +156 -131
- package/src/localization/localization.ts +131 -131
- package/src/utils/hooks.ts +108 -94
- package/src/utils/timeAndDate.ts +33 -4
- package/src/utils/utils.ts +74 -66
- package/dist/components/forms/CreateEditModal.d.ts +0 -41
- package/dist/components/forms/CreateEditModalProvider.d.ts +0 -41
- package/dist/components/forms/FormFields.test.d.ts +0 -4
- package/dist/login/Login.d.ts +0 -70
- package/src/components/forms/FormFields.test.tsx +0 -107
|
@@ -1,92 +1,101 @@
|
|
|
1
1
|
import React, { useState, useContext, ReactNode } from 'react';
|
|
2
|
-
|
|
3
2
|
import { useLocalization } from '../../localization/LocalizationContext';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
IncludeData,
|
|
7
|
-
InitialState,
|
|
8
|
-
OnSave,
|
|
9
|
-
Validate,
|
|
10
|
-
ModalTitle,
|
|
11
|
-
Width,
|
|
12
|
-
FormModal,
|
|
13
|
-
|
|
14
|
-
} from './FormModal';
|
|
3
|
+
import { FormProvider, FormFields, InitialState, OnSubmit, Validate } from './FormProvider';
|
|
4
|
+
import { FormModal, ModalTitle, Width } from './FormModal';
|
|
15
5
|
import { FormValue } from './FormFields';
|
|
16
6
|
import { ButtonProps, CreateButton, EditButton } from '../buttons/IconButtons';
|
|
17
7
|
|
|
8
|
+
// Add the IncludeData type from master version
|
|
9
|
+
export type IncludeData<T extends FormFields> = {
|
|
10
|
+
[key in Exclude<string, keyof T>]: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
18
13
|
export type ShowCreateModal = (show?: boolean) => void;
|
|
19
|
-
export type ShowEditModal<T, K> = (state: { [key in keyof T]: FormValue } & K) => void;
|
|
14
|
+
export type ShowEditModal<T, K> = (state: ({ [key in keyof T]: FormValue } & K) | null) => void;
|
|
15
|
+
|
|
16
|
+
type FormModalContextType<T, K> = {
|
|
17
|
+
showCreateModal: ShowCreateModal;
|
|
18
|
+
showEditModal: ShowEditModal<T, K>;
|
|
19
|
+
hasProvider: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type T = any;
|
|
23
|
+
type K = any;
|
|
24
|
+
const defaultErrorState: FormModalContextType<T, K> = {
|
|
25
|
+
showCreateModal: () => {
|
|
26
|
+
console.error('The showCreateModal function should only be used in a child of the FormModalProvider component.');
|
|
27
|
+
},
|
|
28
|
+
showEditModal: (state: ({ [key in keyof T]: FormValue } & K) | null) => {
|
|
29
|
+
console.error('The showEditModal function should only be used in a child of the FormModalProvider component.');
|
|
30
|
+
},
|
|
31
|
+
hasProvider: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const FormModalContext = React.createContext(defaultErrorState);
|
|
35
|
+
|
|
36
|
+
export const useFormModal = () => useContext(FormModalContext);
|
|
20
37
|
|
|
21
38
|
export type FormCreateModalButton = ButtonProps;
|
|
22
39
|
export const FormCreateModalButton = ({ onClick, ...props }: ButtonProps) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
40
|
+
const context = useContext(FormModalContext);
|
|
41
|
+
|
|
25
42
|
return (
|
|
26
43
|
<CreateButton
|
|
27
44
|
{...props}
|
|
28
45
|
onClick={(e) => {
|
|
29
46
|
// A onClick function was given to CreateButton
|
|
30
47
|
if (onClick) onClick(e);
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
if (hasProvider)
|
|
48
|
+
|
|
49
|
+
// Check if we have a provider context
|
|
50
|
+
if (!context || !context.hasProvider) {
|
|
51
|
+
console.error('The showCreateModal function should only be used in a child of the FormModalProvider component.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// CreateButton is inside a FormModalProvider which can handle showCreateModal
|
|
56
|
+
context.showCreateModal();
|
|
34
57
|
}}
|
|
35
58
|
/>
|
|
36
59
|
)
|
|
37
60
|
}
|
|
61
|
+
|
|
38
62
|
export interface FormEditModalButtonProps<T, K> extends ButtonProps {
|
|
39
63
|
state: { [key in keyof T]: FormValue } & K;
|
|
40
64
|
}
|
|
41
|
-
export const FormEditModalButton = ({ state, onClick, ... props }: FormEditModalButtonProps<T, K>) => {
|
|
42
|
-
const { showEditModal, hasProvider } = useFormModal();
|
|
43
65
|
|
|
66
|
+
export const FormEditModalButton = ({ state, onClick, ...props }: FormEditModalButtonProps<T, K>) => {
|
|
67
|
+
const context = useContext(FormModalContext);
|
|
68
|
+
|
|
44
69
|
return (
|
|
45
70
|
<EditButton
|
|
46
71
|
{...props}
|
|
47
72
|
onClick={(e) => {
|
|
48
|
-
// A onClick function was given to
|
|
73
|
+
// A onClick function was given to EditButton
|
|
49
74
|
if (onClick) onClick(e);
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
if (hasProvider)
|
|
75
|
+
|
|
76
|
+
// Check if we have a provider context
|
|
77
|
+
if (!context || !context.hasProvider) {
|
|
78
|
+
console.error('The showEditModal function should only be used in a child of the FormModalProvider component.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// EditButton is inside a FormModalProvider which can handle showEditModal
|
|
83
|
+
context.showEditModal(state);
|
|
53
84
|
}}
|
|
54
85
|
/>
|
|
55
86
|
)
|
|
56
87
|
}
|
|
57
88
|
|
|
58
|
-
type FormModalContextType<T, K> = {
|
|
59
|
-
showCreateModal: ShowCreateModal;
|
|
60
|
-
showEditModal: ShowEditModal<T, K>;
|
|
61
|
-
hasProvider: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
type T = any;
|
|
65
|
-
type K = any;
|
|
66
|
-
const defaultErrorState: FormModalContextType<T, K> = {
|
|
67
|
-
showCreateModal: () => {
|
|
68
|
-
console.error('The showCreateModal function should only be used in a child of the FormModalProvider component.');
|
|
69
|
-
},
|
|
70
|
-
showEditModal: (state: { [key in keyof T]: FormValue } & K) => {
|
|
71
|
-
console.error('The showEditModal function should only be used in a child of the FormModalProvider component.');
|
|
72
|
-
},
|
|
73
|
-
hasProvider: false,
|
|
74
|
-
};
|
|
75
|
-
export const FormModalContext = React.createContext(defaultErrorState);
|
|
76
|
-
|
|
77
|
-
export const useFormModal = () => useContext(FormModalContext);
|
|
78
|
-
|
|
79
|
-
|
|
80
89
|
export type FormModalProviderProps<
|
|
81
90
|
T extends FormFields,
|
|
82
91
|
K extends IncludeData<T>
|
|
83
92
|
> = {
|
|
93
|
+
formFields: T;
|
|
84
94
|
initialState: InitialState<T> | K;
|
|
85
95
|
includeData?: K;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
onUpdate?: OnSave<T, K>;
|
|
96
|
+
onSave?: (state: { [key in keyof T]: FormValue } & K, callback?: () => void) => void;
|
|
97
|
+
onCreate?: (state: { [key in keyof T]: FormValue } & K, callback?: () => void) => void;
|
|
98
|
+
onUpdate?: (state: { [key in keyof T]: FormValue } & K, callback?: () => void) => void;
|
|
90
99
|
validate?: Validate;
|
|
91
100
|
createModalTitle?: ModalTitle;
|
|
92
101
|
editModalTitle?: ModalTitle;
|
|
@@ -95,58 +104,128 @@ export type FormModalProviderProps<
|
|
|
95
104
|
width?: Width;
|
|
96
105
|
children: ReactNode;
|
|
97
106
|
}
|
|
107
|
+
|
|
98
108
|
export const FormModalProvider: React.FC<FormModalProviderProps<T, K>> = ({
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
createModalTitle,
|
|
110
|
+
editModalTitle,
|
|
111
|
+
formFields,
|
|
102
112
|
initialState,
|
|
113
|
+
includeData,
|
|
103
114
|
validate,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
loading = false,
|
|
116
|
+
onCreate,
|
|
117
|
+
onUpdate,
|
|
107
118
|
onSave,
|
|
108
119
|
dialogClassName,
|
|
109
|
-
|
|
120
|
+
width,
|
|
121
|
+
children,
|
|
110
122
|
}) => {
|
|
111
|
-
const [createModalActive,
|
|
112
|
-
const [
|
|
123
|
+
const [createModalActive, setCreateModalActive] = useState(false);
|
|
124
|
+
const [editModalState, setEditModalState] = useState<({ [key in keyof T]: FormValue } & K) | null>(null);
|
|
113
125
|
const { strings } = useLocalization();
|
|
126
|
+
|
|
127
|
+
// Provide default values
|
|
128
|
+
const safeIncludeData = (includeData || {}) as K;
|
|
129
|
+
|
|
130
|
+
const handleCreateSubmit: OnSubmit<T> = (state, callback) => {
|
|
131
|
+
// Priority: onCreate > onSave fallback
|
|
132
|
+
const submitHandler = onCreate || onSave;
|
|
133
|
+
if (submitHandler) {
|
|
134
|
+
// Merge form state with includeData
|
|
135
|
+
submitHandler(
|
|
136
|
+
{
|
|
137
|
+
...state,
|
|
138
|
+
...safeIncludeData,
|
|
139
|
+
} as { [key in keyof T]: FormValue } & K,
|
|
140
|
+
() => {
|
|
141
|
+
setCreateModalActive(false);
|
|
142
|
+
if (callback) callback();
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
// No handler provided, just close modal
|
|
147
|
+
setCreateModalActive(false);
|
|
148
|
+
if (callback) callback();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleEditSubmit: OnSubmit<T> = (state, callback) => {
|
|
153
|
+
// Priority: onUpdate > onSave fallback
|
|
154
|
+
const submitHandler = onUpdate || onSave;
|
|
155
|
+
if (submitHandler) {
|
|
156
|
+
// Merge form state with includeData
|
|
157
|
+
submitHandler(
|
|
158
|
+
{
|
|
159
|
+
...state,
|
|
160
|
+
...safeIncludeData,
|
|
161
|
+
} as { [key in keyof T]: FormValue } & K,
|
|
162
|
+
() => {
|
|
163
|
+
setEditModalState(null);
|
|
164
|
+
if (callback) callback();
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
// No handler provided, just close modal
|
|
169
|
+
setEditModalState(null);
|
|
170
|
+
if (callback) callback();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Convert entity state to form initial state
|
|
175
|
+
const getFormInitialState = (entityState: ({ [key in keyof T]: FormValue } & K) | null): InitialState<T> => {
|
|
176
|
+
if (!entityState) {
|
|
177
|
+
return initialState as InitialState<T>;
|
|
178
|
+
}
|
|
179
|
+
return entityState as unknown as InitialState<T>;
|
|
180
|
+
};
|
|
181
|
+
|
|
114
182
|
return (
|
|
115
183
|
<FormModalContext.Provider
|
|
116
184
|
value={{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
185
|
+
showCreateModal: (show?: boolean) =>
|
|
186
|
+
setCreateModalActive(typeof show === 'undefined' ? true : show),
|
|
187
|
+
showEditModal: (state: ({ [key in keyof T]: FormValue } & K) | null) => setEditModalState(state),
|
|
120
188
|
hasProvider: true,
|
|
121
|
-
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
122
191
|
{children}
|
|
123
|
-
|
|
192
|
+
|
|
124
193
|
{createModalActive && (onCreate || onSave) && (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
formFields={formFields}
|
|
194
|
+
<FormProvider
|
|
195
|
+
formFields={formFields}
|
|
196
|
+
initialState={initialState as InitialState<T>}
|
|
197
|
+
onSubmit={handleCreateSubmit}
|
|
130
198
|
validate={validate}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
199
|
+
loading={loading}
|
|
200
|
+
resetTrigger={createModalActive}
|
|
201
|
+
>
|
|
202
|
+
<FormModal
|
|
203
|
+
show={createModalActive}
|
|
204
|
+
modalTitle={createModalTitle || strings.getString('modal_create')}
|
|
205
|
+
onHide={() => setCreateModalActive(false)}
|
|
206
|
+
dialogClassName={dialogClassName}
|
|
207
|
+
width={width}
|
|
208
|
+
/>
|
|
209
|
+
</FormProvider>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{editModalState && (onUpdate || onSave) && (
|
|
213
|
+
<FormProvider
|
|
143
214
|
formFields={formFields}
|
|
215
|
+
initialState={getFormInitialState(editModalState)}
|
|
216
|
+
onSubmit={handleEditSubmit}
|
|
144
217
|
validate={validate}
|
|
145
218
|
loading={loading}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
219
|
+
resetTrigger={editModalState}
|
|
220
|
+
>
|
|
221
|
+
<FormModal
|
|
222
|
+
show={!!editModalState}
|
|
223
|
+
modalTitle={editModalTitle || strings.getString('modal_edit')}
|
|
224
|
+
onHide={() => setEditModalState(null)}
|
|
225
|
+
dialogClassName={dialogClassName}
|
|
226
|
+
width={width}
|
|
227
|
+
/>
|
|
228
|
+
</FormProvider>
|
|
150
229
|
)}
|
|
151
230
|
</FormModalContext.Provider>
|
|
152
231
|
);
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React, { useEffect, useState, useContext, ReactNode, createContext } from 'react';
|
|
2
|
+
import { useSetState, usePrevious } from '../../utils/hooks';
|
|
3
|
+
import { isEmpty } from '../../utils/utils';
|
|
4
|
+
import { useLocalization } from '../../localization/LocalizationContext';
|
|
5
|
+
import { FormValue } from './FormFields';
|
|
6
|
+
|
|
7
|
+
export type FormFieldConfig = {
|
|
8
|
+
initialValue?: any;
|
|
9
|
+
type?: 'string' | 'number' | 'select' | 'checkbox' | 'boolean' | 'textarea' | 'dropdown';
|
|
10
|
+
required?: boolean;
|
|
11
|
+
formProps?: any;
|
|
12
|
+
component?: any;
|
|
13
|
+
onChange?: (value: FormValue, formData?: any) => any;
|
|
14
|
+
label?: React.ReactElement | string;
|
|
15
|
+
options?: Array<{ value: string | number; label: string; disabled?: boolean }>; // For select fields
|
|
16
|
+
list?: any[]; // For dropdown fields
|
|
17
|
+
idKey?: string; // For dropdown fields
|
|
18
|
+
nameKey?: string; // For dropdown fields
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FormFields = { [key: string]: FormFieldConfig };
|
|
22
|
+
|
|
23
|
+
export type InitialState<T> = Partial<{
|
|
24
|
+
[key in keyof T]: FormValue;
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
export type OnSubmit<T> = (state: { [key in keyof T]: FormValue }, callback?: () => void) => void;
|
|
28
|
+
|
|
29
|
+
export type Validate = (state: any) => any;
|
|
30
|
+
|
|
31
|
+
type FormContextType<T extends FormFields> = {
|
|
32
|
+
formFields: T;
|
|
33
|
+
formData: { [key in keyof T]: FormValue } | null;
|
|
34
|
+
initialFormData: { [key in keyof T]: FormValue } | null;
|
|
35
|
+
pristine: boolean;
|
|
36
|
+
validated: boolean;
|
|
37
|
+
validationErrors: { [key: string]: any };
|
|
38
|
+
loading: boolean;
|
|
39
|
+
getValue: (key: string) => FormValue;
|
|
40
|
+
setValue: (key: string, value: FormValue) => void;
|
|
41
|
+
setFormData: (data: Partial<{ [key in keyof T]: FormValue }>) => void;
|
|
42
|
+
resetForm: () => void;
|
|
43
|
+
submit: () => void;
|
|
44
|
+
setPristine: (pristine: boolean) => void;
|
|
45
|
+
setLoading: (loading: boolean) => void;
|
|
46
|
+
hasProvider: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultFormState: FormContextType<any> = {
|
|
50
|
+
formFields: {},
|
|
51
|
+
formData: null,
|
|
52
|
+
initialFormData: null,
|
|
53
|
+
pristine: true,
|
|
54
|
+
validated: false,
|
|
55
|
+
validationErrors: {},
|
|
56
|
+
loading: false,
|
|
57
|
+
getValue: () => '',
|
|
58
|
+
setValue: () => console.error('setValue should only be used within a FormProvider'),
|
|
59
|
+
setFormData: () => console.error('setFormData should only be used within a FormProvider'),
|
|
60
|
+
resetForm: () => console.error('resetForm should only be used within a FormProvider'),
|
|
61
|
+
submit: () => console.error('submit should only be used within a FormProvider'),
|
|
62
|
+
setPristine: () => console.error('setPristine should only be used within a FormProvider'),
|
|
63
|
+
setLoading: () => console.error('setLoading should only be used within a FormProvider'),
|
|
64
|
+
hasProvider: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const FormContext = createContext<FormContextType<any>>(defaultFormState);
|
|
68
|
+
|
|
69
|
+
export const useForm = <T extends FormFields>() => useContext(FormContext) as FormContextType<T>;
|
|
70
|
+
|
|
71
|
+
export type FormProviderProps<T extends FormFields> = {
|
|
72
|
+
formFields: T;
|
|
73
|
+
initialState?: InitialState<T>;
|
|
74
|
+
onSubmit: OnSubmit<T>;
|
|
75
|
+
validate?: Validate;
|
|
76
|
+
loading?: boolean;
|
|
77
|
+
children: ReactNode;
|
|
78
|
+
resetTrigger?: any; // When this value changes, the form will reset
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const FormProvider = <T extends FormFields>({
|
|
82
|
+
formFields,
|
|
83
|
+
initialState = {} as InitialState<T>,
|
|
84
|
+
onSubmit,
|
|
85
|
+
validate,
|
|
86
|
+
loading = false,
|
|
87
|
+
children,
|
|
88
|
+
resetTrigger,
|
|
89
|
+
}: FormProviderProps<T>) => {
|
|
90
|
+
const { strings } = useLocalization();
|
|
91
|
+
|
|
92
|
+
if (!formFields) {
|
|
93
|
+
console.error(`Property formFields cannot be empty.`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const getInitialFormData = () => ({
|
|
98
|
+
...Object.entries(formFields).reduce((o, [key, { initialValue }]) => ({ ...o, [key]: initialValue || '' }), {}),
|
|
99
|
+
...initialState || {},
|
|
100
|
+
}) as { [key in keyof T]: FormValue };
|
|
101
|
+
|
|
102
|
+
const [initialFormData, setInitialFormData] = useState<{ [key in keyof T]: FormValue }>(getInitialFormData());
|
|
103
|
+
|
|
104
|
+
const [{ pristine, formData, internalLoading }, setState] = useSetState({
|
|
105
|
+
pristine: true,
|
|
106
|
+
formData: initialFormData,
|
|
107
|
+
internalLoading: false,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const prevResetTrigger = usePrevious(resetTrigger);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (resetTrigger !== undefined && prevResetTrigger !== resetTrigger) {
|
|
114
|
+
const newInitialFormData = getInitialFormData();
|
|
115
|
+
setInitialFormData(newInitialFormData);
|
|
116
|
+
setState({
|
|
117
|
+
pristine: true,
|
|
118
|
+
formData: newInitialFormData,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}, [resetTrigger, prevResetTrigger]);
|
|
122
|
+
|
|
123
|
+
const getValue = (key: string): FormValue => {
|
|
124
|
+
const value = formData[key];
|
|
125
|
+
if (value !== undefined && value !== null) {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
return (formFields[key] || {}).type === 'number' ? '0' : '';
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const setValue = (key: string, value: FormValue) => {
|
|
132
|
+
const fieldOnChange = formFields[key]?.onChange;
|
|
133
|
+
const newFormData = typeof fieldOnChange === 'function'
|
|
134
|
+
? { ...formData, ...fieldOnChange(value, formData) }
|
|
135
|
+
: { ...formData, [key]: value };
|
|
136
|
+
|
|
137
|
+
setState({ formData: newFormData });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const setFormData = (data: Partial<{ [key in keyof T]: FormValue }>) => {
|
|
141
|
+
setState({ formData: { ...formData, ...data } });
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const resetForm = () => {
|
|
145
|
+
const newInitialFormData = getInitialFormData();
|
|
146
|
+
setInitialFormData(newInitialFormData);
|
|
147
|
+
setState({
|
|
148
|
+
pristine: true,
|
|
149
|
+
formData: newInitialFormData,
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const validationErrors = {
|
|
154
|
+
...validate
|
|
155
|
+
? Object.entries(validate(formData) || {})
|
|
156
|
+
.reduce(
|
|
157
|
+
(o, [key, val]) => {
|
|
158
|
+
// Remove all empty elements
|
|
159
|
+
if (isEmpty(val)) return o;
|
|
160
|
+
return { ...o, [key]: val };
|
|
161
|
+
},
|
|
162
|
+
{}
|
|
163
|
+
)
|
|
164
|
+
: {},
|
|
165
|
+
...Object.keys(formData).reduce(
|
|
166
|
+
(o, key) => {
|
|
167
|
+
if (!formFields[key] || !formFields[key].required || !isEmpty(getValue(key))) return o;
|
|
168
|
+
return { ...o, [key]: strings.getString('required_field') };
|
|
169
|
+
},
|
|
170
|
+
{}
|
|
171
|
+
),
|
|
172
|
+
} as { [key: string]: any };
|
|
173
|
+
|
|
174
|
+
const validated = Object.values(validationErrors).length === 0;
|
|
175
|
+
|
|
176
|
+
const submit = () => {
|
|
177
|
+
setState({ pristine: false });
|
|
178
|
+
if (!validated) return;
|
|
179
|
+
|
|
180
|
+
setState({ internalLoading: true });
|
|
181
|
+
onSubmit(
|
|
182
|
+
formData,
|
|
183
|
+
() => setState({ internalLoading: false })
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const setPristine = (pristine: boolean) => {
|
|
188
|
+
setState({ pristine });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const setLoading = (loading: boolean) => {
|
|
192
|
+
setState({ internalLoading: loading });
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const contextValue: FormContextType<T> = {
|
|
196
|
+
formFields,
|
|
197
|
+
formData,
|
|
198
|
+
initialFormData,
|
|
199
|
+
pristine,
|
|
200
|
+
validated,
|
|
201
|
+
validationErrors,
|
|
202
|
+
loading: loading || internalLoading,
|
|
203
|
+
getValue,
|
|
204
|
+
setValue,
|
|
205
|
+
setFormData,
|
|
206
|
+
resetForm,
|
|
207
|
+
submit,
|
|
208
|
+
setPristine,
|
|
209
|
+
setLoading,
|
|
210
|
+
hasProvider: true,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<FormContext.Provider value={contextValue}>
|
|
215
|
+
{children}
|
|
216
|
+
</FormContext.Provider>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Form, Badge } from 'react-bootstrap';
|
|
3
|
+
import { BadgeProps } from 'react-bootstrap';
|
|
4
|
+
import { useFormField } from '../FormField';
|
|
5
|
+
|
|
6
|
+
export interface BadgeSelectionProps extends BadgeProps {
|
|
7
|
+
selected: boolean;
|
|
8
|
+
cursor: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const BadgeSelection = ({ selected = true, disabled, cursor, onClick, style, bg, ...restProps }: BadgeSelectionProps) => (
|
|
13
|
+
<Badge
|
|
14
|
+
bg={bg || (selected ? 'primary' : 'secondary')}
|
|
15
|
+
style={{
|
|
16
|
+
userSelect: 'none',
|
|
17
|
+
...cursor ? { cursor } : {},
|
|
18
|
+
...style || {},
|
|
19
|
+
}}
|
|
20
|
+
{...disabled ? {} : { onClick }}
|
|
21
|
+
{...restProps}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
type DisabledProps = {
|
|
26
|
+
list: any[];
|
|
27
|
+
value: string | number;
|
|
28
|
+
state: any;
|
|
29
|
+
initialState: any;
|
|
30
|
+
initialValue: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FormBadgesSelectionProps extends Omit<
|
|
34
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
35
|
+
'name' | 'value' | 'onChange' | 'disabled' | 'list'
|
|
36
|
+
> {
|
|
37
|
+
name: string;
|
|
38
|
+
label?: React.ReactElement | string;
|
|
39
|
+
list: any[];
|
|
40
|
+
idKey?: string;
|
|
41
|
+
multiple?: boolean;
|
|
42
|
+
integer?: boolean;
|
|
43
|
+
disabled?: boolean | ((props: DisabledProps) => boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const FormBadgesSelection = (props: FormBadgesSelectionProps) => {
|
|
47
|
+
const {
|
|
48
|
+
list,
|
|
49
|
+
idKey = 'id',
|
|
50
|
+
multiple,
|
|
51
|
+
integer,
|
|
52
|
+
disabled,
|
|
53
|
+
...componentProps
|
|
54
|
+
} = props;
|
|
55
|
+
|
|
56
|
+
const { value, onChange, isInvalid, error, label, required, mergedProps } = useFormField(componentProps);
|
|
57
|
+
|
|
58
|
+
const isMultiple = multiple || multiple === false ? multiple : value instanceof Array;
|
|
59
|
+
const parseInteger = (value: string | number): string | number => integer ? parseInt(`${value}`) : `${value}`;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Form.Group controlId={props.name}>
|
|
63
|
+
{label && <Form.Label>{label}{required && ' *'}</Form.Label>}
|
|
64
|
+
{isInvalid && error && (
|
|
65
|
+
<Form.Text className="text-danger">
|
|
66
|
+
{error}
|
|
67
|
+
</Form.Text>
|
|
68
|
+
)}
|
|
69
|
+
<div className={`form-control ${isInvalid ? 'is-invalid' : ''}`}>
|
|
70
|
+
{list.map((item: any, key) => {
|
|
71
|
+
const selected = isMultiple
|
|
72
|
+
? !!value && Array.isArray(value) && (value as Array<any>).includes(item[idKey])
|
|
73
|
+
: value === item[idKey];
|
|
74
|
+
return (
|
|
75
|
+
<BadgeSelection
|
|
76
|
+
key={key}
|
|
77
|
+
disabled={
|
|
78
|
+
typeof disabled === 'function'
|
|
79
|
+
? disabled({ list, value: item[idKey], state: {}, initialState: {}, initialValue: '' })
|
|
80
|
+
: disabled
|
|
81
|
+
}
|
|
82
|
+
selected={selected}
|
|
83
|
+
cursor='pointer'
|
|
84
|
+
onClick={() => {
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
if(selected) {
|
|
87
|
+
onChange(value.filter(id => parseInteger(id) !== parseInteger(item[idKey])).toString());
|
|
88
|
+
} else {
|
|
89
|
+
onChange(
|
|
90
|
+
integer
|
|
91
|
+
? [...value.map((v: string | number ) => parseInt(v as string)), parseInt(item[idKey])]
|
|
92
|
+
: [...value.map((v: string | number ) => `${v}`), `${item[idKey]}`]
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
onChange(item[idKey])
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
{...mergedProps}
|
|
100
|
+
>
|
|
101
|
+
{item.name}
|
|
102
|
+
</BadgeSelection>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
</Form.Group>
|
|
107
|
+
);
|
|
108
|
+
}
|