Email templates UI

This commit is contained in:
Michele Artini 2023-03-03 11:13:24 +01:00
parent 0a70952cda
commit 1f801cb4bf
12 changed files with 294 additions and 22 deletions

View File

@ -0,0 +1,41 @@
package eu.dnetlib.is.email;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import eu.dnetlib.common.controller.AbstractDnetController;
import eu.dnetlib.common.model.EmailTemplate;
import eu.dnetlib.notifications.mail.EmailService;
@RestController
@RequestMapping("/ajax/templates/email")
public class EmailTemplateController extends AbstractDnetController {
@Autowired
protected EmailService emailService;
@GetMapping("/")
public List<EmailTemplate> listEmailTemplates() {
return emailService.listEmailTemplates();
}
@PostMapping("/")
public List<EmailTemplate> saveEmailTemplate(@RequestBody final EmailTemplate email) {
emailService.saveEmailTemplate(email);
return emailService.listEmailTemplates();
}
@DeleteMapping("/{id}")
public List<EmailTemplate> deleteEmailTemplate(@PathVariable final String id) {
emailService.deleteEmailTemplate(id);
return emailService.listEmailTemplates();
}
}

View File

@ -36,7 +36,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { SpinnerHttpInterceptor } from './common/spinner.service'; import { SpinnerHttpInterceptor } from './common/spinner.service';
import { MdstoresComponent, MdstoreInspectorComponent, MDStoreVersionsDialog, AddMDStoreDialog } from './mdstores/mdstores.component'; import { MdstoresComponent, MdstoreInspectorComponent, MDStoreVersionsDialog, AddMDStoreDialog } from './mdstores/mdstores.component';
import { CleanerTesterComponent } from './cleaner-tester/cleaner-tester.component'; import { CleanerTesterComponent } from './cleaner-tester/cleaner-tester.component';
import { EmailsComponent } from './emails/emails.component'; import { EmailDialog, EmailsComponent } from './emails/emails.component';
import { WfsComponent } from './wfs/wfs.component'; import { WfsComponent } from './wfs/wfs.component';
@NgModule({ @NgModule({
@ -70,6 +70,7 @@ import { WfsComponent } from './wfs/wfs.component';
AddMDStoreDialog, AddMDStoreDialog,
CleanerTesterComponent, CleanerTesterComponent,
EmailsComponent, EmailsComponent,
EmailDialog,
WfsComponent WfsComponent
], ],
imports: [ imports: [

View File

@ -198,3 +198,10 @@ export interface MDStoreRecord {
dateOfTransformation: string, dateOfTransformation: string,
provenance: any provenance: any
} }
export interface EmailTemplate {
id: string,
description: string,
subject: string,
message: string
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Page, DsmConf, ResourceType, Protocol, WfHistoryEntry, SimpleResource, Context, ContextNode, Vocabulary, VocabularyTerm, KeyValue, BrowseTerm, Datasource, MDStore, MDStoreVersion, MDStoreRecord } from './is.model'; import { Page, DsmConf, ResourceType, Protocol, WfHistoryEntry, SimpleResource, Context, ContextNode, Vocabulary, VocabularyTerm, KeyValue, BrowseTerm, Datasource, MDStore, MDStoreVersion, MDStoreRecord, EmailTemplate } from './is.model';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
@ -328,6 +328,26 @@ export class ISService {
}); });
} }
loadEmailTemplates(onSuccess: Function): void {
this.client.get<void>('./ajax/templates/email/').subscribe({
next: data => onSuccess(data),
error: error => this.showError(error)
});
}
saveEmailTemplate(email: EmailTemplate, onSuccess: Function, relatedForm?: FormGroup): void {
this.client.post<void>('./ajax/templates/email/', email).subscribe({
next: data => onSuccess(data),
error: error => this.showError(error, relatedForm)
});
}
deleteEmailTemplate(id: string, onSuccess: Function): void {
this.client.delete<void>('./ajax/templates/email/' + encodeURIComponent(id)).subscribe({
next: data => onSuccess(data),
error: error => this.showError(error)
});
}
private showError(error: any, form?: FormGroup) { private showError(error: any, form?: FormGroup) {

View File

@ -0,0 +1,41 @@
<form [formGroup]="emailForm" (ngSubmit)="onSubmit()">
<h1 mat-dialog-title *ngIf="emailForm.get('id')?.value">Edit Email Template</h1>
<h1 mat-dialog-title *ngIf="!emailForm.get('id')?.value">New Email Template</h1>
<div mat-dialog-content>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;" *ngIf="emailForm.get('id')?.value">
<mat-label>ID</mat-label>
<input matInput formControlName="id" readonly="readonly" />
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Description</mat-label>
<input matInput formControlName="description" />
<mat-error *ngIf="emailForm.get('description')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Email: subject</mat-label>
<input matInput formControlName="subject" />
<mat-error *ngIf="emailForm.get('subject')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%;">
<mat-label>Email: message</mat-label>
<textarea matInput formControlName="message" required rows="16" style="font-size: 0.8em;"></textarea>
<mat-error *ngIf="emailForm.get('message')?.invalid">This field is <strong>required</strong></mat-error>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-stroked-button color="primary" type="submit" [disabled]="!emailForm.valid">Submit</button>
<button mat-stroked-button color="primary" mat-dialog-close>Close</button>
<mat-error *ngIf="emailForm.errors?.['serverError']">
{{ emailForm.errors?.['serverError'] }}
</mat-error>
</div>
</form>

View File

@ -1 +1,42 @@
<p>emails works!</p> <h2>Email Templates</h2>
<button mat-stroked-button color="primary" (click)="openAddEmailTemplateDialog()">
<mat-icon fontIcon="add"></mat-icon>
create a new template
</button>
<mat-form-field appearance="fill" floatLabel="always" style="width: 100%; margin-top: 10px;">
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Filter..." #input />
</mat-form-field>
<table mat-table [dataSource]="emailsDatasource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef style="width: 25%;" mat-sort-header sortActionDescription="Sort by ID"> Id
</th>
<td mat-cell *matCellDef="let element">
<a (click)="openEditEmailTemplateDialog(element)">{{element.id}}</a>
</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by Description"> Description </th>
<td mat-cell *matCellDef="let element"> {{element.description}} </td>
</ng-container>
<ng-container matColumnDef="buttons">
<th mat-header-cell *matHeaderCellDef style="text-align: right;" style="width: 20%"></th>
<td mat-cell *matCellDef="let element" class="table-buttons">
<button mat-stroked-button color="warn" (click)="deleteEmailTemplate(element)">delete</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colums"></tr>
<tr mat-row *matRowDef="let row; columns: colums;"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4" style="padding: 0 16px;">No data matching the filter "{{input.value}}"</td>
</tr>
</table>

View File

@ -1,10 +1,98 @@
import { Component } from '@angular/core'; import { AfterViewInit, Component, Inject, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { EmailTemplate } from '../common/is.model';
import { ISService } from '../common/is.service';
@Component({ @Component({
selector: 'app-emails', selector: 'app-emails',
templateUrl: './emails.component.html', templateUrl: './emails.component.html',
styleUrls: ['./emails.component.css'] styleUrls: ['./emails.component.css']
}) })
export class EmailsComponent { export class EmailsComponent implements OnInit, AfterViewInit {
emailsDatasource: MatTableDataSource<EmailTemplate> = new MatTableDataSource<EmailTemplate>([]);
colums: string[] = ['id', 'description', 'buttons'];
@ViewChild(MatSort) sort: MatSort | undefined
searchText: string = '';
constructor(public service: ISService, public route: ActivatedRoute, public dialog: MatDialog) { }
ngOnInit() { this.reload() }
ngAfterViewInit() { if (this.sort) this.emailsDatasource.sort = this.sort; }
reload() { this.service.loadEmailTemplates((data: EmailTemplate[]) => this.emailsDatasource.data = data); }
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.emailsDatasource.filter = filterValue;
}
openAddEmailTemplateDialog(): void {
const dialogRef = this.dialog.open(EmailDialog, {
data: {
id: '',
description: '',
subject: '',
message: ''
},
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
openEditEmailTemplateDialog(email: EmailTemplate): void {
const dialogRef = this.dialog.open(EmailDialog, {
data: email,
width: '80%'
});
dialogRef.afterClosed().subscribe(result => {
if (result) this.reload();
});
}
deleteEmailTemplate(email: EmailTemplate) {
if (confirm('Are you sure?')) {
this.service.deleteEmailTemplate(email.id, (data: void) => this.reload());
}
}
}
@Component({
selector: 'email-dialog',
templateUrl: './email-dialog.html',
styleUrls: ['./emails.component.css']
})
export class EmailDialog {
emailForm = new FormGroup({
id: new FormControl(''),
description: new FormControl('', [Validators.required]),
subject: new FormControl('', [Validators.required]),
message: new FormControl('', [Validators.required])
});
constructor(public dialogRef: MatDialogRef<EmailDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public service: ISService) {
this.emailForm.get('id')?.setValue(data.id);
this.emailForm.get('description')?.setValue(data.description);
this.emailForm.get('subject')?.setValue(data.subject);
this.emailForm.get('message')?.setValue(data.message);
}
onSubmit(): void {
const email = Object.assign({}, this.data, this.emailForm.value);
this.service.saveEmailTemplate(email, (data: void) => this.dialogRef.close(1), this.emailForm);
}
onNoClick(): void {
this.dialogRef.close();
}
} }

View File

@ -137,8 +137,6 @@ export class VocabularyEditorComponent implements OnInit, AfterViewInit {
} }
} }
} }
@Component({ @Component({

View File

@ -17,6 +17,9 @@ public class EmailTemplate implements Serializable {
@Column(name = "id") @Column(name = "id")
private String id; private String id;
@Column(name = "description")
private String description;
@Column(name = "subject") @Column(name = "subject")
private String subject; private String subject;
@ -31,6 +34,14 @@ public class EmailTemplate implements Serializable {
this.id = id; this.id = id;
} }
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
public String getSubject() { public String getSubject() {
return subject; return subject;
} }

View File

@ -253,6 +253,7 @@ GROUP BY md.id,
-- Email Templates -- Email Templates
CREATE TABLE emails ( CREATE TABLE emails (
id text PRIMARY KEY, id text PRIMARY KEY,
description text NOT NULL,
subject text NOT NULL, subject text NOT NULL,
message text NOT NULL message text NOT NULL
); );

View File

@ -1,9 +1,11 @@
package eu.dnetlib.notifications.mail; package eu.dnetlib.notifications.mail;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Properties; import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
@ -17,6 +19,7 @@ import javax.mail.Transport;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -25,12 +28,13 @@ import org.springframework.stereotype.Service;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import eu.dnetlib.common.model.EmailTemplate;
import eu.dnetlib.common.repository.EmailTemplateRepository; import eu.dnetlib.common.repository.EmailTemplateRepository;
@Service @Service
public class EmailDispatcher { public class EmailService {
private static final Log log = LogFactory.getLog(EmailDispatcher.class); private static final Log log = LogFactory.getLog(EmailService.class);
private final BlockingQueue<Message> queue = new LinkedBlockingQueue<>(); private final BlockingQueue<Message> queue = new LinkedBlockingQueue<>();
@Value("${dnet.configuration.mail.sender.email}") @Value("${dnet.configuration.mail.sender.email}")
@ -78,6 +82,22 @@ public class EmailDispatcher {
}).start(); }).start();
} }
public List<EmailTemplate> listEmailTemplates() {
return emailTemplateRepository.findAll();
}
public void saveEmailTemplate(final EmailTemplate email) {
if (StringUtils.isBlank(email.getId()) || email.getId().length() < 10) {
email.setId("email-" + UUID.randomUUID());
log.info("Saving new email with id: " + email.getId());
}
emailTemplateRepository.save(email);
}
public void deleteEmailTemplate(final String id) {
emailTemplateRepository.deleteById(id);
}
public void sendMail(final String to, final String subject, final String message) { public void sendMail(final String to, final String subject, final String message) {
try { try {
final Session session = Session.getInstance(obtainProperties(), obtainAuthenticator()); final Session session = Session.getInstance(obtainProperties(), obtainAuthenticator());
@ -132,9 +152,12 @@ public class EmailDispatcher {
private Authenticator obtainAuthenticator() { private Authenticator obtainAuthenticator() {
if (this.smtpUser == null || this.smtpUser.isEmpty()) { return null; } if (this.smtpUser == null || this.smtpUser.isEmpty()) { return null; }
final String user = this.smtpUser;
final String passwd = this.smtpPassword;
return new Authenticator() { return new Authenticator() {
private final PasswordAuthentication authentication = new PasswordAuthentication(EmailDispatcher.this.smtpUser, EmailDispatcher.this.smtpPassword); private final PasswordAuthentication authentication = new PasswordAuthentication(user, passwd);
@Override @Override
protected PasswordAuthentication getPasswordAuthentication() { protected PasswordAuthentication getPasswordAuthentication() {

View File

@ -12,7 +12,7 @@ import eu.dnetlib.manager.wf.model.NotificationCondition;
import eu.dnetlib.manager.wf.repository.WorkflowSubscriptionRepository; import eu.dnetlib.manager.wf.repository.WorkflowSubscriptionRepository;
import eu.dnetlib.manager.wf.workflows.procs.WorkflowProcess; import eu.dnetlib.manager.wf.workflows.procs.WorkflowProcess;
import eu.dnetlib.manager.wf.workflows.procs.WorkflowProcess.Status; import eu.dnetlib.manager.wf.workflows.procs.WorkflowProcess.Status;
import eu.dnetlib.notifications.mail.EmailDispatcher; import eu.dnetlib.notifications.mail.EmailService;
@Service @Service
public class EmailSender { public class EmailSender {
@ -23,7 +23,7 @@ public class EmailSender {
private WorkflowSubscriptionRepository wfSubscriptionRepository; private WorkflowSubscriptionRepository wfSubscriptionRepository;
@Autowired @Autowired
private EmailDispatcher dispatcher; private EmailService emailService;
public void sendMails(final WorkflowProcess proc) { public void sendMails(final WorkflowProcess proc) {
@ -34,7 +34,7 @@ public class EmailSender {
try { try {
final Map<String, Object> params = new HashMap<>(); final Map<String, Object> params = new HashMap<>();
dispatcher.sendStoredMail(s.getEmail(), s.getMessageId(), params); emailService.sendStoredMail(s.getEmail(), s.getMessageId(), params);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Error sending mail to " + s.getEmail(), e); log.error("Error sending mail to " + s.getEmail(), e);