@jasperoosthoek/react-toolbox 0.8.0 → 0.9.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/change-log.md +330 -309
- 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 +19 -26
- 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 +4 -1
- 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 +141 -95
- 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 -28
- 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,90 @@
|
|
|
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
|
|
|
18
8
|
export type ShowCreateModal = (show?: boolean) => void;
|
|
19
|
-
export type ShowEditModal<T
|
|
9
|
+
export type ShowEditModal<T extends FormFields> = (state: InitialState<T> | null) => void;
|
|
10
|
+
|
|
11
|
+
type FormModalContextType<T extends FormFields> = {
|
|
12
|
+
showCreateModal: ShowCreateModal;
|
|
13
|
+
showEditModal: ShowEditModal<T>;
|
|
14
|
+
hasProvider: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const defaultErrorState: FormModalContextType<any> = {
|
|
18
|
+
showCreateModal: () => {
|
|
19
|
+
console.error('The showCreateModal function should only be used in a child of the FormModalProvider component.');
|
|
20
|
+
},
|
|
21
|
+
showEditModal: () => {
|
|
22
|
+
console.error('The showEditModal function should only be used in a child of the FormModalProvider component.');
|
|
23
|
+
},
|
|
24
|
+
hasProvider: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const FormModalContext = React.createContext(defaultErrorState);
|
|
28
|
+
|
|
29
|
+
export const useFormModal = <T extends FormFields>() => useContext(FormModalContext) as FormModalContextType<T>;
|
|
20
30
|
|
|
21
31
|
export type FormCreateModalButton = ButtonProps;
|
|
22
32
|
export const FormCreateModalButton = ({ onClick, ...props }: ButtonProps) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
33
|
+
const context = useContext(FormModalContext);
|
|
34
|
+
|
|
25
35
|
return (
|
|
26
36
|
<CreateButton
|
|
27
37
|
{...props}
|
|
28
38
|
onClick={(e) => {
|
|
29
39
|
// A onClick function was given to CreateButton
|
|
30
40
|
if (onClick) onClick(e);
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
if (hasProvider)
|
|
41
|
+
|
|
42
|
+
// Check if we have a provider context
|
|
43
|
+
if (!context || !context.hasProvider) {
|
|
44
|
+
console.error('The showCreateModal function should only be used in a child of the FormModalProvider component.');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// CreateButton is inside a FormModalProvider which can handle showCreateModal
|
|
49
|
+
context.showCreateModal();
|
|
34
50
|
}}
|
|
35
51
|
/>
|
|
36
52
|
)
|
|
37
53
|
}
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
|
|
55
|
+
export interface FormEditModalButtonProps<T extends FormFields> extends ButtonProps {
|
|
56
|
+
state: InitialState<T>;
|
|
40
57
|
}
|
|
41
|
-
export const FormEditModalButton = ({ state, onClick, ... props }: FormEditModalButtonProps<T, K>) => {
|
|
42
|
-
const { showEditModal, hasProvider } = useFormModal();
|
|
43
58
|
|
|
59
|
+
export const FormEditModalButton = <T extends FormFields>({ state, onClick, ...props }: FormEditModalButtonProps<T>) => {
|
|
60
|
+
const context = useContext(FormModalContext);
|
|
61
|
+
|
|
44
62
|
return (
|
|
45
63
|
<EditButton
|
|
46
64
|
{...props}
|
|
47
65
|
onClick={(e) => {
|
|
48
|
-
// A onClick function was given to
|
|
66
|
+
// A onClick function was given to EditButton
|
|
49
67
|
if (onClick) onClick(e);
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
if (hasProvider)
|
|
68
|
+
|
|
69
|
+
// Check if we have a provider context
|
|
70
|
+
if (!context || !context.hasProvider) {
|
|
71
|
+
console.error('The showEditModal function should only be used in a child of the FormModalProvider component.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// EditButton is inside a FormModalProvider which can handle showEditModal
|
|
76
|
+
context.showEditModal(state);
|
|
53
77
|
}}
|
|
54
78
|
/>
|
|
55
79
|
)
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
type
|
|
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
|
-
export type FormModalProviderProps<
|
|
81
|
-
T extends FormFields,
|
|
82
|
-
K extends IncludeData<T>
|
|
83
|
-
> = {
|
|
84
|
-
initialState: InitialState<T> | K;
|
|
85
|
-
includeData?: K;
|
|
82
|
+
export type FormModalProviderProps<T extends FormFields> = {
|
|
86
83
|
formFields: T;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
initialState?: InitialState<T>;
|
|
85
|
+
onSave?: OnSubmit<T>;
|
|
86
|
+
onCreate?: OnSubmit<T>;
|
|
87
|
+
onUpdate?: OnSubmit<T>;
|
|
90
88
|
validate?: Validate;
|
|
91
89
|
createModalTitle?: ModalTitle;
|
|
92
90
|
editModalTitle?: ModalTitle;
|
|
@@ -95,58 +93,106 @@ export type FormModalProviderProps<
|
|
|
95
93
|
width?: Width;
|
|
96
94
|
children: ReactNode;
|
|
97
95
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
|
|
97
|
+
export const FormModalProvider = <T extends FormFields>({
|
|
98
|
+
createModalTitle,
|
|
99
|
+
editModalTitle,
|
|
100
|
+
formFields,
|
|
101
|
+
initialState = {} as InitialState<T>,
|
|
103
102
|
validate,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
loading = false,
|
|
104
|
+
onCreate,
|
|
105
|
+
onUpdate,
|
|
107
106
|
onSave,
|
|
108
107
|
dialogClassName,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const [
|
|
108
|
+
width,
|
|
109
|
+
children,
|
|
110
|
+
}: FormModalProviderProps<T>) => {
|
|
111
|
+
const [createModalActive, setCreateModalActive] = useState(false);
|
|
112
|
+
const [editModalState, setEditModalState] = useState<InitialState<T> | null>(null);
|
|
113
113
|
const { strings } = useLocalization();
|
|
114
|
+
|
|
115
|
+
const handleCreateSubmit: OnSubmit<T> = (state, callback) => {
|
|
116
|
+
// Priority: onCreate > onSave fallback
|
|
117
|
+
const submitHandler = onCreate || onSave;
|
|
118
|
+
if (submitHandler) {
|
|
119
|
+
// For testing purposes and direct submission, always call the handler
|
|
120
|
+
// The FormProvider's submit() will call this with current form state
|
|
121
|
+
submitHandler(state, () => {
|
|
122
|
+
setCreateModalActive(false);
|
|
123
|
+
if (callback) callback();
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
// No handler provided, just close modal
|
|
127
|
+
setCreateModalActive(false);
|
|
128
|
+
if (callback) callback();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleEditSubmit: OnSubmit<T> = (state, callback) => {
|
|
133
|
+
// Priority: onUpdate > onSave fallback
|
|
134
|
+
const submitHandler = onUpdate || onSave;
|
|
135
|
+
if (submitHandler) {
|
|
136
|
+
submitHandler(state, () => {
|
|
137
|
+
setEditModalState(null);
|
|
138
|
+
if (callback) callback();
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
// No handler provided, just close modal
|
|
142
|
+
setEditModalState(null);
|
|
143
|
+
if (callback) callback();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
114
147
|
return (
|
|
115
148
|
<FormModalContext.Provider
|
|
116
149
|
value={{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
150
|
+
showCreateModal: (show?: boolean) =>
|
|
151
|
+
setCreateModalActive(typeof show === 'undefined' ? true : show),
|
|
152
|
+
showEditModal: (
|
|
153
|
+
(state: InitialState<T> | null) => setEditModalState(state)
|
|
154
|
+
) as (state: Partial<{ [x: string]: FormValue; }> | null) => void,
|
|
120
155
|
hasProvider: true,
|
|
121
|
-
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
122
158
|
{children}
|
|
123
|
-
|
|
159
|
+
|
|
124
160
|
{createModalActive && (onCreate || onSave) && (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
formFields={formFields}
|
|
161
|
+
<FormProvider
|
|
162
|
+
formFields={formFields}
|
|
163
|
+
initialState={initialState}
|
|
164
|
+
onSubmit={handleCreateSubmit}
|
|
130
165
|
validate={validate}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
loading={loading}
|
|
167
|
+
resetTrigger={createModalActive}
|
|
168
|
+
>
|
|
169
|
+
<FormModal
|
|
170
|
+
show={createModalActive}
|
|
171
|
+
modalTitle={createModalTitle || strings.getString('modal_create')}
|
|
172
|
+
onHide={() => setCreateModalActive(false)}
|
|
173
|
+
dialogClassName={dialogClassName}
|
|
174
|
+
width={width}
|
|
175
|
+
/>
|
|
176
|
+
</FormProvider>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{editModalState && (onUpdate || onSave) && (
|
|
180
|
+
<FormProvider
|
|
143
181
|
formFields={formFields}
|
|
182
|
+
initialState={editModalState}
|
|
183
|
+
onSubmit={handleEditSubmit}
|
|
144
184
|
validate={validate}
|
|
145
185
|
loading={loading}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
186
|
+
resetTrigger={editModalState}
|
|
187
|
+
>
|
|
188
|
+
<FormModal
|
|
189
|
+
show={!!editModalState}
|
|
190
|
+
modalTitle={editModalTitle || strings.getString('modal_edit')}
|
|
191
|
+
onHide={() => setEditModalState(null)}
|
|
192
|
+
dialogClassName={dialogClassName}
|
|
193
|
+
width={width}
|
|
194
|
+
/>
|
|
195
|
+
</FormProvider>
|
|
150
196
|
)}
|
|
151
197
|
</FormModalContext.Provider>
|
|
152
198
|
);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Form } from 'react-bootstrap';
|
|
3
|
+
import { useFormField } from '../FormField';
|
|
4
|
+
|
|
5
|
+
export interface FormCheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'type'> {
|
|
6
|
+
name: string;
|
|
7
|
+
label?: React.ReactElement | string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const FormCheckbox = (props: FormCheckboxProps) => {
|
|
11
|
+
const { value, onChange, isInvalid, error, label, required, mergedProps } = useFormField(props);
|
|
12
|
+
|
|
13
|
+
const errorId = isInvalid && error ? `${props.name}-error` : undefined;
|
|
14
|
+
const checkboxId = `${props.name}-checkbox`; // Use different ID to avoid conflict
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Form.Group controlId={props.name}>
|
|
18
|
+
{isInvalid && error && (
|
|
19
|
+
<Form.Text id={errorId} className="text-danger">
|
|
20
|
+
{error}
|
|
21
|
+
</Form.Text>
|
|
22
|
+
)}
|
|
23
|
+
<Form.Check
|
|
24
|
+
id={checkboxId}
|
|
25
|
+
type="checkbox"
|
|
26
|
+
{...mergedProps}
|
|
27
|
+
checked={!!value}
|
|
28
|
+
isInvalid={isInvalid}
|
|
29
|
+
aria-describedby={errorId}
|
|
30
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
31
|
+
onChange(e.target.checked);
|
|
32
|
+
}}
|
|
33
|
+
label={label ? (
|
|
34
|
+
<span>
|
|
35
|
+
{label}
|
|
36
|
+
{required && ' *'}
|
|
37
|
+
</span>
|
|
38
|
+
) : undefined}
|
|
39
|
+
/>
|
|
40
|
+
</Form.Group>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const FormSwitch = (props: FormCheckboxProps) => {
|
|
45
|
+
const { value, onChange, isInvalid, error, label, required, mergedProps } = useFormField(props);
|
|
46
|
+
|
|
47
|
+
const errorId = isInvalid && error ? `${props.name}-error` : undefined;
|
|
48
|
+
const switchId = `${props.name}-switch`; // Use different ID to avoid conflict
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Form.Group controlId={props.name}>
|
|
52
|
+
{isInvalid && error && (
|
|
53
|
+
<Form.Text id={errorId} className="text-danger">
|
|
54
|
+
{error}
|
|
55
|
+
</Form.Text>
|
|
56
|
+
)}
|
|
57
|
+
<Form.Check
|
|
58
|
+
id={switchId}
|
|
59
|
+
type="switch"
|
|
60
|
+
{...mergedProps}
|
|
61
|
+
checked={!!value}
|
|
62
|
+
isInvalid={isInvalid}
|
|
63
|
+
aria-describedby={errorId}
|
|
64
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
65
|
+
onChange(e.target.checked);
|
|
66
|
+
}}
|
|
67
|
+
label={label ? (
|
|
68
|
+
<span>
|
|
69
|
+
{label}
|
|
70
|
+
{required && ' *'}
|
|
71
|
+
</span>
|
|
72
|
+
) : undefined}
|
|
73
|
+
/>
|
|
74
|
+
</Form.Group>
|
|
75
|
+
);
|
|
76
|
+
};
|