UI: Add copy/edit notification template and notify again

This commit is contained in:
Vladyslav_Prykhodko 2023-01-30 10:23:36 +02:00
parent ebb5c634a3
commit ac9b3303b6
8 changed files with 110 additions and 88 deletions

View File

@ -251,6 +251,9 @@ export class EntityService {
(id) => this.widgetService.getWidgetsBundle(id, config), (id) => this.widgetService.getWidgetsBundle(id, config),
entityIds); entityIds);
break; break;
case EntityType.NOTIFICATION_TARGET:
observable = of([]);
break;
} }
return observable; return observable;
} }

View File

@ -81,7 +81,12 @@ export class RequestTableConfig extends EntityTableConfig<NotificationRequest, P
} }
private configureCellActions(): Array<CellActionDescriptor<NotificationRequestInfo>> { private configureCellActions(): Array<CellActionDescriptor<NotificationRequestInfo>> {
return []; return [{
name: this.translate.instant('notification.notify-again'),
mdiIcon: 'mdi:repeat-variant',
isEnabled: (request) => request.status !== NotificationRequestStatus.SCHEDULED,
onAction: ($event, entity) => this.createRequest($event, entity)
}];
} }
private createRequest($event: Event, request: NotificationRequest, isAdd = false, updateData = true) { private createRequest($event: Event, request: NotificationRequest, isAdd = false, updateData = true) {

View File

@ -21,7 +21,7 @@ import {
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { EntityTypeResource } from '@shared/models/entity-type.models'; import { EntityTypeResource } from '@shared/models/entity-type.models';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { NotificationTemplate } from '@shared/models/notification.models'; import { NotificationTemplate, NotificationTemplateTypeTranslateMap } from '@shared/models/notification.models';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -61,7 +61,8 @@ export class TemplateTableConfig extends EntityTableConfig<NotificationTemplate>
this.defaultSortOrder = {property: 'notificationType', direction: Direction.ASC}; this.defaultSortOrder = {property: 'notificationType', direction: Direction.ASC};
this.columns.push( this.columns.push(
new EntityTableColumn<NotificationTemplate>('notificationType', 'notification.type', '15%'), new EntityTableColumn<NotificationTemplate>('notificationType', 'notification.type', '15%',
(template) => this.translate.instant(NotificationTemplateTypeTranslateMap.get(template.notificationType).name)),
new EntityTableColumn<NotificationTemplate>('name', 'notification.template', '25%'), new EntityTableColumn<NotificationTemplate>('name', 'notification.template', '25%'),
new EntityTableColumn<NotificationTemplate>('configuration.notificationSubject', 'notification.subject', '25%', new EntityTableColumn<NotificationTemplate>('configuration.notificationSubject', 'notification.subject', '25%',
(template) => template.configuration.notificationSubject, () => ({}), false), (template) => template.configuration.notificationSubject, () => ({}), false),
@ -71,10 +72,22 @@ export class TemplateTableConfig extends EntityTableConfig<NotificationTemplate>
} }
private configureCellActions(): Array<CellActionDescriptor<NotificationTemplate>> { private configureCellActions(): Array<CellActionDescriptor<NotificationTemplate>> {
return []; return [
{
name: this.translate.instant('notification.copy-template'),
icon: 'content_copy',
isEnabled: () => true,
onAction: ($event, entity) => this.editTemplate($event, entity, false, true)
}, {
name: this.translate.instant('notification.edit-template'),
icon: 'edit',
isEnabled: () => true,
onAction: ($event, entity) => this.editTemplate($event, entity)
}
];
} }
private editTemplate($event: Event, template: NotificationTemplate, isAdd = false) { private editTemplate($event: Event, template: NotificationTemplate, isAdd = false, isCopy = false) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -84,6 +97,7 @@ export class TemplateTableConfig extends EntityTableConfig<NotificationTemplate>
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
isAdd, isAdd,
isCopy,
template template
} }
}).afterClosed() }).afterClosed()

View File

@ -16,7 +16,7 @@
--> -->
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>notification.new-notification</h2> <h2>{{ dialogTitle | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"

View File

@ -23,7 +23,7 @@ import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { deepTrim, isDefined } from '@core/utils'; import { deepTrim } from '@core/utils';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
@ -55,6 +55,8 @@ export class RequestNotificationDialogComponent extends
selectedIndex = 0; selectedIndex = 0;
preview: NotificationRequestPreview = null; preview: NotificationRequestPreview = null;
dialogTitle = 'notification.notify-again';
private readonly destroy$ = new Subject<void>(); private readonly destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -66,10 +68,6 @@ export class RequestNotificationDialogComponent extends
private notificationService: NotificationService) { private notificationService: NotificationService) {
super(store, router, dialogRef); super(store, router, dialogRef);
if (isDefined(data.isAdd)) {
this.isAdd = data.isAdd;
}
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
@ -77,6 +75,13 @@ export class RequestNotificationDialogComponent extends
templateId: [null, Validators.required], templateId: [null, Validators.required],
targets: [null, Validators.required] targets: [null, Validators.required]
}); });
if (data.isAdd) {
this.dialogTitle = 'notification.new-notification';
}
if (data.request) {
this.notificationRequestForm.patchValue(this.data.request, {emitEvent: false});
}
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -28,7 +28,7 @@
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationTemplate [orientation]="(stepperOrientation | async)" <mat-horizontal-stepper [linear]="true" labelPosition="end" #notificationTemplateStepper [orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)"> (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit"> <ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon> <mat-icon>check</mat-icon>

View File

@ -31,13 +31,14 @@ import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { deepTrim, isDefined } from '@core/utils'; import { deepClone, deepTrim } from '@core/utils';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators'; import { map, takeUntil } from 'rxjs/operators';
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper';
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
export interface TemplateNotificationDialogData { export interface TemplateNotificationDialogData {
template?: NotificationTemplate; template?: NotificationTemplate;
@ -53,7 +54,7 @@ export interface TemplateNotificationDialogData {
export class TemplateNotificationDialogComponent export class TemplateNotificationDialogComponent
extends DialogComponent<TemplateNotificationDialogComponent, NotificationTemplate> implements OnDestroy { extends DialogComponent<TemplateNotificationDialogComponent, NotificationTemplate> implements OnDestroy {
@ViewChild('addNotificationTemplate', {static: true}) addNotificationTemplate: MatStepper; @ViewChild('notificationTemplateStepper', {static: true}) notificationTemplateStepper: MatStepper;
stepperOrientation: Observable<StepperOrientation>; stepperOrientation: Observable<StepperOrientation>;
@ -63,7 +64,6 @@ export class TemplateNotificationDialogComponent
smsTemplateForm: FormGroup; smsTemplateForm: FormGroup;
slackTemplateForm: FormGroup; slackTemplateForm: FormGroup;
dialogTitle = 'notification.edit-notification-template'; dialogTitle = 'notification.edit-notification-template';
saveButtonLabel = 'action.save';
notificationTypes = Object.keys(NotificationType) as NotificationType[]; notificationTypes = Object.keys(NotificationType) as NotificationType[];
slackChanelTypes = Object.keys(SlackChanelType) as SlackChanelType[]; slackChanelTypes = Object.keys(SlackChanelType) as SlackChanelType[];
@ -74,40 +74,33 @@ export class TemplateNotificationDialogComponent
selectedIndex = 0; selectedIndex = 0;
tinyMceOptions: Record<string, any>; tinyMceOptions: Record<string, any> = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['link table image imagetools code fullscreen'],
menubar: 'edit insert tools view format table',
toolbar: 'fontselect fontsizeselect | formatselect | bold italic strikethrough forecolor backcolor ' +
'| link | table | image | alignleft aligncenter alignright alignjustify ' +
'| numlist bullist outdent indent | removeformat | code | fullscreen',
height: 400,
autofocus: false,
branding: false
};
private readonly destroy$ = new Subject<void>(); private readonly destroy$ = new Subject<void>();
private readonly templateNotification: NotificationTemplate;
private deliveryMethodFormsMap: Map<NotificationDeliveryMethod, FormGroup>;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
protected dialogRef: MatDialogRef<TemplateNotificationDialogComponent, NotificationTemplate>, protected dialogRef: MatDialogRef<TemplateNotificationDialogComponent, NotificationTemplate>,
@Inject(MAT_DIALOG_DATA) public data: TemplateNotificationDialogData, @Inject(MAT_DIALOG_DATA) public data: TemplateNotificationDialogData,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private fb: FormBuilder, private fb: FormBuilder,
private notificationService: NotificationService) { private notificationService: NotificationService,
private translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
if (data.isAdd) {
this.dialogTitle = 'notification.add-notification-template';
this.saveButtonLabel = 'action.add';
}
if (data.isCopy) {
this.dialogTitle = 'notification.copy-notification-template';
}
this.tinyMceOptions = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['link table image imagetools code fullscreen'],
menubar: 'edit insert tools view format table',
toolbar: 'fontselect fontsizeselect | formatselect | bold italic strikethrough forecolor backcolor ' +
'| link | table | image | alignleft aligncenter alignright alignjustify ' +
'| numlist bullist outdent indent | removeformat | code | fullscreen',
height: 400,
autofocus: false,
branding: false
};
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
@ -122,7 +115,7 @@ export class TemplateNotificationDialogComponent
}); });
this.notificationDeliveryMethods.forEach(method => { this.notificationDeliveryMethods.forEach(method => {
(this.templateNotificationForm.get('configuration.deliveryMethodsTemplates') as FormGroup) (this.templateNotificationForm.get('configuration.deliveryMethodsTemplates') as FormGroup)
.addControl(method, this.fb.group({method, enabled: method === NotificationDeliveryMethod.PUSH}), {emitEvent: false}); .addControl(method, this.fb.group({enabled: method === NotificationDeliveryMethod.PUSH}), {emitEvent: false});
}); });
this.pushTemplateForm = this.fb.group({ this.pushTemplateForm = this.fb.group({
@ -179,6 +172,30 @@ export class TemplateNotificationDialogComponent
conversationId: ['', Validators.required], conversationId: ['', Validators.required],
conversationType: [SlackChanelType.DIRECT] conversationType: [SlackChanelType.DIRECT]
}); });
this.deliveryMethodFormsMap = new Map<NotificationDeliveryMethod, FormGroup>([
[NotificationDeliveryMethod.PUSH, this.pushTemplateForm],
[NotificationDeliveryMethod.EMAIL, this.emailTemplateForm],
[NotificationDeliveryMethod.SMS, this.smsTemplateForm],
[NotificationDeliveryMethod.SLACK, this.slackTemplateForm]
]);
if (data.isAdd || data.isCopy) {
this.dialogTitle = 'notification.add-notification-template';
}
this.templateNotification = deepClone(this.data.template);
if (this.templateNotification) {
if (this.data.isCopy) {
this.templateNotification.name += ` (${this.translate.instant('action.copy')})`;
}
this.templateNotificationForm.reset({}, {emitEvent: false});
this.templateNotificationForm.patchValue(this.templateNotification, {emitEvent: false});
for (const method in this.templateNotification.configuration.deliveryMethodsTemplates) {
this.deliveryMethodFormsMap.get(NotificationDeliveryMethod[method])
.patchValue(this.templateNotification.configuration.deliveryMethodsTemplates[method]);
}
}
} }
ngOnDestroy() { ngOnDestroy() {
@ -191,86 +208,61 @@ export class TemplateNotificationDialogComponent
this.dialogRef.close(null); this.dialogRef.close(null);
} }
save() {
let formValue = deepTrim(this.templateNotificationForm.value);
if (isDefined(this.data.template)) {
formValue = Object.assign({}, this.data.template, formValue);
}
this.notificationService.saveNotificationTemplate(formValue).subscribe(
(target) => this.dialogRef.close(target)
);
}
changeStep($event: StepperSelectionEvent) { changeStep($event: StepperSelectionEvent) {
this.selectedIndex = $event.selectedIndex; this.selectedIndex = $event.selectedIndex;
} }
backStep() { backStep() {
this.addNotificationTemplate.previous(); this.notificationTemplateStepper.previous();
} }
nextStep() { nextStep() {
if (this.selectedIndex >= this.maxStepperIndex) { if (this.selectedIndex >= this.maxStepperIndex) {
this.add(); this.add();
} else { } else {
this.addNotificationTemplate.next(); this.notificationTemplateStepper.next();
} }
} }
nextStepLabel(): string { nextStepLabel(): string {
if (this.selectedIndex === 1 && this.selectedIndex < this.maxStepperIndex && this.pushTemplateForm.pristine) { if (this.selectedIndex !== 0) {
return 'action.skip'; if (this.selectedIndex >= this.maxStepperIndex) {
} return (this.data.isAdd || this.data.isCopy) ? 'action.add' : 'action.save';
if (this.selectedIndex === 2 && this.selectedIndex < this.maxStepperIndex && this.emailTemplateForm.pristine) { } else if (this.notificationTemplateStepper.selected.stepControl.pristine) {
return 'action.skip'; return 'action.skip';
} }
if (this.selectedIndex === 3 && this.selectedIndex < this.maxStepperIndex && this.smsTemplateForm.pristine) {
return 'action.skip';
}
if (this.selectedIndex !== 0 && this.selectedIndex >= this.maxStepperIndex) {
return 'action.add';
} }
return 'action.next'; return 'action.next';
} }
private get maxStepperIndex(): number { private get maxStepperIndex(): number {
return this.addNotificationTemplate?._steps?.length - 1; return this.notificationTemplateStepper?._steps?.length - 1;
} }
private add(): void { private add(): void {
if (this.allValid()) { if (this.allValid()) {
const formValue: NotificationTemplate = this.templateNotificationForm.value; let template: NotificationTemplate = this.templateNotificationForm.value;
if (formValue.configuration.deliveryMethodsTemplates.PUSH.enabled) { this.notificationDeliveryMethods.forEach(method => {
Object.assign(formValue.configuration.deliveryMethodsTemplates.PUSH, this.pushTemplateForm.value); if (template.configuration.deliveryMethodsTemplates[method].enabled) {
} else { Object.assign(template.configuration.deliveryMethodsTemplates[method], this.deliveryMethodFormsMap.get(method).value, {method});
delete formValue.configuration.deliveryMethodsTemplates.PUSH; } else {
delete template.configuration.deliveryMethodsTemplates[method];
}
});
if (this.templateNotification) {
template = {...this.templateNotification, ...template};
} }
if (formValue.configuration.deliveryMethodsTemplates.EMAIL.enabled) { this.notificationService.saveNotificationTemplate(deepTrim(template)).subscribe(
Object.assign(formValue.configuration.deliveryMethodsTemplates.EMAIL, this.emailTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.EMAIL;
}
if (formValue.configuration.deliveryMethodsTemplates.SMS.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.SMS, this.smsTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.SMS;
}
if (formValue.configuration.deliveryMethodsTemplates.SLACK.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.SLACK, this.slackTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.SLACK;
}
this.notificationService.saveNotificationTemplate(deepTrim(formValue)).subscribe(
(target) => this.dialogRef.close(target) (target) => this.dialogRef.close(target)
); );
} }
} }
private allValid(): boolean { private allValid(): boolean {
return !this.addNotificationTemplate.steps.find((item, index) => { return !this.notificationTemplateStepper.steps.find((item, index) => {
if (item.stepControl.invalid) { if (item.stepControl.invalid) {
item.interacted = true; item.interacted = true;
this.addNotificationTemplate.selectedIndex = index; this.notificationTemplateStepper.selectedIndex = index;
return true; return true;
} else { } else {
return false; return false;

View File

@ -2674,8 +2674,8 @@
"add-notification-template": "Add notification template", "add-notification-template": "Add notification template",
"add-target": "Add recipient", "add-target": "Add recipient",
"add-template": "Add template", "add-template": "Add template",
"at-least-one-should-be-selected": "At least one should be selected",
"all": "All", "all": "All",
"at-least-one-should-be-selected": "At least one should be selected",
"basic-settings": "Basic settings", "basic-settings": "Basic settings",
"button-text": "Button text", "button-text": "Button text",
"button-text-required": "Button text is required", "button-text-required": "Button text is required",
@ -2683,6 +2683,7 @@
"conversation": "Conversation", "conversation": "Conversation",
"conversation-required": "Conversation is required", "conversation-required": "Conversation is required",
"copy-notification-template": "Copy notification template", "copy-notification-template": "Copy notification template",
"copy-template": "Copy template",
"create-target": "Create recipient", "create-target": "Create recipient",
"created-time": "Created time", "created-time": "Created time",
"delete-request-text": "Be careful, after the confirmation the notification request will become unrecoverable.", "delete-request-text": "Be careful, after the confirmation the notification request will become unrecoverable.",
@ -2692,16 +2693,17 @@
"delete-template-text": "Be careful, after the confirmation the notification template will become unrecoverable.", "delete-template-text": "Be careful, after the confirmation the notification template will become unrecoverable.",
"delete-template-title": "Are you sure you want to delete the notification template '{{templateName}}'?", "delete-template-title": "Are you sure you want to delete the notification template '{{templateName}}'?",
"delivery-method": "Delivery method", "delivery-method": "Delivery method",
"delivery-methods": "Delivery methods",
"delivery-method-type": { "delivery-method-type": {
"email": "Email", "email": "Email",
"push": "Web", "push": "Web",
"slack": "Slack", "slack": "Slack",
"sms": "SMS" "sms": "SMS"
}, },
"delivery-methods": "Delivery methods",
"description": "Description", "description": "Description",
"edit-notification-target": "Edit notification recipient", "edit-notification-target": "Edit notification recipient",
"edit-notification-template": "Edit notification template", "edit-notification-template": "Edit notification template",
"edit-template": "Edit template",
"email-settings": "Email settings", "email-settings": "Email settings",
"get-customer-id-from-originator": "Get Customer id from originator", "get-customer-id-from-originator": "Get Customer id from originator",
"inbox": "Inbox", "inbox": "Inbox",
@ -2722,6 +2724,7 @@
"notification": "Notification", "notification": "Notification",
"notification-center": "Notification center", "notification-center": "Notification center",
"notification-target": "Notification recipient", "notification-target": "Notification recipient",
"notify-again": "Notify again",
"recipients-count": "{ count, plural, 1 {1 Recipient} other {# Recipients} }", "recipients-count": "{ count, plural, 1 {1 Recipient} other {# Recipients} }",
"requet-status": { "requet-status": {
"processing": "Processing", "processing": "Processing",