Browse Source

UI: Handle widget page data overflow. Introduce widget messages toast. Fix entity list sort order.

pull/3053/head
Igor Kulikov 6 years ago
parent
commit
ec0058614b
  1. 32
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 16
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  3. 8
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  4. 5
      ui-ngx/src/app/core/api/entity-data.service.ts
  5. 9
      ui-ngx/src/app/core/api/widget-api.models.ts
  6. 39
      ui-ngx/src/app/core/api/widget-subscription.ts
  7. 2
      ui-ngx/src/app/core/notification/notification.models.ts
  8. 3
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  9. 3
      ui-ngx/src/app/modules/home/components/widget/widget.component.html
  10. 34
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  11. 23
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  12. 3
      ui-ngx/src/app/shared/components/snack-bar-component.html
  13. 3
      ui-ngx/src/app/shared/components/snack-bar-component.scss
  14. 300
      ui-ngx/src/app/shared/components/toast.directive.ts
  15. 10
      ui-ngx/src/app/shared/models/page/page-link.ts
  16. 1
      ui-ngx/src/app/shared/models/widget.models.ts
  17. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  18. 4
      ui-ngx/src/styles.scss

32
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

16
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

File diff suppressed because one or more lines are too long

8
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -467,14 +467,15 @@ export class EntityDataSubscription {
{
pageData,
data,
datasourceIndex: this.listener.configDatasourceIndex
datasourceIndex: this.listener.configDatasourceIndex,
pageLink: this.entityDataSubscriptionOptions.pageLink
}
);
this.entityDataResolveSubject.complete();
} else {
if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.listener.dataLoaded(pageData, data,
this.listener.configDatasourceIndex);
this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink);
}
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) {
if (this.datasourceType === DatasourceType.function) {
@ -484,7 +485,8 @@ export class EntityDataSubscription {
{
pageData,
data,
datasourceIndex: this.listener.configDatasourceIndex
datasourceIndex: this.listener.configDatasourceIndex,
pageLink: this.entityDataSubscriptionOptions.pageLink
}
);
this.entityDataResolveSubject.complete();

5
ui-ngx/src/app/core/api/entity-data.service.ts

@ -34,7 +34,9 @@ export interface EntityDataListener {
subscriptionTimewindow?: SubscriptionTimewindow;
configDatasource: Datasource;
configDatasourceIndex: number;
dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void;
dataLoaded: (pageData: PageData<EntityData>,
data: Array<Array<DataSetHolder>>,
datasourceIndex: number, pageLink: EntityDataPageLink) => void;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
initialPageDataChanged?: (nextPageData: PageData<EntityData>) => void;
updateRealtimeSubscription?: () => SubscriptionTimewindow;
@ -46,6 +48,7 @@ export interface EntityDataLoadResult {
pageData: PageData<EntityData>;
data: Array<Array<DataSetHolder>>;
datasourceIndex: number;
pageLink: EntityDataPageLink;
}
@Injectable({

9
ui-ngx/src/app/core/api/widget-api.models.ts

@ -180,9 +180,17 @@ export class WidgetSubscriptionContext {
getServerTimeDiff: () => Observable<number>;
}
export type SubscriptionMessageSeverity = 'info' | 'warn' | 'error' | 'success';
export interface SubscriptionMessage {
severity: SubscriptionMessageSeverity,
message: string;
}
export interface WidgetSubscriptionCallbacks {
onDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
onDataUpdateError?: (subscription: IWidgetSubscription, e: any) => void;
onSubscriptionMessage?: (subscription: IWidgetSubscription, message: SubscriptionMessage) => void;
onInitialPageDataChanged?: (subscription: IWidgetSubscription, nextPageData: PageData<EntityData>) => void;
dataLoading?: (subscription: IWidgetSubscription) => void;
legendDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
@ -204,6 +212,7 @@ export interface WidgetSubscriptionOptions {
datasources?: Array<Datasource>;
hasDataPageLink?: boolean;
singleEntity?: boolean;
warnOnPageDataOverflow?: boolean;
targetDeviceAliasIds?: Array<string>;
targetDeviceIds?: Array<string>;
useDashboardTimewindow?: boolean;

39
ui-ngx/src/app/core/api/widget-subscription.ts

@ -16,7 +16,7 @@
import {
IWidgetSubscription,
SubscriptionEntityInfo,
SubscriptionEntityInfo, SubscriptionMessage,
WidgetSubscriptionCallbacks,
WidgetSubscriptionContext,
WidgetSubscriptionOptions
@ -78,6 +78,7 @@ export class WidgetSubscription implements IWidgetSubscription {
hasDataPageLink: boolean;
singleEntity: boolean;
warnOnPageDataOverflow: boolean;
datasourcePages: PageData<Datasource>[];
dataPages: PageData<Array<DatasourceData>>[];
@ -174,6 +175,7 @@ export class WidgetSubscription implements IWidgetSubscription {
} else if (this.type === widgetType.alarm) {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {});
this.callbacks.onSubscriptionMessage = this.callbacks.onSubscriptionMessage || (() => {});
this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {});
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {});
this.alarmSource = options.alarmSource;
@ -208,6 +210,7 @@ export class WidgetSubscription implements IWidgetSubscription {
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {});
this.callbacks.onSubscriptionMessage = this.callbacks.onSubscriptionMessage || (() => {});
this.callbacks.onInitialPageDataChanged = this.callbacks.onInitialPageDataChanged || (() => {});
this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {});
this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || (() => {});
@ -217,6 +220,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.entityDataListeners = [];
this.hasDataPageLink = options.hasDataPageLink;
this.singleEntity = options.singleEntity;
this.warnOnPageDataOverflow = options.warnOnPageDataOverflow;
this.datasourcePages = [];
this.datasources = [];
this.dataPages = [];
@ -417,8 +421,8 @@ export class WidgetSubscription implements IWidgetSubscription {
subscriptionType: this.type,
configDatasource: datasource,
configDatasourceIndex: index,
dataLoaded: (pageData, data1, datasourceIndex) => {
this.dataLoaded(pageData, data1, datasourceIndex, true)
dataLoaded: (pageData, data1, datasourceIndex, pageLink) => {
this.dataLoaded(pageData, data1, datasourceIndex, pageLink, true)
},
initialPageDataChanged: this.initialPageDataChanged.bind(this),
dataUpdated: this.dataUpdated.bind(this),
@ -443,7 +447,7 @@ export class WidgetSubscription implements IWidgetSubscription {
return forkJoin(resolveResultObservables).pipe(
map((resolveResults) => {
resolveResults.forEach((resolveResult) => {
this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, false);
this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, resolveResult.pageLink, false);
});
this.configureLoadedData();
this.hasResolvedData = this.datasources.length > 0;
@ -533,6 +537,16 @@ export class WidgetSubscription implements IWidgetSubscription {
});
}
private onSubscriptionMessage(message: SubscriptionMessage) {
if (this.cafs.message) {
this.cafs.message();
this.cafs.message = null;
}
this.cafs.message = this.ctx.raf.raf(() => {
this.callbacks.onSubscriptionMessage(this, message);
});
}
onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) {
if (this.type === widgetType.timeseries || this.type === widgetType.alarm) {
if (this.useDashboardTimewindow) {
@ -776,8 +790,8 @@ export class WidgetSubscription implements IWidgetSubscription {
configDatasource: datasource,
configDatasourceIndex: datasourceIndex,
subscriptionTimewindow: this.subscriptionTimewindow,
dataLoaded: (pageData, data1, datasourceIndex1) => {
this.dataLoaded(pageData, data1, datasourceIndex1, true)
dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => {
this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true)
},
dataUpdated: this.dataUpdated.bind(this),
updateRealtimeSubscription: () => {
@ -1039,7 +1053,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private dataLoaded(pageData: PageData<EntityData>,
data: Array<Array<DataSetHolder>>,
datasourceIndex: number, isUpdate: boolean) {
datasourceIndex: number, pageLink: EntityDataPageLink, isUpdate: boolean) {
const datasource = this.configuredDatasources[datasourceIndex];
datasource.dataReceived = true;
const datasources = pageData.data.map((entityData, index) =>
@ -1062,6 +1076,17 @@ export class WidgetSubscription implements IWidgetSubscription {
totalPages: pageData.totalPages
};
this.dataPages[datasourceIndex] = datasourceDataPage;
if (datasource.type === DatasourceType.entity &&
pageData.hasNext && pageLink.pageSize > 1) {
if (this.warnOnPageDataOverflow) {
const message = this.ctx.translate.instant('widget.data-overflow',
{count: pageData.data.length, total: pageData.totalElements});
this.onSubscriptionMessage({
severity: 'warn',
message
})
}
}
if (isUpdate) {
this.configureLoadedData();
this.onDataUpdated(true);

2
ui-ngx/src/app/core/notification/notification.models.ts

@ -20,7 +20,7 @@ export interface NotificationState {
hideNotification: HideNotification;
}
export declare type NotificationType = 'info' | 'success' | 'error';
export declare type NotificationType = 'info' | 'warn' | 'success' | 'error';
export declare type NotificationHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right';
export declare type NotificationVerticalPosition = 'top' | 'bottom';

3
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -358,6 +358,9 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.singleEntity)) {
result.typeParameters.singleEntity = false;
}
if (isUndefined(result.typeParameters.warnOnPageDataOverflow)) {
result.typeParameters.warnOnPageDataOverflow = true;
}
if (isUndefined(result.typeParameters.dataKeysOptional)) {
result.typeParameters.dataKeysOptional = false;
}

3
ui-ngx/src/app/modules/home/components/widget/widget.component.html

@ -15,7 +15,8 @@
limitations under the License.
-->
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType">
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType" tb-toast
toastTarget="{{ toastTargetId }}">
<tb-legend *ngIf="displayLegend && isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"

34
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -68,7 +68,7 @@ import {
StateObject,
StateParams,
SubscriptionEntityInfo,
SubscriptionInfo,
SubscriptionInfo, SubscriptionMessage,
WidgetSubscriptionContext,
WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
@ -93,6 +93,7 @@ import { ServicesMap } from '@home/models/services.map';
import { ResizeObserver } from '@juggle/resize-observer';
import { EntityDataService } from '@core/api/entity-data.service';
import { TranslateService } from '@ngx-translate/core';
import { NotificationType } from '@core/notification/notification.models';
@Component({
selector: 'tb-widget',
@ -142,9 +143,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
widgetSizeDetected = false;
widgetInstanceInited = false;
dataUpdatePending = false;
pendingMessage: SubscriptionMessage;
cafs: {[cafId: string]: CancelAnimationFrame} = {};
toastTargetId = 'widget-messages-' + this.utils.guid();
private widgetResize$: ResizeObserver;
private cssParser = new cssjs();
@ -368,6 +372,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
this.subscriptionInited = false;
this.dataUpdatePending = false;
this.pendingMessage = null;
this.widgetContext.subscriptions = {};
if (this.widgetContext.inited) {
this.widgetContext.inited = false;
@ -490,6 +495,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetTypeInstance.onDataUpdated();
this.dataUpdatePending = false;
}
if (this.pendingMessage) {
this.displayMessage(this.pendingMessage.severity, this.pendingMessage.message);
this.pendingMessage = null;
}
} else {
this.loadingData = false;
this.displayNoData = true;
@ -678,6 +687,14 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.detectChanges();
}
private displayMessage(type: NotificationType, message: string, duration?: number) {
this.widgetContext.showToast(type, message, duration, 'bottom', 'right', this.toastTargetId);
}
private clearMessage() {
this.widgetContext.hideToast(this.toastTargetId);
}
private configureDynamicWidgetComponent() {
this.widgetContentContainer.clear();
const injector: Injector = Injector.create(
@ -815,6 +832,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
onDataUpdateError: (subscription, e) => {
this.handleWidgetException(e);
},
onSubscriptionMessage: (subscription, message) => {
if (this.displayWidgetInstance()) {
if (this.widgetInstanceInited) {
this.displayMessage(message.severity, message.message);
} else {
this.pendingMessage = message;
}
}
},
onInitialPageDataChanged: (subscription, nextPageData) => {
this.reInit();
},
@ -855,6 +881,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
stateData: this.typeParameters.stateData,
hasDataPageLink: this.typeParameters.hasDataPageLink,
singleEntity: this.typeParameters.singleEntity,
warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow,
comparisonEnabled: comparisonSettings.comparisonEnabled,
timeForComparison: comparisonSettings.timeForComparison
};
@ -910,6 +937,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
this.clearMessage();
this.detectChanges();
}
},
@ -918,6 +946,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
if (subscription.rpcErrorText) {
this.displayMessage('error', subscription.rpcErrorText);
}
this.detectChanges();
}
},
@ -925,6 +956,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.rpcErrorText = null;
this.dynamicWidgetComponent.rpcRejection = null;
this.clearMessage();
this.detectChanges();
}
}

23
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -57,7 +57,7 @@ import {
NotificationType,
NotificationVerticalPosition
} from '@core/notification/notification.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { DeviceService } from '@core/http/device.service';
@ -244,6 +244,20 @@ export class WidgetContext {
this.showToast('success', message, duration, verticalPosition, horizontalPosition, target);
}
showInfoToast(message: string,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
this.showToast('info', message, undefined, verticalPosition, horizontalPosition, target);
}
showWarnToast(message: string,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
target?: string) {
this.showToast('warn', message, undefined, verticalPosition, horizontalPosition, target);
}
showErrorToast(message: string,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
@ -268,6 +282,13 @@ export class WidgetContext {
}));
}
hideToast(target?: string) {
this.store.dispatch(new ActionNotificationHide(
{
target,
}));
}
detectChanges(updateWidgetParams: boolean = false) {
if (!this.destroyed) {
if (updateWidgetParams) {

3
ui-ngx/src/app/shared/components/snack-bar-component.html

@ -16,8 +16,11 @@
-->
<div fxLayout="row" fxLayoutAlign="start center" class="tb-toast"
[@showHideAnimation]="{value: animationState, params: animationParams}"
(@showHideAnimation.done)="onHideFinished($event)"
[ngClass]="{
'error-toast': notification.type === 'error',
'warn-toast': notification.type === 'warn',
'success-toast': notification.type === 'success',
'info-toast': notification.type === 'info'
}">

3
ui-ngx/src/app/shared/components/snack-bar-component.scss

@ -33,6 +33,9 @@
&.info-toast {
background: #323232;
}
&.warn-toast {
background: #dc6d1b;
}
&.error-toast {
background: #800000;
}

300
ui-ngx/src/app/shared/components/toast.directive.ts

@ -15,27 +15,27 @@
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
AfterViewInit, ChangeDetectorRef,
Component, ComponentRef,
Directive,
ElementRef,
Inject,
Input,
NgZone,
OnDestroy,
OnDestroy, Optional,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarConfig, MatSnackBarRef } from '@angular/material/snack-bar';
import { NotificationMessage } from '@app/core/notification/notification.models';
import { onParentScrollOrWindowResize } from '@app/core/utils';
import { Subscription } from 'rxjs';
import { NotificationService } from '@app/core/services/notification.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { MatButton } from '@angular/material/button';
import Timeout = NodeJS.Timeout;
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
@Directive({
selector: '[tb-toast]'
@ -48,17 +48,21 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
private notificationSubscription: Subscription = null;
private hideNotificationSubscription: Subscription = null;
private snackBarRef: MatSnackBarRef<TbSnackBarComponent> = null;
private snackBarRef: MatSnackBarRef<any> = null;
private overlayRef: OverlayRef;
private toastComponentRef: ComponentRef<TbSnackBarComponent>;
private currentMessage: NotificationMessage = null;
private dismissTimeout: Timeout = null;
constructor(public elementRef: ElementRef,
public viewContainerRef: ViewContainerRef,
constructor(private elementRef: ElementRef,
private viewContainerRef: ViewContainerRef,
private notificationService: NotificationService,
public snackBar: MatSnackBar,
private overlay: Overlay,
private snackBar: MatSnackBar,
private ngZone: NgZone,
private breakpointObserver: BreakpointObserver) {
private breakpointObserver: BreakpointObserver,
private cd: ChangeDetectorRef) {
}
ngAfterViewInit(): void {
@ -66,45 +70,12 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
(notificationMessage) => {
if (this.shouldDisplayMessage(notificationMessage)) {
this.currentMessage = notificationMessage;
const data = {
parent: this.elementRef,
notification: notificationMessage
};
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
const config: MatSnackBarConfig = {
horizontalPosition: notificationMessage.horizontalPosition || 'left',
verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'),
viewContainerRef: this.viewContainerRef,
duration: notificationMessage.duration,
panelClass: notificationMessage.panelClass,
data
};
this.ngZone.run(() => {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config);
if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.dismissTimeout = setTimeout(() => {
if (this.snackBarRef) {
this.snackBarRef.instance.actionButton._elementRef.nativeElement.click();
}
this.dismissTimeout = null;
}, notificationMessage.duration);
}
this.snackBarRef.afterDismissed().subscribe(() => {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.snackBarRef = null;
this.currentMessage = null;
});
});
if (isGtSm) {
this.showToastPanel(notificationMessage);
} else {
this.showSnackBar(notificationMessage);
}
}
}
);
@ -118,6 +89,9 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
if (this.toastComponentRef) {
this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click();
}
});
}
}
@ -125,6 +99,127 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
);
}
private showToastPanel(notificationMessage: NotificationMessage) {
this.ngZone.run(() => {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
const position = this.overlay.position();
let panelClass = ['tb-toast-panel'];
if (notificationMessage.panelClass) {
if (typeof notificationMessage.panelClass === 'string') {
panelClass.push(notificationMessage.panelClass);
} else if (notificationMessage.panelClass.length) {
panelClass = panelClass.concat(notificationMessage.panelClass);
}
}
const overlayConfig = new OverlayConfig({
panelClass,
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: false,
disposeOnNavigation: true
});
let originX;
let originY;
const horizontalPosition = notificationMessage.horizontalPosition || 'left';
const verticalPosition = notificationMessage.verticalPosition || 'top';
if (horizontalPosition === 'start' || horizontalPosition === 'left') {
originX = 'start';
} else if (horizontalPosition === 'end' || horizontalPosition === 'right') {
originX = 'end';
} else {
originX = 'center';
}
if (verticalPosition === 'top') {
originY = 'top';
} else {
originY = 'bottom';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX: originX,
overlayY: originY
};
overlayConfig.positionStrategy = position.flexibleConnectedTo(this.elementRef)
.withPositions([connectedPosition]);
this.overlayRef = this.overlay.create(overlayConfig);
const data: ToastPanelData = {
notification: notificationMessage
};
const injectionTokens = new WeakMap<any, any>([
[MAT_SNACK_BAR_DATA, data],
[OverlayRef, this.overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
this.toastComponentRef = this.overlayRef.attach(new ComponentPortal(TbSnackBarComponent, this.viewContainerRef, injector));
this.cd.detectChanges();
if (notificationMessage.duration && notificationMessage.duration > 0) {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.dismissTimeout = setTimeout(() => {
if (this.toastComponentRef) {
this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click();
}
this.dismissTimeout = null;
}, notificationMessage.duration + 500);
}
this.toastComponentRef.onDestroy(() => {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.overlayRef = null;
this.toastComponentRef = null;
this.currentMessage = null;
});
});
}
private showSnackBar(notificationMessage: NotificationMessage) {
const data: ToastPanelData = {
notification: notificationMessage
};
const config: MatSnackBarConfig = {
horizontalPosition: notificationMessage.horizontalPosition || 'left',
verticalPosition: 'bottom',
viewContainerRef: this.viewContainerRef,
duration: notificationMessage.duration,
panelClass: notificationMessage.panelClass,
data
};
this.ngZone.run(() => {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config);
});
if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.dismissTimeout = setTimeout(() => {
if (this.snackBarRef) {
this.snackBarRef.instance.actionButton._elementRef.nativeElement.click();
}
this.dismissTimeout = null;
}, notificationMessage.duration);
}
this.snackBarRef.afterDismissed().subscribe(() => {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.snackBarRef = null;
this.currentMessage = null;
});
}
private shouldDisplayMessage(notificationMessage: NotificationMessage): boolean {
if (notificationMessage && notificationMessage.message) {
const target = notificationMessage.target || 'root';
@ -139,6 +234,9 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
}
ngOnDestroy(): void {
if (this.overlayRef) {
this.overlayRef.dispose();
}
if (this.notificationSubscription) {
this.notificationSubscription.unsubscribe();
}
@ -148,72 +246,84 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
}
}
interface ToastPanelData {
notification: NotificationMessage;
}
import {
AnimationTriggerMetadata,
AnimationEvent,
trigger,
state,
transition,
style,
animate,
} from '@angular/animations';
export const toastAnimations: {
readonly showHideToast: AnimationTriggerMetadata;
} = {
showHideToast: trigger('showHideAnimation', [
state('in', style({ transform: 'scale(1)', opacity: 1 })),
transition('void => opened', [style({ transform: 'scale(0)', opacity: 0 }), animate('{{ open }}ms')]),
transition(
'opened => closing',
animate('{{ close }}ms', style({ transform: 'scale(0)', opacity: 0 })),
),
]),
};
export type ToastAnimationState = 'default' | 'opened' | 'closing';
@Component({
selector: 'tb-snack-bar-component',
templateUrl: 'snack-bar-component.html',
styleUrls: ['snack-bar-component.scss']
styleUrls: ['snack-bar-component.scss'],
animations: [toastAnimations.showHideToast]
})
export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
@ViewChild('actionButton', {static: true}) actionButton: MatButton;
private parentEl: HTMLElement;
public snackBarContainerEl: HTMLElement;
private parentScrollSubscription: Subscription = null;
public notification: NotificationMessage;
constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any, private elementRef: ElementRef,
public cd: ChangeDetectorRef,
public snackBarRef: MatSnackBarRef<TbSnackBarComponent>) {
animationState: ToastAnimationState;
animationParams = {
open: 100,
close: 100
};
constructor(@Inject(MAT_SNACK_BAR_DATA)
private data: ToastPanelData,
@Optional()
private snackBarRef: MatSnackBarRef<TbSnackBarComponent>,
@Optional()
private overlayRef: OverlayRef) {
this.animationState = !!this.snackBarRef ? 'default' : 'opened';
this.notification = data.notification;
}
ngAfterViewInit() {
this.parentEl = this.data.parent.nativeElement;
this.snackBarContainerEl = this.elementRef.nativeElement.parentNode;
this.snackBarContainerEl.style.position = 'absolute';
this.updateContainerRect();
this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig);
const snackBarComponent = this;
this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => {
snackBarComponent.updateContainerRect();
});
}
updatePosition(config: MatSnackBarConfig) {
const isRtl = config.direction === 'rtl';
const isLeft = (config.horizontalPosition === 'left' ||
(config.horizontalPosition === 'start' && !isRtl) ||
(config.horizontalPosition === 'end' && isRtl));
const isRight = !isLeft && config.horizontalPosition !== 'center';
if (isLeft) {
this.snackBarContainerEl.style.justifyContent = 'flex-start';
} else if (isRight) {
this.snackBarContainerEl.style.justifyContent = 'flex-end';
} else {
this.snackBarContainerEl.style.justifyContent = 'center';
}
if (config.verticalPosition === 'top') {
this.snackBarContainerEl.style.alignItems = 'flex-start';
} else {
this.snackBarContainerEl.style.alignItems = 'flex-end';
}
}
ngOnDestroy() {
if (this.parentScrollSubscription) {
this.parentScrollSubscription.unsubscribe();
}
}
updateContainerRect() {
const viewportOffset = this.parentEl.getBoundingClientRect();
this.snackBarContainerEl.style.top = viewportOffset.top + 'px';
this.snackBarContainerEl.style.left = viewportOffset.left + 'px';
this.snackBarContainerEl.style.width = viewportOffset.width + 'px';
this.snackBarContainerEl.style.height = viewportOffset.height + 'px';
action(): void {
if (this.snackBarRef) {
this.snackBarRef.dismissWithAction();
} else {
this.animationState = 'closing';
}
}
action(): void {
this.snackBarRef.dismissWithAction();
onHideFinished(event: AnimationEvent) {
const { toState } = event;
const isFadeOut = (toState as ToastAnimationState) === 'closing';
const itFinished = this.animationState === 'closing';
if (isFadeOut && itFinished) {
this.overlayRef.dispose();
}
}
}

10
ui-ngx/src/app/shared/models/page/page-link.ts

@ -70,18 +70,18 @@ export function sortItems(item1: any, item2: any, property: string, asc: boolean
const item2Value = getDescendantProp(item2, property);
let result = 0;
if (item1Value !== item2Value) {
if (typeof item1Value === 'number' && typeof item2Value === 'number') {
const item1Type = typeof item1Value;
const item2Type = typeof item2Value;
if (item1Type === 'number' && item2Type === 'number') {
result = item1Value - item2Value;
} else if (typeof item1Value === 'string' && typeof item2Value === 'string') {
} else if (item1Type === 'string' && item2Type === 'string') {
result = item1Value.localeCompare(item2Value);
} else if (typeof item1Value === 'boolean' && typeof item2Value === 'boolean') {
} else if ((item1Type === 'boolean' && item2Type === 'boolean') || (item1Type !== item2Type)) {
if (item1Value && !item2Value) {
result = 1;
} else if (!item1Value && item2Value) {
result = -1;
}
} else if (typeof item1Value !== typeof item2Value) {
result = 1;
}
}
return asc ? result : result * -1;

1
ui-ngx/src/app/shared/models/widget.models.ts

@ -152,6 +152,7 @@ export interface WidgetTypeParameters {
stateData?: boolean;
hasDataPageLink?: boolean;
singleEntity?: boolean;
warnOnPageDataOverflow?: boolean;
}
export interface WidgetControllerDescriptor {

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1712,7 +1712,8 @@
"add": "Add Widget",
"undo": "Undo widget changes",
"export": "Export widget",
"no-data": "No data to display on widget"
"no-data": "No data to display on widget",
"data-overflow": "Widget displays {{count}} out of {{total}} entities"
},
"widget-action": {
"header-button": "Widget header button",

4
ui-ngx/src/styles.scss

@ -1056,4 +1056,8 @@ mat-label {
line-height: 1.5;
white-space: pre-line;
}
.tb-toast-panel {
pointer-events: none !important;
}
}

Loading…
Cancel
Save