feat: drop empty states and instead render nothing (#17049)

Dropping the empty state as currently seen in production.

## Before

<img width="3738" height="2442" alt="CleanShot 2026-01-09 at 16 09
37@2x"
src="https://github.com/user-attachments/assets/ed6e2132-4a07-4673-ab59-dea37e9949e9"
/>

## After


https://github.com/user-attachments/assets/32566a17-d5b6-4d62-80d5-144ba0050276
This commit is contained in:
Baptiste Devessier
2026-01-09 17:55:17 +01:00
committed by GitHub
parent 9a2f18660f
commit b906b4353c
5 changed files with 28 additions and 311 deletions
@@ -20,15 +20,7 @@ import { generateFieldWidgetInstanceId } from '@/page-layout/widgets/field/utils
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
type FieldWidgetMorphRelationCardProps = {
fieldDefinition: FieldDefinition<FieldMorphRelationMetadata>;
@@ -115,21 +107,7 @@ export const FieldWidgetMorphRelationCard = ({
);
if (validRecords.length === 0) {
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{t`No related records`}
</AnimatedPlaceholderEmptyTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
</RightDrawerProvider>
);
return null;
}
return (
@@ -4,14 +4,6 @@ import { type FieldDefinition } from '@/object-record/record-field/ui/types/Fiel
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -44,38 +36,22 @@ export const FieldWidgetMorphRelationField = ({
morphRelations: fieldMetadata.morphRelations,
});
if (recordsWithObjectNameSingular.length > 0) {
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<StyledContainer>
<StyledRelationChipsContainer>
{recordsWithObjectNameSingular.map((morphItem, index) => (
<RecordChip
key={morphItem.value?.id ?? index}
objectNameSingular={morphItem.objectNameSingular}
record={morphItem.value}
/>
))}
</StyledRelationChipsContainer>
</StyledContainer>
</RightDrawerProvider>
);
if (recordsWithObjectNameSingular.length === 0) {
return null;
}
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<StyledContainer>
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{t`No related records`}
</AnimatedPlaceholderEmptyTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
<StyledRelationChipsContainer>
{recordsWithObjectNameSingular.map((morphItem, index) => (
<RecordChip
key={morphItem.value?.id ?? index}
objectNameSingular={morphItem.objectNameSingular}
record={morphItem.value}
/>
))}
</StyledRelationChipsContainer>
</StyledContainer>
</RightDrawerProvider>
);
@@ -18,15 +18,7 @@ import { generateFieldWidgetInstanceId } from '@/page-layout/widgets/field/utils
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
type FieldWidgetRelationCardProps = {
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
@@ -112,21 +104,7 @@ export const FieldWidgetRelationCard = ({
: [];
if (records.length === 0) {
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{t`No related records`}
</AnimatedPlaceholderEmptyTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
</RightDrawerProvider>
);
return null;
}
return (
@@ -3,15 +3,7 @@ import { type FieldDefinition } from '@/object-record/record-field/ui/types/Fiel
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -41,7 +33,15 @@ export const FieldWidgetRelationField = ({
const relationObjectNameSingular =
fieldMetadata.relationObjectMetadataNameSingular;
if (isOneToMany && Array.isArray(relationValue)) {
if (isOneToMany && !Array.isArray(relationValue)) {
return null;
}
if (!isOneToMany && !isDefined(relationValue)) {
return null;
}
if (isOneToMany) {
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<StyledContainer>
@@ -59,35 +59,15 @@ export const FieldWidgetRelationField = ({
);
}
if (!isOneToMany && isDefined(relationValue)) {
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<StyledContainer>
<StyledRelationChipsContainer>
<RecordChip
objectNameSingular={relationObjectNameSingular}
record={relationValue}
/>
</StyledRelationChipsContainer>
</StyledContainer>
</RightDrawerProvider>
);
}
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<StyledContainer>
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{t`No related records`}
</AnimatedPlaceholderEmptyTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
<StyledRelationChipsContainer>
<RecordChip
objectNameSingular={relationObjectNameSingular}
record={relationValue}
/>
</StyledRelationChipsContainer>
</StyledContainer>
</RightDrawerProvider>
);
@@ -1762,103 +1762,6 @@ export const OneToManyRelationCardWidget: Story = {
},
};
export const ManyToOneRelationCardWidgetEmpty: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
id: 'widget-relation-card-empty',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
title: 'Account Owner',
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 4,
column: 0,
rowSpan: 1,
columnSpan: 2,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: accountOwnerField.id,
layout: 'CARD',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
};
const initializeState = (snapshot: MutableSnapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
snapshot.set(shouldAppBeLoadingState, false);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
snapshot.set(
pageLayoutDraftComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), {
...mockCompanyRecord,
accountOwner: null,
});
};
return (
<div style={{ width: '400px', padding: '20px' }}>
<JestMetadataAndApolloMocksWrapper>
<CoreClientProviderWrapper>
<PageLayoutTestWrapper initializeState={initializeState}>
<LayoutRenderingProvider
value={{
isInRightDrawer: false,
layoutType: PageLayoutType.RECORD_PAGE,
targetRecordIdentifier: {
id: TEST_RECORD_ID,
targetObjectNameSingular:
companyObjectMetadataItem.nameSingular,
},
}}
>
<PageLayoutContentProvider
value={{
layoutMode: 'vertical-list',
tabId: 'fields',
}}
>
<WidgetComponentInstanceContext.Provider
value={{ instanceId: widget.id }}
>
<FieldWidget widget={widget} />
</WidgetComponentInstanceContext.Provider>
</PageLayoutContentProvider>
</LayoutRenderingProvider>
</PageLayoutTestWrapper>
</CoreClientProviderWrapper>
</JestMetadataAndApolloMocksWrapper>
</div>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(() => {
const emptyPlaceholder = canvas.queryByText('No related records');
expect(emptyPlaceholder).toBeInTheDocument();
});
},
};
export const TimelineActivityRelationCardWidget: Story = {
render: () => {
const widget: PageLayoutWidget = {
@@ -1966,101 +1869,3 @@ export const TimelineActivityRelationCardWidget: Story = {
});
},
};
export const TimelineActivityRelationCardWidgetEmpty: Story = {
render: () => {
const widget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
id: 'widget-timeline-activity-relation-card-empty',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELD,
title: 'Workspace Member',
objectMetadataId: timelineActivityObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 12,
column: 0,
rowSpan: 1,
columnSpan: 2,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: timelineActivityWorkspaceMemberField.id,
layout: 'CARD',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
};
const initializeState = (snapshot: MutableSnapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
snapshot.set(shouldAppBeLoadingState, false);
const pageLayoutData = createPageLayoutWithWidget(
widget,
timelineActivityObjectMetadataItem.id,
);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
snapshot.set(
pageLayoutDraftComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_TIMELINE_ACTIVITY_RECORD_ID), {
...mockTimelineActivityRecord,
workspaceMember: null,
});
};
return (
<div style={{ width: '400px', padding: '20px' }}>
<JestMetadataAndApolloMocksWrapper>
<CoreClientProviderWrapper>
<PageLayoutTestWrapper initializeState={initializeState}>
<LayoutRenderingProvider
value={{
isInRightDrawer: false,
layoutType: PageLayoutType.RECORD_PAGE,
targetRecordIdentifier: {
id: TEST_TIMELINE_ACTIVITY_RECORD_ID,
targetObjectNameSingular:
timelineActivityObjectMetadataItem.nameSingular,
},
}}
>
<PageLayoutContentProvider
value={{
layoutMode: 'vertical-list',
tabId: 'fields',
}}
>
<WidgetComponentInstanceContext.Provider
value={{ instanceId: widget.id }}
>
<FieldWidget widget={widget} />
</WidgetComponentInstanceContext.Provider>
</PageLayoutContentProvider>
</LayoutRenderingProvider>
</PageLayoutTestWrapper>
</CoreClientProviderWrapper>
</JestMetadataAndApolloMocksWrapper>
</div>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emptyPlaceholder = await canvas.findByText('No related records');
await waitFor(() => {
expect(emptyPlaceholder).toBeVisible();
});
},
};