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

<img width="1642" height="950" alt="image"
src="https://github.com/user-attachments/assets/e04fb710-e333-4dd7-a29f-82c226202e77"
/>


### After

<img width="1690" height="1112" alt="image"
src="https://github.com/user-attachments/assets/f7d75974-a5cc-401b-bbd2-ab82a2006cf9"
/>
This commit is contained in:
Thomas des Francs
2026-06-24 15:15:41 +02:00
committed by GitHub
parent 73e9374ef8
commit 9e57ec3153
10 changed files with 216 additions and 115 deletions
@@ -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) => (
<StyledSettingsNameCellSecondaryLabel title={title}>
{children}
</StyledSettingsNameCellSecondaryLabel>
);
@@ -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}
</StyledNameLabel>
{!fieldMetadataItem.isActive && (
<StyledInactiveLabel>{t`Deactivated`}</StyledInactiveLabel>
<SettingsNameCellSecondaryLabel>
{t`Deactivated`}
</SettingsNameCellSecondaryLabel>
)}
</StyledNameContainer>
</TableCell>
@@ -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}
</StyledNameLabel>
{!objectMetadataItem.isActive && (
<StyledInactiveLabel>{t`Deactivated`}</StyledInactiveLabel>
<SettingsNameCellSecondaryLabel>
{t`Deactivated`}
</SettingsNameCellSecondaryLabel>
)}
</StyledNameContainer>
</StyledNameTableCell>
@@ -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 (
<TableRow
gridTemplateColumns={OBJECT_RELATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
@@ -174,8 +180,8 @@ export const SettingsObjectRelationItemTableRow = ({
color={themeCssVariables.font.color.primary}
gap={themeCssVariables.spacing[2]}
>
{isDefined(Icon) && (
<Icon
{isDefined(NameIcon) && (
<NameIcon
style={{
minWidth: theme.icon.size.md,
}}
@@ -201,8 +207,15 @@ export const SettingsObjectRelationItemTableRow = ({
{targetObjectLabel}
</StyledNameLabel>
)}
{shouldDisplayFieldLabelAsSubtitle && (
<SettingsNameCellSecondaryLabel title={fieldLabelSubtitle}>
{fieldLabelSubtitle}
</SettingsNameCellSecondaryLabel>
)}
{!fieldMetadataItem.isActive && (
<StyledInactiveLabel>{t`Deactivated`}</StyledInactiveLabel>
<SettingsNameCellSecondaryLabel>
{t`Deactivated`}
</SettingsNameCellSecondaryLabel>
)}
</StyledNameContainer>
</TableCell>
@@ -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 && (
<MenuItemToggle
LeftIcon={IconSettings}
onToggleChange={() =>
setShowSystemRelations(!showSystemRelations)
}
toggled={showSystemRelations}
text={t`System relations`}
toggleSize="small"
/>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
}
@@ -186,13 +246,19 @@ export const SettingsObjectRelationsTable = ({
</TableRow>
<StyledSettingsDataModelTableBodyContainer>
<TableBody>
{filteredRelationFields.map((fieldMetadataItem) => (
<SettingsObjectRelationItemTableRow
key={fieldMetadataItem.id}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
))}
{filteredRelationFields.length > 0 ? (
filteredRelationFields.map((fieldMetadataItem) => (
<SettingsObjectRelationItemTableRow
key={fieldMetadataItem.id}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
))
) : (
<TableCell color={themeCssVariables.font.color.tertiary}>
{t`No relations found`}
</TableCell>
)}
</TableBody>
</StyledSettingsDataModelTableBodyContainer>
</Table>
@@ -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 (
<>
<StyledContentContainer>
{hasRelations && (
<Section>
<H2Title
@@ -102,6 +108,6 @@ export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
)}
</StyledButtonContainer>
</Section>
</>
</StyledContentContainer>
);
};
@@ -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 (
@@ -94,7 +94,7 @@ export const SettingsObjects = () => {
</Section>
<Section>
<H2Title
title={t`Existing objects`}
title={t`Objects`}
description={t`Manage objects, fields and relationships`}
/>
<SettingsObjectTable objectMetadataItems={objectMetadataItems} />
@@ -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 });
},
};
@@ -25,7 +25,7 @@ export const GET_SETTINGS_OBJECT_TABLE_METADATA: TableMetadata<SettingsObjectTab
align: 'right',
},
{
fieldLabel: msg`Instances`,
fieldLabel: msg`Records`,
fieldName: 'totalObjectCount',
fieldType: 'number',
align: 'right',