From 9e57ec3153e2243d5917906989090e7c033f6ee3 Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Wed, 24 Jun 2026 15:15:41 +0200 Subject: [PATCH] Update data model object settings labels (#22070) ## Summary - Update Data Model object list copy: rename the section to **Objects** and the count column to **Records**. - Improve relation rows by showing the related object name with the field name as a secondary label, including morph relation-specific labels/icons. - Hide relations to system objects unless Advanced mode is enabled, and default the System objects filter to on while Advanced mode is on. - Reuse a shared secondary-label component for the light subtitle/deactivated text treatment. ## Screenshots ### Before Mix of field name & object name. Not all relations are navigable image ### After image --- .../SettingsNameCellSecondaryLabel.tsx | 34 ++++ .../SettingsObjectFieldItemTableRow.tsx | 26 +--- .../components/SettingsObjectItemTableRow.tsx | 20 +-- .../SettingsObjectRelationItemTableRow.tsx | 77 +++++---- .../SettingsObjectRelationsTable.tsx | 146 +++++++++++++----- .../components/tabs/ObjectFields.tsx | 10 +- .../data-model/SettingsObjectTable.tsx | 12 +- .../settings/data-model/SettingsObjects.tsx | 2 +- .../__stories__/SettingsObjects.stories.tsx | 2 +- .../constants/SettingsObjectTableMetadata.ts | 2 +- 10 files changed, 216 insertions(+), 115 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/components/SettingsNameCellSecondaryLabel.tsx diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNameCellSecondaryLabel.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNameCellSecondaryLabel.tsx new file mode 100644 index 0000000000..4d705607cf --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/SettingsNameCellSecondaryLabel.tsx @@ -0,0 +1,34 @@ +import { styled } from '@linaria/react'; +import { type ReactNode } from 'react'; + +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledSettingsNameCellSecondaryLabel = styled.span` + color: ${themeCssVariables.font.color.light}; + flex: 0 999 auto; + font-size: ${themeCssVariables.font.size.md}; + font-weight: ${themeCssVariables.font.weight.regular}; + min-width: 48px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &::before { + content: '·'; + margin-right: ${themeCssVariables.spacing[1]}; + } +`; + +type SettingsNameCellSecondaryLabelProps = { + children: ReactNode; + title?: string; +}; + +export const SettingsNameCellSecondaryLabel = ({ + children, + title, +}: SettingsNameCellSecondaryLabelProps) => ( + + {children} + +); diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index 7772f15c07..6e650fa722 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -6,6 +6,7 @@ import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifie import { isDDLLockedState } from '@/client-config/states/isDDLLockedState'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; +import { SettingsNameCellSecondaryLabel } from '@/settings/components/SettingsNameCellSecondaryLabel'; import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown'; import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState'; @@ -56,21 +57,6 @@ const StyledNameLabel = styled.div` white-space: nowrap; `; -const StyledInactiveLabel = styled.span` - color: ${themeCssVariables.font.color.extraLight}; - flex: 0 999 auto; - font-size: ${themeCssVariables.font.size.sm}; - min-width: 48px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &::before { - content: '·'; - margin-right: ${themeCssVariables.spacing[1]}; - } -`; - const StyledIconChevronRightContainer = styled.span` align-items: center; color: ${themeCssVariables.font.color.tertiary}; @@ -159,9 +145,9 @@ export const SettingsObjectFieldItemTableRow = ({ if (!isFieldTypeSupported) return null; - const isRelatedObjectLinkable = - isDefined(relationObjectMetadataItem?.namePlural) && - !relationObjectMetadataItem.isSystem; + const isRelatedObjectLinkable = isDefined( + relationObjectMetadataItem?.namePlural, + ); const morphRelationCount = fieldMetadataItem.morphRelations?.length; const morphRelationLabel = @@ -199,7 +185,9 @@ export const SettingsObjectFieldItemTableRow = ({ {fieldMetadataItem.label} {!fieldMetadataItem.isActive && ( - {t`Deactivated`} + + {t`Deactivated`} + )} diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx index a7d0e479b4..63acf16434 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx @@ -6,6 +6,7 @@ import { ObjectMetadataIcon } from '@/object-metadata/components/ObjectMetadataI import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField'; import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; +import { SettingsNameCellSecondaryLabel } from '@/settings/components/SettingsNameCellSecondaryLabel'; import { SETTINGS_OBJECT_TABLE_ROW_GRID_TEMPLATE_COLUMNS, StyledActionTableCell, @@ -37,21 +38,6 @@ const StyledNameLabel = styled.div` white-space: nowrap; `; -const StyledInactiveLabel = styled.span` - color: ${themeCssVariables.font.color.extraLight}; - flex: 0 999 auto; - font-size: ${themeCssVariables.font.size.sm}; - min-width: 48px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &::before { - content: '·'; - margin-right: ${themeCssVariables.spacing[1]}; - } -`; - export const SettingsObjectMetadataItemTableRow = ({ action, objectMetadataItem, @@ -79,7 +65,9 @@ export const SettingsObjectMetadataItemTableRow = ({ {objectMetadataItem.labelPlural} {!objectMetadataItem.isActive && ( - {t`Deactivated`} + + {t`Deactivated`} + )} diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationItemTableRow.tsx index 33e6219d9b..92a50ee71c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationItemTableRow.tsx @@ -7,6 +7,7 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche import { isDDLLockedState } from '@/client-config/states/isDDLLockedState'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; +import { SettingsNameCellSecondaryLabel } from '@/settings/components/SettingsNameCellSecondaryLabel'; import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown'; import { TableCell } from '@/ui/layout/table/components/TableCell'; @@ -18,7 +19,11 @@ import { useContext, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { FieldMetadataType, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; -import { IconChevronRight, useIcons } from 'twenty-ui/icon'; +import { + IconChevronRight, + IconRelationManyToMany, + useIcons, +} from 'twenty-ui/icon'; import { UndecoratedLink } from 'twenty-ui/navigation'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; @@ -45,21 +50,6 @@ const StyledNameLabel = styled.div` white-space: nowrap; `; -const StyledInactiveLabel = styled.span` - color: ${themeCssVariables.font.color.extraLight}; - flex: 0 999 auto; - font-size: ${themeCssVariables.font.size.sm}; - min-width: 48px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &::before { - content: '·'; - margin-right: ${themeCssVariables.spacing[1]}; - } -`; - const StyledIconChevronRightContainer = styled.span` align-items: center; color: ${themeCssVariables.font.color.tertiary}; @@ -136,31 +126,47 @@ export const SettingsObjectRelationItemTableRow = ({ fieldName: fieldMetadataItem.name, }); - const isRelatedObjectLinkable = - isDefined(relationObjectMetadataItem?.namePlural) && - !relationObjectMetadataItem.isSystem; + const isRelatedObjectLinkable = isDefined( + relationObjectMetadataItem?.namePlural, + ); - const morphRelationCount = fieldMetadataItem.morphRelations?.length; + const isMorphRelation = + fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION; + + const morphRelationCount = fieldMetadataItem.morphRelations?.length ?? 0; + const morphRelationTargetLabel = + morphRelationCount === 1 ? t`1 Object` : t`${morphRelationCount} Objects`; + const morphRelationFieldLabel = fieldMetadataItem.label; + const displayRelationType = isMorphRelation + ? fieldMetadataItem.morphRelations?.[0]?.type + : relationType; const relationTypeLabel = (() => { - if (fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) { - return t`${morphRelationCount} Objects`; - } - if (isDefined(relationType) === true) { - return RELATION_TYPES[relationType].label; + if (isDefined(displayRelationType) === true) { + return RELATION_TYPES[displayRelationType].label; } return ''; })(); - const RelationIcon = relationType - ? RELATION_TYPES[relationType].Icon + const RelationIcon = displayRelationType + ? RELATION_TYPES[displayRelationType].Icon : undefined; - const targetObjectLabel = - isRelatedObjectLinkable && isDefined(relationObjectMetadataItem) + const NameIcon = isMorphRelation ? IconRelationManyToMany : Icon; + + const targetObjectLabel = isMorphRelation + ? morphRelationTargetLabel + : isRelatedObjectLinkable && isDefined(relationObjectMetadataItem) ? relationObjectMetadataItem.labelPlural : fieldMetadataItem.label; + const fieldLabelSubtitle = isMorphRelation + ? morphRelationFieldLabel + : fieldMetadataItem.label; + + const shouldDisplayFieldLabelAsSubtitle = + isMorphRelation || isDefined(relationObjectMetadataItem); + return ( - {isDefined(Icon) && ( - )} + {shouldDisplayFieldLabelAsSubtitle && ( + + {fieldLabelSubtitle} + + )} {!fieldMetadataItem.isActive && ( - {t`Deactivated`} + + {t`Deactivated`} + )} diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx index eb71b23d4b..f6dd780607 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx @@ -1,5 +1,6 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -8,6 +9,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; import { type TableMetadata } from '@/ui/layout/table/types/TableMetadata'; @@ -19,15 +21,11 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react/macro'; import { useMemo, useState } from 'react'; import { FieldMetadataType } from 'twenty-shared/types'; -import { - IconArchive, - IconFilter, - IconSearch, - IconSettings, -} from 'twenty-ui/icon'; +import { IconArchive, IconFilter, IconSearch } from 'twenty-ui/icon'; import { Button } from 'twenty-ui/input'; import { MenuItemToggle } from 'twenty-ui/navigation'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { isDefined } from 'twenty-shared/utils'; import { normalizeSearchText } from '~/utils/normalizeSearchText'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import { @@ -79,26 +77,73 @@ type SettingsObjectRelationsTableProps = { objectMetadataItem: EnrichedObjectMetadataItem; }; +const getRelationTargetObjectMetadataIds = (field: FieldMetadataItem) => { + if (field.type === FieldMetadataType.MORPH_RELATION) { + return ( + field.morphRelations?.map( + (relation) => relation.targetObjectMetadata.id, + ) ?? [] + ); + } + + return isDefined(field.relation?.targetObjectMetadata.id) + ? [field.relation.targetObjectMetadata.id] + : []; +}; + +const getMorphRelationTargetLabel = (field: FieldMetadataItem) => { + const morphRelationCount = field.morphRelations?.length ?? 0; + + return morphRelationCount === 1 + ? '1 Object' + : `${morphRelationCount} Objects`; +}; + +const getMorphRelationFieldLabel = (field: FieldMetadataItem) => { + return field.label; +}; + export const SettingsObjectRelationsTable = ({ objectMetadataItem, }: SettingsObjectRelationsTableProps) => { const { t } = useLingui(); const [searchTerm, setSearchTerm] = useState(''); const [showInactive, setShowInactive] = useState(true); - const [showSystemRelations, setShowSystemRelations] = useState(false); const isAdvancedModeEnabled = useAtomStateValue(isAdvancedModeEnabledState); const tableMetadata = SETTINGS_OBJECT_RELATION_TABLE_METADATA; + const { objectMetadataItems } = useFilteredObjectMetadataItems(); + const relationFields = useMemo(() => { - return objectMetadataItem.fields.filter( - (field) => - (showSystemRelations || !isHiddenSystemField(field)) && - (field.type === FieldMetadataType.RELATION || - field.type === FieldMetadataType.MORPH_RELATION), - ); - }, [objectMetadataItem.fields, showSystemRelations]); + return objectMetadataItem.fields.filter((field) => { + const isRelationField = + field.type === FieldMetadataType.RELATION || + field.type === FieldMetadataType.MORPH_RELATION; + + if (!isRelationField) { + return false; + } + + const relationTargetObjectMetadataIds = + getRelationTargetObjectMetadataIds(field); + + const isRelationToSystemObject = relationTargetObjectMetadataIds.some( + (objectMetadataId) => + objectMetadataItems.some( + (objectMetadataItem) => + objectMetadataItem.id === objectMetadataId && + objectMetadataItem.isSystem, + ), + ); + + return ( + isAdvancedModeEnabled || + (!isHiddenSystemField(field) && !isRelationToSystemObject) + ); + }); + }, [objectMetadataItem.fields, isAdvancedModeEnabled, objectMetadataItems]); const sortedRelationFields = useSortedArray(relationFields, tableMetadata); @@ -107,16 +152,42 @@ export const SettingsObjectRelationsTable = ({ return sortedRelationFields.filter((field) => { const matchesActiveFilter = showInactive || field.isActive; - const matchesSearch = normalizeSearchText(field.label).includes( - searchNormalized, - ); + const relationTargetObjectLabels = getRelationTargetObjectMetadataIds( + field, + ).flatMap((objectMetadataId) => { + const relationObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === objectMetadataId, + ); + + return isDefined(relationObjectMetadataItem) + ? [ + relationObjectMetadataItem.labelSingular, + relationObjectMetadataItem.labelPlural, + ] + : []; + }); + + const morphRelationLabels = + field.type === FieldMetadataType.MORPH_RELATION + ? [ + getMorphRelationTargetLabel(field), + getMorphRelationFieldLabel(field), + ] + : []; + + const searchableText = [ + field.label, + ...relationTargetObjectLabels, + ...morphRelationLabels, + ] + .map(normalizeSearchText) + .join(' '); + + const matchesSearch = searchableText.includes(searchNormalized); + return matchesActiveFilter && matchesSearch; }); - }, [sortedRelationFields, searchTerm, showInactive]); - - if (relationFields.length === 0) { - return null; - } + }, [objectMetadataItems, sortedRelationFields, searchTerm, showInactive]); return ( <> @@ -153,17 +224,6 @@ export const SettingsObjectRelationsTable = ({ text={t`Inactive`} toggleSize="small" /> - {isAdvancedModeEnabled && ( - - setShowSystemRelations(!showSystemRelations) - } - toggled={showSystemRelations} - text={t`System relations`} - toggleSize="small" - /> - )} } @@ -186,13 +246,19 @@ export const SettingsObjectRelationsTable = ({ - {filteredRelationFields.map((fieldMetadataItem) => ( - - ))} + {filteredRelationFields.length > 0 ? ( + filteredRelationFields.map((fieldMetadataItem) => ( + + )) + ) : ( + + {t`No relations found`} + + )} diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx index fd0400f1a9..b2d60a5e2f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx @@ -22,6 +22,12 @@ const StyledButtonContainer = styled.div` padding-top: ${themeCssVariables.spacing[2]}; `; +const StyledContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[8]}; +`; + type ObjectFieldsProps = { objectMetadataItem: EnrichedObjectMetadataItem; }; @@ -45,7 +51,7 @@ export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => { ); return ( - <> + {hasRelations && (
{ )}
- +
); }; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx index 3fdb4c43d5..23644f2c54 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx @@ -79,7 +79,8 @@ export const SettingsObjectTable = ({ const [searchTerm, setSearchTerm] = useState(''); const [showDeactivated, setShowDeactivated] = useState(true); - const [showSystemObjects, setShowSystemObjects] = useState(false); + const [showSystemObjects, setShowSystemObjects] = useState(true); + const shouldShowSystemObjects = isAdvancedModeEnabled && showSystemObjects; const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem(); @@ -142,13 +143,18 @@ export const SettingsObjectTable = ({ } const isSystem = item.objectMetadataItem.isSystem; - if (isSystem && !showSystemObjects) { + if (isSystem && !shouldShowSystemObjects) { return false; } return true; }), - [sortedObjectSettingsItems, searchTerm, showDeactivated, showSystemObjects], + [ + sortedObjectSettingsItems, + searchTerm, + showDeactivated, + shouldShowSystemObjects, + ], ); return ( diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx index 4858ed238d..7da09bd050 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx @@ -94,7 +94,7 @@ export const SettingsObjects = () => {
diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjects.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjects.stories.tsx index c94959e794..0be0d8b33d 100644 --- a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjects.stories.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjects.stories.tsx @@ -27,6 +27,6 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await canvas.findByText('Existing objects', undefined, { timeout: 5000 }); + await canvas.findByRole('heading', { name: 'Objects' }, { timeout: 5000 }); }, }; diff --git a/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts b/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts index 6e7c419097..1770a451a5 100644 --- a/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts +++ b/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts @@ -25,7 +25,7 @@ export const GET_SETTINGS_OBJECT_TABLE_METADATA: TableMetadata