Merge pull request #3975 from CatoTH/OpenSlides-3-Amendments
Amendments
This commit is contained in:
commit
ad1fcfdb00
@ -1,6 +1,15 @@
|
||||
import { Deserializer } from '../base/deserializer';
|
||||
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
|
||||
*
|
||||
@ -18,7 +27,8 @@ export class WorkflowState extends Deserializer {
|
||||
public allow_create_poll: boolean;
|
||||
public allow_submitter_edit: 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 next_states_id: number[];
|
||||
public workflow_id: number;
|
||||
|
@ -23,7 +23,8 @@ import {
|
||||
DateAdapter,
|
||||
MatIconModule,
|
||||
MatButtonToggleModule,
|
||||
MatBadgeModule
|
||||
MatBadgeModule,
|
||||
MatStepperModule
|
||||
} from '@angular/material';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material';
|
||||
@ -110,6 +111,7 @@ import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.com
|
||||
MatIconModule,
|
||||
MatRadioModule,
|
||||
MatButtonToggleModule,
|
||||
MatStepperModule,
|
||||
DragDropModule,
|
||||
TranslateModule.forChild(),
|
||||
RouterModule,
|
||||
@ -147,6 +149,7 @@ import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.com
|
||||
MatIconModule,
|
||||
MatRadioModule,
|
||||
MatButtonToggleModule,
|
||||
MatStepperModule,
|
||||
DragDropModule,
|
||||
NgxMatSelectSearchModule,
|
||||
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 -->
|
||||
<section class="change-recommendation-overview">
|
||||
<strong>
|
||||
{{ 'Summary of changes' | translate }}:
|
||||
</strong>
|
||||
<strong> {{ 'Summary of changes' | translate }}: </strong>
|
||||
|
||||
<!--
|
||||
<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 }}"
|
||||
ng-click="viewChangeRecommendations.rejectAllChangeRecommendations(motion)">
|
||||
<i class="fa fa-thumbs-down"></i>
|
||||
<translate>Reject all change recommendations</translate>
|
||||
</button>
|
||||
<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 }}"
|
||||
ng-click="viewChangeRecommendations.rejectAllChangeRecommendations(motion)">
|
||||
<i class="fa fa-thumbs-down"></i>
|
||||
<translate>Reject all change recommendations</translate>
|
||||
</button>
|
||||
-->
|
||||
|
||||
<ul *ngIf="changes.length > 0">
|
||||
<li *ngFor="let change of changes">
|
||||
<a href='' (click)="scrollToChangeClicked(change, $event)"
|
||||
[class.amendment]="isAmendment(change)"
|
||||
[class.recommendation]="isChangeRecommendation(change)">
|
||||
<span *ngIf="change.getLineFrom() >= change.getLineTo() - 1" class="line-number">
|
||||
{{ 'Line' | translate }} {{ change.getLineFrom() }}<span *ngIf="isChangeRecommendation(change)"></span>
|
||||
</span>
|
||||
<span *ngIf="change.getLineFrom() < change.getLineTo() - 1" class="line-number">
|
||||
{{ '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 }
|
||||
<a
|
||||
href=""
|
||||
(click)="scrollToChangeClicked(change, $event)"
|
||||
[class.amendment]="isAmendment(change)"
|
||||
[class.recommendation]="isChangeRecommendation(change)"
|
||||
>
|
||||
<span *ngIf="change.getLineFrom() >= change.getLineTo() - 1" class="line-number">
|
||||
{{ 'Line' | translate }} {{ change.getLineFrom() }}
|
||||
</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>
|
||||
<span *ngIf="change.getLineFrom() < change.getLineTo() - 1" class="line-number">
|
||||
{{ 'Line' | translate }} {{ change.getLineFrom() }} - {{ change.getLineTo() - 1 }}
|
||||
</span>
|
||||
<span *ngIf="isChangeRecommendation(change)"> ({{ 'Change recommendation' | translate }})</span>
|
||||
<span *ngIf="isAmendment(change)"> ({{ 'Amendment' | translate }} {{ change.getIdentifier() }})</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 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>
|
||||
</ul>
|
||||
|
||||
<div *ngIf="changes.length === 0" class="no-changes">
|
||||
{{ 'No change recommendations yet' | translate }}
|
||||
</div>
|
||||
<div *ngIf="changes.length === 0" class="no-changes">{{ 'No change recommendations yet' | translate }}</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- The actual diff view -->
|
||||
<div class="motion-text-with-diffs">
|
||||
<div *ngFor="let change of changes; let i = index">
|
||||
<div class="motion-text line-numbers-outside">
|
||||
<os-motion-detail-original-change-recommendations
|
||||
[html]="getTextBetweenChanges(changes[i - 1], change)"
|
||||
[changeRecommendations]="[]"
|
||||
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
||||
[html]="getTextBetweenChanges(changes[i - 1], change)"
|
||||
[changeRecommendations]="[]"
|
||||
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
||||
></os-motion-detail-original-change-recommendations>
|
||||
</div>
|
||||
|
||||
<div [class.collides]="hasCollissions(change)"
|
||||
class="diff-box diff-box-{{ change.getChangeId() }} clearfix">
|
||||
<div class="collission-hint" *ngIf="hasCollissions(change)">
|
||||
<div
|
||||
class="diff-box diff-box-{{ change.getChangeId() }} clearfix"
|
||||
[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>
|
||||
</div>
|
||||
<div class="action-row" *osPerms="'motions.can_manage'">
|
||||
<button mat-icon-button *ngIf="isRecommendation(change)" type="button"
|
||||
[matMenuTriggerFor]="changeRecommendationMenu" [matMenuTriggerData]="{change: change}" >
|
||||
<button
|
||||
mat-icon-button
|
||||
*ngIf="isRecommendation(change)"
|
||||
type="button"
|
||||
[matMenuTriggerFor]="changeRecommendationMenu"
|
||||
[matMenuTriggerData]="{ change: change }"
|
||||
>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
@ -78,41 +83,53 @@
|
||||
<i class="fa fa-info"></i>
|
||||
{{ change.original.identifier }}
|
||||
</a>
|
||||
-->
|
||||
-->
|
||||
</div>
|
||||
<div class="status-row" *ngIf="change.isRejected()">
|
||||
<i class="grey">{{ 'Rejected' | translate }}:</i>
|
||||
<i class="grey">{{ 'Rejected' | translate }}</i>
|
||||
</div>
|
||||
|
||||
<div class="motion-text motion-text-diff line-numbers-outside"
|
||||
[attr.data-change-id]="change.getChangeId()"
|
||||
[innerHTML]="getDiff(change)"></div>
|
||||
<div
|
||||
class="motion-text motion-text-diff line-numbers-outside"
|
||||
[attr.data-change-id]="change.getChangeId()"
|
||||
[innerHTML]="getDiff(change)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="motion-text line-numbers-outside">
|
||||
<os-motion-detail-original-change-recommendations
|
||||
[html]="getTextRemainderAfterLastChange()"
|
||||
[changeRecommendations]="[]"
|
||||
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
||||
[html]="getTextRemainderAfterLastChange()"
|
||||
[changeRecommendations]="[]"
|
||||
(createChangeRecommendation)="onCreateChangeRecommendation($event)"
|
||||
></os-motion-detail-original-change-recommendations>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-menu #changeRecommendationMenu="matMenu">
|
||||
<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>
|
||||
<span translate>Accept</span>
|
||||
<mat-icon *ngIf="change.isAccepted()" class="active-indicator">done</mat-icon>
|
||||
</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>
|
||||
<span translate>Reject</span>
|
||||
<mat-icon *ngIf="change.isRejected()" class="active-indicator">done</mat-icon>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
<button type="button" mat-menu-item (click)="deleteChangeRecommendation(change, $event)">
|
||||
|
@ -54,7 +54,6 @@
|
||||
border: solid 1px #ddd;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: -15px;
|
||||
padding: 5px 5px 0 5px;
|
||||
|
||||
a,
|
||||
|
@ -84,11 +84,8 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
to: change2 ? change2.getLineFrom() : null
|
||||
};
|
||||
|
||||
if (lineRange.from > lineRange.to) {
|
||||
const msg = 'Inconsistent data.';
|
||||
return '<em style="color: red; font-weight: bold;">' + msg + '</em>';
|
||||
}
|
||||
if (lineRange.from === lineRange.to) {
|
||||
if (lineRange.from >= lineRange.to) {
|
||||
// Empty space between two amendments, or between colliding amendments
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -97,11 +94,21 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
|
||||
/**
|
||||
* Returns true if this change is colliding with another change
|
||||
* @param change
|
||||
* @param {ViewUnifiedChange} change
|
||||
* @param {ViewUnifiedChange[]} changes
|
||||
*/
|
||||
public hasCollissions(change: ViewUnifiedChange): boolean {
|
||||
// @TODO Implementation
|
||||
return false;
|
||||
public hasCollissions(change: ViewUnifiedChange, changes: ViewUnifiedChange[]): boolean {
|
||||
return (
|
||||
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"
|
||||
(mainEvent)="setEditMode(!editMotion)" (saveEvent)="saveMotion()">
|
||||
|
||||
<os-head-bar
|
||||
[mainButton]="opCanEdit()"
|
||||
mainButtonIcon="edit"
|
||||
[nav]="false"
|
||||
[editMode]="editMotion"
|
||||
(mainEvent)="setEditMode(!editMotion)"
|
||||
(saveEvent)="saveMotion()"
|
||||
>
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 *ngIf="motion && !newMotion">
|
||||
<span translate>Motion</span>
|
||||
<!-- Whitespace between "Motion" and identifier -->
|
||||
<span> </span>
|
||||
<span *ngIf="!editMotion">{{ motion.identifier }}</span>
|
||||
<span *ngIf="editMotion">{{ metaInfoForm.get("identifier").value }}</span>
|
||||
</h2>
|
||||
<h2 *ngIf="newMotion" translate>
|
||||
New motion
|
||||
<span> </span> <span *ngIf="!editMotion">{{ motion.identifier }}</span>
|
||||
<span *ngIf="editMotion">{{ metaInfoForm.get('identifier').value }}</span>
|
||||
</h2>
|
||||
<h2 *ngIf="newMotion" translate>New motion</h2>
|
||||
</div>
|
||||
|
||||
<!-- Back and forth buttons-->
|
||||
<!-- Back and forth buttons -->
|
||||
<div *ngIf="!editMotion" class="extra-controls-slot on-transition-fade">
|
||||
<div *ngIf="previousMotion">
|
||||
<button mat-button (click)="navigateToMotion(previousMotion)">
|
||||
@ -56,24 +58,41 @@
|
||||
<span translate>Project</span>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</mat-menu>
|
||||
</os-head-bar>
|
||||
|
||||
<!-- Title -->
|
||||
<div *ngIf="motion" class="motion-title on-transition-fade">
|
||||
<h2 *ngIf="!editMotion">{{ motion.title }}</h2>
|
||||
<h2 *ngIf="editMotion">{{ contentForm.get("title").value }}</h2>
|
||||
<h2 *ngIf="editMotion">{{ contentForm.get('title').value }}</h2>
|
||||
</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>
|
||||
<mat-accordion multi='true' class='on-transition-fade'>
|
||||
@ -94,7 +113,7 @@
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Content -->
|
||||
<mat-expansion-panel #contentPanel [expanded]='true'>
|
||||
<mat-expansion-panel #contentPanel [expanded]="true">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>format_align_left</mat-icon>
|
||||
@ -115,7 +134,6 @@
|
||||
<ng-template #desktopView>
|
||||
<div class="desktop-view">
|
||||
<div class="desktop-left on-transition-fade">
|
||||
|
||||
<!-- Meta Info -->
|
||||
<div class="meta-info-block meta-info-desktop">
|
||||
<ng-container *ngTemplateOutlet="metaInfoTemplate"></ng-container>
|
||||
@ -123,30 +141,30 @@
|
||||
|
||||
<os-motion-comments *ngIf="!newMotion" [motion]="motion"></os-motion-comments>
|
||||
<os-personal-note *ngIf="!newMotion" [motion]="motion"></os-personal-note>
|
||||
|
||||
</div>
|
||||
<div class="desktop-right ">
|
||||
|
||||
<!-- Content -->
|
||||
<mat-card>
|
||||
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
|
||||
</mat-card>
|
||||
<mat-card> <ng-container *ngTemplateOutlet="contentTemplate"></ng-container> </mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #metaInfoTemplate>
|
||||
<form [formGroup]='metaInfoForm' (keydown)="onKeyDown($event)" (ngSubmit)='saveMotion()'>
|
||||
|
||||
<form [formGroup]="metaInfoForm" (keydown)="onKeyDown($event)" (ngSubmit)="saveMotion()">
|
||||
<!-- Identifier -->
|
||||
<div *ngIf="editMotion && !newMotion">
|
||||
<!-- <div *ngIf="editMotion"> -->
|
||||
<div *ngIf='!editMotion'>
|
||||
<div *ngIf="!editMotion">
|
||||
<h4 translate>Identifier</h4>
|
||||
{{ motion.identifier }}
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -154,8 +172,14 @@
|
||||
<div *ngIf="motion && motion.submitters || newMotion">
|
||||
<div *ngIf="newMotion">
|
||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('submitters_id')"
|
||||
[multiple]="true" listname="{{ 'Submitters' | translate }}" [InputListValues]="submitterObserver"></os-search-value-selector>
|
||||
<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 *ngIf="!editMotion && !newMotion">
|
||||
@ -170,8 +194,14 @@
|
||||
<div *ngIf='motion && minSupporters'>
|
||||
<div *ngIf="editMotion">
|
||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('supporters_id')"
|
||||
[multiple]="true" listname="{{ 'Supporters' | translate }}" [InputListValues]="supporterObserver"></os-search-value-selector>
|
||||
<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 *ngIf="!editMotion">
|
||||
@ -259,54 +289,85 @@
|
||||
<!-- Workflow -->
|
||||
<div *ngIf="editMotion">
|
||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('workflow_id')"
|
||||
[multiple]="false" listname="{{ 'Workflow' | translate }}" [InputListValues]="workflowObserver"></os-search-value-selector>
|
||||
<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>
|
||||
|
||||
<!-- Origin -->
|
||||
<div *ngIf="motion && motion.origin || editMotion">
|
||||
<div *ngIf='!editMotion'>
|
||||
<div *ngIf="(motion && motion.origin) || editMotion">
|
||||
<div *ngIf="!editMotion">
|
||||
<h4 translate>Origin</h4>
|
||||
{{ motion.origin }}
|
||||
</div>
|
||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voting -->
|
||||
<!-- <div *ngIf='motion.polls && motion.polls.length > 0 || editMotion'>
|
||||
<!--
|
||||
<div *ngIf='motion.polls && motion.polls.length > 0 || editMotion'>
|
||||
<h4 translate>Voting</h4>
|
||||
</div> -->
|
||||
</div>
|
||||
-->
|
||||
</form>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #contentTemplate>
|
||||
<form class="motion-content" [formGroup]='contentForm' (clickdown)="onKeyDown($event)" (keydown)="onKeyDown($event)" (ngSubmit)='saveMotion()'>
|
||||
|
||||
<!-- Line Number and Diff buttons-->
|
||||
<form
|
||||
class="motion-content"
|
||||
[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">
|
||||
<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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Selecting statute paragraphs for amendment -->
|
||||
<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
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-form-field *ngIf="contentForm.value.statute_amendment">
|
||||
<mat-select [placeholder]="'Select paragraph to amend' | translate"
|
||||
formControlName='statute_paragraph_id'
|
||||
(valueChange)="onStatuteParagraphChange($event)">
|
||||
<mat-select
|
||||
[placeholder]="'Select paragraph to amend' | translate"
|
||||
formControlName="statute_paragraph_id"
|
||||
(valueChange)="onStatuteParagraphChange($event)"
|
||||
>
|
||||
<mat-option *ngFor="let paragraph of statuteParagraphs" [value]="paragraph.id">
|
||||
{{ paragraph.title }}
|
||||
</mat-option>
|
||||
@ -315,43 +376,75 @@
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div *ngIf="motion && editMotion">
|
||||
<mat-form-field class="wide-form">
|
||||
<input matInput osAutofocus placeholder="{{ 'Title' | translate }}"
|
||||
formControlName='title' [value]='motionCopy.title' required>
|
||||
<div *ngIf="(motion && motion.title) || editMotion">
|
||||
<div *ngIf="!editMotion">
|
||||
<h4>{{ motion.title }}</h4>
|
||||
</div>
|
||||
|
||||
<mat-form-field *ngIf="editMotion" class="wide-form">
|
||||
<input
|
||||
matInput
|
||||
osAutofocus
|
||||
placeholder="{{ 'Title' | translate }}"
|
||||
formControlName="title"
|
||||
[value]="motionCopy.title"
|
||||
required
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Text -->
|
||||
<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()"
|
||||
[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]="getFormattedText()"></div>
|
||||
|
||||
<!-- Regular motions or traditional amendments -->
|
||||
<ng-container
|
||||
*ngIf="motion && !editMotion && !motion.isStatuteAmendment() && !motion.isParagraphBasedAmendment()"
|
||||
>
|
||||
<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>
|
||||
<os-motion-detail-diff *ngIf="isRecoModeDiff()" [motion]="motion" [changes]="allChangingObjects"
|
||||
[scrollToChange]="scrollToChange" (createChangeRecommendation)="createChangeRecommendation($event)"></os-motion-detail-diff>
|
||||
<os-motion-detail-diff
|
||||
*ngIf="isRecoModeDiff()"
|
||||
[motion]="motion"
|
||||
[changes]="allChangingObjects"
|
||||
[scrollToChange]="scrollToChange"
|
||||
(createChangeRecommendation)="createChangeRecommendation($event)"
|
||||
></os-motion-detail-diff>
|
||||
</ng-container>
|
||||
<div class="motion-text line-numbers-none" *ngIf="motion && !editMotion && motion.isStatuteAmendment()"
|
||||
[innerHTML]="getFormattedStatuteAmendment()">
|
||||
</div>
|
||||
<div
|
||||
class="motion-text line-numbers-none"
|
||||
*ngIf="motion && !editMotion && motion.isStatuteAmendment()"
|
||||
[innerHTML]="getFormattedStatuteAmendment()"
|
||||
></div>
|
||||
|
||||
<!-- The HTML Editor -->
|
||||
<editor
|
||||
formControlName='text'
|
||||
[init]="tinyMceSettings"
|
||||
*ngIf="motion && editMotion"
|
||||
></editor>
|
||||
<editor formControlName="text" [init]="tinyMceSettings" *ngIf="motion && editMotion"></editor>
|
||||
|
||||
<!-- Paragraph-based amendments -->
|
||||
<ng-container *ngIf="motion && !editMotion && motion.isParagraphBasedAmendment()">
|
||||
<ng-container *ngTemplateOutlet="paragraphBasedAmendment"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Reason -->
|
||||
<div *ngIf="motion || editMotion">
|
||||
<h5 *ngIf="motion.reason || editMotion" translate>Reason</h5>
|
||||
<div class="motion-text" *ngIf='!editMotion'>
|
||||
<div [innerHtml]='motion.reason'></div>
|
||||
</div>
|
||||
<div class="motion-text" *ngIf="!editMotion"><div [innerHtml]="motion.reason"></div></div>
|
||||
|
||||
<!-- The HTML Editor -->
|
||||
<editor
|
||||
@ -360,10 +453,50 @@
|
||||
*ngIf="editMotion"
|
||||
></editor>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</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 -->
|
||||
<mat-menu #lineNumberingMenu="matMenu">
|
||||
<div *ngIf="motion">
|
||||
@ -375,8 +508,8 @@
|
||||
|
||||
<!-- Diff View Menu -->
|
||||
<mat-menu #changeRecoMenu="matMenu">
|
||||
<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(2)>Diff version</button>
|
||||
<button mat-menu-item translate (click)=setChangeRecoMode(3)>Final 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(2)">Diff version</button>
|
||||
<button mat-menu-item translate (click)="setChangeRecoMode(3)">Final version</button>
|
||||
</mat-menu>
|
||||
|
@ -268,4 +268,33 @@ span {
|
||||
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 { Motion } from '../../../../shared/models/motions/motion';
|
||||
import { BehaviorSubject, Subscription, ReplaySubject, concat } from 'rxjs';
|
||||
import { LineRange } from '../../services/diff.service';
|
||||
import { DiffLinesInParagraph, LineRange } from '../../services/diff.service';
|
||||
import {
|
||||
MotionChangeRecommendationComponent,
|
||||
MotionChangeRecommendationComponentData
|
||||
@ -110,6 +110,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
*/
|
||||
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
|
||||
@ -121,6 +126,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
*/
|
||||
public changeRecommendations: ViewChangeReco[];
|
||||
|
||||
/**
|
||||
* All amendments to this motions
|
||||
*/
|
||||
public amendments: ViewMotion[];
|
||||
|
||||
/**
|
||||
* All change recommendations AND amendments, sorted by line number.
|
||||
*/
|
||||
@ -186,6 +196,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -255,11 +270,18 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
this.minSupporters = supporters;
|
||||
}
|
||||
);
|
||||
|
||||
this.configService.get('motions_preamble').subscribe(
|
||||
(preamble: string): void => {
|
||||
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.
|
||||
*/
|
||||
private recalcUnifiedChanges(): void {
|
||||
// @TODO implement amendments
|
||||
this.allChangingObjects = this.changeRecommendations;
|
||||
this.allChangingObjects = [];
|
||||
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) => {
|
||||
if (a.getLineFrom() < b.getLineFrom()) {
|
||||
return -1;
|
||||
@ -293,18 +332,23 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
} else {
|
||||
// load existing motion
|
||||
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) {
|
||||
this.motion = newViewMotion;
|
||||
this.patchForm(this.motion);
|
||||
}
|
||||
});
|
||||
this.changeRecoRepo
|
||||
.getChangeRecosOfMotionObservable(parseInt(params.id, 10))
|
||||
.subscribe((recos: ViewChangeReco[]) => {
|
||||
this.changeRecommendations = recos;
|
||||
this.repo.amendmentsTo(motionId).subscribe(
|
||||
(amendments: ViewMotion[]): void => {
|
||||
this.amendments = amendments;
|
||||
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 => {
|
||||
contentPatch[ctrl] = formMotion[ctrl];
|
||||
});
|
||||
|
||||
if (formMotion.isParagraphBasedAmendment()) {
|
||||
contentPatch.text = formMotion.amendment_paragraphs.find(
|
||||
(para: string): boolean => {
|
||||
return para !== null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const statuteAmendmentFieldName = 'statute_amendment';
|
||||
contentPatch[statuteAmendmentFieldName] = formMotion.isStatuteAmendment();
|
||||
this.contentForm.patchValue(contentPatch);
|
||||
@ -377,6 +430,18 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
|
||||
|
||||
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);
|
||||
|
||||
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}
|
||||
*/
|
||||
public getFormattedText(): SafeHtml {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(this.getFormattedTextPlain());
|
||||
public sanitizedText(text: string): SafeHtml {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param mode
|
||||
@ -539,14 +636,24 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
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.
|
||||
// takeWhile will get a inclusive parameter, see https://github.com/ReactiveX/rxjs/pull/4115
|
||||
this.configService.get<string>(configKey).pipe(multicast(
|
||||
() => new ReplaySubject(1),
|
||||
(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),
|
||||
this.configService
|
||||
.get<string>(configKey)
|
||||
.pipe(
|
||||
multicast(
|
||||
() => new ReplaySubject(1),
|
||||
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
|
||||
*/
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
motion?: Motion,
|
||||
category?: Category,
|
||||
@ -340,6 +348,14 @@ export class ViewMotion extends BaseViewModel {
|
||||
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
|
||||
*/
|
||||
|
@ -7,6 +7,7 @@ import { MotionCommentSectionListComponent } from './components/motion-comment-s
|
||||
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
|
||||
import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-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 = [
|
||||
{ path: '', component: MotionListComponent },
|
||||
@ -16,7 +17,8 @@ const routes: Routes = [
|
||||
{ path: 'call-list', component: CallListComponent },
|
||||
{ path: 'new', component: MotionDetailComponent },
|
||||
{ path: ':id', component: MotionDetailComponent },
|
||||
{ path: ':id/speakers', component: SpeakerListComponent }
|
||||
{ path: ':id/speakers', component: SpeakerListComponent },
|
||||
{ path: ':id/create-amendment', component: AmendmentCreateWizardComponent }
|
||||
];
|
||||
|
||||
@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 { PersonalNoteComponent } from './components/personal-note/personal-note.component';
|
||||
import { CallListComponent } from './components/call-list/call-list.component';
|
||||
import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
||||
@ -30,7 +31,8 @@ import { CallListComponent } from './components/call-list/call-list.component';
|
||||
MotionCommentsComponent,
|
||||
MetaTextBlockComponent,
|
||||
PersonalNoteComponent,
|
||||
CallListComponent
|
||||
CallListComponent,
|
||||
AmendmentCreateWizardComponent
|
||||
],
|
||||
entryComponents: [
|
||||
MotionChangeRecommendationComponent,
|
||||
|
@ -114,6 +114,44 @@ export interface LineRange {
|
||||
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.
|
||||
*
|
||||
@ -1024,10 +1062,11 @@ export class DiffService {
|
||||
return tagStr.replace(
|
||||
/<(\w+)( [^>]*)?>/gi,
|
||||
(whole: string, tag: string, tagArguments: string): string => {
|
||||
tagArguments = (tagArguments ? tagArguments : '');
|
||||
tagArguments = tagArguments ? tagArguments : '';
|
||||
if (tagArguments.match(/class="/gi)) {
|
||||
// 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 => {
|
||||
return attr + para + content + ' ' + className + para;
|
||||
}
|
||||
@ -1038,7 +1077,7 @@ export class DiffService {
|
||||
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".
|
||||
@ -1450,6 +1489,18 @@ export class DiffService {
|
||||
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,
|
||||
* wraps it with the context and adds line numbers.
|
||||
@ -1459,8 +1510,7 @@ export class DiffService {
|
||||
* @param {number} firstLine
|
||||
*/
|
||||
public formatDiffWithLineNumbers(diff: ExtractedContent, lineLength: number, firstLine: number): string {
|
||||
let text =
|
||||
diff.outerContextStart + diff.innerContextStart + diff.html + diff.innerContextEnd + diff.outerContextEnd;
|
||||
let text = this.formatDiff(diff);
|
||||
text = this.lineNumberingService.insertLineNumbers(text, lineLength, null, null, firstLine);
|
||||
return text;
|
||||
}
|
||||
@ -1921,7 +1971,7 @@ export class DiffService {
|
||||
diffUnnormalized = diffUnnormalized.replace(
|
||||
/<(ins|del)>([\s\S]*?)<\/\1>/gi,
|
||||
(whole: string, insDel: string): string => {
|
||||
const modificationClass = (insDel.toLowerCase() === 'ins' ? 'insert' : 'delete');
|
||||
const modificationClass = insDel.toLowerCase() === 'ins' ? 'insert' : 'delete';
|
||||
return whole.replace(
|
||||
/(<(p|div|blockquote|li)[^>]*>)([\s\S]*?)(<\/\2>)/gi,
|
||||
(whole2: string, opening: string, blockTag: string, content: string, closing: string): string => {
|
||||
@ -2017,4 +2067,62 @@ export class DiffService {
|
||||
|
||||
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 { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { tap, map } from 'rxjs/operators';
|
||||
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
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 { DataStoreService } from '../../../core/services/data-store.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 { MotionChangeReco } from '../../../shared/models/motions/motion-change-reco';
|
||||
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 { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||
import { TreeService } from 'app/core/services/tree.service';
|
||||
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
||||
|
||||
/**
|
||||
* Repository Services for motions (and potentially categories)
|
||||
@ -207,6 +208,25 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
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
|
||||
* reco algorithm.
|
||||
@ -311,6 +331,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
* @param {ViewMotion} motion
|
||||
* @param {ViewUnifiedChange[]} changes
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getTextRemainderAfterLastChange(
|
||||
motion: ViewMotion,
|
||||
@ -381,6 +402,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
* @param {ViewMotion} motion
|
||||
* @param {ViewUnifiedChange} change
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getChangeDiff(motion: ViewMotion, change: ViewUnifiedChange, highlight?: number): string {
|
||||
const lineLength = motion.lineLength,
|
||||
@ -427,4 +449,106 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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)
|
||||
"""
|
||||
If true, an additional input field (from motion comment) is visible
|
||||
|
@ -103,6 +103,7 @@ class StateSerializer(ModelSerializer):
|
||||
'allow_submitter_edit',
|
||||
'dont_set_identifier',
|
||||
'show_state_extension_field',
|
||||
'merge_amendment_into_final',
|
||||
'show_recommendation_extension_field',
|
||||
'next_states',
|
||||
'workflow')
|
||||
|
@ -24,7 +24,8 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
workflow=workflow_1,
|
||||
action_word='Accept',
|
||||
recommendation_label='Acceptance',
|
||||
css_class='success')
|
||||
css_class='success',
|
||||
merge_amendment_into_final=True)
|
||||
state_1_3 = State.objects.create(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_1,
|
||||
action_word='Reject',
|
||||
@ -55,7 +56,8 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
workflow=workflow_2,
|
||||
action_word='Accept',
|
||||
recommendation_label='Acceptance',
|
||||
css_class='success')
|
||||
css_class='success',
|
||||
merge_amendment_into_final=True)
|
||||
state_2_4 = State.objects.create(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_2,
|
||||
action_word='Reject',
|
||||
|
Loading…
Reference in New Issue
Block a user