From b51787129bd0d1bbc4c4742580a56badce031527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Ho=CC=88=C3=9Fl?= Date: Sat, 4 Apr 2020 10:11:56 +0200 Subject: [PATCH] Change recommendations for amendments --- ...hange-recommendation-repository.service.ts | 51 +++- .../motions/motion-repository.service.ts | 225 +++++++++++++++--- .../app/core/ui-services/diff.service.spec.ts | 22 ++ .../src/app/core/ui-services/diff.service.ts | 49 ++-- .../core/ui-services/linenumbering.service.ts | 7 +- .../app/site/motions/models/view-motion.ts | 1 - .../motion-detail-diff.component.ts | 48 ++-- ...iginal-change-recommendations.component.ts | 49 +++- .../motion-detail.component.html | 69 +++--- .../motion-detail/motion-detail.component.ts | 123 ++++++++-- .../services/motion-csv-export.service.ts | 9 +- .../motions/services/motion-pdf.service.ts | 32 +-- .../motions/motion/motion-slide.component.ts | 20 +- 13 files changed, 548 insertions(+), 157 deletions(-) diff --git a/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts b/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts index 290ddcf41..8ca5c2085 100644 --- a/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts +++ b/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts @@ -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. diff --git a/client/src/app/core/repositories/motions/motion-repository.service.ts b/client/src/app/core/repositories/motions/motion-repository.service.ts index 5b79f114a..ea7a69b79 100644 --- a/client/src/app/core/repositories/motions/motion-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-repository.service.ts @@ -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 { @@ -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 { @@ -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 { @@ -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 * diff --git a/client/src/app/core/ui-services/diff.service.spec.ts b/client/src/app/core/ui-services/diff.service.spec.ts index b5afcb9c8..953046a1b 100644 --- a/client/src/app/core/ui-services/diff.service.spec.ts +++ b/client/src/app/core/ui-services/diff.service.spec.ts @@ -1172,6 +1172,28 @@ describe('DiffService', () => { ); } )); + + it('detects a word replacement at the end of line correctly', inject([DiffService], (service: DiffService) => { + const before = + '

' + + 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 = + '

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( + '

' + + noMarkup(1) + + 'wuid Brotzeit? Pfenningguat Stubn bitt da, ' + + 'hog di hi fei nia need nia need Goaßmaß Radler ' + + brMarkup(2) + + 'gscheid kloan mim

' + ); + })); }); describe('addCSSClassToFirstTag function', () => { diff --git a/client/src/app/core/ui-services/diff.service.ts b/client/src/app/core/ui-services/diff.service.ts index 30c6716c1..2dfcee13a 100644 --- a/client/src/app/core/ui-services/diff.service.ts +++ b/client/src/app/core/ui-services/diff.service.ts @@ -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

Line 2

* - extracting line 3 to null/4 results in

Line 3

* - * @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 tags that only delete line numbers // We need to do this before removing as done in one of the next statements diffUnnormalized = diffUnnormalized.replace( - /((
<\/del>)?(]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi, - (found: string, tag: string, br: string, span: string): string => { - return (br !== undefined ? br : '') + span + ' '; + /(((
)<\/del>)?(]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi, + (found: string, tag: string, brWithDel: string, plainBr: string, span: string): string => { + return (plainBr !== undefined ? plainBr : '') + span + ' '; } ); // Merging individual insert/delete statements into bigger blocks diffUnnormalized = diffUnnormalized.replace(/<\/ins>/gi, '').replace(/<\/del>/gi, ''); + // If we have a deleted wordLINEBREAKnew word, 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>)(
]+os-line-number[^>]+?>\s*<\/span>)([\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 + diff --git a/client/src/app/core/ui-services/linenumbering.service.ts b/client/src/app/core/ui-services/linenumbering.service.ts index 593465215..04768861a 100644 --- a/client/src/app/core/ui-services/linenumbering.service.ts +++ b/client/src/app/core/ui-services/linenumbering.service.ts @@ -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) { diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 5b9284938..6cf5bea3b 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -199,7 +199,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers !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); } diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html index c702a10e1..657a2ea0b 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html @@ -530,7 +530,6 @@ [matMenuTriggerFor]="changeRecoMenu" *ngIf=" motion && - !motion.isParagraphBasedAmendment() && ((allChangingObjects && allChangingObjects.length) || motion.modified_final_version) " > @@ -874,33 +873,40 @@
{{ 'No changes at the text.' | translate }}
-
- -

+
- {{ 'Line' | translate }} {{ paragraph.diffLineFrom }}: -

-

- {{ 'Line' | translate }} {{ paragraph.diffLineFrom }} - {{ paragraph.diffLineTo - 1 }}: -

+

{{ getAmendmentParagraphLinesTitle(paragraph) }}

- -
-
-
-
+ +
+ + + +
{{ @@ -910,7 +916,7 @@ -
+
- +