[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 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; }>;
@ -2977,7 +2977,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, 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<{
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`
fragment SettingPermissionFragment on SettingPermission {
id
@ -3259,6 +3250,15 @@ export const DeletedWorkspaceMemberQueryFragmentFragmentDoc = gql`
userEmail
}
`;
export const ObjectPermissionFragmentFragmentDoc = gql`
fragment ObjectPermissionFragment on ObjectPermission {
objectMetadataId
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
`;
export const RoleFragmentFragmentDoc = gql`
fragment RoleFragment on Role {
id
@ -3295,6 +3295,9 @@ export const UserQueryFragmentFragmentDoc = gql`
currentUserWorkspace {
settingsPermissions
objectRecordsPermissions
objectPermissions {
...ObjectPermissionFragment
}
}
currentWorkspace {
id
@ -3364,6 +3367,7 @@ export const UserQueryFragmentFragmentDoc = gql`
}
${WorkspaceMemberQueryFragmentFragmentDoc}
${DeletedWorkspaceMemberQueryFragmentFragmentDoc}
${ObjectPermissionFragmentFragmentDoc}
${RoleFragmentFragmentDoc}`;
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ export const ObjectTasks = ({ targetableObject }: ObjectTasksProps) => {
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: 'entity-tasks-filter-scope' }}
>
<TaskGroups targetableObjects={[targetableObject]} />
<TaskGroups targetableObject={targetableObject} />
</ObjectFilterDropdownComponentInstanceContext.Provider>
</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 { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Task } from '@/activities/types/Task';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import groupBy from 'lodash.groupby';
import { AddTaskButton } from './AddTaskButton';
import { TaskList } from './TaskList';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@ -20,8 +21,8 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display';
import { AddTaskButton } from './AddTaskButton';
import { TaskList } from './TaskList';
const StyledContainer = styled.div`
display: flex;
@ -31,15 +32,23 @@ const StyledContainer = styled.div`
type TaskGroupsProps = {
filterDropdownId?: string;
targetableObjects?: ActivityTargetableObject[];
targetableObject: ActivityTargetableObject;
};
export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
export const TaskGroups = ({ targetableObject }: TaskGroupsProps) => {
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({
activityObjectNameSingular: CoreObjectNameSingular.Task,
@ -74,14 +83,14 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
All tasks addressed. Maintain the momentum.
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{!hasObjectReadOnlyPermission && (
{!hasObjectUpdatePermissions && (
<Button
Icon={IconPlus}
title="New task"
variant={'secondary'}
onClick={() =>
openCreateActivity({
targetableObjects: targetableObjects ?? [],
targetableObjects: [targetableObject],
})
}
/>
@ -107,7 +116,7 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
tasks={tasksByStatus}
button={
(status === 'TODO' || !hasTodoStatus) && (
<AddTaskButton activityTargetableObjects={targetableObjects} />
<AddTaskButton activityTargetableObject={targetableObject} />
)
}
/>

View File

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

View File

@ -13,8 +13,9 @@ import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRec
import { encodeCursor } from '@/apollo/utils/encodeCursor';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
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.
@ -28,6 +29,7 @@ type TriggerCreateRecordsOptimisticEffectArgs = {
objectMetadataItems: ObjectMetadataItem[];
shouldMatchRootQueryFilter?: boolean;
checkForRecordInCache?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
};
export const triggerCreateRecordsOptimisticEffect = ({
cache,
@ -36,6 +38,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
objectMetadataItems,
shouldMatchRootQueryFilter,
checkForRecordInCache = false,
objectPermissionsByObjectMetadataId,
}: TriggerCreateRecordsOptimisticEffectArgs) => {
const getRecordNodeFromCache = (recordId: string): RecordGqlNode | null => {
const cachedRecord = getRecordFromCache({
@ -43,6 +46,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
objectMetadataItem,
objectMetadataItems,
recordId,
objectPermissionsByObjectMetadataId,
});
return getRecordNodeFromRecord({
objectMetadataItem,

View File

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

View File

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

View File

@ -50,6 +50,7 @@ export const initialFavorites: Favorite[] = [
key: mockId,
labelIdentifier: 'favoriteLabel',
avatarUrl: 'example.com',
company: { id: '5', name: 'Company Test 2' },
avatarType: 'squared' as AvatarType,
link: 'example.com',
recordId: '1',
@ -86,17 +87,17 @@ export const sortedFavorites = [
__typename: 'Favorite',
},
{
id: '3',
position: 2,
key: '8f3b2121-f194-4ba4-9fbf-2d5a37126806',
labelIdentifier: 'favoriteLabel',
avatarUrl: 'example.com',
link: 'example.com',
recordId: '1',
__typename: 'Favorite',
avatarType: 'squared',
avatarUrl: undefined,
favoriteFolderId: '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;
}
}
return {
...favorite,
} as ProcessedFavorite;
return null;
})
.filter(isDefined)
.sort((a, b) => a.position - b.position);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
@ -16,6 +17,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({
const apolloClient = useApolloClient();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const upsertFindManyRecordsQueryInCache = <
T extends ObjectRecord = ObjectRecord,
@ -35,6 +37,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({
objectMetadataItems,
recordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
});
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 { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
import { isEmptyObject } from '~/utils/isEmptyObject';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
export type GetRecordFromCacheArgs = {
cache: ApolloCache<object>;
@ -16,6 +17,7 @@ export type GetRecordFromCacheArgs = {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: ObjectMetadataItem;
recordGqlFields?: RecordGqlFields;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
};
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItem,
@ -23,6 +25,7 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
cache,
recordId,
recordGqlFields,
objectPermissionsByObjectMetadataId,
}: GetRecordFromCacheArgs) => {
if (isUndefinedOrNull(objectMetadataItem)) {
return null;
@ -39,6 +42,7 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItems,
objectMetadataItem,
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const updateRecordFromCache = <T extends ObjectRecord>({
@ -15,12 +16,14 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
cache,
recordGqlFields,
record,
objectPermissionsByObjectMetadataId,
}: {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: ObjectMetadataItem;
cache: ApolloCache<object>;
recordGqlFields: Record<string, boolean>;
record: T;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
}) => {
if (isUndefinedOrNull(objectMetadataItem)) {
return null;
@ -35,6 +38,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
objectMetadataItem,
computeReferences: true,
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 { useQuery } from '@apollo/client';
import { renderHook } from '@testing-library/react';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
// Mocks
jest.mock('@apollo/client');
@ -25,6 +26,10 @@ const mockGqlFieldToFieldMap = {
totalCount: ['name', AggregateOperations.COUNT],
};
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useAggregateRecords', () => {
beforeEach(() => {
(useObjectMetadataItem as jest.Mock).mockReturnValue({
@ -44,14 +49,18 @@ describe('useAggregateRecords', () => {
});
it('should format data correctly', () => {
const { result } = renderHook(() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM, AggregateOperations.AVG],
name: [AggregateOperations.COUNT],
},
}),
const { result } = renderHook(
() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM, AggregateOperations.AVG],
name: [AggregateOperations.COUNT],
},
}),
{
wrapper: Wrapper,
},
);
expect(result.current.data).toEqual({
@ -74,13 +83,17 @@ describe('useAggregateRecords', () => {
error: undefined,
});
const { result } = renderHook(() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM],
},
}),
const { result } = renderHook(
() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM],
},
}),
{
wrapper: Wrapper,
},
);
expect(result.current.data).toEqual({});
@ -95,13 +108,17 @@ describe('useAggregateRecords', () => {
error: mockError,
});
const { result } = renderHook(() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM],
},
}),
const { result } = renderHook(
() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM],
},
}),
{
wrapper: Wrapper,
},
);
expect(result.current.data).toEqual({});
@ -109,14 +126,18 @@ describe('useAggregateRecords', () => {
});
it('should skip query when specified', () => {
renderHook(() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM],
},
skip: true,
}),
renderHook(
() =>
useAggregateRecords({
objectNameSingular: 'opportunity',
recordGqlFieldsAggregate: {
amount: [AggregateOperations.SUM],
},
skip: true,
}),
{
wrapper: Wrapper,
},
);
expect(useQuery).toHaveBeenCalledWith(

View File

@ -57,6 +57,7 @@ describe('useDeleteManyRecords', () => {
objectMetadataItem,
objectMetadataItems,
recordId: expectedRecord.id,
objectPermissionsByObjectMetadataId: {},
});
expect(cachedRecord).not.toBeNull();
if (cachedRecord === null) throw new Error('Should never occur');
@ -72,6 +73,7 @@ describe('useDeleteManyRecords', () => {
objectMetadataItem,
objectMetadataItems,
recordId,
objectPermissionsByObjectMetadataId: {},
}),
).toBeNull(),
);
@ -119,6 +121,7 @@ describe('useDeleteManyRecords', () => {
objectMetadataItem,
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 { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-shared/utils';
@ -35,10 +36,16 @@ export const useAggregateRecords = <T extends AggregateRecordsData>({
recordGqlFieldsAggregate,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const { data, loading, error } = useQuery<RecordGqlOperationFindManyResult>(
aggregateQuery,
{
skip: skip || !objectMetadataItem,
skip: skip || !objectMetadataItem || !hasReadPermission,
variables: {
filter,
},

View File

@ -5,6 +5,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
@ -61,7 +62,7 @@ export const useAttachRelatedRecordFromRecord = ({
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecordAndAttachRelations = async ({
recordId,
relatedRecordId,
@ -98,6 +99,7 @@ export const useAttachRelatedRecordFromRecord = ({
[fieldOnRelatedObject]: previousRecord,
},
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 { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -70,7 +71,7 @@ export const useCreateManyRecords = <
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
@ -118,6 +119,7 @@ export const useCreateManyRecords = <
...baseOptimisticRecordInputCreatedBy,
...recordToCreate,
},
objectPermissionsByObjectMetadataId,
}),
id: idForCreation as string,
};
@ -152,6 +154,7 @@ export const useCreateManyRecords = <
recordsToCreate: recordNodeCreatedInCache,
objectMetadataItems,
shouldMatchRootQueryFilter,
objectPermissionsByObjectMetadataId,
});
}
@ -178,6 +181,7 @@ export const useCreateManyRecords = <
objectMetadataItems,
shouldMatchRootQueryFilter,
checkForRecordInCache: true,
objectPermissionsByObjectMetadataId,
});
},
})

View File

@ -6,9 +6,10 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateManyRecordsMutation = ({
objectNameSingular,
@ -21,6 +22,8 @@ export const useCreateManyRecordsMutation = ({
objectNameSingular,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (isUndefinedOrNull(objectMetadataItem)) {
@ -42,6 +45,7 @@ export const useCreateManyRecordsMutation = ({
objectMetadataItems,
objectMetadataItem,
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 { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput';
@ -62,6 +63,7 @@ export const useCreateOneRecord = <
);
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
@ -90,6 +92,7 @@ export const useCreateOneRecord = <
...recordInput,
id: idForCreation,
},
objectPermissionsByObjectMetadataId,
});
const recordCreatedInCache = createOneRecordInCache({
...optimisticRecordInput,
@ -113,6 +116,7 @@ export const useCreateOneRecord = <
recordsToCreate: [optimisticRecordNode],
objectMetadataItems,
shouldMatchRootQueryFilter,
objectPermissionsByObjectMetadataId,
});
}
}
@ -136,6 +140,7 @@ export const useCreateOneRecord = <
objectMetadataItems,
shouldMatchRootQueryFilter,
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 { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateOneRecordMutation = ({
objectNameSingular,
@ -30,6 +31,8 @@ export const useCreateOneRecordMutation = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
if (isUndefinedOrNull(objectMetadataItem)) {
return { createOneRecordMutation: EMPTY_MUTATION };
}
@ -46,6 +49,7 @@ export const useCreateOneRecordMutation = ({
objectMetadataItems,
objectMetadataItem,
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 { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil';
import { sleep } from '~/utils/sleep';
import { isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
type useDeleteManyRecordProps = {
objectNameSingular: string;
@ -52,7 +53,7 @@ export const useDeleteManyRecords = ({
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
@ -115,6 +116,7 @@ export const useDeleteManyRecords = ({
cache: apolloClient.cache,
record: computedOptimisticRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
computedOptimisticRecordsNode.push(optimisticRecordNode);
@ -156,6 +158,7 @@ export const useDeleteManyRecords = ({
cache: apolloClient.cache,
record: { ...cachedRecord, deletedAt: null },
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
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 { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
@ -37,7 +38,7 @@ export const useDeleteOneRecord = ({
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
@ -84,6 +85,7 @@ export const useDeleteOneRecord = ({
cache: apolloClient.cache,
record: computedOptimisticRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
@ -133,6 +135,7 @@ export const useDeleteOneRecord = ({
deletedAt: null,
},
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
@ -156,6 +159,7 @@ export const useDeleteOneRecord = ({
mutationResponseField,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
refetchAggregateQueries,
],
);

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetc
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted';
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
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' } }],
};
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const { data, loading, error, fetchMore } =
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem,
skip: skip || !objectMetadataItem || !hasReadPermission,
variables: {
filter: withSoftDeleted
? {

View File

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

View File

@ -8,6 +8,7 @@ import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
@ -38,10 +39,16 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
withSoftDeleted,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const { data, loading, error } = useQuery<{
[nameSingular: string]: RecordGqlNode;
}>(findOneRecordQuery, {
skip: !objectMetadataItem || !objectRecordId || skip,
skip: !objectMetadataItem || !objectRecordId || skip || !hasReadPermission,
variables: { objectRecordId },
onCompleted: (data) => {
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 { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { capitalize } from 'twenty-shared/utils';
export const useFindOneRecordQuery = ({
@ -22,6 +23,8 @@ export const useFindOneRecordQuery = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const findOneRecordQuery = gql`
query FindOne${capitalize(
objectMetadataItem.nameSingular,
@ -44,6 +47,7 @@ export const useFindOneRecordQuery = ({
objectMetadataItems,
objectMetadataItem,
recordGqlFields,
objectPermissionsByObjectMetadataId,
})}
},
`;

View File

@ -8,6 +8,7 @@ import { UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecor
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted';
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { cursorFamilyState } from '@/object-record/states/cursorFamilyState';
import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -55,6 +56,12 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
onCompleted,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasReadPermission = objectPermissions.canReadObjectRecords;
const [findManyRecords, { data, loading, error, fetchMore }] =
useLazyQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
variables: {
@ -83,6 +90,20 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const findManyRecordsLazy = useRecoilCallback(
({ set }) =>
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 hasNextPage =
@ -98,19 +119,26 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
return result;
},
[queryIdentifier, findManyRecords, objectMetadataItem],
[
hasReadPermission,
findManyRecords,
objectMetadataItem.namePlural,
queryIdentifier,
onCompleted,
],
);
return {
objectMetadataItem,
records,
totalCount,
loading,
error,
loading: hasReadPermission ? loading : false,
error: hasReadPermission ? error : undefined,
fetchMore,
fetchMoreRecords,
queryStateIdentifier: queryIdentifier,
findManyRecords: findManyRecordsLazy,
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 { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil';
import { sleep } from '~/utils/sleep';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
type useRestoreManyRecordProps = {
objectNameSingular: string;
@ -50,7 +51,7 @@ export const useRestoreManyRecords = ({
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
@ -111,6 +112,7 @@ export const useRestoreManyRecords = ({
cache: apolloClient.cache,
record: computedOptimisticRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
@ -169,6 +171,7 @@ export const useRestoreManyRecords = ({
cache: apolloClient.cache,
record: cachedRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { useContext, useState } from 'react';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
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 { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
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 { Tag } from 'twenty-ui/components';
import { IconDotsVertical, IconPlus } from 'twenty-ui/display';
@ -95,7 +95,11 @@ export const RecordBoardColumnHeader = () => {
const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordBoardColumn();
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem: objectMetadataItem,
@ -143,7 +147,7 @@ export const RecordBoardColumnHeader = () => {
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
/>
{!hasObjectReadOnlyPermission && (
{hasObjectUpdatePermissions && (
<LightIconButton
accent="tertiary"
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 { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext } from 'react';
@ -32,13 +32,17 @@ export const RecordBoardColumnNewRecordButton = () => {
const { columnDefinition } = useContext(RecordBoardColumnContext);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem: objectMetadataItem,
});
if (hasObjectReadOnlyPermission) {
if (!hasObjectUpdatePermissions) {
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 { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
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 { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay';
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 { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
@ -49,13 +51,18 @@ import { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid';
export const FieldDisplay = () => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
const { fieldDefinition, isLabelIdentifier, isForbidden } =
useContext(FieldContext);
const isChipDisplay = isFieldIdentifierDisplay(
fieldDefinition,
isLabelIdentifier,
);
if (isDefined(isForbidden) && isForbidden) {
return <ForbiddenFieldDisplay />;
}
return isChipDisplay ? (
<ChipFieldDisplay />
) : isFieldRelationToOneObject(fieldDefinition) ? (

View File

@ -38,6 +38,7 @@ export type GenericFieldContextType = {
onOpenEditMode?: () => void;
onCloseEditMode?: () => void;
triggerEvent?: TriggerEventType;
isForbidden?: boolean;
};
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
type UseIsRecordReadOnlyParams = {
recordId: string;
objectMetadataId: string;
};
export const useIsRecordReadOnly = ({
recordId,
objectMetadataId,
}: UseIsRecordReadOnlyParams) => {
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
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
value={{
objectPermissionsByObjectMetadataId: {},
indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {},
objectNamePlural: CoreObjectNamePlural.Company,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRef } from 'react';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
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';
export const RecordTable = () => {
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const tableBodyRef = useRef<HTMLTableElement>(null);
@ -61,12 +67,16 @@ export const RecordTable = () => {
return (
<>
<RecordTableBodyEffectsWrapper
hasRecordGroups={hasRecordGroups}
tableBodyRef={tableBodyRef}
/>
<RecordTableScrollToFocusedCellEffect />
<RecordTableScrollToFocusedRowEffect />
{objectPermissions.canReadObjectRecords && (
<>
<RecordTableBodyEffectsWrapper
hasRecordGroups={hasRecordGroups}
tableBodyRef={tableBodyRef}
/>
<RecordTableScrollToFocusedCellEffect />
<RecordTableScrollToFocusedRowEffect />
</>
)}
{recordTableIsEmpty && !hasRecordGroups ? (
<RecordTableEmpty tableBodyRef={tableBodyRef} />
) : (

View File

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

View File

@ -1,4 +1,5 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
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 { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
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';
export const RecordTableEmptyState = () => {
const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow();
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;
@ -25,7 +23,13 @@ export const RecordTableEmptyState = () => {
recordTableId,
);
if (hasObjectReadOnlyPermission) {
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return <RecordTableEmptyStateReadOnly />;
}

View File

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

View File

@ -1,6 +1,9 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
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 { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
@ -19,7 +22,8 @@ export const RecordTableCellFieldContextGeneric = ({
useRecordTableRowContextOrThrow();
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } =
useRecordIndexContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext);
const isFieldReadOnly = useIsFieldValueReadOnly({
@ -29,6 +33,28 @@ export const RecordTableCellFieldContextGeneric = ({
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 (
<FieldContext.Provider
value={{
@ -45,6 +71,7 @@ export const RecordTableCellFieldContextGeneric = ({
}),
displayedMaxRows: 1,
isReadOnly: isFieldReadOnly,
isForbidden: !hasObjectReadPermissions,
}}
>
{children}

View File

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

View File

@ -3,6 +3,7 @@ import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
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 { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
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 { PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
@ -223,7 +223,11 @@ export const RecordTableHeaderCell = ({
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const isFirstRowActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState,
@ -256,7 +260,7 @@ export const RecordTableHeaderCell = ({
{(useIsMobile() || iconVisibility) &&
!!column.isLabelIdentifier &&
!isReadOnly &&
!hasObjectReadOnlyPermission && (
hasObjectUpdatePermissions && (
<StyledHeaderIcon>
<LightIconButton
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 { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
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 { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
@ -23,8 +23,6 @@ export const RecordTableRecordGroupSectionAddNew = () => {
recordGroupDefinitionFamilyState(currentRecordGroupId),
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
});
@ -33,7 +31,13 @@ export const RecordTableRecordGroupSectionAddNew = () => {
(field) => field.id === recordGroup?.fieldMetadataId,
);
if (hasObjectReadOnlyPermission) {
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return null;
}

View File

@ -24,6 +24,7 @@ describe('computeOptimisticRecordFromInput', () => {
city: 'Paris',
},
cache,
objectPermissionsByObjectMetadataId: {},
});
expect(result).toEqual({
@ -47,6 +48,7 @@ describe('computeOptimisticRecordFromInput', () => {
createdBy: actorFieldValueForInput,
},
cache,
objectPermissionsByObjectMetadataId: {},
});
expect(result).toEqual({
@ -75,6 +77,7 @@ describe('computeOptimisticRecordFromInput', () => {
} satisfies FieldActorForInputValue,
},
cache,
objectPermissionsByObjectMetadataId: {},
});
expect(result).toEqual({
@ -100,6 +103,7 @@ describe('computeOptimisticRecordFromInput', () => {
companyId: '123',
},
cache,
objectPermissionsByObjectMetadataId: {},
});
expect(result).toEqual({
@ -133,6 +137,7 @@ describe('computeOptimisticRecordFromInput', () => {
cache,
record: companyRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId: {},
});
const result = computeOptimisticRecordFromInput({
@ -144,6 +149,7 @@ describe('computeOptimisticRecordFromInput', () => {
__typename: 'test',
},
cache,
objectPermissionsByObjectMetadataId: {},
});
expect(result).toStrictEqual({
@ -178,6 +184,7 @@ describe('computeOptimisticRecordFromInput', () => {
cache,
record: companyRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId: {},
});
const result = computeOptimisticRecordFromInput({
@ -188,6 +195,7 @@ describe('computeOptimisticRecordFromInput', () => {
companyId: '123',
},
cache,
objectPermissionsByObjectMetadataId: {},
});
expect(result).toEqual({
@ -208,6 +216,7 @@ describe('computeOptimisticRecordFromInput', () => {
companyId: null,
},
cache,
objectPermissionsByObjectMetadataId: {},
});
expect(result).toEqual({
@ -232,6 +241,7 @@ describe('computeOptimisticRecordFromInput', () => {
city: 'Paris',
},
cache,
objectPermissionsByObjectMetadataId: {},
}),
).toThrowErrorMatchingInlineSnapshot(
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadataItem person"`,
@ -252,6 +262,7 @@ describe('computeOptimisticRecordFromInput', () => {
company: {},
},
cache,
objectPermissionsByObjectMetadataId: {},
}),
).toThrowErrorMatchingInlineSnapshot(
`"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,
},
cache,
objectPermissionsByObjectMetadataId: {},
}),
).toThrowErrorMatchingInlineSnapshot(
`"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;
recordInput: Partial<ObjectRecord>;
currentWorkspaceMember: CurrentWorkspaceMember | null;
} & Pick<GetRecordFromCacheArgs, 'cache' | 'objectMetadataItems'>;
} & Pick<
GetRecordFromCacheArgs,
'cache' | 'objectMetadataItems' | 'objectPermissionsByObjectMetadataId'
>;
export const computeOptimisticRecordFromInput = ({
objectMetadataItem,
recordInput,
cache,
objectMetadataItems,
currentWorkspaceMember,
objectPermissionsByObjectMetadataId,
}: ComputeOptimisticCacheRecordInputArgs) => {
const unknownRecordInputFields = Object.keys(recordInput).filter(
(recordKey) => {
@ -177,6 +181,7 @@ export const computeOptimisticRecordFromInput = ({
objectMetadataItem: targetObjectMetataDataItem,
objectMetadataItems,
recordId: recordInputFieldIdValue as string,
objectPermissionsByObjectMetadataId,
});
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;

View File

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

View File

@ -45,6 +45,7 @@ export const SignInBackgroundMockContainer = () => {
<StyledContainer>
<RecordIndexContextProvider
value={{
objectPermissionsByObjectMetadataId: {},
recordIndexId,
objectNamePlural,
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 { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
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`
${ROLE_FRAGMENT}
${OBJECT_PERMISSION_FRAGMENT}
fragment UserQueryFragment on User {
id
firstName
@ -26,6 +28,9 @@ export const USER_QUERY_FRAGMENT = gql`
currentUserWorkspace {
settingsPermissions
objectRecordsPermissions
objectPermissions {
...ObjectPermissionFragment
}
}
currentWorkspace {
id

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObje
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { ViewFilter } from '@/views/types/ViewFilter';
@ -45,6 +46,8 @@ export const useViewFromQueryParams = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const queryParamsValidation = filterQueryParamsSchema.safeParse(
qs.parse(searchParams.toString()),
);
@ -122,6 +125,7 @@ export const useViewFromQueryParams = () => {
query: generateFindManyRecordsQuery({
objectMetadataItem: relationObjectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
}),
variables: {
filter: {
@ -182,6 +186,7 @@ export const useViewFromQueryParams = () => {
hasFiltersQueryParams,
objectMetadataItem.fields,
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 { 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 { 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 { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
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 { WorkflowVersion } from '@/workflow/types/Workflow';
import { useApolloClient, useMutation } from '@apollo/client';
@ -17,6 +18,7 @@ import {
export const useDeleteWorkflowVersionStep = () => {
const apolloClient = useApolloClient();
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
@ -68,6 +70,7 @@ export const useDeleteWorkflowVersionStep = () => {
cache: apolloClient.cache,
record: newCachedRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
};

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