Make workflow objects read only in frontend (#7545)

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
This commit is contained in:
Thomas Trompette
2024-10-10 15:29:43 +02:00
committed by GitHub
parent 66a483c332
commit c055d167f2
16 changed files with 190 additions and 112 deletions

View File

@ -3,6 +3,7 @@ import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData';
import { import {
@ -55,12 +56,13 @@ export const useComputeActionsBasedOnContextStore = ({
filename: `${objectMetadataItem.nameSingular}.csv`, filename: `${objectMetadataItem.nameSingular}.csv`,
}); });
const isRemoteObject = objectMetadataItem.isRemote; const isRemote = objectMetadataItem.isRemote;
const numberOfSelectedRecords = contextStoreTargetedRecordIds.length; const numberOfSelectedRecords = contextStoreTargetedRecordIds.length;
const canDelete = const canDelete =
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; !isObjectMetadataReadOnly(objectMetadataItem) &&
numberOfSelectedRecords < DELETE_MAX_COUNT;
const menuActions: ActionMenuEntry[] = useMemo( const menuActions: ActionMenuEntry[] = useMemo(
() => () =>
@ -125,7 +127,7 @@ export const useComputeActionsBasedOnContextStore = ({
return { return {
availableActionsInContext: [ availableActionsInContext: [
...menuActions, ...menuActions,
...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected ...(!isRemote && isFavorite && hasOnlyOneRecordSelected
? [ ? [
{ {
label: 'Remove from favorites', label: 'Remove from favorites',
@ -134,7 +136,7 @@ export const useComputeActionsBasedOnContextStore = ({
}, },
] ]
: []), : []),
...(!isRemoteObject && !isFavorite && hasOnlyOneRecordSelected ...(!isRemote && !isFavorite && hasOnlyOneRecordSelected
? [ ? [
{ {
label: 'Add to favorites', label: 'Add to favorites',

View File

@ -1,5 +0,0 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const useObjectIsRemote = (objectMetadataItem: ObjectMetadataItem) => {
return objectMetadataItem.isRemote ?? false;
};

View File

@ -31,4 +31,5 @@ export enum CoreObjectNameSingular {
Workflow = 'workflow', Workflow = 'workflow',
MessageChannelMessageAssociation = 'messageChannelMessageAssociation', MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
WorkflowVersion = 'workflowVersion', WorkflowVersion = 'workflowVersion',
WorkflowRun = 'workflowRun',
} }

View File

@ -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);

View File

@ -0,0 +1,7 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
export const isWorkflowSubObjectMetadata = (
objectMetadataNameSingular?: string,
) =>
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowVersion ||
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowRun;

View File

@ -3,14 +3,16 @@ import { useContext } from 'react';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor'; import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { FieldContext } from '../contexts/FieldContext'; import { FieldContext } from '../contexts/FieldContext';
import { isFieldMetadataReadOnly } from '../utils/isFieldMetadataReadOnly';
export const useIsFieldReadOnly = () => { export const useIsFieldReadOnly = () => {
const { fieldDefinition } = useContext(FieldContext); const { fieldDefinition } = useContext(FieldContext);
const { metadata } = fieldDefinition;
return ( return (
fieldDefinition.metadata.fieldName === 'noteTargets' ||
fieldDefinition.metadata.fieldName === 'taskTargets' ||
isFieldActor(fieldDefinition) || isFieldActor(fieldDefinition) ||
isFieldRichText(fieldDefinition) isFieldRichText(fieldDefinition) ||
isFieldMetadataReadOnly(metadata)
); );
}; };

View File

@ -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')
);
};

View File

@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui'; import { useIcons } from 'twenty-ui';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton'; import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
@ -30,8 +31,11 @@ export const RecordIndexPageHeader = () => {
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState); const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
const isTable = const shouldDisplayAddButton = objectMetadataItem
recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote; ? !isObjectMetadataReadOnly(objectMetadataItem)
: false;
const isTable = recordIndexViewType === ViewType.Table;
const pageHeaderTitle = const pageHeaderTitle =
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural); objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
@ -43,11 +47,12 @@ export const RecordIndexPageHeader = () => {
return ( return (
<PageHeader title={pageHeaderTitle} Icon={Icon}> <PageHeader title={pageHeaderTitle} Icon={Icon}>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} /> <PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
{isTable ? ( {shouldDisplayAddButton &&
<PageAddButton onClick={handleAddButtonClick} /> (isTable ? (
) : ( <PageAddButton onClick={handleAddButtonClick} />
<RecordIndexPageKanbanAddButton /> ) : (
)} <RecordIndexPageKanbanAddButton />
))}
</PageHeader> </PageHeader>
); );
}; };

View File

@ -24,6 +24,7 @@ import {
} from '@/object-record/record-field/contexts/FieldContext'; } from '@/object-record/record-field/contexts/FieldContext';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; 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 { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
@ -180,6 +181,8 @@ export const RecordDetailRelationRecordsListItem = ({
[isExpanded], [isExpanded],
); );
const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata);
return ( return (
<> <>
<RecordValueSetterEffect recordId={relationRecord.id} /> <RecordValueSetterEffect recordId={relationRecord.id} />
@ -195,37 +198,39 @@ export const RecordDetailRelationRecordsListItem = ({
accent="tertiary" accent="tertiary"
/> />
</StyledClickableZone> </StyledClickableZone>
<DropdownScope dropdownScopeId={dropdownScopeId}> {canEdit && (
<Dropdown <DropdownScope dropdownScopeId={dropdownScopeId}>
dropdownId={dropdownScopeId} <Dropdown
dropdownPlacement="right-start" dropdownId={dropdownScopeId}
clickableComponent={ dropdownPlacement="right-start"
<LightIconButton clickableComponent={
className="displayOnHover" <LightIconButton
Icon={IconDotsVertical} className="displayOnHover"
accent="tertiary" Icon={IconDotsVertical}
/> accent="tertiary"
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconUnlink}
text="Detach"
onClick={handleDetach}
/> />
{!isAccountOwnerRelation && ( }
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem <MenuItem
LeftIcon={IconTrash} LeftIcon={IconUnlink}
text="Delete" text="Detach"
accent="danger" onClick={handleDetach}
onClick={handleDelete}
/> />
)} {!isAccountOwnerRelation && (
</DropdownMenuItemsContainer> <MenuItem
} LeftIcon={IconTrash}
dropdownHotkeyScope={{ scope: dropdownScopeId }} text="Delete"
/> accent="danger"
</DropdownScope> onClick={handleDelete}
/>
)}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownScopeId }}
/>
</DropdownScope>
)}
</StyledListItem> </StyledListItem>
<AnimatedEaseInOut isOpen={isExpanded}> <AnimatedEaseInOut isOpen={isExpanded}>
<PropertyBox> <PropertyBox>

View File

@ -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 { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; 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 { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection'; import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader'; import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
@ -158,6 +159,8 @@ export const RecordDetailRelationSection = ({
recordId, recordId,
}); });
const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata);
if (loading) return null; if (loading) return null;
return ( return (
@ -178,49 +181,51 @@ export const RecordDetailRelationSection = ({
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile} hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
areRecordsAvailable={relationRecords.length > 0} areRecordsAvailable={relationRecords.length > 0}
rightAdornment={ rightAdornment={
<DropdownScope dropdownScopeId={dropdownId}> canEdit && (
<StyledAddDropdown <DropdownScope dropdownScopeId={dropdownId}>
dropdownId={dropdownId} <StyledAddDropdown
dropdownPlacement="right-start" dropdownId={dropdownId}
onClose={handleCloseRelationPickerDropdown} dropdownPlacement="right-start"
clickableComponent={ onClose={handleCloseRelationPickerDropdown}
<LightIconButton clickableComponent={
className="displayOnHover" <LightIconButton
Icon={isToOneObject ? IconPencil : IconPlus} className="displayOnHover"
accent="tertiary" Icon={isToOneObject ? IconPencil : IconPlus}
/> accent="tertiary"
} />
dropdownComponents={ }
<RelationPickerScope relationPickerScopeId={dropdownId}> dropdownComponents={
{isToOneObject ? ( <RelationPickerScope relationPickerScopeId={dropdownId}>
<SingleEntitySelectMenuItemsWithSearch {isToOneObject ? (
EmptyIcon={IconForbid} <SingleEntitySelectMenuItemsWithSearch
onEntitySelected={handleRelationPickerEntitySelected} EmptyIcon={IconForbid}
selectedRelationRecordIds={relationRecordIds} onEntitySelected={handleRelationPickerEntitySelected}
relationObjectNameSingular={ selectedRelationRecordIds={relationRecordIds}
relationObjectMetadataNameSingular relationObjectNameSingular={
} relationObjectMetadataNameSingular
relationPickerScopeId={dropdownId} }
onCreate={createNewRecordAndOpenRightDrawer} relationPickerScopeId={dropdownId}
/>
) : (
<>
<ObjectMetadataItemsRelationPickerEffect />
<RelationFromManyFieldInputMultiRecordsEffect />
<MultiRecordSelect
onCreate={createNewRecordAndOpenRightDrawer} onCreate={createNewRecordAndOpenRightDrawer}
onChange={updateRelation}
onSubmit={closeDropdown}
/> />
</> ) : (
)} <>
</RelationPickerScope> <ObjectMetadataItemsRelationPickerEffect />
} <RelationFromManyFieldInputMultiRecordsEffect />
dropdownHotkeyScope={{ <MultiRecordSelect
scope: dropdownId, onCreate={createNewRecordAndOpenRightDrawer}
}} onChange={updateRelation}
/> onSubmit={closeDropdown}
</DropdownScope> />
</>
)}
</RelationPickerScope>
}
dropdownHotkeyScope={{
scope: dropdownId,
}}
/>
</DropdownScope>
)
} }
/> />
{showContent()} {showContent()}

View File

@ -70,7 +70,9 @@ export const RecordTable = ({
<RecordTableEmptyState /> <RecordTableEmptyState />
) : ( ) : (
<StyledTable className="entity-table-cell"> <StyledTable className="entity-table-cell">
<RecordTableHeader /> <RecordTableHeader
objectMetadataNameSingular={objectNameSingular}
/>
<RecordTableBody /> <RecordTableBody />
</StyledTable> </StyledTable>
)} )}

View File

@ -1,4 +1,3 @@
import { useObjectIsRemote } from '@/object-metadata/hooks/useObjectIsRemote';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll'; import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
@ -18,7 +17,7 @@ export const RecordTableEmptyState = () => {
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 }); const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0; const noRecordAtAll = totalCount === 0;
const isRemote = useObjectIsRemote(objectMetadataItem); const isRemote = objectMetadataItem.isRemote;
const isSoftDeleteActive = useRecoilValue(isSoftDeleteActiveState); const isSoftDeleteActive = useRecoilValue(isSoftDeleteActiveState);

View File

@ -8,7 +8,10 @@ import {
AnimatedPlaceholderEmptyTitle, AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
import { useContext } from 'react';
import { IconComponent } from 'twenty-ui'; import { IconComponent } from 'twenty-ui';
type RecordTableEmptyStateDisplayProps = { type RecordTableEmptyStateDisplayProps = {
@ -28,6 +31,9 @@ export const RecordTableEmptyStateDisplay = ({
subTitle, subTitle,
title, title,
}: RecordTableEmptyStateDisplayProps) => { }: RecordTableEmptyStateDisplayProps) => {
const { objectMetadataItem } = useContext(RecordTableContext);
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
return ( return (
<AnimatedPlaceholderEmptyContainer> <AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type={animatedPlaceholderType} /> <AnimatedPlaceholder type={animatedPlaceholderType} />
@ -37,12 +43,14 @@ export const RecordTableEmptyStateDisplay = ({
{subTitle} {subTitle}
</AnimatedPlaceholderEmptySubTitle> </AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer> </AnimatedPlaceholderEmptyTextContainer>
<Button {!isReadOnly && (
Icon={Icon} <Button
title={buttonTitle} Icon={Icon}
variant={'secondary'} title={buttonTitle}
onClick={onClick} variant={'secondary'}
/> onClick={onClick}
/>
)}
</AnimatedPlaceholderEmptyContainer> </AnimatedPlaceholderEmptyContainer>
); );
}; };

View File

@ -73,7 +73,11 @@ const StyledTableHead = styled.thead<{
} }
`; `;
export const RecordTableHeader = () => { export const RecordTableHeader = ({
objectMetadataNameSingular,
}: {
objectMetadataNameSingular: string;
}) => {
const { visibleTableColumnsSelector } = useRecordTableStates(); const { visibleTableColumnsSelector } = useRecordTableStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
@ -84,7 +88,11 @@ export const RecordTableHeader = () => {
<RecordTableHeaderDragDropColumn /> <RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn /> <RecordTableHeaderCheckboxColumn />
{visibleTableColumns.map((column) => ( {visibleTableColumns.map((column) => (
<RecordTableHeaderCell key={column.fieldMetadataId} column={column} /> <RecordTableHeaderCell
key={column.fieldMetadataId}
column={column}
objectMetadataNameSingular={objectMetadataNameSingular}
/>
))} ))}
<RecordTableHeaderLastColumn /> <RecordTableHeaderLastColumn />
</tr> </tr>

View File

@ -3,6 +3,8 @@ import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui'; import { IconPlus } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
@ -91,11 +93,17 @@ const StyledHeaderIcon = styled.div`
export const RecordTableHeaderCell = ({ export const RecordTableHeaderCell = ({
column, column,
objectMetadataNameSingular,
}: { }: {
column: ColumnDefinition<FieldMetadata>; column: ColumnDefinition<FieldMetadata>;
objectMetadataNameSingular: string;
}) => { }) => {
const { resizeFieldOffsetState, tableColumnsState } = useRecordTableStates(); const { resizeFieldOffsetState, tableColumnsState } = useRecordTableStates();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: objectMetadataNameSingular,
});
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState( const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
resizeFieldOffsetState, resizeFieldOffsetState,
); );
@ -190,6 +198,8 @@ export const RecordTableHeaderCell = ({
createNewTableRecord(); createNewTableRecord();
}; };
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
return ( return (
<StyledColumnHeaderCell <StyledColumnHeaderCell
key={column.fieldMetadataId} key={column.fieldMetadataId}
@ -205,16 +215,18 @@ export const RecordTableHeaderCell = ({
> >
<StyledColumnHeadContainer> <StyledColumnHeadContainer>
<RecordTableColumnHeadWithDropdown column={column} /> <RecordTableColumnHeadWithDropdown column={column} />
{(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && ( {(useIsMobile() || iconVisibility) &&
<StyledHeaderIcon> !!column.isLabelIdentifier &&
<LightIconButton !isReadOnly && (
Icon={IconPlus} <StyledHeaderIcon>
size="small" <LightIconButton
accent="tertiary" Icon={IconPlus}
onClick={handlePlusButtonClick} size="small"
/> accent="tertiary"
</StyledHeaderIcon> onClick={handlePlusButtonClick}
)} />
</StyledHeaderIcon>
)}
</StyledColumnHeadContainer> </StyledColumnHeadContainer>
{!disableColumnResize && ( {!disableColumnResize && (
<StyledResizeHandler <StyledResizeHandler

View File

@ -109,7 +109,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
description: 'Workflow versions linked to the workflow.', description: 'Workflow versions linked to the workflow.',
icon: 'IconVersions', icon: 'IconVersions',
inverseSideTarget: () => WorkflowVersionWorkspaceEntity, inverseSideTarget: () => WorkflowVersionWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@WorkspaceIsNullable() @WorkspaceIsNullable()
versions: Relation<WorkflowVersionWorkspaceEntity[]>; versions: Relation<WorkflowVersionWorkspaceEntity[]>;
@ -121,7 +121,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
description: 'Workflow runs linked to the workflow.', description: 'Workflow runs linked to the workflow.',
icon: 'IconVersions', icon: 'IconVersions',
inverseSideTarget: () => WorkflowRunWorkspaceEntity, inverseSideTarget: () => WorkflowRunWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@WorkspaceIsNullable() @WorkspaceIsNullable()
runs: Relation<WorkflowRunWorkspaceEntity>; runs: Relation<WorkflowRunWorkspaceEntity>;
@ -133,7 +133,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
description: 'Workflow event listeners linked to the workflow.', description: 'Workflow event listeners linked to the workflow.',
icon: 'IconVersions', icon: 'IconVersions',
inverseSideTarget: () => WorkflowEventListenerWorkspaceEntity, inverseSideTarget: () => WorkflowEventListenerWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@WorkspaceIsNullable() @WorkspaceIsNullable()
eventListeners: Relation<WorkflowEventListenerWorkspaceEntity[]>; eventListeners: Relation<WorkflowEventListenerWorkspaceEntity[]>;