cozy-ui 128.1.0 → 128.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +5 -1
  3. package/react/ActionsMenu/Actions/viewInDrive.js +2 -1
  4. package/react/Contacts/AddModal/ContactAddressDialog/helpers.js +22 -0
  5. package/react/Contacts/AddModal/ContactAddressDialog/helpers.spec.js +64 -0
  6. package/react/Contacts/AddModal/ContactAddressDialog/index.jsx +84 -0
  7. package/react/Contacts/AddModal/ContactAddressDialog/locales/en.json +25 -0
  8. package/react/Contacts/AddModal/ContactAddressDialog/locales/fr.json +25 -0
  9. package/react/Contacts/AddModal/ContactAddressDialog/locales/index.jsx +7 -0
  10. package/react/Contacts/AddModal/ContactForm/FieldInput.jsx +117 -0
  11. package/react/Contacts/AddModal/ContactForm/FieldInputArray.jsx +80 -0
  12. package/react/Contacts/AddModal/ContactForm/FieldInputLayout.jsx +65 -0
  13. package/react/Contacts/AddModal/ContactForm/FieldInputWrapper.jsx +41 -0
  14. package/react/Contacts/AddModal/ContactForm/HasValueCondition.jsx +31 -0
  15. package/react/Contacts/AddModal/ContactForm/HasValueCondition.spec.jsx +79 -0
  16. package/react/Contacts/AddModal/ContactForm/RelatedContactList.jsx +37 -0
  17. package/react/Contacts/AddModal/ContactForm/TextFieldCustomLabelSelect.jsx +78 -0
  18. package/react/Contacts/AddModal/ContactForm/TextFieldSelect.jsx +39 -0
  19. package/react/Contacts/AddModal/ContactForm/__snapshots__/HasValueCondition.spec.jsx.snap +33 -0
  20. package/react/Contacts/AddModal/ContactForm/contactToFormValues.js +99 -0
  21. package/react/Contacts/AddModal/ContactForm/contactToFormValues.spec.js +128 -0
  22. package/react/Contacts/AddModal/ContactForm/fieldsConfig.jsx +341 -0
  23. package/react/Contacts/AddModal/ContactForm/formValuesToContact.js +100 -0
  24. package/react/Contacts/AddModal/ContactForm/formValuesToContact.spec.js +494 -0
  25. package/react/Contacts/AddModal/ContactForm/helpers.js +324 -0
  26. package/react/Contacts/AddModal/ContactForm/helpers.spec.js +152 -0
  27. package/react/Contacts/AddModal/ContactForm/index.jsx +104 -0
  28. package/react/Contacts/AddModal/ContactForm/index.spec.jsx +289 -0
  29. package/react/Contacts/AddModal/ContactForm/locales/en.json +73 -0
  30. package/react/Contacts/AddModal/ContactForm/locales/fr.json +73 -0
  31. package/react/Contacts/AddModal/ContactForm/locales/index.jsx +7 -0
  32. package/react/Contacts/AddModal/ContactForm/styles.styl +2 -0
  33. package/react/Contacts/AddModal/CustomLabelDialog/index.jsx +108 -0
  34. package/react/Contacts/AddModal/CustomLabelDialog/locales/en.json +15 -0
  35. package/react/Contacts/AddModal/CustomLabelDialog/locales/fr.json +15 -0
  36. package/react/Contacts/AddModal/CustomLabelDialog/locales/index.jsx +7 -0
  37. package/react/Contacts/AddModal/Readme.md +46 -0
  38. package/react/Contacts/AddModal/index.jsx +78 -0
  39. package/react/Contacts/AddModal/locales/en.json +13 -0
  40. package/react/Contacts/AddModal/locales/fr.json +13 -0
  41. package/react/Contacts/AddModal/locales/index.jsx +7 -0
  42. package/react/Contacts/AddModal/mocks.js +249 -0
  43. package/react/Contacts/AddModal/types.js +57 -0
  44. package/react/Contacts/Header/Readme.md +0 -2
  45. package/react/providers/DemoProvider.jsx +3 -2
  46. package/transpiled/react/ActionsMenu/Actions/viewInDrive.js +2 -1
  47. package/transpiled/react/Contacts/AddModal/ContactAddressDialog/helpers.d.ts +4 -0
  48. package/transpiled/react/Contacts/AddModal/ContactAddressDialog/helpers.js +20 -0
  49. package/transpiled/react/Contacts/AddModal/ContactAddressDialog/helpers.spec.d.ts +1 -0
  50. package/transpiled/react/Contacts/AddModal/ContactAddressDialog/index.d.ts +39 -0
  51. package/transpiled/react/Contacts/AddModal/ContactAddressDialog/index.js +87 -0
  52. package/transpiled/react/Contacts/AddModal/ContactAddressDialog/locales/index.d.ts +6 -0
  53. package/transpiled/react/Contacts/AddModal/ContactAddressDialog/locales/index.js +54 -0
  54. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInput.d.ts +35 -0
  55. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInput.js +126 -0
  56. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInputArray.d.ts +14 -0
  57. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInputArray.js +82 -0
  58. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInputLayout.d.ts +20 -0
  59. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInputLayout.js +70 -0
  60. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInputWrapper.d.ts +16 -0
  61. package/transpiled/react/Contacts/AddModal/ContactForm/FieldInputWrapper.js +31 -0
  62. package/transpiled/react/Contacts/AddModal/ContactForm/HasValueCondition.d.ts +18 -0
  63. package/transpiled/react/Contacts/AddModal/ContactForm/HasValueCondition.js +32 -0
  64. package/transpiled/react/Contacts/AddModal/ContactForm/HasValueCondition.spec.d.ts +1 -0
  65. package/transpiled/react/Contacts/AddModal/ContactForm/RelatedContactList.d.ts +15 -0
  66. package/transpiled/react/Contacts/AddModal/ContactForm/RelatedContactList.js +39 -0
  67. package/transpiled/react/Contacts/AddModal/ContactForm/TextFieldCustomLabelSelect.d.ts +9 -0
  68. package/transpiled/react/Contacts/AddModal/ContactForm/TextFieldCustomLabelSelect.js +81 -0
  69. package/transpiled/react/Contacts/AddModal/ContactForm/TextFieldSelect.d.ts +5 -0
  70. package/transpiled/react/Contacts/AddModal/ContactForm/TextFieldSelect.js +42 -0
  71. package/transpiled/react/Contacts/AddModal/ContactForm/contactToFormValues.d.ts +2 -0
  72. package/transpiled/react/Contacts/AddModal/ContactForm/contactToFormValues.js +88 -0
  73. package/transpiled/react/Contacts/AddModal/ContactForm/contactToFormValues.spec.d.ts +1 -0
  74. package/transpiled/react/Contacts/AddModal/ContactForm/fieldsConfig.d.ts +4 -0
  75. package/transpiled/react/Contacts/AddModal/ContactForm/fieldsConfig.js +278 -0
  76. package/transpiled/react/Contacts/AddModal/ContactForm/formValuesToContact.d.ts +6 -0
  77. package/transpiled/react/Contacts/AddModal/ContactForm/formValuesToContact.js +94 -0
  78. package/transpiled/react/Contacts/AddModal/ContactForm/formValuesToContact.spec.d.ts +1 -0
  79. package/transpiled/react/Contacts/AddModal/ContactForm/helpers.d.ts +28 -0
  80. package/transpiled/react/Contacts/AddModal/ContactForm/helpers.js +335 -0
  81. package/transpiled/react/Contacts/AddModal/ContactForm/helpers.spec.d.ts +1 -0
  82. package/transpiled/react/Contacts/AddModal/ContactForm/index.d.ts +11 -0
  83. package/transpiled/react/Contacts/AddModal/ContactForm/index.js +114 -0
  84. package/transpiled/react/Contacts/AddModal/ContactForm/index.spec.d.ts +1 -0
  85. package/transpiled/react/Contacts/AddModal/ContactForm/locales/index.d.ts +6 -0
  86. package/transpiled/react/Contacts/AddModal/ContactForm/locales/index.js +150 -0
  87. package/transpiled/react/Contacts/AddModal/CustomLabelDialog/index.d.ts +22 -0
  88. package/transpiled/react/Contacts/AddModal/CustomLabelDialog/index.js +113 -0
  89. package/transpiled/react/Contacts/AddModal/CustomLabelDialog/locales/index.d.ts +6 -0
  90. package/transpiled/react/Contacts/AddModal/CustomLabelDialog/locales/index.js +34 -0
  91. package/transpiled/react/Contacts/AddModal/index.d.ts +7 -0
  92. package/transpiled/react/Contacts/AddModal/index.js +109 -0
  93. package/transpiled/react/Contacts/AddModal/locales/index.d.ts +6 -0
  94. package/transpiled/react/Contacts/AddModal/locales/index.js +30 -0
  95. package/transpiled/react/Contacts/AddModal/mocks.d.ts +270 -0
  96. package/transpiled/react/Contacts/AddModal/mocks.js +214 -0
  97. package/transpiled/react/Contacts/AddModal/types.d.ts +54 -0
  98. package/transpiled/react/Contacts/AddModal/types.js +49 -0
  99. package/transpiled/react/providers/DemoProvider.d.ts +2 -1
  100. package/transpiled/react/providers/DemoProvider.js +7 -3
  101. package/transpiled/react/stylesheet.css +1 -1
@@ -0,0 +1,324 @@
1
+ import get from 'lodash/get'
2
+ import isEqual from 'lodash/isEqual'
3
+ import merge from 'lodash/merge'
4
+ import uniqueId from 'lodash/uniqueId'
5
+
6
+ import { Association } from 'cozy-client'
7
+ import { makeDisplayName } from 'cozy-client/dist/models/contact'
8
+ import { CONTACTS_DOCTYPE } from 'cozy-client/dist/models/contact'
9
+
10
+ import contactToFormValues from './contactToFormValues'
11
+
12
+ export const fieldsRequired = [
13
+ 'givenName',
14
+ 'familyName',
15
+ 'email[0].email',
16
+ 'cozy'
17
+ ]
18
+
19
+ /**
20
+ * Returns errors if all required fields are empty
21
+ * @param {object} values - Fields values
22
+ * @param {func} t - Translation function
23
+ * @returns {object} Errors
24
+ */
25
+ export const validateFields = (values, t) => {
26
+ const errors = {}
27
+ if (fieldsRequired.every(field => !get(values, field))) {
28
+ fieldsRequired.forEach(field => {
29
+ errors[field] = t('Contacts.AddModal.ContactForm.fields.required')
30
+ })
31
+ }
32
+ return errors
33
+ }
34
+
35
+ /**
36
+ * @param {object} [item] - Contact attribute
37
+ * @returns {string} Stringified object
38
+ */
39
+ export const makeItemLabel = item => {
40
+ if (!item) return undefined
41
+
42
+ const res =
43
+ item.label || item.type
44
+ ? JSON.stringify({ type: item.type, label: item.label })
45
+ : undefined
46
+
47
+ return res
48
+ }
49
+
50
+ /**
51
+ *
52
+ * @param {string} [itemLabel] - Value of the label for a contact attribute
53
+ * @returns {{ type?: string, label?: string }}
54
+ */
55
+ export const makeTypeAndLabel = itemLabel => {
56
+ if (!itemLabel) {
57
+ return { type: undefined, label: undefined }
58
+ }
59
+
60
+ const itemLabelObj = JSON.parse(itemLabel)
61
+
62
+ const res = { type: itemLabelObj.type, label: itemLabelObj.label }
63
+
64
+ return res
65
+ }
66
+
67
+ /**
68
+ * @param {object} addressField
69
+ * @returns {boolean} True if addressField has extended address
70
+ */
71
+ export const hasExtendedAddress = addressField => {
72
+ if (!addressField) return false
73
+ const extendedAddressKeys = [
74
+ 'addresslocality',
75
+ 'addressbuilding',
76
+ 'addressstairs',
77
+ 'addressfloor',
78
+ 'addressapartment',
79
+ 'addressentrycode'
80
+ ]
81
+ return Object.keys(addressField).some(ext =>
82
+ extendedAddressKeys.includes(ext)
83
+ )
84
+ }
85
+
86
+ export const moveToHead = shouldBeHead => items =>
87
+ items.reduce((arr, v) => (shouldBeHead(v) ? [v, ...arr] : [...arr, v]), [])
88
+
89
+ export const movePrimaryToHead = moveToHead(v => v?.primary)
90
+
91
+ export const createAddress = ({ address, oldContact, t }) => {
92
+ return address
93
+ ? address
94
+ .filter(val => val && val.address)
95
+ .map((addressField, index) => {
96
+ const oldContactAddress = oldContact?.address?.[index]
97
+ const oldContactFormValues = contactToFormValues(oldContact, t)
98
+ ?.address?.[index]
99
+
100
+ const addressHasBeenModified = !isEqual(
101
+ addressField,
102
+ oldContactFormValues
103
+ )
104
+
105
+ if (addressHasBeenModified) {
106
+ // Use "code" instead "postcode", to be vcard 4.0 rfc 6350 compliant
107
+ // eslint-disable-next-line no-unused-vars
108
+ const { postcode, ...oldContactAddressCleaned } =
109
+ oldContactAddress || {}
110
+ return {
111
+ // For keep other properties form connectors
112
+ ...oldContactAddressCleaned,
113
+ formattedAddress: addressField.address,
114
+ number: addressField.addressnumber,
115
+ street: addressField.addressstreet,
116
+ code: addressField.addresscode,
117
+ city: addressField.addresscity,
118
+ region: addressField.addressregion,
119
+ country: addressField.addresscountry,
120
+ ...(hasExtendedAddress(addressField) && {
121
+ extendedAddress: {
122
+ ...oldContactAddressCleaned.extendedAddress,
123
+ locality: addressField.addresslocality,
124
+ building: addressField.addressbuilding,
125
+ stairs: addressField.addressstairs,
126
+ floor: addressField.addressfloor,
127
+ apartment: addressField.addressapartment,
128
+ entrycode: addressField.addressentrycode
129
+ }
130
+ }),
131
+ ...makeTypeAndLabel(addressField.addressLabel),
132
+ primary: index === 0
133
+ }
134
+ }
135
+ return oldContactAddress
136
+ })
137
+ : []
138
+ }
139
+
140
+ /**
141
+ * @param {(import('../../../types').RelatedContact|undefined)[]} relatedContact - The related contacts array
142
+ * @returns {Record<string, { data: { _id: string, _type: string }[] }>} - The related contacts relationships
143
+ */
144
+ export const getRelatedContactRelationships = relatedContact => {
145
+ // Tips filter Boolean to remove undefined value from array when relatedContact is empty (see contactToFormValues)
146
+ const data = relatedContact.filter(Boolean).reduce((acc, curr) => {
147
+ const relationType = curr.relatedContactLabel
148
+ ? JSON.parse(curr.relatedContactLabel).type
149
+ : 'related'
150
+
151
+ const existingIndex = acc.findIndex(
152
+ item => item._id === curr.relatedContactId
153
+ )
154
+
155
+ if (existingIndex !== -1) {
156
+ acc[existingIndex].metadata.relationTypes = Array.from(
157
+ new Set([...acc[existingIndex].metadata.relationTypes, relationType])
158
+ )
159
+ } else {
160
+ acc.push({
161
+ _id: curr.relatedContactId,
162
+ _type: CONTACTS_DOCTYPE,
163
+ metadata: {
164
+ relationTypes: [relationType]
165
+ }
166
+ })
167
+ }
168
+ return acc
169
+ }, [])
170
+
171
+ // `data` can be empty, you still have to return an object to override the behavior of the cozy-client store, otherwise it will keep the old value, and without refreshing the page, the data will not be up to date in the store and therefore on the interface
172
+ return { related: { data } }
173
+ }
174
+
175
+ /**
176
+ * When changing the type of relationship, it must be ensured that no empty relationship remains.
177
+ * The old and new ones are merged into `formValuesToContact`.
178
+ *
179
+ * @param {import('cozy-client/types/types').IOCozyContact} contact - The contact object with all relationships
180
+ * @returns {import('cozy-client/types/types').IOCozyContact} - The contact object without the related contacts relationships
181
+ */
182
+ export const removeRelatedContactRelationships = contact => {
183
+ if (!contact?.relationships) return contact
184
+ const updatedContact = merge({}, contact)
185
+
186
+ const relationshipsWithoutRelatedContact = Object.entries(
187
+ updatedContact.relationships
188
+ ).reduce((acc, [relName, relValue]) => {
189
+ if ('related' === relName) {
190
+ acc[relName] = relValue
191
+ }
192
+ return acc
193
+ }, {})
194
+
195
+ updatedContact.relationships = relationshipsWithoutRelatedContact
196
+
197
+ return updatedContact
198
+ }
199
+
200
+ // TODO : Update dehydrate function to HasMany class in cozy-client
201
+ /**
202
+ * This function is used to clean the contact object from the associated data
203
+ * cozy-client dehydrates the document before saving it (via the `HasMany` method), but by doing it manually, we ensure that all hydrated relationships in the document (and without data of course) are not saved in the `relationships` of the document, which adds unnecessary data.
204
+ *
205
+ * @param {import('cozy-client/types/types').IOCozyContact} contact - The contact object with associated data
206
+ * @returns {import('cozy-client/types/types').IOCozyContact} - The contact object without associated data
207
+ */
208
+ export const removeAsscociatedData = contact => {
209
+ if (!contact) return {}
210
+ return Object.entries(contact).reduce((cleanedContact, [key, value]) => {
211
+ // Add `groups` condition to keep the old implementation functional, see below
212
+ if (!(value instanceof Association) || key === 'groups') {
213
+ cleanedContact[key] = value
214
+ }
215
+ return cleanedContact
216
+ }, {})
217
+ }
218
+
219
+ /**
220
+ * @param {import('cozy-client/types/types').IOCozyContact} contact
221
+ * @returns {import('../../../types').RelatedContact[]}
222
+ */
223
+ export const makeRelatedContact = contact => {
224
+ if (
225
+ !(contact.related instanceof Association) ||
226
+ !contact.relationships?.related
227
+ ) {
228
+ return [undefined]
229
+ }
230
+
231
+ const relatedData = contact.related.data.reduce((acc, curr) => {
232
+ // Use `makeDisplayName` because if the contact is newly created, it has no `displayName` attribute. (Creation of a contact when selecting a linked contact)
233
+ acc[curr._id] = curr.displayName || makeDisplayName(curr)
234
+ return acc
235
+ }, {})
236
+
237
+ const res = contact.relationships.related.data.flatMap(item => {
238
+ return item.metadata.relationTypes.map(type => {
239
+ return {
240
+ relatedContactId: item._id,
241
+ relatedContact: relatedData[item._id],
242
+ relatedContactLabel: makeItemLabel({
243
+ type: type === 'related' ? '' : type
244
+ })
245
+ }
246
+ })
247
+ })
248
+
249
+ // Useful because a contact always has at least the `related` relationships (see `getRelatedContactRelationships`)
250
+ return res.length > 0 ? res : [undefined]
251
+ }
252
+
253
+ export const addField = fields => fields.push({ fieldId: uniqueId('fieldId_') })
254
+
255
+ export const removeField = (fields, index) => {
256
+ const isLastRemainingField = fields.length === 1
257
+
258
+ if (isLastRemainingField) {
259
+ fields.update(index, undefined)
260
+ } else {
261
+ fields.remove(index)
262
+ }
263
+ }
264
+
265
+ /**
266
+ *
267
+ * @param {string} value
268
+ * @param {func} t
269
+ * @returns {string}
270
+ */
271
+ export const makeCustomLabel = (value, t) => {
272
+ const { type, label } = JSON.parse(value)
273
+
274
+ const firstString = type || ''
275
+ const secondString = label
276
+ ? type
277
+ ? ` (${t(`Contacts.AddModal.ContactForm.label.${label}`)})`.toLowerCase()
278
+ : `label.${label}`
279
+ : ''
280
+
281
+ return firstString + secondString || null
282
+ }
283
+
284
+ /**
285
+ *
286
+ * @param {string} name
287
+ * @param {string} value
288
+ * @returns {string}
289
+ */
290
+ export const makeInitialCustomValue = (name, value) => {
291
+ // gender input doesn't support custom label
292
+ if (!name || !value || name === 'gender') return undefined
293
+
294
+ const valueObj = JSON.parse(value)
295
+
296
+ // Voluntarily before the "backwards compatibility" condition
297
+ if (name.includes('relatedContactLabel')) {
298
+ if (!'related' === valueObj.type) {
299
+ return JSON.stringify({ type: valueObj.type })
300
+ }
301
+ return undefined
302
+ }
303
+
304
+ // for backwards compatiblity - historically there is only type and no label
305
+ if (valueObj.type && !valueObj.label) {
306
+ return JSON.stringify({ type: valueObj.type })
307
+ }
308
+
309
+ // for phone label
310
+ if (name.includes('phoneLabel')) {
311
+ // but unsupported one
312
+ if (!['cell', 'voice', 'fax'].includes(valueObj.type)) {
313
+ return JSON.stringify({ type: valueObj.type, label: valueObj.label })
314
+ }
315
+
316
+ // we don't want to create a custom label if supported
317
+ return undefined
318
+ }
319
+
320
+ // at this point if label and type are both present, it's a custom label
321
+ if (valueObj.type && valueObj.label) {
322
+ return JSON.stringify({ type: valueObj.type, label: valueObj.label })
323
+ }
324
+ }
@@ -0,0 +1,152 @@
1
+ import {
2
+ moveToHead,
3
+ makeItemLabel,
4
+ makeTypeAndLabel,
5
+ makeInitialCustomValue
6
+ } from './helpers'
7
+
8
+ describe('moveToHead function', () => {
9
+ it('should move an item to head of the array', () => {
10
+ const items = [1, 5, 657, 42, 3, 27, 88, 3, 4]
11
+ const shouldBeHead = v => v === 42
12
+ const expected = [42, 1, 5, 657, 3, 27, 88, 3, 4]
13
+ const actual = moveToHead(shouldBeHead)(items)
14
+ expect(actual).toEqual(expected)
15
+ })
16
+ })
17
+
18
+ describe('makeItemLabel', () => {
19
+ it('should return undefined if no arg', () => {
20
+ const res = makeItemLabel()
21
+
22
+ expect(res).toBe(undefined)
23
+ })
24
+
25
+ it('should return undefined if nothing defined', () => {
26
+ const res = makeItemLabel({ type: undefined, label: undefined })
27
+
28
+ expect(res).toBe(undefined)
29
+ })
30
+
31
+ it('should return correct type and label', () => {
32
+ const res = makeItemLabel({ type: 'cell', label: 'work' })
33
+
34
+ expect(res).toBe('{"type":"cell","label":"work"}')
35
+ })
36
+
37
+ it('should return only label if no type', () => {
38
+ const res = makeItemLabel({ type: undefined, label: 'work' })
39
+
40
+ expect(res).toBe('{"label":"work"}')
41
+ })
42
+
43
+ it('should return only type if no label', () => {
44
+ const res = makeItemLabel({ type: 'cell', label: undefined })
45
+
46
+ expect(res).toBe('{"type":"cell"}')
47
+ })
48
+ })
49
+
50
+ describe('makeTypeAndLabel', () => {
51
+ it('should return undefined if no arg', () => {
52
+ const res = makeTypeAndLabel()
53
+
54
+ expect(res).toStrictEqual({ type: undefined, label: undefined })
55
+ })
56
+
57
+ it('should return correct type and label', () => {
58
+ const res = makeTypeAndLabel('{"type":"cell","label":"work"}')
59
+
60
+ expect(res).toStrictEqual({ type: 'cell', label: 'work' })
61
+ })
62
+
63
+ it('should return only label', () => {
64
+ const res = makeTypeAndLabel('{"label":"work"}')
65
+
66
+ expect(res).toStrictEqual({ type: undefined, label: 'work' })
67
+ })
68
+
69
+ it('should return only type', () => {
70
+ const res = makeTypeAndLabel('{"type":"cell"}')
71
+
72
+ expect(res).toStrictEqual({ type: 'cell', label: undefined })
73
+ })
74
+ })
75
+
76
+ describe('makeInitialCustomValue', () => {
77
+ it('should return undefined if no name', () => {
78
+ const res = makeInitialCustomValue(
79
+ undefined,
80
+ '{"type":"fax","label":"work"}'
81
+ )
82
+
83
+ expect(res).toStrictEqual(undefined)
84
+ })
85
+
86
+ it('should return undefined if no value', () => {
87
+ const res = makeInitialCustomValue('phone[0].phoneLabel', undefined)
88
+
89
+ expect(res).toStrictEqual(undefined)
90
+ })
91
+
92
+ it('should return undefined if no name/value', () => {
93
+ const res = makeInitialCustomValue(undefined, undefined)
94
+
95
+ expect(res).toStrictEqual(undefined)
96
+ })
97
+
98
+ it('should return undefined for gender input', () => {
99
+ const res = makeInitialCustomValue(
100
+ 'gender',
101
+ '{"type":"fax","label":"work"}'
102
+ )
103
+
104
+ expect(res).toStrictEqual(undefined)
105
+ })
106
+
107
+ it('should return the type if no label to ensure backwards compatibility', () => {
108
+ const res = makeInitialCustomValue('someInput', '{"type":"someType"}')
109
+
110
+ expect(res).toStrictEqual('{"type":"someType"}')
111
+ })
112
+
113
+ it('should return type and label if present', () => {
114
+ const res = makeInitialCustomValue(
115
+ 'someInput',
116
+ '{"type":"someType","label":"work"}'
117
+ )
118
+
119
+ expect(res).toStrictEqual('{"type":"someType","label":"work"}')
120
+ })
121
+
122
+ describe('for phone input', () => {
123
+ const name = 'phone[0].phoneLabel'
124
+
125
+ it('should not return a custom label if the value is supported', () => {
126
+ const res = makeInitialCustomValue(name, '{"type":"fax","label":"work"}')
127
+
128
+ expect(res).toStrictEqual(undefined)
129
+ })
130
+
131
+ it('should return the custom label', () => {
132
+ const res = makeInitialCustomValue(name, '{"type":"someType"}')
133
+
134
+ expect(res).toStrictEqual('{"type":"someType"}')
135
+ })
136
+
137
+ it('should return the custom label', () => {
138
+ const res = makeInitialCustomValue(name, '{"label":"work"}')
139
+
140
+ expect(res).toStrictEqual('{"label":"work"}')
141
+ })
142
+
143
+ it('should return the custom value if the type is not supported even if there is a label', () => {
144
+ const res = makeInitialCustomValue(
145
+ name,
146
+ '{"type":"someType","label":"work"}'
147
+ )
148
+
149
+ expect(res).toStrictEqual('{"type":"someType","label":"work"}')
150
+ })
151
+ })
152
+ })
@@ -0,0 +1,104 @@
1
+ import arrayMutators from 'final-form-arrays'
2
+ import React from 'react'
3
+ import { Form } from 'react-final-form'
4
+
5
+ import { getHasManyItems } from 'cozy-client/dist/associations/HasMany'
6
+
7
+ import FieldInputLayout from './FieldInputLayout'
8
+ import contactToFormValues from './contactToFormValues'
9
+ import { fields } from './fieldsConfig'
10
+ import formValuesToContact from './formValuesToContact'
11
+ import { validateFields } from './helpers'
12
+ import { locales } from './locales'
13
+ import { useI18n, useExtendI18n } from '../../../providers/I18n'
14
+ // import { fullContactPropTypes } from '../../ContactPropTypes' // !!
15
+
16
+ // this variable will be set in the form's render prop
17
+ // and used by the submit button in ContactFormModal
18
+ // to be able to trigger the submit from outside the form
19
+ // See react-final-form examples here: https://www.npmjs.com/package/react-final-form#external-submit
20
+ let _submitContactForm
21
+
22
+ function setSubmitContactForm(handleSubmit) {
23
+ _submitContactForm = handleSubmit
24
+ }
25
+
26
+ export function getSubmitContactForm() {
27
+ return _submitContactForm
28
+ }
29
+
30
+ /**
31
+ *
32
+ * @param {object} params
33
+ * @param {import('cozy-client/types/types').IOCozyContact} params.contact
34
+ * @param {func} params.onSubmit
35
+ * @param {{ data: Array<object> }} params.contacts
36
+ * @returns
37
+ */
38
+ const ContactForm = ({ contact, onSubmit, contacts }) => {
39
+ useExtendI18n(locales)
40
+ const { t } = useI18n()
41
+
42
+ return (
43
+ <Form
44
+ mutators={{ ...arrayMutators }}
45
+ validate={values => validateFields(values, t)}
46
+ onSubmit={formValues =>
47
+ onSubmit(formValuesToContact({ formValues, oldContact: contact, t }))
48
+ }
49
+ initialValues={contactToFormValues(contact, t)}
50
+ render={({ handleSubmit, valid, submitFailed, errors }) => {
51
+ setSubmitContactForm(handleSubmit)
52
+ return (
53
+ <form
54
+ role="form"
55
+ onSubmit={handleSubmit}
56
+ className="u-flex u-flex-column"
57
+ >
58
+ {fields.map((attributes, index) => (
59
+ <FieldInputLayout
60
+ key={index}
61
+ attributes={attributes}
62
+ contacts={contacts}
63
+ formProps={{
64
+ valid,
65
+ submitFailed,
66
+ errors
67
+ }}
68
+ />
69
+ ))}
70
+ </form>
71
+ )
72
+ }}
73
+ />
74
+ )
75
+ }
76
+
77
+ // Used to avoid unnecessary multiple rendering of ContactForm when creating a new contact in another way.
78
+ // These unnecessary renderings prevented the addition of a newly created linked contact. (Creation of a contact when selecting a linked contact)
79
+ export const isSameContactProp = (prevProps, nextProps) => {
80
+ if (!prevProps.contact?.relationships || !nextProps.contact?.relationships) {
81
+ return false
82
+ }
83
+
84
+ const prevContactIdsRelated = getHasManyItems(
85
+ prevProps.contact,
86
+ 'related'
87
+ ).map(r => r._id)
88
+ const nextContactIdsRelated = getHasManyItems(
89
+ nextProps.contact,
90
+ 'related'
91
+ ).map(r => r._id)
92
+
93
+ if (
94
+ prevContactIdsRelated.length !== nextContactIdsRelated.length ||
95
+ !prevContactIdsRelated.every(id => nextContactIdsRelated.includes(id))
96
+ ) {
97
+ return false
98
+ }
99
+
100
+ return true
101
+ }
102
+
103
+ // export default ContactForm
104
+ export default React.memo(ContactForm, isSameContactProp)