## The Issue
When CALENDAR_BOOKING_PAGE_ID env variable is not configured, the
onboarding flow still sets the booking step as pending in the database.
This causes users to get stuck on a broken booking page after
logout/login, as the Cal.com iframe tries to load with an empty calendar
link.
## The Fix
Made the booking step handling idempotent across the stack:
Backend:
- setOnboardingBookOnboardingPending now checks if calendar is actually
configured before setting the step as pending
- getOnboardingStatus auto-cleans invalid booking states when detected
(booking pending but no calendar configured)
- Empty strings in env are now treated as undefined in client config
Frontend:
- Added navigation protection to redirect away from booking pages when
calendar isn't configured
- Existing defensive logic in useSetNextOnboardingStatus already skips
booking when no calendar ID
Result
- New users won't get invalid booking states
- Existing bad data self-heals when users interact with the system
- Backend and frontend stay in sync about when booking should be shown
Fixes the issue Felix reported where users saw a broken booking page in
production.
I think we should keep the old CAL_LINK constant for now - while we
could remove the booking onboarding step entirely, it would break the
plan/pricing modal which uses it as a fallback when no calendar is
configured. Open for discussion! -- May be we dont show the `Book a
Call` button if the env is not set -- but we should keep it as it is if
we want two different behaviors :)
closes https://github.com/twentyhq/core-team-issues/issues/1558
This PR should fix optimistic rendering issues on step updates:
- compute a diff for trigger and steps on mutations
- build an util that applies that diff (built a more robust version from
https://github.com/AsyncBanana/micropatch)
- apply diff in cache
Add a new graphql mutation in the pageLayout resolver to handle page
layout update with tabs and widgets.
Later we will add validation inside the service to check if the page
layout tab structure is correct (for instance check widgets don't
overlap or aren't out of the grid, or to check that they have a correct
configuration ...).
This mutation will be plugged to the save action of the dashboards in
the frontend.
# Introduction
Honestly this implem is a mess, discussing a potential side effect
handler with @weiko before the build and run that would handle each side
effect per entity and operation
Handling:
- [ ] unique index is generated when a field is updated with the
`isUnique`
- [x] an index is generated when a relation is created
- [x] search vector index creation on custom object creation
- [x] renaming a field metadata or an object should re-create all
related indexes which are composed by their namings
- [x] delete object should remove any related indexes
- [x] delete field should update related indexes ( if index ends up
empty it should be removed )
- [ ] on object renaming that contains morph fields -> triggers update
field -> trigger index recompute
- [x] on update name renaming should recompute all related indexes
## TODO
- [x] Integration testing
- [ ] Refactor the index maps cache to be storing a
`idsByObjectMetadataId`
- [x] Refactor deterministic name to use order sorting
- [x] Remove flat index from flat object
## What's next
Will handle morph indexes in a new dedicated PR for the moment will
stick to this
Same for the cache improvement and uniqueness
## Summary
Implements the foundation for dynamic search field configuration from
[issue #1428](https://github.com/twentyhq/core-team-issues/issues/1428).
## Changes
- Add `SearchFieldMetadataEntity` junction table for storing searchable
field configurations
- Add `SearchFieldMetadataService` with core CRUD operations
- Add `SearchFieldMetadataModule` following existing patterns
- Add `IS_DYNAMIC_SEARCH_FIELDS_ENABLED` feature flag (defaults to
`false`)
- Database migration with proper indexes and foreign keys
## Architecture
Uses a junction table where **record existence = field is searchable**:
**Table: `searchFieldMetadata(objectMetadataId, fieldMetadataId,
workspaceId)`**
- Unique constraint on `(objectMetadataId, fieldMetadataId)`
- `ON DELETE CASCADE` on foreign keys
## Testing
- [x] Migration runs successfully
- [x] Table created with correct schema
- [x] Feature flag seeded properly
- [x] Database reset works correctly
## Next Steps
Future PRs will handle:
- Data migration from existing hardcoded search field configs
- Integration with search services
✅ No breaking changes — fully backward compatible.
Fixes https://github.com/twentyhq/twenty/issues/14442
Issues were
1. Table headers always used label metadata identifier (or record text)
as first column, while table body followed viewFields positions
2. Label metadata identifier should always be visible and in first
position for the table views to work as expected, while when updating
the label metadata identifier for an object, no changes were brought to
the viewFields
To fix that
1. In the BE:
- a new logic is implemented to i) update label identifier's viewFields
position and visibility when an object's label identifier is updated to
a new field; ii) add validation on the update of a viewField's position
and visibility to make sure the label identifier's viewField always has
the lowest position + is visible
- a command was added to check all existing views and viewfields
3. In the FE: at first I tried to replicate the logic of the headers
(based on the label identifier rather than the positions) on the body,
but it was too complex and error-prone as in multiple places we are
based on the positions. It also feels more right to have only one source
of truth which is the viewField position. @lucasbordeau if that does not
suit you, we can throw an error if the field with the lowest position is
not the label metadata identifier, as you said the table view will be
very buggy / wont work if the label identifier is not in the first
position. but now it should never be the case thanks to the validation
implemented in the BE
This should be migrated to viewFieldService V2 when relevant @Weiko
@prastoin
## Introduction
https://github.com/twentyhq/twenty/pull/14363
In this PR we're creating builder and runner for index metadata
Removing its previous integration within object builder
## Next:
- implem index impact on field and object metadata api
- Add integration testing coverage add integration test on index
- Implement uniqueness toggle in field metadata api
- add integration test on uniqueness
related to https://github.com/twentyhq/core-team-issues/issues/1344
## ✨ Add accent-insensitive search functionality
### 🎯 Overview
Implements accent-insensitive search across all searchable fields in
Twenty CRM.
Users can now search for "jose" to find "José", "muller" to find
"Müller", "cafe" to find "café", etc.
### 🔍 Problem
Twenty's search functionality was accent-sensitive, requiring users to
type exact accented characters to find records.
This created a poor user experience, especially for international names
and content.
### 💡 Solution
Added PostgreSQL `unaccent` extension with a custom immutable wrapper
function to enable accent-insensitive full-text search across all
searchable field types.
### 📋 Changes Made
**Modified Files:**
- `packages/twenty-server/scripts/setup-db.ts`
-
`packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts`
-
`packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts`
### 🗄️ Database Setup (`setup-db.ts`)
```sql
-- Added unaccent extension
CREATE EXTENSION IF NOT EXISTS "unaccent";
-- Created immutable wrapper function
CREATE OR REPLACE FUNCTION unaccent_immutable(text) RETURNS text AS $$
SELECT public.unaccent($1)
$$ LANGUAGE sql IMMUTABLE;
```
### 🔍 Search Vector Generation
(`get-ts-vector-column-expression.util.ts`)
Applied `public.unaccent_immutable()` to all searchable field types:
- TEXT fields (job titles, names, etc.)
- FULL_NAME fields (first/last names)
- EMAILS fields (both email address and domain)
- ADDRESS fields
- LINKS fields
- RICH_TEXT and RICH_TEXT_V2 fields
### 🔎 Query Processing (`compute-where-condition-parts.ts`)
Enhanced search queries to use `public.unaccent_immutable()` for both:
- Full-text search (`@@` operator with `to_tsquery`)
- Pattern matching (`ILIKE` operator)
### 🧠 Technical Rationale: Why the Wrapper Function?
**The Challenge:**
PostgreSQL's built-in `unaccent()` is marked as **STABLE**, but
`GENERATED ALWAYS AS` expressions (used for search vector columns)
require **IMMUTABLE** functions.
**The Solution:**
Created an IMMUTABLE wrapper function that calls the underlying
`unaccent()` function:
- ✅ Satisfies PostgreSQL's immutability requirements for generated
columns
- ✅ Maintains the exact same functionality as the original `unaccent()`
- ✅ Uses fully qualified `public.unaccent_immutable()` to ensure
function resolution from workspace schemas
**Alternative Approaches Considered:**
- ❌ Modifying `search_path`: would affect workspace isolation
- ❌ Computing unaccent at query time: would hurt performance
- ❌ Using triggers: would complicate data consistency
### 🎯 Impact
For **Person** records, accent-insensitive search now works on:
- Name (first/last name): `"jose garcia"` finds `"José García"`
- Email: `"jose@cafe.com"` finds `"josé@café.com"`
- Job Title: `"manager"` finds `"Managér"` or `"Gerente de Café"`
Applies to all searchable standard objects:
- Companies, People, Opportunities, Notes, Tasks, etc.
- Any custom fields of searchable types (TEXT, EMAILS, etc.)
### ✅ Testing
- Database reset completes successfully
- Workspace seeding works without errors
- Search vectors generate with unaccent functionality
- All searchable field types properly handle accented characters
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
# Introduction
Initially this PR was about introducing integration coverage for view
field v2 tests
But feature itself wasn't finished, so ended up in a TDD style
devlopment
## The new orchestrator
Orchestrator will organize each flat entity diffing inferring either
validation errors or actions sequentially.
A new builder has been introduced when extended requires to define flat
entity validation utils and action generation for `delete` `update`
`create` operations. It's highly typed using generics
## View field integration testing
Introduce coverage on failing basic tests cases for view field
operations, successfull create is also covered. We could still add more
coverage, will be done later in following PRs
## Remaining tasks for upcoming PRs:
- [ ] rename builder methods names to add generate action in their names
- [x] refactor view validation to handle field and view uniqueness index
combination
- [ ] implem strict update validator on view and view field
- [ ] dynamic cache invalidation post run
- [ ] Add coverage to successful and failing view field operations
- [ ] error formatting summary computation
- [ ] extract types out of the workspace entity builder
## Some vision
Currently flat field metadata maps is nested in the flat object metadata
maps, making its build nested too. ( not extending the new generic
builder )
We will refactor this part for both to be stored extra flat such as the
other existing flat entities.
It will require a small refactor to the generic builder that will not
only have impact on the currently built flat entity optimistic cache but
to an other one ( for example when building a create object action it
will also have an impact flat field maps )
Fixes https://github.com/twentyhq/twenty/issues/14445
## What
In the current `MenuItemWithOptionDropdown` component (which is a
MenuItem with the 3 dots dropdown), we used to have a position: static
on the 3 dots hovered.
This enabled the dropdown to not move when we scroll the MenuItems.
However, this is not playing well with the dropdown autoplacement.
I'm removing it as I think the right solution would actually to prevent
the scroll when the dropdown is open but this is non straight forward
and not really a big issue. I'm fine with the dropdown being "fixed" to
the scrollable content, it also makes sense
Before:
https://github.com/user-attachments/assets/8efec8fa-430a-408e-b549-fd7433b7a38d
After:
https://github.com/user-attachments/assets/b8ee4375-0944-4008-9ab6-f7f9f1d67e9c
## Testing
I was considering adding a story here but this is hard to test: hover +
scroll behavior are not well supported, I don't think it worth the
investment especially as I think the vision is to block the scroll
This componenent is used in ViewPicker and MultiItemsInput (ex.
PhonesFieldInput). I have check that both were still working well
Until we tackle [Implement relations depth 2 for rest
api](https://github.com/twentyhq/twenty/issues/14452), let's remove the
depth 2 query parameter which is not working (times out all the time).
Following https://github.com/twentyhq/twenty/pull/14306 , working on the
deprecation of objectRecordsPermissions + renaming of objectPermissions
-> objectsPermissions
In this PR
- Removal of objectRecordsPermissions (was not used in the FE any
longer)
- Addition of objectsPermissions, same as objectPermission but renamed.
objectPermission is no longer used in the FE.
Next step
- Remove unused objectPermissions
## Introduction
### Twenty-sever
Standardizing resolver input and transpilation models + return type on
destroy and delete
~~Finally~~ Did plug everything under a feature flag and add coverage
Next will do same for the view resolver and service v2
### Twenty-front
Refactored view field service in order to use codegenerated strictly
typed mutations and adapt to new api contract
## Context
Adding a new service that provides an abstract caching system for
FlatEntityMaps.
This takes care of cache invalidation and storing local and remote cache
for the map with retrieval after a comparison with map hash between
local and remote (redis).
## Implementation
Remote (redis) keys
- Flat map data:
`engine:workspace:flat-maps:{flatMapKey}:{workspaceId}:flat-map`
- Content hash:
`engine:workspace:flat-maps:{flatMapKey}:{workspaceId}:hash`
**Local Cache Hit**: If local hash matches Redis hash, return local data
**Remote Cache Hit**: If Redis has data with different hash, update
local cache
**Remote Cache Miss**: Recompute from database, store in Remote and
locally
**Invalidation**: Remove from Remote, triggering recomputation on next
access
## Usage
```typescript
@WorkspaceFlatMapCache('view') // redis key
export class WorkspaceFlatViewMapCacheService extends WorkspaceFlatMapCacheService<FlatViewMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super(cacheStorageService);
}
// only method to implement
public async computeFlatMap(workspaceId: string): Promise<FlatViewMaps> {
const views = await this.viewRepository.find({
where: { workspaceId },
relations: ['viewFields'],
select: { viewFields: { id: true } },
});
return generateFlatViewMaps(views);
}
}
```
```typescript
// 2 public methods, getExistingOrRecomputeFlatMaps to fetch the map and invalidateCache after a mutation
await this.workspaceFlatViewMapCacheService.invalidateCache(
viewData.workspaceId,
);
const flatViewMaps =
await this.workspaceFlatViewMapCacheService.getExistingOrRecomputeFlatMaps(
workspaceId,
);
```
## Multi-Pod Synchronization
- Each pod maintains local cache for performance
- SHA256 hash of flatMap content used for version comparison
- `invalidateCache()` reset Redis data, forcing other pods to refresh
when calling getExistingOrRecomputeFlatMaps
---
<img width="1072" height="325" alt="Screenshot 2025-09-11 at 15 04 48"
src="https://github.com/user-attachments/assets/ed6ce82c-db35-4a1b-8a4e-247b694e2ddf"
/>
<img width="1030" height="328" alt="Screenshot 2025-09-11 at 15 04 40"
src="https://github.com/user-attachments/assets/43a15789-7f27-4104-a9bb-bef19908a5cd"
/>
Next step: Implement a locking mechanism by reusing existing WithLock
decorator