diff --git a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts index 1c619cd87..252f8806d 100644 --- a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts @@ -58,8 +58,9 @@ const dossierTemplateIdRoutes: IqserRoutes = [ allow: [Roles.rules.read], redirectTo: 'info', }, + type: 'ENTITY', }, - loadChildren: () => import('./screens/rules/entity-rules.module').then(m => m.EntityRulesModule), + loadChildren: () => import('./screens/rules/rules.module').then(m => m.RulesModule), }, { path: 'component-rules', @@ -71,8 +72,9 @@ const dossierTemplateIdRoutes: IqserRoutes = [ allow: [Roles.rules.read], redirectTo: 'info', }, + type: 'COMPONENT', }, - loadChildren: () => import('./screens/component-rules/component-rules.module').then(m => m.ComponentRulesModule), + loadChildren: () => import('./screens/rules/rules.module').then(m => m.RulesModule), }, { path: 'file-attributes', diff --git a/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.ts deleted file mode 100644 index cf7839551..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, ElementRef, OnInit, signal, ViewChild } from '@angular/core'; -import { PermissionsService } from '@services/permissions.service'; -import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; -import { saveAs } from 'file-saver'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { firstValueFrom } from 'rxjs'; -import { DOSSIER_TEMPLATE_ID } from '@red/domain'; -import { EditorThemeService } from '@services/editor-theme.service'; -import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; -import { Debounce, getParam } from '@iqser/common-ui/lib/utils'; -import { RulesService } from '../../../services/rules.service'; -import ICodeEditor = monaco.editor.ICodeEditor; -import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration; -import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions; - -interface SyntaxError { - line: number; - column: number; - message: string; -} - -@Component({ - templateUrl: './component-rules-screen.component.html', - styleUrls: ['./component-rules-screen.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ComponentRulesScreenComponent implements OnInit, ComponentCanDeactivate { - readonly iconButtonTypes = IconButtonTypes; - readonly editorOptions: IStandaloneEditorConstructionOptions = { - theme: 'vs', - language: 'java', - automaticLayout: true, - readOnly: !this.permissionsService.canEditRules(), - glyphMargin: true, - }; - initialLines: string[] = []; - currentLines: string[] = []; - isLeaving = false; - @ViewChild('fileInput') - private _fileInput: ElementRef; - private _codeEditor: ICodeEditor; - private _decorations: string[] = []; - readonly #errorGlyphs = signal([]); - readonly numberOfErrors = computed(() => this.#errorGlyphs().length); - readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID); - - constructor( - readonly permissionsService: PermissionsService, - private readonly _rulesService: RulesService, - private readonly _changeDetectorRef: ChangeDetectorRef, - private readonly _toaster: Toaster, - private readonly _loadingService: LoadingService, - private readonly _editorThemeService: EditorThemeService, - ) {} - - set isLeavingPage(isLeaving: boolean) { - this.isLeaving = isLeaving; - this._changeDetectorRef.detectChanges(); - } - - get changed(): boolean { - return this.currentLines.toString() !== this.initialLines.toString(); - } - - get codeEditorText() { - return this.currentLines.join('\n'); - } - - set codeEditorText($event: any) { - this.currentLines = $event.split('\n'); - this.codeEditorTextChanged(); - this._closeProblemsView(); - } - - async ngOnInit() { - await this._initialize(); - } - - onCodeEditorInit(editor: ICodeEditor) { - this._codeEditor = editor; - for (const theme of this._editorThemeService.themes) { - (window as any).monaco.editor.defineTheme(theme, this._editorThemeService.configurations[theme]); - } - (window as any).monaco.editor.setTheme(this._editorThemeService.getTheme(true)); - this._changeDetectorRef.detectChanges(); - } - - @Debounce() - codeEditorTextChanged() { - const newDecorations = this.currentLines.filter(entry => this._isNew(entry)).map(entry => this._makeDecorationFor(entry)); - this._decorations = this._codeEditor.deltaDecorations(this._decorations, newDecorations); - } - - goToErrors() { - this._codeEditor.trigger(null, 'editor.action.marker.next', null); - } - - async save(): Promise { - this._loadingService.start(); - this._removeErrorMarkers(); - await firstValueFrom( - this._rulesService.uploadRules({ - rules: this._codeEditor.getModel().getValue(), - dossierTemplateId: this.#dossierTemplateId, - ruleFileType: 'COMPONENT', - }), - ).then( - async () => { - await this._initialize(); - this._toaster.success(_('component-rules-screen.success.generic')); - }, - error => { - const errors = error.error as SyntaxError[] | undefined; - this._drawErrorMarkers(errors); - this._loadingService.stop(); - this._toaster.error(_('component-rules-screen.error.generic')); - }, - ); - } - - revert(): void { - this.currentLines = this.initialLines; - this._decorations = this._codeEditor?.deltaDecorations(this._decorations, []) || []; - this._removeErrorMarkers(); - this._changeDetectorRef.detectChanges(); - this._loadingService.stop(); - } - - download(): void { - const content = this._codeEditor.getModel().getValue(); - const blob = new Blob([content], { - type: 'text/plain;charset=utf-8', - }); - saveAs(blob, 'rules.txt'); - } - - upload($event): void { - const file: File = $event.target.files[0]; - const fileReader = new FileReader(); - - if (file) { - fileReader.onload = () => { - this._codeEditor.getModel().setValue(fileReader.result as string); - this._fileInput.nativeElement.value = null; - }; - fileReader.readAsText(file); - } - } - - private _isNew(entry: string): boolean { - return this.initialLines.indexOf(entry) < 0 && entry?.trim().length > 0; - } - - private _makeDecorationFor(entry: string): IModelDeltaDecoration { - const line = this.currentLines.indexOf(entry) + 1; - - return { - range: new monaco.Range(line, 1, line, 1), - options: { isWholeLine: true, className: 'changed-row-marker' }, - } as IModelDeltaDecoration; - } - - private _drawErrorMarkers(errors: SyntaxError[] | undefined) { - const model = this._codeEditor?.getModel(); - if (!model || !errors?.length) { - return; - } - const markers = []; - const glyphs = []; - errors - .filter(e => e.line > 0) - .forEach(e => { - const endColumn = model.getLineLength(e.line) + 1; - markers.push({ - message: e.message, - severity: monaco.MarkerSeverity.Error, - startLineNumber: e.line, - startColumn: e.column, - endLineNumber: e.line, - endColumn, - }); - glyphs.push({ - range: new monaco.Range(e.line, e.column, e.line, endColumn), - options: { - glyphMarginClassName: 'error-glyph-margin', - }, - }); - }); - this.#errorGlyphs.set(this._codeEditor.deltaDecorations(this.#errorGlyphs(), glyphs)); - (window as any).monaco.editor.setModelMarkers(model, model.id, markers); - } - - private _closeProblemsView() { - this._codeEditor.trigger(null, 'closeMarkersNavigation', null); - } - - private _removeErrorMarkers() { - const model = this._codeEditor?.getModel(); - if (!model) { - return; - } - (window as any).monaco.editor.setModelMarkers(model, model.id, []); - this.#errorGlyphs.set(this._codeEditor.deltaDecorations(this.#errorGlyphs(), []) || []); - this._closeProblemsView(); - } - - private async _initialize() { - this._loadingService.start(); - await firstValueFrom(this._rulesService.download(this.#dossierTemplateId, 'COMPONENT')).then( - rules => { - this.currentLines = this.initialLines = rules.rules.split('\n'); - this.revert(); - }, - () => this._loadingService.stop(), - ); - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules.module.ts b/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules.module.ts deleted file mode 100644 index d5abe3232..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; -import { ComponentRulesScreenComponent } from './component-rules-screen/component-rules-screen.component'; -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; -import { PendingChangesGuard } from '@guards/can-deactivate.guard'; -import { TranslateModule } from '@ngx-translate/core'; -import { IconButtonComponent } from '@iqser/common-ui'; -import { FormsModule } from '@angular/forms'; -import { MatIconModule } from '@angular/material/icon'; - -const routes = [{ path: '', component: ComponentRulesScreenComponent, canDeactivate: [PendingChangesGuard] }]; - -@NgModule({ - declarations: [ComponentRulesScreenComponent], - imports: [ - RouterModule.forChild(routes), - CommonModule, - MonacoEditorModule, - TranslateModule, - IconButtonComponent, - FormsModule, - MatIconModule, - ], -}) -export class ComponentRulesModule {} diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.html deleted file mode 100644 index d06c91ec7..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
-
-
- - - -
-
- - - - - -
-
- - -
-
-
diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.scss deleted file mode 100644 index 0d44f017a..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.scss +++ /dev/null @@ -1,67 +0,0 @@ -:host { - flex-grow: 1; - overflow: hidden; - padding: 15px 0 0 15px; -} - -ngx-monaco-editor { - height: 100%; - width: 100%; -} - -.header-container { - display: flex; - align-items: center; - padding-bottom: 15px; - margin-left: 30px; - - .error { - color: red; - font-size: 8px; - font-weight: 500; - margin-left: 20px; - font-style: italic; - } -} - -.changes-box { - flex-direction: column; - justify-content: center; - align-items: start; - gap: 10px; - - > div { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - width: 100%; - } - - .errors { - cursor: pointer; - - .icon { - scale: 0.7; - } - - :first-child { - display: flex; - align-items: center; - gap: 0.5em; - font-weight: 500; - /* TODO: Update this to --iqser-error when added */ - color: #dd4d50; - } - - .face-up { - transform: rotate(90deg); - } - - margin-right: 0; - } - - .actions { - gap: 24px; - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html similarity index 65% rename from apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.html rename to apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html index 13c6432f7..84390cb22 100644 --- a/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html @@ -1,6 +1,6 @@
-
-
+
+
@@ -9,18 +9,18 @@
- +
-
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss similarity index 100% rename from apps/red-ui/src/app/modules/admin/screens/component-rules/component-rules-screen/component-rules-screen.component.scss rename to apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts similarity index 82% rename from apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.ts rename to apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts index 2798fed49..a97a56f44 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules-screen/entity-rules-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts @@ -1,14 +1,14 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, ElementRef, OnInit, signal, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, OnInit, signal } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; -import { saveAs } from 'file-saver'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { RulesService } from '../../../services/rules.service'; import { firstValueFrom } from 'rxjs'; -import { DOSSIER_TEMPLATE_ID } from '@red/domain'; +import { DOSSIER_TEMPLATE_ID, IRules } from '@red/domain'; import { EditorThemeService } from '@services/editor-theme.service'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { Debounce, getParam } from '@iqser/common-ui/lib/utils'; +import { ActivatedRoute } from '@angular/router'; +import { rulesScreenTranslations } from '../../../translations/rules-screen-translations'; import ICodeEditor = monaco.editor.ICodeEditor; import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration; import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions; @@ -20,11 +20,13 @@ interface SyntaxError { } @Component({ - templateUrl: './entity-rules-screen.component.html', - styleUrls: ['./entity-rules-screen.component.scss'], + templateUrl: './rules-screen.component.html', + styleUrls: ['./rules-screen.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class EntityRulesScreenComponent implements OnInit, ComponentCanDeactivate { +export class RulesScreenComponent implements OnInit, ComponentCanDeactivate { + readonly translations = rulesScreenTranslations; + readonly iconButtonTypes = IconButtonTypes; readonly editorOptions: IStandaloneEditorConstructionOptions = { theme: 'vs', @@ -36,23 +38,13 @@ export class EntityRulesScreenComponent implements OnInit, ComponentCanDeactivat initialLines: string[] = []; currentLines: string[] = []; isLeaving = false; - @ViewChild('fileInput') - private _fileInput: ElementRef; + readonly type: IRules['ruleFileType']; private _codeEditor: ICodeEditor; private _decorations: string[] = []; readonly #errorGlyphs = signal([]); readonly numberOfErrors = computed(() => this.#errorGlyphs().length); readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID); - constructor( - readonly permissionsService: PermissionsService, - private readonly _rulesService: RulesService, - private readonly _changeDetectorRef: ChangeDetectorRef, - private readonly _toaster: Toaster, - private readonly _loadingService: LoadingService, - private readonly _editorThemeService: EditorThemeService, - ) {} - set isLeavingPage(isLeaving: boolean) { this.isLeaving = isLeaving; this._changeDetectorRef.detectChanges(); @@ -72,6 +64,18 @@ export class EntityRulesScreenComponent implements OnInit, ComponentCanDeactivat this._closeProblemsView(); } + constructor( + readonly permissionsService: PermissionsService, + private readonly _rulesService: RulesService, + private readonly _changeDetectorRef: ChangeDetectorRef, + private readonly _toaster: Toaster, + private readonly _loadingService: LoadingService, + private readonly _editorThemeService: EditorThemeService, + private readonly _route: ActivatedRoute, + ) { + this.type = this._route.snapshot.data.type; + } + async ngOnInit() { await this._initialize(); } @@ -102,17 +106,18 @@ export class EntityRulesScreenComponent implements OnInit, ComponentCanDeactivat this._rulesService.uploadRules({ rules: this._codeEditor.getModel().getValue(), dossierTemplateId: this.#dossierTemplateId, + ruleFileType: this.type, }), ).then( async () => { await this._initialize(); - this._toaster.success(_('entity-rules-screen.success.generic')); + this._toaster.success(rulesScreenTranslations[this.type]['success-generic']); }, error => { const errors = error.error as SyntaxError[] | undefined; this._drawErrorMarkers(errors); this._loadingService.stop(); - this._toaster.error(_('entity-rules-screen.error.generic')); + this._toaster.success(rulesScreenTranslations[this.type]['error-generic']); }, ); } @@ -125,27 +130,6 @@ export class EntityRulesScreenComponent implements OnInit, ComponentCanDeactivat this._loadingService.stop(); } - download(): void { - const content = this._codeEditor.getModel().getValue(); - const blob = new Blob([content], { - type: 'text/plain;charset=utf-8', - }); - saveAs(blob, 'rules.txt'); - } - - upload($event): void { - const file: File = $event.target.files[0]; - const fileReader = new FileReader(); - - if (file) { - fileReader.onload = () => { - this._codeEditor.getModel().setValue(fileReader.result as string); - this._fileInput.nativeElement.value = null; - }; - fileReader.readAsText(file); - } - } - private _isNew(entry: string): boolean { return this.initialLines.indexOf(entry) < 0 && entry?.trim().length > 0; } @@ -189,11 +173,11 @@ export class EntityRulesScreenComponent implements OnInit, ComponentCanDeactivat (window as any).monaco.editor.setModelMarkers(model, model.id, markers); } - private _closeProblemsView() { + private _closeProblemsView(): void { this._codeEditor.trigger(null, 'closeMarkersNavigation', null); } - private _removeErrorMarkers() { + private _removeErrorMarkers(): void { const model = this._codeEditor?.getModel(); if (!model) { return; @@ -205,7 +189,7 @@ export class EntityRulesScreenComponent implements OnInit, ComponentCanDeactivat private async _initialize() { this._loadingService.start(); - await firstValueFrom(this._rulesService.download(this.#dossierTemplateId)).then( + await firstValueFrom(this._rulesService.download(this.#dossierTemplateId, this.type)).then( rules => { this.currentLines = this.initialLines = rules.rules.split('\n'); this.revert(); diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules.module.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules.module.ts similarity index 71% rename from apps/red-ui/src/app/modules/admin/screens/rules/entity-rules.module.ts rename to apps/red-ui/src/app/modules/admin/screens/rules/rules.module.ts index 901f1f727..e6b248714 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/entity-rules.module.ts +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; -import { EntityRulesScreenComponent } from './entity-rules-screen/entity-rules-screen.component'; +import { RulesScreenComponent } from './rules-screen/rules-screen.component'; import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; import { PendingChangesGuard } from '@guards/can-deactivate.guard'; import { TranslateModule } from '@ngx-translate/core'; @@ -9,10 +9,10 @@ import { IconButtonComponent } from '@iqser/common-ui'; import { FormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; -const routes = [{ path: '', component: EntityRulesScreenComponent, canDeactivate: [PendingChangesGuard] }]; +const routes = [{ path: '', component: RulesScreenComponent, canDeactivate: [PendingChangesGuard] }]; @NgModule({ - declarations: [EntityRulesScreenComponent], + declarations: [RulesScreenComponent], imports: [ RouterModule.forChild(routes), CommonModule, @@ -23,4 +23,4 @@ const routes = [{ path: '', component: EntityRulesScreenComponent, canDeactivate MatIconModule, ], }) -export class EntityRulesModule {} +export class RulesModule {} diff --git a/apps/red-ui/src/app/modules/admin/translations/rules-screen-translations.ts b/apps/red-ui/src/app/modules/admin/translations/rules-screen-translations.ts new file mode 100644 index 000000000..f802b8397 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/translations/rules-screen-translations.ts @@ -0,0 +1,22 @@ +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; + +export const rulesScreenTranslations: Record<'ENTITY' | 'COMPONENT', Record> = { + ENTITY: { + 'success.generic': _('entity-rules-screen.success.generic'), + 'error.generic': _('entity-rules-screen.error.generic'), + title: _('entity-rules-screen.title'), + 'warning-text': _('entity-rules-screen.warning-text'), + 'errors-found': _('entity-rules-screen.errors-found'), + 'save-changes': _('entity-rules-screen.save-changes'), + 'revert-changes': _('entity-rules-screen.revert-changes'), + }, + COMPONENT: { + 'success.generic': _('component-rules-screen.success.generic'), + 'error.generic': _('component-rules-screen.error.generic'), + title: _('component-rules-screen.title'), + 'warning-text': _('component-rules-screen.warning-text'), + 'errors-found': _('component-rules-screen.errors-found'), + 'save-changes': _('component-rules-screen.save-changes'), + 'revert-changes': _('component-rules-screen.revert-changes'), + }, +} as const;