@verisoft/ui-core 20.0.0 → 20.1.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.
Files changed (137) hide show
  1. package/.eslintrc.json +48 -0
  2. package/jest.config.ts +21 -0
  3. package/ng-package.json +11 -0
  4. package/package.json +6 -18
  5. package/project.json +36 -0
  6. package/src/index.ts +6 -0
  7. package/src/lib/common/angular-helper.ts +44 -0
  8. package/src/lib/common/constants.ts +5 -0
  9. package/src/lib/common/control.models.ts +80 -0
  10. package/src/lib/common/datasource-component.model.spec.ts +42 -0
  11. package/src/lib/common/datasource-component.model.ts +43 -0
  12. package/src/lib/common/deactivate-guard.model.ts +5 -0
  13. package/src/lib/common/download-file.ts +20 -0
  14. package/src/lib/common/filter.ts +7 -0
  15. package/src/lib/common/icons.ts +34 -0
  16. package/src/lib/common/index.ts +10 -0
  17. package/src/lib/common/notificable-property.model.ts +5 -0
  18. package/src/lib/common/rxjs.spec.ts +58 -0
  19. package/src/lib/common/rxjs.ts +21 -0
  20. package/src/lib/components/action-button-group/action-button-group.model.ts +15 -0
  21. package/src/lib/components/action-button-group/action-button.model.ts +15 -0
  22. package/src/lib/components/action-button-group/index.ts +2 -0
  23. package/src/lib/components/base-form/base-form-input.component.ts +120 -0
  24. package/src/lib/components/base-form/base-form.component.ts +236 -0
  25. package/src/lib/components/base-form/directives/detail-store.directive.ts +219 -0
  26. package/src/lib/components/base-form/index.ts +4 -0
  27. package/src/lib/components/base-form/models/base-form-input.models.ts +11 -0
  28. package/src/lib/components/base-form/models/base-form.models.ts +31 -0
  29. package/src/lib/components/base-form/models/index.ts +2 -0
  30. package/src/lib/components/breadcrumb/breadcrumb.model.ts +21 -0
  31. package/src/lib/components/breadcrumb/breadcrumb.service.ts +9 -0
  32. package/src/lib/components/breadcrumb/breadcrumbcore.component.ts +117 -0
  33. package/src/lib/components/breadcrumb/index.ts +3 -0
  34. package/src/lib/components/button/button.model.ts +21 -0
  35. package/src/lib/components/button/index.ts +1 -0
  36. package/src/lib/components/calendar/calendar.model.ts +16 -0
  37. package/src/lib/components/calendar/index.ts +1 -0
  38. package/src/lib/components/checkbox/checkbox.model.ts +10 -0
  39. package/src/lib/components/checkbox/index.ts +1 -0
  40. package/src/lib/components/confirm-dialog/confirm-dialog.model.ts +31 -0
  41. package/src/lib/components/confirm-dialog/index.ts +1 -0
  42. package/src/lib/components/dropdown/dropdown.model.ts +16 -0
  43. package/src/lib/components/dropdown/index.ts +1 -0
  44. package/src/lib/components/dropdown-button/dropdown-button.model.ts +18 -0
  45. package/src/lib/components/dropdown-button/index.ts +1 -0
  46. package/src/lib/components/dynamic-component/dynamic-component.model.ts +2 -0
  47. package/src/lib/components/dynamic-component/index.ts +1 -0
  48. package/src/lib/components/filter/filter.model.ts +17 -0
  49. package/src/lib/components/filter/index.ts +1 -0
  50. package/src/lib/components/form-field/form-field.model.ts +15 -0
  51. package/src/lib/components/form-field/index.ts +1 -0
  52. package/src/lib/components/generic-field/generic-field.model.ts +10 -0
  53. package/src/lib/components/generic-field/index.ts +1 -0
  54. package/src/lib/components/generic-form/generic-form.component.ts +33 -0
  55. package/src/lib/components/generic-form/index.ts +1 -0
  56. package/src/lib/components/header/header.model.ts +18 -0
  57. package/src/lib/components/header/index.ts +1 -0
  58. package/src/lib/components/icons/icons.component.ts +22 -0
  59. package/src/lib/components/icons/icons.model.ts +16 -0
  60. package/src/lib/components/icons/index.ts +2 -0
  61. package/src/lib/components/index.ts +37 -0
  62. package/src/lib/components/input-group/index.ts +1 -0
  63. package/src/lib/components/input-group/input-group.model.ts +17 -0
  64. package/src/lib/components/loader/index.ts +1 -0
  65. package/src/lib/components/loader/loader.model.ts +7 -0
  66. package/src/lib/components/multiselect/index.ts +1 -0
  67. package/src/lib/components/multiselect/mutiselect.model.ts +13 -0
  68. package/src/lib/components/number-input/index.ts +1 -0
  69. package/src/lib/components/number-input/number-input.model.ts +14 -0
  70. package/src/lib/components/page-header/index.ts +3 -0
  71. package/src/lib/components/page-header/page-header.model.ts +11 -0
  72. package/src/lib/components/page-header/page-header.service.ts +9 -0
  73. package/src/lib/components/page-header/page-headercore.component.ts +42 -0
  74. package/src/lib/components/password/index.ts +1 -0
  75. package/src/lib/components/password/password.model.ts +25 -0
  76. package/src/lib/components/radiobutton/index.ts +1 -0
  77. package/src/lib/components/radiobutton/radiobutton.model.ts +16 -0
  78. package/src/lib/components/section/index.ts +1 -0
  79. package/src/lib/components/section/section.model.ts +11 -0
  80. package/src/lib/components/side-menu/directives/side-menu-service.directive.ts +31 -0
  81. package/src/lib/components/side-menu/index.ts +4 -0
  82. package/src/lib/components/side-menu/services/side-menu-provider.service.ts +13 -0
  83. package/src/lib/components/side-menu/services/side-menu.service.ts +62 -0
  84. package/src/lib/components/side-menu/side-menu.model.ts +67 -0
  85. package/src/lib/components/slider/index.ts +1 -0
  86. package/src/lib/components/slider/slider.model.ts +13 -0
  87. package/src/lib/components/snackbar/index.ts +1 -0
  88. package/src/lib/components/snackbar/snackbar.model.ts +7 -0
  89. package/src/lib/components/stepper/index.ts +1 -0
  90. package/src/lib/components/stepper/stepper.model.ts +24 -0
  91. package/src/lib/components/switch/index.ts +1 -0
  92. package/src/lib/components/switch/switch.model.ts +8 -0
  93. package/src/lib/components/tab-view/index.ts +1 -0
  94. package/src/lib/components/tab-view/tab-view.model.ts +22 -0
  95. package/src/lib/components/table/column-configuration.ts +38 -0
  96. package/src/lib/components/table/index.ts +4 -0
  97. package/src/lib/components/table/table-builder.ts +93 -0
  98. package/src/lib/components/table/table-column.directive.ts +62 -0
  99. package/src/lib/components/table/table.models.ts +261 -0
  100. package/src/lib/components/table-filter/index.ts +1 -0
  101. package/src/lib/components/table-filter/table-filter.model.ts +22 -0
  102. package/src/lib/components/tag/index.ts +1 -0
  103. package/src/lib/components/tag/tag.model.ts +13 -0
  104. package/src/lib/components/textarea/index.ts +1 -0
  105. package/src/lib/components/textarea/textarea.model.ts +13 -0
  106. package/src/lib/components/textfield/index.ts +1 -0
  107. package/src/lib/components/textfield/textfield.model.ts +13 -0
  108. package/src/lib/components/tooltip/index.ts +1 -0
  109. package/src/lib/components/tooltip/tooltip.model.ts +13 -0
  110. package/src/lib/components/unsubscribe.component.ts +12 -0
  111. package/src/lib/directives/datasource.directive.ts +275 -0
  112. package/src/lib/directives/index.ts +4 -0
  113. package/src/lib/directives/shortcut.directive.ts +37 -0
  114. package/src/lib/directives/table-datasource.directive.ts +184 -0
  115. package/src/lib/directives/table-filter.directive.ts +69 -0
  116. package/src/lib/format/format.ts +74 -0
  117. package/src/lib/pipes/error/error.codes.ts +11 -0
  118. package/src/lib/pipes/error/error.models.ts +27 -0
  119. package/src/lib/pipes/error/error.pipe.ts +27 -0
  120. package/src/lib/pipes/error/warning.codes.ts +5 -0
  121. package/src/lib/pipes/error/warning.pipe.ts +27 -0
  122. package/src/lib/pipes/helper/enumToList.pipe.ts +16 -0
  123. package/src/lib/pipes/index.ts +7 -0
  124. package/src/lib/pipes/keyOrFn/keyOrFn.pipe.ts +23 -0
  125. package/src/lib/services/confirm-dialog.service.ts +44 -0
  126. package/src/lib/services/index.ts +4 -0
  127. package/src/lib/services/leave-form.service.ts +53 -0
  128. package/src/lib/services/screen-size.service.ts +25 -0
  129. package/src/lib/services/table.service.ts +22 -0
  130. package/src/test-setup.ts +8 -0
  131. package/tsconfig.json +28 -0
  132. package/tsconfig.lib.json +17 -0
  133. package/tsconfig.lib.prod.json +9 -0
  134. package/tsconfig.spec.json +16 -0
  135. package/fesm2022/verisoft-ui-core.mjs +0 -2021
  136. package/fesm2022/verisoft-ui-core.mjs.map +0 -1
  137. package/index.d.ts +0 -1107
@@ -0,0 +1,261 @@
1
+ import { EventEmitter, InjectionToken, TemplateRef } from '@angular/core';
2
+ import { Params } from '@angular/router';
3
+ import { LazyLoadEvent, Sort, SortDirectionType } from '@verisoft/core';
4
+ import { v4 } from 'uuid';
5
+ import { FieldAlignType } from '../../common';
6
+
7
+ export const TABLE_COMPONENT_TOKEN = new InjectionToken<TableCore<any>>(
8
+ 'TabVIewComponentToken'
9
+ );
10
+
11
+ export const TABLE_COLUMN_PROVIDER = new InjectionToken(
12
+ 'TABLE_COLUMN_PROVIDER'
13
+ );
14
+
15
+ export enum TableSelectionMode {
16
+ single = 'single',
17
+ multiple = 'multiple',
18
+ }
19
+
20
+ export enum TableButtonSeverity {
21
+ success = 'success',
22
+ info = 'info',
23
+ warning = 'warning',
24
+ danger = 'danger',
25
+ help = 'help',
26
+ primary = 'primary',
27
+ secondary = 'secondary',
28
+ contrast = 'contrast',
29
+ }
30
+
31
+ export enum ColumnVisibility {
32
+ visible = 'visible',
33
+ hidden = 'hidden',
34
+ default = 'default',
35
+ }
36
+
37
+ export type TableSelectionModeType = keyof typeof TableSelectionMode;
38
+ export type TableButtonSeverityType = keyof typeof TableButtonSeverity;
39
+ export type ColumnVisibilityType = keyof typeof ColumnVisibility;
40
+
41
+ export interface TableCore<T> {
42
+ sorters: Sort[];
43
+ columns: ColumnDefinition<T>[];
44
+ data: T[];
45
+ total: number;
46
+ filter: Partial<T> | undefined;
47
+ loading: boolean;
48
+ scrollable: boolean;
49
+ pageSize: number;
50
+ currentPage: number;
51
+ showPaginator: boolean;
52
+ sortMultiple: boolean;
53
+ lazy: boolean;
54
+ selection: T[];
55
+ selectionMode: TableSelectionModeType | undefined;
56
+ showPageSizePicker: boolean;
57
+ entityKey: string | undefined;
58
+ selectionChange: EventEmitter<T[]>;
59
+ lazyLoad: EventEmitter<LazyLoadEvent>;
60
+ maximumColumnLength: number;
61
+ disableCustomClicks: boolean;
62
+ }
63
+
64
+ export interface TableSignal {
65
+ name: string,
66
+ symbol: symbol
67
+ }
68
+
69
+ export interface ActionColumnsDefinition<T> {
70
+ severity?: TableButtonSeverity;
71
+ title?: string;
72
+ icon?: string;
73
+ tooltip?: string;
74
+ rounded?: boolean;
75
+ outlined?: boolean;
76
+ raised?: boolean;
77
+ badge?: string;
78
+ visible?: (row: T) => boolean;
79
+ routerLink?: (row: T) => string;
80
+ queryParams?: Params;
81
+ disabled?: boolean | ((row: T) => boolean);
82
+ onClick?: (row: T, event?: MouseEvent) => void;
83
+ }
84
+
85
+ export interface ColumnProvider<T> {
86
+ getDefinition(): ColumnDefinition<T>;
87
+ index: number;
88
+ }
89
+
90
+ export interface ColumnDefinition<T, _KEY = keyof T> {
91
+ id: string;
92
+ value?: (row: T, index?: number) => string;
93
+ headerName?: ((columnId: string, index?: number) => string) | string;
94
+ icon?: string | ((row?: T | undefined) => string);
95
+ type?: string;
96
+ sortable?: boolean;
97
+ format?: (row: T) => string
98
+ routerLink?: (row: T) => string | string;
99
+ queryParams?: Params;
100
+ columnClass?: string;
101
+ template?: TemplateRef<{ $implicit: T }>;
102
+ actions?: ActionColumnsDefinition<T>[];
103
+ textAlign?: FieldAlignType;
104
+ width?: string | number;
105
+ forceVisibility?: ColumnVisibilityType;
106
+ visible?: boolean;
107
+ }
108
+
109
+ export function LinkRenderer<T>(text: string, href: string) {
110
+ return (row: T, index: number | undefined) => {
111
+ return '<a href="' + href + '">' + text + '</a>';
112
+ };
113
+ }
114
+
115
+ export function routerRenderer<T>(link: string, text: string) {
116
+ return (row: T, index?: number | undefined) => {
117
+ return { text: text, link: link };
118
+ };
119
+ }
120
+
121
+ export function Renderer<T>(fnc: (row: T) => string) {
122
+ return (row: T) => {
123
+ return fnc(row);
124
+ };
125
+ }
126
+
127
+ export class ColumnModel<T> {
128
+ sortDirection: SortDirectionType | undefined = undefined;
129
+ columnClass?: string;
130
+ queryParams?: Params;
131
+ routerLink?: (row: T) => string | string;
132
+ valueGetter!: (row: T, index: number) => string;
133
+ headerGetter!: (columnId: string, index: number) => string | string;
134
+ template?: TemplateRef<{ $implicit: T }>;
135
+ actions?: ActionColumnsDefinition<T>[];
136
+ sortable?: boolean;
137
+ id: string;
138
+ // eslint-disable-next-line @typescript-eslint/ban-types
139
+ format!: Function;
140
+ textAlign?: FieldAlignType;
141
+ width: string | undefined;
142
+ forceVisibility: ColumnVisibilityType = ColumnVisibility.default;
143
+ visible = true;
144
+
145
+ constructor(readonly configuration: ColumnDefinition<T>) {
146
+ this.id = this.configuration.id;
147
+
148
+ if (this.configuration.format) {
149
+ this.format = this.configuration.format;
150
+ }
151
+
152
+ if (this.configuration.value) {
153
+ this.valueGetter = (row, index) => {
154
+ const value = this.configuration.value?.(row, index) ?? '-';
155
+ return this.format ? this.format(value, row) : value;
156
+ };
157
+ } else {
158
+ this.valueGetter = (row) => {
159
+ const value = ((row as { [key: string]: string })?.[this.configuration.id]) ?? '';
160
+ return this.format ? this.format(value, row) : value;
161
+ };
162
+ }
163
+
164
+ if (this.configuration.actions) {
165
+ this.actions = this.configuration.actions;
166
+ }
167
+
168
+ if (this.configuration.routerLink) {
169
+ this.routerLink = this.configuration.routerLink;
170
+ }
171
+
172
+ if (this.configuration.columnClass) {
173
+ this.columnClass = this.configuration.columnClass;
174
+ }
175
+
176
+ if (this.configuration.template) {
177
+ this.template = this.configuration.template;
178
+ }
179
+
180
+ if (this.configuration.headerName) {
181
+ this.headerGetter = (
182
+ typeof this.configuration.headerName === 'string'
183
+ ? () => this.configuration.headerName ?? ''
184
+ : (columnId: string, index: number) =>
185
+ (<(columnId: string, index?: number) => string>(
186
+ this.configuration.headerName
187
+ ))?.(columnId, index) ?? this.id
188
+ ) as (columnId: string, index: number) => string;
189
+ } else {
190
+ this.headerGetter = () => "";
191
+ }
192
+
193
+ if (this.configuration.queryParams) {
194
+ this.queryParams = this.configuration.queryParams;
195
+ }
196
+
197
+ if (this.routerLink) {
198
+ this.columnClass += ' ' + 'link';
199
+ }
200
+
201
+ if (this.configuration.sortable !== undefined) {
202
+ this.sortable = this.configuration.sortable;
203
+ } else {
204
+ this.sortable = true;
205
+ }
206
+
207
+ if (this.configuration.width){
208
+ this.width = typeof this.configuration.width === "number" ? this.configuration.width + 'px' : this.configuration.width;
209
+ }
210
+
211
+ if (this.configuration.textAlign !== undefined) {
212
+ this.textAlign = this.configuration.textAlign;
213
+ }
214
+
215
+ if (this.configuration.forceVisibility) {
216
+ this.forceVisibility = this.configuration.forceVisibility;
217
+ }
218
+
219
+ if (this.configuration.visible !== undefined) {
220
+ this.visible = this.configuration.visible;
221
+ }
222
+ }
223
+ }
224
+
225
+ export class RowModel<T> {
226
+ row: T;
227
+ index?: number;
228
+ id: number | string;
229
+ selected: boolean;
230
+ marked?: boolean;
231
+ focused?: boolean;
232
+ expanded: boolean;
233
+ fnc?: void;
234
+ customRoute: string | undefined;
235
+
236
+ constructor(
237
+ row: T,
238
+ selected: boolean,
239
+ expanded: boolean,
240
+ marked?: boolean,
241
+ index?: number,
242
+ fnc?: void,
243
+ customRoute?: string,
244
+ entityKey?: string
245
+ ) {
246
+ this.row = row;
247
+ this.id = (row as { [key: string]: string })['id'] ?? v4();
248
+ this.index = index;
249
+ this.selected = selected;
250
+ this.expanded = expanded;
251
+ this.marked = marked;
252
+ this.fnc = fnc;
253
+ this.customRoute = createCustomRoute<T>(row, entityKey, customRoute);
254
+ }
255
+ }
256
+
257
+ function createCustomRoute<T>(row: T, entityKey?: string, customRoute?: string) {
258
+ return customRoute && entityKey
259
+ ? `${customRoute}/` + (row as { [key: string]: string })[entityKey]
260
+ : undefined;
261
+ }
@@ -0,0 +1 @@
1
+ export * from './table-filter.model';
@@ -0,0 +1,22 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { BaseFormDirectiveCore } from '../base-form';
3
+
4
+ export const TABLE_FILTER_COMPONENT_TOKEN = new InjectionToken<TableFilterCore<any>>(
5
+ 'TableFilterComponentToken'
6
+ );
7
+
8
+ export interface FilterDefinition {
9
+ optionLabel: string;
10
+ label: string;
11
+ optionValue?: string;
12
+ url?: string;
13
+ filterType?: 'dropdown' | 'checkbox' | 'calendar' | 'multiselect';
14
+ initialValue?: any;
15
+ }
16
+
17
+ export interface TableFilterCore<T> extends BaseFormDirectiveCore<T> {
18
+ filterDefinitions: FilterDefinition[];
19
+ title: string;
20
+ hideSearch?: boolean;
21
+ debounceTime?: number;
22
+ }
@@ -0,0 +1 @@
1
+ export * from './tag.model';
@@ -0,0 +1,13 @@
1
+ import { InjectionToken, InputSignal } from "@angular/core";
2
+ import { ControlSeverityType, FieldSizeType } from "../../common";
3
+
4
+ export const TAG_COMPONENT_TOKEN = new InjectionToken<TagCore>('TagComponentToken');
5
+
6
+ export type TagVariant = 'bold' | 'subtle';
7
+
8
+ export interface TagCore {
9
+ label: InputSignal<string | undefined>;
10
+ severity: InputSignal<ControlSeverityType | undefined>;
11
+ variant: InputSignal<TagVariant>;
12
+ size: InputSignal<FieldSizeType>;
13
+ }
@@ -0,0 +1 @@
1
+ export * from './textarea.model';
@@ -0,0 +1,13 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { BaseFormCore } from '../base-form';
3
+
4
+ export const TEXTAREA_COMPONENT_TOKEN = new InjectionToken<TextareaCore>(
5
+ 'TextareaComponentToken'
6
+ );
7
+
8
+ export interface TextareaCore extends BaseFormCore {
9
+ rows: number;
10
+ cols: number;
11
+ autoResize: boolean;
12
+ floatLabel: string | undefined;
13
+ }
@@ -0,0 +1 @@
1
+ export * from './textfield.model';
@@ -0,0 +1,13 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { FieldSizeType, FieldTypeType } from '../../common';
3
+ import { BaseFormCore } from '../base-form';
4
+
5
+ export const TEXTFIELD_COMPONENT_TOKEN = new InjectionToken<TextfieldCore>(
6
+ 'TextfieldComponentToken'
7
+ );
8
+
9
+ export interface TextfieldCore extends BaseFormCore {
10
+ size: FieldSizeType | undefined;
11
+ type: FieldTypeType;
12
+ floatLabel: boolean;
13
+ }
@@ -0,0 +1 @@
1
+ export * from './tooltip.model';
@@ -0,0 +1,13 @@
1
+ import { InjectionToken, Signal } from '@angular/core';
2
+ import { ControlSeverityType, FieldSizeType, PositionType } from '../../common';
3
+
4
+ export const TOOLTIP_COMPONENT_TOKEN = new InjectionToken<TooltipCore>(
5
+ 'TooltipComponentToken'
6
+ );
7
+
8
+ export interface TooltipCore {
9
+ color: Signal<ControlSeverityType | undefined>;
10
+ position: Signal<PositionType | undefined>;
11
+ size: Signal<FieldSizeType | undefined>;
12
+ tooltipText: Signal<string | undefined>;
13
+ }
@@ -0,0 +1,12 @@
1
+ import { Directive, OnDestroy } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+
4
+ @Directive()
5
+ export abstract class UnsubscribeComponent implements OnDestroy {
6
+ destroyed$ = new Subject<void>();
7
+
8
+ ngOnDestroy() {
9
+ this.destroyed$.next();
10
+ this.destroyed$.complete();
11
+ }
12
+ }
@@ -0,0 +1,275 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import {
3
+ ChangeDetectorRef,
4
+ Directive,
5
+ inject,
6
+ Input,
7
+ OnChanges,
8
+ OnInit,
9
+ SimpleChanges,
10
+ } from '@angular/core';
11
+ import {
12
+ BASE_URL_PATH,
13
+ convertDatasource,
14
+ DataSourceFunctionType,
15
+ DatasourceType,
16
+ DEFAULT_SEARCH_LIMIT,
17
+ FilterEvent,
18
+ LazyLoadEvent,
19
+ normalizeRequest,
20
+ Page,
21
+ RequestParams,
22
+ } from '@verisoft/core';
23
+ import {
24
+ BehaviorSubject,
25
+ catchError,
26
+ debounceTime,
27
+ filter,
28
+ map,
29
+ of,
30
+ switchMap,
31
+ takeUntil,
32
+ tap,
33
+ } from 'rxjs';
34
+ import {
35
+ DataSourceComponentModel,
36
+ DEFAULT_DEBOUNCE_TIME,
37
+ ExtendedRequestType,
38
+ setComponentProperties,
39
+ setDataToArray,
40
+ } from '../common';
41
+ import {
42
+ DROPDOWN_COMPONENT_TOKEN,
43
+ DropdownCore,
44
+ GENERIC_FIELD_COMPONENT_TOKEN,
45
+ GenericFieldCore,
46
+ MULTISELECT_COMPONENT_TOKEN,
47
+ MultiselectCore,
48
+ UnsubscribeComponent,
49
+ } from '../components';
50
+
51
+ @Directive({
52
+ selector:
53
+ // eslint-disable-next-line @angular-eslint/directive-selector
54
+ 'v-dropdown[useDatasource], v-multiselect[useDatasource], v-generic-field[useDatasource]',
55
+ standalone: true,
56
+ })
57
+ export class DatasourceDirective<T>
58
+ extends UnsubscribeComponent
59
+ implements OnChanges, OnInit
60
+ {
61
+ @Input() datasource!: DatasourceType<T>;
62
+
63
+ @Input() autoBind = true;
64
+
65
+ @Input() loadingText = '... loading ...';
66
+
67
+ @Input() filterField: string | undefined = 'fulltext';
68
+
69
+ @Input() transformFn?: (data: T) => unknown;
70
+
71
+ @Input() extraFilter!: any;
72
+
73
+ get activeComponent(): DataSourceComponentModel<T> {
74
+ return (this.dropdownComponent ??
75
+ this.multiSelectComponent ??
76
+ this.genericField) as DataSourceComponentModel<T>;
77
+ }
78
+
79
+ private httpClient = inject(HttpClient);
80
+
81
+ private baseUrl: string = inject(BASE_URL_PATH);
82
+
83
+ private changeDetectorRef = inject(ChangeDetectorRef);
84
+
85
+ private isAllDataLoaded = false;
86
+
87
+ private parameters$ = new BehaviorSubject<Partial<ExtendedRequestType<T>>>({});
88
+
89
+ private lastParameter = {};
90
+
91
+ private dropdownComponent = inject<DropdownCore<T>>(
92
+ DROPDOWN_COMPONENT_TOKEN,
93
+ { optional: true }
94
+ );
95
+
96
+ private multiSelectComponent = inject<MultiselectCore<T>>(
97
+ MULTISELECT_COMPONENT_TOKEN,
98
+ {
99
+ optional: true,
100
+ }
101
+ );
102
+
103
+ private genericField = inject<GenericFieldCore<T>>(
104
+ GENERIC_FIELD_COMPONENT_TOKEN,
105
+ { optional: true }
106
+ );
107
+
108
+ private dataSourceService?: DataSourceFunctionType<T>;
109
+
110
+ private loadingPlaceholderItem: { [key: string]: unknown } = {};
111
+
112
+ ngOnInit(): void {
113
+ this.activeComponent.showed
114
+ .pipe(takeUntil(this.destroyed$))
115
+ .subscribe(() => {
116
+ if (
117
+ !this.autoBind &&
118
+ !this.activeComponent.options &&
119
+ !this.activeComponent.loading
120
+ ) {
121
+ this.parameters$.next({});
122
+ }
123
+ });
124
+
125
+ this.activeComponent.lazyLoad
126
+ .pipe(takeUntil(this.destroyed$))
127
+ .subscribe((value: LazyLoadEvent) => {
128
+ this.parameters$.next({ offset: value?.offset, limit: value?.limit });
129
+ });
130
+
131
+ this.activeComponent.filtered
132
+ .pipe(takeUntil(this.destroyed$))
133
+ .subscribe((value: FilterEvent) => {
134
+ const property = this.filterField ?? this.activeComponent.optionLabel;
135
+ if (property) {
136
+ this.parameters$.next({
137
+ offset: 0,
138
+ filter: {
139
+ [property]: value.filter ? value.filter : undefined,
140
+ ...(this.extraFilter ?? {})
141
+ } as Partial<T>,
142
+ useNewData: true,
143
+ });
144
+ }
145
+ });
146
+
147
+ this.parameters$
148
+ .pipe(
149
+ takeUntil(this.destroyed$),
150
+ filter(request => !this.isDataForRequestLoaded(request)),
151
+ map((request) => {
152
+ const extendedParams = normalizeRequest({ ...this.lastParameter, ...request }, DEFAULT_SEARCH_LIMIT) as ExtendedRequestType<T>;
153
+ extendedParams.useNewData = request.useNewData ?? false;
154
+ return extendedParams;
155
+ }),
156
+ tap((request) => {
157
+ this.lastParameter = request;
158
+ }),
159
+ debounceTime(DEFAULT_DEBOUNCE_TIME),
160
+ tap(() => {
161
+ this.changeComponent(this.activeComponent, {
162
+ loading: true,
163
+ });
164
+ }),
165
+ switchMap((request) =>
166
+ this.dataSourceService
167
+ ? this.dataSourceService(request as RequestParams<T>).pipe(
168
+ map((response) => ({ request, response }))
169
+ )
170
+ : of({
171
+ request,
172
+ response: {
173
+ data: [] as T[],
174
+ total: 0,
175
+ limit: request.limit,
176
+ offset: request.offset,
177
+ } as Page<T>,
178
+ })
179
+ ),
180
+ catchError((request) => {
181
+ this.changeComponent(this.activeComponent, {
182
+ loading: false,
183
+ });
184
+ return of({
185
+ request: request as ExtendedRequestType<T>,
186
+ response: {
187
+ data: [] as T[],
188
+ total: 0,
189
+ limit: request.limit,
190
+ offset: request.offset,
191
+ } as Page<T>,
192
+ });
193
+ })
194
+ )
195
+ .subscribe(({ request, response }) =>
196
+ this.setDataToControl(request, response)
197
+ );
198
+ }
199
+
200
+ ngOnChanges(changes: SimpleChanges): void {
201
+ if (changes['datasource']) {
202
+ this.dataSourceService = convertDatasource(
203
+ this.datasource,
204
+ this.baseUrl,
205
+ this.httpClient
206
+ );
207
+
208
+ if (this.autoBind) {
209
+ this.parameters$.next({ offset: 0 });
210
+ }
211
+ }
212
+ }
213
+
214
+ private isDataForRequestLoaded(request: Partial<ExtendedRequestType<T>>) {
215
+ if (request.useNewData) {
216
+ return false;
217
+ }
218
+
219
+ const offset = request.offset ?? 0;
220
+ const limit = request.limit ?? DEFAULT_SEARCH_LIMIT;
221
+ const options = this.activeComponent.options;
222
+ if (!options) {
223
+ return false;
224
+ }
225
+
226
+ if (options.length < offset + limit) {
227
+ return false;
228
+ }
229
+
230
+ const allItemsFilled = options.slice(offset, offset + limit).every(item => item !== undefined && item != this.loadingPlaceholderItem);
231
+ return allItemsFilled;
232
+ }
233
+
234
+ private setDataToControl(request: ExtendedRequestType<T>, result: Page<T>): void {
235
+ this.isAllDataLoaded = result.total <= result.data.length;
236
+ const data = result.data;
237
+ const total = result.total;
238
+ const offset = request.offset;
239
+ const transferedData = this.transformFn
240
+ ? data.map((x) => this.transformFn?.(x))
241
+ : data;
242
+
243
+ if (this.activeComponent.optionLabel) {
244
+ this.loadingPlaceholderItem[this.activeComponent.optionLabel] =
245
+ this.loadingText;
246
+ }
247
+
248
+ if (this.activeComponent.optionValue) {
249
+ this.loadingPlaceholderItem[this.activeComponent.optionValue] = -1;
250
+ }
251
+
252
+ const options = request.useNewData ? undefined : this.activeComponent.options;
253
+ const newOptions = setDataToArray(
254
+ options,
255
+ transferedData,
256
+ offset,
257
+ total,
258
+ this.loadingPlaceholderItem
259
+ );
260
+
261
+ this.changeComponent(this.activeComponent, {
262
+ options: newOptions as T[],
263
+ loading: false,
264
+ lazy: !this.isAllDataLoaded,
265
+ });
266
+ }
267
+
268
+ private changeComponent(
269
+ component: DataSourceComponentModel<T>,
270
+ value: Partial<DataSourceComponentModel<T>>
271
+ ) {
272
+ setComponentProperties(component, value);
273
+ this.changeDetectorRef.detectChanges();
274
+ }
275
+ }
@@ -0,0 +1,4 @@
1
+ export * from './datasource.directive';
2
+ export * from './table-datasource.directive';
3
+ export * from './shortcut.directive';
4
+ export * from './table-filter.directive';
@@ -0,0 +1,37 @@
1
+ import { Directive, HostListener, Input } from "@angular/core";
2
+
3
+ @Directive({
4
+ // eslint-disable-next-line @angular-eslint/directive-selector
5
+ selector: 'v-button[useShortCut]',
6
+ exportAs: 'useShortCut',
7
+ standalone: true,
8
+ })
9
+ export class ButtonShortCutDirective
10
+ {
11
+ @Input() shortCutFn?: () => void;
12
+ @Input() shortCutKey!: string;
13
+
14
+ private keyMap: { [key: string]: boolean } = {};
15
+
16
+ @HostListener('document:keydown', ['$event'])
17
+ onKeyDown(event: KeyboardEvent) {
18
+ this.keyMap[event.key.toLowerCase()] = true;
19
+ this.checkShortcut();
20
+ }
21
+
22
+ @HostListener('document:keyup', ['$event'])
23
+ onKeyUp(event: KeyboardEvent) {
24
+ this.keyMap[event.key.toLowerCase()] = false;
25
+ }
26
+
27
+ private checkShortcut() {
28
+ if (this.shortCutKey && this.shortCutFn) {
29
+ const keys = this.shortCutKey.toLowerCase().split('+');
30
+ const isShortcutPressed = keys.every((key) => this.keyMap[key]);
31
+ if (isShortcutPressed) {
32
+ this.shortCutFn?.();
33
+ keys.forEach((key) => (this.keyMap[key] = false));
34
+ }
35
+ }
36
+ }
37
+ }