Edit relation dialog
This commit is contained in:
parent
851a3657db
commit
b438cdd255
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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 : []));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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
Normal file
180
ui-ngx/src/app/shared/components/json-object-edit.component.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user