Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/compose/compose.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { DraftDeskComponent } from './draftdesk.component';
import { ComposeComponent } from './compose.component';
export { ComposeComponent } from './compose.component';
import { MenuModule } from '../menu/menu.module';
import { DialogModule } from '../dialog/dialog.module';
import { MailRecipientInputComponent} from './mailrecipientinput.component';
export { MailRecipientInputComponent} from './mailrecipientinput.component';

Expand All @@ -56,6 +57,7 @@ export { MailRecipientInputComponent} from './mailrecipientinput.component';
FormsModule,
ReactiveFormsModule,
MenuModule,
DialogModule,
MatTooltipModule
],
declarations: [DraftDeskComponent, ComposeComponent, MailRecipientInputComponent],
Expand Down
8 changes: 8 additions & 0 deletions src/app/compose/draftdesk.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<div class="draftdesk-container">
<div class="appPageHeader">
<h1>Draft Desk</h1>
<button
mat-icon-button
id="emptyDrafts"
matTooltip="Empty Drafts"
[disabled]="savedDraftCount === 0 || isEmptyingDrafts"
(click)="emptyDrafts()">
<mat-icon svgIcon="delete"></mat-icon>
</button>
</div>

<compose [model]="draftModel" (draftDeleted)="draftDeleted($event)" #draft *ngFor="let draftModel of draftModelsInView" [ngClass]="{'draftPreviewContainer': !editing}"></compose>
Expand Down
9 changes: 6 additions & 3 deletions src/app/compose/draftdesk.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
padding: 15px;

.appPageHeader {
position: absolute;
top: 8px;
left: 70px;
position: absolute;
top: 8px;
left: 70px;
display: flex;
align-items: center;
gap: 8px;
}
}

Expand Down
44 changes: 43 additions & 1 deletion src/app/compose/draftdesk.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { RunboxWebmailAPI } from '../rmmapi/rbwebmail';
import { DraftDeskService, DraftFormModel } from './draftdesk.service';
import { RecipientsService } from './recipients.service';
import { ConfirmDialog } from '../dialog/dialog.module';

const MAX_DRAFTS_IN_VIEW = 10;

Expand All @@ -34,12 +36,14 @@ export class DraftDeskComponent implements OnInit {
public draftModelsInView: DraftFormModel[] = [];
public hasMoreDrafts = false;
public currentMaxDraftsInView: number = MAX_DRAFTS_IN_VIEW;
public isEmptyingDrafts = false;
private hasInitialized = false;

constructor(
public rmmapi: RunboxWebmailAPI,
public router: Router,
private route: ActivatedRoute,
private dialog: MatDialog,
public draftDeskservice: DraftDeskService) {

this.draftDeskservice.draftModels.subscribe(
Expand Down Expand Up @@ -109,6 +113,7 @@ export class DraftDeskComponent implements OnInit {
} else {
this.draftModelsInView = this.draftDeskservice.draftModels.value.slice(0, this.currentMaxDraftsInView);
}
this.hasMoreDrafts = this.currentMaxDraftsInView < this.draftDeskservice.draftModels.value.length;
}

draftDeleted(messageId) {
Expand All @@ -124,9 +129,46 @@ export class DraftDeskComponent implements OnInit {
this.updateDraftsInView();
}

get savedDraftCount(): number {
return this.draftDeskservice.draftModels.value.filter((draft) => !draft.isUnsaved()).length;
}

emptyDrafts() {
const savedDraftIds = this.draftDeskservice.draftModels.value
.filter((draft) => !draft.isUnsaved())
.map((draft) => draft.mid);

if (savedDraftIds.length === 0 || this.isEmptyingDrafts) {
return;
}

const confirmDialog = this.dialog.open(ConfirmDialog);
confirmDialog.componentInstance.title = 'Empty Drafts?';
confirmDialog.componentInstance.question =
`Delete ${savedDraftIds.length} saved draft${savedDraftIds.length === 1 ? '' : 's'}?`;
confirmDialog.componentInstance.noOptionTitle = 'cancel';
confirmDialog.componentInstance.yesOptionTitle = 'delete';
confirmDialog.afterClosed().subscribe((confirmed) => {
if (confirmed !== true) {
return;
}

this.isEmptyingDrafts = true;
this.draftDeskservice.deleteDrafts(savedDraftIds).subscribe({
next: () => {
this.isEmptyingDrafts = false;
this.updateDraftsInView();
},
error: (error) => {
this.isEmptyingDrafts = false;
console.error('Could not empty drafts', error);
}
});
});
}

showMore() {
this.currentMaxDraftsInView += MAX_DRAFTS_IN_VIEW;
this.updateDraftsInView();
this.hasMoreDrafts = this.currentMaxDraftsInView < this.draftDeskservice.draftModels.value.length;
}
}
50 changes: 49 additions & 1 deletion src/app/compose/draftdesk.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ describe('DraftDeskService', () => {
headers: { subject: 'Test Subject' },
text: { text: 'Test body', html: '<p>Test body</p>' }
})),
copyAttachmentToDraft: jasmine.createSpy('copyAttachmentToDraft').and.returnValue(of({ filename: 'attachment.txt' }))
copyAttachmentToDraft: jasmine.createSpy('copyAttachmentToDraft').and.returnValue(of({ filename: 'attachment.txt' })),
deleteMessages: jasmine.createSpy('deleteMessages').and.returnValue(of({ status: 'success' }))
};

draftDeskService = new DraftDeskService(
Expand Down Expand Up @@ -522,6 +523,53 @@ describe('DraftDeskService', () => {
});
});

describe('deleteDrafts', () => {
it('should batch-delete saved drafts and remove them locally', (done) => {
const firstDraft = DraftFormModel.create(
12345,
mockProfileService.composeProfile,
'to@runbox.com',
'First Draft'
);
const secondDraft = DraftFormModel.create(
23456,
mockProfileService.composeProfile,
'to@runbox.com',
'Second Draft'
);
const unsavedDraft = DraftFormModel.create(
-1,
mockProfileService.composeProfile,
'to@runbox.com',
'Unsaved Draft'
);
draftDeskService.draftModels.next([firstDraft, secondDraft, unsavedDraft]);

draftDeskService.deleteDrafts([firstDraft.mid, secondDraft.mid, unsavedDraft.mid]).subscribe(() => {
expect(mockRmmapi.deleteMessages).toHaveBeenCalledOnceWith([firstDraft.mid, secondDraft.mid]);
expect(draftDeskService.draftModels.value).toEqual([unsavedDraft]);
done();
});
});

it('should not call the backend when there are no saved drafts', (done) => {
const unsavedDraft = DraftFormModel.create(
-1,
mockProfileService.composeProfile,
'to@runbox.com',
'Unsaved Draft'
);
draftDeskService.draftModels.next([unsavedDraft]);
mockRmmapi.deleteMessages.calls.reset();

draftDeskService.deleteDrafts([unsavedDraft.mid]).subscribe(() => {
expect(mockRmmapi.deleteMessages).not.toHaveBeenCalled();
expect(draftDeskService.draftModels.value).toEqual([unsavedDraft]);
done();
});
});
});

describe('newBugReport with firstValueFrom', () => {
it('should create bug report draft with template from HTTP', async () => {
mockHttp.get.and.returnValue(of('Username: %%USERNAME%%\nUser Agent: %%USERAGENT%%'));
Expand Down
26 changes: 22 additions & 4 deletions src/app/compose/draftdesk.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { MailAddressInfo } from '../common/mailaddressinfo';
import { MessageListService } from '../rmmapi/messagelist.service';
import { MessageTableRowTool} from '../messagetable/messagetablerow';
import { Identity, ProfileService } from '../profiles/profile.service';
import { from, of, BehaviorSubject, firstValueFrom } from 'rxjs';
import { from, of, BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { map, mergeMap, bufferCount, take, distinctUntilChanged } from 'rxjs/operators';

import moment from 'moment';
Expand Down Expand Up @@ -307,9 +307,27 @@ export class DraftDeskService {
}

public deleteDraft(messageId: number) {
let models = this.draftModels.value;
models = models.filter(dm => dm.mid !== messageId);
this.draftModels.next(models);
this.removeDrafts([messageId]);
}

public deleteDrafts(messageIds: number[]): Observable<unknown> {
const savedDraftIds = messageIds.filter((messageId) => messageId > 0);

if (savedDraftIds.length === 0) {
return of({ status: 'success' });
}

return this.rmmapi.deleteMessages(savedDraftIds).pipe(
map((reply) => {
this.removeDrafts(savedDraftIds);
return reply;
})
);
}

private removeDrafts(messageIds: number[]) {
const messageIdSet = new Set(messageIds);
this.draftModels.next(this.draftModels.value.filter((dm) => !messageIdSet.has(dm.mid)));
}

public async newTemplateDraft(
Expand Down