@verisoft/ui-core 18.0.0 → 18.3.1
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.
- package/package.json +7 -4
- package/src/index.ts +3 -1
- package/src/lib/common/constants.ts +5 -1
- package/src/lib/common/control.models.ts +14 -0
- package/src/lib/common/datasource-component.model.ts +6 -4
- package/src/lib/common/deactivate-guard.model.ts +5 -0
- package/src/lib/common/download-file.ts +20 -0
- package/src/lib/common/filter.ts +7 -0
- package/src/lib/common/icons.ts +34 -0
- package/src/lib/common/index.ts +7 -1
- package/src/lib/common/notificable-property.model.ts +5 -0
- package/src/lib/common/rxjs.spec.ts +58 -0
- package/src/lib/common/rxjs.ts +21 -0
- package/src/lib/components/action-button-group/action-button-group.model.ts +9 -10
- package/src/lib/components/action-button-group/action-button.model.ts +14 -15
- package/src/lib/components/base-form/base-form-input.component.ts +7 -1
- package/src/lib/components/base-form/base-form.component.ts +33 -6
- package/src/lib/components/base-form/directives/detail-store.directive.ts +104 -31
- package/src/lib/components/breadcrumb/breadcrumbcore.component.ts +50 -29
- package/src/lib/components/button/button.model.ts +0 -1
- package/src/lib/components/calendar/calendar.model.ts +1 -1
- package/src/lib/components/checkbox/checkbox.model.ts +1 -2
- package/src/lib/components/confirm-dialog/confirm-dialog.model.ts +22 -17
- package/src/lib/components/confirm-dialog/index.ts +1 -1
- package/src/lib/components/dropdown/dropdown.model.ts +3 -0
- package/src/lib/components/dynamic-component/dynamic-component.model.ts +2 -0
- package/src/lib/components/dynamic-component/index.ts +1 -0
- package/src/lib/components/filter/filter.model.ts +17 -0
- package/src/lib/components/filter/index.ts +1 -0
- package/src/lib/components/generic-field/generic-field.model.ts +1 -1
- package/src/lib/components/generic-form/generic-form.component.ts +33 -0
- package/src/lib/components/generic-form/index.ts +1 -0
- package/src/lib/components/header/header.model.ts +2 -2
- package/src/lib/components/icons/icons.component.ts +17 -0
- package/src/lib/components/icons/icons.model.ts +10 -0
- package/src/lib/components/icons/index.ts +2 -0
- package/src/lib/components/index.ts +4 -0
- package/src/lib/components/loader/loader.model.ts +1 -2
- package/src/lib/components/page-header/index.ts +3 -1
- package/src/lib/components/page-header/page-header.model.ts +1 -7
- package/src/lib/components/page-header/page-header.service.ts +9 -0
- package/src/lib/components/page-header/page-headercore.component.ts +40 -0
- package/src/lib/components/password/password.model.ts +14 -0
- package/src/lib/components/side-menu/directives/side-menu-service.directive.ts +31 -0
- package/src/lib/components/side-menu/index.ts +2 -1
- package/src/lib/components/side-menu/services/side-menu.service.ts +8 -4
- package/src/lib/components/side-menu/side-menu.model.ts +14 -11
- package/src/lib/components/snackbar/snackbar.model.ts +1 -2
- package/src/lib/components/stepper/stepper.model.ts +13 -3
- package/src/lib/components/switch/switch.model.ts +1 -2
- package/src/lib/components/tab-view/tab-view.model.ts +7 -4
- package/src/lib/components/table/column-configuration.ts +38 -0
- package/src/lib/components/table/index.ts +3 -1
- package/src/lib/components/table/table-builder.ts +93 -0
- package/src/lib/components/table/table-column.directive.ts +62 -0
- package/src/lib/components/table/table.models.ts +116 -44
- package/src/lib/components/textfield/textfield.model.ts +1 -1
- package/src/lib/components/tristatecheckbox/tristatecheckbox.model.ts +1 -2
- package/src/lib/directives/datasource.directive.ts +10 -10
- package/src/lib/directives/index.ts +3 -0
- package/src/lib/directives/shortcut.directive.ts +37 -0
- package/src/lib/directives/table-datasource.directive.ts +184 -0
- package/src/lib/directives/table-filter.directive.ts +69 -0
- package/src/lib/format/format.ts +74 -0
- package/src/lib/pipes/error/error.codes.ts +6 -1
- package/src/lib/pipes/helper/enumToList.pipe.ts +16 -0
- package/src/lib/pipes/index.ts +1 -2
- package/src/lib/services/confirm-dialog.service.ts +44 -0
- package/src/lib/services/index.ts +4 -0
- package/src/lib/services/leave-form.service.ts +53 -0
- package/src/lib/services/screen-size.service.ts +25 -0
- package/src/lib/services/table.service.ts +22 -0
- package/src/lib/components/table/template-column.directive.ts +0 -45
- package/src/lib/pipes/gov/gov-color.pipe.ts +0 -24
- 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": "18.
|
|
3
|
+
"version": "18.3.1",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@angular/core": "^18.2.8",
|
|
6
6
|
"@angular/router": "18.2.8",
|
|
7
|
-
"@verisoft/core": "18.
|
|
7
|
+
"@verisoft/core": "18.3.1",
|
|
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.
|
|
12
|
+
"@verisoft/store": "18.3.1",
|
|
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
|
@@ -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
|
|
10
|
-
optionValue
|
|
11
|
-
optionLabel
|
|
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,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,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
|
+
}
|
package/src/lib/common/index.ts
CHANGED
|
@@ -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,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 =
|
|
6
|
-
|
|
7
|
-
);
|
|
5
|
+
export const ACTION_BUTTON_GROUP_COMPONENT_TOKEN =
|
|
6
|
+
new InjectionToken<ActionButtonGroupCore>('ActionButtonGroupComponentToken');
|
|
8
7
|
|
|
9
8
|
export interface ActionButtonGroupCore {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
2
|
-
import { ControlSeverityType, FieldSizeType } from
|
|
1
|
+
import { EventEmitter } from '@angular/core';
|
|
2
|
+
import { ControlSeverityType, FieldSizeType, NotificableProperty } from '../../common';
|
|
3
3
|
|
|
4
|
-
export interface ActionButton {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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['
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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.
|
|
69
|
-
|
|
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(
|
|
82
|
-
|
|
83
|
-
item
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
}
|