Add all object level read-only behavior (#10356)

Fixes https://github.com/twentyhq/core-team-issues/issues/427

---------

Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
This commit is contained in:
Weiko
2025-02-21 09:59:47 +01:00
committed by GitHub
parent c46f7848b7
commit e301c7856b
27 changed files with 252 additions and 83 deletions

View File

@ -11,6 +11,7 @@ import { useColumnNewCardActions } from '@/object-record/record-board/record-boa
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui';
@ -97,6 +98,8 @@ export const RecordBoardColumnHeader = () => {
columnDefinition.id ?? '',
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { isOpportunitiesCompanyFieldDisabled } =
useIsOpportunitiesCompanyFieldDisabled();
@ -146,12 +149,13 @@ export const RecordBoardColumnHeader = () => {
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
/>
<LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={() => handleNewButtonClick('first', isOpportunity)}
/>
{!hasObjectReadOnlyPermission && (
<LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={() => handleNewButtonClick('first', isOpportunity)}
/>
)}
</StyledHeaderActions>
)}
</StyledRightContainer>

View File

@ -1,6 +1,7 @@
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilValue } from 'recoil';
export const RecordBoardColumnNewRecord = ({
@ -16,8 +17,15 @@ export const RecordBoardColumnNewRecord = ({
scopeId: columnId,
}),
);
const { handleCreateSuccess } = useAddNewCard();
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<>
{newRecord.isCreating && newRecord.position === position && (

View File

@ -1,4 +1,5 @@
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconPlus } from 'twenty-ui';
@ -29,6 +30,12 @@ export const RecordBoardColumnNewRecordButton = ({
const { handleNewButtonClick } = useColumnNewCardActions(columnId);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<StyledNewButton onClick={() => handleNewButtonClick('last', false)}>
<IconPlus size={theme.icon.size.md} />

View File

@ -3,6 +3,7 @@ import { useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilValue } from 'recoil';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
@ -20,11 +21,14 @@ export const useIsFieldValueReadOnly = () => {
objectNameSingular: metadata.objectMetadataNameSingular ?? '',
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
return isFieldValueReadOnly({
objectNameSingular: metadata.objectMetadataNameSingular,
fieldName: metadata.fieldName,
fieldType: type,
isObjectRemote: objectMetadataItem.isRemote,
isRecordDeleted: recordFromStore?.deletedAt,
hasObjectReadOnlyPermission,
});
};

View File

@ -12,6 +12,7 @@ type isFieldValueReadOnlyParams = {
fieldType?: FieldMetadataType;
isObjectRemote?: boolean;
isRecordDeleted?: boolean;
hasObjectReadOnlyPermission?: boolean;
};
export const isFieldValueReadOnly = ({
@ -20,6 +21,7 @@ export const isFieldValueReadOnly = ({
fieldType,
isObjectRemote = false,
isRecordDeleted = false,
hasObjectReadOnlyPermission = false,
}: isFieldValueReadOnlyParams) => {
if (fieldName === 'noteTargets' || fieldName === 'taskTargets') {
return true;
@ -33,6 +35,10 @@ export const isFieldValueReadOnly = ({
return true;
}
if (hasObjectReadOnlyPermission) {
return true;
}
if (isWorkflowSubObjectMetadata(objectNameSingular)) {
return true;
}

View File

@ -4,14 +4,15 @@ import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/use
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHasSettingsPermission } from '@/settings/roles/hooks/useHasSettingsPermission';
import { SettingsPath } from '@/types/SettingsPath';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useCallback, useContext, useMemo } from 'react';
import { useCallback, useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { isDefined, SettingsFeatures } from 'twenty-shared';
import { IconEyeOff, IconSettings } from 'twenty-ui';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@ -69,37 +70,36 @@ export const useRecordGroupActions = ({
recordGroupFieldMetadata,
]);
const recordGroupActions: RecordGroupAction[] = useMemo(
() =>
[
{
id: 'edit',
label: 'Edit',
icon: IconSettings,
position: 0,
callback: () => {
navigateToSelectSettings();
},
},
{
id: 'hide',
label: 'Hide',
icon: IconEyeOff,
position: 1,
callback: () => {
handleRecordGroupVisibilityChange({
...recordGroupDefinition,
isVisible: false,
});
},
},
].filter(isDefined),
[
handleRecordGroupVisibilityChange,
navigateToSelectSettings,
recordGroupDefinition,
],
const hasAccessToDataModelSettings = useHasSettingsPermission(
SettingsFeatures.DATA_MODEL,
);
const recordGroupActions: RecordGroupAction[] = [];
if (hasAccessToDataModelSettings) {
recordGroupActions.push({
id: 'edit',
label: 'Edit',
icon: IconSettings,
position: 0,
callback: () => {
navigateToSelectSettings();
},
});
}
recordGroupActions.push({
id: 'hide',
label: 'Hide',
icon: IconEyeOff,
position: 1,
callback: () => {
handleRecordGroupVisibilityChange({
...recordGroupDefinition,
isVisible: false,
});
},
});
return recordGroupActions;
};

View File

@ -4,9 +4,11 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { RecordTableEmptyStateByGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateReadOnly } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateReadOnly';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableEmptyState = () => {
@ -17,6 +19,8 @@ export const RecordTableEmptyState = () => {
hasRecordGroupsComponentSelector,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;
@ -27,6 +31,10 @@ export const RecordTableEmptyState = () => {
recordTableId,
);
if (hasObjectReadOnlyPermission) {
return <RecordTableEmptyStateReadOnly />;
}
if (isRemote) {
return <RecordTableEmptyStateRemote />;
} else if (isSoftDeleteActive === true) {

View File

@ -19,6 +19,7 @@ type RecordTableEmptyStateDisplayButtonProps = {
ButtonIcon: IconComponent;
buttonTitle: string;
onClick: () => void;
buttonIsDisabled?: boolean;
};
type RecordTableEmptyStateDisplayProps = {
@ -54,6 +55,7 @@ export const RecordTableEmptyStateDisplay = (
title={props.buttonTitle}
variant={'secondary'}
onClick={props.onClick}
disabled={props.buttonIsDisabled}
/>
)}
</AnimatedPlaceholderEmptyContainer>

View File

@ -0,0 +1,24 @@
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { t } from '@lingui/core/macro';
import { IconPlus } from 'twenty-ui';
export const RecordTableEmptyStateReadOnly = () => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const objectLabel = useObjectLabel(objectMetadataItem);
const buttonTitle = `Add a ${objectLabel}`;
return (
<RecordTableEmptyStateDisplay
title={t`No records found`}
subTitle={t`You are not allowed to create records in this object`}
animatedPlaceholderType="noRecord"
buttonTitle={buttonTitle}
ButtonIcon={IconPlus}
buttonIsDisabled={true}
/>
);
};

View File

@ -13,6 +13,7 @@ import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@ -212,6 +213,8 @@ export const RecordTableHeaderCell = ({
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
return (
<StyledColumnHeaderCell
key={column.fieldMetadataId}
@ -229,7 +232,8 @@ export const RecordTableHeaderCell = ({
<RecordTableColumnHeadWithDropdown column={column} />
{(useIsMobile() || iconVisibility) &&
!!column.isLabelIdentifier &&
!isReadOnly && (
!isReadOnly &&
!hasObjectReadOnlyPermission && (
<StyledHeaderIcon>
<LightIconButton
Icon={IconPlus}

View File

@ -3,6 +3,7 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconPlus } from 'twenty-ui';
@ -15,6 +16,8 @@ export const RecordTableRecordGroupSectionAddNew = () => {
recordIndexAllRecordIdsComponentSelector,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewTableRecordInGroup } = useCreateNewTableRecord({
objectMetadataItem,
recordTableId,
@ -24,6 +27,10 @@ export const RecordTableRecordGroupSectionAddNew = () => {
createNewTableRecordInGroup(currentRecordGroupId);
};
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<RecordTableActionRow
draggableId={`add-new-record-${currentRecordGroupId}`}

View File

@ -4,6 +4,7 @@ import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
@ -75,6 +76,8 @@ export const MultiRecordSelect = ({
instanceId,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
useEffect(() => {
setHotkeyScope(instanceId);
}, [setHotkeyScope, instanceId]);
@ -144,7 +147,7 @@ export const MultiRecordSelect = ({
<DropdownMenu ref={containerRef} data-select-disable width={200}>
{dropdownPlacement?.includes('end') && (
<>
{isDefined(onCreate) && (
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
</DropdownMenuItemsContainer>

View File

@ -5,6 +5,7 @@ import {
import { useRecordPickerRecordsOptions } from '@/object-record/relation-picker/hooks/useRecordPickerRecordsOptions';
import { useRecordSelectSearch } from '@/object-record/relation-picker/hooks/useRecordSelectSearch';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@ -48,6 +49,8 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
RecordPickerComponentInstanceContext,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { records, recordPickerSearchFilter } = useRecordPickerRecordsOptions({
objectNameSingular,
selectedRecordIds,
@ -69,9 +72,11 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
<>
{dropdownPlacement?.includes('end') && (
<>
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
</DropdownMenuItemsContainer>
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
</DropdownMenuItemsContainer>
)}
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
{shouldDisplayDropdownMenuItems && (
<SingleRecordSelectMenuItems
@ -120,7 +125,7 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
<DropdownMenuSeparator />
)}
{isDefined(onCreate) && (
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
</DropdownMenuItemsContainer>