[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
This commit is contained in:
Marie
2025-07-28 18:24:08 +02:00
committed by GitHub
parent 4131be2a68
commit 16ba72edab
52 changed files with 394 additions and 95 deletions
@@ -1803,6 +1803,7 @@ export type ObjectPermission = {
canSoftDeleteObjectRecords?: Maybe<Scalars['Boolean']>;
canUpdateObjectRecords?: Maybe<Scalars['Boolean']>;
objectMetadataId: Scalars['String'];
restrictedFields?: Maybe<Scalars['JSON']>;
};
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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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`
@@ -1714,6 +1714,7 @@ export type ObjectPermission = {
canSoftDeleteObjectRecords?: Maybe<Scalars['Boolean']>;
canUpdateObjectRecords?: Maybe<Scalars['Boolean']>;
objectMetadataId: Scalars['String'];
restrictedFields?: Maybe<Scalars['JSON']>;
};
export type ObjectPermissionInput = {
@@ -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: <CreateNewTableRecordNoSelectionRecordAction />,
},
@@ -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: <DeleteMultipleRecordsAction />,
},
@@ -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: <DestroyMultipleRecordsAction />,
},
@@ -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: <RestoreMultipleRecordsAction />,
},
@@ -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,
@@ -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;
@@ -28,7 +28,7 @@ type AttachmentListProps = {
targetableObject: ActivityTargetableObject;
title: string;
attachments: Attachment[];
button?: ReactElement | false;
button?: ReactElement | false | null;
};
const StyledContainer = styled.div`
@@ -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`
@@ -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';
/*
@@ -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<FieldMetadata>[] =
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) =>
@@ -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;
},
});
@@ -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;
},
@@ -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<string, ObjectPermission>,
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,
};
};
@@ -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);
};
@@ -23,6 +23,7 @@ type MapFieldMetadataToGraphQLQueryArgs = {
relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
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,
})}
}
}`;
@@ -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<string, ObjectPermission>;
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 !== '')
@@ -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 = <T extends ObjectRecord>({
objectMetadataItem,
@@ -25,6 +27,9 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
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 = <T extends ObjectRecord>({
computeReferences: true,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
`;
@@ -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 =
@@ -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({
@@ -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<string, ObjectPermission>;
isFieldsPermissionsEnabled?: boolean;
};
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItem,
@@ -26,6 +27,7 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
recordId,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled = false,
}: GetRecordFromCacheArgs) => {
if (isUndefinedOrNull(objectMetadataItem)) {
return null;
@@ -43,6 +45,7 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItem,
recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
},
)}
`;
@@ -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 = <T extends ObjectRecord>({
@@ -17,6 +17,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
recordGqlFields,
record,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
}: {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: ObjectMetadataItem;
@@ -24,6 +25,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
recordGqlFields: Record<string, boolean>;
record: T;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
isFieldsPermissionsEnabled?: boolean;
}) => {
if (isUndefinedOrNull(objectMetadataItem)) {
return null;
@@ -39,6 +41,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
computeReferences: true,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
},
)}
`;
@@ -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,
},
)}
}`;
@@ -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,
})}
}
`;
@@ -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
}
@@ -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 {
@@ -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,
})}
},
`;
@@ -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<string, ObjectPermission>;
@@ -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,
},
)}
}
@@ -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
}
@@ -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
}
@@ -26,7 +26,7 @@ export const ObjectOptionsDropdownFieldsContent = () => {
handleColumnVisibilityChange,
handleReorderColumns,
visibleTableColumns,
} = useObjectOptionsForTable(recordIndexId);
} = useObjectOptionsForTable(recordIndexId, objectMetadataItem.id);
const {
visibleBoardFields,
@@ -39,7 +39,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
});
const { handleColumnVisibilityChange, hiddenTableColumns } =
useObjectOptionsForTable(recordIndexId);
useObjectOptionsForTable(recordIndexId, objectMetadataItem.id);
const { hiddenBoardFields, handleBoardFieldVisibilityChange } =
useObjectOptionsForBoard({
@@ -16,7 +16,7 @@ describe('useObjectOptionsForTable', () => {
];
const renderWithRecoil = () =>
renderHook(() => useObjectOptionsForTable('instance-id'), {
renderHook(() => useObjectOptionsForTable('instance-id', 'object-id'), {
wrapper: ({ children }) => (
<RecordTableComponentInstanceContext.Provider
value={{ instanceId: 'instance-id', onColumnsChange: jest.fn() }}
@@ -7,7 +7,10 @@ import { visibleTableColumnsComponentSelector } from '@/object-record/record-tab
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
export const useObjectOptionsForTable = (recordTableId: string) => {
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(
@@ -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 = {
@@ -108,7 +108,11 @@ export const useLoadRecordIndexStates = () => {
columnDefinitions,
});
setTableColumns(newFieldDefinitions, recordIndexId);
setTableColumns(
newFieldDefinitions,
recordIndexId,
objectMetadataItem.id,
);
const existingRecordIndexFieldDefinitions = snapshot
.getLoadable(recordIndexFieldDefinitionsState)
@@ -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;
@@ -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<FieldMetadata>[], recordTableId: string) => {
(
columns: ColumnDefinition<FieldMetadata>[],
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 };
@@ -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<FieldMetadata>[]) => {
setTableColumns(columns, instanceId);
setTableColumns(columns, instanceId, props.objectMetadataId);
await onColumnsChange?.(columns);
},
[setTableColumns, instanceId, onColumnsChange],
[setTableColumns, instanceId, onColumnsChange, props.objectMetadataId],
);
const handleColumnVisibilityChange = useCallback(
@@ -24,6 +24,7 @@ import { ColumnDefinition } from '../../types/ColumnDefinition';
export type RecordTableColumnHeadDropdownMenuProps = {
column: ColumnDefinition<FieldMetadata>;
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';
@@ -8,9 +8,11 @@ import { RecordTableColumnHeadDropdownMenu } from './RecordTableColumnHeadDropdo
type RecordTableColumnHeadWithDropdownProps = {
column: ColumnDefinition<FieldMetadata>;
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={<RecordTableColumnHead column={column} />}
dropdownComponents={<RecordTableColumnHeadDropdownMenu column={column} />}
dropdownComponents={
<RecordTableColumnHeadDropdownMenu
column={column}
objectMetadataId={objectMetadataId}
/>
}
dropdownOffset={{ x: -1 }}
dropdownPlacement="bottom-start"
/>
@@ -137,7 +137,9 @@ export const RecordTableHeaderCell = ({
>(null);
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
const { handleColumnsChange } = useTableColumns();
const { handleColumnsChange } = useTableColumns({
objectMetadataId: objectMetadataItem.id,
});
const handleResizeHandlerStart = useCallback<PointerEventListener>(
({ x }) => {
@@ -256,7 +258,10 @@ export const RecordTableHeaderCell = ({
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
>
<StyledColumnHeadContainer>
<RecordTableColumnHeadWithDropdown column={column} />
<RecordTableColumnHeadWithDropdown
column={column}
objectMetadataId={objectMetadataItem.id}
/>
{(useIsMobile() || iconVisibility) &&
!!column.isLabelIdentifier &&
!isReadOnly &&
@@ -30,7 +30,9 @@ export const RecordTableHeaderPlusButtonContent = () => {
);
const { getIcon } = useIcons();
const { handleColumnVisibilityChange } = useTableColumns();
const { handleColumnVisibilityChange } = useTableColumns({
objectMetadataId: objectMetadataItem.id,
});
const handleAddColumn = useCallback(
(column: ColumnDefinition<FieldMetadata>) => {
@@ -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<string, ObjectPermission>;
isFieldsPermissionsEnabled?: boolean;
}) => gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,
@@ -42,6 +44,7 @@ query FindMany${capitalize(
recordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
cursor
}
@@ -7,5 +7,6 @@ export const OBJECT_PERMISSION_FRAGMENT = gql`
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
restrictedFields
}
`;
@@ -61,6 +61,7 @@ export const SignInBackgroundMockContainerEffect = ({
columnDefinitions: SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS,
}),
recordTableId,
objectMetadataItem.id,
);
if (contextStoreCurrentObjectMetadataItemId !== objectMetadataItem.id) {
@@ -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,
],
);
@@ -64,9 +64,18 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
let objectRecords: Partial<ObjectRecord>[] = [];
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,
@@ -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;
}
@@ -16,6 +16,7 @@ export const fromUserWorkspacePermissionsToUserWorkspacePermissionsDto = ({
canUpdateObjectRecords: permissions.canUpdate,
canSoftDeleteObjectRecords: permissions.canSoftDelete,
canDestroyObjectRecords: permissions.canDestroy,
restrictedFields: permissions.restrictedFields,
}),
);
@@ -0,0 +1,4 @@
export type RestrictedField = {
canRead?: boolean | null;
canUpdate?: boolean | null;
};
@@ -1,4 +1,3 @@
export type RestrictedFields = Record<
string,
{ canRead?: boolean | null; canUpdate?: boolean | null }
>;
import { RestrictedField } from './RestrictedField';
export type RestrictedFields = Record<string, RestrictedField>;
@@ -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';