From 16ba72edabc96c0ce7350e881263c61991ca97b6 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:24:08 +0200 Subject: [PATCH] [permissions] Adapt FE to non-readable fields (#13440) Fixes https://github.com/twentyhq/core-team-issues/issues/1261 and https://github.com/twentyhq/core-team-issues/issues/1153 In this PR we - remove fields user do not have read access on from queries (findMany, findDuplicates, returned fields from create or update etc.) - remove fields user do not have read access on from views This is behind a feature flag --- .../src/generated-metadata/graphql.ts | 12 ++-- .../twenty-front/src/generated/graphql.ts | 1 + .../constants/DefaultRecordActionsConfig.tsx | 69 ++++++++++--------- .../hooks/useRelatedRecordActions.ts | 21 +++--- .../types/ShouldBeRegisteredFunctionParams.ts | 4 +- .../files/components/AttachmentList.tsx | 2 +- .../activities/notes/components/NoteList.tsx | 2 +- .../triggerCreateRecordsOptimisticEffect.ts | 2 +- .../useColumnDefinitionsFromFieldMetadata.ts | 25 +++++++ ...eldMetadataItemsForFilterFamilySelector.ts | 27 +++++++- ...FieldMetadataItemsForSortFamilySelector.ts | 29 +++++++- .../utils/getObjectPermissionsForObject.ts | 9 ++- ...edFieldMetadataIdsFromObjectPermissions.ts | 21 ++++++ .../utils/mapFieldMetadataToGraphQLQuery.ts | 4 ++ .../utils/mapObjectMetadataToGraphQLQuery.ts | 21 +++++- .../cache/hooks/useCreateOneRecordInCache.ts | 6 ++ .../useReadFindManyRecordsQueryInCache.ts | 7 ++ .../useUpsertFindManyRecordsQueryInCache.ts | 7 ++ .../cache/utils/getRecordFromCache.ts | 5 +- .../cache/utils/updateRecordFromCache.ts | 5 +- .../hooks/useCreateManyRecordsMutation.ts | 7 ++ .../hooks/useCreateOneRecordMutation.ts | 7 ++ .../hooks/useFindDuplicatesRecordsQuery.ts | 7 ++ .../hooks/useFindManyRecordsQuery.ts | 7 ++ .../hooks/useFindOneRecordQuery.ts | 7 ++ .../hooks/useObjectPermissions.ts | 2 +- .../hooks/useUpdateOneRecordMutation.ts | 7 ++ ...useGenerateCombinedFindManyRecordsQuery.ts | 7 ++ .../usePerformCombinedFindManyRecords.ts | 7 ++ .../ObjectOptionsDropdownFieldsContent.tsx | 2 +- ...jectOptionsDropdownHiddenFieldsContent.tsx | 2 +- .../useObjectOptionsForTable.test.tsx | 2 +- .../hooks/useObjectOptionsForTable.ts | 7 +- .../contexts/RecordIndexContext.ts | 2 +- .../hooks/useLoadRecordIndexStates.ts | 6 +- .../contexts/RecordTableContext.ts | 2 +- .../record-table/hooks/useSetTableColumns.ts | 45 ++++++++++-- .../record-table/hooks/useTableColumns.ts | 7 +- .../RecordTableColumnHeadDropdownMenu.tsx | 4 +- .../RecordTableColumnHeadWithDropdown.tsx | 9 ++- .../components/RecordTableHeaderCell.tsx | 9 ++- .../RecordTableHeaderPlusButtonContent.tsx | 4 +- .../utils/generateFindManyRecordsQuery.ts | 5 +- .../fragments/objectPermissionFragment.ts | 1 + .../SignInBackgroundMockContainerEffect.tsx | 1 + .../hooks/internal/useViewFromQueryParams.ts | 8 +++ ...-query-find-duplicates-resolver.service.ts | 15 ++-- .../dtos/object-permission.dto.ts | 8 +++ ...ermissionsToUserWorkspacePermissionsDto.ts | 1 + .../src/types/RestrictedField.ts | 4 ++ .../src/types/RestrictedFields.ts | 7 +- packages/twenty-shared/src/types/index.ts | 1 + 52 files changed, 394 insertions(+), 95 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions.ts create mode 100644 packages/twenty-shared/src/types/RestrictedField.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index fc20fb7d26..7f34df7989 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1803,6 +1803,7 @@ export type ObjectPermission = { canSoftDeleteObjectRecords?: Maybe; canUpdateObjectRecords?: Maybe; objectMetadataId: Scalars['String']; + restrictedFields?: Maybe; }; export type ObjectPermissionInput = { @@ -3657,7 +3658,7 @@ export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{ export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', updateLabPublicFeatureFlag: { __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean } }; -export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }; +export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }; export type PermissionFlagFragmentFragment = { __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }; @@ -3697,7 +3698,7 @@ export type UpsertObjectPermissionsMutationVariables = Exact<{ }>; -export type UpsertObjectPermissionsMutation = { __typename?: 'Mutation', upsertObjectPermissions: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> }; +export type UpsertObjectPermissionsMutation = { __typename?: 'Mutation', upsertObjectPermissions: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> }; export type UpsertPermissionFlagsMutationVariables = Exact<{ upsertPermissionFlagsInput: UpsertPermissionFlagsInput; @@ -3709,7 +3710,7 @@ export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPer export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>; -export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __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, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null }> }; +export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __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, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null }> }; export type CreateApprovedAccessDomainMutationVariables = Exact<{ input: CreateApprovedAccessDomainInput; @@ -3840,7 +3841,7 @@ export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutationVaria export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutation = { __typename?: 'Mutation', verifyTwoFactorAuthenticationMethodForAuthenticatedUser: { __typename?: 'VerifyTwoFactorAuthenticationMethodOutput', success: boolean } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, 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 }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, 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: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | 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: any, 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 } | null, defaultAgent?: { __typename?: 'Agent', id: any } | 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: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, 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: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, 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: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | 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: any, 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 } | null, defaultAgent?: { __typename?: 'Agent', id: any } | 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 }; @@ -3859,7 +3860,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, 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 }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, 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: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | 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: any, 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 } | null, defaultAgent?: { __typename?: 'Agent', id: any } | 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: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, 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: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, 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: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | 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: any, 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 } | null, defaultAgent?: { __typename?: 'Agent', id: any } | 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 ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -4123,6 +4124,7 @@ export const ObjectPermissionFragmentFragmentDoc = gql` canUpdateObjectRecords canSoftDeleteObjectRecords canDestroyObjectRecords + restrictedFields } `; export const WorkspaceUrlsFragmentFragmentDoc = gql` diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 3e728a714a..6ce18aef82 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1714,6 +1714,7 @@ export type ObjectPermission = { canSoftDeleteObjectRecords?: Maybe; canUpdateObjectRecords?: Maybe; objectMetadataId: Scalars['String']; + restrictedFields?: Maybe; }; export type ObjectPermissionInput = { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx index edea06dd2f..a30a849622 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx @@ -71,7 +71,8 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< isPinned: true, Icon: IconPlus, shouldBeRegistered: ({ objectPermissions, isSoftDeleteFilterActive }) => - objectPermissions.canUpdateObjectRecords && !isSoftDeleteFilterActive, + (objectPermissions.canUpdateObjectRecords && !isSoftDeleteFilterActive) ?? + false, availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], component: , }, @@ -233,11 +234,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< isSoftDeleteFilterActive, objectPermissions, }) => - isDefined(selectedRecord) && - !selectedRecord.isRemote && - !isSoftDeleteFilterActive && - objectPermissions.canSoftDeleteObjectRecords && - !isDefined(selectedRecord?.deletedAt), + (isDefined(selectedRecord) && + !selectedRecord.isRemote && + !isSoftDeleteFilterActive && + objectPermissions.canSoftDeleteObjectRecords && + !isDefined(selectedRecord?.deletedAt)) ?? + false, availableOn: [ ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.SHOW_PAGE, @@ -260,11 +262,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< isSoftDeleteFilterActive, numberOfSelectedRecords, }) => - objectPermissions.canSoftDeleteObjectRecords && - !isRemote && - !isSoftDeleteFilterActive && - isDefined(numberOfSelectedRecords) && - numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT, + (objectPermissions.canSoftDeleteObjectRecords && + !isRemote && + !isSoftDeleteFilterActive && + isDefined(numberOfSelectedRecords) && + numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ?? + false, availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION], component: , }, @@ -324,9 +327,10 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< accent: 'danger', isPinned: true, shouldBeRegistered: ({ selectedRecord, objectPermissions, isRemote }) => - objectPermissions.canDestroyObjectRecords && - !isRemote && - isDefined(selectedRecord?.deletedAt), + (objectPermissions.canDestroyObjectRecords && + !isRemote && + isDefined(selectedRecord?.deletedAt)) ?? + false, availableOn: [ ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.SHOW_PAGE, @@ -373,12 +377,13 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< isSoftDeleteFilterActive, numberOfSelectedRecords, }) => - objectPermissions.canDestroyObjectRecords && - !isRemote && - isDefined(isSoftDeleteFilterActive) && - isSoftDeleteFilterActive && - isDefined(numberOfSelectedRecords) && - numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT, + (objectPermissions.canDestroyObjectRecords && + !isRemote && + isDefined(isSoftDeleteFilterActive) && + isSoftDeleteFilterActive && + isDefined(numberOfSelectedRecords) && + numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ?? + false, availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION], component: , }, @@ -399,11 +404,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< isShowPage, isSoftDeleteFilterActive, }) => - !isRemote && - isDefined(selectedRecord?.deletedAt) && - objectPermissions.canSoftDeleteObjectRecords && - ((isDefined(isShowPage) && isShowPage) || - (isDefined(isSoftDeleteFilterActive) && isSoftDeleteFilterActive)), + (!isRemote && + isDefined(selectedRecord?.deletedAt) && + objectPermissions.canSoftDeleteObjectRecords && + ((isDefined(isShowPage) && isShowPage) || + (isDefined(isSoftDeleteFilterActive) && isSoftDeleteFilterActive))) ?? + false, availableOn: [ ActionViewType.SHOW_PAGE, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, @@ -426,12 +432,13 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< isSoftDeleteFilterActive, numberOfSelectedRecords, }) => - objectPermissions.canSoftDeleteObjectRecords && - !isRemote && - isDefined(isSoftDeleteFilterActive) && - isSoftDeleteFilterActive && - isDefined(numberOfSelectedRecords) && - numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT, + (objectPermissions.canSoftDeleteObjectRecords && + !isRemote && + isDefined(isSoftDeleteFilterActive) && + isSoftDeleteFilterActive && + isDefined(numberOfSelectedRecords) && + numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ?? + false, availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION], component: , }, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts index ea0d229ee1..e31960447e 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts @@ -81,16 +81,17 @@ export const useRelatedRecordActions = ({ objectPermissions, getTargetObjectWritePermission, }) => - isDefined(selectedRecord) && - !selectedRecord.isRemote && - objectPermissions.canUpdateObjectRecords && - getTargetObjectWritePermission( - targetObjectNameSingular === CoreObjectNameSingular.TaskTarget - ? CoreObjectNameSingular.Task - : targetObjectNameSingular === CoreObjectNameSingular.NoteTarget - ? CoreObjectNameSingular.Note - : targetObjectNameSingular, - ), + (isDefined(selectedRecord) && + !selectedRecord.isRemote && + objectPermissions.canUpdateObjectRecords && + getTargetObjectWritePermission( + targetObjectNameSingular === CoreObjectNameSingular.TaskTarget + ? CoreObjectNameSingular.Task + : targetObjectNameSingular === CoreObjectNameSingular.NoteTarget + ? CoreObjectNameSingular.Note + : targetObjectNameSingular, + )) ?? + false, availableOn: [ ActionViewType.SHOW_PAGE, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, diff --git a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts index 147bfa5ac0..342a1b6532 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts @@ -1,13 +1,13 @@ import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ObjectPermissions } from '@/object-record/cache/types/ObjectPermissions'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; +import { ObjectPermission } from '~/generated/graphql'; export type ShouldBeRegisteredFunctionParams = { objectMetadataItem?: ObjectMetadataItem; - objectPermissions: ObjectPermissions; + objectPermissions: ObjectPermission; recordFilters?: RecordFilter[]; isShowPage?: boolean; isSoftDeleteFilterActive?: boolean; diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx index af8cd7d809..3d042e0ca7 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx @@ -28,7 +28,7 @@ type AttachmentListProps = { targetableObject: ActivityTargetableObject; title: string; attachments: Attachment[]; - button?: ReactElement | false; + button?: ReactElement | false | null; }; const StyledContainer = styled.div` diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx index 4993d9ffc1..1aa0aebe91 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx @@ -8,7 +8,7 @@ import { NoteCard } from './NoteCard'; type NoteListProps = { title: string; notes: Note[]; - button?: ReactElement | false; + button?: ReactElement | false | null; }; const StyledContainer = styled.div` diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts index edc0a40ee4..75217b0b47 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts @@ -14,7 +14,7 @@ import { encodeCursor } from '@/apollo/utils/encodeCursor'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; /* diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts index 98125611f1..43c0e8f6a4 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts @@ -5,7 +5,11 @@ import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailab import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector'; +import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useRecoilValue } from 'recoil'; +import { FeatureFlagKey } from '~/generated/graphql'; import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition'; export const useColumnDefinitionsFromFieldMetadata = ( @@ -27,6 +31,24 @@ export const useColumnDefinitionsFromFieldMetadata = ( }), ); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + let restrictedFieldMetadataIds: string[] = []; + + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + + if (isFieldsPermissionsEnabled) { + restrictedFieldMetadataIds = + getReadRestrictedFieldMetadataIdsFromObjectPermissions({ + objectPermissions: [ + objectPermissionsByObjectMetadataId[objectMetadataItem.id], + ], + objectMetadataId: objectMetadataItem.id, + }); + } + const columnDefinitions: ColumnDefinition[] = activeFieldMetadataItems .map((field, index) => @@ -37,6 +59,9 @@ export const useColumnDefinitionsFromFieldMetadata = ( }), ) .filter(filterAvailableTableColumns) + .filter((column) => { + return !restrictedFieldMetadataIds.includes(column.fieldMetadataId); + }) .map((column) => { const existsInFilterDefinitions = filterableFieldMetadataItems.some( (fieldMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts index c51bd276a1..8277486611 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts @@ -1,6 +1,8 @@ +import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getFilterFilterableFieldMetadataItems } from '@/object-metadata/utils/getFilterFilterableFieldMetadataItems'; +import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions'; import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace'; import { selectorFamily } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -18,7 +20,6 @@ export const availableFieldMetadataItemsForFilterFamilySelector = const objectMetadataItem = objectMetadataItems.find( (item) => item.id === objectMetadataItemId, ); - if (!isDefined(objectMetadataItem)) { return []; } @@ -28,14 +29,34 @@ export const availableFieldMetadataItemsForFilterFamilySelector = currentWorkspace, ); + const isFieldsPermissionsEnabled = + checkIfFeatureFlagIsEnabledOnWorkspace( + FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED, + currentWorkspace, + ); + const filterFilterableFieldMetadataItems = getFilterFilterableFieldMetadataItems({ isJsonFilterEnabled: isJsonFeatureFlagEnabled, }); - const availableFieldMetadataItemsForFilter = - objectMetadataItem.fields.filter(filterFilterableFieldMetadataItems); + let restrictedFieldMetadataIds: string[] = []; + if (isFieldsPermissionsEnabled) { + const currentUserWorkspace = get(currentUserWorkspaceState); + + restrictedFieldMetadataIds = + getReadRestrictedFieldMetadataIdsFromObjectPermissions({ + objectPermissions: currentUserWorkspace?.objectPermissions, + objectMetadataId: objectMetadataItem.id, + }); + } + + const availableFieldMetadataItemsForFilter = objectMetadataItem.fields + .filter(filterFilterableFieldMetadataItems) + .filter((field) => { + return !restrictedFieldMetadataIds.includes(field.id); + }); return availableFieldMetadataItemsForFilter; }, }); diff --git a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts index e4d5a137ce..df339ea5dd 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts @@ -1,7 +1,12 @@ +import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems'; +import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions'; +import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace'; import { selectorFamily } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; export const availableFieldMetadataItemsForSortFamilySelector = selectorFamily({ key: 'availableFieldMetadataItemsForSortFamilySelector', @@ -18,8 +23,28 @@ export const availableFieldMetadataItemsForSortFamilySelector = selectorFamily({ return []; } - const availableFieldMetadataItemsForSort = - objectMetadataItem.fields.filter(filterSortableFieldMetadataItems); + const currentWorkspace = get(currentWorkspaceState); + const isFieldsPermissionsEnabled = checkIfFeatureFlagIsEnabledOnWorkspace( + FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED, + currentWorkspace, + ); + + let restrictedFieldMetadataIds: string[] = []; + if (isFieldsPermissionsEnabled) { + const currentUserWorkspace = get(currentUserWorkspaceState); + + restrictedFieldMetadataIds = + getReadRestrictedFieldMetadataIdsFromObjectPermissions({ + objectPermissions: currentUserWorkspace?.objectPermissions, + objectMetadataId: objectMetadataItem.id, + }); + } + + const availableFieldMetadataItemsForSort = objectMetadataItem.fields + .filter(filterSortableFieldMetadataItems) + .filter((field) => { + return !restrictedFieldMetadataIds.includes(field.id); + }); return availableFieldMetadataItemsForSort; }, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts index d22b2eca22..7f0ce27b3b 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts @@ -1,11 +1,10 @@ -import { ObjectPermissions } from '@/object-record/cache/types/ObjectPermissions'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; export const getObjectPermissionsForObject = ( objectPermissionsByObjectMetadataId: Record, objectMetadataId: string, -): ObjectPermissions => { +): ObjectPermission => { const objectPermissions = objectPermissionsByObjectMetadataId[objectMetadataId]; @@ -15,6 +14,8 @@ export const getObjectPermissionsForObject = ( canUpdateObjectRecords: true, canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, + restrictedFields: {}, + objectMetadataId, }; } @@ -24,5 +25,7 @@ export const getObjectPermissionsForObject = ( canSoftDeleteObjectRecords: objectPermissions.canSoftDeleteObjectRecords ?? true, canDestroyObjectRecords: objectPermissions.canDestroyObjectRecords ?? true, + restrictedFields: objectPermissions.restrictedFields ?? {}, + objectMetadataId, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions.ts b/packages/twenty-front/src/modules/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions.ts new file mode 100644 index 0000000000..314f30ba76 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions.ts @@ -0,0 +1,21 @@ +import { RestrictedField } from 'twenty-shared/types'; +import { ObjectPermission } from '~/generated/graphql'; + +export const getReadRestrictedFieldMetadataIdsFromObjectPermissions = ({ + objectPermissions, + objectMetadataId, +}: { + objectPermissions?: ObjectPermission[] | null; + objectMetadataId: string; +}) => { + const restrictedFields = objectPermissions?.find( + (permission) => permission.objectMetadataId === objectMetadataId, + )?.restrictedFields; + + return Object.entries(restrictedFields ?? {}) + .filter( + ([_fieldMetadataId, restrictedField]) => + (restrictedField as RestrictedField).canRead === false, + ) + .map(([fieldMetadataId]) => fieldMetadataId); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index ad1967e594..17d9ed33aa 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -23,6 +23,7 @@ type MapFieldMetadataToGraphQLQueryArgs = { relationRecordGqlFields?: RecordGqlFields; computeReferences?: boolean; objectPermissionsByObjectMetadataId: Record; + isFieldsPermissionsEnabled?: boolean; }; // TODO: change ObjectMetadataItems mock before refactoring with relation computed field export const mapFieldMetadataToGraphQLQuery = ({ @@ -32,6 +33,7 @@ export const mapFieldMetadataToGraphQLQuery = ({ relationRecordGqlFields, computeReferences = false, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }: MapFieldMetadataToGraphQLQueryArgs): string => { const fieldType = fieldMetadata.type; @@ -81,6 +83,7 @@ ${mapObjectMetadataToGraphQLQuery({ computeReferences: computeReferences, isRootLevel: false, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })}`; } @@ -117,6 +120,7 @@ ${mapObjectMetadataToGraphQLQuery({ computeReferences, isRootLevel: false, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} } }`; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index 56a6099220..a96d9b47d8 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -1,3 +1,4 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery'; @@ -6,7 +7,7 @@ import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; type MapObjectMetadataToGraphQLQueryArgs = { objectMetadataItems: ObjectMetadataItem[]; @@ -18,6 +19,7 @@ type MapObjectMetadataToGraphQLQueryArgs = { computeReferences?: boolean; isRootLevel?: boolean; objectPermissionsByObjectMetadataId: Record; + isFieldsPermissionsEnabled?: boolean; }; export const mapObjectMetadataToGraphQLQuery = ({ @@ -27,6 +29,7 @@ export const mapObjectMetadataToGraphQLQuery = ({ computeReferences = false, isRootLevel = true, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled = false, }: MapObjectMetadataToGraphQLQueryArgs): string => { if ( !isRootLevel && @@ -42,10 +45,22 @@ export const mapObjectMetadataToGraphQLQuery = ({ } } + const filterReadableFields = (field: FieldMetadataItem) => { + if (isFieldsPermissionsEnabled !== true) { + return true; + } + const objectPermission = getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + objectMetadataItem.id, + ); + return objectPermission.restrictedFields?.[field.id]?.canRead !== false; + }; + const manyToOneRelationFields = objectMetadataItem?.fields .filter((field) => field.isActive) .filter((field) => field.type === FieldMetadataType.RELATION) - .filter((field) => isDefined(field.settings?.joinColumnName)); + .filter((field) => isDefined(field.settings?.joinColumnName)) + .filter(filterReadableFields); const manyToOneRelationGqlFieldWithFieldMetadata = manyToOneRelationFields.map((field) => ({ @@ -56,6 +71,7 @@ export const mapObjectMetadataToGraphQLQuery = ({ const gqlFieldWithFieldMetadataThatCouldBeQueried = [ ...objectMetadataItem.fields .filter((fieldMetadata) => fieldMetadata.isActive) + .filter(filterReadableFields) .map((fieldMetadata) => ({ gqlField: fieldMetadata.name, fieldMetadata, @@ -99,6 +115,7 @@ export const mapObjectMetadataToGraphQLQuery = ({ relationRecordGqlFields, computeReferences, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }); }) .filter((field) => field !== '') diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts index 0318e33d52..218d69c2c5 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts @@ -11,7 +11,9 @@ import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphq import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { prefillRecord } from '@/object-record/utils/prefillRecord'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useCreateOneRecordInCache = ({ objectMetadataItem, @@ -25,6 +27,9 @@ export const useCreateOneRecordInCache = ({ const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const apolloCoreClient = useApolloCoreClient(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; return (record: ObjectRecord) => { const prefilledRecord = prefillRecord({ @@ -46,6 +51,7 @@ export const useCreateOneRecordInCache = ({ computeReferences: true, recordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} `; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts index 0cde12c205..548d335989 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts @@ -7,7 +7,9 @@ import { RecordGqlOperationVariables } from '@/object-record/graphql/types/Recor import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { isDefined } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useReadFindManyRecordsQueryInCache = ({ objectMetadataItem, @@ -20,6 +22,10 @@ export const useReadFindManyRecordsQueryInCache = ({ const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const readFindManyRecordsQueryInCache = < T extends ObjectRecord = ObjectRecord, >({ @@ -34,6 +40,7 @@ export const useReadFindManyRecordsQueryInCache = ({ objectMetadataItems, recordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }); const existingRecordsQueryResult = diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts index 4677eb8a3d..a6dedc8525 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts @@ -8,6 +8,8 @@ import { RecordGqlOperationVariables } from '@/object-record/graphql/types/Recor import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useUpsertFindManyRecordsQueryInCache = ({ objectMetadataItem, @@ -19,6 +21,10 @@ export const useUpsertFindManyRecordsQueryInCache = ({ const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const upsertFindManyRecordsQueryInCache = < T extends ObjectRecord = ObjectRecord, >({ @@ -38,6 +44,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({ recordGqlFields, computeReferences, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }); const newObjectRecordConnection = getRecordConnectionFromRecords({ diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts index 9019d88965..ea22faaed6 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts @@ -7,7 +7,7 @@ import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { capitalize } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; import { isEmptyObject } from '~/utils/isEmptyObject'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -18,6 +18,7 @@ export type GetRecordFromCacheArgs = { objectMetadataItem: ObjectMetadataItem; recordGqlFields?: RecordGqlFields; objectPermissionsByObjectMetadataId: Record; + isFieldsPermissionsEnabled?: boolean; }; export const getRecordFromCache = ({ objectMetadataItem, @@ -26,6 +27,7 @@ export const getRecordFromCache = ({ recordId, recordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled = false, }: GetRecordFromCacheArgs) => { if (isUndefinedOrNull(objectMetadataItem)) { return null; @@ -43,6 +45,7 @@ export const getRecordFromCache = ({ objectMetadataItem, recordGqlFields: appliedRecordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }, )} `; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts index c4ad25f88e..f3e61bef68 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts @@ -7,7 +7,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { capitalize } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const updateRecordFromCache = ({ @@ -17,6 +17,7 @@ export const updateRecordFromCache = ({ recordGqlFields, record, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }: { objectMetadataItems: ObjectMetadataItem[]; objectMetadataItem: ObjectMetadataItem; @@ -24,6 +25,7 @@ export const updateRecordFromCache = ({ recordGqlFields: Record; record: T; objectPermissionsByObjectMetadataId: Record; + isFieldsPermissionsEnabled?: boolean; }) => { if (isUndefinedOrNull(objectMetadataItem)) { return null; @@ -39,6 +41,7 @@ export const updateRecordFromCache = ({ computeReferences: true, recordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }, )} `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsMutation.ts index 94ef668a2c..1b6ea43604 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsMutation.ts @@ -8,7 +8,9 @@ import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useCreateManyRecordsMutation = ({ @@ -26,6 +28,10 @@ export const useCreateManyRecordsMutation = ({ const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + if (isUndefinedOrNull(objectMetadataItem)) { return { createManyRecordsMutation: EMPTY_MUTATION }; } @@ -46,6 +52,7 @@ export const useCreateManyRecordsMutation = ({ objectMetadataItem, recordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }, )} }`; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordMutation.ts index e4a53fff3d..010a5568c7 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordMutation.ts @@ -9,7 +9,9 @@ import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useCreateOneRecordMutation = ({ @@ -33,6 +35,10 @@ export const useCreateOneRecordMutation = ({ const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + if (isUndefinedOrNull(objectMetadataItem)) { return { createOneRecordMutation: EMPTY_MUTATION }; } @@ -50,6 +56,7 @@ export const useCreateOneRecordMutation = ({ objectMetadataItem, recordGqlFields: appliedRecordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} } `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts index 0d4294bad6..d847cb38bd 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts @@ -7,7 +7,9 @@ import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabl import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useFindDuplicateRecordsQuery = ({ objectNameSingular, @@ -22,6 +24,10 @@ export const useFindDuplicateRecordsQuery = ({ const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const findDuplicateRecordsQuery = gql` query FindDuplicate${capitalize( objectMetadataItem.nameSingular, @@ -34,6 +40,7 @@ export const useFindDuplicateRecordsQuery = ({ objectMetadataItems, objectMetadataItem, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} cursor } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts index 028a6132fd..e4769abed9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts @@ -8,6 +8,8 @@ import { generateFindManyRecordsQuery, QueryCursorDirection, } from '@/object-record/utils/generateFindManyRecordsQuery'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useFindManyRecordsQuery = ({ objectNameSingular, @@ -28,6 +30,10 @@ export const useFindManyRecordsQuery = ({ const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const findManyRecordsQuery = generateFindManyRecordsQuery({ objectMetadataItem, objectMetadataItems, @@ -35,6 +41,7 @@ export const useFindManyRecordsQuery = ({ computeReferences, cursorDirection, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }); return { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts index 1b1984391e..2b7e414682 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts @@ -6,7 +6,9 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useFindOneRecordQuery = ({ objectNameSingular, @@ -25,6 +27,10 @@ export const useFindOneRecordQuery = ({ const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const findOneRecordQuery = gql` query FindOne${capitalize( objectMetadataItem.nameSingular, @@ -48,6 +54,7 @@ export const useFindOneRecordQuery = ({ objectMetadataItem, recordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} }, `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts b/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts index 1c6bb8fbfa..3fac599a84 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts @@ -2,7 +2,7 @@ import { useRecoilValue } from 'recoil'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; type useObjectPermissionsReturnType = { objectPermissionsByObjectMetadataId: Record; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts index fe9a88a1ef..244483ed2c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts @@ -9,7 +9,9 @@ import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useUpdateOneRecordMutation = ({ @@ -29,6 +31,10 @@ export const useUpdateOneRecordMutation = ({ const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + if (isUndefinedOrNull(objectMetadataItem)) { return { updateOneRecordMutation: EMPTY_MUTATION }; } @@ -54,6 +60,7 @@ export const useUpdateOneRecordMutation = ({ computeReferences, recordGqlFields: appliedRecordGqlFields, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }, )} } diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts index 60b3173ab2..5d10131fb5 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts @@ -9,8 +9,10 @@ import { RecordGqlOperationSignature } from '@/object-record/graphql/types/Recor import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import isEmpty from 'lodash.isempty'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; export const useGenerateCombinedFindManyRecordsQuery = ({ @@ -21,6 +23,10 @@ export const useGenerateCombinedFindManyRecordsQuery = ({ const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + if (!isNonEmptyArray(operationSignatures)) { return null; } @@ -112,6 +118,7 @@ export const useGenerateCombinedFindManyRecordsQuery = ({ objectMetadataItem, }), objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} cursor } diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords.ts index ca8b547744..4ae599fb91 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords.ts @@ -13,8 +13,10 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult'; import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables'; import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useRecoilValue } from 'recoil'; import { capitalize } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; export const usePerformCombinedFindManyRecords = () => { const apolloCoreClient = useApolloCoreClient(); @@ -22,6 +24,10 @@ export const usePerformCombinedFindManyRecords = () => { const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const generateCombinedFindManyRecordsQuery = ( operationSignatures: RecordGqlOperationSignature[], objectMetadataItemsValue: ObjectMetadataItem[], @@ -98,6 +104,7 @@ export const usePerformCombinedFindManyRecords = () => { objectMetadataItem, }), objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} cursor } diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx index 2569576eba..e6e89a9bf0 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx @@ -26,7 +26,7 @@ export const ObjectOptionsDropdownFieldsContent = () => { handleColumnVisibilityChange, handleReorderColumns, visibleTableColumns, - } = useObjectOptionsForTable(recordIndexId); + } = useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); const { visibleBoardFields, diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx index be7fd12b3a..aba9cd4b76 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx @@ -39,7 +39,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => { }); const { handleColumnVisibilityChange, hiddenTableColumns } = - useObjectOptionsForTable(recordIndexId); + useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); const { hiddenBoardFields, handleBoardFieldVisibilityChange } = useObjectOptionsForBoard({ diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx index 989ab9d464..ca766d637a 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx @@ -16,7 +16,7 @@ describe('useObjectOptionsForTable', () => { ]; const renderWithRecoil = () => - renderHook(() => useObjectOptionsForTable('instance-id'), { + renderHook(() => useObjectOptionsForTable('instance-id', 'object-id'), { wrapper: ({ children }) => ( { +export const useObjectOptionsForTable = ( + recordTableId: string, + objectMetadataId: string, +) => { const hiddenTableColumns = useRecoilComponentValueV2( hiddenTableColumnsComponentSelector, recordTableId, @@ -18,7 +21,7 @@ export const useObjectOptionsForTable = (recordTableId: string) => { ); const { handleColumnVisibilityChange, handleColumnReorder } = useTableColumns( - { recordTableId: recordTableId }, + { recordTableId, objectMetadataId }, ); const handleReorderColumns: OnDragEndResponder = useCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts index 350ea3897a..7065aeae07 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts @@ -1,5 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; import { createRequiredContext } from '~/utils/createRequiredContext'; export type RecordIndexContextValue = { diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index e906eb3e1b..ffc9abd8e8 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -108,7 +108,11 @@ export const useLoadRecordIndexStates = () => { columnDefinitions, }); - setTableColumns(newFieldDefinitions, recordIndexId); + setTableColumns( + newFieldDefinitions, + recordIndexId, + objectMetadataItem.id, + ); const existingRecordIndexFieldDefinitions = snapshot .getLoadable(recordIndexFieldDefinitionsState) diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts index 09fadd7f20..0f7ce03dad 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts @@ -3,7 +3,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { createRequiredContext } from '~/utils/createRequiredContext'; -export type RecordTableContextValue = { +type RecordTableContextValue = { recordTableId: string; viewBarId: string; objectNameSingular: string; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts index 5739235d85..13000d8cdd 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts @@ -1,14 +1,26 @@ +import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useRecoilCallback } from 'recoil'; +import { RestrictedField } from 'twenty-shared/types'; +import { FeatureFlagKey } from '~/generated/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetTableColumns = () => { + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const setTableColumns = useRecoilCallback( ({ snapshot, set }) => - (columns: ColumnDefinition[], recordTableId: string) => { + ( + columns: ColumnDefinition[], + recordTableId: string, + objectMetadataId: string, + ) => { const tableColumns = getSnapshotValue( snapshot, tableColumnsComponentState.atomFamily({ @@ -16,17 +28,42 @@ export const useSetTableColumns = () => { }), ); - if (isDeeplyEqual(tableColumns, columns)) { + let columnsToSet = columns; + + if (isFieldsPermissionsEnabled) { + const restrictedFields = getSnapshotValue( + snapshot, + currentUserWorkspaceState, + )?.objectPermissions?.find( + (permission) => permission.objectMetadataId === objectMetadataId, + )?.restrictedFields; + + const restrictedFieldMetadataIds = Object.entries( + restrictedFields ?? {}, + ) + .filter( + ([_fieldMetadataId, restrictedField]) => + (restrictedField as RestrictedField).canRead === false, + ) + .map(([fieldMetadataId]) => fieldMetadataId); + const nonRestrictedColumns = columns.filter( + (column) => + !restrictedFieldMetadataIds?.includes(column.fieldMetadataId), + ); + columnsToSet = nonRestrictedColumns; + } + + if (isDeeplyEqual(tableColumns, columnsToSet)) { return; } set( tableColumnsComponentState.atomFamily({ instanceId: recordTableId, }), - columns, + columnsToSet, ); }, - [], + [isFieldsPermissionsEnabled], ); return { setTableColumns }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts index de90261660..e27283cf3a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts @@ -16,9 +16,10 @@ import { ColumnDefinition } from '../types/ColumnDefinition'; type useRecordTableProps = { recordTableId?: string; + objectMetadataId: string; }; -export const useTableColumns = (props?: useRecordTableProps) => { +export const useTableColumns = (props: useRecordTableProps) => { const { onColumnsChange } = useRecordTable({ recordTableId: props?.recordTableId, }); @@ -52,11 +53,11 @@ export const useTableColumns = (props?: useRecordTableProps) => { const handleColumnsChange = useCallback( async (columns: ColumnDefinition[]) => { - setTableColumns(columns, instanceId); + setTableColumns(columns, instanceId, props.objectMetadataId); await onColumnsChange?.(columns); }, - [setTableColumns, instanceId, onColumnsChange], + [setTableColumns, instanceId, onColumnsChange, props.objectMetadataId], ); const handleColumnVisibilityChange = useCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index 3df082f7c6..4b69c2551a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -24,6 +24,7 @@ import { ColumnDefinition } from '../../types/ColumnDefinition'; export type RecordTableColumnHeadDropdownMenuProps = { column: ColumnDefinition; + objectMetadataId: string; }; const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` @@ -32,6 +33,7 @@ const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` export const RecordTableColumnHeadDropdownMenu = ({ column, + objectMetadataId, }: RecordTableColumnHeadDropdownMenuProps) => { const { t } = useLingui(); @@ -52,7 +54,7 @@ export const RecordTableColumnHeadDropdownMenu = ({ column.fieldMetadataId !== lastVisibleColumn?.fieldMetadataId && canMove; const { handleColumnVisibilityChange, handleMoveTableColumn } = - useTableColumns(); + useTableColumns({ objectMetadataId }); const dropdownId = column.fieldMetadataId + '-header'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx index 2ed464b850..def59b0705 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx @@ -8,9 +8,11 @@ import { RecordTableColumnHeadDropdownMenu } from './RecordTableColumnHeadDropdo type RecordTableColumnHeadWithDropdownProps = { column: ColumnDefinition; + objectMetadataId: string; }; export const RecordTableColumnHeadWithDropdown = ({ + objectMetadataId, column, }: RecordTableColumnHeadWithDropdownProps) => { const { toggleScrollXWrapper, toggleScrollYWrapper } = @@ -32,7 +34,12 @@ export const RecordTableColumnHeadWithDropdown = ({ onClose={handleDropdownClose} dropdownId={column.fieldMetadataId + '-header'} clickableComponent={} - dropdownComponents={} + dropdownComponents={ + + } dropdownOffset={{ x: -1 }} dropdownPlacement="bottom-start" /> diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index bc34cd11e1..392bc41f5a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -137,7 +137,9 @@ export const RecordTableHeaderCell = ({ >(null); const [resizedFieldKey, setResizedFieldKey] = useState(null); - const { handleColumnsChange } = useTableColumns(); + const { handleColumnsChange } = useTableColumns({ + objectMetadataId: objectMetadataItem.id, + }); const handleResizeHandlerStart = useCallback( ({ x }) => { @@ -256,7 +258,10 @@ export const RecordTableHeaderCell = ({ isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} > - + {(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && !isReadOnly && diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index 999e0aa00d..5e3658e654 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -30,7 +30,9 @@ export const RecordTableHeaderPlusButtonContent = () => { ); const { getIcon } = useIcons(); - const { handleColumnVisibilityChange } = useTableColumns(); + const { handleColumnVisibilityChange } = useTableColumns({ + objectMetadataId: objectMetadataItem.id, + }); const handleAddColumn = useCallback( (column: ColumnDefinition) => { diff --git a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts index 786256f8a2..771f744371 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts @@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { capitalize } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated-metadata/graphql'; +import { ObjectPermission } from '~/generated/graphql'; export type QueryCursorDirection = 'before' | 'after'; @@ -15,6 +15,7 @@ export const generateFindManyRecordsQuery = ({ computeReferences, cursorDirection, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }: { objectMetadataItem: ObjectMetadataItem; objectMetadataItems: ObjectMetadataItem[]; @@ -22,6 +23,7 @@ export const generateFindManyRecordsQuery = ({ computeReferences?: boolean; cursorDirection?: QueryCursorDirection; objectPermissionsByObjectMetadataId: Record; + isFieldsPermissionsEnabled?: boolean; }) => gql` query FindMany${capitalize( objectMetadataItem.namePlural, @@ -42,6 +44,7 @@ query FindMany${capitalize( recordGqlFields, computeReferences, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, })} cursor } diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts index a1fb6dc253..fe137d2766 100644 --- a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts +++ b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts @@ -7,5 +7,6 @@ export const OBJECT_PERMISSION_FRAGMENT = gql` canUpdateObjectRecords canSoftDeleteObjectRecords canDestroyObjectRecords + restrictedFields } `; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx index 8cb9aacb3c..b9c173bb0e 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx @@ -61,6 +61,7 @@ export const SignInBackgroundMockContainerEffect = ({ columnDefinitions: SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS, }), recordTableId, + objectMetadataItem.id, ); if (contextStoreCurrentObjectMetadataItemId !== objectMetadataItem.id) { diff --git a/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts b/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts index dbd219c78e..330a83fb1e 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts @@ -16,8 +16,10 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery'; import { ViewFilter } from '@/views/types/ViewFilter'; import { relationFilterValueSchemaObject } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand'; import { isDefined } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; const filterQueryParamsSchema = z.object({ viewId: z.string().optional(), @@ -69,6 +71,10 @@ export const useViewFromQueryParams = () => { isDefined(filterQueryParams) && Object.entries(filterQueryParams).length > 0; + const featureFlags = useFeatureFlagsMap(); + const isFieldsPermissionsEnabled = + featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; + const getFiltersFromQueryParams = useRecoilCallback( ({ snapshot }) => async () => { @@ -125,6 +131,7 @@ export const useViewFromQueryParams = () => { objectMetadataItem: relationObjectMetadataItem, objectMetadataItems, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, }), variables: { filter: { @@ -186,6 +193,7 @@ export const useViewFromQueryParams = () => { objectMetadataItem.fields, objectMetadataItems, objectPermissionsByObjectMetadataId, + isFieldsPermissionsEnabled, ], ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts index c2767e9bc6..dc232b86a8 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts @@ -64,9 +64,18 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR let objectRecords: Partial[] = []; + const columnsToSelect = buildColumnsToSelect({ + select: executionArgs.graphqlQuerySelectedFieldsResult.select, + relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + }); + if (executionArgs.args.ids) { objectRecords = (await existingRecordsQueryBuilder .where({ id: In(executionArgs.args.ids) }) + .setFindOptions({ + select: columnsToSelect, + }) .getMany()) as ObjectRecord[]; } else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) { objectRecords = executionArgs.args.data; @@ -92,12 +101,6 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR }); } - const columnsToSelect = buildColumnsToSelect({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - }); - const duplicateRecordsQueryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts index 30456a30aa..92d0d41583 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts @@ -1,5 +1,8 @@ import { Field, ObjectType } from '@nestjs/graphql'; +import GraphQLJSON from 'graphql-type-json'; +import { RestrictedFields } from 'twenty-shared/types'; + @ObjectType('ObjectPermission') export class ObjectPermissionDTO { @Field({ nullable: false }) @@ -16,4 +19,9 @@ export class ObjectPermissionDTO { @Field({ nullable: true }) canDestroyObjectRecords?: boolean; + + @Field(() => GraphQLJSON, { + nullable: true, + }) + restrictedFields?: RestrictedFields; } diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts index c3793c4182..69bc549c14 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts @@ -16,6 +16,7 @@ export const fromUserWorkspacePermissionsToUserWorkspacePermissionsDto = ({ canUpdateObjectRecords: permissions.canUpdate, canSoftDeleteObjectRecords: permissions.canSoftDelete, canDestroyObjectRecords: permissions.canDestroy, + restrictedFields: permissions.restrictedFields, }), ); diff --git a/packages/twenty-shared/src/types/RestrictedField.ts b/packages/twenty-shared/src/types/RestrictedField.ts new file mode 100644 index 0000000000..4d816f53a1 --- /dev/null +++ b/packages/twenty-shared/src/types/RestrictedField.ts @@ -0,0 +1,4 @@ +export type RestrictedField = { + canRead?: boolean | null; + canUpdate?: boolean | null; +}; diff --git a/packages/twenty-shared/src/types/RestrictedFields.ts b/packages/twenty-shared/src/types/RestrictedFields.ts index 023ad5d2d8..12fdadd9e6 100644 --- a/packages/twenty-shared/src/types/RestrictedFields.ts +++ b/packages/twenty-shared/src/types/RestrictedFields.ts @@ -1,4 +1,3 @@ -export type RestrictedFields = Record< - string, - { canRead?: boolean | null; canUpdate?: boolean | null } ->; +import { RestrictedField } from './RestrictedField'; + +export type RestrictedFields = Record; diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index ca248142c3..ec3b148557 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -14,6 +14,7 @@ export type { IsExactly } from './IsExactly'; export type { NullablePartial } from './NullablePartial'; export type { ObjectRecordsPermissions } from './ObjectRecordsPermissions'; export type { ObjectRecordsPermissionsByRoleId } from './ObjectRecordsPermissionsByRoleId'; +export type { RestrictedField } from './RestrictedField'; export type { RestrictedFields } from './RestrictedFields'; export type { StepFilterGroup, StepFilter } from './StepFilters'; export { StepLogicalOperator } from './StepFilters';