Files
twenty/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx
T
Félix Malfait 27da9d60eb fix: set isActive=true in morph migration & add system fields toggle (#17736)
## Summary

Three fixes in this PR:

### 1. Migration commands now set `isActive = true` and use generic
'Target' label

When converting relation fields to `MORPH_RELATION` type, the migration
commands now:
- Set `isActive = true` to prevent inactive fields from being selected
as the representative morph field
- Update all field labels to generic **'Target'** instead of keeping
individual labels like 'Company', 'Person'

This ensures the UI shows a coherent label ('Target') alongside 'X
Objects' for the type.

**Files changed:**
- `1-17-migrate-note-target-to-morph-relations.command.ts`
- `1-17-migrate-task-target-to-morph-relations.command.ts`

### 2. Added system fields/relations toggles in Settings

The fields and relations tables in Settings > Data Model were filtering
out system fields with no way to view them. Added a "System fields" /
"System relations" toggle (visible in advanced mode) to allow viewing
these fields.

**Files changed:**
- `SettingsObjectFieldTable.tsx`
- `SettingsObjectRelationsTable.tsx`

This matches the existing behavior on the Objects table which already
has a "System objects" toggle.
2026-02-05 13:35:46 +00:00

189 lines
6.2 KiB
TypeScript

import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
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';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import styled from '@emotion/styled';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from 'twenty-shared/types';
import {
IconArchive,
IconFilter,
IconSearch,
IconSettings,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { MenuItemToggle } from 'twenty-ui/navigation';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
import {
SettingsObjectRelationItemTableRow,
StyledObjectRelationTableRow,
} from './SettingsObjectRelationItemTableRow';
const StyledSearchAndFilterContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledSearchInput = styled(SettingsTextInput)`
flex: 1;
`;
const SETTINGS_OBJECT_RELATION_TABLE_METADATA: TableMetadata<FieldMetadataItem> =
{
tableId: 'settingsObjectRelations',
fields: [
{
fieldLabel: msg`Name`,
fieldName: 'label',
fieldType: 'string',
align: 'left',
},
{
fieldLabel: msg`App`,
fieldName: 'isCustom',
fieldType: 'string',
align: 'left',
},
{
fieldLabel: msg`Type`,
fieldName: 'type',
fieldType: 'string',
align: 'left',
},
],
initialSort: {
fieldName: 'label',
orderBy: 'AscNullsLast',
},
};
type SettingsObjectRelationsTableProps = {
objectMetadataItem: ObjectMetadataItem;
};
export const SettingsObjectRelationsTable = ({
objectMetadataItem,
}: SettingsObjectRelationsTableProps) => {
const { t } = useLingui();
const [searchTerm, setSearchTerm] = useState('');
const [showInactive, setShowInactive] = useState(true);
const [showSystemRelations, setShowSystemRelations] = useState(false);
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
const tableMetadata = SETTINGS_OBJECT_RELATION_TABLE_METADATA;
const relationFields = useMemo(() => {
return objectMetadataItem.fields.filter(
(field) =>
(showSystemRelations || !field.isSystem) &&
(field.type === FieldMetadataType.RELATION ||
field.type === FieldMetadataType.MORPH_RELATION),
);
}, [objectMetadataItem.fields, showSystemRelations]);
const sortedRelationFields = useSortedArray(relationFields, tableMetadata);
const filteredRelationFields = useMemo(() => {
const searchNormalized = normalizeSearchText(searchTerm);
return sortedRelationFields.filter((field) => {
const matchesActiveFilter = showInactive || field.isActive;
const matchesSearch = normalizeSearchText(field.label).includes(
searchNormalized,
);
return matchesActiveFilter && matchesSearch;
});
}, [sortedRelationFields, searchTerm, showInactive]);
if (relationFields.length === 0) {
return null;
}
return (
<>
<StyledSearchAndFilterContainer>
<StyledSearchInput
instanceId="object-relation-table-search"
LeftIcon={IconSearch}
placeholder={t`Search a field...`}
value={searchTerm}
onChange={setSearchTerm}
/>
<Dropdown
dropdownId="settings-relations-filter-dropdown"
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 8 }}
clickableComponent={
<Button
Icon={IconFilter}
size="medium"
variant="secondary"
accent="default"
ariaLabel={t`Filter`}
/>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconArchive}
onToggleChange={() => setShowInactive(!showInactive)}
toggled={showInactive}
text={t`Inactive`}
toggleSize="small"
/>
{isAdvancedModeEnabled && (
<MenuItemToggle
LeftIcon={IconSettings}
onToggleChange={() =>
setShowSystemRelations(!showSystemRelations)
}
toggled={showSystemRelations}
text={t`System relations`}
toggleSize="small"
/>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</StyledSearchAndFilterContainer>
<Table>
<StyledObjectRelationTableRow>
{tableMetadata.fields.map((item) => (
<SortableTableHeader
key={item.fieldName}
fieldName={item.fieldName}
label={t(item.fieldLabel)}
tableId={tableMetadata.tableId}
initialSort={tableMetadata.initialSort}
/>
))}
<TableHeader></TableHeader>
</StyledObjectRelationTableRow>
{filteredRelationFields.map((fieldMetadataItem) => (
<SettingsObjectRelationItemTableRow
key={fieldMetadataItem.id}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
))}
</Table>
</>
);
};