cozy-ui-plus 4.0.0 → 4.1.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/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [4.1.0](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@4.0.0...cozy-ui-plus@4.1.0) (2025-12-10)
7
+
8
+ ### Features
9
+
10
+ - **ui-plus:** Add possibility to overrides fields in Contacts AddModal ([7224485](https://github.com/cozy/cozy-libs/commit/72244856a99c879d54b1317e402a095afcb8b3f6))
11
+ - **ui-plus:** Change required logic for Contacts AddModal ([cdd5650](https://github.com/cozy/cozy-libs/commit/cdd565070ddb98a515c63ee9f2026b4e2fa1532d))
12
+ - **ui-plus:** Contacts AddModal form input values are now overridable ([50e7502](https://github.com/cozy/cozy-libs/commit/50e7502e5143e0479941c8aeabe67e90796a735c))
13
+
6
14
  # [4.0.0](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@3.1.2...cozy-ui-plus@4.0.0) (2025-12-04)
7
15
 
8
16
  ### Features
@@ -56,8 +56,7 @@ var FieldInput = function FieldInput(_ref) {
56
56
  error = _ref.error,
57
57
  helperText = _ref.helperText,
58
58
  label = _ref.label,
59
- isInvisible = _ref.isInvisible,
60
- required = _ref.required;
59
+ isInvisible = _ref.isInvisible;
61
60
 
62
61
  var _useState = (0, _react.useState)((0, _uniqueId.default)('field_')),
63
62
  _useState2 = (0, _slicedToArray2.default)(_useState, 1),
@@ -104,7 +103,6 @@ var FieldInput = function FieldInput(_ref) {
104
103
  'u-dn': isInvisible
105
104
  })
106
105
  }, /*#__PURE__*/_react.default.createElement(_reactFinalForm.Field, {
107
- required: required,
108
106
  error: error,
109
107
  helperText: helperText,
110
108
  label: label,
@@ -153,12 +151,10 @@ FieldInput.propTypes = {
153
151
  }),
154
152
  // Destructuring props
155
153
  id: _propTypes.default.string,
156
- label: _propTypes.default.string,
157
- required: _propTypes.default.bool
154
+ label: _propTypes.default.string
158
155
  };
159
156
  FieldInput.defaultProps = {
160
- labelProps: null,
161
- required: false
157
+ labelProps: null
162
158
  };
163
159
  var _default = FieldInput;
164
160
  exports.default = _default;
@@ -43,15 +43,13 @@ var FieldInputArray = function FieldInputArray(_ref) {
43
43
  label = _ref$attributes.label,
44
44
  restAttributes = (0, _objectWithoutProperties2.default)(_ref$attributes, _excluded),
45
45
  contacts = _ref.contacts,
46
- _ref$formProps = _ref.formProps,
47
- valid = _ref$formProps.valid,
48
- submitFailed = _ref$formProps.submitFailed,
49
- errors = _ref$formProps.errors;
46
+ formProps = _ref.formProps;
50
47
  (0, _twakeI18n.useExtendI18n)(_locales.locales);
51
48
 
52
49
  var _useI18n = (0, _twakeI18n.useI18n)(),
53
50
  t = _useI18n.t;
54
51
 
52
+ var errors = formProps.errors;
55
53
  return /*#__PURE__*/_react.default.createElement(_reactFinalFormArrays.FieldArray, {
56
54
  name: name
57
55
  }, function (_ref2) {
@@ -62,7 +60,7 @@ var FieldInputArray = function FieldInputArray(_ref) {
62
60
  var key = ((_fields$value$index = fields.value[index]) === null || _fields$value$index === void 0 ? void 0 : _fields$value$index.fieldId) || nameWithIndex;
63
61
  var showRemove = (_fields$value$index2 = fields.value[index]) === null || _fields$value$index2 === void 0 ? void 0 : _fields$value$index2[name];
64
62
  var inputName = "".concat(nameWithIndex, ".").concat(name);
65
- var isError = _helpers.fieldsRequired.includes(inputName) && !valid && submitFailed;
63
+ var isError = (0, _helpers.makeIsRequiredError)(restAttributes.required, formProps);
66
64
  return /*#__PURE__*/_react.default.createElement("div", {
67
65
  key: key,
68
66
  className: (0, _classnames.default)('u-flex u-flex-items-baseline', {
@@ -46,13 +46,11 @@ var FieldInputLayout = function FieldInputLayout(_ref) {
46
46
  var _useI18n = (0, _twakeI18n.useI18n)(),
47
47
  t = _useI18n.t;
48
48
 
49
- var valid = formProps.valid,
50
- submitFailed = formProps.submitFailed,
51
- errors = formProps.errors;
49
+ var errors = formProps.errors;
52
50
  var name = attributes.name,
53
51
  label = attributes.label,
54
52
  restAttributes = (0, _objectWithoutProperties2.default)(attributes, _excluded2);
55
- var isError = _helpers.fieldsRequired.includes(name) && !valid && submitFailed;
53
+ var isError = (0, _helpers.makeIsRequiredError)(restAttributes.required, formProps);
56
54
  return /*#__PURE__*/_react.default.createElement("div", {
57
55
  className: (0, _classnames.default)('u-mt-1', {
58
56
  'u-flex-items-center': !layout,
@@ -35,7 +35,7 @@ var FieldInputWrapper = function FieldInputWrapper(_ref) {
35
35
  fullWidth = _ref.fullWidth,
36
36
  props = (0, _objectWithoutProperties2.default)(_ref, _excluded2);
37
37
  var Component = component || (restAttributes.customLabelOptions ? _TextFieldCustomLabelSelect.default : restAttributes !== null && restAttributes !== void 0 && restAttributes.select ? _TextFieldSelect.default : _TextField.default);
38
- return /*#__PURE__*/_react.default.createElement(Component, (0, _extends2.default)({}, restAttributes, input, props, {
38
+ return /*#__PURE__*/_react.default.createElement(Component, (0, _extends2.default)({}, input, restAttributes, props, {
39
39
  variant: variant,
40
40
  fullWidth: fullWidth,
41
41
  minRows: "2"
@@ -5,16 +5,16 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
5
5
  Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
- exports.validateFields = exports.removeRelatedContactRelationships = exports.removeField = exports.removeAsscociatedData = exports.moveToHead = exports.movePrimaryToHead = exports.makeTypeAndLabel = exports.makeRelatedContact = exports.makeItemLabel = exports.makeInitialCustomValue = exports.makeImppValues = exports.makeFields = exports.makeCustomLabel = exports.hasExtendedAddress = exports.getRelatedContactRelationships = exports.fieldsRequired = exports.createAddress = exports.addField = void 0;
8
+ exports.validateFields = exports.removeRelatedContactRelationships = exports.removeField = exports.removeAsscociatedData = exports.moveToHead = exports.movePrimaryToHead = exports.makeTypeAndLabel = exports.makeRelatedContact = exports.makeItemLabel = exports.makeIsRequiredError = exports.makeInitialCustomValue = exports.makeImppValues = exports.makeFields = exports.makeCustomLabel = exports.hasNoValues = exports.hasExtendedAddress = exports.getRelatedContactRelationships = exports.getFirstValueIfArray = exports.createAddress = exports.addField = void 0;
9
9
 
10
10
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
11
11
 
12
- var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
13
-
14
12
  var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
15
13
 
16
14
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
17
15
 
16
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
17
+
18
18
  var _get = _interopRequireDefault(require("lodash/get"));
19
19
 
20
20
  var _isEqual = _interopRequireDefault(require("lodash/isEqual"));
@@ -35,27 +35,69 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
35
35
 
36
36
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
37
37
 
38
- var fieldsRequired = ['givenName', 'familyName', 'email[0].email', 'cozy'];
38
+ var getFirstValueIfArray = function getFirstValueIfArray(values) {
39
+ return Object.keys(values).reduce(function (acc, key) {
40
+ var val = values[key];
41
+ acc[key] = Array.isArray(val) ? val[0] : val;
42
+ return acc;
43
+ }, {});
44
+ };
39
45
  /**
40
- * Returns errors if all required fields are empty
46
+ * Returns true if all fields have no values
41
47
  * @param {object} values - Fields values
42
- * @param {func} t - Translation function
48
+ * @param {import('../types').Field[]} fields - Fields configuration
49
+ * @returns {boolean} True if all fields have no values
50
+ */
51
+
52
+
53
+ exports.getFirstValueIfArray = getFirstValueIfArray;
54
+
55
+ var hasNoValues = function hasNoValues(values, fields) {
56
+ var _values = getFirstValueIfArray(values);
57
+
58
+ return fields.every(function (field) {
59
+ return !(0, _get.default)(_values, field.name);
60
+ });
61
+ };
62
+ /**
63
+ * Returns true if the input is required and the form is invalid
64
+ * @param {boolean} required - Whether the input is required
65
+ * @param {object} formProps - The form props
66
+ * @returns {boolean} True if the input is required and the form is invalid
67
+ */
68
+
69
+
70
+ exports.hasNoValues = hasNoValues;
71
+
72
+ var makeIsRequiredError = function makeIsRequiredError(required, formProps) {
73
+ var valid = formProps.valid,
74
+ submitFailed = formProps.submitFailed;
75
+ return required && !valid && submitFailed;
76
+ };
77
+ /**
78
+ * Returns errors for required fields that are empty
79
+ * @param {object} values - Fields values
80
+ * @param {import('../types').Field[]} fields - Fields configuration
81
+ * @param {Function} t - Translation function
43
82
  * @returns {object} Errors
44
83
  */
45
84
 
46
- exports.fieldsRequired = fieldsRequired;
47
85
 
48
- var validateFields = function validateFields(values, t) {
86
+ exports.makeIsRequiredError = makeIsRequiredError;
87
+
88
+ var validateFields = function validateFields(values, fields, t) {
49
89
  var errors = {};
90
+ var fieldsRequired = fields.filter(function (field) {
91
+ return field.required;
92
+ });
50
93
 
51
- if (fieldsRequired.every(function (field) {
52
- return !(0, _get.default)(values, field);
53
- })) {
54
- fieldsRequired.forEach(function (field) {
55
- errors[field] = t('Contacts.AddModal.ContactForm.fields.required');
56
- });
57
- }
94
+ var _values = getFirstValueIfArray(values);
58
95
 
96
+ fieldsRequired.forEach(function (field) {
97
+ if (!(0, _get.default)(_values, field.name)) {
98
+ errors[field.name] = field.layout === 'array' ? [(0, _defineProperty2.default)({}, field.name, t('Contacts.AddModal.ContactForm.fields.required'))] : t('Contacts.AddModal.ContactForm.fields.required');
99
+ }
100
+ });
59
101
  return errors;
60
102
  };
61
103
  /**
@@ -130,10 +172,10 @@ var movePrimaryToHead = moveToHead(function (v) {
130
172
  });
131
173
  exports.movePrimaryToHead = movePrimaryToHead;
132
174
 
133
- var createAddress = function createAddress(_ref) {
134
- var address = _ref.address,
135
- oldContact = _ref.oldContact,
136
- t = _ref.t;
175
+ var createAddress = function createAddress(_ref2) {
176
+ var address = _ref2.address,
177
+ oldContact = _ref2.oldContact,
178
+ t = _ref2.t;
137
179
  return address ? address.filter(function (val) {
138
180
  return val && val.address;
139
181
  }).map(function (addressField, index) {
@@ -149,9 +191,9 @@ var createAddress = function createAddress(_ref) {
149
191
  if (addressHasBeenModified) {
150
192
  // Use "code" instead "postcode", to be vcard 4.0 rfc 6350 compliant
151
193
  // eslint-disable-next-line no-unused-vars
152
- var _ref2 = oldContactAddress || {},
153
- postcode = _ref2.postcode,
154
- oldContactAddressCleaned = (0, _objectWithoutProperties2.default)(_ref2, _excluded);
194
+ var _ref3 = oldContactAddress || {},
195
+ postcode = _ref3.postcode,
196
+ oldContactAddressCleaned = (0, _objectWithoutProperties2.default)(_ref3, _excluded);
155
197
 
156
198
  return _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, oldContactAddressCleaned), {}, {
157
199
  formattedAddress: addressField.address,
@@ -229,10 +271,10 @@ exports.getRelatedContactRelationships = getRelatedContactRelationships;
229
271
  var removeRelatedContactRelationships = function removeRelatedContactRelationships(contact) {
230
272
  if (!(contact !== null && contact !== void 0 && contact.relationships)) return contact;
231
273
  var updatedContact = (0, _merge.default)({}, contact);
232
- var relationshipsWithoutRelatedContact = Object.entries(updatedContact.relationships).reduce(function (acc, _ref3) {
233
- var _ref4 = (0, _slicedToArray2.default)(_ref3, 2),
234
- relName = _ref4[0],
235
- relValue = _ref4[1];
274
+ var relationshipsWithoutRelatedContact = Object.entries(updatedContact.relationships).reduce(function (acc, _ref4) {
275
+ var _ref5 = (0, _slicedToArray2.default)(_ref4, 2),
276
+ relName = _ref5[0],
277
+ relValue = _ref5[1];
236
278
 
237
279
  if ('related' === relName) {
238
280
  acc[relName] = relValue;
@@ -257,10 +299,10 @@ exports.removeRelatedContactRelationships = removeRelatedContactRelationships;
257
299
 
258
300
  var removeAsscociatedData = function removeAsscociatedData(contact) {
259
301
  if (!contact) return {};
260
- return Object.entries(contact).reduce(function (cleanedContact, _ref5) {
261
- var _ref6 = (0, _slicedToArray2.default)(_ref5, 2),
262
- key = _ref6[0],
263
- value = _ref6[1];
302
+ return Object.entries(contact).reduce(function (cleanedContact, _ref6) {
303
+ var _ref7 = (0, _slicedToArray2.default)(_ref6, 2),
304
+ key = _ref7[0],
305
+ value = _ref7[1];
264
306
 
265
307
  // Add `groups` condition to keep the old implementation functional, see below
266
308
  if (!(value instanceof _cozyClient.Association) || key === 'groups') {
@@ -456,9 +498,24 @@ var makeFields = function makeFields(customFields, defaultFields) {
456
498
  }
457
499
 
458
500
  var fields = (0, _toConsumableArray2.default)(defaultFields);
459
- customFields.map(function (customField) {
460
- if (customField.position) {
461
- fields.splice(customField.position, 0, customField);
501
+ customFields.forEach(function (customField) {
502
+ var defaultField = fields.find(function (field) {
503
+ return field.name === customField.name;
504
+ });
505
+
506
+ var _field = defaultField || customField;
507
+
508
+ if (defaultField) {
509
+ Object.assign(_field, customField);
510
+ }
511
+
512
+ if (_field.position) {
513
+ if (defaultField) {
514
+ var fieldIndex = fields.indexOf(defaultField);
515
+ fields.splice(fieldIndex, 1);
516
+ }
517
+
518
+ fields.splice(_field.position, 0, _field);
462
519
  }
463
520
  });
464
521
  return fields;
@@ -104,9 +104,14 @@ var ContactForm = function ContactForm(_ref) {
104
104
  t: t
105
105
  }),
106
106
  validate: function validate(values) {
107
- return (0, _helpers.validateFields)(values, t);
107
+ return (0, _helpers.validateFields)(values, _fields, t);
108
108
  },
109
109
  onSubmit: function onSubmit(formValues) {
110
+ // avoid creating an empty contact
111
+ if ((0, _helpers.hasNoValues)(formValues, _fields)) {
112
+ return;
113
+ }
114
+
110
115
  return _onSubmit((0, _formValuesToContact.default)({
111
116
  formValues: formValues,
112
117
  oldContact: contact,
@@ -32,7 +32,7 @@ var en = {
32
32
  relationship: "Relationship",
33
33
  note: "Notes",
34
34
  label: "Label",
35
- required: "One of these fields must be filled in"
35
+ required: "All required fields must be filled in"
36
36
  },
37
37
  label: {
38
38
  none: "None",
@@ -107,7 +107,7 @@ var fr = {
107
107
  relationship: "Relation",
108
108
  note: "Notes",
109
109
  label: "Libell\xE9",
110
- required: "Un de ces champs doit \xEAtre renseign\xE9"
110
+ required: "Tous les champs requis doivent \xEAtre renseign\xE9s"
111
111
  },
112
112
  label: {
113
113
  none: "Aucun",
@@ -182,7 +182,7 @@ var ru = {
182
182
  relationship: "\u041E\u0442\u043D\u043E\u0448\u0435\u043D\u0438\u0435",
183
183
  note: "\u0417\u0430\u043C\u0435\u0442\u043A\u0438",
184
184
  label: "\u041C\u0435\u0442\u043A\u0430",
185
- required: "\u041E\u0434\u043D\u043E \u0438\u0437 \u044D\u0442\u0438\u0445 \u043F\u043E\u043B\u0435\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E"
185
+ required: "\u0412\u0441\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u043E\u043B\u044F \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u044B"
186
186
  },
187
187
  label: {
188
188
  none: "\u041D\u0435\u0442",
@@ -257,7 +257,7 @@ var vi = {
257
257
  relationship: "M\u1ED1i quan h\u1EC7",
258
258
  note: "Ghi ch\xFA",
259
259
  label: "Nh\xE3n",
260
- required: "M\u1ED9t trong nh\u1EEFng tr\u01B0\u1EDDng n\xE0y ph\u1EA3i \u0111\u01B0\u1EE3c \u0111i\u1EC1n"
260
+ required: "T\u1EA5t c\u1EA3 c\xE1c tr\u01B0\u1EDDng b\u1EAFt bu\u1ED9c ph\u1EA3i \u0111\u01B0\u1EE3c \u0111i\u1EC1n"
261
261
  },
262
262
  label: {
263
263
  none: "Kh\xF4ng c\xF3",
@@ -1,5 +1,5 @@
1
1
  export default FieldInput;
2
- declare function FieldInput({ name, labelProps, className, attributes: { subFields, ...restAttributes }, contacts, error, helperText, label, isInvisible, required }: {
2
+ declare function FieldInput({ name, labelProps, className, attributes: { subFields, ...restAttributes }, contacts, error, helperText, label, isInvisible }: {
3
3
  name: any;
4
4
  labelProps: any;
5
5
  className: any;
@@ -12,7 +12,6 @@ declare function FieldInput({ name, labelProps, className, attributes: { subFiel
12
12
  helperText: any;
13
13
  label: any;
14
14
  isInvisible: any;
15
- required: any;
16
15
  }): JSX.Element;
17
16
  declare namespace FieldInput {
18
17
  namespace propTypes {
@@ -26,12 +25,9 @@ declare namespace FieldInput {
26
25
  }>>;
27
26
  export let id: PropTypes.Requireable<string>;
28
27
  export let label: PropTypes.Requireable<string>;
29
- export let required: PropTypes.Requireable<boolean>;
30
28
  }
31
29
  namespace defaultProps {
32
- export let labelProps: any;
33
- let required_1: boolean;
34
- export { required_1 as required };
30
+ let labelProps: any;
35
31
  }
36
32
  }
37
33
  import PropTypes from 'prop-types';
@@ -1,14 +1,10 @@
1
1
  export default FieldInputArray;
2
- declare function FieldInputArray({ attributes: { name, label, ...restAttributes }, contacts, formProps: { valid, submitFailed, errors } }: {
2
+ declare function FieldInputArray({ attributes: { name, label, ...restAttributes }, contacts, formProps }: {
3
3
  attributes: {
4
4
  [x: string]: any;
5
5
  name: any;
6
6
  label: any;
7
7
  };
8
8
  contacts: any;
9
- formProps: {
10
- valid: any;
11
- submitFailed: any;
12
- errors: any;
13
- };
9
+ formProps: any;
14
10
  }): JSX.Element;
@@ -1,5 +1,7 @@
1
- export const fieldsRequired: string[];
2
- export function validateFields(values: object, t: func): object;
1
+ export function getFirstValueIfArray(values: any): {};
2
+ export function hasNoValues(values: object, fields: import("../types").Field[]): boolean;
3
+ export function makeIsRequiredError(required: boolean, formProps: object): boolean;
4
+ export function validateFields(values: object, fields: import("../types").Field[], t: Function): object;
3
5
  export function makeItemLabel(item?: object): string;
4
6
  export function makeTypeAndLabel(itemLabel?: string): {
5
7
  type?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui-plus",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Cozy-ui-plus is an extension of cozy-ui and provides components based on cozy-client",
5
5
  "main": "dist/index.js",
6
6
  "license": "MIT",
@@ -74,5 +74,5 @@
74
74
  "react-dom": "^16 || ^17 || ^18",
75
75
  "twake-i18n": ">=0.3.0"
76
76
  },
77
- "gitHead": "b1d504a12eb88b8ecda4a53f82c400482d79452b"
77
+ "gitHead": "463474b57578fbca2e4fa04165e94abb82627d03"
78
78
  }
@@ -22,8 +22,7 @@ const FieldInput = ({
22
22
  error,
23
23
  helperText,
24
24
  label,
25
- isInvisible,
26
- required
25
+ isInvisible
27
26
  }) => {
28
27
  const [id] = useState(uniqueId('field_')) // state only use to generate id once and not at each render
29
28
  const [hasBeenFocused, setHasBeenFocused] = useState(false)
@@ -57,7 +56,6 @@ const FieldInput = ({
57
56
  )}
58
57
  >
59
58
  <Field
60
- required={required}
61
59
  error={error}
62
60
  helperText={helperText}
63
61
  label={label}
@@ -111,13 +109,11 @@ FieldInput.propTypes = {
111
109
  }),
112
110
  // Destructuring props
113
111
  id: PropTypes.string,
114
- label: PropTypes.string,
115
- required: PropTypes.bool
112
+ label: PropTypes.string
116
113
  }
117
114
 
118
115
  FieldInput.defaultProps = {
119
- labelProps: null,
120
- required: false
116
+ labelProps: null
121
117
  }
122
118
 
123
119
  export default FieldInput
@@ -11,16 +11,17 @@ import PlusIcon from 'cozy-ui/transpiled/react/Icons/Plus'
11
11
  import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
12
12
 
13
13
  import FieldInput from './FieldInput'
14
- import { fieldsRequired, addField, removeField } from './helpers'
14
+ import { addField, removeField, makeIsRequiredError } from './helpers'
15
15
  import { locales } from './locales'
16
16
 
17
17
  const FieldInputArray = ({
18
18
  attributes: { name, label, ...restAttributes },
19
19
  contacts,
20
- formProps: { valid, submitFailed, errors }
20
+ formProps
21
21
  }) => {
22
22
  useExtendI18n(locales)
23
23
  const { t } = useI18n()
24
+ const { errors } = formProps
24
25
 
25
26
  return (
26
27
  <FieldArray name={name}>
@@ -31,8 +32,10 @@ const FieldInputArray = ({
31
32
  const key = fields.value[index]?.fieldId || nameWithIndex
32
33
  const showRemove = fields.value[index]?.[name]
33
34
  const inputName = `${nameWithIndex}.${name}`
34
- const isError =
35
- fieldsRequired.includes(inputName) && !valid && submitFailed
35
+ const isError = makeIsRequiredError(
36
+ restAttributes.required,
37
+ formProps
38
+ )
36
39
 
37
40
  return (
38
41
  <div
@@ -8,7 +8,7 @@ import Icon from 'cozy-ui/transpiled/react/Icon'
8
8
  import FieldInput from './FieldInput'
9
9
  import FieldInputAccordion from './FieldInputAccordion'
10
10
  import FieldInputArray from './FieldInputArray'
11
- import { fieldsRequired } from './helpers'
11
+ import { makeIsRequiredError } from './helpers'
12
12
  import { locales } from './locales'
13
13
 
14
14
  const FieldInputLayout = ({
@@ -19,10 +19,10 @@ const FieldInputLayout = ({
19
19
  }) => {
20
20
  useExtendI18n(locales)
21
21
  const { t } = useI18n()
22
- const { valid, submitFailed, errors } = formProps
22
+ const { errors } = formProps
23
23
  const { name, label, ...restAttributes } = attributes
24
24
 
25
- const isError = fieldsRequired.includes(name) && !valid && submitFailed
25
+ const isError = makeIsRequiredError(restAttributes.required, formProps)
26
26
 
27
27
  return (
28
28
  <div
@@ -25,8 +25,8 @@ const FieldInputWrapper = ({
25
25
 
26
26
  return (
27
27
  <Component
28
- {...restAttributes}
29
28
  {...input}
29
+ {...restAttributes}
30
30
  {...props}
31
31
  variant={variant}
32
32
  fullWidth={fullWidth}
@@ -9,26 +9,63 @@ import { CONTACTS_DOCTYPE } from 'cozy-client/dist/models/contact'
9
9
 
10
10
  import contactToFormValues from './contactToFormValues'
11
11
 
12
- export const fieldsRequired = [
13
- 'givenName',
14
- 'familyName',
15
- 'email[0].email',
16
- 'cozy'
17
- ]
12
+ export const getFirstValueIfArray = values => {
13
+ return Object.keys(values).reduce((acc, key) => {
14
+ const val = values[key]
15
+ acc[key] = Array.isArray(val) ? val[0] : val
16
+ return acc
17
+ }, {})
18
+ }
18
19
 
19
20
  /**
20
- * Returns errors if all required fields are empty
21
+ * Returns true if all fields have no values
21
22
  * @param {object} values - Fields values
22
- * @param {func} t - Translation function
23
+ * @param {import('../types').Field[]} fields - Fields configuration
24
+ * @returns {boolean} True if all fields have no values
25
+ */
26
+ export const hasNoValues = (values, fields) => {
27
+ const _values = getFirstValueIfArray(values)
28
+
29
+ return fields.every(field => !get(_values, field.name))
30
+ }
31
+
32
+ /**
33
+ * Returns true if the input is required and the form is invalid
34
+ * @param {boolean} required - Whether the input is required
35
+ * @param {object} formProps - The form props
36
+ * @returns {boolean} True if the input is required and the form is invalid
37
+ */
38
+ export const makeIsRequiredError = (required, formProps) => {
39
+ const { valid, submitFailed } = formProps
40
+
41
+ return required && !valid && submitFailed
42
+ }
43
+
44
+ /**
45
+ * Returns errors for required fields that are empty
46
+ * @param {object} values - Fields values
47
+ * @param {import('../types').Field[]} fields - Fields configuration
48
+ * @param {Function} t - Translation function
23
49
  * @returns {object} Errors
24
50
  */
25
- export const validateFields = (values, t) => {
51
+ export const validateFields = (values, fields, t) => {
26
52
  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
- }
53
+ const fieldsRequired = fields.filter(field => field.required)
54
+ const _values = getFirstValueIfArray(values)
55
+
56
+ fieldsRequired.forEach(field => {
57
+ if (!get(_values, field.name)) {
58
+ errors[field.name] =
59
+ field.layout === 'array'
60
+ ? [
61
+ {
62
+ [field.name]: t('Contacts.AddModal.ContactForm.fields.required')
63
+ }
64
+ ]
65
+ : t('Contacts.AddModal.ContactForm.fields.required')
66
+ }
67
+ })
68
+
32
69
  return errors
33
70
  }
34
71
 
@@ -384,9 +421,20 @@ export const makeFields = (customFields, defaultFields) => {
384
421
 
385
422
  const fields = [...defaultFields]
386
423
 
387
- customFields.map(customField => {
388
- if (customField.position) {
389
- fields.splice(customField.position, 0, customField)
424
+ customFields.forEach(customField => {
425
+ const defaultField = fields.find(field => field.name === customField.name)
426
+ const _field = defaultField || customField
427
+
428
+ if (defaultField) {
429
+ Object.assign(_field, customField)
430
+ }
431
+
432
+ if (_field.position) {
433
+ if (defaultField) {
434
+ const fieldIndex = fields.indexOf(defaultField)
435
+ fields.splice(fieldIndex, 1)
436
+ }
437
+ fields.splice(_field.position, 0, _field)
390
438
  }
391
439
  })
392
440
 
@@ -7,7 +7,10 @@ import {
7
7
  makeImppValues,
8
8
  makeCustomLabel,
9
9
  makeFields,
10
- makeInitialCustomValue
10
+ makeInitialCustomValue,
11
+ hasNoValues,
12
+ getFirstValueIfArray,
13
+ validateFields
11
14
  } from './helpers'
12
15
  import { locales } from './locales'
13
16
 
@@ -388,4 +391,344 @@ describe('makeFields', () => {
388
391
 
389
392
  expect(res).toStrictEqual(defaultFields)
390
393
  })
394
+
395
+ it('should replace default fields values by custom fields', () => {
396
+ const defaultFields = [
397
+ { name: 'firstname', type: 'text', layout: 'accordion' },
398
+ { name: 'lastname' }
399
+ ]
400
+ const customFields = [
401
+ { name: 'firstname', isSecondary: true, layout: 'array' }
402
+ ]
403
+
404
+ const res = makeFields(customFields, defaultFields)
405
+
406
+ expect(res).toStrictEqual([
407
+ { name: 'firstname', type: 'text', isSecondary: true, layout: 'array' },
408
+ { name: 'lastname' }
409
+ ])
410
+ })
411
+
412
+ it('should replace default fields values and add custom fields at custom position', () => {
413
+ const defaultFields = [
414
+ { name: 'firstname', type: 'text', layout: 'accordion' },
415
+ { name: 'lastname' }
416
+ ]
417
+ const customFields = [
418
+ { name: 'firstname', isSecondary: true, layout: 'array', position: 1 }
419
+ ]
420
+
421
+ const res = makeFields(customFields, defaultFields)
422
+
423
+ expect(res).toStrictEqual([
424
+ { name: 'lastname' },
425
+ {
426
+ name: 'firstname',
427
+ type: 'text',
428
+ isSecondary: true,
429
+ layout: 'array',
430
+ position: 1
431
+ }
432
+ ])
433
+ })
434
+ })
435
+
436
+ describe('hasNoValues', () => {
437
+ it('should return true when all fields have no values', () => {
438
+ const values = {}
439
+ const fields = [{ name: 'firstname' }, { name: 'lastname' }]
440
+
441
+ const res = hasNoValues(values, fields)
442
+
443
+ expect(res).toBe(true)
444
+ })
445
+
446
+ it('should return true when all field values are undefined', () => {
447
+ const values = { firstname: undefined, lastname: undefined }
448
+ const fields = [{ name: 'firstname' }, { name: 'lastname' }]
449
+
450
+ const res = hasNoValues(values, fields)
451
+
452
+ expect(res).toBe(true)
453
+ })
454
+
455
+ it('should return true when all field values are null', () => {
456
+ const values = { firstname: null, lastname: null }
457
+ const fields = [{ name: 'firstname' }, { name: 'lastname' }]
458
+
459
+ const res = hasNoValues(values, fields)
460
+
461
+ expect(res).toBe(true)
462
+ })
463
+
464
+ it('should return true when all field values are empty strings', () => {
465
+ const values = { firstname: '', lastname: '' }
466
+ const fields = [{ name: 'firstname' }, { name: 'lastname' }]
467
+
468
+ const res = hasNoValues(values, fields)
469
+
470
+ expect(res).toBe(true)
471
+ })
472
+
473
+ it('should return true when all field values are empty arrays', () => {
474
+ const values = { email: [], phone: [] }
475
+ const fields = [{ name: 'email' }, { name: 'phone' }]
476
+
477
+ const res = hasNoValues(values, fields)
478
+
479
+ expect(res).toBe(true)
480
+ })
481
+
482
+ it('should return false when at least one field has a value', () => {
483
+ const values = { firstname: 'John', lastname: undefined }
484
+ const fields = [{ name: 'firstname' }, { name: 'lastname' }]
485
+
486
+ const res = hasNoValues(values, fields)
487
+
488
+ expect(res).toBe(false)
489
+ })
490
+
491
+ it('should return false when at least one field has a non-empty string', () => {
492
+ const values = { firstname: '', lastname: 'Doe' }
493
+ const fields = [{ name: 'firstname' }, { name: 'lastname' }]
494
+
495
+ const res = hasNoValues(values, fields)
496
+
497
+ expect(res).toBe(false)
498
+ })
499
+
500
+ it('should return false when at least one field has a non-empty array', () => {
501
+ const values = { email: [{ address: 'john.doe@cozycloud.cc' }], phone: [] }
502
+ const fields = [{ name: 'email' }, { name: 'phone' }]
503
+
504
+ const res = hasNoValues(values, fields)
505
+
506
+ expect(res).toBe(false)
507
+ })
508
+
509
+ it('should return true for nested field paths with no values', () => {
510
+ const values = { phone: [{ number: undefined }] }
511
+ const fields = [{ name: 'phone[0].number' }]
512
+
513
+ const res = hasNoValues(values, fields)
514
+
515
+ expect(res).toBe(true)
516
+ })
517
+
518
+ it('should handle mixed array and non-array values', () => {
519
+ const values = { firstname: 'John', lastname: ['Doe'] }
520
+ const fields = [{ name: 'firstname' }, { name: 'lastname' }]
521
+
522
+ const res = hasNoValues(values, fields)
523
+
524
+ expect(res).toBe(false)
525
+ })
526
+
527
+ it('should return true when fields array is empty', () => {
528
+ const values = { firstname: 'John', lastname: 'Doe' }
529
+ const fields = []
530
+
531
+ const res = hasNoValues(values, fields)
532
+
533
+ expect(res).toBe(true)
534
+ })
535
+ })
536
+
537
+ describe('getFirstValueIfArray', () => {
538
+ it('should return the first value of arrays', () => {
539
+ const values = {
540
+ phone: [{ phone: '123' }, { phone: '456' }],
541
+ email: [{ email: 'test@example.com' }],
542
+ name: 'John'
543
+ }
544
+
545
+ const res = getFirstValueIfArray(values)
546
+
547
+ expect(res).toEqual({
548
+ phone: { phone: '123' },
549
+ email: { email: 'test@example.com' },
550
+ name: 'John'
551
+ })
552
+ })
553
+
554
+ it('should return undefined for empty arrays', () => {
555
+ const values = {
556
+ phone: [],
557
+ email: [{ email: 'test@example.com' }]
558
+ }
559
+
560
+ const res = getFirstValueIfArray(values)
561
+
562
+ expect(res).toEqual({
563
+ phone: undefined,
564
+ email: { email: 'test@example.com' }
565
+ })
566
+ })
567
+
568
+ it('should not mutate the original object', () => {
569
+ const values = {
570
+ phone: [{ phone: '123' }, { phone: '456' }],
571
+ name: 'John'
572
+ }
573
+ const originalValues = JSON.parse(JSON.stringify(values))
574
+
575
+ getFirstValueIfArray(values)
576
+
577
+ expect(values).toEqual(originalValues)
578
+ })
579
+
580
+ it('should handle non-array values', () => {
581
+ const values = {
582
+ name: 'John',
583
+ age: 30,
584
+ active: true,
585
+ address: null
586
+ }
587
+
588
+ const res = getFirstValueIfArray(values)
589
+
590
+ expect(res).toEqual({
591
+ name: 'John',
592
+ age: 30,
593
+ active: true,
594
+ address: null
595
+ })
596
+ })
597
+
598
+ it('should handle mixed array and non-array values', () => {
599
+ const values = {
600
+ phone: [{ phone: '123' }],
601
+ name: 'John',
602
+ email: []
603
+ }
604
+
605
+ const res = getFirstValueIfArray(values)
606
+
607
+ expect(res).toEqual({
608
+ phone: { phone: '123' },
609
+ name: 'John',
610
+ email: undefined
611
+ })
612
+ })
613
+
614
+ it('should return empty object for empty input', () => {
615
+ const values = {}
616
+
617
+ const res = getFirstValueIfArray(values)
618
+
619
+ expect(res).toEqual({})
620
+ })
621
+ })
622
+
623
+ describe('validateFields', () => {
624
+ it('should return errors for required fields that are empty (non-array fields)', () => {
625
+ const values = {
626
+ name: '',
627
+ email: undefined
628
+ }
629
+ const fields = [
630
+ { name: 'name', required: true },
631
+ { name: 'email', required: true },
632
+ { name: 'phone', required: false }
633
+ ]
634
+
635
+ const res = validateFields(values, fields, t)
636
+
637
+ expect(res).toEqual({
638
+ name: 'All required fields must be filled in',
639
+ email: 'All required fields must be filled in'
640
+ })
641
+ })
642
+
643
+ it('should return errors for required array fields that are empty', () => {
644
+ const values = {
645
+ phone: []
646
+ }
647
+ const fields = [{ name: 'phone', required: true, layout: 'array' }]
648
+
649
+ const res = validateFields(values, fields, t)
650
+
651
+ expect(res).toEqual({
652
+ phone: [
653
+ {
654
+ phone: 'All required fields must be filled in'
655
+ }
656
+ ]
657
+ })
658
+ })
659
+
660
+ it('should not return errors for required array fields with valid first element', () => {
661
+ const values = {
662
+ phone: [{ phone: '123456789' }]
663
+ }
664
+ const fields = [{ name: 'phone', required: true, layout: 'array' }]
665
+
666
+ const res = validateFields(values, fields, t)
667
+
668
+ expect(res).toEqual({})
669
+ })
670
+
671
+ it('should not return errors for non-required fields', () => {
672
+ const values = {
673
+ name: '',
674
+ phone: []
675
+ }
676
+ const fields = [
677
+ { name: 'name', required: false },
678
+ { name: 'phone', required: false, layout: 'array' }
679
+ ]
680
+
681
+ const res = validateFields(values, fields, t)
682
+
683
+ expect(res).toEqual({})
684
+ })
685
+
686
+ it('should handle mixed array and non-array required fields', () => {
687
+ const values = {
688
+ name: '',
689
+ phone: []
690
+ }
691
+ const fields = [
692
+ { name: 'name', required: true },
693
+ { name: 'phone', required: true, layout: 'array' }
694
+ ]
695
+
696
+ const res = validateFields(values, fields, t)
697
+
698
+ expect(res).toEqual({
699
+ name: 'All required fields must be filled in',
700
+ phone: [
701
+ {
702
+ phone: 'All required fields must be filled in'
703
+ }
704
+ ]
705
+ })
706
+ })
707
+
708
+ it('should use getFirstValueIfArray to check only first element of arrays', () => {
709
+ const values = {
710
+ phone: [{ phone: '123' }, {}]
711
+ }
712
+ const fields = [{ name: 'phone', required: true, layout: 'array' }]
713
+
714
+ const res = validateFields(values, fields, t)
715
+
716
+ // Should not return error because first element has phone value
717
+ expect(res).toEqual({})
718
+ })
719
+
720
+ it('should return empty errors object when no required fields are empty', () => {
721
+ const values = {
722
+ name: 'John',
723
+ phone: [{ phone: '123456789' }]
724
+ }
725
+ const fields = [
726
+ { name: 'name', required: true },
727
+ { name: 'phone', required: true, layout: 'array' }
728
+ ]
729
+
730
+ const res = validateFields(values, fields, t)
731
+
732
+ expect(res).toEqual({})
733
+ })
391
734
  })
@@ -10,7 +10,7 @@ import FieldInputLayout from './FieldInputLayout'
10
10
  import contactToFormValues from './contactToFormValues'
11
11
  import { fields as defaultFields } from './fieldsConfig'
12
12
  import formValuesToContact from './formValuesToContact'
13
- import { validateFields, makeFields } from './helpers'
13
+ import { validateFields, makeFields, hasNoValues } from './helpers'
14
14
  import { locales } from './locales'
15
15
  // import { fullContactPropTypes } from '../../ContactPropTypes' // !!
16
16
 
@@ -55,9 +55,14 @@ const ContactForm = ({ contacts, contact, customFieldsProps, onSubmit }) => {
55
55
  makeCustomContactValues,
56
56
  t
57
57
  })}
58
- validate={values => validateFields(values, t)}
59
- onSubmit={formValues =>
60
- onSubmit(
58
+ validate={values => validateFields(values, _fields, t)}
59
+ onSubmit={formValues => {
60
+ // avoid creating an empty contact
61
+ if (hasNoValues(formValues, _fields)) {
62
+ return
63
+ }
64
+
65
+ return onSubmit(
61
66
  formValuesToContact({
62
67
  formValues,
63
68
  oldContact: contact,
@@ -65,7 +70,7 @@ const ContactForm = ({ contacts, contact, customFieldsProps, onSubmit }) => {
65
70
  t
66
71
  })
67
72
  )
68
- }
73
+ }}
69
74
  render={({ handleSubmit, valid, submitFailed, errors }) => {
70
75
  setSubmitContactForm(handleSubmit)
71
76
  return (
@@ -26,7 +26,7 @@
26
26
  "relationship": "Relationship",
27
27
  "note": "Notes",
28
28
  "label": "Label",
29
- "required": "One of these fields must be filled in"
29
+ "required": "All required fields must be filled in"
30
30
  },
31
31
  "label": {
32
32
  "none": "None",
@@ -26,7 +26,7 @@
26
26
  "relationship": "Relation",
27
27
  "note": "Notes",
28
28
  "label": "Libellé",
29
- "required": "Un de ces champs doit être renseigné"
29
+ "required": "Tous les champs requis doivent être renseignés"
30
30
  },
31
31
  "label": {
32
32
  "none": "Aucun",
@@ -26,7 +26,7 @@
26
26
  "relationship": "Отношение",
27
27
  "note": "Заметки",
28
28
  "label": "Метка",
29
- "required": "Одно из этих полей должно быть заполнено"
29
+ "required": "Все обязательные поля должны быть заполнены"
30
30
  },
31
31
  "label": {
32
32
  "none": "Нет",
@@ -26,7 +26,7 @@
26
26
  "relationship": "Mối quan hệ",
27
27
  "note": "Ghi chú",
28
28
  "label": "Nhãn",
29
- "required": "Một trong những trường này phải được điền"
29
+ "required": "Tất cả các trường bắt buộc phải được điền"
30
30
  },
31
31
  "label": {
32
32
  "none": "Không có",