@verisoft/ui-core 0.0.1 → 18.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +6 -3
  2. package/src/index.ts +3 -1
  3. package/src/lib/common/constants.ts +5 -1
  4. package/src/lib/common/control.models.ts +14 -0
  5. package/src/lib/common/datasource-component.model.ts +6 -4
  6. package/src/lib/common/deactivate-guard.model.ts +5 -0
  7. package/src/lib/common/download-file.ts +20 -0
  8. package/src/lib/common/filter.ts +7 -0
  9. package/src/lib/common/icons.ts +34 -0
  10. package/src/lib/common/index.ts +7 -1
  11. package/src/lib/common/notificable-property.model.ts +5 -0
  12. package/src/lib/common/rxjs.spec.ts +58 -0
  13. package/src/lib/common/rxjs.ts +21 -0
  14. package/src/lib/components/action-button-group/action-button-group.model.ts +9 -10
  15. package/src/lib/components/action-button-group/action-button.model.ts +14 -15
  16. package/src/lib/components/base-form/base-form-input.component.ts +7 -1
  17. package/src/lib/components/base-form/base-form.component.ts +33 -6
  18. package/src/lib/components/base-form/directives/detail-store.directive.ts +104 -31
  19. package/src/lib/components/breadcrumb/breadcrumbcore.component.ts +50 -29
  20. package/src/lib/components/button/button.model.ts +0 -1
  21. package/src/lib/components/calendar/calendar.model.ts +1 -1
  22. package/src/lib/components/checkbox/checkbox.model.ts +1 -2
  23. package/src/lib/components/confirm-dialog/confirm-dialog.model.ts +22 -17
  24. package/src/lib/components/confirm-dialog/index.ts +1 -1
  25. package/src/lib/components/dropdown/dropdown.model.ts +3 -0
  26. package/src/lib/components/dynamic-component/dynamic-component.model.ts +2 -0
  27. package/src/lib/components/dynamic-component/index.ts +1 -0
  28. package/src/lib/components/filter/filter.model.ts +17 -0
  29. package/src/lib/components/filter/index.ts +1 -0
  30. package/src/lib/components/generic-field/generic-field.model.ts +1 -1
  31. package/src/lib/components/generic-form/generic-form.component.ts +33 -0
  32. package/src/lib/components/generic-form/index.ts +1 -0
  33. package/src/lib/components/header/header.model.ts +2 -2
  34. package/src/lib/components/icons/icons.component.ts +17 -0
  35. package/src/lib/components/icons/icons.model.ts +10 -0
  36. package/src/lib/components/icons/index.ts +2 -0
  37. package/src/lib/components/index.ts +4 -0
  38. package/src/lib/components/loader/loader.model.ts +1 -2
  39. package/src/lib/components/page-header/index.ts +3 -1
  40. package/src/lib/components/page-header/page-header.model.ts +1 -7
  41. package/src/lib/components/page-header/page-header.service.ts +9 -0
  42. package/src/lib/components/page-header/page-headercore.component.ts +40 -0
  43. package/src/lib/components/password/password.model.ts +14 -0
  44. package/src/lib/components/side-menu/directives/side-menu-service.directive.ts +31 -0
  45. package/src/lib/components/side-menu/index.ts +2 -1
  46. package/src/lib/components/side-menu/services/side-menu.service.ts +8 -4
  47. package/src/lib/components/side-menu/side-menu.model.ts +14 -11
  48. package/src/lib/components/snackbar/snackbar.model.ts +1 -2
  49. package/src/lib/components/stepper/stepper.model.ts +13 -3
  50. package/src/lib/components/switch/switch.model.ts +1 -2
  51. package/src/lib/components/tab-view/tab-view.model.ts +7 -4
  52. package/src/lib/components/table/column-configuration.ts +38 -0
  53. package/src/lib/components/table/index.ts +3 -1
  54. package/src/lib/components/table/table-builder.ts +93 -0
  55. package/src/lib/components/table/table-column.directive.ts +62 -0
  56. package/src/lib/components/table/table.models.ts +116 -44
  57. package/src/lib/components/textfield/textfield.model.ts +1 -1
  58. package/src/lib/components/tristatecheckbox/tristatecheckbox.model.ts +1 -2
  59. package/src/lib/directives/datasource.directive.ts +10 -10
  60. package/src/lib/directives/index.ts +3 -0
  61. package/src/lib/directives/shortcut.directive.ts +37 -0
  62. package/src/lib/directives/table-datasource.directive.ts +184 -0
  63. package/src/lib/directives/table-filter.directive.ts +69 -0
  64. package/src/lib/format/format.ts +74 -0
  65. package/src/lib/pipes/error/error.codes.ts +6 -1
  66. package/src/lib/pipes/helper/enumToList.pipe.ts +16 -0
  67. package/src/lib/pipes/index.ts +1 -2
  68. package/src/lib/services/confirm-dialog.service.ts +44 -0
  69. package/src/lib/services/index.ts +4 -0
  70. package/src/lib/services/leave-form.service.ts +53 -0
  71. package/src/lib/services/screen-size.service.ts +25 -0
  72. package/src/lib/services/table.service.ts +22 -0
  73. package/src/lib/components/table/template-column.directive.ts +0 -45
  74. package/src/lib/pipes/gov/gov-color.pipe.ts +0 -24
  75. package/src/lib/pipes/gov/gov-size.pipe.ts +0 -16
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@verisoft/ui-core",
3
- "version": "0.0.1",
3
+ "version": "18.3.0",
4
4
  "peerDependencies": {
5
5
  "@angular/core": "^18.2.8",
6
6
  "@angular/router": "18.2.8",
7
7
  "@verisoft/core": "18.0.0",
8
8
  "@angular/forms": "18.2.8",
9
- "primeng": "^17.18.11",
10
9
  "rxjs": "~7.8.0",
11
10
  "@angular/common": "^18.2.8",
12
11
  "lodash-es": "^4.17.21",
13
- "@verisoft/store": "18.0.0"
12
+ "@verisoft/store": "18.0.0",
13
+ "@ngrx/store": "18.0.2",
14
+ "@angular/platform-browser": "18.2.8",
15
+ "uuid": "^10.0.0",
16
+ "@ngx-translate/core": "^15.0.0"
14
17
  },
15
18
  "sideEffects": false
16
19
  }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './lib/common';
2
2
  export * from './lib/components';
3
3
  export * from './lib/directives';
4
- export * from './lib/pipes';
4
+ export * from './lib/pipes';
5
+ export * from './lib/services';
6
+ export * from './lib/format/format';
@@ -1 +1,5 @@
1
- export const DEFAULT_DEBOUNCE_TIME = 300;
1
+ export const DEFAULT_DEBOUNCE_TIME = 300;
2
+
3
+ export const DEFAULT_PAGINATION = [10, 25, 50, 100];
4
+
5
+ export const MAX_COLUMN_CHAR_COUNT = 30;
@@ -41,11 +41,23 @@ export enum FieldSize {
41
41
  large = 'large',
42
42
  }
43
43
 
44
+ export enum FieldAlign {
45
+ left = 'left',
46
+ center = 'center',
47
+ right = 'right',
48
+ }
49
+
44
50
  export enum FieldType {
45
51
  text = 'text',
46
52
  number = 'number',
47
53
  password = 'password',
48
54
  search = 'search',
55
+ date = 'date',
56
+ }
57
+
58
+ export enum LayoutType {
59
+ horizontal = 'horizontal',
60
+ vertical = 'vertical',
49
61
  }
50
62
 
51
63
  export type ControlSeverityType = keyof typeof ControlSeverity;
@@ -54,4 +66,6 @@ export type GovButtonTypeType = keyof typeof GovButtonType;
54
66
  export type IconPositionType = keyof typeof IconPosition;
55
67
  export type SlotPositionType = keyof typeof SlotPosition;
56
68
  export type FieldSizeType = keyof typeof FieldSize;
69
+ export type FieldAlignType = keyof typeof FieldAlign;
57
70
  export type FieldTypeType = keyof typeof FieldType;
71
+ export type LayoutTypeType = keyof typeof LayoutType;
@@ -1,14 +1,14 @@
1
1
  import { EventEmitter, SimpleChanges } from '@angular/core';
2
- import { FilterEvent, LazyLoadEvent } from '@verisoft/core';
2
+ import { FilterEvent, LazyLoadEvent, RequestParams } from '@verisoft/core';
3
3
 
4
4
  export interface DataSourceComponentModel<TEntity> {
5
5
  ngOnChanges?: (changes: SimpleChanges) => void;
6
6
  lazy: boolean;
7
7
  loading: boolean;
8
8
  filter: boolean;
9
- options?: TEntity[];
10
- optionValue?: string;
11
- optionLabel?: string;
9
+ options: TEntity[] | undefined;
10
+ optionValue: string | undefined;
11
+ optionLabel: string | undefined;
12
12
  showed: EventEmitter<any>;
13
13
  cleared: EventEmitter<any>;
14
14
  filtered: EventEmitter<FilterEvent>;
@@ -39,3 +39,5 @@ export function setDataToArray<T>(
39
39
 
40
40
  return targetArray;
41
41
  }
42
+
43
+ export type ExtendedRequestType<T> = RequestParams<T> & { useNewData: boolean}
@@ -0,0 +1,5 @@
1
+ import { Observable } from "rxjs";
2
+
3
+ export interface PreventUnsavedChangesCore {
4
+ canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
5
+ }
@@ -0,0 +1,20 @@
1
+ export function downloadText(
2
+ filename: string,
3
+ text: string,
4
+ mimeType: string | undefined = 'text/plain'
5
+ ): void {
6
+ const blob = new Blob([text], { type: mimeType });
7
+ downloadFile(filename, blob);
8
+ }
9
+
10
+ export function downloadFile(
11
+ filename: string,
12
+ blob: Blob
13
+ ): void {
14
+ const url = window.URL.createObjectURL(blob);
15
+ const a = document.createElement('a');
16
+ a.href = url;
17
+ a.download = filename;
18
+ a.click();
19
+ window.URL.revokeObjectURL(url);
20
+ }
@@ -0,0 +1,7 @@
1
+ export function isFilterEmpty<T>(filter: Partial<T> | undefined) {
2
+ if (filter == undefined) {
3
+ return true;
4
+ }
5
+
6
+ return !Object.entries(filter).some((x) => x[1] != undefined);
7
+ }
@@ -0,0 +1,34 @@
1
+ export interface CommonIcons {
2
+ add: string;
3
+ minus: string;
4
+ delete: string;
5
+ filter: string;
6
+ download: string;
7
+ save: string;
8
+ print: string;
9
+ edit: string;
10
+ settings: string;
11
+ house: string;
12
+ calendar: string;
13
+ chevronRight: string;
14
+ chevronLeft: string;
15
+ chevronDown: string;
16
+ chevronUp: string;
17
+ checkbox: string;
18
+ warning: string;
19
+ search: string;
20
+ action: string;
21
+ user: string;
22
+ logout: string;
23
+ crossCircle: string;
24
+ infoCircle: string;
25
+ cross: string;
26
+ arrowLeft: string;
27
+ arrowRight: string;
28
+ questionCircle: string;
29
+ checkCircle: string;
30
+ sitemap: string;
31
+ check: string;
32
+ envelope: string;
33
+ loader: string;
34
+ }
@@ -1,4 +1,10 @@
1
1
  export * from './angular-helper';
2
2
  export * from './control.models';
3
3
  export * from './constants';
4
- export * from './datasource-component.model';
4
+ export * from './datasource-component.model';
5
+ export * from './filter';
6
+ export * from './notificable-property.model';
7
+ export * from './rxjs';
8
+ export * from './icons';
9
+ export * from './download-file';
10
+ export * from './deactivate-guard.model'
@@ -0,0 +1,5 @@
1
+ import { Observable } from "rxjs";
2
+
3
+ export interface NotificableProperty {
4
+ propertyChanged: Observable<unknown>;
5
+ }
@@ -0,0 +1,58 @@
1
+ import { QueryList } from '@angular/core';
2
+ import { skip, Subject } from 'rxjs';
3
+ import { NotificableProperty } from './notificable-property.model';
4
+ import { queryListChanged } from './rxjs';
5
+
6
+ describe('queryListChanged', () => {
7
+ let queryList: QueryList<NotificableProperty>;
8
+
9
+ beforeEach(() => {
10
+ queryList = new QueryList<NotificableProperty>();
11
+ });
12
+
13
+ it('should emit initial value', (done) => {
14
+ queryListChanged(queryList).subscribe((result) => {
15
+ expect(result).toEqual([]);
16
+ done();
17
+ });
18
+ });
19
+
20
+ it('should emit when propertyChanged emits', (done) => {
21
+ const propertyChanged = new Subject<void>();
22
+ const item = { propertyChanged } as NotificableProperty;
23
+ queryList.reset([item]);
24
+ queryList.notifyOnChanges();
25
+
26
+ queryListChanged(queryList)
27
+ .pipe(skip(1))
28
+ .subscribe((result) => {
29
+ expect(result).toEqual([item]);
30
+ done();
31
+ });
32
+
33
+ setTimeout(() => {
34
+ propertyChanged.next();
35
+ }, 60);
36
+ });
37
+
38
+ it('should debounce emissions', (done) => {
39
+ const propertyChanged = new Subject<void>();
40
+ const item = { propertyChanged } as NotificableProperty;
41
+ queryList.reset([item]);
42
+ queryList.notifyOnChanges();
43
+
44
+ const emittedValues: NotificableProperty[][] = [];
45
+ queryListChanged(queryList).subscribe((result) => {
46
+ emittedValues.push(result);
47
+ });
48
+
49
+ propertyChanged.next();
50
+ propertyChanged.next();
51
+
52
+ setTimeout(() => {
53
+ expect(emittedValues.length).toBe(1);
54
+ expect(emittedValues[0]).toEqual([item]);
55
+ done();
56
+ }, 100);
57
+ });
58
+ });
@@ -0,0 +1,21 @@
1
+ import { QueryList } from '@angular/core';
2
+ import { debounceTime, map, merge, startWith, switchMap } from 'rxjs';
3
+ import { NotificableProperty } from './notificable-property.model';
4
+
5
+ export function queryListChanged<TEntity>(list: QueryList<TEntity>) {
6
+ return list.changes.pipe(
7
+ startWith({}),
8
+ switchMap(() => {
9
+ const actionPropertyChanges$ = list
10
+ .toArray()
11
+ .filter((action) => (<NotificableProperty>action).propertyChanged)
12
+ .map((action) => (<NotificableProperty>action).propertyChanged);
13
+
14
+ return merge(...actionPropertyChanges$).pipe(
15
+ startWith({}),
16
+ map(() => list.toArray())
17
+ );
18
+ }),
19
+ debounceTime(50)
20
+ );
21
+ }
@@ -2,15 +2,14 @@ import { InjectionToken } from '@angular/core';
2
2
  import { IconPositionType } from '../../common';
3
3
  import { ActionButton } from './action-button.model';
4
4
 
5
- export const ACTION_BUTTON_GROUP_COMPONENT_TOKEN = new InjectionToken<ActionButtonGroupCore>(
6
- 'ActionButtonGroupComponentToken'
7
- );
5
+ export const ACTION_BUTTON_GROUP_COMPONENT_TOKEN =
6
+ new InjectionToken<ActionButtonGroupCore>('ActionButtonGroupComponentToken');
8
7
 
9
8
  export interface ActionButtonGroupCore {
10
- maxItems: number;
11
- maxItemsMobile: number;
12
- items: ActionButton[]
13
- menuIconPos: IconPositionType;
14
- menuIcon: string;
15
- label?: string;
16
- }
9
+ maxItems: number;
10
+ maxItemsMobile: number;
11
+ items: ActionButton[];
12
+ menuIconPos: IconPositionType;
13
+ menuIcon: string;
14
+ label?: string;
15
+ }
@@ -1,16 +1,15 @@
1
- import { EventEmitter } from "@angular/core";
2
- import { ControlSeverityType, FieldSizeType } from "../../common";
1
+ import { EventEmitter } from '@angular/core';
2
+ import { ControlSeverityType, FieldSizeType, NotificableProperty } from '../../common';
3
3
 
4
- export interface ActionButton {
5
- disabled: boolean;
6
- toolTip?: string;
7
- id?: string;
8
- icon?: string;
9
- outlined: boolean;
10
- raised: boolean;
11
- text: boolean;
12
- severity?: ControlSeverityType;
13
- label?: string;
14
- size?: FieldSizeType;
15
- clickEvent: EventEmitter<void>;
16
- }
4
+ export interface ActionButton extends NotificableProperty {
5
+ disabled: boolean;
6
+ toolTip?: string;
7
+ id?: string;
8
+ icon?: string;
9
+ outlined: boolean;
10
+ raised: boolean;
11
+ severity?: ControlSeverityType;
12
+ label?: string;
13
+ size?: FieldSizeType;
14
+ click: EventEmitter<MouseEvent>;
15
+ }
@@ -1,5 +1,5 @@
1
1
  import { CommonModule } from '@angular/common';
2
- import { Component, Input, OnInit } from '@angular/core';
2
+ import { Component, inject, Input, OnInit } from '@angular/core';
3
3
  import {
4
4
  AbstractControl,
5
5
  ControlValueAccessor,
@@ -11,6 +11,7 @@ import {
11
11
  NgModel,
12
12
  ReactiveFormsModule,
13
13
  } from '@angular/forms';
14
+ import { ERROR_PROVIDER_TOKEN } from '@verisoft/core';
14
15
  import { BaseInputControls } from './models/base-form-input.models';
15
16
 
16
17
  const noop = () => {
@@ -27,6 +28,8 @@ export class BaseFormInputComponent
27
28
  {
28
29
  readonly ngControl?: NgControl;
29
30
 
31
+ readonly errorService = inject(ERROR_PROVIDER_TOKEN);
32
+
30
33
  formControl!: FormControl;
31
34
 
32
35
  constructor(private readonly control: NgControl) {
@@ -45,6 +48,9 @@ export class BaseFormInputComponent
45
48
  @Input()
46
49
  readonly!: boolean;
47
50
 
51
+ @Input()
52
+ disabled!: boolean;
53
+
48
54
  @Input()
49
55
  tooltip!: string;
50
56
 
@@ -3,6 +3,7 @@ import {
3
3
  ChangeDetectorRef,
4
4
  Directive,
5
5
  EventEmitter,
6
+ HostListener,
6
7
  inject,
7
8
  Input,
8
9
  OnChanges,
@@ -10,19 +11,21 @@ import {
10
11
  OnInit,
11
12
  Output,
12
13
  SimpleChanges,
14
+ ViewChild,
13
15
  } from '@angular/core';
14
16
  import { FormGroup } from '@angular/forms';
15
17
  import { cloneDeep } from 'lodash-es';
16
- import { Subject, takeUntil, filter, map } from 'rxjs';
18
+ import { Subject, takeUntil, filter, map, Observable } from 'rxjs';
19
+ import { PreventUnsavedChangesCore } from '../../common';
20
+ import { PreventUnsavedChangesDirective } from '../../services';
17
21
  import { FormState, isFormStateEqual } from './models';
18
-
19
22
  @Directive({
20
23
  // eslint-disable-next-line @angular-eslint/directive-selector
21
24
  selector: '[v-baseForm]',
22
25
  standalone: true,
23
26
  })
24
27
  export abstract class BaseFormDirective<T extends object>
25
- implements OnInit, OnChanges, OnDestroy, AfterViewInit
28
+ implements OnInit, OnChanges, OnDestroy, AfterViewInit, PreventUnsavedChangesCore
26
29
  {
27
30
  @Input() data!: T | any;
28
31
  @Output() dataChange = new EventEmitter<T>();
@@ -36,13 +39,32 @@ export abstract class BaseFormDirective<T extends object>
36
39
  cd = inject(ChangeDetectorRef);
37
40
  valueInitialization = false;
38
41
  lastState!: FormState;
42
+ guardViewChild!: ViewChild;
43
+ formSubmitted = false;
44
+ protected guard = inject(PreventUnsavedChangesDirective);
45
+
46
+ @HostListener('window:beforeunload', ['$event'])
47
+ unloadHandler(event: BeforeUnloadEvent) {
48
+ if (this.formGroup.dirty) {
49
+ event.preventDefault();
50
+ }
51
+ }
52
+
53
+ canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
54
+ if (this.formGroup.dirty && !this.formSubmitted) {
55
+ const result = this.guard.showConfirmationDialog();
56
+ return result;
57
+ }
58
+
59
+ return true;
60
+ }
39
61
 
40
62
  ngOnInit(): void {
41
63
  this.initializeFormGroup();
42
64
  }
43
65
 
44
66
  ngOnChanges(changes: SimpleChanges): void {
45
- if (changes['initialData'] && this.formGroup) {
67
+ if (changes['data'] && this.formGroup) {
46
68
  this.valueInitialization = true;
47
69
  const dirty = this.formGroup.dirty;
48
70
 
@@ -84,11 +106,12 @@ export abstract class BaseFormDirective<T extends object>
84
106
  }
85
107
 
86
108
  submit() {
109
+ this.formSubmitted = true;
87
110
  this.formGroup.markAllAsTouched();
88
111
  if (!this.formGroup.invalid) {
89
112
  this.formSubmit.emit(this.createCompleteData());
90
113
  }
91
- this.formGroup.markAsUntouched();
114
+ this.formGroup.markAsPristine();
92
115
  }
93
116
 
94
117
  clear() {
@@ -203,7 +226,11 @@ export abstract class BaseFormDirective<T extends object>
203
226
  private transformEmptyStringToNullStringFn(obj: any, key: string) {
204
227
  // if empty string - transformation to null string
205
228
  if (typeof obj[key] === 'string' && obj[key] === '') {
206
- obj[key] = null;
229
+ try {
230
+ obj[key] = null;
231
+ } catch (error) {
232
+ console.error(`Cannot modify ${key}: ${error}`)
233
+ }
207
234
  }
208
235
  }
209
236
  }
@@ -10,9 +10,12 @@ import {
10
10
  import { ActivatedRoute } from '@angular/router';
11
11
  import { createFeatureSelector, createSelector, Store } from '@ngrx/store';
12
12
  import {
13
+ BackendValidationError,
13
14
  createInitDetailAction,
15
+ createInitNewDetailAction,
14
16
  createResetStateAction,
15
17
  createUpdateDetailAction,
18
+ createUpdateDetailSetErrorsAction,
16
19
  createUpdateFormStateAction,
17
20
  DetailState,
18
21
  } from '@verisoft/store';
@@ -30,17 +33,27 @@ export class DetailStoreDirective
30
33
  extends UnsubscribeComponent
31
34
  implements OnInit, AfterViewInit, OnDestroy
32
35
  {
33
- store = inject(Store);
34
- cdr = inject(ChangeDetectorRef);
35
- route = inject(ActivatedRoute);
36
- @Input({ required: true })
37
- form!: BaseFormDirective<any>;
36
+ @Input({ required: true }) form!: BaseFormDirective<any>;
37
+
38
38
  @Input({ required: true }) detailsRepository!: string;
39
+
39
40
  @Input() autoBind = true;
41
+
40
42
  @Input() detailId!: string | number | undefined;
43
+
41
44
  @Input({ required: true }) ngrxFeatureKey!: string;
45
+
42
46
  @Input() destroyForm = true;
43
47
 
48
+ @Input() readonly = false;
49
+
50
+ @Input() readonlyControlNames: string[] = [];
51
+
52
+ store = inject(Store);
53
+ cdr = inject(ChangeDetectorRef);
54
+ route = inject(ActivatedRoute);
55
+
56
+ private itemCache: any = null;
44
57
  private loaded!: boolean;
45
58
 
46
59
  ngOnInit(): void {
@@ -65,9 +78,22 @@ export class DetailStoreDirective
65
78
  }
66
79
 
67
80
  private initForm() {
68
- this.store.dispatch(
69
- createInitDetailAction(this.detailsRepository)({ obj: this.detailId })
70
- );
81
+ if (this.detailId === 'create') {
82
+ this.store.dispatch(createInitNewDetailAction(this.detailsRepository)());
83
+ } else {
84
+ this.store.dispatch(
85
+ createInitDetailAction(this.detailsRepository)({ obj: this.detailId })
86
+ );
87
+ }
88
+
89
+ if (this.readonly) {
90
+ this.form.formGroup.disable();
91
+ return;
92
+ }
93
+
94
+ this.readonlyControlNames.forEach(x => {
95
+ this.form.formGroup.get(x)?.disable();
96
+ });
71
97
  }
72
98
 
73
99
  private listenFormState() {
@@ -78,29 +104,46 @@ export class DetailStoreDirective
78
104
  this.store
79
105
  .select(selectIncomeData)
80
106
  .pipe(takeUntil(this.destroyed$))
81
- .subscribe(({ item, loaded } = { item: undefined, loaded: false }) => {
82
- if (
83
- item &&
84
- (item.validationErrors || item.validationWarnings) &&
85
- !this.form.formGroup.dirty
86
- ) {
87
- this.handleValidation(
88
- 'propertyName',
89
- item.validationWarnings || [],
90
- 'validationWarning',
91
- 'warningMessage'
92
- );
93
- this.handleValidation(
94
- 'propertyName',
95
- item.validationErrors || [],
96
- 'validationError',
97
- 'errorMessage'
98
- );
99
- }
107
+ .subscribe(
108
+ (
109
+ { item, loaded, backendValidationErrors } = {
110
+ item: undefined,
111
+ loaded: false,
112
+ saveItemState: { saveInProgress: false },
113
+ backendValidationErrors: []
114
+ }
115
+ ) => {
116
+ if (item
117
+ && ((item.validationErrors || item.validationWarnings) && !this.form.formGroup.dirty)
118
+ || backendValidationErrors.length
119
+ ) {
120
+ this.handleValidation(
121
+ 'propertyName',
122
+ item.validationWarnings || [],
123
+ 'validationWarning',
124
+ 'warningMessage'
125
+ );
126
+ this.handleValidation(
127
+ 'propertyName',
128
+ item.validationErrors || [],
129
+ 'validationError',
130
+ 'errorMessage'
131
+ );
100
132
 
101
- this.loaded = loaded;
102
- this.cdr.detectChanges();
103
- });
133
+ if (this.itemCache && this.isStateChanged(item) && backendValidationErrors.length) {
134
+ backendValidationErrors = this.dispatchErrors(item, backendValidationErrors);
135
+ }
136
+
137
+ this.handleBackendValidation(backendValidationErrors);
138
+ this.cdr.markForCheck();
139
+ }
140
+
141
+ this.itemCache = item;
142
+
143
+ this.loaded = loaded;
144
+ this.cdr.detectChanges();
145
+ }
146
+ );
104
147
  }
105
148
 
106
149
  private listenFormChange() {
@@ -141,6 +184,36 @@ export class DetailStoreDirective
141
184
  control.markAsDirty();
142
185
  }
143
186
  });
144
- this.cdr.markForCheck();
145
187
  };
188
+
189
+ private handleBackendValidation(errors: BackendValidationError[]) {
190
+ errors.forEach(({ parameters, code }: BackendValidationError) => {
191
+ const control = this.form.formGroup.get(this.normalizePropertyNames(parameters));
192
+ if (!control) return;
193
+
194
+ control[control.disabled ? "disable" : "enable"]({ emitEvent: false, onlySelf: true });
195
+ control.setErrors({ "validationError": code }, { emitEvent: true });
196
+ control.markAsDirty();
197
+ });
198
+ }
199
+
200
+ private dispatchErrors(item: any, errors: BackendValidationError[]): BackendValidationError[] {
201
+ const error = errors.filter((e: BackendValidationError) => {
202
+ return this.itemCache[this.normalizePropertyNames(e.parameters)] === item[this.normalizePropertyNames(e.parameters)];
203
+ });
204
+
205
+ this.store.dispatch(
206
+ createUpdateDetailSetErrorsAction(this.detailsRepository)({ error })
207
+ );
208
+
209
+ return error;
210
+ }
211
+
212
+ private normalizePropertyNames(input: string): string {
213
+ return String(input[0]).toLocaleLowerCase() + String(input).slice(1);
214
+ }
215
+
216
+ private isStateChanged(item: any): boolean {
217
+ return item !== this.itemCache;
218
+ }
146
219
  }