@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.
Files changed (63) hide show
  1. package/change-log.md +330 -309
  2. package/dist/components/buttons/ConfirmButton.d.ts +2 -2
  3. package/dist/components/buttons/DeleteConfirmButton.d.ts +2 -2
  4. package/dist/components/buttons/IconButtons.d.ts +40 -41
  5. package/dist/components/errors/Errors.d.ts +1 -2
  6. package/dist/components/forms/FormField.d.ts +22 -0
  7. package/dist/components/forms/FormFields.d.ts +1 -56
  8. package/dist/components/forms/FormModal.d.ts +7 -34
  9. package/dist/components/forms/FormModalProvider.d.ts +19 -26
  10. package/dist/components/forms/FormProvider.d.ts +66 -0
  11. package/dist/components/forms/fields/FormBadgesSelection.d.ts +26 -0
  12. package/dist/components/forms/fields/FormCheckbox.d.ts +7 -0
  13. package/dist/components/forms/fields/FormDropdown.d.ts +19 -0
  14. package/dist/components/forms/fields/FormInput.d.ts +17 -0
  15. package/dist/components/forms/fields/FormSelect.d.ts +12 -0
  16. package/dist/components/forms/fields/index.d.ts +5 -0
  17. package/dist/components/indicators/CheckIndicator.d.ts +1 -2
  18. package/dist/components/indicators/LoadingIndicator.d.ts +4 -4
  19. package/dist/components/login/LoginPage.d.ts +1 -1
  20. package/dist/components/tables/DataTable.d.ts +2 -2
  21. package/dist/components/tables/DragAndDropList.d.ts +2 -2
  22. package/dist/components/tables/SearchBox.d.ts +2 -2
  23. package/dist/index.d.ts +4 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.LICENSE.txt +0 -4
  26. package/dist/localization/LocalizationContext.d.ts +1 -1
  27. package/dist/utils/hooks.d.ts +1 -1
  28. package/dist/utils/timeAndDate.d.ts +5 -2
  29. package/dist/utils/utils.d.ts +3 -3
  30. package/package.json +10 -11
  31. package/src/__tests__/buttons.test.tsx +545 -0
  32. package/src/__tests__/errors.test.tsx +339 -0
  33. package/src/__tests__/forms.test.tsx +3021 -0
  34. package/src/__tests__/hooks.test.tsx +413 -0
  35. package/src/__tests__/indicators.test.tsx +284 -0
  36. package/src/__tests__/localization.test.tsx +462 -0
  37. package/src/__tests__/login.test.tsx +417 -0
  38. package/src/__tests__/setupTests.ts +328 -0
  39. package/src/__tests__/tables.test.tsx +609 -0
  40. package/src/__tests__/timeAndDate.test.tsx +308 -0
  41. package/src/__tests__/utils.test.tsx +422 -0
  42. package/src/components/forms/FormField.tsx +92 -0
  43. package/src/components/forms/FormFields.tsx +3 -423
  44. package/src/components/forms/FormModal.tsx +168 -243
  45. package/src/components/forms/FormModalProvider.tsx +141 -95
  46. package/src/components/forms/FormProvider.tsx +218 -0
  47. package/src/components/forms/fields/FormBadgesSelection.tsx +108 -0
  48. package/src/components/forms/fields/FormCheckbox.tsx +76 -0
  49. package/src/components/forms/fields/FormDropdown.tsx +123 -0
  50. package/src/components/forms/fields/FormInput.tsx +114 -0
  51. package/src/components/forms/fields/FormSelect.tsx +47 -0
  52. package/src/components/forms/fields/index.ts +6 -0
  53. package/src/index.ts +32 -28
  54. package/src/localization/LocalizationContext.tsx +156 -131
  55. package/src/localization/localization.ts +131 -131
  56. package/src/utils/hooks.ts +108 -94
  57. package/src/utils/timeAndDate.ts +33 -4
  58. package/src/utils/utils.ts +74 -66
  59. package/dist/components/forms/CreateEditModal.d.ts +0 -41
  60. package/dist/components/forms/CreateEditModalProvider.d.ts +0 -41
  61. package/dist/components/forms/FormFields.test.d.ts +0 -4
  62. package/dist/login/Login.d.ts +0 -70
  63. package/src/components/forms/FormFields.test.tsx +0 -107
@@ -1,243 +1,168 @@
1
- import React, { useEffect, useState, ReactElement, ChangeEvent, KeyboardEvent } from 'react';
2
- import { Form, Modal, Button } from 'react-bootstrap';
3
-
4
- import { SmallSpinner } from '../indicators/LoadingIndicator';
5
- import { usePrevious, useSetState } from '../../utils/hooks';
6
- import { isEmpty } from '../../utils/utils';
7
- import { useLocalization } from '../../localization/LocalizationContext';
8
- import { FormComponentProps, FormSelectProps, FormOnChange, FormValue } from './FormFields';
9
-
10
- export type FormFieldComponent = (props: FormComponentProps | FormSelectProps) => ReactElement
11
-
12
- export type FormField = {
13
- initialValue?: any;
14
- type?: 'string' | 'number';
15
- required?: boolean;
16
- formProps?: any;
17
- component?: FormFieldComponent;
18
- onChange?: FormOnChange;
19
- label?: ReactElement | string;
20
- }
21
-
22
- export type IncludeData<T> = {
23
- [key in Exclude<string, keyof T>]: any;
24
- }
25
- export type InitialState<T> = Partial<{
26
- [key in keyof T]: FormValue;
27
- }>
28
- export type FormFields = { [key: string]: FormField };
29
-
30
- export type OnSave<T, K> = (state: ({ [key in keyof T]: FormValue }), callback: () => void) => void;
31
-
32
- export type Validate = (state: any) => any
33
-
34
- export type ModalTitle = ReactElement | string;
35
-
36
- export type Width = 25 | 50 | 75 | 100;
37
- export type FormModalProps<
38
- T extends FormFields,
39
- K extends IncludeData<T>
40
- > = {
41
- initialState: InitialState<T> | K;
42
- includeData?: K;
43
- formFields: T;
44
- show?: boolean;
45
- onSave: OnSave<T, K>;
46
- onHide: () => void;
47
- validate?: Validate;
48
- modalTitle?: ModalTitle;
49
- loading?: boolean;
50
- dialogClassName?: string;
51
- width?: Width;
52
- }
53
-
54
- export const FormModal = <
55
- T extends FormFields,
56
- K extends IncludeData<T>
57
- >({
58
- initialState = {} as K,
59
- formFields,
60
- includeData = {} as K,
61
- show = true,
62
- onSave,
63
- onHide,
64
- validate,
65
- modalTitle = '',
66
- loading = false,
67
- dialogClassName='',
68
- width,
69
- ...restProps
70
- }: FormModalProps<T, K>) => {
71
-
72
- if (Object.values(restProps).length !==0) {
73
- console.error(`Unrecognised props given to FormModal:`, restProps);
74
- }
75
-
76
- if (!formFields) {
77
- console.error(`Property formFields cannot be empty.`)
78
- return null;
79
- }
80
- const getInitialFormData = () => ({
81
- ...Object.entries(formFields).reduce((o, [key, { initialValue }]) => ({ ...o, [key]: initialValue || '' }), {}),
82
- ...initialState || {},
83
- }) as { [key in keyof T]: FormValue };
84
-
85
- const [initialFormData, setInitialFormData] = useState<{ [key in keyof T]: FormValue } | null>(getInitialFormData());
86
-
87
- const [{ pristine, formData }, setState] = useSetState({
88
- pristine: true,
89
- formData: initialFormData,
90
- })
91
-
92
- const prevShow = usePrevious(show)
93
- useEffect(() => {
94
- if (prevShow && prevShow !== show) {
95
- setState({ formData: null });
96
- } else if (show && prevShow === false) {
97
- //
98
- const initialFormData = getInitialFormData();
99
- setInitialFormData(initialFormData);
100
- setState({
101
- pristine: true,
102
- formData: initialFormData,
103
- });
104
- }
105
- }, [show, prevShow])
106
- const { strings } = useLocalization();
107
-
108
- if (!formData || !initialFormData) return null;
109
- const getValue = (key: string) => {
110
- return(
111
- formData[key]
112
- ? formData[key]
113
- : (formFields[key] || {}).type === 'number' && formData[key] === 0
114
- ? '0'
115
- : ''
116
- )
117
- }
118
- const validationErrors = {
119
- ...validate
120
- ? Object.entries(validate(formData) || {})
121
- .reduce(
122
- (o, [key, val]) => {
123
- // Remove all empty elements
124
- if (isEmpty(val)) return o;
125
- return { ...o, [key]: val };
126
- },
127
- {}
128
- )
129
- : {},
130
- ...Object.keys(formData).reduce(
131
- (o, key) => {
132
- if (!formFields[key] || !formFields[key].required || !isEmpty(getValue(key))) return o;
133
- return { ...o, [key]: strings.getString('required_field') };
134
- },
135
- {}
136
- ),
137
- } as { [key: string]: any}
138
- const validated = Object.values(validationErrors).length === 0;
139
-
140
- const handleSave = () => {
141
- setState({ pristine: false })
142
- if (!validated) return;
143
- onSave(
144
- {
145
- ...formData,
146
- ...includeData,
147
- },
148
- onHide,
149
- );
150
- }
151
-
152
- return (
153
- <Modal
154
- show={show}
155
- onHide={onHide}
156
- onClick={(e: ChangeEvent<HTMLElement>) => e.stopPropagation()}
157
- centered
158
- dialogClassName={`${dialogClassName} ${width ? `mw-100 w-${width}` : ''}`}
159
- >
160
- {modalTitle && (
161
- <Modal.Header closeButton>
162
- <Modal.Title>{modalTitle}</Modal.Title>
163
- </Modal.Header>
164
- )}
165
-
166
- <Modal.Body>
167
- <Form>
168
- {Object.entries(formFields).map(
169
- ([key, { formProps = {}, label, component: Component, onChange, required }]:
170
- [string, FormField]
171
- ) => {
172
- const isInvalid = !pristine && !!(!validated && validationErrors[key]);
173
- const value = getValue(key);
174
- return (
175
- <Form.Group controlId={key} key={key}>
176
- {label && <Form.Label>{label}{required && ' *'}</Form.Label>}
177
- {isInvalid && ` (${validationErrors[key]})`}
178
- {Component
179
- ? <Component
180
- keyName={key}
181
- pristine={pristine}
182
- isInvalid={isInvalid}
183
- value={formData[key]}
184
- state={formData}
185
- setState={(newState = {}) => setState({ formData: { ...formData, ...newState} })}
186
- onChange={(value: FormValue) => setState({ formData: { ...formData, [key]: value } })}
187
- initialState={initialFormData}
188
- initialValue={initialFormData[key]}
189
- {...formProps}
190
- />
191
- : <Form.Control
192
- as="input"
193
- autoComplete="off"
194
- {...formProps}
195
- value={value}
196
- isInvalid={isInvalid}
197
- onChange={(e: ChangeEvent<HTMLInputElement>) => setState({
198
- formData:
199
- typeof onChange === 'function'
200
- ? { ...formData, ...onChange(e.target.value, formData) }
201
- : { ...formData, [key]: e.target.value }
202
- })}
203
- onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
204
- if (e.key === 'Enter' && formProps.as !== 'textarea') {
205
- // Pressing the enter key will save data unless it is a multi line text area
206
- e.preventDefault();
207
- handleSave();
208
- }
209
- }}
210
- />
211
- }
212
- </Form.Group>
213
- )
214
- })}
215
- </Form>
216
- </Modal.Body>
217
-
218
- <Modal.Footer>
219
- {loading && <SmallSpinner />}
220
- <Button
221
- variant="secondary"
222
- onClick={onHide}
223
- >
224
- {strings.getString('close')}
225
- </Button>
226
- <Button
227
- variant="primary"
228
- onClick={handleSave}
229
- >
230
- {strings.getString('save')}
231
- </Button>
232
- </Modal.Footer>
233
- </Modal>
234
- );
235
- }
236
-
237
- export const DisabledFormField = ({ value }: any) => (
238
- <Form.Control
239
- as="input"
240
- disabled
241
- value={value || ''}
242
- />
243
- );
1
+ import React, { ReactElement, ChangeEvent, KeyboardEvent } from 'react';
2
+ import { Form, Modal, Button } from 'react-bootstrap';
3
+ import { SmallSpinner } from '../indicators/LoadingIndicator';
4
+ import { useLocalization } from '../../localization/LocalizationContext';
5
+ import { useForm } from './FormProvider';
6
+ import { FormValue } from './FormFields';
7
+ import { FormInput, FormSelect, FormCheckbox, FormDropdown } from './fields';
8
+
9
+ export type ModalTitle = ReactElement | string;
10
+ export type Width = 25 | 50 | 75 | 100;
11
+
12
+ export type FormModalProps = {
13
+ show?: boolean;
14
+ onHide: () => void;
15
+ modalTitle?: ModalTitle;
16
+ dialogClassName?: string;
17
+ width?: Width;
18
+ submitText?: string;
19
+ cancelText?: string;
20
+ }
21
+
22
+ export const FormModal = ({
23
+ show = true,
24
+ onHide,
25
+ modalTitle = '',
26
+ dialogClassName = '',
27
+ width,
28
+ submitText,
29
+ cancelText,
30
+ }: FormModalProps) => {
31
+ const {
32
+ formData,
33
+ loading,
34
+ submit,
35
+ hasProvider
36
+ } = useForm();
37
+
38
+ const { strings } = useLocalization();
39
+
40
+ if (!hasProvider) {
41
+ console.error('FormModal must be used within a FormProvider');
42
+ return null;
43
+ }
44
+
45
+ if (!formData) return null;
46
+
47
+ // Get formFields from context - we'll need to add this to the context
48
+ // For now, we'll get it from the formData keys, but this is a limitation
49
+ // we might need to pass formFields to the context as well
50
+
51
+ return (
52
+ <Modal
53
+ show={show}
54
+ onHide={onHide}
55
+ onClick={(e: ChangeEvent<HTMLElement>) => e.stopPropagation()}
56
+ centered
57
+ dialogClassName={`${dialogClassName} ${width ? `mw-100 w-${width}` : ''}`}
58
+ >
59
+ {modalTitle && (
60
+ <Modal.Header closeButton>
61
+ <Modal.Title>{modalTitle}</Modal.Title>
62
+ </Modal.Header>
63
+ )}
64
+
65
+ <Modal.Body>
66
+ <FormFieldsRenderer />
67
+ </Modal.Body>
68
+
69
+ <Modal.Footer>
70
+ {loading && <SmallSpinner />}
71
+ <Button
72
+ variant="secondary"
73
+ onClick={onHide}
74
+ disabled={loading}
75
+ >
76
+ {cancelText || strings.getString('close')}
77
+ </Button>
78
+ <Button
79
+ variant="primary"
80
+ onClick={submit}
81
+ disabled={loading}
82
+ >
83
+ {submitText || strings.getString('save')}
84
+ </Button>
85
+ </Modal.Footer>
86
+ </Modal>
87
+ );
88
+ };
89
+
90
+ // Component that renders all form fields using the form context
91
+ export const FormFieldsRenderer = () => {
92
+ const { formFields, hasProvider } = useForm();
93
+
94
+ if (!hasProvider || !formFields) {
95
+ return null;
96
+ }
97
+
98
+ return (
99
+ <Form>
100
+ {Object.entries(formFields).map(([name, config]) => {
101
+ // Renderer decides which component to use based on config
102
+ if (config.component) {
103
+ // Custom component specified in config
104
+ const Component = config.component;
105
+ return (
106
+ <Component
107
+ key={name}
108
+ name={name}
109
+ {...config.formProps}
110
+ />
111
+ );
112
+ }
113
+
114
+ // Built-in component selection based on config properties
115
+ if (config.type === 'select' && config.options) {
116
+ return (
117
+ <FormSelect
118
+ key={name}
119
+ name={name}
120
+ options={config.options}
121
+ {...config.formProps}
122
+ />
123
+ );
124
+ }
125
+
126
+ if (config.type === 'dropdown' && config.list) {
127
+ return (
128
+ <FormDropdown
129
+ key={name}
130
+ name={name}
131
+ list={config.list}
132
+ idKey={config.idKey}
133
+ nameKey={config.nameKey}
134
+ {...config.formProps}
135
+ />
136
+ );
137
+ }
138
+
139
+ if (config.type === 'checkbox' || config.type === 'boolean') {
140
+ return (
141
+ <FormCheckbox
142
+ key={name}
143
+ name={name}
144
+ {...config.formProps}
145
+ />
146
+ );
147
+ }
148
+
149
+ // Default to FormInput for most cases
150
+ return (
151
+ <FormInput
152
+ key={name}
153
+ name={name}
154
+ {...config.formProps}
155
+ />
156
+ );
157
+ })}
158
+ </Form>
159
+ );
160
+ };
161
+
162
+ export const DisabledFormField = ({ value }: any) => (
163
+ <Form.Control
164
+ as="input"
165
+ disabled
166
+ value={value || ''}
167
+ />
168
+ );