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:
@ -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>
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
|
||||
@ -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}`}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user