Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Configuration for AI agents

## ESLint Configuration

This project uses ESLint rules from `cozy-libs`. When generating code, strictly follow these rules:

### Configuration files

- `config/eslint-config-cozy-app/basics.js`
- `config/eslint-config-cozy-app/react.js`

### Important rules to follow

#### Prettier (formatting)

```javascript
'prettier/prettier': [
'error',
{
arrowParens: 'avoid', // No parentheses for single-parameter arrow functions
endOfLine: 'auto', // Automatic line ending
semi: false, // NO SEMICOLONS at end of lines
singleQuote: true, // Single quotes
trailingComma: 'none' // NO TRAILING COMMAS
}
]
```

#### Import/Order (import order)

- Mandatory alphabetical order (`alphabetize: { order: 'asc' }`)
- Import groups in this order:
1. `builtin` (Node.js modules)
2. `external` (npm packages)
3. `internal` (project internal imports)
4. `['parent', 'sibling', 'index']` (relative imports)
- Special pattern for cozy-\_ and twake-\_ (position after external)
- Empty line between each group (`newlines-between: 'always'`)

#### React

- `react/prop-types`: off (disabled)
- `react/jsx-curly-brace-presence`: no unnecessary braces for props and children
- No `display-name` in .spec files

#### TypeScript (if applicable)

- `explicit-function-return-type`: required
- `no-explicit-any`: forbidden

### Compliant code example

```javascript
import React from "react";

import Button from "cozy-ui/transpiled/react/Buttons";
import cx from "classnames";

import FieldInput from "./FieldInput";
import { makeIsRequiredError } from "./helpers";

const MyComponent = ({ name, label }) => {
const isError = makeIsRequiredError(name);

return (
<div className={cx("u-mt-1", { "u-flex": isError })}>
<FieldInput name={name} label={label} />
</div>
);
};

export default MyComponent;
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Note:

- No semicolons
- Single quotes
- No trailing comma
- Alphabetical import order (React before Button, Button before cx)
- Empty line between import groups (builtin/external, external/internal, etc.)
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,32 @@ import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { Field } from 'react-final-form'

import { useBreakpoints } from 'cozy-ui/transpiled/react/providers/Breakpoints'
import { useI18n, useExtendI18n } from 'twake-i18n'

import FieldInputWrapper from './FieldInputWrapper'
import HasValueCondition from './HasValueCondition'
import { RelatedContactList } from './RelatedContactList'
import RemoveButton from './RemoveButton'
import { locales } from './locales'
import styles from './styles.styl'
import ContactAddressDialog from '../ContactAddressDialog'
import { fieldInputAttributesTypes, labelPropTypes } from '../types'

const FieldAndRemove = ({ showRemove, onRemove, ...props }) => {
const { isMobile } = useBreakpoints()

if (isMobile) {
return (
<div className="u-flex u-flex-items-center u-w-100">
<Field {...props} />
{showRemove && <RemoveButton onRemove={onRemove} />}
</div>
)
}

return <Field {...props} />
}

const FieldInput = ({
name,
labelProps,
Expand All @@ -22,6 +38,8 @@ const FieldInput = ({
contacts,
contact,
error,
onRemove,
showRemove,
helperText,
label,
isInvisible
Expand All @@ -33,6 +51,7 @@ const FieldInput = ({
useState(false)
useExtendI18n(locales)
const { t } = useI18n()
const { isMobile } = useBreakpoints()

const handleClick = () => {
if (name.includes('address')) {
Expand All @@ -50,14 +69,12 @@ const FieldInput = ({

return (
<div
className={cx(
className,
styles['contact-form-field__wrapper'],
'u-flex-column-s',
{ 'u-flex': !isInvisible, 'u-dn': isInvisible }
)}
className={cx(className, 'u-flex-column-s u-flex-items-center u-w-100', {
'u-flex': !isInvisible,
'u-dn': isInvisible
})}
>
<Field
<FieldAndRemove
error={error}
helperText={helperText}
label={label}
Expand All @@ -66,6 +83,8 @@ const FieldInput = ({
name={name}
contact={contact}
component={FieldInputWrapper}
showRemove={showRemove}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should do showRemove={showRemove && isMobile} instead of managing mobile directly inside the component?

It is managed differently between desktop and mobile.

Copy link
Copy Markdown
Contributor Author

@JF-Cozy JF-Cozy Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a good point 🤔 I prefer to separate responsibilities. In that case doing showRemove={showRemove && isMobile} will have same result, yes, but responsibilities are not the same. I think it will be easier that way to refactor in the future, if necessary

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responsabilities are better separated if we move it to the parent app now? I feel that the FieldAndRemove does "too much". But non blocking for me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we have the same behavior in 2 apps right now (admin & contacts).

onRemove={onRemove}
onFocus={onFocus}
onClick={handleClick}
/>
Expand All @@ -85,7 +104,7 @@ const FieldInput = ({
)}
{labelProps && (
<HasValueCondition name={name} otherCondition={hasBeenFocused}>
<div className="u-mt-half-s u-ml-half u-ml-0-s u-flex-shrink-0 u-w-auto u-miw-4">
<div className="u-mt-half-s u-ml-half u-ml-0-s u-flex-shrink-0 u-w-100-s u-w-auto u-miw-4">
<Field
attributes={labelProps}
name={`${name}Label`}
Expand All @@ -97,6 +116,7 @@ const FieldInput = ({
</div>
</HasValueCondition>
)}
{showRemove && !isMobile && <RemoveButton onRemove={onRemove} />}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { FieldArray } from 'react-final-form-arrays'

import Button from 'cozy-ui/transpiled/react/Buttons'
import Icon from 'cozy-ui/transpiled/react/Icon'
import IconButton from 'cozy-ui/transpiled/react/IconButton'
import CrossCircleIcon from 'cozy-ui/transpiled/react/Icons/CrossCircle'
import PlusIcon from 'cozy-ui/transpiled/react/Icons/Plus'
import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
import { useI18n, useExtendI18n } from 'twake-i18n'

import FieldInput from './FieldInput'
Expand Down Expand Up @@ -53,22 +50,14 @@ const FieldInputArray = ({
contacts={contacts}
contact={contact}
error={isError}
index={index}
showRemove={showRemove}
helperText={isError ? errors[inputName] : null}
name={inputName}
label={t(`Contacts.AddModal.ContactForm.fields.${name}`)}
labelProps={label}
onRemove={() => removeField(fields, index)}
/>
{showRemove && (
<ListItemIcon className="u-ml-half">
<IconButton
aria-label="delete"
size="medium"
onClick={() => removeField(fields, index)}
>
<Icon icon={CrossCircleIcon} />
</IconButton>
</ListItemIcon>
)}
</div>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ const FieldInputLayout = ({
'u-dn': isSecondary && !showSecondaryFields
})}
>
<div className="u-w-2-half">
{icon && <Icon icon={icon} color="var(--iconTextColor)" />}
</div>
{icon && (
<div className="u-w-2-half">
<Icon icon={icon} color="var(--iconTextColor)" />
</div>
)}
<div className="u-w-100">
{layout === 'array' ? (
<FieldInputArray
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ const FieldInputWrapper = ({
attributes: {
component,
layout,
isHalfWidth,
icon,
isSecondary,
validate,
...restAttributes
}, // ⚠️ `layout` `icon` `isSecondary` `validate` are removed from attributes to avoid DOM propagration, only used for business rules
}, // ⚠️ `layout` `isHalfWidth` `icon` `isSecondary` `validate` are removed from attributes to avoid DOM propagration, only used for business rules
variant,
fullWidth,
...props
Expand All @@ -37,6 +38,7 @@ const FieldInputWrapper = ({
{...props}
variant={variant}
fullWidth={fullWidth}
size="small"
minRows="2"
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'

import Icon from 'cozy-ui/transpiled/react/Icon'
import IconButton from 'cozy-ui/transpiled/react/IconButton'
import CrossCircleIcon from 'cozy-ui/transpiled/react/Icons/CrossCircle'
import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'

const RemoveButton = ({ onRemove }) => {
return (
<ListItemIcon className="u-ml-half">
<IconButton aria-label="delete" size="medium" onClick={onRemove}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize the delete button accessibility label.

Line 11 uses a hardcoded English aria-label, which makes screen-reader text non-localized in an otherwise translated form flow.

Suggested change
-const RemoveButton = ({ onRemove }) => {
+const RemoveButton = ({ onRemove, ariaLabel = 'delete' }) => {
   return (
     <ListItemIcon className="u-ml-half">
-      <IconButton aria-label="delete" size="medium" onClick={onRemove}>
+      <IconButton aria-label={ariaLabel} size="medium" onClick={onRemove}>
         <Icon icon={CrossCircleIcon} />
       </IconButton>
     </ListItemIcon>
   )
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<IconButton aria-label="delete" size="medium" onClick={onRemove}>
const RemoveButton = ({ onRemove, ariaLabel = 'delete' }) => {
return (
<ListItemIcon className="u-ml-half">
<IconButton aria-label={ariaLabel} size="medium" onClick={onRemove}>
<Icon icon={CrossCircleIcon} />
</IconButton>
</ListItemIcon>
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cozy-ui-plus/src/Contacts/AddModal/ContactForm/RemoveButton.jsx` at
line 11, The IconButton in RemoveButton.jsx currently uses a hardcoded English
aria-label ("delete"); replace this with a localized string obtained from the
app's i18n system (e.g. use the existing translation hook or helper in scope)
and pass that value to aria-label so screen readers receive the translated text;
locate the IconButton component in RemoveButton.jsx (the element with
onClick={onRemove}) and swap the literal aria-label for the translated key (e.g.
t('contacts.delete') or the appropriate namespace) ensuring fallback/defaults
are preserved.

<Icon icon={CrossCircleIcon} />
</IconButton>
</ListItemIcon>
)
}

export default RemoveButton
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const fields = [
icon: null,
type: 'text',
layout: 'accordion',
isHalfWidth: true,
subFields: [
{
name: 'additionalName',
Expand All @@ -56,7 +57,8 @@ export const fields = [
{
name: 'familyName',
icon: null,
type: 'text'
type: 'text',
isHalfWidth: true
},
{
name: 'company',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,17 +460,26 @@ export const makeFields = (customFields, defaultFields) => {
return defaultFields
}

const fields = [...defaultFields]
let fields = [...defaultFields]

customFields.forEach(customField => {
const defaultField = fields.find(field => field.name === customField.name)

// If isRemoved is true, remove the field from the result
if (customField.isRemoved) {
if (defaultField) {
fields = fields.filter(field => field.name !== customField.name)
}
return
}

const _field = defaultField || customField

if (defaultField) {
Object.assign(_field, customField)
}

if (_field.position) {
if (_field.position !== undefined) {
if (defaultField) {
const fieldIndex = fields.indexOf(defaultField)
fields.splice(fieldIndex, 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,68 @@ describe('makeFields', () => {
}
])
})

it('should remove default field when custom field has isRemoved set to true', () => {
const defaultFields = [
{ name: 'firstname' },
{ name: 'lastname' },
{ name: 'email' }
]
const customFields = [{ name: 'lastname', isRemoved: true }]

const res = makeFields(customFields, defaultFields)

expect(res).toStrictEqual([{ name: 'firstname' }, { name: 'email' }])
})

it('should remove multiple default fields when multiple custom fields have isRemoved set to true', () => {
const defaultFields = [
{ name: 'firstname' },
{ name: 'lastname' },
{ name: 'email' },
{ name: 'phone' }
]
const customFields = [
{ name: 'lastname', isRemoved: true },
{ name: 'phone', isRemoved: true }
]

const res = makeFields(customFields, defaultFields)

expect(res).toStrictEqual([{ name: 'firstname' }, { name: 'email' }])
})

it('should ignore custom fields with isRemoved but no matching default field', () => {
const defaultFields = [{ name: 'firstname' }, { name: 'lastname' }]
const customFields = [{ name: 'nonexistent', isRemoved: true }]

const res = makeFields(customFields, defaultFields)

expect(res).toStrictEqual(defaultFields)
})

it('should handle mixed custom fields with isRemoved, position, and property overrides', () => {
const defaultFields = [
{ name: 'firstname', type: 'text' },
{ name: 'lastname', type: 'text' },
{ name: 'email', type: 'email' },
{ name: 'phone', type: 'tel' }
]
const customFields = [
{ name: 'lastname', isRemoved: true },
{ name: 'email', isSecondary: true, position: 0 },
{ name: 'middlename', position: 1 }
]

const res = makeFields(customFields, defaultFields)

expect(res).toStrictEqual([
{ name: 'email', type: 'email', isSecondary: true, position: 0 },
{ name: 'middlename', position: 1 },
{ name: 'firstname', type: 'text' },
{ name: 'phone', type: 'tel' }
])
})
})

describe('hasNoValues', () => {
Expand Down
Loading
Loading