[permissions] QA fixes (#13698)

In this PR

- Introduction of readableFields and updatableFields in
objectMetadataItem selector to ease filtering from a developer
experience perspective ( + to help developers think to do it). In
discussion @lucasbordeau @charlesBochet
- Remove non-updatable field from CSV import process (@etiennejouan)
- QA fix / Non-readable fields should not show on show page
- QA fix / It should not be offered to create a kanban view on a
non-readable field
- QA fix / It should not be offered to create view groups on a
non-readable field
- QA fix / Rating field should have a readonly mode

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2025-08-07 18:21:59 +02:00
committed by GitHub
parent 258f21a4be
commit ee19ee5119
120 changed files with 1211 additions and 943 deletions
+1
View File
@@ -15,6 +15,7 @@ module.exports = {
'vite.config.ts',
'setupTests.ts',
'**/__mocks__/**',
'src/testing/mock-data/**',
],
overrides: [
{
@@ -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(
@@ -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' &&
@@ -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;
@@ -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(
@@ -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) => {
@@ -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<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
};
export const triggerCreateRecordsOptimisticEffect = ({
cache,
@@ -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(),
})),
@@ -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,
@@ -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');
@@ -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<ObjectPermissions & { objectMetadataId: string }>;
};
export const currentUserWorkspaceState =
createState<CurrentUserWorkspace | null>({
@@ -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;
})
@@ -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(
@@ -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;
}
@@ -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';
@@ -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';
@@ -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,
@@ -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,
@@ -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,
@@ -9,7 +9,7 @@ export const useActiveFieldMetadataItems = ({
const activeFieldMetadataItems = useMemo(
() =>
objectMetadataItem
? objectMetadataItem.fields.filter(
? objectMetadataItem.readableFields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
)
: [],
@@ -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<FieldMetadata>[] =
activeFieldMetadataItems
@@ -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 = () => {
@@ -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 = () => {
@@ -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';
@@ -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<
@@ -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,
@@ -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<ObjectMetadataItemsQuery>({
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,
};
};
@@ -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<ObjectMetadataItemsQuery>({
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,
};
};
@@ -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';
@@ -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 = () => {
@@ -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;
},
});
@@ -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;
},
@@ -15,6 +15,8 @@ export type ObjectMetadataItem = Omit<
> & {
__typename?: string;
fields: FieldMetadataItem[];
readableFields: FieldMetadataItem[];
updatableFields: FieldMetadataItem[];
labelIdentifierFieldMetadataId: string;
indexMetadatas: IndexMetadataItem[];
};
@@ -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: {},
},
},
});
@@ -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;
};
@@ -1,9 +1,9 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActiveFieldMetadataItems = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
objectMetadataItem: Pick<ObjectMetadataItem, 'readableFields'>,
) =>
objectMetadataItem.fields.filter(
objectMetadataItem.readableFields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
);
@@ -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);
};
@@ -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);
};
@@ -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<string, ObjectPermission>,
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 {
@@ -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);
};
@@ -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<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
// TODO: change ObjectMetadataItems mock before refactoring with relation computed field
@@ -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<string, ObjectPermission>;
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,
@@ -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;
@@ -1,11 +1,11 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const mapSoftDeleteFieldsToGraphQLQuery = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
objectMetadataItem: Pick<ObjectMetadataItem, 'readableFields'>,
): string => {
const softDeleteFields = ['deletedAt', 'id'];
const fieldsThatShouldBeQueried = objectMetadataItem.fields
const fieldsThatShouldBeQueried = objectMetadataItem.readableFields
.filter((field) => field.isActive && softDeleteFields.includes(field.name))
.sort(
(a, b) =>
@@ -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(),
@@ -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<object>;
@@ -17,7 +17,10 @@ export type GetRecordFromCacheArgs = {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: ObjectMetadataItem;
recordGqlFields?: RecordGqlFields;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
@@ -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 = <T extends ObjectRecord>({
@@ -24,7 +24,10 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
cache: ApolloCache<object>;
recordGqlFields: Record<string, boolean>;
record: T;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
}) => {
if (isUndefinedOrNull(objectMetadataItem)) {
@@ -8,14 +8,17 @@ export type GenerateDepthOneRecordGqlFields = {
export const generateDepthOneRecordGqlFields = ({
objectMetadataItem,
}: GenerateDepthOneRecordGqlFields) =>
objectMetadataItem.fields.reduce<Record<string, true>>((acc, field) => {
return {
...acc,
...(isDefined(field.settings?.joinColumnName)
? {
[field.settings.joinColumnName]: true,
}
: {}),
[field.name]: true,
};
}, {});
objectMetadataItem.readableFields.reduce<Record<string, true>>(
(acc, field) => {
return {
...acc,
...(isDefined(field.settings?.joinColumnName)
? {
[field.settings.joinColumnName]: true,
}
: {}),
[field.name]: true,
};
},
{},
);
@@ -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', () => {
@@ -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];
@@ -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;
});
@@ -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;
});
@@ -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<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
};
export const useObjectPermissions = (): useObjectPermissionsReturnType => {
@@ -19,7 +22,10 @@ export const useObjectPermissions = (): useObjectPermissionsReturnType => {
}
const objectPermissionsByObjectMetadataId = objectPermissions?.reduce(
(acc: Record<string, ObjectPermission>, objectPermission) => {
(
acc: Record<string, ObjectPermissions & { objectMetadataId: string }>,
objectPermission,
) => {
acc[objectPermission.objectMetadataId] = objectPermission;
return acc;
},
@@ -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(() => {
@@ -16,7 +16,7 @@ export const useOptionsForSelect = (fieldMetadataId: string) => {
objectNameSingular,
});
const fieldMetadataItem = objectMetadataItem.fields.find(
const fieldMetadataItem = objectMetadataItem.readableFields.find(
(field) => field.id === fieldMetadataId,
);
@@ -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<FieldMetadata>[],
);
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,
},
]);
@@ -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,
},
};
@@ -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,
@@ -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',
@@ -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(
@@ -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(),
@@ -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) ? (
<BooleanFieldDisplay />
) : isFieldRating(fieldDefinition) ? (
<RatingFieldDisplay />
<RatingFieldDisplay readonly={isRecordFieldReadOnly} />
) : isFieldRichText(fieldDefinition) ? (
<RichTextFieldDisplay />
) : isFieldRichTextV2(fieldDefinition) ? (
@@ -139,7 +139,7 @@ export const FieldInput = ({
) : isFieldBoolean(fieldDefinition) ? (
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
) : isFieldRating(fieldDefinition) ? (
<RatingFieldInput onSubmit={onSubmit} />
<RatingFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
) : isFieldSelect(fieldDefinition) ? (
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
) : isFieldMultiSelect(fieldDefinition) ? (
@@ -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 <RatingInput value={rating} readonly />;
return <RatingInput value={rating} readonly={readonly} />;
};
@@ -138,6 +138,8 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'test',
namePlural: 'tests',
fields: [],
readableFields: [],
updatableFields: [],
};
const mockObjectMetadataItemWithAllFields: ObjectMetadataItem = {
@@ -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<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
recordIndexId: string;
};
@@ -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 [
@@ -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,
@@ -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 &&
@@ -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 };
@@ -44,7 +44,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
) => {
const availableFieldMetadataItemsToImport =
spreadsheetImportFilterAvailableFieldMetadataItems(
objectMetadataItem.fields,
objectMetadataItem.updatableFields,
);
const spreadsheetImportFields = buildSpreadsheetImportFields(
@@ -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,
@@ -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<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
}) => gql`
query FindMany${capitalize(
@@ -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: {},
}
);
};
@@ -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);
}
@@ -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);
@@ -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,
@@ -12,7 +12,7 @@ export const useDownloadFakeRecords = () => {
const availableFieldMetadataItems =
spreadsheetImportFilterAvailableFieldMetadataItems(
objectMetadataItem.fields,
objectMetadataItem.updatableFields,
);
const buildTableWithFakeRecords = () => {
@@ -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 {
@@ -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}
`;
@@ -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,
};
};
@@ -26,7 +26,7 @@ export const useGetAvailableFieldsForKanban = () => {
);
const availableFieldsForKanban =
objectMetadataItem?.fields.filter(
objectMetadataItem?.readableFields.filter(
(field) => field.type === FieldMetadataType.SELECT,
) ?? [];
@@ -16,6 +16,48 @@ const meta: Meta<typeof WorkflowFieldsMultiSelect> = {
export default meta;
type Story = StoryObj<typeof WorkflowFieldsMultiSelect>;
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 = {
@@ -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,
@@ -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';
@@ -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,
}),
}}
>
<RecordTableBodyContextProvider
@@ -185,9 +185,26 @@ export const mockedOnboardingUserData = (
supportUserHash:
'4fb61d34ed3a4aeda2476d4b308b5162db9e1809b2b8277e6fdc6efc4a609254',
workspaceMember: null,
workspaceMembers: [],
currentWorkspace: mockCurrentWorkspace,
currentUserWorkspace: {
permissionFlags: [PermissionFlagType.WORKSPACE_MEMBERS],
objectPermissions: generatedMockObjectMetadataItems.map((item) => ({
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: [],
},
};
};
@@ -16,6 +16,8 @@ export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
return {
...objectWithoutFieldsList,
fields: fieldsList,
readableFields: fieldsList,
updatableFields: fieldsList,
labelIdentifierFieldMetadataId,
indexMetadatas: indexMetadataList.map((index) => ({
...index,
@@ -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<ObjectRecordFilter>;
restrictedFields: RestrictedFields;
restrictedFields: RestrictedFieldsPermissions;
}) {
const objectMetadataNameSingular =
objectMetadata.objectMetadataMapItem.nameSingular;
@@ -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);
@@ -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;
}
@@ -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,
@@ -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 (
@@ -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<PermissionFlagType, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
objectPermissions: ObjectRecordsPermissions;
objectPermissions: ObjectsPermissionsDeprecated;
};
@@ -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<ObjectRecordsPermissionsByRoleId>(
this.cacheStorageService.set<ObjectsPermissionsByRoleIdDeprecated>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
permissions,
TTL_INFINITE,
@@ -52,8 +52,8 @@ export class WorkspacePermissionsCacheStorageService {
getRolesPermissions(
workspaceId: string,
): Promise<ObjectRecordsPermissionsByRoleId | undefined> {
return this.cacheStorageService.get<ObjectRecordsPermissionsByRoleId>(
): Promise<ObjectsPermissionsByRoleIdDeprecated | undefined> {
return this.cacheStorageService.get<ObjectsPermissionsByRoleIdDeprecated>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
);
}
@@ -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<void> {
let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined;
let currentRolesPermissions:
| ObjectsPermissionsByRoleIdDeprecated
| undefined;
if (roleIds) {
currentRolesPermissions =
@@ -103,8 +105,11 @@ export class WorkspacePermissionsCacheService {
workspaceId,
}: {
workspaceId: string;
}): Promise<CacheResult<string, ObjectRecordsPermissionsByRoleId>> {
return getFromCacheWithRecompute<string, ObjectRecordsPermissionsByRoleId>({
}): Promise<CacheResult<string, ObjectsPermissionsByRoleIdDeprecated>> {
return getFromCacheWithRecompute<
string,
ObjectsPermissionsByRoleIdDeprecated
>({
workspaceId,
getCacheData: () =>
this.workspacePermissionsCacheStorageService.getRolesPermissions(
@@ -166,7 +171,7 @@ export class WorkspacePermissionsCacheService {
}: {
workspaceId: string;
roleIds?: string[];
}): Promise<ObjectRecordsPermissionsByRoleId> {
}): Promise<ObjectsPermissionsByRoleIdDeprecated> {
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 &&
@@ -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;
}
@@ -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: [],
@@ -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<Entity>,
permissionOptions?: {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectRecordsPermissions;
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
},
selectedColumns: string[] | '*' = '*',
): Promise<InsertResult> {
@@ -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,
@@ -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<CacheResult<string, ObjectRecordsPermissionsByRoleId>> {
return getFromCacheWithRecompute<string, ObjectRecordsPermissionsByRoleId>({
}): Promise<CacheResult<string, ObjectsPermissionsByRoleIdDeprecated>> {
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<void> {
this.updateWorkspaceDataSourceIfNeeded({
workspaceDataSource,

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