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