diff --git a/packages/twenty-front/.eslintrc.cjs b/packages/twenty-front/.eslintrc.cjs index d730af3009..1fe963ff84 100644 --- a/packages/twenty-front/.eslintrc.cjs +++ b/packages/twenty-front/.eslintrc.cjs @@ -15,6 +15,7 @@ module.exports = { 'vite.config.ts', 'setupTests.ts', '**/__mocks__/**', + 'src/testing/mock-data/**', ], overrides: [ { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/__tests__/useRelatedRecordActions.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/__tests__/useRelatedRecordActions.test.tsx index 6355824c76..d44487de90 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/__tests__/useRelatedRecordActions.test.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/__tests__/useRelatedRecordActions.test.tsx @@ -38,6 +38,8 @@ describe('useRelatedRecordActions', () => { it('should return empty object when objectMetadataItem has no fields', () => { const objectMetadataItem = { fields: [], + readableFields: [], + updatableFields: [], } as unknown as ObjectMetadataItem; const { result } = renderHook( @@ -70,33 +72,36 @@ describe('useRelatedRecordActions', () => { }); it('should generate actions for one-to-many relations', () => { + const fields = [ + { + type: 'RELATION', + relation: { + type: 'ONE_TO_MANY', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Person, + namePlural: 'People', + }, + }, + label: 'person', + isSystem: false, + }, + { + type: 'RELATION', + relation: { + type: 'ONE_TO_MANY', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Company, + namePlural: 'Companies', + }, + }, + label: 'company', + isSystem: false, + }, + ]; const objectMetadataItem = { - fields: [ - { - type: 'RELATION', - relation: { - type: 'ONE_TO_MANY', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Person, - namePlural: 'People', - }, - }, - label: 'person', - isSystem: false, - }, - { - type: 'RELATION', - relation: { - type: 'ONE_TO_MANY', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Company, - namePlural: 'Companies', - }, - }, - label: 'company', - isSystem: false, - }, - ], + fields, + readableFields: fields, + updatableFields: fields, } as unknown as ObjectMetadataItem; const { result } = renderHook( @@ -115,36 +120,40 @@ describe('useRelatedRecordActions', () => { }); it('should filter out non-one-to-many relations', () => { + const fields = [ + { + type: 'RELATION', + relation: { + type: 'MANY_TO_ONE', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Person, + namePlural: 'People', + }, + }, + label: 'person', + isSystem: false, + }, + { + type: 'TEXT', + }, + { + type: 'RELATION', + relation: { + type: 'ONE_TO_MANY', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Company, + namePlural: 'Companies', + }, + }, + label: 'company', + isSystem: false, + }, + ]; + const objectMetadataItem = { - fields: [ - { - type: 'RELATION', - relation: { - type: 'MANY_TO_ONE', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Person, - namePlural: 'People', - }, - }, - label: 'person', - isSystem: false, - }, - { - type: 'TEXT', - }, - { - type: 'RELATION', - relation: { - type: 'ONE_TO_MANY', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Company, - namePlural: 'Companies', - }, - }, - label: 'company', - isSystem: false, - }, - ], + fields, + readableFields: fields, + updatableFields: fields, } as unknown as ObjectMetadataItem; const { result } = renderHook( @@ -163,33 +172,36 @@ describe('useRelatedRecordActions', () => { }); it('should assign correct positions to each action', () => { + const fields = [ + { + type: 'RELATION', + relation: { + type: 'ONE_TO_MANY', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Person, + namePlural: 'People', + }, + }, + label: 'person', + isSystem: false, + }, + { + type: 'RELATION', + relation: { + type: 'ONE_TO_MANY', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Company, + namePlural: 'Companies', + }, + }, + label: 'company', + isSystem: false, + }, + ]; const objectMetadataItem = { - fields: [ - { - type: 'RELATION', - relation: { - type: 'ONE_TO_MANY', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Person, - namePlural: 'People', - }, - }, - label: 'person', - isSystem: false, - }, - { - type: 'RELATION', - relation: { - type: 'ONE_TO_MANY', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Company, - namePlural: 'Companies', - }, - }, - label: 'company', - isSystem: false, - }, - ], + fields, + readableFields: fields, + updatableFields: fields, } as unknown as ObjectMetadataItem; const { result } = renderHook( @@ -207,33 +219,36 @@ describe('useRelatedRecordActions', () => { }); it('should filter out system fields', () => { + const fields = [ + { + type: 'RELATION', + relation: { + type: 'ONE_TO_MANY', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Person, + namePlural: 'People', + }, + }, + label: 'person', + isSystem: true, + }, + { + type: 'RELATION', + relation: { + type: 'ONE_TO_MANY', + targetObjectMetadata: { + nameSingular: CoreObjectNameSingular.Company, + namePlural: 'Companies', + }, + }, + label: 'company', + isSystem: false, + }, + ]; const objectMetadataItem = { - fields: [ - { - type: 'RELATION', - relation: { - type: 'ONE_TO_MANY', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Person, - namePlural: 'People', - }, - }, - label: 'person', - isSystem: true, - }, - { - type: 'RELATION', - relation: { - type: 'ONE_TO_MANY', - targetObjectMetadata: { - nameSingular: CoreObjectNameSingular.Company, - namePlural: 'Companies', - }, - }, - label: 'company', - isSystem: false, - }, - ], + fields, + readableFields: fields, + updatableFields: fields, } as unknown as ObjectMetadataItem; const { result } = renderHook( diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts index e31960447e..86677ee8e0 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions.ts @@ -30,7 +30,7 @@ export const useRelatedRecordActions = ({ return relatedActions; } - const oneToManyFields = sourceObjectMetadataItem.fields.filter( + const oneToManyFields = sourceObjectMetadataItem.readableFields.filter( (field) => field.type === 'RELATION' && field.relation?.type === 'ONE_TO_MANY' && diff --git a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts index 342a1b6532..efc0ccfad1 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts @@ -3,11 +3,11 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; -import { ObjectPermission } from '~/generated/graphql'; +import { ObjectPermissions } from 'twenty-shared/types'; export type ShouldBeRegisteredFunctionParams = { objectMetadataItem?: ObjectMetadataItem; - objectPermissions: ObjectPermission; + objectPermissions: ObjectPermissions; recordFilters?: RecordFilter[]; isShowPage?: boolean; isSoftDeleteFilterActive?: boolean; diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts index a685d15055..430c4ca2f7 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts @@ -7,6 +7,8 @@ const noteObjectMetadataItem = { nameSingular: CoreObjectNameSingular.Note, namePlural: 'notes', fields: [{ name: 'field1' }, { name: 'field2' }, { name: 'field3' }], + readableFields: [{ name: 'field1' }, { name: 'field2' }, { name: 'field3' }], + updatableFields: [{ name: 'field1' }, { name: 'field2' }, { name: 'field3' }], } as ObjectMetadataItem; describe('filterOutInvalidTimelineActivities', () => { @@ -40,6 +42,16 @@ describe('filterOutInvalidTimelineActivities', () => { nameSingular: 'objectNameSingular', namePlural: 'objectNamePlural', fields: [{ name: 'field1' }, { name: 'field2' }, { name: 'field3' }], + readableFields: [ + { name: 'field1' }, + { name: 'field2' }, + { name: 'field3' }, + ], + updatableFields: [ + { name: 'field1' }, + { name: 'field2' }, + { name: 'field3' }, + ], } as ObjectMetadataItem; const filteredEvents = filterOutInvalidTimelineActivities( @@ -99,6 +111,8 @@ describe('filterOutInvalidTimelineActivities', () => { nameSingular: 'objectNameSingular', namePlural: 'objectNamePlural', fields: [{ name: 'field1' }, { name: 'field2' }], + readableFields: [{ name: 'field1' }, { name: 'field2' }], + updatableFields: [{ name: 'field1' }, { name: 'field2' }], } as ObjectMetadataItem; const filteredEvents = filterOutInvalidTimelineActivities( @@ -128,6 +142,8 @@ describe('filterOutInvalidTimelineActivities', () => { nameSingular: 'objectNameSingular', namePlural: 'objectNamePlural', fields: [{ name: 'field1' }, { name: 'field2' }], + readableFields: [{ name: 'field1' }, { name: 'field2' }], + updatableFields: [{ name: 'field1' }, { name: 'field2' }], } as ObjectMetadataItem; const filteredEvents = filterOutInvalidTimelineActivities( diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts index 96413c89cd..55b2b215b4 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts @@ -22,11 +22,11 @@ export const filterOutInvalidTimelineActivities = ( } const fieldMetadataItemMap = new Map( - mainObjectMetadataItem.fields.map((field) => [field.name, field]), + mainObjectMetadataItem.readableFields.map((field) => [field.name, field]), ); const noteFieldMetadataItemMap = new Map( - noteObjectMetadataItem.fields.map((field) => [field.name, field]), + noteObjectMetadataItem.readableFields.map((field) => [field.name, field]), ); return timelineActivities.filter((timelineActivity) => { diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts index 75217b0b47..762c99d8ff 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts @@ -14,8 +14,8 @@ import { encodeCursor } from '@/apollo/utils/encodeCursor'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated/graphql'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; +import { ObjectPermissions } from 'twenty-shared/types'; /* TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are. @@ -29,7 +29,10 @@ type TriggerCreateRecordsOptimisticEffectArgs = { objectMetadataItems: ObjectMetadataItem[]; shouldMatchRootQueryFilter?: boolean; checkForRecordInCache?: boolean; - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; }; export const triggerCreateRecordsOptimisticEffect = ({ cache, diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index d37d1e0c5d..e9e7bc8d14 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -26,7 +26,7 @@ jest.mock('@/domain-manager/hooks/useRedirect', () => ({ })), })); -jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItem', () => ({ +jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItems', () => ({ useRefreshObjectMetadataItems: jest.fn().mockImplementation(() => ({ refreshObjectMetadataItems: jest.fn(), })), diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index f586367f6d..9a75f35689 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -9,19 +9,15 @@ import { useSetRecoilState, } from 'recoil'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { billingState } from '@/client-config/states/billingState'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; import { supportChatState } from '@/client-config/states/supportChatState'; -import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { AuthTokenPair, useCheckUserExistsLazyQuery, useGetAuthTokensFromLoginTokenMutation, useGetAuthTokensFromOtpMutation, - useGetCurrentUserLazyQuery, useGetLoginTokenFromCredentialsMutation, useGetLoginTokenFromEmailVerificationTokenMutation, useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation, @@ -30,22 +26,10 @@ import { useSignUpMutation, } from '~/generated-metadata/graphql'; -import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState'; -import { DateFormat } from '@/localization/constants/DateFormat'; -import { TimeFormat } from '@/localization/constants/TimeFormat'; -import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; -import { detectDateFormat } from '@/localization/utils/detectDateFormat'; -import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; -import { detectTimeZone } from '@/localization/utils/detectTimeZone'; -import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat'; -import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat'; -import { currentUserState } from '../states/currentUserState'; import { tokenPairState } from '../states/tokenPairState'; import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace'; -import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState'; -import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState'; import { SignInUpStep, @@ -63,49 +47,39 @@ import { captchaState } from '@/client-config/states/captchaState'; import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; -import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain'; import { useOrigin } from '@/domain-manager/hooks/useOrigin'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { i18n } from '@lingui/core'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; import { iconsState } from 'twenty-ui/display'; import { AuthToken } from '~/generated/graphql'; import { cookieStorage } from '~/utils/cookie-storage'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; -import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; import { loginTokenState } from '../states/loginTokenState'; export const useAuth = () => { const setTokenPair = useSetRecoilState(tokenPairState); const setLoginToken = useSetRecoilState(loginTokenState); - const setCurrentUser = useSetRecoilState(currentUserState); - const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState); - const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState, - ); - const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState); const { origin } = useOrigin(); const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken(); - const setCurrentWorkspaceMembers = useSetRecoilState( - currentWorkspaceMembersState, - ); const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); const isEmailVerificationRequired = useRecoilValue( isEmailVerificationRequiredState, ); + const { loadCurrentUser } = useLoadCurrentUser(); const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems(); const { createWorkspace } = useSignUpInNewWorkspace(); const setSignInUpStep = useSetRecoilState(signInUpStepState); - const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const { redirect } = useRedirect(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); @@ -120,11 +94,8 @@ export const useAuth = () => { useGetLoginTokenFromEmailVerificationTokenMutation(); const [getWorkspaceAgnosticTokenFromEmailVerificationToken] = useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation(); - const [getCurrentUser] = useGetCurrentUserLazyQuery(); const [getAuthTokensFromOtp] = useGetAuthTokensFromOtpMutation(); - const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); - const workspacePublicData = useRecoilValue(workspacePublicDataState); const { setLastAuthenticateWorkspaceDomain } = @@ -136,8 +107,6 @@ export const useAuth = () => { const goToRecoilSnapshot = useGotoRecoilSnapshot(); - const setDateTimeFormat = useSetRecoilState(dateTimeFormatState); - const [, setSearchParams] = useSearchParams(); const navigate = useNavigate(); @@ -207,103 +176,6 @@ export const useAuth = () => { [navigate, client, goToRecoilSnapshot, setLastAuthenticateWorkspaceDomain], ); - const loadCurrentUser = useCallback(async () => { - const currentUserResult = await getCurrentUser({ - fetchPolicy: 'network-only', - }); - - if (isDefined(currentUserResult.error)) { - throw new Error(currentUserResult.error.message); - } - - const user = currentUserResult.data?.currentUser; - - if (!user) { - throw new Error('No current user result'); - } - - let workspaceMember = null; - - setCurrentUser(user); - - if (isDefined(user.workspaceMembers)) { - const workspaceMembers = user.workspaceMembers.map((workspaceMember) => ({ - ...workspaceMember, - colorScheme: workspaceMember.colorScheme as ColorScheme, - locale: workspaceMember.locale ?? SOURCE_LOCALE, - })); - - setCurrentWorkspaceMembers(workspaceMembers); - } - - if (isDefined(user.availableWorkspaces)) { - setAvailableWorkspaces(user.availableWorkspaces); - } - - if (isDefined(user.currentUserWorkspace)) { - setCurrentUserWorkspace(user.currentUserWorkspace); - } - - if (isDefined(user.workspaceMember)) { - workspaceMember = { - ...user.workspaceMember, - colorScheme: user.workspaceMember?.colorScheme as ColorScheme, - locale: user.workspaceMember?.locale ?? SOURCE_LOCALE, - }; - - setCurrentWorkspaceMember(workspaceMember); - - // TODO: factorize with UserProviderEffect - setDateTimeFormat({ - timeZone: - workspaceMember.timeZone && workspaceMember.timeZone !== 'system' - ? workspaceMember.timeZone - : detectTimeZone(), - dateFormat: isDefined(user.workspaceMember.dateFormat) - ? getDateFormatFromWorkspaceDateFormat( - user.workspaceMember.dateFormat, - ) - : DateFormat[detectDateFormat()], - timeFormat: isDefined(user.workspaceMember.timeFormat) - ? getTimeFormatFromWorkspaceTimeFormat( - user.workspaceMember.timeFormat, - ) - : TimeFormat[detectTimeFormat()], - }); - dynamicActivate( - (workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE, - ); - } - - const workspace = user.currentWorkspace ?? null; - - setCurrentWorkspace(workspace); - - if (isDefined(workspace) && isOnAWorkspace) { - setLastAuthenticateWorkspaceDomain({ - workspaceId: workspace.id, - workspaceUrl: getWorkspaceUrl(workspace.workspaceUrls), - }); - } - - return { - user, - workspaceMember, - workspace, - }; - }, [ - getCurrentUser, - isOnAWorkspace, - setCurrentUser, - setCurrentUserWorkspace, - setCurrentWorkspace, - setCurrentWorkspaceMember, - setCurrentWorkspaceMembers, - setDateTimeFormat, - setLastAuthenticateWorkspaceDomain, - setAvailableWorkspaces, - ]); - const handleSetAuthTokens = useCallback( (tokens: AuthTokenPair) => { setTokenPair(tokens); @@ -434,8 +306,8 @@ export const useAuth = () => { // TODO: We can't parallelize this yet because when loadCurrentUSer is loaded // then UserProvider updates its children and PrefetchDataProvider is triggered // which requires the correct metadata to be loaded (not the mocks) - await refreshObjectMetadataItems(); await loadCurrentUser(); + await refreshObjectMetadataItems(); }, [loadCurrentUser, handleSetAuthTokens, refreshObjectMetadataItems], ); @@ -789,9 +661,6 @@ export const useAuth = () => { getLoginTokenFromEmailVerificationToken: handleGetLoginTokenFromEmailVerificationToken, getAuthTokensFromLoginToken: handleGetAuthTokensFromLoginToken, - - loadCurrentUser, - checkUserExists: { checkUserExistsData, checkUserExistsQuery }, clearSession, signOut: handleSignOut, diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx index 1b6a6886a4..707ee869b5 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx @@ -1,17 +1,19 @@ +import { useAuth } from '@/auth/hooks/useAuth'; import { SignInUpStep, signInUpStepState, } from '@/auth/states/signInUpStepState'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { useAuth } from '@/auth/hooks/useAuth'; -import { useSearchParams } from 'react-router-dom'; export const SignInUpGlobalScopeFormEffect = () => { const setSignInUpStep = useSetRecoilState(signInUpStepState); const [searchParams, setSearchParams] = useSearchParams(); - const { setAuthTokens, loadCurrentUser } = useAuth(); + const { setAuthTokens } = useAuth(); + const { loadCurrentUser } = useLoadCurrentUser(); useEffect(() => { const tokenPair = searchParams.get('tokenPair'); diff --git a/packages/twenty-front/src/modules/auth/states/currentUserWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentUserWorkspaceState.ts index 3b426b9974..fe4d09d304 100644 --- a/packages/twenty-front/src/modules/auth/states/currentUserWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentUserWorkspaceState.ts @@ -1,13 +1,15 @@ +import { ObjectPermissions } from 'twenty-shared/types'; import { createState } from 'twenty-ui/utilities'; import { UserWorkspace } from '~/generated/graphql'; export type CurrentUserWorkspace = Pick< UserWorkspace, | 'permissionFlags' - | 'objectRecordsPermissions' - | 'objectPermissions' | 'twoFactorAuthenticationMethodSummary' ->; + | 'objectRecordsPermissions' +> & { + objectPermissions: Array; +}; export const currentUserWorkspaceState = createState({ diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx index 9ae2b7dd4d..138d8424d9 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx @@ -9,6 +9,7 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { AppPath } from '@/types/AppPath'; import { t } from '@lingui/core/macro'; import { useMemo } from 'react'; @@ -28,8 +29,10 @@ export const useCommandMenuSearchRecords = () => { const nonReadableObjectMetadataItemsNameSingular = useMemo(() => { return Object.values(objectMetadataItems) .filter((objectMetadataItem) => { - const objectPermission = - objectPermissionsByObjectMetadataId[objectMetadataItem.id]; + const objectPermission = getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }); return !objectPermission?.canReadObjectRecords; }) diff --git a/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts b/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts index 9874782de1..e295397672 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts +++ b/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts @@ -24,13 +24,13 @@ export const useFavorites = () => { const favoriteRelationFieldMetadataItems = useMemo( () => - favoriteObjectMetadataItem.fields.filter( + favoriteObjectMetadataItem.readableFields.filter( (fieldMetadataItem) => fieldMetadataItem.type === FieldMetadataType.RELATION && fieldMetadataItem.name !== 'forWorkspaceMember' && fieldMetadataItem.name !== 'favoriteFolder', ), - [favoriteObjectMetadataItem.fields], + [favoriteObjectMetadataItem.readableFields], ); const sortedFavorites = useMemo( diff --git a/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts index 310b860fa7..0f615a4d6b 100644 --- a/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts @@ -5,6 +5,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { arePrefetchViewsLoadedState } from '@/prefetch/states/arePrefetchViewsLoaded'; import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; import isEmpty from 'lodash.isempty'; @@ -26,7 +27,10 @@ export const useDefaultHomePagePath = () => { const readableAlphaSortedActiveNonSystemObjectMetadataItems = useMemo(() => { return alphaSortedActiveNonSystemObjectMetadataItems.filter((item) => { - const objectPermissions = objectPermissionsByObjectMetadataId[item.id]; + const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: item.id, + }); return objectPermissions?.canReadObjectRecords; }); }, [ @@ -82,8 +86,10 @@ export const useDefaultHomePagePath = () => { if ( !isDefined(lastVisitedObjectMetadataItemId) || - !objectPermissionsByObjectMetadataId[lastVisitedObjectMetadataItemId] - ?.canReadObjectRecords + !getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: lastVisitedObjectMetadataItemId, + }).canReadObjectRecords ) { return firstObjectPathInfo; } diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx index 1056e5b4a6..2b4101eb50 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx @@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index 8a76e30e99..730c1460fb 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -109,97 +109,6 @@ export const queries = { } } `, - getCurrentUser: gql` - query GetCurrentUser { - currentUser { - ...UserQueryFragment - } - } - - fragment UserQueryFragment on User { - id - firstName - lastName - email - canAccessFullAdminPanel - canImpersonate - supportUserHash - onboardingStatus - workspaceMember { - ...WorkspaceMemberQueryFragment - } - workspaceMembers { - ...WorkspaceMemberQueryFragment - } - currentUserWorkspace { - permissionFlags - objectRecordsPermissions - } - currentWorkspace { - id - displayName - logo - inviteHash - allowImpersonation - activationStatus - isPublicInviteLinkEnabled - isGoogleAuthEnabled - isMicrosoftAuthEnabled - isPasswordAuthEnabled - subdomain - hasValidEnterpriseKey - customDomain - workspaceUrls { - ...WorkspaceUrlsFragment - } - featureFlags { - id - key - value - workspaceId - } - metadataVersion - currentBillingSubscription { - id - status - interval - } - billingSubscriptions { - id - status - } - workspaceMembersCount - } - workspaces { - workspace { - id - logo - displayName - subdomain - customDomain - workspaceUrls { - ...WorkspaceUrlsFragment - } - } - } - userVars - } - - fragment WorkspaceMemberQueryFragment on WorkspaceMember { - id - name { - firstName - lastName - } - colorScheme - avatarUrl - locale - userEmail - timeZone - dateFormat - timeFormat - } - `, }; export const objectMetadataId = '25611fce-6637-4089-b0ca-91afeec95784'; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx index c234da65a1..539b92ca95 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx @@ -14,6 +14,8 @@ import { query as findManyObjectMetadataItemsQuery, responseData as findManyObjectMetadataItemsResponseData, } from '../__mocks__/useFindManyObjectMetadataItems'; +import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { mockedUserData } from '~/testing/mock-data/users'; const mocks = [ { @@ -27,6 +29,17 @@ const mocks = [ }, })), }, + { + request: { + query: GET_CURRENT_USER, + variables: {}, + }, + result: jest.fn(() => ({ + data: { + currentUser: mockedUserData, + }, + })), + }, { request: { query: findManyObjectMetadataItemsQuery, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx index a285397743..b4c33447b5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx @@ -11,10 +11,12 @@ import { variables, } from '../__mocks__/useDeleteOneObjectMetadataItem'; +import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { query as findManyObjectMetadataItemsQuery, responseData as findManyObjectMetadataItemsResponseData, } from '../__mocks__/useFindManyObjectMetadataItems'; +import { mockedUserData } from '~/testing/mock-data/users'; const mocks = [ { @@ -28,6 +30,17 @@ const mocks = [ }, })), }, + { + request: { + query: GET_CURRENT_USER, + variables: {}, + }, + result: jest.fn(() => ({ + data: { + currentUser: mockedUserData, + }, + })), + }, { request: { query: findManyObjectMetadataItemsQuery, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx index d4c6a02aed..cea3e200f0 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx @@ -15,6 +15,8 @@ import { variables, } from '../__mocks__/useFieldMetadataItem'; +import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { mockedUserData } from '~/testing/mock-data/users'; import { query as findManyObjectMetadataItemsQuery, responseData as findManyObjectMetadataItemsResponseData, @@ -97,6 +99,17 @@ const mocks = [ }, })), }, + { + request: { + query: GET_CURRENT_USER, + variables: {}, + }, + result: jest.fn(() => ({ + data: { + currentUser: mockedUserData, + }, + })), + }, { request: { query: queries.deleteMetadataField, @@ -130,15 +143,6 @@ const mocks = [ }, })), }, - { - request: { - query: queries.getCurrentUser, - variables: {}, - }, - result: jest.fn(() => ({ - data: responseData.getCurrentUser, - })), - }, { request: { query: findManyObjectMetadataItemsQuery, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useActiveFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useActiveFieldMetadataItems.ts index e60ab4e1a1..5f903bc413 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useActiveFieldMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useActiveFieldMetadataItems.ts @@ -9,7 +9,7 @@ export const useActiveFieldMetadataItems = ({ const activeFieldMetadataItems = useMemo( () => objectMetadataItem - ? objectMetadataItem.fields.filter( + ? objectMetadataItem.readableFields.filter( ({ isActive, isSystem }) => isActive && !isSystem, ) : [], diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts index 43c0e8f6a4..3ab8249a2e 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts @@ -5,17 +5,13 @@ import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailab import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector'; -import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useRecoilValue } from 'recoil'; -import { FeatureFlagKey } from '~/generated/graphql'; import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition'; export const useColumnDefinitionsFromFieldMetadata = ( objectMetadataItem: ObjectMetadataItem, ) => { - const activeFieldMetadataItems = objectMetadataItem.fields.filter( + const activeFieldMetadataItems = objectMetadataItem.readableFields.filter( ({ isActive, isSystem }) => isActive && !isSystem, ); @@ -31,23 +27,7 @@ export const useColumnDefinitionsFromFieldMetadata = ( }), ); - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - - let restrictedFieldMetadataIds: string[] = []; - - const featureFlags = useFeatureFlagsMap(); - const isFieldsPermissionsEnabled = - featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; - - if (isFieldsPermissionsEnabled) { - restrictedFieldMetadataIds = - getReadRestrictedFieldMetadataIdsFromObjectPermissions({ - objectPermissions: [ - objectPermissionsByObjectMetadataId[objectMetadataItem.id], - ], - objectMetadataId: objectMetadataItem.id, - }); - } + const restrictedFieldMetadataIds: string[] = []; const columnDefinitions: ColumnDefinition[] = activeFieldMetadataItems diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts index 0a9b60be40..91741b71ab 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts @@ -8,7 +8,7 @@ import { import { CREATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews'; export const useCreateOneFieldMetadataItem = () => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index a7b42d6645..04969188c1 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -8,7 +8,7 @@ import { import { CREATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews'; export const useCreateOneObjectMetadataItem = () => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts index 94da4df13d..79b85a3e25 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts @@ -6,7 +6,7 @@ import { } from '~/generated-metadata/graphql'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { useRecoilState } from 'recoil'; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts index 828b466635..9d08fab63b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts @@ -7,7 +7,7 @@ import { import { DELETE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; export const useDeleteOneObjectMetadataItem = () => { const [mutate] = useMutation< diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts index 175aaabb76..59d8220407 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts @@ -8,6 +8,8 @@ import { } from '~/generated-metadata/graphql'; import { logError } from '~/utils/logError'; +import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries'; import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '../utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; @@ -18,6 +20,8 @@ export const useFindManyObjectMetadataItems = ({ } = {}) => { const { enqueueErrorSnackBar } = useSnackBar(); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { data, loading, error, refetch } = useQuery< ObjectMetadataItemsQuery, ObjectMetadataItemsQueryVariables @@ -32,10 +36,16 @@ export const useFindManyObjectMetadataItems = ({ }); const objectMetadataItems = useMemo(() => { - return mapPaginatedObjectMetadataItemsToObjectMetadataItems({ - pagedObjectMetadataItems: data, + const objectMetadataItemsArray = + mapPaginatedObjectMetadataItemsToObjectMetadataItems({ + pagedObjectMetadataItems: data, + }); + + return enrichObjectMetadataItemsWithPermissions({ + objectMetadataItems: objectMetadataItemsArray, + objectPermissionsByObjectMetadataId, }); - }, [data]); + }, [data, objectPermissionsByObjectMetadataId]); return { objectMetadataItems, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItem.ts deleted file mode 100644 index 41362a3e33..0000000000 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItem.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; -import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; -import { FetchPolicy, useApolloClient } from '@apollo/client'; -import { useRecoilCallback } from 'recoil'; -import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useRefreshObjectMetadataItems = ( - fetchPolicy: FetchPolicy = 'network-only', -) => { - const client = useApolloClient(); - - const refreshObjectMetadataItems = async () => { - const result = await client.query({ - query: FIND_MANY_OBJECT_METADATA_ITEMS, - variables: {}, - fetchPolicy, - }); - - const objectMetadataItems = - mapPaginatedObjectMetadataItemsToObjectMetadataItems({ - pagedObjectMetadataItems: result.data, - }); - - replaceObjectMetadataItemIfDifferent(objectMetadataItems); - - return objectMetadataItems; - }; - - const replaceObjectMetadataItemIfDifferent = useRecoilCallback( - ({ set, snapshot }) => - (toSetObjectMetadataItems: ObjectMetadataItem[]) => { - if ( - !isDeeplyEqual( - snapshot.getLoadable(objectMetadataItemsState).getValue(), - toSetObjectMetadataItems, - ) && - toSetObjectMetadataItems.length > 0 - ) { - set(objectMetadataItemsState, toSetObjectMetadataItems); - set(isAppWaitingForFreshObjectMetadataState, false); - } - }, - [], - ); - - return { - refreshObjectMetadataItems, - }; -}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItems.ts new file mode 100644 index 0000000000..ae18246c92 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItems.ts @@ -0,0 +1,89 @@ +import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; +import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions'; +import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; +import { FetchPolicy, useApolloClient } from '@apollo/client'; +import { useRecoilCallback } from 'recoil'; +import { ObjectPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + +export const useRefreshObjectMetadataItems = ( + fetchPolicy: FetchPolicy = 'network-only', +) => { + const client = useApolloClient(); + const refreshObjectMetadataItems = async () => { + const objectMetadataItemsResult = + await client.query({ + query: FIND_MANY_OBJECT_METADATA_ITEMS, + variables: {}, + fetchPolicy, + }); + + const objectMetadataItems = + mapPaginatedObjectMetadataItemsToObjectMetadataItems({ + pagedObjectMetadataItems: objectMetadataItemsResult.data, + }); + + return replaceObjectMetadataItemIfDifferent(objectMetadataItems); + }; + + const replaceObjectMetadataItemIfDifferent = useRecoilCallback( + ({ set, snapshot }) => + ( + toSetObjectMetadataItems: Omit< + ObjectMetadataItem, + 'readableFields' | 'updatableFields' + >[], + ) => { + const currentUserWorkspace = snapshot + .getLoadable(currentUserWorkspaceState) + .getValue(); + + if (!isDefined(currentUserWorkspace)) { + return; + } + + const objectPermissionsByObjectMetadataId = + currentUserWorkspace.objectPermissions.reduce( + (acc, objectPermission) => { + acc[objectPermission.objectMetadataId] = objectPermission; + return acc; + }, + {} as Record< + string, + ObjectPermissions & { objectMetadataId: string } + >, + ); + + const newObjectMetadataItems = enrichObjectMetadataItemsWithPermissions( + { + objectMetadataItems: toSetObjectMetadataItems, + objectPermissionsByObjectMetadataId, + }, + ); + + if ( + !isDeeplyEqual( + snapshot.getLoadable(objectMetadataItemsState).getValue(), + newObjectMetadataItems, + ) && + newObjectMetadataItems.length > 0 + ) { + set(objectMetadataItemsState, newObjectMetadataItems); + set(isAppWaitingForFreshObjectMetadataState, false); + } + + return newObjectMetadataItems; + }, + [], + ); + + return { + refreshObjectMetadataItems, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts index a2534c73d2..ba361b4f13 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts @@ -9,7 +9,7 @@ import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts index 41c0b04eaf..113bf0e1ad 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts @@ -8,7 +8,7 @@ import { import { UPDATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; // TODO: Slice the Apollo store synchronously in the update function instead of subscribing, so we can use update after read in the same function call export const useUpdateOneObjectMetadataItem = () => { diff --git a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts index 8277486611..69cb045926 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts @@ -1,8 +1,6 @@ -import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getFilterFilterableFieldMetadataItems } from '@/object-metadata/utils/getFilterFilterableFieldMetadataItems'; -import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions'; import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace'; import { selectorFamily } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -29,34 +27,15 @@ export const availableFieldMetadataItemsForFilterFamilySelector = currentWorkspace, ); - const isFieldsPermissionsEnabled = - checkIfFeatureFlagIsEnabledOnWorkspace( - FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED, - currentWorkspace, - ); - const filterFilterableFieldMetadataItems = getFilterFilterableFieldMetadataItems({ isJsonFilterEnabled: isJsonFeatureFlagEnabled, }); - let restrictedFieldMetadataIds: string[] = []; - - if (isFieldsPermissionsEnabled) { - const currentUserWorkspace = get(currentUserWorkspaceState); - - restrictedFieldMetadataIds = - getReadRestrictedFieldMetadataIdsFromObjectPermissions({ - objectPermissions: currentUserWorkspace?.objectPermissions, - objectMetadataId: objectMetadataItem.id, - }); - } - - const availableFieldMetadataItemsForFilter = objectMetadataItem.fields - .filter(filterFilterableFieldMetadataItems) - .filter((field) => { - return !restrictedFieldMetadataIds.includes(field.id); - }); + const availableFieldMetadataItemsForFilter = + objectMetadataItem.readableFields.filter( + filterFilterableFieldMetadataItems, + ); return availableFieldMetadataItemsForFilter; }, }); diff --git a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts index df339ea5dd..18f619b6d5 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts @@ -1,12 +1,7 @@ -import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems'; -import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions'; -import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace'; import { selectorFamily } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { FeatureFlagKey } from '~/generated/graphql'; export const availableFieldMetadataItemsForSortFamilySelector = selectorFamily({ key: 'availableFieldMetadataItemsForSortFamilySelector', @@ -23,28 +18,10 @@ export const availableFieldMetadataItemsForSortFamilySelector = selectorFamily({ return []; } - const currentWorkspace = get(currentWorkspaceState); - const isFieldsPermissionsEnabled = checkIfFeatureFlagIsEnabledOnWorkspace( - FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED, - currentWorkspace, - ); - - let restrictedFieldMetadataIds: string[] = []; - if (isFieldsPermissionsEnabled) { - const currentUserWorkspace = get(currentUserWorkspaceState); - - restrictedFieldMetadataIds = - getReadRestrictedFieldMetadataIdsFromObjectPermissions({ - objectPermissions: currentUserWorkspace?.objectPermissions, - objectMetadataId: objectMetadataItem.id, - }); - } - - const availableFieldMetadataItemsForSort = objectMetadataItem.fields - .filter(filterSortableFieldMetadataItems) - .filter((field) => { - return !restrictedFieldMetadataIds.includes(field.id); - }); + const availableFieldMetadataItemsForSort = + objectMetadataItem.readableFields.filter( + filterSortableFieldMetadataItems, + ); return availableFieldMetadataItemsForSort; }, diff --git a/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts index d519a69df9..9314b50cb6 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts @@ -15,6 +15,8 @@ export type ObjectMetadataItem = Omit< > & { __typename?: string; fields: FieldMetadataItem[]; + readableFields: FieldMetadataItem[]; + updatableFields: FieldMetadataItem[]; labelIdentifierFieldMetadataId: string; indexMetadatas: IndexMetadataItem[]; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts index a674326f35..ab297b339e 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts @@ -37,6 +37,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, objectMetadataId: personObjectMetadataItem.id, + restrictedFields: {}, }, }, }); @@ -140,6 +141,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, objectMetadataId: personObjectMetadataItem.id, + restrictedFields: {}, }, }, }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/enrichObjectMetadataItemsWithPermissions.ts b/packages/twenty-front/src/modules/object-metadata/utils/enrichObjectMetadataItemsWithPermissions.ts new file mode 100644 index 0000000000..4e26a3c46b --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/enrichObjectMetadataItemsWithPermissions.ts @@ -0,0 +1,57 @@ +import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions'; +import { getNonUpdatableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; +import { ObjectPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; + +type enrichObjectMetadataItemsWithPermissionsArgs = { + objectMetadataItems: Omit< + ObjectMetadataItem, + 'readableFields' | 'updatableFields' + >[]; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; +}; + +export const enrichObjectMetadataItemsWithPermissions = ({ + objectMetadataItems, + objectPermissionsByObjectMetadataId, +}: enrichObjectMetadataItemsWithPermissionsArgs) => { + const formattedObjects: ObjectMetadataItem[] = + objectMetadataItems.map((object) => { + const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: object.id, + }); + + const nonReadableFieldMetadataIds = !isDefined(objectPermissions) + ? [] + : getNonReadableFieldMetadataIdsFromObjectPermissions({ + objectPermissions: objectPermissions, + }); + + const nonUpdatableFieldMetadataIds = !isDefined(objectPermissions) + ? [] + : getNonUpdatableFieldMetadataIdsFromObjectPermissions({ + objectPermissions: objectPermissions, + }); + + const { fields, ...objectWithoutFields } = object; + + return { + ...objectWithoutFields, + fields: fields, + readableFields: fields.filter( + (field) => !nonReadableFieldMetadataIds.includes(field.id), + ), + updatableFields: fields.filter( + (field) => !nonUpdatableFieldMetadataIds.includes(field.id), + ), + } satisfies ObjectMetadataItem; + }) ?? []; + + return formattedObjects; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getActiveFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/getActiveFieldMetadataItems.ts index e174c2b004..753bec24da 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getActiveFieldMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getActiveFieldMetadataItems.ts @@ -1,9 +1,9 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; export const getActiveFieldMetadataItems = ( - objectMetadataItem: Pick, + objectMetadataItem: Pick, ) => - objectMetadataItem.fields.filter( + objectMetadataItem.readableFields.filter( (fieldMetadataItem) => fieldMetadataItem.isActive && !fieldMetadataItem.isSystem, ); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions.ts b/packages/twenty-front/src/modules/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions.ts new file mode 100644 index 0000000000..ad9650150d --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions.ts @@ -0,0 +1,15 @@ +import { ObjectPermissions } from 'twenty-shared/types'; + +type GetNonReadableFieldMetadataIdsFromObjectPermissionsArgs = { + objectPermissions: ObjectPermissions; +}; + +export const getNonReadableFieldMetadataIdsFromObjectPermissions = ({ + objectPermissions, +}: GetNonReadableFieldMetadataIdsFromObjectPermissionsArgs) => { + const restrictedFields = objectPermissions.restrictedFields; + + return Object.entries(restrictedFields) + .filter(([_, restrictedField]) => restrictedField.canRead === false) + .map(([fieldMetadataId]) => fieldMetadataId); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions.ts b/packages/twenty-front/src/modules/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions.ts new file mode 100644 index 0000000000..cb9bba28cc --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions.ts @@ -0,0 +1,15 @@ +import { ObjectPermissions } from 'twenty-shared/types'; + +type GetNonUpdatableFieldMetadataIdsFromObjectPermissionsArgs = { + objectPermissions: ObjectPermissions; +}; + +export const getNonUpdatableFieldMetadataIdsFromObjectPermissions = ({ + objectPermissions, +}: GetNonUpdatableFieldMetadataIdsFromObjectPermissionsArgs): string[] => { + const restrictedFields = objectPermissions.restrictedFields; + + return Object.entries(restrictedFields) + .filter(([_, restrictedField]) => restrictedField.canUpdate === false) + .map(([fieldMetadataId]) => fieldMetadataId); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts index 7f0ce27b3b..0f33e3e44a 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts @@ -1,12 +1,18 @@ +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; +import { ObjectPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated/graphql'; export const getObjectPermissionsForObject = ( - objectPermissionsByObjectMetadataId: Record, + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >, objectMetadataId: string, -): ObjectPermission => { - const objectPermissions = - objectPermissionsByObjectMetadataId[objectMetadataId]; +): ObjectPermissions & { objectMetadataId: string } => { + const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId, + }); if (!isDefined(objectPermissions)) { return { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions.ts b/packages/twenty-front/src/modules/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions.ts deleted file mode 100644 index 314f30ba76..0000000000 --- a/packages/twenty-front/src/modules/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { RestrictedField } from 'twenty-shared/types'; -import { ObjectPermission } from '~/generated/graphql'; - -export const getReadRestrictedFieldMetadataIdsFromObjectPermissions = ({ - objectPermissions, - objectMetadataId, -}: { - objectPermissions?: ObjectPermission[] | null; - objectMetadataId: string; -}) => { - const restrictedFields = objectPermissions?.find( - (permission) => permission.objectMetadataId === objectMetadataId, - )?.restrictedFields; - - return Object.entries(restrictedFields ?? {}) - .filter( - ([_fieldMetadataId, restrictedField]) => - (restrictedField as RestrictedField).canRead === false, - ) - .map(([fieldMetadataId]) => fieldMetadataId); -}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index 9a8be3d987..72587a00c3 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -1,10 +1,6 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { isUndefined } from '@sniptt/guards'; -import { - FieldMetadataType, - ObjectPermission, - RelationType, -} from '~/generated-metadata/graphql'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; @@ -12,6 +8,7 @@ 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'; +import { ObjectPermissions } from 'twenty-shared/types'; type MapFieldMetadataToGraphQLQueryArgs = { objectMetadataItems: ObjectMetadataItem[]; @@ -22,7 +19,10 @@ type MapFieldMetadataToGraphQLQueryArgs = { >; relationRecordGqlFields?: RecordGqlFields; computeReferences?: boolean; - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; isFieldsPermissionsEnabled?: boolean; }; // TODO: change ObjectMetadataItems mock before refactoring with relation computed field diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index a96d9b47d8..92e4a237a7 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -1,24 +1,25 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery'; 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 { FieldMetadataType, ObjectPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated/graphql'; type MapObjectMetadataToGraphQLQueryArgs = { objectMetadataItems: ObjectMetadataItem[]; objectMetadataItem: Pick< ObjectMetadataItem, - 'nameSingular' | 'fields' | 'id' + 'nameSingular' | 'fields' | 'id' | 'readableFields' >; recordGqlFields?: RecordGqlFields; computeReferences?: boolean; isRootLevel?: boolean; - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; isFieldsPermissionsEnabled?: boolean; }; @@ -45,22 +46,10 @@ export const mapObjectMetadataToGraphQLQuery = ({ } } - const filterReadableFields = (field: FieldMetadataItem) => { - if (isFieldsPermissionsEnabled !== true) { - return true; - } - const objectPermission = getObjectPermissionsForObject( - objectPermissionsByObjectMetadataId, - objectMetadataItem.id, - ); - return objectPermission.restrictedFields?.[field.id]?.canRead !== false; - }; - - const manyToOneRelationFields = objectMetadataItem?.fields + const manyToOneRelationFields = objectMetadataItem?.readableFields .filter((field) => field.isActive) .filter((field) => field.type === FieldMetadataType.RELATION) - .filter((field) => isDefined(field.settings?.joinColumnName)) - .filter(filterReadableFields); + .filter((field) => isDefined(field.settings?.joinColumnName)); const manyToOneRelationGqlFieldWithFieldMetadata = manyToOneRelationFields.map((field) => ({ @@ -69,9 +58,8 @@ export const mapObjectMetadataToGraphQLQuery = ({ })); const gqlFieldWithFieldMetadataThatCouldBeQueried = [ - ...objectMetadataItem.fields + ...objectMetadataItem.readableFields .filter((fieldMetadata) => fieldMetadata.isActive) - .filter(filterReadableFields) .map((fieldMetadata) => ({ gqlField: fieldMetadata.name, fieldMetadata, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts index 5690a64a9d..800887d96b 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts @@ -4,12 +4,17 @@ import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/o import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; +type mapPaginatedObjectMetadataItemsToObjectMetadataItemsArgs = { + pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined; +}; + export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({ pagedObjectMetadataItems, -}: { - pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined; -}) => { - const formattedObjects: ObjectMetadataItem[] = +}: mapPaginatedObjectMetadataItemsToObjectMetadataItemsArgs) => { + const formattedObjects: Omit< + ObjectMetadataItem, + 'readableFields' | 'updatableFields' + >[] = pagedObjectMetadataItems?.objects.edges.map((object) => { const labelIdentifierFieldMetadataId = objectMetadataItemSchema.shape.labelIdentifierFieldMetadataId.parse( @@ -35,7 +40,10 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({ ), }) satisfies IndexMetadataItem, ), - } satisfies ObjectMetadataItem; + } satisfies Omit< + ObjectMetadataItem, + 'readableFields' | 'updatableFields' + >; }) ?? []; return formattedObjects; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts index cbe0fd7db5..fbfcbaf0f9 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts @@ -1,11 +1,11 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; export const mapSoftDeleteFieldsToGraphQLQuery = ( - objectMetadataItem: Pick, + objectMetadataItem: Pick, ): string => { const softDeleteFields = ['deletedAt', 'id']; - const fieldsThatShouldBeQueried = objectMetadataItem.fields + const fieldsThatShouldBeQueried = objectMetadataItem.readableFields .filter((field) => field.isActive && softDeleteFields.includes(field.name)) .sort( (a, b) => diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts index 75b1ec3d57..b5ab98f8cd 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts @@ -12,6 +12,8 @@ export const objectMetadataItemSchema = z.object({ dataSourceId: z.string().uuid(), description: z.string().trim().nullable().optional(), fields: z.array(fieldMetadataItemSchema()), + readableFields: z.array(fieldMetadataItemSchema()), + updatableFields: z.array(fieldMetadataItemSchema()), indexMetadatas: z.array(indexMetadataItemSchema), icon: z.string().startsWith('Icon').trim(), id: z.string().uuid(), diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts index ea22faaed6..6bd9927716 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts @@ -7,9 +7,9 @@ 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/graphql'; import { isEmptyObject } from '~/utils/isEmptyObject'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { ObjectPermissions } from 'twenty-shared/types'; export type GetRecordFromCacheArgs = { cache: ApolloCache; @@ -17,7 +17,10 @@ export type GetRecordFromCacheArgs = { objectMetadataItems: ObjectMetadataItem[]; objectMetadataItem: ObjectMetadataItem; recordGqlFields?: RecordGqlFields; - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; isFieldsPermissionsEnabled?: boolean; }; export const getRecordFromCache = ({ diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts index f3e61bef68..b69347b08e 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts @@ -6,8 +6,8 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectPermissions } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const updateRecordFromCache = ({ @@ -24,7 +24,10 @@ export const updateRecordFromCache = ({ cache: ApolloCache; recordGqlFields: Record; record: T; - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; isFieldsPermissionsEnabled?: boolean; }) => { if (isUndefinedOrNull(objectMetadataItem)) { diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts index 96d0184f2b..7ec0fee6ce 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts @@ -8,14 +8,17 @@ export type GenerateDepthOneRecordGqlFields = { export const generateDepthOneRecordGqlFields = ({ objectMetadataItem, }: GenerateDepthOneRecordGqlFields) => - objectMetadataItem.fields.reduce>((acc, field) => { - return { - ...acc, - ...(isDefined(field.settings?.joinColumnName) - ? { - [field.settings.joinColumnName]: true, - } - : {}), - [field.name]: true, - }; - }, {}); + objectMetadataItem.readableFields.reduce>( + (acc, field) => { + return { + ...acc, + ...(isDefined(field.settings?.joinColumnName) + ? { + [field.settings.joinColumnName]: true, + } + : {}), + [field.name]: true, + }; + }, + {}, + ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx index 46841ed39e..a51255ccd2 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx @@ -11,6 +11,29 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); jest.mock('@/object-record/utils/generateAggregateQuery'); +const fields = [ + { + id: '20202020-fed9-4ce5-9502-02a8efaf46e1', + name: 'amount', + label: 'Amount', + type: FieldMetadataType.NUMBER, + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } as FieldMetadataItem, + { + id: '20202020-dd4a-4ea4-bb7b-1c7300491b65', + name: 'name', + label: 'Name', + type: FieldMetadataType.TEXT, + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } as FieldMetadataItem, +]; + const mockObjectMetadataItem: ObjectMetadataItem = { nameSingular: 'company', namePlural: 'companies', @@ -23,28 +46,9 @@ const mockObjectMetadataItem: ObjectMetadataItem = { isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - fields: [ - { - id: '20202020-fed9-4ce5-9502-02a8efaf46e1', - name: 'amount', - label: 'Amount', - type: FieldMetadataType.NUMBER, - isCustom: false, - isActive: true, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - } as FieldMetadataItem, - { - id: '20202020-dd4a-4ea4-bb7b-1c7300491b65', - name: 'name', - label: 'Name', - type: FieldMetadataType.TEXT, - isCustom: false, - isActive: true, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - } as FieldMetadataItem, - ], + fields, + readableFields: fields, + updatableFields: fields, indexMetadatas: [], isLabelSyncedWithName: true, isRemote: false, @@ -118,19 +122,19 @@ describe('useAggregateRecordsQuery', () => { ); }); - it('should throw error for invalid aggregation operation', () => { - expect(() => - renderHook( - () => - useAggregateRecordsQuery({ - objectNameSingular: 'company', - recordGqlFieldsAggregate: { - name: [AggregateOperations.SUM], - }, - }), - { wrapper: Wrapper }, - ), - ).toThrow(); + it('should early return for invalid aggregation operation', () => { + const { result } = renderHook( + () => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + name: [AggregateOperations.SUM], + }, + }), + { wrapper: Wrapper }, + ); + + expect(result.current.gqlFieldToFieldMap).toEqual({}); }); it('should handle multiple aggregations', () => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts index a9dd2b54dd..f371f19dfe 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts @@ -28,8 +28,11 @@ export const useAggregateRecordsQuery = ({ const apolloCoreClient = useApolloCoreClient(); const availableAggregations = useMemo( - () => getAvailableAggregationsFromObjectFields(objectMetadataItem.fields), - [objectMetadataItem.fields], + () => + getAvailableAggregationsFromObjectFields( + objectMetadataItem.readableFields, + ), + [objectMetadataItem.readableFields], ); const recordGqlFields: RecordGqlFields = {}; @@ -42,9 +45,7 @@ export const useAggregateRecordsQuery = ({ availableAggregations[fieldName]?.[aggregateOperation]; if (!isDefined(fieldToQuery)) { - throw new Error( - `Cannot query operation ${aggregateOperation} on field ${fieldName}`, - ); + return; } gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation]; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts index 6e62bd4a55..ce51ad6b0a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts @@ -25,7 +25,7 @@ export const useAttachRelatedRecordFromRecord = ({ objectNameSingular: recordObjectNameSingular, }); - const fieldOnObject = objectMetadataItem.fields.find((field) => { + const fieldOnObject = objectMetadataItem.readableFields.find((field) => { return field.name === fieldNameOnRecordObject; }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts index b667aa22f3..1ef9e819ae 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts @@ -21,7 +21,7 @@ export const useDetachRelatedRecordFromRecord = ({ objectNameSingular: recordObjectNameSingular, }); - const fieldOnObject = objectMetadataItem.fields.find((field) => { + const fieldOnObject = objectMetadataItem.readableFields.find((field) => { return field.name === fieldNameOnRecordObject; }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts b/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts index 3fac599a84..1ddefa32b2 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissions.ts @@ -1,11 +1,14 @@ import { useRecoilValue } from 'recoil'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { ObjectPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated/graphql'; type useObjectPermissionsReturnType = { - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; }; export const useObjectPermissions = (): useObjectPermissionsReturnType => { @@ -19,7 +22,10 @@ export const useObjectPermissions = (): useObjectPermissionsReturnType => { } const objectPermissionsByObjectMetadataId = objectPermissions?.reduce( - (acc: Record, objectPermission) => { + ( + acc: Record, + objectPermission, + ) => { acc[objectPermission.objectMetadataId] = objectPermission; return acc; }, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissionsForObject.ts b/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissionsForObject.ts index ee3063b47a..ad41b13939 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissionsForObject.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useObjectPermissionsForObject.ts @@ -1,9 +1,12 @@ import { getObjectPermissionsForObject } from '~/modules/object-metadata/utils/getObjectPermissionsForObject'; import { useMemo } from 'react'; +import { ObjectPermissions } from 'twenty-shared/types'; import { useObjectPermissions } from './useObjectPermissions'; -export const useObjectPermissionsForObject = (objectMetadataId: string) => { +export const useObjectPermissionsForObject = ( + objectMetadataId: string, +): ObjectPermissions & { objectMetadataId: string } => { const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); return useMemo(() => { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts index cf8440a10f..f3a92fe6d7 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts @@ -16,7 +16,7 @@ export const useOptionsForSelect = (fieldMetadataId: string) => { objectNameSingular, }); - const fieldMetadataItem = objectMetadataItem.fields.find( + const fieldMetadataItem = objectMetadataItem.readableFields.find( (field) => field.id === fieldMetadataId, ); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx index ca766d637a..95a66bca64 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForTable.test.tsx @@ -1,19 +1,35 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; +import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; +import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { DropResult, ResponderProvided } from '@hello-pangea/dnd'; import { renderHook } from '@testing-library/react'; import { act } from 'react'; import { RecoilRoot } from 'recoil'; describe('useObjectOptionsForTable', () => { - const initialRecoilState = [ - { fieldMetadataId: 'field1', isVisible: true, position: 0 }, - { fieldMetadataId: 'field2', isVisible: true, position: 1 }, - { fieldMetadataId: 'field3', isVisible: true, position: 2 }, - { fieldMetadataId: 'field4', isVisible: true, position: 3 }, - { fieldMetadataId: 'field5', isVisible: true, position: 4 }, - ]; + const fieldMetadataItems = [ + { id: 'id-1', name: 'field1' }, + { id: 'id-2', name: 'field2' }, + { id: 'id-3', name: 'field3' }, + { id: 'id-4', name: 'field4' }, + { id: 'id-5', name: 'field5' }, + ] as FieldMetadataItem[]; + + const columnDefinitionsWithMetadata = fieldMetadataItems.map( + (field, index) => ({ + fieldMetadataId: field.id, + isVisible: true, + position: index + 1, + metadata: { + fieldName: field.name, + }, + }), + ); const renderWithRecoil = () => renderHook(() => useObjectOptionsForTable('instance-id', 'object-id'), { @@ -27,8 +43,18 @@ describe('useObjectOptionsForTable', () => { tableColumnsComponentState.atomFamily({ instanceId: 'instance-id', }), - initialRecoilState as any, + columnDefinitionsWithMetadata as ColumnDefinition[], ); + set(objectMetadataItemsState, [ + { + id: 'object-id', + nameSingular: 'Object', + namePlural: 'Objects', + fields: fieldMetadataItems, + readableFields: fieldMetadataItems, + updatableFields: fieldMetadataItems, + } as ObjectMetadataItem, + ]); }} > {children} @@ -43,7 +69,7 @@ describe('useObjectOptionsForTable', () => { const dropResult = { source: { droppableId: 'droppable', index: 2 }, destination: { droppableId: 'droppable', index: 3 }, - draggableId: 'field3', + draggableId: 'field2', type: 'TYPE', mode: 'FLUID', reason: 'DROP', @@ -60,28 +86,43 @@ describe('useObjectOptionsForTable', () => { expect(result.current.visibleTableColumns).toEqual([ { - fieldMetadataId: 'field1', + fieldMetadataId: 'id-1', isVisible: true, + metadata: { + fieldName: 'field1', + }, position: 0, }, { - fieldMetadataId: 'field3', + fieldMetadataId: 'id-3', isVisible: true, + metadata: { + fieldName: 'field3', + }, position: 1, }, { - fieldMetadataId: 'field2', + fieldMetadataId: 'id-2', isVisible: true, + metadata: { + fieldName: 'field2', + }, position: 2, }, { - fieldMetadataId: 'field4', + fieldMetadataId: 'id-4', isVisible: true, + metadata: { + fieldName: 'field4', + }, position: 3, }, { - fieldMetadataId: 'field5', + fieldMetadataId: 'id-5', isVisible: true, + metadata: { + fieldName: 'field5', + }, position: 4, }, ]); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx index ed3600cc9c..9022cc1223 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx @@ -23,13 +23,16 @@ describe('useSearchRecordGroupField', () => { }); it('filters fields correctly based on input', () => { + const fields = [ + { type: FieldMetadataType.SELECT, label: 'First' }, + { type: FieldMetadataType.SELECT, label: 'Second' }, + { type: FieldMetadataType.TEXT, label: 'Third' }, + ]; const mockContextValue = { objectMetadataItem: { - fields: [ - { type: FieldMetadataType.SELECT, label: 'First' }, - { type: FieldMetadataType.SELECT, label: 'Second' }, - { type: FieldMetadataType.TEXT, label: 'Third' }, - ], + fields, + readableFields: fields, + updatableFields: fields, }, }; @@ -45,13 +48,16 @@ describe('useSearchRecordGroupField', () => { }); it('returns all select fields when search input is empty', () => { + const fields = [ + { type: FieldMetadataType.SELECT, label: 'First' }, + { type: FieldMetadataType.SELECT, label: 'Second' }, + { type: FieldMetadataType.TEXT, label: 'Third' }, + ]; const mockContextValue = { objectMetadataItem: { - fields: [ - { type: FieldMetadataType.SELECT, label: 'First' }, - { type: FieldMetadataType.SELECT, label: 'Second' }, - { type: FieldMetadataType.TEXT, label: 'Third' }, - ], + fields, + readableFields: fields, + updatableFields: fields, }, }; diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts index ffa590bf6f..c5e69d63be 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts @@ -14,12 +14,12 @@ export const useSearchRecordGroupField = () => { const searchInputLowerCase = recordGroupFieldSearchInput.toLocaleLowerCase(); - return objectMetadataItem.fields.filter( + return objectMetadataItem.readableFields.filter( (field) => field.type === FieldMetadataType.SELECT && field.label.toLocaleLowerCase().includes(searchInputLowerCase), ); - }, [objectMetadataItem.fields, recordGroupFieldSearchInput]); + }, [objectMetadataItem.readableFields, recordGroupFieldSearchInput]); return { recordGroupFieldSearchInput, diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts index ef2b78f3f4..910c6d48cc 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts @@ -3,21 +3,25 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { RecordSort } from '@/object-record/record-sort/types/RecordSort'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { EachTestingContext } from 'twenty-shared/testing'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +const fields = [ + { + name: 'name', + updatedAt: '2021-01-01', + createdAt: '2021-01-01', + id: '20202020-18b3-4099-86e3-c46b2d5d42f2', + type: FieldMetadataType.POSITION, + label: 'label', + }, +]; const objectMetadataItemWithPositionField: ObjectMetadataItem = { id: 'object1', - fields: [ - { - name: 'name', - updatedAt: '2021-01-01', - createdAt: '2021-01-01', - id: '20202020-18b3-4099-86e3-c46b2d5d42f2', - type: FieldMetadataType.POSITION, - label: 'label', - }, - ], + fields, + readableFields: fields, + updatableFields: fields, indexMetadatas: [], createdAt: '2021-01-01', updatedAt: '2021-01-01', diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx index 54c23a4608..e0fafe092f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx @@ -1,5 +1,5 @@ import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement'; -import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions'; +import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext'; import { RecordBoardColumnHeaderAggregateDropdownFieldsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent'; @@ -11,6 +11,7 @@ import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions'; import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { t } from '@lingui/core/macro'; export const AggregateDropdownContent = () => { @@ -22,11 +23,11 @@ export const AggregateDropdownContent = () => { const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const restrictedFieldMetadataIds = - getReadRestrictedFieldMetadataIdsFromObjectPermissions({ - objectPermissions: [ - objectPermissionsByObjectMetadataId[objectMetadataItem.id], - ], - objectMetadataId: objectMetadataItem.id, + getNonReadableFieldMetadataIdsFromObjectPermissions({ + objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }), }); const readableFields = objectMetadataItem.fields.filter( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts index 470cdfb394..824d216393 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts @@ -8,6 +8,24 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a'; describe('buildRecordGqlFieldsAggregateForView', () => { + const fields = [ + { + id: MOCK_FIELD_ID, + name: 'amount', + type: FieldMetadataType.NUMBER, + } as FieldMetadataItem, + { + id: '06b33746-5293-4d07-9f7f-ebf5ad396064', + name: 'name', + type: FieldMetadataType.TEXT, + } as FieldMetadataItem, + { + id: 'e46b9ba4-144b-4d10-a092-03a7521c8aa0', + name: 'createdAt', + type: FieldMetadataType.DATE_TIME, + } as FieldMetadataItem, + ]; + const mockObjectMetadata: ObjectMetadataItem = { id: '123', nameSingular: 'opportunity', @@ -22,23 +40,9 @@ describe('buildRecordGqlFieldsAggregateForView', () => { labelIdentifierFieldMetadataId: '06b33746-5293-4d07-9f7f-ebf5ad396064', imageIdentifierFieldMetadataId: null, isLabelSyncedWithName: true, - fields: [ - { - id: MOCK_FIELD_ID, - name: 'amount', - type: FieldMetadataType.NUMBER, - } as FieldMetadataItem, - { - id: '06b33746-5293-4d07-9f7f-ebf5ad396064', - name: 'name', - type: FieldMetadataType.TEXT, - } as FieldMetadataItem, - { - id: 'e46b9ba4-144b-4d10-a092-03a7521c8aa0', - name: 'createdAt', - type: FieldMetadataType.DATE_TIME, - } as FieldMetadataItem, - ], + fields, + readableFields: fields, + updatableFields: fields, indexMetadatas: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx index 22835fd6de..1d05a3cf4b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx @@ -51,8 +51,12 @@ import { isFieldText } from '../types/guards/isFieldText'; import { isFieldUuid } from '../types/guards/isFieldUuid'; export const FieldDisplay = () => { - const { fieldDefinition, isLabelIdentifier, isForbidden } = - useContext(FieldContext); + const { + fieldDefinition, + isLabelIdentifier, + isForbidden, + isRecordFieldReadOnly, + } = useContext(FieldContext); const isChipDisplay = isFieldIdentifierDisplay( fieldDefinition, @@ -96,7 +100,7 @@ export const FieldDisplay = () => { ) : isFieldBoolean(fieldDefinition) ? ( ) : isFieldRating(fieldDefinition) ? ( - + ) : isFieldRichText(fieldDefinition) ? ( ) : isFieldRichTextV2(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index edca66ba9d..29cc5220c2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -139,7 +139,7 @@ export const FieldInput = ({ ) : isFieldBoolean(fieldDefinition) ? ( ) : isFieldRating(fieldDefinition) ? ( - + ) : isFieldSelect(fieldDefinition) ? ( ) : isFieldMultiSelect(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx index 7514480e4b..8270c89e63 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx @@ -1,8 +1,8 @@ import { useRatingFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRatingFieldDisplay'; import { RatingInput } from '@/ui/field/input/components/RatingInput'; -export const RatingFieldDisplay = () => { +export const RatingFieldDisplay = ({ readonly }: { readonly?: boolean }) => { const { rating } = useRatingFieldDisplay(); - return ; + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts index 4e0c9bba4b..3a7378d2ce 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts @@ -138,6 +138,8 @@ const mockObjectMetadataItem: ObjectMetadataItem = { nameSingular: 'test', namePlural: 'tests', fields: [], + readableFields: [], + updatableFields: [], }; const mockObjectMetadataItemWithAllFields: ObjectMetadataItem = { diff --git a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts index 7065aeae07..ae787ff733 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts @@ -1,5 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ObjectPermission } from '~/generated/graphql'; +import { ObjectPermissions } from 'twenty-shared/types'; import { createRequiredContext } from '~/utils/createRequiredContext'; export type RecordIndexContextValue = { @@ -8,7 +8,10 @@ export type RecordIndexContextValue = { objectNamePlural: string; objectNameSingular: string; objectMetadataItem: ObjectMetadataItem; - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; recordIndexId: string; }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts index 0eee1dfcfd..b411d4a2ed 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts @@ -10,6 +10,7 @@ import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/ import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState'; import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { ApolloClient } from '@apollo/client'; import { isNonEmptyArray } from '@sniptt/guards'; import { useRecoilCallback } from 'recoil'; @@ -104,8 +105,10 @@ export const useMultipleRecordPickerPerformSearch = () => { const filteredSearchableObjectMetadataItems = searchableObjectMetadataItems.filter( (objectMetadataItem) => - objectPermissionsByObjectMetadataId[objectMetadataItem.id] - .canReadObjectRecords === true, + getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }).canReadObjectRecords === true, ); const [ diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx index 08ea26caae..6a5c317544 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -21,8 +21,9 @@ import { RecordDetailRelationSection } from '@/object-record/record-show/record- import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from 'twenty-shared/utils'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; type FieldsCardProps = { objectNameSingular: string; @@ -56,7 +57,7 @@ export const FieldsCard = ({ const { isInRightDrawer } = useIsInRightDrawerOrThrow(); - const availableFieldMetadataItems = objectMetadataItem.fields + const availableFieldMetadataItems = objectMetadataItem.readableFields .filter( (fieldMetadataItem) => isFieldCellSupported(fieldMetadataItem, objectMetadataItems) && @@ -138,9 +139,10 @@ export const FieldsCard = ({ isRecordFieldReadOnly: isRecordFieldReadOnly({ isRecordReadOnly, objectPermissions: - objectPermissionsByObjectMetadataId[ - objectMetadataItem.id - ], + getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }), fieldMetadataId: fieldMetadataItem.id, objectNameSingular, fieldName: fieldMetadataItem.name, @@ -188,9 +190,10 @@ export const FieldsCard = ({ isRecordFieldReadOnly: isRecordFieldReadOnly({ isRecordReadOnly, objectPermissions: - objectPermissionsByObjectMetadataId[ - objectMetadataItem.id - ], + getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }), fieldMetadataId: fieldMetadataItem.id, objectNameSingular, fieldName: fieldMetadataItem.name, @@ -239,8 +242,10 @@ export const FieldsCard = ({ isDisplayModeFixHeight: true, isRecordFieldReadOnly: isRecordFieldReadOnly({ isRecordReadOnly, - objectPermissions: - objectPermissionsByObjectMetadataId[objectMetadataItem.id], + objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }), fieldMetadataId: fieldMetadataItem.id, objectNameSingular, fieldName: fieldMetadataItem.name, diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts index ef911b62ec..9767d54474 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts @@ -6,6 +6,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions import { BASE_RECORD_LAYOUT } from '@/object-record/record-show/constants/BaseRecordLayout'; import { CardType } from '@/object-record/record-show/types/CardType'; import { RecordLayout } from '@/object-record/record-show/types/RecordLayout'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { RecordLayoutTab } from '@/ui/layout/tab-list/types/RecordLayoutTab'; import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -300,8 +301,10 @@ export const useRecordShowContainerTabs = ( const permissionHide = hide.ifNoReadPermission && isDefined(targetObjectNameSingular) && - !objectPermissionsByObjectMetadataId[targetObjectMetadataId ?? ''] - ?.canReadObjectRecords; + !getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: targetObjectMetadataId ?? '', + })?.canReadObjectRecords; const requiredObjectsInactive = hide.ifRequiredObjectsInactive.length > 0 && diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts index 13000d8cdd..fb6aa9a370 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts @@ -1,19 +1,13 @@ -import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useRecoilCallback } from 'recoil'; -import { RestrictedField } from 'twenty-shared/types'; -import { FeatureFlagKey } from '~/generated/graphql'; +import { isDefined } from 'twenty-shared/utils'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetTableColumns = () => { - const featureFlags = useFeatureFlagsMap(); - const isFieldsPermissionsEnabled = - featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED]; - const setTableColumns = useRecoilCallback( ({ snapshot, set }) => ( @@ -21,6 +15,19 @@ export const useSetTableColumns = () => { recordTableId: string, objectMetadataId: string, ) => { + const objectMetadataItems = getSnapshotValue( + snapshot, + objectMetadataItemsState, + ); + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.id === objectMetadataId, + ); + + if (!isDefined(objectMetadataItem)) { + return; + } + const tableColumns = getSnapshotValue( snapshot, tableColumnsComponentState.atomFamily({ @@ -28,30 +35,11 @@ export const useSetTableColumns = () => { }), ); - let columnsToSet = columns; - - if (isFieldsPermissionsEnabled) { - const restrictedFields = getSnapshotValue( - snapshot, - currentUserWorkspaceState, - )?.objectPermissions?.find( - (permission) => permission.objectMetadataId === objectMetadataId, - )?.restrictedFields; - - const restrictedFieldMetadataIds = Object.entries( - restrictedFields ?? {}, - ) - .filter( - ([_fieldMetadataId, restrictedField]) => - (restrictedField as RestrictedField).canRead === false, - ) - .map(([fieldMetadataId]) => fieldMetadataId); - const nonRestrictedColumns = columns.filter( - (column) => - !restrictedFieldMetadataIds?.includes(column.fieldMetadataId), - ); - columnsToSet = nonRestrictedColumns; - } + const columnsToSet = columns.filter((column) => + objectMetadataItem.readableFields + .map((field) => field.name) + .includes(column.metadata.fieldName), + ); if (isDeeplyEqual(tableColumns, columnsToSet)) { return; @@ -63,7 +51,7 @@ export const useSetTableColumns = () => { columnsToSet, ); }, - [isFieldsPermissionsEnabled], + [], ); return { setTableColumns }; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts index 331eee1b48..37988d6aa1 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts @@ -44,7 +44,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( ) => { const availableFieldMetadataItemsToImport = spreadsheetImportFilterAvailableFieldMetadataItems( - objectMetadataItem.fields, + objectMetadataItem.updatableFields, ); const spreadsheetImportFields = buildSpreadsheetImportFields( diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts index 226ef3d859..3e2e0415b0 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts @@ -16,6 +16,8 @@ describe('generateAggregateQuery', () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), fields: [], + readableFields: [], + updatableFields: [], indexMetadatas: [], isLabelSyncedWithName: true, isRemote: false, @@ -55,6 +57,8 @@ describe('generateAggregateQuery', () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), fields: [], + readableFields: [], + updatableFields: [], indexMetadatas: [], isLabelSyncedWithName: true, isRemote: false, diff --git a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts index 771f744371..f3853ff98c 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts @@ -3,8 +3,8 @@ import gql from 'graphql-tag'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; +import { ObjectPermissions } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { ObjectPermission } from '~/generated/graphql'; export type QueryCursorDirection = 'before' | 'after'; @@ -22,7 +22,10 @@ export const generateFindManyRecordsQuery = ({ recordGqlFields?: RecordGqlOperationGqlRecordFields; computeReferences?: boolean; cursorDirection?: QueryCursorDirection; - objectPermissionsByObjectMetadataId: Record; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; isFieldsPermissionsEnabled?: boolean; }) => gql` query FindMany${capitalize( diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId.ts new file mode 100644 index 0000000000..f5aee1d4d2 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId.ts @@ -0,0 +1,24 @@ +import { ObjectPermissions } from 'twenty-shared/types'; + +type GetObjectPermissionsFromMapByObjectIdArgs = { + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; + objectMetadataId: string; +}; + +export const getObjectPermissionsFromMapByObjectMetadataId = ({ + objectPermissionsByObjectMetadataId, + objectMetadataId, +}: GetObjectPermissionsFromMapByObjectIdArgs) => { + return ( + objectPermissionsByObjectMetadataId[objectMetadataId] ?? { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canCreateObjectRecords: true, + canDeleteObjectRecords: true, + restrictedFields: {}, + } + ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx index 9ef1ec9773..1d8249143c 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx @@ -1,4 +1,4 @@ -import { useAuth } from '@/auth/hooks/useAuth'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/components/SettingsRoleAssignment'; @@ -16,6 +16,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; import { t } from '@lingui/core/macro'; import { useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; @@ -33,6 +34,7 @@ type SettingsRoleProps = { }; export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { + const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems(); const activeTabId = useRecoilComponentValue( activeTabIdComponentState, SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId, @@ -52,7 +54,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { settingsPersistedRoleFamilyState(roleId), ); - const { loadCurrentUser } = useAuth(); + const { loadCurrentUser } = useLoadCurrentUser(); const { enqueueErrorSnackBar } = useSnackBar(); @@ -111,8 +113,8 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { try { await saveDraftRoleToDB(); - await loadCurrentUser(); + await refreshObjectMetadataItems(); } finally { setIsSaving(false); } diff --git a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/DeleteTwoFactorAuthenticationMethod.tsx b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/DeleteTwoFactorAuthenticationMethod.tsx index 8646ad1b36..e7c74aca99 100644 --- a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/DeleteTwoFactorAuthenticationMethod.tsx +++ b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/DeleteTwoFactorAuthenticationMethod.tsx @@ -15,6 +15,7 @@ import { useDeleteTwoFactorAuthenticationMethodMutation } from '~/generated-meta import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useCurrentUserWorkspaceTwoFactorAuthentication } from '../hooks/useCurrentUserWorkspaceTwoFactorAuthentication'; import { useCurrentWorkspaceTwoFactorAuthenticationPolicy } from '../hooks/useWorkspaceTwoFactorAuthenticationPolicy'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; const DELETE_TWO_FACTOR_AUTHENTICATION_MODAL_ID = 'delete-two-factor-authentication-modal'; @@ -23,7 +24,8 @@ export const DeleteTwoFactorAuthentication = () => { const { openModal } = useModal(); const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); - const { signOut, loadCurrentUser } = useAuth(); + const { signOut } = useAuth(); + const { loadCurrentUser } = useLoadCurrentUser(); const [deleteTwoFactorAuthenticationMethod] = useDeleteTwoFactorAuthenticationMethodMutation(); const currentUser = useRecoilValue(currentUserState); diff --git a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx index 3524ea4145..256ac57557 100644 --- a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx +++ b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx @@ -6,10 +6,10 @@ import { OTPInput, SlotProps } from 'input-otp'; import { useState } from 'react'; import { Controller, useForm, useFormContext } from 'react-hook-form'; -import { useAuth } from '@/auth/hooks/useAuth'; import { VERIFY_TWO_FACTOR_AUTHENTICATION_METHOD_FOR_AUTHENTICATED_USER } from '@/settings/two-factor-authentication/graphql/mutations/verifyTwoFactorAuthenticationMethod'; import { SettingsPath } from '@/types/SettingsPath'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; // OTP Form Types @@ -150,7 +150,7 @@ export const useTwoFactorVerificationForSettings = () => { const navigate = useNavigateSettings(); const { t } = useLingui(); const [isLoading, setIsLoading] = useState(false); - const { loadCurrentUser } = useAuth(); + const { loadCurrentUser } = useLoadCurrentUser(); const [verifyTwoFactorAuthenticationMethod] = useMutation( VERIFY_TWO_FACTOR_AUTHENTICATION_METHOD_FOR_AUTHENTICATED_USER, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts index 256cc54f9f..53075805bd 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts @@ -12,7 +12,7 @@ export const useDownloadFakeRecords = () => { const availableFieldMetadataItems = spreadsheetImportFilterAvailableFieldMetadataItems( - objectMetadataItem.fields, + objectMetadataItem.updatableFields, ); const buildTableWithFakeRecords = () => { diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx index 9d31b7d19f..65691a19cf 100644 --- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx @@ -24,10 +24,11 @@ import { enUS } from 'date-fns/locale'; import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { ObjectPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { - useGetCurrentUserQuery, WorkspaceMember, + useGetCurrentUserQuery, } from '~/generated-metadata/graphql'; import { dateLocaleState } from '~/localization/states/dateLocaleState'; import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; @@ -99,7 +100,14 @@ export const UserProviderEffect = () => { } if (isDefined(queryData.currentUser.currentUserWorkspace)) { - setCurrentUserWorkspace(queryData.currentUser.currentUserWorkspace); + setCurrentUserWorkspace({ + ...queryData.currentUser.currentUserWorkspace, + objectPermissions: + (queryData.currentUser.currentUserWorkspace + .objectPermissions as Array< + ObjectPermissions & { objectMetadataId: string } + >) ?? [], + }); } const { diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index d5af2b3bdb..362a6b390c 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -10,11 +10,6 @@ import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/frag import { gql } from '@apollo/client'; export const USER_QUERY_FRAGMENT = gql` - ${ROLE_FRAGMENT} - ${OBJECT_PERMISSION_FRAGMENT} - ${WORKSPACE_URLS_FRAGMENT} - ${AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT} - ${AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT} fragment UserQueryFragment on User { id firstName @@ -110,4 +105,9 @@ export const USER_QUERY_FRAGMENT = gql` ${WORKSPACE_MEMBER_QUERY_FRAGMENT} ${DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT} + ${OBJECT_PERMISSION_FRAGMENT} + ${WORKSPACE_URLS_FRAGMENT} + ${ROLE_FRAGMENT} + ${AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT} + ${AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT} `; diff --git a/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts b/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts new file mode 100644 index 0000000000..4f56468ff5 --- /dev/null +++ b/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts @@ -0,0 +1,152 @@ +import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; +import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain'; +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; +import { detectTimeZone } from '@/localization/utils/detectTimeZone'; +import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat'; +import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat'; +import { useCallback } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { ObjectPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { ColorScheme } from 'twenty-ui/input'; +import { useGetCurrentUserLazyQuery } from '~/generated-metadata/graphql'; +import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; +import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; + +export const useLoadCurrentUser = () => { + const setCurrentUser = useSetRecoilState(currentUserState); + const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState); + const setCurrentWorkspaceMember = useSetRecoilState( + currentWorkspaceMemberState, + ); + const { setLastAuthenticateWorkspaceDomain } = + useLastAuthenticatedWorkspaceDomain(); + const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState); + const setCurrentWorkspaceMembers = useSetRecoilState( + currentWorkspaceMembersState, + ); + const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); + const setDateTimeFormat = useSetRecoilState(dateTimeFormatState); + + const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); + + const [getCurrentUser] = useGetCurrentUserLazyQuery(); + + const loadCurrentUser = useCallback(async () => { + const currentUserResult = await getCurrentUser({ + fetchPolicy: 'network-only', + }); + + if (isDefined(currentUserResult.error)) { + throw new Error(currentUserResult.error.message); + } + + const user = currentUserResult.data?.currentUser; + + if (!user) { + throw new Error('No current user result'); + } + + let workspaceMember = null; + + setCurrentUser(user); + + if (isDefined(user.workspaceMembers)) { + const workspaceMembers = user.workspaceMembers.map((workspaceMember) => ({ + ...workspaceMember, + colorScheme: workspaceMember.colorScheme as ColorScheme, + locale: workspaceMember.locale ?? SOURCE_LOCALE, + })); + + setCurrentWorkspaceMembers(workspaceMembers); + } + + if (isDefined(user.availableWorkspaces)) { + setAvailableWorkspaces(user.availableWorkspaces); + } + + if (isDefined(user.currentUserWorkspace)) { + setCurrentUserWorkspace({ + ...user.currentUserWorkspace, + objectPermissions: + (user.currentUserWorkspace.objectPermissions as Array< + ObjectPermissions & { objectMetadataId: string } + >) ?? [], + }); + } + + if (isDefined(user.workspaceMember)) { + workspaceMember = { + ...user.workspaceMember, + colorScheme: user.workspaceMember?.colorScheme as ColorScheme, + locale: user.workspaceMember?.locale ?? SOURCE_LOCALE, + }; + + setCurrentWorkspaceMember(workspaceMember); + + // TODO: factorize with UserProviderEffect + setDateTimeFormat({ + timeZone: + workspaceMember.timeZone && workspaceMember.timeZone !== 'system' + ? workspaceMember.timeZone + : detectTimeZone(), + dateFormat: isDefined(user.workspaceMember.dateFormat) + ? getDateFormatFromWorkspaceDateFormat( + user.workspaceMember.dateFormat, + ) + : DateFormat[detectDateFormat()], + timeFormat: isDefined(user.workspaceMember.timeFormat) + ? getTimeFormatFromWorkspaceTimeFormat( + user.workspaceMember.timeFormat, + ) + : TimeFormat[detectTimeFormat()], + }); + dynamicActivate( + (workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE, + ); + } + + const workspace = user.currentWorkspace ?? null; + + setCurrentWorkspace(workspace); + + if (isDefined(workspace) && isOnAWorkspace) { + setLastAuthenticateWorkspaceDomain({ + workspaceId: workspace.id, + workspaceUrl: getWorkspaceUrl(workspace.workspaceUrls), + }); + } + + return { + user, + workspaceMember, + workspace, + }; + }, [ + getCurrentUser, + isOnAWorkspace, + setCurrentUser, + setCurrentUserWorkspace, + setCurrentWorkspace, + setCurrentWorkspaceMember, + setCurrentWorkspaceMembers, + setDateTimeFormat, + setLastAuthenticateWorkspaceDomain, + setAvailableWorkspaces, + ]); + + return { + loadCurrentUser, + }; +}; diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts index 922c2743f4..9738e91b31 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts @@ -26,7 +26,7 @@ export const useGetAvailableFieldsForKanban = () => { ); const availableFieldsForKanban = - objectMetadataItem?.fields.filter( + objectMetadataItem?.readableFields.filter( (field) => field.type === FieldMetadataType.SELECT, ) ?? []; diff --git a/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts b/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts index 8da6673a38..a87ae36f94 100644 --- a/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts +++ b/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts @@ -16,6 +16,48 @@ const meta: Meta = { export default meta; type Story = StoryObj; +const fields = [ + { + id: '1', + name: 'name', + label: 'Name', + type: FieldMetadataType.TEXT, + description: 'Company name', + isCustom: false, + isActive: true, + isSystem: false, + isNullable: false, + createdAt: '', + updatedAt: '', + }, + { + id: '2', + name: 'domainName', + label: 'Domain Name', + type: FieldMetadataType.TEXT, + description: 'Company domain name', + isCustom: false, + isActive: true, + isSystem: false, + isNullable: true, + createdAt: '', + updatedAt: '', + }, + { + id: '3', + name: 'employees', + label: 'Employees', + type: FieldMetadataType.NUMBER, + description: 'Number of employees', + isCustom: false, + isActive: true, + isSystem: false, + isNullable: true, + createdAt: '', + updatedAt: '', + }, +]; + const mockObjectMetadataItem: ObjectMetadataItem = { id: '1', nameSingular: 'company', @@ -34,47 +76,9 @@ const mockObjectMetadataItem: ObjectMetadataItem = { isSearchable: true, labelIdentifierFieldMetadataId: '1', indexMetadatas: [], - fields: [ - { - id: '1', - name: 'name', - label: 'Name', - type: FieldMetadataType.TEXT, - description: 'Company name', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - createdAt: '', - updatedAt: '', - }, - { - id: '2', - name: 'domainName', - label: 'Domain Name', - type: FieldMetadataType.TEXT, - description: 'Company domain name', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - createdAt: '', - updatedAt: '', - }, - { - id: '3', - name: 'employees', - label: 'Employees', - type: FieldMetadataType.NUMBER, - description: 'Number of employees', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - createdAt: '', - updatedAt: '', - }, - ], + fields: fields, + readableFields: fields, + updatableFields: fields, }; export const Default: Story = { diff --git a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx index 6d3cfbfeed..aa837d2df6 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx @@ -8,14 +8,14 @@ import { z } from 'zod'; import { Logo } from '@/auth/components/Logo'; import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; -import { useAuth } from '@/auth/hooks/useAuth'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; import { ApolloError } from '@apollo/client'; import { Trans, useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; @@ -69,7 +69,7 @@ export const CreateWorkspace = () => { const setNextOnboardingStatus = useSetNextOnboardingStatus(); const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems(); - const { loadCurrentUser } = useAuth(); + const { loadCurrentUser } = useLoadCurrentUser(); const [activateWorkspace] = useActivateWorkspaceMutation(); const [pendingCreationLoaderStep, setPendingCreationLoaderStep] = useState( PendingCreationLoaderStep.None, diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx index e51a0d6004..557a69d565 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx @@ -7,7 +7,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util'; import { Select } from '@/ui/input/components/Select'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { useLingui } from '@lingui/react/macro'; import { enUS } from 'date-fns/locale'; import { APP_LOCALES } from 'twenty-shared/translations'; diff --git a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx index 81415c23fa..c26ddb6c36 100644 --- a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx @@ -20,6 +20,7 @@ import { useSetRecordTableData } from '@/object-record/record-table/hooks/intern import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { View } from '@/views/types/View'; @@ -92,8 +93,10 @@ const InternalTableContextProviders = ({ recordTableId: objectMetadataItem.namePlural, viewBarId: 'view-bar', visibleTableColumns: visibleTableColumns, - objectPermissions: - objectPermissionsByObjectMetadataId[objectMetadataItem.id], + objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }), }} > ({ + objectMetadataId: item.id, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + })), + }, locale: 'en', workspaces: [{ workspace: mockCurrentWorkspace }], - onboardingStatus: onboardingStatus || null, + onboardingStatdeus: onboardingStatus || null, + userVars: {}, + availableWorkspaces: { + availableWorkspacesForSignIn: [], + availableWorkspacesForSignUp: [], + }, }; }; diff --git a/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts b/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts index 618067c54c..5e628f44e9 100644 --- a/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts +++ b/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts @@ -16,6 +16,8 @@ export const generatedMockObjectMetadataItems: ObjectMetadataItem[] = return { ...objectWithoutFieldsList, fields: fieldsList, + readableFields: fieldsList, + updatableFields: fieldsList, labelIdentifierFieldMetadataId, indexMetadatas: indexMetadataList.map((index) => ({ ...index, diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index 077cc002fd..a9b9191bfa 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -3,7 +3,10 @@ import { BadRequestException, Inject } from '@nestjs/common'; import { Request } from 'express'; import chunk from 'lodash.chunk'; import isEmpty from 'lodash.isempty'; -import { FieldMetadataType, RestrictedFields } from 'twenty-shared/types'; +import { + FieldMetadataType, + RestrictedFieldsPermissions, +} from 'twenty-shared/types'; import { capitalize, isDefined } from 'twenty-shared/utils'; import { In, ObjectLiteral } from 'typeorm'; @@ -185,7 +188,7 @@ export abstract class RestApiBaseHandler { roleId, ); - let restrictedFields: RestrictedFields = {}; + let restrictedFields: RestrictedFieldsPermissions = {}; if ( await this.featureFlagService.isFeatureEnabled( @@ -298,7 +301,7 @@ export abstract class RestApiBaseHandler { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; }; depth: Depth | undefined; - restrictedFields: RestrictedFields; + restrictedFields: RestrictedFieldsPermissions; }) { const relations = this.getRelations({ objectMetadata, @@ -415,7 +418,7 @@ export abstract class RestApiBaseHandler { }; objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; extraFilters?: Partial; - restrictedFields: RestrictedFields; + restrictedFields: RestrictedFieldsPermissions; }) { const objectMetadataNameSingular = objectMetadata.objectMetadataMapItem.nameSingular; diff --git a/packages/twenty-server/src/engine/api/utils/get-all-selectable-fields.utils.ts b/packages/twenty-server/src/engine/api/utils/get-all-selectable-fields.utils.ts index 00cd564b1b..b4a2fcba89 100644 --- a/packages/twenty-server/src/engine/api/utils/get-all-selectable-fields.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/get-all-selectable-fields.utils.ts @@ -1,16 +1,18 @@ -import { RestrictedFields } from 'twenty-shared/types'; +import { RestrictedFieldsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util'; +type GetAllSelectableFieldsArgs = { + restrictedFields: RestrictedFieldsPermissions; + objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; +}; + export const getAllSelectableFields = ({ restrictedFields, objectMetadata, -}: { - restrictedFields: RestrictedFields; - objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; -}) => { +}: GetAllSelectableFieldsArgs) => { const restrictedFieldsIds = Object.entries(restrictedFields) .filter(([_, value]) => value.canRead === false) .map(([key]) => key); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts index 7de73d1e2d..c99c83ae4e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts @@ -1,7 +1,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; import GraphQLJSON from 'graphql-type-json'; -import { RestrictedFields } from 'twenty-shared/types'; +import { RestrictedFieldsPermissions } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -25,5 +25,5 @@ export class ObjectPermissionDTO { @Field(() => GraphQLJSON, { nullable: true, }) - restrictedFields?: RestrictedFields; + restrictedFields?: RestrictedFieldsPermissions; } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts index 7f7907e0bc..bb2c2d9a9c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; +import { ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; import { In, Repository } from 'typeorm'; import { @@ -53,7 +53,7 @@ describe('FieldPermissionService', () => { isEditable: true, } as RoleEntity; - const mockRolesPermissions: ObjectRecordsPermissionsByRoleId = { + const mockRolesPermissions: ObjectsPermissionsByRoleIdDeprecated = { [testRoleId]: { [testObjectMetadataId]: { canRead: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts index 80551f47af..0f7642127c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; +import { ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In, Repository } from 'typeorm'; @@ -176,7 +176,7 @@ export class FieldPermissionService { }: { fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0]; objectMetadataMapsById: ObjectMetadataMaps['byId']; - rolesPermissions: ObjectRecordsPermissionsByRoleId; + rolesPermissions: ObjectsPermissionsByRoleIdDeprecated; role: RoleEntity; }) { if ( diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts index 6bce6a0d6d..8b847fc610 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts @@ -1,10 +1,10 @@ import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; export type UserWorkspacePermissions = { permissionFlags: Record; objectRecordsPermissions: Record; - objectPermissions: ObjectRecordsPermissions; + objectPermissions: ObjectsPermissionsDeprecated; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts index 97f0d8a1b3..b9ef605018 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; +import { ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; import { v4 } from 'uuid'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; @@ -34,12 +34,12 @@ export class WorkspacePermissionsCacheStorageService { async setRolesPermissions( workspaceId: string, - permissions: ObjectRecordsPermissionsByRoleId, + permissions: ObjectsPermissionsByRoleIdDeprecated, ): Promise<{ newRolesPermissionsVersion: string; }> { const [, newRolesPermissionsVersion] = await Promise.all([ - this.cacheStorageService.set( + this.cacheStorageService.set( `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`, permissions, TTL_INFINITE, @@ -52,8 +52,8 @@ export class WorkspacePermissionsCacheStorageService { getRolesPermissions( workspaceId: string, - ): Promise { - return this.cacheStorageService.get( + ): Promise { + return this.cacheStorageService.get( `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts index 51ceed6553..87ff8e10b0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -2,9 +2,9 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { - ObjectRecordsPermissions, - ObjectRecordsPermissionsByRoleId, - RestrictedFields, + ObjectsPermissionsByRoleIdDeprecated, + ObjectsPermissionsDeprecated, + RestrictedFieldsPermissions, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In, IsNull, Not, Repository } from 'typeorm'; @@ -51,7 +51,9 @@ export class WorkspacePermissionsCacheService { workspaceId: string; roleIds?: string[]; }): Promise { - let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined; + let currentRolesPermissions: + | ObjectsPermissionsByRoleIdDeprecated + | undefined; if (roleIds) { currentRolesPermissions = @@ -103,8 +105,11 @@ export class WorkspacePermissionsCacheService { workspaceId, }: { workspaceId: string; - }): Promise> { - return getFromCacheWithRecompute({ + }): Promise> { + return getFromCacheWithRecompute< + string, + ObjectsPermissionsByRoleIdDeprecated + >({ workspaceId, getCacheData: () => this.workspacePermissionsCacheStorageService.getRolesPermissions( @@ -166,7 +171,7 @@ export class WorkspacePermissionsCacheService { }: { workspaceId: string; roleIds?: string[]; - }): Promise { + }): Promise { let roles: RoleEntity[] = []; const workspaceFeatureFlagsMap = @@ -192,10 +197,10 @@ export class WorkspacePermissionsCacheService { const workspaceObjectMetadataCollection = await this.getWorkspaceObjectMetadataCollection(workspaceId); - const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {}; + const permissionsByRoleId: ObjectsPermissionsByRoleIdDeprecated = {}; for (const role of roles) { - const objectRecordsPermissions: ObjectRecordsPermissions = {}; + const objectRecordsPermissions: ObjectsPermissionsDeprecated = {}; for (const objectMetadata of workspaceObjectMetadataCollection) { const { id: objectMetadataId, isSystem, standardId } = objectMetadata; @@ -204,7 +209,7 @@ export class WorkspacePermissionsCacheService { let canUpdate = role.canUpdateAllObjectRecords; let canSoftDelete = role.canSoftDeleteAllObjectRecords; let canDestroy = role.canDestroyAllObjectRecords; - const restrictedFields: RestrictedFields = {}; + const restrictedFields: RestrictedFieldsPermissions = {}; if ( standardId && diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index 8fc5b94ccd..d8e4ee64dc 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -1,6 +1,6 @@ import { Entity } from '@microsoft/microsoft-graph-types'; import { isDefined } from 'class-validator'; -import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; +import { ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; import { DataSource, DataSourceOptions, @@ -34,7 +34,7 @@ export class WorkspaceDataSource extends DataSource { featureFlagMapVersion: string; featureFlagMap: FeatureFlagMap; rolesPermissionsVersion: string; - permissionsPerRoleId: ObjectRecordsPermissionsByRoleId; + permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated; dataSourceWithOverridenCreateQueryBuilder: WorkspaceDataSource; constructor( @@ -43,7 +43,7 @@ export class WorkspaceDataSource extends DataSource { featureFlagMapVersion: string, featureFlagMap: FeatureFlagMap, rolesPermissionsVersion: string, - permissionsPerRoleId: ObjectRecordsPermissionsByRoleId, + permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated, ) { super(options); this.internalContext = internalContext; @@ -235,7 +235,9 @@ export class WorkspaceDataSource extends DataSource { this.rolesPermissionsVersion = rolesPermissionsVersion; } - setRolesPermissions(permissionsPerRoleId: ObjectRecordsPermissionsByRoleId) { + setRolesPermissions( + permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated, + ) { this.permissionsPerRoleId = permissionsPerRoleId; } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 6981eb054e..b6c8d7b5aa 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { EntityManager } from 'typeorm'; import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor'; import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer'; @@ -75,7 +75,7 @@ describe('WorkspaceEntityManager', () => { let mockDataSource: WorkspaceDataSource; let mockPermissionOptions: { shouldBypassPermissionChecks: boolean; - objectRecordsPermissions?: ObjectRecordsPermissions; + objectRecordsPermissions?: ObjectsPermissionsDeprecated; }; beforeEach(() => { @@ -331,8 +331,7 @@ describe('WorkspaceEntityManager', () => { isFieldPermissionsEnabled: true, operationType: 'update', objectMetadataMaps: mockInternalContext.objectMetadataMaps, - objectRecordsPermissions: - mockPermissionOptions.objectRecordsPermissions, + objectsPermissions: mockPermissionOptions.objectRecordsPermissions, selectedColumns: [], allFieldsSelected: false, updatedColumns: [], @@ -366,8 +365,7 @@ describe('WorkspaceEntityManager', () => { operationType: 'delete', isFieldPermissionsEnabled: true, objectMetadataMaps: mockInternalContext.objectMetadataMaps, - objectRecordsPermissions: - mockPermissionOptions.objectRecordsPermissions, + objectsPermissions: mockPermissionOptions.objectRecordsPermissions, selectedColumns: [], allFieldsSelected: false, updatedColumns: [], diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 4eaa89652b..44f4828cfc 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -1,5 +1,5 @@ import isEmpty from 'lodash.isempty'; -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { DeleteResult, @@ -58,7 +58,7 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g type PermissionOptions = { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectRecordsPermissions; + objectRecordsPermissions?: ObjectsPermissionsDeprecated; }; export class WorkspaceEntityManager extends EntityManager { @@ -130,7 +130,7 @@ export class WorkspaceEntityManager extends EntityManager { queryRunner?: QueryRunner, options: { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectRecordsPermissions; + objectRecordsPermissions?: ObjectsPermissionsDeprecated; } = { shouldBypassPermissionChecks: false, objectRecordsPermissions: {}, @@ -198,7 +198,7 @@ export class WorkspaceEntityManager extends EntityManager { conflictPathsOrOptions: string[] | UpsertOptions, permissionOptions?: { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectRecordsPermissions; + objectRecordsPermissions?: ObjectsPermissionsDeprecated; }, selectedColumns: string[] | '*' = '*', ): Promise { @@ -387,7 +387,7 @@ export class WorkspaceEntityManager extends EntityManager { operationType: OperationType; permissionOptions?: { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectRecordsPermissions; + objectRecordsPermissions?: ObjectsPermissionsDeprecated; }; selectedColumns: string[]; updatedColumns?: string[]; @@ -404,8 +404,7 @@ export class WorkspaceEntityManager extends EntityManager { validateOperationIsPermittedOrThrow({ entityName, operationType, - objectRecordsPermissions: - permissionOptions?.objectRecordsPermissions ?? {}, + objectsPermissions: permissionOptions?.objectRecordsPermissions ?? {}, objectMetadataMaps: this.internalContext.objectMetadataMaps, selectedColumns, allFieldsSelected: false, diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 0a3396c0f1..08241d7d44 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; +import { ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { EntitySchema, Repository } from 'typeorm'; @@ -256,8 +256,11 @@ export class WorkspaceDatasourceFactory { workspaceId, }: { workspaceId: string; - }): Promise> { - return getFromCacheWithRecompute({ + }): Promise> { + return getFromCacheWithRecompute< + string, + ObjectsPermissionsByRoleIdDeprecated + >({ workspaceId, getCacheData: () => this.workspacePermissionsCacheStorageService.getRolesPermissions( @@ -310,7 +313,7 @@ export class WorkspaceDatasourceFactory { }: { workspaceDataSource: WorkspaceDataSource; cachedRolesPermissionsVersion: string; - cachedRolesPermissions: ObjectRecordsPermissionsByRoleId; + cachedRolesPermissions: ObjectsPermissionsByRoleIdDeprecated; }): Promise { this.updateWorkspaceDataSourceIfNeeded({ workspaceDataSource, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index 9ade08866a..9aa773e3a1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { DeepPartial, FindManyOptions, @@ -20,7 +20,7 @@ describe('WorkspaceRepository', () => { let mockEntityManager: jest.Mocked; let mockInternalContext: WorkspaceInternalContext; let mockFeatureFlagMap: FeatureFlagMap; - let mockObjectRecordsPermissions: ObjectRecordsPermissions; + let mockObjectRecordsPermissions: ObjectsPermissionsDeprecated; let mockQueryRunner: QueryRunner; beforeEach(() => { diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts index 2b440b2d03..37d91a50d6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts @@ -1,8 +1,8 @@ import { isNonEmptyString } from '@sniptt/guards'; import isEmpty from 'lodash.isempty'; import { - ObjectRecordsPermissions, - RestrictedFields, + ObjectsPermissionsDeprecated, + RestrictedFieldsPermissions, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap'; @@ -35,25 +35,27 @@ export type OperationType = | 'restore' | 'soft-delete'; -export const validateOperationIsPermittedOrThrow = ({ - entityName, - operationType, - objectRecordsPermissions, - objectMetadataMaps, - selectedColumns, - isFieldPermissionsEnabled, - allFieldsSelected, - updatedColumns, -}: { +type ValidateOperationIsPermittedOrThrowArgs = { entityName: string; operationType: OperationType; - objectRecordsPermissions: ObjectRecordsPermissions; + objectsPermissions: ObjectsPermissionsDeprecated; objectMetadataMaps: ObjectMetadataMaps; selectedColumns: string[] | '*'; isFieldPermissionsEnabled?: boolean; allFieldsSelected: boolean; updatedColumns: string[]; -}) => { +}; + +export const validateOperationIsPermittedOrThrow = ({ + entityName, + operationType, + objectsPermissions, + objectMetadataMaps, + selectedColumns, + isFieldPermissionsEnabled, + allFieldsSelected, + updatedColumns, +}: ValidateOperationIsPermittedOrThrowArgs) => { const objectMetadataIdForEntity = objectMetadataMaps.idByNameSingular[entityName]; @@ -83,8 +85,7 @@ export const validateOperationIsPermittedOrThrow = ({ ? getColumnNameToFieldMetadataIdMap(objectMetadata) : {}; - const permissionsForEntity = - objectRecordsPermissions[objectMetadataIdForEntity]; + const permissionsForEntity = objectsPermissions[objectMetadataIdForEntity]; switch (operationType) { case 'select': @@ -174,19 +175,21 @@ export const validateOperationIsPermittedOrThrow = ({ } }; -export const validateQueryIsPermittedOrThrow = ({ - expressionMap, - objectRecordsPermissions, - objectMetadataMaps, - shouldBypassPermissionChecks, - isFieldPermissionsEnabled, -}: { +type ValidateQueryIsPermittedOrThrowArgs = { expressionMap: QueryExpressionMap; - objectRecordsPermissions: ObjectRecordsPermissions; + objectsPermissions: ObjectsPermissionsDeprecated; objectMetadataMaps: ObjectMetadataMaps; shouldBypassPermissionChecks: boolean; isFieldPermissionsEnabled?: boolean; -}) => { +}; + +export const validateQueryIsPermittedOrThrow = ({ + expressionMap, + objectsPermissions, + objectMetadataMaps, + shouldBypassPermissionChecks, + isFieldPermissionsEnabled, +}: ValidateQueryIsPermittedOrThrowArgs) => { if (shouldBypassPermissionChecks) { return; } @@ -232,7 +235,7 @@ export const validateQueryIsPermittedOrThrow = ({ validateOperationIsPermittedOrThrow({ entityName: mainEntity, operationType: operationType as OperationType, - objectRecordsPermissions, + objectsPermissions, objectMetadataMaps, selectedColumns, isFieldPermissionsEnabled, @@ -247,7 +250,7 @@ const validateReadFieldPermissionOrThrow = ({ columnNameToFieldMetadataIdMap, allFieldsSelected, }: { - restrictedFields: RestrictedFields; + restrictedFields: RestrictedFieldsPermissions; selectedColumns: string[] | '*'; columnNameToFieldMetadataIdMap: Record; allFieldsSelected?: boolean; @@ -290,7 +293,7 @@ const validateUpdateFieldPermissionOrThrow = ({ updatedColumns, columnNameToFieldMetadataIdMap, }: { - restrictedFields: RestrictedFields; + restrictedFields: RestrictedFieldsPermissions; updatedColumns: string[]; columnNameToFieldMetadataIdMap: Record; }) => { diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts index 67dbe467ad..b45cc567f0 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { DeleteQueryBuilder, DeleteResult, @@ -29,14 +29,14 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceDeleteQueryBuilder< T extends ObjectLiteral, > extends DeleteQueryBuilder { - private objectRecordsPermissions: ObjectRecordsPermissions; + private objectRecordsPermissions: ObjectsPermissionsDeprecated; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; private featureFlagMap?: FeatureFlagMap; constructor( queryBuilder: DeleteQueryBuilder, - objectRecordsPermissions: ObjectRecordsPermissions, + objectRecordsPermissions: ObjectsPermissionsDeprecated, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, @@ -66,7 +66,7 @@ export class WorkspaceDeleteQueryBuilder< try { validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, - objectRecordsPermissions: this.objectRecordsPermissions, + objectsPermissions: this.objectRecordsPermissions, objectMetadataMaps: this.internalContext.objectMetadataMaps, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, isFieldPermissionsEnabled: diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts index 6791a04aac..042cd29f1d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { EntityTarget, @@ -35,7 +35,7 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceInsertQueryBuilder< T extends ObjectLiteral, > extends InsertQueryBuilder { - private objectRecordsPermissions: ObjectRecordsPermissions; + private objectRecordsPermissions: ObjectsPermissionsDeprecated; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; @@ -47,7 +47,7 @@ export class WorkspaceInsertQueryBuilder< constructor( queryBuilder: InsertQueryBuilder, - objectRecordsPermissions: ObjectRecordsPermissions, + objectRecordsPermissions: ObjectsPermissionsDeprecated, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, @@ -104,7 +104,7 @@ export class WorkspaceInsertQueryBuilder< try { validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, - objectRecordsPermissions: this.objectRecordsPermissions, + objectsPermissions: this.objectRecordsPermissions, objectMetadataMaps: this.internalContext.objectMetadataMaps, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, isFieldPermissionsEnabled: diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts index aba23753b9..b5fd56dbee 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { EntityTarget, ObjectLiteral, SelectQueryBuilder } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; @@ -27,14 +27,14 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceSelectQueryBuilder< T extends ObjectLiteral, > extends SelectQueryBuilder { - objectRecordsPermissions: ObjectRecordsPermissions; + objectRecordsPermissions: ObjectsPermissionsDeprecated; shouldBypassPermissionChecks: boolean; internalContext: WorkspaceInternalContext; authContext?: AuthContext; featureFlagMap?: FeatureFlagMap; constructor( queryBuilder: SelectQueryBuilder, - objectRecordsPermissions: ObjectRecordsPermissions, + objectRecordsPermissions: ObjectsPermissionsDeprecated, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, @@ -322,7 +322,7 @@ export class WorkspaceSelectQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, - objectRecordsPermissions: this.objectRecordsPermissions, + objectsPermissions: this.objectRecordsPermissions, objectMetadataMaps: this.internalContext.objectMetadataMaps, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, isFieldPermissionsEnabled, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts index c3b4a0c83a..4b5b5f5cd3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { EntityTarget, InsertQueryBuilder, @@ -28,7 +28,7 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceSoftDeleteQueryBuilder< T extends ObjectLiteral, > extends SoftDeleteQueryBuilder { - private objectRecordsPermissions: ObjectRecordsPermissions; + private objectRecordsPermissions: ObjectsPermissionsDeprecated; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; @@ -36,7 +36,7 @@ export class WorkspaceSoftDeleteQueryBuilder< constructor( queryBuilder: SoftDeleteQueryBuilder, - objectRecordsPermissions: ObjectRecordsPermissions, + objectRecordsPermissions: ObjectsPermissionsDeprecated, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, @@ -66,7 +66,7 @@ export class WorkspaceSoftDeleteQueryBuilder< try { validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, - objectRecordsPermissions: this.objectRecordsPermissions, + objectsPermissions: this.objectRecordsPermissions, objectMetadataMaps: this.internalContext.objectMetadataMaps, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, isFieldPermissionsEnabled: diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index eecee5f874..78cb70f761 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { EntityTarget, @@ -34,7 +34,7 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceUpdateQueryBuilder< T extends ObjectLiteral, > extends UpdateQueryBuilder { - private objectRecordsPermissions: ObjectRecordsPermissions; + private objectRecordsPermissions: ObjectsPermissionsDeprecated; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; @@ -50,7 +50,7 @@ export class WorkspaceUpdateQueryBuilder< constructor( queryBuilder: UpdateQueryBuilder, - objectRecordsPermissions: ObjectRecordsPermissions, + objectRecordsPermissions: ObjectsPermissionsDeprecated, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, @@ -88,7 +88,7 @@ export class WorkspaceUpdateQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, - objectRecordsPermissions: this.objectRecordsPermissions, + objectsPermissions: this.objectRecordsPermissions, objectMetadataMaps: this.internalContext.objectMetadataMaps, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, isFieldPermissionsEnabled: @@ -194,7 +194,7 @@ export class WorkspaceUpdateQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: fakeExpressionMapToValidatePermissions, - objectRecordsPermissions: this.objectRecordsPermissions, + objectsPermissions: this.objectRecordsPermissions, objectMetadataMaps: this.internalContext.objectMetadataMaps, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, isFieldPermissionsEnabled: diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 78b6f50569..11992c381c 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -1,4 +1,4 @@ -import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { ObjectsPermissionsDeprecated } from 'twenty-shared/types'; import { DeepPartial, DeleteResult, @@ -40,7 +40,7 @@ export class WorkspaceRepository< private readonly internalContext: WorkspaceInternalContext; private shouldBypassPermissionChecks: boolean; private featureFlagMap: FeatureFlagMap; - public readonly objectRecordsPermissions?: ObjectRecordsPermissions; + public readonly objectRecordsPermissions?: ObjectsPermissionsDeprecated; private authContext?: AuthContext; declare manager: WorkspaceEntityManager; @@ -50,7 +50,7 @@ export class WorkspaceRepository< manager: WorkspaceEntityManager, featureFlagMap: FeatureFlagMap, queryRunner?: QueryRunner, - objectRecordsPermissions?: ObjectRecordsPermissions, + objectRecordsPermissions?: ObjectsPermissionsDeprecated, shouldBypassPermissionChecks = false, authContext?: AuthContext, ) { diff --git a/packages/twenty-shared/src/types/ObjectPermissions.ts b/packages/twenty-shared/src/types/ObjectPermissions.ts new file mode 100644 index 0000000000..9c97ad3e61 --- /dev/null +++ b/packages/twenty-shared/src/types/ObjectPermissions.ts @@ -0,0 +1,9 @@ +import { RestrictedFieldsPermissions } from './RestrictedFieldsPermissions'; + +export type ObjectPermissions = { + canReadObjectRecords: boolean; + canUpdateObjectRecords: boolean; + canSoftDeleteObjectRecords: boolean; + canDestroyObjectRecords: boolean; + restrictedFields: RestrictedFieldsPermissions; +}; diff --git a/packages/twenty-shared/src/types/ObjectPermissionsDeprecated.ts b/packages/twenty-shared/src/types/ObjectPermissionsDeprecated.ts new file mode 100644 index 0000000000..525c464ac2 --- /dev/null +++ b/packages/twenty-shared/src/types/ObjectPermissionsDeprecated.ts @@ -0,0 +1,10 @@ +import { RestrictedFieldsPermissions } from './RestrictedFieldsPermissions'; + +// TODO: DEPRECATE THIS +export type ObjectPermissionsDeprecated = { + canRead: boolean; + canUpdate: boolean; + canSoftDelete: boolean; + canDestroy: boolean; + restrictedFields: RestrictedFieldsPermissions; +}; diff --git a/packages/twenty-shared/src/types/ObjectRecordsPermissions.ts b/packages/twenty-shared/src/types/ObjectRecordsPermissions.ts deleted file mode 100644 index 85ebdc5516..0000000000 --- a/packages/twenty-shared/src/types/ObjectRecordsPermissions.ts +++ /dev/null @@ -1,14 +0,0 @@ -type ObjectMetadataId = string; -export type ObjectRecordsPermissions = Record< - ObjectMetadataId, - { - canRead: boolean; - canUpdate: boolean; - canSoftDelete: boolean; - canDestroy: boolean; - restrictedFields: Record< - string, - { canRead?: boolean | null; canUpdate?: boolean | null } - >; - } ->; diff --git a/packages/twenty-shared/src/types/ObjectRecordsPermissionsByRoleId.ts b/packages/twenty-shared/src/types/ObjectRecordsPermissionsByRoleId.ts deleted file mode 100644 index 0d89f5dc07..0000000000 --- a/packages/twenty-shared/src/types/ObjectRecordsPermissionsByRoleId.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ObjectRecordsPermissions } from './ObjectRecordsPermissions'; - -type RoleId = string; -export type ObjectRecordsPermissionsByRoleId = Record< - RoleId, - ObjectRecordsPermissions ->; diff --git a/packages/twenty-shared/src/types/ObjectsPermissions.ts b/packages/twenty-shared/src/types/ObjectsPermissions.ts new file mode 100644 index 0000000000..20d3190cb1 --- /dev/null +++ b/packages/twenty-shared/src/types/ObjectsPermissions.ts @@ -0,0 +1,5 @@ +import { ObjectPermissions } from './ObjectPermissions'; + +type ObjectMetadataId = string; + +export type ObjectsPermissions = Record; diff --git a/packages/twenty-shared/src/types/ObjectsPermissionsByRoleId.ts b/packages/twenty-shared/src/types/ObjectsPermissionsByRoleId.ts new file mode 100644 index 0000000000..683eccbb5c --- /dev/null +++ b/packages/twenty-shared/src/types/ObjectsPermissionsByRoleId.ts @@ -0,0 +1,5 @@ +import { ObjectsPermissions } from './ObjectsPermissions'; + +type RoleId = string; + +export type ObjectsPermissionsByRoleId = Record; diff --git a/packages/twenty-shared/src/types/ObjectsPermissionsByRoleIdDeprecated.ts b/packages/twenty-shared/src/types/ObjectsPermissionsByRoleIdDeprecated.ts new file mode 100644 index 0000000000..b87cdbf6c0 --- /dev/null +++ b/packages/twenty-shared/src/types/ObjectsPermissionsByRoleIdDeprecated.ts @@ -0,0 +1,9 @@ +import { ObjectsPermissionsDeprecated } from './ObjectsPermissionsDeprecated'; + +type RoleId = string; + +// TODO: DEPRECATE THIS +export type ObjectsPermissionsByRoleIdDeprecated = Record< + RoleId, + ObjectsPermissionsDeprecated +>; diff --git a/packages/twenty-shared/src/types/ObjectsPermissionsDeprecated.ts b/packages/twenty-shared/src/types/ObjectsPermissionsDeprecated.ts new file mode 100644 index 0000000000..962b3ff935 --- /dev/null +++ b/packages/twenty-shared/src/types/ObjectsPermissionsDeprecated.ts @@ -0,0 +1,9 @@ +import { ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated'; + +type ObjectMetadataId = string; + +// TODO: DEPRECATE THIS +export type ObjectsPermissionsDeprecated = Record< + ObjectMetadataId, + ObjectPermissionsDeprecated +>; diff --git a/packages/twenty-shared/src/types/RestrictedField.ts b/packages/twenty-shared/src/types/RestrictedFieldPermissions.ts similarity index 58% rename from packages/twenty-shared/src/types/RestrictedField.ts rename to packages/twenty-shared/src/types/RestrictedFieldPermissions.ts index 4d816f53a1..cb4f01b009 100644 --- a/packages/twenty-shared/src/types/RestrictedField.ts +++ b/packages/twenty-shared/src/types/RestrictedFieldPermissions.ts @@ -1,4 +1,4 @@ -export type RestrictedField = { +export type RestrictedFieldPermissions = { canRead?: boolean | null; canUpdate?: boolean | null; }; diff --git a/packages/twenty-shared/src/types/RestrictedFields.ts b/packages/twenty-shared/src/types/RestrictedFields.ts deleted file mode 100644 index 12fdadd9e6..0000000000 --- a/packages/twenty-shared/src/types/RestrictedFields.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { RestrictedField } from './RestrictedField'; - -export type RestrictedFields = Record; diff --git a/packages/twenty-shared/src/types/RestrictedFieldsPermissions.ts b/packages/twenty-shared/src/types/RestrictedFieldsPermissions.ts new file mode 100644 index 0000000000..34ad15499e --- /dev/null +++ b/packages/twenty-shared/src/types/RestrictedFieldsPermissions.ts @@ -0,0 +1,6 @@ +import { RestrictedFieldPermissions } from './RestrictedFieldPermissions'; + +export type RestrictedFieldsPermissions = Record< + string, + RestrictedFieldPermissions +>; diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index be30724edc..29ac2191e7 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -17,11 +17,15 @@ export type { FromTo } from './FromToType'; export type { IsExactly } from './IsExactly'; export type { NonNullableRequired } from './NonNullableRequired'; export type { NullablePartial } from './NullablePartial'; -export type { ObjectRecordsPermissions } from './ObjectRecordsPermissions'; -export type { ObjectRecordsPermissionsByRoleId } from './ObjectRecordsPermissionsByRoleId'; +export type { ObjectPermissions } from './ObjectPermissions'; +export type { ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated'; +export type { ObjectsPermissions } from './ObjectsPermissions'; +export type { ObjectsPermissionsByRoleId } from './ObjectsPermissionsByRoleId'; +export type { ObjectsPermissionsByRoleIdDeprecated } from './ObjectsPermissionsByRoleIdDeprecated'; +export type { ObjectsPermissionsDeprecated } from './ObjectsPermissionsDeprecated'; export type { RelationAndMorphRelationFieldMetadataType } from './RelationAndMorphRelationFieldMetadataType'; -export type { RestrictedField } from './RestrictedField'; -export type { RestrictedFields } from './RestrictedFields'; +export type { RestrictedFieldPermissions } from './RestrictedFieldPermissions'; +export type { RestrictedFieldsPermissions } from './RestrictedFieldsPermissions'; export type { StepFilterGroup, StepFilter } from './StepFilters'; export { StepLogicalOperator } from './StepFilters'; export { TwoFactorAuthenticationStrategy } from './TwoFactorAuthenticationStrategy';