@dhis2/ui-forms 10.16.2 → 10.16.3
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/package.json +12 -11
- package/src/CheckboxFieldFF/CheckboxFieldFF.e2e.stories.js +52 -0
- package/src/CheckboxFieldFF/CheckboxFieldFF.js +52 -0
- package/src/CheckboxFieldFF/CheckboxFieldFF.prod.stories.js +142 -0
- package/src/CheckboxFieldFF/features/can_toggle_a_boolean/index.js +19 -0
- package/src/CheckboxFieldFF/features/can_toggle_a_boolean.feature +11 -0
- package/src/CheckboxFieldFF/features/can_toggle_a_value/index.js +21 -0
- package/src/CheckboxFieldFF/features/can_toggle_a_value.feature +11 -0
- package/src/CheckboxFieldFF/features/common/index.js +5 -0
- package/src/CheckboxFieldFF/features/displays_error/index.js +18 -0
- package/src/CheckboxFieldFF/features/displays_error.feature +6 -0
- package/src/FieldGroupFF/FieldGroupFF.js +42 -0
- package/src/FieldGroupFF/FieldGroupFF.prod.stories.js +49 -0
- package/src/FileInputFieldFF/FileInputFieldFF.e2e.stories.js +272 -0
- package/src/FileInputFieldFF/FileInputFieldFF.js +100 -0
- package/src/FileInputFieldFF/FileInputFieldFF.prod.stories.js +95 -0
- package/src/FileInputFieldFF/features/accepts_file/index.js +40 -0
- package/src/FileInputFieldFF/features/accepts_file.feature +13 -0
- package/src/FileInputFieldFF/features/common/index.js +9 -0
- package/src/FileInputFieldFF/features/displays_error/index.js +18 -0
- package/src/FileInputFieldFF/features/displays_error.feature +8 -0
- package/src/InputFieldFF/InputFieldFF.e2e.stories.js +19 -0
- package/src/InputFieldFF/InputFieldFF.js +58 -0
- package/src/InputFieldFF/InputFieldFF.prod.stories.js +102 -0
- package/src/InputFieldFF/features/can_set_a_value/index.js +14 -0
- package/src/InputFieldFF/features/can_set_a_value.feature +6 -0
- package/src/InputFieldFF/features/displays_error/index.js +15 -0
- package/src/InputFieldFF/features/displays_error.feature +6 -0
- package/src/MultiSelectFieldFF/MultiSelectFieldFF.e2e.stories.js +27 -0
- package/src/MultiSelectFieldFF/MultiSelectFieldFF.js +72 -0
- package/src/MultiSelectFieldFF/MultiSelectFieldFF.prod.stories.js +79 -0
- package/src/MultiSelectFieldFF/features/can_set_a_value/index.js +38 -0
- package/src/MultiSelectFieldFF/features/can_set_a_value.feature +14 -0
- package/src/MultiSelectFieldFF/features/common/index.js +7 -0
- package/src/MultiSelectFieldFF/features/displays_error/index.js +10 -0
- package/src/MultiSelectFieldFF/features/displays_error.feature +6 -0
- package/src/RadioFieldFF/RadioFieldFF.e2e.stories.js +39 -0
- package/src/RadioFieldFF/RadioFieldFF.js +52 -0
- package/src/RadioFieldFF/RadioFieldFF.prod.stories.js +104 -0
- package/src/RadioFieldFF/features/can_set_a_value/index.js +24 -0
- package/src/RadioFieldFF/features/can_set_a_value.feature +7 -0
- package/src/RadioFieldFF/features/common/index.js +9 -0
- package/src/RadioFieldFF/features/displays_error/index.js +13 -0
- package/src/RadioFieldFF/features/displays_error.feature +6 -0
- package/src/SimpleSingleSelectFieldFF/SimpleSingleSelectFieldFF.js +172 -0
- package/src/SimpleSingleSelectFieldFF/SimpleSingleSelectFieldFF.prod.stories.js +79 -0
- package/src/SimpleSingleSelectFieldFF/SimpleSingleSelectFieldFF.test.js +82 -0
- package/src/SingleSelectFieldFF/SingleSelectFieldFF.e2e.stories.js +23 -0
- package/src/SingleSelectFieldFF/SingleSelectFieldFF.js +70 -0
- package/src/SingleSelectFieldFF/SingleSelectFieldFF.prod.stories.js +77 -0
- package/src/SingleSelectFieldFF/features/can_set_a_value/index.js +22 -0
- package/src/SingleSelectFieldFF/features/can_set_a_value.feature +7 -0
- package/src/SingleSelectFieldFF/features/common/index.js +6 -0
- package/src/SingleSelectFieldFF/features/displays_error/index.js +10 -0
- package/src/SingleSelectFieldFF/features/displays_error.feature +6 -0
- package/src/SwitchFieldFF/SwitchFieldFF.e2e.stories.js +52 -0
- package/src/SwitchFieldFF/SwitchFieldFF.js +52 -0
- package/src/SwitchFieldFF/SwitchFieldFF.prod.stories.js +148 -0
- package/src/SwitchFieldFF/features/can_toggle_a_boolean/index.js +19 -0
- package/src/SwitchFieldFF/features/can_toggle_a_boolean.feature +11 -0
- package/src/SwitchFieldFF/features/can_toggle_a_value/index.js +21 -0
- package/src/SwitchFieldFF/features/can_toggle_a_value.feature +11 -0
- package/src/SwitchFieldFF/features/common/index.js +5 -0
- package/src/SwitchFieldFF/features/displays_error/index.js +18 -0
- package/src/SwitchFieldFF/features/displays_error.feature +6 -0
- package/src/TextAreaFieldFF/TextAreaFieldFF.e2e.stories.js +23 -0
- package/src/TextAreaFieldFF/TextAreaFieldFF.js +57 -0
- package/src/TextAreaFieldFF/TextAreaFieldFF.prod.stories.js +111 -0
- package/src/TextAreaFieldFF/features/can_set_a_value/index.js +14 -0
- package/src/TextAreaFieldFF/features/can_set_a_value.feature +6 -0
- package/src/TextAreaFieldFF/features/displays_error/index.js +15 -0
- package/src/TextAreaFieldFF/features/displays_error.feature +6 -0
- package/src/__tests__/__snapshots__/index.test.js.snap +65 -0
- package/src/__tests__/index.test.js +37 -0
- package/src/formDecorator.js +80 -0
- package/src/index.js +28 -0
- package/src/locales/ar/translations.json +30 -0
- package/src/locales/ar_IQ/translations.json +30 -0
- package/src/locales/ckb/translations.json +30 -0
- package/src/locales/cs/translations.json +30 -0
- package/src/locales/da/translations.json +30 -0
- package/src/locales/en/translations.json +30 -0
- package/src/locales/es/translations.json +30 -0
- package/src/locales/es_419/translations.json +30 -0
- package/src/locales/fr/translations.json +30 -0
- package/src/locales/hi_IN/translations.json +30 -0
- package/src/locales/id/translations.json +30 -0
- package/src/locales/index.js +84 -0
- package/src/locales/km/translations.json +30 -0
- package/src/locales/ko_KR/translations.json +30 -0
- package/src/locales/lo/translations.json +30 -0
- package/src/locales/my/translations.json +30 -0
- package/src/locales/nb/translations.json +30 -0
- package/src/locales/nl/translations.json +30 -0
- package/src/locales/prs/translations.json +30 -0
- package/src/locales/ps/translations.json +30 -0
- package/src/locales/pt/translations.json +30 -0
- package/src/locales/pt_BR/translations.json +30 -0
- package/src/locales/ro/translations.json +30 -0
- package/src/locales/ru/translations.json +30 -0
- package/src/locales/si/translations.json +30 -0
- package/src/locales/sv/translations.json +30 -0
- package/src/locales/tet/translations.json +30 -0
- package/src/locales/tg/translations.json +30 -0
- package/src/locales/uk/translations.json +30 -0
- package/src/locales/ur/translations.json +30 -0
- package/src/locales/uz_Latn/translations.json +30 -0
- package/src/locales/uz_UZ_Cyrl/translations.json +30 -0
- package/src/locales/uz_UZ_Latn/translations.json +30 -0
- package/src/locales/vi/translations.json +30 -0
- package/src/locales/zh/translations.json +30 -0
- package/src/locales/zh_CN/translations.json +30 -0
- package/src/shared/helpers/createBlurHandler.js +9 -0
- package/src/shared/helpers/createChangeHandler.js +21 -0
- package/src/shared/helpers/createFocusHandler.js +9 -0
- package/src/shared/helpers/createSelectChangeHandler.js +6 -0
- package/src/shared/helpers/createToggleChangeHandler.js +9 -0
- package/src/shared/helpers/getValidationText.js +21 -0
- package/src/shared/helpers/hasError.js +3 -0
- package/src/shared/helpers/isLoading.js +4 -0
- package/src/shared/helpers/isValid.js +4 -0
- package/src/shared/helpers.js +9 -0
- package/src/shared/propTypes.js +48 -0
- package/src/transformers/arrayWithIdObjects.js +8 -0
- package/src/transformers/index.js +1 -0
- package/src/validators/__tests__/alphaNumeric.test.js +29 -0
- package/src/validators/__tests__/boolean.test.js +23 -0
- package/src/validators/__tests__/composeValidators.test.js +23 -0
- package/src/validators/__tests__/createCharacterLengthRange.test.js +59 -0
- package/src/validators/__tests__/createEqualTo.test.js +43 -0
- package/src/validators/__tests__/createMaxCharacterLength.test.js +24 -0
- package/src/validators/__tests__/createMaxNumber.test.js +21 -0
- package/src/validators/__tests__/createMinCharacterLength.test.js +24 -0
- package/src/validators/__tests__/createMinNumber.test.js +21 -0
- package/src/validators/__tests__/createNumberRange.test.js +68 -0
- package/src/validators/__tests__/createPattern.test.js +40 -0
- package/src/validators/__tests__/dhis2Password.test.js +51 -0
- package/src/validators/__tests__/dhis2Username.test.js +75 -0
- package/src/validators/__tests__/email.test.js +83 -0
- package/src/validators/__tests__/hasValue.test.js +21 -0
- package/src/validators/__tests__/integer.test.js +48 -0
- package/src/validators/__tests__/internationalPhoneNumber.test.js +49 -0
- package/src/validators/__tests__/number.test.js +39 -0
- package/src/validators/__tests__/string.test.js +29 -0
- package/src/validators/__tests__/url.test.js +106 -0
- package/src/validators/alphaNumeric.js +15 -0
- package/src/validators/boolean.js +11 -0
- package/src/validators/composeValidators.js +8 -0
- package/src/validators/createCharacterLengthRange.js +27 -0
- package/src/validators/createEqualTo.js +16 -0
- package/src/validators/createMaxCharacterLength.js +13 -0
- package/src/validators/createMaxNumber.js +13 -0
- package/src/validators/createMinCharacterLength.js +13 -0
- package/src/validators/createMinNumber.js +13 -0
- package/src/validators/createNumberRange.js +28 -0
- package/src/validators/createPattern.js +22 -0
- package/src/validators/dhis2Password.js +81 -0
- package/src/validators/dhis2Username.js +16 -0
- package/src/validators/email.js +38 -0
- package/src/validators/hasValue.js +8 -0
- package/src/validators/helpers/index.js +23 -0
- package/src/validators/index.js +20 -0
- package/src/validators/integer.js +20 -0
- package/src/validators/internationalPhoneNumber.js +56 -0
- package/src/validators/number.js +9 -0
- package/src/validators/string.js +9 -0
- package/src/validators/test-helpers/index.js +21 -0
- package/src/validators/url.js +15 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Field } from 'react-final-form'
|
|
3
|
+
import { CheckboxFieldFF } from '../CheckboxFieldFF/CheckboxFieldFF.js'
|
|
4
|
+
import { FieldGroupFF } from '../FieldGroupFF/FieldGroupFF.js'
|
|
5
|
+
import { formDecorator } from '../formDecorator.js'
|
|
6
|
+
import { InputFieldFF } from '../InputFieldFF/InputFieldFF.js'
|
|
7
|
+
import { MultiSelectFieldFF } from '../MultiSelectFieldFF/MultiSelectFieldFF.js'
|
|
8
|
+
import { RadioFieldFF } from '../RadioFieldFF/RadioFieldFF.js'
|
|
9
|
+
import { SingleSelectFieldFF } from '../SingleSelectFieldFF/SingleSelectFieldFF.js'
|
|
10
|
+
import { SwitchFieldFF } from '../SwitchFieldFF/SwitchFieldFF.js'
|
|
11
|
+
import { TextAreaFieldFF } from '../TextAreaFieldFF/TextAreaFieldFF.js'
|
|
12
|
+
import { composeValidators, email, hasValue } from '../validators/index.js'
|
|
13
|
+
import { FileInputFieldFF } from './FileInputFieldFF.js'
|
|
14
|
+
|
|
15
|
+
const Form = ({ values }) => {
|
|
16
|
+
return (
|
|
17
|
+
<div style={{ maxWidth: 830 }}>
|
|
18
|
+
<Field
|
|
19
|
+
name="gender"
|
|
20
|
+
label="Gender"
|
|
21
|
+
component={SingleSelectFieldFF}
|
|
22
|
+
initialValue=""
|
|
23
|
+
options={[
|
|
24
|
+
{ value: '', label: 'Please choose' },
|
|
25
|
+
{ value: 'mr', label: 'Mr.' },
|
|
26
|
+
{ value: 'ms', label: 'Ms.' },
|
|
27
|
+
{ value: 'other', label: 'Other' },
|
|
28
|
+
{
|
|
29
|
+
value: 'unknown',
|
|
30
|
+
label: "I'd rather not say",
|
|
31
|
+
},
|
|
32
|
+
]}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<Field
|
|
36
|
+
required
|
|
37
|
+
label="First name"
|
|
38
|
+
name="fname"
|
|
39
|
+
validate={hasValue}
|
|
40
|
+
component={InputFieldFF}
|
|
41
|
+
helpText="Please enter your first name, excluding middle names"
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<Field
|
|
45
|
+
required
|
|
46
|
+
label="Last name"
|
|
47
|
+
name="lname"
|
|
48
|
+
validate={hasValue}
|
|
49
|
+
component={InputFieldFF}
|
|
50
|
+
helpText="Please enter your first name, excluding middle names"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<Field
|
|
54
|
+
name="subscribe"
|
|
55
|
+
initialValue={true}
|
|
56
|
+
type="checkbox"
|
|
57
|
+
label="I want to receive updated and notifications about the latest changes?"
|
|
58
|
+
component={SwitchFieldFF}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
{values.subscribe && (
|
|
62
|
+
<Field
|
|
63
|
+
required={values.subscribe}
|
|
64
|
+
label="E-mail address"
|
|
65
|
+
name="email1"
|
|
66
|
+
validate={composeValidators(email, (value) => {
|
|
67
|
+
if (values.subscribe && !value) {
|
|
68
|
+
return 'You need to provide an e-mail address'
|
|
69
|
+
}
|
|
70
|
+
})}
|
|
71
|
+
component={InputFieldFF}
|
|
72
|
+
helpText="Please enter the e-mail address you want us to send the updates to"
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{values.subscribe && (
|
|
77
|
+
<Field
|
|
78
|
+
disabled={!values.subscribe}
|
|
79
|
+
required={values.subscribe}
|
|
80
|
+
label="E-mail address confirmation"
|
|
81
|
+
name="email2"
|
|
82
|
+
validate={composeValidators(email, hasValue)}
|
|
83
|
+
component={InputFieldFF}
|
|
84
|
+
helpText="Please confirm your e-mail address"
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<FieldGroupFF
|
|
89
|
+
name="food"
|
|
90
|
+
label="Food"
|
|
91
|
+
required
|
|
92
|
+
helpText="If we ever gather for food, what meal type would you like to eat"
|
|
93
|
+
>
|
|
94
|
+
<Field
|
|
95
|
+
type="radio"
|
|
96
|
+
name="food"
|
|
97
|
+
required
|
|
98
|
+
component={RadioFieldFF}
|
|
99
|
+
value="anything"
|
|
100
|
+
label="Don't care"
|
|
101
|
+
validate={hasValue}
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<Field
|
|
105
|
+
type="radio"
|
|
106
|
+
name="food"
|
|
107
|
+
component={RadioFieldFF}
|
|
108
|
+
required
|
|
109
|
+
value="vegan"
|
|
110
|
+
label="Vegan"
|
|
111
|
+
validate={hasValue}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<Field
|
|
115
|
+
type="radio"
|
|
116
|
+
name="food"
|
|
117
|
+
component={RadioFieldFF}
|
|
118
|
+
required
|
|
119
|
+
value="vegetarian"
|
|
120
|
+
label="Vegetarian"
|
|
121
|
+
validate={hasValue}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<Field
|
|
125
|
+
type="radio"
|
|
126
|
+
name="food"
|
|
127
|
+
component={RadioFieldFF}
|
|
128
|
+
required
|
|
129
|
+
value="fish"
|
|
130
|
+
label="Fish"
|
|
131
|
+
validate={hasValue}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<Field
|
|
135
|
+
type="radio"
|
|
136
|
+
name="food"
|
|
137
|
+
required
|
|
138
|
+
component={RadioFieldFF}
|
|
139
|
+
value="Halal"
|
|
140
|
+
label="halal"
|
|
141
|
+
validate={hasValue}
|
|
142
|
+
/>
|
|
143
|
+
</FieldGroupFF>
|
|
144
|
+
|
|
145
|
+
<FieldGroupFF
|
|
146
|
+
name="pizzaToppings"
|
|
147
|
+
label="Pizza toppings"
|
|
148
|
+
helpText="If we ever order pizza, what ingredients would you like on top"
|
|
149
|
+
>
|
|
150
|
+
<Field
|
|
151
|
+
type="checkbox"
|
|
152
|
+
name="pizzaToppings"
|
|
153
|
+
component={CheckboxFieldFF}
|
|
154
|
+
label="Everything"
|
|
155
|
+
value="everything"
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<Field
|
|
159
|
+
type="checkbox"
|
|
160
|
+
name="pizzaToppings"
|
|
161
|
+
component={CheckboxFieldFF}
|
|
162
|
+
label="Ham"
|
|
163
|
+
value="ham"
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<Field
|
|
167
|
+
type="checkbox"
|
|
168
|
+
name="pizzaToppings"
|
|
169
|
+
component={CheckboxFieldFF}
|
|
170
|
+
label="Salami"
|
|
171
|
+
value="salami"
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
<Field
|
|
175
|
+
type="checkbox"
|
|
176
|
+
name="pizzaToppings"
|
|
177
|
+
component={CheckboxFieldFF}
|
|
178
|
+
label="Pineapple"
|
|
179
|
+
value="pineapple"
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<Field
|
|
183
|
+
type="checkbox"
|
|
184
|
+
name="pizzaToppings"
|
|
185
|
+
component={CheckboxFieldFF}
|
|
186
|
+
label="Bellpepper"
|
|
187
|
+
value="bellpepper"
|
|
188
|
+
/>
|
|
189
|
+
</FieldGroupFF>
|
|
190
|
+
|
|
191
|
+
<Field
|
|
192
|
+
label="Sandwich toppings"
|
|
193
|
+
name="sandwhichToppings"
|
|
194
|
+
component={MultiSelectFieldFF}
|
|
195
|
+
options={[
|
|
196
|
+
{ value: '', label: 'All of the options' },
|
|
197
|
+
{ value: 'ham', label: 'Ham' },
|
|
198
|
+
{ value: 'salami', label: 'Salami' },
|
|
199
|
+
{ value: 'pineapple', label: 'Pineapple' },
|
|
200
|
+
{ value: 'bellpepper', label: 'Bellpepper' },
|
|
201
|
+
]}
|
|
202
|
+
helpText="If we ever order sandwiches, what ingredients would you like on top"
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
<Field
|
|
206
|
+
name="message"
|
|
207
|
+
label="If you want to tell us anything, just add your message here"
|
|
208
|
+
component={TextAreaFieldFF}
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
<Field
|
|
212
|
+
name="fileTxt"
|
|
213
|
+
accept=".txt"
|
|
214
|
+
label="If you want to send us a txt file, please attach it here"
|
|
215
|
+
className="fileTxt"
|
|
216
|
+
validate={(files) => {
|
|
217
|
+
if (!files) {
|
|
218
|
+
return undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const [file] = files
|
|
222
|
+
if (file.type !== 'text/plain') {
|
|
223
|
+
return `The file you provided is not a txt file, received "${file.type}"`
|
|
224
|
+
}
|
|
225
|
+
}}
|
|
226
|
+
component={FileInputFieldFF}
|
|
227
|
+
/>
|
|
228
|
+
|
|
229
|
+
<Field
|
|
230
|
+
multiple
|
|
231
|
+
accept="image/jpg"
|
|
232
|
+
name="fileJpgs"
|
|
233
|
+
label="If you want to send us some picture file, please attach it here"
|
|
234
|
+
validate={(files) => {
|
|
235
|
+
if (!files) {
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return files.reduce((error, file) => {
|
|
240
|
+
if (error) {
|
|
241
|
+
return error
|
|
242
|
+
}
|
|
243
|
+
if (file.type !== 'application/jpg') {
|
|
244
|
+
return `One of the files is not a jpg, received "${file.type}"`
|
|
245
|
+
}
|
|
246
|
+
}, undefined)
|
|
247
|
+
}}
|
|
248
|
+
component={FileInputFieldFF}
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
<Field
|
|
252
|
+
required
|
|
253
|
+
name="tnc"
|
|
254
|
+
label="I accept the terms and conditions"
|
|
255
|
+
validate={hasValue}
|
|
256
|
+
component={CheckboxFieldFF}
|
|
257
|
+
type="checkbox"
|
|
258
|
+
className="checkbox"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default {
|
|
265
|
+
title: 'Testing:FileInput',
|
|
266
|
+
decorators: [formDecorator],
|
|
267
|
+
parameters: { options: { showPanel: false } },
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export const StandardForm = (_, { formRenderProps }) => (
|
|
271
|
+
<Form {...formRenderProps} />
|
|
272
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { FileListItem, FileInputField } from '@dhis2-ui/file-input'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import i18n from '../locales/index.js'
|
|
5
|
+
import { hasError, isValid, getValidationText } from '../shared/helpers.js'
|
|
6
|
+
import { inputPropType, metaPropType } from '../shared/propTypes.js'
|
|
7
|
+
|
|
8
|
+
const btnLabel = i18n.t('Upload file')
|
|
9
|
+
const btnLabelMulti = i18n.t('Upload files')
|
|
10
|
+
|
|
11
|
+
const dedupeAndConcat = (currentFiles, newFileList) => {
|
|
12
|
+
return [...currentFiles, ...newFileList].reduceRight(
|
|
13
|
+
(acc, file) => {
|
|
14
|
+
if (!acc.unique.has(file.name)) {
|
|
15
|
+
acc.unique.add(file.name)
|
|
16
|
+
acc.files.unshift(file)
|
|
17
|
+
}
|
|
18
|
+
return acc
|
|
19
|
+
},
|
|
20
|
+
{ files: [], unique: new Set() }
|
|
21
|
+
).files
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const createChangeHandler =
|
|
25
|
+
(input, multifile) =>
|
|
26
|
+
({ files }) => {
|
|
27
|
+
// A JavaScript FileList instance is read-only, so we cannot add files to it
|
|
28
|
+
// FileList also doesn't have a .map method so by destructuring the FileList
|
|
29
|
+
// instance into an array we can add, remove and map
|
|
30
|
+
const currentFiles = Array.isArray(input.value) ? input.value : []
|
|
31
|
+
const value = multifile
|
|
32
|
+
? dedupeAndConcat(currentFiles, files)
|
|
33
|
+
: [...files]
|
|
34
|
+
|
|
35
|
+
input.onChange(value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const createRemoveHandler = (input, fileToDelete) => () => {
|
|
39
|
+
const files = input.value.filter((file) => file !== fileToDelete)
|
|
40
|
+
const value = files.length > 0 ? files : ''
|
|
41
|
+
|
|
42
|
+
input.onChange(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const FileInputFieldFF = ({
|
|
46
|
+
buttonLabel,
|
|
47
|
+
disabled,
|
|
48
|
+
error,
|
|
49
|
+
input,
|
|
50
|
+
meta,
|
|
51
|
+
multifile,
|
|
52
|
+
showValidStatus,
|
|
53
|
+
valid,
|
|
54
|
+
validationText,
|
|
55
|
+
...rest
|
|
56
|
+
}) => {
|
|
57
|
+
const files = input.value || []
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<FileInputField
|
|
61
|
+
{...rest}
|
|
62
|
+
onChange={createChangeHandler(input, multifile)}
|
|
63
|
+
buttonLabel={buttonLabel || (multifile ? btnLabelMulti : btnLabel)}
|
|
64
|
+
disabled={disabled || (!multifile && files.length >= 1)}
|
|
65
|
+
multiple={multifile}
|
|
66
|
+
name={input.name}
|
|
67
|
+
error={hasError(meta, error)}
|
|
68
|
+
valid={isValid(meta, valid, showValidStatus)}
|
|
69
|
+
validationText={getValidationText(meta, validationText, error)}
|
|
70
|
+
>
|
|
71
|
+
{files.map((file) => (
|
|
72
|
+
<FileListItem
|
|
73
|
+
key={file.name}
|
|
74
|
+
label={file.name}
|
|
75
|
+
onRemove={createRemoveHandler(input, file)}
|
|
76
|
+
removeText={i18n.t('Remove')}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</FileInputField>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
FileInputFieldFF.propTypes = {
|
|
84
|
+
/** `input` props provided by Final Form `Field` */
|
|
85
|
+
input: inputPropType.isRequired,
|
|
86
|
+
/** `meta` props provided by Final Form `Field` */
|
|
87
|
+
meta: metaPropType.isRequired,
|
|
88
|
+
|
|
89
|
+
buttonLabel: PropTypes.string,
|
|
90
|
+
disabled: PropTypes.bool,
|
|
91
|
+
error: PropTypes.bool,
|
|
92
|
+
multifile: PropTypes.bool,
|
|
93
|
+
showValidStatus: PropTypes.bool,
|
|
94
|
+
valid: PropTypes.bool,
|
|
95
|
+
validationText: PropTypes.string,
|
|
96
|
+
value: PropTypes.oneOfType([
|
|
97
|
+
PropTypes.arrayOf(PropTypes.instanceOf(File)),
|
|
98
|
+
PropTypes.oneOf(['']),
|
|
99
|
+
]),
|
|
100
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Field } from 'react-final-form'
|
|
3
|
+
import { formDecorator } from '../formDecorator.js'
|
|
4
|
+
import { inputArgType, metaArgType } from '../shared/propTypes.js'
|
|
5
|
+
import { hasValue } from '../validators/index.js'
|
|
6
|
+
import { FileInputFieldFF } from './FileInputFieldFF.js'
|
|
7
|
+
|
|
8
|
+
const description = `
|
|
9
|
+
The \`FileInputFieldFF\` is a wrapper around a \`FileInputField\` that enables it to work with Final Form, the preferred library in DHIS 2 apps for form validation and utilities.
|
|
10
|
+
|
|
11
|
+
#### Final Form
|
|
12
|
+
|
|
13
|
+
See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started).
|
|
14
|
+
|
|
15
|
+
Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={FileInputFieldFF} />\`. See the code samples below for examples.
|
|
16
|
+
|
|
17
|
+
#### Props
|
|
18
|
+
|
|
19
|
+
The props shown in the table below are generally provided to the \`FileInputFieldFF\` wrapper by the Final Form \`Field\`.
|
|
20
|
+
|
|
21
|
+
Note that any props beyond the API of the \`Field\` component will be spread to the \`FileInputFieldFF\`, which passes any extra props to the underlying \`FileInputField\` using \`{...rest}\`.
|
|
22
|
+
|
|
23
|
+
Therefore, to add any props to the \`FileInputFieldFF\` or \`FileInputField\`, add those props to the parent Final Form \`Field\` component.
|
|
24
|
+
|
|
25
|
+
Also see \`FileInput\` and \`FileInputField\` for notes about props and implementation.
|
|
26
|
+
|
|
27
|
+
\`\`\`js
|
|
28
|
+
import { FileInputFieldFF } from '@dhis2/ui'
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
Press **Submit** to see the form values logged to the console.
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
const files = [new File([], 'file1.txt'), new File([], 'file2.txt')]
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
title: 'File Input Field (Final Form)',
|
|
38
|
+
component: FileInputFieldFF,
|
|
39
|
+
decorators: [formDecorator],
|
|
40
|
+
parameters: { docs: { description: { component: description } } },
|
|
41
|
+
argTypes: {
|
|
42
|
+
input: { ...inputArgType },
|
|
43
|
+
meta: { ...metaArgType },
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Default = () => (
|
|
48
|
+
<Field
|
|
49
|
+
component={FileInputFieldFF}
|
|
50
|
+
name="upload"
|
|
51
|
+
label="This is a file upload"
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
export const Required = () => (
|
|
56
|
+
<Field
|
|
57
|
+
component={FileInputFieldFF}
|
|
58
|
+
name="upload"
|
|
59
|
+
label="This is a file upload"
|
|
60
|
+
required
|
|
61
|
+
validate={hasValue}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
export const Multifile = () => (
|
|
66
|
+
<Field
|
|
67
|
+
component={FileInputFieldFF}
|
|
68
|
+
name="upload"
|
|
69
|
+
label="This is a file upload"
|
|
70
|
+
multifile
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
export const WithValues = () => (
|
|
75
|
+
<Field
|
|
76
|
+
component={FileInputFieldFF}
|
|
77
|
+
name="upload"
|
|
78
|
+
label="This is a file upload"
|
|
79
|
+
required
|
|
80
|
+
multifile
|
|
81
|
+
initialValue={files}
|
|
82
|
+
validate={hasValue}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
WithValues.storyName = 'With values'
|
|
86
|
+
|
|
87
|
+
export const PreventPlaceholder = () => (
|
|
88
|
+
<Field
|
|
89
|
+
component={FileInputFieldFF}
|
|
90
|
+
name="upload"
|
|
91
|
+
label="This is a file upload"
|
|
92
|
+
placeholder=""
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
PreventPlaceholder.storyName = 'Prevent placeholder'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a multi-file IputField is rendered', () => {
|
|
4
|
+
cy.visitStory('Testing:FileInput', 'Standard form')
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
When('a file is provided', () => {
|
|
8
|
+
cy.get('[name="fileTxt"]').uploadSingleFile('txt', 'FileInput/file.txt')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
When('more than one files are provided', () => {
|
|
12
|
+
cy.get('[name="fileJpgs"]').uploadMultipleFiles(
|
|
13
|
+
[
|
|
14
|
+
{ fileType: 'txt', fixture: 'FileInput/file.txt' },
|
|
15
|
+
{ fileType: 'md', fixture: 'FileInput/file.md' },
|
|
16
|
+
],
|
|
17
|
+
true
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
Then('the form state contains that file', () => {
|
|
22
|
+
cy.window().then((win) => {
|
|
23
|
+
const { fileTxt } = win.formValues
|
|
24
|
+
expect(fileTxt).to.have.lengthOf(1)
|
|
25
|
+
|
|
26
|
+
const [file] = fileTxt
|
|
27
|
+
expect(file.name).to.equal('file.txt')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
Then('the form state contains those files', () => {
|
|
32
|
+
cy.window().then((win) => {
|
|
33
|
+
const { fileJpgs } = win.formValues
|
|
34
|
+
expect(fileJpgs).to.have.lengthOf(2)
|
|
35
|
+
|
|
36
|
+
const [file1, file2] = fileJpgs
|
|
37
|
+
expect(file1.name).to.equal('file.txt')
|
|
38
|
+
expect(file2.name).to.equal('file.md')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Feature: The FileInput accepts files
|
|
2
|
+
|
|
3
|
+
Scenario: The user provides a file
|
|
4
|
+
Given a single-file FileInput is rendered
|
|
5
|
+
And the InputField does not contain any files
|
|
6
|
+
When a file is provided
|
|
7
|
+
Then the form state contains that file
|
|
8
|
+
|
|
9
|
+
Scenario: The user provides multiple files
|
|
10
|
+
Given a multi-file IputField is rendered
|
|
11
|
+
And the InputField does not contain any files
|
|
12
|
+
When more than one files are provided
|
|
13
|
+
Then the form state contains those files
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Given } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a single-file FileInput is rendered', () => {
|
|
4
|
+
cy.visitStory('Testing:FileInput', 'Standard form')
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
Given('the InputField does not contain any files', () => {
|
|
8
|
+
cy.verifyFormValue('fileTxt', undefined)
|
|
9
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { When, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
When('a file with the wrong file type is provided', () => {
|
|
4
|
+
cy.get('[name="fileTxt"]').uploadSingleFile('md', 'FileInput/file.md')
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
When('the user submits the form', () => {
|
|
8
|
+
cy.get('button[type="submit"]').click()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
Then('an error message is shown', () => {
|
|
12
|
+
cy.get('.fileTxt')
|
|
13
|
+
.get('[data-test="dhis2-uiwidgets-fileinputfield-validation"]')
|
|
14
|
+
.should(
|
|
15
|
+
'contain',
|
|
16
|
+
'The file you provided is not a txt file, received "md"'
|
|
17
|
+
)
|
|
18
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Feature: The FileInput field displays an error when invalid
|
|
2
|
+
|
|
3
|
+
Scenario: The user provides files with the wrong file type
|
|
4
|
+
Given a single-file FileInput is rendered
|
|
5
|
+
And the InputField does not contain any files
|
|
6
|
+
When a file with the wrong file type is provided
|
|
7
|
+
And the user submits the form
|
|
8
|
+
Then an error message is shown
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Field } from 'react-final-form'
|
|
3
|
+
import { formDecorator } from '../formDecorator.js'
|
|
4
|
+
import { hasValue } from '../validators/index.js'
|
|
5
|
+
import { InputFieldFF } from './InputFieldFF.js'
|
|
6
|
+
|
|
7
|
+
export default { title: 'Testing:InputFieldFF', decorators: [formDecorator] }
|
|
8
|
+
export const Default = () => (
|
|
9
|
+
<Field component={InputFieldFF} name="agree" label="Do you agree?" />
|
|
10
|
+
)
|
|
11
|
+
export const Required = () => (
|
|
12
|
+
<Field
|
|
13
|
+
name="agree"
|
|
14
|
+
component={InputFieldFF}
|
|
15
|
+
required
|
|
16
|
+
validate={hasValue}
|
|
17
|
+
label="Do you agree?"
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { InputField } from '@dhis2-ui/input'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
createChangeHandler,
|
|
6
|
+
createFocusHandler,
|
|
7
|
+
createBlurHandler,
|
|
8
|
+
hasError,
|
|
9
|
+
isLoading,
|
|
10
|
+
isValid,
|
|
11
|
+
getValidationText,
|
|
12
|
+
} from '../shared/helpers.js'
|
|
13
|
+
import { metaPropType, inputPropType } from '../shared/propTypes.js'
|
|
14
|
+
|
|
15
|
+
export const InputFieldFF = ({
|
|
16
|
+
input,
|
|
17
|
+
meta,
|
|
18
|
+
error,
|
|
19
|
+
showValidStatus,
|
|
20
|
+
valid,
|
|
21
|
+
validationText,
|
|
22
|
+
onBlur,
|
|
23
|
+
onFocus,
|
|
24
|
+
loading,
|
|
25
|
+
showLoadingStatus,
|
|
26
|
+
...rest
|
|
27
|
+
}) => (
|
|
28
|
+
<InputField
|
|
29
|
+
{...rest}
|
|
30
|
+
name={input.name}
|
|
31
|
+
type={input.type}
|
|
32
|
+
error={hasError(meta, error)}
|
|
33
|
+
valid={isValid(meta, valid, showValidStatus)}
|
|
34
|
+
loading={isLoading(meta, loading, showLoadingStatus)}
|
|
35
|
+
validationText={getValidationText(meta, validationText, error)}
|
|
36
|
+
onFocus={createFocusHandler(input, onFocus)}
|
|
37
|
+
onChange={createChangeHandler(input)}
|
|
38
|
+
onBlur={createBlurHandler(input, onBlur)}
|
|
39
|
+
value={input.value}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
InputFieldFF.propTypes = {
|
|
44
|
+
/** `input` props received from Final Form `Field` */
|
|
45
|
+
input: inputPropType.isRequired,
|
|
46
|
+
/** `meta` props received from Final Form `Field` */
|
|
47
|
+
meta: metaPropType.isRequired,
|
|
48
|
+
|
|
49
|
+
error: PropTypes.bool,
|
|
50
|
+
loading: PropTypes.bool,
|
|
51
|
+
showLoadingStatus: PropTypes.bool,
|
|
52
|
+
showValidStatus: PropTypes.bool,
|
|
53
|
+
valid: PropTypes.bool,
|
|
54
|
+
validationText: PropTypes.string,
|
|
55
|
+
|
|
56
|
+
onBlur: PropTypes.func,
|
|
57
|
+
onFocus: PropTypes.func,
|
|
58
|
+
}
|