Skip to content

Commit

Permalink
Merge pull request #168 from amosproj/87-frontend-email-notification-…
Browse files Browse the repository at this point in the history
…reciever-list

87 frontend email notification reciever list
  • Loading branch information
flo0852 authored Dec 17, 2024
2 parents 215a04c + 697a233 commit 5f80ca8
Show file tree
Hide file tree
Showing 23 changed files with 1,255 additions and 7 deletions.
2 changes: 2 additions & 0 deletions apps/backend/src/app/utils/mail/mail.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Post,
} from '@nestjs/common';
import {
ApiConflictResponse,
ApiCreatedResponse,
ApiNotFoundResponse,
ApiOkResponse,
Expand Down Expand Up @@ -44,6 +45,7 @@ export class MailController {
@Post()
@ApiOperation({ summary: 'Adds a new Mail Receiver.' })
@ApiCreatedResponse()
@ApiConflictResponse({ description: 'Mail Receiver already exists.' })
async create(
@Body() createMailReceiverDto: CreateMailReceiverDto
): Promise<MailReceiverEntity> {
Expand Down
14 changes: 13 additions & 1 deletion apps/backend/src/app/utils/mail/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import {
ConflictException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as path from 'path';
import { Alert } from '../../alerting/entity/alerts/alert';
Expand Down Expand Up @@ -165,6 +170,13 @@ export class MailService {
async addMailReceiver(
createMailReceiverDto: CreateMailReceiverDto
): Promise<MailReceiverEntity> {
if (
await this.mailReceiverEntityRepository.findOneBy({
mail: createMailReceiverDto.mail,
})
) {
throw new ConflictException('Mail receiver already exists');
}
return await this.mailReceiverEntityRepository.save(createMailReceiverDto);
}

Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
<cds-icon shape="bell"></cds-icon>
<span class="nav-text">Notifications</span>
</button>
<button clrDropdownItem>
<cds-icon shape="envelope"></cds-icon>
<span
routerLinkActive="active"
routerLink="email-receiver"
class="nav-text"
>Email Receiver</span
>
</button>
</clr-dropdown-menu>
</clr-dropdown>
<app-notification-settings></app-notification-settings>
Expand Down
14 changes: 13 additions & 1 deletion apps/frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
tagIcon,
dataClusterIcon,
filterIcon,
envelopeIcon,
plusIcon,
lockIcon,
trashIcon,
} from '@cds/core/icon';
import { NgxEchartsModule } from 'ngx-echarts';
import { TestUploadComponent } from './test-upload/component/test-upload/test-upload.component';
Expand All @@ -31,6 +35,8 @@ import { BackupsComponent } from './backups-overview/backups/backups/backups.com
import { BASE_URL } from './shared/types/configuration';
import { AlertComponent } from './alert/component/alert.component';
import { NotificationSettingsComponent } from './management/components/settings/notification-settings/notification-settings.component';
import { EmailReceiverSettingsComponent } from './management/components/settings/email-receiver-settings/email-receiver-settings.component';
import { ConfirmDialogComponent } from './shared/components/confirm-dialog/component/confirm-dialog/confirm-dialog.component';

@NgModule({
declarations: [
Expand All @@ -40,6 +46,8 @@ import { NotificationSettingsComponent } from './management/components/settings/
BackupsComponent,
AlertComponent,
NotificationSettingsComponent,
EmailReceiverSettingsComponent,
ConfirmDialogComponent,
],
imports: [
BrowserModule,
Expand Down Expand Up @@ -69,7 +77,11 @@ export class AppModule {
angleIcon,
tagIcon,
dataClusterIcon,
filterIcon
filterIcon,
envelopeIcon,
plusIcon,
lockIcon,
trashIcon,
);
}
}
3 changes: 3 additions & 0 deletions apps/frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { TestUploadComponent } from './test-upload/component/test-upload/test-up
import { FindTestDataComponent } from './test-upload/component/find-test-data/find-test-data.component';
import { BackupsComponent } from './backups-overview/backups/backups/backups.component';
import { AlertComponent } from './alert/component/alert.component';
import { EmailReceiverSettingsComponent } from './management/components/settings/email-receiver-settings/email-receiver-settings.component';

export const appRoutes: Route[] = [
{ path: 'upload', component: TestUploadComponent },
{ path: 'findData', component: FindTestDataComponent },
{ path: 'email-receiver', component: EmailReceiverSettingsComponent},
{ path: '', component: BackupsComponent },

];
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.clr-dg-cell {
text-align: center ;
}

.error {
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<h3>Email Alert Recipients</h3>
<div class="clr-row">
<div class="clr-col-lg-12 clr-col-lg-12">
<clr-datagrid
*ngIf="emailsSubject$ | async as emails"
[clrDgLoading]="isLoading"
[(clrDgSelected)]="selectedEmails"
>
<clr-dg-action-bar>
<div class="btn-group">
<button
type="button"
class="btn btn-sm btn-secondary"
(click)="showEmailModal = true"
>
<cds-icon shape="plus"></cds-icon>
New
</button>
<button
type="button"
class="btn btn-sm btn-danger"
(click)="removeEmail(selectedEmails)"
[disabled]="selectedEmails.length === 0"
>
<cds-icon shape="lock"></cds-icon>
Delete
</button>
</div>
</clr-dg-action-bar>

<clr-dg-column>Email</clr-dg-column>
<clr-dg-placeholder>We couldn't find any emails!</clr-dg-placeholder>
<clr-dg-row
*ngFor="let email of emailsSubject$.getValue()"
[clrDgItem]="email"
>
<clr-dg-cell class="clr-dg-cell">{{ email.mail }}</clr-dg-cell>
</clr-dg-row>

<clr-dg-footer> </clr-dg-footer>
</clr-datagrid>
</div>
</div>

<clr-modal [(clrModalOpen)]="showEmailModal">
<h3 class="modal-title">Add Email Recipient</h3>
<div class="modal-body">
<form clrForm [formGroup]="emailForm" (ngSubmit)="saveChanges()">
<clr-spinner *ngIf="isLoading"></clr-spinner>
<clr-input-container>
<label>Email Address</label>
<input clrInput type="email" formControlName="email" />
<clr-control-error
*ngIf="
emailForm.get('email')?.touched && emailForm.get('email')?.invalid
"
>Please enter a valid email address</clr-control-error
>
</clr-input-container>
<div class="error" *ngIf="modalError">
{{ modalError }}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="resetForm()">
Cancel
</button>
<button
type="button"
class="btn btn-primary"
(click)="saveChanges()"
[disabled]="!emailForm.valid"
>
Add
</button>
</div>
</clr-modal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EmailReceiverSettingsComponent } from './email-receiver-settings.component';
import { FormBuilder } from '@angular/forms';
import { EmailReceiverService } from '../../../services/email-receiver/email-receiver.service';
import { of, throwError } from 'rxjs';
import { ConfirmDialogService } from '../../../../shared/components/confirm-dialog/service/confirm-dialog.service';
import { EmailType } from '../../../../shared/types/email';

describe('EmailReceiverSettingsComponent', () => {
let component: EmailReceiverSettingsComponent;
let emailService: EmailReceiverService;
let confirmDialogService: ConfirmDialogService;

const mockEmails: EmailType[] = [
{ id: '1', mail: '[email protected]' },
{ id: '2', mail: '[email protected]' },
];

beforeEach(() => {
emailService = {
getAllEmailReceiver: vi.fn().mockReturnValue(of(mockEmails)),
deleteEmail: vi.fn().mockReturnValue(of({})),
updateEmailReceiver: vi.fn(),
} as any;

confirmDialogService = {
handleConfirmation: vi.fn().mockImplementation((options, onConfirm) => {
onConfirm();
return Promise.resolve();
}),
} as any;

component = new EmailReceiverSettingsComponent(
new FormBuilder(),
emailService,
confirmDialogService
);

component.ngOnInit();
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('loadEmailReceiver', () => {
it('should load emails successfully', () => {
component.loadEmailReceiver();

expect(component.isLoading).toBe(false);
expect(emailService.getAllEmailReceiver).toHaveBeenCalled();

component['emailsSubject$'].subscribe((emails) => {
expect(emails).toEqual(mockEmails);
});
});

it('should handle error when loading emails', () => {
emailService.getAllEmailReceiver = vi
.fn()
.mockReturnValue(throwError(() => new Error('Failed to load')));

component.loadEmailReceiver();

expect(component.isLoading).toBe(false);
expect(emailService.getAllEmailReceiver).toHaveBeenCalled();
});
});

describe('removeEmail', () => {
it('should not proceed if no emails to remove', async () => {
await component.removeEmail([]);

expect(confirmDialogService.handleConfirmation).not.toHaveBeenCalled();
expect(emailService.deleteEmail).not.toHaveBeenCalled();
});

it('should delete emails when confirmed', async () => {
const emailsToRemove = [mockEmails[0]];

await component.removeEmail(emailsToRemove);

expect(confirmDialogService.handleConfirmation).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Delete Email Recipients',
confirmText: 'Delete',
cancelText: 'Cancel',
}),
expect.any(Function)
);
expect(emailService.deleteEmail).toHaveBeenCalledWith('1');
});

it('should handle cancel confirmation', async () => {
confirmDialogService.handleConfirmation = vi
.fn()
.mockImplementation((options, onConfirm, onCancel) => {
if (onCancel) onCancel();
return Promise.resolve();
});

await component.removeEmail([mockEmails[0]]);

expect(emailService.deleteEmail).not.toHaveBeenCalled();
});

it('should handle error during deletion', async () => {
emailService.deleteEmail = vi
.fn()
.mockReturnValue(throwError(() => new Error('Delete failed')));

await component.removeEmail([mockEmails[0]]);

expect(component.isLoading).toBe(false);
});
});

describe('saveChanges', () => {
it('should not save when form is invalid', () => {
component.emailForm.controls['email'].setValue('');

component.saveChanges();

expect(emailService.updateEmailReceiver).not.toHaveBeenCalled();
});

it('should create new email when form is valid', () => {
const newEmail = { mail: '[email protected]' };
const createdEmail = { id: '3', mail: '[email protected]' };

emailService.updateEmailReceiver = vi
.fn()
.mockReturnValue(of(createdEmail));
component.emailForm.controls['email'].setValue('[email protected]');

component.saveChanges();

expect(emailService.updateEmailReceiver).toHaveBeenCalledWith(newEmail);
expect(component.showEmailModal).toBe(false);
expect(component.isLoading).toBe(false);
});

it('should handle duplicate email error', () => {
emailService.updateEmailReceiver = vi
.fn()
.mockReturnValue(throwError(() => ({ status: 409 })));
component.emailForm.controls['email'].setValue('[email protected]');

component.saveChanges();

expect(component.modalError).toBe('This email already exists');
expect(component.isLoading).toBe(false);
});
});

describe('resetForm', () => {
it('should reset form and modal state', () => {
// Setup form with values
component.emailForm.controls['email'].setValue('[email protected]');
component.showEmailModal = true;
component.modalError = 'Some error';
component.isLoading = true;

// Reset form
component.resetForm();

// Verify reset state
expect(component.emailForm.get('email')?.value).toBeNull();
expect(component.showEmailModal).toBe(false);
expect(component.modalError).toBe('');
expect(component.isLoading).toBe(false);
});
});

describe('ngOnDestroy', () => {
it('should complete subjects', () => {
const destroySpy = vi.spyOn(component['destroy$'], 'complete');

component.ngOnDestroy();

expect(destroySpy).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 5f80ca8

Please sign in to comment.