new component for editor and fix compare for template dictionaries

This commit is contained in:
Dan Percic 2021-10-05 22:54:16 +03:00
parent 760a446fd5
commit 43a46d04e8
11 changed files with 223 additions and 134 deletions

View File

@ -42,7 +42,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
} }
get hasChanges() { get hasChanges() {
return this._dictionaryManager.hasChanges; return this._dictionaryManager.editor.hasChanges;
} }
async ngOnInit() { async ngOnInit() {
@ -85,7 +85,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
} }
download(): void { download(): void {
const content = this._dictionaryManager.editorValue; const content = this._dictionaryManager.editor.value;
const blob = new Blob([content], { const blob = new Blob([content], {
type: 'text/plain;charset=utf-8' type: 'text/plain;charset=utf-8'
}); });
@ -98,7 +98,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
if (file) { if (file) {
fileReader.onload = () => { fileReader.onload = () => {
this._dictionaryManager.editorValue = fileReader.result as string; this._dictionaryManager.editor.value = fileReader.result as string;
this._fileInput.nativeElement.value = null; this._fileInput.nativeElement.value = null;
}; };
fileReader.readAsText(file); fileReader.readAsText(file);

View File

@ -33,7 +33,7 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
} }
get changed() { get changed() {
return this._dictionaryManager.hasChanges; return this._dictionaryManager.editor.hasChanges;
} }
get disabled() { get disabled() {
@ -58,7 +58,7 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
async save() { async save() {
await this._dictionaryService await this._dictionaryService
.saveEntries( .saveEntries(
this._dictionaryManager.currentEntries, this._dictionaryManager.editor.currentEntries,
this._dictionaryManager.initialEntries, this._dictionaryManager.initialEntries,
this.dossier.dossierTemplateId, this.dossier.dossierTemplateId,
'dossier_redaction', 'dossier_redaction',

View File

@ -30,18 +30,31 @@
{{ 'dictionary-overview.compare.compare' | translate }} {{ 'dictionary-overview.compare.compare' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
<div *ngIf="filterByDossierTemplate" class="iqser-input-group w-200 mt-0 mr-8">
<mat-select [(ngModel)]="dossierTemplate" [disabled]="!compare"> <ng-container *ngIf="filterByDossierTemplate">
<mat-option [value]="selectDossierTemplate">{{ selectDossierTemplate.name | translate }}</mat-option> <div class="iqser-input-group w-200 mt-0 mr-8">
<mat-option *ngFor="let dossierTemplate of dossierTemplatesService.all$ | async" [value]="dossierTemplate"> <mat-select [(ngModel)]="dossierTemplate" [disabled]="!compare">
{{ dossierTemplate.name }} <mat-option [value]="selectDossierTemplate">{{ selectDossierTemplate.name | translate }}</mat-option>
</mat-option> <mat-option *ngFor="let dossierTemplate of dossierTemplatesService.all$ | async" [value]="dossierTemplate">
</mat-select> {{ dossierTemplate.name }}
</div> </mat-option>
<div class="iqser-input-group w-200 mt-0"> </mat-select>
<mat-select [(ngModel)]="dossier" [disabled]="!compare || dossierTemplateIsNotSelected"> </div>
<div class="iqser-input-group w-200 mt-0">
<mat-select [(ngModel)]="dictionary" [disabled]="!compare || dossierTemplateIsNotSelected">
<mat-option [value]="selectDictionary">{{ selectDictionary.label | translate }}</mat-option>
<mat-option *ngFor="let dictionary of dictionaries" [value]="dictionary">
{{ dictionary.label }}
</mat-option>
</mat-select>
</div>
</ng-container>
<div *ngIf="!filterByDossierTemplate" class="iqser-input-group w-200 mt-0">
<mat-select [(ngModel)]="dossier" [disabled]="!compare">
<mat-option [value]="selectDossier">{{ selectDossier.dossierName | translate }}</mat-option> <mat-option [value]="selectDossier">{{ selectDossier.dossierName | translate }}</mat-option>
<mat-option *ngFor="let dossier of dossiers" [value]="dossier"> <mat-option *ngFor="let dossier of dossiersService.all$ | async" [value]="dossier">
{{ dossier.dossierName }} {{ dossier.dossierName }}
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -50,30 +63,22 @@
</div> </div>
<div class="editor-container"> <div class="editor-container">
<ngx-monaco-editor <redaction-editor
(init)="onCodeEditorInit($event)" [canEdit]="canEdit"
*ngIf="!compare || !showDiffEditor" [diffEditorText]="diffEditorText"
[(ngModel)]="editorValue" [initialEntries]="initialEntries"
[options]="editorOptions" [showDiffEditor]="compare && showDiffEditor"
></ngx-monaco-editor> ></redaction-editor>
<ngx-monaco-diff-editor <div *ngIf="compare && optionNotSelected" class="no-dictionary-selected">
(init)="onDiffEditorInit($event)"
*ngIf="compare && showDiffEditor"
[modified]="editorValue"
[options]="editorOptions"
[original]="diffEditorText"
></ngx-monaco-diff-editor>
<div *ngIf="compare && dossier.dossierName === selectDossier.dossierName" class="no-dictionary-selected">
<mat-icon svgIcon="red:dictionary"></mat-icon> <mat-icon svgIcon="red:dictionary"></mat-icon>
<span class="heading-l" translate="dictionary-overview.select-dictionary"></span> <span class="heading-l" translate="dictionary-overview.select-dictionary"></span>
</div> </div>
</div> </div>
<div *ngIf="withFloatingActions && hasChanges && canEdit" [class.offset]="compare" class="changes-box"> <div *ngIf="withFloatingActions && !!editor?.hasChanges && canEdit" [class.offset]="compare" class="changes-box">
<iqser-icon-button <iqser-icon-button
(action)="saveDictionary.emit(currentEntries)" (action)="saveDictionary.emit(editor?.currentEntries)"
[label]="'dictionary-overview.save-changes' | translate" [label]="'dictionary-overview.save-changes' | translate"
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"
icon="iqser:check" icon="iqser:check"

View File

@ -36,12 +36,6 @@ form {
} }
} }
ngx-monaco-diff-editor,
ngx-monaco-editor {
height: 100%;
width: 100%;
}
.content-container { .content-container {
height: 100%; height: 100%;

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
import { Debounce, IconButtonTypes, List } from '@iqser/common-ui'; import { Debounce, IconButtonTypes, List } from '@iqser/common-ui';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
@ -8,13 +8,12 @@ import { DictionaryService } from '@shared/services/dictionary.service';
import { DossiersService } from '../../../dossier/services/dossiers.service'; import { DossiersService } from '../../../dossier/services/dossiers.service';
import { DossierTemplatesService } from '../../../dossier/services/dossier-templates.service'; import { DossierTemplatesService } from '../../../dossier/services/dossier-templates.service';
import { DossierTemplate } from '@models/file/dossier-template'; import { DossierTemplate } from '@models/file/dossier-template';
import ICodeEditor = monaco.editor.ICodeEditor; import { AppStateService } from '@state/app-state.service';
import IDiffEditor = monaco.editor.IDiffEditor; import { EditorComponent } from '@shared/components/editor/editor.component';
import { TypeValue } from '@models/file/type-value';
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration; import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
import FindMatch = monaco.editor.FindMatch; import FindMatch = monaco.editor.FindMatch;
const MIN_WORD_LENGTH = 2;
const SMOOTH_SCROLL = 0; const SMOOTH_SCROLL = 0;
@Component({ @Component({
@ -22,7 +21,7 @@ const SMOOTH_SCROLL = 0;
templateUrl: './dictionary-manager.component.html', templateUrl: './dictionary-manager.component.html',
styleUrls: ['./dictionary-manager.component.scss'] styleUrls: ['./dictionary-manager.component.scss']
}) })
export class DictionaryManagerComponent implements OnChanges, OnInit { export class DictionaryManagerComponent implements OnChanges {
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
@Input() withFloatingActions = true; @Input() withFloatingActions = true;
@ -30,32 +29,30 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
@Input() initialEntries: List; @Input() initialEntries: List;
@Input() canEdit = false; @Input() canEdit = false;
@Output() readonly saveDictionary = new EventEmitter<string[]>(); @Output() readonly saveDictionary = new EventEmitter<string[]>();
@ViewChild(EditorComponent) readonly editor: EditorComponent;
currentMatch = 0; currentMatch = 0;
findMatches: FindMatch[] = []; findMatches: FindMatch[] = [];
currentEntries: string[] = [];
editorOptions: IStandaloneEditorConstructionOptions = {};
diffEditorText = ''; diffEditorText = '';
showDiffEditor = false; showDiffEditor = false;
searchText = ''; searchText = '';
selectDossier = { dossierName: _('dictionary-overview.compare.select-dossier') } as Dossier;
selectDossier = { dossierName: _('dictionary-overview.compare.select-dossier') }; selectDictionary = {
selectDossierTemplate = { name: _('dictionary-overview.compare.select-dossier-template') }; label: _('dictionary-overview.compare.select-dictionary')
} as TypeValue;
selectDossierTemplate = { name: _('dictionary-overview.compare.select-dossier-template') } as DossierTemplate;
compare: false; compare: false;
dossiers: List<Dossier> = this._dossiers; dictionaries: List<TypeValue> = this._dictionaries;
private _codeEditor: ICodeEditor;
private _diffEditor: IDiffEditor;
private _decorations: string[] = [];
private _searchDecorations: string[] = []; private _searchDecorations: string[] = [];
constructor( constructor(
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
readonly dossiersService: DossiersService, readonly dossiersService: DossiersService,
readonly appStateService: AppStateService,
readonly dossierTemplatesService: DossierTemplatesService readonly dossierTemplatesService: DossierTemplatesService
) {} ) {}
private _dossierTemplate: DossierTemplate = this.selectDossierTemplate as DossierTemplate; private _dossierTemplate = this.selectDossierTemplate;
get dossierTemplate() { get dossierTemplate() {
return this._dossierTemplate; return this._dossierTemplate;
@ -63,11 +60,12 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
set dossierTemplate(value) { set dossierTemplate(value) {
this._dossierTemplate = value; this._dossierTemplate = value;
this.dossier = this.selectDossier as Dossier; this.dictionaries = this._dictionaries;
this.dossiers = this._dossiers; this._dictionary = this.selectDictionary;
this.showDiffEditor = false;
} }
private _dossier: Dossier = this.selectDossier as Dossier; private _dossier = this.selectDossier;
get dossier() { get dossier() {
return this._dossier; return this._dossier;
@ -76,77 +74,61 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
set dossier(dossier: Dossier) { set dossier(dossier: Dossier) {
this._dossier = dossier; this._dossier = dossier;
if (dossier === this.selectDossier) { if (dossier.dossierName === this.selectDossier.dossierName) {
this.showDiffEditor = false; this.showDiffEditor = false;
this.diffEditorText = ''; this.diffEditorText = '';
return; return;
} }
this._onDossierChanged(dossier) this._onDossierChanged(dossier.dossierTemplateId, dossier.dossierId)
.pipe(take(1)) .pipe(take(1))
.subscribe(entries => { .subscribe(entries => {
this.diffEditorText = entries; this.diffEditorText = entries;
this.showDiffEditor = true; this.showDiffEditor = true;
if (this.showDiffEditor) {
this._diffEditor?.getOriginalEditor().setValue(this.diffEditorText);
}
}); });
} }
get editorValue(): string { private _dictionary = this.selectDictionary;
return this.currentEntries.join('\n');
get dictionary() {
return this._dictionary;
} }
set editorValue(text: string) { set dictionary(dictionary: TypeValue) {
this.currentEntries = text.split('\n'); this._dictionary = dictionary;
this.codeEditorTextChanged();
}
get hasChanges(): boolean { if (dictionary.label === this.selectDictionary.label) {
return this.currentEntries.toString() !== this.initialEntries.toString(); this.showDiffEditor = false;
} this.diffEditorText = '';
return;
get _dossiers() {
if (this.filterByDossierTemplate) {
return this.dossiersService.all.filter(dossier => dossier.dossierTemplateId === this.dossierTemplate.dossierTemplateId);
} }
return this.dossiersService.all;
const entries = this.appStateService.dictionaryData[this._dictionary.dossierTemplateId][this._dictionary.type].entries;
this.diffEditorText = this._toString(entries);
this.showDiffEditor = true;
}
get _dictionaries() {
if (!this._dossierTemplate || this._dossierTemplate.name === this.selectDossierTemplate.name) {
return;
}
return Object.values(this.appStateService.dictionaryData[this.dossierTemplate?.dossierTemplateId]);
} }
get dossierTemplateIsNotSelected() { get dossierTemplateIsNotSelected() {
return this.filterByDossierTemplate && this._dossierTemplate.name === this.selectDossierTemplate.name; return this.filterByDossierTemplate && this._dossierTemplate.name === this.selectDossierTemplate.name;
} }
ngOnInit(): void { get optionNotSelected() {
this.currentEntries = [...this.initialEntries]; if (this.filterByDossierTemplate) {
return this.selectDictionary.label === this._dictionary.label;
this.editorOptions = { }
theme: 'vs', return this.dossier.dossierName === this.selectDossier.dossierName;
language: 'text/plain',
automaticLayout: true,
readOnly: !this.canEdit
};
}
onDiffEditorInit(editor: IDiffEditor): void {
this._diffEditor = editor;
}
onCodeEditorInit(editor: ICodeEditor): void {
this._codeEditor = editor;
(window as any).monaco.editor.defineTheme('redaction', {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.lineHighlightBackground': '#f4f5f7'
}
});
(window as any).monaco.editor.setTheme('redaction');
} }
revert() { revert() {
this.currentEntries = [...this.initialEntries]; this.editor?.revert();
this.searchChanged(''); this.searchChanged('');
} }
@ -160,13 +142,6 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
this.nextSearchMatch(); this.nextSearchMatch();
} }
@Debounce()
codeEditorTextChanged() {
const newDecorations = this.currentEntries.filter(entry => this._isNew(entry)).map(entry => this._getDecoration(entry));
this._decorations = this._codeEditor.deltaDecorations(this._decorations, newDecorations);
}
nextSearchMatch(): void { nextSearchMatch(): void {
if (this.findMatches.length <= 0) { if (this.findMatches.length <= 0) {
return; return;
@ -190,30 +165,18 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
private _applySearchDecorations() { private _applySearchDecorations() {
this._searchDecorations = this._codeEditor?.deltaDecorations(this._searchDecorations, []) || []; this._searchDecorations = this.editor.codeEditor?.deltaDecorations(this._searchDecorations, []) || [];
const decorations = this.findMatches.map(match => this._getSearchDecoration(match)); const decorations = this.findMatches.map(match => this._getSearchDecoration(match));
this._searchDecorations = this._codeEditor?.deltaDecorations(this._searchDecorations, decorations) || []; this._searchDecorations = this.editor.codeEditor?.deltaDecorations(this._searchDecorations, decorations) || [];
} }
private _getMatches(text: string): FindMatch[] { private _getMatches(text: string): FindMatch[] {
const model = this._codeEditor?.getModel(); const model = this.editor.codeEditor?.getModel();
return model?.findMatches(text, false, false, false, null, false) || []; return model?.findMatches(text, false, false, false, null, false) || [];
} }
private _isNew(entry: string): boolean {
return this.initialEntries.indexOf(entry) < 0 && entry?.trim().length > 0;
}
private _getDecoration(entry: string): IModelDeltaDecoration {
const line = this.currentEntries.indexOf(entry) + 1;
const cssClass = entry.length < MIN_WORD_LENGTH ? 'too-short-marker' : 'changed-row-marker';
const range = new monaco.Range(line, 1, line, 1);
return { range: range, options: { isWholeLine: true, className: cssClass } };
}
private _getSearchDecoration(match: FindMatch): IModelDeltaDecoration { private _getSearchDecoration(match: FindMatch): IModelDeltaDecoration {
return { range: match.range, options: { inlineClassName: 'search-marker' } }; return { range: match.range, options: { inlineClassName: 'search-marker' } };
} }
@ -221,12 +184,12 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
private _scrollToCurrentMatch(): void { private _scrollToCurrentMatch(): void {
const range = this.findMatches[this.currentMatch - 1].range; const range = this.findMatches[this.currentMatch - 1].range;
this._codeEditor.setSelection(range); this.editor.codeEditor.setSelection(range);
this._codeEditor.revealLineInCenter(range.startLineNumber, SMOOTH_SCROLL); this.editor.codeEditor.revealLineInCenter(range.startLineNumber, SMOOTH_SCROLL);
} }
private _onDossierChanged({ id, dossierTemplateId }: Dossier): Observable<string> { private _onDossierChanged(dossierTemplateId: string, dossierId?: string, type = 'dossier_redaction'): Observable<string> {
const dictionary$ = this._dictionaryService.getFor(dossierTemplateId, 'dossier_redaction', id); const dictionary$ = this._dictionaryService.getFor(dossierTemplateId, type, dossierId);
return dictionary$.pipe(map(data => this._toString([...data.entries]))); return dictionary$.pipe(map(data => this._toString([...data.entries])));
} }

View File

@ -0,0 +1,14 @@
<ngx-monaco-editor
(init)="onCodeEditorInit($event)"
*ngIf="!showDiffEditor"
[(ngModel)]="value"
[options]="editorOptions"
></ngx-monaco-editor>
<ngx-monaco-diff-editor
(init)="onDiffEditorInit($event)"
*ngIf="showDiffEditor"
[modified]="value"
[options]="editorOptions"
[original]="diffEditorText"
></ngx-monaco-diff-editor>

View File

@ -0,0 +1,6 @@
:host,
ngx-monaco-diff-editor,
ngx-monaco-editor {
height: 100%;
width: 100%;
}

View File

@ -0,0 +1,104 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Debounce, List } from '@iqser/common-ui';
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
import ICodeEditor = monaco.editor.ICodeEditor;
import IDiffEditor = monaco.editor.IDiffEditor;
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
const MIN_WORD_LENGTH = 2;
@Component({
selector: 'redaction-editor',
templateUrl: './editor.component.html',
styleUrls: ['./editor.component.scss']
})
export class EditorComponent implements OnInit, OnChanges {
editorOptions: IStandaloneEditorConstructionOptions = {};
@Input() showDiffEditor = false;
@Input() initialEntries: List;
@Input() canEdit = false;
currentEntries: string[] = [];
codeEditor: ICodeEditor;
diffEditor: IDiffEditor;
private _decorations: string[] = [];
_diffEditorText = '';
get diffEditorText() {
return this._diffEditorText;
}
@Input()
set diffEditorText(value: string) {
this._diffEditorText = value;
this.diffEditor?.getOriginalEditor().setValue(value);
}
get value(): string {
return this.currentEntries.join('\n');
}
set value(text: string) {
this.currentEntries = text.split('\n');
this.codeEditorTextChanged();
}
get hasChanges(): boolean {
return this.currentEntries.toString() !== this.initialEntries.toString();
}
ngOnInit(): void {
this.currentEntries = [...this.initialEntries];
this.editorOptions = {
theme: 'vs',
language: 'text/plain',
automaticLayout: true,
readOnly: !this.canEdit
};
}
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
this.revert();
}
onDiffEditorInit(editor: IDiffEditor): void {
this.diffEditor = editor;
}
onCodeEditorInit(editor: ICodeEditor): void {
this.codeEditor = editor;
(window as any).monaco.editor.defineTheme('redaction', {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.lineHighlightBackground': '#f4f5f7'
}
});
(window as any).monaco.editor.setTheme('redaction');
}
@Debounce()
codeEditorTextChanged() {
const newDecorations = this.currentEntries.filter(entry => this._isNew(entry)).map(entry => this._getDecoration(entry));
this._decorations = this.codeEditor.deltaDecorations(this._decorations, newDecorations);
}
revert() {
this.currentEntries = [...this.initialEntries];
}
private _isNew(entry: string): boolean {
return this.initialEntries.indexOf(entry) < 0 && entry?.trim().length > 0;
}
private _getDecoration(entry: string): IModelDeltaDecoration {
const line = this.currentEntries.indexOf(entry) + 1;
const cssClass = entry.length < MIN_WORD_LENGTH ? 'too-short-marker' : 'changed-row-marker';
const range = new monaco.Range(line, 1, line, 1);
return { range: range, options: { isWholeLine: true, className: cssClass } };
}
}

View File

@ -24,6 +24,7 @@ import { LongPressDirective } from './directives/long-press.directive';
import { NamePipe } from './pipes/name.pipe'; import { NamePipe } from './pipes/name.pipe';
import { TypeFilterComponent } from './components/type-filter/type-filter.component'; import { TypeFilterComponent } from './components/type-filter/type-filter.component';
import { TeamMembersComponent } from './components/team-members/team-members.component'; import { TeamMembersComponent } from './components/team-members/team-members.component';
import { EditorComponent } from './components/editor/editor.component';
const buttons = [FileDownloadBtnComponent, UserButtonComponent]; const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -47,7 +48,7 @@ const utils = [DatePipe, NamePipe, NavigateLastDossiersScreenDirective, LongPres
const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule, CommonUiModule]; const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule, CommonUiModule];
@NgModule({ @NgModule({
declarations: [...components, ...utils], declarations: [...components, ...utils, EditorComponent],
imports: [CommonModule, ...modules, MonacoEditorModule], imports: [CommonModule, ...modules, MonacoEditorModule],
exports: [...modules, ...components, ...utils], exports: [...modules, ...components, ...utils],
providers: [ providers: [

View File

@ -435,7 +435,8 @@
"compare": { "compare": {
"compare": "Vergleichen Sie", "compare": "Vergleichen Sie",
"select-dossier": "Wählen Sie Dossiervorlage", "select-dossier": "Wählen Sie Dossiervorlage",
"select-dossier-template": "" "select-dossier-template": "",
"select-dictionary": ""
}, },
"dictionary-details": { "dictionary-details": {
"description": "Beschreibung" "description": "Beschreibung"

View File

@ -475,7 +475,8 @@
"compare": { "compare": {
"compare": "Compare", "compare": "Compare",
"select-dossier": "Select Dossier", "select-dossier": "Select Dossier",
"select-dossier-template": "Select Dossier Template" "select-dossier-template": "Select Dossier Template",
"select-dictionary": "Select Dictionary"
}, },
"dictionary-details": { "dictionary-details": {
"description": "Description" "description": "Description"