feat: europeanize mock JMAP data and refine email list layout
Replace all mock data in the dev JMAP server with European-themed content: 42 emails (was 13), 20 contacts across 2 address books (was 3), 5 calendars and 35 events with recurrence, overlaps, and all-day entries. Move keyword tags in email/thread list items from the subject line to the header row alongside the date for a cleaner layout. Update test assertions to match the new mock data.
This commit is contained in:
+1018
-154
File diff suppressed because one or more lines are too long
@@ -147,35 +147,35 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={cn(
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
"text-xs flex-shrink-0 tabular-nums",
|
{keywordDef && (
|
||||||
isUnread
|
<span className={cn(
|
||||||
? "text-foreground font-semibold"
|
"inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-full",
|
||||||
: "text-muted-foreground"
|
KEYWORD_PALETTE[keywordDef.color]?.bg || "bg-muted"
|
||||||
)}>
|
)}>
|
||||||
{formatDate(email.receivedAt)}
|
<span className={cn("w-1.5 h-1.5 rounded-full", KEYWORD_PALETTE[keywordDef.color]?.dot || "bg-gray-400")} />
|
||||||
</span>
|
{keywordDef.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={cn(
|
||||||
|
"text-xs tabular-nums",
|
||||||
|
isUnread
|
||||||
|
? "text-foreground font-semibold"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{formatDate(email.receivedAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Second Line: Subject + Tag */}
|
{/* Second Line: Subject */}
|
||||||
<div className="flex items-center gap-1.5 mb-1">
|
<div className={cn(
|
||||||
<span className={cn(
|
"mb-1 line-clamp-1 text-sm",
|
||||||
"line-clamp-1 text-sm min-w-0",
|
isUnread
|
||||||
isUnread
|
? "font-semibold text-foreground"
|
||||||
? "font-semibold text-foreground"
|
: "font-normal text-foreground/90"
|
||||||
: "font-normal text-foreground/90"
|
)}>
|
||||||
)}>
|
{email.subject || t('no_subject')}
|
||||||
{email.subject || t('no_subject')}
|
|
||||||
</span>
|
|
||||||
{keywordDef && (
|
|
||||||
<span className={cn(
|
|
||||||
"flex-shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-full",
|
|
||||||
KEYWORD_PALETTE[keywordDef.color]?.bg || "bg-muted"
|
|
||||||
)}>
|
|
||||||
<span className={cn("w-1.5 h-1.5 rounded-full", KEYWORD_PALETTE[keywordDef.color]?.dot || "bg-gray-400")} />
|
|
||||||
{keywordDef.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Third Line: Preview (controlled by showPreview setting) */}
|
{/* Third Line: Preview (controlled by showPreview setting) */}
|
||||||
|
|||||||
@@ -129,34 +129,34 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={cn(
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
"text-xs flex-shrink-0 tabular-nums",
|
{resolvedKeywordDef && (
|
||||||
isUnread
|
<span className={cn(
|
||||||
? "text-foreground font-semibold"
|
"inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-full",
|
||||||
: "text-muted-foreground"
|
KEYWORD_PALETTE[resolvedKeywordDef.color]?.bg || "bg-muted"
|
||||||
)}>
|
)}>
|
||||||
{formatDate(email.receivedAt)}
|
<span className={cn("w-1.5 h-1.5 rounded-full", KEYWORD_PALETTE[resolvedKeywordDef.color]?.dot || "bg-gray-400")} />
|
||||||
</span>
|
{resolvedKeywordDef.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={cn(
|
||||||
|
"text-xs tabular-nums",
|
||||||
|
isUnread
|
||||||
|
? "text-foreground font-semibold"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{formatDate(email.receivedAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 mb-1">
|
<div className={cn(
|
||||||
<span className={cn(
|
"mb-1 line-clamp-1 text-sm",
|
||||||
"line-clamp-1 text-sm min-w-0",
|
isUnread
|
||||||
isUnread
|
? "font-semibold text-foreground"
|
||||||
? "font-semibold text-foreground"
|
: "font-normal text-foreground/90"
|
||||||
: "font-normal text-foreground/90"
|
)}>
|
||||||
)}>
|
{email.subject || "(no subject)"}
|
||||||
{email.subject || "(no subject)"}
|
|
||||||
</span>
|
|
||||||
{resolvedKeywordDef && (
|
|
||||||
<span className={cn(
|
|
||||||
"flex-shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-full",
|
|
||||||
KEYWORD_PALETTE[resolvedKeywordDef.color]?.bg || "bg-muted"
|
|
||||||
)}>
|
|
||||||
<span className={cn("w-1.5 h-1.5 rounded-full", KEYWORD_PALETTE[resolvedKeywordDef.color]?.dot || "bg-gray-400")} />
|
|
||||||
{resolvedKeywordDef.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPreview && (
|
{showPreview && (
|
||||||
@@ -354,34 +354,34 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={cn(
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
"text-xs flex-shrink-0 tabular-nums",
|
{keywordDef && (
|
||||||
hasUnread
|
<span className={cn(
|
||||||
? "text-foreground font-semibold"
|
"inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-full",
|
||||||
: "text-muted-foreground"
|
KEYWORD_PALETTE[keywordDef.color]?.bg || "bg-muted"
|
||||||
)}>
|
)}>
|
||||||
{formatDate(latestEmail.receivedAt)}
|
<span className={cn("w-1.5 h-1.5 rounded-full", KEYWORD_PALETTE[keywordDef.color]?.dot || "bg-gray-400")} />
|
||||||
</span>
|
{keywordDef.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={cn(
|
||||||
|
"text-xs tabular-nums",
|
||||||
|
hasUnread
|
||||||
|
? "text-foreground font-semibold"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{formatDate(latestEmail.receivedAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 mb-1">
|
<div className={cn(
|
||||||
<span className={cn(
|
"mb-1 line-clamp-1 text-sm",
|
||||||
"line-clamp-1 text-sm min-w-0",
|
hasUnread
|
||||||
hasUnread
|
? "font-semibold text-foreground"
|
||||||
? "font-semibold text-foreground"
|
: "font-normal text-foreground/90"
|
||||||
: "font-normal text-foreground/90"
|
)}>
|
||||||
)}>
|
{latestEmail.subject || "(no subject)"}
|
||||||
{latestEmail.subject || "(no subject)"}
|
|
||||||
</span>
|
|
||||||
{keywordDef && (
|
|
||||||
<span className={cn(
|
|
||||||
"flex-shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-full",
|
|
||||||
KEYWORD_PALETTE[keywordDef.color]?.bg || "bg-muted"
|
|
||||||
)}>
|
|
||||||
<span className={cn("w-1.5 h-1.5 rounded-full", KEYWORD_PALETTE[keywordDef.color]?.dot || "bg-gray-400")} />
|
|
||||||
{keywordDef.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPreview && (
|
{showPreview && (
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ describe('dev-jmap mock server', () => {
|
|||||||
|
|
||||||
describe('GET /download', () => {
|
describe('GET /download', () => {
|
||||||
it('should return a response with attachment disposition', async () => {
|
it('should return a response with attachment disposition', async () => {
|
||||||
const req = makeRequest('http://localhost:3000/api/dev-jmap/download/dev-account-001/blob-att-001/Q1-Report.pdf?accept=application/pdf', {
|
const req = makeRequest('http://localhost:3000/api/dev-jmap/download/dev-account-001/blob-att-001/Q1-Bericht.pdf?accept=application/pdf', {
|
||||||
headers: { host: 'localhost:3000' },
|
headers: { host: 'localhost:3000' },
|
||||||
});
|
});
|
||||||
const res = await GET(req, { params: Promise.resolve({ path: ['download', 'dev-account-001', 'blob-att-001', 'Q1-Report.pdf'] }) });
|
const res = await GET(req, { params: Promise.resolve({ path: ['download', 'dev-account-001', 'blob-att-001', 'Q1-Bericht.pdf'] }) });
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.headers.get('Content-Type')).toBe('application/pdf');
|
expect(res.headers.get('Content-Type')).toBe('application/pdf');
|
||||||
expect(res.headers.get('Content-Disposition')).toContain('Q1-Report.pdf');
|
expect(res.headers.get('Content-Disposition')).toContain('Q1-Bericht.pdf');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ describe('dev-jmap mock server', () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', host: 'localhost:3000' },
|
headers: { 'Content-Type': 'application/json', host: 'localhost:3000' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
methodCalls: [['Email/query', { accountId: 'dev-account-001', filter: { text: 'welcome' } }, 'c0']],
|
methodCalls: [['Email/query', { accountId: 'dev-account-001', filter: { text: 'willkommen' } }, 'c0']],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const res = await POST(req, { params: Promise.resolve({ path: ['api'] }) });
|
const res = await POST(req, { params: Promise.resolve({ path: ['api'] }) });
|
||||||
@@ -130,7 +130,7 @@ describe('dev-jmap mock server', () => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
expect(data.methodResponses[0][0]).toBe('Email/get');
|
expect(data.methodResponses[0][0]).toBe('Email/get');
|
||||||
expect(data.methodResponses[0][1].list).toHaveLength(1);
|
expect(data.methodResponses[0][1].list).toHaveLength(1);
|
||||||
expect(data.methodResponses[0][1].list[0].subject).toContain('Welcome');
|
expect(data.methodResponses[0][1].list[0].subject).toContain('Willkommen');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter properties when specified', async () => {
|
it('should filter properties when specified', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user