feat: Implement dees-statsgrid in DCRouter UI for enhanced stats visualization
- Added new readme.statsgrid.md outlining the implementation plan for dees-statsgrid component. - Replaced custom stats cards in ops-view-overview.ts and ops-view-network.ts with dees-statsgrid for better visualization. - Introduced consistent color scheme for success, warning, error, and info states. - Enhanced interactive features including click actions, context menus, and real-time updates. - Developed ops-view-emails.ts for email management with features like composing, searching, and viewing emails. - Integrated mock data generation for emails and network requests to facilitate testing. - Added responsive design elements and improved UI consistency across components.
This commit is contained in:
697
ts_web/elements/ops-view-emails.ts
Normal file
697
ts_web/elements/ops-view-emails.ts
Normal file
@ -0,0 +1,697 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-emails': OpsViewEmails;
|
||||
}
|
||||
}
|
||||
|
||||
interface IEmail {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string[];
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
subject: string;
|
||||
body: string;
|
||||
html?: string;
|
||||
attachments?: Array<{
|
||||
filename: string;
|
||||
size: number;
|
||||
contentType: string;
|
||||
}>;
|
||||
date: number;
|
||||
read: boolean;
|
||||
folder: 'inbox' | 'sent' | 'draft' | 'trash';
|
||||
flags?: string[];
|
||||
messageId?: string;
|
||||
inReplyTo?: string;
|
||||
}
|
||||
|
||||
@customElement('ops-view-emails')
|
||||
export class OpsViewEmails extends DeesElement {
|
||||
@state()
|
||||
private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
|
||||
|
||||
@state()
|
||||
private emails: IEmail[] = [];
|
||||
|
||||
@state()
|
||||
private selectedEmail: IEmail | null = null;
|
||||
|
||||
@state()
|
||||
private showCompose = false;
|
||||
|
||||
@state()
|
||||
private isLoading = false;
|
||||
|
||||
@state()
|
||||
private searchTerm = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadEmails();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.emailContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr;
|
||||
gap: 24px;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.folderList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.folderItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.folderItem:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.folderItem.selected {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.folderIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.folderLabel {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.folderCount {
|
||||
background: #e0e0e0;
|
||||
color: #666;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.folderItem.selected .folderCount {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.emailToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.emailList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.emailPreview {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emailHeader {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.emailSubject {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.emailMeta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.emailBody {
|
||||
padding: 20px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.emailActions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.composeModal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.composeContent {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.composeHeader {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.composeTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.composeForm {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.composeActions {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 18px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Emails</ops-sectionheading>
|
||||
|
||||
<div class="emailContainer">
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<dees-button @click=${() => this.showCompose = true} type="highlighted">
|
||||
<dees-icon name="penToSquare" slot="iconSlot"></dees-icon>
|
||||
Compose
|
||||
</dees-button>
|
||||
|
||||
<div class="folderList">
|
||||
${this.renderFolderItem('inbox', 'inbox', 'Inbox', this.getEmailCount('inbox'))}
|
||||
${this.renderFolderItem('sent', 'paperPlane', 'Sent', this.getEmailCount('sent'))}
|
||||
${this.renderFolderItem('draft', 'file', 'Drafts', this.getEmailCount('draft'))}
|
||||
${this.renderFolderItem('trash', 'trash', 'Trash', this.getEmailCount('trash'))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="mainContent">
|
||||
<!-- Toolbar -->
|
||||
<div class="emailToolbar">
|
||||
<dees-input-text
|
||||
class="searchBox"
|
||||
placeholder="Search emails..."
|
||||
.value=${this.searchTerm}
|
||||
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
|
||||
>
|
||||
<dees-icon name="magnifyingGlass" slot="iconSlot"></dees-icon>
|
||||
</dees-input-text>
|
||||
|
||||
<dees-button @click=${() => this.refreshEmails()}>
|
||||
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : html`<dees-icon name="arrowsRotate"></dees-icon>`}
|
||||
</dees-button>
|
||||
|
||||
<dees-button @click=${() => this.markAllAsRead()}>
|
||||
<dees-icon name="envelopeOpen"></dees-icon>
|
||||
Mark all read
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<!-- Email List or Preview -->
|
||||
${this.selectedEmail ? this.renderEmailPreview() : this.renderEmailList()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compose Modal -->
|
||||
${this.showCompose ? this.renderComposeModal() : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderFolderItem(folder: string, icon: string, label: string, count: number) {
|
||||
return html`
|
||||
<div
|
||||
class="folderItem ${this.selectedFolder === folder ? 'selected' : ''}"
|
||||
@click=${() => this.selectFolder(folder as any)}
|
||||
>
|
||||
<dees-icon class="folderIcon" name="${icon}"></dees-icon>
|
||||
<span class="folderLabel">${label}</span>
|
||||
${count > 0 ? html`<span class="folderCount">${count}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailList() {
|
||||
const filteredEmails = this.getFilteredEmails();
|
||||
|
||||
if (filteredEmails.length === 0) {
|
||||
return html`
|
||||
<div class="emptyState">
|
||||
<dees-icon class="emptyIcon" name="envelope"></dees-icon>
|
||||
<div class="emptyText">No emails in ${this.selectedFolder}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="emailList">
|
||||
<dees-table
|
||||
.data=${filteredEmails}
|
||||
.displayFunction=${(email: IEmail) => ({
|
||||
'Status': html`<dees-icon name="${email.read ? 'envelopeOpen' : 'envelope'}" style="color: ${email.read ? '#999' : '#1976d2'}"></dees-icon>`,
|
||||
From: email.from,
|
||||
Subject: html`<strong style="${!email.read ? 'font-weight: 600' : ''}">${email.subject}</strong>`,
|
||||
Date: this.formatDate(email.date),
|
||||
'Attach': html`
|
||||
${email.attachments?.length ? html`<dees-icon name="paperclip" style="color: #666"></dees-icon>` : ''}
|
||||
`,
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Read',
|
||||
iconName: 'eye',
|
||||
type: ['doubleClick', 'inRow'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.selectedEmail = actionData.item;
|
||||
if (!actionData.item.read) {
|
||||
this.markAsRead(actionData.item.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Reply',
|
||||
iconName: 'reply',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.replyToEmail(actionData.item);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Forward',
|
||||
iconName: 'share',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.forwardEmail(actionData.item);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.deleteEmail(actionData.item.id);
|
||||
}
|
||||
}
|
||||
]}
|
||||
.selectionMode=${'single'}
|
||||
></dees-table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailPreview() {
|
||||
if (!this.selectedEmail) return '';
|
||||
|
||||
return html`
|
||||
<div class="emailPreview">
|
||||
<div class="emailHeader">
|
||||
<div class="emailSubject">${this.selectedEmail.subject}</div>
|
||||
<div class="emailMeta">
|
||||
<span><strong>From:</strong> ${this.selectedEmail.from}</span>
|
||||
<span><strong>To:</strong> ${this.selectedEmail.to.join(', ')}</span>
|
||||
<span><strong>Date:</strong> ${this.formatDate(this.selectedEmail.date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="emailBody">
|
||||
${this.selectedEmail.html ?
|
||||
html`<div .innerHTML=${this.selectedEmail.html}></div>` :
|
||||
html`<pre style="white-space: pre-wrap; font-family: inherit;">${this.selectedEmail.body}</pre>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="emailActions">
|
||||
<dees-button @click=${() => this.selectedEmail = null}>
|
||||
<dees-icon name="arrowLeft"></dees-icon>
|
||||
Back
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
|
||||
<dees-icon name="reply"></dees-icon>
|
||||
Reply
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
|
||||
<dees-icon name="replyAll"></dees-icon>
|
||||
Reply All
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
|
||||
<dees-icon name="share"></dees-icon>
|
||||
Forward
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
|
||||
<dees-icon name="trash"></dees-icon>
|
||||
Delete
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderComposeModal() {
|
||||
return html`
|
||||
<div class="composeModal" @click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) this.showCompose = false;
|
||||
}}>
|
||||
<div class="composeContent">
|
||||
<div class="composeHeader">
|
||||
<div class="composeTitle">New Email</div>
|
||||
<dees-button @click=${() => this.showCompose = false} type="ghost">
|
||||
<dees-icon name="xmark"></dees-icon>
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<div class="composeForm">
|
||||
<dees-form @formData=${(e: CustomEvent) => this.sendEmail(e.detail)}>
|
||||
<dees-input-tags
|
||||
key="to"
|
||||
label="To"
|
||||
placeholder="Enter recipient email addresses..."
|
||||
required
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
key="cc"
|
||||
label="CC"
|
||||
placeholder="Enter CC recipients..."
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
key="bcc"
|
||||
label="BCC"
|
||||
placeholder="Enter BCC recipients..."
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-text
|
||||
key="subject"
|
||||
label="Subject"
|
||||
placeholder="Enter email subject..."
|
||||
required
|
||||
></dees-input-text>
|
||||
|
||||
<dees-editor
|
||||
key="body"
|
||||
label="Message"
|
||||
.mode=${'markdown'}
|
||||
.height=${300}
|
||||
required
|
||||
></dees-editor>
|
||||
|
||||
<dees-input-fileupload
|
||||
key="attachments"
|
||||
label="Attachments"
|
||||
multiple
|
||||
></dees-input-fileupload>
|
||||
|
||||
<div class="composeActions">
|
||||
<dees-button @click=${() => this.showCompose = false} type="secondary">
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-form-submit>
|
||||
<dees-icon name="paperPlane"></dees-icon>
|
||||
Send Email
|
||||
</dees-form-submit>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getFilteredEmails(): IEmail[] {
|
||||
let emails = this.emails.filter(e => e.folder === this.selectedFolder);
|
||||
|
||||
if (this.searchTerm) {
|
||||
const search = this.searchTerm.toLowerCase();
|
||||
emails = emails.filter(e =>
|
||||
e.subject.toLowerCase().includes(search) ||
|
||||
e.from.toLowerCase().includes(search) ||
|
||||
e.body.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
return emails.sort((a, b) => b.date - a.date);
|
||||
}
|
||||
|
||||
private getEmailCount(folder: string): number {
|
||||
return this.emails.filter(e => e.folder === folder && !e.read).length;
|
||||
}
|
||||
|
||||
private selectFolder(folder: 'inbox' | 'sent' | 'draft' | 'trash') {
|
||||
this.selectedFolder = folder;
|
||||
this.selectedEmail = null;
|
||||
}
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = diff / (1000 * 60 * 60);
|
||||
|
||||
if (hours < 24) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (hours < 168) { // 7 days
|
||||
return date.toLocaleDateString([], { weekday: 'short' });
|
||||
} else {
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEmails() {
|
||||
// TODO: Load real emails from server
|
||||
// For now, generate mock data
|
||||
this.generateMockEmails();
|
||||
}
|
||||
|
||||
private async refreshEmails() {
|
||||
this.isLoading = true;
|
||||
await this.loadEmails();
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private async sendEmail(formData: any) {
|
||||
|
||||
try {
|
||||
// TODO: Implement actual email sending
|
||||
console.log('Sending email:', formData);
|
||||
|
||||
// Add to sent folder
|
||||
const newEmail: IEmail = {
|
||||
id: `email-${Date.now()}`,
|
||||
from: 'me@dcrouter.local',
|
||||
to: formData.to,
|
||||
cc: formData.cc || [],
|
||||
bcc: formData.bcc || [],
|
||||
subject: formData.subject,
|
||||
body: formData.body,
|
||||
date: Date.now(),
|
||||
read: true,
|
||||
folder: 'sent',
|
||||
};
|
||||
|
||||
this.emails = [...this.emails, newEmail];
|
||||
this.showCompose = false;
|
||||
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.log('Email sent successfully');
|
||||
} catch (error: any) {
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.error('Failed to send email', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async markAsRead(emailId: string) {
|
||||
const email = this.emails.find(e => e.id === emailId);
|
||||
if (email) {
|
||||
email.read = true;
|
||||
this.emails = [...this.emails];
|
||||
}
|
||||
}
|
||||
|
||||
private async markAllAsRead() {
|
||||
this.emails = this.emails.map(e =>
|
||||
e.folder === this.selectedFolder ? { ...e, read: true } : e
|
||||
);
|
||||
}
|
||||
|
||||
private async deleteEmail(emailId: string) {
|
||||
const email = this.emails.find(e => e.id === emailId);
|
||||
if (email) {
|
||||
if (email.folder === 'trash') {
|
||||
// Permanently delete
|
||||
this.emails = this.emails.filter(e => e.id !== emailId);
|
||||
} else {
|
||||
// Move to trash
|
||||
email.folder = 'trash';
|
||||
this.emails = [...this.emails];
|
||||
}
|
||||
|
||||
if (this.selectedEmail?.id === emailId) {
|
||||
this.selectedEmail = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async replyToEmail(email: IEmail) {
|
||||
// TODO: Open compose with reply context
|
||||
this.showCompose = true;
|
||||
}
|
||||
|
||||
private async replyAllToEmail(email: IEmail) {
|
||||
// TODO: Open compose with reply all context
|
||||
this.showCompose = true;
|
||||
}
|
||||
|
||||
private async forwardEmail(email: IEmail) {
|
||||
// TODO: Open compose with forward context
|
||||
this.showCompose = true;
|
||||
}
|
||||
|
||||
private generateMockEmails() {
|
||||
const subjects = [
|
||||
'Server Alert: High CPU Usage',
|
||||
'Daily Report - Network Activity',
|
||||
'Security Update Required',
|
||||
'New User Registration',
|
||||
'Backup Completed Successfully',
|
||||
'DNS Query Spike Detected',
|
||||
'SSL Certificate Renewal Notice',
|
||||
'Monthly Usage Summary',
|
||||
];
|
||||
|
||||
const senders = [
|
||||
'monitoring@dcrouter.local',
|
||||
'alerts@system.local',
|
||||
'admin@company.com',
|
||||
'noreply@service.com',
|
||||
'support@vendor.com',
|
||||
];
|
||||
|
||||
const bodies = [
|
||||
'This is an automated alert regarding your server status.',
|
||||
'Please review the attached report for detailed information.',
|
||||
'Action required: Update your security settings.',
|
||||
'Your daily summary is ready for review.',
|
||||
'All systems are operating normally.',
|
||||
];
|
||||
|
||||
this.emails = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `email-${i}`,
|
||||
from: senders[Math.floor(Math.random() * senders.length)],
|
||||
to: ['admin@dcrouter.local'],
|
||||
subject: subjects[Math.floor(Math.random() * subjects.length)],
|
||||
body: bodies[Math.floor(Math.random() * bodies.length)],
|
||||
date: Date.now() - (i * 3600000), // 1 hour apart
|
||||
read: Math.random() > 0.3,
|
||||
folder: i < 40 ? 'inbox' : i < 45 ? 'sent' : 'trash',
|
||||
attachments: Math.random() > 0.8 ? [{
|
||||
filename: 'report.pdf',
|
||||
size: 1024 * 1024,
|
||||
contentType: 'application/pdf',
|
||||
}] : undefined,
|
||||
}));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user