Compare commits

...
Author SHA1 Message Date
neo773 f5717752be wip 2026-02-12 02:38:33 +05:30
neo773 24796e9355 refactor caldav 2026-02-12 01:42:59 +05:30
15fc850212 Remove redundant self-build from Nx targets that compile on the fly (#17851)
## Summary

- Changed 6 Nx targets (`start`, `start:debug`, `typeorm`, `ts-node`,
`database:migrate`, `database:migrate:revert`) from `dependsOn:
["build"]` to `dependsOn: ["^build"]` to eliminate a redundant full SWC
compilation step (~30-60s per invocation).
- These targets all use `nest start --watch`, `ts-node`, or TypeORM's
CLI (which runs under ts-node), so they already compile TypeScript
themselves. The `build` dependency was causing `rimraf dist && nest
build` to run first, only for the target's own command to recompile
everything again.
- Fixed `start:debug` to call `nest start --watch --debug` directly
instead of routing through `nx start --debug`, which would trigger yet
another build cycle.

Note: targets that run pre-compiled code from `dist/` (like
`database:reset`) intentionally keep `dependsOn: ["build"]`.

## Test plan

- [ ] Run `npx nx start twenty-server` and verify the server starts with
only one SWC compilation pass instead of two
- [ ] Run `npx nx start:debug twenty-server` and verify the debugger
attaches correctly
- [ ] Run `npx nx run twenty-server:database:migrate` and verify
migrations run correctly
- [ ] Run `npx nx run twenty-server:database:reset twenty-server` and
verify it still builds before running (unchanged)


Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-11 17:38:22 +00:00
e6d5df751b i18n - translations (#17874)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-11 18:25:44 +01:00
5b4fed1afe Fix redirect to deleted workspace subdomain after workspace deletion (#17865)
## Summary
- After deleting a workspace, the app was incorrectly redirecting to the
deleted workspace's subdomain (e.g. `myworkspace.ourapp.com/sign-in-up`)
because `signOut()` only performs a client-side React Router navigation
which stays on the current domain.
- Added an explicit `redirectToDefaultDomain()` call after sign out in
the workspace deletion flow, which does a hard browser redirect to the
base domain (e.g. `app.ourapp.com`).

## Test plan
- [ ] Delete a workspace in a multi-workspace environment
- [ ] Verify the browser redirects to the base domain (`app.ourapp.com`)
instead of staying on the deleted workspace's subdomain
- [ ] Verify normal sign-out (without workspace deletion) still works as
expected

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-11 18:25:27 +01:00
e757a1418d Fix hardcoded colors in 2FA verification screen for dark mode (#17868)
## Summary
- The dash separator and blinking caret in the sign-in 2FA OTP input had
hardcoded `black` and `white` background colors, making them invisible
in dark mode (black on black / white on white).
- Replaced with theme-aware colors (`theme.font.color.light` for the
dash, `theme.font.color.primary` for the caret) to match the existing
settings 2FA component.

## Test plan
- [ ] Open the 2FA verification screen in dark mode and verify the dash
between digit groups is visible
- [ ] Verify the blinking caret is visible in dark mode
- [ ] Confirm both still look correct in light mode

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-11 18:23:42 +01:00
EtienneandGitHub 2f298307f9 Handle 413 with user friendly message (#17870)
Add friendly message for 413 errors. Currently, 413 errors originate
from the nginx server.
2026-02-11 17:03:22 +00:00
WeikoandGitHub c15536f9f8 Fix page layout seeding for record page layouts (#17871)
## Context
Fix broken record page layout seeding. This was not detected by the CI
because it doesn't have the env variable yet.

Following the same mechanism as labelIdentifier in object for circular
dependency resolution
2026-02-11 16:56:21 +00:00
Paul RastoinandGitHub b2f7c745f8 Solo transaction application synchronization service refactor (#17864)
# Introduction
Refactoring the application sync service to be making a single validate
build and run transaction instead of calling all the services n times
thanks to the universal workspace migration refactor that allow doing so

## What's next
Migrating all below entities to by syncableEntities so they can be
universalised too ( right now they're still calling the services n times
and won't be returned in the workspace migration )
-
[objectPermission](https://github.com/twentyhq/core-team-issues/issues/2223)
-
[fieldPermission](https://github.com/twentyhq/core-team-issues/issues/2224)
-
[permissionFlag](https://github.com/twentyhq/core-team-issues/issues/2225)
2026-02-11 16:30:01 +00:00
0ee63a0525 i18n - translations (#17872)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-11 17:35:20 +01:00
18880f0385 i18n - translations (#17869)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-11 17:05:37 +01:00
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>Devessier
0902579fbe Feat: Navbar customization (#17728)
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <[email protected]>
2026-02-11 15:33:51 +00:00
52e57e70fd Add wildcard documentation for like/ilike/containsIlike filters (#17825)
Add documentation for issue #16602 
After discussing with the team (Thomas),
https://discord.com/channels/1130383047699738754/1443986309436936212 we
decided that updating the documentation.
The issue is In compute-where-condition-parts.ts, the like/ilike cases
pass values directly to SQL without adding % wildcards for api using, so
they behave like exact matches.

This PR updates the documentation regarding the use of `like`, `ilike`
and `containsIlike` filters. Instead of auto-wrapping values with %
wildcards in the backend, we are choosing to leave the control to the
API users (%value% or value%).

<img width="651" height="409" alt="image"
src="https://github.com/user-attachments/assets/b3537af6-a0b0-4fff-a86d-a9ae334d628e"
/>

But I add wildcard for `startsWith` and `endsWith` because these
operators have a fixed semantic meaning.

(To see the results, please refresh the cache first, then restart the
server.)

<img width="878" height="458" alt="image"
src="https://github.com/user-attachments/assets/ab0f4e7c-df50-45ef-b1c8-e43c8881a9a3"
/><img width="482" height="288" alt="image"
src="https://github.com/user-attachments/assets/20dc39ee-2417-4ecc-810e-ea0ead33d803"
/>

---------

Co-authored-by: Thomas Trompette <[email protected]>
2026-02-11 15:32:41 +00:00
Thomas TrompetteandGitHub 1e01f15182 Fix code step and logic function step in workflows (#17856)
- AI still often forgets to update the code step after creating it.
Adding a next step
- Starting by loading logic functions, so it avoids creating code steps
when a function exists
- Fix create complete workflow logic. Should not create code steps
directly
2026-02-11 15:08:23 +00:00
EtienneandGitHub d0c1841f0f File - Migrate avatarUrl > avatarFile on person (data migration + logic) + Attachment data migration (#17752)
- Migration command
    - Check IS_FILES_FIELD_MIGRATED:false
    - Check or create avatarFile field
    - Fetch all people with avatarUrl
           - Move (Copy/move) file in storage
           - Create core.file record
           - Update person record
           
    - bonus : attachment migration  : fullPath > file (same logic)
   
- BE logic
    - Add avatarFile field on person

- FE logic 
   - Adapt logic to upload on/display avatarFile data

The whole imageIdentifier logic will be done later
2026-02-11 15:07:05 +00:00
Thomas TrompetteandGitHub 5be64bf4be Remove guard from find logic functions (#17862)
As title
2026-02-11 14:41:40 +01:00
Paul RastoinandGitHub d9dab75052 Do not throw on corrupted labelFieldMetadataIdentifier (#17859)
# Introduction
As we don't enforce any FK on object labelIdentifierFieldMetadataId we
have some that are either null or pointing to non-existing field
metadata resulting in exception thrown at cache computation lvl

Commenting the exception throw until we've closed
https://github.com/twentyhq/core-team-issues/issues/2172

closes https://github.com/twentyhq/core-team-issues/issues/2221
2026-02-11 12:33:38 +00:00
2237273869 Fix spurious logouts by deduplicating concurrent token renewals (#17858)
## Summary

- When returning to the app after idle (or after a deploy), the expired
access token causes multiple simultaneous GraphQL queries to fail with
`UNAUTHENTICATED`. Previously, each failure independently triggered its
own `renewToken` call with the same refresh token. If **any single**
renewal failed (e.g. server briefly slow after a deploy), the `catch`
handler would nuke the session and redirect to sign-in — even if another
concurrent renewal had already succeeded and written valid tokens.
- This adds a shared `renewalPromise` so that only the first
`UNAUTHENTICATED` error triggers a server-side renewal. All concurrent
callers await the same promise and replay their operations once it
resolves. This eliminates redundant refresh token rotation on the server
and removes the race condition where a straggling failure could log out
an already-renewed session.

## Test plan

- [ ] Log in, wait >30 minutes (or manually expire the access token),
then interact with the app — should silently renew without redirect to
sign-in
- [ ] Open browser DevTools Network tab, trigger the above scenario, and
verify only **one** `renewToken` mutation is sent (instead of N)
- [ ] With server temporarily stopped, verify that a genuine renewal
failure still correctly redirects to sign-in (single
`onUnauthenticatedError` call)
- [ ] Open multiple browser tabs, let access tokens expire, interact in
one tab — other tabs should also recover gracefully on their next
request


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
2026-02-11 12:28:52 +00:00
Charles BochetandGitHub 9e21e55db4 Prevent leak between /metadata and /graphql GQL schemas (#17845)
## Fix resolver schema leaking between `/metadata` and `/graphql`
endpoints

### Summary
- Patch `@nestjs/graphql` to support a `resolverSchemaScope` option that
filters resolvers at both schema generation and runtime, preventing
cross-endpoint leaking
- Introduce `@CoreResolver()` and `@MetadataResolver()` decorators to
explicitly scope each resolver to its endpoint
- Move most resolvers (auth, billing, workspace, user, etc.) to the
metadata schema where the frontend expects them; only workflow and
timeline calendar/messaging resolvers remain on `/graphql`
- Fix frontend `SSEQuerySubscribeEffect` to use the default (metadata)
Apollo client instead of the core client

### Problem
NestJS GraphQL's module-based resolver discovery traverses transitive
imports, causing resolvers from `/metadata` modules to leak into the
`/graphql` schema and vice versa. This made the schemas unpredictable
and tightly coupled to module import order.

### Approach
- Added `resolverSchemaScope` to `GqlModuleOptions` via a patch on
`@nestjs/graphql`, filtering in both `filterResolvers()` (runtime
binding) and `getAllCtors()` (schema generation)
- Each resolver is explicitly decorated with `@CoreResolver()` or
`@MetadataResolver()`
- Organized decorator, constant, and type files under `graphql-config/`
following project conventions


Core GQL Schema: (see: no more fields!)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/668f3f0f-485e-43f0-92be-4345aeccacb6"
/>

Metadata GQL Schema (see no more getTimelineCalendarEventsFromCompany)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/443913db-e5fe-4161-b0e7-4a971cc80a71"
/>
2026-02-11 10:05:24 +00:00
859241d237 Fix merge records page accumulating duplicate morph items (#17705)
## Why
When opening **Merge records** repeatedly, morph items for the same
command-menu page were appended instead of replaced. This could produce
duplicated IDs (e.g. `[A,B,B,A]`) in the merge flow and extra duplicate
tabs in the UI.

## What
- Update `useCommandMenuUpdateNavigationMorphItemsByPage` to replace
page morph items instead of appending existing ones.
- Add regression tests covering:
  - replacing existing morph items for the same page
  - keeping only the latest payload when called twice for the same page

## Notes
I could not run the full workspace tests locally in this environment
because of existing test/build setup issues unrelated to this change
(missing `packages/twenty-front/tsconfig.spec.json` and
`temporal-polyfill` resolution in dependent tasks).

Co-authored-by: remi <[email protected]>
2026-02-11 09:55:19 +00:00
Paul RastoinandGitHub 41e413d7b1 Runner metadata events (#17841)
# Introduction
Followup https://github.com/twentyhq/twenty/pull/17622

Refactoring the actions handler to be returning a metadata event
It has to be done incrementally, as if not update metadata event would
be stale as depends on the incremental action execution order and
optimistic application

## What's next
- Builder should consume and regroup each metadata even in order to
batch emit them ( within a single metadata actions batch order matters )
=> refactoring https://github.com/twentyhq/twenty/pull/17622 in order to
consume runner returned metadata events
2026-02-11 09:09:54 +00:00
b0cb29c11c perf: cache ServerBlockNoteEditor instance in transformRichTextV2Value (#17844)
## Summary
- `transformRichTextV2Value` was calling `await
import('@blocknote/server-util')` and `ServerBlockNoteEditor.create()`
on **every single invocation**, adding ~90ms of overhead each time
(visible as the highest avg-duration frame in profiling at 93.49ms).
- Cache the `ServerBlockNoteEditor` instance at module level so the
dynamic import + creation only happens once for the lifetime of the
process.
- Also removes debug timing instrumentation (`performance.now()`,
`calculateInputSize`, `Logger`) that is no longer needed.

## Test plan
- [ ] Verify rich text fields (blocknote/markdown) still round-trip
correctly on create and update
- [ ] Confirm reduced CPU time for `transformRichTextV2Value` in
profiling


Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-11 09:08:59 +00:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
b4d957306e fix: show user-friendly error message when duplicate invite is sent (#17827)
Fixes #17822

Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-11 00:06:42 +01:00
9c984b137f Fix phone validation performance by using Set/Map instead of Array lookups (#17843)
## Summary
- **`isValidCountryCode`** was using `Array.includes()` on ~250 country
codes (O(n) per call). Replaced with a `Set.has()` lookup (O(1)).
- **`getCountryCodesForCallingCode`** was iterating all ~250 countries
and calling `getCountryCallingCode()` on each one **every invocation**.
Replaced with a precomputed `Map<callingCode, CountryCode[]>` built once
at module load (O(1) per call).

Both functions are called from `transformPhonesValue` on every phone
field mutation, causing cumulative overhead visible in profiling (p95
self-time ~20-35ms).

## Test plan
- [ ] Verify phone field creation/update still works correctly (country
code validation, calling code resolution)
- [ ] Verify spreadsheet import with phone fields still validates
properly
- [ ] Confirm no regression in `isValidCountryCode` or
`getCountryCodesForCallingCode` behavior


Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-10 19:56:43 +00:00
05a6a96b13 i18n - translations (#17842)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-02-10 20:23:26 +01:00
16d414590b Lowercase email (#17775)
Fixes https://github.com/twentyhq/core-team-issues/issues/120 #16976
Partially related to #17711

Frontend check surprisingly was one-liner covering both email input and
import files

Migration script will be done in next commit

---------

Co-authored-by: Etienne <[email protected]>
2026-02-10 18:24:49 +00:00
bc72879c70 Fix commands order for v1.17.0 (#17839)
Order should be

1. We delete all the file records (from core.file table)
2. We add the foreign key file / applicationId (pg constraint)
3. We further update the table structure: fullPath is deleted; path is
created; unicity constraint between workspaceId/applicationId/path is
created (pg constraint)
4. we migrate the workflow steps (this will create files in core.file)
5. we backfill the application package (same)

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-02-10 18:47:59 +01:00
bb037e13dd Fix blocknote.map crash with generic field-level RICH_TEXT_V2 handler (#17834)
## Summary

Fixes #17667

- **Root cause**: `ActivityQueryResultGetterHandler` called
`JSON.parse()` on the `blocknote` field and assumed the result was
always an array. When the stored value was valid JSON but not an array
(e.g., `"{}"`), `blocknote.map()` crashed with `blocknote.map is not a
function`, breaking the entire notes page.
- **Fix**: Replaced the object-level `ActivityQueryResultGetterHandler`
(hardcoded for `note`/`task` only) with a generic field-level
`RichTextV2FieldQueryResultGetterHandler` that safely parses blocknote
JSON with `Array.isArray` validation and gracefully skips malformed
values instead of crashing.
- **Bonus**: The new handler works for **all** objects with
`RICH_TEXT_V2` fields (not just `note`/`task`), following the same
pattern as the existing `FilesFieldQueryResultGetterHandler`.

## Changes

| File | Change |
|------|--------|
| `rich-text-v2-field-query-result-getter.handler.ts` | New field-level
handler with safe blocknote parsing |
| `common-result-getters.service.ts` | Register new handler, remove
`note`/`task` object handlers |
| `activity-query-result-getter.handler.ts` | Deleted (replaced by
field-level handler) |
| `rich-text-v2-field-query-result-getter.handler.spec.ts` | 9 tests
covering all edge cases |

## Test plan

- [x] Unit tests pass (9 tests covering: null blocknote, non-string
blocknote, invalid JSON, non-array JSON like `"{}"`, no images, external
URLs, internal URLs, multiple fields)
- [x] Lint passes (`lint:diff-with-main`)
- [x] Typecheck passes


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
2026-02-10 17:07:49 +00:00
Thomas TrompetteandGitHub 17786a3298 SSO - Check if assertion is signed (#17837)
As title
2026-02-10 15:29:34 +00:00
8f91529153 i18n - translations (#17838)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-10 15:24:40 +01:00
e995e84621 [DASHBOARDS] chat agent improvements + new validation layer (#17722)
https://github.com/user-attachments/assets/09550210-76c5-4a40-83b6-9ab785ca10c3



https://github.com/user-attachments/assets/352427fc-0a2a-4f1b-86e9-db99daea0018

---------

Co-authored-by: Félix Malfait <[email protected]>
Co-authored-by: Félix Malfait <[email protected]>
2026-02-10 13:43:36 +00:00
Paul RastoinandGitHub 148584c730 Migrate all remaining workspace migration create action to universal (#17836)
# Introduction
Completely finalize the universal migration at builder and runner
levels.
Which mean that from now on the builder only compares
`universalFlatEntity` and produces universal workspace migration that
the runner ingest

## What's done
Migrated all create metadata remaining for
- agent
- skill
- commandMenuItem
- navigationMenuItem
- fieldMetadata
- objectMetadata
- view
- viewField
- viewFilter
- viewGroup
- viewFilterGroup
- index
- logicFunction
- role
- roleTarget
- pageLayout
- pageLayoutTab
- pageLayoutWidget
- rowLevelPermissionPredicate
- rowLevelPermissionPredicateGroup
- frontComponent
2026-02-10 13:39:37 +00:00
Charles BochetandGitHub b7ff587b5e Query cache instead of database for event listener webhook, logicFunction, triggers (#17824)
## Fix
Replaced direct database queries with existing flat entity map caches
for the two that already have cache infrastructure:
- **CallDatabaseEventTriggerJobsJob** - now uses flatLogicFunctionMaps
cache via WorkspaceCacheService.getOrRecompute(), filtering in memory
for non-deleted logic functions with databaseEventTriggerSettings.
- **CallWebhookJobsJob** - now uses flatWebhookMaps cache via
WorkspaceCacheService.getOrRecompute(), filtering in memory for webhooks
matching the event's operations.
- Note: **WorkflowDatabaseEventTriggerListener** - left as-is since
WorkflowAutomatedTriggerWorkspaceEntity extends BaseWorkspaceEntity (not
SyncableEntity) and has no flat entity map cache infrastructure yet.
2026-02-10 13:14:06 +00:00
2e77a68daf i18n - translations (#17835)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-10 12:44:49 +01:00
BOHEUSandGitHub 382746dd52 Clarify forms usage (#17772)
Fixes https://github.com/twentyhq/core-team-issues/issues/1704
2026-02-10 12:35:37 +01:00
WeikoandGitHub 202a130ac8 revert releasing RLS (#17809) (#17832) 2026-02-10 11:48:59 +01:00
Thomas des FrancsandGitHub 24c6d3fef3 Fix row-level permissions 'Where' left spacing (#17831)
<img width="634" height="289" alt="image"
src="https://github.com/user-attachments/assets/c8fe85e1-ecac-4b57-be36-6c4a5e426c60"
/>
2026-02-10 11:48:32 +01:00
martmullandGitHub 52fe21c04b Fix ci + improvements (#17795)
as title
2026-02-10 11:48:10 +01:00
Paul RastoinandGitHub ee0474e287 Remove standard ids (#17833)
Deadcode
2026-02-10 10:37:55 +00:00
Thomas des FrancsandGitHub 3f202c5c6a Fix downgrade to Pro CTA icon and wording (#17829)
## Summary
- Fixes the downgrade CTA icon for `Switch to Pro` from an up arrow to a
down arrow.
- Fixes wording generation so no `undefined` suffix is concatenated in
downgrade/plan-switch confirmation copy.

## Before

<img width="717" height="535" alt="image"
src="https://github.com/user-attachments/assets/f934d130-c9fa-46bb-b677-88edf64bc623"
/>

<img width="508" height="325" alt="image"
src="https://github.com/user-attachments/assets/4c776922-a169-4543-b791-fcefc093fa7f"
/>

## Testing
- Not run locally in this environment.
2026-02-10 09:41:48 +00:00
a63b31931f Extract SecureHttpClientService into its own module (#17828)
## Summary

- **Extract `SecureHttpClientService`** from `tool` module into a
dedicated `core-modules/secure-http-client/` module with proper NestJS
module encapsulation
- **Fix module hygiene**: 12 modules that incorrectly listed
`SecureHttpClientService` as a direct provider now properly import
`SecureHttpClientModule`
- **Add structured logging** for outbound HTTP requests with
workspace/user context (for GuardDuty alert correlation)
- **Rename type files** to follow one-export-per-file convention
(`get-secure-axios-adapter.types.ts` ->
`secure-adapter-dependencies.type.ts`, new
`outbound-request-context.type.ts` / `outbound-request-source.type.ts`)

### Why

`SecureHttpClientService` is a cross-cutting concern (used by auth,
captcha, file upload, geo-map, telemetry, admin-panel, REST API, contact
creation, webhooks, and workflow tools) but was bundled inside the
`tool` module. Most consumers worked around this by listing it as a
direct provider instead of importing a module, which is fragile and not
idiomatic NestJS.

## Test plan

- [x] All 60 unit tests pass (`secure-http-client.service.spec.ts`,
`get-secure-axios-adapter.util.spec.ts`, `is-private-ip.util.spec.ts`)
- [x] Related module tests pass (admin-panel, contact-creation, tool)
- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] Server compiles and bootstraps successfully


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
2026-02-10 09:16:51 +00:00
3c2aec1894 i18n - translations (#17830)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-10 10:32:49 +01:00
Baptiste DevessierandGitHub b250de216e Remove the code of old show pages (#17811)
Closes https://github.com/twentyhq/core-team-issues/issues/2213
2026-02-10 09:10:00 +00:00
2d42b0a726 - Added accomodation of mobile navigation bar in command menu (#17419)
Fix for #16743 

Added styles to accommodate for mobile navigation bar for better
visibility and better access of the command menu buttons.

## CommandMenuAskAIPage.tsx (Not shown in the issue)
### Before
<img width="1100" height="1792" alt="screenshot-2026-01-24_17-22-45"
src="https://github.com/user-attachments/assets/8a337ffe-9dfc-4e52-b744-622cc6989ae4"
/>

### After 
Since the component here already had some padding on it the mobile
navigation bar offset was lesser than it is in other places.
<img width="1092" height="1778" alt="screenshot-2026-01-24_17-28-33"
src="https://github.com/user-attachments/assets/00a217f1-d98e-4303-bdc1-df112d71a553"
/>


## CommandMenuWorkflowEditStep.tsx
### Before
As mentioned in the issue

### After
<img width="755" height="1061" alt="image"
src="https://github.com/user-attachments/assets/487d0b20-f544-4e85-99d5-eb5add4b2cb4"
/>

## CommandMenuWorkflowRunViewStep.tsx
### Before
As shown in the issue

### After
<img width="1102" height="1784" alt="screenshot-2026-01-24_17-19-31"
src="https://github.com/user-attachments/assets/a95773e3-5f61-46d0-93cb-e678e087a42f"
/>

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-02-09 23:48:45 +01:00
Abdullah.andGitHub 7e5e800f63 fix: remove axios stale entry by ensuring transitive version of axios is also latest (#17823)
Last upgrade PR left stale entries for transitive import of Axios. 

Raising this PR to fix and ensure Axios 1.13.5 is the de-facto in
yarn.lock for consistency.
2026-02-09 23:23:06 +01:00
Baptiste DevessierandGitHub 8c70a875f8 fix: set a margin even for empty side-column widgets (#17806)
| | Read | Edit |
|--------|--------|--------|
| **Before** | <img width="762" height="2144" alt="CleanShot 2026-02-09
at 16 07 06@2x"
src="https://github.com/user-attachments/assets/d93b5928-2c7b-438a-b16a-d17a434276fc"
/> | <img width="762" height="2144" alt="CleanShot 2026-02-09 at 16 07
01@2x"
src="https://github.com/user-attachments/assets/c5fb94db-947d-4832-9bc2-7942faa290eb"
/> |
| **After** | <img width="762" height="2144" alt="CleanShot 2026-02-09
at 16 06 33@2x"
src="https://github.com/user-attachments/assets/761352f3-cf18-4f41-98f0-fb0b4367f486"
/> | <img width="762" height="2144" alt="CleanShot 2026-02-09 at 16 06
41@2x"
src="https://github.com/user-attachments/assets/d1780671-1043-4087-860c-020343fcda87"
/> |


Closes https://github.com/twentyhq/core-team-issues/issues/2193
2026-02-09 22:59:00 +01:00
Abdullah.andGitHub a40612ef9e fix: axios related dependabot alerts (#17821)
Resolves [Dependabot Alert
436](https://github.com/twentyhq/twenty/security/dependabot/436),
[Dependabot Alert
437](https://github.com/twentyhq/twenty/security/dependabot/437) and
[Dependabot Alert
438](https://github.com/twentyhq/twenty/security/dependabot/438).
2026-02-09 22:56:34 +01:00
neo773andGitHub 1979e013e9 Google OAuth check real permissions before creating channels (#17714) 2026-02-09 22:49:03 +01:00
3747005fd5 i18n - translations (#17820)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-09 22:43:06 +01:00
neo773andGitHub 268cc40d80 Show accounts with pending message channel configuration (#17770) 2026-02-09 22:29:27 +01:00
MarieGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
11fd1a41f3 [Fix] fix timelineActivities on notes and tasks (#17814)
[Fixes
sentry](https://twenty-v7.sentry.io/issues/7238708876/?environment=prod&environment=prod-eu&project=4507072499810304&query=Field%20metadata%20for%20field&referrer=issue-stream&sort=date):
_Field metadata for field "targetTargetPersonId" is missing in object
metadata timelineActivity_

Regression caused by migration of note and task targets - fields were
renamed from personId to targetPersonId, impacting the event names,
while we still expected them in their previous shape.

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <[email protected]>
2026-02-09 20:47:43 +00:00
05eca08ad2 releasing RLS (#17809)
Co-authored-by: Charles Bochet <[email protected]>
2026-02-09 21:56:41 +01:00
Abdullah.andGitHub ef1464db73 Improve AI-chat design to match the one provided in Figma (#17816)
Fixed colors, gaps, component nesting, alignment to match Figma.
2026-02-09 18:28:25 +00:00
neo773andGitHub 5ccf18fdf5 Add handling for internal_failure in Gmail API error parser (#17718)
fixes TWENTY-SERVER-D3X
2026-02-09 18:02:09 +00:00
9e6f19d16e Fix RLS creation logic (#17815)
Fix after resolveEntityRelationUniversalIdentifiers introduction
```
FlatEntityMapsException [Error]: Could not find rowLevelPermissionPredicateGroup for given rowLevelPermissionPredicateGroupId
        at resolveEntityRelationUniversalIdentifiers
```

- Creating util for RLS flat entity creation to be aligned with other
entities
- Progressively update the flat entity maps as each new group is built,
following the same optimistic pattern used by
computeUniversalFlatEntityMapsFromTo in the validate build and run. The
updated maps are now passed to computePredicateOperations so predicates
can also resolve references to the newly created groups.
- Adding more coverage

Co-authored-by: Charles Bochet <[email protected]>
2026-02-09 19:19:23 +01:00
Charles BochetandGitHub 987ed845ac Release Twenty SDK 0.5.0 (#17818)
As per title + create-twenty-app
2026-02-09 19:16:24 +01:00
f51291704d [FRONT COMPONENTS] Retrieve the front components from the backend (#17813)
- Pass the auth token to worker
- Fetch the file from the rest API with the auth token
- Create the blob url
- Update the stories to pass a fake token which will be ignored

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-02-09 19:10:29 +01:00
Charles BochetandGitHub 37b9a55382 Migrate metrics to prometheus (#17810)
<img width="1316" height="611" alt="image"
src="https://github.com/user-attachments/assets/277a63ed-2a8b-41ff-be78-281de8891579"
/>
2026-02-09 19:09:13 +01:00
aa7973e5b8 i18n - translations (#17817)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-09 19:08:48 +01:00
8c951d3623 Migrate Views-xxx Index Field Object Skill to be fully universal ( all actions and metadata runner and builder ) + all metadata update actions runner (#17687)
# What this PR does
Overall naming `universal` versus `flat` is not always the most updated
and so on
Will make a big cleaning tour after I've finished the whole migration
Migrating all `view` and ( filter fields etc ) `field` `object` `index`
to the universal pattern on all `services`, `builder` and `runner`
levels

## Universal and flat optimistic tooling

`addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow`
and its delete counterpart maintain the consistency of
`UniversalFlatEntityMaps` when an entity is created or removed. Beyond
inserting/removing the entity from its own maps, they walk through
`ALL_UNIVERSAL_METADATA_RELATIONS` to update the **aggregator arrays**
on related parent entities — the add appends the new entity's
`universalIdentifier` to the parent's aggregator (e.g. a new viewField's
identifier gets appended to its parent view's
`viewFieldUniversalIdentifiers`), and the delete filters it out. This
keeps the maps in sync so that diff computations and relation lookups
remain accurate throughout the migration building process.

## ALL_UNIVERSAL_METADATA_RELATIONS
`ALL_UNIVERSAL_METADATA_RELATIONS` is the universal counterpart of
`ALL_METADATA_RELATIONS`. It maps each metadata entity to its
many-to-one and one-to-many relations using universal foreign keys
(`*UniversalIdentifier`) instead of database IDs (`*Id`). This allows
migration actions to reference related entities in a workspace-agnostic
way. Relations that are workspace-specific (e.g. `workspace`,
`dataSource`, `userWorkspace`) are set to `null` and skipped during
resolution.

## `workspaceMigrationCreateIdEnrichment`
Reserved to API metadata ( will be able to validate at app installation
lvl )
- Workspace migration `create` actions now carry an optional `id` (and
`fieldIdByUniversalIdentifier` for object/field actions) so that
caller-provided IDs flow through the entire build-validate-run pipeline.
- New `enrichCreateWorkspaceMigrationActionsWithIds` utility resolves
`universalIdentifier → id` mappings after the builder runs and injects
them into the migration actions before the runner persists entities.
- Runner action handlers use the provided IDs instead of generating new
UUIDs, enabling deterministic entity creation for synchronization
workflows.


## `resolveUniversalUpdateRelationIdentifiersToIds`
`resolveUniversalUpdateRelationIdentifiersToIds` converts universal
identifiers (workspace-agnostic, stable keys) in a migration update
payload into concrete database UUIDs, so the update can be applied to a
specific workspace.

It iterates over the many-to-one relations defined in
`ALL_UNIVERSAL_METADATA_RELATIONS` for the given entity type, replaces
each `*UniversalIdentifier` property with its corresponding `*Id` by
looking up the target entity in `allFlatEntityMaps`, and throws if a
non-null identifier can't be resolved.

Used by all `update` action handlers in
`transpileUniversalActionToFlatAction`, avoiding duplicated resolution
logic across handlers.

## What this PR does not
- Migrating twenty-standard declaration to universal
- Migrating all the inputs transpilers to universal
- Migrating all metadata to be fully universal ( we still need to
de-scope the type of all of them and refactor their validator very close
)

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-02-09 19:02:38 +01:00
2a76e1791e Allow AI to update code steps (#17761)
https://github.com/user-attachments/assets/35ac5c23-4d5c-4c86-8233-7b58fbeb5a27

- add tool
- move resolver code into a service

---------

Co-authored-by: Félix Malfait <[email protected]>
2026-02-09 15:40:44 +00:00
Weiko 4ae375308f Revert "Releasing RLS"
This reverts commit c01853c349.
2026-02-09 16:52:58 +01:00
Weiko c01853c349 Releasing RLS 2026-02-09 16:52:12 +01:00
MarieandGitHub 21b2b65dbe [Fix] fix relations in apps (#17791)
When syncing an app with relation multiple times, it would fail because
the relation fields could not be identified due to universalIdentifier
being overwritten at field cretion
2026-02-09 15:22:14 +00:00
Raphaël BosiandGitHub c6d04ccced [FRONT COMPONENTS] Serialize events through the worker boundary (#17767)
- Introduces a SerializedEventData type that captures only serializable
properties from DOM/React events
- Updates `wrapEventHandler` in the host component registry to serialize
native events via serializeEvent() before passing them across the worker
boundary via postMessage, avoiding circular references and non-cloneable
DOM nodes
- Updates generated remote element event signatures to use
RemoteEvent<SerializedEventData> instead of bare RemoteEvent, giving
front-component authors typed access to event details
2026-02-09 16:22:54 +01:00
c18726d712 Fix captcha validation failing due to missing URL in secure axios adapter (#17807)
## Summary

- Fixes login failing with `Error: URL is required` when captcha (Google
reCAPTCHA or Turnstile) is enabled
- The secure axios adapter (`getSecureAxiosAdapter`) checked
`config.url` before `baseURL` resolution. In Axios, `baseURL + url`
combination happens inside the default HTTP adapter, not before custom
adapters are called. When captcha drivers configure a `baseURL` and call
`.post('', data)`, the empty string url was incorrectly treated as
missing.
- The adapter now resolves `baseURL` + `url` itself before validation,
matching the default Axios HTTP adapter behavior

## Test plan

- [x] Existing unit tests (23 tests) all pass
- [ ] Verify login works with captcha enabled (`CAPTCHA_DRIVER` set to
`google-recaptcha` or `turnstile`)


Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-09 16:20:41 +01:00
WeikoandGitHub 96bc3594a3 Serve frontend components (#17798)
## Context
Ability to serve frontend component

See:
```typescript
curl -i 'http://localhost:3000/rest/front-components/35063b3f-bc4c-4358-8966-7762677802a3' \
--header 'Authorization: Bearer eyJhb...'
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/javascript
Date: Mon, 09 Feb 2026 10:28:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

// react-globals:react/jsx-runtime
var jsx = globalThis.jsx;
var jsxs = globalThis.jsxs;
var Fragment = globalThis.React.Fragment;

// src/front-components/test.tsx
var RemoteComponents = globalThis.RemoteComponents;
var Component = () => {
  return /* @__PURE__ */ jsxs(RemoteComponents.HtmlDiv, { style: { padding: "20px", fontFamily: "sans-serif" }, children: [
    /* @__PURE__ */ jsx(RemoteComponents.HtmlH1, { children: "My new component!" }),
    /* @__PURE__ */ jsx(RemoteComponents.HtmlP, { children: "This is your front component: test" })
  ] });
};
var test_default = globalThis.jsx(Component, {});
export {
  test_default as default
};
//# sourceMappingURL=test.mjs.map
```

readFile_v2 returns a Node.js Stream object (Readable). Here we are
using stream pipeline which connects the readable stream (file) to the
writable stream (HTTP response) which efficiently streams the file
content directly to the HTTP response without loading the entire file
into memory. (in chunks, handling backpressure and closing the
connection when the file is fully sent)
2026-02-09 14:52:14 +00:00
Baptiste DevessierandGitHub 617f634b6d Use backend types in Record Page Layouts' frontend (#17794) 2026-02-09 14:36:02 +00:00
8e977912e2 i18n - translations (#17804)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-09 15:44:40 +01:00
4cdb7d61b2 Redesign AI chat and add pre-existing prompts. (#17787)
Redesigned AI-chat based on the following provided design.

<p align="center">
<img
src="https://github.com/user-attachments/assets/f10ebbd2-9ee9-402f-b246-6e8f8cedbd53"
width="225" />
</p>

---------

Co-authored-by: Félix Malfait <[email protected]>
2026-02-09 15:29:32 +01:00
Raphaël BosiandGitHub 7fbd50e5a0 [FRONT COMPONENTS] Create Icon component in twenty ui (#17797)
This will be used in the remote dom. We need a component which can take
a icon name as a parameter and return an icon component.
2026-02-09 13:57:22 +00:00
Abdullah.andGitHub 0b4cebfe0e Resolve tar related dependabot alerts in the application-layer. (#17801)
Resolves [Dependabot Alert
424](https://github.com/twentyhq/twenty/security/dependabot/424),
[Dependabot Alert
425](https://github.com/twentyhq/twenty/security/dependabot/425) and
[Dependabot Alert
426](https://github.com/twentyhq/twenty/security/dependabot/426).
2026-02-09 14:41:26 +01:00
Charles BochetandGitHub d7c28d6455 Fix file constraint migration (#17796)
## Summary

- Wrap `AddFileEntityUniqueConstraint` migration in a SAVEPOINT
try-catch so it doesn't break when duplicate `(workspaceId,
applicationId, path)` rows exist in `core.file`
- Extend `DeleteFileRecordsCommand` upgrade command to add the unique
constraint after cleaning up file records
2026-02-09 14:40:35 +01:00
Raphaël BosiandGitHub 2b29918bf8 [FRONT COMPONENTS] Navigate from the remote (#17762)
## PR Description

This PR:
- Introduces a `FrontComponentHostCommunicationApi`, which allows us to
pass functions to be executed from the worker
- Exposes a navigate function from the host to the front component
remote workers, enabling SDK components to trigger in-app navigation
- Gets rid of `useSyncExternalStore` and makes the execution context
reactive without it
- Refactors and improves the `esbuild` plugin system

## Video


https://github.com/user-attachments/assets/7b26a1c2-f85f-4898-a71d-f60c70e61711
2026-02-09 14:38:35 +01:00
2106f46e9e i18n - translations (#17803)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-09 14:35:41 +01:00
MarieandGitHub 1edce5088c Flush cache before and after upgrade for self-hosts (#17800)
as per title
2026-02-09 14:35:13 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
3216b634a3 feat: improve AI chat - system prompt, tool output, context window display (#17769)
⚠️ **AI-generated PR — not ready for review** ⚠️

cc @FelixMalfait

---

## Changes

### System prompt improvements
- Explicit skill-before-tools workflow to prevent the model from calling
tools without loading the matching skill first
- Data efficiency guidance (default small limits, use filters)
- Pluralized `load_skill` → `load_skills` for consistency with
`load_tools`

### Token usage reduction
- Output serialization layer: strips null/undefined/empty values from
tool results
- Lowered default `find_*` limit from 100 → 10, max from 1000 → 100

### System object tool generation
- System objects (calendar events, messages, etc.) now generate AI tools
- Only workflow-related and favorite-related objects are excluded

### Context window display fix
- **Bug**: UI compared cumulative tokens (sum of all turns) against
single-request context window → showed 100% after a few turns
- **Fix**: Track `conversationSize` (last step's `inputTokens`) which
represents the actual conversation history size sent to the model
- New `conversationSize` column on thread entity with migration

### Workspace AI instructions
- Support for custom workspace-level AI instructions

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-09 14:26:02 +01:00
Abdullah.andGitHub 6c7c389785 fix: webpack related dependabot alerts (#17792)
Resolves [Dependabot Alert
422](https://github.com/twentyhq/twenty/security/dependabot/422) and
[Dependabot Alert
423](https://github.com/twentyhq/twenty/security/dependabot/423).
2026-02-09 13:12:05 +01:00
martmullandGitHub 9162685b2e Reorganize logic function files (#17766)
reorganize according to

<img width="1243" height="725" alt="Pasted Graphic"
src="https://github.com/user-attachments/assets/ba65dd10-8eec-4b13-ad49-9726edd3b79c"
/>

Not working yet
2026-02-09 12:36:39 +01:00
bf4c348c8b i18n - translations (#17790)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-09 11:18:24 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Etienne
ece265c6e4 Harden local file storage driver path resolution (#17783)
## Summary

- Normalize all file paths with `path.resolve` instead of `join` to
properly handle `..` segments in file path inputs
- Add `assertPathIsWithinStorage` guard on all write, delete, move,
copy, and existence-check operations
- Introduce `ACCESS_DENIED` exception code with i18n-ready user-friendly
message
- Read path already had realpath-based validation; updated its error
code to `ACCESS_DENIED` for consistency

## Test plan

- [x] Typecheck passes
- [x] Lint passes
- [x] Manual: verify file upload/download still works with valid paths
- [x] Manual: verify `../` in file paths is rejected with ACCESS_DENIED


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <[email protected]>
2026-02-09 09:54:10 +00:00
15f09736b2 Fix importing people with mixed-case domain URL failing to match company (#17774)
Fix #17711 
### Reproduce steps
1. import this xlsx file
[test-import.xlsx](https://github.com/user-attachments/files/25156642/test-import.xlsx)

2. import company worksheet at first and then import people worksheet
3. you will see an error <img width="324" height="101"
alt="Snipaste_2026-02-07_19-22-04"
src="https://github.com/user-attachments/assets/bf2703da-a57a-4795-805b-6ddcc689c621"
/>

And I found neither the frontend nor the backend applies lowercase
normalization to the query value. Although the standard UI input always
displays company links in lowercase, user might mixed the case in their
xlsx/csv bulk import. So I did a simple check in frontend.

### Additional findings:
1. Email has the same case sensitivity issue when opportunities import.
You can test same as in test-import.xlsx file (I created a opportunities
worksheet. It will have same issue: <img width="330" height="101"
alt="email error"
src="https://github.com/user-attachments/assets/db36b19b-2abe-4c61-a661-f8d1eb357a5e"
/>

2. API also has: I also tested via the GraphQL API Playground and
confirmed that createOnePerson with a mixed-case URL fails to find an
existing company. <img width="1419" height="513"
alt="Snipaste_2026-02-07_23-33-56"
src="https://github.com/user-attachments/assets/c189f1fd-9793-42d5-a1a9-616cffd2592e"
/>

### Approach:
1. Only change it in frontend, and I will add email check later; (my
current commit)
2. Or backend: Normalize values in computeUniqueConstraintCondition in
twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts
(which handles connect.where queries, and this will cover xlsx import
and api import: createone, createmany, updatemany, updateone. And much
better for future extension.

(Personally, I'd prefer the backend approach. But I'd love to hear your
thoughts on which approach you'd prefer, and whether my analysis is on
the right track.

---------

Co-authored-by: Etienne <[email protected]>
2026-02-09 09:38:09 +00:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
bade5289b6 Fix Cloudflare webhook guard validation logic (#17780)
## Summary

- Restructures the `CloudflareSecretMatchGuard` to properly validate the
`cf-webhook-auth` header before performing the comparison — checks for
missing, empty, or mismatched-length values first
- Removes `@ts-expect-error` workarounds with explicit typed header
access
- Expands test coverage with additional edge cases (missing header,
wrong value, empty string, length mismatch)

## Test plan

- [x] Unit tests pass (6 tests covering matching, missing config,
missing header, wrong value, empty string, length mismatch)


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-09 08:23:57 +00:00
497230a052 i18n - translations (#17789)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-09 00:22:32 +01:00
WeikoandGitHub 9aa63f7ddc Add sync front component (#17748)
## Context
Allow twenty apps to sync front components
2026-02-08 23:00:23 +00:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
40d7e740ef Add token type validation and remove dead code in JWT verification (#17784)
## Summary

- Removes unreachable dead code in `verifyJwtToken` — a legacy API key
verification block where the condition (`!payload.type && type ===
API_KEY`) was logically impossible. Also removes the now-unused
`isLegacyApiKey` parameter and `generateAppSecretLegacy` method.
- Adds explicit token type validation after JWT decode in
`verifyLoginToken`, `verifyRefreshToken`, and `verifyTransientToken`.
Each function now rejects tokens whose `type` field doesn't match what's
expected (defense-in-depth — the HMAC secret already binds the type, but
this makes the contract explicit).

## Test plan

- [x] Updated existing specs for login-token, refresh-token, renew-token
services — all 16 tests passing
- [x] Lint clean

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-08 19:44:42 +00:00
7402edb887 Remove unused GraphQL throttler plugin and graphql-rate-limit dependency (#17785)
## Summary

- Deletes `use-throttler.ts` — an envelop plugin that was never
registered in the GraphQL config plugin chain (dead code since
introduction)
- Removes the `graphql-rate-limit` dependency from `package.json` (only
consumer was the deleted file)

API rate limiting continues to work via `ThrottlerService` in the query
runner layer (for API key auth). Global coverage should be handled at
the infrastructure level (Cloudflare).

## Test plan

- [x] Confirmed no imports of `useThrottler`, `ThrottlerPluginOptions`,
or `UnauthenticatedError` from this file exist anywhere in the codebase
- [x] `graphql-rate-limit` has no other consumers

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-08 19:52:59 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
d7132b35d3 Centralize outbound HTTP requests through SecureHttpClientService (#17779)
## Summary

- Migrates all direct `axios` and `@nestjs/axios` `HttpService` usages
across the server to go through `SecureHttpClientService`, which
conditionally applies SSRF protection based on the
`OUTBOUND_HTTP_SAFE_MODE_ENABLED` config flag
- `SecureHttpClientService.getHttpClient()` now accepts optional
`AxiosRequestConfig` (e.g., `baseURL`) so callers can configure their
client while still getting protection
- Adds `getInternalHttpClient()` for trusted same-server requests (e.g.,
REST-to-GraphQL proxy, code-interpreter downloading internal files)
- Renames `getSecureAdapter` to `getSecureAxiosAdapter` for clarity
- Captcha drivers now receive a pre-configured `AxiosInstance` from the
module factory instead of creating their own

## Migrated services

| Service | Previous | Risk level |
|---------|----------|-----------|
| `file-upload.service` | `HttpService` | High (user-provided image
URLs) |
| `code-interpreter-tool` | `HttpService` + direct adapter | High
(user-provided file URLs) |
| `search-help-center-tool` | `axios.post()` | Low (hardcoded endpoints)
|
| `http-tool` | Already migrated | High (user-provided URLs) |
| `admin-panel.service` | `axios.get()` | Low (Docker Hub API) |
| `sign-in-up.service` | `HttpService` | Medium (logo URL validation) |
| `google-apis-scopes` | `HttpService` | Low (Google API) |
| `geo-map.service` | `HttpService` | Low (Google Maps API) |
| `telemetry.service` | `HttpService` | Low (telemetry endpoint) |
| `rest-api.service` | `HttpService` | Internal (uses
`getInternalHttpClient`) |
| `create-company.service` | `axios.create()` | Low (Twenty companies
API) |
| `google-recaptcha.driver` | `axios.create()` | Low (Google reCAPTCHA)
|
| `turnstile.driver` | `axios.create()` | Low (Cloudflare Turnstile) |

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] Admin panel unit tests pass
- [x] Secure adapter unit tests pass

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-08 19:52:35 +01:00
bc6268bb29 Rename REFRESH_TOKEN_COOL_DOWN to REFRESH_TOKEN_REUSE_GRACE_PERIOD and anchor the grace window (#17782)
## Summary

- Renames `REFRESH_TOKEN_COOL_DOWN` to
`REFRESH_TOKEN_REUSE_GRACE_PERIOD` — the old name was misleading and
suggested a security mechanism rather than what it actually is: a grace
period for concurrent refresh token use (e.g. two browser tabs
refreshing simultaneously).
- Makes the token revocation in `renew-token.service.ts` conditional
(`revokedAt: IsNull()`), so if the token was already revoked by a
concurrent request, the original `revokedAt` timestamp is preserved and
the grace window stays anchored.
- Updates comments and config description to clarify intent.

## Test plan

- [x] Existing unit tests updated and passing
(`refresh-token.service.spec.ts`, `renew-token.service.spec.ts`)
- [x] Lint clean

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-08 18:15:37 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
c44d61f324 Validate timezone input in group-by date queries (#17777)
## Summary

- Validates that the timezone parameter in group-by date expressions is
a recognized IANA timezone
- Adds SQL string literal escaping as a defense-in-depth measure for the
timezone value interpolated into SQL expressions
- Moves the `IANA_TIME_ZONES` constant to `twenty-shared` so it can be
reused across frontend and server packages
- Adds `INVALID_TIMEZONE` error code mapped to 400 Bad Request in both
GraphQL and REST API exception handlers

## Test plan

- [x] Unit tests for `validateIanaTimeZone` (valid IANA zones, fixed
offsets, rejects invalid strings)
- [x] Unit tests for `escapeSqlStringLiteral`
- [x] Integration tests for `getGroupByExpression` covering timezone
validation and granularity handling


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-08 16:06:19 +00:00
WeikoandGitHub cc86b9acae Fix record page layout BE (#17758)
## Context
- Add missing conditional display
- Set default tab as null for most of the page layout, letting the FE
handle that
- Fix duplicate "Note" widget in both task and note pages
- Simplify typing, removing redundant layoutName
2026-02-06 18:34:35 +00:00
30cbf0e40e i18n - translations (#17768)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-06 18:27:52 +01:00
Baptiste DevessierandGitHub 4acca5b10d Store record page layouts in a global state (#17759) 2026-02-06 17:12:00 +00:00
Paul RastoinandGitHub 3dc5b162c7 Spread in parent and requires FlatEntity.__universal (#17753)
# Introduction
Requiring the spreaded `__universal` record that aggregates all the
universal identifier ( relations fk and aggregators ) of an entity to
its root

It's blockin for https://github.com/twentyhq/twenty/pull/17687 to be
finalized because if we don't we would have to migrated all related
entities at once in order for them to always have the universal
properties

## `resolveEntityRelationUniversalIdentifiers`
Introduced `resolveEntityRelationUniversalIdentifiers` a centralized
utility that resolves foreign key IDs to universal identifiers using
ALL_METADATA_RELATIONS metadata. It provides strict typing for both
input (foreign keys) and output (universal identifiers), with
nullability dynamically inferred from entity relation types.
Strictly and dynamically typed for both output and input

To do so added a new type and const/runtime grain to
ALL_METADATA_RELATIONS `isNullable`to many-to-one entries, derived from
the entity relation property types. And fixed incorrectly typed typeorm
entities

### Usage
```ts
  const {
    availabilityObjectMetadataUniversalIdentifier,
    frontComponentUniversalIdentifier,
  } = resolveEntityRelationUniversalIdentifiers({
    metadataName: 'commandMenuItem',
    foreignKeyValues: {
      availabilityObjectMetadataId:
        createCommandMenuItemInput.availabilityObjectMetadataId,
      frontComponentId: createCommandMenuItemInput.frontComponentId,
    },
    flatEntityMaps: { flatObjectMetadataMaps, flatFrontComponentMaps },
  });
```
2026-02-06 17:02:02 +00:00
Thomas TrompetteandGitHub a494a7a902 Fix iterator creation (#17763)
On iterator creation, we get an error "Bad configuration". Looks like a
race condition with generation that happens before apollo cache gets
updated. Adding defensive checks
2026-02-06 15:25:09 +00:00
f6beb06364 Migrate favorites to navigation menu items (1.17 upgrade) (#17477)
**What this PR does:**
- Migrates existing `Favorite` and `FavoriteFolder` entities to the new
`NavigationMenuItem` structure
- Preserves user-level vs workspace-level ownership
- Preserves folder structure, positions, and relationships
- Handles both view-based favorites (linked to views) and record-based
favorites (linked to records)
- Soft-deletes original favorites and folders after successful migration
- Enables the `IS_NAVIGATION_MENU_ITEM_ENABLED` feature flag
post-migration
- Skips migration if the feature flag is already enabled
- Idempotent: checks for existing navigation menu items to prevent
duplicates

**Migration flow:**
1. Migrate favorite folders first (creates folder mapping)
2. Migrate favorites
3. Soft-delete migrated favorites and folders
4. Enable feature flag

**What's next:**
- After all workspaces are migrated and the navigation menu item feature
is fully rolled out, we can:
- Remove the old `Favorite` and `FavoriteFolder` entities and related
code
- Remove the feature flag check and make navigation menu items the
default
  - Clean up any deprecated favorites-related code paths in the frontend

---------

Co-authored-by: Etienne <[email protected]>
2026-02-06 13:42:20 +00:00
7074d9ce1b Fix CSV preview duplicate key warning (#10920) (#17754)
Fix issue #10920
<img width="1264" height="630" alt="image"
src="https://github.com/user-attachments/assets/d7a00e4f-85cb-49e1-aa38-4c807f1e1b69"
/>


The root cause is that `@cyntler/react-doc-viewer`'s CSV renderer uses
**cell values as React keys** instead of indices.

### There are two approaches:
1. **patch-package** — Directly fix the key usage in the library's
source code and pull a request to the author of
`@cyntler/react-doc-viewer` to fix it.

<img width="880" height="469" alt="image"
src="https://github.com/user-attachments/assets/819e5b6a-21ae-4333-87f5-3b7f6d7e2738"
/>

<img width="1753" height="568" alt="image"
src="https://github.com/user-attachments/assets/c8a0ee49-9bf4-4002-aad2-65914ac10254"
/>



2. Custom CSV renderer (My current code commit)

### Changes
- `fetchCsvPreview.ts` — Fetches CSV and parses it into headers and rows
- `DocumentViewer.tsx` — Renders CSV with a custom table instead of
passing it to DocViewer

---------

Co-authored-by: Etienne <[email protected]>
2026-02-06 13:35:47 +00:00
WeikoandGitHub b3c95744ef Fix twenty-emails build (#17760)
## Context
I've seen errors in twenty-emails build where I18n from @lingui/core was
resolved to two different declaration files
```typescript
src/components/BaseEmail.tsx:20:19 - error TS2719: Type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n' is not assignable to type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n'. Two different types with this name exist, but they are unrelated.
  Types have separate declarations of a private property '_locale'.

20     <I18nProvider i18n={i18nInstance}>
                     ~~~~

  node_modules/@lingui/react/dist/shared/react.b2b749a9.d.ts:42:5
    42     i18n: I18n;
           ~~~~
    The expected type comes from property 'i18n' which is declared here on type 'IntrinsicAttributes & Omit<I18nContext, "_"> & { children?: ReactNode; }'
 ```
Seems to be related to https://github.com/twentyhq/twenty/pull/17380
The tsconfig simplification changed how vite plugin resolves types during build. With the inherited moduleResolution: "node", the plugin and source code resolved @lingui/react's types differently, creating two incompatible I18n types from the same package.

## Fix
Add moduleResolution: "bundler" to packages/twenty-emails/tsconfig.json, aligning with twenty-front, twenty-ui, twenty-shared should fix the issue
2026-02-06 11:17:11 +00:00
Lakshay ManchandaandGitHub 10de51fcbd - fixed coloring of item type tag in dark mode (#17745)
Fixed issue #17694 

<img width="963" height="1021" alt="image"
src="https://github.com/user-attachments/assets/a28948a1-126d-4f3c-8a53-c39adbb7f52d"
/>
2026-02-06 10:28:04 +00:00
Félix MalfaitandGitHub d537144ab1 fix: add command to fix morph relation field name mismatches (#17757)
## Summary

When a custom object is renamed after creation, the relation fields on
`noteTarget`, `taskTarget`, `attachment`, and `timelineActivity` keep
their old names but point to the renamed object.

For example:
- User creates custom object `solution`
- System creates field `solution` on NoteTarget → pointing to Solution
- User renames object from `solution` to `productCatalog`
- Field name stays as `solution` but points to `productCatalog`
- Migration runs: `solution` → `targetSolution`
- Now `targetSolution` points to `productCatalog` 

This causes the morph name computation to fail:
- `getMorphNameFromMorphFieldMetadataName("targetSolution",
"productCatalog")`
- Tries to remove `ProductCatalog` from `targetSolution` → no match
- Name stays as `targetSolution` instead of becoming `target`
- Frontend computes: `targetSolution` + `Company` =
`targetSolutionCompany` 💥

## The Fix

This command finds and fixes all such mismatches by:
1. Finding MORPH_RELATION fields where `field.name !=
target${capitalize(targetObject.nameSingular)}`
2. Renaming the column in the workspace schema
3. Updating the field metadata (name + joinColumnName in settings)
4. Invalidating the cache

## Usage

**Dry run (to see what would be fixed):**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names -- --workspace-id <id> --dry-run
```

**Fix a specific workspace:**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names -- --workspace-id <id>
```

**Fix all workspaces:**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names
```

## SQL to find affected workspaces

```sql
SELECT 
  fm."workspaceId",
  COUNT(*) AS mismatched_fields
FROM core."fieldMetadata" fm
JOIN core."objectMetadata" target_om ON fm."relationTargetObjectMetadataId" = target_om.id
JOIN core."objectMetadata" source_om ON fm."objectMetadataId" = source_om.id
WHERE fm.type = 'MORPH_RELATION'
  AND fm.name LIKE 'target%'
  AND source_om."nameSingular" IN ('noteTarget', 'taskTarget', 'attachment', 'timelineActivity')
GROUP BY fm."workspaceId"
HAVING COUNT(*) FILTER (
  WHERE fm.name != 'target' || initcap(replace(target_om."nameSingular", '_', ''))
) > 0;
```
2026-02-06 11:38:50 +01:00
Baptiste DevessierandGitHub 955c1b4916 Allow filtering by page layout type and fetch more fields (#17750)
<img width="3456" height="2160" alt="CleanShot 2026-02-05 at 18 01
28@2x"
src="https://github.com/user-attachments/assets/d8914bdd-e44c-459d-a87f-1b00dd4c29c2"
/>
2026-02-05 17:34:28 +00:00
e505eba978 i18n - translations (#17751)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-05 18:14:47 +01:00
EtienneandGitHub 77d15356e1 Files v2 - Use Files field in attachment (#17707)
- Add FILES field on attachment
- Adapt Attachment logic in front to use new resolver/controller
- Update files-field logic to infer applicationId from fieldMetadataId +
ask for fieldMetadataId in upload resolver
- Design update


To do in next PR : 
- Adapt activity files logic
2026-02-05 16:42:19 +00:00
Félix MalfaitandGitHub e382641ac3 fix: use real founder headshots in seed data (#17749)
## Summary
Updates the avatar URLs for seeded founders to use properly named images
instead of generic numbered placeholder images.

## Changes
Updates `prefill-people.ts` to reference new image paths in
`twentyhq/placeholder-images/founders/`:

| Person | Company | New Image Path |
|--------|---------|----------------|
| Brian Chesky | Airbnb | `founders/brian-chesky.jpg` |
| Dario Amodei | Anthropic | `founders/dario-amodei.jpg` |
| Patrick Collison | Stripe | `founders/patrick-collison.jpg` |
| Dylan Field | Figma | `founders/dylan-field.jpg` |
| Ivan Zhao | Notion | `founders/ivan-zhao.jpg` |

## Related
Requires a corresponding PR on `twentyhq/placeholder-images` to add the
actual founder headshot images to the `founders/` directory.

## Why
The previous placeholder images were generic numbered images that didn't
represent the actual people. This creates a single source of truth for
founder images that all workspaces can reference.
2026-02-05 17:49:43 +01:00
Thomas TrompetteandGitHub 04b8f2aaf5 Add gauge for awaiting jobs (#17747)
<img width="774" height="333" alt="Capture d’écran 2026-02-05 à 16 18
06"
src="https://github.com/user-attachments/assets/ec938a39-801d-4d71-a2de-7ced1795a0bd"
/>
2026-02-05 15:47:53 +00:00
martmullandGitHub 23df33172a Fix twenty sdk build 5 (#17744)
use prepublish instead of prepack
2026-02-05 15:54:06 +01:00
5b701f5ba4 Never display broken relations in record page layouts (#17738)
We generate Field widgets for relations on-the-fly, when a record page
layout is first requested by the user. When the user changed their data
model and then returned to a record page layout, the Field widgets
weren't updated. **This PR ensures Field widgets are recomputed when
relations change.**

Future subjects:

- Now that we will fetch the configuration from the backend and start
storing updates, we will have to think about how we deal with these
generated relation Field widgets.

## Before


https://github.com/user-attachments/assets/99d53b19-b231-435f-b14f-4473ba269ad2

## After


https://github.com/user-attachments/assets/357d956d-8b3b-448c-a983-82569a7dd0a0

---------

Co-authored-by: Félix Malfait <[email protected]>
2026-02-05 14:07:04 +00:00
Thomas TrompetteandGitHub df516a904b Add lock on version creation (#17740)
Should avoid duplicates we have sometimes
2026-02-05 14:01:21 +00:00
c0fd4c7fed i18n - translations (#17743)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-05 15:07:16 +01:00
Thomas TrompetteandGitHub c521406bf8 Improve workflow crons (#17720)
Issue 1: no info to debug cron trigger. Stop catching exception + using
logs instead of throwing for now

Issue 2: sentry often send timeouts errors for workflow crons. Probably
not real ones, it sends it if the job takes more than 5 minutes to run.
To fix, on each workflow cron we do:
- loop over active workspaces
- perform a query check that workspace is relevant, using count for
performances
- send a job if relevant
2026-02-05 13:36:32 +00:00
Félix MalfaitandGitHub 27da9d60eb fix: set isActive=true in morph migration & add system fields toggle (#17736)
## Summary

Three fixes in this PR:

### 1. Migration commands now set `isActive = true` and use generic
'Target' label

When converting relation fields to `MORPH_RELATION` type, the migration
commands now:
- Set `isActive = true` to prevent inactive fields from being selected
as the representative morph field
- Update all field labels to generic **'Target'** instead of keeping
individual labels like 'Company', 'Person'

This ensures the UI shows a coherent label ('Target') alongside 'X
Objects' for the type.

**Files changed:**
- `1-17-migrate-note-target-to-morph-relations.command.ts`
- `1-17-migrate-task-target-to-morph-relations.command.ts`

### 2. Added system fields/relations toggles in Settings

The fields and relations tables in Settings > Data Model were filtering
out system fields with no way to view them. Added a "System fields" /
"System relations" toggle (visible in advanced mode) to allow viewing
these fields.

**Files changed:**
- `SettingsObjectFieldTable.tsx`
- `SettingsObjectRelationsTable.tsx`

This matches the existing behavior on the Objects table which already
has a "System objects" toggle.
2026-02-05 13:35:46 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
0473efcef0 Bump drizzle-kit from 0.31.5 to 0.31.8 (#17725)
Bumps [drizzle-kit](https://github.com/drizzle-team/drizzle-orm) from
0.31.5 to 0.31.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/drizzle-team/drizzle-orm/releases">drizzle-kit's
releases</a>.</em></p>
<blockquote>
<h2>[email protected].8</h2>
<h3>Bug fixes</h3>
<ul>
<li>Fixed <code>algorythm</code> =&gt; <code>algorithm</code> typo.</li>
<li>Fixed external dependencies in build configuration.</li>
</ul>
<h2>[email protected].6</h2>
<h3>Bug fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/2853">[BUG]:
Importing drizzle-kit/api fails in ESM modules</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/c445637df39366bcf47b12601896ce851771c1c2"><code>c445637</code></a>
Merge pull request <a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/5095">#5095</a>
from drizzle-team/main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/e7b3aaa26456b88cd23a7843ebc95b3bddde1ba4"><code>e7b3aaa</code></a>
Merge branch 'main' into main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/0d885a54ddafd8717f8610cf3d2899f3eef61e65"><code>0d885a5</code></a>
refactor: Update condition for run-feature job to improve clarity and
functio...</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/45a1ffbcbfdd96772d0aba7d9e43744db2dce471"><code>45a1ffb</code></a>
Merge pull request <a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/5087">#5087</a>
from drizzle-team/main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/6357645bd33b1f444e1d081769dd4b71c3de31f8"><code>6357645</code></a>
chore: Comment out NEON_HTTP_CONNECTION_STRING requirement in release
workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/53dec98a936f549d0cc2e668f19db3a2df842f51"><code>53dec98</code></a>
refactor: Simplify release router workflow by removing unnecessary
switch job...</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/ce88a181e03d8b9b3fd0b62c93cc1faa05b0e000"><code>ce88a18</code></a>
Merge remote-tracking branch 'origin/ext-deps-kit' into
main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/5c8a4c508b36813599e6de891166a6888720a307"><code>5c8a4c5</code></a>
+</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/73e2ea486f6781bc7bfd2c287590d9c96e319b51"><code>73e2ea4</code></a>
feat: Add release router workflow to manage feature and latest
releases</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/378b0432d549441fa61de200589a790f1171b6fe"><code>378b043</code></a>
Merge pull request <a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/5002">#5002</a>
from drizzle-team/main-next-pack</li>
<li>Additional commits viewable in <a
href="https://github.com/drizzle-team/drizzle-orm/compare/[email protected]@0.31.8">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for drizzle-kit since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=drizzle-kit&package-manager=npm_and_yarn&previous-version=0.31.5&new-version=0.31.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abdullah <[email protected]>
2026-02-05 13:02:33 +00:00
martmullandGitHub de5764ede0 Fix twenty sdk build (#17729)
- remove worksapce:* dependencies from published packages
- Use common tsconfig.ts in create-twenty-app
- Increase version to 0.4.4
2026-02-05 14:25:49 +01:00
Félix MalfaitandGitHub f899804db3 fix: update lockfile for twenty CLI bin path change (#17739)
## Summary

The lockfile had a stale reference to `dist/cli/index.cjs` but the
twenty CLI bin was changed to `dist/cli.cjs`.

This was causing CI to fail with:
```
YN0028: -    twenty: dist/cli/index.cjs
YN0028: +    twenty: dist/cli.cjs
YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
```

This PR updates the lockfile to match the new path.
2026-02-05 14:12:04 +01:00
Lucas BordeauandGitHub 66b87c641c Fixed SSE connection retry infinite loop (#17723)
This bug was cause by a combination of an expired token stored inside
SSE client on the front end, and a server restart or a cache flush, we
ended up with an SSE client that tries to reconnect infinitely with an
expired token.

The fix is to improve the connection retry handler in the frontend to
detect an expired token and recreate an SSE client.
2026-02-05 13:19:43 +01:00
Raphaël BosiandGitHub 309785f7ff Refactor twenty-sdk folder stucture (#17733)
Only three barrels: `root`, `ui` and `front-component`
2026-02-05 13:14:27 +01:00
Abdullah.andGitHub 60f2a74bf0 fix: fast-xml-parser has rangeerror dos numeric entities bug (#17732)
Resolves [Dependabot Alert
412](https://github.com/twentyhq/twenty/security/dependabot/412).

AWS SDK minor-releases are backward compatible.
2026-02-05 13:09:47 +01:00
Félix MalfaitandGitHub 6851085143 Fix note/task target creation to support morph relations (#17734)
## Overview
Fixes the `unknown fields opportunityId in objectMetadataItem
noteTarget` error when creating notes/tasks from record pages (like the
Notes/Tasks tab on an opportunity).

## Root Cause
The `useOpenCreateActivityDrawer` hook was not handling `MORPH_RELATION`
field types:

1. Code only checked `field.relation` (which is populated for `RELATION`
type fields)
2. When `field.relation` was undefined (because the field is
`MORPH_RELATION`), it fell back to the old naming convention (e.g.,
`opportunityId`)
3. But after the morph migration, the columns are named differently
(e.g., `targetOpportunityId`)

This caused the error on both new workspaces (which are created with
morph relations from the start) and existing workspaces that ran the
migration.

## Solution
Use the existing `findTargetFieldInfo()` utility which properly handles
both `RELATION` and `MORPH_RELATION` field types by:
- Checking `morphRelations` for morph fields and computing the correct
field name
- Checking `relation` for regular relation fields
- Returning the correct `joinColumnName` for each case

This utility is already used elsewhere in the codebase (e.g., in
junction field handling) and is the cleanest solution.
2026-02-05 11:52:23 +00:00
9604dbe78a Front components communication between host and remote (#17716)
Co-authored-by: Charles Bochet <[email protected]>
2026-02-05 11:44:02 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f733f30b62 Bump @cyntler/react-doc-viewer from 1.17.0 to 1.17.1 (#17727)
Bumps
[@cyntler/react-doc-viewer](https://github.com/cyntler/react-doc-viewer)
from 1.17.0 to 1.17.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cyntler/react-doc-viewer/releases"><code>@​cyntler/react-doc-viewer</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v1.17.1</h2>
<ul>
<li>style: prettier fix tr.json (2392605)</li>
<li>Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/305">#305</a>
from wangyinyuan/main (36029ea)</li>
<li>Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/297">#297</a>
from ysaribulut/feature/turkish-translation (fa591ca)</li>
<li>fix: correct Chinese and special characters display in HTML renderer
(c358c9d)</li>
<li>Update README.md (e6fe4e0)</li>
<li>feat: add Turkish translations (4066963)</li>
<li>Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/290">#290</a>
from Pespiri/main (c6ded83)</li>
<li>fix styled components prop pollution (5bd551a)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/955795bca8bb5c81f76674321a019ef4f838f307"><code>955795b</code></a>
Release 1.17.1</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/239260507647a0ac282a45960ac8d7e01b35bbaf"><code>2392605</code></a>
style: prettier fix tr.json</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/36029ea39c28b2a90adc605d46a45265f14ddc16"><code>36029ea</code></a>
Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/305">#305</a>
from wangyinyuan/main</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/fa591caf45e819f125efe979b34ec753362176a3"><code>fa591ca</code></a>
Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/297">#297</a>
from ysaribulut/feature/turkish-translation</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/c358c9d103beaafb1dded42fd142f2ede9482598"><code>c358c9d</code></a>
fix: correct Chinese and special characters display in HTML
renderer</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/e6fe4e089ba869f5e2abdee9b308baa42ff7b1c5"><code>e6fe4e0</code></a>
Update README.md</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/4066963b06f306af9e5029ee29bef89c815c7e4b"><code>4066963</code></a>
feat: add Turkish translations</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/c6ded83f9032f4ed2a44a947e854976e2dce8398"><code>c6ded83</code></a>
Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/290">#290</a>
from Pespiri/main</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/5bd551a7382b636c371733c2e34e1e89e2129daf"><code>5bd551a</code></a>
fix styled components prop pollution</li>
<li>See full diff in <a
href="https://github.com/cyntler/react-doc-viewer/compare/v1.17.0...v1.17.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@cyntler/react-doc-viewer&package-manager=npm_and_yarn&previous-version=1.17.0&new-version=1.17.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 10:07:38 +01:00
1d6dc04cda i18n - translations (#17724)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-05 02:30:12 +01:00
Charles BochetandGitHub 67a98f77e3 Refactor workflow-logic-function-interaction (#17699)
# Refactor workflow–logic function interaction

## Why

Workflow code steps and standalone logic functions shared the same build
layer and DB layer, which blurred two use cases: code steps belong to a
workflow version; standalone functions are deployable units. That made
workflow code steps harder to own and evolve.

## Goal

Treat code steps as **workflow-owned**: build and run them in workflow
context, and expose workflow-scoped APIs so the editor can load, test,
and save code step source without going through the generic
logic-function layer.
2026-02-05 00:46:55 +00:00
neo773andGitHub 752359335e exclude Gmail category labels only for system folders (#17640)
Previous code in `MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS` was
problematic as we grouped `category` labels along with `system folders`
labels together.

This fixes partially missing emails issue by splitting it and not
applying category exclusion when querying for a singular custom label.

Also removes old approach of getting message label_id's association from
additional network call overhead to local utility
`filterGmailMessagesByFolderPolicy`
2026-02-04 17:57:48 +00:00
WeikoGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Weiko
64700a00b0 Fix typeorm internal query builder missing twenty internal context (#17719)
## Context
Fix TypeError: Cannot read properties of undefined (reading
'coreDataSource') when updating records with RLS predicates enabled

## Implementation
Use lazy initialization for FilesFieldSync and RelationNestedQueries in
workspace query builder

## Technical details
When Row-Level Security (RLS) predicates are applied during an update
operation, TypeORM internally creates sub-query builders to evaluate
Brackets in WHERE clauses. TypeORM's createQueryBuilder() method calls
new this.constructor(connection, queryRunner) with only 2 arguments, but
WorkspaceUpdateQueryBuilder expects 6 arguments including
internalContext.
This caused FilesFieldSync to be instantiated with undefined context,
resulting in the error when accessing internalContext.coreDataSource.

Converting filesFieldSync and relationNestedQueries from eager
initialization in the constructor to lazy getters. This way:
TypeORM internal sub-query builders work fine (they never access these
dependencies)
Real query builders create dependencies on-demand when execute() runs

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Weiko <[email protected]>
2026-02-04 16:57:42 +00:00
WeikoandGitHub 8674cf973e Display "Not shared" indicator for RLS-restricted relation fields (#17713)
## Context
Previously, if for example a Person had a companyId but the company
relation was null due to RLS, the company column appeared empty. Now it
displays ForbiddenFieldDisplay to indicate the relation exists but is
inaccessible.

When RLS restricts access to a related record, the frontend now shows a
"Not shared" indicator (with lock icon) instead of an empty field.

<img width="1275" height="399" alt="Screenshot 2026-02-04 at 15 37 53"
src="https://github.com/user-attachments/assets/870306f8-f811-4dc0-b23b-a33e89043a37"
/>
2026-02-04 16:49:39 +00:00
Félix MalfaitandGitHub 96c37b30c5 fix: use TwentyConfigService instead of ConfigService for ENTERPRISE_KEY (#17721)
## Summary

Replace NestJS `ConfigService` with `TwentyConfigService` for
`ENTERPRISE_KEY` access in row-level permission services.

## Changes

- `row-level-permission-predicate.service.ts`: Updated to use
`TwentyConfigService`
- `row-level-permission-predicate-group.service.ts`: Updated to use
`TwentyConfigService`

## Why

`TwentyConfigService` also pulls config values from the database, not
just environment variables. This ensures consistent config access across
the codebase.
2026-02-04 16:35:53 +00:00
martmullandGitHub bef643970b Fix twenty sdk build 3 (#17715)
final final fix
2026-02-04 15:21:17 +00:00
MarieandGitHub 6a5974e06c [fix] Some fixes (#17674)
- fixes
[sentry](https://twenty-v7.sentry.io/issues/7239112455/?environment=prod&environment=prod-eu&project=4507072563183616&query=anything%20else&referrer=issue-stream&sort=date)
- adapt useClearField logic to morph relations
- adapt useClearField logic to one-to-many relations (early return)
- fix creation of objects without "name" field from specific parts of
the product (which involved a "name" field)
2026-02-04 14:51:33 +00:00
MarieandGitHub d4303469ff Improve qualification of rest errors (#17629)
Will help close
[sentry](https://twenty-v7.sentry.io/issues/6827612895/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=date):
a 400 error (attempt to filter on a `null` id) qualified as a 500 error
from rest api.

I don't know what was our long term plan on validation for rest api, I
suppose it's not a priority, making it acceptable to interceipt some
postgres errors based on their messages. Ideally we would have a
validation earlier, at args parsing level.

before
<img width="1286" height="460" alt="Capture d’écran 2026-02-02 à 14 08
53"
src="https://github.com/user-attachments/assets/bf3d34f9-1436-4739-8f8e-95b18f59045b"
/>

after
<img width="1222" height="438" alt="Capture d’écran 2026-02-02 à 14 09
01"
src="https://github.com/user-attachments/assets/e817eaff-1ab9-49fb-869d-ddfd00671fbf"
/>
2026-02-04 14:33:30 +00:00
4629fb90be i18n - translations (#17712)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-04 15:24:23 +01:00
Paul RastoinandGitHub 48c8fa6809 Refactor workspace migration update action (#17701)
# Introduction
Removing:
- `from` property from actions definition, as it's a legitimate source
of truth. The stored comparison might have been compromised since action
generation. If from is needed it should be computed from the optimistic
cache at runner lvl
- Removed the `FlatEntityPropertyUpdates` Array complexity in favor of

From
```ts
export type PropertyUpdate<T, P extends keyof T> = {
  property: P;
} & FromTo<T[P]>;
```

To
```ts
export type FlatEntityUpdate<T extends AllMetadataName> = Partial<
  Pick<
    MetadataFlatEntity<T>,
    Extract<FlatEntityPropertiesToCompare<T>, keyof MetadataFlatEntity<T>>
  >
>;
```

## New interactions
From
```ts
    const positionUpdate = findFlatEntityPropertyUpdate({
      flatEntityUpdates,
      property: 'position',
    });

    if (
      isDefined(positionUpdate) &&
      (!Number.isInteger(positionUpdate.to) || positionUpdate.to < 0)
    ) {


   const toFlatNavigationMenuItem = {
      ...fromFlatNavigationMenuItem,
      ...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
        updates: flatEntityUpdates,
      }),
    };
```

To
```ts
    const positionUpdate = flatEntityUpdate.position;
    if (
      isDefined(positionUpdate) &&
      (!Number.isInteger(positionUpdate) || positionUpdate < 0)
    ) {

    const toFlatNavigationMenuItem = {
      ...fromFlatNavigationMenuItem,
      ...flatEntityUpdate,
    };
```

## `SanitizeFlatEntityUpdate`
Enforcing the `flatEntityUpdate` to only contains comparable properties
per flat entity by striping out all unexpected keys
In the future we will also move the whole validation at runner lvl at
some point

```ts
export const sanitizeFlatEntityUpdate = <T extends AllMetadataName>({
  flatEntityUpdate,
  metadataName,
}: {
  flatEntityUpdate: FlatEntityUpdate<T>;
  metadataName: T;
}): FlatEntityUpdate<T> => {
  const { propertiesToCompare } =
    ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[metadataName];

  const initialAccumulator: FlatEntityUpdate<T> = {};

  return propertiesToCompare.reduce((accumulator, property) => {
    const updatedValue =
      flatEntityUpdate[property as MetadataFlatEntityComparableProperties<T>];

    if (updatedValue === undefined) {
      return accumulator;
    }

    return {
      ...accumulator,
      [property]: updatedValue,
    };
  }, initialAccumulator);
};
```
2026-02-04 13:50:46 +00:00
Thomas TrompetteandGitHub b03e3772e6 Add real time image (#17710)
As title
2026-02-04 14:43:42 +01:00
nitinandGitHub 660a15b721 [FRONT COMPONENT] Stories in twenty-sdk for front component generation and upon render interactivity (#17675)
closes https://github.com/twentyhq/core-team-issues/issues/2179
2026-02-04 12:39:54 +00:00
Thomas TrompetteandGitHub 59c36736d4 Put SSE in lab (#17709)
as title
2026-02-04 13:57:25 +01:00
92359603c1 i18n - translations (#17708)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-04 13:44:21 +01:00
Félix MalfaitandGitHub 177cae8c53 feat: audit Logs (#17660) 2026-02-04 13:30:55 +01:00
martmullandGitHub 6407474461 Fix build without nx build (#17700)
as title
2026-02-04 11:14:28 +00:00
bcfacdead9 i18n - translations (#17706)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-04 12:23:29 +01:00
EtienneandGitHub 4fcc424e24 Files field - add files field display and input + filtering (#17637)
This PR introduces a new FILES field type for Twenty CRM, allowing users
to attach multiple files to any record.

- Files field display
- Files field preview
- Files field input
- Files field filtering

To test : 
1/ need to activate feature flag `IS_FILES_FIELD_ENABLED` + create a new
FILES field

- display in read only, edit, inline, table
- edit
- filter
- export


closes https://github.com/twentyhq/core-team-issues/issues/2154


<img width="354" height="134" alt="Screenshot 2026-02-02 at 19 11 01"
src="https://github.com/user-attachments/assets/3c3de89e-f6b6-4526-b710-e4c7ee1a6d30"
/>
<img width="1081" height="933" alt="Screenshot 2026-02-02 at 19 10 41"
src="https://github.com/user-attachments/assets/7c9a7278-edb7-4d4a-882c-f7404689d011"
/>
<img width="1073" height="719" alt="Screenshot 2026-02-02 at 19 10 33"
src="https://github.com/user-attachments/assets/79cc372b-56a2-4cbf-b4fe-023adc4753a8"
/>
2026-02-04 10:55:48 +00:00
Raphaël BosiandGitHub 9b8eacb7f7 [FRONT COMPONENTS] Expose twenty UI in twenty sdk (#17645)
- An app will declare its `twenty-sdk` version in the manifest.
- `twenty-sdk` will be served by a CDN to be imported in front component
host.
- When we render the host we will load twenty-ui through the correct
version of the sdk

We will proceed this way because twenty-ui is not mature enough yet to
be deployed as its own package. So instead, since the sdk is already
deployed as its own package, we will serve the ui with it. It allows us
to have versioning to handle breaking changes in twenty-ui.
2026-02-04 10:43:08 +00:00
EtienneandGitHub f6b210dd0d Files v2 - File table migration + data migration command (#17661)
To add also in 1.16 patch
2026-02-04 10:28:55 +00:00
EtienneandGitHub 50ef231704 Billing - Fix inactivity (#17703)
- Add suspendedAt column on workspace table
- Update suspendedAt field when workspace suspended or re-activated
- Compute inactivity on suspendedAt



Tested : 
- Cancel subscription
- Cancel subscription + Subscribe again
- Cleaning command
2026-02-04 10:17:27 +00:00
aa06dab920 i18n - translations (#17704)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-02-04 11:11:10 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
414f68fb63 fix(workspace-cache): memory leak in deleteFromLocalCache (#17686)
## Problem

The `deleteFromLocalCache` method was only setting `lastHashCheckedAt=0`
instead of actually deleting the cache entry. This caused old versions
to accumulate in the local cache when `invalidateAndRecompute` was
called.

### Flow that causes the leak:

1. `invalidateAndRecompute()` is called
2. `flush()` → `deleteFromLocalCache()` — **only sets
`lastHashCheckedAt=0`, keeps old data**
3. `recomputeDataFromProvider()` → `setInLocalCache()` — **adds new
version with new hash**
4. `cleanupStaleVersions()` — **never called** (only triggered from
`getFromLocalCache` path)

Result: Each `invalidateAndRecompute` call adds a new version to
`entry.versions` without removing the old one.

### Impact

For migration commands (like
`1-17-migrate-attachment-to-morph-relations`) that process thousands of
workspaces, this caused significant memory growth:
- Each workspace calls `getOrRecompute` (version 1)
- Then calls `invalidateAndRecompute` (version 2 added, version 1 stays)
- Memory accumulates as the command processes more workspaces

## Solution

Actually delete the local cache entry in `deleteFromLocalCache`, so
`recomputeDataFromProvider` starts fresh.

```typescript
// Before (memory leak)
if (isDefined(entry)) {
  entry.lastHashCheckedAt = 0;
}

// After (proper cleanup)
this.localCache.delete(localKey);
```

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-04 09:47:45 +00:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
0edc3a385c fix: prevent anonymous users from bypassing workspace creation restriction (#17635)
When `IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS` is true, anonymous
users could still create workspaces because
`checkWorkspaceCreationIsAllowedOrThrow` checked per-user workspace
count (always 0 for new users) instead of system-wide workspace count.

The bootstrap bypass now only applies when no workspaces exist in the
entire system. Also adds the same check in `signUpOnNewWorkspace` to
guard the `signInUp` code path.

Fixes #17631

Generated with [Claude Code](https://claude.ai/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-04 09:47:24 +00:00
Baptiste DevessierandGitHub 29093fdbf1 Fetch soft-deleted records in Record Page Layouts (#17698)
## Before


https://github.com/user-attachments/assets/35a69015-8d3a-498f-8cb3-07bbf3f4d307

## After


https://github.com/user-attachments/assets/1232b41f-50a1-4f7d-afbe-f1fe57bd7de0
2026-02-04 09:07:00 +00:00
Charles BochetandGitHub 402d149ee1 Remove logic function layer (#17697)
## Remove logic function layer

Package.json and yarn.lock are now on the application entity, so the
logic function layer is no longer used except as a legacy source for the
1.17 backfill. This PR removes all layer usage outside of that migration
and keeps only the entity for backfill.

### Summary

- **Kept:** `LogicFunctionLayerEntity` and its table, only used by the
1.17 backfill command to read legacy layer data and backfill application
package files.
- **Removed:** All other layer logic: CRUD, cache, resolvers, services,
DTOs, and frontend types. Logic functions now depend only on the
application for package/dependency context.


### Why Dependencies instead of Source for package files

Package.json and yarn.lock are the application’s dependency set and are
stored under the application in **FileFolder.Dependencies**. The build
service and drivers now read them only from Dependencies; nothing is
written to Source for these files.
2026-02-04 10:17:27 +01:00
martmullandGitHub e10e0b337e Fix twenty sdk build (#17696)
- add twenty-ui in dist/vendor folder
- fix ts issue due to react version mismatch
2026-02-04 08:46:13 +00:00
Abdullah.andGitHub 7867617385 Migrate noteTarget and taskTarget to Morph. (#17476)
This PR migrates `noteTarget` and `taskTarget` to morph relations behind
separate feature flags, following the Attachment/TimelineActivity
pattern.

It introduces the `IS_NOTE_TARGET_MIGRATED` and
`IS_TASK_TARGET_MIGRATED` flags, updates standard field metadata and
indexes to use morph relations, and adds two **1.17 workspace
migrations** that:
- rename `noteTarget.*Id` / `taskTarget.*Id` columns to `target*Id`
- convert the corresponding field metadata to `MORPH_RELATION` with a
shared `morphId`

On the frontend, note/task target read and write paths switch to
`target*Id` when the respective flag is enabled. Deleted targets are
filtered on reload to prevent reappearing relations.
2026-02-04 02:53:06 +00:00
2039 changed files with 84848 additions and 41678 deletions
+1 -1
View File
@@ -913,7 +913,7 @@ export class WorkspaceMigrationMyEntityActionsBuilderService extends WorkspaceEn
type: 'update',
metadataName: 'myEntity',
entityId: flatEntityId,
updates: flatEntityUpdates,
update: flatEntityUpdates,
};
return {
+48 -58
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test:unit]
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
@@ -39,72 +39,62 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Install Playwright
if: contains(matrix.task, 'storybook')
run: npx playwright install chromium
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:sdk
tasks: ${{ matrix.task }}
# TODO uncomment when syncApplication resolver is fixed
# sdk-e2e-integration-test:
# timeout-minutes: 30
# runs-on: ubuntu-latest-8-cores
# needs: [changed-files-check, sdk-test]
# strategy:
# matrix:
# task: [test:integration, test:e2e]
# if: needs.changed-files-check.outputs.any_changed == 'true'
# services:
# postgres:
# image: twentycrm/twenty-postgres-spilo
# env:
# PGUSER_SUPERUSER: postgres
# PGPASSWORD_SUPERUSER: postgres
# ALLOW_NOSSL: 'true'
# SPILO_PROVIDER: 'local'
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# redis:
# image: redis
# ports:
# - 6379:6379
# env:
# NODE_ENV: test
# steps:
# - name: Fetch custom Github Actions and base branch history
# uses: actions/checkout@v4
# with:
# fetch-depth: 0
# - name: Install dependencies
# uses: ./.github/actions/yarn-install
# - name: Server / Append billing config to .env.test
# working-directory: packages/twenty-server
# run: |
# echo "" >> .env.test
# echo "IS_BILLING_ENABLED=true" >> .env.test
# echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
# echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
# echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
# echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
# - name: Server / Create Test DB
# run: |
# PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
# - name: SDK / Run ${{ matrix.task }} Tests
# uses: ./.github/actions/nx-affected
# with:
# tag: scope:sdk
# tasks: ${{ matrix.task }}
sdk-e2e-test:
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
needs: [changed-files-check, sdk-test]
if: needs.changed-files-check.outputs.any_changed == 'true'
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
NODE_ENV: test
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Server / Create Test DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: SDK / Run e2e Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:sdk
tasks: test:e2e
ci-sdk-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, sdk-test]
# TODO uncomment when syncApplication resolver is fixed
# needs: [changed-files-check, sdk-test, sdk-e2e-integration-test]
needs: [changed-files-check, sdk-test, sdk-e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+8 -2
View File
@@ -11,7 +11,9 @@ import unicornPlugin from 'eslint-plugin-unicorn';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import jsoncParser from 'jsonc-eslint-parser';
const twentyRules = await nxPlugin.loadWorkspaceRules('packages/twenty-eslint-rules');
const twentyRules = await nxPlugin.loadWorkspaceRules(
'packages/twenty-eslint-rules',
);
export default [
// Base JavaScript configuration
@@ -65,6 +67,10 @@ export default [
sourceTag: 'scope:sdk',
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
},
{
sourceTag: 'scope:create-app',
onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'],
},
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
@@ -197,7 +203,7 @@ export default [
plugins: {
...mdxPlugin.flat.plugins,
'@nx': nxPlugin,
'twenty': { rules: twentyRules },
twenty: { rules: twentyRules },
},
},
mdxPlugin.flatCodeBlocks,
+8 -99
View File
@@ -1,111 +1,20 @@
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import prettierPlugin from 'eslint-plugin-prettier';
import baseConfig from '../../eslint.config.mjs';
export default [
js.configs.recommended,
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
// Node.js globals
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
// Browser globals that Node.js also has
URL: 'readonly',
URLSearchParams: 'readonly',
// Node.js types
NodeJS: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
prettier: prettierPlugin,
},
ignores: ['**/dist/**'],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
...typescriptEslint.configs.recommended.rules,
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-useless-escape': 'off',
},
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
},
},
},
{
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
// Node.js globals
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
// Jest globals
describe: 'readonly',
it: 'readonly',
test: 'readonly',
expect: 'readonly',
jest: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
prettier: prettierPlugin,
},
rules: {
...typescriptEslint.configs.recommended.rules,
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-useless-escape': 'off',
'no-console': 'off',
},
},
{
ignores: ['dist/**', 'node_modules/**'],
ignores: ['src/**/*.ts', '!src/cli/**/*.ts'],
},
];
+9 -3
View File
@@ -1,14 +1,18 @@
{
"name": "create-twenty-app",
"version": "0.4.0",
"version": "0.5.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
"files": [
"dist/**/*"
"dist",
"README.md",
"package.json"
],
"scripts": {
"build": "npx rimraf dist && npx vite build"
"build": "npx rimraf dist && npx vite build",
"prepublishOnly": "tsx ../twenty-utils/pack-scripts/pre-publish-only.ts",
"postpublish": "tsx ../twenty-utils/pack-scripts/post-publish.ts"
},
"keywords": [
"twenty",
@@ -34,6 +38,7 @@
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0",
"twenty-shared": "workspace:*",
"uuid": "^13.0.0"
},
"devDependencies": {
@@ -43,6 +48,7 @@
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"typescript": "^5.9.2",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
@@ -70,7 +70,7 @@ describe('copyBaseApplicationProject', () => {
const packageJson = await fs.readJson(packageJsonPath);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.5.0');
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
});
@@ -189,7 +189,6 @@ const createDefaultFunction = async ({
fileName: string;
}) => {
const universalIdentifier = v4();
const triggerUniversalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
@@ -204,15 +203,11 @@ export default defineLogicFunction({
description: 'A simple logic function',
timeoutSeconds: 5,
handler,
triggers: [
{
universalIdentifier: '${triggerUniversalIdentifier}',
type: 'route',
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
],
httpRouteTriggerSettings: {
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
});
`;
@@ -282,7 +277,7 @@ const createPackageJson = async ({
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': '0.4.0',
'twenty-sdk': '0.5.0',
},
devDependencies: {
typescript: '^5.9.3',
+2 -1
View File
@@ -12,7 +12,8 @@
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
}
},
"jsx": "react"
},
"include": [
"src/**/*.ts",
@@ -17,7 +17,10 @@ setup_and_migrate_db() {
yarn database:migrate:prod
fi
yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod cache:flush
echo "Successfully migrated DB!"
}
@@ -7,6 +7,9 @@ test('Create workflow', async ({ page }) => {
await page.goto(process.env.LINK);
const workflowsFolder = page.getByRole('button', { name: 'Workflows' });
await workflowsFolder.click();
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
+1
View File
@@ -2,6 +2,7 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "bundler",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
+4 -1
View File
@@ -18,9 +18,9 @@ module.exports = {
'./src/modules/billing/graphql/**/*.{ts,tsx}',
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
'./src/modules/databases/graphql/**/*.{ts,tsx}',
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
'./src/modules/analytics/graphql/**/*.{ts,tsx}',
'./src/modules/object-metadata/graphql/**/*.{ts,tsx}',
'./src/modules/navigation-menu-item/graphql/**/*.{ts,tsx}',
@@ -31,6 +31,9 @@ module.exports = {
'./src/modules/page-layout/widgets/**/graphql/**/*.{ts,tsx}',
'./src/modules/dashboards/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/__mocks__/*.ts',
+3 -14
View File
@@ -5,22 +5,11 @@ module.exports = {
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
'/graphql',
documents: [
'./src/modules/activities/graphql/**/*.{ts,tsx}',
'./src/modules/companies/graphql/**/*.{ts,tsx}',
'./src/modules/people/graphql/**/*.{ts,tsx}',
'./src/modules/opportunities/graphql/**/*.{ts,tsx}',
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
'./src/modules/activities/emails/graphql/**/*.{ts,tsx}',
'./src/modules/activities/calendar/graphql/**/*.{ts,tsx}',
'./src/modules/search/graphql/**/*.{ts,tsx}',
'./src/modules/views/graphql/**/*.{ts,tsx}',
'./src/modules/favorites/graphql/**/*.{ts,tsx}',
'./src/modules/spreadsheet-import/graphql/**/*.{ts,tsx}',
'./src/modules/command-menu/graphql/**/*.{ts,tsx}',
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
'./src/modules/prefetch/graphql/**/*.{ts,tsx}',
'./src/modules/subscription/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
+3 -3
View File
@@ -62,9 +62,9 @@ const jestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 50,
lines: 48.9,
functions: 40.9,
statements: 49.5,
lines: 48,
functions: 40,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { type AppPath } from 'twenty-shared/types';
import { type AppPath, type NavigateOptions } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
export const useNavigateApp = () => {
@@ -9,10 +9,7 @@ export const useNavigateApp = () => {
to: T,
params?: Parameters<typeof getAppPath<T>>[1],
queryParams?: Record<string, any>,
options?: {
replace?: boolean;
state?: any;
},
options?: NavigateOptions,
) => {
const path = getAppPath(to, params, queryParams);
return navigate(path, options);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import { type ConnectionParameters } from '~/generated/graphql';
import { type ConnectionParameters } from '~/generated-metadata/graphql';
export type ImapSmtpCaldavAccount = {
IMAP?: ConnectionParameters;
@@ -10,15 +10,18 @@ import { downloadFile } from '@/activities/files/utils/downloadFile';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { ActivityList } from '@/activities/components/ActivityList';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { IconDownload, IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { PermissionFlagType } from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
import { AttachmentRow } from './AttachmentRow';
const DocumentViewer = lazy(() =>
@@ -134,6 +137,10 @@ export const AttachmentList = ({
isAttachmentPreviewEnabledState,
);
const isFilesFieldMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
);
const hasDownloadPermission = useHasPermissionFlag(
PermissionFlagType.DOWNLOAD_FILE,
);
@@ -144,6 +151,16 @@ export const AttachmentList = ({
const { openModal, closeModal } = useModal();
const getAttachmentUrl = (attachment: Attachment) => {
const fileUrl = isFilesFieldMigrated
? attachment.file?.[0]?.url || attachment.fullPath
: attachment.fullPath;
assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`));
return fileUrl;
};
const onUploadFile = async (file: File) => {
await uploadAttachmentFile(file, targetableObject);
};
@@ -167,7 +184,10 @@ export const AttachmentList = ({
const handleDownload = () => {
if (!previewedAttachment) return;
downloadFile(previewedAttachment.fullPath, previewedAttachment.name);
downloadFile(
getAttachmentUrl(previewedAttachment),
previewedAttachment.name,
);
};
return (
@@ -248,7 +268,7 @@ export const AttachmentList = ({
>
<DocumentViewer
documentName={previewedAttachment.name}
documentUrl={previewedAttachment.fullPath}
documentUrl={getAttachmentUrl(previewedAttachment)}
/>
</Suspense>
</StyledModalContent>
@@ -9,17 +9,21 @@ import {
FieldContext,
type GenericFieldContextType,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { getFileCategoryFromExtension } from '@/object-record/record-field/ui/utils/getFileCategoryFromExtension';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { FileIcon } from '@/file/components/FileIcon';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { t } from '@lingui/core/macro';
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { isNavigationModifierPressed } from 'twenty-ui/utilities';
import { PermissionFlagType } from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils/date-utils';
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
@@ -82,6 +86,10 @@ export const AttachmentRow = ({
const theme = useTheme();
const [isEditing, setIsEditing] = useState(false);
const isFilesFieldMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
);
const hasDownloadPermission = useHasPermissionFlag(
PermissionFlagType.DOWNLOAD_FILE,
);
@@ -92,6 +100,16 @@ export const AttachmentRow = ({
const [attachmentFileName, setAttachmentFileName] =
useState(originalFileName);
const fileCategory = isFilesFieldMigrated
? getFileCategoryFromExtension(attachment.file?.[0]?.extension)
: attachment.fileCategory;
const fileUrl = isFilesFieldMigrated
? attachment.file?.[0]?.url
: attachment.fullPath;
assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`));
const { destroyOneRecord: destroyOneAttachment } = useDestroyOneRecord({
objectNameSingular: CoreObjectNameSingular.Attachment,
});
@@ -114,7 +132,19 @@ export const AttachmentRow = ({
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.Attachment,
idToUpdate: attachment.id,
updateOneRecordInput: { name: newFileName },
updateOneRecordInput: {
name: newFileName,
...(isFilesFieldMigrated && isDefined(attachment.file?.[0]?.fileId)
? {
file: [
{
fileId: attachment.file?.[0]?.fileId,
label: newFileName,
},
],
}
: {}),
},
});
};
@@ -133,10 +163,7 @@ export const AttachmentRow = ({
};
const handleDownload = () => {
downloadFile(
attachment.fullPath,
`${attachmentFileName}${attachmentFileExtension}`,
);
downloadFile(fileUrl, `${attachmentFileName}${attachmentFileExtension}`);
};
const handleOpenDocument = (e: React.MouseEvent) => {
@@ -162,7 +189,7 @@ export const AttachmentRow = ({
>
<ActivityRow disabled>
<StyledLeftContent>
<FileIcon fileCategory={attachment.fileCategory} />
<FileIcon fileCategory={fileCategory} />
{isEditing ? (
<SettingsTextInput
instanceId={`attachment-${attachment.id}-name`}
@@ -176,7 +203,7 @@ export const AttachmentRow = ({
<StyledLinkContainer>
<StyledLink
onClick={handleOpenDocument}
href={attachment.fullPath}
href={fileUrl}
target="_blank"
rel="noopener noreferrer"
>
@@ -1,6 +1,9 @@
import { PREVIEWABLE_EXTENSIONS } from '@/activities/files/const/previewable-extensions.const';
import { downloadFile } from '@/activities/files/utils/downloadFile';
import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview';
import {
type CsvPreviewData,
fetchCsvPreview,
} from '@/activities/files/utils/fetchCsvPreview';
import { getFileType } from '@/activities/files/utils/getFileType';
import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer';
import '@cyntler/react-doc-viewer/dist/index.css';
@@ -79,9 +82,43 @@ const StyledTitle = styled.div`
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;
const StyledCsvTable = styled.table`
border-collapse: collapse;
font-size: ${({ theme }) => theme.font.size.sm};
text-align: left;
width: 100%;
th {
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.tertiary};
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${({ theme }) => theme.spacing(8)};
padding: 0 ${({ theme }) => theme.spacing(2)};
}
td {
color: ${({ theme }) => theme.font.color.secondary};
height: ${({ theme }) => theme.spacing(8)};
max-width: 200px;
overflow: hidden;
padding: 0 ${({ theme }) => theme.spacing(2)};
text-overflow: ellipsis;
white-space: nowrap;
}
tbody tr {
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
}
tbody tr:hover {
background-color: ${({ theme }) => theme.background.transparent.light};
}
`;
type DocumentViewerProps = {
documentName: string;
documentUrl: string;
documentExtension?: string;
};
// MS Office Online viewer requires documents to be publicly accessible from the internet.
@@ -160,26 +197,27 @@ const MIME_TYPE_MAPPING: Record<
export const DocumentViewer = ({
documentName,
documentUrl,
documentExtension,
}: DocumentViewerProps) => {
const { t } = useLingui();
const theme = useTheme();
const [csvPreview, setCsvPreview] = useState<string | undefined>(undefined);
const [csvPreview, setCsvPreview] = useState<CsvPreviewData | undefined>(
undefined,
);
const { extension } = getFileNameAndExtension(documentName);
const fileExtension = extension?.toLowerCase().replace('.', '') ?? '';
const fileExtension = isDefined(documentExtension)
? documentExtension.toLowerCase().replace('.', '')
: (extension?.toLowerCase().replace('.', '') ?? '');
const fileCategory = getFileType(documentName);
const isPreviewable = PREVIEWABLE_EXTENSIONS.includes(fileExtension);
const isMsOfficeFile = MS_OFFICE_EXTENSIONS.includes(fileExtension);
const mimeType = PREVIEWABLE_EXTENSIONS.includes(fileExtension)
? MIME_TYPE_MAPPING[fileExtension]
: undefined;
const mimeType = isPreviewable ? MIME_TYPE_MAPPING[fileExtension] : undefined;
useEffect(() => {
if (fileExtension === 'csv') {
fetchCsvPreview(documentUrl).then((content) => {
setCsvPreview(content);
});
fetchCsvPreview(documentUrl).then(setCsvPreview);
}
}, [documentUrl, fileExtension]);
@@ -214,12 +252,37 @@ export const DocumentViewer = ({
);
}
if (fileExtension === 'csv' && !isDefined(csvPreview))
if (fileExtension === 'csv') {
if (!isDefined(csvPreview)) {
return (
<StyledDocumentViewerContainer>
<Trans>Loading csv ... </Trans>
</StyledDocumentViewerContainer>
);
}
return (
<StyledDocumentViewerContainer>
<Trans>Loading csv ... </Trans>
<StyledDocumentViewerContainer style={{ background: 'transparent' }}>
<StyledCsvTable>
<thead>
<tr>
{csvPreview.headers.map((header, columnIndex) => (
<th key={columnIndex}>{header}</th>
))}
</tr>
</thead>
<tbody>
{csvPreview.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td key={cellIndex}>{cell}</td>
))}
</tr>
))}
</tbody>
</StyledCsvTable>
</StyledDocumentViewerContainer>
);
}
if (isMsOfficeFile && isPrivateUrl(documentUrl)) {
return (
@@ -247,12 +310,7 @@ export const DocumentViewer = ({
<DocViewer
documents={[
{
uri:
fileExtension === 'csv' && isDefined(csvPreview)
? window.URL.createObjectURL(
new Blob([csvPreview], { type: 'text/csv' }),
)
: documentUrl,
uri: documentUrl,
fileName: documentName,
fileType: mimeType,
},
@@ -3,22 +3,39 @@ import { getFileType } from '@/activities/files/utils/getFileType';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { t } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import {
FileFolder,
useUploadFileMutation,
useUploadFilesFieldFileMutation,
} from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
import { FeatureFlagKey, FieldMetadataType } from '~/generated/graphql';
export const useUploadAttachmentFile = () => {
const coreClient = useApolloCoreClient();
const [uploadFile] = useUploadFileMutation({ client: coreClient });
const [uploadFilesFieldFile] = useUploadFilesFieldFileMutation({
client: coreClient,
});
const isAttachmentMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
);
const isFilesFieldMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
);
const { objectMetadataItem: attachmentMetadata } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Attachment,
});
const filesFieldMetadataId = attachmentMetadata.fields.find(
(field) => field.type === FieldMetadataType.FILES && field.name === 'file',
)?.id;
const { createOneRecord: createOneAttachment } =
useCreateOneRecord<Attachment>({
@@ -30,21 +47,44 @@ export const useUploadAttachmentFile = () => {
file: File,
targetableObject: ActivityTargetableObject,
) => {
const result = await uploadFile({
variables: {
file,
fileFolder: FileFolder.Attachment,
},
});
let attachmentPath: string;
let fileId: string | undefined;
const signedFile = result?.data?.uploadFile;
if (isFilesFieldMigrated) {
assertIsDefinedOrThrow(
filesFieldMetadataId,
new Error(t`File field not found for attachment object`),
);
if (!isDefined(signedFile)) {
throw new Error("Couldn't upload the attachment.");
const result = await uploadFilesFieldFile({
variables: { file, fieldMetadataId: filesFieldMetadataId },
});
const uploadedFile = result?.data?.uploadFilesFieldFile;
if (!isDefined(uploadedFile)) {
throw new Error("Couldn't upload the attachment.");
}
attachmentPath = uploadedFile.path;
fileId = uploadedFile.id;
} else {
const result = await uploadFile({
variables: {
file,
fileFolder: FileFolder.Attachment,
},
});
const signedFile = result?.data?.uploadFile;
if (!isDefined(signedFile)) {
throw new Error("Couldn't upload the attachment.");
}
attachmentPath = signedFile.path;
}
const { path: attachmentPath } = signedFile;
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
isMorphRelation: isAttachmentMigrated,
@@ -55,6 +95,16 @@ export const useUploadAttachmentFile = () => {
fullPath: attachmentPath,
fileCategory: getFileType(file.name),
[targetableObjectFieldIdName]: targetableObject.id,
...(isFilesFieldMigrated && isDefined(fileId)
? {
file: [
{
fileId,
label: file.name,
},
],
}
: {}),
} as Partial<Attachment>;
const createdAttachment = await createOneAttachment(attachmentToCreate);
@@ -1,3 +1,5 @@
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { type AttachmentFileCategory } from './AttachmentFileCategory';
export type { AttachmentFileCategory };
@@ -7,6 +9,7 @@ export type Attachment = {
name: string;
fullPath: string;
fileCategory: AttachmentFileCategory;
file?: FieldFilesValue[] | null;
companyId?: string | null;
personId?: string | null;
taskId?: string | null;
@@ -0,0 +1,81 @@
import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview';
const mockFetch = (text: string) => {
global.fetch = jest.fn(() =>
Promise.resolve({
text: () => Promise.resolve(text),
} as unknown as Response),
);
};
describe('fetchCsvPreview', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should parse headers and rows from CSV', async () => {
mockFetch('Name,Age,City\nAlice,30,Paris\nBob,25,London\n');
const result = await fetchCsvPreview('https://example.com/file.csv');
expect(result.headers).toEqual(['Name', 'Age', 'City']);
expect(result.rows).toEqual([
['Alice', '30', 'Paris'],
['Bob', '25', 'London'],
]);
});
it('should return empty headers and rows for empty CSV', async () => {
mockFetch('');
const result = await fetchCsvPreview('https://example.com/empty.csv');
expect(result.headers).toEqual([]);
expect(result.rows).toEqual([]);
});
it('should return headers with no rows when CSV has only a header line', async () => {
mockFetch('Name,Age,City\n');
const result = await fetchCsvPreview('https://example.com/header-only.csv');
expect(result.headers).toEqual(['Name', 'Age', 'City']);
expect(result.rows).toEqual([]);
});
it('should skip empty lines', async () => {
mockFetch('Name,Age\n\nAlice,30\n\nBob,25\n');
const result = await fetchCsvPreview('https://example.com/file.csv');
expect(result.rows).toEqual([
['Alice', '30'],
['Bob', '25'],
]);
});
it('should handle rows with inconsistent column counts', async () => {
mockFetch('Name,Age,City\nAlice,30\nBob,25,London,Extra\n');
const result = await fetchCsvPreview('https://example.com/malformed.csv');
expect(result.headers).toEqual(['Name', 'Age', 'City']);
expect(result.rows).toEqual([
['Alice', '30'],
['Bob', '25', 'London', 'Extra'],
]);
});
it('should limit rows to the preview amount', async () => {
const lines = ['Name'];
for (let i = 0; i < 100; i++) {
lines.push(`Person${i}`);
}
mockFetch(lines.join('\n'));
const result = await fetchCsvPreview('https://example.com/large.csv');
expect(result.headers).toEqual(['Name']);
expect(result.rows).toHaveLength(50);
});
});
@@ -2,21 +2,22 @@ import Papa from 'papaparse';
const DEFAULT_PREVIEW_ROWS = 50;
export const fetchCsvPreview = async (url: string): Promise<string> => {
export type CsvPreviewData = {
headers: string[];
rows: string[][];
};
export const fetchCsvPreview = async (url: string): Promise<CsvPreviewData> => {
const response = await fetch(url);
const text = await response.text();
const result = Papa.parse(text, {
preview: DEFAULT_PREVIEW_ROWS,
const result = Papa.parse<string[]>(text, {
preview: DEFAULT_PREVIEW_ROWS + 1, // +1 for header row
skipEmptyLines: true,
header: true,
header: false,
});
const data = result.data as Record<string, string>[];
const [headers = [], ...rows] = result.data;
const csvContent = Papa.unparse(data, {
header: true,
});
return csvContent;
return { headers, rows };
};
@@ -6,6 +6,7 @@ import { type RecordGqlOperationSignatureFactory } from '@/object-record/graphql
type FindActivitiesOperationSignatureFactory = {
objectMetadataItems: ObjectMetadataItem[];
objectNameSingular: CoreObjectNameSingular;
isMorphRelation: boolean;
};
export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatureFactory<
@@ -13,6 +14,7 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
> = ({
objectMetadataItems,
objectNameSingular,
isMorphRelation,
}: FindActivitiesOperationSignatureFactory) => {
const body = {
bodyV2: {
@@ -62,9 +64,13 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
__typename: true,
createdAt: true,
updatedAt: true,
deletedAt: true,
note: true,
noteId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
...generateActivityTargetMorphFieldKeys(
objectMetadataItems,
isMorphRelation,
),
},
}
: {
@@ -73,9 +79,13 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
__typename: true,
createdAt: true,
updatedAt: true,
deletedAt: true,
task: true,
taskId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
...generateActivityTargetMorphFieldKeys(
objectMetadataItems,
isMorphRelation,
),
},
}),
},
@@ -1,4 +1,5 @@
import { useRecoilValue } from 'recoil';
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
import { findActivityTargetsOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
@@ -6,9 +7,12 @@ import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const useActivityTargetsForTargetableObjects = ({
objectNameSingular,
@@ -28,12 +32,25 @@ export const useActivityTargetsForTargetableObjects = ({
activityTargetsOrderByVariables: RecordGqlOperationOrderBy;
limit: number;
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItems = useRecoilValue<ObjectMetadataItem[]>(
objectMetadataItemsState,
);
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const isMorphRelation =
objectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated;
const activityTargetsFilter = getActivityTargetsFilter({
targetableObjects: targetableObjects,
activityObjectNameSingular: objectNameSingular,
objectMetadataItems,
isMorphRelation,
});
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
@@ -7,6 +7,7 @@ import {
import { useState } from 'react';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -37,6 +38,7 @@ export const useCustomResolver = <
fetchMoreRecords: () => Promise<void>;
} => {
const { enqueueErrorSnackBar } = useSnackBar();
const apolloCoreClient = useApolloCoreClient();
const [page, setPage] = useState({
pageNumber: 1,
@@ -62,6 +64,7 @@ export const useCustomResolver = <
loading: firstQueryLoading,
fetchMore,
} = useQuery<CustomResolverQueryResult<T>>(query, {
client: apolloCoreClient,
variables: queryVariables,
onError: (error) => {
enqueueErrorSnackBar({
@@ -15,7 +15,7 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isDefined } from 'twenty-shared/utils';
import { findTargetFieldInfo } from '@/object-record/record-field/ui/utils/junction/findTargetFieldInfo';
export const useOpenCreateActivityDrawer = ({
activityObjectNameSingular,
@@ -90,17 +90,15 @@ export const useOpenCreateActivityDrawer = ({
item.nameSingular === targetableObjects[0].targetObjectNameSingular,
);
const targetField = activityTargetObjectMetadata?.fields.find(
(field) =>
field.relation?.targetObjectMetadata.id ===
targetObjectMetadataItem?.id,
const targetFieldInfo = findTargetFieldInfo(
activityTargetObjectMetadata?.fields ?? [],
targetObjectMetadataItem?.id ?? '',
objectMetadataItems,
);
const joinColumnName = targetField?.settings?.joinColumnName;
const targetableObjectRelationIdName = isDefined(joinColumnName)
? joinColumnName
: `${targetableObjects[0].targetObjectNameSingular}Id`;
const targetableObjectRelationIdName =
targetFieldInfo?.joinColumnName ??
`${targetableObjects[0].targetObjectNameSingular}Id`;
await createOneActivityTarget({
...(activityObjectNameSingular === CoreObjectNameSingular.Task
@@ -7,13 +7,15 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const usePrepareFindManyActivitiesQuery = ({
@@ -33,6 +35,12 @@ export const usePrepareFindManyActivitiesQuery = ({
const cache = useApolloCoreClient().cache;
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } =
useUpsertFindManyRecordsQueryInCache({
@@ -116,6 +124,10 @@ export const usePrepareFindManyActivitiesQuery = ({
findActivitiesOperationSignatureFactory({
objectNameSingular: activityObjectNameSingular,
objectMetadataItems,
isMorphRelation:
activityObjectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated,
});
upsertFindManyActivitiesInCache({
@@ -5,14 +5,17 @@ import { getActivityTargetFieldNameForObject } from '@/activities/utils/getActiv
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { FeatureFlagKey } from '~/generated/graphql';
type UpdateActivityTargetFromCellProps = {
recordPickerInstanceId: string;
@@ -50,6 +53,16 @@ export const useUpdateActivityTargetFromCell = ({
? activityObjectNameSingular
: joinObjectNameSingular,
});
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
);
const isTaskTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
);
const isMorphRelation =
activityObjectNameSingular === CoreObjectNameSingular.Task
? isTaskTargetMigrated
: isNoteTargetMigrated;
const setActivityFromStore = useSetRecoilState(
recordStoreFamilyState(activityId),
@@ -67,7 +80,7 @@ export const useUpdateActivityTargetFromCell = ({
: 'note';
const objectMetadataItems = snapshot
.getLoadable(objectMetadataItemsState)
.getLoadable<ObjectMetadataItem[]>(objectMetadataItemsState)
.getValue();
const pickedObjectMetadataItem = objectMetadataItems.find(
@@ -83,6 +96,7 @@ export const useUpdateActivityTargetFromCell = ({
activityObjectNameSingular,
targetObjectMetadataId: morphItem.objectMetadataId,
objectMetadataItems,
isMorphRelation,
});
if (!isDefined(targetFieldName)) {
@@ -132,7 +146,7 @@ export const useUpdateActivityTargetFromCell = ({
const targetRecord = searchRecord.record;
const activityTarget =
const activityTarget: NoteTarget | TaskTarget =
activityObjectNameSingular === CoreObjectNameSingular.Task
? {
id: v4(),
@@ -167,15 +181,17 @@ export const useUpdateActivityTargetFromCell = ({
...activityTargetWithTargetRecords.map((activityTarget) => {
return activityTarget.activityTarget;
}),
activityTarget as NoteTarget | TaskTarget,
activityTarget,
];
await createOneActivityTarget({
const activityTargetInput: Partial<NoteTarget | TaskTarget> = {
...activityTarget,
[targetObjectName]: undefined,
[targetFieldName]: undefined,
[`${targetFieldName}Id`]: morphItem.recordId,
} as Partial<NoteTarget | TaskTarget>);
};
await createOneActivityTarget(activityTargetInput);
}
setActivityFromStore((currentActivity) => {
@@ -194,6 +210,7 @@ export const useUpdateActivityTargetFromCell = ({
activityObjectNameSingular,
createOneActivityTarget,
deleteOneActivityTarget,
isMorphRelation,
setActivityFromStore,
],
);
@@ -41,7 +41,7 @@ export const Empty: Story = {};
export const WithTasks: Story = {
args: {
targetableObject: {
id: mockedTasks[0].taskTargets?.[0].personId,
id: mockedTasks[0].taskTargets?.[0].targetPersonId,
targetObjectNameSingular: 'person',
} as ActivityTargetableObject,
},
@@ -1,28 +1,17 @@
import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/useLinkedObjectsTitle';
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
// do we need to test this?
export const useTimelineActivities = (
targetableObject: ActivityTargetableObject,
) => {
const isTimelineActivityMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
);
const targetableObjectFieldIdName = isTimelineActivityMigrated
? `target${capitalize(targetableObject.targetObjectNameSingular)}Id`
: getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
const targetableObjectFieldIdName = `target${capitalize(targetableObject.targetObjectNameSingular)}Id`;
const { objectMetadataItem: timelineActivityMetadata } =
useObjectMetadataItem({
@@ -6,8 +6,13 @@ export type NoteTarget = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt?: string | null;
companyId?: string | null;
personId?: string | null;
opportunityId?: string | null;
targetCompanyId?: string | null;
targetPersonId?: string | null;
targetOpportunityId?: string | null;
note: Pick<Note, 'id' | 'createdAt' | 'updatedAt' | '__typename'>;
person?: Pick<Person, 'id' | 'name' | 'avatarUrl' | '__typename'> | null;
company?: Pick<Company, 'id' | 'name' | 'domainName' | '__typename'> | null;
@@ -7,8 +7,13 @@ export type TaskTarget = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt?: string | null;
companyId?: string | null;
personId?: string | null;
opportunityId?: string | null;
targetCompanyId?: string | null;
targetPersonId?: string | null;
targetOpportunityId?: string | null;
taskId: string | null;
task: Pick<Task, 'id' | 'createdAt' | 'updatedAt' | '__typename'>;
person?: Pick<Person, 'id' | 'name' | 'avatarUrl' | '__typename'> | null;
@@ -1,27 +1,35 @@
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const generateActivityTargetMorphFieldKeys = (
objectMetadataItems: ObjectMetadataItem[],
isMorphRelation: boolean,
) => {
const targetableObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
);
const targetableObjects = Object.fromEntries(
objectMetadataItems
.filter(
(objectMetadataItem) =>
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
)
.map((objectMetadataItem) => [objectMetadataItem.nameSingular, true]),
targetableObjectMetadataItems.map((objectMetadataItem) => {
const targetFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: objectMetadataItem.nameSingular,
isMorphRelation,
});
return [targetFieldIdName.replace(/Id$/, ''), true];
}),
);
const targetableObjectIds = Object.fromEntries(
objectMetadataItems
.filter(
(objectMetadataItem) =>
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
)
.map((objectMetadataItem) => [
`${objectMetadataItem.nameSingular}Id`,
true,
]),
targetableObjectMetadataItems.map((objectMetadataItem) => {
const targetFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: objectMetadataItem.nameSingular,
isMorphRelation,
});
return [targetFieldIdName, true];
}),
);
return {
@@ -1,3 +1,4 @@
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
@@ -8,12 +9,14 @@ type GetActivityTargetFieldNameForObjectArgs = {
| CoreObjectNameSingular.Task;
targetObjectMetadataId: string;
objectMetadataItems: ObjectMetadataItem[];
isMorphRelation?: boolean;
};
export const getActivityTargetFieldNameForObject = ({
activityObjectNameSingular,
targetObjectMetadataId,
objectMetadataItems,
isMorphRelation = false,
}: GetActivityTargetFieldNameForObjectArgs): string | undefined => {
const activityTargetObjectNameSingular =
activityObjectNameSingular === CoreObjectNameSingular.Task
@@ -29,10 +32,21 @@ export const getActivityTargetFieldNameForObject = ({
return undefined;
}
const targetField = activityTargetObjectMetadata.fields.find(
(field) =>
field.relation?.targetObjectMetadata.id === targetObjectMetadataId,
const targetObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.id === targetObjectMetadataId,
);
return targetField?.name;
if (isMorphRelation && isDefined(targetObjectMetadataItem)) {
const fieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetObjectMetadataItem.nameSingular,
isMorphRelation: true,
});
return fieldIdName.replace(/Id$/, '');
}
return activityTargetObjectMetadata.fields.find(
(field) =>
field.relation?.targetObjectMetadata.id === targetObjectMetadataId,
)?.name;
};
@@ -6,8 +6,8 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { type Nullable } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType, type Nullable } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
type GetActivityTargetObjectRecordsProps = {
activityRecord: Note | Task;
@@ -47,41 +47,100 @@ export const getActivityTargetObjectRecords = ({
);
const activityTargetRelationFields =
activityTargetObjectMetadata?.fields.filter(
(field) =>
isDefined(field.relation?.targetObjectMetadata.id) &&
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
field.relation?.targetObjectMetadata
.nameSingular as CoreObjectNameSingular,
),
) ?? [];
activityTargetObjectMetadata?.fields.filter((field) => {
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphRelations) &&
field.morphRelations.length > 0
) {
return true;
}
const targetObjectNameSingular =
field.relation?.targetObjectMetadata.nameSingular;
if (!isDefined(field.relation?.targetObjectMetadata.id)) {
return false;
}
if (!isDefined(targetObjectNameSingular)) {
return false;
}
return (
targetObjectNameSingular !== CoreObjectNameSingular.Note &&
targetObjectNameSingular !== CoreObjectNameSingular.Task
);
}) ?? [];
const activityTargetObjectRecords = targets
.map<ActivityTargetWithTargetRecord | undefined>((activityTarget) => {
if (!isDefined(activityTarget)) {
throw new Error('Cannot find activity target');
}
const matchingField = activityTargetRelationFields.find((field) =>
isDefined(activityTarget[field.name]),
);
if (!matchingField || !matchingField.relation) {
if (isDefined(activityTarget.deletedAt)) {
return undefined;
}
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
matchingField.relation?.targetObjectMetadata.id,
);
let matchingFieldName: string | undefined;
let correspondingObjectMetadataItem: ObjectMetadataItem | undefined;
if (!correspondingObjectMetadataItem) {
for (const field of activityTargetRelationFields) {
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphRelations)
) {
const matchingMorphRelation = field.morphRelations.find(
(morphRelation) => {
const morphFieldName = computeMorphRelationFieldName({
fieldName: field.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
});
return isDefined(activityTarget[morphFieldName]);
},
);
if (isDefined(matchingMorphRelation)) {
matchingFieldName = computeMorphRelationFieldName({
fieldName: field.name,
relationType: matchingMorphRelation.type,
targetObjectMetadataNameSingular:
matchingMorphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
matchingMorphRelation.targetObjectMetadata.namePlural,
});
correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
matchingMorphRelation.targetObjectMetadata.id,
);
break;
}
} else if (isDefined(activityTarget[field.name])) {
matchingFieldName = field.name;
correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === field.relation?.targetObjectMetadata.id,
);
break;
}
}
if (
!isDefined(matchingFieldName) ||
!isDefined(correspondingObjectMetadataItem)
) {
return undefined;
}
const targetObjectRecord = activityTarget[matchingField.name] as
| ObjectRecord
| undefined;
const targetObjectRecord: ObjectRecord | undefined =
activityTarget[matchingFieldName];
if (!isDefined(targetObjectRecord)) {
throw new Error(

Some files were not shown because too many files have changed in this diff Show More