mirror of
https://github.com/valitydev/ng-libs.git
synced 2024-11-06 00:35:21 +00:00
TD-666: 16.2 (#33)
This commit is contained in:
parent
fd4e28bc23
commit
235e19fb69
4372
package-lock.json
generated
4372
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@
|
||||
"@angular/cli": "~16.1.0",
|
||||
"@angular/compiler-cli": "^16.1.1",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"cspell": "^6.31.1",
|
||||
"cspell": "^7.0.0",
|
||||
"eslint": "^8.39.0",
|
||||
"jasmine-core": "~4.5.0",
|
||||
"karma": "~6.4.0",
|
||||
@ -47,7 +47,7 @@
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"ng-packagr": "^16.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier": "^3.0.1",
|
||||
"typescript": "~5.1.3"
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vality/cspell-config",
|
||||
"version": "0.1.0",
|
||||
"version": "7.0.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@ -9,7 +9,10 @@
|
||||
"default": "./cspell.config.js"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cspell": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cspell/dict-ru_ru": "^2.0.5"
|
||||
"@cspell/dict-ru_ru": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vality/eslint-config",
|
||||
"version": "1.0.0",
|
||||
"version": "8.0.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@ -12,13 +12,13 @@
|
||||
"eslint": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-eslint/eslint-plugin": "^16.0.3",
|
||||
"@angular-eslint/eslint-plugin-template": "^16.0.3",
|
||||
"@angular-eslint/template-parser": "^16.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
||||
"@typescript-eslint/parser": "^5.59.11",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"@angular-eslint/eslint-plugin": "^16.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^16.1.0",
|
||||
"@angular-eslint/template-parser": "^16.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vality/ng-core",
|
||||
"version": "16.1.1",
|
||||
"version": "16.2.0",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
|
@ -17,7 +17,7 @@ export class ConfirmDialogComponent extends DialogSuperclass<
|
||||
|
||||
confirm() {
|
||||
this.closeWithSuccess(
|
||||
this.dialogData?.hasReason ? { reason: this.control.value } : undefined
|
||||
this.dialogData?.hasReason ? { reason: this.control.value } : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export class DialogService {
|
||||
private dialog: MatDialog,
|
||||
@Optional()
|
||||
@Inject(DIALOG_CONFIG)
|
||||
private readonly dialogConfig: DialogConfig
|
||||
private readonly dialogConfig: DialogConfig,
|
||||
) {
|
||||
if (!dialogConfig) this.dialogConfig = DEFAULT_DIALOG_CONFIG;
|
||||
}
|
||||
@ -39,7 +39,7 @@ export class DialogService {
|
||||
data: TDialogData,
|
||||
configOrConfigName?:
|
||||
| Omit<MatDialogConfig<TDialogData>, 'data'>
|
||||
| keyof DialogConfig
|
||||
| keyof DialogConfig,
|
||||
]
|
||||
): MatDialogRef<TDialogComponent, DialogResponse<TDialogResponseData, TDialogResponseStatus>> {
|
||||
let config: Partial<MatDialogConfig<TDialogData>>;
|
||||
|
@ -10,7 +10,7 @@ export class DialogSuperclass<
|
||||
TDialogComponent,
|
||||
TDialogData = void,
|
||||
TDialogResponseData = void,
|
||||
TDialogResponseStatus = void
|
||||
TDialogResponseStatus = void,
|
||||
> {
|
||||
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.medium;
|
||||
|
||||
|
@ -54,13 +54,16 @@ export class FiltersComponent {
|
||||
if (b.breakpoints[Breakpoints.Medium]) return 3;
|
||||
if (b.breakpoints[Breakpoints.Small]) return 2;
|
||||
return 1;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
displayedFiltersCount$ = this.repeat$.pipe(map((r) => r * 2));
|
||||
filtersCount$ = new BehaviorSubject(0);
|
||||
|
||||
constructor(private dialog: DialogService, private breakpointObserver: BreakpointObserver) {}
|
||||
constructor(
|
||||
private dialog: DialogService,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
) {}
|
||||
|
||||
open() {
|
||||
this.dialog.open(FiltersDialogComponent, { filters: this });
|
||||
|
@ -2,15 +2,60 @@
|
||||
<mat-label>{{ label }}</mat-label>
|
||||
|
||||
<mtx-select
|
||||
[closeOnSelect]="!multiple"
|
||||
[formControl]="control"
|
||||
[items]="options"
|
||||
[loading]="progress"
|
||||
[markFirst]="true"
|
||||
[multiple]="multiple"
|
||||
[notFoundText]="externalSearch && !searchStr ? 'Enter your search term' : 'No items found'"
|
||||
[required]="required"
|
||||
[searchable]="true"
|
||||
[searchFn]="search"
|
||||
[selectableGroup]="true"
|
||||
[selectableGroupAsModel]="false"
|
||||
bindLabel="label"
|
||||
bindValue="value"
|
||||
/>
|
||||
groupBy="type"
|
||||
(clear)="searchChange.emit(''); searchStr = ''"
|
||||
(search)="searchChange.emit($event.term); searchStr = $event.term"
|
||||
>
|
||||
<ng-template let-index="index" let-item="item" let-item$="item$" ng-optgroup-tmp>
|
||||
<mat-checkbox
|
||||
*ngIf="multiple; else text"
|
||||
[ngModel]="item$.selected"
|
||||
class="checkbox-option"
|
||||
id="item-{{ index }}"
|
||||
>
|
||||
{{ item.type | uppercase }}
|
||||
</mat-checkbox>
|
||||
<ng-template #text>
|
||||
{{ item.type | uppercase }}
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template let-index="index" let-item="item" let-item$="item$" ng-option-tmp>
|
||||
<mat-checkbox
|
||||
*ngIf="multiple; else text"
|
||||
[ngModel]="item$.selected"
|
||||
class="checkbox-option"
|
||||
id="item-{{ index }}"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="text"></ng-container>
|
||||
</mat-checkbox>
|
||||
<ng-template #text>
|
||||
<div class="label">{{ item.label }}</div>
|
||||
<div *ngIf="item.description" class="description mat-caption mat-secondary-text">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</mtx-select>
|
||||
|
||||
<mat-hint>{{ hint }}</mat-hint>
|
||||
<mat-hint>{{
|
||||
hint ||
|
||||
(multiple && options && options.length > 1
|
||||
? (control.value?.length || 0) + '/' + options.length
|
||||
: '')
|
||||
}}</mat-hint>
|
||||
<mat-error>{{ error }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
@ -1,7 +1,26 @@
|
||||
.mat-mdc-form-field {
|
||||
::ng-deep .mat-mdc-form-field {
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
max-height: 56px;
|
||||
}
|
||||
|
||||
.ng-value-container {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .ng-dropdown-panel.ng-select-bottom {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.checkbox-option {
|
||||
margin: -12px 0 -12px -8px;
|
||||
}
|
||||
|
||||
.label,
|
||||
.description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { booleanAttribute, Component, Input } from '@angular/core';
|
||||
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
import { createControlProviders, FormControlSuperclass } from '../../utils';
|
||||
|
||||
@ -10,13 +10,27 @@ import { Option } from './types/option';
|
||||
styleUrls: ['./select-field.component.scss'],
|
||||
providers: createControlProviders(() => SelectFieldComponent),
|
||||
})
|
||||
export class SelectFieldComponent<T> extends FormControlSuperclass<T> {
|
||||
export class SelectFieldComponent<T> extends FormControlSuperclass<T[]> {
|
||||
@Input() options: Option<T>[] = [];
|
||||
@Output() searchChange = new EventEmitter<string>();
|
||||
|
||||
@Input() label?: string;
|
||||
@Input() hint?: string;
|
||||
@Input() error?: string;
|
||||
@Input() progress = false;
|
||||
|
||||
@Input({ transform: booleanAttribute }) externalSearch = false;
|
||||
@Input({ transform: booleanAttribute }) multiple = false;
|
||||
@Input({ transform: booleanAttribute }) required = false;
|
||||
|
||||
searchStr: string = '';
|
||||
|
||||
search = (term: string, item: Option<T>) => {
|
||||
if (this.externalSearch) return true;
|
||||
const termLowerCase = term.toLowerCase();
|
||||
return (
|
||||
item.label.toLowerCase().includes(termLowerCase) ||
|
||||
!!item.description?.includes?.(termLowerCase)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MtxSelectModule } from '@ng-matero/extensions/select';
|
||||
|
||||
@ -9,6 +10,13 @@ import { SelectFieldComponent } from './select-field.component';
|
||||
@NgModule({
|
||||
declarations: [SelectFieldComponent],
|
||||
exports: [SelectFieldComponent],
|
||||
imports: [CommonModule, MatInputModule, MtxSelectModule, FormsModule, ReactiveFormsModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatInputModule,
|
||||
MtxSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatCheckboxModule,
|
||||
],
|
||||
})
|
||||
export class SelectFieldModule {}
|
||||
|
@ -1,4 +1,6 @@
|
||||
export interface Option<T> {
|
||||
value: T;
|
||||
label: string;
|
||||
description?: string;
|
||||
type?: unknown;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="wrapper">
|
||||
<ng-container
|
||||
*ngIf="rowData | vSelect : colDef.tooltip : '' : [colDef] as tooltip; else linkTemplate"
|
||||
*ngIf="rowData | vSelect: colDef.tooltip : '' : [colDef] as tooltip; else linkTemplate"
|
||||
>
|
||||
<div [matTooltip]="tooltip | json" class="tooltip" matTooltipPosition="right">
|
||||
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
|
||||
@ -21,17 +21,16 @@
|
||||
|
||||
<ng-template #contentTemplate>
|
||||
<div
|
||||
*ngLet="rowData | vSelect : colDef.formatter ?? colDef.field : '' : [colDef] as value"
|
||||
*ngLet="rowData | vSelect: colDef.formatter ?? colDef.field : '' : [colDef] as value"
|
||||
class="value"
|
||||
>
|
||||
<ng-template [ngIf]="colDef.type === 'datetime'">
|
||||
{{ value | date : 'dd.MM.yyyy HH:mm:ss' : '+0000' }}
|
||||
{{ value | date: 'dd.MM.yyyy HH:mm:ss' : '+0000' }}
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="colDef.type === 'tag'">
|
||||
<v-tag *ngIf="colDef.typeParameters.tags[value] ?? {} as tag" [color]="tag.color">
|
||||
{{
|
||||
tag.label ??
|
||||
(rowData | vSelect : colDef.typeParameters.label : value : [colDef])
|
||||
tag.label ?? (rowData | vSelect: colDef.typeParameters.label : value : [colDef])
|
||||
}}
|
||||
</v-tag>
|
||||
</ng-template>
|
||||
@ -47,7 +46,7 @@
|
||||
{{
|
||||
value
|
||||
| amountCurrency
|
||||
: (rowData | vSelect : colDef?.typeParameters?.currencyCode : '' : [colDef])
|
||||
: (rowData | vSelect: colDef?.typeParameters?.currencyCode : '' : [colDef])
|
||||
: 'long'
|
||||
: (colDef.typeParameters.isMinor ? undefined : 0)
|
||||
}}
|
||||
@ -71,6 +70,6 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="description mat-caption mat-secondary-text">
|
||||
{{ rowData | vSelect : colDef.description : '' : [colDef] }}
|
||||
{{ rowData | vSelect: colDef.description : '' : [colDef] }}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -2,7 +2,7 @@ import { MenuColumn } from '../types/column';
|
||||
|
||||
export function createOperationColumn<T extends object>(
|
||||
items: MenuColumn<T>['typeParameters']['items'],
|
||||
other?: Partial<MenuColumn<T>>
|
||||
other?: Partial<MenuColumn<T>>,
|
||||
): MenuColumn<T> {
|
||||
return {
|
||||
typeParameters: { ...(other?.typeParameters || {}), items },
|
||||
|
@ -148,7 +148,7 @@ export class TableComponent<T extends object> implements Progressable, OnChanges
|
||||
this.renderedColumns.map((c) => [
|
||||
c.field,
|
||||
this.cellTemplate?.[c.field as never] ?? c.cellTemplate ?? this.defaultCellTemplate,
|
||||
])
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ type ColumnFn<TObject extends object, TResult = unknown> = (rowData: TObject) =>
|
||||
export type BaseColumn<
|
||||
T extends object,
|
||||
TType extends string | undefined = undefined,
|
||||
TTypeParameters extends object = never
|
||||
TTypeParameters extends object = never,
|
||||
> = Pick<
|
||||
MtxGridColumn<T>,
|
||||
| 'field'
|
||||
|
@ -9,14 +9,14 @@ import { formatCurrency } from '../utils';
|
||||
export class AmountCurrencyPipe implements PipeTransform {
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private _locale: string,
|
||||
@Inject(DEFAULT_CURRENCY_CODE) private _defaultCurrencyCode: string = 'USD'
|
||||
@Inject(DEFAULT_CURRENCY_CODE) private _defaultCurrencyCode: string = 'USD',
|
||||
) {}
|
||||
|
||||
transform(
|
||||
amount: unknown,
|
||||
currencyCode: string = this._defaultCurrencyCode,
|
||||
format: 'short' | 'long' = 'long',
|
||||
exponent?: number
|
||||
exponent?: number,
|
||||
): unknown {
|
||||
if (typeof amount === 'number')
|
||||
return formatCurrency(amount, currencyCode, format, this._locale, exponent);
|
||||
|
@ -5,3 +5,4 @@ export * from './enum-keys.pipe';
|
||||
export * from './enum-key-values.pipe';
|
||||
export * from './amount-currency.pipe';
|
||||
export * from './select.pipe';
|
||||
export * from './possibly-async.pipe';
|
||||
|
@ -6,6 +6,7 @@ import { EnumKeyValuesPipe } from './enum-key-values.pipe';
|
||||
import { EnumKeyPipe } from './enum-key.pipe';
|
||||
import { EnumKeysPipe } from './enum-keys.pipe';
|
||||
import { InlineJsonPipe } from './inline-json.pipe';
|
||||
import { VPossiblyAsyncPipe } from './possibly-async.pipe';
|
||||
import { VSelectPipe } from './select.pipe';
|
||||
|
||||
@NgModule({
|
||||
@ -17,6 +18,7 @@ import { VSelectPipe } from './select.pipe';
|
||||
EnumKeyValuesPipe,
|
||||
AmountCurrencyPipe,
|
||||
VSelectPipe,
|
||||
VPossiblyAsyncPipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -25,6 +27,7 @@ import { VSelectPipe } from './select.pipe';
|
||||
EnumKeyValuesPipe,
|
||||
AmountCurrencyPipe,
|
||||
VSelectPipe,
|
||||
VPossiblyAsyncPipe,
|
||||
],
|
||||
})
|
||||
export class PipesModule {}
|
||||
|
10
projects/ng-core/src/lib/pipes/possibly-async.pipe.ts
Normal file
10
projects/ng-core/src/lib/pipes/possibly-async.pipe.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { AsyncTransform } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'vPossiblyAsync',
|
||||
pure: false,
|
||||
})
|
||||
export class VPossiblyAsyncPipe<T> extends AsyncTransform<T> implements PipeTransform {}
|
@ -20,7 +20,7 @@ export class VSelectPipe<TObject extends object, TResult, TParams = void>
|
||||
obj: TObject,
|
||||
selectFn: SelectFn<TObject, TResult, TParams>,
|
||||
defaultValue?: TResult,
|
||||
rest: TParams[] = []
|
||||
rest: TParams[] = [],
|
||||
): TResult | null {
|
||||
const res = obj && selectFn ? select(obj, selectFn, defaultValue, rest) : null;
|
||||
if (isObservable(res) && !this.asyncPipe) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Observable, defer, mergeScan, map, BehaviorSubject, Subject } from 'rxjs';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
import { Observable, defer, mergeScan, map, BehaviorSubject, ReplaySubject, skipWhile } from 'rxjs';
|
||||
import { shareReplay, startWith } from 'rxjs/operators';
|
||||
|
||||
import { inProgressFrom, progressTo } from '../../utils';
|
||||
|
||||
@ -30,17 +30,19 @@ export interface Accumulator<TResultItem, TParams, TContinuationToken> {
|
||||
continuationToken?: TContinuationToken;
|
||||
}
|
||||
|
||||
const DEFAULT_SIZE = 25;
|
||||
|
||||
export abstract class FetchSuperclass<TResultItem, TParams = void, TContinuationToken = string> {
|
||||
result$ = defer(() => this.state$).pipe(map(({ result }) => result));
|
||||
hasMore$ = defer(() => this.state$).pipe(map(({ continuationToken }) => !!continuationToken));
|
||||
isLoading$ = inProgressFrom(
|
||||
() => this.progress$,
|
||||
() => this.state$
|
||||
() => this.state$,
|
||||
);
|
||||
|
||||
private fetch$ = new Subject<Action<TParams>>();
|
||||
private fetch$ = new ReplaySubject<Action<TParams>>(1);
|
||||
private progress$ = new BehaviorSubject(0);
|
||||
private state$ = this.fetch$.pipe(
|
||||
private state$ = defer(() => this.fetch$.pipe(skipWhile(({ type }) => type !== 'load'))).pipe(
|
||||
mergeScan<Action<TParams>, Accumulator<TResultItem, TParams, TContinuationToken>>(
|
||||
(acc, action) => {
|
||||
const params = (action.type === 'load' ? action.params : acc.params) as TParams;
|
||||
@ -48,6 +50,16 @@ export abstract class FetchSuperclass<TResultItem, TParams = void, TContinuation
|
||||
const continuationToken =
|
||||
action.type === 'more' ? acc.continuationToken : undefined;
|
||||
return this.fetch(params, { size, continuationToken }).pipe(
|
||||
...((action.type === 'load'
|
||||
? ([
|
||||
startWith({
|
||||
params,
|
||||
result: [],
|
||||
size,
|
||||
continuationToken: undefined,
|
||||
}),
|
||||
] as unknown)
|
||||
: []) as []),
|
||||
map(({ result, continuationToken }) => ({
|
||||
params,
|
||||
result:
|
||||
@ -55,16 +67,16 @@ export abstract class FetchSuperclass<TResultItem, TParams = void, TContinuation
|
||||
size,
|
||||
continuationToken,
|
||||
})),
|
||||
progressTo(this.progress$)
|
||||
progressTo(this.progress$),
|
||||
);
|
||||
},
|
||||
{
|
||||
size: 25,
|
||||
size: DEFAULT_SIZE,
|
||||
result: [],
|
||||
},
|
||||
1
|
||||
1,
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
load(params: TParams, options: LoadOptions = {}): void {
|
||||
@ -77,6 +89,6 @@ export abstract class FetchSuperclass<TResultItem, TParams = void, TContinuation
|
||||
|
||||
protected abstract fetch(
|
||||
params: TParams,
|
||||
options: FetchOptions<TContinuationToken>
|
||||
options: FetchOptions<TContinuationToken>,
|
||||
): Observable<FetchResult<TResultItem, TContinuationToken>>;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export class LogError {
|
||||
|
||||
get details() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.source).filter(([k, v]) => k !== 'name' && k !== 'message' && v)
|
||||
Object.entries(this.source).filter(([k, v]) => k !== 'name' && k !== 'message' && v),
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ export class LogError {
|
||||
this.source = (isObject(error)
|
||||
? error
|
||||
: new Error(
|
||||
error ? String(error) : DEFAULT_ERROR_NAME
|
||||
error ? String(error) : DEFAULT_ERROR_NAME,
|
||||
)) as unknown as LogError['source'];
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import { first, of, timeout, Observer } from 'rxjs';
|
||||
|
||||
import { isAsync, PossiblyAsync } from '../../utils';
|
||||
|
||||
import { DEFAULT_ERROR_NAME, LogError } from './log-error';
|
||||
import { Operation } from './types/operation';
|
||||
|
||||
const DEFAULT_DURATION_MS = 3000;
|
||||
const DEFAULT_ERROR_DURATION_MS = 10000;
|
||||
const DEFAULT_DURATION_MS = 3_000;
|
||||
const DEFAULT_ERROR_DURATION_MS = 10_000;
|
||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NotifyLogService {
|
||||
constructor(private snackBar: MatSnackBar) {}
|
||||
|
||||
success = (message: string = 'Completed successfully'): void => {
|
||||
success = (message: PossiblyAsync<string> = 'Completed successfully'): void => {
|
||||
this.notify(message);
|
||||
};
|
||||
|
||||
error = (errors: unknown | unknown[], message?: string): void => {
|
||||
error = (errors: unknown | unknown[], message?: PossiblyAsync<string>): void => {
|
||||
const logErrors = (Array.isArray(errors) ? errors : [errors]).map((e) => new LogError(e));
|
||||
message = message || (logErrors.length === 1 ? logErrors[0].message : DEFAULT_ERROR_NAME);
|
||||
console.warn(
|
||||
[`Caught error: ${message}.`, ...logErrors.map((e) => e.getLogMessage())].join('\n')
|
||||
);
|
||||
this.subscribeWithTimeout(message, (msg) => {
|
||||
console.warn(
|
||||
[`Caught error: ${msg}.`, ...logErrors.map((e) => e.getLogMessage())].join('\n'),
|
||||
);
|
||||
});
|
||||
this.notify(message, DEFAULT_ERROR_DURATION_MS);
|
||||
};
|
||||
|
||||
@ -67,9 +73,25 @@ export class NotifyLogService {
|
||||
this.error(errors, message);
|
||||
}
|
||||
|
||||
private notify(message: string, duration = DEFAULT_DURATION_MS) {
|
||||
return this.snackBar.open(message, 'OK', {
|
||||
duration,
|
||||
private notify(message: PossiblyAsync<string>, duration = DEFAULT_DURATION_MS) {
|
||||
this.subscribeWithTimeout(message, {
|
||||
next: (msg) => {
|
||||
this.snackBar.open(msg, 'OK', {
|
||||
duration,
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
// TODO: Default message
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeWithTimeout<T>(
|
||||
possiblyAsync: PossiblyAsync<T>,
|
||||
subscribe: Partial<Observer<T>> | Observer<T>['next'],
|
||||
) {
|
||||
(isAsync(possiblyAsync) ? possiblyAsync : of(possiblyAsync))
|
||||
.pipe(first(), timeout(DEFAULT_TIMEOUT_MS))
|
||||
.subscribe(subscribe);
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export class QueryParamsService<P extends object> {
|
||||
startWith(this.route.snapshot.queryParams),
|
||||
distinctUntilChanged(isEqual),
|
||||
map((params) => this.deserialize(params)),
|
||||
shareReplay({ refCount: true, bufferSize: 1 })
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
get params(): P {
|
||||
@ -32,7 +32,7 @@ export class QueryParamsService<P extends object> {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@Optional() @Inject(QUERY_PARAMS_SERIALIZERS) private readonly serializers?: Serializer[]
|
||||
@Optional() @Inject(QUERY_PARAMS_SERIALIZERS) private readonly serializers?: Serializer[],
|
||||
) {
|
||||
// Angular @Optional not support TS syntax: `serializers: Serializer[] = []`
|
||||
if (!this.serializers) {
|
||||
@ -54,12 +54,15 @@ export class QueryParamsService<P extends object> {
|
||||
|
||||
private serialize(
|
||||
params: P,
|
||||
{ filter = negate(isEmpty) }: Options = {}
|
||||
{ filter = negate(isEmpty) }: Options = {},
|
||||
): { [key: string]: string } {
|
||||
return Object.entries(params).reduce((acc, [k, v]) => {
|
||||
if (filter(v, k)) acc[k] = serializeQueryParam(v, this.serializers);
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
return Object.entries(params).reduce(
|
||||
(acc, [k, v]) => {
|
||||
if (filter(v, k)) acc[k] = serializeQueryParam(v, this.serializers);
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
}
|
||||
|
||||
private deserialize(params: Params): P {
|
||||
|
@ -3,5 +3,5 @@ import { InjectionToken } from '@angular/core';
|
||||
import { Serializer } from '../types/serializer';
|
||||
|
||||
export const QUERY_PARAMS_SERIALIZERS = new InjectionToken<Serializer[]>(
|
||||
'query params serializers'
|
||||
'query params serializers',
|
||||
);
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { ValuesType } from 'utility-types';
|
||||
|
||||
import { isEmpty } from './is-empty';
|
||||
import { isEmptyPrimitive } from './is-empty-primitive';
|
||||
import { isEmpty } from '../empty/is-empty';
|
||||
import { isEmptyPrimitive } from '../empty/is-empty-primitive';
|
||||
|
||||
export function clean<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
T extends ReadonlyArray<any> | ArrayLike<any> | Record<any, any>,
|
||||
TAllowRootRemoval extends boolean = false
|
||||
TAllowRootRemoval extends boolean = false,
|
||||
>(
|
||||
obj: T,
|
||||
allowRootRemoval: TAllowRootRemoval = false as TAllowRootRemoval,
|
||||
isNotDeep = false,
|
||||
filterPredicate: (v: unknown, k?: PropertyKey) => boolean = (v) => !isEmpty(v)
|
||||
filterPredicate: (v: unknown, k?: PropertyKey) => boolean = (v) => !isEmpty(v),
|
||||
): TAllowRootRemoval extends true ? T | null : T {
|
||||
if (allowRootRemoval && !filterPredicate(obj)) {
|
||||
return null as never;
|
||||
@ -27,7 +27,7 @@ export function clean<
|
||||
return Object.fromEntries(
|
||||
(Object.entries(obj) as [keyof T, ValuesType<T>][])
|
||||
.map(([k, v]) => [k, cleanChild(v)] as const)
|
||||
.filter(([k, v]) => filterPredicate(v, k))
|
||||
.filter(([k, v]) => filterPredicate(v, k)),
|
||||
) as never;
|
||||
}
|
||||
return obj;
|
||||
@ -36,7 +36,7 @@ export function clean<
|
||||
export function cleanPrimitiveProps<T extends object>(
|
||||
obj: T,
|
||||
allowRootRemoval = false,
|
||||
isNotDeep = false
|
||||
isNotDeep = false,
|
||||
) {
|
||||
return clean(obj, allowRootRemoval, isNotDeep, (v) => !isEmptyPrimitive(v));
|
||||
}
|
1
projects/ng-core/src/lib/utils/clean/index.ts
Normal file
1
projects/ng-core/src/lib/utils/clean/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './clean';
|
1
projects/ng-core/src/lib/utils/compare/index.ts
Normal file
1
projects/ng-core/src/lib/utils/compare/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './compare-different-types';
|
@ -8,13 +8,13 @@ export function formatCurrency(
|
||||
currencyCode: string = 'USD',
|
||||
format: 'short' | 'long' = 'long',
|
||||
locale = 'en-GB',
|
||||
exponent?: number
|
||||
exponent?: number,
|
||||
): string {
|
||||
return ngFormatCurrency(
|
||||
isNil(exponent) ? toMajor(amount, currencyCode) : toMajorByExponent(amount, exponent),
|
||||
locale,
|
||||
getCurrencySymbol(currencyCode, 'narrow', locale),
|
||||
currencyCode,
|
||||
format === 'short' ? '0.0-2' : undefined
|
||||
format === 'short' ? '0.0-2' : undefined,
|
||||
);
|
||||
}
|
||||
|
2
projects/ng-core/src/lib/utils/empty/index.ts
Normal file
2
projects/ng-core/src/lib/utils/empty/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './is-empty';
|
||||
export * from './is-empty-primitive';
|
@ -1,7 +1,7 @@
|
||||
import { ValuesType } from 'utility-types';
|
||||
|
||||
export function getEnumEntries<E extends Record<PropertyKey, unknown>>(
|
||||
srcEnum: E
|
||||
srcEnum: E,
|
||||
): [key: keyof E, value: ValuesType<E>][] {
|
||||
if (!srcEnum) return [];
|
||||
const entries = Object.entries(srcEnum);
|
||||
@ -12,7 +12,7 @@ export function getEnumEntries<E extends Record<PropertyKey, unknown>>(
|
||||
}
|
||||
|
||||
export function getEnumKeyValues<E extends Record<PropertyKey, unknown>>(
|
||||
srcEnum: E
|
||||
srcEnum: E,
|
||||
): { key: keyof E; value: ValuesType<E> }[] {
|
||||
return getEnumEntries(srcEnum).map(([key, value]) => ({ key, value }));
|
||||
}
|
||||
@ -27,14 +27,14 @@ export function getEnumValues<E extends Record<PropertyKey, unknown>>(srcEnum: E
|
||||
|
||||
export function getEnumKey<E extends Record<PropertyKey, unknown>>(
|
||||
srcEnum: E,
|
||||
value?: ValuesType<E>
|
||||
value?: ValuesType<E>,
|
||||
): keyof E {
|
||||
return getEnumKeyValues(srcEnum).find((e) => String(e.value) === String(value))?.key as keyof E;
|
||||
}
|
||||
|
||||
export function enumHasValue<E extends Record<PropertyKey, unknown>>(
|
||||
srcEnum: E,
|
||||
value: ValuesType<E> | string
|
||||
value: ValuesType<E> | string,
|
||||
): value is ValuesType<E> {
|
||||
return getEnumValues(srcEnum).includes(value as ValuesType<E>);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export abstract class AbstractControlSuperclass<OuterType, InnerType = OuterType
|
||||
.pipe(
|
||||
filter(() => this.control.invalid),
|
||||
take(1),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.onValidatorChange();
|
||||
|
@ -5,5 +5,5 @@ import { FormGroupSuperclass } from './form-group-superclass.directive';
|
||||
@Directive()
|
||||
export abstract class FormArraySuperclass<
|
||||
OuterType,
|
||||
InnerType = OuterType
|
||||
InnerType = OuterType,
|
||||
> extends FormGroupSuperclass<OuterType, InnerType> {}
|
||||
|
@ -6,7 +6,7 @@ import { AbstractControlSuperclass } from './abstract-control-superclass';
|
||||
@Directive()
|
||||
export class FormControlSuperclass<
|
||||
OuterType,
|
||||
InnerType = OuterType
|
||||
InnerType = OuterType,
|
||||
> extends AbstractControlSuperclass<OuterType, InnerType> {
|
||||
control = new FormControl() as FormControl<InnerType>;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export function getErrorsTree(control: AbstractControl): ValidationErrors | null
|
||||
errors['formGroupErrors'] = Object.fromEntries(
|
||||
Array.from(Object.entries(control.controls))
|
||||
.map(([k, c]) => [k, getErrorsTree(c)])
|
||||
.filter(([, v]) => !!v)
|
||||
.filter(([, v]) => !!v),
|
||||
) as ValidationErrors;
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ import { getValue } from './get-value';
|
||||
|
||||
export function getFormValueChanges<T>(
|
||||
form: AbstractControl<FormControlState<T> | T>,
|
||||
hasStart = false
|
||||
hasStart = false,
|
||||
): Observable<T> {
|
||||
return form.valueChanges.pipe(
|
||||
...((hasStart ? [startWith(form.value)] : []) as []),
|
||||
map(() => getValue(form))
|
||||
map(() => getValue(form)),
|
||||
) as Observable<T>;
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ import { AbstractControl } from '@angular/forms';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
|
||||
import { isEmptyPrimitive } from '../is-empty-primitive';
|
||||
import { isEmptyPrimitive } from '../empty/is-empty-primitive';
|
||||
|
||||
import { getFormValueChanges } from './get-form-value-changes';
|
||||
|
||||
export function getValidValueChanges(control: AbstractControl, predicate = isEmptyPrimitive) {
|
||||
return getFormValueChanges(control, true).pipe(
|
||||
filter(() => control.valid),
|
||||
map((value) => omitBy(value, predicate))
|
||||
map((value) => omitBy(value, predicate)),
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { AbstractControl } from '@angular/forms';
|
||||
export function setDisabled(
|
||||
control?: AbstractControl,
|
||||
disabled: boolean = true,
|
||||
options: Parameters<AbstractControl['disable']>[0] = {}
|
||||
options: Parameters<AbstractControl['disable']>[0] = {},
|
||||
) {
|
||||
if (!control || control.disabled === disabled) return;
|
||||
if (disabled) control.disable(options);
|
||||
|
@ -7,6 +7,6 @@ export function getImportValue<T>(imp: Promise<unknown>, prop: string = 'default
|
||||
map((module) => {
|
||||
if (!prop) return module as T;
|
||||
return get(module, prop, null) as T;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
1
projects/ng-core/src/lib/utils/import/index.ts
Normal file
1
projects/ng-core/src/lib/utils/import/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './get-import-value';
|
@ -7,10 +7,8 @@ export * from './date';
|
||||
export * from './currency';
|
||||
export * from './object';
|
||||
export * from './enum';
|
||||
|
||||
export * from './pipe';
|
||||
export * from './clean';
|
||||
export * from './is-empty';
|
||||
export * from './compare-different-types';
|
||||
export * from './is-empty-primitive';
|
||||
|
||||
export * from './get-import-value';
|
||||
export * from './empty';
|
||||
export * from './compare';
|
||||
export * from './import';
|
||||
|
@ -9,7 +9,7 @@ export function select<TObject extends object, TResult, TParams = void>(
|
||||
obj: TObject,
|
||||
selectFn: SelectFn<TObject, TResult, TParams>,
|
||||
defaultValue?: TResult,
|
||||
restParams: TParams[] = []
|
||||
restParams: TParams[] = [],
|
||||
): TResult | Observable<TResult> {
|
||||
if (typeof selectFn === 'string') return _get(obj, selectFn, defaultValue) as TResult;
|
||||
return selectFn(obj, ...restParams);
|
||||
@ -19,7 +19,7 @@ export function selectAsObservable<TObject extends object, TResult, TParams = vo
|
||||
obj: TObject,
|
||||
selectFn: SelectFn<TObject, TResult, TParams>,
|
||||
defaultValue?: TResult,
|
||||
restParams: TParams[] = []
|
||||
restParams: TParams[] = [],
|
||||
): TResult | Observable<TResult> {
|
||||
const res = select(obj, selectFn, defaultValue, restParams);
|
||||
return isObservable(res) ? res : of(res);
|
||||
|
@ -14,7 +14,7 @@ export interface Result<T> {
|
||||
export function forkJoinToResult<T>(
|
||||
sources: Observable<T>[],
|
||||
concurrency = 4,
|
||||
progress$?: Subject<number>
|
||||
progress$?: Subject<number>,
|
||||
): Observable<Result<T>[]> {
|
||||
let completed = 0;
|
||||
if (progress$) progress$.next(getProgressByCount(sources.length));
|
||||
@ -32,16 +32,16 @@ export function forkJoinToResult<T>(
|
||||
finalize(() => {
|
||||
completed += 1;
|
||||
if (progress$) progress$.next(getProgressByCount(sources.length, completed));
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
concurrency
|
||||
concurrency,
|
||||
).pipe(
|
||||
scan((acc, value) => {
|
||||
acc.push(value);
|
||||
return acc;
|
||||
}, [] as Result<T>[]),
|
||||
takeLast(1),
|
||||
map((r) => r.sort((a, b) => a.index - b.index))
|
||||
map((r) => r.sort((a, b) => a.index - b.index)),
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,6 @@ export function inProgressFrom(progress: ObservableOrFn<number>, main?: Observab
|
||||
map(Boolean),
|
||||
// make async to bypass angular detect changes
|
||||
delay(0),
|
||||
share()
|
||||
share(),
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { catchError, EMPTY, MonoTypeOperatorFunction, of } from 'rxjs';
|
||||
|
||||
export function passError<T>(
|
||||
handler: (err: unknown) => void,
|
||||
value?: T
|
||||
value?: T,
|
||||
): MonoTypeOperatorFunction<T> {
|
||||
return (source) =>
|
||||
source.pipe(
|
||||
@ -10,6 +10,6 @@ export function passError<T>(
|
||||
handler(err);
|
||||
if (arguments.length >= 2) return of(value as T);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { BehaviorSubject, defer, MonoTypeOperatorFunction, isObservable } from '
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
export function progressTo<T>(
|
||||
subject$: BehaviorSubject<number> | (() => BehaviorSubject<number>)
|
||||
subject$: BehaviorSubject<number> | (() => BehaviorSubject<number>),
|
||||
): MonoTypeOperatorFunction<T> {
|
||||
const getSub = () => (isObservable(subject$) ? subject$ : subject$());
|
||||
return (src$) =>
|
||||
|
31
projects/ng-core/src/lib/utils/pipe/async-transform.ts
Normal file
31
projects/ng-core/src/lib/utils/pipe/async-transform.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { ChangeDetectorRef, Inject, Injectable, OnDestroy, PipeTransform } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { isAsync, PossiblyAsync } from './is-async';
|
||||
|
||||
@Injectable()
|
||||
export abstract class AsyncTransform<T = unknown> implements OnDestroy, PipeTransform {
|
||||
private asyncPipe?: AsyncPipe;
|
||||
private cdr = Inject(ChangeDetectorRef);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transform(value: any, ...args: any[]): any {
|
||||
return this.asyncTransform(this.getValue(value, ...args));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.asyncPipe?.ngOnDestroy?.();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected getValue(value: any, ..._args: any[]): Observable<T> {
|
||||
return value;
|
||||
}
|
||||
|
||||
protected asyncTransform(value: PossiblyAsync<T>) {
|
||||
if (!isAsync(value)) return value;
|
||||
if (!this.asyncPipe) this.asyncPipe = new AsyncPipe(this.cdr);
|
||||
return this.asyncPipe.transform(value);
|
||||
}
|
||||
}
|
2
projects/ng-core/src/lib/utils/pipe/index.ts
Normal file
2
projects/ng-core/src/lib/utils/pipe/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './async-transform';
|
||||
export * from './is-async';
|
8
projects/ng-core/src/lib/utils/pipe/is-async.ts
Normal file
8
projects/ng-core/src/lib/utils/pipe/is-async.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { isObservable, Observable } from 'rxjs';
|
||||
|
||||
export type Async<T> = Observable<T>;
|
||||
export type PossiblyAsync<T> = Async<T> | T;
|
||||
|
||||
export function isAsync<T>(value: PossiblyAsync<T>): value is Async<T> {
|
||||
return isObservable(value);
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"plugins": ["prettier-plugin-organize-attributes"],
|
||||
"attributeSort": "ASC",
|
||||
"attributeGroups": [
|
||||
"$ANGULAR_ELEMENT_REF",
|
||||
@ -20,6 +21,10 @@
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.svg",
|
||||
"options": { "parser": "html" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@vality/prettier-config",
|
||||
"version": "0.1.0",
|
||||
"version": "3.0.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "index.json",
|
||||
"peerDependencies": {
|
||||
"prettier": "^2.0.0"
|
||||
"prettier": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prettier-plugin-organize-attributes": "^0.0.5"
|
||||
"prettier-plugin-organize-attributes": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user