Fix readonly mode with permissions v2 for tables (#12617)

isReadonly was not set anymore, this PR put it back with the new
permission check
Also fix missing readonly mode for title cell
This commit is contained in:
Weiko
2025-06-17 16:03:50 +02:00
committed by GitHub
parent 8f07f681d2
commit c79daced48
11 changed files with 50 additions and 100 deletions

View File

@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
FieldContext,
@ -33,6 +34,10 @@ export const FieldContextProvider = ({
objectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const fieldMetadataItem = objectMetadataItem?.fields.find(
(field) => field.name === fieldMetadataName,
);
@ -56,6 +61,8 @@ export const FieldContextProvider = ({
return null;
}
const isObjectReadOnly = !objectPermissions.canUpdateObjectRecords;
return (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
@ -73,7 +80,7 @@ export const FieldContextProvider = ({
customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation,
clearable,
overridenIsFieldEmpty,
isReadOnly: false,
isReadOnly: isObjectReadOnly,
}}
>
{children}

View File

@ -308,7 +308,7 @@ export const RecordDetailRelationRecordsListItem = ({
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isReadOnly: false,
isReadOnly: isFieldReadOnly,
}}
>
<RecordFieldComponentInstanceContext.Provider

View File

@ -5,8 +5,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
import { RecordDetailRelationSectionDropdown } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown';
@ -117,17 +115,7 @@ export const RecordDetailRelationSection = ({
},
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
objectMetadataId: relationObjectMetadataItem.id,
});
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition,
isRecordReadOnly,
});
if (loading || isFieldReadOnly) return null;
if (loading) return null;
const relationRecordsCount = relationAggregateResult?.id?.COUNT ?? 0;

View File

@ -44,10 +44,16 @@ export const RecordDetailRelationSectionDropdown = ({
relationFieldMetadataId,
relationObjectMetadataNameSingular,
relationType,
objectMetadataNameSingular,
} = fieldDefinition.metadata as FieldRelationMetadata;
const record = useRecoilValue(recordStoreFamilyState(recordId));
const { objectMetadataItem: recordObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: objectMetadataNameSingular ?? '',
});
const { objectMetadataItem: relationObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: relationObjectMetadataNameSingular,
@ -148,7 +154,9 @@ export const RecordDetailRelationSectionDropdown = ({
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
objectMetadataId: relationObjectMetadataItem.id,
objectMetadataId: isToOneObject
? recordObjectMetadataItem.id
: relationObjectMetadataItem.id,
});
const isFieldReadOnly = useIsFieldValueReadOnly({

View File

@ -1,5 +1,6 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
@ -27,6 +28,10 @@ export const RecordTableCellPortalWrapper = ({
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
);
@ -36,6 +41,8 @@ export const RecordTableCellPortalWrapper = ({
return null;
}
const isReadOnly = !objectPermissions.canUpdateObjectRecords;
return ReactDOM.createPortal(
<RecordTableRowContextProvider
value={{
@ -48,6 +55,7 @@ export const RecordTableCellPortalWrapper = ({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isReadOnly,
}}
>
<RecordTableCellContext.Provider

View File

@ -1,4 +1,5 @@
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
@ -84,6 +85,9 @@ export const RecordTableTr = forwardRef<
RecordTableTrProps
>(({ children, recordId, focusIndex, isDragging = false, ...props }, ref) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const currentRowSelected = useRecoilComponentFamilyValueV2(
isRowSelectedComponentFamilyState,
recordId,
@ -121,6 +125,8 @@ export const RecordTableTr = forwardRef<
const isNextRowActiveOrFocused =
(isRowFocusActive && isNextRowFocused) || isNextRowActive;
const isReadOnly = !objectPermissions.canUpdateObjectRecords;
return (
<RecordTableRowContextProvider
value={{
@ -133,6 +139,7 @@ export const RecordTableTr = forwardRef<
objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected,
inView: isRowVisible,
isReadOnly,
}}
>
<StyledTr

View File

@ -32,7 +32,7 @@ export const RecordTitleCell = ({
sizeVariant,
containerType,
}: RecordTitleCellProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const { fieldDefinition, recordId, isReadOnly } = useContext(FieldContext);
const isFieldInputOnly = useIsFieldInputOnly();
@ -82,6 +82,7 @@ export const RecordTitleCell = ({
displayModeContent: <RecordTitleCellFieldDisplay />,
editModeContentOnly: isFieldInputOnly,
loading: loading,
isReadOnly,
};
return (

View File

@ -1,13 +1,18 @@
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { RecordTitleCellContext } from '@/object-record/record-title-cell/components/RecordTitleCellContext';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const RecordTitleCellContainer = () => {
const { displayModeContent, editModeContent } = useContext(
const { displayModeContent, editModeContent, isReadOnly } = useContext(
RecordTitleCellContext,
);
const { isInlineCellInEditMode } = useInlineCell();
if (isDefined(isReadOnly) && isReadOnly) {
return <>{displayModeContent}</>;
}
return <>{isInlineCellInEditMode ? editModeContent : displayModeContent}</>;
};

View File

@ -5,6 +5,7 @@ export type RecordTitleCellContextProps = {
editModeContentOnly?: boolean;
displayModeContent?: ReactElement;
loading?: boolean;
isReadOnly?: boolean;
};
const defaultRecordTitleCellContextProp: RecordTitleCellContextProps = {
@ -12,6 +13,7 @@ const defaultRecordTitleCellContextProp: RecordTitleCellContextProps = {
editModeContentOnly: false,
displayModeContent: undefined,
loading: false,
isReadOnly: false,
};
export const RecordTitleCellContext =

View File

@ -24,7 +24,7 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { AvailableWorkspaces } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
@ -46,9 +46,12 @@ import {
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthProvider } from 'src/engine/decorators/auth/auth-provider.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions';
@ -57,10 +60,6 @@ import { fromUserWorkspacePermissionsToUserWorkspacePermissionsDto } from 'src/e
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { AvailableWorkspaces } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { AuthProvider } from 'src/engine/decorators/auth/auth-provider.decorator';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -106,20 +105,7 @@ export class UserResolver {
return this.permissionsService.getDefaultUserWorkspacePermissions();
}
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspace.id,
);
if (!isPermissionsV2Enabled) {
return await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
});
}
return await this.permissionsService.getUserWorkspacePermissionsV2({
return await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
});

View File

@ -27,7 +27,7 @@ export class PermissionsService {
private readonly featureFlagService: FeatureFlagService,
) {}
public async getUserWorkspacePermissionsV2({
public async getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
}: {
@ -117,68 +117,6 @@ export class PermissionsService {
objectPermissions: {},
}) as const satisfies UserWorkspacePermissions;
public async getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
workspaceId: string;
}): Promise<UserWorkspacePermissions> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
let hasPermissionOnSettingFeature = false;
if (!isDefined(roleOfUserWorkspace)) {
throw new PermissionsException(
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
);
}
if (roleOfUserWorkspace.canUpdateAllSettings === true) {
hasPermissionOnSettingFeature = true;
}
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
const defaultSettingsPermissions =
this.getDefaultUserWorkspacePermissions().settingsPermissions;
const settingsPermissions = Object.keys(SettingPermissionType).reduce(
(acc, feature) => ({
...acc,
[feature]:
hasPermissionOnSettingFeature ||
settingPermissions.some(
(settingPermission) => settingPermission.setting === feature,
),
}),
defaultSettingsPermissions,
);
const objectRecordsPermissions: UserWorkspacePermissions['objectRecordsPermissions'] =
{
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canReadAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canUpdateAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canDestroyAllObjectRecords ?? false,
};
return {
settingsPermissions,
objectRecordsPermissions,
objectPermissions: {},
};
}
public async userHasWorkspaceSettingPermission({
userWorkspaceId,
workspaceId,