[FE] handle restricted objects 2 (#12437)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2025-06-05 15:49:22 +02:00
committed by GitHub
parent ad804ebecd
commit 3f30964523
109 changed files with 904 additions and 306 deletions

View File

@ -2960,7 +2960,7 @@ export type OnDbEventSubscriptionVariables = Exact<{
export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array<string> | null, record: any } }; export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array<string> | null, record: any } };
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, 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', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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, 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; 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, 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', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | 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, 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -2977,7 +2977,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; 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, 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', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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, 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; 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, 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', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | 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, 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
export type ActivateWorkflowVersionMutationVariables = Exact<{ export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String']; workflowVersionId: Scalars['String'];
@ -3216,15 +3216,6 @@ export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql`
} }
} }
`; `;
export const ObjectPermissionFragmentFragmentDoc = gql`
fragment ObjectPermissionFragment on ObjectPermission {
objectMetadataId
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
`;
export const SettingPermissionFragmentFragmentDoc = gql` export const SettingPermissionFragmentFragmentDoc = gql`
fragment SettingPermissionFragment on SettingPermission { fragment SettingPermissionFragment on SettingPermission {
id id
@ -3259,6 +3250,15 @@ export const DeletedWorkspaceMemberQueryFragmentFragmentDoc = gql`
userEmail userEmail
} }
`; `;
export const ObjectPermissionFragmentFragmentDoc = gql`
fragment ObjectPermissionFragment on ObjectPermission {
objectMetadataId
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
`;
export const RoleFragmentFragmentDoc = gql` export const RoleFragmentFragmentDoc = gql`
fragment RoleFragment on Role { fragment RoleFragment on Role {
id id
@ -3295,6 +3295,9 @@ export const UserQueryFragmentFragmentDoc = gql`
currentUserWorkspace { currentUserWorkspace {
settingsPermissions settingsPermissions
objectRecordsPermissions objectRecordsPermissions
objectPermissions {
...ObjectPermissionFragment
}
} }
currentWorkspace { currentWorkspace {
id id
@ -3364,6 +3367,7 @@ export const UserQueryFragmentFragmentDoc = gql`
} }
${WorkspaceMemberQueryFragmentFragmentDoc} ${WorkspaceMemberQueryFragmentFragmentDoc}
${DeletedWorkspaceMemberQueryFragmentFragmentDoc} ${DeletedWorkspaceMemberQueryFragmentFragmentDoc}
${ObjectPermissionFragmentFragmentDoc}
${RoleFragmentFragmentDoc}`; ${RoleFragmentFragmentDoc}`;
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql` export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) { query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) {

View File

@ -66,8 +66,8 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
position: 0, position: 0,
isPinned: true, isPinned: true,
Icon: IconPlus, Icon: IconPlus,
shouldBeRegistered: ({ hasObjectReadOnlyPermission }) => shouldBeRegistered: ({ objectPermissions }) =>
!hasObjectReadOnlyPermission, objectPermissions.canUpdateObjectRecords,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
component: <CreateNewTableRecordNoSelectionRecordAction />, component: <CreateNewTableRecordNoSelectionRecordAction />,
}, },
@ -194,10 +194,15 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
Icon: IconTrash, Icon: IconTrash,
accent: 'default', accent: 'default',
isPinned: true, isPinned: true,
shouldBeRegistered: ({ selectedRecord, isSoftDeleteFilterActive }) => shouldBeRegistered: ({
selectedRecord,
isSoftDeleteFilterActive,
objectPermissions,
}) =>
isDefined(selectedRecord) && isDefined(selectedRecord) &&
!selectedRecord.isRemote && !selectedRecord.isRemote &&
!isSoftDeleteFilterActive, !isSoftDeleteFilterActive &&
objectPermissions.canSoftDeleteObjectRecords,
availableOn: [ availableOn: [
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE, ActionViewType.SHOW_PAGE,
@ -215,12 +220,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
accent: 'default', accent: 'default',
isPinned: true, isPinned: true,
shouldBeRegistered: ({ shouldBeRegistered: ({
hasObjectReadOnlyPermission, objectPermissions,
isRemote, isRemote,
isSoftDeleteFilterActive, isSoftDeleteFilterActive,
numberOfSelectedRecords, numberOfSelectedRecords,
}) => }) =>
!hasObjectReadOnlyPermission && objectPermissions.canSoftDeleteObjectRecords &&
!isRemote && !isRemote &&
!isSoftDeleteFilterActive && !isSoftDeleteFilterActive &&
isDefined(numberOfSelectedRecords) && isDefined(numberOfSelectedRecords) &&
@ -268,12 +273,8 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
Icon: IconTrashX, Icon: IconTrashX,
accent: 'danger', accent: 'danger',
isPinned: true, isPinned: true,
shouldBeRegistered: ({ shouldBeRegistered: ({ selectedRecord, objectPermissions, isRemote }) =>
selectedRecord, objectPermissions.canDestroyObjectRecords &&
hasObjectReadOnlyPermission,
isRemote,
}) =>
!hasObjectReadOnlyPermission &&
!isRemote && !isRemote &&
isDefined(selectedRecord?.deletedAt), isDefined(selectedRecord?.deletedAt),
availableOn: [ availableOn: [
@ -317,12 +318,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
accent: 'danger', accent: 'danger',
isPinned: true, isPinned: true,
shouldBeRegistered: ({ shouldBeRegistered: ({
hasObjectReadOnlyPermission, objectPermissions,
isRemote, isRemote,
isSoftDeleteFilterActive, isSoftDeleteFilterActive,
numberOfSelectedRecords, numberOfSelectedRecords,
}) => }) =>
!hasObjectReadOnlyPermission && objectPermissions.canDestroyObjectRecords &&
!isRemote && !isRemote &&
isDefined(isSoftDeleteFilterActive) && isDefined(isSoftDeleteFilterActive) &&
isSoftDeleteFilterActive && isSoftDeleteFilterActive &&
@ -343,14 +344,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
isPinned: true, isPinned: true,
shouldBeRegistered: ({ shouldBeRegistered: ({
selectedRecord, selectedRecord,
hasObjectReadOnlyPermission, objectPermissions,
isRemote, isRemote,
isShowPage, isShowPage,
isSoftDeleteFilterActive, isSoftDeleteFilterActive,
}) => }) =>
!isRemote && !isRemote &&
isDefined(selectedRecord?.deletedAt) && isDefined(selectedRecord?.deletedAt) &&
!hasObjectReadOnlyPermission && objectPermissions.canSoftDeleteObjectRecords &&
((isDefined(isShowPage) && isShowPage) || ((isDefined(isShowPage) && isShowPage) ||
(isDefined(isSoftDeleteFilterActive) && isSoftDeleteFilterActive)), (isDefined(isSoftDeleteFilterActive) && isSoftDeleteFilterActive)),
availableOn: [ availableOn: [
@ -370,12 +371,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
accent: 'default', accent: 'default',
isPinned: true, isPinned: true,
shouldBeRegistered: ({ shouldBeRegistered: ({
hasObjectReadOnlyPermission, objectPermissions,
isRemote, isRemote,
isSoftDeleteFilterActive, isSoftDeleteFilterActive,
numberOfSelectedRecords, numberOfSelectedRecords,
}) => }) =>
!hasObjectReadOnlyPermission && objectPermissions.canSoftDeleteObjectRecords &&
!isRemote && !isRemote &&
isDefined(isSoftDeleteFilterActive) && isDefined(isSoftDeleteFilterActive) &&
isSoftDeleteFilterActive && isSoftDeleteFilterActive &&

View File

@ -1,12 +1,13 @@
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; 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 { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
export type ShouldBeRegisteredFunctionParams = { export type ShouldBeRegisteredFunctionParams = {
objectMetadataItem?: ObjectMetadataItem; objectMetadataItem?: ObjectMetadataItem;
hasObjectReadOnlyPermission?: boolean; objectPermissions: ObjectPermissions;
isWorkflowEnabled: boolean; isWorkflowEnabled: boolean;
recordFilters?: RecordFilter[]; recordFilters?: RecordFilter[];
isShowPage?: boolean; isShowPage?: boolean;

View File

@ -8,9 +8,9 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState'; import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react'; import { useContext } from 'react';
@ -42,7 +42,9 @@ export const useShouldActionBeRegisteredParams = ({
const selectedRecord = const selectedRecord =
useRecoilValue(recordStoreFamilyState(recordId ?? '')) || undefined; useRecoilValue(recordStoreFamilyState(recordId ?? '')) || undefined;
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem?.id,
);
const isNoteOrTask = const isNoteOrTask =
objectMetadataItem?.nameSingular === CoreObjectNameSingular.Note || objectMetadataItem?.nameSingular === CoreObjectNameSingular.Note ||
@ -78,7 +80,7 @@ export const useShouldActionBeRegisteredParams = ({
return { return {
objectMetadataItem, objectMetadataItem,
isFavorite, isFavorite,
hasObjectReadOnlyPermission, objectPermissions,
isNoteOrTask, isNoteOrTask,
isInRightDrawer, isInRightDrawer,
isSoftDeleteFilterActive, isSoftDeleteFilterActive,

View File

@ -67,7 +67,10 @@ export const ActivityRichTextEditor = ({
objectNameSingular: activityObjectNameSingular, objectNameSingular: activityObjectNameSingular,
}); });
const isRecordReadOnly = useIsRecordReadOnly({ recordId: activityId }); const isRecordReadOnly = useIsRecordReadOnly({
recordId: activityId,
objectMetadataId: objectMetadataItemActivity.id,
});
const isReadOnly = isFieldValueReadOnly({ const isReadOnly = isFieldValueReadOnly({
objectNameSingular: activityObjectNameSingular, objectNameSingular: activityObjectNameSingular,

View File

@ -7,8 +7,11 @@ import { DropZone } from '@/activities/files/components/DropZone';
import { useAttachments } from '@/activities/files/hooks/useAttachments'; import { useAttachments } from '@/activities/files/hooks/useAttachments';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { import {
AnimatedPlaceholder, AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptyContainer,
@ -17,8 +20,6 @@ import {
AnimatedPlaceholderEmptyTitle, AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS, EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout'; } from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display';
const StyledAttachmentsContainer = styled.div` const StyledAttachmentsContainer = styled.div`
display: flex; display: flex;
@ -47,8 +48,6 @@ export const Attachments = ({
const [isDraggingFile, setIsDraggingFile] = useState(false); const [isDraggingFile, setIsDraggingFile] = useState(false);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (isDefined(e.target.files)) onUploadFile?.(e.target.files[0]); if (isDefined(e.target.files)) onUploadFile?.(e.target.files[0]);
}; };
@ -63,6 +62,16 @@ export const Attachments = ({
const isAttachmentsEmpty = !attachments || attachments.length === 0; const isAttachmentsEmpty = !attachments || attachments.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (loading && isAttachmentsEmpty) { if (loading && isAttachmentsEmpty) {
return <SkeletonLoader />; return <SkeletonLoader />;
} }
@ -94,7 +103,7 @@ export const Attachments = ({
onChange={handleFileChange} onChange={handleFileChange}
type="file" type="file"
/> />
{!hasObjectReadOnlyPermission && ( {!hasObjectUpdatePermissions && (
<Button <Button
Icon={IconPlus} Icon={IconPlus}
title="Add file" title="Add file"
@ -120,7 +129,7 @@ export const Attachments = ({
title="All" title="All"
attachments={attachments ?? []} attachments={attachments ?? []}
button={ button={
!hasObjectReadOnlyPermission && ( !hasObjectUpdatePermissions && (
<Button <Button
Icon={IconPlus} Icon={IconPlus}
size="small" size="small"

View File

@ -12,9 +12,10 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sortByAscString } from '~/utils/array/sortByAscString';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const usePrepareFindManyActivitiesQuery = ({ export const usePrepareFindManyActivitiesQuery = ({
activityObjectNameSingular, activityObjectNameSingular,
@ -32,6 +33,7 @@ export const usePrepareFindManyActivitiesQuery = ({
const cache = useApolloClient().cache; const cache = useApolloClient().cache;
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } = const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } =
useUpsertFindManyRecordsQueryInCache({ useUpsertFindManyRecordsQueryInCache({
@ -64,6 +66,7 @@ export const usePrepareFindManyActivitiesQuery = ({
objectMetadataItem: targetableObjectMetadataItem, objectMetadataItem: targetableObjectMetadataItem,
objectMetadataItems, objectMetadataItems,
cache, cache,
objectPermissionsByObjectMetadataId,
}); });
const activityTargets: (TaskTarget | NoteTarget)[] = const activityTargets: (TaskTarget | NoteTarget)[] =

View File

@ -3,9 +3,12 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
import { NoteList } from '@/activities/notes/components/NoteList'; import { NoteList } from '@/activities/notes/components/NoteList';
import { useNotes } from '@/activities/notes/hooks/useNotes'; import { useNotes } from '@/activities/notes/hooks/useNotes';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { import {
AnimatedPlaceholder, AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptyContainer,
@ -14,8 +17,6 @@ import {
AnimatedPlaceholderEmptyTitle, AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS, EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout'; } from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display';
const StyledNotesContainer = styled.div` const StyledNotesContainer = styled.div`
display: flex; display: flex;
@ -32,14 +33,22 @@ export const Notes = ({
}) => { }) => {
const { notes, loading } = useNotes(targetableObject); const { notes, loading } = useNotes(targetableObject);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const openCreateActivity = useOpenCreateActivityDrawer({ const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Note, activityObjectNameSingular: CoreObjectNameSingular.Note,
}); });
const isNotesEmpty = !notes || notes.length === 0; const isNotesEmpty = !notes || notes.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (loading && isNotesEmpty) { if (loading && isNotesEmpty) {
return <SkeletonLoader />; return <SkeletonLoader />;
} }
@ -59,7 +68,7 @@ export const Notes = ({
There are no associated notes with this record. There are no associated notes with this record.
</AnimatedPlaceholderEmptySubTitle> </AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer> </AnimatedPlaceholderEmptyTextContainer>
{!hasObjectReadOnlyPermission && ( {!hasObjectUpdatePermissions && (
<Button <Button
Icon={IconPlus} Icon={IconPlus}
title="New note" title="New note"
@ -81,7 +90,7 @@ export const Notes = ({
title="All" title="All"
notes={notes} notes={notes}
button={ button={
!hasObjectReadOnlyPermission && ( !hasObjectUpdatePermissions && (
<Button <Button
Icon={IconPlus} Icon={IconPlus}
size="small" size="small"

View File

@ -1,27 +1,31 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display'; import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
export const AddTaskButton = ({ export const AddTaskButton = ({
activityTargetableObjects, activityTargetableObject,
}: { }: {
activityTargetableObjects?: ActivityTargetableObject[]; activityTargetableObject: ActivityTargetableObject;
}) => { }) => {
const openCreateActivity = useOpenCreateActivityDrawer({ const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task, activityObjectNameSingular: CoreObjectNameSingular.Task,
}); });
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: activityTargetableObject.targetObjectNameSingular,
});
if ( const objectPermissions = useObjectPermissionsForObject(
!isNonEmptyArray(activityTargetableObjects) || objectMetadataItem.id,
hasObjectReadOnlyPermission );
) {
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return null; return null;
} }
@ -33,7 +37,7 @@ export const AddTaskButton = ({
title="Add task" title="Add task"
onClick={() => onClick={() =>
openCreateActivity({ openCreateActivity({
targetableObjects: activityTargetableObjects, targetableObjects: [activityTargetableObject],
}) })
} }
/> />

View File

@ -22,7 +22,7 @@ export const ObjectTasks = ({ targetableObject }: ObjectTasksProps) => {
<ObjectFilterDropdownComponentInstanceContext.Provider <ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: 'entity-tasks-filter-scope' }} value={{ instanceId: 'entity-tasks-filter-scope' }}
> >
<TaskGroups targetableObjects={[targetableObject]} /> <TaskGroups targetableObject={targetableObject} />
</ObjectFilterDropdownComponentInstanceContext.Provider> </ObjectFilterDropdownComponentInstanceContext.Provider>
</StyledContainer> </StyledContainer>
); );

View File

@ -1,19 +0,0 @@
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
export const PageAddTaskButton = () => {
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
// TODO: fetch workspace member from filter here
const handleClick = () => {
openCreateActivity({
targetableObjects: [],
});
};
return <PageAddButton onClick={handleClick} />;
};

View File

@ -5,13 +5,14 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
import { useTasks } from '@/activities/tasks/hooks/useTasks'; import { useTasks } from '@/activities/tasks/hooks/useTasks';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Task } from '@/activities/types/Task'; import { Task } from '@/activities/types/Task';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import groupBy from 'lodash.groupby'; import groupBy from 'lodash.groupby';
import { AddTaskButton } from './AddTaskButton'; import { IconPlus } from 'twenty-ui/display';
import { TaskList } from './TaskList'; import { Button } from 'twenty-ui/input';
import { import {
AnimatedPlaceholder, AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptyContainer,
@ -20,8 +21,8 @@ import {
AnimatedPlaceholderEmptyTitle, AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS, EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout'; } from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input'; import { AddTaskButton } from './AddTaskButton';
import { IconPlus } from 'twenty-ui/display'; import { TaskList } from './TaskList';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
@ -31,15 +32,23 @@ const StyledContainer = styled.div`
type TaskGroupsProps = { type TaskGroupsProps = {
filterDropdownId?: string; filterDropdownId?: string;
targetableObjects?: ActivityTargetableObject[]; targetableObject: ActivityTargetableObject;
}; };
export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => { export const TaskGroups = ({ targetableObject }: TaskGroupsProps) => {
const { tasks, tasksLoading } = useTasks({ const { tasks, tasksLoading } = useTasks({
targetableObjects: targetableObjects ?? [], targetableObjects: [targetableObject],
}); });
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const openCreateActivity = useOpenCreateActivityDrawer({ const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task, activityObjectNameSingular: CoreObjectNameSingular.Task,
@ -74,14 +83,14 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
All tasks addressed. Maintain the momentum. All tasks addressed. Maintain the momentum.
</AnimatedPlaceholderEmptySubTitle> </AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer> </AnimatedPlaceholderEmptyTextContainer>
{!hasObjectReadOnlyPermission && ( {!hasObjectUpdatePermissions && (
<Button <Button
Icon={IconPlus} Icon={IconPlus}
title="New task" title="New task"
variant={'secondary'} variant={'secondary'}
onClick={() => onClick={() =>
openCreateActivity({ openCreateActivity({
targetableObjects: targetableObjects ?? [], targetableObjects: [targetableObject],
}) })
} }
/> />
@ -107,7 +116,7 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
tasks={tasksByStatus} tasks={tasksByStatus}
button={ button={
(status === 'TODO' || !hasTodoStatus) && ( (status === 'TODO' || !hasTodoStatus) && (
<AddTaskButton activityTargetableObjects={targetableObjects} /> <AddTaskButton activityTargetableObject={targetableObject} />
) )
} }
/> />

View File

@ -42,12 +42,10 @@ export const Empty: Story = {};
export const WithTasks: Story = { export const WithTasks: Story = {
args: { args: {
targetableObjects: [ targetableObject: {
{ id: mockedTasks[0].taskTargets?.[0].personId,
id: mockedTasks[0].taskTargets?.[0].personId, targetObjectNameSingular: 'person',
targetObjectNameSingular: 'person', } as ActivityTargetableObject,
},
] as ActivityTargetableObject[],
}, },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -13,8 +13,9 @@ import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRec
import { encodeCursor } from '@/apollo/utils/encodeCursor'; import { encodeCursor } from '@/apollo/utils/encodeCursor';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
/* /*
TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are. TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are.
@ -28,6 +29,7 @@ type TriggerCreateRecordsOptimisticEffectArgs = {
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
shouldMatchRootQueryFilter?: boolean; shouldMatchRootQueryFilter?: boolean;
checkForRecordInCache?: boolean; checkForRecordInCache?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
}; };
export const triggerCreateRecordsOptimisticEffect = ({ export const triggerCreateRecordsOptimisticEffect = ({
cache, cache,
@ -36,6 +38,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
objectMetadataItems, objectMetadataItems,
shouldMatchRootQueryFilter, shouldMatchRootQueryFilter,
checkForRecordInCache = false, checkForRecordInCache = false,
objectPermissionsByObjectMetadataId,
}: TriggerCreateRecordsOptimisticEffectArgs) => { }: TriggerCreateRecordsOptimisticEffectArgs) => {
const getRecordNodeFromCache = (recordId: string): RecordGqlNode | null => { const getRecordNodeFromCache = (recordId: string): RecordGqlNode | null => {
const cachedRecord = getRecordFromCache({ const cachedRecord = getRecordFromCache({
@ -43,6 +46,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordId, recordId,
objectPermissionsByObjectMetadataId,
}); });
return getRecordNodeFromRecord({ return getRecordNodeFromRecord({
objectMetadataItem, objectMetadataItem,

View File

@ -1,9 +1,9 @@
import { UserWorkspace } from '~/generated/graphql';
import { createState } from 'twenty-ui/utilities'; import { createState } from 'twenty-ui/utilities';
import { UserWorkspace } from '~/generated/graphql';
export type CurrentUserWorkspace = Pick< export type CurrentUserWorkspace = Pick<
UserWorkspace, UserWorkspace,
'settingsPermissions' | 'objectRecordsPermissions' 'settingsPermissions' | 'objectRecordsPermissions' | 'objectPermissions'
>; >;
export const currentUserWorkspaceState = export const currentUserWorkspaceState =

View File

@ -3,6 +3,7 @@ import { commandMenuNavigationRecordsState } from '@/command-menu/states/command
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
@ -18,6 +19,7 @@ export const CommandMenuContextChipRecordSetterEffect = () => {
); );
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const commandMenuNavigationStack = useRecoilValue( const commandMenuNavigationStack = useRecoilValue(
commandMenuNavigationStackState, commandMenuNavigationStackState,
@ -46,6 +48,7 @@ export const CommandMenuContextChipRecordSetterEffect = () => {
cache: apolloClient.cache, cache: apolloClient.cache,
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
objectPermissionsByObjectMetadataId,
}); });
if (!record) { if (!record) {
@ -68,6 +71,7 @@ export const CommandMenuContextChipRecordSetterEffect = () => {
commandMenuNavigationStack.length, commandMenuNavigationStack.length,
objectMetadataItems, objectMetadataItems,
setCommandMenuNavigationRecords, setCommandMenuNavigationRecords,
objectPermissionsByObjectMetadataId,
]); ]);
return null; return null;

View File

@ -50,6 +50,7 @@ export const initialFavorites: Favorite[] = [
key: mockId, key: mockId,
labelIdentifier: 'favoriteLabel', labelIdentifier: 'favoriteLabel',
avatarUrl: 'example.com', avatarUrl: 'example.com',
company: { id: '5', name: 'Company Test 2' },
avatarType: 'squared' as AvatarType, avatarType: 'squared' as AvatarType,
link: 'example.com', link: 'example.com',
recordId: '1', recordId: '1',
@ -86,17 +87,17 @@ export const sortedFavorites = [
__typename: 'Favorite', __typename: 'Favorite',
}, },
{ {
id: '3', __typename: 'Favorite',
position: 2,
key: '8f3b2121-f194-4ba4-9fbf-2d5a37126806',
labelIdentifier: 'favoriteLabel',
avatarUrl: 'example.com',
link: 'example.com',
recordId: '1',
avatarType: 'squared', avatarType: 'squared',
avatarUrl: undefined,
favoriteFolderId: '1', favoriteFolderId: '1',
forWorkspaceMemberId: '1', forWorkspaceMemberId: '1',
__typename: 'Favorite', id: '3',
labelIdentifier: 'Company Test 2',
link: '/object/company/5',
objectNameSingular: 'company',
position: 2,
recordId: '5',
}, },
]; ];

View File

@ -95,9 +95,8 @@ export const sortFavorites = (
} as ProcessedFavorite; } as ProcessedFavorite;
} }
} }
return { return null;
...favorite,
} as ProcessedFavorite;
}) })
.filter(isDefined)
.sort((a, b) => a.position - b.position); .sort((a, b) => a.position - b.position);
}; };

View File

@ -1,5 +1,7 @@
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem'; import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
@ -27,6 +29,8 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace')); useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace'));
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const sortedStandardObjectMetadataItems = [...objectMetadataItems] const sortedStandardObjectMetadataItems = [...objectMetadataItems]
.filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
.sort((objectMetadataItemA, objectMetadataItemB) => { .sort((objectMetadataItemA, objectMetadataItemB) => {
@ -58,6 +62,15 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
...sortedCustomObjectMetadataItems, ...sortedCustomObjectMetadataItems,
]; ];
const objectMetadataItemsForNavigationItemsWithReadPermission =
objectMetadataItemsForNavigationItems.filter(
(objectMetadataItem) =>
getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
).canReadObjectRecords,
);
return ( return (
objectMetadataItems.length > 0 && ( objectMetadataItems.length > 0 && (
<NavigationDrawerSection> <NavigationDrawerSection>
@ -68,12 +81,14 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
/> />
</NavigationDrawerAnimatedCollapseWrapper> </NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && {isNavigationSectionOpen &&
objectMetadataItemsForNavigationItems.map((objectMetadataItem) => ( objectMetadataItemsForNavigationItemsWithReadPermission.map(
<NavigationDrawerItemForObjectMetadataItem (objectMetadataItem) => (
key={`navigation-drawer-item-${objectMetadataItem.id}`} <NavigationDrawerItemForObjectMetadataItem
objectMetadataItem={objectMetadataItem} key={`navigation-drawer-item-${objectMetadataItem.id}`}
/> objectMetadataItem={objectMetadataItem}
))} />
),
)}
</NavigationDrawerSection> </NavigationDrawerSection>
) )
); );

View File

@ -18,6 +18,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
fieldMetadata: personObjectMetadataItem.fields.find( fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'id', (field) => field.name === 'id',
)!, )!,
objectPermissionsByObjectMetadataId: {},
}); });
expect(normalizeGQLField(res)).toEqual(normalizeGQLField('id')); expect(normalizeGQLField(res)).toEqual(normalizeGQLField('id'));
}); });
@ -28,6 +29,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
fieldMetadata: personObjectMetadataItem.fields.find( fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'name', (field) => field.name === 'name',
)!, )!,
objectPermissionsByObjectMetadataId: {},
}); });
expect(normalizeGQLField(res)).toEqual( expect(normalizeGQLField(res)).toEqual(
normalizeGQLField(`name normalizeGQLField(`name
@ -45,6 +47,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
fieldMetadata: personObjectMetadataItem.fields.find( fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'company', (field) => field.name === 'company',
)!, )!,
objectPermissionsByObjectMetadataId: {},
}); });
expect(normalizeGQLField(res)).toEqual( expect(normalizeGQLField(res)).toEqual(
normalizeGQLField(`company normalizeGQLField(`company
@ -122,6 +125,7 @@ idealCustomerProfile
fieldMetadata: personObjectMetadataItem.fields.find( fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'company', (field) => field.name === 'company',
)!, )!,
objectPermissionsByObjectMetadataId: {},
}); });
expect(normalizeGQLField(res)).toEqual( expect(normalizeGQLField(res)).toEqual(
normalizeGQLField(`company normalizeGQLField(`company

View File

@ -30,6 +30,15 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
avatarUrl: true, avatarUrl: true,
companyId: true, companyId: true,
}, },
objectPermissionsByObjectMetadataId: {
[personObjectMetadataItem.id]: {
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
},
},
}); });
expect(normalizeGQLQuery(res)).toEqual( expect(normalizeGQLQuery(res)).toEqual(
normalizeGQLQuery(`{ normalizeGQLQuery(`{
@ -124,6 +133,15 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
objectMetadataItems: generatedMockObjectMetadataItems, objectMetadataItems: generatedMockObjectMetadataItems,
objectMetadataItem: personObjectMetadataItem, objectMetadataItem: personObjectMetadataItem,
recordGqlFields: { company: { id: true }, id: true, name: true }, recordGqlFields: { company: { id: true }, id: true, name: true },
objectPermissionsByObjectMetadataId: {
[personObjectMetadataItem.id]: {
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
},
},
}); });
expect(normalizeGQLQuery(res)).toEqual( expect(normalizeGQLQuery(res)).toEqual(
normalizeGQLQuery(`{ normalizeGQLQuery(`{

View File

@ -33,6 +33,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
relationObjectMetadataItem?.nameSingular ?? '', relationObjectMetadataItem?.nameSingular ?? '',
relationObjectMetadataNamePlural: relationObjectMetadataNamePlural:
relationObjectMetadataItem?.namePlural ?? '', relationObjectMetadataItem?.namePlural ?? '',
relationObjectMetadataId: relationObjectMetadataItem?.id ?? '',
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '', objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
targetFieldMetadataName: targetFieldMetadataName:
field.relationDefinition?.targetFieldMetadata?.name ?? '', field.relationDefinition?.targetFieldMetadata?.name ?? '',

View File

@ -0,0 +1,28 @@
import { ObjectPermissions } from '@/object-record/cache/types/ObjectPermissions';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
export const getObjectPermissionsForObject = (
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>,
objectMetadataId: string,
): ObjectPermissions => {
const objectPermissions =
objectPermissionsByObjectMetadataId[objectMetadataId];
if (!isDefined(objectPermissions)) {
return {
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
};
}
return {
canReadObjectRecords: objectPermissions.canReadObjectRecords ?? true,
canUpdateObjectRecords: objectPermissions.canUpdateObjectRecords ?? true,
canSoftDeleteObjectRecords:
objectPermissions.canSoftDeleteObjectRecords ?? true,
canDestroyObjectRecords: objectPermissions.canDestroyObjectRecords ?? true,
};
};

View File

@ -2,12 +2,15 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje
import { isUndefined } from '@sniptt/guards'; import { isUndefined } from '@sniptt/guards';
import { import {
FieldMetadataType, FieldMetadataType,
ObjectPermission,
RelationDefinitionType, RelationDefinitionType,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField'; import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
type MapFieldMetadataToGraphQLQueryArgs = { type MapFieldMetadataToGraphQLQueryArgs = {
@ -19,6 +22,7 @@ type MapFieldMetadataToGraphQLQueryArgs = {
>; >;
relationRecordGqlFields?: RecordGqlFields; relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean; computeReferences?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
}; };
// TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field // TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field
export const mapFieldMetadataToGraphQLQuery = ({ export const mapFieldMetadataToGraphQLQuery = ({
@ -27,11 +31,17 @@ export const mapFieldMetadataToGraphQLQuery = ({
fieldMetadata, fieldMetadata,
relationRecordGqlFields, relationRecordGqlFields,
computeReferences = false, computeReferences = false,
objectPermissionsByObjectMetadataId,
}: MapFieldMetadataToGraphQLQueryArgs): string => { }: MapFieldMetadataToGraphQLQueryArgs): string => {
const fieldType = fieldMetadata.type; const fieldType = fieldMetadata.type;
const fieldIsNonCompositeField = isNonCompositeField(fieldType); const fieldIsNonCompositeField = isNonCompositeField(fieldType);
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
fieldMetadata.relationDefinition?.targetObjectMetadata.id,
);
if (fieldIsNonCompositeField) { if (fieldIsNonCompositeField) {
return gqlField; return gqlField;
} }
@ -51,6 +61,15 @@ export const mapFieldMetadataToGraphQLQuery = ({
return ''; return '';
} }
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
if (gqlField === fieldMetadata.settings?.joinColumnName) { if (gqlField === fieldMetadata.settings?.joinColumnName) {
return `${gqlField}`; return `${gqlField}`;
} }
@ -62,6 +81,7 @@ ${mapObjectMetadataToGraphQLQuery({
recordGqlFields: relationRecordGqlFields, recordGqlFields: relationRecordGqlFields,
computeReferences: computeReferences, computeReferences: computeReferences,
isRootLevel: false, isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}`; })}`;
} }
@ -80,6 +100,15 @@ ${mapObjectMetadataToGraphQLQuery({
return ''; return '';
} }
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
return `${gqlField} return `${gqlField}
{ {
edges { edges {
@ -89,6 +118,7 @@ ${mapObjectMetadataToGraphQLQuery({
recordGqlFields: relationRecordGqlFields, recordGqlFields: relationRecordGqlFields,
computeReferences, computeReferences,
isRootLevel: false, isRootLevel: false,
objectPermissionsByObjectMetadataId,
})} })}
} }
}`; }`;

View File

@ -1,25 +1,47 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery'; import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried'; import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode'; import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
type MapObjectMetadataToGraphQLQueryArgs = { type MapObjectMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>; objectMetadataItem: Pick<
ObjectMetadataItem,
'nameSingular' | 'fields' | 'id'
>;
recordGqlFields?: RecordGqlFields; recordGqlFields?: RecordGqlFields;
computeReferences?: boolean; computeReferences?: boolean;
isRootLevel?: boolean; isRootLevel?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
}; };
export const mapObjectMetadataToGraphQLQuery = ({ export const mapObjectMetadataToGraphQLQuery = ({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
recordGqlFields, recordGqlFields,
computeReferences = false, computeReferences = false,
isRootLevel = true, isRootLevel = true,
objectPermissionsByObjectMetadataId,
}: MapObjectMetadataToGraphQLQueryArgs): string => { }: MapObjectMetadataToGraphQLQueryArgs): string => {
if (
!isRootLevel &&
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(objectMetadataItem.id)
) {
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
const manyToOneRelationFields = objectMetadataItem?.fields const manyToOneRelationFields = objectMetadataItem?.fields
.filter((field) => field.isActive) .filter((field) => field.isActive)
.filter((field) => field.type === FieldMetadataType.RELATION) .filter((field) => field.type === FieldMetadataType.RELATION)
@ -61,25 +83,29 @@ export const mapObjectMetadataToGraphQLQuery = ({
}`; }`;
} }
const mappedFields = gqlFieldWithFieldMetadataThatSouldBeQueried
.map((gqlFieldWithFieldMetadata) => {
const currentRecordGqlFields =
recordGqlFields?.[gqlFieldWithFieldMetadata.gqlField];
const relationRecordGqlFields = isRecordGqlFieldsNode(
currentRecordGqlFields,
)
? currentRecordGqlFields
: undefined;
return mapFieldMetadataToGraphQLQuery({
objectMetadataItems,
gqlField: gqlFieldWithFieldMetadata.gqlField,
fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata,
relationRecordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
});
})
.filter((field) => field !== '')
.join('\n');
return `{ return `{
__typename __typename
${gqlFieldWithFieldMetadataThatSouldBeQueried ${mappedFields}
.map((gqlFieldWithFieldMetadata) => {
const currentRecordGqlFields =
recordGqlFields?.[gqlFieldWithFieldMetadata.gqlField];
const relationRecordGqlFields = isRecordGqlFieldsNode(
currentRecordGqlFields,
)
? currentRecordGqlFields
: undefined;
return mapFieldMetadataToGraphQLQuery({
objectMetadataItems,
gqlField: gqlFieldWithFieldMetadata.gqlField,
fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata,
relationRecordGqlFields,
computeReferences,
});
})
.join('\n')}
}`; }`;
}; };

View File

@ -8,6 +8,7 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { prefillRecord } from '@/object-record/utils/prefillRecord'; import { prefillRecord } from '@/object-record/utils/prefillRecord';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
@ -21,6 +22,8 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
}); });
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
return (record: ObjectRecord) => { return (record: ObjectRecord) => {
@ -42,6 +45,7 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
objectMetadataItem, objectMetadataItem,
computeReferences: true, computeReferences: true,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
})} })}
`; `;

View File

@ -7,6 +7,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const useGetRecordFromCache = ({ export const useGetRecordFromCache = ({
@ -24,6 +25,7 @@ export const useGetRecordFromCache = ({
recordGqlFields ?? generateDepthOneRecordGqlFields({ objectMetadataItem }); recordGqlFields ?? generateDepthOneRecordGqlFields({ objectMetadataItem });
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
@ -38,6 +40,7 @@ export const useGetRecordFromCache = ({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
recordGqlFields: appliedRecordGqlFields, recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
}, },
[ [
@ -45,6 +48,7 @@ export const useGetRecordFromCache = ({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
appliedRecordGqlFields, appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
], ],
); );
}; };

View File

@ -5,6 +5,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery'; import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -18,6 +19,8 @@ export const useReadFindManyRecordsQueryInCache = ({
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const readFindManyRecordsQueryInCache = < const readFindManyRecordsQueryInCache = <
T extends ObjectRecord = ObjectRecord, T extends ObjectRecord = ObjectRecord,
>({ >({
@ -31,6 +34,7 @@ export const useReadFindManyRecordsQueryInCache = ({
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
const existingRecordsQueryResult = const existingRecordsQueryResult =

View File

@ -5,6 +5,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery'; import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
@ -16,6 +17,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const upsertFindManyRecordsQueryInCache = < const upsertFindManyRecordsQueryInCache = <
T extends ObjectRecord = ObjectRecord, T extends ObjectRecord = ObjectRecord,
@ -35,6 +37,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({
objectMetadataItems, objectMetadataItems,
recordGqlFields, recordGqlFields,
computeReferences, computeReferences,
objectPermissionsByObjectMetadataId,
}); });
const newObjectRecordConnection = getRecordConnectionFromRecords({ const newObjectRecordConnection = getRecordConnectionFromRecords({

View File

@ -0,0 +1,8 @@
import { ObjectPermission } from '~/generated-metadata/graphql';
export type ObjectPermissions = {
[K in keyof Omit<
ObjectPermission,
'objectMetadataId' | '__typename'
>]-?: boolean;
};

View File

@ -6,9 +6,10 @@ import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFr
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
import { isEmptyObject } from '~/utils/isEmptyObject'; import { isEmptyObject } from '~/utils/isEmptyObject';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
export type GetRecordFromCacheArgs = { export type GetRecordFromCacheArgs = {
cache: ApolloCache<object>; cache: ApolloCache<object>;
@ -16,6 +17,7 @@ export type GetRecordFromCacheArgs = {
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
recordGqlFields?: RecordGqlFields; recordGqlFields?: RecordGqlFields;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
}; };
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItem, objectMetadataItem,
@ -23,6 +25,7 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
cache, cache,
recordId, recordId,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}: GetRecordFromCacheArgs) => { }: GetRecordFromCacheArgs) => {
if (isUndefinedOrNull(objectMetadataItem)) { if (isUndefinedOrNull(objectMetadataItem)) {
return null; return null;
@ -39,6 +42,7 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
recordGqlFields: appliedRecordGqlFields, recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
}, },
)} )}
`; `;

View File

@ -7,6 +7,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const updateRecordFromCache = <T extends ObjectRecord>({ export const updateRecordFromCache = <T extends ObjectRecord>({
@ -15,12 +16,14 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
cache, cache,
recordGqlFields, recordGqlFields,
record, record,
objectPermissionsByObjectMetadataId,
}: { }: {
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
cache: ApolloCache<object>; cache: ApolloCache<object>;
recordGqlFields: Record<string, boolean>; recordGqlFields: Record<string, boolean>;
record: T; record: T;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
}) => { }) => {
if (isUndefinedOrNull(objectMetadataItem)) { if (isUndefinedOrNull(objectMetadataItem)) {
return null; return null;
@ -35,6 +38,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
objectMetadataItem, objectMetadataItem,
computeReferences: true, computeReferences: true,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}, },
)} )}
`; `;

View File

@ -8,6 +8,7 @@ import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateReco
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
// Mocks // Mocks
jest.mock('@apollo/client'); jest.mock('@apollo/client');
@ -25,6 +26,10 @@ const mockGqlFieldToFieldMap = {
totalCount: ['name', AggregateOperations.COUNT], totalCount: ['name', AggregateOperations.COUNT],
}; };
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useAggregateRecords', () => { describe('useAggregateRecords', () => {
beforeEach(() => { beforeEach(() => {
(useObjectMetadataItem as jest.Mock).mockReturnValue({ (useObjectMetadataItem as jest.Mock).mockReturnValue({
@ -44,14 +49,18 @@ describe('useAggregateRecords', () => {
}); });
it('should format data correctly', () => { it('should format data correctly', () => {
const { result } = renderHook(() => const { result } = renderHook(
useAggregateRecords({ () =>
objectNameSingular: 'opportunity', useAggregateRecords({
recordGqlFieldsAggregate: { objectNameSingular: 'opportunity',
amount: [AggregateOperations.SUM, AggregateOperations.AVG], recordGqlFieldsAggregate: {
name: [AggregateOperations.COUNT], amount: [AggregateOperations.SUM, AggregateOperations.AVG],
}, name: [AggregateOperations.COUNT],
}), },
}),
{
wrapper: Wrapper,
},
); );
expect(result.current.data).toEqual({ expect(result.current.data).toEqual({
@ -74,13 +83,17 @@ describe('useAggregateRecords', () => {
error: undefined, error: undefined,
}); });
const { result } = renderHook(() => const { result } = renderHook(
useAggregateRecords({ () =>
objectNameSingular: 'opportunity', useAggregateRecords({
recordGqlFieldsAggregate: { objectNameSingular: 'opportunity',
amount: [AggregateOperations.SUM], recordGqlFieldsAggregate: {
}, amount: [AggregateOperations.SUM],
}), },
}),
{
wrapper: Wrapper,
},
); );
expect(result.current.data).toEqual({}); expect(result.current.data).toEqual({});
@ -95,13 +108,17 @@ describe('useAggregateRecords', () => {
error: mockError, error: mockError,
}); });
const { result } = renderHook(() => const { result } = renderHook(
useAggregateRecords({ () =>
objectNameSingular: 'opportunity', useAggregateRecords({
recordGqlFieldsAggregate: { objectNameSingular: 'opportunity',
amount: [AggregateOperations.SUM], recordGqlFieldsAggregate: {
}, amount: [AggregateOperations.SUM],
}), },
}),
{
wrapper: Wrapper,
},
); );
expect(result.current.data).toEqual({}); expect(result.current.data).toEqual({});
@ -109,14 +126,18 @@ describe('useAggregateRecords', () => {
}); });
it('should skip query when specified', () => { it('should skip query when specified', () => {
renderHook(() => renderHook(
useAggregateRecords({ () =>
objectNameSingular: 'opportunity', useAggregateRecords({
recordGqlFieldsAggregate: { objectNameSingular: 'opportunity',
amount: [AggregateOperations.SUM], recordGqlFieldsAggregate: {
}, amount: [AggregateOperations.SUM],
skip: true, },
}), skip: true,
}),
{
wrapper: Wrapper,
},
); );
expect(useQuery).toHaveBeenCalledWith( expect(useQuery).toHaveBeenCalledWith(

View File

@ -57,6 +57,7 @@ describe('useDeleteManyRecords', () => {
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordId: expectedRecord.id, recordId: expectedRecord.id,
objectPermissionsByObjectMetadataId: {},
}); });
expect(cachedRecord).not.toBeNull(); expect(cachedRecord).not.toBeNull();
if (cachedRecord === null) throw new Error('Should never occur'); if (cachedRecord === null) throw new Error('Should never occur');
@ -72,6 +73,7 @@ describe('useDeleteManyRecords', () => {
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordId, recordId,
objectPermissionsByObjectMetadataId: {},
}), }),
).toBeNull(), ).toBeNull(),
); );
@ -119,6 +121,7 @@ describe('useDeleteManyRecords', () => {
objectMetadataItem, objectMetadataItem,
record, record,
}), }),
objectPermissionsByObjectMetadataId: {},
}), }),
); );
}); });

View File

@ -5,6 +5,7 @@ import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGq
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -35,10 +36,16 @@ export const useAggregateRecords = <T extends AggregateRecordsData>({
recordGqlFieldsAggregate, recordGqlFieldsAggregate,
}); });
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const { data, loading, error } = useQuery<RecordGqlOperationFindManyResult>( const { data, loading, error } = useQuery<RecordGqlOperationFindManyResult>(
aggregateQuery, aggregateQuery,
{ {
skip: skip || !objectMetadataItem, skip: skip || !objectMetadataItem || !hasReadPermission,
variables: { variables: {
filter, filter,
}, },

View File

@ -5,6 +5,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -61,7 +62,7 @@ export const useAttachRelatedRecordFromRecord = ({
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecordAndAttachRelations = async ({ const updateOneRecordAndAttachRelations = async ({
recordId, recordId,
relatedRecordId, relatedRecordId,
@ -98,6 +99,7 @@ export const useAttachRelatedRecordFromRecord = ({
[fieldOnRelatedObject]: previousRecord, [fieldOnRelatedObject]: previousRecord,
}, },
recordGqlFields: gqlFields, recordGqlFields: gqlFields,
objectPermissionsByObjectMetadataId,
}); });
} }

View File

@ -14,6 +14,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation'; import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -70,7 +71,7 @@ export const useCreateManyRecords = <
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({ const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural, objectMetadataNamePlural: objectMetadataItem.namePlural,
}); });
@ -118,6 +119,7 @@ export const useCreateManyRecords = <
...baseOptimisticRecordInputCreatedBy, ...baseOptimisticRecordInputCreatedBy,
...recordToCreate, ...recordToCreate,
}, },
objectPermissionsByObjectMetadataId,
}), }),
id: idForCreation as string, id: idForCreation as string,
}; };
@ -152,6 +154,7 @@ export const useCreateManyRecords = <
recordsToCreate: recordNodeCreatedInCache, recordsToCreate: recordNodeCreatedInCache,
objectMetadataItems, objectMetadataItems,
shouldMatchRootQueryFilter, shouldMatchRootQueryFilter,
objectPermissionsByObjectMetadataId,
}); });
} }
@ -178,6 +181,7 @@ export const useCreateManyRecords = <
objectMetadataItems, objectMetadataItems,
shouldMatchRootQueryFilter, shouldMatchRootQueryFilter,
checkForRecordInCache: true, checkForRecordInCache: true,
objectPermissionsByObjectMetadataId,
}); });
}, },
}) })

View File

@ -6,9 +6,10 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateManyRecordsMutation = ({ export const useCreateManyRecordsMutation = ({
objectNameSingular, objectNameSingular,
@ -21,6 +22,8 @@ export const useCreateManyRecordsMutation = ({
objectNameSingular, objectNameSingular,
}); });
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (isUndefinedOrNull(objectMetadataItem)) { if (isUndefinedOrNull(objectMetadataItem)) {
@ -42,6 +45,7 @@ export const useCreateManyRecordsMutation = ({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}, },
)} )}
}`; }`;

View File

@ -14,6 +14,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput'; import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput';
@ -62,6 +63,7 @@ export const useCreateOneRecord = <
); );
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({ const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural, objectMetadataNamePlural: objectMetadataItem.namePlural,
@ -90,6 +92,7 @@ export const useCreateOneRecord = <
...recordInput, ...recordInput,
id: idForCreation, id: idForCreation,
}, },
objectPermissionsByObjectMetadataId,
}); });
const recordCreatedInCache = createOneRecordInCache({ const recordCreatedInCache = createOneRecordInCache({
...optimisticRecordInput, ...optimisticRecordInput,
@ -113,6 +116,7 @@ export const useCreateOneRecord = <
recordsToCreate: [optimisticRecordNode], recordsToCreate: [optimisticRecordNode],
objectMetadataItems, objectMetadataItems,
shouldMatchRootQueryFilter, shouldMatchRootQueryFilter,
objectPermissionsByObjectMetadataId,
}); });
} }
} }
@ -136,6 +140,7 @@ export const useCreateOneRecord = <
objectMetadataItems, objectMetadataItems,
shouldMatchRootQueryFilter, shouldMatchRootQueryFilter,
checkForRecordInCache: true, checkForRecordInCache: true,
objectPermissionsByObjectMetadataId,
}); });
} }

View File

@ -7,9 +7,10 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField'; import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateOneRecordMutation = ({ export const useCreateOneRecordMutation = ({
objectNameSingular, objectNameSingular,
@ -30,6 +31,8 @@ export const useCreateOneRecordMutation = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
if (isUndefinedOrNull(objectMetadataItem)) { if (isUndefinedOrNull(objectMetadataItem)) {
return { createOneRecordMutation: EMPTY_MUTATION }; return { createOneRecordMutation: EMPTY_MUTATION };
} }
@ -46,6 +49,7 @@ export const useCreateOneRecordMutation = ({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
recordGqlFields: appliedRecordGqlFields, recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
})} })}
} }
`; `;

View File

@ -11,12 +11,13 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { sleep } from '~/utils/sleep';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
type useDeleteManyRecordProps = { type useDeleteManyRecordProps = {
objectNameSingular: string; objectNameSingular: string;
@ -52,7 +53,7 @@ export const useDeleteManyRecords = ({
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({ const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural, objectMetadataNamePlural: objectMetadataItem.namePlural,
}); });
@ -115,6 +116,7 @@ export const useDeleteManyRecords = ({
cache: apolloClient.cache, cache: apolloClient.cache,
record: computedOptimisticRecord, record: computedOptimisticRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
computedOptimisticRecordsNode.push(optimisticRecordNode); computedOptimisticRecordsNode.push(optimisticRecordNode);
@ -156,6 +158,7 @@ export const useDeleteManyRecords = ({
cache: apolloClient.cache, cache: apolloClient.cache,
record: { ...cachedRecord, deletedAt: null }, record: { ...cachedRecord, deletedAt: null },
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
const cachedRecordWithConnection = const cachedRecordWithConnection =

View File

@ -9,6 +9,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
@ -37,7 +38,7 @@ export const useDeleteOneRecord = ({
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({ const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural, objectMetadataNamePlural: objectMetadataItem.namePlural,
}); });
@ -84,6 +85,7 @@ export const useDeleteOneRecord = ({
cache: apolloClient.cache, cache: apolloClient.cache,
record: computedOptimisticRecord, record: computedOptimisticRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
triggerUpdateRecordOptimisticEffect({ triggerUpdateRecordOptimisticEffect({
@ -133,6 +135,7 @@ export const useDeleteOneRecord = ({
deletedAt: null, deletedAt: null,
}, },
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
triggerUpdateRecordOptimisticEffect({ triggerUpdateRecordOptimisticEffect({
@ -156,6 +159,7 @@ export const useDeleteOneRecord = ({
mutationResponseField, mutationResponseField,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
refetchAggregateQueries, refetchAggregateQueries,
], ],
); );

View File

@ -8,12 +8,13 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation'; import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField'; import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { sleep } from '~/utils/sleep';
import { capitalize, isDefined } from 'twenty-shared/utils'; import { capitalize, isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
type useDestroyManyRecordProps = { type useDestroyManyRecordProps = {
objectNameSingular: string; objectNameSingular: string;
@ -47,7 +48,7 @@ export const useDestroyManyRecords = ({
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({ const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural, objectMetadataNamePlural: objectMetadataItem.namePlural,
}); });
@ -120,6 +121,7 @@ export const useDestroyManyRecords = ({
objectMetadataItem, objectMetadataItem,
recordsToCreate: cachedRecords, recordsToCreate: cachedRecords,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}); });
} }
throw error; throw error;

View File

@ -7,6 +7,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField'; import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
import { capitalize, isDefined } from 'twenty-shared/utils'; import { capitalize, isDefined } from 'twenty-shared/utils';
@ -31,7 +32,7 @@ export const useDestroyOneRecord = ({
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const mutationResponseField = const mutationResponseField =
getDestroyOneRecordMutationResponseField(objectNameSingular); getDestroyOneRecordMutationResponseField(objectNameSingular);
@ -73,6 +74,7 @@ export const useDestroyOneRecord = ({
objectMetadataItem, objectMetadataItem,
recordsToCreate: [originalRecord], recordsToCreate: [originalRecord],
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}); });
} }
@ -89,6 +91,7 @@ export const useDestroyOneRecord = ({
objectMetadataItem, objectMetadataItem,
objectNameSingular, objectNameSingular,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
], ],
); );

View File

@ -5,6 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled'; import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField'; import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
@ -17,6 +18,8 @@ export const useFindDuplicateRecordsQuery = ({
objectNameSingular, objectNameSingular,
}); });
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const findDuplicateRecordsQuery = gql` const findDuplicateRecordsQuery = gql`
@ -30,6 +33,7 @@ export const useFindDuplicateRecordsQuery = ({
node ${mapObjectMetadataToGraphQLQuery({ node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
objectPermissionsByObjectMetadataId,
})} })}
cursor cursor
} }

View File

@ -9,6 +9,7 @@ import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetc
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted'; import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted';
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError'; import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted'; import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier'; import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
@ -68,9 +69,15 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }], or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }],
}; };
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const { data, loading, error, fetchMore } = const { data, loading, error, fetchMore } =
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, { useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem, skip: skip || !objectMetadataItem || !hasReadPermission,
variables: { variables: {
filter: withSoftDeleted filter: withSoftDeleted
? { ? {

View File

@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { import {
generateFindManyRecordsQuery, generateFindManyRecordsQuery,
QueryCursorDirection, QueryCursorDirection,
@ -25,12 +26,15 @@ export const useFindManyRecordsQuery = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const findManyRecordsQuery = generateFindManyRecordsQuery({ const findManyRecordsQuery = generateFindManyRecordsQuery({
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordGqlFields, recordGqlFields,
computeReferences, computeReferences,
cursorDirection, cursorDirection,
objectPermissionsByObjectMetadataId,
}); });
return { return {

View File

@ -8,6 +8,7 @@ import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery'; import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -38,10 +39,16 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
withSoftDeleted, withSoftDeleted,
}); });
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const { data, loading, error } = useQuery<{ const { data, loading, error } = useQuery<{
[nameSingular: string]: RecordGqlNode; [nameSingular: string]: RecordGqlNode;
}>(findOneRecordQuery, { }>(findOneRecordQuery, {
skip: !objectMetadataItem || !objectRecordId || skip, skip: !objectMetadataItem || !objectRecordId || skip || !hasReadPermission,
variables: { objectRecordId }, variables: { objectRecordId },
onCompleted: (data) => { onCompleted: (data) => {
const recordWithoutConnection = getRecordFromRecordNode<T>({ const recordWithoutConnection = getRecordFromRecordNode<T>({

View File

@ -5,6 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
export const useFindOneRecordQuery = ({ export const useFindOneRecordQuery = ({
@ -22,6 +23,8 @@ export const useFindOneRecordQuery = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const findOneRecordQuery = gql` const findOneRecordQuery = gql`
query FindOne${capitalize( query FindOne${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
@ -44,6 +47,7 @@ export const useFindOneRecordQuery = ({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
})} })}
}, },
`; `;

View File

@ -8,6 +8,7 @@ import { UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecor
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted'; import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted';
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError'; import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { cursorFamilyState } from '@/object-record/states/cursorFamilyState'; import { cursorFamilyState } from '@/object-record/states/cursorFamilyState';
import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState'; import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -55,6 +56,12 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
onCompleted, onCompleted,
}); });
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const [findManyRecords, { data, loading, error, fetchMore }] = const [findManyRecords, { data, loading, error, fetchMore }] =
useLazyQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, { useLazyQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
variables: { variables: {
@ -83,6 +90,20 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const findManyRecordsLazy = useRecoilCallback( const findManyRecordsLazy = useRecoilCallback(
({ set }) => ({ set }) =>
async () => { async () => {
if (!hasReadPermission) {
set(hasNextPageFamilyState(queryIdentifier), false);
set(cursorFamilyState(queryIdentifier), '');
onCompleted?.([]);
return {
data: null,
loading: false,
error: undefined,
called: true,
};
}
const result = await findManyRecords(); const result = await findManyRecords();
const hasNextPage = const hasNextPage =
@ -98,19 +119,26 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
return result; return result;
}, },
[queryIdentifier, findManyRecords, objectMetadataItem], [
hasReadPermission,
findManyRecords,
objectMetadataItem.namePlural,
queryIdentifier,
onCompleted,
],
); );
return { return {
objectMetadataItem, objectMetadataItem,
records, records,
totalCount, totalCount,
loading, loading: hasReadPermission ? loading : false,
error, error: hasReadPermission ? error : undefined,
fetchMore, fetchMore,
fetchMoreRecords, fetchMoreRecords,
queryStateIdentifier: queryIdentifier, queryStateIdentifier: queryIdentifier,
findManyRecords: findManyRecordsLazy, findManyRecords: findManyRecordsLazy,
hasNextPage, hasNextPage,
hasReadPermission,
}; };
}; };

View File

@ -0,0 +1,32 @@
import { useRecoilValue } from 'recoil';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
type useObjectPermissionsReturnType = {
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
};
export const useObjectPermissions = (): useObjectPermissionsReturnType => {
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
const objectPermissions = currentUserWorkspace?.objectPermissions;
if (!isDefined(objectPermissions)) {
return {
objectPermissionsByObjectMetadataId: {},
};
}
const objectPermissionsByObjectMetadataId = objectPermissions?.reduce(
(acc: Record<string, ObjectPermission>, objectPermission) => {
acc[objectPermission.objectMetadataId] = objectPermission;
return acc;
},
{},
);
return {
objectPermissionsByObjectMetadataId,
};
};

View File

@ -0,0 +1,15 @@
import { getObjectPermissionsForObject } from '~/modules/object-metadata/utils/getObjectPermissionsForObject';
import { useMemo } from 'react';
import { useObjectPermissions } from './useObjectPermissions';
export const useObjectPermissionsForObject = (objectMetadataId: string) => {
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
return useMemo(() => {
return getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataId,
);
}, [objectPermissionsByObjectMetadataId, objectMetadataId]);
};

View File

@ -9,12 +9,13 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation'; import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField'; import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { sleep } from '~/utils/sleep';
import { capitalize, isDefined } from 'twenty-shared/utils'; import { capitalize, isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
type useRestoreManyRecordProps = { type useRestoreManyRecordProps = {
objectNameSingular: string; objectNameSingular: string;
@ -50,7 +51,7 @@ export const useRestoreManyRecords = ({
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const mutationResponseField = getRestoreManyRecordsMutationResponseField( const mutationResponseField = getRestoreManyRecordsMutationResponseField(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
); );
@ -111,6 +112,7 @@ export const useRestoreManyRecords = ({
cache: apolloClient.cache, cache: apolloClient.cache,
record: computedOptimisticRecord, record: computedOptimisticRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
triggerUpdateRecordOptimisticEffect({ triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache, cache: apolloClient.cache,
@ -169,6 +171,7 @@ export const useRestoreManyRecords = ({
cache: apolloClient.cache, cache: apolloClient.cache,
record: cachedRecord, record: cachedRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
triggerUpdateRecordOptimisticEffect({ triggerUpdateRecordOptimisticEffect({

View File

@ -10,6 +10,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -57,6 +58,7 @@ export const useUpdateOneRecord = <
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({ const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural, objectMetadataNamePlural: objectMetadataItem.namePlural,
@ -75,6 +77,7 @@ export const useUpdateOneRecord = <
recordInput: updateOneRecordInput, recordInput: updateOneRecordInput,
cache: apolloClient.cache, cache: apolloClient.cache,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}); });
const cachedRecord = getRecordFromCache<ObjectRecord>(idToUpdate); const cachedRecord = getRecordFromCache<ObjectRecord>(idToUpdate);
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({ const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
@ -118,6 +121,7 @@ export const useUpdateOneRecord = <
cache: apolloClient.cache, cache: apolloClient.cache,
record: computedOptimisticRecord, record: computedOptimisticRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
triggerUpdateRecordOptimisticEffect({ triggerUpdateRecordOptimisticEffect({
@ -190,6 +194,7 @@ export const useUpdateOneRecord = <
), ),
}, },
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
triggerUpdateRecordOptimisticEffect({ triggerUpdateRecordOptimisticEffect({

View File

@ -7,6 +7,7 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -26,6 +27,8 @@ export const useUpdateOneRecordMutation = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
if (isUndefinedOrNull(objectMetadataItem)) { if (isUndefinedOrNull(objectMetadataItem)) {
return { updateOneRecordMutation: EMPTY_MUTATION }; return { updateOneRecordMutation: EMPTY_MUTATION };
} }
@ -44,14 +47,15 @@ export const useUpdateOneRecordMutation = ({
const updateOneRecordMutation = gql` const updateOneRecordMutation = gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) { mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) {
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery( ${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
{ {
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
computeReferences, computeReferences,
recordGqlFields: appliedRecordGqlFields, recordGqlFields: appliedRecordGqlFields,
}, objectPermissionsByObjectMetadataId,
)} },
)}
} }
`; `;

View File

@ -6,9 +6,10 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; 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 { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
export const useGenerateCombinedFindManyRecordsQuery = ({ export const useGenerateCombinedFindManyRecordsQuery = ({
operationSignatures, operationSignatures,
@ -16,6 +17,7 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
operationSignatures: RecordGqlOperationSignature[]; operationSignatures: RecordGqlOperationSignature[];
}) => { }) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
if (!isNonEmptyArray(operationSignatures)) { if (!isNonEmptyArray(operationSignatures)) {
return null; return null;
@ -92,6 +94,7 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
generateDepthOneRecordGqlFields({ generateDepthOneRecordGqlFields({
objectMetadataItem, objectMetadataItem,
}), }),
objectPermissionsByObjectMetadataId,
})} })}
cursor cursor
} }

View File

@ -8,6 +8,7 @@ import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getR
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult'; import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables'; import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart'; import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart';
@ -18,6 +19,8 @@ export const usePerformCombinedFindManyRecords = () => {
const client = useApolloClient(); const client = useApolloClient();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const generateCombinedFindManyRecordsQuery = ( const generateCombinedFindManyRecordsQuery = (
operationSignatures: RecordGqlOperationSignature[], operationSignatures: RecordGqlOperationSignature[],
objectMetadataItemsValue: ObjectMetadataItem[], objectMetadataItemsValue: ObjectMetadataItem[],
@ -93,6 +96,7 @@ export const usePerformCombinedFindManyRecords = () => {
generateDepthOneRecordGqlFields({ generateDepthOneRecordGqlFields({
objectMetadataItem, objectMetadataItem,
}), }),
objectPermissionsByObjectMetadataId,
})} })}
cursor cursor
} }

View File

@ -92,6 +92,7 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({
return ( return (
<RecordIndexContextProvider <RecordIndexContextProvider
value={{ value={{
objectPermissionsByObjectMetadataId: {},
indexIdentifierUrl: () => '', indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {}, onIndexRecordsLoaded: () => {},
objectNamePlural: 'companies', objectNamePlural: 'companies',

View File

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { Draggable } from '@hello-pangea/dnd'; import { Draggable } from '@hello-pangea/dnd';
import { useContext } from 'react'; import { useContext } from 'react';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard'; import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
import { RecordBoardCardFocusHotkeyEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect'; import { RecordBoardCardFocusHotkeyEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
@ -23,8 +24,11 @@ export const RecordBoardCardDraggableContainer = ({
recordId: string; recordId: string;
rowIndex: number; rowIndex: number;
}) => { }) => {
const { objectMetadataItem } = useContext(RecordBoardContext);
const isRecordReadOnly = useIsRecordReadOnly({ const isRecordReadOnly = useIsRecordReadOnly({
recordId, recordId,
objectMetadataId: objectMetadataItem.id,
}); });
const { columnIndex } = useContext(RecordBoardColumnContext); const { columnIndex } = useContext(RecordBoardColumnContext);

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu'; import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
@ -9,7 +10,6 @@ import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { Tag } from 'twenty-ui/components'; import { Tag } from 'twenty-ui/components';
import { IconDotsVertical, IconPlus } from 'twenty-ui/display'; import { IconDotsVertical, IconPlus } from 'twenty-ui/display';
@ -95,7 +95,11 @@ export const RecordBoardColumnHeader = () => {
const { aggregateValue, aggregateLabel } = const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordBoardColumn(); useAggregateRecordsForRecordBoardColumn();
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const { createNewIndexRecord } = useCreateNewIndexRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem: objectMetadataItem, objectMetadataItem: objectMetadataItem,
@ -143,7 +147,7 @@ export const RecordBoardColumnHeader = () => {
Icon={IconDotsVertical} Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen} onClick={handleBoardColumnMenuOpen}
/> />
{!hasObjectReadOnlyPermission && ( {hasObjectUpdatePermissions && (
<LightIconButton <LightIconButton
accent="tertiary" accent="tertiary"
Icon={IconPlus} Icon={IconPlus}

View File

@ -1,7 +1,7 @@
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useContext } from 'react'; import { useContext } from 'react';
@ -32,13 +32,17 @@ export const RecordBoardColumnNewRecordButton = () => {
const { columnDefinition } = useContext(RecordBoardColumnContext); const { columnDefinition } = useContext(RecordBoardColumnContext);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const { createNewIndexRecord } = useCreateNewIndexRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem: objectMetadataItem, objectMetadataItem: objectMetadataItem,
}); });
if (hasObjectReadOnlyPermission) { if (!hasObjectUpdatePermissions) {
return null; return null;
} }

View File

@ -4,6 +4,7 @@ import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/displ
import { ArrayFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ArrayFieldDisplay'; import { ArrayFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ArrayFieldDisplay';
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay'; import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
import { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay'; import { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay';
import { ForbiddenFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ForbiddenFieldDisplay';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay'; import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay'; import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay';
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay'; import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
@ -22,6 +23,7 @@ import { isFieldRelationFromManyObjects } from '@/object-record/record-field/typ
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject'; import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '../contexts/FieldContext'; import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay'; import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay'; import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
@ -49,13 +51,18 @@ import { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid'; import { isFieldUuid } from '../types/guards/isFieldUuid';
export const FieldDisplay = () => { export const FieldDisplay = () => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext); const { fieldDefinition, isLabelIdentifier, isForbidden } =
useContext(FieldContext);
const isChipDisplay = isFieldIdentifierDisplay( const isChipDisplay = isFieldIdentifierDisplay(
fieldDefinition, fieldDefinition,
isLabelIdentifier, isLabelIdentifier,
); );
if (isDefined(isForbidden) && isForbidden) {
return <ForbiddenFieldDisplay />;
}
return isChipDisplay ? ( return isChipDisplay ? (
<ChipFieldDisplay /> <ChipFieldDisplay />
) : isFieldRelationToOneObject(fieldDefinition) ? ( ) : isFieldRelationToOneObject(fieldDefinition) ? (

View File

@ -38,6 +38,7 @@ export type GenericFieldContextType = {
onOpenEditMode?: () => void; onOpenEditMode?: () => void;
onCloseEditMode?: () => void; onCloseEditMode?: () => void;
triggerEvent?: TriggerEventType; triggerEvent?: TriggerEventType;
isForbidden?: boolean;
}; };
export const FieldContext = createContext<GenericFieldContextType>( export const FieldContext = createContext<GenericFieldContextType>(

View File

@ -1,15 +1,17 @@
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
type UseIsRecordReadOnlyParams = { type UseIsRecordReadOnlyParams = {
recordId: string; recordId: string;
objectMetadataId: string;
}; };
export const useIsRecordReadOnly = ({ export const useIsRecordReadOnly = ({
recordId, recordId,
objectMetadataId,
}: UseIsRecordReadOnlyParams) => { }: UseIsRecordReadOnlyParams) => {
const recordDeletedAt = useRecoilValue<ObjectRecord | null>( const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
recordStoreFamilySelector({ recordStoreFamilySelector({
@ -18,7 +20,9 @@ export const useIsRecordReadOnly = ({
}), }),
); );
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const objectPermissions = useObjectPermissionsForObject(objectMetadataId);
return hasObjectReadOnlyPermission || isDefined(recordDeletedAt); const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
return !hasObjectUpdatePermissions || isDefined(recordDeletedAt);
}; };

View File

@ -0,0 +1,27 @@
import { Theme, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
const StyledContainer = styled.div<{ theme: Theme }>`
align-items: center;
display: flex;
background: ${({ theme }) => theme.background.transparent.lighter};
color: ${({ theme }) => theme.font.color.tertiary};
font-weight: ${({ theme }) => theme.font.weight.regular};
font-size: ${({ theme }) => theme.font.size.sm};
padding: ${({ theme }) => theme.spacing(1, 2)};
border-radius: 4px;
border: 1px solid ${({ theme }) => theme.border.color.light};
`;
export const ForbiddenFieldDisplay = () => {
const theme = useTheme();
return (
<StyledContainer theme={theme}>
<Trans>Forbidden</Trans>
</StyledContainer>
);
};

View File

@ -31,6 +31,7 @@ const meta: Meta = {
> >
<RecordIndexContextProvider <RecordIndexContextProvider
value={{ value={{
objectPermissionsByObjectMetadataId: {},
indexIdentifierUrl: () => '', indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {}, onIndexRecordsLoaded: () => {},
objectNamePlural: CoreObjectNamePlural.Company, objectNamePlural: CoreObjectNamePlural.Company,

View File

@ -82,6 +82,7 @@ const RelationToOneFieldInputWithContext = ({
relationObjectMetadataNamePlural: 'companies', relationObjectMetadataNamePlural: 'companies',
relationObjectMetadataNameSingular: relationObjectMetadataNameSingular:
CoreObjectNameSingular.Company, CoreObjectNameSingular.Company,
relationObjectMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
objectMetadataNameSingular: 'person', objectMetadataNameSingular: 'person',
relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e', relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
}, },

View File

@ -131,6 +131,7 @@ export type FieldRelationMetadata = BaseFieldMetadata & {
relationFieldMetadataId: string; relationFieldMetadataId: string;
relationObjectMetadataNamePlural: string; relationObjectMetadataNamePlural: string;
relationObjectMetadataNameSingular: string; relationObjectMetadataNameSingular: string;
relationObjectMetadataId: string;
relationType?: RelationDefinitionType; relationType?: RelationDefinitionType;
targetFieldMetadataName?: string; targetFieldMetadataName?: string;
useEditButton?: boolean; useEditButton?: boolean;

View File

@ -23,6 +23,8 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
const StyledIndexContainer = styled.div` const StyledIndexContainer = styled.div`
display: flex; display: flex;
@ -57,10 +59,23 @@ export const RecordIndexContainerGater = () => {
recordIndexId, recordIndexId,
}); });
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
const hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
if (!hasObjectReadPermissions) {
return <></>;
}
return ( return (
<> <>
<RecordIndexContextProvider <RecordIndexContextProvider
value={{ value={{
objectPermissionsByObjectMetadataId,
recordIndexId, recordIndexId,
objectNamePlural: objectMetadataItem.namePlural, objectNamePlural: objectMetadataItem.namePlural,
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,

View File

@ -1,4 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectPermission } from '~/generated-metadata/graphql';
import { createRequiredContext } from '~/utils/createRequiredContext'; import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordIndexContextValue = { export type RecordIndexContextValue = {
@ -7,6 +8,7 @@ export type RecordIndexContextValue = {
objectNamePlural: string; objectNamePlural: string;
objectNameSingular: string; objectNameSingular: string;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
recordIndexId: string; recordIndexId: string;
}; };

View File

@ -229,6 +229,7 @@ describe('useRecordData', () => {
options: null, options: null,
placeHolder: 'Last update', placeHolder: 'Last update',
relationFieldMetadataId: undefined, relationFieldMetadataId: undefined,
relationObjectMetadataId: '',
relationObjectMetadataNamePlural: '', relationObjectMetadataNamePlural: '',
relationObjectMetadataNameSingular: '', relationObjectMetadataNameSingular: '',
relationType: undefined, relationType: undefined,

View File

@ -1,3 +1,5 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { import {
SingleRecordPickerMenuItems, SingleRecordPickerMenuItems,
SingleRecordPickerMenuItemsProps, SingleRecordPickerMenuItemsProps,
@ -7,7 +9,6 @@ import { useSingleRecordPickerSearch } from '@/object-record/record-picker/singl
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection'; import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton'; import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@ -48,8 +49,6 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
SingleRecordPickerComponentInstanceContext, SingleRecordPickerComponentInstanceContext,
); );
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const recordPickerSearchFilter = useRecoilComponentValueV2( const recordPickerSearchFilter = useRecoilComponentValueV2(
singleRecordPickerSearchFilterComponentState, singleRecordPickerSearchFilterComponentState,
recordPickerInstanceId, recordPickerInstanceId,
@ -60,6 +59,16 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
excludedRecordIds, excludedRecordIds,
}); });
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const createNewButton = isDefined(onCreate) && ( const createNewButton = isDefined(onCreate) && (
<CreateNewButton <CreateNewButton
onClick={() => onCreate?.(recordPickerSearchFilter)} onClick={() => onCreate?.(recordPickerSearchFilter)}
@ -72,7 +81,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
<> <>
{layoutDirection === 'search-bar-on-bottom' && ( {layoutDirection === 'search-bar-on-bottom' && (
<> <>
{isDefined(onCreate) && !hasObjectReadOnlyPermission && ( {isDefined(onCreate) && !hasObjectUpdatePermissions && (
<DropdownMenuItemsContainer scrollable={false}> <DropdownMenuItemsContainer scrollable={false}>
{createNewButton} {createNewButton}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
@ -116,7 +125,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
{records.recordsToSelect.length > 0 && isDefined(onCreate) && ( {records.recordsToSelect.length > 0 && isDefined(onCreate) && (
<DropdownMenuSeparator /> <DropdownMenuSeparator />
)} )}
{isDefined(onCreate) && !hasObjectReadOnlyPermission && ( {isDefined(onCreate) && !hasObjectUpdatePermissions && (
<DropdownMenuItemsContainer scrollable={false}> <DropdownMenuItemsContainer scrollable={false}>
{createNewButton} {createNewButton}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>

View File

@ -5,6 +5,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly'; import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
@ -39,6 +41,7 @@ export const FieldsCard = ({
objectNameSingular, objectNameSingular,
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular, objectNameSingular,
@ -87,11 +90,16 @@ export const FieldsCard = ({
fieldMetadataItem.name === 'noteTargets') || fieldMetadataItem.name === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task && (objectNameSingular === CoreObjectNameSingular.Task &&
fieldMetadataItem.name === 'taskTargets') fieldMetadataItem.name === 'taskTargets')
), ) &&
getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
fieldMetadataItem.relationDefinition?.targetObjectMetadata.id,
).canReadObjectRecords,
); );
const isRecordReadOnly = useIsRecordReadOnly({ const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId, recordId: objectRecordId,
objectMetadataId: objectMetadataItem.id,
}); });
return ( return (

View File

@ -1,3 +1,4 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -58,6 +59,10 @@ export const ObjectRecordShowPageBreadcrumb = ({
}, },
}); });
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
@ -65,6 +70,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
const isRecordReadOnly = useIsRecordReadOnly({ const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId, recordId: objectRecordId,
objectMetadataId: objectMetadataItem.id,
}); });
const { navigateToIndexView, rankInView, totalCount } = const { navigateToIndexView, rankInView, totalCount } =

View File

@ -1,4 +1,5 @@
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon'; import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly'; import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
@ -55,8 +56,13 @@ export const SummaryCard = ({
}), }),
); );
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const isRecordReadOnly = useIsRecordReadOnly({ const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId, recordId: objectRecordId,
objectMetadataId: objectMetadataItem.id,
}); });
return ( return (

View File

@ -10,6 +10,7 @@ import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/uti
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { RecordChip } from '@/object-record/components/RecordChip'; import { RecordChip } from '@/object-record/components/RecordChip';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { import {
FieldContext, FieldContext,
@ -124,6 +125,10 @@ export const RecordDetailRelationRecordsListItem = ({
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const relationObjectPermissions = useObjectPermissionsForObject(
relationObjectMetadataItem.id,
);
const persistField = usePersistField(); const persistField = usePersistField();
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({ const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
@ -213,6 +218,7 @@ export const RecordDetailRelationRecordsListItem = ({
const isRecordReadOnly = useIsRecordReadOnly({ const isRecordReadOnly = useIsRecordReadOnly({
recordId: relationRecord.id, recordId: relationRecord.id,
objectMetadataId: relationObjectMetadataItem.id,
}); });
const isFieldReadOnly = useIsFieldValueReadOnly({ const isFieldReadOnly = useIsFieldValueReadOnly({
@ -254,14 +260,15 @@ export const RecordDetailRelationRecordsListItem = ({
text="Detach" text="Detach"
onClick={handleDetach} onClick={handleDetach}
/> />
{!isAccountOwnerRelation && ( {!isAccountOwnerRelation &&
<MenuItem relationObjectPermissions.canSoftDeleteObjectRecords && (
LeftIcon={IconTrash} <MenuItem
text="Delete" LeftIcon={IconTrash}
accent="danger" text="Delete"
onClick={handleDelete} accent="danger"
/> onClick={handleDelete}
)} />
)}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownContent> </DropdownContent>
} }

View File

@ -115,6 +115,7 @@ export const RecordDetailRelationSection = ({
const isRecordReadOnly = useIsRecordReadOnly({ const isRecordReadOnly = useIsRecordReadOnly({
recordId, recordId,
objectMetadataId: relationObjectMetadataItem.id,
}); });
const isFieldReadOnly = useIsFieldValueReadOnly({ const isFieldReadOnly = useIsFieldValueReadOnly({

View File

@ -144,6 +144,7 @@ export const RecordDetailRelationSectionDropdown = ({
const isRecordReadOnly = useIsRecordReadOnly({ const isRecordReadOnly = useIsRecordReadOnly({
recordId, recordId,
objectMetadataId: relationObjectMetadataItem.id,
}); });
const isFieldReadOnly = useIsFieldValueReadOnly({ const isFieldReadOnly = useIsFieldValueReadOnly({

View File

@ -1,6 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useRef } from 'react'; import { useRef } from 'react';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableBodyEffectsWrapper } from '@/object-record/record-table/components/RecordTableBodyEffectsWrapper'; import { RecordTableBodyEffectsWrapper } from '@/object-record/record-table/components/RecordTableBodyEffectsWrapper';
@ -16,7 +17,12 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTable = () => { export const RecordTable = () => {
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow(); const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const tableBodyRef = useRef<HTMLTableElement>(null); const tableBodyRef = useRef<HTMLTableElement>(null);
@ -61,12 +67,16 @@ export const RecordTable = () => {
return ( return (
<> <>
<RecordTableBodyEffectsWrapper {objectPermissions.canReadObjectRecords && (
hasRecordGroups={hasRecordGroups} <>
tableBodyRef={tableBodyRef} <RecordTableBodyEffectsWrapper
/> hasRecordGroups={hasRecordGroups}
<RecordTableScrollToFocusedCellEffect /> tableBodyRef={tableBodyRef}
<RecordTableScrollToFocusedRowEffect /> />
<RecordTableScrollToFocusedCellEffect />
<RecordTableScrollToFocusedRowEffect />
</>
)}
{recordTableIsEmpty && !hasRecordGroups ? ( {recordTableIsEmpty && !hasRecordGroups ? (
<RecordTableEmpty tableBodyRef={tableBodyRef} /> <RecordTableEmpty tableBodyRef={tableBodyRef} />
) : ( ) : (

View File

@ -53,6 +53,7 @@ const meta: Meta = {
return ( return (
<RecordIndexContextProvider <RecordIndexContextProvider
value={{ value={{
objectPermissionsByObjectMetadataId: {},
indexIdentifierUrl: (_recordId: string) => '', indexIdentifierUrl: (_recordId: string) => '',
onIndexRecordsLoaded: () => {}, onIndexRecordsLoaded: () => {},
objectNamePlural: 'companies', objectNamePlural: 'companies',

View File

@ -1,4 +1,5 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll'; import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter'; import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
@ -6,15 +7,12 @@ import { RecordTableEmptyStateReadOnly } from '@/object-record/record-table/empt
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote'; import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete'; import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState'; import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableEmptyState = () => { export const RecordTableEmptyState = () => {
const { recordTableId, objectNameSingular, objectMetadataItem } = const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow(); useRecordTableContextOrThrow();
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 }); const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0; const noRecordAtAll = totalCount === 0;
@ -25,7 +23,13 @@ export const RecordTableEmptyState = () => {
recordTableId, recordTableId,
); );
if (hasObjectReadOnlyPermission) { const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return <RecordTableEmptyStateReadOnly />; return <RecordTableEmptyStateReadOnly />;
} }

View File

@ -16,6 +16,8 @@ import { OnboardingStatus } from '~/generated-metadata/graphql';
export const RecordTableRecordGroupBodyEffect = () => { export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow(); const { objectNameSingular } = useRecordTableContextOrThrow();
const [hasInitialized, setHasInitialized] = useState(false);
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const recordGroupId = useCurrentRecordGroupId(); const recordGroupId = useCurrentRecordGroupId();
@ -84,8 +86,11 @@ export const RecordTableRecordGroupBodyEffect = () => {
return; return;
} }
findManyRecords(); if (!hasInitialized) {
}, [onboardingStatus, findManyRecords]); findManyRecords();
setHasInitialized(true);
}
}, [onboardingStatus, findManyRecords, hasInitialized]);
return <></>; return <></>;
}; };

View File

@ -1,6 +1,9 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext'; import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
@ -19,7 +22,8 @@ export const RecordTableCellFieldContextGeneric = ({
useRecordTableRowContextOrThrow(); useRecordTableRowContextOrThrow();
const { objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const { indexIdentifierUrl } = useRecordIndexContextOrThrow(); const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } =
useRecordIndexContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext); const { columnDefinition } = useContext(RecordTableCellContext);
const isFieldReadOnly = useIsFieldValueReadOnly({ const isFieldReadOnly = useIsFieldValueReadOnly({
@ -29,6 +33,28 @@ export const RecordTableCellFieldContextGeneric = ({
const updateRecord = useContext(RecordUpdateContext); const updateRecord = useContext(RecordUpdateContext);
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
let hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
if (
isFieldRelationToOneObject(columnDefinition) ||
isFieldRelationFromManyObjects(columnDefinition)
) {
const relationObjectMetadataId =
columnDefinition.metadata.relationObjectMetadataId;
const relationObjectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
relationObjectMetadataId,
);
hasObjectReadPermissions = relationObjectPermissions.canReadObjectRecords;
}
return ( return (
<FieldContext.Provider <FieldContext.Provider
value={{ value={{
@ -45,6 +71,7 @@ export const RecordTableCellFieldContextGeneric = ({
}), }),
displayedMaxRows: 1, displayedMaxRows: 1,
isReadOnly: isFieldReadOnly, isReadOnly: isFieldReadOnly,
isForbidden: !hasObjectReadPermissions,
}} }}
> >
{children} {children}

View File

@ -1,8 +1,10 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext'; import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -16,11 +18,13 @@ type RecordTableCellFieldContextLabelIdentifierProps = {
export const RecordTableCellFieldContextLabelIdentifier = ({ export const RecordTableCellFieldContextLabelIdentifier = ({
children, children,
}: RecordTableCellFieldContextLabelIdentifierProps) => { }: RecordTableCellFieldContextLabelIdentifierProps) => {
const { indexIdentifierUrl } = useRecordIndexContextOrThrow(); const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } =
useRecordIndexContextOrThrow();
const { recordId, isReadOnly: isTableRowReadOnly } = const { recordId, isReadOnly: isTableRowReadOnly } =
useRecordTableRowContextOrThrow(); useRecordTableRowContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext); const { columnDefinition } = useContext(RecordTableCellContext);
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isRecordTableScrolledLeftComponent = useRecoilComponentValueV2( const isRecordTableScrolledLeftComponent = useRecoilComponentValueV2(
@ -32,6 +36,13 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
isRecordReadOnly: isTableRowReadOnly ?? false, isRecordReadOnly: isTableRowReadOnly ?? false,
}); });
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
const hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
const updateRecord = useContext(RecordUpdateContext); const updateRecord = useContext(RecordUpdateContext);
const isLabelIdentifierCompact = const isLabelIdentifierCompact =
@ -49,6 +60,7 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
displayedMaxRows: 1, displayedMaxRows: 1,
isReadOnly: isFieldReadOnly, isReadOnly: isFieldReadOnly,
maxWidth: columnDefinition.size, maxWidth: columnDefinition.size,
isForbidden: !hasObjectReadPermissions,
}} }}
> >
{children} {children}

View File

@ -3,6 +3,7 @@ import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
@ -14,7 +15,6 @@ import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener'; import { PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
@ -223,7 +223,11 @@ export const RecordTableHeaderCell = ({
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem); const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const isFirstRowActive = useRecoilComponentFamilyValueV2( const isFirstRowActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState, isRecordTableRowActiveComponentFamilyState,
@ -256,7 +260,7 @@ export const RecordTableHeaderCell = ({
{(useIsMobile() || iconVisibility) && {(useIsMobile() || iconVisibility) &&
!!column.isLabelIdentifier && !!column.isLabelIdentifier &&
!isReadOnly && !isReadOnly &&
!hasObjectReadOnlyPermission && ( hasObjectUpdatePermissions && (
<StyledHeaderIcon> <StyledHeaderIcon>
<LightIconButton <LightIconButton
Icon={IconPlus} Icon={IconPlus}

View File

@ -1,10 +1,10 @@
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow'; import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -23,8 +23,6 @@ export const RecordTableRecordGroupSectionAddNew = () => {
recordGroupDefinitionFamilyState(currentRecordGroupId), recordGroupDefinitionFamilyState(currentRecordGroupId),
); );
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewIndexRecord } = useCreateNewIndexRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem, objectMetadataItem,
}); });
@ -33,7 +31,13 @@ export const RecordTableRecordGroupSectionAddNew = () => {
(field) => field.id === recordGroup?.fieldMetadataId, (field) => field.id === recordGroup?.fieldMetadataId,
); );
if (hasObjectReadOnlyPermission) { const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return null; return null;
} }

View File

@ -24,6 +24,7 @@ describe('computeOptimisticRecordFromInput', () => {
city: 'Paris', city: 'Paris',
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -47,6 +48,7 @@ describe('computeOptimisticRecordFromInput', () => {
createdBy: actorFieldValueForInput, createdBy: actorFieldValueForInput,
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -75,6 +77,7 @@ describe('computeOptimisticRecordFromInput', () => {
} satisfies FieldActorForInputValue, } satisfies FieldActorForInputValue,
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -100,6 +103,7 @@ describe('computeOptimisticRecordFromInput', () => {
companyId: '123', companyId: '123',
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -133,6 +137,7 @@ describe('computeOptimisticRecordFromInput', () => {
cache, cache,
record: companyRecord, record: companyRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId: {},
}); });
const result = computeOptimisticRecordFromInput({ const result = computeOptimisticRecordFromInput({
@ -144,6 +149,7 @@ describe('computeOptimisticRecordFromInput', () => {
__typename: 'test', __typename: 'test',
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}); });
expect(result).toStrictEqual({ expect(result).toStrictEqual({
@ -178,6 +184,7 @@ describe('computeOptimisticRecordFromInput', () => {
cache, cache,
record: companyRecord, record: companyRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId: {},
}); });
const result = computeOptimisticRecordFromInput({ const result = computeOptimisticRecordFromInput({
@ -188,6 +195,7 @@ describe('computeOptimisticRecordFromInput', () => {
companyId: '123', companyId: '123',
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -208,6 +216,7 @@ describe('computeOptimisticRecordFromInput', () => {
companyId: null, companyId: null,
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -232,6 +241,7 @@ describe('computeOptimisticRecordFromInput', () => {
city: 'Paris', city: 'Paris',
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}), }),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadataItem person"`, `"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadataItem person"`,
@ -252,6 +262,7 @@ describe('computeOptimisticRecordFromInput', () => {
company: {}, company: {},
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}), }),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: company"`, `"Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: company"`,
@ -272,6 +283,7 @@ describe('computeOptimisticRecordFromInput', () => {
company: null, company: null,
}, },
cache, cache,
objectPermissionsByObjectMetadataId: {},
}), }),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: company"`, `"Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: company"`,

View File

@ -22,13 +22,17 @@ type ComputeOptimisticCacheRecordInputArgs = {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
recordInput: Partial<ObjectRecord>; recordInput: Partial<ObjectRecord>;
currentWorkspaceMember: CurrentWorkspaceMember | null; currentWorkspaceMember: CurrentWorkspaceMember | null;
} & Pick<GetRecordFromCacheArgs, 'cache' | 'objectMetadataItems'>; } & Pick<
GetRecordFromCacheArgs,
'cache' | 'objectMetadataItems' | 'objectPermissionsByObjectMetadataId'
>;
export const computeOptimisticRecordFromInput = ({ export const computeOptimisticRecordFromInput = ({
objectMetadataItem, objectMetadataItem,
recordInput, recordInput,
cache, cache,
objectMetadataItems, objectMetadataItems,
currentWorkspaceMember, currentWorkspaceMember,
objectPermissionsByObjectMetadataId,
}: ComputeOptimisticCacheRecordInputArgs) => { }: ComputeOptimisticCacheRecordInputArgs) => {
const unknownRecordInputFields = Object.keys(recordInput).filter( const unknownRecordInputFields = Object.keys(recordInput).filter(
(recordKey) => { (recordKey) => {
@ -177,6 +181,7 @@ export const computeOptimisticRecordFromInput = ({
objectMetadataItem: targetObjectMetataDataItem, objectMetadataItem: targetObjectMetataDataItem,
objectMetadataItems, objectMetadataItems,
recordId: recordInputFieldIdValue as string, recordId: recordInputFieldIdValue as string,
objectPermissionsByObjectMetadataId,
}); });
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue; optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;

View File

@ -4,6 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
export type QueryCursorDirection = 'before' | 'after'; export type QueryCursorDirection = 'before' | 'after';
@ -13,12 +14,14 @@ export const generateFindManyRecordsQuery = ({
recordGqlFields, recordGqlFields,
computeReferences, computeReferences,
cursorDirection, cursorDirection,
objectPermissionsByObjectMetadataId,
}: { }: {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
recordGqlFields?: RecordGqlOperationGqlRecordFields; recordGqlFields?: RecordGqlOperationGqlRecordFields;
computeReferences?: boolean; computeReferences?: boolean;
cursorDirection?: QueryCursorDirection; cursorDirection?: QueryCursorDirection;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
}) => gql` }) => gql`
query FindMany${capitalize( query FindMany${capitalize(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
@ -38,6 +41,7 @@ query FindMany${capitalize(
objectMetadataItem, objectMetadataItem,
recordGqlFields, recordGqlFields,
computeReferences, computeReferences,
objectPermissionsByObjectMetadataId,
})} })}
cursor cursor
} }

View File

@ -45,6 +45,7 @@ export const SignInBackgroundMockContainer = () => {
<StyledContainer> <StyledContainer>
<RecordIndexContextProvider <RecordIndexContextProvider
value={{ value={{
objectPermissionsByObjectMetadataId: {},
recordIndexId, recordIndexId,
objectNamePlural, objectNamePlural,
objectNameSingular, objectNameSingular,

View File

@ -1,34 +0,0 @@
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useLingui } from '@lingui/react/macro';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display';
import { useIsMobile } from 'twenty-ui/utilities';
type PageAddButtonProps = {
onClick?: () => void;
};
export const PageAddButton = ({ onClick }: PageAddButtonProps) => {
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const isMobile = useIsMobile();
const { t } = useLingui();
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<Button
Icon={IconPlus}
dataTestId="add-button"
size={isMobile ? 'medium' : 'small'}
variant="secondary"
accent="default"
title={isMobile ? '' : t`New record`}
onClick={onClick}
ariaLabel={t`New record`}
/>
);
};

View File

@ -1,3 +1,4 @@
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment'; import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment'; import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment'; import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
@ -5,6 +6,7 @@ import { gql } from '@apollo/client';
export const USER_QUERY_FRAGMENT = gql` export const USER_QUERY_FRAGMENT = gql`
${ROLE_FRAGMENT} ${ROLE_FRAGMENT}
${OBJECT_PERMISSION_FRAGMENT}
fragment UserQueryFragment on User { fragment UserQueryFragment on User {
id id
firstName firstName
@ -26,6 +28,9 @@ export const USER_QUERY_FRAGMENT = gql`
currentUserWorkspace { currentUserWorkspace {
settingsPermissions settingsPermissions
objectRecordsPermissions objectRecordsPermissions
objectPermissions {
...ObjectPermissionFragment
}
} }
currentWorkspace { currentWorkspace {
id id

View File

@ -97,6 +97,7 @@ const meta: Meta<typeof ViewBarFilterDropdown> = {
return ( return (
<RecordIndexContextProvider <RecordIndexContextProvider
value={{ value={{
objectPermissionsByObjectMetadataId: {},
indexIdentifierUrl: () => '', indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {}, onIndexRecordsLoaded: () => {},
objectNamePlural: CoreObjectNamePlural.Company, objectNamePlural: CoreObjectNamePlural.Company,

View File

@ -9,6 +9,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewField } from '@/views/types/ViewField'; import { ViewField } from '@/views/types/ViewField';
@ -33,7 +34,7 @@ export const usePersistViewFieldRecords = () => {
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const createViewFieldRecords = useCallback( const createViewFieldRecords = useCallback(
@ -62,6 +63,7 @@ export const usePersistViewFieldRecords = () => {
objectMetadataItem, objectMetadataItem,
recordsToCreate: [record], recordsToCreate: [record],
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}); });
}, },
}), }),
@ -73,6 +75,7 @@ export const usePersistViewFieldRecords = () => {
createOneRecordMutation, createOneRecordMutation,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
], ],
); );

View File

@ -10,6 +10,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
@ -37,7 +38,7 @@ export const usePersistViewFilterGroupRecords = () => {
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const createViewFilterGroupRecord = useCallback( const createViewFilterGroupRecord = useCallback(
@ -65,6 +66,7 @@ export const usePersistViewFilterGroupRecords = () => {
objectMetadataItem, objectMetadataItem,
recordsToCreate: [record], recordsToCreate: [record],
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}); });
}, },
}); });
@ -80,6 +82,7 @@ export const usePersistViewFilterGroupRecords = () => {
createOneRecordMutation, createOneRecordMutation,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
], ],
); );

View File

@ -10,6 +10,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewFilter } from '@/views/types/ViewFilter'; import { ViewFilter } from '@/views/types/ViewFilter';
@ -37,7 +38,7 @@ export const usePersistViewFilterRecords = () => {
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const createViewFilterRecords = useCallback( const createViewFilterRecords = useCallback(
@ -70,6 +71,7 @@ export const usePersistViewFilterRecords = () => {
objectMetadataItem, objectMetadataItem,
recordsToCreate: [record], recordsToCreate: [record],
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}); });
}, },
}), }),
@ -81,6 +83,7 @@ export const usePersistViewFilterRecords = () => {
createOneRecordMutation, createOneRecordMutation,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
], ],
); );

View File

@ -10,6 +10,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewSort } from '@/views/types/ViewSort'; import { ViewSort } from '@/views/types/ViewSort';
@ -37,7 +38,7 @@ export const usePersistViewSortRecords = () => {
}); });
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const createViewSortRecords = useCallback( const createViewSortRecords = useCallback(
@ -64,6 +65,7 @@ export const usePersistViewSortRecords = () => {
objectMetadataItem, objectMetadataItem,
recordsToCreate: [record], recordsToCreate: [record],
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}); });
}, },
}), }),
@ -75,6 +77,7 @@ export const usePersistViewSortRecords = () => {
createOneRecordMutation, createOneRecordMutation,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
], ],
); );

View File

@ -11,6 +11,7 @@ import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObje
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery'; import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { ViewFilter } from '@/views/types/ViewFilter'; import { ViewFilter } from '@/views/types/ViewFilter';
@ -45,6 +46,8 @@ export const useViewFromQueryParams = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const queryParamsValidation = filterQueryParamsSchema.safeParse( const queryParamsValidation = filterQueryParamsSchema.safeParse(
qs.parse(searchParams.toString()), qs.parse(searchParams.toString()),
); );
@ -122,6 +125,7 @@ export const useViewFromQueryParams = () => {
query: generateFindManyRecordsQuery({ query: generateFindManyRecordsQuery({
objectMetadataItem: relationObjectMetadataItem, objectMetadataItem: relationObjectMetadataItem,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
}), }),
variables: { variables: {
filter: { filter: {
@ -182,6 +186,7 @@ export const useViewFromQueryParams = () => {
hasFiltersQueryParams, hasFiltersQueryParams,
objectMetadataItem.fields, objectMetadataItem.fields,
objectMetadataItems, objectMetadataItems,
objectPermissionsByObjectMetadataId,
], ],
); );

View File

@ -1,7 +1,7 @@
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';

View File

@ -3,6 +3,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep'; import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep';
import { WorkflowVersion } from '@/workflow/types/Workflow'; import { WorkflowVersion } from '@/workflow/types/Workflow';
import { useApolloClient, useMutation } from '@apollo/client'; import { useApolloClient, useMutation } from '@apollo/client';
@ -17,6 +18,7 @@ import {
export const useDeleteWorkflowVersionStep = () => { export const useDeleteWorkflowVersionStep = () => {
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion, objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
}); });
@ -68,6 +70,7 @@ export const useDeleteWorkflowVersionStep = () => {
cache: apolloClient.cache, cache: apolloClient.cache,
record: newCachedRecord, record: newCachedRecord,
recordGqlFields, recordGqlFields,
objectPermissionsByObjectMetadataId,
}); });
}; };

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