[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:
@@ -15,6 +15,7 @@ module.exports = {
|
||||
'vite.config.ts',
|
||||
'setupTests.ts',
|
||||
'**/__mocks__/**',
|
||||
'src/testing/mock-data/**',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
|
||||
+122
-107
@@ -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(
|
||||
|
||||
+1
-1
@@ -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' &&
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+16
@@ -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(
|
||||
|
||||
+2
-2
@@ -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) => {
|
||||
|
||||
+5
-2
@@ -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,
|
||||
|
||||
+5
-3
@@ -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>({
|
||||
|
||||
+5
-2
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
-91
@@ -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';
|
||||
|
||||
+13
@@ -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,
|
||||
|
||||
+13
@@ -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,
|
||||
|
||||
+13
-9
@@ -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,
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ export const useActiveFieldMetadataItems = ({
|
||||
const activeFieldMetadataItems = useMemo(
|
||||
() =>
|
||||
objectMetadataItem
|
||||
? objectMetadataItem.fields.filter(
|
||||
? objectMetadataItem.readableFields.filter(
|
||||
({ isActive, isSystem }) => isActive && !isSystem,
|
||||
)
|
||||
: [],
|
||||
|
||||
+2
-22
@@ -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
|
||||
|
||||
+1
-1
@@ -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 = () => {
|
||||
|
||||
+1
-1
@@ -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 = () => {
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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<
|
||||
|
||||
+13
-3
@@ -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,
|
||||
|
||||
-53
@@ -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,
|
||||
};
|
||||
};
|
||||
+89
@@ -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,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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 = () => {
|
||||
|
||||
+4
-25
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
+4
-27
@@ -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[];
|
||||
};
|
||||
|
||||
+2
@@ -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: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+57
@@ -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;
|
||||
};
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
|
||||
+15
@@ -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);
|
||||
};
|
||||
+15
@@ -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);
|
||||
};
|
||||
+11
-5
@@ -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 {
|
||||
|
||||
-21
@@ -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);
|
||||
};
|
||||
+6
-6
@@ -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
|
||||
|
||||
+9
-21
@@ -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,
|
||||
|
||||
+13
-5
@@ -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;
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
@@ -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(),
|
||||
|
||||
+5
-2
@@ -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>({
|
||||
|
||||
+5
-2
@@ -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)) {
|
||||
|
||||
+14
-11
@@ -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,
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
+39
-35
@@ -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];
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
},
|
||||
|
||||
+4
-1
@@ -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(() => {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export const useOptionsForSelect = (fieldMetadataId: string) => {
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
const fieldMetadataItem = objectMetadataItem.readableFields.find(
|
||||
(field) => field.id === fieldMetadataId,
|
||||
);
|
||||
|
||||
|
||||
+55
-14
@@ -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,
|
||||
},
|
||||
]);
|
||||
|
||||
+16
-10
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+15
-11
@@ -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',
|
||||
|
||||
+7
-6
@@ -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(
|
||||
|
||||
+21
-17
@@ -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(),
|
||||
|
||||
+7
-3
@@ -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) ? (
|
||||
|
||||
+1
-1
@@ -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) ? (
|
||||
|
||||
+2
-2
@@ -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} />;
|
||||
};
|
||||
|
||||
+2
@@ -138,6 +138,8 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'test',
|
||||
namePlural: 'tests',
|
||||
fields: [],
|
||||
readableFields: [],
|
||||
updatableFields: [],
|
||||
};
|
||||
|
||||
const mockObjectMetadataItemWithAllFields: ObjectMetadataItem = {
|
||||
|
||||
+5
-2
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+5
-2
@@ -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 [
|
||||
|
||||
+15
-10
@@ -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,
|
||||
|
||||
+5
-2
@@ -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 &&
|
||||
|
||||
+21
-33
@@ -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 };
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||
) => {
|
||||
const availableFieldMetadataItemsToImport =
|
||||
spreadsheetImportFilterAvailableFieldMetadataItems(
|
||||
objectMetadataItem.fields,
|
||||
objectMetadataItem.updatableFields,
|
||||
);
|
||||
|
||||
const spreadsheetImportFields = buildSpreadsheetImportFields(
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
+5
-2
@@ -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(
|
||||
|
||||
+24
@@ -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);
|
||||
}
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -26,7 +26,7 @@ export const useGetAvailableFieldsForKanban = () => {
|
||||
);
|
||||
|
||||
const availableFieldsForKanban =
|
||||
objectMetadataItem?.fields.filter(
|
||||
objectMetadataItem?.readableFields.filter(
|
||||
(field) => field.type === FieldMetadataType.SELECT,
|
||||
) ?? [];
|
||||
|
||||
|
||||
+45
-41
@@ -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,
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 (
|
||||
|
||||
+2
-2
@@ -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;
|
||||
};
|
||||
|
||||
+5
-5
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
+15
-10
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+4
-6
@@ -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: [],
|
||||
|
||||
+6
-7
@@ -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,
|
||||
|
||||
+7
-4
@@ -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
Reference in New Issue
Block a user