Rework website EmailsVisual to match twenty-front emails tab (#22146)
- Rework `EmailsVisual` to match twenty-front's emails tab: bordered thread card with dividers, `Inbox` header sizing, and twenty-front's row/sender/date colors and hard-clipped sender names (kept `+ Compose`). - Replace the "not shared" row with four fully-populated shared threads (subjects/bodies sourced from twenty-server's dev seed), two participants each, full `MMM D, YYYY` dates. <img width="575" height="607" alt="image" src="https://github.com/user-attachments/assets/68d1fd6d-e030-4f3c-b775-687f7e1ab173" />
This commit is contained in:
+48
@@ -0,0 +1,48 @@
|
||||
import { i18n } from '@lingui/core';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { EmailsVisual } from './EmailsVisual';
|
||||
|
||||
i18n.load('en', {});
|
||||
i18n.activate('en');
|
||||
|
||||
function renderVisual() {
|
||||
return render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<EmailsVisual active />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('EmailsVisual', () => {
|
||||
it('renders the inbox chrome', () => {
|
||||
renderVisual();
|
||||
|
||||
expect(screen.getByText('Compose')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Inbox', { exact: false, selector: 'span' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders every thread subject', () => {
|
||||
renderVisual();
|
||||
|
||||
expect(screen.getByText('Partnerships - Q4 Strategy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Proposal Submission')).toBeInTheDocument();
|
||||
expect(screen.getByText('Follow-up on Discussion')).toBeInTheDocument();
|
||||
expect(screen.getByText('Customer Feedback')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders thread previews, participants and dates', () => {
|
||||
renderVisual();
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Hey team, I've been in touch with Notion and Figma about potential integrations.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Félix, Thomas')).toBeInTheDocument();
|
||||
expect(screen.getByText('Jun 24, 2026')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+17
-12
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
import { THEME_LIGHT } from 'twenty-ui/theme';
|
||||
@@ -41,14 +43,13 @@ const InboxTitle = styled.span`
|
||||
align-items: baseline;
|
||||
color: ${THEME_LIGHT.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.lg)};
|
||||
font-weight: ${THEME_LIGHT.font.weight.semiBold};
|
||||
gap: 6px;
|
||||
gap: ${THEME_LIGHT.spacing(2)};
|
||||
`;
|
||||
|
||||
const InboxCount = styled.span`
|
||||
color: ${THEME_LIGHT.font.color.light};
|
||||
font-weight: ${THEME_LIGHT.font.weight.regular};
|
||||
`;
|
||||
|
||||
const ComposeButton = styled.span`
|
||||
@@ -63,34 +64,38 @@ const ComposeButton = styled.span`
|
||||
padding: 4px 8px;
|
||||
`;
|
||||
|
||||
const ThreadList = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
const ThreadCard = styled.div`
|
||||
border: 1px solid ${THEME_LIGHT.border.color.medium};
|
||||
border-radius: ${THEME_LIGHT.border.radius.sm};
|
||||
margin: 0 16px;
|
||||
overflow: hidden;
|
||||
|
||||
& > * + * {
|
||||
border-top: 1px solid ${THEME_LIGHT.border.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
export function EmailsVisual({ active: _active }: { active: boolean }) {
|
||||
const { i18n } = useLingui();
|
||||
return (
|
||||
<Root>
|
||||
<RecordTabHeader active="Emails" />
|
||||
<Panel>
|
||||
<InboxHeader>
|
||||
<InboxTitle>
|
||||
Inbox
|
||||
{i18n._(msg`Inbox`)}
|
||||
<InboxCount>{THREADS.length}</InboxCount>
|
||||
</InboxTitle>
|
||||
<ComposeButton>
|
||||
<IconPlus size={12} stroke={2} />
|
||||
Compose
|
||||
{i18n._(msg`Compose`)}
|
||||
</ComposeButton>
|
||||
</InboxHeader>
|
||||
<ThreadList>
|
||||
<ThreadCard>
|
||||
{THREADS.map((thread) => (
|
||||
<ThreadRow key={thread.date} thread={thread} />
|
||||
))}
|
||||
</ThreadList>
|
||||
</ThreadCard>
|
||||
</Panel>
|
||||
</Root>
|
||||
);
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { i18n } from '@lingui/core';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { type EmailThread } from '../types/email-thread';
|
||||
import { ThreadRow } from './ThreadRow';
|
||||
|
||||
i18n.load('en', {});
|
||||
i18n.activate('en');
|
||||
|
||||
const THREAD: EmailThread = {
|
||||
date: 'Jan 1, 2026',
|
||||
messageCount: 3,
|
||||
participants: [
|
||||
{ avatarUrl: '', name: 'Ada' },
|
||||
{ avatarUrl: '', name: 'Linus' },
|
||||
],
|
||||
preview: { id: 'A short preview.' },
|
||||
subject: { id: 'A subject line' },
|
||||
};
|
||||
|
||||
describe('ThreadRow', () => {
|
||||
it('renders the subject, preview, participants, count and date', () => {
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ThreadRow thread={THREAD} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('A subject line')).toBeInTheDocument();
|
||||
expect(screen.getByText('A short preview.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ada, Linus')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByText('Jan 1, 2026')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+17
-44
@@ -1,33 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconLock } from '@tabler/icons-react';
|
||||
import { THEME_LIGHT } from 'twenty-ui/theme';
|
||||
|
||||
import { PersonAvatar } from '@/app-preview/primitives/PersonAvatar';
|
||||
import { previewFontSize } from '@/app-preview/preview-font-size';
|
||||
import { PersonAvatar } from '@/app-preview/primitives/PersonAvatar';
|
||||
|
||||
import { type EmailThread } from '../types/email-thread';
|
||||
|
||||
const Row = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${THEME_LIGHT.background.secondary};
|
||||
box-sizing: border-box;
|
||||
color: ${THEME_LIGHT.font.color.primary};
|
||||
color: ${THEME_LIGHT.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
|
||||
gap: 8px;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${THEME_LIGHT.background.transparent.light};
|
||||
background-color: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const Heading = styled.div`
|
||||
const Sender = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
max-width: 40%;
|
||||
flex: 0 0 20%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
const AvatarStack = styled.div`
|
||||
@@ -41,18 +44,15 @@ const AvatarStack = styled.div`
|
||||
`;
|
||||
|
||||
const SenderNames = styled.span`
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
|
||||
margin: 0 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const ThreadCount = styled.span`
|
||||
const MessageCount = styled.span`
|
||||
color: ${THEME_LIGHT.font.color.tertiary};
|
||||
flex-shrink: 0;
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
|
||||
`;
|
||||
|
||||
const SubjectBody = styled.div`
|
||||
@@ -66,9 +66,6 @@ const SubjectBody = styled.div`
|
||||
|
||||
const Subject = styled.span`
|
||||
color: ${THEME_LIGHT.font.color.primary};
|
||||
flex-shrink: 0;
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -77,29 +74,13 @@ const Subject = styled.span`
|
||||
const Preview = styled.span`
|
||||
color: ${THEME_LIGHT.font.color.tertiary};
|
||||
flex: 1;
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const NotShared = styled.span`
|
||||
align-items: center;
|
||||
background: ${THEME_LIGHT.background.transparent.lighter};
|
||||
border: 1px solid ${THEME_LIGHT.border.color.light};
|
||||
border-radius: ${THEME_LIGHT.border.radius.sm};
|
||||
color: ${THEME_LIGHT.font.color.tertiary};
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
|
||||
gap: 4px;
|
||||
height: 20px;
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
const ReceivedAt = styled.span`
|
||||
color: ${THEME_LIGHT.font.color.tertiary};
|
||||
flex-shrink: 0;
|
||||
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
|
||||
padding: 0 4px;
|
||||
@@ -107,9 +88,10 @@ const ReceivedAt = styled.span`
|
||||
`;
|
||||
|
||||
export function ThreadRow({ thread }: { thread: EmailThread }) {
|
||||
const { i18n } = useLingui();
|
||||
return (
|
||||
<Row>
|
||||
<Heading>
|
||||
<Sender>
|
||||
<AvatarStack>
|
||||
{thread.participants.map((participant) => (
|
||||
<PersonAvatar
|
||||
@@ -127,20 +109,11 @@ export function ThreadRow({ thread }: { thread: EmailThread }) {
|
||||
.map((participant) => participant.name)
|
||||
.join(', ')}
|
||||
</SenderNames>
|
||||
<ThreadCount>{thread.messageCount}</ThreadCount>
|
||||
</Heading>
|
||||
<MessageCount>{thread.messageCount}</MessageCount>
|
||||
</Sender>
|
||||
<SubjectBody>
|
||||
{thread.shared ? (
|
||||
<>
|
||||
<Subject>{thread.subject}</Subject>
|
||||
<Preview>{thread.preview}</Preview>
|
||||
</>
|
||||
) : (
|
||||
<NotShared>
|
||||
<IconLock size={14} stroke={2} />
|
||||
Not shared
|
||||
</NotShared>
|
||||
)}
|
||||
<Subject>{i18n._(thread.subject)}</Subject>
|
||||
<Preview>{i18n._(thread.preview)}</Preview>
|
||||
</SubjectBody>
|
||||
<ReceivedAt>{thread.date}</ReceivedAt>
|
||||
</Row>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { sharedAssetUrls } from '@/app-preview/data/shared-asset-urls';
|
||||
|
||||
import { type EmailThread } from '../types/email-thread';
|
||||
@@ -6,21 +8,43 @@ const PEOPLE = sharedAssetUrls.peopleAvatars;
|
||||
|
||||
export const THREADS: EmailThread[] = [
|
||||
{
|
||||
date: '1:30pm',
|
||||
messageCount: 2,
|
||||
participants: [{ avatarUrl: PEOPLE.anonymousMike, name: 'Mike' }],
|
||||
shared: false,
|
||||
},
|
||||
{
|
||||
date: '4 nov 2023',
|
||||
date: 'Jun 24, 2026',
|
||||
messageCount: 4,
|
||||
participants: [
|
||||
{ avatarUrl: PEOPLE.anonymousFelix, name: 'Félix' },
|
||||
{ avatarUrl: PEOPLE.anonymousThomas, name: 'Thomas' },
|
||||
],
|
||||
preview:
|
||||
"Hey team, I've been in touch with Notion and Figma about potential integrations.",
|
||||
shared: true,
|
||||
subject: 'Partnerships - Q4 Strategy',
|
||||
preview: msg`Hey team, I've been in touch with Notion and Figma about potential integrations.`,
|
||||
subject: msg`Partnerships - Q4 Strategy`,
|
||||
},
|
||||
{
|
||||
date: 'Jun 23, 2026',
|
||||
messageCount: 2,
|
||||
participants: [
|
||||
{ avatarUrl: PEOPLE.anonymousLaura, name: 'Laura' },
|
||||
{ avatarUrl: PEOPLE.anonymousIndira, name: 'Indira' },
|
||||
],
|
||||
preview: msg`Dear Team, I am pleased to submit our proposal for your consideration. We have carefully reviewed your requirements.`,
|
||||
subject: msg`Proposal Submission`,
|
||||
},
|
||||
{
|
||||
date: 'Jun 20, 2026',
|
||||
messageCount: 3,
|
||||
participants: [
|
||||
{ avatarUrl: PEOPLE.anonymousIndira, name: 'Indira' },
|
||||
{ avatarUrl: PEOPLE.anonymousMike, name: 'Mike' },
|
||||
],
|
||||
preview: msg`Hi, I wanted to follow up on our conversation from last week regarding the new initiative.`,
|
||||
subject: msg`Follow-up on Discussion`,
|
||||
},
|
||||
{
|
||||
date: 'Jun 18, 2026',
|
||||
messageCount: 1,
|
||||
participants: [
|
||||
{ avatarUrl: PEOPLE.anonymousThomas, name: 'Thomas' },
|
||||
{ avatarUrl: PEOPLE.anonymousLaura, name: 'Laura' },
|
||||
],
|
||||
preview: msg`Hello, I wanted to share some positive feedback from our recent customer satisfaction survey.`,
|
||||
subject: msg`Customer Feedback`,
|
||||
},
|
||||
];
|
||||
|
||||
+4
-3
@@ -1,3 +1,5 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
type Participant = {
|
||||
avatarUrl: string;
|
||||
name: string;
|
||||
@@ -7,7 +9,6 @@ export type EmailThread = {
|
||||
date: string;
|
||||
messageCount: number;
|
||||
participants: Participant[];
|
||||
preview?: string;
|
||||
shared: boolean;
|
||||
subject?: string;
|
||||
preview: MessageDescriptor;
|
||||
subject: MessageDescriptor;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user