Change recommendations for titles

- Title changes in PDF, Diff-view and slides
This commit is contained in:
Tobias Hößl 2019-06-30 09:30:11 +02:00 committed by Sean Engelhardt
parent b06f879602
commit 2592862384
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 { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.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 * Repository Services for change recommendations
@ -43,16 +46,20 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
* Converts existing and incoming motions to ViewMotions * Converts existing and incoming motions to ViewMotions
* Handles CRUD using an observer to the DataStore * Handles CRUD using an observer to the DataStore
* *
* @param DS The DataStore * @param {DataStoreService} DS The DataStore
* @param mapperService Maps collection strings to classes * @param {DataSendService} dataSend sending changed objects
* @param dataSend sending changed objects * @param {CollectionStringMapperService} mapperService Maps collection strings to classes
* @param {ViewModelStoreService} viewModelStoreService
* @param {TranslateService} translate
* @param {DiffService} diffService
*/ */
public constructor( public constructor(
DS: DataStoreService, DS: DataStoreService,
dataSend: DataSendService, dataSend: DataSendService,
mapperService: CollectionStringMapperService, mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService translate: TranslateService,
private diffService: DiffService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionChangeRecommendation, [ super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionChangeRecommendation, [
Category, Category,
@ -147,4 +154,75 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
}); });
await this.dataSend.partialUpdateModel(changeReco); 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 { map } from 'rxjs/operators';
import { Category } from 'app/shared/models/motions/category'; 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 { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.service'; import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService, CollectionIds } from '../../core-services/data-store.service'; import { DataStoreService, CollectionIds } from 'app/core/core-services/data-store.service';
import { DiffLinesInParagraph, DiffService, LineRange, ModificationType } from '../../ui-services/diff.service'; import { DiffService, DiffLinesInParagraph } from 'app/core/ui-services/diff.service';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service'; import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
@ -543,8 +543,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
}) })
.forEach((change: ViewUnifiedChange, idx: number) => { .forEach((change: ViewUnifiedChange, idx: number) => {
if (idx === 0) { if (idx === 0) {
text += this.extractMotionLineRange( text += this.diff.extractMotionLineRange(
id, targetMotion.text,
{ {
from: 1, from: 1,
to: change.getLineFrom() to: change.getLineFrom()
@ -554,8 +554,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
highlightLine highlightLine
); );
} else if (changes[idx - 1].getLineTo() < change.getLineFrom()) { } else if (changes[idx - 1].getLineTo() < change.getLineFrom()) {
text += this.extractMotionLineRange( text += this.diff.extractMotionLineRange(
id, targetMotion.text,
{ {
from: changes[idx - 1].getLineTo(), from: changes[idx - 1].getLineTo(),
to: change.getLineFrom() 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 * Returns the last line number of a motion
* *
@ -655,30 +625,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return range.to; 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 * Given an amendment, this returns the motion affected by this amendments
* *

View File

@ -2317,4 +2317,34 @@ export class DiffService {
} }
return html; 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; 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 * An id that is unique considering both change recommendations and amendments, therefore needs to be
* "namespaced" (e.g. "amendment.23" or "recommendation.42") * "namespaced" (e.g. "amendment.23" or "recommendation.42")

View File

@ -111,4 +111,8 @@ export class ViewMotionAmendedParagraph implements ViewUnifiedChange {
public showInFinalView(): boolean { public showInFinalView(): boolean {
return this.amendment.state && this.amendment.state.merge_amendment_into_final === MergeAmendment.YES; 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 { public showInFinalView(): boolean {
return !this.rejected; 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { import {
MotionChangeRecommendationComponent, MotionChangeRecommendationDialogComponent,
MotionChangeRecommendationComponentData MotionChangeRecommendationDialogComponentData
} from './motion-change-recommendation.component'; } from './motion-change-recommendation-dialog.component';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 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'; import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
describe('MotionChangeRecommendationComponent', () => { describe('MotionChangeRecommendationComponent', () => {
let component: MotionChangeRecommendationComponent; let component: MotionChangeRecommendationDialogComponent;
let fixture: ComponentFixture<MotionChangeRecommendationComponent>; let fixture: ComponentFixture<MotionChangeRecommendationDialogComponent>;
const changeReco = <ViewMotionChangeRecommendation>{ const changeReco = <ViewMotionChangeRecommendation>{
line_from: 1, line_from: 1,
@ -22,7 +22,7 @@ describe('MotionChangeRecommendationComponent', () => {
rejected: false, rejected: false,
motion_id: 1 motion_id: 1
}; };
const dialogData: MotionChangeRecommendationComponentData = { const dialogData: MotionChangeRecommendationDialogComponentData = {
newChangeRecommendation: true, newChangeRecommendation: true,
editChangeRecommendation: false, editChangeRecommendation: false,
changeRecommendation: changeReco, changeRecommendation: changeReco,
@ -32,7 +32,7 @@ describe('MotionChangeRecommendationComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [E2EImportsModule], imports: [E2EImportsModule],
declarations: [MotionChangeRecommendationComponent], declarations: [MotionChangeRecommendationDialogComponent],
providers: [ providers: [
{ provide: MatDialogRef, useValue: {} }, { provide: MatDialogRef, useValue: {} },
{ {
@ -44,7 +44,7 @@ describe('MotionChangeRecommendationComponent', () => {
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(MotionChangeRecommendationComponent); fixture = TestBed.createComponent(MotionChangeRecommendationDialogComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); 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 * Data that needs to be provided to the MotionChangeRecommendationComponent dialog
*/ */
export interface MotionChangeRecommendationComponentData { export interface MotionChangeRecommendationDialogComponentData {
editChangeRecommendation: boolean; editChangeRecommendation: boolean;
newChangeRecommendation: boolean; newChangeRecommendation: boolean;
lineRange: LineRange; lineRange: LineRange;
@ -26,13 +26,13 @@ export interface MotionChangeRecommendationComponentData {
* *
* @example * @example
* ```ts * ```ts
* const data: MotionChangeRecommendationComponentData = { * const data: MotionChangeRecommendationDialogComponentData = {
* editChangeRecommendation: false, * editChangeRecommendation: false,
* newChangeRecommendation: true, * newChangeRecommendation: true,
* lineRange: lineRange, * lineRange: lineRange,
* changeReco: this.changeRecommendation, * changeReco: this.changeRecommendation,
* }; * };
* this.dialogService.open(MotionChangeRecommendationComponent, { * this.dialogService.open(MotionChangeRecommendationDialogComponent, {
* height: '400px', * height: '400px',
* width: '600px', * width: '600px',
* data: data, * data: data,
@ -42,10 +42,10 @@ export interface MotionChangeRecommendationComponentData {
*/ */
@Component({ @Component({
selector: 'os-motion-change-recommendation', selector: 'os-motion-change-recommendation',
templateUrl: './motion-change-recommendation.component.html', templateUrl: './motion-change-recommendation-dialog.component.html',
styleUrls: ['./motion-change-recommendation.component.scss'] styleUrls: ['./motion-change-recommendation-dialog.component.scss']
}) })
export class MotionChangeRecommendationComponent extends BaseViewComponent { export class MotionChangeRecommendationDialogComponent extends BaseViewComponent {
/** /**
* Determine if the change recommendation is edited * Determine if the change recommendation is edited
*/ */
@ -91,13 +91,13 @@ export class MotionChangeRecommendationComponent extends BaseViewComponent {
]; ];
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: MotionChangeRecommendationComponentData, @Inject(MAT_DIALOG_DATA) public data: MotionChangeRecommendationDialogComponentData,
title: Title, title: Title,
protected translate: TranslateService, protected translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private repo: ChangeRecommendationRepositoryService, private repo: ChangeRecommendationRepositoryService,
private dialogRef: MatDialogRef<MotionChangeRecommendationComponent> private dialogRef: MatDialogRef<MotionChangeRecommendationDialogComponent>
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);

View File

@ -10,12 +10,10 @@
[class.amendment]="isAmendment(change)" [class.amendment]="isAmendment(change)"
[class.recommendation]="isChangeRecommendation(change)" [class.recommendation]="isChangeRecommendation(change)"
> >
<span *ngIf="change.getLineFrom() >= change.getLineTo() - 1" class="line-number"> <span *ngIf="!change.isTitleChange()" class="line-number">
{{ 'Line' | translate }} {{ change.getLineFrom() }} {{ 'Line' | translate }} {{ formatLineRange(change) }}
</span>
<span *ngIf="change.getLineFrom() < change.getLineTo() - 1" class="line-number">
{{ 'Line' | translate }} {{ change.getLineFrom() }} - {{ change.getLineTo() - 1 }}
</span> </span>
<span *ngIf="change.isTitleChange()">{{ 'Title' | translate }}</span>
<span *ngIf="isChangeRecommendation(change)"> ({{ 'Change recommendation' | translate }})</span> <span *ngIf="isChangeRecommendation(change)"> ({{ 'Change recommendation' | translate }})</span>
<span *ngIf="isAmendment(change)"> ({{ 'Amendment' | translate }} {{ change.getIdentifier() }})</span> <span *ngIf="isAmendment(change)"> ({{ 'Amendment' | translate }} {{ change.getIdentifier() }})</span>
<span class="operation" *ngIf="isChangeRecommendation(change)"> <span class="operation" *ngIf="isChangeRecommendation(change)">
@ -44,7 +42,38 @@
<!-- The actual diff view --> <!-- The actual diff view -->
<div class="motion-text-with-diffs"> <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 <div
class="motion-text" class="motion-text"
[class.line-numbers-none]="isLineNumberingNone()" [class.line-numbers-none]="isLineNumberingNone()"
@ -52,7 +81,7 @@
[class.line-numbers-outside]="isLineNumberingOutside()" [class.line-numbers-outside]="isLineNumberingOutside()"
> >
<os-motion-detail-original-change-recommendations <os-motion-detail-original-change-recommendations
[html]="getTextBetweenChanges(changes[i - 1], change)" [html]="getTextBetweenChanges(getAllTextChangingObjects()[i - 1], change)"
[changeRecommendations]="[]" [changeRecommendations]="[]"
(createChangeRecommendation)="onCreateChangeRecommendation($event)" (createChangeRecommendation)="onCreateChangeRecommendation($event)"
></os-motion-detail-original-change-recommendations> ></os-motion-detail-original-change-recommendations>
@ -60,9 +89,9 @@
<div <div
class="diff-box diff-box-{{ change.getChangeId() }} clearfix" 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> <mat-icon matTooltip="{{ 'This change collides with another one.' | translate }}">warning</mat-icon>
</div> </div>
<div class="action-row" *osPerms="'motions.can_manage'"> <div class="action-row" *osPerms="'motions.can_manage'">
@ -144,7 +173,11 @@
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>
</button> </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> <mat-icon>edit</mat-icon>
<span translate>Edit</span> <span translate>Edit</span>
</button> </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 { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { DiffService, LineRange } from 'app/core/ui-services/diff.service'; import { DiffService, LineRange } from 'app/core/ui-services/diff.service';
import { import {
MotionChangeRecommendationComponent, MotionChangeRecommendationDialogComponent,
MotionChangeRecommendationComponentData MotionChangeRecommendationDialogComponentData
} from '../motion-change-recommendation/motion-change-recommendation.component'; } from '../motion-change-recommendation-dialog/motion-change-recommendation-dialog.component';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; 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 { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewMotion, LineNumberingMode } from 'app/site/motions/models/view-motion'; import { ViewMotion, LineNumberingMode } from 'app/site/motions/models/view-motion';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change'; 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 translate
* @param matSnackBar * @param matSnackBar
* @param sanitizer * @param sanitizer
* @param motionRepo
* @param diff * @param diff
* @param recoRepo * @param recoRepo
* @param dialogService * @param dialogService
@ -91,7 +93,6 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
protected translate: TranslateService, // protected required for ng-translate-extract protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private motionRepo: MotionRepositoryService,
private diff: DiffService, private diff: DiffService,
private recoRepo: ChangeRecommendationRepositoryService, private recoRepo: ChangeRecommendationRepositoryService,
private dialogService: MatDialog, private dialogService: MatDialog,
@ -121,8 +122,8 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
return ''; return '';
} }
return this.motionRepo.extractMotionLineRange( return this.diff.extractMotionLineRange(
this.motion.id, this.motion.text,
lineRange, lineRange,
true, true,
this.lineLength, 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 * 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; 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. * Sets a change recommendation to accepted or rejected.
* The template has to make sure only to pass change recommendations to this method. * 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.stopPropagation();
$event.preventDefault(); $event.preventDefault();
const data: MotionChangeRecommendationComponentData = { const data: MotionChangeRecommendationDialogComponentData = {
editChangeRecommendation: true, editChangeRecommendation: true,
newChangeRecommendation: false, newChangeRecommendation: false,
lineRange: { lineRange: {
@ -295,7 +323,26 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
}, },
changeRecommendation: reco 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', height: '600px',
width: '800px', width: '800px',
maxHeight: '90vh', maxHeight: '90vh',

View File

@ -1,6 +1,6 @@
<div class="text"></div> <div class="text"></div>
<ul class="change-recommendation-list" *ngIf="showChangeRecommendations"> <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)" [style.top]="calcRecoTop(reco)" [style.height]="calcRecoHeight(reco)"
[class.delete]="recoIsDeletion(reco)" [class.insert]="recoIsInsertion(reco)" [class.delete]="recoIsDeletion(reco)" [class.insert]="recoIsInsertion(reco)"
[class.replace]="recoIsReplacement(reco)" (click)="gotoReco(reco)"></li> [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 * Returns an array with all line numbers that are currently affected by a change recommendation
* and therefor not subject to further changes * and therefor not subject to further changes
@ -112,6 +116,9 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
private getAffectedLineNumbers(): number[] { private getAffectedLineNumbers(): number[] {
const affectedLines = []; const affectedLines = [];
this.changeRecommendations.forEach((change: ViewMotionChangeRecommendation) => { this.changeRecommendations.forEach((change: ViewMotionChangeRecommendation) => {
if (change.isTitleChange()) {
return;
}
for (let j = change.line_from; j < change.line_to; j++) { for (let j = change.line_from; j < change.line_to; j++) {
affectedLines.push(j); affectedLines.push(j);
} }

View File

@ -124,7 +124,16 @@
<!-- Title --> <!-- Title -->
<div class="title" *ngIf="motion && !editMotion"> <div class="title" *ngIf="motion && !editMotion">
<div class="title-line"> <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()"> <button mat-icon-button color="primary" (click)="toggleFavorite()">
<mat-icon>{{ motion.star ? 'star' : 'star_border' }}</mat-icon> <mat-icon>{{ motion.star ? 'star' : 'star_border' }}</mat-icon>
</button> </button>

View File

@ -170,6 +170,48 @@ span {
} }
.title-line { .title-line {
display: flex; 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 { .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 { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { CreateMotion } from 'app/site/motions/models/create-motion'; import { CreateMotion } from 'app/site/motions/models/create-motion';
import { ConfigService } from 'app/core/ui-services/config.service'; 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 { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.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 { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
import { Motion } from 'app/shared/models/motions/motion'; import { Motion } from 'app/shared/models/motions/motion';
import { import {
MotionChangeRecommendationComponentData, MotionChangeRecommendationDialogComponentData,
MotionChangeRecommendationComponent MotionChangeRecommendationDialogComponent
} from '../motion-change-recommendation/motion-change-recommendation.component'; } 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 { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service'; import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.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 pdfExport export the motion to pdf
* @param personalNoteService: personal comments and favorite marker * @param personalNoteService: personal comments and favorite marker
* @param linenumberingService The line numbering service * @param linenumberingService The line numbering service
* @param diffService The diff service
* @param categoryRepo Repository for categories * @param categoryRepo Repository for categories
* @param viewModelStore accessing view models * @param viewModelStore accessing view models
* @param categoryRepo access the category repository * @param categoryRepo access the category repository
@ -435,6 +440,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
private pdfExport: MotionPdfExportService, private pdfExport: MotionPdfExportService,
private personalNoteService: PersonalNoteService, private personalNoteService: PersonalNoteService,
private linenumberingService: LinenumberingService, private linenumberingService: LinenumberingService,
private diffService: DiffService,
private categoryRepo: CategoryRepositoryService, private categoryRepo: CategoryRepositoryService,
private userRepo: UserRepositoryService, private userRepo: UserRepositoryService,
private notifyService: NotifyService, private notifyService: NotifyService,
@ -569,7 +575,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
this.allChangingObjects = []; this.allChangingObjects = [];
if (this.changeRecommendations) { if (this.changeRecommendations) {
this.changeRecommendations.forEach((change: ViewUnifiedChange): void => { this.changeRecommendations.forEach((change: ViewMotionChangeRecommendation): void => {
this.allChangingObjects.push(change); this.allChangingObjects.push(change);
}); });
} }
@ -854,7 +860,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
*/ */
public getFormattedTextPlain(): string { public getFormattedTextPlain(): string {
// Prevent this.allChangingObjects to be reordered from within formatMotion // 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); 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 * @returns safe html strings
*/ */
public getParentMotionRange(from: number, to: number): SafeHtml { public getParentMotionRange(from: number, to: number): SafeHtml {
const str = this.repo.extractMotionLineRange( const parentMotion = this.repo.getViewModel(this.motion.parent_id);
this.motion.parent_id, const str = this.diffService.extractMotionLineRange(
parentMotion.text,
{ from, to }, { from, to },
true, true,
this.lineLength, 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. * Trigger to delete the motion.
*/ */
@ -1027,17 +1046,17 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* @param lineRange * @param lineRange
*/ */
public createChangeRecommendation(lineRange: LineRange): void { public createChangeRecommendation(lineRange: LineRange): void {
const data: MotionChangeRecommendationComponentData = { const data: MotionChangeRecommendationDialogComponentData = {
editChangeRecommendation: false, editChangeRecommendation: false,
newChangeRecommendation: true, newChangeRecommendation: true,
lineRange: lineRange, lineRange: lineRange,
changeRecommendation: this.repo.createChangeRecommendationTemplate( changeRecommendation: this.changeRecoRepo.createChangeRecommendationTemplate(
this.motion.id, this.motion,
lineRange, lineRange,
this.lineLength this.lineLength
) )
}; };
this.dialogService.open(MotionChangeRecommendationComponent, { this.dialogService.open(MotionChangeRecommendationDialogComponent, {
height: '600px', height: '600px',
width: '800px', width: '800px',
maxHeight: '90vh', 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 * In the original version, a change-recommendation-annotation has been clicked
* -> Go to the diff view and scroll to the change recommendation * -> 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 { MotionPollComponent } from './components/motion-poll/motion-poll.component';
import { MotionDetailOriginalChangeRecommendationsComponent } from './components/motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.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 { 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({ @NgModule({
imports: [CommonModule, MotionDetailRoutingModule, SharedModule], imports: [CommonModule, MotionDetailRoutingModule, SharedModule],
@ -26,14 +27,16 @@ import { MotionChangeRecommendationComponent } from './components/motion-change-
MotionPollDialogComponent, MotionPollDialogComponent,
MotionDetailDiffComponent, MotionDetailDiffComponent,
MotionDetailOriginalChangeRecommendationsComponent, MotionDetailOriginalChangeRecommendationsComponent,
MotionChangeRecommendationComponent MotionChangeRecommendationDialogComponent,
MotionTitleChangeRecommendationDialogComponent
], ],
entryComponents: [ entryComponents: [
MotionCommentsComponent, MotionCommentsComponent,
PersonalNoteComponent, PersonalNoteComponent,
ManageSubmittersComponent, ManageSubmittersComponent,
MotionPollDialogComponent, MotionPollDialogComponent,
MotionChangeRecommendationComponent MotionChangeRecommendationDialogComponent,
MotionTitleChangeRecommendationDialogComponent
] ]
}) })
export class MotionDetailModule {} 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 { MotionPollService } from './motion-poll.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-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 { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service'; import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.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'); 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 sequential = !infoToExport || infoToExport.includes('id');
const subtitle = this.createSubtitle(motion, sequential); const subtitle = this.createSubtitle(motion, sequential);
@ -167,11 +167,18 @@ export class MotionPdfService {
* Create the motion title part of the doc definition * Create the motion title part of the doc definition
* *
* @param motion the target motion * @param motion the target motion
* @param crMode the change recommendation mode
* @param lineLength the line length
* @returns doc def for the document title * @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 identifier = motion.identifier ? ' ' + motion.identifier : '';
const title = `${this.translate.instant('Motion')} ${identifier}: ${motion.title}`; const title = `${this.translate.instant('Motion')} ${identifier}: ${changedTitle}`;
return { return {
text: title, text: title,
@ -399,39 +406,44 @@ export class MotionPdfService {
const columnChangeType = []; const columnChangeType = [];
changes.forEach(change => { changes.forEach(change => {
// TODO: the function isTitleRecommendation() does not exist anymore. if (change.isTitleChange()) {
// Not sure if required or not // Is always a change recommendation
// 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) {
const changeReco = change as ViewMotionChangeRecommendation; const changeReco = change as ViewMotionChangeRecommendation;
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `); columnLineNumbers.push(`${this.translate.instant('Title')}: `);
columnChangeType.push( columnChangeType.push(
`(${this.translate.instant('Change recommendation')}) - ${this.translate.instant( `(${this.translate.instant('Change recommendation')}) - ${this.translate.instant(
this.getRecommendationTypeName(changeReco) this.getRecommendationTypeName(changeReco)
)}` )}`
); );
} else if (change.getChangeType() === ViewUnifiedChangeType.TYPE_AMENDMENT) { } else {
const amendment = change as ViewMotionAmendedParagraph; // line numbers column
let summaryText = `(${this.translate.instant('Amendment')} ${amendment.getIdentifier()}) -`; let line;
if (amendment.isRejected()) { if (change.getLineFrom() >= change.getLineTo() - 1) {
summaryText += ` ${this.translate.instant('Rejected')}`; line = change.getLineFrom();
} else if (amendment.isAccepted()) { } else {
summaryText += ` ${this.translate.instant(amendment.stateName)}`; line = change.getLineFrom() + ' - ' + (change.getLineTo() - 1);
// only append line and change, if the merge of the state of the amendment is accepted. }
// change type column
if (change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION) {
const changeReco = change as ViewMotionChangeRecommendation;
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `); 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 * Creates the motion text - uses HTML to PDF
* *
* @param motion the motion to convert to pdf * @param motion the motion to convert to pdf
* @param lineLength the current line length
* @param lnMode determine the used line mode * @param lnMode determine the used line mode
* @param crMode determine the used change Recommendation mode * @param crMode determine the used change Recommendation mode
* @returns doc def for the "the assembly may decide" preamble * @returns doc def for the "the assembly may decide" preamble
@ -547,10 +560,9 @@ export class MotionPdfService {
lnMode: LineNumberingMode, lnMode: LineNumberingMode,
crMode: ChangeRecoMode crMode: ChangeRecoMode
): object { ): object {
let motionText: string; let motionText = '';
if (motion.isParagraphBasedAmendment()) { if (motion.isParagraphBasedAmendment()) {
motionText = '';
// this is logically redundant with the formation of amendments in the motion-detail html. // 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 // 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)) { for (const paragraph of this.motionRepo.getAmendmentParagraphs(motion, lineLength, false)) {
@ -577,9 +589,21 @@ export class MotionPdfService {
// TODO: Consider tile change recommendation // TODO: Consider tile change recommendation
const changes = this.getUnifiedChanges(motion, lineLength); 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 // 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); return this.htmlToPdfService.convertHtml(motionText, lnMode);
@ -755,14 +779,11 @@ export class MotionPdfService {
* @returns pdfMake definitions * @returns pdfMake definitions
*/ */
public textToDocDef(note: string, motion: ViewMotion, noteTitle: string): object { 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 subtitle = this.createSubtitle(motion);
const metaInfo = this.createMetaInfoTable( const metaInfo = this.createMetaInfoTable(motion, lineLength, crMode, ['submitters', 'state', 'category']);
motion,
this.configService.instant<number>('motions_line_length'),
this.configService.instant('motions_recommendation_text_mode'),
['submitters', 'state', 'category']
);
const noteContent = this.htmlToPdfService.convertHtml(note, LineNumberingMode.None); const noteContent = this.htmlToPdfService.convertHtml(note, LineNumberingMode.None);
const subHeading = { const subHeading = {

View File

@ -58,4 +58,8 @@ export class MotionSlideObjAmendmentParagraph implements ViewUnifiedChange {
public showInFinalView(): boolean { public showInFinalView(): boolean {
return this.merge_amendment_into_final === 1; 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 { public showInFinalView(): boolean {
return !this.rejected; return !this.rejected;
} }
public isTitleChange(): boolean {
return this.line_from === 0 && this.line_to === 0;
}
} }

View File

@ -25,7 +25,7 @@
<!-- Title --> <!-- Title -->
<div class="spacer" [ngStyle]="{height: projector.show_header_footer ? '50px' : '0'}"></div> <div class="spacer" [ngStyle]="{height: projector.show_header_footer ? '50px' : '0'}"></div>
<div class="slidetitle"> <div class="slidetitle">
<h1>{{ data.data.title }}</h1> <h1>{{ getTitleWithChanges() }}</h1>
<h2><span translate>Motion</span> {{ data.data.identifier }}</h2> <h2><span translate>Motion</span> {{ data.data.identifier }}</h2>
</div> </div>
</div> </div>
@ -43,6 +43,12 @@
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()" [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 [innerHTML]="sanitizedText(getFormattedText())"></div>
</div> </div>
</ng-container> </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 { MotionSlideObjAmendmentParagraph } from './motion-slide-obj-amendment-paragraph';
import { BaseMotionSlideComponent } from '../base/base-motion-slide'; import { BaseMotionSlideComponent } from '../base/base-motion-slide';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; 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'; import { IBaseScaleScrollSlideComponent } from 'app/slides/base-scale-scroll-slide-component';
@Component({ @Component({
@ -111,6 +112,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
public constructor( public constructor(
translate: TranslateService, translate: TranslateService,
motionRepo: MotionRepositoryService, motionRepo: MotionRepositoryService,
private changeRepo: ChangeRecommendationRepositoryService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private lineNumbering: LinenumberingService, private lineNumbering: LinenumberingService,
private diff: DiffService private diff: DiffService
@ -293,6 +295,24 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
return html; 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. * get the formated motion text from the repository.
* *
@ -309,13 +329,13 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
case ChangeRecoMode.Changed: case ChangeRecoMode.Changed:
return this.diff.getTextWithChanges( return this.diff.getTextWithChanges(
motion.text, motion.text,
this.allChangingObjects, this.getAllTextChangingObjects(),
this.lineLength, this.lineLength,
this.highlightedLine this.highlightedLine
); );
case ChangeRecoMode.Diff: case ChangeRecoMode.Diff:
let text = ''; let text = '';
const changes = this.allChangingObjects.filter(change => { const changes = this.getAllTextChangingObjects().filter(change => {
return change.showInDiffView(); return change.showInDiffView();
}); });
changes.forEach((change: ViewUnifiedChange, idx: number) => { changes.forEach((change: ViewUnifiedChange, idx: number) => {
@ -339,7 +359,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
); );
return text; return text;
case ChangeRecoMode.Final: case ChangeRecoMode.Final:
const appliedChanges: ViewUnifiedChange[] = this.allChangingObjects.filter(change => const appliedChanges: ViewUnifiedChange[] = this.getAllTextChangingObjects().filter(change =>
change.showInFinalView() change.showInFinalView()
); );
return this.diff.getTextWithChanges(motion.text, appliedChanges, this.lineLength, this.highlightedLine); return this.diff.getTextWithChanges(motion.text, appliedChanges, this.lineLength, this.highlightedLine);
@ -354,7 +374,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
); );
} else { } else {
// Use the final version as fallback, if the modified does not exist. // 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() change.showInFinalView()
); );
return this.diff.getTextWithChanges( return this.diff.getTextWithChanges(

View File

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