I am seeing an issue where this migrations fails because the
`metadata._typeorm_migrations` table does not exist.
```pgsql
copy _typeorm_migrations from metadata to core
query failed: SELECT * FROM metadata._typeorm_migrations ORDER BY id ASC
error: error: relation "metadata._typeorm_migrations" does not exist
[Nest] 430 - 06/01/2025, 10:22:35 PM ERROR [CopyTypeormMigrationsCommand] Failed to copy migrations: relation "metadata._typeorm_migrations" does not exist
[Nest] 430 - 06/01/2025, 10:22:35 PM ERROR [CopyTypeormMigrationsCommand] undefined
[Nest] 430 - 06/01/2025, 10:22:35 PM ERROR [DatabaseMigrationService] Error running database migrations:
[Nest] 430 - 06/01/2025, 10:22:35 PM ERROR [DatabaseMigrationService] QueryFailedError: relation "metadata._typeorm_migrations" does not exist
[Nest] 430 - 06/01/2025, 10:22:35 PM ERROR [UpgradeCommand] Command failed
[Nest] 430 - 06/01/2025, 10:22:35 PM ERROR [UpgradeCommand] undefined
[Nest] 430 - 06/01/2025, 10:22:35 PM LOG [UpgradeCommand] Command completed!
[Nest] 430 - 06/01/2025, 10:22:35 PM ERROR [QueryFailedError] relation "metadata._typeorm_migrations" does not exist
```
I _think_ this table is not meant to exist anymore - which means that
anyone who is onboarding into the project will run into an issue unless
we handle the case where the table doesn't exist.
We need to handle both the existing case and the non existing case to
support people who _do_ have metadata._typeorm_migrations` to migrate.
Closes https://github.com/twentyhq/core-team-issues/issues/748
In the frame of the work on permissions we
- remove all raw queries possible to use repositories instead
- forbid usage workspaceDataSource.executeRawQueries()
- restrict usage of workspaceDataSource.query() to force developers to
pass on shouldBypassPermissionChecks to use it.
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Creating manual chunk was a bad idea, we should always solve lazy
loading problem at the source instance.
Setting a 4.5MB for the index bundle size, CI will fail if we go above.
There is still a lot of room for optimizations!
- More agressive lazy loading (e.g. xyflow and tiptap are still loaded
in index!)
- Add a prefetch mechanism
- Add stronger CI checks to make sure libraries we've set asides are not
added back
- Fix AllIcons component with does not work as intended (loaded on
initial load)
# Implementation Details
- Added support for 5 operators: `contains`, `containsAny`,
`containsAll`, `matches`, and `fuzzy`
- Works on any field of type `TS_VECTOR`
- Added PostgreSQL `pg_trgm` extension for fuzzy search functionality.
The extension provides the `similarity()` function needed for text
similarity searches.
- Not implemented in GraphQL
## Tradeoffs & Decisions
1. **Fuzzy Search Performance**: Using `pg_trgm` for fuzzy search is
more accurate but slower than simple text matching. We might want to add
a similarity threshold parameter in the future to control the tradeoff
between accuracy and performance.
2. **Operator Naming**: Chose `contains`/`containsAny`/`containsAll` to
be consistent with existing filter operators, though they might be less
intuitive than `search`/`searchAny`/`searchAll`.
## Demo
https://github.com/user-attachments/assets/790fc3ed-a188-4b49-864f-996a37481d99
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This PR fixes a edge case where the user tries to create a non-advanced
filter that already exists in advanced filters, from the table header
drodpown.
This was because the hook that handles the creation was checking for
duplicate filters but without discerning between advanced and
non-advanced, and we want to be able to create non-advanced filters no
matter what we have in advanced filters.
Fixes https://github.com/twentyhq/twenty/issues/12316
Changes the default behavior for settings navigation items to stay
active when navigating to sub-pages.
**Problem:**
- Navigation items like "Data Model" and "Webhooks" were not staying
highlighted when navigating to detail pages
- This was because `matchSubPages` defaulted to requiring exact path
matches
**Solution:**
- Updated logic to make sub-page matching the default behavior (`end:
item.matchSubPages === false`)
- Only "Accounts" explicitly sets `matchSubPages: false` for its custom
sub-item navigation
- Removed redundant `matchSubPages: true` declarations throughout the
codebase
**URL Changes:** -- checked with @Bonapara
- `/settings/workspace` → `/settings/general`
- `/settings/workspace-members` → `/settings/members`
- `/settings/api-keys` → `/settings/apis`
- `/settings/developers/webhooks` → `/settings/webhooks`
before:
https://github.com/user-attachments/assets/56b94a49-9c31-4bb5-9875-ec24f4bc4d1e
after:
https://github.com/user-attachments/assets/38742599-c045-44d1-8020-56f3eacca779
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This PR fixes an edge case where we couldn't apply a filter on the field
metadata item that is used by a kanban.
As this has already been fixed for tables with groups, this PR just uses
the same technique.
Fixes https://github.com/twentyhq/twenty/issues/12311
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Context :
Plan choice [on pricing page on website](https://twenty.com/pricing)
should redirect you the right plan on app /plan-required page (after
sign in), thanks to query parameters and BillingCheckoutSessionState
sync.
With email verification, an other session starts at CTA click in
verification email. Initial BillingCheckoutSessionState is lost and user
can't submit to the plan he choose.
Solution :
Pass a nextPath query parameter in email verification link
To test :
- Modify .env to add IS_BILLING_ENABLED (+ reset db + sync billing) +
IS_EMAIL_VERIFICATION_REQUIRED
- Start test from this page
http://app.localhost:3001/welcome?billingCheckoutSession={%22plan%22:%22ENTERPRISE%22,%22interval%22:%22Year%22,%22requirePaymentMethod%22:true}
- After verification, check you arrive on /plan-required page with
Enterprise plan on a yearly interval (default is Pro/monthly).
closes https://github.com/twentyhq/twenty/issues/12288
This PR fixes an infinite loop that was appearing on IconPicker, since
it was about setting and unsetting the hotkey scope, it wasn't really
noticeable.
The direct cause was using the mouse enter and mouse leave events to set
and unset the hotkey scope, without using a local state to prevent race
condition, so this PR just adds this local state.
Fixes https://github.com/twentyhq/twenty/issues/12344
Backend part of https://github.com/twentyhq/core-team-issues/issues/928
- Add fields to database event settings
- If not set, match all automated triggers with the right event name
- If set, event needs at least one updated field listened to be treated
# Introdution
Reverting introduced bug by
https://github.com/twentyhq/twenty/pull/12082
We need to address the bug that was "fixed" by this in order to refresh
the recordFilters state, will have a look with @lucasbordeau 🙏
Small optimization for faster loading (gaining ~80ms - average time of a
click)
It might seem a little over-engineered but there are a lot of edge cases
and I couldn't find a simpler solution
I also tried to tackle Link Chips but it's more complex so this will be
for another PR
# Introduction
Diff description: ~500 tests and +500 additions
close https://github.com/twentyhq/core-team-issues/issues/731
## What has been done here
In a nutshell on a field metadata type ( `SELECT MULTI_SELECT` ) update,
we will be browsing all `ViewFilters` in a post hook searching for some
referencing related updated `fieldMetadata` select. In order to update
or delete the `viewFilter` depending on the associated mutations.
## How to test:
- Add FieldMetadata `SELECT | MULTI_SELECT` to an existing or a new
`objectMetadata`
- Create a filtered view on created `fieldMetadata` with any options you
would like
- Remove some options ( in the best of the world some that are selected
by the filter ) from the `fieldMetadata` settings page
- Go back to the filtered view, removed or updated options should have
been hydrated in the `displayValue` and the filtered data should make
sense
## All filtered options are deleted edge case
If an update implies that a viewFilter does not have any existing
related options anymore, then we remove the viewFilter
## Testing
```sh
PASS test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts (27 s)
update-one-field-metadata-related-record
SELECT
✓ should delete related view filter if all select field options got deleted (2799 ms)
✓ should update related multi selected options view filter (1244 ms)
✓ should update related solo selected option view filter (1235 ms)
✓ should handle partial deletion of selected options in view filter (1210 ms)
✓ should handle reordering of options while maintaining view filter values (1487 ms)
✓ should handle no changes update of options while maintaining existing view filter values (1174 ms)
✓ should handle adding new options while maintaining existing view filter (1174 ms)
✓ should update display value with options label if less than 3 options are selected (1249 ms)
✓ should throw error if view filter value is not a stringified JSON array (1300 ms)
MULTI_SELECT
✓ should delete related view filter if all select field options got deleted (1127 ms)
✓ should update related multi selected options view filter (1215 ms)
✓ should update related solo selected option view filter (1404 ms)
✓ should handle partial deletion of selected options in view filter (1936 ms)
✓ should handle reordering of options while maintaining view filter values (1261 ms)
✓ should handle no changes update of options while maintaining existing view filter values (1831 ms)
✓ should handle adding new options while maintaining existing view filter (1610 ms)
✓ should update display value with options label if less than 3 options are selected (1889 ms)
✓ should throw error if view filter value is not a stringified JSON array (1365 ms)
Test Suites: 1 passed, 1 total
Tests: 18 passed, 18 total
Snapshots: 18 passed, 18 total
Time: 27.039 s
```
## Out of scope
- We should handle ViewFilter validation when extracting its definition
from the metadata
https://github.com/twentyhq/core-team-issues/issues/1009
## Concerns
- Are we able through the api to update an RATING fieldMetadata ? ( if
yes than that's an issue and we should handle RATING the same way than
for SELECT and MULTI_SELECT )
- It's not possible to group a view from a MULTI_SELECT field
The above points create a double nor a triple "lecture" to the post hook
effect:
- ViewGroup -> only SELECT
- VIewFilter -> only SELECT || MULTI_SELECT
- Rating nothing
I think we should determine the scope of all of that
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
### Primary Changes: Dynamic Driver Configuration
Refactors FileStorageService and EmailSenderService to support dynamic
driver configuration changes at runtime without requiring application
restarts.
**Key Architectural Change**: Instead of conditionally registering
drivers at build time based on configuration, we now **register all
possible drivers eagerly** and select the appropriate one at runtime.
### What Changed:
- **Before**: Modules conditionally registered only the configured
driver (e.g., only S3Driver if STORAGE_TYPE=S3)
- **After**: All drivers (LocalDriver, S3Driver, SmtpDriver,
LoggerDriver) are registered at startup
- **Runtime Selection**: Services dynamically choose and instantiate the
correct driver based on current configuration
### Secondary Fix: Integration Test Log Cleanup
Addresses ConfigStorageService error logs appearing in integration test
output by using injected LoggerService for consistent log handling.
This PR refactors all the dropdown content wrapping mechanism across the
entire app.
It refactors the internals of the `Dropdown` component and introduces a
new generic `DropdownContent` component that is a generic wrapper used
for each dropdown.
## Why this PR ?
Because we’ve been experiencing continuous regressions for months on the
dropdown content width, with weird scrolling behaviors in some and not
in others, and every time a solution was found for a particular set of
dropdowns, it broke another set of dropdowns, which wasn’t noticed
because doing the QA of all dropdowns of the app is very difficult for
fixing an apparently small bug.
## Don’t we already have a `DropdownMenu` component ?
Indeed, this new `DropdownContent` is almost like `DropdownMenu` and
took inspiration from it but `DropdownContent` acts as a generic content
container that sets the width of the whole dropdown, whether we have a
menu or not.
## Why don’t we put it directly in Dropdown internals ?
Because the Dropdown component is using a complex logic with floating-ui
middleware to compute its position and size, and for this logic to work
correctly, it cannot be responsible for the “wanted” width of its
content, because the children components, which the dropdown is not
aware of, can request different widths after the dropdown has been
mounted.
A good example with multiple use cases inside the same dropdown can be
found in `AdvancedFilterDropdownFilterInput`
Thus, it is the responsibility of the content of the dropdown to
determine the width it wants to have.
## What is the difference with DropdownMenuItemsContainer ?
We can have multiple `DropdownMenuItemsContainer` in a dropdown,
alongside other components like `DropdownMenuSeparator` or
`DropdownMenuHeader`, and each of those components behaves differently
regarding to its width, paddings, etc. Therefore it is logical that the
`DropdownMenuItemsContainer` cannot be responsible for the whole
dropdown content width, and trying to do so has been the cause of many
regressions for months.
Now `DropdownMenuItemsContainer` is taking a width of `auto` by default,
which is the best to adapt to a parent which has a defined width.
## How do I set the width of my dropdown now ?
By passing a pixel width to the props `widthInPixels` of
`DropdownContent`, which only accepts numbers to avoid any confusion
with `auto` , `100%` or `160px` and other specific width variables.
The `dropdownWidth` props has been removed from `<Dropdown>` to avoid
any confusion.
Also the `DropdownMenuItemsContainer` is now using `auto` as its default
width to fill the available space inside `DropdownContent` .
It is highly recommended to use the enum `GenericDropdownContentWidt` to
define your width.
## Where to use this new `DropdownContent` component ?
There are two main use cases.
If the dropdown content is defined directly inline in the Dropdown
props, then it is recommended to use it here too.
On the other hand if the dropdown content is abstracted in another
component, it’s recommended to use this new component alongside the
others components like `DropdownMenuItemsContainer`.
A good rule of thumb is to place `DropdownContent` where
`DropdownMenuItemsContainer`, `DropdownMenuSearchInput`, etc. are
placed.
## What if I have a custom width ?
Just define a constant like `ICON_PICKER_DROPDOWN_CONTENT_WIDTH` and use
it with the props `widthInPixels` .
Otherwise there’s a `GenericDropdownContentWidth` enum. The default
value being `GenericDropdownContentWidth.Medium` (or 200px), which most
dropdowns use.
## QA
Component | Comment
-- | --
AttachmentDropdown | Fixed overflowing (thanks to DropdownContent)
RecordIndexActionMenuDropdown |
CommandMenuActionMenuDropdown |
SupportDropdown | Fixed overflowing (thanks to DropdownContent)
MessageThreadSubscribersDropdownButton | Removed because unused
FavoriteFolderNavigationDrawerItemDropdown | Set width at Narrow
FavoriteFolderPicker |
ViewPickerOptionDropdown |
PageFavoriteFolderDropdown | Removed because unused
AdvancedFilterAddFilterRuleSelect |
AdvancedFilterAddFilterRuleSelect |
AdvancedFilterFieldSelectMenu |
AdvancedFilterRecordFilterGroupOptionsDropdown |
AdvancedFilterRecordFilterOperanceSelect | Set width at Narrow
AdvancedFilterLogicalOperatorDropdown | Set width at Narrow
AdvancedFilterRecordFilterOptionsDropdown |
AdvancedFilterRootRecordFilterGroup | Fixed broken horizontal scrolling
behavior
AdvancedFilterSubFieldSelectMenu |
AdvancedFilterDropdownFilterInput |
ObjectFilterDropdownBooleanSelect |
ObjectFilterDropdownCountrySelect | Fixed broken menu items container
ObjectFilterDropdownCurrencySelect | Set width to Large
ObjectFilterDropdownFilterInput |
ObjectFilterDropdownOperandDropdown | Fixed width that was not fixed
ObjectFilterDropdownFilterInput | Fixed width that wasn’t the same for
EditableFilterChip
ObjectFilterDropdownOperandSelect | Refactored
ObjectOptionsDropdownRecordGroupFieldsContent | Added missing separator
ObjectOptionDropdownFieldsContent |
ObjectOptionsDropdownHiddenFieldsContent |
ObjectOptionsDropdownLayoutContent |
ObjectOptionsDropdownLayoutOpenInContent |
ObjectOptionsDropdownMenuContent |
ObjectOptionsDropdownRecordGroupFieldsContent |
ObjectOptionsDropdownRecordGroupsContent |
ObjectOptionsDropdownRecordGroupSortContent |
ObjectOptionsDropdownHiddenRecordGroupsContent | Removed unnecessary
DropdownMenuItemsContainer
RecordBoardColumnHeaderAggregateDropdown | Fixed overflowing (thanks to
DropdownContent)
RecordBoardColumnHeaderAggregateDropdownFieldsContent | Fixed
overflowing (thanks to DropdownContent)
RecordBoardColumnHeaderAggregateDropdownMenuContent | Fixed overflowing
(thanks to DropdownContent)
RecordBoardColumnHeaderAggregateDropdownOptionsContent | Fixed
overflowing (thanks to DropdownContent)
MultiItemFieldInput | Fixed overflowing (thanks to DropdownContent)
MultiItemFieldMenuItem |
MultipleRecordPicker | Fixed overflowing (thanks to DropdownContent)
SingleRecordPicker |
RecordTableColumnAggregateDropdownSubmenuContent |
RecordTableColumnAggregateFooterMenuContent |
RecordTableColumnHeadDropdownMenu | Fixed overflowing (thanks to
DropdownContent)
RecordTableHeaderPlusButtonContent |
MultipleSelectDropdown | Broken width fixed
ObjectSortDropdownButton |
RecordDetailRelationRecordsListItem |
ConfigVariableDatabaseInput |
ConfigVariableOptionsDropdownContent |
SettingsObjectFieldActiveActionDropdown | Fixed overflowing (thanks to
DropdownContent)
SettingsObjectFieldDisabledActionDropdown | Set width at Narrow
SettingsObjectSummaryCard | Removed because unused
SettingsDataModelFieldSelectFormOptionRow |
SettingsDataModelNewFieldBreadcrumbDropdown |
SettingsObjectInactiveMenuDropDown |
SettingsRoleAssignementWorkspaceMemberPickerDropdown |
SettingsRolePermissionObjectLevelObjectPickerDropdownContent |
SettingsSecurityApprovedAccessDomainRowDropdownMenu | Couldn’t test
SettingsSecuritySSORowDropdownMenu | Couldn’t test
SettingsAccountsRowDropdownMenu | Fixed overflowing (thanks to
DropdownContent)
SettingsIntegrationDatabaseConnectionSummaryCard | Couldn’t test
SettingsServerlessFunctionTablEnvironmentVariableTableRow | Deactivated
scope
MatchColumnSelectFieldSelectDropdownContent | Removed now unnecessary
width on DropdownMenuItemsContainer
MatchColumnSelectSubFieldSelectDropdownContent |
SubMatchingSelectInput |
CurrencyPickerDropdownSelect |
IconPicker | Fixed overflowing (thanks to DropdownContent)
PhoneCountryPickerDropdownSelect |
Select | Refactored to drilldown wanted width of content, in this case
it’s intended
ExpandedListDropdown |
ShowPageAddButton | Removed because unused
MultiWorkspaceDropdownDefaultComponent |
MultiWorkspaceDropdownThemesComponent |
MultiWorkspaceDropdownWorkspacesListComponent |
AdvancedFilterDropdownButton |
EditableFilterChip |
EditableFilterDropdownButton |
UpdateViewButtonGroup |
ViewBarFilterDropdown |
ViewBarFilterDropdownFieldSelectMenu |
ViewPickerContentCreateMode |
ViewPickerContentEditMode |
ViewPickerListContent |
WorkflowEditTriggerDatabaseEventForm |
WorkflowVariablesDropdownFieldItems |
WorkflowVariablesDropdownObjectItems |
WorkflowVariablesDropdownWorkflowStepItems |
CommandMenuContextChipGroups |
RecordBoardColumnDropdownMenu |
MultiSelectInput |
SelectInput |
CustomSlashMenu |
DropdownMenu | Removed and replaced by DropdownContent
OverlayContainer and around |
<!-- notionvc: 1e23bdb8-2dda-4f8d-a64d-ecc829a768a2 -->
## Miscellaneous
Side notes :
- The `Select` component is now wrapping the `DropdownContent` because
it computes a dynamic width.
- The advanced filter dropdown has been fixed, it was broken when
resizing the window horizontally, we couldn’t scroll. This specific edge
case was taken into account when refactoring the whole dropdown content
system
- As discussed with Nitin, data-select-disable will probably be removed
entirely, so I let it as is, because right now it is not used by the
refactored d&d selection.
- Duplicate separators under DropdownMenuHeader have been removed.
Fixes : https://github.com/twentyhq/twenty/issues/12327
Fixes : https://github.com/twentyhq/core-team-issues/issues/951
# extracting domain emails
Added new test cases covering weird but valid email formats (plus
addressing, subdomains, international domains, etc.) to identify
potential failures in the current implementation.
Two tests with quoted local parts containing @ symbols or quotes are
marked as skipped since they're expected to fail with the current simple
string splitting approach. They are too exotic IMO, we should throw
errors.
## Next
We will monitor errors related to this and update accordingly later on.
### Note
technically, quotes are possible in RFC see
[here](https://stackoverflow.com/questions/4816424/are-single-quotes-legal-in-the-name-part-of-an-email-address)
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
Various fixes from fast follows
- Sort roles by alphabetical order
- Change some tooltips
- During role creation, role should have all permissions enabled by
default
- Changed Permission icons design and refactored duplicating logic in a
dedicated component
- Changed "Revoked by" design
- Display role icon in default role picker
- Workspace member avatar was missing in role list and member picker
- Set "seeded" member role as editable for new workspaces
- Various css fixes
## Context
- Introduced objectPermissions in currentUserWorkspace which uses role
permissions from cache so we can fetch granular permissions from the API
- Refactored cached role permissions to map permissions with object
metadata id instead of object metadata name singular to be more flexible
New Cache
<img width="574" alt="Screenshot 2025-05-27 at 11 59 06"
src="https://github.com/user-attachments/assets/1a090134-1b8a-4681-a630-29f1472178bd"
/>
GQL
<img width="977" alt="Screenshot 2025-05-27 at 11 58 53"
src="https://github.com/user-attachments/assets/3b9a82b0-6019-4a25-a6e2-a9e0fb4bb8a0"
/>
Next steps: Use the updated API in the FE to fetch granular permissions
and update useHasObjectReadOnlyPermission hook
I encountered a bug where I was missing permissions while calling
searchResolver because the repository from
`twentyORMManager.getRepository` was missing permissions itself.
The repository was returned from the cached repositories map using a
repository key feature the roleId, the rolesVersion and
featureFlagMapVersion.
I was not able to reproduce but this error should not go unnoticed: we
always expect to find objectPermissions for every roleId in the
datasource now.
I was not able to understand what happened for now but I think throwing
the error will help keeping an eye on it
# Gmail OAuth authentication flow issues
### TLDR
This error is not an error and therefore should be treated as a simple
redirect with a snackbar.
### More details
Fixing incomplete OAuth token exchange processes and improving error
handling for empty Gmail inboxes.
The changes include modifications to OAuth guards, to ensure that if a
user clicks "cancel" instead of completing the authentication workflow
if fails
## Before:
Redirection from `/settings/accounts` to `app.twenty.com` with an
`UNAUTHORIZED` error
## After :
<img width="948" alt="Screenshot 2025-05-26 at 18 04 37"
src="https://github.com/user-attachments/assets/62c8721e-c2b3-4e3d-ad0b-e4059dfb7a98"
/>
Fixes https://github.com/twentyhq/twenty/issues/11895
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
For database event triggers, we remove the before / after logic. We go
directly with the properties
<img width="211" alt="Capture d’écran 2025-05-27 à 11 40 36"
src="https://github.com/user-attachments/assets/a05bd3c1-104b-477b-be52-d56846ce7e63"
/>
To achieve this without changing the shape of events, we need to handle
keys using dots, such:
```
'properties.after.name': {
icon: 'IconBuildingSkyscraper',
type: FieldMetadataType.TEXT,
label: 'Name',
value: 'My text',
isLeaf: true,
},
```
This PR:
- adds logic to handle the case where the key has dot included
- adds tests
closes#12309
Fixes input elements becoming unusable due to drag selection preventing
default browser behavior.
**Problem:**
- Input elements couldn't receive focus because `event.preventDefault()`
was called unconditionally
- Removing `preventDefault()` broke click-outside-to-deselect
functionality
**Solution:**
- Only call `preventDefault()` when actually starting drag selection
- Preserves input focus while maintaining drag selection and deselection
behavior
**Changes:**
- Move `event.preventDefault()` inside the `shouldStartSelecting`
condition
- Update test to reflect correct behavior for disabled elements
## 🛠️ What this PR fixes
Fixes#12268
This PR fixes the UI behavior where the "Set as Primary" button was
incorrectly shown for emails or phones that are already marked as
primary. Instead, users now see a bookmark icon indicating the entry as
primary.
## 🎥 Demo
The attached video demonstrates the updated UI where the "Set as
Primary" button is hidden for primary contacts or emails and replaced by
a bookmark icon.
https://github.com/user-attachments/assets/9afcc818-fbb4-4e7c-8fa2-093fdc7d8a26
---------
Co-authored-by: Davinder Kumar <davinder.kumar@intverse.io>
Co-authored-by: Devessier <baptiste@devessier.fr>