9626c7ce02027884f67be2d6ef488f781222d404
72 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9626c7ce02 |
Twenty standard app static options id (#18089)
# Introduction While preparing the twenty-standard as code migration to twenty-app through sdk I've faced permanent field enum update as the id was generated dynamically at each twenty standard app construction Making them deterministic in order to avoid having this noise Won't backfill this on existing workspace as it's not critical and that we will rework the options in the future |
||
|
|
88424611ec |
Refactor and standardize isSystem field and object (#17992)
# Introduction ## Centralize system field definitions - Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`), eliminating duplication across custom object and standard app field builders - Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper ## Mark system fields as `isSystem: true` - Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector` are now properly flagged as system fields across all standard objects and custom object creation - Standard app field builders for all ~30 standard objects updated to set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy` - System-only standard objects (blocklist, calendar channels, message threads, etc.) now also include `createdBy`, `updatedBy`, `position`, `searchVector` field definitions that were previously missing ## Validate system fields on object creation - New transversal validation (`crossEntityTransversalValidation`) runs after all atomic entity validations in the build orchestrator, ensuring all 8 system fields are present with correct `type` and `isSystem: true` when an object is created - New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to resolve field names to universal identifiers for a given object - New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD` on `ObjectMetadataExceptionCode` ## Protect system fields and objects from mutation - Field validators now block update/delete of `isSystem` fields by non-system callers (`FIELD_MUTATION_NOT_ALLOWED`) - Object validators now block update/delete of `isSystem` objects by non-system callers - `POSITION` and `TS_VECTOR` field type validators replaced: instead of rejecting creation outright, they now validate that the field is named correctly (`position` / `searchVector`) and has `isSystem: true` ## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp` - New `isCallerTwentyStandardApp` utility checks whether the caller's `applicationUniversalIdentifier` matches the twenty standard app - Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`, `areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use `isCallerTwentyStandardApp` for custom suffix decisions, keeping `isSystemBuild` for mutation permission checks - `WorkspaceMigrationBuilderOptions` type updated to include `applicationUniversalIdentifier` ## Adapt frontend filtering - New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`, `searchVector`) and `isHiddenSystemField` utility to only hide truly internal fields while keeping user-facing system fields (`createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI - ~20 frontend files updated to replace `!field.isSystem` checks with `!isHiddenSystemField(field)` across record index, settings, data model, charts, workflows, spreadsheet import, aggregations, and role permissions ## Add 1.19 upgrade commands - **`backfill-system-fields-is-system`**: Raw SQL command to set `isSystem = true` on existing workspace fields matching system field names, and fix `position` field type from `NUMBER` to `POSITION` for `favorite`/`favoriteFolder` objects. Includes proper cache invalidation. - **`add-missing-system-fields-to-standard-objects`**: Codegen'd workspace migration to create missing `position`, `searchVector`, `createdBy`, `updatedBy` fields on standard objects that didn't previously have them. Runs via `WorkspaceMigrationRunnerService` in a single transaction with idempotency check. **Known limitation**: assumes all standard objects exist and are valid in the target workspace. ## Add `universalIdentifier` for system fields in standard object constants - `standard-object.constant.ts` updated to include `universalIdentifier` for `createdBy`, `updatedBy`, `position`, and `searchVector` across all standard objects - `fieldManifestType.ts` updated to support the new field manifest shape ## System relation Completely removed and backfilled all `isSystem` relation to be false false As we won't require an object to have any relation system fields ## Add integration tests - New test suite `failing-sync-application-object-system-fields` covering: missing system fields, wrong field types (`id` as TEXT, `createdAt` as TEXT, `position` as TEXT), system field deletion attempts, and system field update attempts - New test utilities: `buildDefaultObjectManifest` (builds an object manifest with all 8 system fields) and `setupApplicationForSync` (centralizes application setup) - Existing successful sync test updated to verify system fields are created with correct properties ## Next step Make the builder scope the compared entity to be the currently built app + nor twenty standard app |
||
|
|
04502e5abb |
Messages Message Folder Association (#17398)
This PR adds Message folder association for message channel messages, Currently under testing phase, not ready yet. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
ce1ffa8550 |
Refactor page layout types (#18042)
## Refactor page layout widget types into shared package and expose from SDK ### Why Widget configuration types were defined only on the server, forcing SDK consumer apps to import from deep internal `twenty-shared/dist` paths — fragile and breaks on structural changes. Server DTOs also had no compile-time guarantee they matched the canonical types. ### What changed - **`twenty-shared`**: Migrated `ChartFilter`, `GridPosition`, `RatioAggregateConfig` and all 20 widget configuration variants into `twenty-shared/types`. `PageLayoutWidgetConfiguration` (base, with `SerializedRelation`) and `PageLayoutWidgetUniversalConfiguration` (derived via `FormatRecordSerializedRelationProperties`) are now the single source of truth. - **`twenty-sdk`**: Re-exported `AggregateOperations`, `ObjectRecordGroupByDateGranularity`, `PageLayoutTabLayoutMode`, and `PageLayoutWidgetUniversalConfiguration` so consumer apps import from `twenty-sdk` directly. - **`twenty-server`**: All widget DTOs now `implements` their shared type for compile-time enforcement. Added helpers to convert nested `fieldMetadataId` ↔ `fieldMetadataUniversalIdentifier` inside chart filters. Removed redundant local type re-exports. |
||
|
|
f3faa11dd2 |
New field creates fields widget field (#18022)
## Context
Introducing "NewFieldDefaultConfiguration" to FIELDS widget
configurations
```typescript
{
isVisible: boolean;
viewFieldGroupId: string | null;
}
```
This configuration will define where a new field should be added (which
section) and its default visibility inside FIELDS widget views.
The new field position should always be at the end (meaning the last
position for the view fields OR the last position of a viewFieldGroup)
See "New fields" on this screenshot
<img width="401" height="724" alt="Layout V1"
src="https://github.com/user-attachments/assets/4969bcaa-f244-4504-8947-778a02c24c47"
/>
|
||
|
|
015ccbf0a7 |
Navbar customization improvements (#17863)
- Add folder icon editing support - And other navbar customization fixes/improvements. --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: Devessier <baptiste@devessier.fr> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
b4e924b671 |
Sync views and navigation items (#18003)
Both objects are necessary to fully enjoy objects within applications <img width="770" height="311" alt="Capture d’écran 2026-02-17 à 15 19 43" src="https://github.com/user-attachments/assets/48c51fa4-63f4-45b2-a40a-df73f3aa79be" /> |
||
|
|
20977428a1 |
Page layout various fixes (#17996)
## Context - Add missing fields widget and FIELDS_WIDGET view for workflow run and workflow version standard objects - Fix FIELDS_WIDGET configuration fieldId universalIdentifier not being converted to id when migration is executed. |
||
|
|
2b7b05de2e |
[OBJECT_MANIFEST_BREAKING_CHANGE] Sync returns workspace migration (#17918)
# Introduction In this PR we start returning a workspace migration post sync so it can committed and provided within the tarball ## Universal aggregators utils Created two utils ### deleteUniversalFlatEntityForeignKeyAggregators Used when building a universal create action, a newly created actions should not contain any aggregated foreign key so they won't be codegen in the workspace migration but also they are overriden at uninversal to flat transpilation anw ### resetUniversalFlatEntityForeignKeyAggregators Used before validating a new flat entity creation, some validator will consume the fk aggregator in order to validate integrity, but of optimstically provided it can result to errors. To avoid caller responsability we override them here ## create-field-action refactor Refactored the universal and flat field create action to be following the base actions in order to ease typing Also it was tailored to handle unlimited amount of flat field metadata in the same actions whereas in the reality we were always only sending at max 2 ( for relation fields ) Note: relation field has to be provided at the same as if not optimistic would fail to retrieve circular universal identifiers ## ObjectManifest Now always expect a `labelIdentifierFieldMetadataUniversalIdentifier` ## Integration test Created an integration test that creates an app, sync a first manifest and a second implying update workspace migration action generation |
||
|
|
c775d65952 |
feat: configure standard views and migrate attachment seeds to FILES field (#17958)
## Summary - Add default visible view fields for `timelineActivity`, `attachment`, `noteTarget`, `taskTarget`, and `workspaceMember` objects so they display useful columns out of the box - Standardize morph relation field labels to "Target" with `IconArrowUpRight` for consistency across all pivot/junction tables - Mark deprecated fields (`fullPath`, `fileCategory`, `linkedRecordCachedName`, `linkedRecordId`, `linkedObjectMetadataId`) as `isSystem` to hide them from the UI column picker - Fix morph field deduplication logic (`pickMorphGroupSurvivor`) to prefer active, non-system fields over auto-generated system fields from custom objects - Migrate attachment seeds from legacy `fullPath`/`fileCategory` to the new `FILES` field type, creating proper `FileEntity` records in `core.file` via `fileStorageService.writeFile()` - Restore `customDomain` in the user query fragment <img width="825" height="754" alt="Screenshot 2026-02-15 at 15 44 27" src="https://github.com/user-attachments/assets/9596a3dd-8d3a-43c0-925a-0adef9ee68a8" /> <img width="736" height="731" alt="Screenshot 2026-02-15 at 15 44 13" src="https://github.com/user-attachments/assets/cd1a66c5-731d-43e6-bbc3-703cbeda1652" /> <img width="722" height="757" alt="Screenshot 2026-02-15 at 15 44 03" src="https://github.com/user-attachments/assets/b5210546-6a40-4940-8e4f-874818a614fb" /> <img width="907" height="757" alt="Screenshot 2026-02-15 at 15 43 52" src="https://github.com/user-attachments/assets/ead5b9a8-1989-4d68-9640-583da6233711" /> <img width="1002" height="731" alt="Screenshot 2026-02-15 at 15 43 38" src="https://github.com/user-attachments/assets/38accb8c-f5d5-4bfc-b245-06389849810b" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches migration/upgrade commands that write to core metadata tables and adjust field/view definitions, plus changes dev seeding to create `core.file` records; mistakes could affect UI visibility or seed integrity across workspaces. > > **Overview** > Adds a new `upgrade:1-18:backfill-standard-views-and-field-metadata` command that, per workspace, marks specific fields as `isSystem`, normalizes morph-relation field `label`/`icon` to `Target`/`IconArrowUpRight`, and backfills missing standard `view`/`viewField` rows for `attachment`, `noteTarget`, `taskTarget`, `timelineActivity`, and `workspaceMember`, followed by cache invalidation + metadata version bump. > > Refactors morph-relation deduplication to pick a single survivor per `morphId` using a new `pickMorphGroupSurvivor` rule (prefer active + non-system, then smallest id), with new unit tests. > > Updates standard metadata generators and snapshots to reflect the new system flags and default view fields, and rewrites attachment dev seeding to populate the new `file` (FILES field) JSON and create corresponding `core.file` entries via `FileStorageService.writeFile` with workspace-scoped file IDs. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b1939bbf6f8cce294f9b4cdec06b19778daa205e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
d54b713264 |
Respect Gmail retry-after in messaging throttle (#17850)
Gmail 429/403 rate-limit responses include an explicit retry-after timestamp, usually ~15 minutes out. The exponential backoff starts at 1 minute, so the channel burns through all 5 retry attempts before the window actually closes and gets marked as permanently failed. Adds throttleRetryAfter to the message channel and uses max(backoff, retryAfter) in isThrottled(). |
||
|
|
3075707bf8 |
Prefil fields widgets to standard app (#17897)
## Context
Prefill the FIELDS widget configuration in standard page layouts during
workspace creation, linking each widget to a dedicated view with
positioned fields organized into sections (via view field groups)
We wanted something very declarative (by manually setting position and
visibility of each field per standard object).
In this PR I've generated all the compute- utils via AI (😨) for
position/visibility, we'll probably want to confirm with the product
which ordering/visibility we want for each standard object but I feel
like this can be merged as it is since it's behind a feature flag and
this will unblock the work on the frontend
|
||
|
|
6aca1dd013 |
Introducing view field group syncable entity (#17867)
## Context Introduces a new viewFieldGroup entity that allows grouping view fields into sections (e.g. "General", "Additional", "Other") within a view. The page layout fields widget needs a way to organize fields into sections. Today, views have no concept of field grouping. This PR introduces the viewFieldGroup entity which sits between a view and its viewFields, enabling section-based organization. <img width="401" height="724" alt="Layout - V2 (customize visibility)" src="https://github.com/user-attachments/assets/6376e2ab-44db-42bf-9d2c-758f56f6b548" /> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
9bc63a01c9 |
Generate GQL schema based on applicationId (#17860)
## Add application-scoped GraphQL schema generation When an application token is used to authenticate, the `/graphql` schema is now dynamically filtered to only include entities belonging to that application (plus the Twenty Standard Application). This enables third-party applications and the SDK to introspect a schema that is relevant to their scope, rather than seeing the full workspace schema with all custom objects. ### Changes - **New `generateApplicationToken` mutation** on the `/metadata` endpoint, allowing callers to exchange an API key for an application-scoped JWT token - **Schema filtering by application** in `WorkspaceSchemaFactory` — when `request.application` is present (from an application token), flat entity maps are filtered by `[appId, standardAppId]` before schema generation - **Per-app caching** — both the Yoga in-memory cache and Redis cache now include the `appId` in their keys to avoid serving wrong schemas - **Consolidated `getSubFlatEntityMapsByApplicationIdsOrThrow`** — unified the single-ID and multi-ID filtering utilities into one - **Integration tests** covering token generation (admin + API key auth) and schema introspection filtering (standard app token excludes custom objects) Schema generated on seeds with applicationToken (see that pets is missing) <img width="782" height="994" alt="image" src="https://github.com/user-attachments/assets/82510031-0965-435d-bc26-77c9f5d74e1f" /> |
||
|
|
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) |
||
|
|
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 <baptiste@devessier.fr> |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
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 <charles@twenty.com> |
||
|
|
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 <felix.malfait@gmail.com> |
||
|
|
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 |
||
|
|
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 }, }); ``` |
||
|
|
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 <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
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 |
||
|
|
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. |
||
|
|
476bdf764c |
Refactor flat entity maps to be universal oriented (#17665)
# Introduction
In preparation of the workspace agnostic builder, we're migrating
`FlatEntityMaps` to be universal identifier oriented and based
As in the builder context there're won't be any ids at all
Please also note that the FlatEntity is a UniversalFlatEntity superset
From
```ts
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
export type FlatEntityMaps<T extends SyncableFlatEntity> = {
byId: Partial<Record<string, T>>;
idByUniversalIdentifier: Partial<Record<string, string>>;
universalIdentifiersByApplicationId: Partial<Record<string, string[]>>;
};
```
To
```ts
export type FlatEntityMaps<
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
> = {
byUniversalIdentifier: Partial<Record<string, T>>;
universalIdentifierById: Partial<Record<string, string>>;
universalIdentifiersByApplicationId: Partial<Record<string, string[]>>; // this might make more sense to be migrated to universalIdentifiersByApplicationUniversalIdentifier but it's the main topic of this PR
};
```
## Low level maps tools
Had to refactor find | create | delete | replace | find-many | get-sub
tools ( through mutations and or throw equivalent )
|
||
|
|
5c91a96e84 |
Add position to page layout widgets (#17643)
## Context
Introducing a new position column in PageLayoutWidget. This is because
we already have gridPosition but all widgets are not supposed to fit in
a grid (see pageLayoutTab layoutMode (GRID, VERTICAL_LIST, CANVAS...),
position will now support the different layoutMode
## Implementation
- Adding the new column (not renaming to avoid breaking changes in the
dashboards)
- Add proper validation for each layout mode
- Update standard page layout to not always use GRID but rely on the tab
layout mode
<details>
<summary>Graphql Query</summary>
```graphql
{
"data": {
"getPageLayouts": [
{
"tabs": [
{
"title": "Tab 1",
"layoutMode": "GRID",
"widgets": [
{
"title": "Untitled Rich Text",
"position": {
"column": 0,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 6
}
},
{
"title": "Deals by Company",
"position": {
"column": 6,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 6
}
},
{
"title": "Pipeline Value by Stage",
"position": {
"column": 0,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 6,
"rowSpan": 6
}
},
{
"title": "Revenue Timeline",
"position": {
"column": 6,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 6,
"rowSpan": 6
}
},
{
"title": "Opportunities by Owner",
"position": {
"column": 0,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 12,
"rowSpan": 6
}
},
{
"title": "Stock market (Iframe)",
"position": {
"column": 6,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 12,
"rowSpan": 8
}
},
{
"title": "Deals created this month",
"position": {
"column": 0,
"columnSpan": 3,
"layoutMode": "GRID",
"row": 18,
"rowSpan": 2
}
},
{
"title": "Deal value created this month",
"position": {
"column": 3,
"columnSpan": 3,
"layoutMode": "GRID",
"row": 18,
"rowSpan": 2
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Tasks",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Tasks",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Notes",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Notes",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Emails",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Emails",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Calendar",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Calendar",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Tasks",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Tasks",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Notes",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Notes",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Emails",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Emails",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Calendar",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Calendar",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Tasks",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Tasks",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Notes",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Notes",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Emails",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Emails",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Calendar",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Calendar",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
},
{
"title": "Note",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 1
}
}
]
},
{
"title": "Note",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Note",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
},
{
"title": "Note",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 1
}
}
]
},
{
"title": "Note",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Note",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Flow",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Flow",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Flow",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Flow",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Flow",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Flow",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
}
]
}
}
```
</details>
|
||
|
|
d35d5c0463 |
[BREAKING_CHANGE] Deprecate remaining entities standardId (#17639)
# Introduction Following https://github.com/twentyhq/twenty/pull/17632 and https://github.com/twentyhq/twenty/pull/17572 This PR deprecates the agent, skill, field metadata and role `standardId` in favor of the `universalIdentifier` usage ## Note - Removed previous standard ids declaration modules - Twenty-sdk now re-exports the `STANDARD_OBJECTS` universalIdentifier hashmap constant - deleted some sync-metadata deadcode too ( mainly types ) |
||
|
|
75921e79bf |
[FIXES_MAIN] Remove objectMetadata standardId (#17632)
# Introduction In this PR we're deprecating the object metadata standard id and replacing it to the universalIdentifier usage As we've totally removed its insertion for both new field and object in https://github.com/twentyhq/twenty/pull/17572 ## Note - Removed upgrade commands before `1.17` |
||
|
|
bd9688421f |
ObjectMetadata and FieldMetadata agnostic workspace migration runner (#17572)
# Introduction
Important note: This PR officially deprecates the `standardId`, about to
drop col and entity property after this has been merged
Important note2: Haven't updated the optimistic tool to also update the
universal identifier aggregators only the ids one, they should not be
consumed in the runner context -> need to improve typing or either the
optimistic tooling
In this PR we're introducing all the devxp allowing future metadata
incremental universal migration -> this has an impact on all existing
metadata actions handler ( explaining its size )
This PR also introduce workspace agnostic create update actions runner
for both field and object metadata in order to battle test the described
above devxp
Noting that these two metadata are the most complex to handle
Notes:
- A workspace migration is now highly bind to a
`applicationUniversalIdentifier`. Though we don't strictly validate
application scope for the moment
## Next
Migrate both object and field builder to universal comparison
## Universal Actions vs Flat Actions Architecture
### Concept
The migration system uses a two-phase action model:
1. **Universal Actions** - Actions defined using `universalIdentifier`
(stable, portable identifiers like `standardId` + `applicationId`)
2. **Flat Actions** - Actions defined using database `entityId` (UUIDs
specific to a workspace)
### Why This Separation?
- **Universal actions are portable**: They can be serialized, stored,
and replayed across different workspaces
- **Flat actions are executable**: They contain the actual database IDs
needed to perform operations
- **Decoupling**: The builder produces universal actions; the runner
transpiles them to flat actions at execution time
### Transpiler Pattern
Each action handler must implement
`transpileUniversalActionToFlatAction()`:
```typescript
@Injectable()
export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'fieldMetadata',
) {
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalCreateFieldAction>,
): Promise<FlatCreateFieldAction> {
// Resolve universal identifiers to database IDs
const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatObjectMetadataMaps,
universalIdentifier: action.objectMetadataUniversalIdentifier,
});
return {
type: action.type,
metadataName: action.metadataName,
objectMetadataId: flatObjectMetadata.id, // Resolved ID
flatFieldMetadatas: /* ... transpiled entities ... */,
};
}
}
```
### Action Handler Base Class
`BaseWorkspaceMigrationRunnerActionHandlerService<TActionType,
TMetadataName>` provides:
- **`transpileUniversalActionToFlatAction()`** - Abstract method each
handler must implement
- **`transpileUniversalDeleteActionToFlatDeleteAction()`** - Shared
helper for delete actions
## FlatEntityMaps custom properties
Introduced a `TWithCustomMapsProperties` generic parameter to control
whether custom indexing structures are included:
- **`false` (default)**: Returns `FlatEntityMaps<MetadataFlatEntity<T>>`
- used in builder/runner contexts
- **`true`**: Returns the full maps type with custom properties (e.g.,
`byUserWorkspaceIdAndFolderId`) - used in cache contexts
## Create Field Actions Refactor
Refactored create-field actions to support relation field pairs
bundling.
**Problem:** Relation fields (e.g., `Attachment.targetTask` ↔
`Task.attachments`) couldn't resolve each other's IDs during
transpilation because they were in separate actions with independent
`fieldIdByUniversalIdentifier` maps.
**Solution:**
- Removed `objectMetadataUniversalIdentifier` from
`UniversalCreateFieldAction` and `objectMetadataId` from
`FlatCreateFieldAction` - each field now carries its own
- Runner groups fields by object internally and processes each table
separately
- Split aggregator into two focused utilities:
- `aggregateNonRelationFieldsIntoObjectActions` - merges non-relation
fields into object actions
- `aggregateRelationFieldPairs` - bundles relation pairs with shared
`fieldIdByUniversalIdentifier`
|
||
|
|
0001c2e7d0 |
[Apps] Apps marketplace (first draft) (#17562)
https://github.com/user-attachments/assets/c4e63edb-6e98-44bc-841f-ee110ae712d4 How it works - `manifest.json` files are now committed when apps are published to our repo - to display available apps, from the server, we read into our github repo, using a pod-scoped cache - feature flagged - app installation will be behind permission gate MARKETPLACE_APPS Limitations and what is yet to develop - content and settings tabs - installed apps tab - app installation - test and potentially fix reading from .manifest.json once [Reshape manifest structure](https://github.com/twentyhq/core-team-issues/issues/2183) is done. additional work is expected on assets notably. (couldnt properly do it here as manifest.json will only be committed after this pr) - we only read in community/ folder for now - we may want tochange that - the cache is rather artisanal for now and scoped by pod - we may want to change that |
||
|
|
446afcbc30 | Add standard record page layout (#17567) | ||
|
|
fbbd8fe967 |
[REQUIRES_CACHE_FLUSH_FOR_FIELD_AND_OBJECT]FlatFieldMetadata and FlatObjectMetadata required universal (#17557)
# Introduction
In this PR we're migrating both the `field` and `object` metadata to be
using the new `FlatEntityFromV2` that requires all the
`UniversalFlatEntityExtraProperties` to be spread at the flat entity
root.
This means that we have to update all of their flat declaration
This type swap allows to isole a specific entity migration into his own
type scope and avoid to have everything handled at once
```ts
/**
* Currently under migration but aims to replace FlatEntity afterwards
*/
export type FlatEntityFromV2<
TEntity,
TMetadataName extends AllMetadataName | undefined = undefined,
TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
TEntity,
TMetadataName
>,
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];
```
## Impact
Both object and field:
- Create input transpilation utils
- from entity to flat tools
- mocks
## Note
Removed from the universal extra properties the jsonb properties that do
not contain a serialized
## Next
Next step is to incrementally make the builder and runner expect
`UniversalFlatEntity` for both of these metadata
This way we will be able to fully migrate an entity e2e typesafely
|
||
|
|
7d69000ab7 |
Update pageLayout* data models for backend recordPageLayout refactor (#17446)
<img width="814" height="356" alt="Screenshot 2026-01-26 at 16 09 27" src="https://github.com/user-attachments/assets/91d95a1d-cc33-4bf0-a63e-4d1815030983" /> |
||
|
|
2daebc6d0f |
Add WorkspaceAuthContextMiddleware (#17487)
## Context
Introduces a middleware that automatically sets the workspace auth
context in AsyncLocalStorage for HTTP requests, making it available
throughout the request lifecycle without explicit parameter passing.
The motivation behind this change is to reduce boilerplate and simplify
the developer experience when working with workspace data in HTTP
request handlers.
The Problem (Before)
Every HTTP request handler that needed to access workspace data had to:
- Extract auth-related info from decorators (@AuthWorkspace(),
@AuthUserWorkspaceId(), etc.) in controller/resolver and pass down to
services
- Build or pass the authContext explicitly (sometimes with type
assertion which was flaky)
Then call executeInWorkspaceContext(authContext, async () => { ... })
## Changes
- Add WorkspaceAuthContextMiddleware that extracts auth context from the
request and stores it in AsyncLocalStorage
- Register middleware for GraphQL, metadata, and REST routes (runs after
hydration middlewares)
- Simplify executeInWorkspaceContext signature: fn is now the first
parameter, authContext is optional second
- If authContext is not provided, it's automatically retrieved from the
storage (set by middleware)
- Update all callers (~120 files) to use the new parameter order
- Fixes a bug in search where system auth context was used, bypassing
RLS feature.
|
||
|
|
2a8f834377 |
Integrate NavigationMenuItem with feature flag support (#17268)
## Implement Navigation Menu Items Frontend Implements the frontend for navigation menu items, the new system replacing favorites. ### Changes - Added GraphQL fragments and queries for navigation menu items - Added hooks for managing navigation menu items (create, update, delete, sorting, filtering) - Updated components to use navigation menu items instead of favorites - Added test coverage for utility functions ### Migration Note The favorites and navigation menu item modules currently exist in parallel. The favorites code will be removed once all data has been migrated to navigation menu items. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Replaces Favorites with feature-flagged `NavigationMenuItem` across frontend and backend, while keeping Favorites as fallback until migration completes. > > - UI: new `navigation-menu-item` components (folders, orphan items, drag provider/droppable, icons, skeleton), dispatcher components to switch from Favorites, and updated “Add to favorites” action to create `NavigationMenuItem` when `IS_NAVIGATION_MENU_ITEM_ENABLED` > - DnD: shared `validateAndExtractFolderId` and droppable id utils moved to `ui/layout/draggable-list`; favorites DnD updated to use shared utils > - GraphQL (client): add fragments, queries, mutations, hooks (create/update/delete/find), and generated types; added `RecordIdentifier` and `targetRecordIdentifier` on `NavigationMenuItem` > - Prefetch: new prefetch state/effect for navigation menu items; skip favorites prefetch when flag enabled > - Backend: add DTOs (`NavigationMenuItem`, `RecordIdentifier`), resolver `targetRecordIdentifier` field, service logic to fetch record identifiers with permission-aware access and image signing, `getRecordImageIdentifier` util, entity relation to `view`, and migration adding FK on `viewId` > - Feature flags & seeding: add `IS_NAVIGATION_MENU_ITEM_ENABLED` to enums, dev seeder enables it; standard app seeds workspace navigation menu items instead of favorites when flag on > - Tests: add unit tests for sorting/labels/folder id and related utils > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c99746f08b9f84fc8cec4fcc3a7d7afb8ea92db7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
d0bc9a94c0 |
[OBJECT_CACHE_FLUSH_REQUIRED_WHEN_RELEASED] Remove FlatObjectMetadata custom fieldMetadataIds fk aggregator property (#17438)
# Introduction Currently refactoring `flatEntity` typing, encountering some tsc errors due to this fk aggregator custom override It shall now follow generic pattern leading to be named `fieldIds` Needs to flush object cache when released |
||
|
|
fa0615d41e |
Migrate attachments to morph relations + fix morph join column filtering (#17381)
Closes [1744](https://github.com/twentyhq/core-team-issues/issues/1744). This PR migrates attachments to morph relations behind a feature flag, following the TimelineActivity pattern. it introduces the `IS_ATTACHMENT_MIGRATED` flag, updates standard field metadata and indexes to use morph relations, adds a workspace migration that renames `attachment.*Id` columns to `target*Id` and converts the corresponding field metadata to `MORPH_RELATION` with a shared `morphId`. On the frontend, attachment read/write paths now switch to `target*Id` when the flag is enabled. It also fixes optimistic filtering for morph join columns. The metadata API deduplicates morph fields, so attachments now expose a single target field of type `MORPH_RELATION` plus a `morphRelations` array listing each target object. Because only one `settings.joinColumnName` is returned (e.g. `targetRocketId`), filters like `targetCompanyId` don’t map to any field and the optimistic cache code throws. `doesMorphRelationJoinColumnMatch` resolves this by computing all valid join column names from `morphRelations` using `computeMorphRelationFieldName` and comparing them to the filter key. That makes filters like `targetCompanyId` resolvable even with a single target field, so attachment uploads and list matching no longer crash. <img width="477" height="474" alt="image" src="https://github.com/user-attachments/assets/50e19418-3438-4d1e-9f1f-1bc1a03174a9" /> <br /> <br /> Today the metadata API returns one morph field called `target` and a list of possible targets (`morphRelations`), but it does not tell us the join column for each target. That’s why the Frontend had to compute join column names. If we want to fix this at the API level, there are two options: - Add join column names to each target in `morphRelations` (e.g. company → `targetCompanyId`). This is additive and low‑risk. - Return each target as its own field (`targetCompany`, `targetPerson`, etc.) instead of a single target. This is a larger change because it changes the shape of metadata and would require more UI updates. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces morph relations for attachments behind `IS_ATTACHMENT_MIGRATED`, aligning server schema/metadata and frontend behavior. > > - Adds `IS_ATTACHMENT_MIGRATED` flag (frontend/server) and seeds/defaults; updates generated GraphQL enums > - New workspace upgrade `1.17` command migrates data: renames `attachment.*Id` → `target*Id` and converts related fields to `MORPH_RELATION` with shared `morphId` > - Updates standard field metadata and indexes to `target*` (attachment + related objects), dev seeds, snapshots, and workspace entity types > - Frontend: switches attachment read/write filters via `getActivityTargetObjectFieldIdName` using the feature flag; updates hooks/components (`useAttachments`, `useUploadAttachmentFile`, editors); expands `Attachment` type > - Fixes optimistic cache filtering to recognize morph join columns in `isRecordMatchingFilter` by computing valid join-column keys from `morphRelations` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f208fa23b1135af114f484f6d65041a816d6e718. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
3ed67b825e |
feat: implement generic many-to-many junction relation support (#16820)
## Overview
This PR implements **generic many-to-many relation support** through
junction tables (also known as associative entities or join tables).
This replaces the need for hardcoded taskTarget/noteTarget logic and
provides a flexible foundation for modeling complex entity
relationships.
## Architecture
### Data Model
Many-to-many relationships are implemented using a **junction object
pattern**:
```
┌─────────┐ ┌──────────────────┐ ┌─────────┐
│ Pet │──────>│ PetRocket │<──────│ Rocket │
│ │ 1:N │ (junction) │ N:1 │ │
│ rockets ├───────┤ pet : Pet ├───────┤ │
└─────────┘ │ rocket : Rocket │ └─────────┘
└──────────────────┘
```
The junction object (PetRocket) has:
- A `MANY_TO_ONE` relation to **Pet** (the source)
- A `MANY_TO_ONE` relation to **Rocket** (the target)
The source object (Pet) has a `ONE_TO_MANY` relation pointing to the
junction, with **field settings** that specify which target field to
follow.
### Field Settings Schema
Junction configuration is stored in `FieldMetadataRelationSettings`:
```typescript
{
relationType: "ONE_TO_MANY",
// Points to the target field on the junction object
junctionTargetFieldId?: string; // For regular relations
junctionTargetMorphId?: string; // For polymorphic relations
}
```
**Two configuration modes:**
1. **`junctionTargetFieldId`** - References a specific `RELATION` field
on the junction
2. **`junctionTargetMorphId`** - References a `morphId` group for
polymorphic targets (e.g., link to Person OR Company)
### GraphQL Query Generation
When a junction relation is detected, the GraphQL fields are generated
to fetch the nested target:
```graphql
query GetPetWithRockets {
pet(id: "...") {
rockets { # ONE_TO_MANY to junction
id
rocket { # Target field on junction
id
name
__typename
}
}
}
}
```
For polymorphic junction targets:
```graphql
caretakerPerson { id, name }
caretakerCompany { id, name }
```
## Frontend Architecture
### Display Flow
1. **Detection**: `hasJunctionConfig()` checks if field has junction
settings
2. **Config Resolution**: `getJunctionConfig()` resolves junction object
metadata and target fields
3. **Record Extraction**: `extractTargetRecordsFromJunction()` extracts
target records from junction records
4. **Rendering**: Target records displayed as chips (not junction
records)
### Edit Flow
1. **Picker Opening**: Initializes the multi-record picker with:
- Searchable object types (derived from junction target fields)
- Pre-selected items (extracted from existing junction records)
2. **Selection Handling**: Manages create/delete of junction records:
- **Select**: Creates new junction record with source + target IDs
- **Deselect**: Finds and deletes the junction record
- **Optimistic Updates**: Manually updates Recoil store before API call
### Key Trade-offs
| Decision | Trade-off |
|----------|-----------|
| Junction records managed manually | More control over optimistic
updates, but requires manual cache management |
| Settings stored per-field | Flexible (same junction can power
different views), but requires UI to configure |
| Polymorphic via morphId groups | Supports N target types, but adds
query complexity |
| Feature flag gated | Safe rollout, but requires flag management |
## Backend Changes
- **Validation**: Junction target field must exist and be a valid
`MANY_TO_ONE` relation
- **Settings**: Extended `FieldMetadataRelationSettings` type with
junction fields
- **Dev Seeder**: Added sample junction objects (PetRocket,
EmploymentHistory, PetCareAgreement) for testing
## How to Test
1. Enable the `IS_JUNCTION_RELATIONS_ENABLED` feature flag
2. Create objects with junction pattern (Pet → PetRocket → Rocket)
3. Configure the junction target in field settings (advanced mode)
4. Verify:
- Display shows target objects (Rockets), not junction records
(PetRockets)
- Picker allows selecting/deselecting targets
- Changes persist correctly
https://github.com/user-attachments/assets/d04f057a-228c-4de8-af48-76bb2d72cac1
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
9620961f16 |
[Dashboards] [ai] expand widget schemas with subfield grouping, color, and styling options (#17185)
Got rid of filters for now -- filters would need extra tailoring since they depend on field names/types and the RecordGqlOperationFilter DSL (nested AND/OR and composite subfields), which the AI can’t reliably construct without additional metadata and skills created a followup to tackle the same https://github.com/twentyhq/core-team-issues/issues/2108 |
||
|
|
8908e0785f |
[Dashboards] [ai] update dashboard tools schema and skill with correct config field names (#17180)
<img width="2560" height="1353" alt="CleanShot 2026-01-16 at 00 55 56" src="https://github.com/user-attachments/assets/c242071c-13d3-494d-bcce-9d45f36fcd77" /> |
||
|
|
8413c6f3dd |
Fix insert new record with RLS (#17164)
## Context Now that RLS predicates are applied, creating a record through the FE (which is empty by default) is failing if your role has predicates and your input does not respect them (which will always be true since, as said above, input will be pretty much empty) ## Implementation - Moved isMatching* filters to twenty-shared - Implemented isMatchingRlsPredicates utils in the backend (ORM) to check before insertion/update if the record is matching the current user role Rls predicates, reusing the isMatching* filters utils moved to twenty-shared - Frontend now applies RLS predicates before creating a new record (similarly to what we do with view filters) Note: It seems composite were not properly handled with view-filter insertion logic, since I'm reusing the util for now, the issue remains for RLS and will need to be addressed |
||
|
|
2c8d3f02e1 |
feat: upgrade to Storybook version 10 (#17110)
Upgraded to Storybook 10. We still use `@storybook/test-runner` for testing since it appears it'd require more work to move from Jest to Vitest than I initially anticipated, but I completed this PR to fix `storybook:serve:dev` - it takes time to load, but it works the way it used to with Storybook 8. https://github.com/user-attachments/assets/7afc32c6-4bcf-4b37-b83b-8d00d28dda15 |
||
|
|
46e5420d20 |
dashboard workspace standard seeds (#16962)
# Introduction In this pull request we're introducing new standard page layout, tabs and widget ( 1 page layout, 1 tab and 8 widgets ) and also a new opportunity field Also now prefilling new records, 6 opportunities and a dashboard. ## Standard declaration ### New workspace creation Relies on existing standard declaration builder ### Backfill command We've been hacking through the standard builder in order to extract only the standard page layout entities, updated their entity dependencies to match the workspace ids so the validation passes ## Remark - Refactored the `PageLayoutWidget` configuration type to be dynamically typed through a generic discriminated union --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
d7b7e9def1 |
Cascade delete Task targets when tasks deleted - logic + migration command (#17019)
Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
942d2fef83 |
Remove sync-metadata and IS_WORKSPACE_CREATION_V2_ENABLED feature flag (#16997)
# Introduction Followup of https://github.com/twentyhq/twenty/pull/17001#pullrequestreview-3638508738 close https://github.com/twentyhq/core-team-issues/issues/1910 We've completely decom the `sync-metadata` in production. We're now then removing its implementation in favor of the v2. ## TODO: - [x] Remove sync-metadata implem and commands - [x] Remove workspace decorators - [x] Type each deprecated field to deprecated on their workspaceEntity - [x] Remove the `workspace-sync-metadata` folder entirely - [x] remove workspace migration - [x] workspace migration removal migration - [x] remove the `v2` references from workspace manager file names - [x] remove the `v2` references from workspace manager modules - [ ] Double check impact on translation file path updates ## Note - Removed the gate logic - Remains some service v2 naming, serverless needs to be migrated on v2 fully - Removed workspaceMigration service app health consumption, making it always returning up ( no more down ) cc @FelixMalfait ( quite obsolete health check now, will require complete refactor once we introduce inter app dependency etc ) |
||
|
|
21ff42074d |
feat: implement skills system for AI agents (#16865)
## Summary This PR introduces a Skills system for AI agents, inspired by the [Agent Skills specification](https://agentskills.io/specification). ## Changes ### Backend - **SkillEntity**: New database entity with migration for storing skills - **V2 Sync Mechanism**: Implemented FlatSkill, builders, validators, and action handlers following the v2 flat entity pattern - **Standard Skills**: Pre-defined skills (workflow-building, data-manipulation, dashboard-building, metadata-building, research, code-interpreter, xlsx, pdf, docx, pptx) - **GraphQL API**: CRUD operations for skills with proper guards and permissions - **Workspace Cache**: Integrated skills into the workspace cache system ### Frontend - **Skills Table**: Searchable table in AI settings showing all skills - **Skill Form**: Create/edit page with Label (primary), Description, and Content (markdown editor) - **API Name**: Following existing patterns, name is derived from label with advanced settings toggle for custom API names - **Standard vs Custom**: Standard skills are read-only, custom skills can be edited/deleted ## Key Design Decisions - Skills are stored in the database (Salesforce-like approach) rather than files - Name is derived from Label by default (isLabelSyncedWithName pattern) - Skills reference functions/files via @ mentions in markdown content rather than explicit relations - Standard skills are synced from code, custom skills are created via UI ## Screenshots Skills table and form UI follow existing settings patterns. ## Testing - [x] Lint passes - [x] Typecheck passes - [ ] CI tests |
||
|
|
98a9ae2a0e |
Migrate view filter group to v2 (#16876)
## Introduction On the side hanlded PR with a // agents on another repo Made several iterations to fix behavior and direction Find below auto-generated PR description closes https://github.com/twentyhq/core-team-issues/issues/2037 Created generic tooling for entity circular dep checking, will be useful for permissions validation too @Weiko ## Migrate `viewFilterGroup` entity to v2 flat architecture ### Summary Migrates the `viewFilterGroup` entity from v1 to the v2 flat entity architecture, following the established patterns for other v2 entities like `viewFilter`, `view`, and `viewField`. ### Changes **Types & Constants** - Added `FlatViewFilterGroup` and `FlatViewFilterGroupMaps` types - Added editable properties constant for `viewFilterGroup` - Registered `viewFilterGroup` in `ALL_METADATA_NAME`, `ALL_METADATA_RELATION_PROPERTIES`, `ALL_METADATA_MANY_TO_ONE_RELATIONS`, and related constants **Cache Service** - Created `WorkspaceFlatViewFilterGroupMapCacheService` with proper relation loading for `viewFilters` and `childViewFilterGroups` - Updated `WorkspaceFlatViewMapCacheService` to load `viewFilterGroups` relation **Builder & Validator** - Created `WorkspaceMigrationV2ViewFilterGroupActionsBuilderService` - Created `FlatViewFilterGroupValidatorService` with creation, update, and deletion validation - Integrated validation into the orchestrator service (runs before `viewFilter` validation) **Action Handlers** - Created create, update, and delete action handlers for `viewFilterGroup` **Service Migration** - Rewrote `ViewFilterGroupService` to use v2 migration pattern with `WorkspaceMigrationValidateBuildAndRunService` - Created utility functions for transforming DTOs to flat entities **Database Migration** - Added migration to make `parentViewFilterGroupId` foreign key deferrable (handles self-referential parent/child insertions) **ViewFilter Integration** - Added `viewFilterGroupId` validation in `FlatViewFilterValidatorService` - Updated `viewFilter` many-to-one relations to include `viewFilterGroup` **Tests** - Added integration tests for successful creation, update, deletion, and destruction - Added failing test cases for non-existent entities and invalid references - Added failing test for `viewFilter` creation with non-existent `viewFilterGroupId` ### Breaking Changes None - existing API contracts are preserved. |
||
|
|
d6acef24d4 |
Fix timeline activities display (#16880)
Fixes https://github.com/twentyhq/twenty/issues/16809 # Introduction Front was expected timeline activities field to be relation and not morph relation Also fixed the new v2 workspace creation that had a bug in the createStandardRelation util ( it's not in production yet so everything's fine ) |