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:
@ -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',
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
|
|
||||||
export const useObjectIsRemote = (objectMetadataItem: ObjectMetadataItem) => {
|
|
||||||
return objectMetadataItem.isRemote ?? false;
|
|
||||||
};
|
|
||||||
@ -31,4 +31,5 @@ export enum CoreObjectNameSingular {
|
|||||||
Workflow = 'workflow',
|
Workflow = 'workflow',
|
||||||
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
|
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
|
||||||
WorkflowVersion = 'workflowVersion',
|
WorkflowVersion = 'workflowVersion',
|
||||||
|
WorkflowRun = 'workflowRun',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
|
||||||
|
export const isWorkflowSubObjectMetadata = (
|
||||||
|
objectMetadataNameSingular?: string,
|
||||||
|
) =>
|
||||||
|
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowVersion ||
|
||||||
|
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowRun;
|
||||||
@ -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)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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')
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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()}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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[]>;
|
||||||
|
|||||||
Reference in New Issue
Block a user