From c8bfbf00d770a664c873fdc7359cb0b91bce9286 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:25:36 +0200 Subject: [PATCH] [`CoreViewField` BREAKING_CHANGES] Refactor view field service v2 and resolver (#14396) ## Introduction ### Twenty-sever Standardizing resolver input and transpilation models + return type on destroy and delete ~~Finally~~ Did plug everything under a feature flag and add coverage Next will do same for the view resolver and service v2 ### Twenty-front Refactored view field service in order to use codegenerated strictly typed mutations and adapt to new api contract --- .../src/generated-metadata/graphql.ts | 87 ++++--- .../twenty-front/src/generated/graphql.ts | 83 ++++--- .../hooks/useViewFieldAggregateOperation.tsx | 17 +- .../graphql/fragments/viewFieldFragment.ts | 3 + .../graphql/mutations/deleteCoreViewField.ts | 8 +- .../graphql/mutations/destroyCoreViewField.ts | 8 +- .../graphql/mutations/updateCoreViewField.ts | 4 +- .../internal/usePersistViewFieldRecords.ts | 136 ++++++---- .../hooks/useCreateViewFromCurrentView.ts | 13 +- .../useRefreshCoreViewsByObjectMetadataId.ts | 14 +- .../views/hooks/useSaveCurrentViewFields.ts | 116 ++++++--- .../useTriggerViewFieldOptimisticEffect.ts | 4 +- .../exceptions/flat-entity-maps.exception.ts | 14 ++ .../exceptions/flat-entity.exception.ts | 14 -- ...ntity-to-flat-entity-maps-or-throw.util.ts | 10 +- ...ity-from-flat-entity-maps-or-throw.util.ts | 12 +- ...by-id-in-flat-entity-maps-or-throw.util.ts | 30 +++ .../view/controllers/view-field.controller.ts | 2 +- .../dtos/inputs/create-view-field.input.ts | 20 ++ .../dtos/inputs/delete-view-field.input.ts | 15 ++ .../dtos/inputs/destroy-view-field.input.ts | 15 ++ .../dtos/inputs/update-view-field.input.ts | 39 ++- ...input-to-flat-view-field-to-create.util.ts | 37 +++ ...-input-to-flat-view-field-or-throw.util.ts | 40 +++ ...-input-to-flat-view-field-or-throw.util.ts | 37 +++ ...ld-to-flat-view-field-with-default.util.ts | 27 -- ...flat-view-field-to-update-or-throw.util.ts | 50 ++++ .../view/resolvers/view-field.resolver.ts | 115 +++++++-- .../services/tests/view-field.service.spec.ts | 15 +- .../view/services/view-field-v2.service.ts | 232 ++++++++++-------- .../view/services/view-field.service.ts | 26 +- .../engine/core-modules/view/view.module.ts | 2 + .../merge-update-in-existing-record.util.ts | 21 ++ .../view-field-resolver.integration-spec.ts | 23 +- ...elete-view-field-operation-factory.util.ts | 11 +- ...stroy-view-field-operation-factory.util.ts | 11 +- ...pdate-view-field-operation-factory.util.ts | 15 +- .../graphql/utils/view-data-factory.util.ts | 3 +- .../suites/view-field.integration-spec.ts | 5 +- .../src/testing/types/TestingGenerics.type.ts | 3 +- 40 files changed, 940 insertions(+), 397 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity-maps.exception.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/dtos/inputs/delete-view-field.input.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/dtos/inputs/destroy-view-field.input.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts create mode 100644 packages/twenty-server/src/utils/merge-update-in-existing-record.util.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 6ecba74c2e0..740f6e54df6 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -917,6 +917,11 @@ export type DeleteTwoFactorAuthenticationMethodOutput = { success: Scalars['Boolean']; }; +export type DeleteViewFieldInput = { + /** The id of the view field to delete. */ + id: Scalars['UUID']; +}; + export type DeleteWebhookDto = { id: Scalars['UUID']; }; @@ -937,6 +942,11 @@ export type DeletedWorkspaceMember = { userWorkspaceId?: Maybe; }; +export type DestroyViewFieldInput = { + /** The id of the view field to destroy. */ + id: Scalars['UUID']; +}; + /** Schema update on a table */ export enum DistantTableUpdate { COLUMNS_ADDED = 'COLUMNS_ADDED', @@ -1446,7 +1456,7 @@ export type Mutation = { deactivateWorkflowVersion: Scalars['Boolean']; deleteApprovedAccessDomain: Scalars['Boolean']; deleteCoreView: Scalars['Boolean']; - deleteCoreViewField: Scalars['Boolean']; + deleteCoreViewField: CoreViewField; deleteCoreViewFilter: Scalars['Boolean']; deleteCoreViewFilterGroup: Scalars['Boolean']; deleteCoreViewGroup: Scalars['Boolean']; @@ -1472,7 +1482,7 @@ export type Mutation = { deleteWorkflowVersionStep: WorkflowVersionStepChanges; deleteWorkspaceInvitation: Scalars['String']; destroyCoreView: Scalars['Boolean']; - destroyCoreViewField: Scalars['Boolean']; + destroyCoreViewField: CoreViewField; destroyCoreViewFilter: Scalars['Boolean']; destroyCoreViewFilterGroup: Scalars['Boolean']; destroyCoreViewGroup: Scalars['Boolean']; @@ -1777,7 +1787,7 @@ export type MutationDeleteCoreViewArgs = { export type MutationDeleteCoreViewFieldArgs = { - id: Scalars['String']; + input: DeleteViewFieldInput; }; @@ -1897,7 +1907,7 @@ export type MutationDestroyCoreViewArgs = { export type MutationDestroyCoreViewFieldArgs = { - id: Scalars['String']; + input: DestroyViewFieldInput; }; @@ -2163,7 +2173,6 @@ export type MutationUpdateCoreViewArgs = { export type MutationUpdateCoreViewFieldArgs = { - id: Scalars['String']; input: UpdateViewFieldInput; }; @@ -3604,6 +3613,13 @@ export type UpdateServerlessFunctionInput = { }; export type UpdateViewFieldInput = { + /** The id of the view field to update */ + id: Scalars['UUID']; + /** The view field to update */ + update: UpdateViewFieldInputUpdates; +}; + +export type UpdateViewFieldInputUpdates = { aggregateOperation?: InputMaybe; isVisible?: InputMaybe; position?: InputMaybe; @@ -4973,7 +4989,7 @@ export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutationVaria export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutation = { __typename?: 'Mutation', verifyTwoFactorAuthenticationMethodForAuthenticatedUser: { __typename?: 'VerifyTwoFactorAuthenticationMethodOutput', success: boolean } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId?: string | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: string } | null, views?: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }> | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId?: string | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: string } | null, views?: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }> | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }; @@ -4992,15 +5008,15 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId?: string | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: string } | null, views?: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }> | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId?: string | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: string } | null, views?: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }> | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; -export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }; +export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }; export type ViewFilterFragmentFragment = { __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }; export type ViewFilterGroupFragmentFragment = { __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }; -export type ViewFragmentFragment = { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }; +export type ViewFragmentFragment = { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }; export type ViewGroupFragmentFragment = { __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }; @@ -5011,14 +5027,14 @@ export type CreateCoreViewMutationVariables = Exact<{ }>; -export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> } }; +export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> } }; export type CreateCoreViewFieldMutationVariables = Exact<{ input: CreateViewFieldInput; }>; -export type CreateCoreViewFieldMutation = { __typename?: 'Mutation', createCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null } }; +export type CreateCoreViewFieldMutation = { __typename?: 'Mutation', createCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type CreateCoreViewFilterMutationVariables = Exact<{ input: CreateViewFilterInput; @@ -5056,11 +5072,11 @@ export type DeleteCoreViewMutationVariables = Exact<{ export type DeleteCoreViewMutation = { __typename?: 'Mutation', deleteCoreView: boolean }; export type DeleteCoreViewFieldMutationVariables = Exact<{ - id: Scalars['String']; + input: DeleteViewFieldInput; }>; -export type DeleteCoreViewFieldMutation = { __typename?: 'Mutation', deleteCoreViewField: boolean }; +export type DeleteCoreViewFieldMutation = { __typename?: 'Mutation', deleteCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DeleteCoreViewFilterMutationVariables = Exact<{ id: Scalars['String']; @@ -5098,11 +5114,11 @@ export type DestroyCoreViewMutationVariables = Exact<{ export type DestroyCoreViewMutation = { __typename?: 'Mutation', destroyCoreView: boolean }; export type DestroyCoreViewFieldMutationVariables = Exact<{ - id: Scalars['String']; + input: DestroyViewFieldInput; }>; -export type DestroyCoreViewFieldMutation = { __typename?: 'Mutation', destroyCoreViewField: boolean }; +export type DestroyCoreViewFieldMutation = { __typename?: 'Mutation', destroyCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DestroyCoreViewFilterMutationVariables = Exact<{ id: Scalars['String']; @@ -5138,15 +5154,14 @@ export type UpdateCoreViewMutationVariables = Exact<{ }>; -export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> } }; +export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> } }; export type UpdateCoreViewFieldMutationVariables = Exact<{ - id: Scalars['String']; input: UpdateViewFieldInput; }>; -export type UpdateCoreViewFieldMutation = { __typename?: 'Mutation', updateCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null } }; +export type UpdateCoreViewFieldMutation = { __typename?: 'Mutation', updateCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type UpdateCoreViewFilterMutationVariables = Exact<{ id: Scalars['String']; @@ -5185,7 +5200,7 @@ export type FindManyCoreViewFieldsQueryVariables = Exact<{ }>; -export type FindManyCoreViewFieldsQuery = { __typename?: 'Query', getCoreViewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }> }; +export type FindManyCoreViewFieldsQuery = { __typename?: 'Query', getCoreViewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; export type FindManyCoreViewFilterGroupsQueryVariables = Exact<{ viewId?: InputMaybe; @@ -5220,21 +5235,21 @@ export type FindManyCoreViewsQueryVariables = Exact<{ }>; -export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }> }; +export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> }> }; export type FindOneCoreViewQueryVariables = Exact<{ id: Scalars['String']; }>; -export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> } | null }; +export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string }> } | null }; export type FindOneCoreViewFieldQueryVariables = Exact<{ id: Scalars['String']; }>; -export type FindOneCoreViewFieldQuery = { __typename?: 'Query', getCoreViewField?: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null } | null }; +export type FindOneCoreViewFieldQuery = { __typename?: 'Query', getCoreViewField?: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } | null }; export type FindOneCoreViewFilterQueryVariables = Exact<{ id: Scalars['String']; @@ -5643,6 +5658,9 @@ export const ViewFieldFragmentFragmentDoc = gql` position size aggregateOperation + createdAt + updatedAt + deletedAt } `; export const ViewFilterFragmentFragmentDoc = gql` @@ -10862,10 +10880,12 @@ export type DeleteCoreViewMutationHookResult = ReturnType; export type DeleteCoreViewMutationOptions = Apollo.BaseMutationOptions; export const DeleteCoreViewFieldDocument = gql` - mutation DeleteCoreViewField($id: String!) { - deleteCoreViewField(id: $id) + mutation DeleteCoreViewField($input: DeleteViewFieldInput!) { + deleteCoreViewField(input: $input) { + ...ViewFieldFragment + } } - `; + ${ViewFieldFragmentFragmentDoc}`; export type DeleteCoreViewFieldMutationFn = Apollo.MutationFunction; /** @@ -10881,7 +10901,7 @@ export type DeleteCoreViewFieldMutationFn = Apollo.MutationFunction; export type DestroyCoreViewMutationOptions = Apollo.BaseMutationOptions; export const DestroyCoreViewFieldDocument = gql` - mutation DestroyCoreViewField($id: String!) { - destroyCoreViewField(id: $id) + mutation DestroyCoreViewField($input: DestroyViewFieldInput!) { + destroyCoreViewField(input: $input) { + ...ViewFieldFragment + } } - `; + ${ViewFieldFragmentFragmentDoc}`; export type DestroyCoreViewFieldMutationFn = Apollo.MutationFunction; /** @@ -11067,7 +11089,7 @@ export type DestroyCoreViewFieldMutationFn = Apollo.MutationFunction; export type UpdateCoreViewMutationOptions = Apollo.BaseMutationOptions; export const UpdateCoreViewFieldDocument = gql` - mutation UpdateCoreViewField($id: String!, $input: UpdateViewFieldInput!) { - updateCoreViewField(id: $id, input: $input) { + mutation UpdateCoreViewField($input: UpdateViewFieldInput!) { + updateCoreViewField(input: $input) { ...ViewFieldFragment } } @@ -11258,7 +11280,6 @@ export type UpdateCoreViewFieldMutationFn = Apollo.MutationFunction; }; +export type DestroyViewFieldInput = { + /** The id of the view field to destroy. */ + id: Scalars['UUID']; +}; + /** Schema update on a table */ export enum DistantTableUpdate { COLUMNS_ADDED = 'COLUMNS_ADDED', @@ -1402,7 +1412,7 @@ export type Mutation = { deactivateWorkflowVersion: Scalars['Boolean']; deleteApprovedAccessDomain: Scalars['Boolean']; deleteCoreView: Scalars['Boolean']; - deleteCoreViewField: Scalars['Boolean']; + deleteCoreViewField: CoreViewField; deleteCoreViewFilter: Scalars['Boolean']; deleteCoreViewFilterGroup: Scalars['Boolean']; deleteCoreViewGroup: Scalars['Boolean']; @@ -1427,7 +1437,7 @@ export type Mutation = { deleteWorkflowVersionStep: WorkflowVersionStepChanges; deleteWorkspaceInvitation: Scalars['String']; destroyCoreView: Scalars['Boolean']; - destroyCoreViewField: Scalars['Boolean']; + destroyCoreViewField: CoreViewField; destroyCoreViewFilter: Scalars['Boolean']; destroyCoreViewFilterGroup: Scalars['Boolean']; destroyCoreViewGroup: Scalars['Boolean']; @@ -1713,7 +1723,7 @@ export type MutationDeleteCoreViewArgs = { export type MutationDeleteCoreViewFieldArgs = { - id: Scalars['String']; + input: DeleteViewFieldInput; }; @@ -1828,7 +1838,7 @@ export type MutationDestroyCoreViewArgs = { export type MutationDestroyCoreViewFieldArgs = { - id: Scalars['String']; + input: DestroyViewFieldInput; }; @@ -2079,7 +2089,6 @@ export type MutationUpdateCoreViewArgs = { export type MutationUpdateCoreViewFieldArgs = { - id: Scalars['String']; input: UpdateViewFieldInput; }; @@ -3442,6 +3451,13 @@ export type UpdateServerlessFunctionInput = { }; export type UpdateViewFieldInput = { + /** The id of the view field to update */ + id: Scalars['UUID']; + /** The view field to update */ + update: UpdateViewFieldInputUpdates; +}; + +export type UpdateViewFieldInputUpdates = { aggregateOperation?: InputMaybe; isVisible?: InputMaybe; position?: InputMaybe; @@ -3923,13 +3939,13 @@ export type OnDbEventSubscriptionVariables = Exact<{ export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array | null, record: any } }; -export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }; +export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }; export type ViewFilterFragmentFragment = { __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }; export type ViewFilterGroupFragmentFragment = { __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }; -export type ViewFragmentFragment = { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> }; +export type ViewFragmentFragment = { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> }; export type ViewGroupFragmentFragment = { __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }; @@ -3940,14 +3956,14 @@ export type CreateCoreViewMutationVariables = Exact<{ }>; -export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> } }; +export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> } }; export type CreateCoreViewFieldMutationVariables = Exact<{ input: CreateViewFieldInput; }>; -export type CreateCoreViewFieldMutation = { __typename?: 'Mutation', createCoreViewField: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null } }; +export type CreateCoreViewFieldMutation = { __typename?: 'Mutation', createCoreViewField: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type CreateCoreViewFilterMutationVariables = Exact<{ input: CreateViewFilterInput; @@ -3985,11 +4001,11 @@ export type DeleteCoreViewMutationVariables = Exact<{ export type DeleteCoreViewMutation = { __typename?: 'Mutation', deleteCoreView: boolean }; export type DeleteCoreViewFieldMutationVariables = Exact<{ - id: Scalars['String']; + input: DeleteViewFieldInput; }>; -export type DeleteCoreViewFieldMutation = { __typename?: 'Mutation', deleteCoreViewField: boolean }; +export type DeleteCoreViewFieldMutation = { __typename?: 'Mutation', deleteCoreViewField: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DeleteCoreViewFilterMutationVariables = Exact<{ id: Scalars['String']; @@ -4027,11 +4043,11 @@ export type DestroyCoreViewMutationVariables = Exact<{ export type DestroyCoreViewMutation = { __typename?: 'Mutation', destroyCoreView: boolean }; export type DestroyCoreViewFieldMutationVariables = Exact<{ - id: Scalars['String']; + input: DestroyViewFieldInput; }>; -export type DestroyCoreViewFieldMutation = { __typename?: 'Mutation', destroyCoreViewField: boolean }; +export type DestroyCoreViewFieldMutation = { __typename?: 'Mutation', destroyCoreViewField: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DestroyCoreViewFilterMutationVariables = Exact<{ id: Scalars['String']; @@ -4067,15 +4083,14 @@ export type UpdateCoreViewMutationVariables = Exact<{ }>; -export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> } }; +export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> } }; export type UpdateCoreViewFieldMutationVariables = Exact<{ - id: Scalars['String']; input: UpdateViewFieldInput; }>; -export type UpdateCoreViewFieldMutation = { __typename?: 'Mutation', updateCoreViewField: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null } }; +export type UpdateCoreViewFieldMutation = { __typename?: 'Mutation', updateCoreViewField: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type UpdateCoreViewFilterMutationVariables = Exact<{ id: Scalars['String']; @@ -4114,7 +4129,7 @@ export type FindManyCoreViewFieldsQueryVariables = Exact<{ }>; -export type FindManyCoreViewFieldsQuery = { __typename?: 'Query', getCoreViewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }> }; +export type FindManyCoreViewFieldsQuery = { __typename?: 'Query', getCoreViewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; export type FindManyCoreViewFilterGroupsQueryVariables = Exact<{ viewId?: InputMaybe; @@ -4149,21 +4164,21 @@ export type FindManyCoreViewsQueryVariables = Exact<{ }>; -export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> }> }; +export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> }> }; export type FindOneCoreViewQueryVariables = Exact<{ id: Scalars['String']; }>; -export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> } | null }; +export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> } | null }; export type FindOneCoreViewFieldQueryVariables = Exact<{ id: Scalars['String']; }>; -export type FindOneCoreViewFieldQuery = { __typename?: 'Query', getCoreViewField?: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null } | null }; +export type FindOneCoreViewFieldQuery = { __typename?: 'Query', getCoreViewField?: { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } | null }; export type FindOneCoreViewFilterQueryVariables = Exact<{ id: Scalars['String']; @@ -4202,6 +4217,9 @@ export const ViewFieldFragmentFragmentDoc = gql` position size aggregateOperation + createdAt + updatedAt + deletedAt } `; export const ViewFilterFragmentFragmentDoc = gql` @@ -4603,10 +4621,12 @@ export type DeleteCoreViewMutationHookResult = ReturnType; export type DeleteCoreViewMutationOptions = Apollo.BaseMutationOptions; export const DeleteCoreViewFieldDocument = gql` - mutation DeleteCoreViewField($id: String!) { - deleteCoreViewField(id: $id) + mutation DeleteCoreViewField($input: DeleteViewFieldInput!) { + deleteCoreViewField(input: $input) { + ...ViewFieldFragment + } } - `; + ${ViewFieldFragmentFragmentDoc}`; export type DeleteCoreViewFieldMutationFn = Apollo.MutationFunction; /** @@ -4622,7 +4642,7 @@ export type DeleteCoreViewFieldMutationFn = Apollo.MutationFunction; export type DestroyCoreViewMutationOptions = Apollo.BaseMutationOptions; export const DestroyCoreViewFieldDocument = gql` - mutation DestroyCoreViewField($id: String!) { - destroyCoreViewField(id: $id) + mutation DestroyCoreViewField($input: DestroyViewFieldInput!) { + destroyCoreViewField(input: $input) { + ...ViewFieldFragment + } } - `; + ${ViewFieldFragmentFragmentDoc}`; export type DestroyCoreViewFieldMutationFn = Apollo.MutationFunction; /** @@ -4808,7 +4830,7 @@ export type DestroyCoreViewFieldMutationFn = Apollo.MutationFunction; export type UpdateCoreViewMutationOptions = Apollo.BaseMutationOptions; export const UpdateCoreViewFieldDocument = gql` - mutation UpdateCoreViewField($id: String!, $input: UpdateViewFieldInput!) { - updateCoreViewField(id: $id, input: $input) { + mutation UpdateCoreViewField($input: UpdateViewFieldInput!) { + updateCoreViewField(input: $input) { ...ViewFieldFragment } } @@ -4999,7 +5021,6 @@ export type UpdateCoreViewFieldMutationFn = Apollo.MutationFunction { } await updateViewFieldRecords([ { - ...currentViewField, - aggregateOperation: - convertExtendedAggregateOperationToAggregateOperation( - aggregateOperation, - ), + input: { + id: currentViewField.id, + update: { + isVisible: currentViewField.isVisible, + position: currentViewField.position, + size: currentViewField.size, + aggregateOperation: + convertExtendedAggregateOperationToAggregateOperation( + aggregateOperation, + ), + }, + }, }, ]); diff --git a/packages/twenty-front/src/modules/views/graphql/fragments/viewFieldFragment.ts b/packages/twenty-front/src/modules/views/graphql/fragments/viewFieldFragment.ts index cb168a618f5..132c57443bd 100644 --- a/packages/twenty-front/src/modules/views/graphql/fragments/viewFieldFragment.ts +++ b/packages/twenty-front/src/modules/views/graphql/fragments/viewFieldFragment.ts @@ -9,5 +9,8 @@ export const VIEW_FIELD_FRAGMENT = gql` position size aggregateOperation + createdAt + updatedAt + deletedAt } `; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewField.ts b/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewField.ts index 2a79144c0f4..4d25f61bbf5 100644 --- a/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewField.ts +++ b/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewField.ts @@ -1,7 +1,11 @@ +import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment'; import { gql } from '@apollo/client'; export const DELETE_CORE_VIEW_FIELD = gql` - mutation DeleteCoreViewField($id: String!) { - deleteCoreViewField(id: $id) + ${VIEW_FIELD_FRAGMENT} + mutation DeleteCoreViewField($input: DeleteViewFieldInput!) { + deleteCoreViewField(input: $input) { + ...ViewFieldFragment + } } `; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewField.ts b/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewField.ts index 6d9aac7a2f6..caf7d9c11a5 100644 --- a/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewField.ts +++ b/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewField.ts @@ -1,7 +1,11 @@ +import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment'; import { gql } from '@apollo/client'; export const DESTROY_CORE_VIEW_FIELD = gql` - mutation DestroyCoreViewField($id: String!) { - destroyCoreViewField(id: $id) + ${VIEW_FIELD_FRAGMENT} + mutation DestroyCoreViewField($input: DestroyViewFieldInput!) { + destroyCoreViewField(input: $input) { + ...ViewFieldFragment + } } `; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewField.ts b/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewField.ts index 2c8798e8372..3e407fa8743 100644 --- a/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewField.ts +++ b/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewField.ts @@ -3,8 +3,8 @@ import { gql } from '@apollo/client'; export const UPDATE_CORE_VIEW_FIELD = gql` ${VIEW_FIELD_FRAGMENT} - mutation UpdateCoreViewField($id: String!, $input: UpdateViewFieldInput!) { - updateCoreViewField(id: $id, input: $input) { + mutation UpdateCoreViewField($input: UpdateViewFieldInput!) { + updateCoreViewField(input: $input) { ...ViewFieldFragment } } diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts index baf6ab2508b..c2c0f74939a 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts @@ -1,90 +1,128 @@ import { useCallback } from 'react'; -import { v4 } from 'uuid'; -import { CREATE_CORE_VIEW_FIELD } from '@/views/graphql/mutations/createCoreViewField'; -import { UPDATE_CORE_VIEW_FIELD } from '@/views/graphql/mutations/updateCoreViewField'; import { useTriggerViewFieldOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect'; -import { type GraphQLView } from '@/views/types/GraphQLView'; -import { type ViewField } from '@/views/types/ViewField'; -import { useApolloClient } from '@apollo/client'; import { isDefined } from 'twenty-shared/utils'; -import { type CoreViewField } from '~/generated/graphql'; +import { + type CreateCoreViewFieldMutationVariables, + type DeleteCoreViewFieldMutationVariables, + type DestroyCoreViewFieldMutationVariables, + type UpdateCoreViewFieldMutationVariables, + useCreateCoreViewFieldMutation, + useDeleteCoreViewFieldMutation, + useDestroyCoreViewFieldMutation, + useUpdateCoreViewFieldMutation, +} from '~/generated/graphql'; export const usePersistViewFieldRecords = () => { - const apolloClient = useApolloClient(); - const { triggerViewFieldOptimisticEffect } = useTriggerViewFieldOptimisticEffect(); + const [createCoreViewFieldMutation] = useCreateCoreViewFieldMutation(); + const [updateCoreViewFieldMutation] = useUpdateCoreViewFieldMutation(); + const [deleteCoreViewFieldMutation] = useDeleteCoreViewFieldMutation(); + const [destroyCoreViewFieldMutation] = useDestroyCoreViewFieldMutation(); const createCoreViewFieldRecords = useCallback( - ( - viewFieldsToCreate: Omit[], - view: Pick, - ) => { - if (!viewFieldsToCreate.length) return; + (createCoreViewFieldInputs: CreateCoreViewFieldMutationVariables[]) => { + if (createCoreViewFieldInputs.length === 0) { + return; + } + return Promise.all( - viewFieldsToCreate.map((viewField) => - apolloClient.mutate({ - mutation: CREATE_CORE_VIEW_FIELD, - variables: { - input: { - id: v4(), - fieldMetadataId: viewField.fieldMetadataId, - viewId: view.id, - isVisible: viewField.isVisible, - position: viewField.position, - size: viewField.size, - } satisfies Partial, - }, + createCoreViewFieldInputs.map(async (variables) => + createCoreViewFieldMutation({ + variables, update: (_cache, { data }) => { - const record = data?.['createCoreViewField']; - if (!record) return; + const createdViewField = data?.createCoreViewField; + if (!isDefined(createdViewField)) { + return; + } triggerViewFieldOptimisticEffect({ - createdViewFields: [record], + createdViewFields: [createdViewField], }); }, }), ), ); }, - [apolloClient, triggerViewFieldOptimisticEffect], + [triggerViewFieldOptimisticEffect, createCoreViewFieldMutation], ); const updateCoreViewFieldRecords = useCallback( - (viewFieldsToUpdate: Omit[]) => { - if (!viewFieldsToUpdate.length) return; + (createCoreViewFieldInputs: UpdateCoreViewFieldMutationVariables[]) => { + if (createCoreViewFieldInputs.length === 0) { + return; + } return Promise.all( - viewFieldsToUpdate.map((viewField) => - apolloClient.mutate({ - mutation: UPDATE_CORE_VIEW_FIELD, - variables: { - id: viewField.id, - input: { - isVisible: viewField.isVisible, - position: viewField.position, - size: viewField.size, - aggregateOperation: viewField.aggregateOperation, - } satisfies Partial, - }, + createCoreViewFieldInputs.map((variables) => + updateCoreViewFieldMutation({ + variables, update: (_cache, { data }) => { - const record = data?.['updateCoreViewField']; - if (!isDefined(record)) return; + const updatedViewField = data?.updateCoreViewField; + if (!isDefined(updatedViewField)) { + return; + } triggerViewFieldOptimisticEffect({ - updatedViewFields: [record], + updatedViewFields: [updatedViewField], }); }, }), ), ); }, - [apolloClient, triggerViewFieldOptimisticEffect], + [triggerViewFieldOptimisticEffect, updateCoreViewFieldMutation], + ); + + const deleteCoreViewFieldRecords = useCallback( + (deleteCoreViewFieldInputs: DeleteCoreViewFieldMutationVariables[]) => { + if (deleteCoreViewFieldInputs.length === 0) { + return; + } + + return Promise.all( + deleteCoreViewFieldInputs.map((variables) => + deleteCoreViewFieldMutation({ + variables, + update: (_cache, { data }) => { + const deletedViewField = data?.deleteCoreViewField; + if (!isDefined(deletedViewField)) { + return; + } + + triggerViewFieldOptimisticEffect({ + deletedViewFields: [deletedViewField], + }); + }, + }), + ), + ); + }, + [triggerViewFieldOptimisticEffect, deleteCoreViewFieldMutation], + ); + + const destroyCoreViewFieldRecords = useCallback( + (destroyCoreViewFieldInputs: DestroyCoreViewFieldMutationVariables[]) => { + if (destroyCoreViewFieldInputs.length === 0) { + return; + } + + return Promise.all( + destroyCoreViewFieldInputs.map((variables) => + destroyCoreViewFieldMutation({ + variables, + }), + ), + ); + }, + [destroyCoreViewFieldMutation], ); return { createViewFieldRecords: createCoreViewFieldRecords, updateViewFieldRecords: updateCoreViewFieldRecords, + deleteViewFieldRecords: deleteCoreViewFieldRecords, + destroyViewFieldRecords: destroyCoreViewFieldRecords, }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index 58734464ee8..09e510e5d39 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -28,7 +28,10 @@ import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; -import { useCreateCoreViewMutation } from '~/generated/graphql'; +import { + type CreateCoreViewFieldMutationVariables, + useCreateCoreViewMutation, +} from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { @@ -138,7 +141,13 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { throw new Error('Failed to create view'); } - await createViewFieldRecords(sourceView.viewFields, { id: newViewId }); + await createViewFieldRecords( + sourceView.viewFields.map( + ({ __typename, ...viewField }) => ({ + input: { ...viewField, viewId: newViewId }, + }), + ), + ); if (type === ViewType.Kanban) { if (!isDefined(kanbanFieldMetadataId)) { diff --git a/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts b/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts index 677f6b02b6b..39d1297ddf9 100644 --- a/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts +++ b/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts @@ -11,7 +11,7 @@ import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordFie import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; import { useFindManyCoreViewsLazyQuery } from '~/generated/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; @@ -71,7 +71,17 @@ export const useRefreshCoreViewsByObjectMetadataId = () => { continue; } - if (!isDeeplyEqual(coreView.viewFields, existingView.viewFields)) { + if ( + !isDeeplyEqual( + coreView.viewFields.map((viewField) => + removePropertiesFromRecord(viewField, [ + 'updatedAt', + 'createdAt', + ]), + ), + existingView.viewFields, + ) + ) { const view = convertCoreViewToView(coreView); set( currentRecordFieldsComponentState.atomFamily({ diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts index 12df40f2c05..af939e62263 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts @@ -6,7 +6,11 @@ import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistVie import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState'; import { type ViewField } from '@/views/types/ViewField'; -import { isDefined } from 'twenty-shared/utils'; +import { + type CreateCoreViewFieldMutationVariables, + type CreateViewFieldInput, + type UpdateCoreViewFieldMutationVariables, +} from '~/generated/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -41,50 +45,82 @@ export const useSaveCurrentViewFields = () => { const currentViewFields = view.viewFields; - const viewFieldsToUpdate = viewFieldsToSave - .map((viewFieldToSave) => { - const existingField = currentViewFields.find( - (currentViewField) => - currentViewField.fieldMetadataId === - viewFieldToSave.fieldMetadataId, - ); + const { viewFieldsToCreate, viewFieldsToUpdate } = + viewFieldsToSave.reduce<{ + viewFieldsToCreate: CreateCoreViewFieldMutationVariables[]; + viewFieldsToUpdate: UpdateCoreViewFieldMutationVariables[]; + }>( + ( + { viewFieldsToCreate, viewFieldsToUpdate }, + { __typename, ...viewFieldToCreateOrUpdate }, + ) => { + const createViewFieldInput: CreateViewFieldInput = { + ...viewFieldToCreateOrUpdate, + viewId: currentViewId, + }; + const existingField = currentViewFields.find( + (currentViewField) => + currentViewField.fieldMetadataId === + createViewFieldInput.fieldMetadataId, + ); - if (isUndefinedOrNull(existingField)) { - return undefined; - } + if (isUndefinedOrNull(existingField)) { + return { + viewFieldsToCreate: [ + ...viewFieldsToCreate, + { input: createViewFieldInput }, + ], + viewFieldsToUpdate, + }; + } - if ( - isDeeplyEqual( - { - position: existingField.position, - size: existingField.size, - isVisible: existingField.isVisible, - }, - { - position: viewFieldToSave.position, - size: viewFieldToSave.size, - isVisible: viewFieldToSave.isVisible, - }, - ) - ) { - return undefined; - } + if ( + isDeeplyEqual( + { + position: existingField.position, + size: existingField.size, + isVisible: existingField.isVisible, + }, + { + position: createViewFieldInput.position, + size: createViewFieldInput.size, + isVisible: createViewFieldInput.isVisible, + }, + ) + ) { + return { + viewFieldsToCreate, + viewFieldsToUpdate, + }; + } - return { ...viewFieldToSave, id: existingField.id }; - }) - .filter(isDefined); - - const viewFieldsToCreate = viewFieldsToSave.filter( - (viewFieldToSave) => - !currentViewFields.some( - (currentViewField) => - currentViewField.fieldMetadataId === - viewFieldToSave.fieldMetadataId, - ), - ); + return { + viewFieldsToCreate, + viewFieldsToUpdate: [ + ...viewFieldsToUpdate, + { + input: { + id: createViewFieldInput.id, + update: { + aggregateOperation: + createViewFieldInput.aggregateOperation, + isVisible: createViewFieldInput.isVisible, + position: createViewFieldInput.position, + size: createViewFieldInput.size, + }, + }, + }, + ], + }; + }, + { + viewFieldsToUpdate: [], + viewFieldsToCreate: [], + }, + ); await Promise.all([ - createViewFieldRecords(viewFieldsToCreate, view), + createViewFieldRecords(viewFieldsToCreate), updateViewFieldRecords(viewFieldsToUpdate), ]); diff --git a/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect.ts b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect.ts index 64bd5de5b7e..2e0fdc30670 100644 --- a/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect.ts @@ -19,8 +19,8 @@ export const useTriggerViewFieldOptimisticEffect = () => { updatedViewFields = [], deletedViewFields = [], }: { - createdViewFields?: CoreViewField[]; - updatedViewFields?: CoreViewField[]; + createdViewFields?: Omit[]; + updatedViewFields?: Omit[]; deletedViewFields?: Pick[]; }) => { const coreViews = getSnapshotValue(snapshot, coreViewsState); diff --git a/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity-maps.exception.ts b/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity-maps.exception.ts new file mode 100644 index 00000000000..7b71726baa1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity-maps.exception.ts @@ -0,0 +1,14 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class FlatEntityMapsException extends CustomException { + code: FlatEntityMapsExceptionCode; + + constructor(message: string, code: FlatEntityMapsExceptionCode) { + super(message, code); + } +} + +export enum FlatEntityMapsExceptionCode { + ENTITY_ALREADY_EXISTS = 'ENTITY_ALREADY_EXISTS', + ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND', +} diff --git a/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts b/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts deleted file mode 100644 index f93d6b95de0..00000000000 --- a/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CustomException } from 'src/utils/custom-exception'; - -export class FlatEntityException extends CustomException { - code: FlatEntityExceptionCode; - - constructor(message: string, code: FlatEntityExceptionCode) { - super(message, code); - } -} - -export enum FlatEntityExceptionCode { - ENTITY_ALREADY_EXISTS = 'ENTITY_ALREADY_EXISTS', - ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND', -} diff --git a/packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts index 707918202f6..8ef4deda395 100644 --- a/packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts +++ b/packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts @@ -1,9 +1,9 @@ import { isDefined } from 'class-validator'; import { - FlatEntityException, - FlatEntityExceptionCode, -} from 'src/engine/core-modules/common/exceptions/flat-entity.exception'; + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception'; import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; @@ -17,9 +17,9 @@ export const addFlatEntityToFlatEntityMapsOrThrow = ({ flatEntityMaps, }: AddFlatEntityToFlatEntityMapsOrThrowArgs): FlatEntityMaps => { if (isDefined(flatEntityMaps.byId[flatEntity.id])) { - throw new FlatEntityException( + throw new FlatEntityMapsException( 'addFlatEntityToFlatEntityMapsOrThrow: flat entity to add already exists', - FlatEntityExceptionCode.ENTITY_ALREADY_EXISTS, + FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS, ); } diff --git a/packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts index 1b00662ac11..8f9970106ae 100644 --- a/packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts +++ b/packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts @@ -1,11 +1,11 @@ import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception'; import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; -import { - FlatEntityException, - FlatEntityExceptionCode, -} from 'src/engine/core-modules/common/exceptions/flat-entity.exception'; export type DeleteFlatEntityFromFlatEntityMapsOrThrowArgs< T extends FlatEntity, @@ -21,9 +21,9 @@ export const deleteFlatEntityFromFlatEntityMapsOrThrow = < entityToDeleteId, }: DeleteFlatEntityFromFlatEntityMapsOrThrowArgs): FlatEntityMaps => { if (!isDefined(flatEntityMaps.byId[entityToDeleteId])) { - throw new FlatEntityException( + throw new FlatEntityMapsException( 'deleteFlatEntityFromFlatEntityMapsOrThrow: entity to delete not found', - FlatEntityExceptionCode.ENTITY_NOT_FOUND, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, ); } diff --git a/packages/twenty-server/src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util.ts new file mode 100644 index 00000000000..5c86ebafb61 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util.ts @@ -0,0 +1,30 @@ +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception'; +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; + +export const findFlatEntityByIdInFlatEntityMapsOrThrow = < + T extends FlatEntity, +>({ + flatEntityMaps, + flatEntityId, +}: { + flatEntityMaps: FlatEntityMaps; + flatEntityId: string; +}): T => { + const flatEntity = flatEntityMaps.byId[flatEntityId]; + + if (!isDefined(flatEntity)) { + throw new FlatEntityMapsException( + t`Could not find flat entity in maps`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return flatEntity; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts b/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts index 7673713fc1c..ab870397730 100644 --- a/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts @@ -75,7 +75,7 @@ export class ViewFieldController { @Patch(':id') async update( @Param('id') id: string, - @Body() input: UpdateViewFieldInput, + @Body() input: UpdateViewFieldInput['update'], @AuthWorkspace() workspace: Workspace, ): Promise { const updatedViewField = await this.viewFieldService.update( diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view-field.input.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view-field.input.ts index 55977e8ae56..cd81a2c1204 100644 --- a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view-field.input.ts +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/create-view-field.input.ts @@ -1,28 +1,48 @@ import { Field, InputType } from '@nestjs/graphql'; +import { + IsBoolean, + IsEnum, + IsNumber, + IsOptional, + IsUUID, +} from 'class-validator'; + import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @InputType() export class CreateViewFieldInput { + @IsOptional() + @IsUUID() @Field(() => UUIDScalarType, { nullable: true }) id?: string; + @IsUUID() @Field(() => UUIDScalarType, { nullable: false }) fieldMetadataId: string; + @IsUUID() @Field(() => UUIDScalarType, { nullable: false }) viewId: string; + @IsOptional() + @IsBoolean() @Field({ nullable: true, defaultValue: true }) isVisible?: boolean; + @IsOptional() + @IsNumber() @Field({ nullable: true, defaultValue: 0 }) size?: number; + @IsOptional() + @IsNumber() @Field({ nullable: true, defaultValue: 0 }) position?: number; + @IsOptional() + @IsEnum(AggregateOperations) @Field(() => AggregateOperations, { nullable: true }) aggregateOperation?: AggregateOperations; } diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/delete-view-field.input.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/delete-view-field.input.ts new file mode 100644 index 00000000000..96fc9477c24 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/delete-view-field.input.ts @@ -0,0 +1,15 @@ +import { InputType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class DeleteViewFieldInput { + @IDField(() => UUIDScalarType, { + description: 'The id of the view field to delete.', + }) + @IsUUID() + id: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/destroy-view-field.input.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/destroy-view-field.input.ts new file mode 100644 index 00000000000..169879e61a0 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/destroy-view-field.input.ts @@ -0,0 +1,15 @@ +import { InputType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class DestroyViewFieldInput { + @IDField(() => UUIDScalarType, { + description: 'The id of the view field to destroy.', + }) + @IsUUID() + id: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view-field.input.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view-field.input.ts index 659f66f21c8..65bb439d9fd 100644 --- a/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view-field.input.ts +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/inputs/update-view-field.input.ts @@ -1,18 +1,55 @@ import { Field, InputType } from '@nestjs/graphql'; +import { Type } from 'class-transformer'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsUUID, + ValidateNested, +} from 'class-validator'; + import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @InputType() -export class UpdateViewFieldInput { +class UpdateViewFieldInputUpdates { + @IsOptional() + @IsBoolean() @Field({ nullable: true }) isVisible?: boolean; + @IsOptional() + @IsNumber() @Field({ nullable: true }) size?: number; + @IsOptional() + @IsNumber() @Field({ nullable: true }) position?: number; + @IsOptional() + @IsEnum(AggregateOperations) @Field(() => AggregateOperations, { nullable: true }) aggregateOperation?: AggregateOperations; } + +@InputType() +export class UpdateViewFieldInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType, { + description: 'The id of the view field to update', + }) + id: string; + + @Type(() => UpdateViewFieldInputUpdates) + @ValidateNested() + @Field(() => UpdateViewFieldInputUpdates, { + description: 'The view field to update', + }) + update: UpdateViewFieldInputUpdates; +} diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts new file mode 100644 index 00000000000..c65a5fbec5d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts @@ -0,0 +1,37 @@ +import { v4 } from 'uuid'; +import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; + +import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({ + createViewFieldInput: rawCreateViewFieldInput, + workspaceId, +}: { + createViewFieldInput: CreateViewFieldInput; + workspaceId: string; +}): FlatViewField => { + const { fieldMetadataId, viewId, ...createViewFieldInput } = + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + rawCreateViewFieldInput, + ['aggregateOperation', 'fieldMetadataId', 'id', 'viewId'], + ); + + const createdAt = new Date(); + const viewFieldId = createViewFieldInput.id ?? v4(); + + return { + id: viewFieldId, + fieldMetadataId, + viewId, + workspaceId, + createdAt: createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: viewFieldId, + isVisible: createViewFieldInput.isVisible ?? true, + size: createViewFieldInput.size ?? 0, + position: createViewFieldInput.position ?? 0, + aggregateOperation: createViewFieldInput.aggregateOperation ?? null, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util.ts new file mode 100644 index 00000000000..bfaa291b7b9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util.ts @@ -0,0 +1,40 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +import { type DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input'; +import { + ViewFieldException, + ViewFieldExceptionCode, +} from 'src/engine/core-modules/view/exceptions/view-field.exception'; +import { type FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export const fromDeleteViewFieldInputToFlatViewFieldOrThrow = ({ + deleteViewFieldInput: rawDeleteViewFieldInput, + flatViewFieldMaps, +}: { + deleteViewFieldInput: DeleteViewFieldInput; + flatViewFieldMaps: FlatViewFieldMaps; +}): FlatViewField => { + const { id: viewFieldId } = extractAndSanitizeObjectStringFields( + rawDeleteViewFieldInput, + ['id'], + ); + + const existingFlatViewFieldToDelete = flatViewFieldMaps.byId[viewFieldId]; + + if (!isDefined(existingFlatViewFieldToDelete)) { + throw new ViewFieldException( + t`View field to delete not found`, + ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + ); + } + + return { + ...existingFlatViewFieldToDelete, + deletedAt: new Date(), + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util.ts new file mode 100644 index 00000000000..43a48df0a95 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util.ts @@ -0,0 +1,37 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +import { type DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input'; +import { + ViewFieldException, + ViewFieldExceptionCode, +} from 'src/engine/core-modules/view/exceptions/view-field.exception'; +import { type FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export const fromDestroyViewFieldInputToFlatViewFieldOrThrow = ({ + destroyViewFieldInput: rawDeleteDestroyViewInput, + flatViewFieldMaps, +}: { + destroyViewFieldInput: DestroyViewFieldInput; + flatViewFieldMaps: FlatViewFieldMaps; +}): FlatViewField => { + const { id: viewFieldId } = extractAndSanitizeObjectStringFields( + rawDeleteDestroyViewInput, + ['id'], + ); + + const existingFlatViewFieldToDestroy = flatViewFieldMaps.byId[viewFieldId]; + + if (!isDefined(existingFlatViewFieldToDestroy)) { + throw new ViewFieldException( + t`View field to destroy not found`, + ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + ); + } + + return existingFlatViewFieldToDestroy; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts deleted file mode 100644 index b97ec21d0f0..00000000000 --- a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { v4 } from 'uuid'; - -import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; - -export const fromPartialFlatViewFieldToFlatViewFieldWithDefault = ( - partialFlatViewField: Partial, -): FlatViewField => { - const createdAt = new Date(); - const viewFieldId = partialFlatViewField.id ?? v4(); - - return { - ...partialFlatViewField, - id: viewFieldId, - fieldMetadataId: partialFlatViewField.fieldMetadataId ?? '', - isVisible: partialFlatViewField.isVisible ?? true, - size: partialFlatViewField.size ?? 0, - position: partialFlatViewField.position ?? 0, - aggregateOperation: partialFlatViewField.aggregateOperation ?? null, - viewId: partialFlatViewField.viewId ?? '', - workspaceId: partialFlatViewField.workspaceId ?? '', - createdAt: createdAt, - updatedAt: createdAt, - deletedAt: null, - universalIdentifier: - partialFlatViewField.universalIdentifier ?? viewFieldId, - }; -}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts new file mode 100644 index 00000000000..77292f810f2 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts @@ -0,0 +1,50 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, +} from 'twenty-shared/utils'; + +import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input'; +import { + ViewFieldException, + ViewFieldExceptionCode, +} from 'src/engine/core-modules/view/exceptions/view-field.exception'; +import { FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant'; +import { type FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; +import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; + +export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({ + updateViewFieldInput: rawUpdateViewFieldInput, + flatViewFieldMaps, +}: { + updateViewFieldInput: UpdateViewFieldInput; + flatViewFieldMaps: FlatViewFieldMaps; +}): FlatViewField => { + const { id: viewFieldToUpdateId } = + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + rawUpdateViewFieldInput, + ['id'], + ); + + const existingFlatViewFieldToUpdate = + flatViewFieldMaps.byId[viewFieldToUpdateId]; + + if (!isDefined(existingFlatViewFieldToUpdate)) { + throw new ViewFieldException( + t`View field to update not found`, + ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + ); + } + const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( + rawUpdateViewFieldInput.update, + FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE, + ); + + return mergeUpdateInExistingRecord({ + existing: existingFlatViewFieldToUpdate, + properties: FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE, + update: updatedEditableFieldProperties, + }); +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/resolvers/view-field.resolver.ts b/packages/twenty-server/src/engine/core-modules/view/resolvers/view-field.resolver.ts index 0c5ed785a4a..a66a14170d2 100644 --- a/packages/twenty-server/src/engine/core-modules/view/resolvers/view-field.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/view/resolvers/view-field.resolver.ts @@ -1,12 +1,15 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { isDefined } from 'twenty-shared/utils'; - +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input'; +import { DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input'; +import { DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input'; import { UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input'; import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto'; import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { ViewFieldV2Service } from 'src/engine/core-modules/view/services/view-field-v2.service'; import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service'; import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -17,7 +20,11 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; @UseFilters(ViewGraphqlApiExceptionFilter) @UseGuards(WorkspaceAuthGuard) export class ViewFieldResolver { - constructor(private readonly viewFieldService: ViewFieldService) {} + constructor( + private readonly viewFieldService: ViewFieldService, + private readonly featureFlagService: FeatureFlagService, + private readonly viewFieldV2Service: ViewFieldV2Service, + ) {} @Query(() => [ViewFieldDTO]) async getCoreViewFields( @@ -37,47 +44,101 @@ export class ViewFieldResolver { @Mutation(() => ViewFieldDTO) async updateCoreViewField( - @Args('id', { type: () => String }) id: string, - @Args('input') input: UpdateViewFieldInput, - @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.viewFieldService.update(id, workspace.id, input); + @Args('input') updateViewFieldInput: UpdateViewFieldInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + const isWorkspaceMigrationV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + workspaceId, + ); + + if (isWorkspaceMigrationV2Enabled) { + return await this.viewFieldV2Service.updateOne({ + updateViewFieldInput, + workspaceId, + }); + } + + return this.viewFieldService.update( + updateViewFieldInput.id, + workspaceId, + updateViewFieldInput.update, + ); } @Mutation(() => ViewFieldDTO) async createCoreViewField( - @Args('input') input: CreateViewFieldInput, - @AuthWorkspace() workspace: Workspace, - ): Promise { + @Args('input') createViewFieldInput: CreateViewFieldInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + const isWorkspaceMigrationV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + workspaceId, + ); + + if (isWorkspaceMigrationV2Enabled) { + return await this.viewFieldV2Service.createOne({ + createViewFieldInput, + workspaceId, + }); + } + return this.viewFieldService.create({ - ...input, - workspaceId: workspace.id, + ...createViewFieldInput, + workspaceId, }); } - @Mutation(() => Boolean) + @Mutation(() => ViewFieldDTO) async deleteCoreViewField( - @Args('id', { type: () => String }) id: string, - @AuthWorkspace() workspace: Workspace, - ): Promise { + @Args('input') deleteViewFieldInput: DeleteViewFieldInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + const isWorkspaceMigrationV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + workspaceId, + ); + + if (isWorkspaceMigrationV2Enabled) { + return await this.viewFieldV2Service.deleteOne({ + deleteViewFieldInput, + workspaceId, + }); + } const deletedViewField = await this.viewFieldService.delete( - id, - workspace.id, + deleteViewFieldInput.id, + workspaceId, ); - return isDefined(deletedViewField); + return deletedViewField; } - @Mutation(() => Boolean) + @Mutation(() => ViewFieldDTO) async destroyCoreViewField( - @Args('id', { type: () => String }) id: string, - @AuthWorkspace() workspace: Workspace, - ): Promise { + @Args('input') destroyViewFieldInput: DestroyViewFieldInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + const isWorkspaceMigrationV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + workspaceId, + ); + + if (isWorkspaceMigrationV2Enabled) { + return await this.viewFieldV2Service.destroyOne({ + destroyViewFieldInput, + workspaceId, + }); + } + const deletedViewField = await this.viewFieldService.destroy( - id, - workspace.id, + destroyViewFieldInput.id, + workspaceId, ); - return isDefined(deletedViewField); + return deletedViewField; } } diff --git a/packages/twenty-server/src/engine/core-modules/view/services/tests/view-field.service.spec.ts b/packages/twenty-server/src/engine/core-modules/view/services/tests/view-field.service.spec.ts index 6cc291195b5..7bf501b16b8 100644 --- a/packages/twenty-server/src/engine/core-modules/view/services/tests/view-field.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/view/services/tests/view-field.service.spec.ts @@ -150,19 +150,6 @@ describe('ViewFieldService', () => { size: 100, }; - it('should create a view field successfully', async () => { - jest.spyOn(viewFieldRepository, 'create').mockReturnValue(mockViewField); - jest.spyOn(viewFieldRepository, 'save').mockResolvedValue(mockViewField); - - const result = await viewFieldService.create(validViewFieldData); - - expect(viewFieldRepository.create).toHaveBeenCalledWith( - validViewFieldData, - ); - expect(viewFieldRepository.save).toHaveBeenCalledWith(mockViewField); - expect(result).toEqual(mockViewField); - }); - it('should throw exception when workspaceId is missing', async () => { const invalidData = { ...validViewFieldData, workspaceId: undefined }; @@ -308,7 +295,7 @@ describe('ViewFieldService', () => { expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId); expect(viewFieldRepository.delete).toHaveBeenCalledWith(id); - expect(result).toEqual(true); + expect(result).toBeDefined(); }); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts b/packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts index d76b21b8ff8..1bfdcb8ab21 100644 --- a/packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts +++ b/packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts @@ -1,24 +1,24 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; -import { Equal, Repository } from 'typeorm'; -import { v4 } from 'uuid'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input'; +import { DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input'; +import { DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input'; +import { UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input'; +import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto'; import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; -import { - ViewFieldException, - ViewFieldExceptionCode, - ViewFieldExceptionMessageKey, - generateViewFieldExceptionMessage, - generateViewFieldUserFriendlyExceptionMessage, -} from 'src/engine/core-modules/view/exceptions/view-field.exception'; -import { VIEW_FIELD_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant'; import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; -import { fromPartialFlatViewFieldToFlatViewFieldWithDefault } from 'src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util'; +import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util'; +import { fromDeleteViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/core-modules/view/flat-view/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util'; +import { fromDestroyViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/core-modules/view/flat-view/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util'; +import { fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow } from 'src/engine/core-modules/view/flat-view/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util'; import { fromViewFieldEntityToFlatViewField } from 'src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util'; import { WorkspaceMigrationOrchestratorException } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception'; import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service'; @@ -32,7 +32,7 @@ export class ViewFieldV2Service { ) {} // TODO: move to cache service - private async getExistingFlatViewFieldMaps( + private async getExistingFlatViewFieldMapsFromCache( workspaceId: string, ): Promise { const existingViewFields = await this.viewFieldRepository.find({ @@ -56,38 +56,26 @@ export class ViewFieldV2Service { return flatViewFieldMaps; } - async createOne( - viewFieldData: Partial, - ): Promise { - if (!isDefined(viewFieldData.workspaceId)) { - throw new ViewFieldException( - generateViewFieldExceptionMessage( - ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED, - ), - ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA, - { - userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage( - ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED, - ), - }, - ); - } + async createOne({ + createViewFieldInput, + workspaceId, + }: { + createViewFieldInput: CreateViewFieldInput; + workspaceId: string; + }): Promise { + const existingFlatViewFieldMaps = + await this.getExistingFlatViewFieldMapsFromCache(workspaceId); - const existingFlatViewFieldMaps = await this.getExistingFlatViewFieldMaps( - viewFieldData.workspaceId, - ); - - const flatViewFieldFromCreateInput = - fromPartialFlatViewFieldToFlatViewFieldWithDefault({ - ...viewFieldData, - universalIdentifier: viewFieldData.universalIdentifier ?? v4(), + const flatViewFieldToCreate = + fromCreateViewFieldInputToFlatViewFieldToCreate({ + createViewFieldInput, + workspaceId, }); - const toFlatViewFieldMaps: FlatViewFieldMaps = - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatViewFieldFromCreateInput, - flatEntityMaps: existingFlatViewFieldMaps, - }); + const toFlatViewFieldMaps = addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatViewFieldToCreate, + flatEntityMaps: existingFlatViewFieldMaps, + }); const validateAndBuildResult = await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( @@ -102,7 +90,7 @@ export class ViewFieldV2Service { isSystemBuild: false, inferDeletionFromMissingEntities: false, }, - workspaceId: viewFieldData.workspaceId, + workspaceId, }, ); @@ -112,55 +100,35 @@ export class ViewFieldV2Service { ); } - const [createdViewField] = await this.viewFieldRepository.find({ - where: { - id: flatViewFieldFromCreateInput.id, - }, - }); + const recomputedExistingFlatViewFieldMaps = + await this.getExistingFlatViewFieldMapsFromCache(workspaceId); - return createdViewField; + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatViewFieldToCreate.id, + flatEntityMaps: recomputedExistingFlatViewFieldMaps, + }); } - async updateOne( - id: string, - workspaceId: string, - updateData: Partial, - ): Promise { + async updateOne({ + updateViewFieldInput, + workspaceId, + }: { + workspaceId: string; + updateViewFieldInput: UpdateViewFieldInput; + }): Promise { const existingFlatViewFieldMaps = - await this.getExistingFlatViewFieldMaps(workspaceId); + await this.getExistingFlatViewFieldMapsFromCache(workspaceId); - const existingViewField = existingFlatViewFieldMaps.byId[id]; - - if (!isDefined(existingViewField)) { - throw new ViewFieldException( - generateViewFieldExceptionMessage( - ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, - id, - ), - ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, - ); - } - - const existingViewFieldToUpdate = removePropertiesFromRecord( - { - ...existingViewField, - ...updateData, - }, - VIEW_FIELD_ENTITY_RELATION_PROPERTIES, - ); - - const flatViewFieldFromUpdateInput = - fromPartialFlatViewFieldToFlatViewFieldWithDefault({ - ...existingViewFieldToUpdate, - universalIdentifier: - existingViewFieldToUpdate.universalIdentifier ?? '', + const optimisticallyUpdatedFlatView = + fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow({ + flatViewFieldMaps: existingFlatViewFieldMaps, + updateViewFieldInput, }); - const toFlatViewFieldMaps: FlatViewFieldMaps = - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatViewFieldFromUpdateInput, - flatEntityMaps: existingFlatViewFieldMaps, - }); + const toFlatViewFieldMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: optimisticallyUpdatedFlatView, + flatEntityMaps: existingFlatViewFieldMaps, + }); const validateAndBuildResult = await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( @@ -185,37 +153,89 @@ export class ViewFieldV2Service { ); } - const [updatedViewField] = await this.viewFieldRepository.find({ - where: { - id: Equal(id), - }, - }); + const recomputedExistingFlatViewFieldMaps = + await this.getExistingFlatViewFieldMapsFromCache(workspaceId); - return updatedViewField; + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: optimisticallyUpdatedFlatView.id, + flatEntityMaps: recomputedExistingFlatViewFieldMaps, + }); } - async deleteOne(id: string, workspaceId: string): Promise { + async deleteOne({ + deleteViewFieldInput, + workspaceId, + }: { + deleteViewFieldInput: DeleteViewFieldInput; + workspaceId: string; + }): Promise { const existingFlatViewFieldMaps = - await this.getExistingFlatViewFieldMaps(workspaceId); + await this.getExistingFlatViewFieldMapsFromCache(workspaceId); - const existingViewFieldToDelete = existingFlatViewFieldMaps.byId[id]; + const optimisticallyUpdatedFlatViewWithDeletedAt = + fromDeleteViewFieldInputToFlatViewFieldOrThrow({ + flatViewFieldMaps: existingFlatViewFieldMaps, + deleteViewFieldInput, + }); - if (!isDefined(existingViewFieldToDelete)) { - throw new ViewFieldException( - generateViewFieldExceptionMessage( - ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, - id, - ), - ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + const toFlatViewFieldMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: optimisticallyUpdatedFlatViewWithDeletedAt, + flatEntityMaps: existingFlatViewFieldMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( + { + entityMaps: { + viewField: { + fromFlatViewFieldMaps: existingFlatViewFieldMaps, + toFlatViewFieldMaps, + }, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: false, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationOrchestratorException( + 'Multiple validation errors occurred while updating view field', ); } - const toFlatViewFieldMaps: FlatViewFieldMaps = - deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: existingFlatViewFieldMaps, - entityToDeleteId: existingViewFieldToDelete.id, + const recomputedExistingFlatViewFieldMaps = + await this.getExistingFlatViewFieldMapsFromCache(workspaceId); + + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: optimisticallyUpdatedFlatViewWithDeletedAt.id, + flatEntityMaps: recomputedExistingFlatViewFieldMaps, + }); + } + + async destroyOne({ + destroyViewFieldInput, + workspaceId, + }: { + destroyViewFieldInput: DestroyViewFieldInput; + workspaceId: string; + }): Promise { + const existingFlatViewFieldMaps = + await this.getExistingFlatViewFieldMapsFromCache(workspaceId); + + const existingViewFieldToDelete = + fromDestroyViewFieldInputToFlatViewFieldOrThrow({ + destroyViewFieldInput, + flatViewFieldMaps: existingFlatViewFieldMaps, }); + const toFlatViewFieldMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ + flatEntityMaps: existingFlatViewFieldMaps, + entityToDeleteId: existingViewFieldToDelete.id, + }); + const validateAndBuildResult = await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( { @@ -239,6 +259,6 @@ export class ViewFieldV2Service { ); } - return true; + return existingViewFieldToDelete; } } diff --git a/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts b/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts index f7e954131e8..39e4077fffd 100644 --- a/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts +++ b/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts @@ -110,7 +110,27 @@ export class ViewFieldService { try { const viewField = this.viewFieldRepository.create(viewFieldData); - return await this.viewFieldRepository.save(viewField); + const savedViewField = await this.viewFieldRepository.save(viewField); + const createdViewField = await this.findById( + savedViewField.id, + viewFieldData.workspaceId, + ); + + if (!isDefined(createdViewField)) { + throw new ViewFieldException( + generateViewFieldExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, + ), + ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + { + userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, + ), + }, + ); + } + + return createdViewField; } catch (error) { if ( error.message.includes('duplicate key value violates unique constraint') @@ -175,7 +195,7 @@ export class ViewFieldService { return viewField; } - async destroy(id: string, workspaceId: string): Promise { + async destroy(id: string, workspaceId: string): Promise { const viewField = await this.findById(id, workspaceId); if (!isDefined(viewField)) { @@ -190,6 +210,6 @@ export class ViewFieldService { await this.viewFieldRepository.delete(id); - return true; + return viewField; } } diff --git a/packages/twenty-server/src/engine/core-modules/view/view.module.ts b/packages/twenty-server/src/engine/core-modules/view/view.module.ts index 0d6776be52a..63da1a708e0 100644 --- a/packages/twenty-server/src/engine/core-modules/view/view.module.ts +++ b/packages/twenty-server/src/engine/core-modules/view/view.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module'; import { ViewCacheModule } from 'src/engine/core-modules/view/cache/services/view-cache.module'; import { ViewFieldController } from 'src/engine/core-modules/view/controllers/view-field.controller'; @@ -45,6 +46,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa ViewSortEntity, ]), I18nModule, + FeatureFlagModule, WorkspaceCacheStorageModule, WorkspaceMetadataCacheModule, WorkspaceMigrationV2Module, diff --git a/packages/twenty-server/src/utils/merge-update-in-existing-record.util.ts b/packages/twenty-server/src/utils/merge-update-in-existing-record.util.ts new file mode 100644 index 00000000000..df9f53d3df2 --- /dev/null +++ b/packages/twenty-server/src/utils/merge-update-in-existing-record.util.ts @@ -0,0 +1,21 @@ +export const mergeUpdateInExistingRecord = < + TExisting, + P extends keyof TExisting, + TUpdate extends Partial, +>({ + existing, + properties, + update, +}: { + existing: TExisting; + update: TUpdate; + properties: P[]; +}) => + properties.reduce((acc, property) => { + const isPropertyUpdated = update[property] !== undefined; + + return { + ...acc, + ...(isPropertyUpdated ? { [property]: update[property] } : {}), + }; + }, existing); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts index d9ddefad84d..1b1502d7f41 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts @@ -15,13 +15,13 @@ import { } from 'test/integration/graphql/utils/view-data-factory.util'; import { createTestViewWithGraphQL } from 'test/integration/graphql/utils/view-graphql.util'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { assertViewFieldStructure, cleanupViewRecords, } from 'test/integration/utils/view-test.util'; import { FieldMetadataType } from 'twenty-shared/types'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { @@ -201,8 +201,10 @@ describe('View Field Resolver', () => { size: 300, }); const updateOperation = updateViewFieldOperationFactory({ - viewFieldId: viewField.id, - data: updateInput, + input: { + id: viewField.id, + update: updateInput, + }, }); const response = await makeGraphqlAPIRequest(updateOperation); @@ -217,7 +219,12 @@ describe('View Field Resolver', () => { it('should throw an error when updating non-existent view field', async () => { const operation = updateViewFieldOperationFactory({ - viewFieldId: TEST_NOT_EXISTING_VIEW_FIELD_ID, + input: { + id: TEST_NOT_EXISTING_VIEW_FIELD_ID, + update: { + position: 1, + }, + }, }); const response = await makeGraphqlAPIRequest(operation); @@ -249,7 +256,9 @@ describe('View Field Resolver', () => { const response = await makeGraphqlAPIRequest(deleteOperation); assertGraphQLSuccessfulResponse(response); - expect(response.body.data.deleteCoreViewField).toBe(true); + expect(response.body.data.deleteCoreViewField).toMatchObject({ + id: viewField.id, + }); }); it('should throw an error when deleting non-existent view field', async () => { @@ -286,7 +295,9 @@ describe('View Field Resolver', () => { const response = await makeGraphqlAPIRequest(destroyOperation); assertGraphQLSuccessfulResponse(response); - expect(response.body.data.destroyCoreViewField).toBe(true); + expect(response.body.data.destroyCoreViewField).toMatchObject({ + id: viewField.id, + }); }); it('should throw an error when destroying non-existent view field', async () => { diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-view-field-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-view-field-operation-factory.util.ts index 4251298564a..7839415f9ca 100644 --- a/packages/twenty-server/test/integration/graphql/utils/delete-view-field-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/delete-view-field-operation-factory.util.ts @@ -1,16 +1,21 @@ import gql from 'graphql-tag'; +import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; export const deleteViewFieldOperationFactory = ({ viewFieldId, + gqlFields = VIEW_FIELD_GQL_FIELDS, }: { + gqlFields?: string; viewFieldId: string; }) => ({ query: gql` - mutation DeleteCoreViewField($id: String!) { - deleteCoreViewField(id: $id) + mutation DeleteCoreViewField($input: DeleteViewFieldInput!) { + deleteCoreViewField(input: $input) { + ${gqlFields} + } } `, variables: { - id: viewFieldId, + input: { id: viewFieldId }, }, }); diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-view-field-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/destroy-view-field-operation-factory.util.ts index f492121e00b..6efcad0ed22 100644 --- a/packages/twenty-server/test/integration/graphql/utils/destroy-view-field-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/destroy-view-field-operation-factory.util.ts @@ -1,16 +1,21 @@ import gql from 'graphql-tag'; +import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; export const destroyViewFieldOperationFactory = ({ viewFieldId, + gqlFields = VIEW_FIELD_GQL_FIELDS, }: { + gqlFields?: string; viewFieldId: string; }) => ({ query: gql` - mutation DestroyCoreViewField($id: String!) { - destroyCoreViewField(id: $id) + mutation DestroyCoreViewField($input: DestroyViewFieldInput!) { + destroyCoreViewField(input: $input){ + ${gqlFields} + } } `, variables: { - id: viewFieldId, + input: { id: viewFieldId }, }, }); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-view-field-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-view-field-operation-factory.util.ts index 087103f87d7..281ef8d3fbd 100644 --- a/packages/twenty-server/test/integration/graphql/utils/update-view-field-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/update-view-field-operation-factory.util.ts @@ -1,24 +1,23 @@ import gql from 'graphql-tag'; import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input'; + export const updateViewFieldOperationFactory = ({ gqlFields = VIEW_FIELD_GQL_FIELDS, - viewFieldId, - data = {}, + input, }: { gqlFields?: string; - viewFieldId: string; - data?: object; + input: UpdateViewFieldInput; }) => ({ query: gql` - mutation UpdateCoreViewField($id: String!, $input: UpdateViewFieldInput!) { - updateCoreViewField(id: $id, input: $input) { + mutation UpdateCoreViewField($input: UpdateViewFieldInput!) { + updateCoreViewField(input: $input) { ${gqlFields} } } `, variables: { - id: viewFieldId, - input: data, + input, }, }); diff --git a/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts index 405082c772c..171870e3e83 100644 --- a/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts @@ -1,3 +1,4 @@ +import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input'; import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; import { type ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity'; import { type ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity'; @@ -40,7 +41,7 @@ export const createViewFieldData = ( }); export const updateViewFieldData = ( - overrides: Partial = {}, + overrides: Partial = {}, ) => ({ position: 5, isVisible: false, diff --git a/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts index ca5e1bf0e96..8ff8e20d29c 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts @@ -3,7 +3,6 @@ import { TEST_VIEW_1_ID, } from 'test/integration/constants/test-view-ids.constants'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { @@ -21,6 +20,7 @@ import { cleanupViewRecords, } from 'test/integration/utils/view-test.util'; import { FieldMetadataType } from 'twenty-shared/types'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { generateViewFieldExceptionMessage, @@ -37,6 +37,7 @@ describe('View Field REST API', () => { createOneObject: { id: objectMetadataId }, }, } = await createOneObjectMetadata({ + expectToFail: false, input: { nameSingular: 'myTestObject', namePlural: 'myTestObjects', @@ -61,6 +62,7 @@ describe('View Field REST API', () => { createOneField: { id: fieldMetadataId }, }, } = await createOneFieldMetadata({ + expectToFail: false, input: createFieldInput, gqlFields: ` id @@ -75,6 +77,7 @@ describe('View Field REST API', () => { afterAll(async () => { await deleteOneObjectMetadata({ + expectToFail: false, input: { idToDelete: testObjectMetadataId }, }); }); diff --git a/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts b/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts index dd6e0c9ff03..d0164ad8a9b 100644 --- a/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts +++ b/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts @@ -33,7 +33,7 @@ export type HasAllProperties = [T] extends [new (...args: any[]) => any] : [U] extends [new (...args: any[]) => any] ? HasAllProperties> : { - [K in keyof U]: K extends keyof T ? Equal : false; + [K in keyof U]-?: K extends keyof T ? Equal : false; }[keyof U] extends true ? true : false; @@ -51,6 +51,7 @@ type BasicTests = [ Expect>, Expect>, + Expect>, Expect>,