diff --git a/client/src/app/shared/models/motions/workflow-state.ts b/client/src/app/shared/models/motions/workflow-state.ts
index 75e9c5d13..ac649ac7c 100644
--- a/client/src/app/shared/models/motions/workflow-state.ts
+++ b/client/src/app/shared/models/motions/workflow-state.ts
@@ -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;
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 68d03aafb..1100680e4 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -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,
diff --git a/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.html b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.html
new file mode 100644
index 000000000..f35d4faaa
--- /dev/null
+++ b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.html
@@ -0,0 +1,52 @@
+
+
+ Create amendment
+
+
+
diff --git a/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.scss b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.scss
new file mode 100644
index 000000000..22d512dec
--- /dev/null
+++ b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.scss
@@ -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%;
+ }
+}
diff --git a/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.spec.ts b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.spec.ts
new file mode 100644
index 000000000..ada8008cd
--- /dev/null
+++ b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.spec.ts
@@ -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;
+
+ 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();
+ });
+});
diff --git a/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.ts b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.ts
new file mode 100644
index 000000000..9161f8713
--- /dev/null
+++ b/client/src/app/site/motions/components/amendment-create-wizard/amendment-create-wizard.component.ts
@@ -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}
+ */
+ public async saveAmendment(): Promise {
+ 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]);
+ }
+}
diff --git a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.html b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.html
index 9638638c8..16a8245c2 100644
--- a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.html
+++ b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.html
@@ -1,73 +1,78 @@
-
- {{ 'Summary of changes' | translate }}:
-
+ {{ 'Summary of changes' | translate }}:
-
- {{ 'No change recommendations yet' | translate }}
-
+ {{ 'No change recommendations yet' | translate }}
-
-
-
+
+
warning
-
- {{ 'Rejected' | translate }}:
+ {{ 'Rejected' | translate }}
-
+
-
+
thumb_up
Accept
done
-
+
thumb_down
Reject
done
- {{ change.internal ? "check_box_outline_blank" : "check_box" }}
+ {{ change.internal ? 'check_box_outline_blank' : 'check_box' }}
Public
diff --git a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.scss b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.scss
index 1fb3cfb1d..02a27fd66 100644
--- a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.scss
+++ b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.scss
@@ -54,7 +54,6 @@
border: solid 1px #ddd;
border-radius: 3px;
margin-bottom: 5px;
- margin-top: -15px;
padding: 5px 5px 0 5px;
a,
diff --git a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts
index 0939796dc..931dee776 100644
--- a/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts
+++ b/client/src/app/site/motions/components/motion-detail-diff/motion-detail-diff.component.ts
@@ -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 '' + msg + '';
- }
- 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
+ );
}
/**
diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html
index b47dd608b..75b9b5cbe 100644
--- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html
+++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html
@@ -1,21 +1,23 @@
-
-
+
Motion
-
- {{ motion.identifier }}
- {{ metaInfoForm.get("identifier").value }}
-
-
- New motion
+ {{ motion.identifier }}
+ {{ metaInfoForm.get('identifier').value }}
+ New motion
-
+