[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:
+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(),
|
||||
|
||||
Reference in New Issue
Block a user