diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 1d171ff460..b972a5ed51 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -4828,7 +4828,8 @@ export type Workspace = { viewGroups?: Maybe>; viewSorts?: Maybe>; views?: Maybe>; - workspaceCustomApplicationId?: Maybe; + workspaceCustomApplication?: Maybe; + workspaceCustomApplicationId: Scalars['String']; workspaceMembersCount?: Maybe; workspaceUrls: WorkspaceUrls; }; @@ -5973,7 +5974,7 @@ export type BillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscri export type CurrentBillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, 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, numberFormat?: WorkspaceMemberNumberFormatEnum | 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', id: string, permissionFlags?: Array | null, objectsPermissions?: 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, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, routerModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, 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, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, 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 } | 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, hasPassword: boolean, 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, numberFormat?: WorkspaceMemberNumberFormatEnum | 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', id: string, permissionFlags?: Array | null, objectsPermissions?: 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, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, routerModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, 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, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, 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 } | 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 }; @@ -5999,7 +6000,7 @@ 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, hasPassword: boolean, 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, numberFormat?: WorkspaceMemberNumberFormatEnum | 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', id: string, permissionFlags?: Array | null, objectsPermissions?: 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, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, routerModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, 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, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, 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 } | 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, hasPassword: boolean, 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, numberFormat?: WorkspaceMemberNumberFormatEnum | 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', id: string, permissionFlags?: Array | null, objectsPermissions?: 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, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, routerModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, 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, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, 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 } | 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, createdAt: string, updatedAt: string, deletedAt?: string | null }; @@ -6988,7 +6989,9 @@ export const UserQueryFragmentFragmentDoc = gql` isPasswordAuthBypassEnabled subdomain hasValidEnterpriseKey - customDomain + workspaceCustomApplication { + id + } isCustomDomainEnabled workspaceUrls { ...WorkspaceUrlsFragment diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 8be536d39b..4e1694ad96 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -4680,7 +4680,8 @@ export type Workspace = { viewGroups?: Maybe>; viewSorts?: Maybe>; views?: Maybe>; - workspaceCustomApplicationId?: Maybe; + workspaceCustomApplication?: Maybe; + workspaceCustomApplicationId: Scalars['String']; workspaceMembersCount?: Maybe; workspaceUrls: WorkspaceUrls; }; diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index 2ff2143ee1..16d24d2903 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -1,6 +1,7 @@ import { ApolloError, gql, InMemoryCache } from '@apollo/client'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; +import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant'; import { WorkspaceActivationStatus } from '~/generated/graphql'; import { ApolloFactory, type Options } from '../apollo.factory'; @@ -62,6 +63,8 @@ const mockWorkspace = { isTwoFactorAuthenticationEnforced: false, trashRetentionDays: 14, routerModel: 'auto', + workspaceCustomApplication: CUSTOM_WORKSPACE_APPLICATION_MOCK, + workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id, }; const createMockOptions = (): Options => ({ diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index e8d15079c5..92e9f34910 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -1,5 +1,9 @@ import { createState } from 'twenty-ui/utilities'; -import { type Role, type Workspace } from '~/generated/graphql'; +import { + type Application, + type Role, + type Workspace, +} from '~/generated/graphql'; export type CurrentWorkspace = Pick< Workspace, @@ -32,6 +36,7 @@ export type CurrentWorkspace = Pick< | 'editableProfileFields' > & { defaultRole?: Omit | null; + workspaceCustomApplication: Pick | null; }; export const currentWorkspaceState = createState({ diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant.ts new file mode 100644 index 0000000000..a8ac1d44ff --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant.ts @@ -0,0 +1,14 @@ +import { type Application } from '~/generated/graphql'; + +export const CUSTOM_WORKSPACE_APPLICATION_MOCK = { + id: 'dc75f982-35a2-4c1b-a63d-bd1131215377', + agents: [], + applicationVariables: [], + canBeUninstalled: false, + description: 'workpace custom application', + name: 'custom', + objects: [], + serverlessFunctions: [], + universalIdentifier: '66a698b6-f6c1-4d35-a6e7-20aeadc3cd95', + version: '1.0.0', +} as const satisfies Application; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts index 9e5e9870e6..6a4b87b173 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant'; import { useColumnDefinitionsFromObjectMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { @@ -17,6 +18,9 @@ const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ contextStoreCurrentObjectMetadataNameSingular: 'company', onInitializeRecoilSnapshot: ({ set }) => { set(currentWorkspaceState, { + workspaceCustomApplication: { + id: CUSTOM_WORKSPACE_APPLICATION_MOCK.id, + }, id: '1', featureFlags: [], allowImpersonation: false, diff --git a/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts b/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts index 5ba8306e0c..57a5fe227c 100644 --- a/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts +++ b/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts @@ -12,6 +12,7 @@ describe('isObjectMetadataSettingsReadOnly', () => { isUIReadOnly: false, isRemote: false, }, + workspaceCustomApplicationId: 'workspaceApplicationId', }); expect(result).toBe(false); @@ -29,8 +30,22 @@ describe('isObjectMetadataSettingsReadOnly', () => { isUIReadOnly: false, isRemote: false, }, + workspaceCustomApplicationId: null, }); expect(result).toBe(true); }); + + it('should return false if object is owned by workspace custom application', () => { + const result = isObjectMetadataSettingsReadOnly({ + objectMetadataItem: { + isUIReadOnly: false, + isRemote: false, + applicationId: 'workspaceApplicationId', + }, + workspaceCustomApplicationId: 'workspaceApplicationId', + }); + + expect(result).toBe(false); + }); }); diff --git a/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts b/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts index 7b8877271f..d3f339e71d 100644 --- a/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts @@ -1,7 +1,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { type ObjectPermission } from '~/generated/graphql'; -import { isDefined } from 'twenty-shared/utils'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; +import { isDefined } from 'twenty-shared/utils'; +import { type ObjectPermission } from '~/generated/graphql'; type IsObjectMetadataReadOnlyParams = { objectPermissions?: ObjectPermission; @@ -9,14 +9,20 @@ type IsObjectMetadataReadOnlyParams = { ObjectMetadataItem, 'isUIReadOnly' | 'isRemote' | 'applicationId' >; + workspaceCustomApplicationId: string | null; }; export const isObjectMetadataSettingsReadOnly = ({ objectPermissions, objectMetadataItem, + workspaceCustomApplicationId, }: IsObjectMetadataReadOnlyParams) => { return ( isObjectMetadataReadOnly({ objectPermissions, objectMetadataItem }) || - isDefined(objectMetadataItem?.applicationId) + (isDefined(objectMetadataItem?.applicationId) + ? isDefined(workspaceCustomApplicationId) + ? objectMetadataItem.applicationId !== workspaceCustomApplicationId + : true + : false) ); }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx b/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx index 7a823af584..086249851f 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx @@ -1,5 +1,7 @@ -import { Tag } from 'twenty-ui/components'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo'; +import { useRecoilValue } from 'recoil'; +import { Tag } from 'twenty-ui/components'; type SettingsItemTypeTagProps = { item: { @@ -14,7 +16,12 @@ export const SettingsItemTypeTag = ({ className, item: { isCustom, isRemote, applicationId }, }: SettingsItemTypeTagProps) => { - const itemTagInfo = getItemTagInfo({ isCustom, isRemote, applicationId }); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const itemTagInfo = getItemTagInfo({ + objectMetadataItem: { isCustom, isRemote, applicationId }, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, + }); return ( { - const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem }); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const readonly = isObjectMetadataSettingsReadOnly({ + objectMetadataItem, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, + }); const navigate = useNavigateSettings(); const setUpdatedObjectNamePlural = useSetRecoilState( updatedObjectNamePluralState, diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx index 2c0a160c08..6e0240a8e6 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx @@ -1,3 +1,4 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; @@ -10,6 +11,7 @@ import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { UndecoratedLink } from 'twenty-ui/navigation'; import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; +import { useRecoilValue } from 'recoil'; const StyledDiv = styled.div` display: flex; @@ -22,8 +24,11 @@ type ObjectFieldsProps = { }; export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => { + const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, }); const { t } = useLingui(); diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx index 4ce80f5450..44b2338aaa 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx @@ -1,16 +1,18 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; +import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; import { SettingsUpdateDataModelObjectAboutForm } from '@/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm'; import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; +import { useRecoilValue } from 'recoil'; import { SettingsPath } from 'twenty-shared/types'; import { H2Title, IconArchive } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; type ObjectSettingsProps = { objectMetadataItem: ObjectMetadataItem; @@ -28,7 +30,12 @@ const StyledFormSection = styled(Section)` export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { const { t } = useLingui(); - const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem }); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const readonly = isObjectMetadataSettingsReadOnly({ + objectMetadataItem, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, + }); const navigate = useNavigateSettings(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index e115aa7aae..47d722c030 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -1,3 +1,4 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems'; @@ -9,6 +10,7 @@ import { t } from '@lingui/core/macro'; import { useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { isLabelIdentifierFieldMetadataTypes } from 'twenty-shared/utils'; import { IconCircleOff, IconPlus, useIcons } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; @@ -42,7 +44,12 @@ const StyledContainer = styled.div` export const SettingsDataModelObjectIdentifiersForm = ({ objectMetadataItem, }: SettingsDataModelObjectIdentifiersFormProps) => { - const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem }); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const readonly = isObjectMetadataSettingsReadOnly({ + objectMetadataItem, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, + }); const formConfig = useForm({ mode: 'onTouched', resolver: zodResolver(settingsDataModelObjectIdentifiersFormSchema), diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/getItemTagInfo.ts b/packages/twenty-front/src/modules/settings/data-model/utils/getItemTagInfo.ts index 36c6cb6ce4..a07208003c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/utils/getItemTagInfo.ts +++ b/packages/twenty-front/src/modules/settings/data-model/utils/getItemTagInfo.ts @@ -27,15 +27,20 @@ type ManagedItemTagInfo = { }; export const getItemTagInfo = ({ - isCustom, - isRemote, - applicationId, + objectMetadataItem: { isCustom, isRemote, applicationId }, + workspaceCustomApplicationId, }: { - isCustom?: boolean; - isRemote?: boolean; - applicationId?: string | null; + objectMetadataItem: { + isCustom?: boolean; + isRemote?: boolean; + applicationId?: string | null; + }; + workspaceCustomApplicationId?: string; }): ItemTagInfo => { - if (isDefined(applicationId)) { + if ( + isDefined(applicationId) && + applicationId !== workspaceCustomApplicationId + ) { return { labelText: 'Managed', labelColor: 'sky' }; } diff --git a/packages/twenty-front/src/modules/users/components/UserAndViewsProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserAndViewsProviderEffect.tsx index be4dc159b4..e4158c6e00 100644 --- a/packages/twenty-front/src/modules/users/components/UserAndViewsProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/UserAndViewsProviderEffect.tsx @@ -118,6 +118,9 @@ export const UserAndViewsProviderEffect = () => { ...userQueryData.currentUser.currentWorkspace, defaultRole: userQueryData.currentUser.currentWorkspace.defaultRole ?? null, + workspaceCustomApplication: + userQueryData.currentUser.currentWorkspace + .workspaceCustomApplication ?? null, }); } diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index f7c12e8e29..5ef403ab6b 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -60,7 +60,9 @@ export const USER_QUERY_FRAGMENT = gql` isPasswordAuthBypassEnabled subdomain hasValidEnterpriseKey - customDomain + workspaceCustomApplication { + id + } isCustomDomainEnabled workspaceUrls { ...WorkspaceUrlsFragment diff --git a/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts b/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts index b201b96899..3fd0288e9f 100644 --- a/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts +++ b/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts @@ -107,7 +107,13 @@ export const useLoadCurrentUser = () => { ); } - const workspace = user.currentWorkspace ?? null; + const workspace = isDefined(user.currentWorkspace) + ? { + ...user.currentWorkspace, + workspaceCustomApplication: + user.currentWorkspace.workspaceCustomApplication ?? null, + } + : null; setCurrentWorkspace(workspace); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx index bd686c4bc0..7ebbabc062 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx @@ -14,6 +14,7 @@ import styled from '@emotion/styled'; import { useRecoilState, useRecoilValue } from 'recoil'; import { AppPath, SettingsPath } from 'twenty-shared/types'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; @@ -67,7 +68,12 @@ export const SettingsObjectDetailPage = () => { findObjectMetadataItemByNamePlural(objectNamePlural) ?? findObjectMetadataItemByNamePlural(updatedObjectNamePlural); - const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem }); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const readonly = isObjectMetadataSettingsReadOnly({ + objectMetadataItem, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, + }); const activeTabId = useRecoilComponentValue( activeTabIdComponentState, diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index 2b067240af..4be7821506 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -5,6 +5,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate, useParams } from 'react-router-dom'; import { type z } from 'zod'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; @@ -12,6 +13,7 @@ import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdate import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; +import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength'; @@ -24,7 +26,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { shouldNavigateBackToMemorizedUrlOnSaveState } from '@/ui/navigation/states/shouldNavigateBackToMemorizedUrlOnSaveState'; import { useLingui } from '@lingui/react/macro'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { AppPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { H2Title, IconArchive, IconArchiveOff } from 'twenty-ui/display'; @@ -33,7 +35,6 @@ import { Section } from 'twenty-ui/layout'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; //TODO: fix this type export type SettingsDataModelFieldEditFormValues = z.infer< @@ -64,7 +65,12 @@ export const SettingsObjectFieldEdit = () => { const objectMetadataItem = findObjectMetadataItemByNamePlural(objectNamePlural); - const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem }); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const readonly = isObjectMetadataSettingsReadOnly({ + objectMetadataItem, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, + }); const { deactivateMetadataField, activateMetadataField } = useFieldMetadataItem(); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx index 5b103097ef..7320a2ef0e 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx @@ -1,3 +1,4 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -80,6 +81,8 @@ export const SettingsObjectTable = ({ }, ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const allObjectSettingsArray = useMemo( () => objectMetadataItems.map( @@ -87,7 +90,11 @@ export const SettingsObjectTable = ({ ({ objectMetadataItem, labelPlural: objectMetadataItem.labelPlural, - objectTypeLabel: getItemTagInfo(objectMetadataItem).labelText, + objectTypeLabel: getItemTagInfo({ + objectMetadataItem, + workspaceCustomApplicationId: + currentWorkspace?.workspaceCustomApplication?.id, + }).labelText, fieldsCount: objectMetadataItem.fields.filter( (field) => !field.isSystem, ).length, @@ -97,7 +104,11 @@ export const SettingsObjectTable = ({ ] ?? 0, }) satisfies SettingsObjectTableItem, ), - [objectMetadataItems, totalCountByObjectMetadataItemNamePlural], + [ + objectMetadataItems, + totalCountByObjectMetadataItemNamePlural, + currentWorkspace, + ], ); const sortedObjectSettingsItems = useSortedArray( diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 43c4c84159..88012e285e 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -1,4 +1,5 @@ import { type CurrentUserWorkspace } from '@/auth/states/currentUserWorkspaceState'; +import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant'; import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { FeatureFlagKey, @@ -54,7 +55,9 @@ const PRO_METERED_MONTHLY_PRICE = PRO_METERED_PRODUCT?.prices?.find( (pr) => pr.recurringInterval === 'Month', )!; -export const mockCurrentWorkspace: Workspace = { +export const mockCurrentWorkspace = { + workspaceCustomApplication: CUSTOM_WORKSPACE_APPLICATION_MOCK, + workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id, subdomain: 'acme.twenty.com', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6w', displayName: 'Twenty', @@ -158,7 +161,7 @@ export const mockCurrentWorkspace: Workspace = { databaseUrl: '', isTwoFactorAuthenticationEnforced: false, __typename: 'Workspace', -}; +} as const satisfies Workspace; export const mockedWorkspaceMemberData: WorkspaceMember = { __typename: 'WorkspaceMember', diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts index 84059fb5ed..03ec7b862c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command'; import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command'; +import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; @@ -16,10 +17,12 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc providers: [ CreateWorkspaceCustomApplicationCommand, SetStandardApplicationNotUninstallableCommand, + WorkspaceCustomApplicationIdNonNullableCommand, ], exports: [ CreateWorkspaceCustomApplicationCommand, SetStandardApplicationNotUninstallableCommand, + WorkspaceCustomApplicationIdNonNullableCommand, ], }) export class V1_12_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command.ts new file mode 100644 index 0000000000..226f112a36 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command.ts @@ -0,0 +1,72 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@Command({ + name: '1-12-workspace-custom-application-id-non-nullable-migration', + description: 'Create FK_3b1acb13a5dac9956d1a4b32755 foreign key', +}) +export class WorkspaceCustomApplicationIdNonNullableCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + private hasRunOnce = false; + + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectDataSource() + private readonly coreDataSource: DataSource, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + options, + }: RunOnWorkspaceArgs): Promise { + if (this.hasRunOnce) { + this.logger.log( + 'Skipping has already been run once WorkspaceCustomApplicationIdNonNullableCommand', + ); + + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + if (!options.dryRun) { + try { + await queryRunner.query( + `ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "workspaceCustomApplicationId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755" FOREIGN KEY ("workspaceCustomApplicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + + await queryRunner.commitTransaction(); + this.logger.log( + 'Successfully run WorkspaceCustomApplicationIdNonNullableCommand', + ); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.log( + `Rollbacking WorkspaceCustomApplicationIdNonNullableCommand: ${error.message}`, + ); + } finally { + await queryRunner.release(); + } + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 0d930992a6..08d5aedac2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -23,6 +23,7 @@ import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrad import { CreateTwentyStandardApplicationCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command'; import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command'; import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command'; +import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command'; import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command'; import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command'; import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command'; @@ -79,9 +80,10 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly seedStandardApplicationsCommand: CreateTwentyStandardApplicationCommand, // 1.12 Commands - protected readonlysetStandardApplicationNotUninstallableCommand: SetStandardApplicationNotUninstallableCommand, + protected readonly setStandardApplicationNotUninstallableCommand: SetStandardApplicationNotUninstallableCommand, protected readonly createTwentyStandardApplicationCommand: CreateTwentyStandardApplicationCommand, protected readonly createWorkspaceCustomApplicationCommand: CreateWorkspaceCustomApplicationCommand, + protected readonly workspaceCustomApplicationIdNonNullableCommand: WorkspaceCustomApplicationIdNonNullableCommand, ) { super( workspaceRepository, @@ -138,10 +140,11 @@ export class UpgradeCommand extends UpgradeCommandRunner { }; const commands_1120: VersionCommands = { - beforeSyncMetadata: [this.createWorkspaceCustomApplicationCommand], - afterSyncMetadata: [ - this.readonlysetStandardApplicationNotUninstallableCommand, + beforeSyncMetadata: [ + this.createWorkspaceCustomApplicationCommand, + this.workspaceCustomApplicationIdNonNullableCommand, ], + afterSyncMetadata: [this.setStandardApplicationNotUninstallableCommand], }; this.allCommands = { diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1763977334519-workspace-custom-application-id-non-nullable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1763977334519-workspace-custom-application-id-non-nullable.ts new file mode 100644 index 0000000000..0090fa851e --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1763977334519-workspace-custom-application-id-non-nullable.ts @@ -0,0 +1,39 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class WorkspaceCustomApplicationIdNonNullable1763977334519 + implements MigrationInterface +{ + name = 'WorkspaceCustomApplicationIdNonNullable1763977334519'; + + public async up(queryRunner: QueryRunner): Promise { + try { + await queryRunner.query( + `ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "workspaceCustomApplicationId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755" FOREIGN KEY ("workspaceCustomApplicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + } catch (e) { + // eslint-disable-next-line no-console + console.error( + 'Swallowing WorkspaceCustomApplicationIdNonNullable1763977334519 error', + e, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "workspaceCustomApplicationId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755" FOREIGN KEY ("workspaceCustomApplicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts index a6f88f162e..c3a1f99418 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts @@ -44,6 +44,7 @@ export class ApplicationEntity { @Column({ nullable: true, type: 'text' }) description: string | null; + // TODO should not be nullable @Column({ nullable: true, type: 'text' }) version: string | null; diff --git a/packages/twenty-server/src/engine/core-modules/application/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/application.service.ts index 5e32260b1e..5cd6a57cd8 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.service.ts @@ -27,15 +27,21 @@ export class ApplicationService { ) {} async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({ + workspace: workspaceInput, workspaceId, - }: { - workspaceId: string; - }) { - const workspace = await this.workspaceRepository.findOne({ - where: { - id: workspaceId, - }, - }); + }: + | { + workspaceId: string; + workspace?: never; + } + | { workspace: WorkspaceEntity; workspaceId?: never }) { + const workspace = isDefined(workspaceInput) + ? workspaceInput + : await this.workspaceRepository.findOne({ + where: { + id: workspaceId, + }, + }); if (!isDefined(workspace)) { throw new ApplicationException( @@ -47,7 +53,7 @@ export class ApplicationService { const flatApplicationMaps = await this.workspaceFlatApplicationMapCacheService.getExistingOrRecomputeFlatMaps( { - workspaceId, + workspaceId: workspace.id, }, ); const twentyStandardApplicationId = diff --git a/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts index d2ebb15276..c531ea8a2e 100644 --- a/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts @@ -1,6 +1,12 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { IsBoolean, IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { + IsBoolean, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto'; @@ -19,13 +25,15 @@ export class ApplicationDTO { @Field() name: string; + @IsOptional() @IsString() @Field() - description: string; + description?: string; + @IsOptional() @IsString() @Field() - version: string; + version?: string; @IsString() @Field() @@ -36,14 +44,14 @@ export class ApplicationDTO { canBeUninstalled: boolean; @Field(() => [AgentDTO]) - agents: AgentDTO[]; + agents?: AgentDTO[]; @Field(() => [ServerlessFunctionDTO]) - serverlessFunctions: ServerlessFunctionDTO[]; + serverlessFunctions?: ServerlessFunctionDTO[]; @Field(() => [ObjectMetadataDTO]) - objects: ObjectMetadataDTO[]; + objects?: ObjectMetadataDTO[]; @Field(() => [ApplicationVariableEntityDTO]) - applicationVariables: ApplicationVariableEntityDTO[]; + applicationVariables?: ApplicationVariableEntityDTO[]; } diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util.ts new file mode 100644 index 0000000000..664a93d6bc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util.ts @@ -0,0 +1,23 @@ +import { type ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; + +export const fromFlatApplicationToApplicationDto = ({ + canBeUninstalled, + + description, + id, + name, + + universalIdentifier, + version, +}: FlatApplication): ApplicationDTO => { + return { + canBeUninstalled, + description: description ?? undefined, + id, + name, + objects: [], + universalIdentifier, + version: version ?? undefined, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index b06800e950..50e914624b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -23,6 +23,7 @@ import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; import { ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity'; import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -286,12 +287,10 @@ export class WorkspaceEntity { @Column({ type: 'varchar', nullable: false, default: 'auto' }) routerModel: ModelId; - // TODO prastoin - // Temporarily setting as nullable for retro compatibility, not udpating TypeScript types - @Field(() => String, { nullable: true }) - @Column({ nullable: true, type: 'uuid' }) + @Column({ nullable: false, type: 'uuid' }) workspaceCustomApplicationId: string; + @Field(() => ApplicationDTO, { nullable: true }) @ManyToOne(() => ApplicationEntity, { onDelete: 'RESTRICT', nullable: false, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 090f0a537e..2139ec9761 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -28,6 +28,7 @@ import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/worksp import { workspaceAutoResolverOpts } from 'src/engine/core-modules/workspace/workspace.auto-resolver-opts'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; @@ -72,6 +73,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m CustomDomainManagerModule, ViewModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, + ApplicationModule, ], services: [WorkspaceService], resolvers: workspaceAutoResolverOpts, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 80cac26cf5..65a5f37a4c 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -23,6 +23,9 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder. import type { FileUpload } from 'graphql-upload/processRequest.mjs'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; +import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records'; @@ -103,6 +106,7 @@ export class WorkspaceResolver { private readonly viewService: ViewService, private readonly dnsManagerService: DnsManagerService, private readonly customDomainManagerService: CustomDomainManagerService, + private readonly applicationService: ApplicationService, ) {} @Query(() => WorkspaceEntity) @@ -240,6 +244,27 @@ export class WorkspaceResolver { return workspace.routerModel; } + @ResolveField(() => ApplicationDTO, { nullable: true }) + async workspaceCustomApplication( + @Parent() workspace: WorkspaceEntity, + ): Promise { + try { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspace, + }, + ); + + return fromFlatApplicationToApplicationDto( + workspaceCustomFlatApplication, + ); + } catch { + // Temporary should be removed after CreateWorkspaceCustomApplicationCommand is run + return null; + } + } + @ResolveField(() => BillingSubscriptionEntity, { nullable: true }) async currentBillingSubscription( @Parent() workspace: WorkspaceEntity,