67 changed files with 4197 additions and 189 deletions
@ -0,0 +1,53 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-custom-action-pretty mat-elevation-z1" tb-fullscreen [fullscreen]="fullscreen"> |
|||
<div fxLayout="row" fxLayoutAlign="end center" class="tb-action-expand-button" [ngClass]="{'tb-fullscreen-editor': fullscreen}"> |
|||
<button mat-button fxHide.xs fxHide.sm |
|||
matTooltip="{{ 'widget.toggle-fullscreen' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="fullscreen = !fullscreen"> |
|||
<mat-icon>{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
|||
<span *ngIf="fullscreen" translate>widget.toggle-fullscreen</span> |
|||
</button> |
|||
</div> |
|||
<div class="tb-custom-action-editor" [ngClass]="{'tb-fullscreen-editor': fullscreen}"> |
|||
<div *ngIf="!fullscreen; else fullscreenEditor"> |
|||
<tb-custom-action-pretty-resources-tabs [hasCustomFunction]="true" |
|||
[action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> |
|||
</tb-custom-action-pretty-resources-tabs> |
|||
</div> |
|||
<ng-template #fullscreenEditor> |
|||
<div class="tb-fullscreen-panel tb-layout-fill" fxLayout="row"> |
|||
<div #leftPanel class="tb-split tb-content"> |
|||
<tb-custom-action-pretty-resources-tabs [hasCustomFunction]="false" |
|||
[action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> |
|||
</tb-custom-action-pretty-resources-tabs> |
|||
</div> |
|||
<div #rightPanel class="tb-split tb-content right-panel"> |
|||
<tb-js-func |
|||
[(ngModel)]="action.customFunction" |
|||
(ngModelChange)="onActionUpdated()" |
|||
[fillHeight]="true" |
|||
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']" |
|||
[validationArgs]="[]"> |
|||
</tb-js-func> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,107 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
.tb-custom-action-pretty { |
|||
box-sizing: border-box; |
|||
position: relative; |
|||
padding: 8px; |
|||
background-color: #fff; |
|||
|
|||
.tb-fullscreen-panel { |
|||
.tb-custom-action-editor-container { |
|||
height: calc(100% - 40px); |
|||
} |
|||
|
|||
.right-panel { |
|||
padding-top: 8px; |
|||
padding-left: 3px; |
|||
} |
|||
|
|||
tb-js-func .tb-js-func-panel { |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
mat-tab-group { |
|||
.mat-tab-body-wrapper { |
|||
height: 100%; |
|||
mat-tab-body { |
|||
height: 100%; |
|||
& > div { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-split { |
|||
box-sizing: border-box; |
|||
overflow-x: hidden; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.tb-content { |
|||
border: 1px solid #c0c0c0; |
|||
} |
|||
|
|||
.gutter { |
|||
background-color: #eee; |
|||
background-repeat: no-repeat; |
|||
background-position: 50%; |
|||
} |
|||
|
|||
.gutter.gutter-horizontal { |
|||
cursor: col-resize; |
|||
background-image: url("../../../../../../assets/split.js/grips/vertical.png"); |
|||
} |
|||
|
|||
.tb-split.tb-split-horizontal, |
|||
.gutter.gutter-horizontal { |
|||
float: left; |
|||
height: 100%; |
|||
} |
|||
|
|||
.tb-action-expand-button { |
|||
position: absolute; |
|||
right: 14px; |
|||
z-index: 2; |
|||
|
|||
&.tb-fullscreen-editor { |
|||
position: relative; |
|||
right: 0; |
|||
.mat-button { |
|||
.mat-icon { |
|||
margin-right: 5px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.mat-button { |
|||
min-width: 36px; |
|||
padding: 0; |
|||
.mat-icon { |
|||
margin-right: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-custom-action-editor { |
|||
&.tb-fullscreen-editor { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,138 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild, ViewEncapsulation, ViewChildren, QueryList, ComponentFactoryResolver |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
|||
import { MatPaginator } from '@angular/material/paginator'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { combineLatest, fromEvent, merge } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; |
|||
import { |
|||
WidgetActionDescriptorInfo, |
|||
WidgetActionsData, |
|||
WidgetActionsDatasource, |
|||
WidgetActionCallbacks, toWidgetActionDescriptor |
|||
} from '@home/components/widget/action/manage-widget-actions.component.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; |
|||
import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; |
|||
import { CustomActionDescriptor, WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; |
|||
import { |
|||
WidgetActionDialogComponent, |
|||
WidgetActionDialogData |
|||
} from '@home/components/widget/action/widget-action-dialog.component'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
|||
import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; |
|||
import { MatTab, MatTabGroup } from '@angular/material/tabs'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-custom-action-pretty-editor', |
|||
templateUrl: './custom-action-pretty-editor.component.html', |
|||
styleUrls: ['./custom-action-pretty-editor.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => CustomActionPrettyEditorComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
action: CustomActionDescriptor; |
|||
|
|||
fullscreen = false; |
|||
|
|||
@ViewChildren('leftPanel') |
|||
leftPanelElmRef: QueryList<ElementRef<HTMLElement>>; |
|||
|
|||
@ViewChildren('rightPanel') |
|||
rightPanelElmRef: QueryList<ElementRef<HTMLElement>>; |
|||
|
|||
private propagateChange = (_: any) => {}; |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
combineLatest(this.leftPanelElmRef.changes, this.rightPanelElmRef.changes).subscribe(() => { |
|||
if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) { |
|||
this.initSplitLayout(this.leftPanelElmRef.first.nativeElement, |
|||
this.rightPanelElmRef.first.nativeElement); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private initSplitLayout(leftPanel: any, rightPanel: any) { |
|||
Split([leftPanel, rightPanel], { |
|||
sizes: [50, 50], |
|||
gutterSize: 8, |
|||
cursor: 'col-resize' |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(obj: CustomActionDescriptor): void { |
|||
this.action = obj; |
|||
} |
|||
|
|||
public onActionUpdated(valid: boolean = true) { |
|||
if (!valid) { |
|||
this.propagateChange(null); |
|||
} else { |
|||
this.propagateChange(this.action); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-tab-group selectedIndex="{{hasCustomFunction ? 3 : 2}}" dynamicHeight="true" style="width: 100%; height: 100%;"> |
|||
<mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;"> |
|||
<div class="tb-custom-action-editor-container" style="background-color: #fff;"> |
|||
<div class="mat-padding"> |
|||
<div fxFlex fxLayout="row" style="max-height: 40px;" |
|||
fxLayoutAlign="start center" |
|||
*ngFor="let resource of action.customResources; let i = index" > |
|||
<mat-form-field fxFlex class="mat-block resource-field" floatLabel="never" |
|||
style="margin: 10px 0px 0px 0px; max-height: 40px;"> |
|||
<input required matInput [(ngModel)]="resource.url" |
|||
(ngModelChange)="notifyActionUpdated()" |
|||
placeholder="{{ 'widget.resource-url' | translate }}"/> |
|||
</mat-form-field> |
|||
<button mat-button mat-icon-button color="primary" |
|||
[disabled]="isLoading$ | async" |
|||
type="button" |
|||
(click)="removeResource(i)" |
|||
matTooltip="{{'widget.remove-resource' | translate}}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div style="margin-top: 6px;"> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[disabled]="isLoading$ | async" |
|||
type="button" |
|||
(click)="addResource()" |
|||
matTooltip="{{'widget.add-resource' | translate}}" |
|||
matTooltipPosition="above"> |
|||
<span translate>action.add</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;"> |
|||
<div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="cssFullscreen"> |
|||
<div class="tb-editor-area-title-panel"> |
|||
<button mat-button |
|||
type="button" |
|||
(click)="beautifyCss()"> |
|||
{{ 'widget.tidy' | translate }} |
|||
</button> |
|||
<button mat-button |
|||
type="button" |
|||
mat-icon-button class="tb-mat-32" |
|||
(click)="cssFullscreen = !cssFullscreen" |
|||
matTooltip="{{(cssFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>{{ cssFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div #cssInput class="css-panel"></div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;"> |
|||
<div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="htmlFullscreen"> |
|||
<div class="tb-editor-area-title-panel"> |
|||
<button mat-button |
|||
type="button" (click)="beautifyHtml()"> |
|||
{{ 'widget.tidy' | translate }} |
|||
</button> |
|||
<button mat-button |
|||
type="button" |
|||
mat-icon-button class="tb-mat-32" |
|||
(click)="htmlFullscreen = !htmlFullscreen" |
|||
matTooltip="{{(htmlFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>{{ htmlFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div #htmlInput class="html-panel"></div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab *ngIf="hasCustomFunction" label="{{ 'widget.js' | translate }}" style="width: 100%; height: 100%;"> |
|||
<tb-js-func |
|||
[(ngModel)]="action.customFunction" |
|||
(ngModelChange)="notifyActionUpdated()" |
|||
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']" |
|||
[validationArgs]="[]"> |
|||
</tb-js-func> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
@ -0,0 +1,79 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
.tb-custom-action-editor-container { |
|||
|
|||
mat-form-field.resource-field { |
|||
max-height: 40px; |
|||
margin: 10px 0px 0px 0px; |
|||
.mat-form-field-wrapper { |
|||
padding-bottom: 0; |
|||
.mat-form-field-flex { |
|||
max-height: 40px; |
|||
.mat-form-field-infix { |
|||
border: 0; |
|||
} |
|||
} |
|||
.mat-form-field-underline { |
|||
bottom: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.html-panel, |
|||
.css-panel { |
|||
width: 100%; |
|||
min-width: 200px; |
|||
height: 100%; |
|||
min-height: 200px; |
|||
} |
|||
|
|||
div.tb-editor-area-title-panel { |
|||
position: absolute; |
|||
top: 5px; |
|||
right: 20px; |
|||
z-index: 5; |
|||
font-size: .8rem; |
|||
font-weight: 500; |
|||
|
|||
label { |
|||
padding: 4px; |
|||
color: #00acc1; |
|||
text-transform: uppercase; |
|||
background: rgba(220, 220, 220, .35); |
|||
border-radius: 5px; |
|||
&:not(:last-child) { |
|||
margin-right: 4px; |
|||
} |
|||
} |
|||
|
|||
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { |
|||
align-items: center; |
|||
vertical-align: middle; |
|||
min-width: 32px; |
|||
min-height: 15px; |
|||
padding: 4px; |
|||
margin: 0; |
|||
font-size: .8rem; |
|||
line-height: 15px; |
|||
color: #7b7b7b; |
|||
background: rgba(220, 220, 220, .35); |
|||
&:not(:last-child) { |
|||
margin-right: 4px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,211 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
Component, |
|||
ElementRef, |
|||
EventEmitter, |
|||
Input, |
|||
OnChanges, |
|||
OnDestroy, |
|||
OnInit, |
|||
Output, QueryList, |
|||
SimpleChanges, |
|||
ViewChild, ViewChildren, ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { CustomActionDescriptor } from '@shared/models/widget.models'; |
|||
import * as ace from 'ace-builds'; |
|||
import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; |
|||
import { css_beautify, html_beautify } from 'js-beautify'; |
|||
import { MatTab } from '@angular/material/tabs'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-custom-action-pretty-resources-tabs', |
|||
templateUrl: './custom-action-pretty-resources-tabs.component.html', |
|||
styleUrls: ['./custom-action-pretty-resources-tabs.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class CustomActionPrettyResourcesTabsComponent extends PageComponent implements OnInit, OnChanges, OnDestroy { |
|||
|
|||
@Input() |
|||
action: CustomActionDescriptor; |
|||
|
|||
@Input() |
|||
hasCustomFunction: boolean; |
|||
|
|||
@Output() |
|||
actionUpdated: EventEmitter<CustomActionDescriptor> = new EventEmitter<CustomActionDescriptor>(); |
|||
|
|||
@ViewChild('htmlInput', {static: true}) |
|||
htmlInputElmRef: ElementRef; |
|||
|
|||
@ViewChild('cssInput', {static: true}) |
|||
cssInputElmRef: ElementRef; |
|||
|
|||
htmlFullscreen = false; |
|||
cssFullscreen = false; |
|||
|
|||
aceEditors: ace.Ace.Editor[] = []; |
|||
editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {}; |
|||
aceResizeListeners: { element: any, resizeListener: any }[] = []; |
|||
htmlEditor: ace.Ace.Editor; |
|||
cssEditor: ace.Ace.Editor; |
|||
setValuesPending = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private translate: TranslateService, |
|||
private raf: RafService) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.initAceEditors(); |
|||
if (this.setValuesPending) { |
|||
this.setAceEditorValues(); |
|||
this.setValuesPending = false; |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.aceResizeListeners.forEach((resizeListener) => { |
|||
// @ts-ignore
|
|||
removeResizeListener(resizeListener.element, resizeListener.resizeListener); |
|||
}); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (propName === 'action') { |
|||
if (this.aceEditors.length) { |
|||
this.setAceEditorValues(); |
|||
} else { |
|||
this.setValuesPending = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public notifyActionUpdated() { |
|||
this.actionUpdated.emit(this.validate() ? this.action : null); |
|||
} |
|||
|
|||
private validate(): boolean { |
|||
for (const resource of this.action.customResources) { |
|||
if (!resource.url) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
public addResource() { |
|||
this.action.customResources.push({url: ''}); |
|||
this.notifyActionUpdated(); |
|||
} |
|||
|
|||
public removeResource(index: number) { |
|||
if (index > -1) { |
|||
if (this.action.customResources.splice(index, 1).length > 0) { |
|||
this.notifyActionUpdated(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public beautifyCss(): void { |
|||
const res = css_beautify(this.action.customCss, {indent_size: 4}); |
|||
if (this.action.customCss !== res) { |
|||
this.action.customCss = res; |
|||
this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); |
|||
this.notifyActionUpdated(); |
|||
} |
|||
} |
|||
|
|||
public beautifyHtml(): void { |
|||
const res = html_beautify(this.action.customHtml, {indent_size: 4, wrap_line_length: 60}); |
|||
if (this.action.customHtml !== res) { |
|||
this.action.customHtml = res; |
|||
this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); |
|||
this.notifyActionUpdated(); |
|||
} |
|||
} |
|||
|
|||
private initAceEditors() { |
|||
this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html'); |
|||
this.htmlEditor.on('input', () => { |
|||
const editorValue = this.htmlEditor.getValue(); |
|||
if (this.action.customHtml !== editorValue) { |
|||
this.action.customHtml = editorValue; |
|||
this.notifyActionUpdated(); |
|||
} |
|||
}); |
|||
this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css'); |
|||
this.cssEditor.on('input', () => { |
|||
const editorValue = this.cssEditor.getValue(); |
|||
if (this.action.customCss !== editorValue) { |
|||
this.action.customCss = editorValue; |
|||
this.notifyActionUpdated(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor { |
|||
const editorElement = editorElementRef.nativeElement; |
|||
let editorOptions: Partial<ace.Ace.EditorOptions> = { |
|||
mode: `ace/mode/${mode}`, |
|||
showGutter: true, |
|||
showPrintMargin: true |
|||
}; |
|||
const advancedOptions = { |
|||
enableSnippets: true, |
|||
enableBasicAutocompletion: true, |
|||
enableLiveAutocompletion: true |
|||
}; |
|||
editorOptions = {...editorOptions, ...advancedOptions}; |
|||
const aceEditor = ace.edit(editorElement, editorOptions); |
|||
aceEditor.session.setUseWrapMode(true); |
|||
this.aceEditors.push(aceEditor); |
|||
|
|||
const resizeListener = this.onAceEditorResize.bind(this, aceEditor); |
|||
|
|||
// @ts-ignore
|
|||
addResizeListener(editorElement, resizeListener); |
|||
this.aceResizeListeners.push({element: editorElement, resizeListener}); |
|||
return aceEditor; |
|||
} |
|||
|
|||
private setAceEditorValues() { |
|||
this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); |
|||
this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); |
|||
} |
|||
|
|||
private onAceEditorResize(aceEditor: ace.Ace.Editor) { |
|||
if (this.editorsResizeCafs[aceEditor.id]) { |
|||
this.editorsResizeCafs[aceEditor.id](); |
|||
delete this.editorsResizeCafs[aceEditor.id]; |
|||
} |
|||
this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => { |
|||
aceEditor.resize(); |
|||
aceEditor.renderer.updateFull(); |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
/*=======================================================================*/ |
|||
/*========== There are two examples: for edit and add entity ==========*/ |
|||
/*=======================================================================*/ |
|||
/*======================== Edit entity example ========================*/ |
|||
/*=======================================================================*/ |
|||
/* |
|||
.edit-entity-form md-input-container { |
|||
padding-right: 10px; |
|||
} |
|||
|
|||
.edit-entity-form .boolean-value-input { |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.edit-entity-form .boolean-value-input .checkbox-label { |
|||
margin-bottom: 8px; |
|||
color: rgba(0,0,0,0.54); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.relations-list .header { |
|||
padding-right: 5px; |
|||
padding-bottom: 5px; |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.relations-list .header .cell { |
|||
padding-right: 5px; |
|||
padding-left: 5px; |
|||
font-size: 12px; |
|||
font-weight: 700; |
|||
color: rgba(0, 0, 0, .54); |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.relations-list .body { |
|||
padding-right: 5px; |
|||
padding-bottom: 15px; |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.relations-list .body .row { |
|||
padding-top: 5px; |
|||
} |
|||
|
|||
.relations-list .body .cell { |
|||
padding-right: 5px; |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.relations-list .body md-autocomplete-wrap md-input-container { |
|||
height: 30px; |
|||
} |
|||
|
|||
.relations-list .body .md-button { |
|||
margin: 0; |
|||
} |
|||
|
|||
.relations-list.old-relations tb-entity-select tb-entity-autocomplete button { |
|||
display: none; |
|||
} |
|||
*/ |
|||
/*========================================================================*/ |
|||
/*========================= Add entity example =========================*/ |
|||
/*========================================================================*/ |
|||
/* |
|||
.add-entity-form md-input-container { |
|||
padding-right: 10px; |
|||
} |
|||
|
|||
.add-entity-form .boolean-value-input { |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.add-entity-form .boolean-value-input .checkbox-label { |
|||
margin-bottom: 8px; |
|||
color: rgba(0,0,0,0.54); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.relations-list .header { |
|||
padding-right: 5px; |
|||
padding-bottom: 5px; |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.relations-list .header .cell { |
|||
padding-right: 5px; |
|||
padding-left: 5px; |
|||
font-size: 12px; |
|||
font-weight: 700; |
|||
color: rgba(0, 0, 0, .54); |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.relations-list .body { |
|||
padding-right: 5px; |
|||
padding-bottom: 15px; |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.relations-list .body .row { |
|||
padding-top: 5px; |
|||
} |
|||
|
|||
.relations-list .body .cell { |
|||
padding-right: 5px; |
|||
padding-left: 5px; |
|||
} |
|||
|
|||
.relations-list .body md-autocomplete-wrap md-input-container { |
|||
height: 30px; |
|||
} |
|||
|
|||
.relations-list .body .md-button { |
|||
margin: 0; |
|||
} |
|||
*/ |
|||
@ -0,0 +1,304 @@ |
|||
<!--=======================================================================--> |
|||
<!--===== There are two example templates: for edit and add entity =====--> |
|||
<!--=======================================================================--> |
|||
<!--======================== Edit entity example ========================--> |
|||
<!--=======================================================================--> |
|||
<!-- --> |
|||
<!--<md-dialog aria-label="Edit entity">--> |
|||
<!-- <form name="editEntityForm" class="edit-entity-form" ng-submit="vm.save()">--> |
|||
<!-- <md-toolbar>--> |
|||
<!-- <div class="md-toolbar-tools">--> |
|||
<!-- <h2>Edit {{vm.entityType.toLowerCase()}} {{vm.entityName}}</h2>--> |
|||
<!-- <span flex></span>--> |
|||
<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> |
|||
<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> |
|||
<!-- </md-button>--> |
|||
<!-- </div>--> |
|||
<!-- </md-toolbar>--> |
|||
<!-- <md-dialog-content>--> |
|||
<!-- <div class="md-dialog-content">--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Entity Name</label>--> |
|||
<!-- <input ng-model="vm.entityName" readonly>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Entity Type</label>--> |
|||
<!-- <input ng-model="vm.entityType" readonly>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Type</label>--> |
|||
<!-- <input ng-model="vm.type" readonly>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Latitude</label>--> |
|||
<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Longitude</label>--> |
|||
<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Address</label>--> |
|||
<!-- <input ng-model="vm.attributes.address">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Owner</label>--> |
|||
<!-- <input ng-model="vm.attributes.owner">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Integer Value</label>--> |
|||
<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> |
|||
<!-- <div ng-messages="editEntityForm.integerNumber.$error">--> |
|||
<!-- <div ng-message="pattern">Invalid integer value.</div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> |
|||
<!-- <label class="checkbox-label">Boolean Value</label>--> |
|||
<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> |
|||
<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> |
|||
<!-- </md-checkbox>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div class="relations-list old-relations">--> |
|||
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> |
|||
<!-- <div class="body" ng-show="vm.relations.length">--> |
|||
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> |
|||
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> |
|||
<!-- <div flex layout="column">--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> |
|||
<!-- <label>Direction</label>--> |
|||
<!-- <md-select ng-disabled="true" required ng-model="relation.direction">--> |
|||
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> |
|||
<!-- {{ ("relation.search-direction." + direction) | translate}}--> |
|||
<!-- </md-option>--> |
|||
<!-- </md-select>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <tb-relation-type-autocomplete ng-disabled="true" flex class="md-block"--> |
|||
<!-- the-form="editEntityForm"--> |
|||
<!-- ng-model="relation.relationType"--> |
|||
<!-- tb-required="true">--> |
|||
<!-- </tb-relation-type-autocomplete>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <tb-entity-select flex class="md-block"--> |
|||
<!-- the-form="editEntityForm"--> |
|||
<!-- ng-disabled="true"--> |
|||
<!-- tb-required="true"--> |
|||
<!-- ng-model="relation.relatedEntity">--> |
|||
<!-- </tb-entity-select>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="column" layout-align="center center">--> |
|||
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> |
|||
<!-- ng-click="vm.removeOldRelation($index,relation)" aria-label="Remove">--> |
|||
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> |
|||
<!-- <md-icon aria-label="Remove" class="material-icons">--> |
|||
<!-- close--> |
|||
<!-- </md-icon>--> |
|||
<!-- </md-button>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div class="relations-list">--> |
|||
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>--> |
|||
<!-- <div class="body" ng-show="vm.newRelations.length">--> |
|||
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.newRelations track by $index">--> |
|||
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> |
|||
<!-- <div flex layout="column">--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> |
|||
<!-- <label>Direction</label>--> |
|||
<!-- <md-select name="direction" required ng-model="relation.direction">--> |
|||
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> |
|||
<!-- {{ ("relation.search-direction." + direction) | translate}}--> |
|||
<!-- </md-option>--> |
|||
<!-- </md-select>--> |
|||
<!-- <div ng-messages="editEntityForm.direction.$error">--> |
|||
<!-- <div ng-message="required">Relation direction is required.</div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <tb-relation-type-autocomplete flex class="md-block"--> |
|||
<!-- the-form="editEntityForm"--> |
|||
<!-- ng-model="relation.relationType"--> |
|||
<!-- tb-required="true">--> |
|||
<!-- </tb-relation-type-autocomplete>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <tb-entity-select flex class="md-block"--> |
|||
<!-- the-form="editEntityForm"--> |
|||
<!-- tb-required="true"--> |
|||
<!-- ng-model="relation.relatedEntity">--> |
|||
<!-- </tb-entity-select>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="column" layout-align="center center">--> |
|||
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> |
|||
<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> |
|||
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> |
|||
<!-- <md-icon aria-label="Remove" class="material-icons">--> |
|||
<!-- close--> |
|||
<!-- </md-icon>--> |
|||
<!-- </md-button>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div>--> |
|||
<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> |
|||
<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> |
|||
<!-- Add--> |
|||
<!-- </md-button>--> |
|||
<!-- </div> --> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-dialog-content>--> |
|||
<!-- <md-dialog-actions>--> |
|||
<!-- <md-button type="submit" ng-disabled="editEntityForm.$invalid || !editEntityForm.$dirty" class="md-raised md-primary">Save</md-button>--> |
|||
<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> |
|||
<!-- </md-dialog-actions>--> |
|||
<!-- </form>--> |
|||
<!--</md-dialog>--> |
|||
<!----> |
|||
<!--========================================================================--> |
|||
<!--========================= Add entity example =========================--> |
|||
<!--========================================================================--> |
|||
<!----> |
|||
<!--<md-dialog aria-label="Add entity">--> |
|||
<!-- <form name="addEntityForm" class="add-entity-form" ng-submit="vm.save()">--> |
|||
<!-- <md-toolbar>--> |
|||
<!-- <div class="md-toolbar-tools">--> |
|||
<!-- <h2>Add entity</h2>--> |
|||
<!-- <span flex></span>--> |
|||
<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> |
|||
<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> |
|||
<!-- </md-button>--> |
|||
<!-- </div>--> |
|||
<!-- </md-toolbar>--> |
|||
<!-- <md-dialog-content>--> |
|||
<!-- <div class="md-dialog-content">--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Entity Name</label>--> |
|||
<!-- <input ng-model="vm.entityName" name=entityName required>--> |
|||
<!-- <div ng-messages="addEntityForm.entityName.$error">--> |
|||
<!-- <div ng-message="required">Entity name is required.</div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <tb-entity-type-select class="md-block" style="min-width: 100px; width: 100px;"--> |
|||
<!-- the-form="addEntityForm"--> |
|||
<!-- tb-required="true"--> |
|||
<!-- allowed-entity-types="vm.allowedEntityTypes"--> |
|||
<!-- ng-model="vm.entityType">--> |
|||
<!-- </tb-entity-type-select>--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Entity Subtype</label>--> |
|||
<!-- <input ng-model="vm.type" name=type required>--> |
|||
<!-- <div ng-messages="addEntityForm.type.$error">--> |
|||
<!-- <div ng-message="required">Entity subtype is required.</div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Latitude</label>--> |
|||
<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Longitude</label>--> |
|||
<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Address</label>--> |
|||
<!-- <input ng-model="vm.attributes.address">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Owner</label>--> |
|||
<!-- <input ng-model="vm.attributes.owner">--> |
|||
<!-- </md-input-container>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container flex class="md-block">--> |
|||
<!-- <label>Integer Value</label>--> |
|||
<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> |
|||
<!-- <div ng-messages="addEntityForm.integerNumber.$error">--> |
|||
<!-- <div ng-message="pattern">Invalid integer value.</div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> |
|||
<!-- <label class="checkbox-label">Boolean Value</label>--> |
|||
<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> |
|||
<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> |
|||
<!-- </md-checkbox>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div class="relations-list">--> |
|||
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> |
|||
<!-- <div class="body" ng-show="vm.relations.length">--> |
|||
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> |
|||
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px;">--> |
|||
<!-- <div flex layout="column">--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> |
|||
<!-- <label>Direction</label>--> |
|||
<!-- <md-select name="direction" required ng-model="relation.direction">--> |
|||
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> |
|||
<!-- {{ ("relation.search-direction." + direction) | translate}}--> |
|||
<!-- </md-option>--> |
|||
<!-- </md-select>--> |
|||
<!-- <div ng-messages="addEntityForm.direction.$error">--> |
|||
<!-- <div ng-message="required">Relation direction is required.</div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-input-container>--> |
|||
<!-- <tb-relation-type-autocomplete flex class="md-block"--> |
|||
<!-- the-form="addEntityForm"--> |
|||
<!-- ng-model="relation.relationType"--> |
|||
<!-- tb-required="true">--> |
|||
<!-- </tb-relation-type-autocomplete>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="row">--> |
|||
<!-- <tb-entity-select flex class="md-block"--> |
|||
<!-- the-form="addEntityForm"--> |
|||
<!-- tb-required="true"--> |
|||
<!-- ng-model="relation.relatedEntity">--> |
|||
<!-- </tb-entity-select>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div layout="column" layout-align="center center">--> |
|||
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> |
|||
<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> |
|||
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> |
|||
<!-- <md-icon aria-label="Remove" class="material-icons">--> |
|||
<!-- close--> |
|||
<!-- </md-icon>--> |
|||
<!-- </md-button>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- <div>--> |
|||
<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> |
|||
<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> |
|||
<!-- Add--> |
|||
<!-- </md-button>--> |
|||
<!-- </div> --> |
|||
<!-- </div>--> |
|||
<!-- </div>--> |
|||
<!-- </md-dialog-content>--> |
|||
<!-- <md-dialog-actions>--> |
|||
<!-- <md-button type="submit" ng-disabled="addEntityForm.$invalid || !addEntityForm.$dirty" class="md-raised md-primary">Create</md-button>--> |
|||
<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> |
|||
<!-- </md-dialog-actions>--> |
|||
<!-- </form>--> |
|||
<!--</md-dialog>--> |
|||
@ -0,0 +1,397 @@ |
|||
/*=======================================================================*/ |
|||
/*===== There are three examples: for delete, edit and add entity =====*/ |
|||
/*=======================================================================*/ |
|||
/*======================= Delete entity example =======================*/ |
|||
/*=======================================================================*/ |
|||
// |
|||
//var $injector = widgetContext.$scope.$injector; |
|||
//var $mdDialog = $injector.get('$mdDialog'), |
|||
// $document = $injector.get('$document'), |
|||
// types = $injector.get('types'), |
|||
// assetService = $injector.get('assetService'), |
|||
// deviceService = $injector.get('deviceService') |
|||
// $rootScope = $injector.get('$rootScope'), |
|||
// $q = $injector.get('$q'); |
|||
// |
|||
//openDeleteEntityDialog(); |
|||
// |
|||
//function openDeleteEntityDialog() { |
|||
// var title = 'Delete ' + entityId.entityType |
|||
// .toLowerCase() + ' ' + |
|||
// entityName; |
|||
// var content = 'Are you sure you want to delete the ' + |
|||
// entityId.entityType.toLowerCase() + ' ' + |
|||
// entityName + '?'; |
|||
// var confirm = $mdDialog.confirm() |
|||
// .targetEvent($event) |
|||
// .title(title) |
|||
// .htmlContent(content) |
|||
// .ariaLabel(title) |
|||
// .cancel('Cancel') |
|||
// .ok('Delete'); |
|||
// $mdDialog.show(confirm).then(function() { |
|||
// deleteEntity(); |
|||
// }) |
|||
//} |
|||
// |
|||
//function deleteEntity() { |
|||
// deleteEntityPromise(entityId).then( |
|||
// function success() { |
|||
// updateAliasData(); |
|||
// }, |
|||
// function fail() { |
|||
// showErrorDialog(); |
|||
// } |
|||
// ); |
|||
//} |
|||
// |
|||
//function deleteEntityPromise(entityId) { |
|||
// if (entityId.entityType == types.entityType.asset) { |
|||
// return assetService.deleteAsset(entityId.id); |
|||
// } else if (entityId.entityType == types.entityType.device) { |
|||
// return deviceService.deleteDevice(entityId.id); |
|||
// } |
|||
//} |
|||
// |
|||
//function updateAliasData() { |
|||
// var aliasIds = []; |
|||
// for (var id in widgetContext.aliasController.resolvedAliases) { |
|||
// aliasIds.push(id); |
|||
// } |
|||
// var tasks = []; |
|||
// aliasIds.forEach(function(aliasId) { |
|||
// widgetContext.aliasController.setAliasUnresolved(aliasId); |
|||
// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); |
|||
// }); |
|||
// $q.all(tasks).then(function() { |
|||
// $rootScope.$broadcast('entityAliasesChanged', aliasIds); |
|||
// }); |
|||
//} |
|||
// |
|||
//function showErrorDialog() { |
|||
// var title = 'Error'; |
|||
// var content = 'An error occurred while deleting the entity. Please try again.'; |
|||
// var alert = $mdDialog.alert() |
|||
// .title(title) |
|||
// .htmlContent(content) |
|||
// .ariaLabel(title) |
|||
// .parent(angular.element($document[0].body)) |
|||
// .targetEvent($event) |
|||
// .multiple(true) |
|||
// .clickOutsideToClose(true) |
|||
// .ok('CLOSE'); |
|||
// $mdDialog.show(alert); |
|||
//} |
|||
// |
|||
/*=======================================================================*/ |
|||
/*======================== Edit entity example ========================*/ |
|||
/*=======================================================================*/ |
|||
// |
|||
//var $injector = widgetContext.$scope.$injector; |
|||
//var $mdDialog = $injector.get('$mdDialog'), |
|||
// $document = $injector.get('$document'), |
|||
// $q = $injector.get('$q'), |
|||
// types = $injector.get('types'), |
|||
// $rootScope = $injector.get('$rootScope'), |
|||
// entityService = $injector.get('entityService'), |
|||
// attributeService = $injector.get('attributeService'), |
|||
// entityRelationService = $injector.get('entityRelationService'); |
|||
// |
|||
//openEditEntityDialog(); |
|||
// |
|||
//function openEditEntityDialog() { |
|||
// $mdDialog.show({ |
|||
// controller: ['$scope','$mdDialog', EditEntityDialogController], |
|||
// controllerAs: 'vm', |
|||
// template: htmlTemplate, |
|||
// locals: { |
|||
// entityId: entityId |
|||
// }, |
|||
// parent: angular.element($document[0].body), |
|||
// targetEvent: $event, |
|||
// multiple: true, |
|||
// clickOutsideToClose: false |
|||
// }); |
|||
//} |
|||
// |
|||
//function EditEntityDialogController($scope,$mdDialog) { |
|||
// var vm = this; |
|||
// vm.entityId = entityId; |
|||
// vm.entityName = entityName; |
|||
// vm.entityType = entityId.entityType; |
|||
// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; |
|||
// vm.allowedRelatedEntityTypes = []; |
|||
// vm.entitySearchDirection = types.entitySearchDirection; |
|||
// vm.attributes = {}; |
|||
// vm.serverAttributes = {}; |
|||
// vm.relations = []; |
|||
// vm.newRelations = []; |
|||
// vm.relationsToDelete = []; |
|||
// getEntityInfo(); |
|||
// |
|||
// vm.addRelation = function() { |
|||
// var relation = { |
|||
// direction: null, |
|||
// relationType: null, |
|||
// relatedEntity: null |
|||
// }; |
|||
// vm.newRelations.push(relation); |
|||
// $scope.editEntityForm.$setDirty(); |
|||
// }; |
|||
// vm.removeRelation = function(index) { |
|||
// if (index > -1) { |
|||
// vm.newRelations.splice(index, 1); |
|||
// $scope.editEntityForm.$setDirty(); |
|||
// } |
|||
// }; |
|||
// vm.removeOldRelation = function(index, relation) { |
|||
// if (index > -1) { |
|||
// vm.relations.splice(index, 1); |
|||
// vm.relationsToDelete.push(relation); |
|||
// $scope.editEntityForm.$setDirty(); |
|||
// } |
|||
// }; |
|||
// vm.save = function() { |
|||
// saveAttributes(); |
|||
// saveRelations(); |
|||
// $scope.editEntityForm.$setPristine(); |
|||
// }; |
|||
// vm.cancel = function() { |
|||
// $mdDialog.hide(); |
|||
// }; |
|||
// |
|||
// function getEntityAttributes(attributes) { |
|||
// for (var i = 0; i < attributes.length; i++) { |
|||
// vm.attributes[attributes[i].key] = attributes[i].value; |
|||
// } |
|||
// vm.serverAttributes = angular.copy(vm.attributes); |
|||
// } |
|||
// |
|||
// function getEntityRelations(relations) { |
|||
// var relationsFrom = relations[0]; |
|||
// var relationsTo = relations[1]; |
|||
// for (var i=0; i < relationsFrom.length; i++) { |
|||
// var relation = { |
|||
// direction: types.entitySearchDirection.from, |
|||
// relationType: relationsFrom[i].type, |
|||
// relatedEntity: relationsFrom[i].to |
|||
// }; |
|||
// vm.relations.push(relation); |
|||
// } |
|||
// for (var i=0; i < relationsTo.length; i++) { |
|||
// var relation = { |
|||
// direction: types.entitySearchDirection.to, |
|||
// relationType: relationsTo[i].type, |
|||
// relatedEntity: relationsTo[i].from |
|||
// }; |
|||
// vm.relations.push(relation); |
|||
// } |
|||
// } |
|||
// |
|||
// function getEntityInfo() { |
|||
// entityService.getEntity(entityId.entityType, entityId.id).then( |
|||
// function(entity) { |
|||
// vm.entity = entity; |
|||
// vm.type = vm.entity.type; |
|||
// }); |
|||
// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then( |
|||
// function(data){ |
|||
// if (data.length) { |
|||
// getEntityAttributes(data); |
|||
// } |
|||
// }); |
|||
// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then( |
|||
// function(relations){ |
|||
// getEntityRelations(relations); |
|||
// }); |
|||
// } |
|||
// |
|||
// function saveAttributes() { |
|||
// var attributesArray = []; |
|||
// for (var key in vm.attributes) { |
|||
// if (vm.attributes[key] !== vm.serverAttributes[key]) { |
|||
// attributesArray.push({key: key, value: vm.attributes[key]}); |
|||
// } |
|||
// } |
|||
// if (attributesArray.length > 0) { |
|||
// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); |
|||
// } |
|||
// } |
|||
// |
|||
// function saveRelations() { |
|||
// var tasks = []; |
|||
// for (var i=0; i < vm.newRelations.length; i++) { |
|||
// var relation = { |
|||
// type: vm.newRelations[i].relationType |
|||
// }; |
|||
// if (vm.newRelations[i].direction == types.entitySearchDirection.from) { |
|||
// relation.to = vm.newRelations[i].relatedEntity; |
|||
// relation.from = entityId; |
|||
// } else { |
|||
// relation.to = entityId; |
|||
// relation.from = vm.newRelations[i].relatedEntity; |
|||
// } |
|||
// tasks.push(entityRelationService.saveRelation(relation)); |
|||
// } |
|||
// for (var i=0; i < vm.relationsToDelete.length; i++) { |
|||
// var relation = { |
|||
// type: vm.relationsToDelete[i].relationType |
|||
// }; |
|||
// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) { |
|||
// relation.to = vm.relationsToDelete[i].relatedEntity; |
|||
// relation.from = entityId; |
|||
// } else { |
|||
// relation.to = entityId; |
|||
// relation.from = vm.relationsToDelete[i].relatedEntity; |
|||
// } |
|||
// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType)); |
|||
// } |
|||
// $q.all(tasks).then(function(){ |
|||
// vm.relations = vm.relations.concat(vm.newRelations); |
|||
// vm.newRelations = []; |
|||
// vm.relationsToDelete = []; |
|||
// updateAliasData(); |
|||
// }); |
|||
// } |
|||
// |
|||
// function updateAliasData() { |
|||
// var aliasIds = []; |
|||
// for (var id in widgetContext.aliasController.resolvedAliases) { |
|||
// aliasIds.push(id); |
|||
// } |
|||
// var tasks = []; |
|||
// aliasIds.forEach(function(aliasId) { |
|||
// widgetContext.aliasController.setAliasUnresolved(aliasId); |
|||
// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); |
|||
// }); |
|||
// $q.all(tasks).then(function() { |
|||
// $rootScope.$broadcast('entityAliasesChanged', aliasIds); |
|||
// }); |
|||
// } |
|||
//} |
|||
// |
|||
/*========================================================================*/ |
|||
/*========================= Add entity example =========================*/ |
|||
/*========================================================================*/ |
|||
// |
|||
//var $injector = widgetContext.$scope.$injector; |
|||
//var $mdDialog = $injector.get('$mdDialog'), |
|||
// $document = $injector.get('$document'), |
|||
// $q = $injector.get('$q'), |
|||
// $rootScope = $injector.get('$rootScope'), |
|||
// types = $injector.get('types'), |
|||
// assetService = $injector.get('assetService'), |
|||
// deviceService = $injector.get('deviceService'), |
|||
// attributeService = $injector.get('attributeService'), |
|||
// entityRelationService = $injector.get('entityRelationService'); |
|||
// |
|||
//openAddEntityDialog(); |
|||
// |
|||
//function openAddEntityDialog() { |
|||
// $mdDialog.show({ |
|||
// controller: ['$scope','$mdDialog', AddEntityDialogController], |
|||
// controllerAs: 'vm', |
|||
// template: htmlTemplate, |
|||
// locals: { |
|||
// entityId: entityId |
|||
// }, |
|||
// parent: angular.element($document[0].body), |
|||
// targetEvent: $event, |
|||
// multiple: true, |
|||
// clickOutsideToClose: false |
|||
// }); |
|||
//} |
|||
// |
|||
//function AddEntityDialogController($scope, $mdDialog) { |
|||
// var vm = this; |
|||
// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; |
|||
// vm.allowedRelatedEntityTypes = []; |
|||
// vm.entitySearchDirection = types.entitySearchDirection; |
|||
// vm.attributes = {}; |
|||
// vm.relations = []; |
|||
// |
|||
// vm.addRelation = function() { |
|||
// var relation = { |
|||
// direction: null, |
|||
// relationType: null, |
|||
// relatedEntity: null |
|||
// }; |
|||
// vm.relations.push(relation); |
|||
// }; |
|||
// vm.removeRelation = function(index) { |
|||
// if (index > -1) { |
|||
// vm.relations.splice(index, 1); |
|||
// } |
|||
// }; |
|||
// vm.save = function() { |
|||
// $scope.addEntityForm.$setPristine(); |
|||
// saveEntityPromise().then( |
|||
// function (entity) { |
|||
// saveAttributes(entity.id); |
|||
// saveRelations(entity.id); |
|||
// $mdDialog.hide(); |
|||
// } |
|||
// ); |
|||
// }; |
|||
// vm.cancel = function() { |
|||
// $mdDialog.hide(); |
|||
// }; |
|||
// |
|||
// |
|||
// function saveEntityPromise() { |
|||
// var entity = { |
|||
// name: vm.entityName, |
|||
// type: vm.type |
|||
// }; |
|||
// if (vm.entityType == types.entityType.asset) { |
|||
// return assetService.saveAsset(entity); |
|||
// } else if (vm.entityType == types.entityType.device) { |
|||
// return deviceService.saveDevice(entity); |
|||
// } |
|||
// } |
|||
// |
|||
// function saveAttributes(entityId) { |
|||
// var attributesArray = []; |
|||
// for (var key in vm.attributes) { |
|||
// attributesArray.push({key: key, value: vm.attributes[key]}); |
|||
// } |
|||
// if (attributesArray.length > 0) { |
|||
// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); |
|||
// } |
|||
// } |
|||
// |
|||
// function saveRelations(entityId) { |
|||
// var tasks = []; |
|||
// for (var i=0; i < vm.relations.length; i++) { |
|||
// var relation = { |
|||
// type: vm.relations[i].relationType |
|||
// }; |
|||
// if (vm.relations[i].direction == types.entitySearchDirection.from) { |
|||
// relation.to = vm.relations[i].relatedEntity; |
|||
// relation.from = entityId; |
|||
// } else { |
|||
// relation.to = entityId; |
|||
// relation.from = vm.relations[i].relatedEntity; |
|||
// } |
|||
// tasks.push(entityRelationService.saveRelation(relation)); |
|||
// } |
|||
// $q.all(tasks).then(function(){ |
|||
// updateAliasData(); |
|||
// }); |
|||
// } |
|||
// |
|||
// function updateAliasData() { |
|||
// var aliasIds = []; |
|||
// for (var id in widgetContext.aliasController.resolvedAliases) { |
|||
// aliasIds.push(id); |
|||
// } |
|||
// var tasks = []; |
|||
// aliasIds.forEach(function(aliasId) { |
|||
// widgetContext.aliasController.setAliasUnresolved(aliasId); |
|||
// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); |
|||
// }); |
|||
// $q.all(tasks).then(function() { |
|||
// $rootScope.$broadcast('entityAliasesChanged', aliasIds); |
|||
// }); |
|||
// } |
|||
//} |
|||
@ -0,0 +1,118 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="mat-padding tb-entity-table tb-absolute-fill"> |
|||
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> |
|||
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode"> |
|||
<div class="mat-toolbar-tools"> |
|||
<span class="tb-entity-table-title" translate>widget-config.actions</span> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
|||
(click)="addAction($event)" |
|||
matTooltip="{{ 'widget-config.add-action' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>add</mat-icon> |
|||
</button> |
|||
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" |
|||
matTooltip="{{ 'action.search' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> |
|||
<div class="mat-toolbar-tools"> |
|||
<button mat-button mat-icon-button |
|||
matTooltip="{{ 'widget-config.search-actions' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
<mat-form-field fxFlex> |
|||
<mat-label> </mat-label> |
|||
<input #searchInput matInput |
|||
[(ngModel)]="pageLink.textSearch" |
|||
placeholder="{{ 'widget-config.search-actions' | translate }}"/> |
|||
</mat-form-field> |
|||
<button mat-button mat-icon-button (click)="exitFilterMode()" |
|||
matTooltip="{{ 'action.close' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<div fxFlex class="table-container"> |
|||
<mat-table [dataSource]="dataSource" |
|||
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
|||
<ng-container matColumnDef="actionSourceName"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-source' | translate }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let action"> |
|||
{{ action.actionSourceName }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="name"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-name' | translate }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let action"> |
|||
{{ action.name }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="icon"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-icon' | translate }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let action"> |
|||
<mat-icon>{{ action.icon }}</mat-icon> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="typeName"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-type' | translate }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let action"> |
|||
{{ action.typeName }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="end"> |
|||
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
|||
matTooltip="{{ 'widget-config.edit-action' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="editAction($event, action)"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
|||
matTooltip="{{ 'widget-config.delete-action' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="deleteAction($event, action)"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
|||
<mat-row [ngClass]="{'mat-row-select': true}" |
|||
*matRowDef="let action; columns: displayedColumns;"></mat-row> |
|||
</mat-table> |
|||
<span [fxShow]="dataSource.isEmpty() | async" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found" translate>{{ 'widget-config.no-actions-text' }}</span> |
|||
</div> |
|||
<mat-divider></mat-divider> |
|||
<mat-paginator [length]="dataSource.total() | async" |
|||
[pageIndex]="pageLink.page" |
|||
[pageSize]="pageLink.pageSize" |
|||
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,167 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { WidgetActionDescriptor, WidgetActionSource, |
|||
widgetActionTypeTranslationMap, CustomActionDescriptor } from '@app/shared/models/widget.models'; |
|||
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; |
|||
import { EntityRelationInfo, EntitySearchDirection } from '@shared/models/relation.models'; |
|||
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; |
|||
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
|||
import { SelectionModel } from '@angular/cdk/collections'; |
|||
import { EntityRelationService } from '@core/http/entity-relation.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { catchError, map, publishReplay, refCount, share, take, tap } from 'rxjs/operators'; |
|||
import { entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { deepClone, isUndefined } from '@core/utils'; |
|||
|
|||
import * as customSampleJs from '!raw-loader!./custom-sample-js.raw'; |
|||
import * as customSampleCss from '!raw-loader!./custom-sample-css.raw'; |
|||
import * as customSampleHtml from '!raw-loader!./custom-sample-html.raw'; |
|||
|
|||
export interface WidgetActionCallbacks { |
|||
fetchDashboardStates: (query: string) => Array<string>; |
|||
} |
|||
|
|||
export interface WidgetActionsData { |
|||
actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; |
|||
actionSources: {[actionSourceId: string]: WidgetActionSource}; |
|||
} |
|||
|
|||
export interface WidgetActionDescriptorInfo extends WidgetActionDescriptor { |
|||
actionSourceId?: string; |
|||
actionSourceName?: string; |
|||
typeName?: string; |
|||
} |
|||
|
|||
export function toWidgetActionDescriptor(action: WidgetActionDescriptorInfo): WidgetActionDescriptor { |
|||
const copy = deepClone(action); |
|||
delete copy.actionSourceId; |
|||
delete copy.actionSourceName; |
|||
delete copy.typeName; |
|||
return copy; |
|||
} |
|||
|
|||
export function toCustomAction(action: WidgetActionDescriptorInfo): CustomActionDescriptor { |
|||
let result: CustomActionDescriptor; |
|||
if (!action || (isUndefined(action.customFunction) && isUndefined(action.customHtml) && isUndefined(action.customCss))) { |
|||
result = { |
|||
customHtml: customSampleHtml, |
|||
customCss: customSampleCss, |
|||
customFunction: customSampleJs |
|||
}; |
|||
} else { |
|||
result = { |
|||
customHtml: action.customHtml, |
|||
customCss: action.customCss, |
|||
customFunction: action.customFunction |
|||
}; |
|||
} |
|||
result.customResources = action ? deepClone(action.customResources) : []; |
|||
return result; |
|||
} |
|||
|
|||
export class WidgetActionsDatasource implements DataSource<WidgetActionDescriptorInfo> { |
|||
|
|||
private actionsSubject = new BehaviorSubject<WidgetActionDescriptorInfo[]>([]); |
|||
private pageDataSubject = new BehaviorSubject<PageData<WidgetActionDescriptorInfo>>(emptyPageData<WidgetActionDescriptorInfo>()); |
|||
|
|||
public pageData$ = this.pageDataSubject.asObservable(); |
|||
|
|||
private allActions: Observable<Array<WidgetActionDescriptorInfo>>; |
|||
|
|||
private actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; |
|||
private actionSources: {[actionSourceId: string]: WidgetActionSource}; |
|||
|
|||
constructor(private translate: TranslateService, |
|||
private utils: UtilsService) {} |
|||
|
|||
connect(collectionViewer: CollectionViewer): Observable<WidgetActionDescriptorInfo[] | ReadonlyArray<WidgetActionDescriptorInfo>> { |
|||
return this.actionsSubject.asObservable(); |
|||
} |
|||
|
|||
disconnect(collectionViewer: CollectionViewer): void { |
|||
this.actionsSubject.complete(); |
|||
this.pageDataSubject.complete(); |
|||
} |
|||
|
|||
setActions(actionsData: WidgetActionsData) { |
|||
this.actionsMap = actionsData.actionsMap; |
|||
this.actionSources = actionsData.actionSources; |
|||
} |
|||
|
|||
loadActions(pageLink: PageLink, reload: boolean = false): Observable<PageData<WidgetActionDescriptorInfo>> { |
|||
if (reload) { |
|||
this.allActions = null; |
|||
} |
|||
const result = new ReplaySubject<PageData<WidgetActionDescriptorInfo>>(); |
|||
this.fetchActions(pageLink).pipe( |
|||
catchError(() => of(emptyPageData<WidgetActionDescriptorInfo>())), |
|||
).subscribe( |
|||
(pageData) => { |
|||
this.actionsSubject.next(pageData.data); |
|||
this.pageDataSubject.next(pageData); |
|||
result.next(pageData); |
|||
} |
|||
); |
|||
return result; |
|||
} |
|||
|
|||
fetchActions(pageLink: PageLink): Observable<PageData<WidgetActionDescriptorInfo>> { |
|||
return this.getAllActions().pipe( |
|||
map((data) => pageLink.filterData(data)) |
|||
); |
|||
} |
|||
|
|||
getAllActions(): Observable<Array<WidgetActionDescriptorInfo>> { |
|||
if (!this.allActions) { |
|||
const actions: WidgetActionDescriptorInfo[] = []; |
|||
for (const actionSourceId of Object.keys(this.actionsMap)) { |
|||
const descriptors = this.actionsMap[actionSourceId]; |
|||
descriptors.forEach((descriptor) => { |
|||
actions.push(this.toWidgetActionDescriptorInfo(actionSourceId, descriptor)); |
|||
}); |
|||
} |
|||
this.allActions = of(actions).pipe( |
|||
publishReplay(1), |
|||
refCount() |
|||
); |
|||
} |
|||
return this.allActions; |
|||
} |
|||
|
|||
private toWidgetActionDescriptorInfo(actionSourceId: string, action: WidgetActionDescriptor): WidgetActionDescriptorInfo { |
|||
const actionSource = this.actionSources[actionSourceId]; |
|||
const actionSourceName = actionSource ? this.utils.customTranslation(actionSource.name, actionSource.name) : actionSourceId; |
|||
const typeName = this.translate.instant(widgetActionTypeTranslationMap.get(action.type)); |
|||
return { actionSourceId, actionSourceName, typeName, ...action}; |
|||
} |
|||
|
|||
isEmpty(): Observable<boolean> { |
|||
return this.actionsSubject.pipe( |
|||
map((actions) => !actions.length) |
|||
); |
|||
} |
|||
|
|||
total(): Observable<number> { |
|||
return this.pageDataSubject.pipe( |
|||
map((pageData) => pageData.totalElements) |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
.tb-entity-table { |
|||
.tb-entity-table-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: #fff; |
|||
|
|||
.tb-entity-table-title { |
|||
padding-right: 20px; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.table-container { |
|||
overflow: auto; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-sort-header-sorted .mat-sort-header-arrow { |
|||
opacity: 1 !important; |
|||
} |
|||
} |
|||
@ -0,0 +1,316 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
|||
import { MatPaginator } from '@angular/material/paginator'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { fromEvent, merge } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; |
|||
import { |
|||
WidgetActionDescriptorInfo, |
|||
WidgetActionsData, |
|||
WidgetActionsDatasource, |
|||
WidgetActionCallbacks, toWidgetActionDescriptor |
|||
} from '@home/components/widget/action/manage-widget-actions.component.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; |
|||
import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; |
|||
import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; |
|||
import { |
|||
WidgetActionDialogComponent, |
|||
WidgetActionDialogData |
|||
} from '@home/components/widget/action/widget-action-dialog.component'; |
|||
import { deepClone } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-manage-widget-actions', |
|||
templateUrl: './manage-widget-actions.component.html', |
|||
styleUrls: ['./manage-widget-actions.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ManageWidgetActionsComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class ManageWidgetActionsComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() callbacks: WidgetActionCallbacks; |
|||
|
|||
innerValue: WidgetActionsData; |
|||
|
|||
displayedColumns: string[]; |
|||
pageLink: PageLink; |
|||
textSearchMode = false; |
|||
dataSource: WidgetActionsDatasource; |
|||
|
|||
viewsInited = false; |
|||
dirtyValue = false; |
|||
|
|||
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef; |
|||
|
|||
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; |
|||
@ViewChild(MatSort, {static: false}) sort: MatSort; |
|||
|
|||
private propagateChange = (_: any) => {}; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private translate: TranslateService, |
|||
private utils: UtilsService, |
|||
private dialog: MatDialog, |
|||
private dialogs: DialogService) { |
|||
super(store); |
|||
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; |
|||
this.pageLink = new PageLink(10, 0, null, sortOrder); |
|||
this.dataSource = new WidgetActionsDatasource(this.translate, this.utils); |
|||
this.displayedColumns = ['actionSourceName', 'name', 'icon', 'typeName', 'actions']; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
|
|||
fromEvent(this.searchInputField.nativeElement, 'keyup') |
|||
.pipe( |
|||
debounceTime(150), |
|||
distinctUntilChanged(), |
|||
tap(() => { |
|||
this.paginator.pageIndex = 0; |
|||
this.updateData(); |
|||
}) |
|||
) |
|||
.subscribe(); |
|||
|
|||
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); |
|||
|
|||
merge(this.sort.sortChange, this.paginator.page) |
|||
.pipe( |
|||
tap(() => this.updateData()) |
|||
) |
|||
.subscribe(); |
|||
|
|||
this.viewsInited = true; |
|||
if (this.dirtyValue) { |
|||
this.dirtyValue = false; |
|||
this.updateData(true); |
|||
} |
|||
|
|||
} |
|||
|
|||
updateData(reload: boolean = false) { |
|||
this.pageLink.page = this.paginator.pageIndex; |
|||
this.pageLink.pageSize = this.paginator.pageSize; |
|||
this.pageLink.sortOrder.property = this.sort.active; |
|||
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
|||
this.dataSource.loadActions(this.pageLink, reload); |
|||
} |
|||
|
|||
addAction($event: Event) { |
|||
this.openWidgetActionDialog($event); |
|||
} |
|||
|
|||
editAction($event: Event, action: WidgetActionDescriptorInfo) { |
|||
this.openWidgetActionDialog($event, action); |
|||
} |
|||
|
|||
openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const isAdd = action === null; |
|||
let prevActionSourceId = null; |
|||
if (!isAdd) { |
|||
prevActionSourceId = action.actionSourceId; |
|||
} |
|||
const availableActionSources: {[actionSourceId: string]: WidgetActionSource} = {}; |
|||
for (const id of Object.keys(this.innerValue.actionSources)) { |
|||
const actionSource = this.innerValue.actionSources[id]; |
|||
if (actionSource.multiple) { |
|||
availableActionSources[id] = actionSource; |
|||
} else { |
|||
if (!isAdd && action.actionSourceId === id) { |
|||
availableActionSources[id] = actionSource; |
|||
} else { |
|||
const existing = this.innerValue.actionsMap[id]; |
|||
if (!existing || !existing.length) { |
|||
availableActionSources[id] = actionSource; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
const actionsData: WidgetActionsData = { |
|||
actionsMap: this.innerValue.actionsMap, |
|||
actionSources: availableActionSources |
|||
}; |
|||
|
|||
this.dialog.open<WidgetActionDialogComponent, WidgetActionDialogData, |
|||
WidgetActionDescriptorInfo>(WidgetActionDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
isAdd, |
|||
callbacks: this.callbacks, |
|||
actionsData, |
|||
action: deepClone(action) |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (res) { |
|||
this.saveAction(res, isAdd, prevActionSourceId); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
private saveAction(actionInfo: WidgetActionDescriptorInfo, isAdd: boolean, prevActionSourceId: string) { |
|||
const actionSourceId = actionInfo.actionSourceId; |
|||
const action = toWidgetActionDescriptor(actionInfo); |
|||
if (isAdd) { |
|||
const targetActions = this.getOrCreateTargetActions(actionSourceId); |
|||
targetActions.push(action); |
|||
} else { |
|||
if (actionSourceId !== prevActionSourceId) { |
|||
let targetActions = this.getOrCreateTargetActions(prevActionSourceId); |
|||
const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); |
|||
if (targetIndex > -1) { |
|||
targetActions.splice(targetIndex, 1); |
|||
} |
|||
targetActions = this.getOrCreateTargetActions(actionSourceId); |
|||
targetActions.push(action); |
|||
} else { |
|||
const targetActions = this.getOrCreateTargetActions(actionSourceId); |
|||
const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); |
|||
if (targetIndex > -1) { |
|||
targetActions[targetIndex] = action; |
|||
} |
|||
} |
|||
} |
|||
this.onActionsUpdated(); |
|||
} |
|||
|
|||
private getOrCreateTargetActions(actionSourceId: string): Array<WidgetActionDescriptor> { |
|||
const actionsMap = this.innerValue.actionsMap; |
|||
let targetActions = actionsMap[actionSourceId]; |
|||
if (!targetActions) { |
|||
targetActions = []; |
|||
actionsMap[actionSourceId] = targetActions; |
|||
} |
|||
return targetActions; |
|||
} |
|||
|
|||
deleteAction($event: Event, action: WidgetActionDescriptorInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const title = this.translate.instant('widget-config.delete-action-title'); |
|||
const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); |
|||
this.dialogs.confirm(title, content, |
|||
this.translate.instant('action.no'), |
|||
this.translate.instant('action.yes'), true).subscribe( |
|||
(res) => { |
|||
if (res) { |
|||
const targetActions = this.getOrCreateTargetActions(action.actionSourceId); |
|||
const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); |
|||
if (targetIndex > -1) { |
|||
targetActions.splice(targetIndex, 1); |
|||
this.onActionsUpdated(); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
enterFilterMode() { |
|||
this.textSearchMode = true; |
|||
this.pageLink.textSearch = ''; |
|||
setTimeout(() => { |
|||
this.searchInputField.nativeElement.focus(); |
|||
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
|||
}, 10); |
|||
} |
|||
|
|||
exitFilterMode() { |
|||
this.textSearchMode = false; |
|||
this.pageLink.textSearch = null; |
|||
this.paginator.pageIndex = 0; |
|||
this.updateData(); |
|||
} |
|||
|
|||
resetSortAndFilter(update: boolean = true) { |
|||
this.pageLink.textSearch = null; |
|||
this.paginator.pageIndex = 0; |
|||
const sortable = this.sort.sortables.get('actionSourceName'); |
|||
this.sort.active = sortable.id; |
|||
this.sort.direction = 'asc'; |
|||
if (update) { |
|||
this.updateData(true); |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(obj: WidgetActionsData): void { |
|||
this.innerValue = obj; |
|||
setTimeout(() => { |
|||
this.dataSource.setActions(this.innerValue); |
|||
if (this.viewsInited) { |
|||
this.resetSortAndFilter(true); |
|||
} else { |
|||
this.dirtyValue = true; |
|||
} |
|||
}, 0); |
|||
} |
|||
|
|||
private onActionsUpdated() { |
|||
this.updateData(true); |
|||
this.propagateChange(this.innerValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form #widgetActionForm="ngForm" [formGroup]="widgetActionFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2>{{ (isAdd ? 'widget-config.add-action' : 'widget-config.edit-action' ) | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>widget-config.action-source</mat-label> |
|||
<mat-select required matInput formControlName="actionSourceId"> |
|||
<mat-option *ngFor="let actionSourceItem of data.actionsData.actionSources | keyvalue" [value]="actionSourceItem.key"> |
|||
{{ actionSourceName(actionSourceItem.value) }} |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="widgetActionFormGroup.get('actionSourceId').hasError('required')"> |
|||
{{ 'widget-config.action-source-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block" style="padding-bottom: 20px;"> |
|||
<mat-label translate>widget-config.action-name</mat-label> |
|||
<input required matInput formControlName="name"> |
|||
<mat-error *ngIf="widgetActionFormGroup.get('name').hasError('required')"> |
|||
{{ 'widget-config.action-name-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="widgetActionFormGroup.get('name').hasError('actionNameNotUnique')" |
|||
[innerHTML]="'widget-config.action-name-not-unique' | translate"> |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<tb-material-icon-select |
|||
formControlName="icon"> |
|||
</tb-material-icon-select> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>widget-config.action-type</mat-label> |
|||
<mat-select required matInput formControlName="type"> |
|||
<mat-option *ngFor="let actionType of widgetActionTypes" [value]="actionType"> |
|||
{{ widgetActionTypeTranslations.get(actionType) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="widgetActionFormGroup.get('type').hasError('required')"> |
|||
{{ 'widget-config.action-type-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value"> |
|||
<ng-template [ngSwitchCase]="widgetActionType.openDashboard"> |
|||
<div fxLayout="column" style="padding-bottom: 20px;"> |
|||
<div class="mat-caption tb-required" |
|||
style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div> |
|||
<tb-dashboard-autocomplete |
|||
formControlName="targetDashboardId" |
|||
required |
|||
[selectFirstDashboard]="true" |
|||
></tb-dashboard-autocomplete> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<mat-form-field class="mat-block"> |
|||
<input matInput type="text" placeholder="{{ 'widget-action.target-dashboard-state' | translate }}" |
|||
#dashboardStateInput |
|||
formControlName="targetDashboardStateId" |
|||
[required]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState" |
|||
[matAutocomplete]="targetDashboardStateAutocomplete"> |
|||
<button *ngIf="actionTypeFormGroup.get('targetDashboardStateId').value" |
|||
type="button" |
|||
matSuffix mat-button mat-icon-button aria-label="Clear" |
|||
(click)="clearTargetDashboardState()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-autocomplete |
|||
class="tb-autocomplete" |
|||
#targetDashboardStateAutocomplete="matAutocomplete"> |
|||
<mat-option *ngFor="let state of filteredDashboardStates | async" [value]="state"> |
|||
<span [innerHTML]="state | highlight:targetDashboardStateSearchText"></span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="actionTypeFormGroup.get('targetDashboardStateId').hasError('required')"> |
|||
{{ 'widget-action.target-dashboard-state-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<mat-checkbox formControlName="openRightLayout"> |
|||
{{ 'widget-action.open-right-layout' | translate }} |
|||
</mat-checkbox> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<div fxFlex fxLayout="column"> |
|||
<mat-checkbox formControlName="setEntityId"> |
|||
{{ 'widget-action.set-entity-from-widget' | translate }} |
|||
</mat-checkbox> |
|||
<mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value" |
|||
floatLabel="always" |
|||
class="mat-block"> |
|||
<mat-label translate>alias.state-entity-parameter-name</mat-label> |
|||
<input matInput |
|||
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}" |
|||
formControlName="stateEntityParamName"> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionType.custom"> |
|||
<tb-js-func |
|||
formControlName="customFunction" |
|||
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams']" |
|||
[validationArgs]="[]" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionType.customPretty"> |
|||
<tb-custom-action-pretty-editor |
|||
formControlName="customAction"> |
|||
</tb-custom-action-pretty-editor> |
|||
</ng-template> |
|||
</section> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || widgetActionFormGroup.invalid || actionTypeFormGroup.invalid || (!widgetActionFormGroup.dirty && !actionTypeFormGroup.dirty)"> |
|||
{{ (isAdd ? 'action.add' : 'action.save') | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
style="margin-right: 20px;" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,295 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; |
|||
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NgForm, |
|||
ValidatorFn, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { |
|||
toCustomAction, |
|||
WidgetActionCallbacks, |
|||
WidgetActionDescriptorInfo, |
|||
WidgetActionsData |
|||
} from '@home/components/widget/action/manage-widget-actions.component.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; |
|||
import { map, mergeMap, startWith, tap } from 'rxjs/operators'; |
|||
import { DashboardService } from '@core/http/dashboard.service'; |
|||
import { Dashboard } from '@shared/models/dashboard.models'; |
|||
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
|||
|
|||
export interface WidgetActionDialogData { |
|||
isAdd: boolean; |
|||
callbacks: WidgetActionCallbacks; |
|||
actionsData: WidgetActionsData; |
|||
action?: WidgetActionDescriptorInfo; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-action-dialog', |
|||
templateUrl: './widget-action-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: WidgetActionDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDialogComponent, |
|||
WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher { |
|||
|
|||
@ViewChild('dashboardStateInput', {static: false}) dashboardStateInput: ElementRef; |
|||
|
|||
widgetActionFormGroup: FormGroup; |
|||
actionTypeFormGroup: FormGroup; |
|||
|
|||
isAdd: boolean; |
|||
action: WidgetActionDescriptorInfo; |
|||
|
|||
widgetActionTypes = Object.keys(WidgetActionType); |
|||
widgetActionTypeTranslations = widgetActionTypeTranslationMap; |
|||
widgetActionType = WidgetActionType; |
|||
|
|||
filteredDashboardStates: Observable<Array<string>>; |
|||
targetDashboardStateSearchText = ''; |
|||
selectedDashboardStateIds: Observable<Array<string>>; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
private utils: UtilsService, |
|||
private dashboardService: DashboardService, |
|||
private dashboardUtils: DashboardUtilsService, |
|||
@Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<WidgetActionDialogComponent, WidgetActionDescriptorInfo>, |
|||
public fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
this.isAdd = data.isAdd; |
|||
if (this.isAdd) { |
|||
this.action = { |
|||
id: this.utils.guid(), |
|||
name: '', |
|||
icon: 'more_horiz', |
|||
type: null |
|||
}; |
|||
} else { |
|||
this.action = this.data.action; |
|||
} |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.widgetActionFormGroup = this.fb.group({}); |
|||
this.widgetActionFormGroup.addControl('actionSourceId', |
|||
this.fb.control(this.action.actionSourceId, [Validators.required])); |
|||
this.widgetActionFormGroup.addControl('name', |
|||
this.fb.control(this.action.name, [this.validateActionName(), Validators.required])); |
|||
this.widgetActionFormGroup.addControl('icon', |
|||
this.fb.control(this.action.icon, [Validators.required])); |
|||
this.widgetActionFormGroup.addControl('type', |
|||
this.fb.control(this.action.type, [Validators.required])); |
|||
this.updateActionTypeFormGroup(this.action.type, this.action); |
|||
this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => { |
|||
this.updateActionTypeFormGroup(type); |
|||
}); |
|||
this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { |
|||
this.widgetActionFormGroup.get('name').updateValueAndValidity(); |
|||
}); |
|||
} |
|||
|
|||
private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { |
|||
this.actionTypeFormGroup = this.fb.group({}); |
|||
if (type) { |
|||
switch (type) { |
|||
case WidgetActionType.openDashboard: |
|||
case WidgetActionType.openDashboardState: |
|||
case WidgetActionType.updateDashboardState: |
|||
this.actionTypeFormGroup.addControl( |
|||
'targetDashboardStateId', |
|||
this.fb.control(action ? action.targetDashboardStateId : null, |
|||
type === WidgetActionType.openDashboardState ? [Validators.required] : []) |
|||
); |
|||
this.actionTypeFormGroup.addControl( |
|||
'setEntityId', |
|||
this.fb.control(action ? action.setEntityId : true, []) |
|||
); |
|||
this.actionTypeFormGroup.addControl( |
|||
'stateEntityParamName', |
|||
this.fb.control(action ? action.stateEntityParamName : null, []) |
|||
); |
|||
if (type === WidgetActionType.openDashboard) { |
|||
this.actionTypeFormGroup.addControl( |
|||
'targetDashboardId', |
|||
this.fb.control(action ? action.targetDashboardId : null, |
|||
[Validators.required]) |
|||
); |
|||
this.setupSelectedDashboardStateIds(action ? action.targetDashboardId : null); |
|||
} else { |
|||
this.actionTypeFormGroup.addControl( |
|||
'openRightLayout', |
|||
this.fb.control(action ? action.openRightLayout : false, []) |
|||
); |
|||
} |
|||
this.setupFilteredDashboardStates(); |
|||
break; |
|||
case WidgetActionType.custom: |
|||
this.actionTypeFormGroup.addControl( |
|||
'customFunction', |
|||
this.fb.control(action ? action.customFunction : null, []) |
|||
); |
|||
break; |
|||
case WidgetActionType.customPretty: |
|||
this.actionTypeFormGroup.addControl( |
|||
'customAction', |
|||
this.fb.control(toCustomAction(action), [Validators.required]) |
|||
); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private setupSelectedDashboardStateIds(targetDashboardId?: string) { |
|||
this.selectedDashboardStateIds = |
|||
this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe( |
|||
// startWith<string>(targetDashboardId),
|
|||
tap(() => { |
|||
this.targetDashboardStateSearchText = ''; |
|||
}), |
|||
mergeMap((dashboardId) => { |
|||
if (dashboardId) { |
|||
return this.dashboardService.getDashboard(dashboardId); |
|||
} else { |
|||
return of(null); |
|||
} |
|||
}), |
|||
map((dashboard: Dashboard) => { |
|||
if (dashboard) { |
|||
dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); |
|||
const states = dashboard.configuration.states; |
|||
return Object.keys(states); |
|||
} else { |
|||
return []; |
|||
} |
|||
}) |
|||
); |
|||
} |
|||
|
|||
private setupFilteredDashboardStates() { |
|||
this.targetDashboardStateSearchText = ''; |
|||
this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges |
|||
.pipe( |
|||
startWith(''), |
|||
map(value => value ? value : ''), |
|||
mergeMap(name => this.fetchDashboardStates(name) ) |
|||
); |
|||
} |
|||
|
|||
private fetchDashboardStates(searchText?: string): Observable<Array<string>> { |
|||
this.targetDashboardStateSearchText = searchText; |
|||
if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) { |
|||
return this.selectedDashboardStateIds.pipe( |
|||
map(stateIds => { |
|||
const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds; |
|||
if (result && result.length) { |
|||
return result; |
|||
} else { |
|||
return [searchText]; |
|||
} |
|||
}) |
|||
); |
|||
} else { |
|||
return of(this.data.callbacks.fetchDashboardStates(searchText)); |
|||
} |
|||
} |
|||
|
|||
private createFilterForDashboardState(query: string): (stateId: string) => boolean { |
|||
const lowercaseQuery = query.toLowerCase(); |
|||
return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; |
|||
} |
|||
|
|||
public clearTargetDashboardState(value: string = '') { |
|||
this.dashboardStateInput.nativeElement.value = value; |
|||
this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.dashboardStateInput.nativeElement.blur(); |
|||
this.dashboardStateInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
private validateActionName(): ValidatorFn { |
|||
return (c: FormControl) => { |
|||
const newName = c.value; |
|||
const valid = this.checkActionName(newName, this.widgetActionFormGroup.get('actionSourceId').value); |
|||
return !valid ? { |
|||
actionNameNotUnique: true |
|||
} : null; |
|||
}; |
|||
} |
|||
|
|||
private checkActionName(name: string, actionSourceId: string): boolean { |
|||
let actionNameIsUnique = true; |
|||
if (name && actionSourceId) { |
|||
const sourceActions = this.data.actionsData.actionsMap[actionSourceId]; |
|||
if (sourceActions) { |
|||
const result = sourceActions.filter((sourceAction) => sourceAction.name === name); |
|||
if (result && result.length && result[0].id !== this.action.id) { |
|||
actionNameIsUnique = false; |
|||
} |
|||
} |
|||
} |
|||
return actionNameIsUnique; |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
public actionSourceName(actionSource: WidgetActionSource): string { |
|||
if (actionSource) { |
|||
return this.utils.customTranslation(actionSource.name, actionSource.name); |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; |
|||
let result: WidgetActionDescriptorInfo; |
|||
if (type === WidgetActionType.customPretty) { |
|||
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value}; |
|||
} else { |
|||
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value}; |
|||
} |
|||
result.id = this.action.id; |
|||
this.dialogRef.close(result); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="legendConfigForm"> |
|||
<fieldset [disabled]="(isLoading$ | async)"> |
|||
<div class="mat-content" style="height: 100%;"> |
|||
<div class="mat-padding"> |
|||
<section fxLayout="column"> |
|||
<mat-form-field> |
|||
<mat-label translate>legend.direction</mat-label> |
|||
<mat-select matInput formControlName="direction" style="min-width: 150px;"> |
|||
<mat-option *ngFor="let direction of legendDirections" [value]="direction"> |
|||
{{ legendDirectionTranslations.get(direction) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field> |
|||
<mat-label translate>legend.position</mat-label> |
|||
<mat-select matInput formControlName="position" style="min-width: 150px;"> |
|||
<mat-option *ngFor="let pos of legendPositions" [value]="pos" |
|||
[disabled]="legendConfigForm.get('direction').value === legendDirection.row && |
|||
(pos === legendPosition.left || pos === legendPosition.right)"> |
|||
{{ legendPositionTranslations.get(pos) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-checkbox fxFlex formControlName="showMin"> |
|||
{{ 'legend.show-min' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="showMax"> |
|||
{{ 'legend.show-max' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="showAvg"> |
|||
{{ 'legend.show-avg' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="showTotal"> |
|||
{{ 'legend.show-total' | translate }} |
|||
</mat-checkbox> |
|||
</section> |
|||
</div> |
|||
</div> |
|||
</fieldset> |
|||
</form> |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
form, |
|||
fieldset { |
|||
height: 100%; |
|||
} |
|||
|
|||
.mat-content { |
|||
overflow: hidden; |
|||
background-color: #fff; |
|||
} |
|||
|
|||
.mat-padding { |
|||
padding: 16px; |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core'; |
|||
import { Overlay, OverlayRef } from '@angular/cdk/overlay'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { |
|||
LegendConfig, |
|||
LegendDirection, |
|||
legendDirectionTranslationMap, |
|||
LegendPosition, |
|||
legendPositionTranslationMap |
|||
} from '@shared/models/widget.models'; |
|||
|
|||
export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken<any>('LegendConfigPanelData'); |
|||
|
|||
export interface LegendConfigPanelData { |
|||
legendConfig: LegendConfig; |
|||
legendConfigUpdated: (legendConfig: LegendConfig) => void; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-legend-config-panel', |
|||
templateUrl: './legend-config-panel.component.html', |
|||
styleUrls: ['./legend-config-panel.component.scss'] |
|||
}) |
|||
export class LegendConfigPanelComponent extends PageComponent implements OnInit { |
|||
|
|||
legendConfigForm: FormGroup; |
|||
|
|||
legendDirection = LegendDirection; |
|||
|
|||
legendDirections = Object.keys(LegendDirection); |
|||
|
|||
legendDirectionTranslations = legendDirectionTranslationMap; |
|||
|
|||
legendPosition = LegendPosition; |
|||
|
|||
legendPositions = Object.keys(LegendPosition); |
|||
|
|||
legendPositionTranslations = legendPositionTranslationMap; |
|||
|
|||
constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData, |
|||
public overlayRef: OverlayRef, |
|||
protected store: Store<AppState>, |
|||
public fb: FormBuilder, |
|||
private overlay: Overlay, |
|||
public viewContainerRef: ViewContainerRef) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.legendConfigForm = this.fb.group({ |
|||
direction: [this.data.legendConfig.direction, []], |
|||
position: [this.data.legendConfig.position, []], |
|||
showMin: [this.data.legendConfig.showMin, []], |
|||
showMax: [this.data.legendConfig.showMax, []], |
|||
showAvg: [this.data.legendConfig.showAvg, []], |
|||
showTotal: [this.data.legendConfig.showTotal, []] |
|||
}); |
|||
this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => { |
|||
this.onDirectionChanged(direction); |
|||
}); |
|||
this.onDirectionChanged(this.data.legendConfig.direction); |
|||
this.legendConfigForm.valueChanges.subscribe(() => { |
|||
this.update(); |
|||
}); |
|||
} |
|||
|
|||
private onDirectionChanged(direction: LegendDirection) { |
|||
if (direction === LegendDirection.row) { |
|||
let position: LegendPosition = this.legendConfigForm.get('position').value; |
|||
if (position !== LegendPosition.bottom && position !== LegendPosition.top) { |
|||
position = LegendPosition.bottom; |
|||
} |
|||
this.legendConfigForm.patchValue( |
|||
{ |
|||
position, |
|||
showMin: false, |
|||
showMax: false, |
|||
showAvg: false, |
|||
showTotal: false |
|||
}, {emitEvent: false} |
|||
); |
|||
this.legendConfigForm.get('showMin').disable({emitEvent: false}); |
|||
this.legendConfigForm.get('showMax').disable({emitEvent: false}); |
|||
this.legendConfigForm.get('showAvg').disable({emitEvent: false}); |
|||
this.legendConfigForm.get('showTotal').disable({emitEvent: false}); |
|||
} else { |
|||
this.legendConfigForm.get('showMin').enable({emitEvent: false}); |
|||
this.legendConfigForm.get('showMax').enable({emitEvent: false}); |
|||
this.legendConfigForm.get('showAvg').enable({emitEvent: false}); |
|||
this.legendConfigForm.get('showTotal').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
update() { |
|||
const newLegendConfig: LegendConfig = this.legendConfigForm.value; |
|||
this.data.legendConfigUpdated(newLegendConfig); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" |
|||
mat-button mat-raised-button color="primary" (click)="openEditMode($event)"> |
|||
<mat-icon class="material-icons">toc</mat-icon> |
|||
<span translate>legend.settings</span> |
|||
</button> |
|||
@ -0,0 +1,198 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
Component, |
|||
forwardRef, Inject, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; |
|||
import { |
|||
HistoryWindowType, |
|||
Timewindow, |
|||
TimewindowType, |
|||
initModelFromDefaultTimewindow, cloneSelectedTimewindow |
|||
} from '@shared/models/time/time.models'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { |
|||
Overlay, |
|||
CdkOverlayOrigin, |
|||
OverlayConfig, |
|||
OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef |
|||
} from '@angular/cdk/overlay'; |
|||
import { |
|||
TIMEWINDOW_PANEL_DATA, |
|||
TimewindowPanelComponent, |
|||
TimewindowPanelData |
|||
} from '@shared/components/time/timewindow-panel.component'; |
|||
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; |
|||
import { MediaBreakpoints } from '@shared/models/constants'; |
|||
import { BreakpointObserver } from '@angular/cdk/layout'; |
|||
import { DOCUMENT } from '@angular/common'; |
|||
import { WINDOW } from '@core/services/window.service'; |
|||
import { TimeService } from '@core/services/time.service'; |
|||
import { TooltipPosition } from '@angular/material/typings/tooltip'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { LegendConfig } from '@shared/models/widget.models'; |
|||
import { |
|||
LEGEND_CONFIG_PANEL_DATA, |
|||
LegendConfigPanelComponent, |
|||
LegendConfigPanelData |
|||
} from '@home/components/widget/legend-config-panel.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-legend-config', |
|||
templateUrl: './legend-config.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => LegendConfigComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@ViewChild('legendConfigPanelOrigin', {static: false}) legendConfigPanelOrigin: CdkOverlayOrigin; |
|||
|
|||
innerValue: LegendConfig; |
|||
|
|||
private propagateChange = (_: any) => {}; |
|||
|
|||
constructor(private overlay: Overlay, |
|||
public viewContainerRef: ViewContainerRef, |
|||
public breakpointObserver: BreakpointObserver, |
|||
@Inject(DOCUMENT) private document: Document, |
|||
@Inject(WINDOW) private window: Window) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
} |
|||
|
|||
openEditMode() { |
|||
if (this.disabled) { |
|||
return; |
|||
} |
|||
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); |
|||
const position = this.overlay.position(); |
|||
const config = new OverlayConfig({ |
|||
panelClass: 'tb-legend-config-panel', |
|||
backdropClass: 'cdk-overlay-transparent-backdrop', |
|||
hasBackdrop: isGtSm, |
|||
}); |
|||
if (isGtSm) { |
|||
config.minWidth = '220px'; |
|||
config.maxHeight = '300px'; |
|||
const panelHeight = 220; |
|||
const panelWidth = 220; |
|||
const el = this.legendConfigPanelOrigin.elementRef.nativeElement; |
|||
const offset = el.getBoundingClientRect(); |
|||
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; |
|||
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; |
|||
const bottomY = offset.bottom - scrollTop; |
|||
const leftX = offset.left - scrollLeft; |
|||
let originX; |
|||
let originY; |
|||
let overlayX; |
|||
let overlayY; |
|||
const wHeight = this.document.documentElement.clientHeight; |
|||
const wWidth = this.document.documentElement.clientWidth; |
|||
if (bottomY + panelHeight > wHeight) { |
|||
originY = 'top'; |
|||
overlayY = 'bottom'; |
|||
} else { |
|||
originY = 'bottom'; |
|||
overlayY = 'top'; |
|||
} |
|||
if (leftX + panelWidth > wWidth) { |
|||
originX = 'end'; |
|||
overlayX = 'end'; |
|||
} else { |
|||
originX = 'start'; |
|||
overlayX = 'start'; |
|||
} |
|||
const connectedPosition: ConnectedPosition = { |
|||
originX, |
|||
originY, |
|||
overlayX, |
|||
overlayY |
|||
}; |
|||
config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef) |
|||
.withPositions([connectedPosition]); |
|||
} else { |
|||
config.minWidth = '100%'; |
|||
config.minHeight = '100%'; |
|||
config.positionStrategy = position.global().top('0%').left('0%') |
|||
.right('0%').bottom('0%'); |
|||
} |
|||
|
|||
const overlayRef = this.overlay.create(config); |
|||
|
|||
overlayRef.backdropClick().subscribe(() => { |
|||
overlayRef.dispose(); |
|||
}); |
|||
|
|||
const injector = this._createLegendConfigPanelInjector( |
|||
overlayRef, |
|||
{ |
|||
legendConfig: deepClone(this.innerValue), |
|||
legendConfigUpdated: this.legendConfigUpdated.bind(this) |
|||
} |
|||
); |
|||
|
|||
overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector)); |
|||
} |
|||
|
|||
private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector { |
|||
const injectionTokens = new WeakMap<any, any>([ |
|||
[LEGEND_CONFIG_PANEL_DATA, data], |
|||
[OverlayRef, overlayRef] |
|||
]); |
|||
return new PortalInjector(this.viewContainerRef.injector, injectionTokens); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(obj: LegendConfig): void { |
|||
this.innerValue = obj; |
|||
} |
|||
|
|||
private legendConfigUpdated(legendConfig: LegendConfig) { |
|||
this.innerValue = legendConfig; |
|||
this.propagateChange(this.innerValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form class="tb-material-icons-dialog" style="min-width: 600px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2>{{ 'icon.select-icon' | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-slide-toggle [formControl]="showAllControl"> |
|||
</mat-slide-toggle> |
|||
<label translate>icon.show-all</label> |
|||
</section> |
|||
<button mat-button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div class="tb-absolute-fill tb-icons-load" *ngIf="loadingIcons$ | async" fxLayout="column" fxLayoutAlign="center center"> |
|||
<mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner> |
|||
</div> |
|||
<div mat-dialog-content> |
|||
<div class="mat-content mat-padding" fxLayout="column"> |
|||
<fieldset [disabled]="(isLoading$ | async)"> |
|||
<ng-template ngFor let-icon [ngForOf]="icons$ | async" let-last="last"> |
|||
<ng-container #iconButtons> |
|||
<button *ngIf="icon === selectedIcon" |
|||
class="tb-select-icon-button" |
|||
mat-button mat-icon-button |
|||
mat-raised-button |
|||
color="primary" |
|||
(click)="selectIcon(icon)" |
|||
matTooltip="{{ icon }}" |
|||
matTooltipPosition="above" |
|||
type="button"> |
|||
<mat-icon>{{icon}}</mat-icon> |
|||
</button> |
|||
<button *ngIf="icon !== selectedIcon" |
|||
class="tb-select-icon-button" |
|||
mat-button mat-icon-button |
|||
(click)="selectIcon(icon)" |
|||
matTooltip="{{ icon }}" |
|||
matTooltipPosition="above" |
|||
type="button"> |
|||
<mat-icon>{{icon}}</mat-icon> |
|||
</button> |
|||
</ng-container> |
|||
</ng-template> |
|||
</fieldset> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,39 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
.tb-material-icons-dialog { |
|||
position: relative; |
|||
} |
|||
.tb-icons-load { |
|||
top: 64px; |
|||
z-index: 3; |
|||
background: rgba(255, 255, 255, .75); |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-material-icons-dialog { |
|||
button.mat-icon-button.tb-select-icon-button { |
|||
width: 56px; |
|||
height: 56px; |
|||
padding: 16px; |
|||
margin: 10px; |
|||
border: solid 1px #ffa500; |
|||
border-radius: 0%; |
|||
line-height: 0; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnInit, QueryList, ViewChildren, TemplateRef, AfterViewInit } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { FormControl } from '@angular/forms'; |
|||
import { Observable, of, merge, noop } from 'rxjs'; |
|||
import { delay, map, mergeMap, share, startWith, tap, mapTo } from 'rxjs/operators'; |
|||
import { DashboardInfo } from '@shared/models/dashboard.models'; |
|||
import { MatTab } from '@angular/material/tabs'; |
|||
|
|||
export interface MaterialIconsDialogData { |
|||
icon: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-material-icons-dialog', |
|||
templateUrl: './material-icons-dialog.component.html', |
|||
providers: [], |
|||
styleUrls: ['./material-icons-dialog.component.scss'] |
|||
}) |
|||
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> |
|||
implements OnInit, AfterViewInit { |
|||
|
|||
@ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>; |
|||
|
|||
selectedIcon: string; |
|||
icons$: Observable<Array<string>>; |
|||
loadingIcons$: Observable<boolean>; |
|||
|
|||
showAllControl: FormControl; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, |
|||
private utils: UtilsService, |
|||
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) { |
|||
super(store, router, dialogRef); |
|||
this.selectedIcon = data.icon; |
|||
this.showAllControl = new FormControl(false); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.icons$ = this.showAllControl.valueChanges.pipe( |
|||
map((showAll) => { |
|||
return {firstTime: false, showAll}; |
|||
}), |
|||
startWith<{firstTime: boolean, showAll: boolean}>({firstTime: true, showAll: false}), |
|||
mergeMap((data) => { |
|||
if (data.showAll) { |
|||
return this.utils.getMaterialIcons().pipe(delay(100)); |
|||
} else { |
|||
const res = of(this.utils.getCommonMaterialIcons()); |
|||
return data.firstTime ? res : res.pipe(delay(50)); |
|||
} |
|||
}), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
this.loadingIcons$ = merge( |
|||
this.showAllControl.valueChanges.pipe( |
|||
mapTo(true), |
|||
), |
|||
this.iconButtons.changes.pipe( |
|||
delay(100), |
|||
mapTo( false), |
|||
) |
|||
).pipe( |
|||
tap((loadingIcons) => { |
|||
if (loadingIcons) { |
|||
this.showAllControl.disable({emitEvent: false}); |
|||
} else { |
|||
this.showAllControl.enable({emitEvent: false}); |
|||
} |
|||
}), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
selectIcon(icon: string) { |
|||
this.dialogRef.close(icon); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="row" [formGroup]="materialIconFormGroup"> |
|||
<mat-icon (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon> |
|||
<mat-form-field> |
|||
<mat-label translate>icon.icon</mat-label> |
|||
<input matInput formControlName="icon" (mousedown)="openIconDialog()"> |
|||
</mat-form-field> |
|||
</div> |
|||
@ -0,0 +1,23 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
.mat-icon { |
|||
padding: 4px; |
|||
margin: 8px 4px 4px; |
|||
cursor: pointer; |
|||
border: solid 1px rgba(0, 0, 0, .27); |
|||
} |
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { DataKey, DatasourceType } from '@shared/models/widget.models'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { map, mergeMap, tap } from 'rxjs/operators'; |
|||
import { alarmFields } from '@shared/models/alarm.models'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-material-icon-select', |
|||
templateUrl: './material-icon-select.component.html', |
|||
styleUrls: ['./material-icon-select.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => MaterialIconSelectComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class MaterialIconSelectComponent extends PageComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
private modelValue: string; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public materialIconFormGroup: FormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private dialogs: DialogService, |
|||
private fb: FormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.materialIconFormGroup = this.fb.group({ |
|||
icon: [null, []] |
|||
}); |
|||
|
|||
this.materialIconFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.materialIconFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.materialIconFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: string): void { |
|||
this.modelValue = value; |
|||
this.materialIconFormGroup.patchValue( |
|||
{ icon: this.modelValue }, {emitEvent: false} |
|||
); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const icon: string = this.materialIconFormGroup.get('icon').value; |
|||
if (this.modelValue !== icon) { |
|||
this.modelValue = icon; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
openIconDialog() { |
|||
this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe( |
|||
(icon) => { |
|||
if (icon) { |
|||
this.materialIconFormGroup.patchValue( |
|||
{icon}, {emitEvent: true} |
|||
); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
declare module '!raw-loader!*' { |
|||
const contents: string; |
|||
export = contents; |
|||
} |
|||
Loading…
Reference in new issue