Redesign Object/Field tables (#16844)
Redesign the data model pages for more clarity / better distinction between fields and relations
This commit is contained in:
@@ -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(
|
||||
|
||||
-5
@@ -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);
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
-9
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user