Adds notifications if multiple users want to edit the same motion.
Fixes #4217 Adds notifications - Created a class to define notification-objects, which should notify other persons editing the same motion. - Added functions to send notifications, listen to them and unsubscribing them. - Added a warning-function to the `base-view.ts`, which raises the snack bar with the given message and has no duration. Fixes #4217 - Removed unnecessary lines of code. - Fixed merge. Prettified - Added a random number to identify different user. - Now the user can sign in as the same user, but still receive a message if multiple people edit the same motion. Fix the detail of motions - If the user does not click to edit, then the `editNotificationSubscription` was not set.
This commit is contained in:
parent
4c4a2c600b
commit
80cb8051f6
@ -15,7 +15,7 @@ export abstract class BaseViewComponent extends BaseComponent implements OnDestr
|
||||
/**
|
||||
* A reference to the current error snack bar.
|
||||
*/
|
||||
private errorSnackBar: MatSnackBarRef<SimpleSnackBar>;
|
||||
private messageSnackBar: MatSnackBarRef<SimpleSnackBar>;
|
||||
|
||||
/**
|
||||
* Constructor for bas elist views
|
||||
@ -27,6 +27,14 @@ export abstract class BaseViewComponent extends BaseComponent implements OnDestr
|
||||
super(titleService, translate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the snack bar with the given message.
|
||||
* This snack bar will only dismiss if the user clicks the 'OK'-button.
|
||||
*/
|
||||
protected raiseWarning = (message: string): void => {
|
||||
this.messageSnackBar = this.matSnackBar.open(message, this.translate.instant('OK'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens an error snack bar with the given error message.
|
||||
* This is implemented as an arrow function to capture the called `this`. You can use this function
|
||||
@ -34,17 +42,27 @@ export abstract class BaseViewComponent extends BaseComponent implements OnDestr
|
||||
* @param message The message to show.
|
||||
*/
|
||||
protected raiseError = (message: string): void => {
|
||||
this.errorSnackBar = this.matSnackBar.open(message, this.translate.instant('OK'), {
|
||||
this.messageSnackBar = this.matSnackBar.open(message, this.translate.instant('OK'), {
|
||||
duration: 0
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to manually close the snack bar if it will not automatically close
|
||||
* or it should close in a previous step.
|
||||
*/
|
||||
protected closeSnackBar(): void {
|
||||
if (this.matSnackBar) {
|
||||
this.matSnackBar.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* automatically dismisses the error snack bar, if the component is destroyed.
|
||||
*/
|
||||
public ngOnDestroy(): void {
|
||||
if (this.errorSnackBar) {
|
||||
this.errorSnackBar.dismiss();
|
||||
if (this.messageSnackBar) {
|
||||
this.messageSnackBar.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Component, OnInit, ElementRef, HostListener, TemplateRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ElementRef, HostListener, TemplateRef } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
|
||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||
import { MatDialog, MatSnackBar, MatCheckboxChange, ErrorStateMatcher } from '@angular/material';
|
||||
@ -7,7 +7,6 @@ import { MatDialog, MatSnackBar, MatCheckboxChange, ErrorStateMatcher } from '@a
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||
import { BaseViewComponent } from '../../../base/base-view';
|
||||
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
|
||||
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
|
||||
@ -16,7 +15,9 @@ import { CreateMotion } from '../../models/create-motion';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
||||
import { DiffLinesInParagraph, LineRange } from 'app/core/ui-services/diff.service';
|
||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||
import { itemVisibilityChoices, Item } from 'app/shared/models/agenda/item';
|
||||
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
||||
import { LocalPermissionsService } from '../../services/local-permissions.service';
|
||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||
import { Motion } from 'app/shared/models/motions/motion';
|
||||
@ -27,27 +28,30 @@ import {
|
||||
} from '../motion-change-recommendation/motion-change-recommendation.component';
|
||||
import { MotionPdfExportService } from '../../services/motion-pdf-export.service';
|
||||
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
||||
import { NotifyService } from 'app/core/core-services/notify.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
||||
import { PersonalNoteService } from 'app/core/ui-services/personal-note.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
|
||||
import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';
|
||||
import { ViewCreateMotion } from '../../models/view-create-motion';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { ViewUnifiedChange } from '../../../../shared/models/motions/view-unified-change';
|
||||
import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
|
||||
import { Workflow } from 'app/shared/models/motions/workflow';
|
||||
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
||||
import { Tag } from 'app/shared/models/core/tag';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ViewMotionBlock } from '../../models/view-motion-block';
|
||||
import { ViewWorkflow, StateCssClassMapping } from '../../models/view-workflow';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { ViewCategory } from '../../models/view-category';
|
||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||
import { ViewCreateMotion } from '../../models/view-create-motion';
|
||||
import { ViewItem } from 'app/site/agenda/models/view-item';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||
import { ViewMotionChangeRecommendation } from '../../models/view-change-recommendation';
|
||||
import { ViewMotionNotificationEditMotion, TypeOfNotificationViewMotion } from '../../models/view-motion-notify';
|
||||
import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
|
||||
|
||||
import { Workflow } from 'app/shared/models/motions/workflow';
|
||||
|
||||
/**
|
||||
* Component for the motion detail view
|
||||
@ -57,7 +61,7 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
|
||||
templateUrl: './motion-detail.component.html',
|
||||
styleUrls: ['./motion-detail.component.scss']
|
||||
})
|
||||
export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
export class MotionDetailComponent extends BaseViewComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Motion content. Can be a new version
|
||||
*/
|
||||
@ -320,6 +324,23 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
*/
|
||||
public newStateExtension = '';
|
||||
|
||||
/**
|
||||
* Constant to identify the notification-message.
|
||||
*/
|
||||
public NOTIFICATION_EDIT_MOTION = 'notifyEditMotion';
|
||||
|
||||
/**
|
||||
* Array to recognize, if there are other persons working on the same
|
||||
* motion and see, if those persons leave the editing-view.
|
||||
*/
|
||||
private otherWorkOnMotion: string[] = [];
|
||||
|
||||
/**
|
||||
* The variable to hold the subscription for notifications in editing-view.
|
||||
* Necessary to unsubscribe after leaving the editing-view.
|
||||
*/
|
||||
private editNotificationSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* Constuct the detail view.
|
||||
*
|
||||
@ -327,7 +348,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
* @param translate
|
||||
* @param matSnackBar
|
||||
* @param vp the viewport service
|
||||
* @param op Operator Service
|
||||
* @param operator Operator Service
|
||||
* @param perms local permissions
|
||||
* @param router to navigate back to the motion list and to an existing motion
|
||||
* @param route determine if this is a new or an existing motion
|
||||
* @param formBuilder For reactive forms. Form Group and Form Control
|
||||
@ -345,13 +367,17 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
* @param personalNoteService: personal comments and favorite marker
|
||||
* @param linenumberingService The line numbering service
|
||||
* @param categoryRepo Repository for categories
|
||||
* @param viewModelStore accessing view models
|
||||
* @param categoryRepo access the category repository
|
||||
* @param userRepo Repository for users
|
||||
* @param notifyService: NotifyService work with notification
|
||||
*/
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
public vp: ViewportService,
|
||||
private operator: OperatorService,
|
||||
public perms: LocalPermissionsService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@ -371,7 +397,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
private linenumberingService: LinenumberingService,
|
||||
private viewModelStore: ViewModelStoreService,
|
||||
private categoryRepo: CategoryRepositoryService,
|
||||
private userRepo: UserRepositoryService
|
||||
private userRepo: UserRepositoryService,
|
||||
private notifyService: NotifyService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
|
||||
@ -474,6 +501,10 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.unsubscribeEditNotifications(TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges amendments and change recommendations and sorts them by the line numbers.
|
||||
* Called each time one of these arrays changes.
|
||||
@ -738,6 +769,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
this.createMotion();
|
||||
} else {
|
||||
this.updateMotionFromForm();
|
||||
// When saving the changes, notify other users if they edit the same motion.
|
||||
this.unsubscribeEditNotifications(TypeOfNotificationViewMotion.TYPE_SAVING_EDITING_MOTION);
|
||||
}
|
||||
}
|
||||
|
||||
@ -988,10 +1021,17 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
if (mode) {
|
||||
this.motionCopy = this.motion.copy();
|
||||
this.patchForm(this.motionCopy);
|
||||
this.editNotificationSubscription = this.listenToEditNotification();
|
||||
this.sendEditNotification(TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION);
|
||||
}
|
||||
if (!mode && this.newMotion) {
|
||||
this.router.navigate(['./motions/']);
|
||||
}
|
||||
// If the user cancels the work on this motion,
|
||||
// notify the users who are still editing the same motion
|
||||
if (!mode && !this.newMotion) {
|
||||
this.unsubscribeEditNotifications(TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1250,6 +1290,104 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
this.repo.followRecommendation(this.motion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to send a notification, so that other persons can recognize editing the same motion, if they're doing.
|
||||
*
|
||||
* @param type TypeOfNotificationViewMotion defines the type of the notification which is sent.
|
||||
* @param user Optional userId. If set the function will send a notification to the given userId.
|
||||
*/
|
||||
private sendEditNotification(type: TypeOfNotificationViewMotion, user?: number): void {
|
||||
const content: ViewMotionNotificationEditMotion = {
|
||||
motionId: this.motion.id,
|
||||
senderId: this.operator.viewUser.id,
|
||||
senderName: this.operator.viewUser.short_name,
|
||||
type: type
|
||||
};
|
||||
if (user) {
|
||||
this.notifyService.sendToUsers(this.NOTIFICATION_EDIT_MOTION, content, user);
|
||||
} else {
|
||||
this.notifyService.sendToAllUsers<ViewMotionNotificationEditMotion>(this.NOTIFICATION_EDIT_MOTION, content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to listen to notifications if the user edits this motion.
|
||||
* Handles the notification messages.
|
||||
*
|
||||
* @returns A subscription, only if the user wants to edit this motion, to listen to notifications.
|
||||
*/
|
||||
private listenToEditNotification(): Subscription {
|
||||
return this.notifyService.getMessageObservable(this.NOTIFICATION_EDIT_MOTION).subscribe(message => {
|
||||
const content = <ViewMotionNotificationEditMotion>message.content;
|
||||
if (this.operator.viewUser.id !== content.senderId && content.motionId === this.motion.id) {
|
||||
let warning = '';
|
||||
|
||||
switch (content.type) {
|
||||
case TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION:
|
||||
case TypeOfNotificationViewMotion.TYPE_ALSO_EDITING_MOTION: {
|
||||
if (!this.otherWorkOnMotion.includes(content.senderName)) {
|
||||
this.otherWorkOnMotion.push(content.senderName);
|
||||
}
|
||||
|
||||
warning = `${this.translate.instant('Following users are currently editing this motion:')} ${
|
||||
this.otherWorkOnMotion
|
||||
}`;
|
||||
if (content.type === TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION) {
|
||||
this.sendEditNotification(
|
||||
TypeOfNotificationViewMotion.TYPE_ALSO_EDITING_MOTION,
|
||||
message.senderUserId
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION: {
|
||||
this.recognizeOtherWorkerOnMotion(content.senderName);
|
||||
break;
|
||||
}
|
||||
case TypeOfNotificationViewMotion.TYPE_SAVING_EDITING_MOTION: {
|
||||
warning = `${content.senderName} ${this.translate.instant(
|
||||
'has saved his work on this motion.'
|
||||
)}`;
|
||||
// Wait, to prevent overlapping snack bars
|
||||
setTimeout(() => this.recognizeOtherWorkerOnMotion(content.senderName), 2000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (warning !== '') {
|
||||
this.raiseWarning(warning);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle leaving persons and
|
||||
* recognize if there is no other person editing the same motion anymore.
|
||||
*
|
||||
* @param senderId The id of the sender who has left the editing-view.
|
||||
*/
|
||||
private recognizeOtherWorkerOnMotion(senderName: string): void {
|
||||
this.otherWorkOnMotion = this.otherWorkOnMotion.filter(value => value !== senderName);
|
||||
if (this.otherWorkOnMotion.length === 0) {
|
||||
this.closeSnackBar();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to unsubscribe the notification subscription.
|
||||
* Before unsubscribing a notification will send with the reason.
|
||||
*
|
||||
* @param unsubscriptionReason The reason for the unsubscription.
|
||||
*/
|
||||
private unsubscribeEditNotifications(unsubscriptionReason: TypeOfNotificationViewMotion): void {
|
||||
if (!!this.editNotificationSubscription && !this.editNotificationSubscription.closed) {
|
||||
this.sendEditNotification(unsubscriptionReason);
|
||||
this.closeSnackBar();
|
||||
this.editNotificationSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the favorite status
|
||||
*/
|
||||
|
52
client/src/app/site/motions/models/view-motion-notify.ts
Normal file
52
client/src/app/site/motions/models/view-motion-notify.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Enum to define different types of notifications.
|
||||
*/
|
||||
export enum TypeOfNotificationViewMotion {
|
||||
/**
|
||||
* Type to declare editing a motion.
|
||||
*/
|
||||
TYPE_BEGIN_EDITING_MOTION = 'typeBeginEditingMotion',
|
||||
|
||||
/**
|
||||
* Type if the edit-view is closing.
|
||||
*/
|
||||
TYPE_CLOSING_EDITING_MOTION = 'typeClosingEditingMotion',
|
||||
|
||||
/**
|
||||
* Type if changes are saved.
|
||||
*/
|
||||
TYPE_SAVING_EDITING_MOTION = 'typeSavingEditingMotion',
|
||||
|
||||
/**
|
||||
* Type to declare if another person is also editing the same motion.
|
||||
*/
|
||||
TYPE_ALSO_EDITING_MOTION = 'typeAlsoEditingMotion'
|
||||
}
|
||||
/**
|
||||
* Class to specify the notifications for editing a motion.
|
||||
*/
|
||||
export interface ViewMotionNotificationEditMotion {
|
||||
/**
|
||||
* The id of the motion the user wants to edit.
|
||||
* Necessary to identify if users edit the same motion.
|
||||
*/
|
||||
motionId: number;
|
||||
|
||||
/**
|
||||
* The id of the sender.
|
||||
* Necessary if this differs from senderUserId.
|
||||
*/
|
||||
senderId: number;
|
||||
|
||||
/**
|
||||
* The name of the sender.
|
||||
* To show the names of the other editors
|
||||
*/
|
||||
senderName: string;
|
||||
|
||||
/**
|
||||
* The type of the notification.
|
||||
* Separates if the user is beginning the work or closing the edit-view.
|
||||
*/
|
||||
type: TypeOfNotificationViewMotion;
|
||||
}
|
Loading…
Reference in New Issue
Block a user