mirror of
https://github.com/twentyhq/twenty.git
synced 2026-08-02 18:54:43 -04:00
# 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.
214 lines
5.3 KiB
JSON
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"
|
|
]
|
|
}
|
|
}
|
|
}
|