Skip to content

Commit

Permalink
Make workflow objects read only in frontend (twentyhq#7545)
Browse files Browse the repository at this point in the history
Expected behavior:
- workflows can be added and deleted. Only name field is editable
- versions and runs cannot be added nor deleted. No fields are editable

Added two new utils for those needs:
- `isReadOnlyObject` the similar logic between remote objects, versions
and runs
- `isFieldReadonlyFromObjectMetadataName` to easily block field edition
from object context
  • Loading branch information
thomtrp authored and harshit078 committed Oct 14, 2024
1 parent c609d11 commit 693d457
Show file tree
Hide file tree
Showing 16 changed files with 190 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData';
import {
Expand Down Expand Up @@ -55,12 +56,13 @@ export const useComputeActionsBasedOnContextStore = ({
filename: `${objectMetadataItem.nameSingular}.csv`,
});

const isRemoteObject = objectMetadataItem.isRemote;
const isRemote = objectMetadataItem.isRemote;

const numberOfSelectedRecords = contextStoreTargetedRecordIds.length;

const canDelete =
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;
!isObjectMetadataReadOnly(objectMetadataItem) &&
numberOfSelectedRecords < DELETE_MAX_COUNT;

const menuActions: ActionMenuEntry[] = useMemo(
() =>
Expand Down Expand Up @@ -125,7 +127,7 @@ export const useComputeActionsBasedOnContextStore = ({
return {
availableActionsInContext: [
...menuActions,
...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected
...(!isRemote && isFavorite && hasOnlyOneRecordSelected
? [
{
label: 'Remove from favorites',
Expand All @@ -134,7 +136,7 @@ export const useComputeActionsBasedOnContextStore = ({
},
]
: []),
...(!isRemoteObject && !isFavorite && hasOnlyOneRecordSelected
...(!isRemote && !isFavorite && hasOnlyOneRecordSelected
? [
{
label: 'Add to favorites',
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ export enum CoreObjectNameSingular {
Workflow = 'workflow',
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
WorkflowVersion = 'workflowVersion',
WorkflowRun = 'workflowRun',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';

export const isObjectMetadataReadOnly = (
objectMetadataItem: Pick<ObjectMetadataItem, 'isRemote' | 'nameSingular'>,
) =>
objectMetadataItem.isRemote ||
isWorkflowSubObjectMetadata(objectMetadataItem.nameSingular);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';

export const isWorkflowSubObjectMetadata = (
objectMetadataNameSingular?: string,
) =>
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowVersion ||
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowRun;
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { useContext } from 'react';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldMetadataReadOnly } from '../utils/isFieldMetadataReadOnly';

export const useIsFieldReadOnly = () => {
const { fieldDefinition } = useContext(FieldContext);

const { metadata } = fieldDefinition;

return (
fieldDefinition.metadata.fieldName === 'noteTargets' ||
fieldDefinition.metadata.fieldName === 'taskTargets' ||
isFieldActor(fieldDefinition) ||
isFieldRichText(fieldDefinition)
isFieldRichText(fieldDefinition) ||
isFieldMetadataReadOnly(metadata)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';

export const isFieldMetadataReadOnly = (fieldMetadata: FieldMetadata) => {
if (
fieldMetadata.fieldName === 'noteTargets' ||
fieldMetadata.fieldName === 'taskTargets'
) {
return true;
}

return (
isWorkflowSubObjectMetadata(fieldMetadata.objectMetadataNameSingular) ||
(fieldMetadata.objectMetadataNameSingular ===
CoreObjectNameSingular.Workflow &&
fieldMetadata.fieldName !== 'name')
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';

import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
Expand Down Expand Up @@ -30,8 +31,11 @@ export const RecordIndexPageHeader = () => {

const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);

const isTable =
recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote;
const shouldDisplayAddButton = objectMetadataItem
? !isObjectMetadataReadOnly(objectMetadataItem)
: false;

const isTable = recordIndexViewType === ViewType.Table;

const pageHeaderTitle =
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
Expand All @@ -43,11 +47,12 @@ export const RecordIndexPageHeader = () => {
return (
<PageHeader title={pageHeaderTitle} Icon={Icon}>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
{isTable ? (
<PageAddButton onClick={handleAddButtonClick} />
) : (
<RecordIndexPageKanbanAddButton />
)}
{shouldDisplayAddButton &&
(isTable ? (
<PageAddButton onClick={handleAddButtonClick} />
) : (
<RecordIndexPageKanbanAddButton />
))}
</PageHeader>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@/object-record/record-field/contexts/FieldContext';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldMetadataReadOnly } from '@/object-record/record-field/utils/isFieldMetadataReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
Expand Down Expand Up @@ -180,6 +181,8 @@ export const RecordDetailRelationRecordsListItem = ({
[isExpanded],
);

const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata);

return (
<>
<RecordValueSetterEffect recordId={relationRecord.id} />
Expand All @@ -195,37 +198,39 @@ export const RecordDetailRelationRecordsListItem = ({
accent="tertiary"
/>
</StyledClickableZone>
<DropdownScope dropdownScopeId={dropdownScopeId}>
<Dropdown
dropdownId={dropdownScopeId}
dropdownPlacement="right-start"
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={IconDotsVertical}
accent="tertiary"
/>
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconUnlink}
text="Detach"
onClick={handleDetach}
{canEdit && (
<DropdownScope dropdownScopeId={dropdownScopeId}>
<Dropdown
dropdownId={dropdownScopeId}
dropdownPlacement="right-start"
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={IconDotsVertical}
accent="tertiary"
/>
{!isAccountOwnerRelation && (
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconTrash}
text="Delete"
accent="danger"
onClick={handleDelete}
LeftIcon={IconUnlink}
text="Detach"
onClick={handleDetach}
/>
)}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownScopeId }}
/>
</DropdownScope>
{!isAccountOwnerRelation && (
<MenuItem
LeftIcon={IconTrash}
text="Delete"
accent="danger"
onClick={handleDelete}
/>
)}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownScopeId }}
/>
</DropdownScope>
)}
</StyledListItem>
<AnimatedEaseInOut isOpen={isExpanded}>
<PropertyBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { usePersistField } from '@/object-record/record-field/hooks/usePersistFi
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldMetadataReadOnly } from '@/object-record/record-field/utils/isFieldMetadataReadOnly';
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
Expand Down Expand Up @@ -158,6 +159,8 @@ export const RecordDetailRelationSection = ({
recordId,
});

const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata);

if (loading) return null;

return (
Expand All @@ -178,49 +181,51 @@ export const RecordDetailRelationSection = ({
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
areRecordsAvailable={relationRecords.length > 0}
rightAdornment={
<DropdownScope dropdownScopeId={dropdownId}>
<StyledAddDropdown
dropdownId={dropdownId}
dropdownPlacement="right-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={isToOneObject ? IconPencil : IconPlus}
accent="tertiary"
/>
}
dropdownComponents={
<RelationPickerScope relationPickerScopeId={dropdownId}>
{isToOneObject ? (
<SingleEntitySelectMenuItemsWithSearch
EmptyIcon={IconForbid}
onEntitySelected={handleRelationPickerEntitySelected}
selectedRelationRecordIds={relationRecordIds}
relationObjectNameSingular={
relationObjectMetadataNameSingular
}
relationPickerScopeId={dropdownId}
onCreate={createNewRecordAndOpenRightDrawer}
/>
) : (
<>
<ObjectMetadataItemsRelationPickerEffect />
<RelationFromManyFieldInputMultiRecordsEffect />
<MultiRecordSelect
canEdit && (
<DropdownScope dropdownScopeId={dropdownId}>
<StyledAddDropdown
dropdownId={dropdownId}
dropdownPlacement="right-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={isToOneObject ? IconPencil : IconPlus}
accent="tertiary"
/>
}
dropdownComponents={
<RelationPickerScope relationPickerScopeId={dropdownId}>
{isToOneObject ? (
<SingleEntitySelectMenuItemsWithSearch
EmptyIcon={IconForbid}
onEntitySelected={handleRelationPickerEntitySelected}
selectedRelationRecordIds={relationRecordIds}
relationObjectNameSingular={
relationObjectMetadataNameSingular
}
relationPickerScopeId={dropdownId}
onCreate={createNewRecordAndOpenRightDrawer}
onChange={updateRelation}
onSubmit={closeDropdown}
/>
</>
)}
</RelationPickerScope>
}
dropdownHotkeyScope={{
scope: dropdownId,
}}
/>
</DropdownScope>
) : (
<>
<ObjectMetadataItemsRelationPickerEffect />
<RelationFromManyFieldInputMultiRecordsEffect />
<MultiRecordSelect
onCreate={createNewRecordAndOpenRightDrawer}
onChange={updateRelation}
onSubmit={closeDropdown}
/>
</>
)}
</RelationPickerScope>
}
dropdownHotkeyScope={{
scope: dropdownId,
}}
/>
</DropdownScope>
)
}
/>
{showContent()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export const RecordTable = ({
<RecordTableEmptyState />
) : (
<StyledTable className="entity-table-cell">
<RecordTableHeader />
<RecordTableHeader
objectMetadataNameSingular={objectNameSingular}
/>
<RecordTableBody />
</StyledTable>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useObjectIsRemote } from '@/object-metadata/hooks/useObjectIsRemote';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
Expand All @@ -18,7 +17,7 @@ export const RecordTableEmptyState = () => {
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;

const isRemote = useObjectIsRemote(objectMetadataItem);
const isRemote = objectMetadataItem.isRemote;

const isSoftDeleteActive = useRecoilValue(isSoftDeleteActiveState);

Expand Down
Loading

0 comments on commit 693d457

Please sign in to comment.