@verisoft/core 18.0.1-0 → 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.
- package/package.json +14 -11
- package/src/index.ts +3 -7
- package/src/lib/index.ts +1 -0
- package/src/lib/models/base-http.models.ts +144 -0
- package/src/lib/models/constants.ts +8 -0
- package/src/lib/models/datasource.model.ts +77 -0
- package/src/lib/models/environment.model.ts +5 -0
- package/src/lib/models/error-provider.model.ts +20 -0
- package/src/lib/models/event.models.ts +7 -0
- package/src/lib/models/index.ts +7 -0
- package/src/lib/services/base-http.service.ts +44 -34
- package/src/lib/services/error-provider.service.ts +29 -0
- package/src/lib/services/index.ts +3 -0
- package/src/lib/services/local-storage.service.ts +13 -0
- package/src/lib/services/storage.service.ts +13 -0
- package/src/lib/utils/array.utils.spec.ts +49 -0
- package/src/lib/utils/array.utils.ts +45 -0
- package/src/lib/utils/data.utils.ts +4 -4
- package/src/lib/utils/date.utils.ts +30 -0
- package/src/lib/utils/index.ts +1 -0
- package/src/lib/utils/keyOrFn.utils.ts +2 -2
- package/src/lib/utils/object.utils.spec.ts +36 -1
- package/src/lib/utils/object.utils.ts +16 -0
- package/src/lib/base/models/base-http.models.ts +0 -96
- package/src/lib/base/models/table-datasource.models.ts +0 -9
- package/src/lib/models/lazy-load.model.ts +0 -4
- /package/src/lib/{base/models → models}/all-item.datasource.ts +0 -0
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@verisoft/core",
|
|
3
|
-
"version": "18.
|
|
4
|
-
"peerDependencies": {
|
|
5
|
-
"@angular/common": "^18.2.8",
|
|
6
|
-
"@angular/core": "^18.2.8",
|
|
7
|
-
"moment": "^2.30.1",
|
|
8
|
-
"rxjs": "~7.8.0"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@verisoft/core",
|
|
3
|
+
"version": "18.3.0",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@angular/common": "^18.2.8",
|
|
6
|
+
"@angular/core": "^18.2.8",
|
|
7
|
+
"moment": "^2.30.1",
|
|
8
|
+
"rxjs": "~7.8.0",
|
|
9
|
+
"@angular/forms": "18.2.8",
|
|
10
|
+
"@ngx-translate/core": "^15.0.0"
|
|
11
|
+
},
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"dependencies": {}
|
|
14
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export * from './lib/
|
|
2
|
-
export * from './lib/
|
|
3
|
-
export * from './lib/
|
|
4
|
-
|
|
5
|
-
export * from './lib/services/base-http.service';
|
|
6
|
-
|
|
7
|
-
export * from './lib/utils';
|
|
1
|
+
export * from './lib/models';
|
|
2
|
+
export * from './lib/services';
|
|
3
|
+
export * from './lib/utils';
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './models';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { HttpParams, HttpResponse } from '@angular/common/http';
|
|
3
|
+
import { InjectionToken } from '@angular/core';
|
|
4
|
+
|
|
5
|
+
export const BASE_URL_PATH = new InjectionToken<string>('BASE_URL_PATH');
|
|
6
|
+
|
|
7
|
+
export function requestParamsToHttpParams<T>(
|
|
8
|
+
requestParams: Partial<RequestParams<T>>,
|
|
9
|
+
httpParams: HttpParams = new HttpParams()
|
|
10
|
+
): HttpParams {
|
|
11
|
+
if (requestParams.limit != undefined) {
|
|
12
|
+
httpParams = httpParams.append('limit', requestParams.limit.toString());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (requestParams.offset != undefined) {
|
|
16
|
+
httpParams = httpParams.append('offset', requestParams.offset.toString());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (requestParams.id != '' && requestParams.id != undefined) {
|
|
20
|
+
httpParams = httpParams.append('id', requestParams.id as any);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
httpParams = getFilter(requestParams, httpParams);
|
|
24
|
+
httpParams = getSort(requestParams, httpParams);
|
|
25
|
+
|
|
26
|
+
return httpParams;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFilter<T>(
|
|
30
|
+
requestParams: Partial<RequestParams<any>>,
|
|
31
|
+
httpParams: HttpParams
|
|
32
|
+
): HttpParams {
|
|
33
|
+
if (!requestParams.filter) {
|
|
34
|
+
return httpParams;
|
|
35
|
+
}
|
|
36
|
+
Object.keys(requestParams?.filter).forEach((key) => {
|
|
37
|
+
const value = requestParams.filter?.[key as keyof Partial<T>];
|
|
38
|
+
if (value != undefined && !(typeof value === 'string' && value.trim() === "")) {
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
value.forEach((valueItem: any) => {
|
|
41
|
+
httpParams = httpParams.append(
|
|
42
|
+
'Filter.' + key, valueItem
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
httpParams = httpParams.append('Filter.' + key, value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return httpParams;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getSort(
|
|
54
|
+
requestParams: Partial<RequestParams<any>>,
|
|
55
|
+
httpParams: HttpParams
|
|
56
|
+
): HttpParams {
|
|
57
|
+
if (!requestParams.sort) {
|
|
58
|
+
return httpParams;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
requestParams.sort?.forEach((sort) => {
|
|
62
|
+
httpParams = httpParams
|
|
63
|
+
.append('Sort.Field', sort.field)
|
|
64
|
+
.append('Sort.Direction', sort.direction);
|
|
65
|
+
});
|
|
66
|
+
return httpParams;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function saveFile(response: HttpResponse<Blob>) {
|
|
70
|
+
const fileName = response.headers
|
|
71
|
+
.get('Content-Disposition')
|
|
72
|
+
?.split(';')[1]
|
|
73
|
+
.split('=')[1];
|
|
74
|
+
const blob: Blob = response.body as Blob;
|
|
75
|
+
const a = document.createElement('a');
|
|
76
|
+
a.download = fileName ?? 'undefined';
|
|
77
|
+
a.href = window.URL.createObjectURL(blob);
|
|
78
|
+
a.click();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export enum SortDirection {
|
|
82
|
+
asc = 'asc',
|
|
83
|
+
desc = 'desc',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export declare type SortDirectionType = keyof typeof SortDirection;
|
|
87
|
+
|
|
88
|
+
export declare interface Sort {
|
|
89
|
+
field: string;
|
|
90
|
+
direction: SortDirectionType;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface RequestParams<T> extends AllDataRequestParams<T> {
|
|
94
|
+
offset: number;
|
|
95
|
+
limit: number;
|
|
96
|
+
id?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AllDataRequestParams<T> {
|
|
100
|
+
filter?: Partial<T>;
|
|
101
|
+
sort?: Sort[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface Page<T> {
|
|
105
|
+
data: T[];
|
|
106
|
+
total: number;
|
|
107
|
+
limit: number;
|
|
108
|
+
offset: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CustomExport<T> {
|
|
112
|
+
sortDefinition?: Sort;
|
|
113
|
+
filter?: Partial<T>;
|
|
114
|
+
columnsToExport: ColumnExportSpecification[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ColumnExportSpecification {
|
|
118
|
+
name: string;
|
|
119
|
+
header: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const DEFAULT_SEARCH_LIMIT = 50;
|
|
123
|
+
|
|
124
|
+
export const DEFAULT_SEARCH_PARAMS: RequestParams<any> = {
|
|
125
|
+
offset: 0,
|
|
126
|
+
limit: DEFAULT_SEARCH_LIMIT,
|
|
127
|
+
id: '',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export function normalizeRequest<T>(
|
|
131
|
+
request: Partial<RequestParams<T>>,
|
|
132
|
+
minLimit: number | undefined = undefined
|
|
133
|
+
): RequestParams<T> {
|
|
134
|
+
return {
|
|
135
|
+
offset: request?.offset ?? 0,
|
|
136
|
+
limit: !request?.limit
|
|
137
|
+
? DEFAULT_SEARCH_LIMIT
|
|
138
|
+
: minLimit && request.limit < minLimit
|
|
139
|
+
? minLimit
|
|
140
|
+
: request.limit,
|
|
141
|
+
filter: request?.filter,
|
|
142
|
+
sort: request?.sort,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { HttpClient } from '@angular/common/http';
|
|
2
|
+
import { isObservable, map, Observable, of } from 'rxjs';
|
|
3
|
+
import { BaseHttpService } from '../services/base-http.service';
|
|
4
|
+
import { AllItemDatasource } from './all-item.datasource';
|
|
5
|
+
import { Page, RequestParams } from './base-http.models';
|
|
6
|
+
import { DEFAULT_PAGE_SIZE } from './constants';
|
|
7
|
+
|
|
8
|
+
export type DatasourceType<T> =
|
|
9
|
+
| string
|
|
10
|
+
| Observable<T[]>
|
|
11
|
+
| Observable<Page<T>>
|
|
12
|
+
| BaseHttpService<T>
|
|
13
|
+
| T[]
|
|
14
|
+
| Page<T>
|
|
15
|
+
| AllItemDatasource<T>;
|
|
16
|
+
|
|
17
|
+
export type DataSourceFunctionType<T> = (
|
|
18
|
+
requestParams: RequestParams<T>
|
|
19
|
+
) => Observable<Page<T>>;
|
|
20
|
+
|
|
21
|
+
export function convertDatasource<T>(
|
|
22
|
+
datasource: DatasourceType<T>,
|
|
23
|
+
basePath: string,
|
|
24
|
+
httpClient: HttpClient
|
|
25
|
+
): DataSourceFunctionType<T> {
|
|
26
|
+
if (!datasource) {
|
|
27
|
+
throw new Error('Datasource is not defined');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof datasource === 'string') {
|
|
31
|
+
const service = new BaseHttpService<T>(httpClient, basePath, datasource);
|
|
32
|
+
return (requestParams: RequestParams<T>) =>
|
|
33
|
+
service.fetchList(requestParams);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (datasource instanceof BaseHttpService) {
|
|
37
|
+
return (requestParams: RequestParams<T>) =>
|
|
38
|
+
datasource.fetchList(requestParams);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isObservable(datasource)) {
|
|
42
|
+
return () =>
|
|
43
|
+
(datasource as Observable<Page<T> | T[]>).pipe(map(convertArrayToPage));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const allItemDatasource = <AllItemDatasource<T>>datasource;
|
|
47
|
+
if (allItemDatasource.getData$) {
|
|
48
|
+
return () =>
|
|
49
|
+
allItemDatasource
|
|
50
|
+
.getData$()
|
|
51
|
+
.pipe(map((data) => convertArrayToPage(data)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const page = <Page<T>>datasource;
|
|
55
|
+
if (page.data) {
|
|
56
|
+
return () => of(page);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(datasource)) {
|
|
60
|
+
return () => of(convertArrayToPage(datasource));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error('Datasource is not supported');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function convertArrayToPage<T>(data: Page<T> | T[]): Page<T> {
|
|
67
|
+
if (Array.isArray(data)) {
|
|
68
|
+
return {
|
|
69
|
+
data: data ?? [],
|
|
70
|
+
total: data?.length ?? 0,
|
|
71
|
+
limit: data?.length ?? DEFAULT_PAGE_SIZE,
|
|
72
|
+
offset: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return data as Page<T>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
import { ValidationErrors } from '@angular/forms';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
export interface ErrorProvider {
|
|
6
|
+
mapError(errors: ValidationErrors): Observable<string>;
|
|
7
|
+
errors: Record<string, (value?: any) => string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ERROR_PROVIDER_TOKEN
|
|
11
|
+
= new InjectionToken<ErrorProvider>('ERROR_PROVIDER_TOKEN');
|
|
12
|
+
|
|
13
|
+
export enum CustomValidationCodes {}
|
|
14
|
+
|
|
15
|
+
export const ErrorMap: Record<string, (value?: any) => string> = {
|
|
16
|
+
required: () => `VALIDATIONS.REQUIRED`,
|
|
17
|
+
email: () => `VALIDATIONS.EMAIL`,
|
|
18
|
+
minLength: () => 'VALIDATIONS.MIN_LENGTH',
|
|
19
|
+
maxLength: () => 'VALIDATIONS.MAX_LENGTH',
|
|
20
|
+
};
|
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { HttpClient } from '@angular/common/http';
|
|
3
|
-
import { Inject, Injectable } from '@angular/core';
|
|
4
3
|
import { map, Observable } from 'rxjs';
|
|
5
4
|
import {
|
|
6
|
-
|
|
5
|
+
AllDataRequestParams,
|
|
6
|
+
DEFAULT_SEARCH_PARAMS,
|
|
7
7
|
Page,
|
|
8
8
|
RequestParams,
|
|
9
9
|
requestParamsToHttpParams,
|
|
10
|
-
} from '../
|
|
11
|
-
import {
|
|
12
|
-
import { LazyLoad } from '../models/lazy-load.model';
|
|
10
|
+
} from '../models/base-http.models';
|
|
11
|
+
import { LazyLoadEvent } from '../models/event.models';
|
|
13
12
|
import { ClearUtils } from '../utils/clear.utils';
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
export abstract class BaseHttpService<T, TKey = number> {
|
|
17
|
-
basePath = '';
|
|
18
|
-
entityName!: string;
|
|
14
|
+
export class BaseHttpService<T, TKey = number | string> {
|
|
19
15
|
constructor(
|
|
20
16
|
readonly http: HttpClient,
|
|
21
|
-
|
|
17
|
+
readonly basePath: string,
|
|
18
|
+
readonly entityName: string
|
|
22
19
|
) {
|
|
23
20
|
this.basePath = basePath;
|
|
24
21
|
}
|
|
22
|
+
|
|
23
|
+
protected get apiPath() {
|
|
24
|
+
return this.basePath ? this.basePath + this.entityName : this.entityName;
|
|
25
|
+
}
|
|
26
|
+
|
|
25
27
|
fetchList(requestParams: RequestParams<T>): Observable<Page<T>> {
|
|
26
28
|
const params = requestParamsToHttpParams<T>(requestParams);
|
|
27
|
-
return this.http
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
34
|
-
);
|
|
29
|
+
return this.http.get<Page<T>>(this.apiPath, { params }).pipe(
|
|
30
|
+
map((response: Page<T>) => {
|
|
31
|
+
response.limit = requestParams.limit;
|
|
32
|
+
return response;
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
getData$(
|
|
38
38
|
entityName: keyof T,
|
|
39
39
|
searchTerm?: string | undefined,
|
|
40
|
-
lazyLoad?:
|
|
40
|
+
lazyLoad?: LazyLoadEvent
|
|
41
41
|
): Observable<T[]> {
|
|
42
42
|
const filter: Partial<T> = { [entityName]: searchTerm } as Partial<T>;
|
|
43
43
|
const params = lazyLoad
|
|
44
44
|
? requestParamsToHttpParams<T>({
|
|
45
45
|
...DEFAULT_SEARCH_PARAMS,
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
offset: lazyLoad.offset,
|
|
47
|
+
limit: lazyLoad.limit,
|
|
48
48
|
filter,
|
|
49
49
|
})
|
|
50
50
|
: requestParamsToHttpParams<T>({
|
|
@@ -52,26 +52,36 @@ export abstract class BaseHttpService<T, TKey = number> {
|
|
|
52
52
|
filter,
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
return this.http.get<T[]>(this.
|
|
55
|
+
return this.http.get<T[]>(this.apiPath, { params });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get(id: TKey): Observable<T> {
|
|
59
|
+
const url = `${this.apiPath}/${id}`;
|
|
60
|
+
return this.http.get<T>(url);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
post(entity: Partial<T>): Observable<T> {
|
|
64
|
+
return this.http.post<T>(this.apiPath, entity);
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
return this.http.
|
|
67
|
+
put(id: TKey, entity: Partial<T>): Observable<T> {
|
|
68
|
+
const url = `${this.apiPath}/${id}`;
|
|
69
|
+
return this.http.put<T>(url, entity);
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
return this.http.
|
|
72
|
+
delete(id: TKey): Observable<T> {
|
|
73
|
+
const url = `${this.apiPath}/${id}`;
|
|
74
|
+
return this.http.delete<T>(url);
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
77
|
+
export(requestParams: AllDataRequestParams<T>): Observable<Blob> {
|
|
78
|
+
const httpParams = requestParamsToHttpParams<T>(requestParams);
|
|
79
|
+
const url = `${this.apiPath}/export`;
|
|
80
|
+
return this.http.get(url, { responseType: 'blob', params: httpParams });
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
removeRange(entity: string[]) {
|
|
74
|
-
return this.http.delete(this.
|
|
84
|
+
return this.http.delete(this.apiPath, {
|
|
75
85
|
body: entity,
|
|
76
86
|
});
|
|
77
87
|
}
|
|
@@ -83,8 +93,8 @@ export abstract class BaseHttpService<T, TKey = number> {
|
|
|
83
93
|
ClearUtils.recursiveObjectAttributesDeletation(transformedFilter);
|
|
84
94
|
return {
|
|
85
95
|
paging: {
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
offset: requestParams.offset,
|
|
97
|
+
limit: requestParams.limit,
|
|
88
98
|
},
|
|
89
99
|
...(sorter && { sorting: sorter }),
|
|
90
100
|
...(transformedFilter && { filter: transformedFilter }),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { ValidationErrors } from '@angular/forms';
|
|
3
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
4
|
+
import { Observable } from 'rxjs';
|
|
5
|
+
import { ErrorProvider, ErrorMap } from '../models/error-provider.model';
|
|
6
|
+
|
|
7
|
+
@Injectable({
|
|
8
|
+
providedIn: 'root'
|
|
9
|
+
})
|
|
10
|
+
export class LocalizableErrorProviderService implements ErrorProvider {
|
|
11
|
+
private readonly translateService = inject(TranslateService);
|
|
12
|
+
readonly errors: Record<string, (value?: any) => string> = ErrorMap;
|
|
13
|
+
|
|
14
|
+
mapError(errors: ValidationErrors): Observable<string> {
|
|
15
|
+
const error = this.getFirstValue(errors);
|
|
16
|
+
try {
|
|
17
|
+
const value = this.errors[error.key](error.value);
|
|
18
|
+
return this.translateService.get(value);
|
|
19
|
+
} catch (_) {
|
|
20
|
+
throw new Error(`Key "${error.key}" is not specified in the current error map!`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private getFirstValue(errors: ValidationErrors): { key: string; value: string } {
|
|
25
|
+
const key = Object.keys(errors)[0];
|
|
26
|
+
const value = errors[key];
|
|
27
|
+
return { key, value };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class LocalStorageService {
|
|
2
|
+
setItem(key: string, value: string) {
|
|
3
|
+
localStorage.setItem(key, value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
getItem(key: string): string | null {
|
|
7
|
+
return localStorage.getItem(key);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
removeItem(key: string) {
|
|
11
|
+
localStorage.removeItem(key);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class StorageService {
|
|
2
|
+
setItem(key: string, value: string) {
|
|
3
|
+
localStorage.setItem(key, value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
getItem(key: string): string | null {
|
|
7
|
+
return localStorage.getItem(key);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
removeItem(key: string) {
|
|
11
|
+
localStorage.removeItem(key);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { applyFilter } from './array.utils';
|
|
2
|
+
describe('array.utils', () => {
|
|
3
|
+
describe('applyFilter', () => {
|
|
4
|
+
interface TestData {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
tags: string[];
|
|
8
|
+
age: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const data: TestData[] = [
|
|
12
|
+
{ id: 1, name: 'Alice', tags: ['developer', 'angular'], age: 25 },
|
|
13
|
+
{ id: 2, name: 'Bob', tags: ['designer', 'figma'], age: 30 },
|
|
14
|
+
{ id: 3, name: 'Charlie', tags: ['developer'], age: 25 },
|
|
15
|
+
{ id: 4, name: 'Dave', tags: ['developer', 'react'], age: 35 },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const [alice, , charlie] = data;
|
|
19
|
+
|
|
20
|
+
test('Should filter data by contains string value when a single string filter is applied', () => {
|
|
21
|
+
const filter = { name: 'lice' };
|
|
22
|
+
const result = applyFilter(data, filter);
|
|
23
|
+
expect(result).toEqual([alice]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('Should filter data when filter contains an array value', () => {
|
|
27
|
+
const filter = { tags: ['develop'] };
|
|
28
|
+
const result = applyFilter(data, filter);
|
|
29
|
+
expect(result).toHaveLength(3);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Should filter data with multiple conditions when filter properties use AND logic', () => {
|
|
33
|
+
const filter = { age: 25, tags: ['developer'] };
|
|
34
|
+
const result = applyFilter(data, filter);
|
|
35
|
+
expect(result).toEqual([alice, charlie]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Should return all data when the filter is empty', () => {
|
|
39
|
+
const result = applyFilter(data, {});
|
|
40
|
+
expect(result).toEqual(data);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('Should return no data when no items match the filter', () => {
|
|
44
|
+
const filter = { name: 'Zoe' };
|
|
45
|
+
const result = applyFilter(data, filter);
|
|
46
|
+
expect(result).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Sort } from "../models";
|
|
2
|
+
import { getValueByPath } from "./object.utils";
|
|
3
|
+
|
|
1
4
|
type ArrayIntersectionType = (string | number)[]
|
|
2
5
|
|
|
3
6
|
export function isArrayIntersected(
|
|
@@ -7,3 +10,45 @@ export function isArrayIntersected(
|
|
|
7
10
|
const filteredArray = array1.filter(value => array2.includes(value));
|
|
8
11
|
return filteredArray.length > 0;
|
|
9
12
|
}
|
|
13
|
+
|
|
14
|
+
export function multiSort<T>(values: T[], sorts: Sort[]): T[] {
|
|
15
|
+
return [...values].sort((a, b) => {
|
|
16
|
+
for (const sort of sorts) {
|
|
17
|
+
const { field, direction } = sort;
|
|
18
|
+
const valueA = getValueByPath(a, field);
|
|
19
|
+
const valueB = getValueByPath(b, field);
|
|
20
|
+
|
|
21
|
+
if (valueA !== valueB) {
|
|
22
|
+
if (isComparable(valueA) && isComparable(valueB)) {
|
|
23
|
+
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
|
24
|
+
const baseCompare = valueA.localeCompare(valueB, undefined, {
|
|
25
|
+
sensitivity: 'base',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (baseCompare === 0) {
|
|
29
|
+
return valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return direction === 'asc' ? baseCompare : -baseCompare;
|
|
33
|
+
} else {
|
|
34
|
+
if (valueA < valueB) {
|
|
35
|
+
return direction === 'asc' ? -1 : 1;
|
|
36
|
+
}
|
|
37
|
+
if (valueA > valueB) {
|
|
38
|
+
return direction === 'asc' ? 1 : -1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 0;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isComparable(value: unknown): value is string | number | Date {
|
|
50
|
+
return typeof value === 'string'
|
|
51
|
+
|| typeof value === 'number'
|
|
52
|
+
|| typeof value === 'boolean'
|
|
53
|
+
|| value instanceof Date;
|
|
54
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import moment from 'moment';
|
|
2
2
|
|
|
3
|
-
export function transformData(data: any){
|
|
3
|
+
export function transformData(data: any) {
|
|
4
4
|
if (data) {
|
|
5
5
|
if (moment.isMoment(data)) {
|
|
6
6
|
return moment(data)
|
|
@@ -14,7 +14,7 @@ export function transformData(data: any){
|
|
|
14
14
|
return data;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function convertToDate(data: any){
|
|
17
|
+
export function convertToDate(data: any) {
|
|
18
18
|
if (data) {
|
|
19
19
|
if (!isNaN(Date.parse(data))) {
|
|
20
20
|
return new Date(data);
|
|
@@ -23,7 +23,7 @@ export function convertToDate(data: any){
|
|
|
23
23
|
return data;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function convertToDateTime(data: any){
|
|
26
|
+
export function convertToDateTime(data: any) {
|
|
27
27
|
if (data) {
|
|
28
28
|
if (!isNaN(Date.parse(data))) {
|
|
29
29
|
const date = new Date(data);
|
|
@@ -31,4 +31,4 @@ export function convertToDateTime(data: any){
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
return data;
|
|
34
|
-
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const czechDateFormat = new Intl.DateTimeFormat("cs-CZ", {
|
|
2
|
+
day: "numeric",
|
|
3
|
+
month: "numeric",
|
|
4
|
+
year: "numeric"
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const czechDateTimeFormat = new Intl.DateTimeFormat("cs-CZ", {
|
|
8
|
+
second: "numeric",
|
|
9
|
+
minute: "numeric",
|
|
10
|
+
hour: "numeric",
|
|
11
|
+
day: "numeric",
|
|
12
|
+
month: "numeric",
|
|
13
|
+
year: "numeric"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function toCzechDateTimeString(value?: Date | string, time = false): string {
|
|
17
|
+
if (!value) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
value = new Date(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (time) {
|
|
26
|
+
return czechDateTimeFormat.format(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return czechDateFormat.format(value);
|
|
30
|
+
}
|
package/src/lib/utils/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export function keyOrFn(keyOrFn: ((row: any) => string | undefined) | string, row: any){
|
|
1
|
+
export function keyOrFn(keyOrFn: ((row: any) => string | undefined) | string, row: any) {
|
|
2
2
|
if (keyOrFn instanceof Function) {
|
|
3
3
|
return keyOrFn(row);
|
|
4
4
|
}
|
|
5
5
|
else if (typeof keyOrFn === 'string') {
|
|
6
6
|
const value = row[keyOrFn];
|
|
7
|
-
if (value){
|
|
7
|
+
if (value) {
|
|
8
8
|
return value;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sortBy } from './object.utils';
|
|
1
|
+
import { getValueByPath, sortBy } from './object.utils';
|
|
2
2
|
|
|
3
3
|
describe('object.utils', () => {
|
|
4
4
|
describe('sortBy', () => {
|
|
@@ -31,4 +31,39 @@ describe('object.utils', () => {
|
|
|
31
31
|
expect(sortedItems?.[3].name).toBeUndefined();
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
describe('getValueByPath', () => {
|
|
36
|
+
const complexObject = {
|
|
37
|
+
name: 'Martin',
|
|
38
|
+
age: 40,
|
|
39
|
+
address: {
|
|
40
|
+
city: 'Brno',
|
|
41
|
+
street: 'Kralova',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
test('Should return property value when name of property is provided.', () => {
|
|
46
|
+
const value = getValueByPath(complexObject, 'name');
|
|
47
|
+
|
|
48
|
+
expect(value).toBe('Martin');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('Should return undefined when name is nonexistent.', () => {
|
|
52
|
+
const value = getValueByPath(complexObject, 'nonexistentProperty');
|
|
53
|
+
|
|
54
|
+
expect(value).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('Should return property value when name of property is complex path.', () => {
|
|
58
|
+
const value = getValueByPath(complexObject, 'address.city');
|
|
59
|
+
|
|
60
|
+
expect(value).toBe('Brno');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('Should return undefined when some object on complex path missing.', () => {
|
|
64
|
+
const value = getValueByPath(complexObject, 'car.model');
|
|
65
|
+
|
|
66
|
+
expect(value).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
34
69
|
});
|
|
@@ -29,3 +29,19 @@ export function sortBy<TEntity, TProperty>(
|
|
|
29
29
|
return 0;
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
export function getValueByPath<T, TValue>(
|
|
34
|
+
obj: T,
|
|
35
|
+
path: string | undefined,
|
|
36
|
+
): TValue | undefined {
|
|
37
|
+
if (path == undefined || path == ''){
|
|
38
|
+
return obj as unknown as TValue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return path.split('.').reduce((acc, key) => {
|
|
42
|
+
if (acc && typeof acc === 'object' && key in acc) {
|
|
43
|
+
return (acc as Record<string, TValue>)[key];
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}, obj as unknown) as TValue;
|
|
47
|
+
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { HttpParams, HttpResponse } from '@angular/common/http';
|
|
3
|
-
import { InjectionToken } from '@angular/core';
|
|
4
|
-
|
|
5
|
-
export const BASE_URL_PATH = new InjectionToken("BASE_URL_PATH");
|
|
6
|
-
|
|
7
|
-
export function requestParamsToHttpParams<T>(
|
|
8
|
-
requestParams: RequestParams<T>,
|
|
9
|
-
httpParams: HttpParams = new HttpParams()
|
|
10
|
-
): HttpParams {
|
|
11
|
-
httpParams = httpParams
|
|
12
|
-
.append('Limit', requestParams.size.toString())
|
|
13
|
-
.append('Offset', (requestParams.page * requestParams.size).toString());
|
|
14
|
-
if (requestParams.id != '' && requestParams.id != undefined) {
|
|
15
|
-
httpParams = httpParams.append('Id', requestParams.id as any);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
httpParams = getFilter(requestParams, httpParams);
|
|
19
|
-
httpParams = getSort(requestParams, httpParams);
|
|
20
|
-
|
|
21
|
-
return httpParams;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getFilter<T>(
|
|
25
|
-
requestParams: RequestParams<any>,
|
|
26
|
-
httpParams: HttpParams
|
|
27
|
-
): HttpParams {
|
|
28
|
-
if (!requestParams.filter) {
|
|
29
|
-
return httpParams;
|
|
30
|
-
}
|
|
31
|
-
Object.keys(requestParams?.filter).forEach((key) => {
|
|
32
|
-
if (requestParams.filter?.[key as keyof Partial<T>]) {
|
|
33
|
-
httpParams = httpParams.append(
|
|
34
|
-
'Filter.' + key,
|
|
35
|
-
requestParams.filter?.[key as keyof Partial<T>]
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
return httpParams;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getSort(
|
|
43
|
-
requestParams: RequestParams<any>,
|
|
44
|
-
httpParams: HttpParams
|
|
45
|
-
): HttpParams {
|
|
46
|
-
if (!requestParams.sort) {
|
|
47
|
-
return httpParams;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
requestParams.sort?.forEach((sort) => {
|
|
51
|
-
httpParams = httpParams
|
|
52
|
-
.append('Sort.Field', sort.field)
|
|
53
|
-
.append('Sort.Direction', sort.direction === -1 ? 'Desc' : 'Asc');
|
|
54
|
-
});
|
|
55
|
-
return httpParams;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function saveFile(response: HttpResponse<Blob>) {
|
|
59
|
-
const fileName = response.headers
|
|
60
|
-
.get('Content-Disposition')
|
|
61
|
-
?.split(';')[1]
|
|
62
|
-
.split('=')[1];
|
|
63
|
-
const blob: Blob = response.body as Blob;
|
|
64
|
-
const a = document.createElement('a');
|
|
65
|
-
a.download = fileName ?? 'undefined';
|
|
66
|
-
a.href = window.URL.createObjectURL(blob);
|
|
67
|
-
a.click();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export declare interface Sort<T> {
|
|
71
|
-
field: string;
|
|
72
|
-
direction: number;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface RequestParams<T> {
|
|
76
|
-
page: number;
|
|
77
|
-
size: number;
|
|
78
|
-
id?: string;
|
|
79
|
-
filter?: Partial<T>;
|
|
80
|
-
sort?: Sort<T>[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface Page<T> {
|
|
84
|
-
data: T[];
|
|
85
|
-
total: number;
|
|
86
|
-
size: number;
|
|
87
|
-
number: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const DEFAULT_SEARCH_LIMIT = 50;
|
|
91
|
-
|
|
92
|
-
export const DEFAULT_SEARCH_PARAMS: RequestParams<any> = {
|
|
93
|
-
page: 0,
|
|
94
|
-
size: DEFAULT_SEARCH_LIMIT,
|
|
95
|
-
id: '',
|
|
96
|
-
};
|
|
File without changes
|