Change recommendations for amendments

This commit is contained in:
Tobias Hößl 2020-04-04 10:11:56 +02:00
parent e0069f734a
commit b51787129b
No known key found for this signature in database
GPG Key ID: 1D780C7599C2D2A2
13 changed files with 548 additions and 157 deletions

View File

@ -18,6 +18,7 @@ import {
import { ChangeRecoMode } from 'app/site/motions/motions.constants';
import { BaseRepository } from '../base-repository';
import { DiffService, LineRange, ModificationType } from '../../ui-services/diff.service';
import { LinenumberingService } from '../../ui-services/linenumbering.service';
import { ViewMotion } from '../../../site/motions/models/view-motion';
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';
@ -50,7 +51,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
* @param {CollectionStringMapperService} mapperService Maps collection strings to classes
* @param {ViewModelStoreService} viewModelStoreService
* @param {TranslateService} translate
* @param {RelationManagerService} relationManager
* @param {DiffService} diffService
* @param {LinenumberingService} lineNumbering Line numbering service
*/
public constructor(
DS: DataStoreService,
@ -59,7 +62,8 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
viewModelStoreService: ViewModelStoreService,
translate: TranslateService,
relationManager: RelationManagerService,
private diffService: DiffService
private diffService: DiffService,
private lineNumbering: LinenumberingService
) {
super(
DS,
@ -103,7 +107,7 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
/**
* Synchronously getting the change recommendations of the corresponding motion.
*
* @param motionId the id of the target motion
* @param motion_id the id of the target motion
* @returns the array of change recommendations to the motions.
*/
public getChangeRecoOfMotion(motion_id: number): ViewMotionChangeRecommendation[] {
@ -171,22 +175,61 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
* @param {LineRange} lineRange
* @param {number} lineLength
*/
public createChangeRecommendationTemplate(
public createMotionChangeRecommendationTemplate(
motion: ViewMotion,
lineRange: LineRange,
lineLength: number
): ViewMotionChangeRecommendation {
const motionText = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
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.text = this.diffService.extractMotionLineRange(motionText, lineRange, false, lineLength, null);
changeReco.rejected = false;
changeReco.motion_id = motion.id;
return new ViewMotionChangeRecommendation(changeReco);
}
/**
* Creates a {@link ViewMotionChangeRecommendation} object based on the amendment ID, the precalculated
* paragraphs (because we don't have access to motion-repository serice here) 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} amendment
* @param {string[]} lineNumberedParagraphs
* @param {LineRange} lineRange
* @param {number} lineLength
*/
public createAmendmentChangeRecommendationTemplate(
amendment: ViewMotion,
lineNumberedParagraphs: string[],
lineRange: LineRange,
lineLength: number
): ViewMotionChangeRecommendation {
const consolidatedText = lineNumberedParagraphs.join('\n');
const extracted = this.diffService.extractRangeByLineNumbers(consolidatedText, lineRange.from, lineRange.to);
const extractedHtml =
extracted.outerContextStart +
extracted.innerContextStart +
extracted.html +
extracted.innerContextEnd +
extracted.outerContextEnd;
const changeReco = new MotionChangeRecommendation();
changeReco.line_from = lineRange.from;
changeReco.line_to = lineRange.to;
changeReco.type = ModificationType.TYPE_REPLACEMENT;
changeReco.rejected = false;
changeReco.motion_id = amendment.id;
changeReco.text = extractedHtml;
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.

View File

@ -37,7 +37,7 @@ import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../bas
import { NestedModelDescriptors } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
import { LineNumberedString, LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
type SortProperty = 'weight' | 'identifier';
@ -201,11 +201,14 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
* @param DS The DataStore
* @param mapperService Maps collection strings to classes
* @param dataSend sending changed objects
* @param viewModelStoreService ViewModelStoreService
* @param translate
* @param relationManager
* @param httpService OpenSlides own Http service
* @param lineNumbering Line numbering for motion text
* @param diff Display changes in motion text as diff.
* @param personalNoteService service fo personal notes
* @param config ConfigService (subscribe to sorting config)
* @param operator
*/
public constructor(
DS: DataStoreService,
@ -322,7 +325,16 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
ownKey: 'diffLines',
get: (motion: Motion, viewMotion: ViewMotion) => {
if (viewMotion.parent) {
return this.getAmendmentParagraphs(viewMotion, this.motionLineLength, false);
const changeRecos = viewMotion.changeRecommendations.filter(changeReco =>
changeReco.showInFinalView()
);
return this.getAmendmentParagraphLines(
viewMotion,
this.motionLineLength,
ChangeRecoMode.Changed,
changeRecos,
false
);
}
},
getCacheObjectToCheck: (viewMotion: ViewMotion) => viewMotion.parent
@ -376,7 +388,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
/**
* Set the state of motions in bulk
*
* @param viewMotion target motion
* @param viewMotions target motions
* @param stateId the number that indicates the state
*/
public async setMultiState(viewMotions: ViewMotion[], stateId: number): Promise<void> {
@ -390,7 +402,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
/**
* Set the motion blocks of motions in bulk
*
* @param viewMotion target motion
* @param viewMotions target motions
* @param motionblockId the number that indicates the motion block
*/
public async setMultiMotionBlock(viewMotions: ViewMotion[], motionblockId: number): Promise<void> {
@ -404,7 +416,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
/**
* Set the category of motions in bulk
*
* @param viewMotion target motion
* @param viewMotions target motions
* @param categoryId the number that indicates the category
*/
public async setMultiCategory(viewMotions: ViewMotion[], categoryId: number): Promise<void> {
@ -609,11 +621,12 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
case ChangeRecoMode.Diff:
const text = [];
const changesToShow = changes.filter(change => change.showInDiffView());
const motionText = this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength);
for (let i = 0; i < changesToShow.length; i++) {
text.push(
this.diff.extractMotionLineRange(
targetMotion.text,
motionText,
{
from: i === 0 ? 1 : changesToShow[i - 1].getLineTo(),
to: changesToShow[i].getLineFrom()
@ -624,18 +637,11 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
)
);
text.push(
this.diff.getChangeDiff(targetMotion.text, changesToShow[i], lineLength, highlightLine)
);
text.push(this.diff.getChangeDiff(motionText, changesToShow[i], lineLength, highlightLine));
}
text.push(
this.diff.getTextRemainderAfterLastChange(
targetMotion.text,
changesToShow,
lineLength,
highlightLine
)
this.diff.getTextRemainderAfterLastChange(motionText, changesToShow, lineLength, highlightLine)
);
return text.join('');
case ChangeRecoMode.Final:
@ -760,66 +766,173 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
}
/**
* Returns all paragraphs that are affected by the given amendment in diff-format
* Returns the amended paragraphs by an amendment. Correlates to the amendment_paragraphs field,
* but also considers relevant change recommendations.
* The returned array includes "null" values for paragraphs that have not been changed.
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {ViewMotionChangeRecommendation[]} changes
* @param {boolean} includeUnchanged
* @returns {string[]}
*/
public applyChangesToAmendment(
amendment: ViewMotion,
lineLength: number,
changes: ViewMotionChangeRecommendation[],
includeUnchanged: boolean
): string[] {
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
// Changes need to be applied from the bottom up, to prevent conflicts with changing line numbers.
changes.sort((change1: ViewUnifiedChange, change2: ViewUnifiedChange) => {
if (change1.getLineFrom() < change2.getLineFrom()) {
return 1;
} else if (change1.getLineFrom() > change2.getLineFrom()) {
return -1;
} else {
return 0;
}
});
return amendment.amendment_paragraphs.map((newText: string, paraNo: number) => {
let paragraph: string;
let paragraphHasChanges;
if (newText === null) {
paragraph = baseParagraphs[paraNo];
paragraphHasChanges = false;
} else {
// Add line numbers to newText, relative to the baseParagraph, by creating a diff
// to the line numbered base version any applying it right away
const diff = this.diff.diff(baseParagraphs[paraNo], newText);
paragraph = this.diff.diffHtmlToFinalText(diff);
paragraphHasChanges = true;
}
const affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
changes.forEach((change: ViewMotionChangeRecommendation) => {
// Hint: this assumes that change recommendations only affect one specific paragraph, not multiple
if (change.line_from >= affected.from && change.line_from < affected.to) {
paragraph = this.diff.replaceLines(paragraph, change.text, change.line_from, change.line_to);
// Reapply relative line numbers
const diff = this.diff.diff(baseParagraphs[paraNo], paragraph);
paragraph = this.diff.diffHtmlToFinalText(diff);
paragraphHasChanges = true;
}
});
if (paragraphHasChanges || includeUnchanged) {
return paragraph;
} else {
return null;
}
});
}
/**
* Returns all paragraph lines that are affected by the given amendment in diff-format, including context
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {ChangeRecoMode} crMode
* @param {ViewMotionChangeRecommendation[]} changeRecommendations
* @param {boolean} includeUnchanged
* @returns {DiffLinesInParagraph}
*/
public getAmendmentParagraphs(
public getAmendmentParagraphLines(
amendment: ViewMotion,
lineLength: number,
crMode: ChangeRecoMode,
changeRecommendations: ViewMotionChangeRecommendation[],
includeUnchanged: boolean
): DiffLinesInParagraph[] {
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return (amendment.amendment_paragraphs || [])
let amendmentParagraphs;
if (crMode === ChangeRecoMode.Changed) {
amendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecommendations, true);
} else {
amendmentParagraphs = amendment.amendment_paragraphs || [];
}
return amendmentParagraphs
.map(
(newText: string, paraNo: number): DiffLinesInParagraph => {
if (newText !== null) {
return this.diff.getAmendmentParagraphsLinesByMode(
return this.diff.getAmendmentParagraphsLines(
paraNo,
baseParagraphs[paraNo],
newText,
lineLength
);
} else {
// Nothing has changed in this paragraph
if (includeUnchanged) {
const paragraph_line_range = this.lineNumbering.getLineNumberRange(baseParagraphs[paraNo]);
return {
paragraphNo: paraNo,
paragraphLineFrom: paragraph_line_range.from,
paragraphLineTo: paragraph_line_range.to,
diffLineFrom: paragraph_line_range.to,
diffLineTo: paragraph_line_range.to,
textPre: baseParagraphs[paraNo],
text: '',
textPost: ''
} as DiffLinesInParagraph;
} else {
return null; // null will make this paragraph filtered out
}
return null; // Nothing has changed in this paragraph
}
}
)
.map((diffLines: DiffLinesInParagraph, paraNo: number) => {
// If nothing has changed and we want to keep unchanged paragraphs for the context,
// return the original text in "textPre"
if (diffLines === null && includeUnchanged) {
const paragraph_line_range = this.lineNumbering.getLineNumberRange(baseParagraphs[paraNo]);
return {
paragraphNo: paraNo,
paragraphLineFrom: paragraph_line_range.from,
paragraphLineTo: paragraph_line_range.to,
diffLineFrom: paragraph_line_range.to,
diffLineTo: paragraph_line_range.to,
textPre: baseParagraphs[paraNo],
text: '',
textPost: ''
} as DiffLinesInParagraph;
} else {
return diffLines;
}
})
.filter((para: DiffLinesInParagraph) => para !== null);
}
public getAmendmentParagraphLinesTitle(paragraph: DiffLinesInParagraph): string {
if (paragraph.diffLineTo === paragraph.diffLineFrom + 1) {
return this.translate.instant('Line') + ' ' + paragraph.diffLineFrom.toString(10);
} else {
return (
this.translate.instant('Line') +
' ' +
paragraph.diffLineFrom.toString(10) +
' - ' +
(paragraph.diffLineTo - 1).toString(10)
);
}
}
/**
* Returns all paragraphs that are affected by the given amendment as unified change objects.
* Only the affected part of each paragraph is returned.
* Change recommendations to this amendment are considered here, too. That is, if a change recommendation
* for an amendment exists and is not rejected, the changed amendment will be returned here.
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {ViewMotionChangeRecommendation[]} changeRecos
* @returns {ViewMotionAmendedParagraph[]}
*/
public getAmendmentAmendedParagraphs(amendment: ViewMotion, lineLength: number): ViewMotionAmendedParagraph[] {
public getAmendmentAmendedParagraphs(
amendment: ViewMotion,
lineLength: number,
changeRecos: ViewMotionChangeRecommendation[]
): ViewMotionAmendedParagraph[] {
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
const changedAmendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecos, false);
return (amendment.amendment_paragraphs || [])
return changedAmendmentParagraphs
.map(
(newText: string, paraNo: number): ViewMotionAmendedParagraph => {
if (newText === null) {
@ -844,6 +957,42 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
.filter((para: ViewMotionAmendedParagraph) => para !== null);
}
/**
* For unchanged paragraphs, this returns the original motion paragraph, including line numbers.
* For changed paragraphs, this returns the content of the amendment_paragraphs-field,
* but including line numbers relative to the original motion line numbers,
* so they can be used for the amendment change recommendations
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {boolean} withDiff
* @returns {LineNumberedString[]}
*/
public getAllAmendmentParagraphsWithOriginalLineNumbers(
amendment: ViewMotion,
lineLength: number,
withDiff: boolean
): LineNumberedString[] {
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return (amendment.amendment_paragraphs || []).map((newText: string, paraNo: number): string => {
const origText = baseParagraphs[paraNo];
if (newText === null) {
return origText;
}
const diff = this.diff.diff(origText, newText);
if (withDiff) {
return diff;
} else {
return this.diff.diffHtmlToFinalText(diff);
}
});
}
/**
* Signals the acceptance of the current recommendation to the server
*

View File

@ -1172,6 +1172,28 @@ describe('DiffService', () => {
);
}
));
it('detects a word replacement at the end of line correctly', inject([DiffService], (service: DiffService) => {
const before =
'<p>' +
noMarkup(1) +
'wuid Brotzeit? Pfenningguat Stubn bitt da, hog di hi fei nia need nia need Goaßmaß ' +
brMarkup(2) +
'gscheid kloan mim';
const after =
'<P>wuid Brotzeit? Pfenningguat Stubn bitt da, ' +
'hog di hi fei nia need nia need Radler gscheid kloan mim';
const diff = service.diff(before, after);
expect(diff).toBe(
'<p>' +
noMarkup(1) +
'wuid Brotzeit? Pfenningguat Stubn bitt da, ' +
'hog di hi fei nia need nia need <del>Goaßmaß </del><ins>Radler </ins>' +
brMarkup(2) +
'gscheid kloan mim</p>'
);
}));
});
describe('addCSSClassToFirstTag function', () => {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { LinenumberingService } from './linenumbering.service';
import { LineNumberedString, LinenumberingService } from './linenumbering.service';
import { ViewUnifiedChange } from '../../shared/models/motions/view-unified-change';
const ELEMENT_NODE = 1;
@ -1318,12 +1318,12 @@ export class DiffService {
* - extracting line 2 to 3 results in <p class="os-split-after os-split-before">Line 2</p>
* - extracting line 3 to null/4 results in <p class="os-split-before">Line 3</p>
*
* @param {string} htmlIn
* @param {LineNumberedString} htmlIn
* @param {number} fromLine
* @param {number} toLine
* @returns {ExtractedContent}
*/
public extractRangeByLineNumbers(htmlIn: string, fromLine: number, toLine: number): ExtractedContent {
public extractRangeByLineNumbers(htmlIn: LineNumberedString, fromLine: number, toLine: number): ExtractedContent {
if (typeof htmlIn !== 'string') {
throw new Error('Invalid call - extractRangeByLineNumbers expects a string as first argument');
}
@ -1878,15 +1878,28 @@ export class DiffService {
// Remove <del> tags that only delete line numbers
// We need to do this before removing </del><del> as done in one of the next statements
diffUnnormalized = diffUnnormalized.replace(
/<del>((<BR CLASS="os-line-break"><\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi,
(found: string, tag: string, br: string, span: string): string => {
return (br !== undefined ? br : '') + span + ' </span>';
/<del>(((<BR CLASS="os-line-break">)<\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi,
(found: string, tag: string, brWithDel: string, plainBr: string, span: string): string => {
return (plainBr !== undefined ? plainBr : '') + span + ' </span>';
}
);
// Merging individual insert/delete statements into bigger blocks
diffUnnormalized = diffUnnormalized.replace(/<\/ins><ins>/gi, '').replace(/<\/del><del>/gi, '');
// If we have a <del>deleted word</del>LINEBREAK<ins>new word</ins>, let's assume that the insertion
// was actually done in the same line as the deletion.
// We don't have the LINEBREAK-markers in the new string, hence we can't be a 100% sure, but
// this will probably the more frequent case.
// This only really makes a differences for change recommendations anyway, where we split the text into lines
// Hint: if there is no deletion before the line break, we have the same issue, but cannot solve this here.
diffUnnormalized = diffUnnormalized.replace(
/(<\/del>)(<BR CLASS="os-line-break"><span[^>]+os-line-number[^>]+?>\s*<\/span>)(<ins>[\s\S]*?<\/ins>)/gi,
(found: string, del: string, br: string, ins: string): string => {
return del + ins + br;
}
);
// If only a few characters of a word have changed, don't display this as a replacement of the whole word,
// but only of these specific characters
diffUnnormalized = diffUnnormalized.replace(
@ -2138,7 +2151,7 @@ export class DiffService {
* @param {number} lineLength the line length
* @return {DiffLinesInParagraph|null}
*/
public getAmendmentParagraphsLinesByMode(
public getAmendmentParagraphsLines(
paragraphNo: number,
origText: string,
newText: string,
@ -2190,20 +2203,18 @@ export class DiffService {
* Returns the HTML with the changes, optionally with a highlighted line.
* The original motion needs to be provided.
*
* @param {string} motionHtml
* @param {LineNumberedString} html
* @param {ViewUnifiedChange} change
* @param {number} lineLength
* @param {number} highlight
* @returns {string}
*/
public getChangeDiff(
motionHtml: string,
html: LineNumberedString,
change: ViewUnifiedChange,
lineLength: number,
highlight?: number
): string {
const html = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength);
let data, oldText;
try {
@ -2248,14 +2259,14 @@ export class DiffService {
/**
* Returns the remainder text of the motion after the last change
*
* @param {string} motionHtml
* @param {LineNumberedString} motionHtml
* @param {ViewUnifiedChange[]} changes
* @param {number} lineLength
* @param {number} highlight
* @returns {string}
*/
public getTextRemainderAfterLastChange(
motionHtml: string,
motionHtml: LineNumberedString,
changes: ViewUnifiedChange[],
lineLength: number,
highlight?: number
@ -2267,15 +2278,14 @@ export class DiffService {
}
}, 0);
const numberedHtml = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength, highlight);
if (changes.length === 0) {
return numberedHtml;
return motionHtml;
}
let data;
try {
data = this.extractRangeByLineNumbers(numberedHtml, maxLine, null);
data = this.extractRangeByLineNumbers(motionHtml, maxLine, null);
} catch (e) {
// This only happens (as far as we know) when the motion text has been altered (shortened)
// without modifying the change recommendations accordingly.
@ -2305,21 +2315,20 @@ export class DiffService {
/**
* Extracts a renderable HTML string representing the given line number range of this motion text
*
* @param {string} motionText
* @param {LineNumberedString} 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,
motionText: LineNumberedString,
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);
const extracted = this.extractRangeByLineNumbers(motionText, lineRange.from, lineRange.to);
let html =
extracted.outerContextStart +
extracted.innerContextStart +

View File

@ -3,6 +3,11 @@ import { Injectable } from '@angular/core';
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
/**
* A helper to indicate that certain functions expect the provided HTML strings to contain line numbers
*/
export type LineNumberedString = string;
/**
* Specifies a point within a HTML Text Node where a line break might be possible, if the following word
* exceeds the maximum line length.
@ -894,7 +899,7 @@ export class LinenumberingService {
highlight?: number,
callback?: () => void,
firstLine?: number
): string {
): LineNumberedString {
let newHtml, newRoot;
if (highlight > 0) {

View File

@ -199,7 +199,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* Extract the lines of the amendments
* If an amendments has multiple changes, they will be printed like an array of strings
*
* @param amendment the motion to create the amendment to
* @return The lines of the amendment
*/
public getChangeLines(): string {

View File

@ -6,8 +6,10 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DiffService, LineRange } from 'app/core/ui-services/diff.service';
import { LineNumberedString, LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
import { mediumDialogSettings } from 'app/shared/utils/dialog-settings';
@ -84,7 +86,9 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param translate
* @param matSnackBar
* @param diff
* @param lineNumbering
* @param recoRepo
* @param motionRepo
* @param dialogService
* @param configService
* @param el
@ -95,7 +99,9 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar,
private diff: DiffService,
private lineNumbering: LinenumberingService,
private recoRepo: ChangeRecommendationRepositoryService,
private motionRepo: MotionRepositoryService,
private dialogService: MatDialog,
private configService: ConfigService,
private el: ElementRef,
@ -123,13 +129,16 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
return '';
}
return this.diff.extractMotionLineRange(
this.motion.text,
lineRange,
true,
this.lineLength,
this.highlightedLine
);
let baseText: LineNumberedString;
if (this.motion.isParagraphBasedAmendment()) {
baseText = this.motionRepo
.getAllAmendmentParagraphsWithOriginalLineNumbers(this.motion, this.lineLength, true)
.join('\n');
} else {
baseText = this.lineNumbering.insertLineNumbers(this.motion.text, this.lineLength);
}
return this.diff.extractMotionLineRange(baseText, lineRange, true, this.lineLength, this.highlightedLine);
}
/**
@ -158,7 +167,15 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param {ViewUnifiedChange} change
*/
public getDiff(change: ViewUnifiedChange): string {
return this.diff.getChangeDiff(this.motion.text, change, this.lineLength, this.highlightedLine);
let motionHtml: string;
if (this.motion.isParagraphBasedAmendment()) {
const parentMotion = this.motionRepo.getViewModel(this.motion.parent_id);
motionHtml = parentMotion.text;
} else {
motionHtml = this.motion.text;
}
const baseHtml = this.lineNumbering.insertLineNumbers(motionHtml, this.lineLength);
return this.diff.getChangeDiff(baseHtml, change, this.lineLength, this.highlightedLine);
}
/**
@ -168,12 +185,15 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
if (!this.lineLength) {
return ''; // @TODO This happens in the test case when the lineLength-variable is not set
}
return this.diff.getTextRemainderAfterLastChange(
this.motion.text,
this.changes,
this.lineLength,
this.highlightedLine
);
let baseText: LineNumberedString;
if (this.motion.isParagraphBasedAmendment()) {
baseText = this.motionRepo
.getAllAmendmentParagraphsWithOriginalLineNumbers(this.motion, this.lineLength, true)
.join('\n');
} else {
baseText = this.lineNumbering.insertLineNumbers(this.motion.text, this.lineLength);
}
return this.diff.getTextRemainderAfterLastChange(baseText, this.changes, this.lineLength, this.highlightedLine);
}
/**

View File

@ -1,4 +1,5 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
@ -15,16 +16,23 @@ import { LineRange, ModificationType } from 'app/core/ui-services/diff.service';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
/**
* This component displays the original motion text with annotated change commendations
* and a method to create new change recommendations from the line numbers to the left of the text.
* It's called from motion-details for displaying the whole motion text as well as from the diff view to show the
* unchanged parts of the motion.
* This component displays either the original motion text or the original amendment diff text
* with annotated change commendations and a method to create new change recommendations
* from the line numbers to the left of the text.
* It's called from motion-details for displaying the whole motion text as well as from the
* motion's or amendment's diff view to show the unchanged parts of the motion.
*
* The line numbers are provided within the pre-rendered HTML, so we have to work with raw HTML
* and native HTML elements.
*
* It takes the styling from the parent component.
*
* Special hints regarding amendments:
* When used for paragraph-based amendments, this component is embedded once for each paragraph. Hence,
* not all changeRecommendations provided are relevant for this paragraph (as we put the decision about
* which changeRecommendations are relevant in this component, not the caller).
* TODO: Right now, only change recommendations affecting only one paragraph are supported
*
* ## Examples
*
* ```html
@ -63,12 +71,25 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
public can_manage = false;
// Calculated from the embedded line numbers after the text has been set.
// Hint: this numbering refers to the actual lines, not the line number markers;
// hence, if maxLineNo === 10, line no. 10 is still visible.
// This is semantically different from the diff algorithms.
private minLineNo: number = null;
private maxLineNo: number = null;
/**
* @param {Renderer2} renderer
* @param {ElementRef} el
* @param {ChangeDetectorRef} cd
* @param {OperatorService} operator
*/
public constructor(private renderer: Renderer2, private el: ElementRef, private operator: OperatorService) {
public constructor(
private renderer: Renderer2,
private el: ElementRef,
private cd: ChangeDetectorRef,
private operator: OperatorService
) {
this.operator.getUserObservable().subscribe(this.onPermissionsChanged.bind(this));
}
@ -107,7 +128,21 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
}
public getTextChangeRecommendations(): ViewMotionChangeRecommendation[] {
return this.changeRecommendations.filter(reco => !reco.isTitleChange());
return this.changeRecommendations
.filter(reco => !reco.isTitleChange())
.filter(reco => reco.line_from >= this.minLineNo && reco.line_from <= this.maxLineNo);
}
private setLineNumberCache(): void {
Array.from(this.element.querySelectorAll('.os-line-number')).forEach((lineNumberEl: Element) => {
const lineNumber = parseInt(lineNumberEl.getAttribute('data-line-number'), 10);
if (this.minLineNo === null || lineNumber < this.minLineNo) {
this.minLineNo = lineNumber;
}
if (this.maxLineNo === null || lineNumber > this.maxLineNo) {
this.maxLineNo = lineNumber;
}
});
}
/**
@ -282,7 +317,9 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
// If we show it right away, there will be nasty Angular warnings about changed values, as the position
// is changing while the DOM updates
window.setTimeout(() => {
this.setLineNumberCache();
this.showChangeRecommendations = true;
this.cd.detectChanges();
}, 1);
}

View File

@ -530,7 +530,6 @@
[matMenuTriggerFor]="changeRecoMenu"
*ngIf="
motion &&
!motion.isParagraphBasedAmendment() &&
((allChangingObjects && allChangingObjects.length) || motion.modified_final_version)
"
>
@ -874,33 +873,40 @@
<div class="alert alert-info" *ngIf="motion.diffLines.length === 0">
<span>{{ 'No changes at the text.' | translate }}</span>
</div>
<div
*ngFor="let paragraph of getAmendmentParagraphs(showAmendmentContext)"
class="motion-text motion-text-diff amendment-view"
[class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()"
[class.amendment-context]="showAmendmentContext"
>
<!-- TODO: everything here is required for PDF as well. Should be in a service -->
<h3
*ngIf="paragraph.diffLineTo === paragraph.diffLineFrom + 1 && !showAmendmentContext"
class="amendment-line-header"
<ng-container *ngIf="!isRecoMode(ChangeRecoMode.Diff) && !isFinalEdit">
<div
*ngFor="let paragraph of getAmendmentParagraphs()"
class="motion-text motion-text-diff amendment-view"
[class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()"
[class.amendment-context]="showAmendmentContext"
>
{{ 'Line' | translate }} {{ paragraph.diffLineFrom }}:
</h3>
<h3
*ngIf="paragraph.diffLineTo !== paragraph.diffLineFrom + 1 && !showAmendmentContext"
class="amendment-line-header"
>
{{ 'Line' | translate }} {{ paragraph.diffLineFrom }} - {{ paragraph.diffLineTo - 1 }}:
</h3>
<h3 class="amendment-line-header" *ngIf="!showAmendmentContext">{{ getAmendmentParagraphLinesTitle(paragraph) }}</h3>
<!-- TODO: Seems to be directly duplicated in the slide -->
<div class="paragraphcontext" [innerHtml]="paragraph.textPre | trust: 'html'"></div>
<div [innerHtml]="paragraph.text | trust: 'html'"></div>
<div class="paragraphcontext" [innerHtml]="paragraph.textPost | trust: 'html'"></div>
</div>
<os-motion-detail-original-change-recommendations
*ngIf="isLineNumberingOutside() && (isRecoMode(ChangeRecoMode.Original) || isRecoMode(ChangeRecoMode.Changed))"
[html]="getAmendmentDiffTextWithContext(paragraph)"
[changeRecommendations]="changeRecommendations"
(createChangeRecommendation)="createChangeRecommendation($event)"
(gotoChangeRecommendation)="gotoChangeRecommendation($event)"
></os-motion-detail-original-change-recommendations>
<div
*ngIf="!isLineNumberingOutside() || !(isRecoMode(ChangeRecoMode.Original) || isRecoMode(ChangeRecoMode.Changed))"
[outerHTML]="getAmendmentDiffTextWithContext(paragraph) | trust: 'html'"
></div><!-- the <div> element is only a placeholder -> outerHTML to replace it -->
</div>
</ng-container>
<os-motion-detail-diff
*ngIf="isRecoMode(ChangeRecoMode.Diff)"
[motion]="motion"
[changes]="getChangesForDiffMode()"
[scrollToChange]="scrollToChange"
[highlightedLine]="highlightedLine"
[lineNumberingMode]="lnMode"
(createChangeRecommendation)="createChangeRecommendation($event)"
></os-motion-detail-diff>
</div>
<div *ngIf="!motion.diffLines">
<span class="red-warning-text">{{
@ -910,7 +916,7 @@
</section>
<!-- Show entire motion text -->
<div>
<div *ngIf="isRecoMode(ChangeRecoMode.Original) || isRecoMode(ChangeRecoMode.Changed)">
<mat-checkbox
(change)="showAmendmentContext = !showAmendmentContext"
*ngIf="motion && motion.isParagraphBasedAmendment()"
@ -953,7 +959,10 @@
</div>
</mat-menu>
<!-- Diff View Menu -->
<!-- Diff View Menu
For motions, all items are available if there are changing objects. The final print template only after is has been created.
For paragraph-based amendments, only the original and the diff version is available.
-->
<mat-menu #changeRecoMenu="matMenu">
<button
mat-menu-item
@ -982,13 +991,13 @@
mat-menu-item
(click)="setChangeRecoMode(ChangeRecoMode.Final)"
[ngClass]="{ selected: crMode === ChangeRecoMode.Final }"
*ngIf="allChangingObjects && allChangingObjects.length"
*ngIf="motion && !motion.isParagraphBasedAmendment() && allChangingObjects && allChangingObjects.length"
>
{{ 'Final version' | translate }}
</button>
<button
mat-menu-item
*ngIf="motion && motion.modified_final_version"
*ngIf="motion && motion.modified_final_version && !motion.isParagraphBasedAmendment()"
(click)="setChangeRecoMode(ChangeRecoMode.ModifiedFinal)"
[ngClass]="{ selected: crMode === ChangeRecoMode.ModifiedFinal }"
>

View File

@ -215,10 +215,21 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
public changeRecommendations: ViewMotionChangeRecommendation[];
/**
* All amendments to this motions
* All amendments to this motion
*/
public amendments: ViewMotion[];
/**
* The change recommendations to amendments to this motion
*/
public amendmentChangeRecos: { [amendmentId: string]: ViewMotionChangeRecommendation[] } = {};
/**
* The observables for the `amendmentChangeRecos` field above.
* Necessary to track which amendments' change recommendations we have already subscribed to.
*/
public amendmentChangeRecoSubscriptions: { [amendmentId: string]: Subscription } = {};
/**
* All change recommendations AND amendments, sorted by line number.
*/
@ -416,25 +427,25 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* @param repo Motion Repository
* @param changeRecoRepo Change Recommendation Repository
* @param statuteRepo: Statute Paragraph Repository
* @param mediafileRepo Mediafile Repository
* @param DS The DataStoreService
* @param configService The configuration provider
* @param promptService ensure safe deletion
* @param pdfExport export the motion to pdf
* @param personalNoteService: personal comments and favorite marker
* @param linenumberingService The line numbering service
* @param categoryRepo Repository for categories
* @param viewModelStore accessing view models
* @param categoryRepo access the category repository
* @param userRepo Repository for users
* @param notifyService: NotifyService work with notification
* @param tagRepo
* @param mediaFilerepo
* @param workflowRepo
* @param blockRepo
* @param itemRepo
* @param motionSortService
* @param motionFilterListService
* @param amendmentSortService
* @param motionFilterService
* @param amendmentFilterService
* @param cd ChangeDetectorRef
* @param pollDialog
* @param motionPollService
*/
public constructor(
title: Title,
@ -590,6 +601,24 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
});
}
/**
* Subscribes to all new amendment's change recommendations so we can access their data for the diff view
*/
private resetAmendmentChangeRecoListener(): void {
this.amendments.forEach((amendment: ViewMotion) => {
if (this.amendmentChangeRecoSubscriptions[amendment.id] === undefined) {
this.amendmentChangeRecoSubscriptions[
amendment.id
] = this.changeRecoRepo
.getChangeRecosOfMotionObservable(amendment.id)
.subscribe((changeRecos: ViewMotionChangeRecommendation[]): void => {
this.amendmentChangeRecos[amendment.id] = changeRecos;
this.recalcUnifiedChanges();
});
}
});
}
/**
* Merges amendments and change recommendations and sorts them by the line numbers.
* Called each time one of these arrays changes.
@ -614,8 +643,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
}
if (this.amendments) {
this.amendments.forEach((amendment: ViewMotion): void => {
const toApplyChanges = (this.amendmentChangeRecos[amendment.id] || []).filter(
// The rejected change recommendations for amendments should not be considered
change => change.showInFinalView()
);
this.repo
.getAmendmentAmendedParagraphs(amendment, this.lineLength)
.getAmendmentAmendedParagraphs(amendment, this.lineLength, toApplyChanges)
.forEach((change: ViewUnifiedChange): void => {
this.allChangingObjects.push(change);
});
@ -630,6 +663,14 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
return 0;
}
});
// When "diff" is the default view mode, the crMode might have been set
// before the motion and allChangingObjects have been loaded. As the availability of "diff" depends on
// allChangingObjects, we set "diff" first in this case (in the config-listener) and perform the actual
// check if "diff" is possible now.
// Test: "diff" as default view. Open a motion, create an amendment. "Original" should be set automatically.
this.crMode = this.determineCrMode(this.crMode);
this.cd.markForCheck();
}
@ -664,6 +705,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
this.repo.amendmentsTo(motionId).subscribe((amendments: ViewMotion[]): void => {
this.amendments = amendments;
this.resetAmendmentChangeRecoListener();
this.recalcUnifiedChanges();
}),
this.repo
@ -908,15 +950,48 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
return formatedText;
}
/**
* This returns the plain HTML of a changed area in an amendment, including its context,
* for the purpose of piping it into <motion-detail-original-change-recommendations>.
* This component works with plain HTML, hence we are composing plain HTML here, too.
*
* @param {DiffLinesInParagraph} paragraph
* @returns {string}
*
* TODO: Seems to be directly duplicated in the slide
*/
public getAmendmentDiffTextWithContext(paragraph: DiffLinesInParagraph): string {
return (
'<div class="paragraphcontext">' +
paragraph.textPre +
'</div>' +
'<div>' +
paragraph.text +
'</div>' +
'<div class="paragraphcontext">' +
paragraph.textPost +
'</div>'
);
}
/**
* If `this.motion` is an amendment, this returns the list of all changed paragraphs.
* TODO: Cleanup: repo function could be injected part of the model, to have easier access
*
* @param {boolean} includeUnchanged
* @returns {DiffLinesInParagraph[]}
*/
public getAmendmentParagraphs(includeUnchanged: boolean): DiffLinesInParagraph[] {
return this.repo.getAmendmentParagraphs(this.motion, this.lineLength, includeUnchanged);
public getAmendmentParagraphs(): DiffLinesInParagraph[] {
return this.repo.getAmendmentParagraphLines(
this.motion,
this.lineLength,
this.crMode,
this.changeRecommendations,
this.showAmendmentContext
);
}
public getAmendmentParagraphLinesTitle(paragraph: DiffLinesInParagraph): string {
return this.repo.getAmendmentParagraphLinesTitle(paragraph);
}
/**
@ -1063,12 +1138,27 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
editChangeRecommendation: false,
newChangeRecommendation: true,
lineRange: lineRange,
changeRecommendation: this.changeRecoRepo.createChangeRecommendationTemplate(
changeRecommendation: null
};
if (this.motion.isParagraphBasedAmendment()) {
const lineNumberedParagraphs = this.repo.getAllAmendmentParagraphsWithOriginalLineNumbers(
this.motion,
this.lineLength,
false
);
data.changeRecommendation = this.changeRecoRepo.createAmendmentChangeRecommendationTemplate(
this.motion,
lineNumberedParagraphs,
lineRange,
this.lineLength
);
} else {
data.changeRecommendation = this.changeRecoRepo.createMotionChangeRecommendationTemplate(
this.motion,
lineRange,
this.lineLength
)
};
);
}
this.dialogService.open(MotionChangeRecommendationDialogComponent, {
...mediumDialogSettings,
data: data
@ -1329,7 +1419,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
/**
* Adds or removes a tag to the current motion
*
* @param id Motion tag id
* @param {MouseEvent} event
* @param {number} id Motion tag id
*/
public setTag(event: MouseEvent, id: number): void {
event.stopPropagation();
@ -1497,7 +1588,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* Function to handle leaving persons and
* recognize if there is no other person editing the same motion anymore.
*
* @param senderId The id of the sender who has left the editing-view.
* @param senderName The name of the sender who has left the editing-view.
*/
private recognizeOtherWorkerOnMotion(senderName: string): void {
this.otherWorkOnMotion = this.otherWorkOnMotion.filter(value => value !== senderName);

View File

@ -64,7 +64,14 @@ export class MotionCsvExportService {
const amendments = this.motionRepo.getAmendmentsInstantly(motion.id);
if (amendments) {
for (const amendment of amendments) {
const changedParagraphs = this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength);
const changeRecos = this.changeRecoRepo
.getChangeRecoOfMotion(amendment.id)
.filter(reco => reco.showInFinalView());
const changedParagraphs = this.motionRepo.getAmendmentAmendedParagraphs(
amendment,
lineLength,
changeRecos
);
for (const change of changedParagraphs) {
changes.push(change as ViewUnifiedChange);
}

View File

@ -576,17 +576,16 @@ export class MotionPdfService {
if (motion.isParagraphBasedAmendment()) {
// 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)) {
if (paragraph.diffLineTo === paragraph.diffLineFrom + 1) {
motionText += `<h3>
${this.translate.instant('Line')} ${paragraph.diffLineFrom}:
</h3>`;
} else {
motionText += `<h3>
${this.translate.instant('Line')} ${paragraph.diffLineFrom} - ${paragraph.diffLineTo - 1}:
</h3>`;
}
const changeRecos = this.changeRecoRepo.getChangeRecoOfMotion(motion.id);
const amendmentParas = this.motionRepo.getAmendmentParagraphLines(
motion,
lineLength,
crMode,
changeRecos,
false
);
for (const paragraph of amendmentParas) {
motionText += '<h3>' + this.motionRepo.getAmendmentParagraphLinesTitle(paragraph) + '</h3>';
motionText += `<div class="paragraphcontext">${paragraph.textPre}</div>`;
motionText += paragraph.text;
motionText += `<div class="paragraphcontext">${paragraph.textPost}</div>`;
@ -634,11 +633,12 @@ export class MotionPdfService {
return this.changeRecoRepo
.getChangeRecoOfMotion(motion.id)
.concat(
this.motionRepo
.getAmendmentsInstantly(motion.id)
.flatMap((amendment: ViewMotion) =>
this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength)
)
this.motionRepo.getAmendmentsInstantly(motion.id).flatMap((amendment: ViewMotion) => {
const changeRecos = this.changeRecoRepo
.getChangeRecoOfMotion(amendment.id)
.filter(reco => reco.showInFinalView());
return this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength, changeRecos);
})
)
.sort((a, b) => a.getLineFrom() - b.getLineFrom()) as ViewUnifiedChange[];
}

View File

@ -6,7 +6,7 @@ import { SlideData } from 'app/core/core-services/projector-data.service';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { DiffLinesInParagraph, DiffService, LineRange } from 'app/core/ui-services/diff.service';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { LineNumberedString, LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
import { MotionTitleInformation } from 'app/site/motions/models/view-motion';
import { ChangeRecoMode, LineNumberingMode } from 'app/site/motions/motions.constants';
@ -270,19 +270,18 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
/**
* Extracts a renderable HTML string representing the given line number range of this motion
*
* @param {string} motionHtml
* @param {LineNumberedString} motionHtml
* @param {LineRange} lineRange
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
* @param {number} lineLength
*/
public extractMotionLineRange(
motionHtml: string,
motionHtml: LineNumberedString,
lineRange: LineRange,
lineNumbers: boolean,
lineLength: number
): string {
const origHtml = this.lineNumbering.insertLineNumbers(motionHtml, this.lineLength, this.highlightedLine);
const extracted = this.diff.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
const extracted = this.diff.extractRangeByLineNumbers(motionHtml, lineRange.from, lineRange.to);
let html =
extracted.outerContextStart +
extracted.innerContextStart +
@ -343,21 +342,22 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
const changes = this.getAllTextChangingObjects().filter(change => {
return change.showInDiffView();
});
const motionText = this.lineNumbering.insertLineNumbers(motion.text, this.lineLength);
changes.forEach((change: ViewUnifiedChange, idx: number) => {
if (idx === 0) {
const lineRange = { from: 1, to: change.getLineFrom() };
text += this.extractMotionLineRange(motion.text, lineRange, true, this.lineLength);
text += this.extractMotionLineRange(motionText, lineRange, true, this.lineLength);
} else if (changes[idx - 1].getLineTo() < change.getLineFrom()) {
const lineRange = {
from: changes[idx - 1].getLineTo(),
to: change.getLineFrom()
};
text += this.extractMotionLineRange(motion.text, lineRange, true, this.lineLength);
text += this.extractMotionLineRange(motionText, lineRange, true, this.lineLength);
}
text += this.diff.getChangeDiff(motion.text, change, this.lineLength, this.highlightedLine);
text += this.diff.getChangeDiff(motionText, change, this.lineLength, this.highlightedLine);
});
text += this.diff.getTextRemainderAfterLastChange(
motion.text,
motionText,
changes,
this.lineLength,
this.highlightedLine
@ -412,7 +412,7 @@ export class MotionSlideComponent extends BaseMotionSlideComponent<MotionSlideDa
return null;
}
// Hint: can be either DiffLinesInParagraph or null, if no changes are made
return this.diff.getAmendmentParagraphsLinesByMode(
return this.diff.getAmendmentParagraphsLines(
paraNo,
baseParagraphs[paraNo],
newText,