diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 4be576ec3..2275fe59b 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -18,6 +18,16 @@ import { SpinnerService } from './core/ui-services/spinner.service'; import { Router } from '@angular/router'; import { ViewUser } from './site/users/models/view-user'; +/** + * Enhance array with own functions + * TODO: Remove once flatMap made its way into official JS/TS (ES 2019?) + */ +declare global { + interface Array { + flatMap(o: any): Array; + } +} + /** * Angular's global App Component */ @@ -82,6 +92,7 @@ export class AppComponent { translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en'); // change default JS functions this.overloadArrayToString(); + this.overloadFlatMap(); // Show the spinner initial spinnerService.setVisibility(true, translate.instant('Loading data. Please wait...')); @@ -148,6 +159,18 @@ export class AppComponent { }; } + /** + * Adds an implementation of flatMap. + * TODO: Remove once flatMap made its way into official JS/TS (ES 2019?) + */ + private overloadFlatMap(): void { + const concat = (x: any, y: any) => x.concat(y); + const flatMap = (f: any, xs: any) => xs.map(f).reduce(concat, []); + Array.prototype.flatMap = function(f: any): any[] { + return flatMap(f, this); + }; + } + /** * Function to check if the user is existing and the app is already stable. * If both conditions true, hide the spinner. diff --git a/client/src/app/shared/utils/recommendation-type-names.ts b/client/src/app/shared/utils/recommendation-type-names.ts new file mode 100644 index 000000000..e1889a7f0 --- /dev/null +++ b/client/src/app/shared/utils/recommendation-type-names.ts @@ -0,0 +1,21 @@ +import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation'; +import { ModificationType } from 'app/core/ui-services/diff.service'; + +/** + * Gets the name of the modification type + * + * @param change + * @returns the name of a recommendation type + */ +export function getRecommendationTypeName(change: ViewMotionChangeRecommendation): string { + switch (change.type) { + case ModificationType.TYPE_REPLACEMENT: + return 'Replacement'; + case ModificationType.TYPE_INSERTION: + return 'Insertion'; + case ModificationType.TYPE_DELETION: + return 'Deletion'; + default: + return change.other_description; + } +} diff --git a/client/src/app/site/motions/models/view-motion-amended-paragraph.ts b/client/src/app/site/motions/models/view-motion-amended-paragraph.ts index d49774fd0..793a81af7 100644 --- a/client/src/app/site/motions/models/view-motion-amended-paragraph.ts +++ b/client/src/app/site/motions/models/view-motion-amended-paragraph.ts @@ -10,6 +10,10 @@ import { MergeAmendment } from 'app/shared/models/motions/workflow-state'; * Amendments <-> ViewMotionAmendedParagraph is potentially a 1:n-relation */ export class ViewMotionAmendedParagraph implements ViewUnifiedChange { + public get stateName(): string { + return this.amendment.state.name; + } + public constructor( private amendment: ViewMotion, private paragraphNo: number, diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail-diff/motion-detail-diff.component.html b/client/src/app/site/motions/modules/motion-detail/components/motion-detail-diff/motion-detail-diff.component.html index b259f3031..ddd48ccbd 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail-diff/motion-detail-diff.component.html +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail-diff/motion-detail-diff.component.html @@ -18,8 +18,8 @@ ({{ 'Change recommendation' | translate }}) ({{ 'Amendment' | translate }} {{ change.getIdentifier() }}) - – {{ getRecommendationTypeName(change) | translate }} + + – {{ getRecommendationTypeName(change) | translate }}
-
-
('motions_line_length'); let motionPdfContent = []; // Enforces that statutes should always have Diff Mode and no line numbers @@ -116,14 +126,14 @@ export class MotionPdfService { motionPdfContent = [title, subtitle]; if ((infoToExport && infoToExport.length > 0) || !infoToExport) { - const metaInfo = this.createMetaInfoTable(motion, crMode, infoToExport); + const metaInfo = this.createMetaInfoTable(motion, lineLength, crMode, infoToExport); motionPdfContent.push(metaInfo); } if (!contentToExport || contentToExport.includes('text')) { const preamble = this.createPreamble(motion); motionPdfContent.push(preamble); - const text = this.createText(motion, lnMode, crMode); + const text = this.createText(motion, lineLength, lnMode, crMode); motionPdfContent.push(text); } @@ -192,7 +202,12 @@ export class MotionPdfService { * @param motion the target motion * @returns doc def for the meta infos */ - private createMetaInfoTable(motion: ViewMotion, crMode: ChangeRecoMode, infoToExport?: InfoToExport[]): object { + private createMetaInfoTable( + motion: ViewMotion, + lineLength: number, + crMode: ChangeRecoMode, + infoToExport?: InfoToExport[] + ): object { const metaTableBody = []; // submitters @@ -367,23 +382,13 @@ export class MotionPdfService { } // summary of change recommendations (for motion diff version only) - const changeRecos = this.changeRecoRepo - .getChangeRecoOfMotion(motion.id) - .sort((a: ViewUnifiedChange, b: ViewUnifiedChange) => { - if (a.getLineFrom() < b.getLineFrom()) { - return -1; - } else if (a.getLineFrom() > b.getLineFrom()) { - return 1; - } else { - return 0; - } - }); + const changes = this.getUnifiedChanges(motion, lineLength); - if (crMode === ChangeRecoMode.Diff && changeRecos.length > 0) { + if (crMode === ChangeRecoMode.Diff && changes.length > 0) { const columnLineNumbers = []; const columnChangeType = []; - changeRecos.forEach(changeReco => { + changes.forEach(change => { // TODO: the function isTitleRecommendation() does not exist anymore. // Not sure if required or not // if (changeReco.isTitleRecommendation()) { @@ -392,44 +397,56 @@ export class MotionPdfService { // line numbers column let line; - if (changeReco.line_from >= changeReco.line_to - 1) { - line = changeReco.line_from; + if (change.getLineFrom() >= change.getLineTo() - 1) { + line = change.getLineFrom(); } else { - line = changeReco.line_from + ' - ' + (changeReco.line_to - 1); + line = change.getLineFrom() + ' - ' + (change.getLineTo() - 1); } - columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `); // change type column - if (changeReco.type === 0) { - columnChangeType.push(this.translate.instant('Replacement')); - } else if (changeReco.type === 1) { - columnChangeType.push(this.translate.instant('Insertion')); - } else if (changeReco.type === 2) { - columnChangeType.push(this.translate.instant('Deletion')); - } else if (changeReco.type === 3) { - columnChangeType.push(changeReco.other_description); + if (change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION) { + const changeReco = change as ViewMotionChangeRecommendation; + columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `); + 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); + } } }); - metaTableBody.push([ - { - text: this.translate.instant('Summary of changes'), - style: 'boldText' - }, - { - columns: [ - { - text: columnLineNumbers.join('\n'), - width: 'auto' - }, - { - text: columnChangeType.join('\n'), - width: 'auto' - } - ], - columnGap: 7 - } - ]); + if (columnChangeType.length > 0) { + metaTableBody.push([ + { + text: this.translate.instant('Summary of changes'), + style: 'boldText' + }, + { + columns: [ + { + text: columnLineNumbers.join('\n'), + width: 'auto' + }, + { + text: columnChangeType.join('\n'), + width: 'auto' + } + ], + columnGap: 7 + } + ]); + } } if (metaTableBody.length > 0) { @@ -479,10 +496,13 @@ export class MotionPdfService { * @param crMode determine the used change Recommendation mode * @returns doc def for the "the assembly may decide" preamble */ - private createText(motion: ViewMotion, lnMode: LineNumberingMode, crMode: ChangeRecoMode): object { + private createText( + motion: ViewMotion, + lineLength: number, + lnMode: LineNumberingMode, + crMode: ChangeRecoMode + ): object { let motionText: string; - // get the line length from the config - const lineLength = this.configService.instant('motions_line_length'); if (motion.isParagraphBasedAmendment()) { motionText = ''; @@ -510,26 +530,8 @@ export class MotionPdfService { } else { // lead motion or normal amendments // TODO: Consider tile change recommendation - const changes: ViewUnifiedChange[] = Object.assign( - [], - this.changeRecoRepo.getChangeRecoOfMotion(motion.id) - ); - // TODO: Cleanup, everything change reco and amendment based needs a unified structure. - const amendments = this.motionRepo.getAmendmentsInstantly(motion.id); - if (amendments) { - for (const amendment of amendments) { - const changedParagraphs = this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength); - for (const change of changedParagraphs) { - changes.push(change as ViewUnifiedChange); - } - } - } - - // changes need to be sorted, by "line from". - // otherwise, formatMotion will make unexpected results by messing up the - // order of changes applied to the motion - changes.sort((a, b) => a.getLineFrom() - b.getLineFrom()); + const changes = this.getUnifiedChanges(motion, lineLength); motionText = this.motionRepo.formatMotion(motion.id, crMode, changes, lineLength); // reformat motion text to split long HTML elements to easier convert into PDF motionText = this.linenumberingService.splitInlineElementsAtLineBreaks(motionText); @@ -538,6 +540,30 @@ export class MotionPdfService { return this.htmlToPdfService.convertHtml(motionText, lnMode); } + /** + * changes need to be sorted, by "line from". + * otherwise, formatMotion will make unexpected results by messing up the + * order of changes applied to the motion + * + * TODO: Cleanup, everything change reco and amendment based needs a unified structure. + * + * @param motion + * @param lineLength + * @returns + */ + private getUnifiedChanges(motion: ViewMotion, lineLength: number): ViewUnifiedChange[] { + return this.changeRecoRepo + .getChangeRecoOfMotion(motion.id) + .concat( + this.motionRepo + .getAmendmentsInstantly(motion.id) + .flatMap((amendment: ViewMotion) => + this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength) + ) + ) + .sort((a, b) => a.getLineFrom() - b.getLineFrom()) as ViewUnifiedChange[]; + } + /** * Creates the motion reason - uses HTML to PDF * @@ -688,6 +714,7 @@ export class MotionPdfService { const subtitle = this.createSubtitle(motion); const metaInfo = this.createMetaInfoTable( motion, + this.configService.instant('motions_line_length'), this.configService.instant('motions_recommendation_text_mode'), ['submitters', 'state', 'category'] );