fix: update dependencies and improve email view layout in OpsViewEmails component

This commit is contained in:
Juergen Kunz
2025-06-17 14:37:05 +00:00
parent 8ce6c88d58
commit 2ab2e30336
3 changed files with 304 additions and 284 deletions

View File

@ -94,15 +94,19 @@ export class OpsViewEmails extends DeesElement {
.searchBox {
flex: 1;
min-width: 200px;
max-width: 400px;
}
.emailList {
flex: 1;
overflow-y: auto;
overflow: hidden;
}
.emailPreview {
flex: 1;
display: flex;
flex-direction: column;
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
@ -110,89 +114,51 @@ export class OpsViewEmails extends DeesElement {
}
.emailHeader {
padding: 20px;
padding: 24px;
border-bottom: 1px solid #e9ecef;
background: #fafafa;
}
.emailSubject {
font-size: 20px;
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
margin-bottom: 16px;
color: #333;
}
.emailMeta {
display: flex;
gap: 16px;
flex-direction: column;
gap: 8px;
font-size: 14px;
color: #666;
}
.emailMetaRow {
display: flex;
gap: 8px;
}
.emailMetaLabel {
font-weight: 600;
min-width: 60px;
}
.emailBody {
padding: 20px;
max-height: 500px;
flex: 1;
padding: 24px;
overflow-y: auto;
font-size: 15px;
line-height: 1.6;
}
.emailActions {
display: flex;
gap: 8px;
padding: 16px;
padding: 16px 24px;
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;
@ -220,63 +186,77 @@ export class OpsViewEmails extends DeesElement {
<div class="emailContainer">
<!-- Sidebar -->
<div class="sidebar">
<dees-button @click=${() => this.showCompose = true} type="highlighted">
<dees-windowbox>
<dees-button @click=${() => this.openComposeModal()} type="highlighted" style="width: 100%;">
<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>
<dees-menu style="margin-top: 16px;">
<dees-menu-item
.active=${this.selectedFolder === 'inbox'}
@click=${() => this.selectFolder('inbox')}
.iconName=${'inbox'}
.label=${'Inbox'}
.badgeText=${this.getEmailCount('inbox') > 0 ? String(this.getEmailCount('inbox')) : ''}
></dees-menu-item>
<dees-menu-item
.active=${this.selectedFolder === 'sent'}
@click=${() => this.selectFolder('sent')}
.iconName=${'paperPlane'}
.label=${'Sent'}
.badgeText=${this.getEmailCount('sent') > 0 ? String(this.getEmailCount('sent')) : ''}
></dees-menu-item>
<dees-menu-item
.active=${this.selectedFolder === 'draft'}
@click=${() => this.selectFolder('draft')}
.iconName=${'file'}
.label=${'Drafts'}
.badgeText=${this.getEmailCount('draft') > 0 ? String(this.getEmailCount('draft')) : ''}
></dees-menu-item>
<dees-menu-item
.active=${this.selectedFolder === 'trash'}
@click=${() => this.selectFolder('trash')}
.iconName=${'trash'}
.label=${'Trash'}
.badgeText=${this.getEmailCount('trash') > 0 ? String(this.getEmailCount('trash')) : ''}
></dees-menu-item>
</dees-menu>
</dees-windowbox>
<!-- 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()}
${this.selectedEmail ? this.renderEmailPreview() : this.renderEmailListView()}
</div>
</div>
<!-- Compose Modal -->
${this.showCompose ? this.renderComposeModal() : ''}
`;
}
private renderFolderItem(folder: string, icon: string, label: string, count: number) {
private renderEmailListView() {
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>` : ''}
<!-- 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 -->
${this.renderEmailList()}
`;
}
@ -293,58 +273,58 @@ export class OpsViewEmails extends DeesElement {
}
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);
<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);
}
}
]}
.selectionMode=${'single'}
></dees-table>
</div>
},
{
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'}
heading1=${this.selectedFolder.charAt(0).toUpperCase() + this.selectedFolder.slice(1)}
heading2=${`${filteredEmails.length} emails`}
></dees-table>
`;
}
@ -356,114 +336,136 @@ export class OpsViewEmails extends DeesElement {
<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 class="emailMetaRow">
<span class="emailMetaLabel">From:</span>
<span>${this.selectedEmail.from}</span>
</div>
<div class="emailMetaRow">
<span class="emailMetaLabel">To:</span>
<span>${this.selectedEmail.to.join(', ')}</span>
</div>
${this.selectedEmail.cc?.length ? html`
<div class="emailMetaRow">
<span class="emailMetaLabel">CC:</span>
<span>${this.selectedEmail.cc.join(', ')}</span>
</div>
` : ''}
<div class="emailMetaRow">
<span class="emailMetaLabel">Date:</span>
<span>${new Date(this.selectedEmail.date).toLocaleString()}</span>
</div>
</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>`
html`<div style="white-space: pre-wrap;">${this.selectedEmail.body}</div>`
}
</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 @click=${() => this.selectedEmail = null} type="secondary">
<dees-icon name="arrowLeft" slot="iconSlot"></dees-icon>
Back to List
</dees-button>
<div style="margin-left: auto; display: flex; gap: 8px;">
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
<dees-icon name="reply" slot="iconSlot"></dees-icon>
Reply
</dees-button>
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
<dees-icon name="replyAll" slot="iconSlot"></dees-icon>
Reply All
</dees-button>
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
<dees-icon name="share" slot="iconSlot"></dees-icon>
Forward
</dees-button>
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
<dees-icon name="trash" slot="iconSlot"></dees-icon>
Delete
</dees-button>
</div>
</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>
private async openComposeModal(replyTo?: IEmail, replyAll = false, forward = false) {
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: forward ? 'Forward Email' : replyTo ? 'Reply to Email' : 'New Email',
content: html`
<div style="width: 700px; max-width: 90vw;">
<dees-form @formData=${async (e: CustomEvent) => {
await this.sendEmail(e.detail);
// Close modal after sending
const modals = document.querySelectorAll('dees-modal');
modals.forEach(m => (m as any).destroy?.());
}}>
<dees-input-tags
key="to"
label="To"
placeholder="Enter recipient email addresses..."
.value=${replyTo ? (replyAll ? [replyTo.from, ...replyTo.to].filter((v, i, a) => a.indexOf(v) === i) : [replyTo.from]) : []}
required
></dees-input-tags>
<dees-input-tags
key="cc"
label="CC"
placeholder="Enter CC recipients..."
.value=${replyAll && replyTo?.cc ? replyTo.cc : []}
></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..."
.value=${replyTo ? `${forward ? 'Fwd' : 'Re'}: ${replyTo.subject}` : ''}
required
></dees-input-text>
<dees-editor
key="body"
label="Message"
.mode=${'markdown'}
.height=${400}
.value=${replyTo && !forward ? `\n\n---\nOn ${new Date(replyTo.date).toLocaleString()}, ${replyTo.from} wrote:\n\n${replyTo.body}` : replyTo && forward ? replyTo.body : ''}
></dees-editor>
<dees-input-fileupload
key="attachments"
label="Attachments"
multiple
></dees-input-fileupload>
</dees-form>
</div>
</div>
`;
`,
menuOptions: [
{
name: 'Send',
iconName: 'paperPlane',
action: async (modalArg) => {
const form = modalArg.shadowRoot?.querySelector('dees-form') as any;
form?.submit();
}
},
{
name: 'Cancel',
iconName: 'xmark',
action: async (modalArg) => await modalArg.destroy()
}
]
});
}
private getFilteredEmails(): IEmail[] {
@ -518,16 +520,15 @@ export class OpsViewEmails extends DeesElement {
}
private async sendEmail(formData: any) {
try {
// TODO: Implement actual email sending
// TODO: Implement actual email sending via API
console.log('Sending email:', formData);
// Add to sent folder
// Add to sent folder (mock)
const newEmail: IEmail = {
id: `email-${Date.now()}`,
from: 'me@dcrouter.local',
to: formData.to,
to: formData.to || [],
cc: formData.cc || [],
bcc: formData.bcc || [],
subject: formData.subject,
@ -538,13 +539,13 @@ export class OpsViewEmails extends DeesElement {
};
this.emails = [...this.emails, newEmail];
this.showCompose = false;
// TODO: Implement toast notification when DeesToast.show is available
// Show success notification
console.log('Email sent successfully');
// TODO: Show toast notification when interface is available
} catch (error: any) {
// TODO: Implement toast notification when DeesToast.show is available
console.error('Failed to send email', error);
// TODO: Show error toast notification when interface is available
}
}
@ -581,18 +582,15 @@ export class OpsViewEmails extends DeesElement {
}
private async replyToEmail(email: IEmail) {
// TODO: Open compose with reply context
this.showCompose = true;
this.openComposeModal(email, false, false);
}
private async replyAllToEmail(email: IEmail) {
// TODO: Open compose with reply all context
this.showCompose = true;
this.openComposeModal(email, true, false);
}
private async forwardEmail(email: IEmail) {
// TODO: Open compose with forward context
this.showCompose = true;
this.openComposeModal(email, false, true);
}
private generateMockEmails() {