Skip to content

Commit

Permalink
Refactored all FieldDisplay types for performance optimization (#5768)
Browse files Browse the repository at this point in the history
This PR is the second part of
#5693.

It optimizes all remaining field types.

The observed improvements are :
- x2 loading time improvement on table rows
- more consistent render time

Here's a summary of measured improvements, what's given here is the
average of hundreds of renders with a React Profiler component. (in our
Storybook performance stories)

| Component | Before (µs) | After (µs) |
| ----- | ------------- | --- |
| TextFieldDisplay | 127 | 83 |
| EmailFieldDisplay | 117 | 83 |
| NumberFieldDisplay | 97 | 56 |
| DateFieldDisplay | 240 | 52 |
| CurrencyFieldDisplay | 236 | 110 |
| FullNameFieldDisplay | 131 | 85 |
| AddressFieldDisplay | 118 | 81 |
| BooleanFieldDisplay | 130 | 100 |
| JSONFieldDisplay | 248 | 49 |
| LinksFieldDisplay | 1180 | 140 |
| LinkFieldDisplay | 140 | 78 |
| MultiSelectFieldDisplay | 770 | 130 |
| SelectFieldDisplay | 230 | 87 |
  • Loading branch information
lucasbordeau committed Jun 12, 2024
1 parent 007e0e8 commit 03b3c8a
Show file tree
Hide file tree
Showing 101 changed files with 17,163 additions and 15,791 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"lodash.upperfirst": "^4.3.1",
"luxon": "^3.3.0",
"microdiff": "^1.3.2",
"moize": "^6.1.6",
"nest-commander": "^3.12.0",
"next": "14.0.4",
"next-mdx-remote": "^4.4.1",
Expand Down
6 changes: 4 additions & 2 deletions packages/twenty-front/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ThemeProvider } from '@emotion/react';
import { Preview } from '@storybook/react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { useDarkMode } from 'storybook-dark-mode';
import { THEME_DARK, THEME_LIGHT } from 'twenty-ui';
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui';

import { RootDecorator } from '../src/testing/decorators/RootDecorator';
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
Expand Down Expand Up @@ -39,7 +39,9 @@ const preview: Preview = {

return (
<ThemeProvider theme={theme}>
<Story />
<ThemeContextProvider theme={theme}>
<Story />
</ThemeContextProvider>
</ThemeProvider>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils';
import { formatToHumanReadableDate } from '~/utils/date-utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';

const StyledCardContent = styled(CardContent)<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
GenericFieldContextType,
} from '@/object-record/record-field/contexts/FieldContext';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { formatToHumanReadableDate } from '~/utils';
import { formatToHumanReadableDate } from '~/utils/date-utils';

const StyledRow = styled.div`
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { getPeopleMock } from '~/testing/mock-data/people';
import {
mockDefaultWorkspace,
mockedWorkspaceMemberData,
Expand All @@ -21,6 +23,9 @@ import { sleep } from '~/testing/sleep';

import { CommandMenu } from '../CommandMenu';

const peopleMock = getPeopleMock();
const companiesMock = getCompaniesMock();

const openTimeout = 50;

const meta: Meta<typeof CommandMenu> = {
Expand Down Expand Up @@ -94,8 +99,12 @@ export const MatchingPersonCompanyActivityCreateNavigate: Story = {
const searchInput = await canvas.findByPlaceholderText('Search');
await sleep(openTimeout);
await userEvent.type(searchInput, 'n');
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
expect(
await canvas.findByText(
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
),
).toBeInTheDocument();
expect(await canvas.findByText(companiesMock[0].name)).toBeInTheDocument();
expect(await canvas.findByText('My very first note')).toBeInTheDocument();
expect(await canvas.findByText('Create Note')).toBeInTheDocument();
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
Expand All @@ -119,7 +128,11 @@ export const AtleastMatchingOnePerson: Story = {
const searchInput = await canvas.findByPlaceholderText('Search');
await sleep(openTimeout);
await userEvent.type(searchInput, 'alex');
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
expect(
await canvas.findByText(
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
),
).toBeInTheDocument();
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {
mockedObjectMetadataItems,
mockedPersonObjectMetadataItem,
} from '~/testing/mock-data/metadata';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { getPeopleMock } from '~/testing/mock-data/people';

import { getRecordNodeFromRecord } from '../getRecordNodeFromRecord';

const peopleMock = getPeopleMock();

describe('getRecordNodeFromRecord', () => {
it('computes relation records cache references by default', () => {
// Given
Expand All @@ -19,7 +21,7 @@ describe('getRecordNodeFromRecord', () => {
name: true,
company: true,
};
const record = mockedPeopleData[0];
const record = peopleMock[0];

// When
const result = getRecordNodeFromRecord({
Expand All @@ -33,12 +35,12 @@ describe('getRecordNodeFromRecord', () => {
expect(result).toEqual({
__typename: 'Person',
company: {
__ref: 'Company:5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
__ref: `Company:${record.company.id}`,
},
name: {
__typename: 'FullName',
firstName: 'Alexandre',
lastName: 'Prot',
firstName: record.name.firstName,
lastName: record.name.lastName,
},
});
});
Expand All @@ -54,7 +56,7 @@ describe('getRecordNodeFromRecord', () => {
name: true,
company: true,
};
const record = mockedPeopleData[0];
const record = peopleMock[0];
const computeReferences = false;

// When
Expand All @@ -72,8 +74,8 @@ describe('getRecordNodeFromRecord', () => {
company: record.company,
name: {
__typename: 'FullName',
firstName: 'Alexandre',
lastName: 'Prot',
firstName: record.name.firstName,
lastName: record.name.lastName,
},
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { gql } from '@apollo/client';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { getPeopleMock } from '~/testing/mock-data/people';

const peopleMock = getPeopleMock();

export const query = gql`
query FindDuplicatePerson($id: ID!) {
Expand Down Expand Up @@ -49,11 +51,11 @@ export const responseData = {
personDuplicates: {
edges: [
{
node: { ...mockedPeopleData[0], updatedAt: '' },
node: { ...peopleMock[0], updatedAt: '' },
cursor: 'cursor1',
},
{
node: { ...mockedPeopleData[1], updatedAt: '' },
node: { ...peopleMock[1], updatedAt: '' },
cursor: 'cursor2',
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
import { isNonEmptyString } from '@sniptt/guards';

import { useAddressFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useAddressFieldDisplay';
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';

export const AddressFieldDisplay = () => {
const { fieldValue } = useAddressField();
const { fieldValue } = useAddressFieldDisplay();

const content = [
fieldValue?.addressStreet1,
fieldValue?.addressStreet2,
fieldValue?.addressCity,
fieldValue?.addressCountry,
]
.filter(Boolean)
.filter(isNonEmptyString)
.join(', ');

return <TextDisplay text={content} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useBooleanField } from '@/object-record/record-field/meta-types/hooks/useBooleanField';
import { useBooleanFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useBooleanFieldDisplay';
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';

export const BooleanFieldDisplay = () => {
const { fieldValue } = useBooleanField();
const { fieldValue } = useBooleanFieldDisplay();

return <BooleanDisplay value={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useCurrencyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useCurrencyFieldDisplay';
import { CurrencyDisplay } from '@/ui/field/display/components/CurrencyDisplay';

import { useCurrencyField } from '../../hooks/useCurrencyField';

export const CurrencyFieldDisplay = () => {
const { fieldValue } = useCurrencyField();
const { fieldValue } = useCurrencyFieldDisplay();

return <CurrencyDisplay currencyValue={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useDateFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useDateFieldDisplay';
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';

import { useDateField } from '../../hooks/useDateField';

export const DateFieldDisplay = () => {
const { fieldValue } = useDateField();
const { fieldValue } = useDateFieldDisplay();

return <DateDisplay value={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useDateTimeFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useDateTimeFieldDisplay';
import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';

import { useDateTimeField } from '../../hooks/useDateTimeField';

export const DateTimeFieldDisplay = () => {
const { fieldValue } = useDateTimeField();
const { fieldValue } = useDateTimeFieldDisplay();

return <DateTimeDisplay value={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useEmailFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useEmailFieldDisplay';
import { EmailDisplay } from '@/ui/field/display/components/EmailDisplay';

import { useEmailField } from '../../hooks/useEmailField';

export const EmailFieldDisplay = () => {
const { fieldValue } = useEmailField();
const { fieldValue } = useEmailFieldDisplay();

return <EmailDisplay value={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
import { isNonEmptyString } from '@sniptt/guards';

import { useFullNameFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useFullNameFieldDisplay';
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';

export const FullNameFieldDisplay = () => {
const { fieldValue } = useFullNameField();
const { fieldValue } = useFullNameFieldDisplay();

const content = [fieldValue.firstName, fieldValue.lastName]
.filter(Boolean)
const content = [fieldValue?.firstName, fieldValue?.lastName]
.filter(isNonEmptyString)
.join(' ');

return <TextDisplay text={content} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { useJsonField } from '@/object-record/record-field/meta-types/hooks/useJsonField';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { useJsonFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useJsonFieldDisplay';
import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay';
import { isDefined } from '~/utils/isDefined';

export const JsonFieldDisplay = () => {
const { fieldValue, maxWidth } = useJsonField();
const { fieldValue, maxWidth } = useJsonFieldDisplay();

return (
<JsonDisplay
text={
isFieldRawJsonValue(fieldValue) && isDefined(fieldValue)
? JSON.stringify(fieldValue)
: ''
}
maxWidth={maxWidth}
/>
);
if (!isDefined(fieldValue)) {
return <></>;
}

const value = JSON.stringify(fieldValue);

return <JsonDisplay text={value} maxWidth={maxWidth} />;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useLinkFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useLinkFieldDisplay';
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';

import { useLinkField } from '../../hooks/useLinkField';

export const LinkFieldDisplay = () => {
const { fieldValue } = useLinkField();
const { fieldValue } = useLinkFieldDisplay();

return <LinkDisplay value={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { useLinksFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useLinksFieldDisplay';
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';

export const LinksFieldDisplay = () => {
const { fieldValue } = useLinksField();
const { fieldValue } = useLinksFieldDisplay();

const { isFocused } = useFieldFocus();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
import { Tag } from 'twenty-ui';
import { styled } from '@linaria/react';
import { Tag, THEME_COMMON } from 'twenty-ui';

import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { useMultiSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useMultiSelectFieldDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';

const spacing1 = THEME_COMMON.spacing(1);

const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${spacing1};
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
width: 100%;
`;

export const MultiSelectFieldDisplay = () => {
const { fieldValues, fieldDefinition } = useMultiSelectField();
const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();

const { isFocused } = useFieldFocus();

const selectedOptions = fieldValues
const selectedOptions = fieldValue
? fieldDefinition.metadata.options?.filter((option) =>
fieldValues.includes(option.value),
fieldValue.includes(option.value),
)
: [];

if (!selectedOptions) return null;

return (
return isFocused ? (
<ExpandableList isChipCountDisplayed={isFocused}>
{selectedOptions.map((selectedOption, index) => (
<Tag
Expand All @@ -27,5 +43,16 @@ export const MultiSelectFieldDisplay = () => {
/>
))}
</ExpandableList>
) : (
<StyledContainer>
{selectedOptions.map((selectedOption, index) => (
<Tag
preventShrink
key={index}
color={selectedOption.color}
text={selectedOption.label}
/>
))}
</StyledContainer>
);
};
Loading

0 comments on commit 03b3c8a

Please sign in to comment.