Files
twenty/packages/twenty-shared/package.json
Félix Malfait 7e133a4930 Converge email recipient fields on existing patterns: shared parser/formatter, search-index members, one display-name rule (#22997)
# Why

Follow-up to #22668, addressing @charlesBochet's five post-merge review
comments. They all point the same direction: the recipient fields
rebuilt things the codebase already had. This PR converges on the
existing patterns where that holds up, and answers on the threads where
it deliberately does not.

# What changed, per comment

**Parser duplication
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064242))**:
`parseEmailAddressList` now lives in twenty-shared (addressparser, group
flattening, try/catch). The server's `safeParseEmailAddresses` delegates
to it, the front wrapper keeps only paste normalization (newlines to
commas) and invalid-token preservation for red chips. The
`addressparser` dependency moves from twenty-front to twenty-shared.
Side effect worth knowing: RFC 5322 group members in inbound To/Cc
headers were previously dropped entirely (group entries have no
top-level address, so the filter removed them); flattening now imports
those participants. Covered by a new regression test.

**Formatter duplication
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064243))**:
`formatEmailAddress` (quote only when specials require it) lives in
twenty-shared. The composer chips and the server's
`formatMessageFromHeader` both delegate to it. The Gmail From header
output is byte-identical: the name is mime-encoded first and encoded
words never contain characters that trigger quoting. CodeQL then caught
that the quoting (ported from the original front util) escaped quotes
but not backslashes, letting a crafted name close the quoted string
early; escaping now covers both as RFC 5322 quoted-pairs, with a
containment test proving a hostile name cannot split into extra
recipients on reparse.

**Member search divergence
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064231))**:
suggestions now search WorkspaceMember through the search index in the
same `useObjectRecordSearchRecords` call as Person (one ranked query),
and enrich hits from `currentWorkspaceMembersState`, exactly like
`SettingsRoleAssignmentWorkspaceMemberPickerDropdown`. The client-side
`filterBySearchQuery` pass is gone. The hook is now what the comment
described: the merge of context people, searched people, and members
into one ranked list, rendered with the same
`SelectableList`/`MenuItemAvatar` primitives the pickers use. Also fixed
while in there: searched person ids are sliced to the suggestion limit
before hydration, so top-ranked people can no longer be crowded out of
the hydration page.

**Chip resolution duplication
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064236))**:
the display-name preference is now one rule,
`getEmailIdentityDisplayName`, used by both
`getDisplayNameFromParticipant` (threads) and the composer chip/menu, so
the same address renders identically everywhere. The order is workspace
member, then person, then display name, then handle: when an address
belongs to both a teammate and a Person record, the internal identity
wins (product call from Felix). `BaseChip.maxLabelWidth` is renamed
`maxWidth` to match the twenty-ui `Chip` API. `ParticipantChip` itself
is not used inside the field: it renders a navigating `RecordChip` when
a person is linked, and navigation from the composer destroys the draft
(no draft persistence yet), plus the field chips need
remove/selected/danger/edit affordances it does not have.

**Rebuilding on MultiItemFieldInput
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064221))**:
answered on the thread rather than in code, deliberately.
`MultiItemFieldInput` is a dropdown-panel list editor (vertical rows,
one input at a time, bound to record-field contexts and
`FieldMetadataType`), and its own TODO says the API should be refactored
into a hook before growing. The inline wrapping chip row commits batches
(paste), dedupes with a flash, and keeps a persistent inline input with
suggestions; layering that through `renderItem`/`renderInput` would
strain both components. On the menu overlap: after comparing side by
side, the shared surface between `MultiItemFieldMenuItem`'s dropdown and
the chip menu is three `MenuItem` rows with different copy, order, and
neighbors; `MenuItem` is already the shared primitive, and a
config-driven fragment would be indirection without deduplication. If
deeper convergence is wanted, the honest path is the existing TODO
(extract the multi-item state machine into a hook, rebase both editors
on it); that touches the Links/Phones/Emails/Array/Files cell editors
and deserves its own PR.

# Verification

- New twenty-shared suites for the parser and formatter (16 tests),
including parse/format round-trips, the encoded-word case, and the
backslash-escaping containment case.
- Server messaging util specs all pass (70 tests), including new
group-flattening regression tests; From-header spec output unchanged.
- Front email module suites all pass (59 tests) with the slimmed
wrappers.
- Typecheck and lint green on twenty-shared, twenty-front,
twenty-server; oxfmt clean on all three.
- Playwright smoke against the seeded dev stack passes end to end:
context suggestions on the Google company, typed search showing people
and the workspace member row (now served by the search index), Enter
picking the top suggestion, duplicate merge, keyboard delete, chip menu
with clipboard copy, Ctrl+Enter committing the buffer then triggering
send.
2026-07-17 21:20:14 +02:00

214 lines
5.3 KiB
JSON

{
"name": "twenty-shared",
"private": true,
"sideEffects": false,
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"license": "AGPL-3.0",
"scripts": {
"build": "npx vite build"
},
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
},
"devDependencies": {
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.24.6",
"@lingui/core": "^5.9.5",
"@prettier/sync": "^0.5.2",
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@types/addressparser": "^1.0.3",
"@types/babel__preset-env": "^7",
"@types/handlebars": "^4.1.0",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/qs": "6.9.16",
"@types/uuid": "^9.0.2",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"babel-plugin-module-resolver": "^5.0.2",
"glob": "^11.1.0",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"prettier": "^3.1.1",
"slash": "^5.1.0",
"tsc-alias": "^1.8.16",
"tsx": "^4.19.3",
"typescript": "^5.9.3",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"@dagrejs/dagre": "^1.1.8",
"@sniptt/guards": "^0.2.0",
"addressparser": "1.0.1",
"ai": "6.0.97",
"class-validator": "^0.14.0",
"expr-eval-fork": "3.0.3",
"handlebars": "^4.7.9",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.escaperegexp": "^4.1.2",
"microdiff": "^1.3.2",
"qs": "^6.15.2",
"react-router-dom": "^6.4.4",
"temporal-polyfill": "^0.3.0",
"transliteration": "^2.3.5",
"uuid": "^11.1.1",
"zod": "^4.1.11"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./ai": {
"types": "./dist/ai/index.d.ts",
"import": "./dist/ai.mjs",
"require": "./dist/ai.cjs"
},
"./application": {
"types": "./dist/application/index.d.ts",
"import": "./dist/application.mjs",
"require": "./dist/application.cjs"
},
"./constants": {
"types": "./dist/constants/index.d.ts",
"import": "./dist/constants.mjs",
"require": "./dist/constants.cjs"
},
"./database-events": {
"types": "./dist/database-events/index.d.ts",
"import": "./dist/database-events.mjs",
"require": "./dist/database-events.cjs"
},
"./i18n": {
"types": "./dist/i18n/index.d.ts",
"import": "./dist/i18n.mjs",
"require": "./dist/i18n.cjs"
},
"./logic-function": {
"types": "./dist/logic-function/index.d.ts",
"import": "./dist/logic-function.mjs",
"require": "./dist/logic-function.cjs"
},
"./metadata": {
"types": "./dist/metadata/index.d.ts",
"import": "./dist/metadata.mjs",
"require": "./dist/metadata.cjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing.mjs",
"require": "./dist/testing.cjs"
},
"./timeline": {
"types": "./dist/timeline/index.d.ts",
"import": "./dist/timeline.mjs",
"require": "./dist/timeline.cjs"
},
"./translations": {
"types": "./dist/translations/index.d.ts",
"import": "./dist/translations.mjs",
"require": "./dist/translations.cjs"
},
"./types": {
"types": "./dist/types/index.d.ts",
"import": "./dist/types.mjs",
"require": "./dist/types.cjs"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
"./vite": {
"types": "./dist/vite/index.d.ts",
"import": "./dist/vite.mjs",
"require": "./dist/vite.cjs"
},
"./workflow": {
"types": "./dist/workflow/index.d.ts",
"import": "./dist/workflow.mjs",
"require": "./dist/workflow.cjs"
},
"./workspace": {
"types": "./dist/workspace/index.d.ts",
"import": "./dist/workspace.mjs",
"require": "./dist/workspace.cjs"
}
},
"files": [
"dist",
"ai",
"application",
"constants",
"database-events",
"i18n",
"logic-function",
"metadata",
"testing",
"timeline",
"translations",
"types",
"utils",
"vite",
"workflow",
"workspace"
],
"typesVersions": {
"*": {
"ai": [
"dist/ai/index.d.ts"
],
"application": [
"dist/application/index.d.ts"
],
"constants": [
"dist/constants/index.d.ts"
],
"database-events": [
"dist/database-events/index.d.ts"
],
"i18n": [
"dist/i18n/index.d.ts"
],
"logic-function": [
"dist/logic-function/index.d.ts"
],
"metadata": [
"dist/metadata/index.d.ts"
],
"testing": [
"dist/testing/index.d.ts"
],
"timeline": [
"dist/timeline/index.d.ts"
],
"translations": [
"dist/translations/index.d.ts"
],
"types": [
"dist/types/index.d.ts"
],
"utils": [
"dist/utils/index.d.ts"
],
"vite": [
"dist/vite/index.d.ts"
],
"workflow": [
"dist/workflow/index.d.ts"
],
"workspace": [
"dist/workspace/index.d.ts"
]
}
}
}