Compare commits

...
Author SHA1 Message Date
prastoin 891d6a375c fix(server): set AddIsInitialToUpgradeMigration1775909335324 name for upgrade backfill 2026-04-17 08:35:45 +02:00
Etienne 1c0a7029c3 Fix slow db query issue (#19770)
https://github.com/twentyhq/twenty/pull/19586#discussion_r3074136617
2026-04-16 17:57:15 +02:00
Charles BochetandEtienne 613be773cb fix: replace slow deep-equal with fastDeepEqual to resolve CPU bottleneck (#19771)
## Summary

- Replaced the `deep-equal` npm package with the existing
`fastDeepEqual` from `twenty-shared/utils` across 5 files in the server
and shared packages
- `deep-equal` was causing severe CPU overhead in the record update hot
path (`executeMany` → `formatTwentyOrmEventToDatabaseBatchEvent` →
`objectRecordChangedValues` → `deepEqual`, called **per field per
record**)
- `fastDeepEqual` is ~100x faster for plain JSON database records since
it skips unnecessary prototype chain inspection and edge-case handling
- Removed the now-unnecessary `LARGE_JSON_FIELDS` branching in
`objectRecordChangedValues` since all fields now use the fast
implementation
2026-04-16 17:56:56 +02:00
prastoin 2dd1fffe02 fix(server): cherry-pick breaking changes 2026-04-15 17:55:17 +02:00
Paul Rastoinandprastoin e13c9ef46c [Upgrade] Fix workspace creation cursor (#19701)
The upgrade migration system required new workspaces to always start
from a workspace command, which was too rigid. When the system was
mid-upgrade within an instance command (IC) segment, workspace creation
would fail or produce inconsistent state.

Instance commands now write upgrade migration rows for **all
active/suspended workspaces** alongside the global row. This means every
workspace has a complete migration history, including instance command
records.

- `InstanceCommandRunnerService` reloads `activeOrSuspendedWorkspaceIds`
immediately before writing records (both success and failure paths) to
mitigate race conditions with concurrent workspace creation.
- `recordUpgradeMigration` in `UpgradeMigrationService` accepts a
discriminated union over `status`, handles `error: unknown` formatting
internally, and writes global + workspace rows in batch.

`getInitialCursorForNewWorkspace` now accepts the last **attempted**
(not just completed) instance command with its status:

- If the IC is `completed` and the next step is a workspace segment →
cursor is set to the last WC of that segment (existing behavior).
- If the IC is `failed` or not the last of its segment → cursor is set
to that IC itself, preserving its status.

This allows workspaces to be created at any point during the upgrade
lifecycle, including mid-IC-segment and after IC failure.

`validateWorkspaceCursorsAreInWorkspaceSegment` accepts workspaces whose
cursor is:
1. Within the current workspace segment, OR
2. At the immediately preceding instance command with `completed` status
(handles the `-w` single-workspace upgrade scenario).

Workspaces with cursors in a previous segment, ahead of the current
segment, or at a preceding IC with `failed` status are rejected.

created empty workspaces to allow testing upgrade with several active
workspaces
2026-04-15 17:51:35 +02:00
prastoin 438d502988 fid(server): workspace activation set workspace.version 2026-04-14 16:46:56 +02:00
edc47bd458 i18n - docs translations (#19677)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 12:45:12 +02:00
WeikoandGitHub 47bdcb11d8 Move is active to fe (#19649)
## Context
Moving isActive filtering to the frontend for page layout tabs and
widgets, hiding inactive entities from the UI while keeping them in
state for future reactivation

Next we will implement deactivated standard tab re-activation during tab
creation (cc @Devessier)
<img width="234" height="303" alt="📋 Menu (Slots)"
src="https://github.com/user-attachments/assets/17a25ac6-55e2-4778-b7f0-e7554ed69704"
/>
2026-04-14 10:18:09 +00:00
Baptiste DevessierandGitHub b817bdca02 Rpl various fixes (#19668) 2026-04-14 09:46:15 +00:00
EtienneandGitHub 9c07ecd363 Fix view filter/sort deletion (#19567)
fixes https://github.com/twentyhq/twenty/issues/19543

+ bonus bug : when deleting an advanced filter, it triggers a destroy
which cascade-deletes associated view filters. Then, view filters
deletion throws.
2026-04-14 09:43:52 +00:00
Raphaël BosiandGitHub eb13378760 Fix Quick Lead command menu item not appearing (#19635)
- Refactored prefillWorkflowCommandMenuItems and
prefillFrontComponentCommandMenuItems to use
validateBuildAndRunWorkspaceMigration instead of raw TypeORM
createQueryBuilder inserts
- This ensures the flat entity cache is properly updated when seeding
command menu items, fixing the Quick Lead item not appearing after
workspace creation
- Moved command menu item prefill calls outside the transaction since
they now go through the migration pipeline
2026-04-14 09:30:31 +00:00
Paul RastoinandGitHub 3e699c4458 Fix upgrade commands discovery outside of cli (#19671)
# Introduction
We were allowing the sequence to be empty in the worker context that was
facing an edge case importing the UpgradeModule through the
WorkspaceModule god module, no commands were discovered and it was
throwing as the sequence must have at least one workspace commands to
allow a workspace creation

Though the issue was also applicable to the twenty-server `AppModule`
too that was not discovering any commands

## Integration tests were passing
The integration test were importing the `CommandModule` at the nest
testing app creating leading to asymmetric testing context
It was a requirement for a legacy commands import and global assignation

## Fix
The `UpgradeModule` now import both `WorkspaceCommandsProviderModule`
and `InstanceCommandProviderModule` which ships the commands directly in
the module
We could consider moving the commands into the `engine/upgrade` folder

## Concern
Bootstrap could become more and more long to load at both server and
worker start
When this becomes a problem we will have to only import the latest
workspace command or whatever
For the moment this is not worth it the risk to import not the latest
workspace command
2026-04-14 09:20:33 +00:00
EtienneandGitHub f738961127 Add gql operationName metadata in sentry (#19564) 2026-04-14 08:55:54 +00:00
49aac04b84 fix: edit button not coming up on avatar right after image upload (#19596)
## Summary

After uploading an image/file to the empty avatar field in the person
tab, the edit icon next to the field would not appear until the browser
was refreshed or another field was clicked.

### Root cause

- When user clicks over the avatar field,
`recordFieldListCellEditModePosition` is set to `globalIndex`
- That is fine when a avatar already exists. But when there is no avatar
already set, the native file picker is opened with no `onClose` handler
attached.
- So after the file upload is completed,
`recordFieldListCellEditModePosition` is never reset to null.
- `FieldsWidgetCellEditModePortal` stays anchored to the avatar file
element
- When the user hovers over the same field again, its hover portal tries
to compete to anchor for the same element
- So, `RecordInlineCellDisplayMode ` (the edit button) doesn't render

### Fix

- Pass the `onClose` function through `openFieldInput` to
`openFilesFieldInput`
- `onClose` resets `recordFieldListCellEditModePosition` back to null,
when the upload completes.

## Before


https://github.com/user-attachments/assets/ac9318e9-5471-434c-8af3-5c20d0112460

## After


https://github.com/user-attachments/assets/0d064a7f-95ad-4b92-a9ee-d9570f360972

Fixes #19595

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-14 08:54:14 +00:00
40c6c63bf5 i18n - docs translations (#19672)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 10:54:27 +02:00
d583984bf0 fix(api-key): batch role resolution with DataLoader to fix N+1 (#19590)
## Summary

The `role` @ResolveField on `ApiKeyResolver` calls `getRolesByApiKeys`
with a single-element array per API key. When a query returns N API
keys, this produces N separate DB queries to resolve their roles.

This adds an `apiKeyRoleLoader` to the existing DataLoader
infrastructure. All API key IDs in a single GraphQL request are
collected and resolved in one batched query.

- Before: N queries (one per API key)
- After: 1 query (batched via DataLoader)

## Changes

- `dataloader.service.ts` - new `createApiKeyRoleLoader` method,
delegates to `ApiKeyRoleService.getRolesByApiKeys`
- `dataloader.interface.ts` - `apiKeyRoleLoader` added to `IDataloaders`
- `dataloader.module.ts` - import `ApiKeyModule` so `ApiKeyRoleService`
is available
- `api-key.resolver.ts` - `role()` now uses
`context.loaders.apiKeyRoleLoader.load()` instead of calling the service
directly

## Test plan

- [ ] Verify `apiKeys { id role { label } }` query returns the same
results as before
- [ ] Confirm only 1 role_target query fires regardless of how many API
keys are returned

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-14 08:43:31 +00:00
Paul RastoinandGitHub 714f149b0c Move backfill page layout to 1.23 (#19670) 2026-04-14 08:39:24 +00:00
martmullandGitHub 194f0963dc Remove 'twenty-app' keyword by default (#19669)
as title
2026-04-14 08:26:45 +00:00
5fa3094800 test: fix failing useColorScheme test and remove FIXME (#19593)
Summary
This PR fixes a bug in the `useColorScheme` test suite and removes a
lingering `FIXME` comment where the color scheme was unexpectedly
unsetting during state updates.

Root Cause
Previously, the Jotai state was being initialized *inside* the
`renderHook` callback using `useSetAtomState`. When the `setColorScheme`
function was called, it triggered a hook re-render, which caused the
callback to execute again and overwrite the new state with the hardcoded
`'System'` initial state.

The Fix
- Removed the state initialization from inside the render cycle.
- Bootstrapped the state on a fresh store using `resetJotaiStore()` and
`store.set()` *before* rendering the hook.
- Updated the mock `workspaceMember` to correctly use the
`CurrentWorkspaceMember` type.
- Removed the `FIXME` comment and successfully asserted that the color
scheme updates to `'Dark'`.

Testing
Ran tests locally to confirm the fix works as expected:
`corepack yarn jest --config packages/twenty-front/jest.config.mjs
--testPathPattern=useColorScheme.test.tsx`

---------

Co-authored-by: Srabani Ghorai <subhojit04ghorai@gmail.com>
2026-04-14 06:40:40 +00:00
87f8e5ca19 few website updates (#19663)
## Summary
- refresh pricing page content, plan cards, CTA styling, and Salesforce
comparison visuals
- update partner and hero/testimonial visuals, including pulled
carousel-compatible partner testimonial data
- improve halftone export and illustration mounting flows, plus related
button and hydration fixes
- add updated website illustration and pricing assets

## Testing
- Not run (not requested)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-14 06:23:20 +00:00
219 changed files with 6971 additions and 7776 deletions
@@ -7,9 +7,7 @@
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [
"twenty-app"
],
"keywords": [],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
@@ -583,6 +583,7 @@ type ViewField {
workspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
@@ -691,6 +692,7 @@ type ViewFieldGroup {
workspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
viewFields: [ViewField!]!
isOverridden: Boolean! @deprecated(reason: "isOverridden is deprecated")
@@ -899,6 +901,7 @@ type PageLayoutWidget {
conditionalAvailabilityExpression: String
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
@@ -1234,6 +1237,7 @@ type PageLayoutTab {
layoutMode: PageLayoutTabLayoutMode
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
@@ -2193,30 +2197,18 @@ type DeletedWorkspaceMember {
userWorkspaceId: UUID
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
type Relation {
type: RelationType!
sourceObjectMetadata: Object!
targetObjectMetadata: Object!
sourceFieldMetadata: Field!
targetFieldMetadata: Field!
}
enum BillingEntitlementKey {
SSO
CUSTOM_DOMAIN
RLS
AUDIT_LOGS
}
type DomainRecord {
validationType: String!
type: String!
status: String!
key: String!
value: String!
}
type DomainValidRecords {
id: UUID!
domain: String!
records: [DomainRecord!]!
"""Relation type"""
enum RelationType {
ONE_TO_MANY
MANY_TO_ONE
}
type IndexEdge {
@@ -2318,25 +2310,6 @@ type ObjectFieldsConnection {
edges: [FieldEdge!]!
}
type UpsertRowLevelPermissionPredicatesResult {
predicates: [RowLevelPermissionPredicate!]!
predicateGroups: [RowLevelPermissionPredicateGroup!]!
}
type Relation {
type: RelationType!
sourceObjectMetadata: Object!
targetObjectMetadata: Object!
sourceFieldMetadata: Field!
targetFieldMetadata: Field!
}
"""Relation type"""
enum RelationType {
ONE_TO_MANY
MANY_TO_ONE
}
type FieldConnection {
"""Paging information"""
pageInfo: PageInfo!
@@ -2345,6 +2318,37 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
}
enum BillingEntitlementKey {
SSO
CUSTOM_DOMAIN
RLS
AUDIT_LOGS
}
type DomainRecord {
validationType: String!
type: String!
status: String!
key: String!
value: String!
}
type DomainValidRecords {
id: UUID!
domain: String!
records: [DomainRecord!]!
}
type UpsertRowLevelPermissionPredicatesResult {
predicates: [RowLevelPermissionPredicate!]!
predicateGroups: [RowLevelPermissionPredicateGroup!]!
}
type AuthToken {
token: String!
expiresAt: DateTime!
@@ -415,6 +415,7 @@ export interface ViewField {
workspaceId: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
isActive: Scalars['Boolean']
deletedAt?: Scalars['DateTime']
/** @deprecated isOverridden is deprecated */
isOverridden?: Scalars['Boolean']
@@ -492,6 +493,7 @@ export interface ViewFieldGroup {
workspaceId: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
isActive: Scalars['Boolean']
deletedAt?: Scalars['DateTime']
viewFields: ViewField[]
/** @deprecated isOverridden is deprecated */
@@ -674,6 +676,7 @@ export interface PageLayoutWidget {
conditionalAvailabilityExpression?: Scalars['String']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
isActive: Scalars['Boolean']
deletedAt?: Scalars['DateTime']
/** @deprecated isOverridden is deprecated */
isOverridden?: Scalars['Boolean']
@@ -959,6 +962,7 @@ export interface PageLayoutTab {
layoutMode?: PageLayoutTabLayoutMode
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
isActive: Scalars['Boolean']
deletedAt?: Scalars['DateTime']
/** @deprecated isOverridden is deprecated */
isOverridden?: Scalars['Boolean']
@@ -1879,29 +1883,18 @@ export interface DeletedWorkspaceMember {
__typename: 'DeletedWorkspaceMember'
}
export interface BillingEntitlement {
key: BillingEntitlementKey
value: Scalars['Boolean']
__typename: 'BillingEntitlement'
export interface Relation {
type: RelationType
sourceObjectMetadata: Object
targetObjectMetadata: Object
sourceFieldMetadata: Field
targetFieldMetadata: Field
__typename: 'Relation'
}
export type BillingEntitlementKey = 'SSO' | 'CUSTOM_DOMAIN' | 'RLS' | 'AUDIT_LOGS'
export interface DomainRecord {
validationType: Scalars['String']
type: Scalars['String']
status: Scalars['String']
key: Scalars['String']
value: Scalars['String']
__typename: 'DomainRecord'
}
export interface DomainValidRecords {
id: Scalars['UUID']
domain: Scalars['String']
records: DomainRecord[]
__typename: 'DomainValidRecords'
}
/** Relation type */
export type RelationType = 'ONE_TO_MANY' | 'MANY_TO_ONE'
export interface IndexEdge {
/** The node containing the Index */
@@ -2001,25 +1994,6 @@ export interface ObjectFieldsConnection {
__typename: 'ObjectFieldsConnection'
}
export interface UpsertRowLevelPermissionPredicatesResult {
predicates: RowLevelPermissionPredicate[]
predicateGroups: RowLevelPermissionPredicateGroup[]
__typename: 'UpsertRowLevelPermissionPredicatesResult'
}
export interface Relation {
type: RelationType
sourceObjectMetadata: Object
targetObjectMetadata: Object
sourceFieldMetadata: Field
targetFieldMetadata: Field
__typename: 'Relation'
}
/** Relation type */
export type RelationType = 'ONE_TO_MANY' | 'MANY_TO_ONE'
export interface FieldConnection {
/** Paging information */
pageInfo: PageInfo
@@ -2028,6 +2002,36 @@ export interface FieldConnection {
__typename: 'FieldConnection'
}
export interface BillingEntitlement {
key: BillingEntitlementKey
value: Scalars['Boolean']
__typename: 'BillingEntitlement'
}
export type BillingEntitlementKey = 'SSO' | 'CUSTOM_DOMAIN' | 'RLS' | 'AUDIT_LOGS'
export interface DomainRecord {
validationType: Scalars['String']
type: Scalars['String']
status: Scalars['String']
key: Scalars['String']
value: Scalars['String']
__typename: 'DomainRecord'
}
export interface DomainValidRecords {
id: Scalars['UUID']
domain: Scalars['String']
records: DomainRecord[]
__typename: 'DomainValidRecords'
}
export interface UpsertRowLevelPermissionPredicatesResult {
predicates: RowLevelPermissionPredicate[]
predicateGroups: RowLevelPermissionPredicateGroup[]
__typename: 'UpsertRowLevelPermissionPredicatesResult'
}
export interface AuthToken {
token: Scalars['String']
expiresAt: Scalars['DateTime']
@@ -3631,6 +3635,7 @@ export interface ViewFieldGenqlSelection{
workspaceId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
isActive?: boolean | number
deletedAt?: boolean | number
/** @deprecated isOverridden is deprecated */
isOverridden?: boolean | number
@@ -3705,6 +3710,7 @@ export interface ViewFieldGroupGenqlSelection{
workspaceId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
isActive?: boolean | number
deletedAt?: boolean | number
viewFields?: ViewFieldGenqlSelection
/** @deprecated isOverridden is deprecated */
@@ -3879,6 +3885,7 @@ export interface PageLayoutWidgetGenqlSelection{
conditionalAvailabilityExpression?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
isActive?: boolean | number
deletedAt?: boolean | number
/** @deprecated isOverridden is deprecated */
isOverridden?: boolean | number
@@ -4191,6 +4198,7 @@ export interface PageLayoutTabGenqlSelection{
layoutMode?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
isActive?: boolean | number
deletedAt?: boolean | number
/** @deprecated isOverridden is deprecated */
isOverridden?: boolean | number
@@ -5149,27 +5157,12 @@ export interface DeletedWorkspaceMemberGenqlSelection{
__scalar?: boolean | number
}
export interface BillingEntitlementGenqlSelection{
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainRecordGenqlSelection{
validationType?: boolean | number
export interface RelationGenqlSelection{
type?: boolean | number
status?: boolean | number
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainValidRecordsGenqlSelection{
id?: boolean | number
domain?: boolean | number
records?: DomainRecordGenqlSelection
sourceObjectMetadata?: ObjectGenqlSelection
targetObjectMetadata?: ObjectGenqlSelection
sourceFieldMetadata?: FieldGenqlSelection
targetFieldMetadata?: FieldGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -5284,23 +5277,6 @@ export interface ObjectFieldsConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface UpsertRowLevelPermissionPredicatesResultGenqlSelection{
predicates?: RowLevelPermissionPredicateGenqlSelection
predicateGroups?: RowLevelPermissionPredicateGroupGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface RelationGenqlSelection{
type?: boolean | number
sourceObjectMetadata?: ObjectGenqlSelection
targetObjectMetadata?: ObjectGenqlSelection
sourceFieldMetadata?: FieldGenqlSelection
targetFieldMetadata?: FieldGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FieldConnectionGenqlSelection{
/** Paging information */
pageInfo?: PageInfoGenqlSelection
@@ -5310,6 +5286,38 @@ export interface FieldConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface BillingEntitlementGenqlSelection{
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainRecordGenqlSelection{
validationType?: boolean | number
type?: boolean | number
status?: boolean | number
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainValidRecordsGenqlSelection{
id?: boolean | number
domain?: boolean | number
records?: DomainRecordGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface UpsertRowLevelPermissionPredicatesResultGenqlSelection{
predicates?: RowLevelPermissionPredicateGenqlSelection
predicateGroups?: RowLevelPermissionPredicateGroupGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AuthTokenGenqlSelection{
token?: boolean | number
expiresAt?: boolean | number
@@ -8232,26 +8240,10 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const BillingEntitlement_possibleTypes: string[] = ['BillingEntitlement']
export const isBillingEntitlement = (obj?: { __typename?: any } | null): obj is BillingEntitlement => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingEntitlement"')
return BillingEntitlement_possibleTypes.includes(obj.__typename)
}
const DomainRecord_possibleTypes: string[] = ['DomainRecord']
export const isDomainRecord = (obj?: { __typename?: any } | null): obj is DomainRecord => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainRecord"')
return DomainRecord_possibleTypes.includes(obj.__typename)
}
const DomainValidRecords_possibleTypes: string[] = ['DomainValidRecords']
export const isDomainValidRecords = (obj?: { __typename?: any } | null): obj is DomainValidRecords => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainValidRecords"')
return DomainValidRecords_possibleTypes.includes(obj.__typename)
const Relation_possibleTypes: string[] = ['Relation']
export const isRelation = (obj?: { __typename?: any } | null): obj is Relation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRelation"')
return Relation_possibleTypes.includes(obj.__typename)
}
@@ -8352,22 +8344,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const UpsertRowLevelPermissionPredicatesResult_possibleTypes: string[] = ['UpsertRowLevelPermissionPredicatesResult']
export const isUpsertRowLevelPermissionPredicatesResult = (obj?: { __typename?: any } | null): obj is UpsertRowLevelPermissionPredicatesResult => {
if (!obj?.__typename) throw new Error('__typename is missing in "isUpsertRowLevelPermissionPredicatesResult"')
return UpsertRowLevelPermissionPredicatesResult_possibleTypes.includes(obj.__typename)
}
const Relation_possibleTypes: string[] = ['Relation']
export const isRelation = (obj?: { __typename?: any } | null): obj is Relation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRelation"')
return Relation_possibleTypes.includes(obj.__typename)
}
const FieldConnection_possibleTypes: string[] = ['FieldConnection']
export const isFieldConnection = (obj?: { __typename?: any } | null): obj is FieldConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFieldConnection"')
@@ -8376,6 +8352,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const BillingEntitlement_possibleTypes: string[] = ['BillingEntitlement']
export const isBillingEntitlement = (obj?: { __typename?: any } | null): obj is BillingEntitlement => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingEntitlement"')
return BillingEntitlement_possibleTypes.includes(obj.__typename)
}
const DomainRecord_possibleTypes: string[] = ['DomainRecord']
export const isDomainRecord = (obj?: { __typename?: any } | null): obj is DomainRecord => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainRecord"')
return DomainRecord_possibleTypes.includes(obj.__typename)
}
const DomainValidRecords_possibleTypes: string[] = ['DomainValidRecords']
export const isDomainValidRecords = (obj?: { __typename?: any } | null): obj is DomainValidRecords => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainValidRecords"')
return DomainValidRecords_possibleTypes.includes(obj.__typename)
}
const UpsertRowLevelPermissionPredicatesResult_possibleTypes: string[] = ['UpsertRowLevelPermissionPredicatesResult']
export const isUpsertRowLevelPermissionPredicatesResult = (obj?: { __typename?: any } | null): obj is UpsertRowLevelPermissionPredicatesResult => {
if (!obj?.__typename) throw new Error('__typename is missing in "isUpsertRowLevelPermissionPredicatesResult"')
return UpsertRowLevelPermissionPredicatesResult_possibleTypes.includes(obj.__typename)
}
const AuthToken_possibleTypes: string[] = ['AuthToken']
export const isAuthToken = (obj?: { __typename?: any } | null): obj is AuthToken => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAuthToken"')
@@ -9576,6 +9584,11 @@ export const enumQueueMetricsTimeRange = {
OneHour: 'OneHour' as const
}
export const enumRelationType = {
ONE_TO_MANY: 'ONE_TO_MANY' as const,
MANY_TO_ONE: 'MANY_TO_ONE' as const
}
export const enumBillingEntitlementKey = {
SSO: 'SSO' as const,
CUSTOM_DOMAIN: 'CUSTOM_DOMAIN' as const,
@@ -9583,11 +9596,6 @@ export const enumBillingEntitlementKey = {
AUDIT_LOGS: 'AUDIT_LOGS' as const
}
export const enumRelationType = {
ONE_TO_MANY: 'ONE_TO_MANY' as const,
MANY_TO_ONE: 'MANY_TO_ONE' as const
}
export const enumEmailingDomainDriver = {
AWS_SES: 'AWS_SES' as const
}
@@ -63,7 +63,7 @@ export default {
209,
221,
238,
255,
253,
292,
293,
303,
@@ -789,10 +789,10 @@ export default {
3
],
"relation": [
254
237
],
"morphRelations": [
254
237
],
"object": [
46
@@ -851,7 +851,7 @@ export default {
36
],
"objectMetadata": [
247,
245,
{
"paging": [
39,
@@ -864,7 +864,7 @@ export default {
}
],
"indexFieldMetadatas": [
245,
243,
{
"paging": [
39,
@@ -1109,7 +1109,7 @@ export default {
37
],
"fields": [
252,
250,
{
"paging": [
39,
@@ -1122,7 +1122,7 @@ export default {
}
],
"indexMetadatas": [
250,
248,
{
"paging": [
39,
@@ -1283,6 +1283,9 @@ export default {
"updatedAt": [
4
],
"isActive": [
6
],
"deletedAt": [
4
],
@@ -1456,6 +1459,9 @@ export default {
"updatedAt": [
4
],
"isActive": [
6
],
"deletedAt": [
4
],
@@ -1703,7 +1709,7 @@ export default {
131
],
"billingEntitlements": [
237
252
],
"hasValidEnterpriseKey": [
6
@@ -1903,6 +1909,9 @@ export default {
"updatedAt": [
4
],
"isActive": [
6
],
"deletedAt": [
4
],
@@ -2561,6 +2570,9 @@ export default {
"updatedAt": [
4
],
"isActive": [
6
],
"deletedAt": [
4
],
@@ -4419,9 +4431,179 @@ export default {
1
]
},
"Relation": {
"type": [
238
],
"sourceObjectMetadata": [
46
],
"targetObjectMetadata": [
46
],
"sourceFieldMetadata": [
34
],
"targetFieldMetadata": [
34
],
"__typename": [
1
]
},
"RelationType": {},
"IndexEdge": {
"node": [
37
],
"cursor": [
40
],
"__typename": [
1
]
},
"PageInfo": {
"hasNextPage": [
6
],
"hasPreviousPage": [
6
],
"startCursor": [
40
],
"endCursor": [
40
],
"__typename": [
1
]
},
"IndexConnection": {
"pageInfo": [
240
],
"edges": [
239
],
"__typename": [
1
]
},
"IndexFieldEdge": {
"node": [
36
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexIndexFieldMetadatasConnection": {
"pageInfo": [
240
],
"edges": [
242
],
"__typename": [
1
]
},
"ObjectEdge": {
"node": [
46
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexObjectMetadataConnection": {
"pageInfo": [
240
],
"edges": [
244
],
"__typename": [
1
]
},
"ObjectRecordCount": {
"objectNamePlural": [
1
],
"totalCount": [
21
],
"__typename": [
1
]
},
"ObjectConnection": {
"pageInfo": [
240
],
"edges": [
244
],
"__typename": [
1
]
},
"ObjectIndexMetadatasConnection": {
"pageInfo": [
240
],
"edges": [
239
],
"__typename": [
1
]
},
"FieldEdge": {
"node": [
34
],
"cursor": [
40
],
"__typename": [
1
]
},
"ObjectFieldsConnection": {
"pageInfo": [
240
],
"edges": [
249
],
"__typename": [
1
]
},
"FieldConnection": {
"pageInfo": [
240
],
"edges": [
249
],
"__typename": [
1
]
},
"BillingEntitlement": {
"key": [
238
253
],
"value": [
6
@@ -4459,145 +4641,7 @@ export default {
1
],
"records": [
239
],
"__typename": [
1
]
},
"IndexEdge": {
"node": [
37
],
"cursor": [
40
],
"__typename": [
1
]
},
"PageInfo": {
"hasNextPage": [
6
],
"hasPreviousPage": [
6
],
"startCursor": [
40
],
"endCursor": [
40
],
"__typename": [
1
]
},
"IndexConnection": {
"pageInfo": [
242
],
"edges": [
241
],
"__typename": [
1
]
},
"IndexFieldEdge": {
"node": [
36
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexIndexFieldMetadatasConnection": {
"pageInfo": [
242
],
"edges": [
244
],
"__typename": [
1
]
},
"ObjectEdge": {
"node": [
46
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexObjectMetadataConnection": {
"pageInfo": [
242
],
"edges": [
246
],
"__typename": [
1
]
},
"ObjectRecordCount": {
"objectNamePlural": [
1
],
"totalCount": [
21
],
"__typename": [
1
]
},
"ObjectConnection": {
"pageInfo": [
242
],
"edges": [
246
],
"__typename": [
1
]
},
"ObjectIndexMetadatasConnection": {
"pageInfo": [
242
],
"edges": [
241
],
"__typename": [
1
]
},
"FieldEdge": {
"node": [
34
],
"cursor": [
40
],
"__typename": [
1
]
},
"ObjectFieldsConnection": {
"pageInfo": [
242
],
"edges": [
251
254
],
"__typename": [
1
@@ -4614,38 +4658,6 @@ export default {
1
]
},
"Relation": {
"type": [
255
],
"sourceObjectMetadata": [
46
],
"targetObjectMetadata": [
46
],
"sourceFieldMetadata": [
34
],
"targetFieldMetadata": [
34
],
"__typename": [
1
]
},
"RelationType": {},
"FieldConnection": {
"pageInfo": [
242
],
"edges": [
251
],
"__typename": [
1
]
},
"AuthToken": {
"token": [
1
@@ -5975,7 +5987,7 @@ export default {
},
"AgentChatThreadConnection": {
"pageInfo": [
242
240
],
"edges": [
335
@@ -6582,7 +6594,7 @@ export default {
}
],
"objectRecordCounts": [
248
246
],
"object": [
46,
@@ -6594,7 +6606,7 @@ export default {
}
],
"objects": [
249,
247,
{
"paging": [
39,
@@ -6616,7 +6628,7 @@ export default {
}
],
"indexMetadatas": [
243,
241,
{
"paging": [
39,
@@ -6665,7 +6677,7 @@ export default {
}
],
"fields": [
256,
251,
{
"paging": [
39,
@@ -8340,7 +8352,7 @@ export default {
}
],
"upsertRowLevelPermissionPredicates": [
253,
256,
{
"input": [
456,
@@ -9082,7 +9094,7 @@ export default {
66
],
"checkCustomDomainValidRecords": [
240
255
],
"createOIDCIdentityProvider": [
232,
@@ -9415,7 +9427,7 @@ export default {
}
],
"checkPublicDomainValidRecords": [
240,
255,
{
"domain": [
1,
@@ -87,7 +87,7 @@ Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twe
### Requirements
- An [npm](https://www.npmjs.com) account
- The `twenty-app` keyword in your `package.json` `keywords` array (already included when you scaffold with `create-twenty-app`)
- The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template)
```json filename="package.json"
{
@@ -87,7 +87,7 @@ yarn twenty deploy
### المتطلبات
* حساب على [npm](https://www.npmjs.com)
* الكلمة المفتاحية `twenty-app` في مصفوفة `keywords` في `package.json` (موجودة مسبقًا عند تهيئة المشروع باستخدام `create-twenty-app`)
* الكلمة المفتاحية `twenty-app` في مصفوفة `keywords` في `package.json` (أضفها يدويًا — فهي غير مضمنة افتراضيًا في قالب `create-twenty-app`)
```json filename="package.json"
{
@@ -87,7 +87,7 @@ Publikování na npm zajistí, že bude vaše aplikace dohledatelná v Marketpla
### Požadavky
* Účet na [npm](https://www.npmjs.com)
* Klíčové slovo `twenty-app` ve vašem poli `keywords` v souboru `package.json` (již je zahrnuto, když založíte projekt pomocí `create-twenty-app`)
* Klíčové slovo `twenty-app` ve vašem poli `keywords` v souboru `package.json` (přidejte je ručně — ve výchozím nastavení není zahrnuto v šabloně `create-twenty-app`)
```json filename="package.json"
{
@@ -87,7 +87,7 @@ Die Veröffentlichung auf npm macht Ihre App im Twenty-Marktplatz auffindbar. Je
### Anforderungen
* Ein [npm](https://www.npmjs.com)-Konto
* Das Schlüsselwort `twenty-app` in Ihrem `package.json`-Array `keywords` (bereits enthalten, wenn Sie mit `create-twenty-app` ein Gerüst erstellen)
* Das Schlüsselwort `twenty-app` in Ihrem `package.json`-Array `keywords` (manuell hinzufügen — es ist in der `create-twenty-app`-Vorlage standardmäßig nicht enthalten)
```json filename="package.json"
{
@@ -87,7 +87,7 @@ La pubblicazione su npm rende la tua app scopribile nel marketplace di Twenty. Q
### Requisiti
* Un account [npm](https://www.npmjs.com)
* La parola chiave `twenty-app` nell'array `keywords` del tuo `package.json` (già inclusa quando inizializzi con `create-twenty-app`)
* La parola chiave `twenty-app` nell'array `keywords` del tuo `package.json` (aggiungila manualmente — non è inclusa per impostazione predefinita nel template `create-twenty-app`)
```json filename="package.json"
{
@@ -87,7 +87,7 @@ Publicar no npm torna seu aplicativo descobrível no Marketplace da Twenty. Qual
### Requisitos
* Uma conta no [npm](https://www.npmjs.com)
* A palavra-chave `twenty-app` no array `keywords` do seu `package.json` (já incluída quando você cria o projeto com `create-twenty-app`)
* A palavra-chave `twenty-app` no array `keywords` do seu `package.json` (adicione-a manualmente — não é incluída por padrão no template `create-twenty-app`)
```json filename="package.json"
{
@@ -87,7 +87,7 @@ When updating an already deployed tarball app, the server requires the `version`
### Требования
* Учётная запись [npm](https://www.npmjs.com)
* Ключевое слово `twenty-app` в массиве `keywords` вашего `package.json` (уже добавлено при создании проекта с помощью `create-twenty-app`)
* Ключевое слово `twenty-app` в массиве `keywords` вашего `package.json` (добавьте его вручную — по умолчанию оно не включено в шаблон `create-twenty-app`)
```json filename="package.json"
{
@@ -87,7 +87,7 @@ npmye yayımlamak, uygulamanızın Twenty pazaryerinde keşfedilebilir olmas
### Gereksinimler
* Bir [npm](https://www.npmjs.com) hesabı
* `package.json` içindeki `keywords` dizinizdeki `twenty-app` anahtar sözcüğü (`create-twenty-app` ile iskelet oluşturduğunuzda zaten eklenir)
* `package.json` içindeki `keywords` dizinizdeki `twenty-app` anahtar sözcüğü (elle ekleyin — varsayılan olarak `create-twenty-app` şablonunda yer almaz)
```json filename="package.json"
{
File diff suppressed because one or more lines are too long
@@ -14,6 +14,7 @@ export const PAGE_LAYOUT_TAB_FRAGMENT = gql`
...PageLayoutWidgetFragment
}
pageLayoutId
isActive
createdAt
updatedAt
}
@@ -79,7 +79,9 @@ export const splitViewWithRelated = (
const { viewFields: _viewFields, ...viewFieldGroupProperties } =
viewFieldGroup;
flatViewFieldGroups.push(viewFieldGroupProperties);
flatViewFieldGroups.push({
...viewFieldGroupProperties,
});
}
}
@@ -48,6 +48,7 @@ export const RecordBoardCardCellHoveredPortalContent = () => {
fieldDefinition,
recordId,
prefix: RECORD_BOARD_CARD_INPUT_ID_PREFIX,
onFileUploadClose: () => setRecordBoardCardEditModePosition(null),
});
}
};
@@ -48,6 +48,7 @@ export const RecordCalendarCardCellHoveredPortalContent = () => {
fieldDefinition,
recordId,
prefix: RECORD_CALENDAR_CARD_INPUT_ID_PREFIX,
onFileUploadClose: () => setRecordCalendarCardEditModePosition(null),
});
}
};
@@ -52,6 +52,7 @@ export const RecordFieldListCellHoveredPortalContent = () => {
fieldDefinition,
recordId,
prefix: instanceId,
onFileUploadClose: () => setRecordFieldListCellEditModePosition(null),
});
}
};
@@ -66,10 +66,12 @@ export const useOpenFieldInputEditMode = () => {
fieldDefinition,
recordId,
prefix,
onFileUploadClose,
}: {
fieldDefinition: FieldDefinition<FieldMetadata>;
recordId: string;
prefix?: string;
onFileUploadClose?: () => void;
}) => {
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
@@ -106,6 +108,7 @@ export const useOpenFieldInputEditMode = () => {
updateOneRecordInput: updateInput,
});
},
onFileUploadClose,
fieldDefinition: {
metadata: {
settings: fieldDefinition.metadata.settings ?? undefined,
@@ -4,16 +4,10 @@ import { uploadMultipleFiles } from '@/object-record/record-field/ui/meta-types/
import { filesFieldUploadState } from '@/object-record/record-field/ui/states/filesFieldUploadState';
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveLastFocusItemFromFocusStackByComponentType } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackByComponentType';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useStore } from 'jotai';
import { useLingui } from '@lingui/react/macro';
import { useCallback } from 'react';
@@ -24,13 +18,6 @@ export const useOpenFilesFieldInput = () => {
const { openFileUpload } = useFileUpload();
const { uploadFile } = useUploadFilesFieldFile();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeLastFocusItemFromFocusStackByComponentType } =
useRemoveLastFocusItemFromFocusStackByComponentType();
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const recordTableId = useAvailableComponentInstanceId(
RecordTableComponentInstanceContext,
);
const { enqueueErrorSnackBar } = useSnackBar();
const { t } = useLingui();
const store = useStore();
@@ -42,7 +29,7 @@ export const useOpenFilesFieldInput = () => {
recordId,
prefix,
updateRecord,
onClose,
onFileUploadClose,
fieldDefinition,
}: {
fieldName: string;
@@ -50,7 +37,7 @@ export const useOpenFilesFieldInput = () => {
recordId: string;
prefix?: string;
updateRecord: (updateInput: Record<string, unknown>) => void;
onClose?: () => void;
onFileUploadClose?: () => void;
fieldDefinition?: {
metadata: {
settings?: {
@@ -86,8 +73,6 @@ export const useOpenFilesFieldInput = () => {
return;
}
const isTableContext = prefix === RECORD_TABLE_CELL_INPUT_ID_PREFIX;
const maxNumberOfValues =
fieldDefinition?.metadata?.settings?.maxNumberOfValues ??
MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES;
@@ -112,20 +97,7 @@ export const useOpenFilesFieldInput = () => {
null,
);
if (isTableContext && isDefined(recordTableId)) {
store.set(
recordTableCellEditModePositionComponentState.atomFamily({
instanceId: recordTableId,
}),
null,
);
goBackToPreviousDropdownFocusId();
removeLastFocusItemFromFocusStackByComponentType({
componentType: FocusComponentType.OPENED_FIELD_INPUT,
});
} else {
onClose?.();
}
onFileUploadClose?.();
return;
}
@@ -152,20 +124,7 @@ export const useOpenFilesFieldInput = () => {
null,
);
if (isTableContext && isDefined(recordTableId)) {
store.set(
recordTableCellEditModePositionComponentState.atomFamily({
instanceId: recordTableId,
}),
null,
);
goBackToPreviousDropdownFocusId();
removeLastFocusItemFromFocusStackByComponentType({
componentType: FocusComponentType.OPENED_FIELD_INPUT,
});
} else {
onClose?.();
}
onFileUploadClose?.();
}
},
onCancel: () => {
@@ -174,20 +133,7 @@ export const useOpenFilesFieldInput = () => {
null,
);
if (isTableContext && isDefined(recordTableId)) {
store.set(
recordTableCellEditModePositionComponentState.atomFamily({
instanceId: recordTableId,
}),
null,
);
goBackToPreviousDropdownFocusId();
removeLastFocusItemFromFocusStackByComponentType({
componentType: FocusComponentType.OPENED_FIELD_INPUT,
});
} else {
onClose?.();
}
onFileUploadClose?.();
},
});
},
@@ -195,9 +141,6 @@ export const useOpenFilesFieldInput = () => {
openFileUpload,
uploadFile,
pushFocusItemToFocusStack,
recordTableId,
goBackToPreviousDropdownFocusId,
removeLastFocusItemFromFocusStackByComponentType,
enqueueErrorSnackBar,
t,
store,
@@ -24,6 +24,9 @@ import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/use
import { useFocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useFocusRecordTableCell';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useRemoveLastFocusItemFromFocusStackByComponentType } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackByComponentType';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@@ -61,6 +64,12 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
const { openFieldInput } = useOpenFieldInputEditMode();
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const { removeLastFocusItemFromFocusStackByComponentType } =
useRemoveLastFocusItemFromFocusStackByComponentType();
const { activateRecordTableRow, deactivateRecordTableRow } =
useActiveRecordTableRow(recordTableId);
@@ -137,6 +146,13 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
fieldDefinition,
recordId,
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
onFileUploadClose: () => {
setRecordTableCellEditModePosition(null);
goBackToPreviousDropdownFocusId();
removeLastFocusItemFromFocusStackByComponentType({
componentType: FocusComponentType.OPENED_FIELD_INPUT,
});
},
});
setRecordTableCellEditModePosition(cellPosition);
@@ -170,6 +186,8 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
setDragSelectionStartEnabled,
openFieldInput,
setRecordTableCellEditModePosition,
goBackToPreviousDropdownFocusId,
removeLastFocusItemFromFocusStackByComponentType,
initDraftValue,
toggleClickOutside,
setActiveDropdownFocusIdAndMemorizePrevious,
@@ -58,6 +58,7 @@ const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
height: ${TAB_LIST_HEIGHT};
padding-left: ${themeCssVariables.spacing[2]};
position: relative;
user-select: none;
width: 100%;
@@ -28,7 +28,6 @@ import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { SidePanelPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
@@ -47,10 +46,6 @@ const StyledTabsAndDashboardContainer = styled.div`
overflow: hidden;
`;
const StyledPageLayoutTabListContainer = styled.div`
padding-left: ${themeCssVariables.spacing[2]};
`;
const StyledScrollWrapperContainer = styled.div`
flex: 1;
min-height: 0;
@@ -168,27 +163,25 @@ export const PageLayoutTabsRenderer = () => {
}
/>
{(sortedTabs.length > 1 || isPageLayoutInEditMode) && (
<StyledPageLayoutTabListContainer>
<PageLayoutTabList
tabs={sortedTabs}
behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode}
isInSidePanel={isInSidePanel}
componentInstanceId={tabListInstanceId}
onAddTab={handleAddTab}
isReorderEnabled={canEnableTabEditing}
onReorder={
canEnableTabEditing
? (result, provided) =>
reorderRecordPageTabs(
result,
provided,
isDefined(pinnedLeftTab),
)
: undefined
}
pageLayoutType={currentPageLayout.type}
/>
</StyledPageLayoutTabListContainer>
<PageLayoutTabList
tabs={sortedTabs}
behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode}
isInSidePanel={isInSidePanel}
componentInstanceId={tabListInstanceId}
onAddTab={handleAddTab}
isReorderEnabled={canEnableTabEditing}
onReorder={
canEnableTabEditing
? (result, provided) =>
reorderRecordPageTabs(
result,
provided,
isDefined(pinnedLeftTab),
)
: undefined
}
pageLayoutType={currentPageLayout.type}
/>
)}
<StyledScrollWrapperContainer>
@@ -87,7 +87,7 @@ export const PageLayoutVerticalListEditor = ({
<StyledVerticalListContainer
ref={provided.innerRef}
variant={variant}
shouldUseWhiteBackground={isMobile || isInSidePanel}
shouldUseWhiteBackground={!isInPinnedTab || isMobile}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
@@ -45,7 +45,7 @@ export const PageLayoutVerticalListViewer = ({
return (
<StyledVerticalListContainer
variant={variant}
shouldUseWhiteBackground={isMobile || isInSidePanel}
shouldUseWhiteBackground={!isInPinnedTab || isMobile}
>
{widgets.map((widget) => (
<div key={widget.id}>
@@ -64,6 +64,7 @@ const mixedGraphsPageLayoutMocks = {
tabs: [
{
__typename: 'PageLayoutTab',
isActive: true,
id: 'mixed-tab',
title: 'Mixed Graphs',
position: 0,
@@ -74,6 +75,7 @@ const mixedGraphsPageLayoutMocks = {
widgets: [
{
__typename: 'PageLayoutWidget',
isActive: true,
id: 'number-widget',
pageLayoutTabId: 'mixed-tab',
type: WidgetType.GRAPH,
@@ -98,6 +100,7 @@ const mixedGraphsPageLayoutMocks = {
} satisfies PageLayoutWidget,
{
__typename: 'PageLayoutWidget',
isActive: true,
id: 'gauge-widget',
pageLayoutTabId: 'mixed-tab',
type: WidgetType.GRAPH,
@@ -123,6 +126,7 @@ const mixedGraphsPageLayoutMocks = {
} satisfies PageLayoutWidget,
{
__typename: 'PageLayoutWidget',
isActive: true,
id: 'pie-widget',
pageLayoutTabId: 'mixed-tab',
type: WidgetType.GRAPH,
@@ -149,6 +153,7 @@ const mixedGraphsPageLayoutMocks = {
} satisfies PageLayoutWidget,
{
__typename: 'PageLayoutWidget',
isActive: true,
id: 'bar-widget',
pageLayoutTabId: 'mixed-tab',
type: WidgetType.GRAPH,
@@ -23,6 +23,7 @@ const StyledContainer = styled.div`
const createInitialTabs = (): PageLayoutTab[] => [
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: 'overview',
title: 'Overview',
@@ -36,6 +37,7 @@ const createInitialTabs = (): PageLayoutTab[] => [
},
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: 'revenue',
title: 'Revenue',
@@ -48,6 +50,7 @@ const createInitialTabs = (): PageLayoutTab[] => [
},
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: 'forecasts',
title: 'Forecasts',
@@ -77,6 +80,7 @@ const PageLayoutTabListPlayground = ({
...prev,
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: `new-tab-${nextIndex}`,
title: `New Tab ${nextIndex}`,
@@ -21,6 +21,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'company-tab-fields',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -33,6 +34,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'company-widget-fields',
isActive: true,
pageLayoutTabId: 'company-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -59,6 +61,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'company-tab-timeline',
isActive: true,
title: 'Timeline',
icon: 'IconTimelineEvent',
position: 200,
@@ -71,6 +74,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'company-widget-timeline',
isActive: true,
pageLayoutTabId: 'company-tab-timeline',
title: 'Timeline',
type: WidgetType.TIMELINE,
@@ -97,6 +101,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'company-tab-tasks',
isActive: true,
title: 'Tasks',
icon: 'IconCheckbox',
position: 300,
@@ -109,6 +114,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'company-widget-tasks',
isActive: true,
pageLayoutTabId: 'company-tab-tasks',
title: 'Tasks',
type: WidgetType.TASKS,
@@ -135,6 +141,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'company-tab-notes',
isActive: true,
title: 'Notes',
icon: 'IconNotes',
position: 400,
@@ -147,6 +154,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'company-widget-notes',
isActive: true,
pageLayoutTabId: 'company-tab-notes',
title: 'Notes',
type: WidgetType.NOTES,
@@ -173,6 +181,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'company-tab-files',
isActive: true,
title: 'Files',
icon: 'IconPaperclip',
position: 500,
@@ -185,6 +194,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'company-widget-files',
isActive: true,
pageLayoutTabId: 'company-tab-files',
title: 'Files',
type: WidgetType.FILES,
@@ -211,6 +221,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'company-tab-emails',
isActive: true,
title: 'Emails',
icon: 'IconMail',
position: 600,
@@ -223,6 +234,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'company-widget-emails',
isActive: true,
pageLayoutTabId: 'company-tab-emails',
title: 'Emails',
type: WidgetType.EMAILS,
@@ -249,6 +261,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'company-tab-calendar',
isActive: true,
title: 'Calendar',
icon: 'IconCalendarEvent',
position: 700,
@@ -261,6 +274,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'company-widget-calendar',
isActive: true,
pageLayoutTabId: 'company-tab-calendar',
title: 'Calendar',
type: WidgetType.CALENDAR,
@@ -21,6 +21,7 @@ export const DEFAULT_MESSAGE_THREAD_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'message-thread-tab-home',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -33,6 +34,7 @@ export const DEFAULT_MESSAGE_THREAD_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'message-thread-widget-email-thread',
isActive: true,
pageLayoutTabId: 'message-thread-tab-home',
title: 'Thread',
type: WidgetType.EMAIL_THREAD,
@@ -21,6 +21,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'note-tab-fields',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -33,6 +34,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'note-widget-fields',
isActive: true,
pageLayoutTabId: 'note-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -59,6 +61,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'note-widget-note',
isActive: true,
pageLayoutTabId: 'note-tab-fields',
title: 'Note',
type: WidgetType.FIELD_RICH_TEXT,
@@ -92,6 +95,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'note-tab-note',
isActive: true,
title: 'Note',
icon: 'IconNotes',
position: 150,
@@ -104,6 +108,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'note-widget-note',
isActive: true,
pageLayoutTabId: 'note-tab-note',
title: 'Note',
type: WidgetType.FIELD_RICH_TEXT,
@@ -137,6 +142,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'note-tab-timeline',
isActive: true,
title: 'Timeline',
icon: 'IconTimelineEvent',
position: 200,
@@ -149,6 +155,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'note-widget-timeline',
isActive: true,
pageLayoutTabId: 'note-tab-timeline',
title: 'Timeline',
type: WidgetType.TIMELINE,
@@ -175,6 +182,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'note-tab-files',
isActive: true,
title: 'Files',
icon: 'IconPaperclip',
position: 300,
@@ -187,6 +195,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'note-widget-files',
isActive: true,
pageLayoutTabId: 'note-tab-files',
title: 'Files',
type: WidgetType.FILES,
@@ -21,6 +21,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'opportunity-tab-fields',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -33,6 +34,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'opportunity-widget-fields',
isActive: true,
pageLayoutTabId: 'opportunity-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -59,6 +61,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'opportunity-tab-timeline',
isActive: true,
title: 'Timeline',
icon: 'IconTimelineEvent',
position: 200,
@@ -71,6 +74,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'opportunity-widget-timeline',
isActive: true,
pageLayoutTabId: 'opportunity-tab-timeline',
title: 'Timeline',
type: WidgetType.TIMELINE,
@@ -97,6 +101,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'opportunity-tab-tasks',
isActive: true,
title: 'Tasks',
icon: 'IconCheckbox',
position: 300,
@@ -109,6 +114,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'opportunity-widget-tasks',
isActive: true,
pageLayoutTabId: 'opportunity-tab-tasks',
title: 'Tasks',
type: WidgetType.TASKS,
@@ -135,6 +141,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'opportunity-tab-notes',
isActive: true,
title: 'Notes',
icon: 'IconNotes',
position: 400,
@@ -147,6 +154,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'opportunity-widget-notes',
isActive: true,
pageLayoutTabId: 'opportunity-tab-notes',
title: 'Notes',
type: WidgetType.NOTES,
@@ -173,6 +181,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'opportunity-tab-files',
isActive: true,
title: 'Files',
icon: 'IconPaperclip',
position: 500,
@@ -185,6 +194,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'opportunity-widget-files',
isActive: true,
pageLayoutTabId: 'opportunity-tab-files',
title: 'Files',
type: WidgetType.FILES,
@@ -211,6 +221,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'opportunity-tab-emails',
isActive: true,
title: 'Emails',
icon: 'IconMail',
position: 600,
@@ -223,6 +234,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'opportunity-widget-emails',
isActive: true,
pageLayoutTabId: 'opportunity-tab-emails',
title: 'Emails',
type: WidgetType.EMAILS,
@@ -249,6 +261,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'opportunity-tab-calendar',
isActive: true,
title: 'Calendar',
icon: 'IconCalendarEvent',
position: 700,
@@ -261,6 +274,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'opportunity-widget-calendar',
isActive: true,
pageLayoutTabId: 'opportunity-tab-calendar',
title: 'Calendar',
type: WidgetType.CALENDAR,
@@ -21,6 +21,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'person-tab-fields',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -33,6 +34,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'person-widget-fields',
isActive: true,
pageLayoutTabId: 'person-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -59,6 +61,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'person-tab-timeline',
isActive: true,
title: 'Timeline',
icon: 'IconTimelineEvent',
position: 200,
@@ -71,6 +74,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'person-widget-timeline',
isActive: true,
pageLayoutTabId: 'person-tab-timeline',
title: 'Timeline',
type: WidgetType.TIMELINE,
@@ -97,6 +101,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'person-tab-tasks',
isActive: true,
title: 'Tasks',
icon: 'IconCheckbox',
position: 300,
@@ -109,6 +114,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'person-widget-tasks',
isActive: true,
pageLayoutTabId: 'person-tab-tasks',
title: 'Tasks',
type: WidgetType.TASKS,
@@ -135,6 +141,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'person-tab-notes',
isActive: true,
title: 'Notes',
icon: 'IconNotes',
position: 400,
@@ -147,6 +154,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'person-widget-notes',
isActive: true,
pageLayoutTabId: 'person-tab-notes',
title: 'Notes',
type: WidgetType.NOTES,
@@ -173,6 +181,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'person-tab-files',
isActive: true,
title: 'Files',
icon: 'IconPaperclip',
position: 500,
@@ -185,6 +194,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'person-widget-files',
isActive: true,
pageLayoutTabId: 'person-tab-files',
title: 'Files',
type: WidgetType.FILES,
@@ -211,6 +221,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'person-tab-emails',
isActive: true,
title: 'Emails',
icon: 'IconMail',
position: 600,
@@ -223,6 +234,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'person-widget-emails',
isActive: true,
pageLayoutTabId: 'person-tab-emails',
title: 'Emails',
type: WidgetType.EMAILS,
@@ -249,6 +261,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'person-tab-calendar',
isActive: true,
title: 'Calendar',
icon: 'IconCalendarEvent',
position: 700,
@@ -261,6 +274,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'person-widget-calendar',
isActive: true,
pageLayoutTabId: 'person-tab-calendar',
title: 'Calendar',
type: WidgetType.CALENDAR,
@@ -21,6 +21,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'default-tab-fields',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -33,6 +34,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'default-widget-fields',
isActive: true,
pageLayoutTabId: 'default-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -59,6 +61,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'default-tab-timeline',
isActive: true,
title: 'Timeline',
icon: 'IconTimelineEvent',
position: 200,
@@ -71,6 +74,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'default-widget-timeline',
isActive: true,
pageLayoutTabId: 'default-tab-timeline',
title: 'Timeline',
type: WidgetType.TIMELINE,
@@ -97,6 +101,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'default-tab-tasks',
isActive: true,
title: 'Tasks',
icon: 'IconCheckbox',
position: 300,
@@ -109,6 +114,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'default-widget-tasks',
isActive: true,
pageLayoutTabId: 'default-tab-tasks',
title: 'Tasks',
type: WidgetType.TASKS,
@@ -135,6 +141,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'default-tab-notes',
isActive: true,
title: 'Notes',
icon: 'IconNotes',
position: 400,
@@ -147,6 +154,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'default-widget-notes',
isActive: true,
pageLayoutTabId: 'default-tab-notes',
title: 'Notes',
type: WidgetType.NOTES,
@@ -173,6 +181,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'default-tab-files',
isActive: true,
title: 'Files',
icon: 'IconPaperclip',
position: 500,
@@ -185,6 +194,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'default-widget-files',
isActive: true,
pageLayoutTabId: 'default-tab-files',
title: 'Files',
type: WidgetType.FILES,
@@ -21,6 +21,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'task-tab-fields',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -33,6 +34,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'task-widget-fields',
isActive: true,
pageLayoutTabId: 'task-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -56,6 +58,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'task-widget-note',
isActive: true,
pageLayoutTabId: 'task-tab-fields',
title: 'Note',
type: WidgetType.FIELD_RICH_TEXT,
@@ -89,6 +92,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'task-tab-note',
isActive: true,
title: 'Note',
icon: 'IconNotes',
position: 150,
@@ -101,6 +105,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'task-widget-note',
isActive: true,
pageLayoutTabId: 'task-tab-note',
title: 'Note',
type: WidgetType.FIELD_RICH_TEXT,
@@ -134,6 +139,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'task-tab-timeline',
isActive: true,
title: 'Timeline',
icon: 'IconTimelineEvent',
position: 200,
@@ -146,6 +152,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'task-widget-timeline',
isActive: true,
pageLayoutTabId: 'task-tab-timeline',
title: 'Timeline',
type: WidgetType.TIMELINE,
@@ -172,6 +179,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'task-tab-files',
isActive: true,
title: 'Files',
icon: 'IconPaperclip',
position: 300,
@@ -184,6 +192,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'task-widget-files',
isActive: true,
pageLayoutTabId: 'task-tab-files',
title: 'Files',
type: WidgetType.FILES,
@@ -26,6 +26,7 @@ export const DEFAULT_WORKFLOW_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'workflow-tab-flow',
isActive: true,
title: 'Flow',
position: 100,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
@@ -38,6 +39,7 @@ export const DEFAULT_WORKFLOW_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'workflow-widget-flow',
isActive: true,
pageLayoutTabId: 'workflow-tab-flow',
title: 'Flow',
type: WidgetType.WORKFLOW,
@@ -27,6 +27,7 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'workflow-run-tab-fields',
isActive: true,
title: 'Home',
position: 100,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
@@ -39,6 +40,7 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'workflow-run-widget-fields',
isActive: true,
pageLayoutTabId: 'workflow-run-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -66,6 +68,7 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'workflow-run-tab-flow',
isActive: true,
title: 'Flow',
position: 200,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
@@ -78,6 +81,7 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'workflow-run-widget-flow',
isActive: true,
pageLayoutTabId: 'workflow-run-tab-flow',
title: 'Flow',
type: WidgetType.WORKFLOW_RUN,
@@ -27,6 +27,7 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'workflow-version-tab-fields',
isActive: true,
title: 'Home',
icon: 'IconHome',
position: 100,
@@ -39,6 +40,7 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'workflow-version-widget-fields',
isActive: true,
pageLayoutTabId: 'workflow-version-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
@@ -66,6 +68,7 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
__typename: 'PageLayoutTab',
applicationId: '',
id: 'workflow-version-tab-flow',
isActive: true,
title: 'Flow',
icon: 'IconSettings',
position: 200,
@@ -78,6 +81,7 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutWidget',
id: 'workflow-version-widget-flow',
isActive: true,
pageLayoutTabId: 'workflow-version-tab-flow',
title: 'Flow',
type: WidgetType.WORKFLOW_VERSION,
@@ -8,6 +8,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
objectMetadataId
createdAt
updatedAt
isActive
deletedAt
conditionalDisplay
conditionalAvailabilityExpression
@@ -24,6 +24,7 @@ const makeWidget = (
id,
pageLayoutTabId: tabId,
title: id,
isActive: true,
type: WidgetType.FIELDS,
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
configuration: { __typename: 'FieldsConfiguration' as const },
@@ -46,6 +47,7 @@ const makeTab = (
id,
applicationId: '',
title: id,
isActive: true,
position,
layoutMode,
pageLayoutId: '',
@@ -24,6 +24,7 @@ const makeWidget = (
id,
pageLayoutTabId: tabId,
title: id,
isActive: true,
type: WidgetType.FIELDS,
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
configuration: { __typename: 'FieldsConfiguration' as const },
@@ -46,6 +47,7 @@ const makeTab = (
id,
applicationId: '',
title: id,
isActive: true,
position,
layoutMode,
pageLayoutId: '',
@@ -79,6 +79,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
id: 'tab-1',
applicationId: '',
title: 'Tab 1',
isActive: true,
position: 0,
pageLayoutId: '',
widgets: [],
@@ -162,6 +163,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
id: 'tab-1',
applicationId: '',
title: 'Tab 1',
isActive: true,
position: 0,
pageLayoutId: '',
widgets: [],
@@ -308,6 +308,7 @@ describe('useCreatePageLayoutTab', () => {
id: 'existing-tab',
applicationId: '',
title: 'Existing Tab',
isActive: true,
position: 0,
pageLayoutId: 'test-layout',
widgets: [],
@@ -25,6 +25,7 @@ const makeWidget = (
id,
pageLayoutTabId: tabId,
title: id,
isActive: true,
type: WidgetType.FIELDS,
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
configuration: { __typename: 'FieldsConfiguration' as const },
@@ -46,6 +47,7 @@ const makeTab = (
id,
applicationId: '',
title: id,
isActive: true,
position,
pageLayoutId: '',
widgets,
@@ -24,6 +24,7 @@ const makeWidget = (
id,
pageLayoutTabId: tabId,
title: id,
isActive: true,
type: WidgetType.FIELDS,
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
configuration: { __typename: 'FieldsConfiguration' as const },
@@ -45,6 +46,7 @@ const makeTab = (
id,
applicationId: '',
title: id,
isActive: true,
position,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
pageLayoutId: '',
@@ -24,6 +24,7 @@ const makeWidget = (
id,
pageLayoutTabId: tabId,
title: id,
isActive: true,
type: WidgetType.FIELDS,
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
configuration: { __typename: 'FieldsConfiguration' as const },
@@ -45,6 +46,7 @@ const makeTab = (
id,
applicationId: '',
title: id,
isActive: true,
position,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
pageLayoutId: '',
@@ -24,6 +24,7 @@ const makeWidget = (
id,
pageLayoutTabId: tabId,
title: id,
isActive: true,
type: WidgetType.FIELDS,
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
configuration: { __typename: 'FieldsConfiguration' as const },
@@ -45,6 +46,7 @@ const makeTab = (
id,
applicationId: '',
title: id,
isActive: true,
position,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
pageLayoutId: '',
@@ -90,6 +90,7 @@ describe('usePageLayoutDraftState', () => {
id: 'tab-1',
applicationId: '',
title: 'Tab 1',
isActive: true,
position: 0,
pageLayoutId: '',
createdAt: new Date().toISOString(),
@@ -100,6 +101,7 @@ describe('usePageLayoutDraftState', () => {
id: 'widget-1',
pageLayoutTabId: 'tab-1',
title: 'New Widget',
isActive: true,
type: WidgetType.GRAPH,
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
configuration: {
@@ -35,6 +35,7 @@ describe('usePageLayoutWithRelationWidgets', () => {
applicationId: '',
id: 'tab-1',
title: 'Fields',
isActive: true,
icon: 'IconList',
position: 100,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
@@ -48,6 +49,7 @@ describe('usePageLayoutWithRelationWidgets', () => {
id: 'widget-fields',
pageLayoutTabId: 'tab-1',
title: 'Fields',
isActive: true,
type: WidgetType.FIELDS,
objectMetadataId: null,
gridPosition: {
@@ -71,6 +73,7 @@ describe('usePageLayoutWithRelationWidgets', () => {
id: 'widget-notes',
pageLayoutTabId: 'tab-1',
title: 'Notes',
isActive: true,
type: WidgetType.NOTES,
objectMetadataId: null,
gridPosition: {
@@ -93,6 +96,7 @@ describe('usePageLayoutWithRelationWidgets', () => {
id: 'widget-other',
pageLayoutTabId: 'tab-1',
title: 'Other',
isActive: true,
type: WidgetType.GRAPH,
objectMetadataId: null,
gridPosition: {
@@ -217,6 +221,7 @@ describe('usePageLayoutWithRelationWidgets', () => {
id: 'widget-other',
pageLayoutTabId: 'tab-1',
title: 'Other',
isActive: true,
type: WidgetType.GRAPH,
objectMetadataId: null,
gridPosition: {
@@ -54,6 +54,7 @@ export const useCreatePageLayoutTab = ({
const newTab: PageLayoutTab = {
id: newTabId,
applicationId: '',
isActive: true,
title: title || `Tab ${tabsLength + 1}`,
position: maxPosition + 1,
pageLayoutId: pageLayoutId,
@@ -39,7 +39,6 @@ export const useCreatePendingFieldsWidgetViews = () => {
if (widget.type !== WidgetType.FIELDS) {
return false;
}
const viewId = getWidgetConfigurationViewId(widget.configuration);
return isDefined(viewId) && !persistedWidgetIds.has(widget.id);
@@ -2,18 +2,14 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { WidgetType } from '~/generated-metadata/graphql';
export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -38,16 +34,6 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const { closeSidePanelMenu } = useSidePanelMenu();
const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget();
const { deleteViewForRecordTableWidget } =
useDeleteViewForRecordTableWidget();
const pageLayoutPersistedState = useAtomComponentStateCallbackState(
pageLayoutPersistedComponentState,
pageLayoutId,
);
const store = useStore();
const deletePageLayoutWidget = useCallback(
@@ -61,43 +47,6 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
tab.widgets.some((widget) => widget.id === widgetId),
);
const widgetToDelete = tabWithWidget?.widgets.find(
(widget) => widget.id === widgetId,
);
const persisted = store.get(pageLayoutPersistedState);
const persistedWidgetIds = new Set(
persisted?.tabs.flatMap((tab) =>
tab.widgets.map((widget) => widget.id),
) ?? [],
);
const isWidgetPersisted = persistedWidgetIds.has(widgetId);
if (
isWidgetPersisted &&
isDefined(widgetToDelete) &&
widgetToDelete.type === WidgetType.RECORD_TABLE &&
'viewId' in widgetToDelete.configuration &&
isDefined(widgetToDelete.configuration.viewId)
) {
deleteViewForRecordTableWidget(
widgetToDelete.configuration.viewId as string,
);
}
if (
isWidgetPersisted &&
isDefined(widgetToDelete) &&
widgetToDelete.type === WidgetType.FIELDS &&
'viewId' in widgetToDelete.configuration &&
isDefined(widgetToDelete.configuration.viewId)
) {
deleteViewForFieldsWidget(
widgetToDelete.configuration.viewId as string,
);
}
const tabId = tabWithWidget?.id;
if (isDefined(tabId)) {
@@ -124,12 +73,9 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
},
[
closeSidePanelMenu,
deleteViewForFieldsWidget,
deleteViewForRecordTableWidget,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutEditingWidgetIdState,
pageLayoutPersistedState,
store,
],
);
@@ -27,20 +27,22 @@ export const usePageLayoutTabWithVisibleWidgetsOrThrow = (
throw new Error('Tab not found');
}
const activeWidgets = tab.widgets.filter((widget) => widget.isActive);
if (isPageLayoutInEditMode) {
return {
...tab,
widgets:
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
? sortWidgetsByVerticalListPosition(tab.widgets)
: tab.widgets,
? sortWidgetsByVerticalListPosition(activeWidgets)
: activeWidgets,
};
}
const context = buildWidgetVisibilityContext({ isMobile, isInSidePanel });
const visibleWidgets = filterVisibleWidgets({
widgets: tab.widgets,
widgets: activeWidgets,
context,
});
@@ -8,6 +8,10 @@ import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/use
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
@@ -50,6 +54,28 @@ export const useRefreshPageLayoutAfterReset = (
pageLayoutId,
);
const fieldsWidgetGroupsDraftState = useAtomComponentStateCallbackState(
fieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const fieldsWidgetEditorModeDraftState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModeDraftComponentState,
pageLayoutId,
);
const hasInitializedFieldsWidgetGroupsDraftState =
useAtomComponentStateCallbackState(
hasInitializedFieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const refreshPageLayoutAfterReset = useCallback(
async (collectAffectedViewIds: (layout: PageLayout) => Set<string>) => {
const { data } = await client.query({
@@ -73,6 +99,11 @@ export const useRefreshPageLayoutAfterReset = (
);
}
store.set(fieldsWidgetGroupsDraftState, {});
store.set(fieldsWidgetUngroupedFieldsDraftState, {});
store.set(fieldsWidgetEditorModeDraftState, {});
store.set(hasInitializedFieldsWidgetGroupsDraftState, {});
setIsPageLayoutInEditMode(false);
exitLayoutCustomizationMode();
evictViewMetadataForViewIds(store, affectedViewIds);
@@ -85,6 +116,10 @@ export const useRefreshPageLayoutAfterReset = (
pageLayoutPersistedState,
pageLayoutDraftState,
pageLayoutCurrentLayoutsState,
fieldsWidgetGroupsDraftState,
fieldsWidgetUngroupedFieldsDraftState,
fieldsWidgetEditorModeDraftState,
hasInitializedFieldsWidgetGroupsDraftState,
setIsPageLayoutInEditMode,
exitLayoutCustomizationMode,
invalidateMetadataStore,
@@ -3,18 +3,13 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { WidgetType } from '~/generated-metadata/graphql';
export const useRemovePageLayoutWidgetAndPreservePosition = (
pageLayoutIdFromProps?: string,
@@ -44,16 +39,6 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
pageLayoutId,
);
const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget();
const { deleteViewForRecordTableWidget } =
useDeleteViewForRecordTableWidget();
const pageLayoutPersistedState = useAtomComponentStateCallbackState(
pageLayoutPersistedComponentState,
pageLayoutId,
);
const store = useStore();
const removePageLayoutWidgetAndPreservePosition = useCallback(
@@ -65,35 +50,6 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
tab.widgets.some((widget) => widget.id === widgetId),
);
const widgetToRemove = tabWithWidget?.widgets.find(
(widget) => widget.id === widgetId,
);
const persisted = store.get(pageLayoutPersistedState);
const persistedWidgetIds = new Set(
persisted?.tabs.flatMap((tab) =>
tab.widgets.map((widget) => widget.id),
) ?? [],
);
const isWidgetPersisted = persistedWidgetIds.has(widgetId);
if (isWidgetPersisted && isDefined(widgetToRemove)) {
const viewId = getWidgetConfigurationViewId(
widgetToRemove.configuration,
);
if (isDefined(viewId)) {
if (widgetToRemove.type === WidgetType.RECORD_TABLE) {
deleteViewForRecordTableWidget(viewId);
}
if (widgetToRemove.type === WidgetType.FIELDS) {
deleteViewForFieldsWidget(viewId);
}
}
}
const tabId = tabWithWidget?.id;
if (!isDefined(tabId)) {
@@ -131,13 +87,10 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
store.set(pageLayoutEditingWidgetIdState, null);
},
[
deleteViewForFieldsWidget,
deleteViewForRecordTableWidget,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutDraggedAreaState,
pageLayoutEditingWidgetIdState,
pageLayoutPersistedState,
store,
],
);
@@ -18,10 +18,15 @@ export const pageLayoutsWithRelationsSelector = createAtomSelector<
const allFlatWidgets = get(metadataStoreState, 'pageLayoutWidgets')
.current as FlatPageLayoutWidget[];
const activeFlatTabs = allFlatTabs.filter((tab) => tab.isActive);
const activeFlatWidgets = allFlatWidgets.filter(
(widget) => widget.isActive,
);
const tabsByPageLayoutId = new Map<string, FlatPageLayoutTab[]>();
const widgetsByTabId = new Map<string, FlatPageLayoutWidget[]>();
for (const tab of allFlatTabs) {
for (const tab of activeFlatTabs) {
const existing = tabsByPageLayoutId.get(tab.pageLayoutId);
if (isDefined(existing)) {
@@ -31,7 +36,7 @@ export const pageLayoutsWithRelationsSelector = createAtomSelector<
}
}
for (const widget of allFlatWidgets) {
for (const widget of activeFlatWidgets) {
const existing = widgetsByTabId.get(widget.pageLayoutTabId);
if (isDefined(existing)) {
@@ -11,6 +11,7 @@ describe('addWidgetToTab', () => {
const mockWidget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
id: 'widget-1',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Test Widget',
type: WidgetType.GRAPH,
@@ -31,6 +32,7 @@ describe('addWidgetToTab', () => {
{
id: 'tab-1',
applicationId: '',
isActive: true,
title: 'Tab 1',
position: 0,
pageLayoutId: 'layout-1',
@@ -42,6 +44,7 @@ describe('addWidgetToTab', () => {
{
id: 'tab-2',
applicationId: '',
isActive: true,
title: 'Tab 2',
position: 1,
pageLayoutId: 'layout-1',
@@ -12,6 +12,7 @@ describe('convertLayoutsToWidgets', () => {
const mockWidgets: PageLayoutWidget[] = [
{
id: 'widget-1',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Widget 1',
type: WidgetType.GRAPH,
@@ -34,6 +35,7 @@ describe('convertLayoutsToWidgets', () => {
},
{
id: 'widget-2',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Widget 2',
type: WidgetType.GRAPH,
@@ -20,6 +20,7 @@ describe('convertPageLayoutToTabLayouts', () => {
{
id: 'tab-1',
applicationId: '',
isActive: true,
title: 'Tab 1',
position: 0,
pageLayoutId: 'page-layout-1',
@@ -27,6 +28,7 @@ describe('convertPageLayoutToTabLayouts', () => {
{
__typename: 'PageLayoutWidget',
id: 'widget-1',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Widget 1',
type: WidgetType.GRAPH,
@@ -45,6 +47,7 @@ describe('convertPageLayoutToTabLayouts', () => {
{
__typename: 'PageLayoutWidget',
id: 'widget-2',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Widget 2',
type: WidgetType.GRAPH,
@@ -99,6 +102,7 @@ describe('convertPageLayoutToTabLayouts', () => {
{
id: 'tab-1',
applicationId: '',
isActive: true,
title: 'Tab 1',
position: 0,
pageLayoutId: 'page-layout-1',
@@ -106,6 +110,7 @@ describe('convertPageLayoutToTabLayouts', () => {
{
__typename: 'PageLayoutWidget',
id: 'rich-text-widget',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Rich Text',
type: WidgetType.STANDALONE_RICH_TEXT,
@@ -23,6 +23,7 @@ const createMockWidget = (
},
): PageLayoutWidget => ({
id: 'widget-1',
isActive: true,
type: WidgetType.GRAPH,
title: 'Test',
objectMetadataId: null,
@@ -12,6 +12,7 @@ describe('filterVisibleWidgets', () => {
): PageLayoutTab['widgets'][0] => ({
__typename: 'PageLayoutWidget',
id,
isActive: true,
pageLayoutTabId: 'tab-1',
title: `Widget ${id}`,
type: WidgetType.FIELDS,
@@ -5,6 +5,7 @@ describe('getPageLayoutTabListInitialActiveTabId', () => {
const createMockTab = (id: string): PageLayoutTab => ({
id,
applicationId: '',
isActive: true,
pageLayoutId: 'page-layout-1',
title: `Tab ${id}`,
position: 0,
@@ -10,6 +10,7 @@ describe('getTabsByDisplayMode', () => {
const createMockTab = (id: string): PageLayoutTab => ({
id,
applicationId: '',
isActive: true,
pageLayoutId: 'page-layout-1',
title: `Tab ${id}`,
position: 0,
@@ -12,6 +12,7 @@ describe('getTabsWithVisibleWidgets', () => {
): PageLayoutTab['widgets'][0] => ({
__typename: 'PageLayoutWidget',
id,
isActive: true,
pageLayoutTabId: 'tab-1',
title: `Widget ${id}`,
type: WidgetType.FIELDS,
@@ -41,6 +42,7 @@ describe('getTabsWithVisibleWidgets', () => {
__typename: 'PageLayoutTab',
applicationId: '',
id,
isActive: true,
pageLayoutId: 'page-layout-1',
title: `Tab ${id}`,
position: 0,
@@ -10,6 +10,7 @@ import {
describe('prepareGridLayoutItemsWithPlaceholders', () => {
const createMockWidget = (id: string): PageLayoutWidget => ({
id,
isActive: true,
pageLayoutTabId: 'tab-1',
title: `Test Widget ${id}`,
type: WidgetType.GRAPH,
@@ -215,6 +216,7 @@ describe('prepareGridLayoutItemsWithPlaceholders', () => {
expect(resultWidget.createdAt).toBe(widget.createdAt);
expect(resultWidget.updatedAt).toBe(widget.updatedAt);
expect(resultWidget.deletedAt).toBe(widget.deletedAt);
expect(resultWidget.isActive).toBe(widget.isActive);
}
});
});
@@ -12,6 +12,7 @@ describe('removeWidgetFromTab', () => {
{
id: 'tab-1',
applicationId: '',
isActive: true,
title: 'Tab 1',
position: 0,
pageLayoutId: 'layout-1',
@@ -19,6 +20,7 @@ describe('removeWidgetFromTab', () => {
{
__typename: 'PageLayoutWidget' as const,
id: 'widget-1',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Widget 1',
type: WidgetType.GRAPH,
@@ -37,6 +39,7 @@ describe('removeWidgetFromTab', () => {
{
__typename: 'PageLayoutWidget' as const,
id: 'widget-2',
isActive: true,
pageLayoutTabId: 'tab-1',
title: 'Widget 2',
type: WidgetType.GRAPH,
@@ -62,6 +65,7 @@ describe('removeWidgetFromTab', () => {
{
id: 'tab-2',
applicationId: '',
isActive: true,
title: 'Tab 2',
position: 1,
pageLayoutId: 'layout-1',
@@ -69,6 +73,7 @@ describe('removeWidgetFromTab', () => {
{
__typename: 'PageLayoutWidget' as const,
id: 'widget-3',
isActive: true,
pageLayoutTabId: 'tab-2',
title: 'Widget 3',
type: WidgetType.IFRAME,
@@ -28,6 +28,7 @@ export const createDefaultFieldWidget = ({
id,
pageLayoutTabId,
title,
isActive: true,
type: WidgetType.FIELD,
configuration: {
__typename: 'FieldConfiguration',
@@ -23,6 +23,7 @@ export const createDefaultFieldsWidget = ({
id,
pageLayoutTabId,
title: 'Fields',
isActive: true,
type: WidgetType.FIELDS,
configuration: {
__typename: 'FieldsConfiguration',
@@ -18,6 +18,7 @@ export const createDefaultFrontComponentWidget = (
id,
pageLayoutTabId,
title,
isActive: true,
type: WidgetType.FRONT_COMPONENT,
configuration: {
__typename: 'FrontComponentConfiguration',
@@ -72,6 +72,7 @@ export const createDefaultGraphWidget = ({
id,
pageLayoutTabId,
title,
isActive: true,
type: WidgetType.GRAPH,
configuration,
gridPosition,
@@ -19,6 +19,7 @@ export const createDefaultIframeWidget = (
id,
pageLayoutTabId,
title,
isActive: true,
type: WidgetType.IFRAME,
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
@@ -24,6 +24,7 @@ export const createDefaultRecordTableWidget = ({
id,
pageLayoutTabId,
title,
isActive: true,
type: WidgetType.RECORD_TABLE,
configuration: {
configurationType: WidgetConfigurationType.RECORD_TABLE,
@@ -19,6 +19,7 @@ export const createDefaultStandaloneRichTextWidget = (
id,
pageLayoutTabId,
title: 'Untitled Rich Text',
isActive: true,
type: WidgetType.STANDALONE_RICH_TEXT,
configuration: {
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
@@ -15,13 +15,15 @@ export const getTabsWithVisibleWidgets = ({
isInSidePanel,
isEditMode,
}: GetTabsWithVisibleWidgetsParams): PageLayoutTab[] => {
const activeTabs = tabs.filter((tab) => tab.isActive);
if (isEditMode) {
return tabs;
return activeTabs;
}
const context = buildWidgetVisibilityContext({ isMobile, isInSidePanel });
const tabsWithFilteredWidgets = tabs.map((tab) => ({
const tabsWithFilteredWidgets = activeTabs.map((tab) => ({
...tab,
widgets: filterVisibleWidgets({ widgets: tab.widgets, context }),
}));
@@ -30,7 +32,7 @@ export const getTabsWithVisibleWidgets = ({
(tab) => tab.widgets.length > 0,
);
if (tabsWithVisibleWidgets.length === 0 && tabs.length > 0) {
if (tabsWithVisibleWidgets.length === 0 && activeTabs.length > 0) {
return tabsWithFilteredWidgets.slice(0, 1);
}
@@ -18,6 +18,7 @@ const getRelationFieldWidgetToInsert = (
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${field.id}-${field.label}`,
pageLayoutTabId: tabId,
title: field.label,
isActive: true,
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
@@ -9,7 +9,6 @@ import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useRe
import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { indexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/indexViewIdFromObjectMetadataItemFamilySelector';
import { styled } from '@linaria/react';
@@ -24,7 +23,6 @@ import {
TooltipPosition,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { RelationType } from '~/generated-metadata/graphql';
const StyledLinkContainer = styled.div`
@@ -35,21 +33,14 @@ const StyledLinkContainer = styled.div`
}
`;
const StyledSeeAllButtonWrapper = styled.div<{ isMobile: boolean }>`
opacity: ${({ isMobile }) => (isMobile ? '1' : '0')};
pointer-events: none;
transition: opacity ${themeCssVariables.animation.duration.instant}s ease;
.widget:hover & {
opacity: 1;
pointer-events: auto;
}
const StyledSeeAllButtonWrapper = styled.div`
opacity: 1;
pointer-events: auto;
`;
export const WidgetActionFieldSeeAll = () => {
const widget = useCurrentWidget();
const targetRecord = useTargetRecord();
const isMobile = useIsMobile();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetRecord.targetObjectNameSingular,
@@ -145,7 +136,7 @@ export const WidgetActionFieldSeeAll = () => {
<div id={tooltipId}>
<StyledLinkContainer>
<Link to={filterLinkHref} data-testid="widget-see-all-link">
<StyledSeeAllButtonWrapper isMobile={isMobile}>
<StyledSeeAllButtonWrapper>
<LightIconButton Icon={IconArrowUpRight} accent="secondary" />
</StyledSeeAllButtonWrapper>
</Link>
@@ -205,6 +205,10 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
variant={variant}
hasHeader={showHeader}
isEditable={isWidgetEditable}
isInVerticalListTab={
layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
}
isMobile={isMobile}
hasInteractiveContent={widget.type === WidgetType.RECORD_TABLE}
>
{hasAccess ? (
@@ -143,6 +143,7 @@ const createPageLayoutWithWidget = (
tabs: [
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: TAB_ID_OVERVIEW,
title: 'Overview',
@@ -252,6 +253,7 @@ export const WithNumberChart: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_NUMBER_CHART,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -338,6 +340,7 @@ export const WithGaugeChart: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_GAUGE_CHART,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -424,6 +427,7 @@ export const WithBarChart: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_BAR_CHART,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -521,6 +525,7 @@ export const SmallWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_SMALL,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -614,6 +619,7 @@ export const MediumWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_MEDIUM,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -711,6 +717,7 @@ export const LargeWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_LARGE,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -808,6 +815,7 @@ export const WideWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_WIDE,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -901,6 +909,7 @@ export const TallWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_TALL,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -999,6 +1008,7 @@ export const WithManyToOneRelationFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_MANY_TO_ONE_RELATION,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1117,6 +1127,7 @@ export const WithOneToManyRelationFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_ONE_TO_MANY_RELATION,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1227,6 +1238,7 @@ export const OneToManyRelationFieldWidgetWithSeeAllButton: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_ONE_TO_MANY_RELATION,
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1363,6 +1375,7 @@ export const OnMobile: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-mobile',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -1460,6 +1473,7 @@ export const InSidePanel: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-side-panel',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.GRAPH,
@@ -1617,6 +1631,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: WIDGET_ID_CATALOG,
pageLayoutTabId:
variant === 'side-column' ? 'pinned-tab' : TAB_ID_OVERVIEW,
@@ -1723,6 +1738,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
? [
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: 'pinned-tab',
title: 'Pinned Tab',
@@ -1735,6 +1751,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
},
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: 'other-tab',
title: 'Other Tab',
@@ -1749,6 +1766,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
: [
{
__typename: 'PageLayoutTab',
isActive: true,
applicationId: '',
id: TAB_ID_OVERVIEW,
title: 'Overview',
@@ -1,5 +1,4 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { RecordDetailMorphRelationSectionDropdown } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdown';
import { RecordDetailRelationSectionDropdown } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdown';
import {
@@ -17,15 +16,11 @@ import {
type FieldRelationMetadata,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { CustomError } from 'twenty-shared/utils';
import { IconPencil } from 'twenty-ui/display';
import { IconPencil, IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { RelationType } from '~/generated-metadata/graphql';
type FieldWidgetRelationEditActionProps = {
fieldDefinition:
@@ -34,30 +29,15 @@ type FieldWidgetRelationEditActionProps = {
recordId: string;
};
const StyledEditButtonWrapper = styled.div<{
isDropdownOpen: boolean;
isMobile: boolean;
}>`
opacity: ${({ isDropdownOpen, isMobile }) =>
isDropdownOpen ? '1' : isMobile ? '1' : '0'};
pointer-events: ${({ isDropdownOpen }) => (isDropdownOpen ? 'auto' : 'none')};
transition: ${({ isDropdownOpen }) =>
isDropdownOpen
? 'none'
: `opacity ${themeCssVariables.animation.duration.instant}s ease`};
.widget:hover & {
opacity: 1;
pointer-events: auto;
}
const StyledEditButtonWrapper = styled.div`
opacity: 1;
pointer-events: auto;
`;
export const FieldWidgetRelationEditAction = ({
fieldDefinition,
recordId,
}: FieldWidgetRelationEditActionProps) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { objectMetadataItems } = useObjectMetadataItems();
const objectMetadataItem = objectMetadataItems.find(
(item) =>
@@ -92,26 +72,14 @@ export const FieldWidgetRelationEditAction = ({
const isMorphRelation = isFieldMorphRelation(fieldDefinition);
const relationSelectionDropdownId =
getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const isDropdownOpen = useAtomComponentStateValue(
isDropdownOpenComponentState,
relationSelectionDropdownId,
);
const isMobile = useIsMobile();
const triggerIcon =
fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE
? IconPencil
: IconPlus;
const dropdownTriggerClickableComponent = (
<StyledEditButtonWrapper
isDropdownOpen={isDropdownOpen}
isMobile={isMobile}
>
<LightIconButton Icon={IconPencil} accent="secondary" />
<StyledEditButtonWrapper>
<LightIconButton Icon={triggerIcon} accent="secondary" />
</StyledEditButtonWrapper>
);
@@ -291,6 +291,7 @@ const createPageLayoutWithWidget = (
tabs: [
{
__typename: 'PageLayoutTab' as const,
isActive: true,
applicationId: '',
id: TAB_ID_OVERVIEW,
title: 'Overview',
@@ -332,6 +333,7 @@ export const TextFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-text-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -425,6 +427,7 @@ export const AddressFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-address-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -521,6 +524,7 @@ export const NumberFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-number-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -614,6 +618,7 @@ export const LinkFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-link-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -707,6 +712,7 @@ export const ManyToOneRelationFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-relation-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -810,6 +816,7 @@ export const OneToManyRelationFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-one-to-many-relation-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -905,6 +912,7 @@ export const BooleanFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-boolean-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -997,6 +1005,7 @@ export const CurrencyFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-currency-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1089,6 +1098,7 @@ export const EmailsFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-emails-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1182,6 +1192,7 @@ export const PhonesFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-phones-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1275,6 +1286,7 @@ export const SelectFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-select-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1369,6 +1381,7 @@ export const MultiSelectFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-multi-select-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1466,6 +1479,7 @@ export const TimelineActivityRelationFieldWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-timeline-activity-relation-field',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1564,6 +1578,7 @@ export const ManyToOneRelationCardWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-relation-card',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1675,6 +1690,7 @@ export const OneToManyRelationCardWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-one-to-many-relation-card',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1769,6 +1785,7 @@ export const TimelineActivityRelationCardWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-timeline-activity-relation-card',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -1929,6 +1946,7 @@ export const OneToManyRelationCardWidgetWithProgressiveLoading: Story = {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-one-to-many-relation-card-progressive',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
@@ -141,6 +141,7 @@ const createPageLayoutWithWidget = (
tabs: [
{
__typename: 'PageLayoutTab' as const,
isActive: true,
applicationId: '',
id: TAB_ID_OVERVIEW,
title: 'Overview',
@@ -159,6 +160,7 @@ const createPageLayoutWithWidget = (
const createFieldsWidget = (viewId: string | null): PageLayoutWidget => ({
__typename: 'PageLayoutWidget',
isActive: true,
id: 'widget-fields',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELDS,
@@ -213,6 +215,7 @@ const createViewField = (
fieldMetadataId,
position,
isVisible: true,
isActive: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
@@ -230,6 +233,7 @@ const createViewFieldGroup = (
name,
position,
isVisible,
isActive: true,
viewId: FIELDS_VIEW_ID,
viewFields,
});
@@ -29,9 +29,12 @@ export const useFieldsWidgetGroups = ({
objectNameSingular,
});
const groups = useMemo<FieldsWidgetGroup[]>(() => {
const { groups, displayMode } = useMemo<{
groups: FieldsWidgetGroup[];
displayMode: FieldsWidgetDisplayMode;
}>(() => {
if (!isDefined(objectMetadataItem)) {
return [];
return { groups: [], displayMode: 'grouped' };
}
if (isDefined(view) && isNonEmptyArray(view.viewFieldGroups)) {
@@ -41,7 +44,7 @@ export const useFieldsWidgetGroups = ({
let globalIndex = 0;
return sortedGroups
const resultGroups = sortedGroups
.filter((group) => group.isVisible)
.map((group) => {
const groupFields = [...(group.viewFields ?? [])].sort(
@@ -79,28 +82,76 @@ export const useFieldsWidgetGroups = ({
};
})
.filter((group) => group.fields.length > 0);
return { groups: resultGroups, displayMode: 'grouped' };
}
return filterDraftGroupsForDisplay(
buildDefaultFieldsWidgetGroups({
fields: objectMetadataItem.fields,
objectNameSingular,
labelIdentifierFieldMetadataItemId:
labelIdentifierFieldMetadataItem?.id,
}),
);
if (isDefined(view) && isNonEmptyArray(view.viewFields)) {
let globalIndex = 0;
const fields: FieldsWidgetGroupField[] = [...view.viewFields]
.sort((a, b) => a.position - b.position)
.filter((viewField) => viewField.isVisible)
.map((viewField) => {
const fieldMetadataItem = objectMetadataItem.fields.find(
(f) => f.id === viewField.fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
return null;
}
return {
fieldMetadataItem,
position: viewField.position,
isVisible: viewField.isVisible,
globalIndex: globalIndex++,
};
})
.filter(isDefined);
return {
groups:
fields.length > 0
? [
{
id: `${viewId}-ungrouped`,
name: '',
position: 0,
isVisible: true,
fields,
},
]
: [],
displayMode: 'inline',
};
}
return {
groups: filterDraftGroupsForDisplay(
buildDefaultFieldsWidgetGroups({
fields: objectMetadataItem.fields,
objectNameSingular,
labelIdentifierFieldMetadataItemId:
labelIdentifierFieldMetadataItem?.id,
}),
),
displayMode: 'grouped',
};
}, [
objectMetadataItem,
objectNameSingular,
labelIdentifierFieldMetadataItem,
view,
viewId,
]);
const displayMode: FieldsWidgetDisplayMode = 'grouped';
return {
groups,
displayMode,
isFromView: isDefined(view) && isNonEmptyArray(view.viewFieldGroups),
isFromView:
isDefined(view) &&
(isNonEmptyArray(view.viewFieldGroups) ||
isNonEmptyArray(view.viewFields)),
};
};
@@ -29,7 +29,7 @@ export const useSaveRecordTableWidgetFiltersToView = (
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
} = usePerformViewFilterGroupAPIPersist();
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
@@ -142,7 +142,7 @@ export const useSaveRecordTableWidgetFiltersToView = (
})),
);
await performViewFilterGroupAPIDelete(
await performViewFilterGroupAPIDestroy(
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
);
}, [
@@ -155,7 +155,7 @@ export const useSaveRecordTableWidgetFiltersToView = (
performViewFilterAPIDelete,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
]);
return { saveRecordTableWidgetFiltersToView };
@@ -39,7 +39,7 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
} = usePerformViewFilterGroupAPIPersist();
const {
@@ -251,7 +251,7 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
},
})),
);
await performViewFilterGroupAPIDelete(
await performViewFilterGroupAPIDestroy(
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
);
@@ -297,7 +297,7 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
performViewFilterAPIDelete,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
@@ -10,6 +10,7 @@ const buildViewField = (
id: v4(),
fieldMetadataId: v4(),
position: 0,
isActive: true,
isVisible: true,
size: 100,
aggregateOperation: null,
@@ -20,6 +21,7 @@ const buildExistingViewField = (overrides: Partial<ViewField>): ViewField => ({
id: v4(),
fieldMetadataId: v4(),
position: 0,
isActive: true,
isVisible: true,
size: 100,
aggregateOperation: null,
@@ -1,14 +1,20 @@
import { styled } from '@linaria/react';
import { type WidgetCardVariant } from '~/modules/page-layout/widgets/types/WidgetCardVariant';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type WidgetCardVariant } from '~/modules/page-layout/widgets/types/WidgetCardVariant';
type WidgetCardContentStyledProps = {
variant: WidgetCardVariant;
hasHeader: boolean;
isEditable: boolean;
isInVerticalListTab: boolean;
isMobile: boolean;
};
const StyledWidgetCardContent = styled.div<WidgetCardContentStyledProps>`
background-color: ${({ variant, isInVerticalListTab, isMobile }) =>
variant === 'record-page' && isInVerticalListTab && !isMobile
? themeCssVariables.background.secondary
: 'transparent'};
border: ${({ variant, isEditable }) =>
variant === 'record-page' || (variant === 'side-column' && isEditable)
? `1px solid ${themeCssVariables.border.color.medium}`
@@ -40,10 +46,10 @@ const StyledWidgetCardContent = styled.div<WidgetCardContentStyledProps>`
}};
&:empty {
margin-top: ${({ hasHeader, variant, isEditable }) => {
if (hasHeader && variant === 'side-column' && !isEditable) return '0';
return hasHeader ? themeCssVariables.spacing[2] : '0';
}};
border: none;
border-radius: 0;
margin-top: 0;
padding: 0;
}
`;
@@ -51,6 +57,8 @@ type WidgetCardContentProps = {
variant: WidgetCardVariant;
hasHeader: boolean;
isEditable: boolean;
isInVerticalListTab: boolean;
isMobile: boolean;
hasInteractiveContent?: boolean;
className?: string;
children?: React.ReactNode;
@@ -60,6 +68,8 @@ export const WidgetCardContent = ({
variant,
hasHeader,
isEditable,
isInVerticalListTab,
isMobile,
hasInteractiveContent = false,
className,
children,
@@ -77,6 +87,8 @@ export const WidgetCardContent = ({
variant={variant}
hasHeader={hasHeader}
isEditable={isEditable}
isInVerticalListTab={isInVerticalListTab}
isMobile={isMobile}
className={className}
onClick={handleContentClick}
>
@@ -10,13 +10,10 @@ import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget';
import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields';
import { getFieldWidgetDefaultDisplayMode } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig';
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
@@ -91,11 +88,6 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
const { insertCreatedWidgetAtContext } =
useInsertCreatedWidgetAtContext(pageLayoutId);
const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget();
const { deleteViewForRecordTableWidget } =
useDeleteViewForRecordTableWidget();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetObjectNameSingular,
});
@@ -142,28 +134,11 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
return;
}
if (isDefined(existingWidget)) {
const viewId = getWidgetConfigurationViewId(existingWidget.configuration);
if (isDefined(viewId)) {
if (existingWidget.type === WidgetType.RECORD_TABLE) {
deleteViewForRecordTableWidget(viewId);
}
if (existingWidget.type === WidgetType.FIELDS) {
deleteViewForFieldsWidget(viewId);
}
}
}
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: removeWidgetFromTab(prev.tabs, tabId, pageLayoutEditingWidgetId),
}));
}, [
deleteViewForFieldsWidget,
deleteViewForRecordTableWidget,
existingWidget,
isReplaceMode,
pageLayoutDraftState,
pageLayoutEditingWidgetId,
@@ -328,6 +303,7 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
const newWidget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
id: widgetId,
isActive: true,
pageLayoutTabId: tabId,
title: frontComponent.name,
type: WidgetType.FRONT_COMPONENT,
@@ -1,10 +1,11 @@
import { act, renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import {
currentWorkspaceMemberState,
type CurrentWorkspaceMember,
} from '@/auth/states/currentWorkspaceMemberState';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { resetJotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const updateOneRecordMock = jest.fn();
@@ -14,11 +15,7 @@ jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
}),
}));
const workspaceMember: Omit<
WorkspaceMember,
'createdAt' | 'updatedAt' | 'userId'
> = {
__typename: 'WorkspaceMember',
const workspaceMember: CurrentWorkspaceMember = {
id: 'id',
name: {
firstName: 'firstName',
@@ -31,20 +28,16 @@ const workspaceMember: Omit<
describe('useColorScheme', () => {
it('should update color scheme', async () => {
const store = resetJotaiStore();
store.set(currentWorkspaceMemberState.atom, workspaceMember);
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
<JotaiProvider store={store}>{children}</JotaiProvider>
);
const { result } = renderHook(
() => {
const colorScheme = useColorScheme();
const setCurrentWorkspaceMember = useSetAtomState(
currentWorkspaceMemberState,
);
setCurrentWorkspaceMember(workspaceMember);
return colorScheme;
},
{
@@ -58,7 +51,6 @@ describe('useColorScheme', () => {
await result.current.setColorScheme('Dark');
});
// FIXME: For some reason, the color gets unset
// expect(result.current.colorScheme).toEqual('Dark');
expect(result.current.colorScheme).toEqual('Dark');
});
});
@@ -10,6 +10,7 @@ export const VIEW_FIELD_FRAGMENT = gql`
size
aggregateOperation
viewFieldGroupId
isActive
createdAt
updatedAt
deletedAt
@@ -9,6 +9,7 @@ export const VIEW_FIELD_GROUP_FRAGMENT = gql`
position
isVisible
viewId
isActive
createdAt
updatedAt
deletedAt
@@ -104,11 +104,11 @@ export const usePerformViewFilterGroupAPIPersist = () => {
[apolloClient],
);
const performViewFilterGroupAPIDelete = useCallback(
(viewFilterGroupIdsToDelete: string[]) => {
if (!viewFilterGroupIdsToDelete.length) return;
const performViewFilterGroupAPIDestroy = useCallback(
(viewFilterGroupIdsToDestroy: string[]) => {
if (!viewFilterGroupIdsToDestroy.length) return;
return Promise.all(
viewFilterGroupIdsToDelete.map((viewFilterGroupId) =>
viewFilterGroupIdsToDestroy.map((viewFilterGroupId) =>
apolloClient.mutate<{ destroyViewFilterGroup: ViewFilterGroup }>({
mutation: DESTROY_VIEW_FILTER_GROUP,
variables: {
@@ -124,6 +124,6 @@ export const usePerformViewFilterGroupAPIPersist = () => {
return {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
};
};
@@ -1,14 +1,10 @@
import { useSaveAnyFieldFilterToView } from '@/views/hooks/useSaveAnyFieldFilterToView';
import { useSaveRecordFilterGroupsToViewFilterGroups } from '@/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups';
import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters';
import { useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters } from '@/views/hooks/useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters';
import { useSaveRecordSortsToViewSorts } from '@/views/hooks/useSaveRecordSortsToViewSorts';
export const useSaveCurrentViewFiltersAndSorts = () => {
const { saveRecordFilterGroupsToViewFilterGroups } =
useSaveRecordFilterGroupsToViewFilterGroups();
const { saveRecordFiltersToViewFilters } =
useSaveRecordFiltersToViewFilters();
const { saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters } =
useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters();
const { saveRecordSortsToViewSorts } = useSaveRecordSortsToViewSorts();
@@ -16,8 +12,7 @@ export const useSaveCurrentViewFiltersAndSorts = () => {
const saveCurrentViewFilterAndSorts = async () => {
await saveRecordSortsToViewSorts();
await saveRecordFilterGroupsToViewFilterGroups();
await saveRecordFiltersToViewFilters();
await saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters();
await saveAnyFieldFilterToView();
};
@@ -1,86 +0,0 @@
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
} = usePerformViewFilterGroupAPIPersist();
const { currentView } = useGetCurrentViewOnly();
const currentRecordFilterGroupsCallbackState =
useAtomComponentStateCallbackState(currentRecordFilterGroupsComponentState);
const store = useStore();
const saveRecordFilterGroupsToViewFilterGroups = useCallback(async () => {
if (!canPersistChanges || !isDefined(currentView)) {
return;
}
const currentViewFilterGroups = currentView?.viewFilterGroups ?? [];
const currentRecordFilterGroups = store.get(
currentRecordFilterGroupsCallbackState,
);
const newViewFilterGroups = currentRecordFilterGroups.map(
(recordFilterGroup) =>
mapRecordFilterGroupToViewFilterGroup({
recordFilterGroup,
view: currentView,
}),
);
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupIdsToDelete = viewFilterGroupsToDelete.map(
(viewFilterGroup) => viewFilterGroup.id,
);
await performViewFilterGroupAPICreate(
viewFilterGroupsToCreate,
currentView,
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
await performViewFilterGroupAPIDelete(viewFilterGroupIdsToDelete);
}, [
canPersistChanges,
currentView,
store,
currentRecordFilterGroupsCallbackState,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
]);
return {
saveRecordFilterGroupsToViewFilterGroups,
};
};
@@ -0,0 +1,211 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters =
() => {
const { canPersistChanges } = useCanPersistViewChanges();
const { currentView } = useGetCurrentViewOnly();
const store = useStore();
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDestroy,
} = usePerformViewFilterGroupAPIPersist();
const {
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDestroy,
} = usePerformViewFilterAPIPersist();
const currentRecordFilterGroupsCallbackState =
useAtomComponentStateCallbackState(
currentRecordFilterGroupsComponentState,
);
const currentRecordFiltersCallbackState =
useAtomComponentStateCallbackState(currentRecordFiltersComponentState);
const saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters =
useCallback(async () => {
if (!canPersistChanges || !isDefined(currentView)) {
return;
}
const currentViewFilterGroups = currentView.viewFilterGroups ?? [];
const currentRecordFilterGroups = store.get(
currentRecordFilterGroupsCallbackState,
);
const newViewFilterGroups = currentRecordFilterGroups.map(
(recordFilterGroup) =>
mapRecordFilterGroupToViewFilterGroup({
recordFilterGroup,
view: currentView,
}),
);
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupIdsToDestroy = viewFilterGroupsToDelete.map(
(viewFilterGroup) => viewFilterGroup.id,
);
await performViewFilterGroupAPICreate(
viewFilterGroupsToCreate,
currentView,
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
await performViewFilterGroupAPIDestroy(viewFilterGroupIdsToDestroy);
// Mirror the DB cascade: remove cascade-deleted viewFilters from the store
if (viewFilterGroupIdsToDestroy.length > 0) {
const destroyedIdsSet = new Set(viewFilterGroupIdsToDestroy);
store.set(metadataStoreState.atomFamily('viewFilters'), (prev) => ({
...prev,
current: (prev.current as FlatViewFilter[]).filter(
(viewFilter) =>
!isDefined(viewFilter.viewFilterGroupId) ||
!destroyedIdsSet.has(viewFilter.viewFilterGroupId),
),
}));
}
const currentViewFilters = currentView.viewFilters ?? [];
const currentRecordFilters = store.get(
currentRecordFiltersCallbackState,
);
const newViewFilters = currentRecordFilters.map(
mapRecordFilterToViewFilter,
);
const viewFiltersToCreate = getViewFiltersToCreate(
currentViewFilters,
newViewFilters,
);
const viewFiltersToDelete = getViewFiltersToDelete(
currentViewFilters,
newViewFilters,
).filter(
(viewFilter) =>
!isDefined(viewFilter.viewFilterGroupId) ||
!viewFilterGroupIdsToDestroy.includes(viewFilter.viewFilterGroupId),
);
const viewFiltersToUpdate = getViewFiltersToUpdate(
currentViewFilters,
newViewFilters,
);
const createViewFilterInputs = viewFiltersToCreate.map(
(viewFilter) => ({
input: {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: currentView.id,
value: viewFilter.value,
operand: viewFilter.operand,
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
},
}),
);
const updateViewFilterInputs = viewFiltersToUpdate.map(
(viewFilter) => ({
input: {
id: viewFilter.id,
update: {
value: viewFilter.value,
operand: viewFilter.operand,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
subFieldName: viewFilter.subFieldName ?? null,
},
},
}),
);
const destroyViewFilterInputs = viewFiltersToDelete.map(
(viewFilter) => ({
input: {
id: viewFilter.id,
},
}),
);
const createResult = await performViewFilterAPICreate(
createViewFilterInputs,
);
if (createResult.status === 'failed') {
return;
}
const updateResult = await performViewFilterAPIUpdate(
updateViewFilterInputs,
);
if (updateResult.status === 'failed') {
return;
}
const deleteResult = await performViewFilterAPIDestroy(
destroyViewFilterInputs,
);
if (deleteResult.status === 'failed') {
return;
}
}, [
canPersistChanges,
currentView,
store,
currentRecordFilterGroupsCallbackState,
currentRecordFiltersCallbackState,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDestroy,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDestroy,
]);
return {
saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters,
};
};
@@ -1,137 +0,0 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFiltersToViewFilters = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const {
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
} = usePerformViewFilterAPIPersist();
const contextStoreCurrentViewId = useAtomComponentStateValue(
contextStoreCurrentViewIdComponentState,
);
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
currentRecordFiltersComponentState,
);
const store = useStore();
const saveRecordFiltersToViewFilters = useCallback(async () => {
const views = store.get(viewsSelector.atom);
const currentView = views.find(
(view) => view.id === contextStoreCurrentViewId,
);
if (!isDefined(currentView)) {
return;
}
if (!canPersistChanges || !isDefined(currentView)) {
return;
}
const currentViewFilters = currentView?.viewFilters ?? [];
const currentRecordFilters = store.get(currentRecordFiltersCallbackState);
const newViewFilters = currentRecordFilters.map(
mapRecordFilterToViewFilter,
);
const viewFiltersToCreate = getViewFiltersToCreate(
currentViewFilters,
newViewFilters,
);
const viewFiltersToDelete = getViewFiltersToDelete(
currentViewFilters,
newViewFilters,
);
const viewFiltersToUpdate = getViewFiltersToUpdate(
currentViewFilters,
newViewFilters,
);
const createViewFilterInputs = viewFiltersToCreate.map((viewFilter) => ({
input: {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: currentView.id,
value: viewFilter.value,
operand: viewFilter.operand,
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
},
}));
const updateViewFilterInputs = viewFiltersToUpdate.map((viewFilter) => ({
input: {
id: viewFilter.id,
update: {
value: viewFilter.value,
operand: viewFilter.operand,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
subFieldName: viewFilter.subFieldName ?? null,
},
},
}));
const deleteViewFilterInputs = viewFiltersToDelete.map((viewFilter) => ({
input: {
id: viewFilter.id,
},
}));
const createResult = await performViewFilterAPICreate(
createViewFilterInputs,
);
if (createResult.status === 'failed') {
return;
}
const updateResult = await performViewFilterAPIUpdate(
updateViewFilterInputs,
);
if (updateResult.status === 'failed') {
return;
}
const deleteResult = await performViewFilterAPIDelete(
deleteViewFilterInputs,
);
if (deleteResult.status === 'failed') {
return;
}
}, [
store,
canPersistChanges,
currentRecordFiltersCallbackState,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
contextStoreCurrentViewId,
]);
return {
saveRecordFiltersToViewFilters,
};
};
@@ -16,7 +16,7 @@ export const useSaveRecordSortsToViewSorts = () => {
const {
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
performViewSortAPIDestroy,
} = usePerformViewSortAPIPersist();
const { currentView } = useGetCurrentViewOnly();
@@ -71,7 +71,7 @@ export const useSaveRecordSortsToViewSorts = () => {
},
}));
const deleteViewSortInputs = viewSortsToDelete.map((viewSort) => ({
const destroyViewSortInputs = viewSortsToDelete.map((viewSort) => ({
input: {
id: viewSort.id,
},
@@ -87,7 +87,7 @@ export const useSaveRecordSortsToViewSorts = () => {
return;
}
const deleteResult = await performViewSortAPIDelete(deleteViewSortInputs);
const deleteResult = await performViewSortAPIDestroy(destroyViewSortInputs);
if (deleteResult.status === 'failed') {
return;
}
@@ -98,7 +98,7 @@ export const useSaveRecordSortsToViewSorts = () => {
currentRecordSortsCallbackState,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
performViewSortAPIDestroy,
]);
return {
@@ -24,7 +24,7 @@ export const viewsSelector = createAtomSelector<ViewWithRelations[]>({
flatObjectMetadataItems.map((item) => [item.id, item]),
);
const flatViewFields = get(metadataStoreState, 'viewFields')
const allFlatViewFields = get(metadataStoreState, 'viewFields')
.current as FlatViewField[];
const flatViewFilters = get(metadataStoreState, 'viewFilters')
.current as FlatViewFilter[];
@@ -34,9 +34,14 @@ export const viewsSelector = createAtomSelector<ViewWithRelations[]>({
.current as FlatViewGroup[];
const flatViewFilterGroups = get(metadataStoreState, 'viewFilterGroups')
.current as FlatViewFilterGroup[];
const flatViewFieldGroups = get(metadataStoreState, 'viewFieldGroups')
const allFlatViewFieldGroups = get(metadataStoreState, 'viewFieldGroups')
.current as FlatViewFieldGroup[];
const flatViewFields = allFlatViewFields.filter((field) => field.isActive);
const flatViewFieldGroups = allFlatViewFieldGroups.filter(
(group) => group.isActive,
);
const viewFieldsByViewId = new Map<string, FlatViewField[]>();
const viewFiltersByViewId = new Map<string, FlatViewFilter[]>();
const viewSortsByViewId = new Map<string, FlatViewSort[]>();
@@ -8,6 +8,7 @@ export type ViewField = {
viewId?: string;
fieldMetadataId: string;
position: number;
isActive: boolean;
isVisible: boolean;
size: number;
aggregateOperation?: AggregateOperations | null;
@@ -4,6 +4,7 @@ export type ViewFieldGroup = {
id: string;
name: string;
position: number;
isActive: boolean;
isVisible: boolean;
viewId: string;
viewFields: ViewField[];
@@ -60,6 +60,7 @@ describe('mapViewFieldsToColumnDefinitions', () => {
position: 1,
size: 1,
isVisible: false,
isActive: true,
definition: {
fieldMetadataId: '1',
label: 'label 1',
@@ -78,6 +79,7 @@ describe('mapViewFieldsToColumnDefinitions', () => {
position: 2,
size: 2,
isVisible: false,
isActive: true,
definition: {
fieldMetadataId: '2',
label: 'label 2',
@@ -96,6 +98,7 @@ describe('mapViewFieldsToColumnDefinitions', () => {
position: 3,
size: 3,
isVisible: true,
isActive: true,
definition: {
fieldMetadataId: '3',
label: 'label 3',
@@ -188,6 +191,7 @@ describe('mapColumnDefinitionsToViewFields', () => {
fieldMetadataId: 1,
position: 1,
isVisible: true,
isActive: true,
definition: columnDefinitions[0],
size: undefined,
},
@@ -197,6 +201,7 @@ describe('mapColumnDefinitionsToViewFields', () => {
position: 2,
size: 200,
isVisible: false,
isActive: true,
definition: columnDefinitions[1],
},
];
@@ -12,6 +12,7 @@ export const mapBoardFieldDefinitionsToViewFields = (
size: 0,
position: fieldDefinition.position,
isVisible: fieldDefinition.isVisible ?? true,
isActive: true,
definition: fieldDefinition,
}),
);

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