Redesign Object/Field tables (#16844)

Redesign the data model pages for more clarity / better distinction
between fields and relations
This commit is contained in:
Félix Malfait
2025-12-30 09:49:55 +01:00
committed by GitHub
parent 8d130908c2
commit 4f1f0eb177
16 changed files with 571 additions and 200 deletions
@@ -14,16 +14,13 @@ import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import {
H3Title,
IconCodeCircle,
IconListDetails,
IconPlus,
@@ -43,16 +40,6 @@ const StyledContentContainer = styled.div`
padding-left: 0;
`;
const StyledObjectTypeTag = styled(SettingsItemTypeTag)`
box-sizing: border-box;
height: ${({ theme }) => theme.spacing(5)};
margin-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledTitleContainer = styled.div`
display: flex;
`;
export const SettingsObjectDetailPage = () => {
const navigateApp = useNavigateApp();
const { t } = useLingui();
@@ -70,11 +57,8 @@ export const SettingsObjectDetailPage = () => {
findObjectMetadataItemByNamePlural(objectNamePlural) ??
findObjectMetadataItemByNamePlural(updatedObjectNamePlural);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const readonly = isObjectMetadataSettingsReadOnly({
objectMetadataItem,
workspaceCustomApplicationId:
currentWorkspace?.workspaceCustomApplication?.id,
});
const activeTabId = useRecoilComponentValue(
@@ -157,12 +141,7 @@ export const SettingsObjectDetailPage = () => {
return (
<>
<SubMenuTopBarContainer
title={
<StyledTitleContainer>
<H3Title title={objectMetadataItem.labelPlural} />
<StyledObjectTypeTag item={objectMetadataItem} />
</StyledTitleContainer>
}
title={objectMetadataItem.labelPlural}
links={[
{
children: t`Workspace`,
@@ -5,7 +5,6 @@ import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { type z } from 'zod';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
@@ -30,7 +29,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe
import { shouldNavigateBackToMemorizedUrlOnSaveState } from '@/ui/navigation/states/shouldNavigateBackToMemorizedUrlOnSaveState';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilState } from 'recoil';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import {
@@ -84,11 +83,8 @@ export const SettingsObjectFieldEdit = () => {
const objectMetadataItem =
findObjectMetadataItemByNamePlural(objectNamePlural);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const readonly = isObjectMetadataSettingsReadOnly({
objectMetadataItem,
workspaceCustomApplicationId:
currentWorkspace?.workspaceCustomApplication?.id,
});
const {
@@ -18,6 +18,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useMemo, useState } from 'react';
import { useRecoilState } from 'recoil';
import { FieldMetadataType } from 'twenty-shared/types';
import { IconArchive, IconFilter, IconSearch } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { MenuItemToggle } from 'twenty-ui/navigation';
@@ -25,7 +26,18 @@ import { useMapFieldMetadataItemToSettingsObjectDetailTableItem } from '~/pages/
import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD: TableMetadata<SettingsObjectDetailTableItem> =
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_FIELD_TABLE_METADATA: TableMetadata<SettingsObjectDetailTableItem> =
{
tableId: 'settingsObjectDetail',
fields: [
@@ -36,7 +48,7 @@ const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD: TableMetadata<Settings
align: 'left',
},
{
fieldLabel: msg`Field type`,
fieldLabel: msg`App`,
fieldName: 'fieldType',
fieldType: 'string',
align: 'left',
@@ -54,63 +66,23 @@ const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD: TableMetadata<Settings
},
};
const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM: TableMetadata<SettingsObjectDetailTableItem> =
{
tableId: 'settingsObjectDetail',
fields: [
{
fieldLabel: msg`Name`,
fieldName: 'label',
fieldType: 'string',
align: 'left',
},
{
fieldLabel: msg`Identifier`,
fieldName: 'identifierType',
fieldType: 'string',
align: 'left',
},
{
fieldLabel: msg`Data type`,
fieldName: 'dataType',
fieldType: 'string',
align: 'left',
},
],
initialSort: {
fieldName: 'label',
orderBy: 'AscNullsLast',
},
};
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;
`;
export type SettingsObjectFieldTableProps = {
objectMetadataItem: ObjectMetadataItem;
mode: 'view' | 'new-field';
excludeRelations?: boolean;
};
// TODO: find another way than using mode which feels like it could be replaced by another pattern
export const SettingsObjectFieldTable = ({
objectMetadataItem,
mode,
excludeRelations = false,
}: SettingsObjectFieldTableProps) => {
const { t } = useLingui();
const [searchTerm, setSearchTerm] = useState('');
const [showInactive, setShowInactive] = useState(true);
const tableMetadata = objectMetadataItem.isCustom
? GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM
: GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD;
const tableMetadata = SETTINGS_OBJECT_FIELD_TABLE_METADATA;
const { mapFieldMetadataItemToSettingsObjectDetailTableItem } =
useMapFieldMetadataItemToSettingsObjectDetailTableItem(objectMetadataItem);
@@ -130,14 +102,23 @@ export const SettingsObjectFieldTable = ({
(fieldMetadataItem) => !fieldMetadataItem.isSystem,
);
const fieldsToDisplay = excludeRelations
? nonSystemFields?.filter(
(fieldMetadataItem) =>
fieldMetadataItem.type !== FieldMetadataType.RELATION &&
fieldMetadataItem.type !== FieldMetadataType.MORPH_RELATION,
)
: nonSystemFields;
return (
nonSystemFields?.map(
fieldsToDisplay?.map(
mapFieldMetadataItemToSettingsObjectDetailTableItem,
) ?? []
);
}, [
settingsObjectFields,
mapFieldMetadataItemToSettingsObjectDetailTableItem,
excludeRelations,
]);
const sortedAllObjectSettingsDetailItems = useSortedArray(
@@ -35,11 +35,6 @@ export const Default: Story = {
const employeeInput = await canvas.findByPlaceholderText('Employees');
await userEvent.type(employeeInput, 'Test');
const descriptionInput = await canvas.findByPlaceholderText(
'Write a description',
);
await userEvent.type(descriptionInput, 'Test description');
const saveButton = await canvas.findByText('Save');
await new Promise((resolve) => setTimeout(resolve, 5000));
await userEvent.click(saveButton);
@@ -13,7 +13,7 @@ export const GET_SETTINGS_OBJECT_TABLE_METADATA: TableMetadata<SettingsObjectTab
align: 'left',
},
{
fieldLabel: msg`Type`,
fieldLabel: msg`App`,
fieldName: 'objectTypeLabel',
fieldType: 'string',
align: 'left',
@@ -4,7 +4,6 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsDataModelNewFieldBreadcrumbDropDown } from '@/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown';
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm';
import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
@@ -67,7 +66,6 @@ export const SettingsObjectNewFieldConfigure = () => {
icon:
DEFAULT_ICONS_BY_FIELD_TYPE[fieldType] ?? DEFAULT_ICON_FOR_NEW_FIELD,
label: '',
description: '',
name: '',
},
});
@@ -237,13 +235,6 @@ export const SettingsObjectNewFieldConfigure = () => {
objectNameSingular={activeObjectMetadataItem.nameSingular}
/>
</Section>
<Section>
<H2Title
title={t`Description`}
description={t`The description of this field`}
/>
<SettingsDataModelFieldDescriptionForm />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>