Show amendments in detail view
This commit is contained in:
parent
e694d9e0dd
commit
afad235bad
@ -1,6 +1,15 @@
|
|||||||
import { Deserializer } from '../base/deserializer';
|
import { Deserializer } from '../base/deserializer';
|
||||||
import { Workflow } from './workflow';
|
import { Workflow } from './workflow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies if an amendment of this state/recommendation should be merged into the motion
|
||||||
|
*/
|
||||||
|
export enum MergeAmendment {
|
||||||
|
NO = -1,
|
||||||
|
UNDEFINED = 0,
|
||||||
|
YES = 1
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a workflow state
|
* Representation of a workflow state
|
||||||
*
|
*
|
||||||
@ -18,7 +27,8 @@ export class WorkflowState extends Deserializer {
|
|||||||
public allow_create_poll: boolean;
|
public allow_create_poll: boolean;
|
||||||
public allow_submitter_edit: boolean;
|
public allow_submitter_edit: boolean;
|
||||||
public dont_set_identifier: boolean;
|
public dont_set_identifier: boolean;
|
||||||
public show_state_extension_field: boolean;
|
public show_state_extension_field: number;
|
||||||
|
public merge_amendment_into_final: MergeAmendment;
|
||||||
public show_recommendation_extension_field: boolean;
|
public show_recommendation_extension_field: boolean;
|
||||||
public next_states_id: number[];
|
public next_states_id: number[];
|
||||||
public workflow_id: number;
|
public workflow_id: number;
|
||||||
|
@ -23,7 +23,8 @@ import {
|
|||||||
DateAdapter,
|
DateAdapter,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
MatBadgeModule
|
MatBadgeModule,
|
||||||
|
MatStepperModule
|
||||||
} from '@angular/material';
|
} from '@angular/material';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatChipsModule } from '@angular/material';
|
import { MatChipsModule } from '@angular/material';
|
||||||
@ -110,6 +111,7 @@ import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.com
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatRadioModule,
|
MatRadioModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
|
MatStepperModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
RouterModule,
|
RouterModule,
|
||||||
@ -147,6 +149,7 @@ import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.com
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatRadioModule,
|
MatRadioModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
|
MatStepperModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgxMatSelectSearchModule,
|
NgxMatSelectSearchModule,
|
||||||
FileDropModule,
|
FileDropModule,
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
<os-head-bar [nav]="false">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot"><h2 translate>Create amendment</h2></div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<form [formGroup]="contentForm" (ngSubmit)="saveAmendment()" class="on-transition-fade">
|
||||||
|
<mat-horizontal-stepper linear>
|
||||||
|
<mat-step [label]="'Select paragraph' | translate" [completed]="contentForm.value.selectedParagraph">
|
||||||
|
<ng-template matStepLabel translate>Select paragraph</ng-template>
|
||||||
|
<div>
|
||||||
|
<section
|
||||||
|
*ngFor="let paragraph of paragraphs"
|
||||||
|
class="paragraph-row"
|
||||||
|
[class.active]="selectedParagraph === paragraph.paragraphNo"
|
||||||
|
(click)="selectParagraph(paragraph)"
|
||||||
|
>
|
||||||
|
<mat-radio-button
|
||||||
|
class="paragraph-select"
|
||||||
|
[checked]="contentForm.value.selectedParagraph === paragraph.paragraphNo"
|
||||||
|
></mat-radio-button>
|
||||||
|
<div class="paragraph-text motion-text" [innerHTML]="paragraph.safeHtml"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-button
|
||||||
|
matStepperNext
|
||||||
|
[disabled]="contentForm.value.selectedParagraph === null"
|
||||||
|
>
|
||||||
|
<span translate>Next</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-step>
|
||||||
|
<mat-step [label]="'Specify the change' | translate">
|
||||||
|
<ng-template matStepLabel translate>Specify changes</ng-template>
|
||||||
|
|
||||||
|
<h5 translate>Amended paragraph</h5>
|
||||||
|
<!-- The HTML Editor -->
|
||||||
|
<editor formControlName="text" [init]="tinyMceSettings"></editor>
|
||||||
|
|
||||||
|
<h5 translate>Reason</h5>
|
||||||
|
<!-- The HTML Editor -->
|
||||||
|
<editor formControlName="reason" [init]="tinyMceSettings"></editor>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="button" mat-button matStepperPrevious><span translate>Back</span></button>
|
||||||
|
<button type="button" mat-button (click)="saveAmendment()"><span translate>Create</span></button>
|
||||||
|
</div>
|
||||||
|
</mat-step>
|
||||||
|
</mat-horizontal-stepper>
|
||||||
|
</form>
|
@ -0,0 +1,85 @@
|
|||||||
|
.paragraph-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #ccc;
|
||||||
|
&:hover {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph-select {
|
||||||
|
flex-basis: 50px;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.paragraph-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .motion-text {
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
li,
|
||||||
|
blockquote {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-left: 40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.os-line-number {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
padding-right: 55px;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: attr(data-line-number);
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
color: gray;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide-form {
|
||||||
|
textarea {
|
||||||
|
height: 25vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AmendmentCreateWizardComponent } from './amendment-create-wizard.component';
|
||||||
|
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||||
|
|
||||||
|
describe('AmendmentCreateWizardComponent', () => {
|
||||||
|
let component: AmendmentCreateWizardComponent;
|
||||||
|
let fixture: ComponentFixture<AmendmentCreateWizardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [AmendmentCreateWizardComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AmendmentCreateWizardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,176 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { MotionRepositoryService } from '../../services/motion-repository.service';
|
||||||
|
import { ViewMotion } from '../../models/view-motion';
|
||||||
|
import { LinenumberingService } from '../../services/linenumbering.service';
|
||||||
|
import { Motion } from '../../../../shared/models/motions/motion';
|
||||||
|
import { BaseViewComponent } from '../../../base/base-view';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the single paragraphs from the base motion.
|
||||||
|
*/
|
||||||
|
interface ParagraphToChoose {
|
||||||
|
/**
|
||||||
|
* The paragraph number.
|
||||||
|
*/
|
||||||
|
paragraphNo: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw HTML of this paragraph.
|
||||||
|
*/
|
||||||
|
rawHtml: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTML of this paragraph, wrapped in a `SafeHtml`-object.
|
||||||
|
*/
|
||||||
|
safeHtml: SafeHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wizard used to create a new amendment based on a motion.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-amendment-create-wizard',
|
||||||
|
templateUrl: './amendment-create-wizard.component.html',
|
||||||
|
styleUrls: ['./amendment-create-wizard.component.scss']
|
||||||
|
})
|
||||||
|
export class AmendmentCreateWizardComponent extends BaseViewComponent {
|
||||||
|
/**
|
||||||
|
* The motion to be amended
|
||||||
|
*/
|
||||||
|
public motion: ViewMotion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The paragraphs of the base motion
|
||||||
|
*/
|
||||||
|
public paragraphs: ParagraphToChoose[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change recommendation content.
|
||||||
|
*/
|
||||||
|
public contentForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motions meta-info
|
||||||
|
*/
|
||||||
|
public metaInfoForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs this component.
|
||||||
|
*
|
||||||
|
* @param {Title} titleService set the browser title
|
||||||
|
* @param {TranslateService} translate the translation service
|
||||||
|
* @param {FormBuilder} formBuilder Form builder
|
||||||
|
* @param {MotionRepositoryService} repo Motion Repository
|
||||||
|
* @param {ActivatedRoute} route The activated route
|
||||||
|
* @param {Router} router The router
|
||||||
|
* @param {DomSanitizer} sanitizer The DOM Sanitizing library
|
||||||
|
* @param {LinenumberingService} lineNumbering The line numbering service
|
||||||
|
* @param {MatSnackBar} matSnackBar Material Design SnackBar
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
titleService: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private repo: MotionRepositoryService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private lineNumbering: LinenumberingService,
|
||||||
|
matSnackBar: MatSnackBar
|
||||||
|
) {
|
||||||
|
super(titleService, translate, matSnackBar);
|
||||||
|
this.getMotionByUrl();
|
||||||
|
this.createForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determine the motion to display using the URL
|
||||||
|
*/
|
||||||
|
public getMotionByUrl(): void {
|
||||||
|
// load existing motion
|
||||||
|
this.route.params.subscribe(params => {
|
||||||
|
this.repo.getViewModelObservable(params.id).subscribe(newViewMotion => {
|
||||||
|
this.motion = newViewMotion;
|
||||||
|
|
||||||
|
this.paragraphs = this.repo
|
||||||
|
.getTextParagraphs(this.motion, true)
|
||||||
|
.map((paragraph: string, index: number) => {
|
||||||
|
return {
|
||||||
|
paragraphNo: index,
|
||||||
|
safeHtml: this.sanitizer.bypassSecurityTrustHtml(paragraph),
|
||||||
|
rawHtml: this.lineNumbering.stripLineNumbers(paragraph)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the forms for the Motion and the MotionVersion
|
||||||
|
*/
|
||||||
|
public createForm(): void {
|
||||||
|
this.contentForm = this.formBuilder.group({
|
||||||
|
selectedParagraph: [null, Validators.required],
|
||||||
|
text: ['', Validators.required],
|
||||||
|
reason: ['', Validators.required]
|
||||||
|
});
|
||||||
|
this.metaInfoForm = this.formBuilder.group({
|
||||||
|
identifier: [''],
|
||||||
|
category_id: [''],
|
||||||
|
state_id: [''],
|
||||||
|
recommendation_id: [''],
|
||||||
|
submitters_id: [],
|
||||||
|
supporters_id: [],
|
||||||
|
origin: ['']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the template when a paragraph is clicked.
|
||||||
|
*
|
||||||
|
* @param {ParagraphToChoose} paragraph
|
||||||
|
*/
|
||||||
|
public selectParagraph(paragraph: ParagraphToChoose): void {
|
||||||
|
this.contentForm.patchValue({
|
||||||
|
selectedParagraph: paragraph.paragraphNo,
|
||||||
|
text: paragraph.rawHtml
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the amendment and navigates to detail view of this amendment
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async saveAmendment(): Promise<void> {
|
||||||
|
const amendedParagraphs = this.paragraphs.map(
|
||||||
|
(paragraph: ParagraphToChoose, index: number): string => {
|
||||||
|
if (index === this.contentForm.value.selectedParagraph) {
|
||||||
|
return this.contentForm.value.text;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const newMotionValues = {
|
||||||
|
...this.metaInfoForm.value,
|
||||||
|
...this.contentForm.value,
|
||||||
|
title: this.translate.instant('Amendment to') + ' ' + this.motion.identifier,
|
||||||
|
parent_id: this.motion.id,
|
||||||
|
amendment_paragraphs: amendedParagraphs
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromForm = new Motion();
|
||||||
|
fromForm.deserialize(newMotionValues);
|
||||||
|
|
||||||
|
const response = await this.repo.create(fromForm);
|
||||||
|
this.router.navigate(['./motions/' + response.id]);
|
||||||
|
}
|
||||||
|
}
|
@ -1,73 +1,78 @@
|
|||||||
<!-- A summary of all changes -->
|
<!-- A summary of all changes -->
|
||||||
<section class="change-recommendation-overview">
|
<section class="change-recommendation-overview">
|
||||||
<strong>
|
<strong> {{ 'Summary of changes' | translate }}: </strong>
|
||||||
{{ 'Summary of changes' | translate }}:
|
|
||||||
</strong>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<button os-perms="motions.can_manage" class="btn btn-sm btn-default pull-right"
|
<button os-perms="motions.can_manage" class="btn btn-sm btn-default pull-right"
|
||||||
uib-tooltip="{{ 'Note: You have to reject all change recommendations if the plenum does not follow the recommendation. This does not affect amendments.' | translate }}"
|
uib-tooltip="{{ 'Note: You have to reject all change recommendations if the plenum does not follow the recommendation. This does not affect amendments.' | translate }}"
|
||||||
ng-click="viewChangeRecommendations.rejectAllChangeRecommendations(motion)">
|
ng-click="viewChangeRecommendations.rejectAllChangeRecommendations(motion)">
|
||||||
<i class="fa fa-thumbs-down"></i>
|
<i class="fa fa-thumbs-down"></i>
|
||||||
<translate>Reject all change recommendations</translate>
|
<translate>Reject all change recommendations</translate>
|
||||||
</button>
|
</button>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<ul *ngIf="changes.length > 0">
|
<ul *ngIf="changes.length > 0">
|
||||||
<li *ngFor="let change of changes">
|
<li *ngFor="let change of changes">
|
||||||
<a href='' (click)="scrollToChangeClicked(change, $event)"
|
<a
|
||||||
[class.amendment]="isAmendment(change)"
|
href=""
|
||||||
[class.recommendation]="isChangeRecommendation(change)">
|
(click)="scrollToChangeClicked(change, $event)"
|
||||||
<span *ngIf="change.getLineFrom() >= change.getLineTo() - 1" class="line-number">
|
[class.amendment]="isAmendment(change)"
|
||||||
{{ 'Line' | translate }} {{ change.getLineFrom() }}<span *ngIf="isChangeRecommendation(change)"></span>
|
[class.recommendation]="isChangeRecommendation(change)"
|
||||||
</span>
|
>
|
||||||
<span *ngIf="change.getLineFrom() < change.getLineTo() - 1" class="line-number">
|
<span *ngIf="change.getLineFrom() >= change.getLineTo() - 1" class="line-number">
|
||||||
{{ 'Line' | translate }} {{ change.getLineFrom() }} -
|
{{ 'Line' | translate }} {{ change.getLineFrom() }}
|
||||||
{{ change.getLineTo() - 1 }}<span *ngIf="isChangeRecommendation(change)"></span>
|
|
||||||
</span>
|
|
||||||
<span *ngIf="isChangeRecommendation(change)">({{ 'Change recommendation' | translate }})</span>
|
|
||||||
<span *ngIf="isAmendment(change)">@TODO Identifier</span>
|
|
||||||
<span class="operation" *ngIf="isChangeRecommendation(change)">–
|
|
||||||
{{ getRecommendationTypeName(change) }}
|
|
||||||
<!-- @TODO
|
|
||||||
<span ng-if="change.original.getType(motion.getVersion(version).text) == 3">
|
|
||||||
{ change.other_description }
|
|
||||||
</span>
|
</span>
|
||||||
-->
|
<span *ngIf="change.getLineFrom() < change.getLineTo() - 1" class="line-number">
|
||||||
</span>
|
{{ 'Line' | translate }} {{ change.getLineFrom() }} - {{ change.getLineTo() - 1 }}
|
||||||
<span class="status">
|
</span>
|
||||||
<ng-container *ngIf="change.isRejected()" translate>Rejected</ng-container>
|
<span *ngIf="isChangeRecommendation(change)"> ({{ 'Change recommendation' | translate }})</span>
|
||||||
<ng-container *ngIf="change.isAccepted() && isAmendment(change)" translate>Accepted</ng-container>
|
<span *ngIf="isAmendment(change)"> ({{ 'Amendment' | translate }} {{ change.getIdentifier() }})</span>
|
||||||
</span>
|
<span class="operation" *ngIf="isChangeRecommendation(change)"
|
||||||
</a>
|
>– {{ getRecommendationTypeName(change) }}
|
||||||
|
<!--
|
||||||
|
@TODO
|
||||||
|
<span ng-if="change.original.getType(motion.getVersion(version).text) == 3">
|
||||||
|
{ change.other_description }
|
||||||
|
</span>
|
||||||
|
-->
|
||||||
|
</span>
|
||||||
|
<span class="status">
|
||||||
|
<ng-container *ngIf="change.isRejected()" translate>Rejected</ng-container>
|
||||||
|
<ng-container *ngIf="change.isAccepted() && isAmendment(change)" translate>Accepted</ng-container>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div *ngIf="changes.length === 0" class="no-changes">
|
<div *ngIf="changes.length === 0" class="no-changes">{{ 'No change recommendations yet' | translate }}</div>
|
||||||
{{ 'No change recommendations yet' | translate }}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<!-- 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 *ngFor="let change of changes; let i = index">
|
||||||
<div class="motion-text line-numbers-outside">
|
<div class="motion-text line-numbers-outside">
|
||||||
<os-motion-detail-original-change-recommendations
|
<os-motion-detail-original-change-recommendations
|
||||||
[html]="getTextBetweenChanges(changes[i - 1], change)"
|
[html]="getTextBetweenChanges(changes[i - 1], change)"
|
||||||
[changeRecommendations]="[]"
|
[changeRecommendations]="[]"
|
||||||
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
||||||
></os-motion-detail-original-change-recommendations>
|
></os-motion-detail-original-change-recommendations>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div [class.collides]="hasCollissions(change)"
|
<div
|
||||||
class="diff-box diff-box-{{ change.getChangeId() }} clearfix">
|
class="diff-box diff-box-{{ change.getChangeId() }} clearfix"
|
||||||
<div class="collission-hint" *ngIf="hasCollissions(change)">
|
[class.collides]="hasCollissions(change, changes)"
|
||||||
|
>
|
||||||
|
<div class="collission-hint" *ngIf="hasCollissions(change, changes)">
|
||||||
<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'">
|
||||||
<button mat-icon-button *ngIf="isRecommendation(change)" type="button"
|
<button
|
||||||
[matMenuTriggerFor]="changeRecommendationMenu" [matMenuTriggerData]="{change: change}" >
|
mat-icon-button
|
||||||
|
*ngIf="isRecommendation(change)"
|
||||||
|
type="button"
|
||||||
|
[matMenuTriggerFor]="changeRecommendationMenu"
|
||||||
|
[matMenuTriggerData]="{ change: change }"
|
||||||
|
>
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -78,41 +83,53 @@
|
|||||||
<i class="fa fa-info"></i>
|
<i class="fa fa-info"></i>
|
||||||
{{ change.original.identifier }}
|
{{ change.original.identifier }}
|
||||||
</a>
|
</a>
|
||||||
-->
|
-->
|
||||||
</div>
|
</div>
|
||||||
<div class="status-row" *ngIf="change.isRejected()">
|
<div class="status-row" *ngIf="change.isRejected()">
|
||||||
<i class="grey">{{ 'Rejected' | translate }}:</i>
|
<i class="grey">{{ 'Rejected' | translate }}</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="motion-text motion-text-diff line-numbers-outside"
|
<div
|
||||||
[attr.data-change-id]="change.getChangeId()"
|
class="motion-text motion-text-diff line-numbers-outside"
|
||||||
[innerHTML]="getDiff(change)"></div>
|
[attr.data-change-id]="change.getChangeId()"
|
||||||
|
[innerHTML]="getDiff(change)"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="motion-text line-numbers-outside">
|
<div class="motion-text line-numbers-outside">
|
||||||
<os-motion-detail-original-change-recommendations
|
<os-motion-detail-original-change-recommendations
|
||||||
[html]="getTextRemainderAfterLastChange()"
|
[html]="getTextRemainderAfterLastChange()"
|
||||||
[changeRecommendations]="[]"
|
[changeRecommendations]="[]"
|
||||||
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
||||||
></os-motion-detail-original-change-recommendations>
|
></os-motion-detail-original-change-recommendations>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-menu #changeRecommendationMenu="matMenu">
|
<mat-menu #changeRecommendationMenu="matMenu">
|
||||||
<ng-template matMenuContent let-change="change">
|
<ng-template matMenuContent let-change="change">
|
||||||
<button type="button" mat-menu-item [disabled]="hasCollissions(change)" (click)="setAcceptanceValue(change, 'accepted')">
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="hasCollissions(change)"
|
||||||
|
(click)="setAcceptanceValue(change, 'accepted')"
|
||||||
|
>
|
||||||
<mat-icon>thumb_up</mat-icon>
|
<mat-icon>thumb_up</mat-icon>
|
||||||
<span translate>Accept</span>
|
<span translate>Accept</span>
|
||||||
<mat-icon *ngIf="change.isAccepted()" class="active-indicator">done</mat-icon>
|
<mat-icon *ngIf="change.isAccepted()" class="active-indicator">done</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" mat-menu-item [disabled]="hasCollissions(change)" (click)="setAcceptanceValue(change, 'rejected')">
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="hasCollissions(change)"
|
||||||
|
(click)="setAcceptanceValue(change, 'rejected')"
|
||||||
|
>
|
||||||
<mat-icon>thumb_down</mat-icon>
|
<mat-icon>thumb_down</mat-icon>
|
||||||
<span translate>Reject</span>
|
<span translate>Reject</span>
|
||||||
<mat-icon *ngIf="change.isRejected()" class="active-indicator">done</mat-icon>
|
<mat-icon *ngIf="change.isRejected()" class="active-indicator">done</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" mat-menu-item (click)="setInternal(change, !change.internal)">
|
<button type="button" mat-menu-item (click)="setInternal(change, !change.internal)">
|
||||||
<mat-icon>{{ change.internal ? "check_box_outline_blank" : "check_box" }}</mat-icon>
|
<mat-icon>{{ change.internal ? 'check_box_outline_blank' : 'check_box' }}</mat-icon>
|
||||||
<span translate>Public</span>
|
<span translate>Public</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" mat-menu-item (click)="deleteChangeRecommendation(change, $event)">
|
<button type="button" mat-menu-item (click)="deleteChangeRecommendation(change, $event)">
|
||||||
|
@ -54,7 +54,6 @@
|
|||||||
border: solid 1px #ddd;
|
border: solid 1px #ddd;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
margin-top: -15px;
|
|
||||||
padding: 5px 5px 0 5px;
|
padding: 5px 5px 0 5px;
|
||||||
|
|
||||||
a,
|
a,
|
||||||
|
@ -84,11 +84,8 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
|||||||
to: change2 ? change2.getLineFrom() : null
|
to: change2 ? change2.getLineFrom() : null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lineRange.from > lineRange.to) {
|
if (lineRange.from >= lineRange.to) {
|
||||||
const msg = 'Inconsistent data.';
|
// Empty space between two amendments, or between colliding amendments
|
||||||
return '<em style="color: red; font-weight: bold;">' + msg + '</em>';
|
|
||||||
}
|
|
||||||
if (lineRange.from === lineRange.to) {
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,11 +94,21 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this change is colliding with another change
|
* Returns true if this change is colliding with another change
|
||||||
* @param change
|
* @param {ViewUnifiedChange} change
|
||||||
|
* @param {ViewUnifiedChange[]} changes
|
||||||
*/
|
*/
|
||||||
public hasCollissions(change: ViewUnifiedChange): boolean {
|
public hasCollissions(change: ViewUnifiedChange, changes: ViewUnifiedChange[]): boolean {
|
||||||
// @TODO Implementation
|
return (
|
||||||
return false;
|
changes.filter((otherChange: ViewUnifiedChange) => {
|
||||||
|
return (
|
||||||
|
(otherChange.getChangeId() === change.getChangeId() &&
|
||||||
|
(otherChange.getLineFrom() >= change.getLineFrom() &&
|
||||||
|
otherChange.getLineFrom() < change.getLineTo())) ||
|
||||||
|
(otherChange.getLineTo() > change.getLineFrom() && otherChange.getLineTo() <= change.getLineTo()) ||
|
||||||
|
(otherChange.getLineFrom() < change.getLineFrom() && otherChange.getLineTo() > change.getLineTo())
|
||||||
|
);
|
||||||
|
}).length > 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
<os-head-bar [mainButton]="opCanEdit()" mainButtonIcon="edit" [nav]="false" [editMode]="editMotion"
|
<os-head-bar
|
||||||
(mainEvent)="setEditMode(!editMotion)" (saveEvent)="saveMotion()">
|
[mainButton]="opCanEdit()"
|
||||||
|
mainButtonIcon="edit"
|
||||||
|
[nav]="false"
|
||||||
|
[editMode]="editMotion"
|
||||||
|
(mainEvent)="setEditMode(!editMotion)"
|
||||||
|
(saveEvent)="saveMotion()"
|
||||||
|
>
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 *ngIf="motion && !newMotion">
|
<h2 *ngIf="motion && !newMotion">
|
||||||
<span translate>Motion</span>
|
<span translate>Motion</span>
|
||||||
<!-- Whitespace between "Motion" and identifier -->
|
<!-- Whitespace between "Motion" and identifier -->
|
||||||
<span> </span>
|
<span> </span> <span *ngIf="!editMotion">{{ motion.identifier }}</span>
|
||||||
<span *ngIf="!editMotion">{{ motion.identifier }}</span>
|
<span *ngIf="editMotion">{{ metaInfoForm.get('identifier').value }}</span>
|
||||||
<span *ngIf="editMotion">{{ metaInfoForm.get("identifier").value }}</span>
|
|
||||||
</h2>
|
|
||||||
<h2 *ngIf="newMotion" translate>
|
|
||||||
New motion
|
|
||||||
</h2>
|
</h2>
|
||||||
|
<h2 *ngIf="newMotion" translate>New motion</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Back and forth buttons-->
|
<!-- Back and forth buttons -->
|
||||||
<div *ngIf="!editMotion" class="extra-controls-slot on-transition-fade">
|
<div *ngIf="!editMotion" class="extra-controls-slot on-transition-fade">
|
||||||
<div *ngIf="previousMotion">
|
<div *ngIf="previousMotion">
|
||||||
<button mat-button (click)="navigateToMotion(previousMotion)">
|
<button mat-button (click)="navigateToMotion(previousMotion)">
|
||||||
@ -56,24 +58,41 @@
|
|||||||
<span translate>Project</span>
|
<span translate>Project</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="createAmendment()"
|
||||||
|
*ngIf="amendmentsEnabled && motion && !motion.isParagraphBasedAmendment()"
|
||||||
|
>
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
<span translate>Create amendment</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="showAmendmentContext = !showAmendmentContext"
|
||||||
|
*ngIf="motion && motion.isParagraphBasedAmendment()"
|
||||||
|
>
|
||||||
|
<mat-icon>{{ !showAmendmentContext ? 'check_box_outline_blank' : 'check_box' }}</mat-icon>
|
||||||
|
<span translate>Show context</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<button mat-menu-item class='red-warning-text' (click)='deleteMotionButton()'>
|
<button mat-menu-item class="red-warning-text" (click)="deleteMotionButton()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div *ngIf="motion" class="motion-title on-transition-fade">
|
<div *ngIf="motion" class="motion-title on-transition-fade">
|
||||||
<h2 *ngIf="!editMotion">{{ motion.title }}</h2>
|
<h2 *ngIf="!editMotion">{{ motion.title }}</h2>
|
||||||
<h2 *ngIf="editMotion">{{ contentForm.get("title").value }}</h2>
|
<h2 *ngIf="editMotion">{{ contentForm.get('title').value }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="vp.isMobile; then mobileView; else desktopView"></ng-container>
|
<ng-container *ngIf="vp.isMobile; then: mobileView; else: desktopView"></ng-container>
|
||||||
|
|
||||||
<ng-template #mobileView>
|
<ng-template #mobileView>
|
||||||
<mat-accordion multi='true' class='on-transition-fade'>
|
<mat-accordion multi='true' class='on-transition-fade'>
|
||||||
@ -94,7 +113,7 @@
|
|||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<mat-expansion-panel #contentPanel [expanded]='true'>
|
<mat-expansion-panel #contentPanel [expanded]="true">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>format_align_left</mat-icon>
|
<mat-icon>format_align_left</mat-icon>
|
||||||
@ -115,7 +134,6 @@
|
|||||||
<ng-template #desktopView>
|
<ng-template #desktopView>
|
||||||
<div class="desktop-view">
|
<div class="desktop-view">
|
||||||
<div class="desktop-left on-transition-fade">
|
<div class="desktop-left on-transition-fade">
|
||||||
|
|
||||||
<!-- Meta Info -->
|
<!-- Meta Info -->
|
||||||
<div class="meta-info-block meta-info-desktop">
|
<div class="meta-info-block meta-info-desktop">
|
||||||
<ng-container *ngTemplateOutlet="metaInfoTemplate"></ng-container>
|
<ng-container *ngTemplateOutlet="metaInfoTemplate"></ng-container>
|
||||||
@ -123,30 +141,30 @@
|
|||||||
|
|
||||||
<os-motion-comments *ngIf="!newMotion" [motion]="motion"></os-motion-comments>
|
<os-motion-comments *ngIf="!newMotion" [motion]="motion"></os-motion-comments>
|
||||||
<os-personal-note *ngIf="!newMotion" [motion]="motion"></os-personal-note>
|
<os-personal-note *ngIf="!newMotion" [motion]="motion"></os-personal-note>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop-right ">
|
<div class="desktop-right ">
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<mat-card>
|
<mat-card> <ng-container *ngTemplateOutlet="contentTemplate"></ng-container> </mat-card>
|
||||||
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #metaInfoTemplate>
|
<ng-template #metaInfoTemplate>
|
||||||
<form [formGroup]='metaInfoForm' (keydown)="onKeyDown($event)" (ngSubmit)='saveMotion()'>
|
<form [formGroup]="metaInfoForm" (keydown)="onKeyDown($event)" (ngSubmit)="saveMotion()">
|
||||||
|
|
||||||
<!-- Identifier -->
|
<!-- Identifier -->
|
||||||
<div *ngIf="editMotion && !newMotion">
|
<div *ngIf="editMotion && !newMotion">
|
||||||
<!-- <div *ngIf="editMotion"> -->
|
<!-- <div *ngIf="editMotion"> -->
|
||||||
<div *ngIf='!editMotion'>
|
<div *ngIf="!editMotion">
|
||||||
<h4 translate>Identifier</h4>
|
<h4 translate>Identifier</h4>
|
||||||
{{ motion.identifier }}
|
{{ motion.identifier }}
|
||||||
</div>
|
</div>
|
||||||
<mat-form-field *ngIf="editMotion">
|
<mat-form-field *ngIf="editMotion">
|
||||||
<input matInput placeholder='{{ "Identifier" | translate }}' formControlName='identifier' [value]='motionCopy.identifier'>
|
<input
|
||||||
|
matInput
|
||||||
|
placeholder="{{ "Identifier" | translate }}"
|
||||||
|
formControlName="identifier"
|
||||||
|
[value]="motionCopy.identifier"
|
||||||
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -154,8 +172,14 @@
|
|||||||
<div *ngIf="motion && motion.submitters || newMotion">
|
<div *ngIf="motion && motion.submitters || newMotion">
|
||||||
<div *ngIf="newMotion">
|
<div *ngIf="newMotion">
|
||||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||||
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('submitters_id')"
|
<os-search-value-selector
|
||||||
[multiple]="true" listname="{{ 'Submitters' | translate }}" [InputListValues]="submitterObserver"></os-search-value-selector>
|
ngDefaultControl
|
||||||
|
[form]="metaInfoForm"
|
||||||
|
[formControl]="metaInfoForm.get('submitters_id')"
|
||||||
|
[multiple]="true"
|
||||||
|
listname="{{ 'Submitters' | translate }}"
|
||||||
|
[InputListValues]="submitterObserver"
|
||||||
|
></os-search-value-selector>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!editMotion && !newMotion">
|
<div *ngIf="!editMotion && !newMotion">
|
||||||
@ -170,8 +194,14 @@
|
|||||||
<div *ngIf='motion && minSupporters'>
|
<div *ngIf='motion && minSupporters'>
|
||||||
<div *ngIf="editMotion">
|
<div *ngIf="editMotion">
|
||||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||||
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('supporters_id')"
|
<os-search-value-selector
|
||||||
[multiple]="true" listname="{{ 'Supporters' | translate }}" [InputListValues]="supporterObserver"></os-search-value-selector>
|
ngDefaultControl
|
||||||
|
[form]="metaInfoForm"
|
||||||
|
[formControl]="metaInfoForm.get('supporters_id')"
|
||||||
|
[multiple]="true"
|
||||||
|
listname="{{ 'Supporters' | translate }}"
|
||||||
|
[InputListValues]="supporterObserver"
|
||||||
|
></os-search-value-selector>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!editMotion">
|
<div *ngIf="!editMotion">
|
||||||
@ -259,54 +289,85 @@
|
|||||||
<!-- Workflow -->
|
<!-- Workflow -->
|
||||||
<div *ngIf="editMotion">
|
<div *ngIf="editMotion">
|
||||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||||
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('workflow_id')"
|
<os-search-value-selector
|
||||||
[multiple]="false" listname="{{ 'Workflow' | translate }}" [InputListValues]="workflowObserver"></os-search-value-selector>
|
ngDefaultControl
|
||||||
|
[form]="metaInfoForm"
|
||||||
|
[formControl]="metaInfoForm.get('workflow_id')"
|
||||||
|
[multiple]="false"
|
||||||
|
listname="{{ 'Workflow' | translate }}"
|
||||||
|
[InputListValues]="workflowObserver"
|
||||||
|
></os-search-value-selector>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Origin -->
|
<!-- Origin -->
|
||||||
<div *ngIf="motion && motion.origin || editMotion">
|
<div *ngIf="(motion && motion.origin) || editMotion">
|
||||||
<div *ngIf='!editMotion'>
|
<div *ngIf="!editMotion">
|
||||||
<h4 translate>Origin</h4>
|
<h4 translate>Origin</h4>
|
||||||
{{ motion.origin }}
|
{{ motion.origin }}
|
||||||
</div>
|
</div>
|
||||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||||
<mat-form-field *ngIf="editMotion">
|
<mat-form-field *ngIf="editMotion">
|
||||||
<input matInput placeholder="{{ 'Origin' | translate}}" formControlName='origin' [value]='motionCopy.origin'>
|
<input
|
||||||
|
matInput
|
||||||
|
placeholder="{{ 'Origin' | translate}}"
|
||||||
|
formControlName="origin"
|
||||||
|
[value]="motionCopy.origin"
|
||||||
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Voting -->
|
<!-- Voting -->
|
||||||
<!-- <div *ngIf='motion.polls && motion.polls.length > 0 || editMotion'>
|
<!--
|
||||||
|
<div *ngIf='motion.polls && motion.polls.length > 0 || editMotion'>
|
||||||
<h4 translate>Voting</h4>
|
<h4 translate>Voting</h4>
|
||||||
</div> -->
|
</div>
|
||||||
|
-->
|
||||||
</form>
|
</form>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #contentTemplate>
|
<ng-template #contentTemplate>
|
||||||
<form class="motion-content" [formGroup]='contentForm' (clickdown)="onKeyDown($event)" (keydown)="onKeyDown($event)" (ngSubmit)='saveMotion()'>
|
<form
|
||||||
|
class="motion-content"
|
||||||
<!-- Line Number and Diff buttons-->
|
[formGroup]="contentForm"
|
||||||
|
(clickdown)="onKeyDown($event)"
|
||||||
|
(keydown)="onKeyDown($event)"
|
||||||
|
(ngSubmit)="saveMotion()"
|
||||||
|
>
|
||||||
|
<!-- Line Number and Diff buttons -->
|
||||||
<div *ngIf="motion && !editMotion && !motion.isStatuteAmendment()" class="motion-text-controls">
|
<div *ngIf="motion && !editMotion && !motion.isStatuteAmendment()" class="motion-text-controls">
|
||||||
<button type="button" mat-icon-button [matMenuTriggerFor]="lineNumberingMenu" matTooltip="{{ 'Line numbering' | translate }}">
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
[matMenuTriggerFor]="lineNumberingMenu"
|
||||||
|
matTooltip="{{ 'Line numbering' | translate }}"
|
||||||
|
>
|
||||||
<mat-icon>format_list_numbered</mat-icon>
|
<mat-icon>format_list_numbered</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="allChangingObjects.length > 0" type="button" mat-icon-button [matMenuTriggerFor]="changeRecoMenu" matTooltip="{{ 'Change recommendations' | translate }}">
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
[matMenuTriggerFor]="changeRecoMenu"
|
||||||
|
matTooltip="{{ 'Change recommendations' | translate }}"
|
||||||
|
*ngIf="motion && !motion.isParagraphBasedAmendment() && allChangingObjects.length > 0"
|
||||||
|
>
|
||||||
<mat-icon>rate_review</mat-icon>
|
<mat-icon>rate_review</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selecting statute paragraphs for amendment -->
|
<!-- Selecting statute paragraphs for amendment -->
|
||||||
<div class="statute-amendment-selector" *ngIf="editMotion && statuteParagraphs.length > 0 && statutesEnabled">
|
<div class="statute-amendment-selector" *ngIf="editMotion && statuteParagraphs.length > 0 && statutesEnabled">
|
||||||
<mat-checkbox formControlName='statute_amendment' translate (change)="onStatuteAmendmentChange($event)">
|
<mat-checkbox formControlName="statute_amendment" translate (change)="onStatuteAmendmentChange($event)">
|
||||||
Statute amendment
|
Statute amendment
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
|
||||||
<mat-form-field *ngIf="contentForm.value.statute_amendment">
|
<mat-form-field *ngIf="contentForm.value.statute_amendment">
|
||||||
<mat-select [placeholder]="'Select paragraph to amend' | translate"
|
<mat-select
|
||||||
formControlName='statute_paragraph_id'
|
[placeholder]="'Select paragraph to amend' | translate"
|
||||||
(valueChange)="onStatuteParagraphChange($event)">
|
formControlName="statute_paragraph_id"
|
||||||
|
(valueChange)="onStatuteParagraphChange($event)"
|
||||||
|
>
|
||||||
<mat-option *ngFor="let paragraph of statuteParagraphs" [value]="paragraph.id">
|
<mat-option *ngFor="let paragraph of statuteParagraphs" [value]="paragraph.id">
|
||||||
{{ paragraph.title }}
|
{{ paragraph.title }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
@ -315,43 +376,75 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div *ngIf="motion && editMotion">
|
<div *ngIf="(motion && motion.title) || editMotion">
|
||||||
<mat-form-field class="wide-form">
|
<div *ngIf="!editMotion">
|
||||||
<input matInput osAutofocus placeholder="{{ 'Title' | translate }}"
|
<h4>{{ motion.title }}</h4>
|
||||||
formControlName='title' [value]='motionCopy.title' required>
|
</div>
|
||||||
|
|
||||||
|
<mat-form-field *ngIf="editMotion" class="wide-form">
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
osAutofocus
|
||||||
|
placeholder="{{ 'Title' | translate }}"
|
||||||
|
formControlName="title"
|
||||||
|
[value]="motionCopy.title"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Text -->
|
<!-- Text -->
|
||||||
<span class="text-prefix-label">{{ preamble | translate }}</span>
|
<span class="text-prefix-label">{{ preamble | translate }}</span>
|
||||||
<ng-container *ngIf='motion && !editMotion && !motion.isStatuteAmendment()'>
|
|
||||||
<div *ngIf="!isRecoModeDiff()" class="motion-text" [class.line-numbers-none]="isLineNumberingNone()"
|
<!-- Regular motions or traditional amendments -->
|
||||||
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()">
|
<ng-container
|
||||||
<os-motion-detail-original-change-recommendations *ngIf="isLineNumberingOutside() && isRecoModeOriginal()"
|
*ngIf="motion && !editMotion && !motion.isStatuteAmendment() && !motion.isParagraphBasedAmendment()"
|
||||||
[html]="getFormattedTextPlain()" [changeRecommendations]="changeRecommendations"
|
>
|
||||||
(createChangeRecommendation)="createChangeRecommendation($event)" (gotoChangeRecommendation)="gotoChangeRecommendation($event)"></os-motion-detail-original-change-recommendations>
|
<div
|
||||||
<div *ngIf="!isLineNumberingOutside() || !isRecoModeOriginal()" [innerHTML]="getFormattedText()"></div>
|
*ngIf="!isRecoModeDiff()"
|
||||||
|
class="motion-text"
|
||||||
|
[class.line-numbers-none]="isLineNumberingNone()"
|
||||||
|
[class.line-numbers-inline]="isLineNumberingInline()"
|
||||||
|
[class.line-numbers-outside]="isLineNumberingOutside()"
|
||||||
|
>
|
||||||
|
<os-motion-detail-original-change-recommendations
|
||||||
|
*ngIf="isLineNumberingOutside() && isRecoModeOriginal()"
|
||||||
|
[html]="getFormattedTextPlain()"
|
||||||
|
[changeRecommendations]="changeRecommendations"
|
||||||
|
(createChangeRecommendation)="createChangeRecommendation($event)"
|
||||||
|
(gotoChangeRecommendation)="gotoChangeRecommendation($event)"
|
||||||
|
></os-motion-detail-original-change-recommendations>
|
||||||
|
<div
|
||||||
|
*ngIf="!isLineNumberingOutside() || !isRecoModeOriginal()"
|
||||||
|
[innerHTML]="sanitizedText(getFormattedTextPlain())"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<os-motion-detail-diff *ngIf="isRecoModeDiff()" [motion]="motion" [changes]="allChangingObjects"
|
<os-motion-detail-diff
|
||||||
[scrollToChange]="scrollToChange" (createChangeRecommendation)="createChangeRecommendation($event)"></os-motion-detail-diff>
|
*ngIf="isRecoModeDiff()"
|
||||||
|
[motion]="motion"
|
||||||
|
[changes]="allChangingObjects"
|
||||||
|
[scrollToChange]="scrollToChange"
|
||||||
|
(createChangeRecommendation)="createChangeRecommendation($event)"
|
||||||
|
></os-motion-detail-diff>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="motion-text line-numbers-none" *ngIf="motion && !editMotion && motion.isStatuteAmendment()"
|
<div
|
||||||
[innerHTML]="getFormattedStatuteAmendment()">
|
class="motion-text line-numbers-none"
|
||||||
</div>
|
*ngIf="motion && !editMotion && motion.isStatuteAmendment()"
|
||||||
|
[innerHTML]="getFormattedStatuteAmendment()"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- The HTML Editor -->
|
<!-- The HTML Editor -->
|
||||||
<editor
|
<editor formControlName="text" [init]="tinyMceSettings" *ngIf="motion && editMotion"></editor>
|
||||||
formControlName='text'
|
|
||||||
[init]="tinyMceSettings"
|
<!-- Paragraph-based amendments -->
|
||||||
*ngIf="motion && editMotion"
|
<ng-container *ngIf="motion && !editMotion && motion.isParagraphBasedAmendment()">
|
||||||
></editor>
|
<ng-container *ngTemplateOutlet="paragraphBasedAmendment"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Reason -->
|
<!-- Reason -->
|
||||||
<div *ngIf="motion || editMotion">
|
<div *ngIf="motion || editMotion">
|
||||||
<h5 *ngIf="motion.reason || editMotion" translate>Reason</h5>
|
<h5 *ngIf="motion.reason || editMotion" translate>Reason</h5>
|
||||||
<div class="motion-text" *ngIf='!editMotion'>
|
<div class="motion-text" *ngIf="!editMotion"><div [innerHtml]="motion.reason"></div></div>
|
||||||
<div [innerHtml]='motion.reason'></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- The HTML Editor -->
|
<!-- The HTML Editor -->
|
||||||
<editor
|
<editor
|
||||||
@ -360,10 +453,50 @@
|
|||||||
*ngIf="editMotion"
|
*ngIf="editMotion"
|
||||||
></editor>
|
></editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #paragraphBasedAmendment>
|
||||||
|
<section class="text-holder">
|
||||||
|
<div class="alert alert-info" *ngIf="this.getAmendedParagraphs().length === 0">
|
||||||
|
<span translate>No changes at the text.</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngFor="let paragraph of this.getAmendedParagraphs()"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div class="amendment-context" *ngIf="showAmendmentContext">
|
||||||
|
<div [innerHTML]="getParentMotionRange(1, paragraph.paragraphLineFrom)" class="context"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3
|
||||||
|
*ngIf="paragraph.diffLineTo === paragraph.diffLineFrom + 1 && !showAmendmentContext"
|
||||||
|
class="amendment-line-header"
|
||||||
|
>
|
||||||
|
<span translate>Line</span> {{ paragraph.diffLineFrom }}:
|
||||||
|
</h3>
|
||||||
|
<h3
|
||||||
|
*ngIf="paragraph.diffLineTo !== paragraph.diffLineFrom + 1 && !showAmendmentContext"
|
||||||
|
class="amendment-line-header"
|
||||||
|
>
|
||||||
|
<span translate>Line</span> {{ paragraph.diffLineFrom }} - {{ paragraph.diffLineTo - 1 }}:
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="paragraph-context" [innerHtml]="sanitizedText(paragraph.textPre)"></div>
|
||||||
|
<div [innerHtml]="sanitizedText(paragraph.text)"></div>
|
||||||
|
<div class="paragraph-context" [innerHtml]="sanitizedText(paragraph.textPost)"></div>
|
||||||
|
|
||||||
|
<div class="amendment-context" *ngIf="showAmendmentContext">
|
||||||
|
<div [innerHtml]="getParentMotionRange(paragraph.paragraphLineTo, null)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<!-- Line number Menu -->
|
<!-- Line number Menu -->
|
||||||
<mat-menu #lineNumberingMenu="matMenu">
|
<mat-menu #lineNumberingMenu="matMenu">
|
||||||
<div *ngIf="motion">
|
<div *ngIf="motion">
|
||||||
@ -375,8 +508,8 @@
|
|||||||
|
|
||||||
<!-- Diff View Menu -->
|
<!-- Diff View Menu -->
|
||||||
<mat-menu #changeRecoMenu="matMenu">
|
<mat-menu #changeRecoMenu="matMenu">
|
||||||
<button mat-menu-item translate (click)=setChangeRecoMode(0)>Original version</button>
|
<button mat-menu-item translate (click)="setChangeRecoMode(0)">Original version</button>
|
||||||
<button mat-menu-item translate (click)=setChangeRecoMode(1)>Changed version</button>
|
<button mat-menu-item translate (click)="setChangeRecoMode(1)">Changed version</button>
|
||||||
<button mat-menu-item translate (click)=setChangeRecoMode(2)>Diff version</button>
|
<button mat-menu-item translate (click)="setChangeRecoMode(2)">Diff version</button>
|
||||||
<button mat-menu-item translate (click)=setChangeRecoMode(3)>Final version</button>
|
<button mat-menu-item translate (click)="setChangeRecoMode(3)">Final version</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -268,4 +268,33 @@ span {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.os-split-before {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-split-after {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.os-split-before {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .amendment-view {
|
||||||
|
.os-split-after {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.os-split-before {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.paragraph-context {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
&.amendment-context .paragraph-context {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { DataStoreService } from '../../../../core/services/data-store.service';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Motion } from '../../../../shared/models/motions/motion';
|
import { Motion } from '../../../../shared/models/motions/motion';
|
||||||
import { BehaviorSubject, Subscription, ReplaySubject, concat } from 'rxjs';
|
import { BehaviorSubject, Subscription, ReplaySubject, concat } from 'rxjs';
|
||||||
import { LineRange } from '../../services/diff.service';
|
import { DiffLinesInParagraph, LineRange } from '../../services/diff.service';
|
||||||
import {
|
import {
|
||||||
MotionChangeRecommendationComponent,
|
MotionChangeRecommendationComponent,
|
||||||
MotionChangeRecommendationComponentData
|
MotionChangeRecommendationComponentData
|
||||||
@ -110,6 +110,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public preamble: string;
|
public preamble: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value of the configuration variable `motions_amendments_enabled` - are amendments enabled?
|
||||||
|
* @TODO replace by direct access to config variable, once it's available from the templates
|
||||||
|
*/
|
||||||
|
public amendmentsEnabled: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy of the motion that the user might edit
|
* Copy of the motion that the user might edit
|
||||||
@ -121,6 +126,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public changeRecommendations: ViewChangeReco[];
|
public changeRecommendations: ViewChangeReco[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All amendments to this motions
|
||||||
|
*/
|
||||||
|
public amendments: ViewMotion[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All change recommendations AND amendments, sorted by line number.
|
* All change recommendations AND amendments, sorted by line number.
|
||||||
*/
|
*/
|
||||||
@ -186,6 +196,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
private recommenderSubscription: Subscription;
|
private recommenderSubscription: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is a paragraph-based amendment, this indicates if the non-affected paragraphs should be shown as well
|
||||||
|
*/
|
||||||
|
public showAmendmentContext = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constuct the detail view.
|
* Constuct the detail view.
|
||||||
*
|
*
|
||||||
@ -255,11 +270,18 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
this.minSupporters = supporters;
|
this.minSupporters = supporters;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.configService.get('motions_preamble').subscribe(
|
this.configService.get('motions_preamble').subscribe(
|
||||||
(preamble: string): void => {
|
(preamble: string): void => {
|
||||||
this.preamble = preamble;
|
this.preamble = preamble;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.configService.get('motions_amendments_enabled').subscribe(
|
||||||
|
(enabled: boolean): void => {
|
||||||
|
this.amendmentsEnabled = enabled;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,8 +289,25 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
* Called each time one of these arrays changes.
|
* Called each time one of these arrays changes.
|
||||||
*/
|
*/
|
||||||
private recalcUnifiedChanges(): void {
|
private recalcUnifiedChanges(): void {
|
||||||
// @TODO implement amendments
|
this.allChangingObjects = [];
|
||||||
this.allChangingObjects = this.changeRecommendations;
|
if (this.changeRecommendations) {
|
||||||
|
this.changeRecommendations.forEach(
|
||||||
|
(change: ViewUnifiedChange): void => {
|
||||||
|
this.allChangingObjects.push(change);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.amendments) {
|
||||||
|
this.amendments.forEach(
|
||||||
|
(amendment: ViewMotion): void => {
|
||||||
|
this.repo.getAmendmentAmendedParagraphs(amendment).forEach(
|
||||||
|
(change: ViewUnifiedChange): void => {
|
||||||
|
this.allChangingObjects.push(change);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
this.allChangingObjects.sort((a: ViewUnifiedChange, b: ViewUnifiedChange) => {
|
this.allChangingObjects.sort((a: ViewUnifiedChange, b: ViewUnifiedChange) => {
|
||||||
if (a.getLineFrom() < b.getLineFrom()) {
|
if (a.getLineFrom() < b.getLineFrom()) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -293,18 +332,23 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
// load existing motion
|
// load existing motion
|
||||||
this.route.params.subscribe(params => {
|
this.route.params.subscribe(params => {
|
||||||
this.repo.getViewModelObservable(params.id).subscribe(newViewMotion => {
|
const motionId: number = parseInt(params.id, 10);
|
||||||
|
this.repo.getViewModelObservable(motionId).subscribe(newViewMotion => {
|
||||||
if (newViewMotion) {
|
if (newViewMotion) {
|
||||||
this.motion = newViewMotion;
|
this.motion = newViewMotion;
|
||||||
this.patchForm(this.motion);
|
this.patchForm(this.motion);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.changeRecoRepo
|
this.repo.amendmentsTo(motionId).subscribe(
|
||||||
.getChangeRecosOfMotionObservable(parseInt(params.id, 10))
|
(amendments: ViewMotion[]): void => {
|
||||||
.subscribe((recos: ViewChangeReco[]) => {
|
this.amendments = amendments;
|
||||||
this.changeRecommendations = recos;
|
|
||||||
this.recalcUnifiedChanges();
|
this.recalcUnifiedChanges();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
this.changeRecoRepo.getChangeRecosOfMotionObservable(motionId).subscribe((recos: ViewChangeReco[]) => {
|
||||||
|
this.changeRecommendations = recos;
|
||||||
|
this.recalcUnifiedChanges();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -323,6 +367,15 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
Object.keys(this.contentForm.controls).forEach(ctrl => {
|
Object.keys(this.contentForm.controls).forEach(ctrl => {
|
||||||
contentPatch[ctrl] = formMotion[ctrl];
|
contentPatch[ctrl] = formMotion[ctrl];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (formMotion.isParagraphBasedAmendment()) {
|
||||||
|
contentPatch.text = formMotion.amendment_paragraphs.find(
|
||||||
|
(para: string): boolean => {
|
||||||
|
return para !== null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const statuteAmendmentFieldName = 'statute_amendment';
|
const statuteAmendmentFieldName = 'statute_amendment';
|
||||||
contentPatch[statuteAmendmentFieldName] = formMotion.isStatuteAmendment();
|
contentPatch[statuteAmendmentFieldName] = formMotion.isStatuteAmendment();
|
||||||
this.contentForm.patchValue(contentPatch);
|
this.contentForm.patchValue(contentPatch);
|
||||||
@ -377,6 +430,18 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
|
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
|
||||||
|
|
||||||
const fromForm = new Motion();
|
const fromForm = new Motion();
|
||||||
|
if (this.motion.isParagraphBasedAmendment()) {
|
||||||
|
fromForm.amendment_paragraphs = this.motion.amendment_paragraphs.map(
|
||||||
|
(para: string): string => {
|
||||||
|
if (para === null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return newMotionValues.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
newMotionValues.text = '';
|
||||||
|
}
|
||||||
fromForm.deserialize(newMotionValues);
|
fromForm.deserialize(newMotionValues);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -409,11 +474,36 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the formatted motion text from the repository, as SafeHTML for [innerHTML]
|
* Called from the template to make a HTML string compatible with [innerHTML]
|
||||||
|
* (otherwise line-number-data-attributes would be stripped out)
|
||||||
|
*
|
||||||
|
* @param {string} text
|
||||||
* @returns {SafeHtml}
|
* @returns {SafeHtml}
|
||||||
*/
|
*/
|
||||||
public getFormattedText(): SafeHtml {
|
public sanitizedText(text: string): SafeHtml {
|
||||||
return this.sanitizer.bypassSecurityTrustHtml(this.getFormattedTextPlain());
|
return this.sanitizer.bypassSecurityTrustHtml(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `this.motion` is an amendment, this returns the list of all changed paragraphs.
|
||||||
|
*
|
||||||
|
* @returns {DiffLinesInParagraph[]}
|
||||||
|
*/
|
||||||
|
public getAmendedParagraphs(): DiffLinesInParagraph[] {
|
||||||
|
return this.repo.getAmendedParagraphs(this.motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `this.motion` is an amendment, this returns a specified line range from the parent motion
|
||||||
|
* (e.g. to show the contect in which this amendment is happening)
|
||||||
|
*
|
||||||
|
* @param {number} from
|
||||||
|
* @param {number} to
|
||||||
|
* @returns {SafeHtml}
|
||||||
|
*/
|
||||||
|
public getParentMotionRange(from: number, to: number): SafeHtml {
|
||||||
|
const str = this.repo.extractMotionLineRange(this.motion.parent_id, { from, to }, true);
|
||||||
|
return this.sanitizer.bypassSecurityTrustHtml(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -515,6 +605,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
this.setChangeRecoMode(ChangeRecoMode.Diff);
|
this.setChangeRecoMode(ChangeRecoMode.Diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Goes to the amendment creation wizard. Executed via click.
|
||||||
|
*/
|
||||||
|
public createAmendment(): void {
|
||||||
|
this.router.navigate(['./create-amendment'], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comes from the head bar
|
* Comes from the head bar
|
||||||
* @param mode
|
* @param mode
|
||||||
@ -539,14 +636,24 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
const configKey = isStatuteAmendment ? 'motions_statute_amendments_workflow' : 'motions_workflow';
|
const configKey = isStatuteAmendment ? 'motions_statute_amendments_workflow' : 'motions_workflow';
|
||||||
// TODO: This should just be a takeWhile(id => !id), but should include the last one where the id is OK.
|
// TODO: This should just be a takeWhile(id => !id), but should include the last one where the id is OK.
|
||||||
// takeWhile will get a inclusive parameter, see https://github.com/ReactiveX/rxjs/pull/4115
|
// takeWhile will get a inclusive parameter, see https://github.com/ReactiveX/rxjs/pull/4115
|
||||||
this.configService.get<string>(configKey).pipe(multicast(
|
this.configService
|
||||||
() => new ReplaySubject(1),
|
.get<string>(configKey)
|
||||||
(ids) => ids.pipe(takeWhile(id => !id), o => concat(o, ids.pipe(take(1))))
|
.pipe(
|
||||||
), skipWhile(id => !id)).subscribe(id => {
|
multicast(
|
||||||
this.metaInfoForm.patchValue({
|
() => new ReplaySubject(1),
|
||||||
workflow_id: parseInt(id, 10),
|
ids =>
|
||||||
|
ids.pipe(
|
||||||
|
takeWhile(id => !id),
|
||||||
|
o => concat(o, ids.pipe(take(1)))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
skipWhile(id => !id)
|
||||||
|
)
|
||||||
|
.subscribe(id => {
|
||||||
|
this.metaInfoForm.patchValue({
|
||||||
|
workflow_id: parseInt(id, 10)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -655,7 +762,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
* Observes the repository for changes in the motion recommender
|
* Observes the repository for changes in the motion recommender
|
||||||
*/
|
*/
|
||||||
public setupRecommender(): void {
|
public setupRecommender(): void {
|
||||||
const configKey = this.motion.isStatuteAmendment() ? 'motions_statute_recommendations_by' : 'motions_recommendations_by';
|
const configKey = this.motion.isStatuteAmendment()
|
||||||
|
? 'motions_statute_recommendations_by'
|
||||||
|
: 'motions_recommendations_by';
|
||||||
if (this.recommenderSubscription) {
|
if (this.recommenderSubscription) {
|
||||||
this.recommenderSubscription.unsubscribe();
|
this.recommenderSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
import { ViewUnifiedChange, ViewUnifiedChangeType } from './view-unified-change';
|
||||||
|
import { ViewMotion } from './view-motion';
|
||||||
|
import { LineRange } from '../services/diff.service';
|
||||||
|
import { MergeAmendment } from '../../../shared/models/motions/workflow-state';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This represents the Unified Diff part of an amendments.
|
||||||
|
*
|
||||||
|
* Hint: As we will probably support multiple affected paragraphs in one amendment in the future,
|
||||||
|
* Amendments <-> ViewMotionAmendedParagraph is potentially a 1:n-relation
|
||||||
|
*/
|
||||||
|
export class ViewMotionAmendedParagraph implements ViewUnifiedChange {
|
||||||
|
public constructor(
|
||||||
|
private amendment: ViewMotion,
|
||||||
|
private paragraphNo: number,
|
||||||
|
private newText: string,
|
||||||
|
private lineRange: LineRange
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public getChangeId(): string {
|
||||||
|
return 'amendment-' + this.amendment.id.toString(10) + '-' + this.paragraphNo.toString(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getChangeType(): ViewUnifiedChangeType {
|
||||||
|
return ViewUnifiedChangeType.TYPE_AMENDMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLineFrom(): number {
|
||||||
|
return this.lineRange.from;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLineTo(): number {
|
||||||
|
return this.lineRange.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getChangeNewText(): string {
|
||||||
|
return this.newText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state and recommendation of this amendment is considered.
|
||||||
|
* The state takes precedence.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isAccepted(): boolean {
|
||||||
|
const mergeState = this.amendment.state
|
||||||
|
? this.amendment.state.merge_amendment_into_final
|
||||||
|
: MergeAmendment.UNDEFINED;
|
||||||
|
switch (mergeState) {
|
||||||
|
case MergeAmendment.YES:
|
||||||
|
return true;
|
||||||
|
case MergeAmendment.NO:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
const mergeRecommendation = this.amendment.recommendation
|
||||||
|
? this.amendment.recommendation.merge_amendment_into_final
|
||||||
|
: MergeAmendment.UNDEFINED;
|
||||||
|
switch (mergeRecommendation) {
|
||||||
|
case MergeAmendment.YES:
|
||||||
|
return true;
|
||||||
|
case MergeAmendment.NO:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isRejected(): boolean {
|
||||||
|
return !this.isAccepted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIdentifier(): string {
|
||||||
|
return this.amendment.identifier;
|
||||||
|
}
|
||||||
|
}
|
@ -205,6 +205,14 @@ export class ViewMotion extends BaseViewModel {
|
|||||||
return this.item ? this.item.speakerAmount : null;
|
return this.item ? this.item.speakerAmount : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get parent_id(): number {
|
||||||
|
return this.motion && this.motion.parent_id ? this.motion.parent_id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get amendment_paragraphs(): string[] {
|
||||||
|
return this.motion && this.motion.amendment_paragraphs ? this.motion.amendment_paragraphs : [];
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
motion?: Motion,
|
motion?: Motion,
|
||||||
category?: Category,
|
category?: Category,
|
||||||
@ -340,6 +348,14 @@ export class ViewMotion extends BaseViewModel {
|
|||||||
return !!this.statute_paragraph_id;
|
return !!this.statute_paragraph_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It's a paragraph-based amendments if only one paragraph is to be changed,
|
||||||
|
* specified by amendment_paragraphs-array
|
||||||
|
*/
|
||||||
|
public isParagraphBasedAmendment(): boolean {
|
||||||
|
return this.amendment_paragraphs.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duplicate this motion into a copy of itself
|
* Duplicate this motion into a copy of itself
|
||||||
*/
|
*/
|
||||||
|
@ -7,6 +7,7 @@ import { MotionCommentSectionListComponent } from './components/motion-comment-s
|
|||||||
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
|
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
|
||||||
import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component';
|
import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component';
|
||||||
import { CallListComponent } from './components/call-list/call-list.component';
|
import { CallListComponent } from './components/call-list/call-list.component';
|
||||||
|
import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: MotionListComponent },
|
{ path: '', component: MotionListComponent },
|
||||||
@ -16,7 +17,8 @@ const routes: Routes = [
|
|||||||
{ path: 'call-list', component: CallListComponent },
|
{ path: 'call-list', component: CallListComponent },
|
||||||
{ path: 'new', component: MotionDetailComponent },
|
{ path: 'new', component: MotionDetailComponent },
|
||||||
{ path: ':id', component: MotionDetailComponent },
|
{ path: ':id', component: MotionDetailComponent },
|
||||||
{ path: ':id/speakers', component: SpeakerListComponent }
|
{ path: ':id/speakers', component: SpeakerListComponent },
|
||||||
|
{ path: ':id/create-amendment', component: AmendmentCreateWizardComponent }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -15,6 +15,7 @@ import { MotionCommentsComponent } from './components/motion-comments/motion-com
|
|||||||
import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-block.component';
|
import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-block.component';
|
||||||
import { PersonalNoteComponent } from './components/personal-note/personal-note.component';
|
import { PersonalNoteComponent } from './components/personal-note/personal-note.component';
|
||||||
import { CallListComponent } from './components/call-list/call-list.component';
|
import { CallListComponent } from './components/call-list/call-list.component';
|
||||||
|
import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
||||||
@ -30,7 +31,8 @@ import { CallListComponent } from './components/call-list/call-list.component';
|
|||||||
MotionCommentsComponent,
|
MotionCommentsComponent,
|
||||||
MetaTextBlockComponent,
|
MetaTextBlockComponent,
|
||||||
PersonalNoteComponent,
|
PersonalNoteComponent,
|
||||||
CallListComponent
|
CallListComponent,
|
||||||
|
AmendmentCreateWizardComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
MotionChangeRecommendationComponent,
|
MotionChangeRecommendationComponent,
|
||||||
|
@ -114,6 +114,44 @@ export interface LineRange {
|
|||||||
to: number;
|
to: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object representing a paragraph with some changed lines
|
||||||
|
*/
|
||||||
|
export interface DiffLinesInParagraph {
|
||||||
|
/**
|
||||||
|
* The paragraph number
|
||||||
|
*/
|
||||||
|
paragraphNo: number;
|
||||||
|
/**
|
||||||
|
* The first line of the paragraph
|
||||||
|
*/
|
||||||
|
paragraphLineFrom: number;
|
||||||
|
/**
|
||||||
|
* The end line number (after the paragraph)
|
||||||
|
*/
|
||||||
|
paragraphLineTo: number;
|
||||||
|
/**
|
||||||
|
* The first line number with changes
|
||||||
|
*/
|
||||||
|
diffLineFrom: number;
|
||||||
|
/**
|
||||||
|
* The line number after the last change
|
||||||
|
*/
|
||||||
|
diffLineTo: number;
|
||||||
|
/**
|
||||||
|
* The HTML of the not-changed lines before the changed ones
|
||||||
|
*/
|
||||||
|
textPre: string;
|
||||||
|
/**
|
||||||
|
* The HTML of the changed lines
|
||||||
|
*/
|
||||||
|
text: string;
|
||||||
|
/**
|
||||||
|
* The HTML of the not-changed lines after the changed ones
|
||||||
|
*/
|
||||||
|
textPost: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functionality regarding diffing, merging and extracting line ranges.
|
* Functionality regarding diffing, merging and extracting line ranges.
|
||||||
*
|
*
|
||||||
@ -1024,10 +1062,11 @@ export class DiffService {
|
|||||||
return tagStr.replace(
|
return tagStr.replace(
|
||||||
/<(\w+)( [^>]*)?>/gi,
|
/<(\w+)( [^>]*)?>/gi,
|
||||||
(whole: string, tag: string, tagArguments: string): string => {
|
(whole: string, tag: string, tagArguments: string): string => {
|
||||||
tagArguments = (tagArguments ? tagArguments : '');
|
tagArguments = tagArguments ? tagArguments : '';
|
||||||
if (tagArguments.match(/class="/gi)) {
|
if (tagArguments.match(/class="/gi)) {
|
||||||
// class="someclass" => class="someclass insert"
|
// class="someclass" => class="someclass insert"
|
||||||
tagArguments = tagArguments.replace(/(class\s*=\s*)(["'])([^\2]*)\2/gi,
|
tagArguments = tagArguments.replace(
|
||||||
|
/(class\s*=\s*)(["'])([^\2]*)\2/gi,
|
||||||
(classWhole: string, attr: string, para: string, content: string): string => {
|
(classWhole: string, attr: string, para: string, content: string): string => {
|
||||||
return attr + para + content + ' ' + className + para;
|
return attr + para + content + ' ' + className + para;
|
||||||
}
|
}
|
||||||
@ -1038,7 +1077,7 @@ export class DiffService {
|
|||||||
return '<' + tag + tagArguments + '>';
|
return '<' + tag + tagArguments + '>';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This fixes a very specific, really weird bug that is tested in the test case "does not a change in a very specific case".
|
* This fixes a very specific, really weird bug that is tested in the test case "does not a change in a very specific case".
|
||||||
@ -1450,6 +1489,18 @@ export class DiffService {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that takes the html-attribute from an extractRangeByLineNumbers()-method and
|
||||||
|
* wraps it with the context.
|
||||||
|
*
|
||||||
|
* @param {ExtractedContent} diff
|
||||||
|
*/
|
||||||
|
public formatDiff(diff: ExtractedContent): string {
|
||||||
|
return (
|
||||||
|
diff.outerContextStart + diff.innerContextStart + diff.html + diff.innerContextEnd + diff.outerContextEnd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method that takes the html-attribute from an extractRangeByLineNumbers()-method,
|
* Convenience method that takes the html-attribute from an extractRangeByLineNumbers()-method,
|
||||||
* wraps it with the context and adds line numbers.
|
* wraps it with the context and adds line numbers.
|
||||||
@ -1459,8 +1510,7 @@ export class DiffService {
|
|||||||
* @param {number} firstLine
|
* @param {number} firstLine
|
||||||
*/
|
*/
|
||||||
public formatDiffWithLineNumbers(diff: ExtractedContent, lineLength: number, firstLine: number): string {
|
public formatDiffWithLineNumbers(diff: ExtractedContent, lineLength: number, firstLine: number): string {
|
||||||
let text =
|
let text = this.formatDiff(diff);
|
||||||
diff.outerContextStart + diff.innerContextStart + diff.html + diff.innerContextEnd + diff.outerContextEnd;
|
|
||||||
text = this.lineNumberingService.insertLineNumbers(text, lineLength, null, null, firstLine);
|
text = this.lineNumberingService.insertLineNumbers(text, lineLength, null, null, firstLine);
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
@ -1921,7 +1971,7 @@ export class DiffService {
|
|||||||
diffUnnormalized = diffUnnormalized.replace(
|
diffUnnormalized = diffUnnormalized.replace(
|
||||||
/<(ins|del)>([\s\S]*?)<\/\1>/gi,
|
/<(ins|del)>([\s\S]*?)<\/\1>/gi,
|
||||||
(whole: string, insDel: string): string => {
|
(whole: string, insDel: string): string => {
|
||||||
const modificationClass = (insDel.toLowerCase() === 'ins' ? 'insert' : 'delete');
|
const modificationClass = insDel.toLowerCase() === 'ins' ? 'insert' : 'delete';
|
||||||
return whole.replace(
|
return whole.replace(
|
||||||
/(<(p|div|blockquote|li)[^>]*>)([\s\S]*?)(<\/\2>)/gi,
|
/(<(p|div|blockquote|li)[^>]*>)([\s\S]*?)(<\/\2>)/gi,
|
||||||
(whole2: string, opening: string, blockTag: string, content: string, closing: string): string => {
|
(whole2: string, opening: string, blockTag: string, content: string, closing: string): string => {
|
||||||
@ -2017,4 +2067,62 @@ export class DiffService {
|
|||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to extract affected lines of a paragraph with the possibility to show the context (lines before
|
||||||
|
* and after) the changed lines and displaying the line numbers.
|
||||||
|
*
|
||||||
|
* @param {number} paragraphNo The paragraph number
|
||||||
|
* @param {string} origText The original text - needs to be line-numbered
|
||||||
|
* @param {string} newText The changed text
|
||||||
|
* @param {number} lineLength the line length
|
||||||
|
* @return {DiffLinesInParagraph|null}
|
||||||
|
*/
|
||||||
|
public getAmendmentParagraphsLinesByMode(
|
||||||
|
paragraphNo: number,
|
||||||
|
origText: string,
|
||||||
|
newText: string,
|
||||||
|
lineLength: number
|
||||||
|
): DiffLinesInParagraph {
|
||||||
|
const paragraph_line_range = this.lineNumberingService.getLineNumberRange(origText),
|
||||||
|
diff = this.diff(origText, newText),
|
||||||
|
affected_lines = this.detectAffectedLineRange(diff);
|
||||||
|
|
||||||
|
if (affected_lines === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textPre = '';
|
||||||
|
let textPost = '';
|
||||||
|
if (affected_lines.from > paragraph_line_range.from) {
|
||||||
|
textPre = this.formatDiffWithLineNumbers(
|
||||||
|
this.extractRangeByLineNumbers(diff, paragraph_line_range.from, affected_lines.from),
|
||||||
|
lineLength,
|
||||||
|
paragraph_line_range.from
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (paragraph_line_range.to > affected_lines.to) {
|
||||||
|
textPost = this.formatDiffWithLineNumbers(
|
||||||
|
this.extractRangeByLineNumbers(diff, affected_lines.to, paragraph_line_range.to),
|
||||||
|
lineLength,
|
||||||
|
affected_lines.to
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const text = this.formatDiffWithLineNumbers(
|
||||||
|
this.extractRangeByLineNumbers(diff, affected_lines.from, affected_lines.to),
|
||||||
|
lineLength,
|
||||||
|
affected_lines.from
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
paragraphNo: paragraphNo,
|
||||||
|
paragraphLineFrom: paragraph_line_range.from,
|
||||||
|
paragraphLineTo: paragraph_line_range.to,
|
||||||
|
diffLineFrom: affected_lines.from,
|
||||||
|
diffLineTo: affected_lines.to,
|
||||||
|
textPre: textPre,
|
||||||
|
text: text,
|
||||||
|
textPost: textPost
|
||||||
|
} as DiffLinesInParagraph;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap, map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataSendService } from '../../../core/services/data-send.service';
|
import { DataSendService } from '../../../core/services/data-send.service';
|
||||||
import { Motion } from '../../../shared/models/motions/motion';
|
import { Motion } from '../../../shared/models/motions/motion';
|
||||||
@ -13,7 +13,7 @@ import { ChangeRecoMode, ViewMotion } from '../models/view-motion';
|
|||||||
import { BaseRepository } from '../../base/base-repository';
|
import { BaseRepository } from '../../base/base-repository';
|
||||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||||
import { LinenumberingService } from './linenumbering.service';
|
import { LinenumberingService } from './linenumbering.service';
|
||||||
import { DiffService, LineRange, ModificationType } from './diff.service';
|
import { DiffLinesInParagraph, DiffService, LineRange, ModificationType } from './diff.service';
|
||||||
import { ViewChangeReco } from '../models/view-change-reco';
|
import { ViewChangeReco } from '../models/view-change-reco';
|
||||||
import { MotionChangeReco } from '../../../shared/models/motions/motion-change-reco';
|
import { MotionChangeReco } from '../../../shared/models/motions/motion-change-reco';
|
||||||
import { ViewUnifiedChange } from '../models/view-unified-change';
|
import { ViewUnifiedChange } from '../models/view-unified-change';
|
||||||
@ -24,6 +24,7 @@ import { HttpService } from 'app/core/services/http.service';
|
|||||||
import { Item } from 'app/shared/models/agenda/item';
|
import { Item } from 'app/shared/models/agenda/item';
|
||||||
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||||
import { TreeService } from 'app/core/services/tree.service';
|
import { TreeService } from 'app/core/services/tree.service';
|
||||||
|
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository Services for motions (and potentially categories)
|
* Repository Services for motions (and potentially categories)
|
||||||
@ -207,6 +208,25 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
|||||||
await this.httpService.delete(url);
|
await this.httpService.delete(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns an observable returning the amendments to a given motion
|
||||||
|
*
|
||||||
|
* @param {number} motionId
|
||||||
|
* @returns {Observable<ViewMotion[]>}
|
||||||
|
*/
|
||||||
|
public amendmentsTo(motionId: number): Observable<ViewMotion[]> {
|
||||||
|
return this.getViewModelListObservable().pipe(
|
||||||
|
map(
|
||||||
|
(motions: ViewMotion[]): ViewMotion[] => {
|
||||||
|
return motions.filter(
|
||||||
|
(motion: ViewMotion): boolean => {
|
||||||
|
return motion.parent_id === motionId;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the motion text using the line numbering and change
|
* Format the motion text using the line numbering and change
|
||||||
* reco algorithm.
|
* reco algorithm.
|
||||||
@ -311,6 +331,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
|||||||
* @param {ViewMotion} motion
|
* @param {ViewMotion} motion
|
||||||
* @param {ViewUnifiedChange[]} changes
|
* @param {ViewUnifiedChange[]} changes
|
||||||
* @param {number} highlight
|
* @param {number} highlight
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
public getTextRemainderAfterLastChange(
|
public getTextRemainderAfterLastChange(
|
||||||
motion: ViewMotion,
|
motion: ViewMotion,
|
||||||
@ -381,6 +402,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
|||||||
* @param {ViewMotion} motion
|
* @param {ViewMotion} motion
|
||||||
* @param {ViewUnifiedChange} change
|
* @param {ViewUnifiedChange} change
|
||||||
* @param {number} highlight
|
* @param {number} highlight
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
public getChangeDiff(motion: ViewMotion, change: ViewUnifiedChange, highlight?: number): string {
|
public getChangeDiff(motion: ViewMotion, change: ViewUnifiedChange, highlight?: number): string {
|
||||||
const lineLength = motion.lineLength,
|
const lineLength = motion.lineLength,
|
||||||
@ -427,4 +449,106 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
|||||||
|
|
||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an amendment, this returns the motion affected by this amendments
|
||||||
|
*
|
||||||
|
* @param {ViewMotion} amendment
|
||||||
|
* @returns {ViewMotion}
|
||||||
|
*/
|
||||||
|
public getAmendmentBaseMotion(amendment: ViewMotion): ViewMotion {
|
||||||
|
return this.getViewModel(amendment.parent_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a motion into paragraphs, optionally adding line numbers
|
||||||
|
*
|
||||||
|
* @param {ViewMotion} motion
|
||||||
|
* @param {boolean} lineBreaks
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public getTextParagraphs(motion: ViewMotion, lineBreaks: boolean): string[] {
|
||||||
|
if (!motion) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let html = motion.text;
|
||||||
|
if (lineBreaks) {
|
||||||
|
const lineLength = motion.lineLength;
|
||||||
|
html = this.lineNumbering.insertLineNumbers(html, lineLength);
|
||||||
|
}
|
||||||
|
return this.lineNumbering.splitToParagraphs(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all paragraphs that are affected by the given amendment in diff-format
|
||||||
|
*
|
||||||
|
* @param {ViewMotion} amendment
|
||||||
|
* @returns {DiffLinesInParagraph}
|
||||||
|
*/
|
||||||
|
public getAmendedParagraphs(amendment: ViewMotion): DiffLinesInParagraph[] {
|
||||||
|
const motion = this.getAmendmentBaseMotion(amendment);
|
||||||
|
const baseParagraphs = this.getTextParagraphs(motion, true);
|
||||||
|
const lineLength = amendment.lineLength;
|
||||||
|
|
||||||
|
return amendment.amendment_paragraphs
|
||||||
|
.map(
|
||||||
|
(newText: string, paraNo: number): DiffLinesInParagraph => {
|
||||||
|
if (newText === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Hint: can be either DiffLinesInParagraph or null, if no changes are made
|
||||||
|
return this.diff.getAmendmentParagraphsLinesByMode(
|
||||||
|
paraNo,
|
||||||
|
baseParagraphs[paraNo],
|
||||||
|
newText,
|
||||||
|
lineLength
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.filter((para: DiffLinesInParagraph) => para !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all paragraphs that are affected by the given amendment as unified change objects.
|
||||||
|
*
|
||||||
|
* @param {ViewMotion} amendment
|
||||||
|
* @returns {ViewMotionAmendedParagraph[]}
|
||||||
|
*/
|
||||||
|
public getAmendmentAmendedParagraphs(amendment: ViewMotion): ViewMotionAmendedParagraph[] {
|
||||||
|
const motion = this.getAmendmentBaseMotion(amendment);
|
||||||
|
const baseParagraphs = this.getTextParagraphs(motion, true);
|
||||||
|
const lineLength = amendment.lineLength;
|
||||||
|
|
||||||
|
return amendment.amendment_paragraphs
|
||||||
|
.map(
|
||||||
|
(newText: string, paraNo: number): ViewMotionAmendedParagraph => {
|
||||||
|
if (newText === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origText = baseParagraphs[paraNo],
|
||||||
|
paragraphLines = this.lineNumbering.getLineNumberRange(origText),
|
||||||
|
diff = this.diff.diff(origText, newText),
|
||||||
|
affectedLines = this.diff.detectAffectedLineRange(diff);
|
||||||
|
|
||||||
|
if (affectedLines === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newTextLines = this.lineNumbering.insertLineNumbers(
|
||||||
|
newText,
|
||||||
|
lineLength,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
paragraphLines.from
|
||||||
|
);
|
||||||
|
newTextLines = this.diff.formatDiff(
|
||||||
|
this.diff.extractRangeByLineNumbers(newTextLines, affectedLines.from, affectedLines.to)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ViewMotionAmendedParagraph(amendment, paraNo, newTextLines, affectedLines);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.filter((para: ViewMotionAmendedParagraph) => para !== null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.1.2 on 2018-10-29 13:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('motions', '0015_metadata_permission'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='state',
|
||||||
|
name='merge_amendment_into_final',
|
||||||
|
field=models.SmallIntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
@ -1099,6 +1099,20 @@ class State(RESTModelMixin, models.Model):
|
|||||||
state name and the entered value of this input field.
|
state name and the entered value of this input field.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
merge_amendment_into_final = models.SmallIntegerField(default=0)
|
||||||
|
"""
|
||||||
|
Relevant for amendments:
|
||||||
|
1: Amendments of this statue or recommendation will be merged into the
|
||||||
|
final version of the motion.
|
||||||
|
0: Undefined.
|
||||||
|
-1: Amendments of this status or recommendation will not be merged into the
|
||||||
|
final version of the motion.
|
||||||
|
|
||||||
|
(Hint: The status field takes precedence. That means, if status is 1 or -1,
|
||||||
|
this is the final decision. The recommendation only is considered if the
|
||||||
|
status is 0)
|
||||||
|
"""
|
||||||
|
|
||||||
show_recommendation_extension_field = models.BooleanField(default=False)
|
show_recommendation_extension_field = models.BooleanField(default=False)
|
||||||
"""
|
"""
|
||||||
If true, an additional input field (from motion comment) is visible
|
If true, an additional input field (from motion comment) is visible
|
||||||
|
@ -103,6 +103,7 @@ class StateSerializer(ModelSerializer):
|
|||||||
'allow_submitter_edit',
|
'allow_submitter_edit',
|
||||||
'dont_set_identifier',
|
'dont_set_identifier',
|
||||||
'show_state_extension_field',
|
'show_state_extension_field',
|
||||||
|
'merge_amendment_into_final',
|
||||||
'show_recommendation_extension_field',
|
'show_recommendation_extension_field',
|
||||||
'next_states',
|
'next_states',
|
||||||
'workflow')
|
'workflow')
|
||||||
|
@ -24,7 +24,8 @@ def create_builtin_workflows(sender, **kwargs):
|
|||||||
workflow=workflow_1,
|
workflow=workflow_1,
|
||||||
action_word='Accept',
|
action_word='Accept',
|
||||||
recommendation_label='Acceptance',
|
recommendation_label='Acceptance',
|
||||||
css_class='success')
|
css_class='success',
|
||||||
|
merge_amendment_into_final=True)
|
||||||
state_1_3 = State.objects.create(name=ugettext_noop('rejected'),
|
state_1_3 = State.objects.create(name=ugettext_noop('rejected'),
|
||||||
workflow=workflow_1,
|
workflow=workflow_1,
|
||||||
action_word='Reject',
|
action_word='Reject',
|
||||||
@ -55,7 +56,8 @@ def create_builtin_workflows(sender, **kwargs):
|
|||||||
workflow=workflow_2,
|
workflow=workflow_2,
|
||||||
action_word='Accept',
|
action_word='Accept',
|
||||||
recommendation_label='Acceptance',
|
recommendation_label='Acceptance',
|
||||||
css_class='success')
|
css_class='success',
|
||||||
|
merge_amendment_into_final=True)
|
||||||
state_2_4 = State.objects.create(name=ugettext_noop('rejected'),
|
state_2_4 = State.objects.create(name=ugettext_noop('rejected'),
|
||||||
workflow=workflow_2,
|
workflow=workflow_2,
|
||||||
action_word='Reject',
|
action_word='Reject',
|
||||||
|
Loading…
Reference in New Issue
Block a user