Merge pull request #4818 from CatoTH/OS3-title-change-recommendation

Change recommendations for titles
This commit is contained in:
Emanuel Schütze 2019-07-05 11:53:23 +02:00 committed by GitHub
commit e416231ef4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 674 additions and 165 deletions

View File

@ -18,6 +18,9 @@ import {
import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ChangeRecoMode, ViewMotion } from '../../../site/motions/models/view-motion';
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';
import { DiffService, LineRange, ModificationType } from '../../ui-services/diff.service';
/**
* Repository Services for change recommendations
@ -43,16 +46,20 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
* Converts existing and incoming motions to ViewMotions
* Handles CRUD using an observer to the DataStore
*
* @param DS The DataStore
* @param mapperService Maps collection strings to classes
* @param dataSend sending changed objects
* @param {DataStoreService} DS The DataStore
* @param {DataSendService} dataSend sending changed objects
* @param {CollectionStringMapperService} mapperService Maps collection strings to classes
* @param {ViewModelStoreService} viewModelStoreService
* @param {TranslateService} translate
* @param {DiffService} diffService
*/
public constructor(
DS: DataStoreService,
dataSend: DataSendService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
translate: TranslateService
translate: TranslateService,
private diffService: DiffService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionChangeRecommendation, [
Category,
@ -147,4 +154,75 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
});
await this.dataSend.partialUpdateModel(changeReco);
}
public getTitleWithChanges = (originalTitle: string, change: ViewUnifiedChange, crMode: ChangeRecoMode): string => {
if (change) {
if (crMode === ChangeRecoMode.Changed) {
return change.getChangeNewText();
} else if (
(crMode === ChangeRecoMode.Final || crMode === ChangeRecoMode.ModifiedFinal) &&
!change.isRejected()
) {
return change.getChangeNewText();
} else {
return originalTitle;
}
} else {
return originalTitle;
}
};
public getTitleChangesAsDiff = (originalTitle: string, change: ViewUnifiedChange): string => {
if (change) {
return this.diffService.diff(originalTitle, change.getChangeNewText());
} else {
return '';
}
};
/**
* Creates a {@link ViewMotionChangeRecommendation} object based on the motion ID and the given lange range.
* This object is not saved yet and does not yet have any changed HTML. It's meant to populate the UI form.
*
* @param {ViewMotion} motion
* @param {LineRange} lineRange
* @param {number} lineLength
*/
public createChangeRecommendationTemplate(
motion: ViewMotion,
lineRange: LineRange,
lineLength: number
): ViewMotionChangeRecommendation {
const changeReco = new MotionChangeRecommendation();
changeReco.line_from = lineRange.from;
changeReco.line_to = lineRange.to;
changeReco.type = ModificationType.TYPE_REPLACEMENT;
changeReco.text = this.diffService.extractMotionLineRange(motion.text, lineRange, false, lineLength, null);
changeReco.rejected = false;
changeReco.motion_id = motion.id;
return new ViewMotionChangeRecommendation(changeReco);
}
/**
* Creates a {@link ViewMotionChangeRecommendation} object to change the title, based on the motion ID.
* This object is not saved yet and does not yet have any changed title. It's meant to populate the UI form.
*
* @param {ViewMotion} motion
* @param {number} lineLength
*/
public createTitleChangeRecommendationTemplate(
motion: ViewMotion,
lineLength: number
): ViewMotionChangeRecommendation {
const changeReco = new MotionChangeRecommendation();
changeReco.line_from = 0;
changeReco.line_to = 0;
changeReco.type = ModificationType.TYPE_REPLACEMENT;
changeReco.text = motion.title;
changeReco.rejected = false;
changeReco.motion_id = motion.id;
return new ViewMotionChangeRecommendation(changeReco);
}
}

View File

@ -6,12 +6,12 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Category } from 'app/shared/models/motions/category';
import { ChangeRecoMode, ViewMotion, MotionTitleInformation } from 'app/site/motions/models/view-motion';
import { ChangeRecoMode, MotionTitleInformation, ViewMotion } from 'app/site/motions/models/view-motion';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService, CollectionIds } from '../../core-services/data-store.service';
import { DiffLinesInParagraph, DiffService, LineRange, ModificationType } from '../../ui-services/diff.service';
import { DataStoreService, CollectionIds } from 'app/core/core-services/data-store.service';
import { DiffService, DiffLinesInParagraph } from 'app/core/ui-services/diff.service';
import { HttpService } from 'app/core/core-services/http.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
@ -543,8 +543,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
})
.forEach((change: ViewUnifiedChange, idx: number) => {
if (idx === 0) {
text += this.extractMotionLineRange(
id,
text += this.diff.extractMotionLineRange(
targetMotion.text,
{
from: 1,
to: change.getLineFrom()
@ -554,8 +554,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
highlightLine
);
} else if (changes[idx - 1].getLineTo() < change.getLineFrom()) {
text += this.extractMotionLineRange(
id,
text += this.diff.extractMotionLineRange(
targetMotion.text,
{
from: changes[idx - 1].getLineTo(),
to: change.getLineFrom()
@ -612,36 +612,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
}
}
/**
* Extracts a renderable HTML string representing the given line number range of this motion
*
* @param {number} id
* @param {LineRange} lineRange
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
* @param {number} lineLength
* @param {number|null} highlightedLine
*/
public extractMotionLineRange(
id: number,
lineRange: LineRange,
lineNumbers: boolean,
lineLength: number,
highlightedLine: number
): string {
const origHtml = this.formatMotion(id, ChangeRecoMode.Original, [], lineLength);
const extracted = this.diff.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
let html =
extracted.outerContextStart +
extracted.innerContextStart +
extracted.html +
extracted.innerContextEnd +
extracted.outerContextEnd;
if (lineNumbers) {
html = this.lineNumbering.insertLineNumbers(html, lineLength, highlightedLine, null, lineRange.from);
}
return html;
}
/**
* Returns the last line number of a motion
*
@ -655,30 +625,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return range.to;
}
/**
* Creates a {@link ViewMotionChangeRecommendation} object based on the motion ID and the given lange range.
* This object is not saved yet and does not yet have any changed HTML. It's meant to populate the UI form.
*
* @param {number} motionId
* @param {LineRange} lineRange
* @param {number} lineLength
*/
public createChangeRecommendationTemplate(
motionId: number,
lineRange: LineRange,
lineLength: number
): ViewMotionChangeRecommendation {
const changeReco = new MotionChangeRecommendation();
changeReco.line_from = lineRange.from;
changeReco.line_to = lineRange.to;
changeReco.type = ModificationType.TYPE_REPLACEMENT;
changeReco.text = this.extractMotionLineRange(motionId, lineRange, false, lineLength, null);
changeReco.rejected = false;
changeReco.motion_id = motionId;
return new ViewMotionChangeRecommendation(changeReco);
}
/**
* Given an amendment, this returns the motion affected by this amendments
*

View File

@ -2317,4 +2317,34 @@ export class DiffService {
}
return html;
}
/**
* Extracts a renderable HTML string representing the given line number range of this motion text
*
* @param {string} motionText
* @param {LineRange} lineRange
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
* @param {number} lineLength
* @param {number|null} highlightedLine
*/
public extractMotionLineRange(
motionText: string,
lineRange: LineRange,
lineNumbers: boolean,
lineLength: number,
highlightedLine: number
): string {
const origHtml = this.lineNumberingService.insertLineNumbers(motionText, lineLength, highlightedLine);
const extracted = this.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
let html =
extracted.outerContextStart +
extracted.innerContextStart +
extracted.html +
extracted.innerContextEnd +
extracted.outerContextEnd;
if (lineNumbers) {
html = this.lineNumberingService.insertLineNumbers(html, lineLength, highlightedLine, null, lineRange.from);
}
return html;
}
}

View File

@ -13,6 +13,11 @@ export interface ViewUnifiedChange {
*/
getChangeType(): ViewUnifiedChangeType;
/**
* If this is a title-related change (only implemented for change recommendations)
*/
isTitleChange(): boolean;
/**
* An id that is unique considering both change recommendations and amendments, therefore needs to be
* "namespaced" (e.g. "amendment.23" or "recommendation.42")

View File

@ -111,4 +111,8 @@ export class ViewMotionAmendedParagraph implements ViewUnifiedChange {
public showInFinalView(): boolean {
return this.amendment.state && this.amendment.state.merge_amendment_into_final === MergeAmendment.YES;
}
public isTitleChange(): boolean {
return false; // Not implemented for amendments
}
}

View File

@ -100,4 +100,8 @@ export class ViewMotionChangeRecommendation extends BaseViewModel<MotionChangeRe
public showInFinalView(): boolean {
return !this.rejected;
}
public isTitleChange(): boolean {
return this.line_from === 0 && this.line_to === 0;
}
}

View File

@ -1,9 +1,9 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
MotionChangeRecommendationComponent,
MotionChangeRecommendationComponentData
} from './motion-change-recommendation.component';
MotionChangeRecommendationDialogComponent,
MotionChangeRecommendationDialogComponentData
} from './motion-change-recommendation-dialog.component';
import { E2EImportsModule } from 'e2e-imports.module';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@ -11,8 +11,8 @@ import { ModificationType } from 'app/core/ui-services/diff.service';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
describe('MotionChangeRecommendationComponent', () => {
let component: MotionChangeRecommendationComponent;
let fixture: ComponentFixture<MotionChangeRecommendationComponent>;
let component: MotionChangeRecommendationDialogComponent;
let fixture: ComponentFixture<MotionChangeRecommendationDialogComponent>;
const changeReco = <ViewMotionChangeRecommendation>{
line_from: 1,
@ -22,7 +22,7 @@ describe('MotionChangeRecommendationComponent', () => {
rejected: false,
motion_id: 1
};
const dialogData: MotionChangeRecommendationComponentData = {
const dialogData: MotionChangeRecommendationDialogComponentData = {
newChangeRecommendation: true,
editChangeRecommendation: false,
changeRecommendation: changeReco,
@ -32,7 +32,7 @@ describe('MotionChangeRecommendationComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [MotionChangeRecommendationComponent],
declarations: [MotionChangeRecommendationDialogComponent],
providers: [
{ provide: MatDialogRef, useValue: {} },
{
@ -44,7 +44,7 @@ describe('MotionChangeRecommendationComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(MotionChangeRecommendationComponent);
fixture = TestBed.createComponent(MotionChangeRecommendationDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -14,7 +14,7 @@ import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-mot
/**
* Data that needs to be provided to the MotionChangeRecommendationComponent dialog
*/
export interface MotionChangeRecommendationComponentData {
export interface MotionChangeRecommendationDialogComponentData {
editChangeRecommendation: boolean;
newChangeRecommendation: boolean;
lineRange: LineRange;
@ -26,13 +26,13 @@ export interface MotionChangeRecommendationComponentData {
*
* @example
* ```ts
* const data: MotionChangeRecommendationComponentData = {
* const data: MotionChangeRecommendationDialogComponentData = {
* editChangeRecommendation: false,
* newChangeRecommendation: true,
* lineRange: lineRange,
* changeReco: this.changeRecommendation,
* };
* this.dialogService.open(MotionChangeRecommendationComponent, {
* this.dialogService.open(MotionChangeRecommendationDialogComponent, {
* height: '400px',
* width: '600px',
* data: data,
@ -42,10 +42,10 @@ export interface MotionChangeRecommendationComponentData {
*/
@Component({
selector: 'os-motion-change-recommendation',
templateUrl: './motion-change-recommendation.component.html',
styleUrls: ['./motion-change-recommendation.component.scss']
templateUrl: './motion-change-recommendation-dialog.component.html',
styleUrls: ['./motion-change-recommendation-dialog.component.scss']
})
export class MotionChangeRecommendationComponent extends BaseViewComponent {
export class MotionChangeRecommendationDialogComponent extends BaseViewComponent {
/**
* Determine if the change recommendation is edited
*/
@ -91,13 +91,13 @@ export class MotionChangeRecommendationComponent extends BaseViewComponent {
];
public constructor(
@Inject(MAT_DIALOG_DATA) public data: MotionChangeRecommendationComponentData,
@Inject(MAT_DIALOG_DATA) public data: MotionChangeRecommendationDialogComponentData,
title: Title,
protected translate: TranslateService,
matSnackBar: MatSnackBar,
private formBuilder: FormBuilder,
private repo: ChangeRecommendationRepositoryService,
private dialogRef: MatDialogRef<MotionChangeRecommendationComponent>
private dialogRef: MatDialogRef<MotionChangeRecommendationDialogComponent>
) {
super(title, translate, matSnackBar);

View File

@ -10,12 +10,10 @@
[class.amendment]="isAmendment(change)"
[class.recommendation]="isChangeRecommendation(change)"
>
<span *ngIf="change.getLineFrom() >= change.getLineTo() - 1" class="line-number">
{{ 'Line' | translate }} {{ change.getLineFrom() }}
</span>
<span *ngIf="change.getLineFrom() < change.getLineTo() - 1" class="line-number">
{{ 'Line' | translate }} {{ change.getLineFrom() }} - {{ change.getLineTo() - 1 }}
<span *ngIf="!change.isTitleChange()" class="line-number">
{{ 'Line' | translate }} {{ formatLineRange(change) }}
</span>
<span *ngIf="change.isTitleChange()">{{ 'Title' | translate }}</span>
<span *ngIf="isChangeRecommendation(change)"> ({{ 'Change recommendation' | translate }})</span>
<span *ngIf="isAmendment(change)"> ({{ 'Amendment' | translate }} {{ change.getIdentifier() }})</span>
<span class="operation" *ngIf="isChangeRecommendation(change)">
@ -44,7 +42,38 @@
<!-- The actual diff view -->
<div class="motion-text-with-diffs">
<div *ngFor="let change of changes; let i = index">
<div *ngIf="getTitleChangingObject() as changedTitle">
<div
class="diff-box diff-box-{{ changedTitle.getChangeId() }} clearfix">
<div class="action-row" *osPerms="'motions.can_manage'">
<button
mat-icon-button
*ngIf="isRecommendation(changedTitle)"
type="button"
[matMenuTriggerFor]="changeRecommendationMenu"
[matMenuTriggerData]="{ change: changedTitle }"
>
<mat-icon>more_horiz</mat-icon>
</button>
</div>
<div class="status-row" *ngIf="changedTitle.isRejected()">
<i class="grey">{{ 'Rejected' | translate }}</i>
</div>
<div class="motion-text motion-text-diff"
[class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()"
[attr.data-change-id]="changedTitle.getChangeId()"
>
<div class="bold">
{{ 'Changed title' | translate }}:
</div>
<div [innerHTML]="getFormattedTitleDiff()"></div>
</div>
</div>
</div>
<div *ngFor="let change of getAllTextChangingObjects(); let i = index">
<div
class="motion-text"
[class.line-numbers-none]="isLineNumberingNone()"
@ -52,7 +81,7 @@
[class.line-numbers-outside]="isLineNumberingOutside()"
>
<os-motion-detail-original-change-recommendations
[html]="getTextBetweenChanges(changes[i - 1], change)"
[html]="getTextBetweenChanges(getAllTextChangingObjects()[i - 1], change)"
[changeRecommendations]="[]"
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
></os-motion-detail-original-change-recommendations>
@ -60,9 +89,9 @@
<div
class="diff-box diff-box-{{ change.getChangeId() }} clearfix"
[class.collides]="hasCollissions(change, changes)"
[class.collides]="hasCollissions(change, getAllTextChangingObjects())"
>
<div class="collission-hint" *ngIf="hasCollissions(change, changes)">
<div class="collission-hint" *ngIf="hasCollissions(change, getAllTextChangingObjects())">
<mat-icon matTooltip="{{ 'This change collides with another one.' | translate }}">warning</mat-icon>
</div>
<div class="action-row" *osPerms="'motions.can_manage'">
@ -144,7 +173,11 @@
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
<button type="button" mat-menu-item (click)="editChangeRecommendation(change, $event)">
<button type="button" mat-menu-item (click)="editChangeRecommendation(change, $event)" *ngIf="!change.isTitleChange()">
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>
<button type="button" mat-menu-item (click)="editTitleChangeRecommendation(change, $event)" *ngIf="change.isTitleChange()">
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>

View File

@ -10,10 +10,13 @@ import { ConfigService } from 'app/core/ui-services/config.service';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { DiffService, LineRange } from 'app/core/ui-services/diff.service';
import {
MotionChangeRecommendationComponent,
MotionChangeRecommendationComponentData
} from '../motion-change-recommendation/motion-change-recommendation.component';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
MotionChangeRecommendationDialogComponent,
MotionChangeRecommendationDialogComponentData
} from '../motion-change-recommendation-dialog/motion-change-recommendation-dialog.component';
import {
MotionTitleChangeRecommendationDialogComponent,
MotionTitleChangeRecommendationDialogComponentData
} from '../motion-title-change-recommendation-dialog/motion-title-change-recommendation-dialog.component';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewMotion, LineNumberingMode } from 'app/site/motions/models/view-motion';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
@ -78,7 +81,6 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param translate
* @param matSnackBar
* @param sanitizer
* @param motionRepo
* @param diff
* @param recoRepo
* @param dialogService
@ -91,7 +93,6 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar,
private sanitizer: DomSanitizer,
private motionRepo: MotionRepositoryService,
private diff: DiffService,
private recoRepo: ChangeRecommendationRepositoryService,
private dialogService: MatDialog,
@ -121,8 +122,8 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
return '';
}
return this.motionRepo.extractMotionLineRange(
this.motion.id,
return this.diff.extractMotionLineRange(
this.motion.text,
lineRange,
true,
this.lineLength,
@ -175,6 +176,20 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
);
}
/**
* If only one line is affected, the line number is returned; otherwise, a string like [line] "1 - 5"
*
* @param {ViewUnifiedChange} change
* @returns string
*/
public formatLineRange(change: ViewUnifiedChange): string {
if (change.getLineFrom() < change.getLineTo() - 1) {
return change.getLineFrom().toString(10) + ' - ' + (change.getLineTo() - 1).toString(10);
} else {
return change.getLineFrom().toString(10);
}
}
/**
* Returns true if the change is a Change Recommendation
*
@ -229,6 +244,19 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
return change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION;
}
public getAllTextChangingObjects(): ViewUnifiedChange[] {
return this.changes.filter((obj: ViewUnifiedChange) => !obj.isTitleChange());
}
public getTitleChangingObject(): ViewUnifiedChange {
return this.changes.find((obj: ViewUnifiedChange) => obj.isTitleChange());
}
public getFormattedTitleDiff(): SafeHtml {
const change = this.getTitleChangingObject();
return this.sanitizer.bypassSecurityTrustHtml(this.recoRepo.getTitleChangesAsDiff(this.motion.title, change));
}
/**
* Sets a change recommendation to accepted or rejected.
* The template has to make sure only to pass change recommendations to this method.
@ -286,7 +314,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
$event.stopPropagation();
$event.preventDefault();
const data: MotionChangeRecommendationComponentData = {
const data: MotionChangeRecommendationDialogComponentData = {
editChangeRecommendation: true,
newChangeRecommendation: false,
lineRange: {
@ -295,7 +323,26 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
},
changeRecommendation: reco
};
this.dialogService.open(MotionChangeRecommendationComponent, {
this.dialogService.open(MotionChangeRecommendationDialogComponent, {
height: '600px',
width: '800px',
maxHeight: '90vh',
maxWidth: '90vw',
data: data,
disableClose: true
});
}
public editTitleChangeRecommendation(reco: ViewMotionChangeRecommendation, $event: MouseEvent): void {
$event.stopPropagation();
$event.preventDefault();
const data: MotionTitleChangeRecommendationDialogComponentData = {
editChangeRecommendation: true,
newChangeRecommendation: false,
changeRecommendation: reco
};
this.dialogService.open(MotionTitleChangeRecommendationDialogComponent, {
height: '600px',
width: '800px',
maxHeight: '90vh',

View File

@ -1,6 +1,6 @@
<div class="text"></div>
<ul class="change-recommendation-list" *ngIf="showChangeRecommendations">
<li *ngFor="let reco of changeRecommendations" [title]="reco.getTitle()"
<li *ngFor="let reco of getTextChangeRecommendations()" [title]="reco.getTitle()"
[style.top]="calcRecoTop(reco)" [style.height]="calcRecoHeight(reco)"
[class.delete]="recoIsDeletion(reco)" [class.insert]="recoIsInsertion(reco)"
[class.replace]="recoIsReplacement(reco)" (click)="gotoReco(reco)"></li>

View File

@ -105,6 +105,10 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
}
}
public getTextChangeRecommendations(): ViewMotionChangeRecommendation[] {
return this.changeRecommendations.filter(reco => !reco.isTitleChange());
}
/**
* Returns an array with all line numbers that are currently affected by a change recommendation
* and therefor not subject to further changes
@ -112,6 +116,9 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
private getAffectedLineNumbers(): number[] {
const affectedLines = [];
this.changeRecommendations.forEach((change: ViewMotionChangeRecommendation) => {
if (change.isTitleChange()) {
return;
}
for (let j = change.line_from; j < change.line_to; j++) {
affectedLines.push(j);
}

View File

@ -124,7 +124,16 @@
<!-- Title -->
<div class="title" *ngIf="motion && !editMotion">
<div class="title-line">
<h1>{{ motion.title }}</h1>
<h1 class="motion-title">
<span *ngIf="titleCanBeChanged()">
<span class="title-change-indicator" *ngIf="getTitleChangingObject()"
(click)="gotoChangeRecommendation(getTitleChangingObject())"></span>
<span class="change-title" *osPerms="'motions.can_manage'; and: !getTitleChangingObject()"
(click)="createTitleChangeRecommendation()"></span>
</span>
{{ getTitleWithChanges() }}
</h1>
<button mat-icon-button color="primary" (click)="toggleFavorite()">
<mat-icon>{{ motion.star ? 'star' : 'star_border' }}</mat-icon>
</button>

View File

@ -170,6 +170,48 @@ span {
}
.title-line {
display: flex;
.motion-title {
position: relative;
z-index: 1;
// Grab the left padding of the parent element to catch hover-events for the :before element
margin-left: -20px;
padding-left: 20px;
.change-title {
position: relative;
width: 0;
height: 0;
}
.change-title:before {
position: absolute;
top: 18px;
left: -17px;
display: none;
cursor: pointer;
content: '';
width: 16px;
height: 16px;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" fill="%23337ab7"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
background-size: 16px 16px;
}
&:hover .change-title:before {
display: block;
}
.title-change-indicator {
background-color: #0333ff;
position: absolute;
width: 4px;
height: 32px;
left: 10px;
top: 5px;
cursor: pointer;
}
}
}
.create-poll-button {

View File

@ -15,7 +15,7 @@ import { CategoryRepositoryService } from 'app/core/repositories/motions/categor
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { CreateMotion } from 'app/site/motions/models/create-motion';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DiffLinesInParagraph, LineRange } from 'app/core/ui-services/diff.service';
import { DiffLinesInParagraph, DiffService, LineRange } from 'app/core/ui-services/diff.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
@ -23,9 +23,13 @@ import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
import { Motion } from 'app/shared/models/motions/motion';
import {
MotionChangeRecommendationComponentData,
MotionChangeRecommendationComponent
} from '../motion-change-recommendation/motion-change-recommendation.component';
MotionChangeRecommendationDialogComponentData,
MotionChangeRecommendationDialogComponent
} from '../motion-change-recommendation-dialog/motion-change-recommendation-dialog.component';
import {
MotionTitleChangeRecommendationDialogComponentData,
MotionTitleChangeRecommendationDialogComponent
} from '../motion-title-change-recommendation-dialog/motion-title-change-recommendation-dialog.component';
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.service';
@ -401,6 +405,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* @param pdfExport export the motion to pdf
* @param personalNoteService: personal comments and favorite marker
* @param linenumberingService The line numbering service
* @param diffService The diff service
* @param categoryRepo Repository for categories
* @param viewModelStore accessing view models
* @param categoryRepo access the category repository
@ -435,6 +440,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
private pdfExport: MotionPdfExportService,
private personalNoteService: PersonalNoteService,
private linenumberingService: LinenumberingService,
private diffService: DiffService,
private categoryRepo: CategoryRepositoryService,
private userRepo: UserRepositoryService,
private notifyService: NotifyService,
@ -569,7 +575,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
this.allChangingObjects = [];
if (this.changeRecommendations) {
this.changeRecommendations.forEach((change: ViewUnifiedChange): void => {
this.changeRecommendations.forEach((change: ViewMotionChangeRecommendation): void => {
this.allChangingObjects.push(change);
});
}
@ -854,7 +860,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
*/
public getFormattedTextPlain(): string {
// Prevent this.allChangingObjects to be reordered from within formatMotion
const changes: ViewUnifiedChange[] = Object.assign([], this.allChangingObjects);
const changes: ViewUnifiedChange[] = Object.assign([], this.getAllTextChangingObjects());
return this.repo.formatMotion(this.motion.id, this.crMode, changes, this.lineLength, this.highlightedLine);
}
@ -888,8 +894,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* @returns safe html strings
*/
public getParentMotionRange(from: number, to: number): SafeHtml {
const str = this.repo.extractMotionLineRange(
this.motion.parent_id,
const parentMotion = this.repo.getViewModel(this.motion.parent_id);
const str = this.diffService.extractMotionLineRange(
parentMotion.text,
{ from, to },
true,
this.lineLength,
@ -920,6 +927,18 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
});
}
public getAllTextChangingObjects(): ViewUnifiedChange[] {
return this.allChangingObjects.filter((obj: ViewUnifiedChange) => !obj.isTitleChange());
}
public getTitleChangingObject(): ViewUnifiedChange {
return this.allChangingObjects.find((obj: ViewUnifiedChange) => obj.isTitleChange());
}
public getTitleWithChanges(): string {
return this.changeRecoRepo.getTitleWithChanges(this.motion.title, this.getTitleChangingObject(), this.crMode);
}
/**
* Trigger to delete the motion.
*/
@ -1027,17 +1046,17 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* @param lineRange
*/
public createChangeRecommendation(lineRange: LineRange): void {
const data: MotionChangeRecommendationComponentData = {
const data: MotionChangeRecommendationDialogComponentData = {
editChangeRecommendation: false,
newChangeRecommendation: true,
lineRange: lineRange,
changeRecommendation: this.repo.createChangeRecommendationTemplate(
this.motion.id,
changeRecommendation: this.changeRecoRepo.createChangeRecommendationTemplate(
this.motion,
lineRange,
this.lineLength
)
};
this.dialogService.open(MotionChangeRecommendationComponent, {
this.dialogService.open(MotionChangeRecommendationDialogComponent, {
height: '600px',
width: '800px',
maxHeight: '90vh',
@ -1047,6 +1066,37 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
});
}
/**
* In the original version, the title has been clicked to create a new change recommendation
*/
public createTitleChangeRecommendation(): void {
const data: MotionTitleChangeRecommendationDialogComponentData = {
editChangeRecommendation: false,
newChangeRecommendation: true,
changeRecommendation: this.changeRecoRepo.createTitleChangeRecommendationTemplate(
this.motion,
this.lineLength
)
};
this.dialogService.open(MotionTitleChangeRecommendationDialogComponent, {
width: '400px',
maxHeight: '90vh',
maxWidth: '90vw',
data: data,
disableClose: true
});
}
public titleCanBeChanged(): boolean {
if (this.editMotion) {
return false;
}
if (this.motion.isStatuteAmendment() || this.motion.isParagraphBasedAmendment()) {
return false;
}
return this.isRecoMode(ChangeRecoMode.Original) || this.isRecoMode(ChangeRecoMode.Diff);
}
/**
* In the original version, a change-recommendation-annotation has been clicked
* -> Go to the diff view and scroll to the change recommendation

View File

@ -0,0 +1,19 @@
<h1 mat-dialog-title translate>New change recommendation</h1>
<mat-dialog-content>
<form class="motion-content" [formGroup]="contentForm" (ngSubmit)="saveChangeRecommendation()">
<mat-form-field>
<input matInput placeholder="{{ 'New title' | translate }}" formControlName="title" />
</mat-form-field>
<mat-checkbox formControlName="public">{{ 'Public' | translate }}</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button (click)="saveChangeRecommendation()">
<span translate>Save</span>
</button>
<button mat-button mat-dialog-close>
<span translate>Cancel</span>
</button>
</mat-dialog-actions>

View File

@ -0,0 +1,9 @@
.motion-content {
.mat-form-field {
width: 100%;
}
}
.mat-dialog-content {
overflow: hidden;
}

View File

@ -0,0 +1,54 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
MotionTitleChangeRecommendationDialogComponent,
MotionTitleChangeRecommendationDialogComponentData
} from './motion-title-change-recommendation-dialog.component';
import { E2EImportsModule } from 'e2e-imports.module';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { ModificationType } from 'app/core/ui-services/diff.service';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
describe('MotionTitleChangeRecommendationDialogComponent', () => {
let component: MotionTitleChangeRecommendationDialogComponent;
let fixture: ComponentFixture<MotionTitleChangeRecommendationDialogComponent>;
const changeReco = <ViewMotionChangeRecommendation>{
line_from: 0,
line_to: 0,
type: ModificationType.TYPE_REPLACEMENT,
text: 'Motion title',
rejected: false,
motion_id: 1
};
const dialogData: MotionTitleChangeRecommendationDialogComponentData = {
newChangeRecommendation: true,
editChangeRecommendation: false,
changeRecommendation: changeReco
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [MotionTitleChangeRecommendationDialogComponent],
providers: [
{ provide: MatDialogRef, useValue: {} },
{
provide: MAT_DIALOG_DATA,
useValue: dialogData
}
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MotionTitleChangeRecommendationDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,113 @@
import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatSnackBar } from '@angular/material';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { ModificationType } from 'app/core/ui-services/diff.service';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
/**
* Data that needs to be provided to the MotionTitleChangeRecommendationComponent dialog
*/
export interface MotionTitleChangeRecommendationDialogComponentData {
editChangeRecommendation: boolean;
newChangeRecommendation: boolean;
changeRecommendation: ViewMotionChangeRecommendation;
}
/**
* The dialog for creating and editing title change recommendations from within the os-motion-detail-component.
*
* @example
* ```ts
* const data: MotionTitleChangeRecommendationDialogComponentData = {
* editChangeRecommendation: false,
* newChangeRecommendation: true,
* changeReco: this.changeRecommendation,
* };
* this.dialogService.open(MotionTitleChangeRecommendationDialogComponent, {
* height: '400px',
* width: '600px',
* data: data,
* });
* ```
*/
@Component({
selector: 'os-title-motion-change-recommendation-dialog',
templateUrl: './motion-title-change-recommendation-dialog.component.html',
styleUrls: ['./motion-title-change-recommendation-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MotionTitleChangeRecommendationDialogComponent extends BaseViewComponent {
/**
* Determine if the change recommendation is edited
*/
public editReco = false;
/**
* Determine if the change recommendation is new
*/
public newReco = false;
/**
* The change recommendation
*/
public changeReco: ViewMotionChangeRecommendation;
/**
* Change recommendation content.
*/
public contentForm: FormGroup;
public constructor(
@Inject(MAT_DIALOG_DATA) public data: MotionTitleChangeRecommendationDialogComponentData,
title: Title,
protected translate: TranslateService,
matSnackBar: MatSnackBar,
private formBuilder: FormBuilder,
private repo: ChangeRecommendationRepositoryService,
private dialogRef: MatDialogRef<MotionTitleChangeRecommendationDialogComponent>
) {
super(title, translate, matSnackBar);
this.editReco = data.editChangeRecommendation;
this.newReco = data.newChangeRecommendation;
this.changeReco = data.changeRecommendation;
this.createForm();
}
/**
* Creates the forms for the Motion and the MotionVersion
*/
public createForm(): void {
this.contentForm = this.formBuilder.group({
title: [this.changeReco.text, Validators.required],
public: [!this.changeReco.internal]
});
}
public async saveChangeRecommendation(): Promise<void> {
this.changeReco.updateChangeReco(
ModificationType.TYPE_REPLACEMENT,
this.contentForm.controls.title.value,
!this.contentForm.controls.public.value
);
try {
if (this.newReco) {
await this.repo.createByViewModel(this.changeReco);
this.dialogRef.close();
} else {
await this.repo.update(this.changeReco.changeRecommendation, this.changeReco);
this.dialogRef.close();
}
} catch (e) {
this.raiseError(e);
}
}
}

View File

@ -12,7 +12,8 @@ import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-
import { MotionPollComponent } from './components/motion-poll/motion-poll.component';
import { MotionDetailOriginalChangeRecommendationsComponent } from './components/motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
import { MotionDetailDiffComponent } from './components/motion-detail-diff/motion-detail-diff.component';
import { MotionChangeRecommendationComponent } from './components/motion-change-recommendation/motion-change-recommendation.component';
import { MotionChangeRecommendationDialogComponent } from './components/motion-change-recommendation-dialog/motion-change-recommendation-dialog.component';
import { MotionTitleChangeRecommendationDialogComponent } from './components/motion-title-change-recommendation-dialog/motion-title-change-recommendation-dialog.component';
@NgModule({
imports: [CommonModule, MotionDetailRoutingModule, SharedModule],
@ -26,14 +27,16 @@ import { MotionChangeRecommendationComponent } from './components/motion-change-
MotionPollDialogComponent,
MotionDetailDiffComponent,
MotionDetailOriginalChangeRecommendationsComponent,
MotionChangeRecommendationComponent
MotionChangeRecommendationDialogComponent,
MotionTitleChangeRecommendationDialogComponent
],
entryComponents: [
MotionCommentsComponent,
PersonalNoteComponent,
ManageSubmittersComponent,
MotionPollDialogComponent,
MotionChangeRecommendationComponent
MotionChangeRecommendationDialogComponent,
MotionTitleChangeRecommendationDialogComponent
]
})
export class MotionDetailModule {}

View File

@ -10,7 +10,7 @@ import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service';
import { MotionPollService } from './motion-poll.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion';
import { ChangeRecoMode, LineNumberingMode, ViewMotion } from '../models/view-motion';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
@ -124,7 +124,7 @@ export class MotionPdfService {
crMode = this.configService.instant('motions_recommendation_text_mode');
}
const title = this.createTitle(motion);
const title = this.createTitle(motion, crMode, lineLength);
const sequential = !infoToExport || infoToExport.includes('id');
const subtitle = this.createSubtitle(motion, sequential);
@ -167,11 +167,18 @@ export class MotionPdfService {
* Create the motion title part of the doc definition
*
* @param motion the target motion
* @param crMode the change recommendation mode
* @param lineLength the line length
* @returns doc def for the document title
*/
private createTitle(motion: ViewMotion): object {
private createTitle(motion: ViewMotion, crMode: ChangeRecoMode, lineLength: number): object {
// summary of change recommendations (for motion diff version only)
const changes = this.getUnifiedChanges(motion, lineLength);
const titleChange = changes.find(change => change.isTitleChange());
const changedTitle = this.changeRecoRepo.getTitleWithChanges(motion.title, titleChange, crMode);
const identifier = motion.identifier ? ' ' + motion.identifier : '';
const title = `${this.translate.instant('Motion')} ${identifier}: ${motion.title}`;
const title = `${this.translate.instant('Motion')} ${identifier}: ${changedTitle}`;
return {
text: title,
@ -399,39 +406,44 @@ export class MotionPdfService {
const columnChangeType = [];
changes.forEach(change => {
// TODO: the function isTitleRecommendation() does not exist anymore.
// Not sure if required or not
// if (changeReco.isTitleRecommendation()) {
// columnLineNumbers.push(gettextCatalog.getString('Title') + ': ');
// } else { ... }
// line numbers column
let line;
if (change.getLineFrom() >= change.getLineTo() - 1) {
line = change.getLineFrom();
} else {
line = change.getLineFrom() + ' - ' + (change.getLineTo() - 1);
}
// change type column
if (change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION) {
if (change.isTitleChange()) {
// Is always a change recommendation
const changeReco = change as ViewMotionChangeRecommendation;
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
columnLineNumbers.push(`${this.translate.instant('Title')}: `);
columnChangeType.push(
`(${this.translate.instant('Change recommendation')}) - ${this.translate.instant(
this.getRecommendationTypeName(changeReco)
)}`
);
} else if (change.getChangeType() === ViewUnifiedChangeType.TYPE_AMENDMENT) {
const amendment = change as ViewMotionAmendedParagraph;
let summaryText = `(${this.translate.instant('Amendment')} ${amendment.getIdentifier()}) -`;
if (amendment.isRejected()) {
summaryText += ` ${this.translate.instant('Rejected')}`;
} else if (amendment.isAccepted()) {
summaryText += ` ${this.translate.instant(amendment.stateName)}`;
// only append line and change, if the merge of the state of the amendment is accepted.
} else {
// line numbers column
let line;
if (change.getLineFrom() >= change.getLineTo() - 1) {
line = change.getLineFrom();
} else {
line = change.getLineFrom() + ' - ' + (change.getLineTo() - 1);
}
// change type column
if (change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION) {
const changeReco = change as ViewMotionChangeRecommendation;
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
columnChangeType.push(summaryText);
columnChangeType.push(
`(${this.translate.instant('Change recommendation')}) - ${this.translate.instant(
this.getRecommendationTypeName(changeReco)
)}`
);
} else if (change.getChangeType() === ViewUnifiedChangeType.TYPE_AMENDMENT) {
const amendment = change as ViewMotionAmendedParagraph;
let summaryText = `(${this.translate.instant('Amendment')} ${amendment.getIdentifier()}) -`;
if (amendment.isRejected()) {
summaryText += ` ${this.translate.instant('Rejected')}`;
} else if (amendment.isAccepted()) {
summaryText += ` ${this.translate.instant(amendment.stateName)}`;
// only append line and change, if the merge of the state of the amendment is accepted.
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
columnChangeType.push(summaryText);
}
}
}
});
@ -537,6 +549,7 @@ export class MotionPdfService {
* Creates the motion text - uses HTML to PDF
*
* @param motion the motion to convert to pdf
* @param lineLength the current line length
* @param lnMode determine the used line mode
* @param crMode determine the used change Recommendation mode
* @returns doc def for the "the assembly may decide" preamble
@ -547,10 +560,9 @@ export class MotionPdfService {
lnMode: LineNumberingMode,
crMode: ChangeRecoMode
): object {
let motionText: string;
let motionText = '';
if (motion.isParagraphBasedAmendment()) {
motionText = '';
// this is logically redundant with the formation of amendments in the motion-detail html.
// Should be refactored in a way that a service returns the correct html for both cases
for (const paragraph of this.motionRepo.getAmendmentParagraphs(motion, lineLength, false)) {
@ -577,9 +589,21 @@ export class MotionPdfService {
// TODO: Consider tile change recommendation
const changes = this.getUnifiedChanges(motion, lineLength);
motionText = this.motionRepo.formatMotion(motion.id, crMode, changes, lineLength);
const textChanges = changes.filter(change => !change.isTitleChange());
const titleChange = changes.find(change => change.isTitleChange());
if (crMode === ChangeRecoMode.Diff && titleChange) {
const changedTitle = this.changeRecoRepo.getTitleChangesAsDiff(motion.title, titleChange);
motionText +=
'<span><strong>' +
this.translate.instant('Changed title') +
':</strong><br>' +
changedTitle +
'</span><br>';
}
const formattedText = this.motionRepo.formatMotion(motion.id, crMode, textChanges, lineLength);
// reformat motion text to split long HTML elements to easier convert into PDF
motionText = this.linenumberingService.splitInlineElementsAtLineBreaks(motionText);
motionText += this.linenumberingService.splitInlineElementsAtLineBreaks(formattedText);
}
return this.htmlToPdfService.convertHtml(motionText, lnMode);
@ -755,14 +779,11 @@ export class MotionPdfService {
* @returns pdfMake definitions
*/
public textToDocDef(note: string, motion: ViewMotion, noteTitle: string): object {
const title = this.createTitle(motion);
const lineLength = this.configService.instant<number>('motions_line_length');
const crMode = this.configService.instant<ChangeRecoMode>('motions_recommendation_text_mode');
const title = this.createTitle(motion, crMode, lineLength);
const subtitle = this.createSubtitle(motion);
const metaInfo = this.createMetaInfoTable(
motion,
this.configService.instant<number>('motions_line_length'),
this.configService.instant('motions_recommendation_text_mode'),
['submitters', 'state', 'category']
);
const metaInfo = this.createMetaInfoTable(motion, lineLength, crMode, ['submitters', 'state', 'category']);
const noteContent = this.htmlToPdfService.convertHtml(note, LineNumberingMode.None);
const subHeading = {

View File

@ -58,4 +58,8 @@ export class MotionSlideObjAmendmentParagraph implements ViewUnifiedChange {
public showInFinalView(): boolean {
return this.merge_amendment_into_final === 1;
}
public isTitleChange(): boolean {
return false; // Not implemented for amendments
}
}

View File

@ -56,4 +56,8 @@ export class MotionSlideObjChangeReco implements MotionSlideDataChangeReco, View
public showInFinalView(): boolean {
return !this.rejected;
}
public isTitleChange(): boolean {
return this.line_from === 0 && this.line_to === 0;
}
}

View File

@ -25,7 +25,7 @@
<!-- Title -->
<div class="spacer" [ngStyle]="{height: projector.show_header_footer ? '50px' : '0'}"></div>
<div class="slidetitle">
<h1>{{ data.data.title }}</h1>
<h1>{{ getTitleWithChanges() }}</h1>
<h2><span translate>Motion</span> {{ data.data.identifier }}</h2>
</div>
</div>
@ -43,6 +43,12 @@
[class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()"
>
<div *ngIf="getTitleChangingObject() && crMode === 'diff'">
<div class="bold">
{{ 'Changed title' | translate }}:
</div>
<div [innerHTML]="getFormattedTitleDiff()"></div>
</div>
<div [innerHTML]="sanitizedText(getFormattedText())"></div>
</div>
</ng-container>

View File

@ -13,6 +13,7 @@ import { SlideData } from 'app/core/core-services/projector-data.service';
import { MotionSlideObjAmendmentParagraph } from './motion-slide-obj-amendment-paragraph';
import { BaseMotionSlideComponent } from '../base/base-motion-slide';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { IBaseScaleScrollSlideComponent } from 'app/slides/base-scale-scroll-slide-component';
@Component({
@ -111,6 +112,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
public constructor(
translate: TranslateService,
motionRepo: MotionRepositoryService,
private changeRepo: ChangeRecommendationRepositoryService,
private sanitizer: DomSanitizer,
private lineNumbering: LinenumberingService,
private diff: DiffService
@ -293,6 +295,24 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
return html;
}
public getAllTextChangingObjects(): ViewUnifiedChange[] {
return this.allChangingObjects.filter((obj: ViewUnifiedChange) => !obj.isTitleChange());
}
public getTitleChangingObject(): ViewUnifiedChange {
return this.allChangingObjects.find((obj: ViewUnifiedChange) => obj.isTitleChange());
}
public getTitleWithChanges(): string {
return this.changeRepo.getTitleWithChanges(this.data.data.title, this.getTitleChangingObject(), this.crMode);
}
public getFormattedTitleDiff(): SafeHtml {
const change = this.getTitleChangingObject();
const diff = this.changeRepo.getTitleChangesAsDiff(this.data.data.title, change);
return this.sanitizer.bypassSecurityTrustHtml(diff);
}
/**
* get the formated motion text from the repository.
*
@ -309,13 +329,13 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
case ChangeRecoMode.Changed:
return this.diff.getTextWithChanges(
motion.text,
this.allChangingObjects,
this.getAllTextChangingObjects(),
this.lineLength,
this.highlightedLine
);
case ChangeRecoMode.Diff:
let text = '';
const changes = this.allChangingObjects.filter(change => {
const changes = this.getAllTextChangingObjects().filter(change => {
return change.showInDiffView();
});
changes.forEach((change: ViewUnifiedChange, idx: number) => {
@ -339,7 +359,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
);
return text;
case ChangeRecoMode.Final:
const appliedChanges: ViewUnifiedChange[] = this.allChangingObjects.filter(change =>
const appliedChanges: ViewUnifiedChange[] = this.getAllTextChangingObjects().filter(change =>
change.showInFinalView()
);
return this.diff.getTextWithChanges(motion.text, appliedChanges, this.lineLength, this.highlightedLine);
@ -354,7 +374,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
);
} else {
// Use the final version as fallback, if the modified does not exist.
const appliedChangeObjects: ViewUnifiedChange[] = this.allChangingObjects.filter(change =>
const appliedChangeObjects: ViewUnifiedChange[] = this.getAllTextChangingObjects().filter(change =>
change.showInFinalView()
);
return this.diff.getTextWithChanges(

View File

@ -262,7 +262,8 @@ a {
}
strong,
b {
b,
.bold {
font-weight: 500;
}