Edit relation dialog

This commit is contained in:
Igor Kulikov 2019-08-27 20:07:09 +03:00
parent 851a3657db
commit b438cdd255
17 changed files with 1099 additions and 30 deletions

View File

@ -27,12 +27,14 @@ import { AuditLogTableComponent } from './audit-log/audit-log-table.component';
import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
import { EventTableComponent } from '@home/components/event/event-table.component'; import { EventTableComponent } from '@home/components/event/event-table.component';
import { RelationTableComponent } from '@home/components/relation/relation-table.component'; import { RelationTableComponent } from '@home/components/relation/relation-table.component';
import { RelationDialogComponent } from './relation/relation-dialog.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
AddEntityDialogComponent, AddEntityDialogComponent,
AuditLogDetailsDialogComponent, AuditLogDetailsDialogComponent,
EventTableHeaderComponent EventTableHeaderComponent,
RelationDialogComponent
], ],
declarations: declarations:
[ [
@ -45,7 +47,8 @@ import { RelationTableComponent } from '@home/components/relation/relation-table
AuditLogDetailsDialogComponent, AuditLogDetailsDialogComponent,
EventTableHeaderComponent, EventTableHeaderComponent,
EventTableComponent, EventTableComponent,
RelationTableComponent RelationTableComponent,
RelationDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -0,0 +1,68 @@
<!--
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 #relationForm="ngForm" [formGroup]="relationFormGroup" (ngSubmit)="save()" style="min-width: 600px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ (isAdd ? 'relation.add' : 'relation.edit' ) | 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">
<tb-relation-type-autocomplete
formControlName="type"
required="true">
</tb-relation-type-autocomplete>
<small>{{(direction === entitySearchDirection.FROM ?
'relation.to-entity' : 'relation.from-entity') | translate}}</small>
<tb-entity-list-select
formControlName="targetEntityIds"
required="true">
</tb-entity-list-select>
<tb-json-object-edit
formControlName="additionalInfo"
label="{{ 'relation.additional-info' | translate }}">
</tb-json-object-edit>
<div class="tb-error-messages" *ngIf="submitted &&
relationFormGroup.get('additionalInfo').invalid" role="alert">
<div translate class="tb-error-message">relation.invalid-additional-info</div>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async)">
{{ (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>

View File

@ -0,0 +1,18 @@
/**
* 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 {
}

View File

@ -0,0 +1,134 @@
///
/// 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, SkipSelf } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import {
CONTAINS_TYPE,
EntityRelation,
EntitySearchDirection,
RelationTypeGroup
} from '@shared/models/relation.models';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { EntityId } from '@shared/models/id/entity-id';
import { Observable, forkJoin } from 'rxjs';
export interface RelationDialogData {
isAdd: boolean;
direction: EntitySearchDirection;
relation: EntityRelation;
}
@Component({
selector: 'tb-relation-dialog',
templateUrl: './relation-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: RelationDialogComponent}],
styleUrls: ['./relation-dialog.component.scss']
})
export class RelationDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
relationFormGroup: FormGroup;
isAdd: boolean;
direction: EntitySearchDirection;
entitySearchDirection = EntitySearchDirection;
submitted = false;
constructor(protected store: Store<AppState>,
@Inject(MAT_DIALOG_DATA) public data: RelationDialogData,
private entityRelationService: EntityRelationService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<RelationDialogComponent, boolean>,
public fb: FormBuilder) {
super(store);
this.isAdd = data.isAdd;
this.direction = data.direction;
}
ngOnInit(): void {
this.relationFormGroup = this.fb.group({
type: [this.isAdd ? CONTAINS_TYPE : this.data.relation.type, [Validators.required]],
targetEntityIds: [this.isAdd ? null :
[this.direction === EntitySearchDirection.FROM ? this.data.relation.to : this.data.relation.from],
[Validators.required]],
additionalInfo: [this.data.relation.additionalInfo]
});
if (!this.isAdd) {
this.relationFormGroup.get('type').disable();
this.relationFormGroup.get('targetEntityIds').disable();
}
this.relationFormGroup.valueChanges.subscribe(
() => {
this.submitted = false;
}
);
}
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;
}
cancel(): void {
this.dialogRef.close(false);
}
save(): void {
this.submitted = true;
if (this.relationFormGroup.valid) {
const additionalInfo = this.relationFormGroup.get('additionalInfo').value;
if (this.isAdd) {
const tasks: Observable<EntityRelation>[] = [];
const type: string = this.relationFormGroup.get('type').value;
const entityIds: Array<EntityId> = this.relationFormGroup.get('targetEntityIds').value;
entityIds.forEach(entityId => {
const relation = {
type,
additionalInfo,
typeGroup: RelationTypeGroup.COMMON
} as EntityRelation;
if (this.direction === EntitySearchDirection.FROM) {
relation.from = this.data.relation.from;
relation.to = entityId;
} else {
relation.from = entityId;
relation.to = this.data.relation.to;
}
tasks.push(this.entityRelationService.saveRelation(relation));
});
forkJoin(tasks).subscribe(
() => {
this.dialogRef.close(true);
}
);
} else {
const relation: EntityRelation = {...this.data.relation};
relation.additionalInfo = additionalInfo;
this.entityRelationService.saveRelation(relation).subscribe(
() => {
this.dialogRef.close(true);
}
);
}
}
}
}

View File

@ -26,12 +26,18 @@ import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { EntityRelationService } from '@core/http/entity-relation.service'; import { EntityRelationService } from '@core/http/entity-relation.service';
import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { fromEvent, merge } from 'rxjs'; import { forkJoin, fromEvent, merge, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { EntityRelationInfo, EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models'; import {
EntityRelation,
EntityRelationInfo,
EntitySearchDirection,
entitySearchDirectionTranslations,
RelationTypeGroup
} from '@shared/models/relation.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { RelationsDatasource } from '../../models/datasource/relation-datasource'; import { RelationsDatasource } from '../../models/datasource/relation-datasource';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
@Component({ @Component({
selector: 'tb-relation-table', selector: 'tb-relation-table',
@ -201,8 +207,35 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
let title;
let content;
if (this.direction === EntitySearchDirection.FROM) {
title = this.translate.instant('relation.delete-to-relation-title', {entityName: relation.toName});
content = this.translate.instant('relation.delete-to-relation-text', {entityName: relation.toName});
} else {
title = this.translate.instant('relation.delete-from-relation-title', {entityName: relation.fromName});
content = this.translate.instant('relation.delete-from-relation-text', {entityName: relation.fromName});
}
// TODO: this.dialogService.confirm(
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.entityRelationService.deleteRelation(
relation.from,
relation.type,
relation.to
).subscribe(
() => {
this.reloadRelations();
}
);
}
});
} }
deleteRelations($event: Event) { deleteRelations($event: Event) {
@ -210,16 +243,79 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.dataSource.selection.selected.length > 0) { if (this.dataSource.selection.selected.length > 0) {
// TODO: let title;
let content;
if (this.direction === EntitySearchDirection.FROM) {
title = this.translate.instant('relation.delete-to-relations-title', {count: this.dataSource.selection.selected.length});
content = this.translate.instant('relation.delete-to-relations-text');
} else {
title = this.translate.instant('relation.delete-from-relations-title', {count: this.dataSource.selection.selected.length});
content = this.translate.instant('relation.delete-from-relations-text');
}
this.dialogService.confirm(
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
const tasks: Observable<any>[] = [];
this.dataSource.selection.selected.forEach((relation) => {
tasks.push(this.entityRelationService.deleteRelation(
relation.from,
relation.type,
relation.to
));
});
forkJoin(tasks).subscribe(
() => {
this.reloadRelations();
}
);
}
});
} }
} }
openRelationDialog($event: Event, relation: EntityRelationInfo = null) { openRelationDialog($event: Event, relation: EntityRelation = null) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
// TODO:
let isAdd = false;
if (!relation) {
isAdd = true;
relation = {
from: null,
to: null,
type: null,
typeGroup: RelationTypeGroup.COMMON
};
if (this.direction === EntitySearchDirection.FROM) {
relation.from = this.entityIdValue;
} else {
relation.to = this.entityIdValue;
}
} }
this.dialog.open<RelationDialogComponent, RelationDialogData, boolean>(RelationDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
direction: this.direction,
relation: {...relation}
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.reloadRelations();
}
}
);
}
} }

View File

@ -0,0 +1,36 @@
<!--
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" class="tb-entity-list-select" [formGroup]="entityListSelectFormGroup">
<tb-entity-type-select
style="min-width: 100px; padding-right: 8px;"
*ngIf="displayEntityTypeSelect"
[showLabel]="true"
[required]="required"
[useAliasEntityTypes]="useAliasEntityTypes"
[allowedEntityTypes]="allowedEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<tb-entity-list
[ngClass]="{'tb-not-empty': this.modelValue.ids?.length > 0}"
fxFlex
*ngIf="modelValue.entityType"
[required]="required"
[entityType]="modelValue.entityType"
formControlName="entityIds">
</tb-entity-list>
</div>

View File

@ -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 {
}
:host ::ng-deep {
tb-entity-list {
&.tb-not-empty {
.mat-form-field-flex {
padding-top: 0;
}
}
.mat-form-field-flex {
.mat-form-field-infix {
border-top: 0;
}
}
}
}

View File

@ -0,0 +1,170 @@
///
/// 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, Component, forwardRef, Input, OnInit} from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Store} from '@ngrx/store';
import {AppState} from '@core/core.state';
import {TranslateService} from '@ngx-translate/core';
import {AliasEntityType, EntityType, entityTypeTranslations} from '@shared/models/entity-type.models';
import {EntityService} from '@core/http/entity.service';
import {EntityId} from '@shared/models/id/entity-id';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
interface EntityListSelectModel {
entityType: EntityType | AliasEntityType;
ids: Array<string>;
}
@Component({
selector: 'tb-entity-list-select',
templateUrl: './entity-list-select.component.html',
styleUrls: ['./entity-list-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityListSelectComponent),
multi: true
}]
})
export class EntityListSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit {
entityListSelectFormGroup: FormGroup;
modelValue: EntityListSelectModel = {entityType: null, ids: []};
@Input()
allowedEntityTypes: Array<EntityType | AliasEntityType>;
@Input()
useAliasEntityTypes: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
displayEntityTypeSelect: boolean;
private defaultEntityType: EntityType | AliasEntityType = null;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private entityService: EntityService,
public translate: TranslateService,
private fb: FormBuilder) {
const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes,
this.useAliasEntityTypes);
if (entityTypes.length === 1) {
this.displayEntityTypeSelect = false;
this.defaultEntityType = entityTypes[0];
} else {
this.displayEntityTypeSelect = true;
}
this.entityListSelectFormGroup = this.fb.group({
entityType: [this.defaultEntityType],
entityIds: [[]]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.entityListSelectFormGroup.get('entityType').valueChanges.subscribe(
(value) => {
this.updateView(value, this.modelValue.ids);
}
);
this.entityListSelectFormGroup.get('entityIds').valueChanges.subscribe(
(values) => {
this.updateView(this.modelValue.entityType, values);
}
);
}
ngAfterViewInit(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.entityListSelectFormGroup.disable();
} else {
this.entityListSelectFormGroup.enable();
}
}
writeValue(value: Array<EntityId> | null): void {
if (value != null && value.length > 0) {
const id = value[0];
this.modelValue = {
entityType: id.entityType,
ids: value.map(val => val.id)
};
} else {
this.modelValue = {
entityType: this.defaultEntityType,
ids: []
};
}
this.entityListSelectFormGroup.get('entityType').patchValue(this.modelValue.entityType, {emitEvent: true});
this.entityListSelectFormGroup.get('entityIds').patchValue([...this.modelValue.ids], {emitEvent: true});
}
updateView(entityType: EntityType | AliasEntityType | null, entityIds: Array<string> | null) {
if (this.modelValue.entityType !== entityType ||
!this.compareIds(this.modelValue.ids, entityIds)) {
this.modelValue = {
entityType,
ids: this.modelValue.entityType !== entityType || !entityIds ? [] : [...entityIds]
};
this.propagateChange(this.toEntityIds(this.modelValue));
}
}
compareIds(ids1: Array<string> | null, ids2: Array<string> | null): boolean {
if (ids1 !== null && ids2 !== null) {
return JSON.stringify(ids1) === JSON.stringify(ids2);
} else {
return ids1 === ids2;
}
}
toEntityIds(modelValue: EntityListSelectModel): Array<EntityId> {
if (modelValue !== null && modelValue.entityType && modelValue.ids && modelValue.ids.length > 0) {
const entityType = modelValue.entityType;
return modelValue.ids.map(id => ({entityType, id}));
} else {
return null;
}
}
}

View File

@ -16,7 +16,7 @@
--> -->
<mat-form-field appearance="standard" [formGroup]="entityListFormGroup" class="mat-block"> <mat-form-field appearance="standard" [formGroup]="entityListFormGroup" class="mat-block">
<mat-chip-list #chipList> <mat-chip-list #chipList formControlName="entities">
<mat-chip <mat-chip
*ngFor="let entity of entities" *ngFor="let entity of entities"
[selectable]="!disabled" [selectable]="!disabled"
@ -47,7 +47,7 @@
</span> </span>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-error> <mat-error *ngIf="entityListFormGroup.get('entities').hasError('required')">
{{ 'entity.entity-list-empty' | translate }} {{ 'entity.entity-list-empty' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>

View File

@ -14,16 +14,26 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild} from '@angular/core'; import {
AfterContentInit,
AfterViewInit,
Component,
ElementRef,
forwardRef,
Input,
OnInit,
SkipSelf,
ViewChild
} from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
FormGroupDirective, FormGroupDirective,
NG_VALUE_ACCESSOR, NgForm NG_VALUE_ACCESSOR, NgForm, Validators
} from '@angular/forms'; } from '@angular/forms';
import {Observable} from 'rxjs'; import {Observable, of} from 'rxjs';
import {map, mergeMap, startWith, tap, share, pairwise, filter} from 'rxjs/operators'; import {map, mergeMap, startWith, tap, share, pairwise, filter} from 'rxjs/operators';
import {Store} from '@ngrx/store'; import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state'; import {AppState} from '@app/core/core.state';
@ -34,6 +44,7 @@ import {EntityId} from '@shared/models/id/entity-id';
import {EntityService} from '@core/http/entity.service'; import {EntityService} from '@core/http/entity.service';
import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material'; import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { emptyPageData } from '@shared/models/page/page-data';
@Component({ @Component({
selector: 'tb-entity-list', selector: 'tb-entity-list',
@ -69,7 +80,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
} }
@Input() @Input()
set required(value: boolean) { set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value); const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
this.updateValidators();
}
} }
@Input() @Input()
@ -77,7 +92,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
@ViewChild('entityInput', {static: false}) entityInput: ElementRef<HTMLInputElement>; @ViewChild('entityInput', {static: false}) entityInput: ElementRef<HTMLInputElement>;
@ViewChild('entityAutocomplete', {static: false}) matAutocomplete: MatAutocomplete; @ViewChild('entityAutocomplete', {static: false}) matAutocomplete: MatAutocomplete;
@ViewChild('chipList', {static: false}) chipList: MatChipList; @ViewChild('chipList', {static: true}) chipList: MatChipList;
entities: Array<BaseData<EntityId>> = []; entities: Array<BaseData<EntityId>> = [];
filteredEntities: Observable<Array<BaseData<EntityId>>>; filteredEntities: Observable<Array<BaseData<EntityId>>>;
@ -91,10 +106,16 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
private entityService: EntityService, private entityService: EntityService,
private fb: FormBuilder) { private fb: FormBuilder) {
this.entityListFormGroup = this.fb.group({ this.entityListFormGroup = this.fb.group({
entities: [this.entities, this.required ? [Validators.required] : []],
entity: [null] entity: [null]
}); });
} }
updateValidators() {
this.entityListFormGroup.get('entities').setValidators(this.required ? [Validators.required] : []);
this.entityListFormGroup.get('entities').updateValueAndValidity();
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -120,34 +141,39 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
); );
} }
ngAfterViewInit(): void {} ngAfterViewInit(): void {
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
const emitEvent = this.disabled !== isDisabled;
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (isDisabled) {
this.entityListFormGroup.disable(); this.entityListFormGroup.disable({emitEvent});
} else { } else {
this.entityListFormGroup.enable(); this.entityListFormGroup.enable({emitEvent});
} }
} }
writeValue(value: Array<string> | null): void { writeValue(value: Array<string> | null): void {
this.searchText = ''; this.searchText = '';
if (value != null) { if (value != null && value.length > 0) {
this.modelValue = [...value]; this.modelValue = [...value];
this.entityService.getEntities(this.entityTypeValue, value).subscribe( this.entityService.getEntities(this.entityTypeValue, value).subscribe(
(entities) => { (entities) => {
this.entities = entities; this.entities = entities;
this.entityListFormGroup.get('entities').setValue(this.entities);
} }
); );
} else { } else {
this.entities = []; this.entities = [];
this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue = null; this.modelValue = null;
} }
} }
reset() { reset() {
this.entities = []; this.entities = [];
this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue = null; this.modelValue = null;
this.entityListFormGroup.get('entity').patchValue('', {emitEvent: true}); this.entityListFormGroup.get('entity').patchValue('', {emitEvent: true});
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
@ -160,9 +186,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
} }
this.modelValue.push(entity.id.id); this.modelValue.push(entity.id.id);
this.entities.push(entity); this.entities.push(entity);
if (this.required) { this.entityListFormGroup.get('entities').setValue(this.entities);
this.chipList.errorState = false;
}
} }
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
this.clear(); this.clear();
@ -172,12 +196,10 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
const index = this.entities.indexOf(entity); const index = this.entities.indexOf(entity);
if (index >= 0) { if (index >= 0) {
this.entities.splice(index, 1); this.entities.splice(index, 1);
this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue.splice(index, 1); this.modelValue.splice(index, 1);
if (!this.modelValue.length) { if (!this.modelValue.length) {
this.modelValue = null; this.modelValue = null;
if (this.required) {
this.chipList.errorState = true;
}
} }
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
this.clear(); this.clear();
@ -190,7 +212,8 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> { fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> {
this.searchText = searchText; this.searchText = searchText;
return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText, return this.disabled ? of([]) :
this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
50, '', false, true).pipe( 50, '', false, true).pipe(
map((data) => data ? data : [])); map((data) => data ? data : []));
} }

View File

@ -0,0 +1,35 @@
<!--
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 style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center">
<label class="tb-title no-padding"
ng-class="{'tb-required': required,
'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label>
<span fxFlex></span>
<button mat-button mat-icon-button (click)="fullscreen = !fullscreen"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div fxFlex="0%" id="tb-json-panel" class="tb-json-object-panel" fxLayout="column">
<div fxFlex #jsonEditor id="tb-json-input" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
</div>

View File

@ -0,0 +1,55 @@
/**
* 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.
*/
/**
* 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 {
position: relative;
.fill-height {
height: 100%;
}
.tb-json-object-panel {
height: 100%;
margin-left: 15px;
border: 1px solid #c0c0c0;
#tb-json-input {
width: 100%;
min-width: 200px;
height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
}
}
}

View File

@ -0,0 +1,180 @@
///
/// 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 {
Attribute,
Component,
ElementRef,
forwardRef,
Input,
OnInit,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, Validator, NG_VALIDATORS } from '@angular/forms';
import * as ace from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({
selector: 'tb-json-object-edit',
templateUrl: './json-object-edit.component.html',
styleUrls: ['./json-object-edit.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => JsonObjectEditComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => JsonObjectEditComponent),
multi: true,
}
]
})
export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator {
@ViewChild('jsonEditor', {static: true})
jsonEditorElmRef: ElementRef;
private jsonEditor: ace.Ace.Editor;
@Input() label: string;
@Input() disabled: boolean;
@Input() fillHeight: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
private readonlyValue: boolean;
get readonly(): boolean {
return this.readonlyValue;
}
@Input()
set readonly(value: boolean) {
this.readonlyValue = coerceBooleanProperty(value);
}
fullscreen = false;
modelValue: any;
contentValue: string;
objectValid: boolean;
private propagateChange = null;
constructor() {
}
ngOnInit(): void {
const editorElement = this.jsonEditorElmRef.nativeElement;
let editorOptions: Partial<ace.Ace.EditorOptions> = {
mode: 'ace/mode/json',
theme: 'ace/theme/github',
showGutter: true,
showPrintMargin: false,
readOnly: this.readonly
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
this.jsonEditor = ace.edit(editorElement, editorOptions);
this.jsonEditor.session.setUseWrapMode(false);
this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1);
this.jsonEditor.on('change', () => {
this.updateView();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
public validate(c: FormControl) {
return (this.objectValid) ? null : {
jsonParseError: {
valid: false,
},
};
}
writeValue(value: any): void {
this.modelValue = value;
this.contentValue = '';
this.objectValid = false;
try {
if (this.modelValue) {
this.contentValue = JSON.stringify(this.modelValue, undefined, 2);
this.objectValid = true;
} else {
this.objectValid = !this.required;
}
} catch (e) {
//
}
if (this.jsonEditor) {
this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1);
}
}
updateView() {
const editorValue = this.jsonEditor.getValue();
if (this.contentValue !== editorValue) {
this.contentValue = editorValue;
let data = null;
this.objectValid = false;
if (this.contentValue && this.contentValue.length > 0) {
try {
data = JSON.parse(this.contentValue);
this.objectValid = true;
} catch (ex) {}
} else {
this.objectValid = !this.required;
}
this.propagateChange(data);
}
}
onFullscreen() {
if (this.jsonEditor) {
setTimeout(() => {
this.jsonEditor.resize();
}, 0);
}
}
}

View File

@ -0,0 +1,42 @@
<!--
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-form-field [formGroup]="relationTypeFormGroup" class="mat-block">
<input matInput type="text" placeholder="{{ required ? ('relation.relation-type' | translate) : ( !modelValue ? ('relation.any-relation-type' | translate) : ' ') }}"
#relationTypeInput
formControlName="relationType"
(focusin)="onFocus()"
[required]="required"
[matAutocomplete]="relationTypeAutocomplete">
<button *ngIf="relationTypeFormGroup.get('relationType').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#relationTypeAutocomplete="matAutocomplete"
[displayWith]="displayRelationTypeFn">
<mat-option *ngFor="let relationType of filteredRelationTypes | async" [value]="relationType">
<span [innerHTML]="relationType | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="relationTypeFormGroup.get('relationType').hasError('required')">
{{ 'relation.relation-type-required' | translate }}
</mat-error>
</mat-form-field>

View File

@ -0,0 +1,168 @@
///
/// 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, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {Observable, of, throwError, Subscription} from 'rxjs';
import {PageLink} from '@shared/models/page/page-link';
import {Direction} from '@shared/models/page/sort-order';
import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators';
import {PageData, emptyPageData} from '@shared/models/page/page-data';
import {DashboardInfo} from '@app/shared/models/dashboard.models';
import {DashboardId} from '@app/shared/models/id/dashboard-id';
import {DashboardService} from '@core/http/dashboard.service';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
import {Authority} from '@shared/models/authority.enum';
import {TranslateService} from '@ngx-translate/core';
import {DeviceService} from '@core/http/device.service';
import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models';
import {BroadcastService} from '@app/core/services/broadcast.service';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {AssetService} from '@core/http/asset.service';
import {EntityViewService} from '@core/http/entity-view.service';
import { RelationTypes } from '@app/shared/models/relation.models';
@Component({
selector: 'tb-relation-type-autocomplete',
templateUrl: './relation-type-autocomplete.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RelationTypeAutocompleteComponent),
multi: true
}]
})
export class RelationTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
relationTypeFormGroup: FormGroup;
modelValue: string | null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ViewChild('relationTypeInput', {static: true}) relationTypeInput: ElementRef;
filteredRelationTypes: Observable<Array<string>>;
private searchText = '';
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private broadcast: BroadcastService,
public translate: TranslateService,
private fb: FormBuilder) {
this.relationTypeFormGroup = this.fb.group({
relationType: [null, this.required ? [Validators.required] : []]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.filteredRelationTypes = this.relationTypeFormGroup.get('relationType').valueChanges
.pipe(
tap(value => {
this.updateView(value);
}),
// startWith<string | EntitySubtype>(''),
map(value => value ? value : ''),
mergeMap(type => this.fetchRelationTypes(type) )
);
}
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.relationTypeFormGroup.disable({emitEvent: false});
} else {
this.relationTypeFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string | null): void {
this.searchText = '';
this.modelValue = value;
this.relationTypeFormGroup.get('relationType').patchValue(value, {emitEvent: false});
this.dirty = true;
}
onFocus() {
if (this.dirty) {
this.relationTypeFormGroup.get('relationType').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
updateView(value: string | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displayRelationTypeFn(relationType?: string): string | undefined {
return relationType ? relationType : undefined;
}
fetchRelationTypes(searchText?: string, strictMatch: boolean = false): Observable<Array<string>> {
this.searchText = searchText;
return of(RelationTypes).pipe(
map(relationTypes => relationTypes.filter( relationType => {
if (strictMatch) {
return searchText ? relationType === searchText : false;
} else {
return searchText ? relationType.toUpperCase().startsWith(searchText.toUpperCase()) : true;
}
}))
);
}
clear() {
this.relationTypeFormGroup.get('relationType').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.relationTypeInput.nativeElement.blur();
this.relationTypeInput.nativeElement.focus();
}, 0);
}
}

View File

@ -96,7 +96,7 @@ export class PageLink {
pageData.totalElements = pageData.data.length; pageData.totalElements = pageData.data.length;
pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize); pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize);
if (this.sortOrder) { if (this.sortOrder) {
pageData.data = pageData.data.sort(this.sort); pageData.data = pageData.data.sort((a, b) => this.sort(a, b));
} }
const startIndex = this.pageSize * this.page; const startIndex = this.pageSize * this.page;
const endIndex = startIndex + this.pageSize; const endIndex = startIndex + this.pageSize;

View File

@ -83,6 +83,9 @@ import {EntitySelectComponent} from './components/entity/entity-select.component
import {DatetimeComponent} from '@shared/components/time/datetime.component'; import {DatetimeComponent} from '@shared/components/time/datetime.component';
import {EntityKeysListComponent} from './components/entity/entity-keys-list.component'; import {EntityKeysListComponent} from './components/entity/entity-keys-list.component';
import {SocialSharePanelComponent} from './components/socialshare-panel.component'; import {SocialSharePanelComponent} from './components/socialshare-panel.component';
import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component';
import { EntityListSelectComponent } from './components/entity/entity-list-select.component';
import { JsonObjectEditComponent } from './components/json-object-edit.component';
@NgModule({ @NgModule({
providers: [ providers: [
@ -122,7 +125,10 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
EntityTypeSelectComponent, EntityTypeSelectComponent,
EntitySelectComponent, EntitySelectComponent,
EntityKeysListComponent, EntityKeysListComponent,
EntityListSelectComponent,
RelationTypeAutocompleteComponent,
SocialSharePanelComponent, SocialSharePanelComponent,
JsonObjectEditComponent,
NospacePipe, NospacePipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,
@ -192,7 +198,10 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
EntityTypeSelectComponent, EntityTypeSelectComponent,
EntitySelectComponent, EntitySelectComponent,
EntityKeysListComponent, EntityKeysListComponent,
EntityListSelectComponent,
RelationTypeAutocompleteComponent,
SocialSharePanelComponent, SocialSharePanelComponent,
JsonObjectEditComponent,
// ValueInputComponent, // ValueInputComponent,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,