Browse Source

UI: Implement persistent RPC table widget settings form

pull/6545/head
Igor Kulikov 5 years ago
parent
commit
1cb4b6076e
  1. 3
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  2. 111
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html
  3. 221
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts
  4. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  5. 14
      ui-ngx/src/assets/locale/locale.constant-en_US.json

3
application/src/main/data/json/system/widget_bundles/control_widgets.json

@ -165,8 +165,9 @@
"templateHtml": "<tb-persistent-table-widget [ctx]='ctx'></tb-persistent-table-widget>", "templateHtml": "<tb-persistent-table-widget [ctx]='ctx'></tb-persistent-table-widget>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"PersistentTableSettings\",\n \"properties\": {\n \"enableStickyHeader\": {\n \"title\": \"Display header while scrolling\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowSendRequest\": {\n \"title\": \"Allow send RPC request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Display actions column while scrolling\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display request details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowDelete\": {\n \"title\": \"Allow delete request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n },\n \"displayColumns\": {\n \"title\": \"Columns for display\",\n \"type\": \"array\",\n \"minItems\": 1\n }\n },\n \"required\": [\"displayColumns\"]\n },\n \"uiSchema\": {\n \"type\": \"VerticalLayout\",\n \"elements\": [\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableStickyHeader\"\n },\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableFilter\"\n }\n ]\n },\n \"form\": [\n [\n \"enableStickyHeader\",\n \"enableFilter\",\n \"allowSendRequest\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowDelete\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ],\n [\n {\n \"key\": \"displayColumns\",\n \"type\": \"rc-select\",\n \"multiple\": true,\n \"default\": [\"rpcId\", \"messageType\", \"status\", \"method\", \"createdTime\", \"expirationTime\"],\n \"items\": [\n {\n \"value\": \"rpcId\",\n \"label\": \"RPC ID\"\n },\n {\n \"value\": \"messageType\",\n \"label\": \"Message type\"\n },\n {\n \"value\": \"status\",\n \"label\": \"Status\"\n },\n {\n \"value\": \"method\",\n \"label\": \"Method\"\n },\n {\n \"value\": \"createdTime\",\n \"label\": \"Created time\"\n },\n {\n \"value\": \"expirationTime\",\n \"label\": \"Expiration time\"\n }\n ]\n }\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Columns settings\"\n }]\n}", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-persistent-table-widget-settings",
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}" "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}"
} }
}, },

111
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html

@ -0,0 +1,111 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="persistentTableWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.persistent-table.general-settings</legend>
<section fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="8px">
<section fxLayout="column" fxFlex>
<mat-checkbox formControlName="enableFilter" style="margin-bottom: 16px;">
{{ 'widgets.persistent-table.enable-filter' | translate }}
</mat-checkbox>
</section>
<section fxLayout="column" fxFlex>
<mat-checkbox formControlName="enableStickyHeader" style="margin-bottom: 16px;">
{{ 'widgets.persistent-table.enable-sticky-header' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="enableStickyAction" style="margin-bottom: 16px;">
{{ 'widgets.persistent-table.enable-sticky-action' | translate }}
</mat-checkbox>
</section>
</section>
<section fxLayout="column" fxLayoutGap="8px">
<mat-slide-toggle fxFlex formControlName="displayDetails">
{{ 'widgets.persistent-table.display-request-details' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="allowSendRequest">
{{ 'widgets.persistent-table.allow-send-request' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="allowDelete">
{{ 'widgets.persistent-table.allow-delete-request' | translate }}
</mat-slide-toggle>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-slide-toggle fxFlex formControlName="displayPagination">
{{ 'widgets.table.display-pagination' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-page-size</mat-label>
<input matInput type="number" min="1" step="1" formControlName="defaultPageSize">
</mat-form-field>
</section>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-sort-order</mat-label>
<input matInput formControlName="defaultSortOrder">
</mat-form-field>
</section>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.persistent-table.columns-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.display-columns</mat-label>
<mat-chip-list #columnsChipList class="tb-drop-list"
required
cdkDropList
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="columnDrop($event)">
<mat-chip class="tb-draggable" cdkDrag *ngFor="let column of persistentTableWidgetSettingsForm.get('displayColumns').value"
[removable]="true" (removed)="onColumnRemoved(column)">
{{ displayColumnFromValue(column)?.name }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{ 'widgets.persistent-table.column' | translate }}"
style="max-width: 200px;"
#columnInput
(focusin)="onColumnInputFocus()"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
(input)="columnInputChange.next(columnInput.value)"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="columnAutocomplete"
[matChipInputFor]="columnsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addColumnFromChipInput($event)">
</mat-chip-list>
<mat-autocomplete #columnAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="columnSelected($event)"
[displayWith]="displayColumnFn">
<mat-option *ngFor="let column of filteredDisplayColumns | async" [value]="column">
<span [innerHTML]="column.name | highlight:columnSearchText"></span>
</mat-option>
<mat-option *ngIf="(filteredDisplayColumns | async)?.length === 0" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(columnSearchText); else searchNotEmpty">
<span translate>widgets.persistent-table.no-columns-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('widgets.persistent-table.no-columns-matching',
{column: truncate.transform(columnSearchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</fieldset>
</section>

221
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts

@ -0,0 +1,221 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, ViewChild } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of, Subject } from 'rxjs';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { map, mergeMap, share, startWith } from 'rxjs/operators';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
interface DisplayColumn {
name: string;
value: string;
}
@Component({
selector: 'tb-persistent-table-widget-settings',
templateUrl: './persistent-table-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class PersistentTableWidgetSettingsComponent extends WidgetSettingsComponent {
@ViewChild('columnsChipList') columnsChipList: MatChipList;
@ViewChild('columnAutocomplete') columnAutocomplete: MatAutocomplete;
@ViewChild('columnInput') columnInput: ElementRef<HTMLInputElement>;
displayColumns: Array<DisplayColumn> = [
{value: 'rpcId', name: this.translate.instant('widgets.persistent-table.rpc-id')},
{value: 'messageType', name: this.translate.instant('widgets.persistent-table.message-type')},
{value: 'status', name: this.translate.instant('widgets.persistent-table.status')},
{value: 'method', name: this.translate.instant('widgets.persistent-table.method')},
{value: 'createdTime', name: this.translate.instant('widgets.persistent-table.created-time')},
{value: 'expirationTime', name: this.translate.instant('widgets.persistent-table.expiration-time')},
];
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
persistentTableWidgetSettingsForm: FormGroup;
filteredDisplayColumns: Observable<Array<DisplayColumn>>;
columnSearchText = '';
columnInputChange = new Subject<string>();
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public truncate: TruncatePipe,
private fb: FormBuilder) {
super(store);
this.filteredDisplayColumns = this.columnInputChange
.pipe(
startWith(''),
map((value) => value ? value : ''),
mergeMap(name => this.fetchColumns(name) ),
share()
);
}
protected settingsForm(): FormGroup {
return this.persistentTableWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
enableFilter: true,
enableStickyHeader: true,
enableStickyAction: true,
displayDetails: true,
allowSendRequest: true,
allowDelete: true,
displayPagination: true,
defaultPageSize: 10,
defaultSortOrder: '-createdTime',
displayColumns: ['rpcId', 'messageType', 'status', 'method', 'createdTime', 'expirationTime']
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.persistentTableWidgetSettingsForm = this.fb.group({
enableFilter: [settings.enableFilter, []],
enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []],
allowSendRequest: [settings.allowSendRequest, []],
allowDelete: [settings.allowDelete, []],
displayDetails: [settings.displayDetails, []],
displayPagination: [settings.displayPagination, []],
defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]],
defaultSortOrder: [settings.defaultSortOrder, []],
displayColumns: [settings.displayColumns, [Validators.required]]
});
}
protected validateSettings(): boolean {
const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value;
this.columnsChipList.errorState = !displayColumns?.length;
return super.validateSettings();
}
protected validatorTriggers(): string[] {
return ['displayPagination'];
}
protected updateValidators(emitEvent: boolean) {
const displayPagination: boolean = this.persistentTableWidgetSettingsForm.get('displayPagination').value;
if (displayPagination) {
this.persistentTableWidgetSettingsForm.get('defaultPageSize').enable();
} else {
this.persistentTableWidgetSettingsForm.get('defaultPageSize').disable();
}
this.persistentTableWidgetSettingsForm.get('defaultPageSize').updateValueAndValidity({emitEvent});
}
private fetchColumns(searchText?: string): Observable<Array<DisplayColumn>> {
this.columnSearchText = searchText;
if (this.columnSearchText && this.columnSearchText.length) {
const search = this.columnSearchText.toUpperCase();
return of(this.displayColumns.filter(column => column.name.toUpperCase().includes(search)));
} else {
return of(this.displayColumns);
}
}
private addColumn(existingColumn: DisplayColumn): boolean {
if (existingColumn) {
const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value;
const index = displayColumns.indexOf(existingColumn.value);
if (index === -1) {
displayColumns.push(existingColumn.value);
this.persistentTableWidgetSettingsForm.get('displayColumns').setValue(displayColumns);
this.persistentTableWidgetSettingsForm.get('displayColumns').markAsDirty();
this.columnsChipList.errorState = false;
return true;
}
}
return false;
}
displayColumnFromValue(columnValue: string): DisplayColumn | undefined {
return this.displayColumns.find((column) => column.value === columnValue);
}
displayColumnFn(column?: DisplayColumn): string | undefined {
return column ? column.name : undefined;
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
columnDrop(event: CdkDragDrop<string[]>): void {
const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value;
moveItemInArray(displayColumns, event.previousIndex, event.currentIndex);
this.persistentTableWidgetSettingsForm.get('displayColumns').setValue(displayColumns);
this.persistentTableWidgetSettingsForm.get('displayColumns').markAsDirty();
}
onColumnRemoved(column: string): void {
const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value;
const index = displayColumns.indexOf(column);
if (index > -1) {
displayColumns.splice(index, 1);
this.persistentTableWidgetSettingsForm.get('displayColumns').setValue(displayColumns);
this.persistentTableWidgetSettingsForm.get('displayColumns').markAsDirty();
this.columnsChipList.errorState = !displayColumns.length;
}
}
onColumnInputFocus() {
this.columnInputChange.next(this.columnInput.nativeElement.value);
}
addColumnFromChipInput(event: MatChipInputEvent): void {
const value = event.value;
if ((value || '').trim()) {
const columnName = value.trim().toUpperCase();
const existingColumn = this.displayColumns.find(column => column.name.toUpperCase() === columnName);
if (this.addColumn(existingColumn)) {
this.clearColumnInput('');
}
}
}
columnSelected(event: MatAutocompleteSelectedEvent): void {
this.addColumn(event.option.value);
this.clearColumnInput('');
}
clearColumnInput(value: string = '') {
this.columnInput.nativeElement.value = value;
this.columnInputChange.next(null);
setTimeout(() => {
this.columnInput.nativeElement.blur();
this.columnInput.nativeElement.focus();
}, 0);
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -116,6 +116,9 @@ import {
import { import {
SlideToggleWidgetSettingsComponent SlideToggleWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/slide-toggle-widget-settings.component'; } from '@home/components/widget/lib/settings/control/slide-toggle-widget-settings.component';
import {
PersistentTableWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/persistent-table-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -159,7 +162,8 @@ import {
SwitchRpcSettingsComponent, SwitchRpcSettingsComponent,
RoundSwitchWidgetSettingsComponent, RoundSwitchWidgetSettingsComponent,
SwitchControlWidgetSettingsComponent, SwitchControlWidgetSettingsComponent,
SlideToggleWidgetSettingsComponent SlideToggleWidgetSettingsComponent,
PersistentTableWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -207,7 +211,8 @@ import {
SwitchRpcSettingsComponent, SwitchRpcSettingsComponent,
RoundSwitchWidgetSettingsComponent, RoundSwitchWidgetSettingsComponent,
SwitchControlWidgetSettingsComponent, SwitchControlWidgetSettingsComponent,
SlideToggleWidgetSettingsComponent SlideToggleWidgetSettingsComponent,
PersistentTableWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -243,5 +248,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-doughnut-chart-widget-settings': DoughnutChartWidgetSettingsComponent, 'tb-doughnut-chart-widget-settings': DoughnutChartWidgetSettingsComponent,
'tb-round-switch-widget-settings': RoundSwitchWidgetSettingsComponent, 'tb-round-switch-widget-settings': RoundSwitchWidgetSettingsComponent,
'tb-switch-control-widget-settings': SwitchControlWidgetSettingsComponent, 'tb-switch-control-widget-settings': SwitchControlWidgetSettingsComponent,
'tb-slide-toggle-widget-settings': SlideToggleWidgetSettingsComponent 'tb-slide-toggle-widget-settings': SlideToggleWidgetSettingsComponent,
'tb-persistent-table-widget-settings': PersistentTableWidgetSettingsComponent
}; };

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

@ -3597,7 +3597,19 @@
"message-types": { "message-types": {
"false": "Two-way", "false": "Two-way",
"true": "One-way" "true": "One-way"
} },
"general-settings": "General settings",
"enable-filter": "Enable filter",
"enable-sticky-header": "Display header while scrolling",
"enable-sticky-action": "Display actions column while scrolling",
"display-request-details": "Display request details",
"allow-send-request": "Allow send RPC request",
"allow-delete-request": "Allow delete request",
"columns-settings": "Columns settings",
"display-columns": "Columns to display",
"column": "Column",
"no-columns-found": "No columns found",
"no-columns-matching": "'{{column}}' not found."
}, },
"rpc": { "rpc": {
"value-settings": "Value settings", "value-settings": "Value settings",

Loading…
Cancel
Save