Merge pull request #4339 from MaximilianKrambach/finalVersion

editor for final modified version of a motion
This commit is contained in:
Emanuel Schütze 2019-05-27 14:38:02 +02:00 committed by GitHub
commit 441e10ed59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 243 additions and 100 deletions

View File

@ -42,6 +42,14 @@ export enum ChangeRecoMode {
ModifiedFinal = 'modified_final_version' ModifiedFinal = 'modified_final_version'
} }
export const verboseChangeRecoMode = {
original: 'Original version',
changed: 'Changed version',
diff: 'Diff version',
agreed: 'Final version',
modified_final_version: 'Final print template'
};
export interface MotionTitleInformation extends TitleInformationWithAgendaItem { export interface MotionTitleInformation extends TitleInformationWithAgendaItem {
title: string; title: string;
identifier?: string; identifier?: string;

View File

@ -38,6 +38,8 @@
<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> </section>
<span class="text-prefix-label">{{ preamble | translate }}</span>
<!-- 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">

View File

@ -58,7 +58,7 @@
background-color: #eee; background-color: #eee;
border: solid 1px #ddd; border: solid 1px #ddd;
border-radius: 3px; border-radius: 3px;
margin-bottom: 5px; margin-bottom: 15px;
padding: 5px 10px; padding: 5px 10px;
a, a,

View File

@ -64,6 +64,8 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
*/ */
public lineLength: number; public lineLength: number;
public preamble: string;
/** /**
* @param title * @param title
* @param translate * @param translate
@ -92,6 +94,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.configService.get<number>('motions_line_length').subscribe(lineLength => (this.lineLength = lineLength)); this.configService.get<number>('motions_line_length').subscribe(lineLength => (this.lineLength = lineLength));
this.configService.get<string>('motions_preamble').subscribe(preamble => (this.preamble = preamble));
} }
/** /**

View File

@ -436,83 +436,115 @@
<ng-template #contentTemplate> <ng-template #contentTemplate>
<form class="motion-content" [formGroup]="contentForm" (keydown)="onKeyDown($event)"> <form class="motion-content" [formGroup]="contentForm" (keydown)="onKeyDown($event)">
<!-- Line Number and Diff buttons -->
<div *ngIf="!editMotion && !motion.isStatuteAmendment()" class="motion-text-controls"> <!-- Toolbar with text controls and buttonf for managing the (modified) final version-->
<mat-form-field class="motion-goto-line" *ngIf="highlightedLineOpened"> <div class="motion-text-toolbar-wrapper outline-border-bottom">
<input <!-- Line Number and Diff buttons -->
type="number" <div *ngIf="!editMotion && !motion.isStatuteAmendment()" class="motion-text-controls">
min="1" <mat-form-field class="motion-goto-line" *ngIf="highlightedLineOpened">
matInput <input
placeholder="{{ 'Go to line' | translate }}" type="number"
osAutofocus min="1"
[(ngModel)]="highlightedLineTyping" matInput
[ngModelOptions]="{ standalone: true }" placeholder="{{ 'Go to line' | translate }}"
[errorStateMatcher]="highlightedLineMatcher" osAutofocus
/> [(ngModel)]="highlightedLineTyping"
<mat-error *ngIf="highlightedLineTyping > 10" translate>Invalid line number</mat-error> [ngModelOptions]="{ standalone: true }"
<button [errorStateMatcher]="highlightedLineMatcher"
type="submit" />
mat-button <mat-error *ngIf="highlightedLineTyping > 10" translate>Invalid line number</mat-error>
matSuffix <button
mat-icon-button type="submit"
aria-label="Go to line" mat-button
*ngIf="highlightedLineTyping" matSuffix
(click)="gotoHighlightedLine(highlightedLineTyping); highlightedLineTyping = ''" mat-icon-button
> aria-label="Go to line"
<mat-icon>redo</mat-icon> *ngIf="highlightedLineTyping"
(click)="gotoHighlightedLine(highlightedLineTyping); highlightedLineTyping = ''"
>
<mat-icon>redo</mat-icon>
</button>
</mat-form-field>
<button mat-icon-button (click)="highlightedLineOpened = false" *ngIf="highlightedLineOpened">
<mat-icon>cancel</mat-icon>
</button> </button>
</mat-form-field>
<button
mat-icon-button
matTooltip="{{ 'Go to line' | translate }}"
*ngIf="!highlightedLineOpened"
(click)="highlightedLineOpened = true"
>
<mat-icon>redo</mat-icon>
</button>
<button mat-icon-button (click)="highlightedLineOpened = false" *ngIf="highlightedLineOpened">
<mat-icon>cancel</mat-icon>
</button>
<button <button
type="button" type="button"
mat-icon-button mat-button
[matMenuTriggerFor]="lineNumberingMenu" [matMenuTriggerFor]="lineNumberingMenu"
matTooltip="{{ 'Line numbering' | translate }}" >
> <mat-icon>format_list_numbered</mat-icon>
<mat-icon>format_list_numbered</mat-icon> &nbsp;<span translate>Line numbering</span>
</button> <span *ngIf="lnMode === LineNumberingMode.None">
<button &nbsp;(<span translate>none</span>)
type="button" </span>
mat-icon-button </button>
[matMenuTriggerFor]="changeRecoMenu" <button
matTooltip="{{ 'Change recommendations' | translate }}" type="button"
*ngIf=" mat-button
motion && !motion.isParagraphBasedAmendment() && allChangingObjects && allChangingObjects.length > 0 [matMenuTriggerFor]="changeRecoMenu"
" *ngIf="
> motion && !motion.isParagraphBasedAmendment() && allChangingObjects && allChangingObjects.length > 0
<mat-icon>rate_review</mat-icon> "
</button> >
<mat-icon>rate_review</mat-icon>
&nbsp;<span>{{ verboseChangeRecoMode[crMode] | translate }}</span>
</button>
</div>
<button <!-- Final edit buttons -->
type="button" <div *ngIf="isRecoMode(ChangeRecoMode.ModifiedFinal) || isRecoMode(ChangeRecoMode.Final)">
mat-icon-button <!-- create final version -->
matTooltip="{{ 'Create final print template' | translate }}" <button
*osPerms="'motions.can_manage'; and: isRecoMode(ChangeRecoMode.Final)" type="button"
(click)="createModifiedFinalVersion()" mat-icon-button
> matTooltip="{{ 'Copy to final print template' | translate }}"
<mat-icon>description</mat-icon> *osPerms="'motions.can_manage'; and: isRecoMode(ChangeRecoMode.Final)"
</button> (click)="createModifiedFinalVersion()"
<button >
type="button" <mat-icon>file_copy</mat-icon>
class="red-warning-text" </button>
mat-button <!-- edit final version -->
matTooltip="{{ 'Delete final print template' | translate }}" <button
*ngIf="isRecoMode(ChangeRecoMode.ModifiedFinal)" type="button"
(click)="deleteModifiedFinalVersion()" mat-icon-button
> *ngIf="isRecoMode(ChangeRecoMode.ModifiedFinal) && !isFinalEdit"
<mat-icon>description</mat-icon> (click)="editModifiedFinal()"
>
<mat-icon>edit</mat-icon>
</button>
<!-- save final version -->
<button
type="button"
*ngIf="isRecoMode(ChangeRecoMode.ModifiedFinal) && isFinalEdit"
mat-icon-button
[disabled]="!finalVersionEdited"
(click)="onSubmitFinalVersion()"
>
<mat-icon>save</mat-icon>
</button>
<!-- cancel final version edit -->
<button
type="button"
*ngIf="isRecoMode(ChangeRecoMode.ModifiedFinal) && isFinalEdit"
mat-icon-button
(click)="cancelFinalVersionEdit()"
>
<mat-icon>close</mat-icon>
</button>
<!-- delete final version edit -->
<button
type="button"
class="red-warning-text"
mat-icon-button
matTooltip="{{ 'Delete final print template' | translate }}"
*ngIf="isRecoMode(ChangeRecoMode.ModifiedFinal) && !isFinalEdit"
(click)="deleteModifiedFinalVersion()"
>
<mat-icon>delete</mat-icon>
</button> </button>
</div>
</div> </div>
<!-- Selecting statute paragraphs for amendment --> <!-- Selecting statute paragraphs for amendment -->
@ -579,13 +611,13 @@
</div> </div>
</div> </div>
<!-- Text --> <!-- Text (hide preamble, if diff mode. The preample is included in the motion-detail-diff component) -->
<span class="text-prefix-label">{{ preamble | translate }}</span> <span *ngIf="!isRecoMode(ChangeRecoMode.Diff)" class="text-prefix-label">{{ preamble | translate }}</span>
<!-- Regular motions or traditional amendments --> <!-- Regular motions or traditional amendments -->
<ng-container *ngIf="!editMotion && !motion.isStatuteAmendment() && !motion.isParagraphBasedAmendment()"> <ng-container *ngIf="!editMotion && !motion.isStatuteAmendment() && !motion.isParagraphBasedAmendment()">
<div <div
*ngIf="!isRecoMode(ChangeRecoMode.Diff)" *ngIf="!isRecoMode(ChangeRecoMode.Diff) && !isFinalEdit"
class="motion-text" class="motion-text"
[class.line-numbers-none]="isLineNumberingNone()" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-inline]="isLineNumberingInline()"
@ -612,7 +644,23 @@
[lineNumberingMode]="lnMode" [lineNumberingMode]="lnMode"
(createChangeRecommendation)="createChangeRecommendation($event)" (createChangeRecommendation)="createChangeRecommendation($event)"
></os-motion-detail-diff> ></os-motion-detail-diff>
<div *ngIf="isFinalEdit">
<editor [hidden]="!isFinalEdit" formControlName="modified_final_version" [init]="tinyMceSettings" required></editor>
<div
*ngIf="
contentForm.get('modified_final_version').invalid &&
(contentForm.get('modified_final_version').dirty || contentForm.get('modified_final_version').touched)
"
class="red-warning-text"
translate
>
This field is required.
</div>
</div>
</ng-container> </ng-container>
<!-- formatted statute amendment -->
<div <div
class="motion-text line-numbers-none" class="motion-text line-numbers-none"
*ngIf="!editMotion && motion.isStatuteAmendment()" *ngIf="!editMotion && motion.isStatuteAmendment()"
@ -878,6 +926,15 @@
> >
outside outside
</button> </button>
<mat-divider></mat-divider>
<button
mat-menu-item
*ngIf="!highlightedLineOpened"
(click)="highlightedLineOpened = true"
>
<mat-icon>redo</mat-icon>
<span translate>Go to line</span>
</button>
</div> </div>
</mat-menu> </mat-menu>
@ -918,9 +975,9 @@
<button <button
mat-menu-item mat-menu-item
translate translate
*osPerms="'motions.can_manage'; and: isRecoMode(ChangeRecoMode.Final)" *ngIf="motion && motion.modified_final_version"
(click)="setChangeRecoMode(ChangeRecoMode.ModifiedFinal)" (click)="setChangeRecoMode(ChangeRecoMode.ModifiedFinal)"
[ngClass]="{ selected: motion?.crMode === ChangeRecoMode.ModifiedFinal }" [ngClass]="{ selected: crMode === ChangeRecoMode.ModifiedFinal }"
> >
Final print template Final print template
</button> </button>

View File

@ -23,30 +23,36 @@ span {
display: flow-root; display: flow-root;
} }
.motion-text-controls { .motion-text-toolbar-wrapper {
float: right; display: flex;
margin-left: 5px; justify-content: flex-end;
margin-top: -15px;
height: 50px;
button { button {
font-size: 115%; font-weight: 400;
margin-top: 7px;
} }
height: 50px;
margin: -16px -16px 5px -16px;
padding: 0 16px;
> button { > button {
// Prevent moving the buttons when the "go to line"-input is shown // Prevent moving the buttons when the "go to line"-input is shown
margin-top: 7px; margin-top: 7px;
} }
.motion-goto-line {
width: 150px; .motion-text-controls {
} .motion-goto-line {
input[type='number']::-webkit-inner-spin-button, width: 150px;
input[type='number']::-webkit-outer-spin-button { }
-webkit-appearance: none; input[type='number']::-webkit-inner-spin-button,
margin: 0; input[type='number']::-webkit-outer-spin-button {
} -webkit-appearance: none;
input[type='number'] { margin: 0;
-moz-appearance: textfield; }
input[type='number'] {
-moz-appearance: textfield;
}
} }
} }

View File

@ -45,7 +45,12 @@ import {
ViewMotionNotificationEditMotion, ViewMotionNotificationEditMotion,
TypeOfNotificationViewMotion TypeOfNotificationViewMotion
} from 'app/site/motions/models/view-motion-notify'; } from 'app/site/motions/models/view-motion-notify';
import { ViewMotion, ChangeRecoMode, LineNumberingMode } from 'app/site/motions/models/view-motion'; import {
ViewMotion,
ChangeRecoMode,
LineNumberingMode,
verboseChangeRecoMode
} from 'app/site/motions/models/view-motion';
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph'; import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change'; import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
@ -123,6 +128,30 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
return this.repo.getExtendedStateLabel(this.motion); return this.repo.getExtendedStateLabel(this.motion);
} }
private finalEditMode = false;
/**
* check if the 'final version edit mode' is active
*
* @returns true if active
*/
public get isFinalEdit(): boolean {
return this.finalEditMode;
}
/**
* Helper to check the current state of the final version edit
*
* @returns true if the local edit of the modified_final_version differs
* from the submitted version
*/
public get finalVersionEdited(): boolean {
return (
this.crMode === ChangeRecoMode.ModifiedFinal &&
this.contentForm.get('modified_final_version').value !== this.motion.modified_final_version
);
}
/** /**
* Saves the target motion. Accessed via the getter and setter. * Saves the target motion. Accessed via the getter and setter.
*/ */
@ -275,6 +304,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
*/ */
public ChangeRecoMode = ChangeRecoMode; public ChangeRecoMode = ChangeRecoMode;
public verboseChangeRecoMode = verboseChangeRecoMode;
/** /**
* For using the enum constants from the template * For using the enum constants from the template
*/ */
@ -704,8 +735,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
selected_paragraphs: [], selected_paragraphs: [],
statute_amendment: [''], // Internal value for the checkbox, not saved to the model statute_amendment: [''], // Internal value for the checkbox, not saved to the model
statute_paragraph_id: [''], statute_paragraph_id: [''],
motion_block_id: [], // TODO: Can be removed if this is not required motion_block_id: [],
parent_id: [] parent_id: [],
modified_final_version: ['']
}); });
this.updateWorkflowIdForCreateForm(); this.updateWorkflowIdForCreateForm();
@ -1062,7 +1094,10 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
'Are you sure you want to copy the final version to the print template?' 'Are you sure you want to copy the final version to the print template?'
); );
if (await this.promptService.open(title, null)) { if (await this.promptService.open(title, null)) {
await this.updateMotion({ modified_final_version: finalVersion }, this.motion); this.updateMotion({ modified_final_version: finalVersion }, this.motion).then(
() => this.setChangeRecoMode(ChangeRecoMode.ModifiedFinal),
this.raiseError
);
} }
} else { } else {
await this.updateMotion({ modified_final_version: finalVersion }, this.motion); await this.updateMotion({ modified_final_version: finalVersion }, this.motion);
@ -1070,7 +1105,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
} catch (e) { } catch (e) {
this.raiseError(e); this.raiseError(e);
} }
this.setChangeRecoMode(ChangeRecoMode.ModifiedFinal);
} }
/** /**
@ -1079,6 +1113,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
public async deleteModifiedFinalVersion(): Promise<void> { public async deleteModifiedFinalVersion(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete the print template?'); const title = this.translate.instant('Are you sure you want to delete the print template?');
if (await this.promptService.open(title, null)) { if (await this.promptService.open(title, null)) {
this.finalEditMode = false;
this.updateMotion({ modified_final_version: '' }, this.motion).then( this.updateMotion({ modified_final_version: '' }, this.motion).then(
() => this.setChangeRecoMode(ChangeRecoMode.Final), () => this.setChangeRecoMode(ChangeRecoMode.Final),
this.raiseError this.raiseError
@ -1444,6 +1479,27 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
} }
} }
/**
* Submits the modified final version of the motion
*/
public onSubmitFinalVersion(): void {
const val = this.contentForm.get('modified_final_version').value;
this.updateMotion({ modified_final_version: val }, this.motion).then(() => {
this.finalEditMode = false;
this.contentForm.get('modified_final_version').markAsPristine();
}, this.raiseError);
}
/**
* Cancels the final version edit and resets the form value
*
* TODO: the tinyMCE editor content should reset, too
*/
public cancelFinalVersionEdit(): void {
this.finalEditMode = false;
this.contentForm.patchValue({ modified_final_version: this.motion.modified_final_version });
}
/** /**
* Toggles the favorite status * Toggles the favorite status
*/ */
@ -1548,4 +1604,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
} }
} }
} }
/**
* Activates the 'edit final version' mode
*/
public editModifiedFinal(): void {
this.finalEditMode = true;
}
} }

View File

@ -46,6 +46,10 @@
color: mat-color($warn); color: mat-color($warn);
} }
.outline-border-bottom {
border-bottom: 1px solid $os-outline;
}
/* motion list/detail view */ /* motion list/detail view */
.mat-chip-list.user .mat-chip { .mat-chip-list.user .mat-chip {
color: mat-color($foreground, text); color: mat-color($foreground, text);