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 { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRec
import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useState } from 'react';
@ -21,6 +22,8 @@ export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
@ -77,6 +80,7 @@ export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
const isRemoteObject = objectMetadataItem.isRemote;
const shouldBeRegistered =
!hasObjectReadOnlyPermission &&
!isRemoteObject &&
!isDeletedFilterActive &&
isDefined(contextStoreNumberOfSelectedRecords) &&

View File

@ -12,6 +12,7 @@ import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRec
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useState } from 'react';
@ -26,6 +27,8 @@ export const useDestroyMultipleRecordsAction: ActionHookWithObjectMetadataItem =
recordTableId: objectMetadataItem.namePlural,
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { destroyManyRecords } = useDestroyManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
});
@ -86,6 +89,7 @@ export const useDestroyMultipleRecordsAction: ActionHookWithObjectMetadataItem =
const isRemoteObject = objectMetadataItem.isRemote;
const shouldBeRegistered =
!hasObjectReadOnlyPermission &&
!isRemoteObject &&
isDeletedFilterActive &&
isDefined(contextStoreNumberOfSelectedRecords) &&

View File

@ -1,6 +1,7 @@
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { getRecordIndexIdFromObjectNamePlural } from '@/object-record/utils/getRecordIndexIdFromObjectNamePlural';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
export const useCreateNewTableRecordNoSelectionRecordAction: ActionHookWithObjectMetadataItem =
({ objectMetadataItem }) => {
@ -8,6 +9,8 @@ export const useCreateNewTableRecordNoSelectionRecordAction: ActionHookWithObjec
objectMetadataItem.namePlural,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewTableRecord } = useCreateNewTableRecord({
objectMetadataItem,
recordTableId,
@ -18,7 +21,7 @@ export const useCreateNewTableRecordNoSelectionRecordAction: ActionHookWithObjec
};
return {
shouldBeRegistered: true,
shouldBeRegistered: !hasObjectReadOnlyPermission,
onClick,
};
};

View File

@ -6,6 +6,7 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { isNull } from '@sniptt/guards';
@ -21,6 +22,8 @@ export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
@ -61,7 +64,9 @@ export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
const { isInRightDrawer } = useContext(ActionMenuContext);
const shouldBeRegistered =
!isRemoteObject && isNull(selectedRecord?.deletedAt);
!isRemoteObject &&
isNull(selectedRecord?.deletedAt) &&
!hasObjectReadOnlyPermission;
const onClick = () => {
if (!shouldBeRegistered) {

View File

@ -4,6 +4,7 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { AppPath } from '@/types/AppPath';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
@ -20,6 +21,8 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
useState(false);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const navigateApp = useNavigateApp();
const { resetTableRowSelection } = useRecordTable({
@ -54,7 +57,9 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
const { isInRightDrawer } = useContext(ActionMenuContext);
const shouldBeRegistered =
!isRemoteObject && isDefined(selectedRecord?.deletedAt);
!hasObjectReadOnlyPermission &&
!isRemoteObject &&
isDefined(selectedRecord?.deletedAt);
const onClick = () => {
if (!shouldBeRegistered) {

View File

@ -17,6 +17,7 @@ import { DropZone } from '@/activities/files/components/DropZone';
import { useAttachments } from '@/activities/files/hooks/useAttachments';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { isDefined } from 'twenty-shared';
const StyledAttachmentsContainer = styled.div`
@ -46,6 +47,8 @@ export const Attachments = ({
const [isDraggingFile, setIsDraggingFile] = useState(false);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (isDefined(e.target.files)) onUploadFile?.(e.target.files[0]);
};
@ -91,12 +94,14 @@ export const Attachments = ({
onChange={handleFileChange}
type="file"
/>
<Button
Icon={IconPlus}
title="Add file"
variant="secondary"
onClick={handleUploadFileClick}
/>
{!hasObjectReadOnlyPermission && (
<Button
Icon={IconPlus}
title="Add file"
variant="secondary"
onClick={handleUploadFileClick}
/>
)}
</AnimatedPlaceholderEmptyContainer>
)}
</StyledDropZoneContainer>
@ -115,13 +120,15 @@ export const Attachments = ({
title="All"
attachments={attachments ?? []}
button={
<Button
Icon={IconPlus}
size="small"
variant="secondary"
title="Add file"
onClick={handleUploadFileClick}
></Button>
!hasObjectReadOnlyPermission && (
<Button
Icon={IconPlus}
size="small"
variant="secondary"
title="Add file"
onClick={handleUploadFileClick}
></Button>
)
}
/>
</StyledAttachmentsContainer>

View File

@ -4,6 +4,7 @@ import { NoteList } from '@/activities/notes/components/NoteList';
import { useNotes } from '@/activities/notes/hooks/useNotes';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import styled from '@emotion/styled';
import {
AnimatedPlaceholder,
@ -31,6 +32,8 @@ export const Notes = ({
}) => {
const { notes, loading } = useNotes(targetableObject);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Note,
});
@ -56,16 +59,18 @@ export const Notes = ({
There are no associated notes with this record.
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<Button
Icon={IconPlus}
title="New note"
variant="secondary"
onClick={() =>
openCreateActivity({
targetableObjects: [targetableObject],
})
}
/>
{!hasObjectReadOnlyPermission && (
<Button
Icon={IconPlus}
title="New note"
variant="secondary"
onClick={() =>
openCreateActivity({
targetableObjects: [targetableObject],
})
}
/>
)}
</AnimatedPlaceholderEmptyContainer>
);
}

View File

@ -17,6 +17,7 @@ import { useTasks } from '@/activities/tasks/hooks/useTasks';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import groupBy from 'lodash.groupby';
import { AddTaskButton } from './AddTaskButton';
@ -38,6 +39,8 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
targetableObjects: targetableObjects ?? [],
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
@ -71,16 +74,18 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
All tasks addressed. Maintain the momentum.
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<Button
Icon={IconPlus}
title="New task"
variant={'secondary'}
onClick={() =>
openCreateActivity({
targetableObjects: targetableObjects ?? [],
})
}
/>
{!hasObjectReadOnlyPermission && (
<Button
Icon={IconPlus}
title="New task"
variant={'secondary'}
onClick={() =>
openCreateActivity({
targetableObjects: targetableObjects ?? [],
})
}
/>
)}
</AnimatedPlaceholderEmptyContainer>
);
}

View File

@ -1,7 +1,10 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { UserWorkspace } from '~/generated/graphql';
export type CurrentUserWorkspace = Pick<UserWorkspace, 'settingsPermissions'>;
export type CurrentUserWorkspace = Pick<
UserWorkspace,
'settingsPermissions' | 'objectRecordsPermissions'
>;
export const currentUserWorkspaceState =
createState<CurrentUserWorkspace | null>({

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>

View File

@ -50,7 +50,7 @@ export type SettingsNavigationItem = {
soon?: boolean;
};
export const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const billing = useRecoilValue(billingState);
const isFunctionSettingsEnabled = false;
@ -195,3 +195,5 @@ export const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
},
];
};
export { useSettingsNavigationItems };

View File

@ -0,0 +1,27 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { isDefined, PermissionsOnAllObjectRecords } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useHasObjectReadOnlyPermission = () => {
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
const isPermissionEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
);
if (!isPermissionEnabled) {
return false;
}
if (!isDefined(currentUserWorkspace?.objectRecordsPermissions)) {
return true;
}
return (
currentUserWorkspace?.objectRecordsPermissions.length === 1 &&
currentUserWorkspace?.objectRecordsPermissions.includes(
PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS,
)
);
};

View File

@ -1,3 +1,4 @@
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { Button, IconButton, IconPlus, useIsMobile } from 'twenty-ui';
@ -11,10 +12,17 @@ export const PageAddButton = ({ onClick }: PageAddButtonProps) => {
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const isMobile = useIsMobile();
const { t } = useLingui();
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<>
{isCommandMenuV2Enabled ? (

View File

@ -17,6 +17,7 @@ import { SHOW_PAGE_ADD_BUTTON_DROPDOWN_ID } from '@/ui/layout/show-page/constant
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { Dropdown } from '../../dropdown/components/Dropdown';
@ -39,6 +40,8 @@ export const ShowPageAddButton = ({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const handleSelect = (objectNameSingular: CoreObjectNameSingular) => {
if (objectNameSingular === CoreObjectNameSingular.Note) {
openNote({
@ -67,6 +70,10 @@ export const ShowPageAddButton = ({
return;
}
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<StyledContainer>
<Dropdown

View File

@ -13,7 +13,9 @@ import tsconfigPaths from 'vite-tsconfig-paths';
type Checkers = Parameters<typeof checker>[0];
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const env = loadEnv(mode, __dirname, '');
console.log(__dirname);
const {
REACT_APP_SERVER_BASE_URL,