Browse Source

Edit relation dialog

pull/2147/head
Igor Kulikov 7 years ago
parent
commit
b438cdd255
  1. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  2. 68
      ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html
  3. 18
      ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss
  4. 134
      ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts
  5. 112
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts
  6. 36
      ui-ngx/src/app/shared/components/entity/entity-list-select.component.html
  7. 32
      ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss
  8. 170
      ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts
  9. 4
      ui-ngx/src/app/shared/components/entity/entity-list.component.html
  10. 57
      ui-ngx/src/app/shared/components/entity/entity-list.component.ts
  11. 35
      ui-ngx/src/app/shared/components/json-object-edit.component.html
  12. 55
      ui-ngx/src/app/shared/components/json-object-edit.component.scss
  13. 180
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  14. 42
      ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html
  15. 168
      ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts
  16. 2
      ui-ngx/src/app/shared/models/page/page-link.ts
  17. 9
      ui-ngx/src/app/shared/shared.module.ts

7
ui-ngx/src/app/modules/home/components/home-components.module.ts

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

68
ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html

@ -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>

18
ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss

@ -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 {
}

134
ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts

@ -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);
}
);
}
}
}
}

112
ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts

@ -26,12 +26,18 @@ import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { EntityRelationService } from '@core/http/entity-relation.service';
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 { 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 { 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({
selector: 'tb-relation-table',
@ -201,8 +207,35 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
if ($event) {
$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) {
@ -210,16 +243,79 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
$event.stopPropagation();
}
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) {
$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();
}
}
);
}
}

36
ui-ngx/src/app/shared/components/entity/entity-list-select.component.html

@ -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>

32
ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss

@ -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;
}
}
}
}

170
ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts

@ -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;
}
}
}

4
ui-ngx/src/app/shared/components/entity/entity-list.component.html

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

57
ui-ngx/src/app/shared/components/entity/entity-list.component.ts

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

35
ui-ngx/src/app/shared/components/json-object-edit.component.html

@ -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>

55
ui-ngx/src/app/shared/components/json-object-edit.component.scss

@ -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;
}
}
}
}

180
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -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);
}
}
}

42
ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html

@ -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>

168
ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts

@ -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);
}
}

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

@ -96,7 +96,7 @@ export class PageLink {
pageData.totalElements = pageData.data.length;
pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize);
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 endIndex = startIndex + this.pageSize;

9
ui-ngx/src/app/shared/shared.module.ts

@ -83,6 +83,9 @@ import {EntitySelectComponent} from './components/entity/entity-select.component
import {DatetimeComponent} from '@shared/components/time/datetime.component';
import {EntityKeysListComponent} from './components/entity/entity-keys-list.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({
providers: [
@ -122,7 +125,10 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
EntityTypeSelectComponent,
EntitySelectComponent,
EntityKeysListComponent,
EntityListSelectComponent,
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -192,7 +198,10 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
EntityTypeSelectComponent,
EntitySelectComponent,
EntityKeysListComponent,
EntityListSelectComponent,
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
// ValueInputComponent,
MatButtonModule,
MatCheckboxModule,

Loading…
Cancel
Save