20 changed files with 748 additions and 31 deletions
@ -0,0 +1,43 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { |
||||
|
ScadaSymbolMetadataTagComponent |
||||
|
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component'; |
||||
|
import { ScadaSymbolMetadataComponent } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component'; |
||||
|
import { |
||||
|
ScadaSymbolMetadataTagsComponent |
||||
|
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: |
||||
|
[ |
||||
|
ScadaSymbolMetadataComponent, |
||||
|
ScadaSymbolMetadataTagComponent, |
||||
|
ScadaSymbolMetadataTagsComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule |
||||
|
], |
||||
|
exports: [ |
||||
|
ScadaSymbolMetadataComponent |
||||
|
] |
||||
|
}) |
||||
|
export class ScadaSymbolMetadataComponentsModule { } |
||||
@ -0,0 +1,74 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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 [formGroup]="tagFormGroup" class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel #expansionPanel class="tb-settings"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<div class="tb-scada-symbol-metadata-tag-header"> |
||||
|
<div class="tb-form-row tb-flex"> |
||||
|
<div class="fixed-title-width" translate>scada.tag</div> |
||||
|
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" (click)="$event.stopPropagation()"> |
||||
|
<mat-select required formControlName="tag" placeholder="{{ 'scada.select-tag' | translate }}"> |
||||
|
<mat-option *ngFor="let tag of availableTags" [value]="tag"> |
||||
|
{{ tag }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
(click)="tagRemoved.emit()" |
||||
|
matTooltip="{{ 'scada.remove-tag' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
{{ 'scada.state-render-function' | translate }} |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<tb-js-func formControlName="stateRenderFunction" |
||||
|
[functionArgs]="['element', 'ctx']" |
||||
|
helpId="widget/lib/timeseries/cell_style_fn"> |
||||
|
</tb-js-func> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
{{ 'scada.on-click-action' | translate }} |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<tb-js-func formControlName="clickAction" |
||||
|
[functionArgs]="['event', 'ctx']" |
||||
|
helpId="widget/lib/timeseries/cell_style_fn"> |
||||
|
</tb-js-func> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
.tb-scada-symbol-metadata-tag-header { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
gap: 8px; |
||||
|
padding-right: 8px; |
||||
|
} |
||||
@ -0,0 +1,154 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectorRef, |
||||
|
Component, EventEmitter, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, Output, |
||||
|
SimpleChanges, ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormGroup, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { IotSvgTag } from '@home/components/widget/lib/svg/iot-svg.models'; |
||||
|
import { MatExpansionPanel } from '@angular/material/expansion'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-scada-symbol-metadata-tag', |
||||
|
templateUrl: './scada-symbol-metadata-tag.component.html', |
||||
|
styleUrls: ['./scada-symbol-metadata-tag.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, OnInit, OnChanges { |
||||
|
|
||||
|
@ViewChild('expansionPanel') |
||||
|
expansionPanel: MatExpansionPanel; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
tags: string[]; |
||||
|
|
||||
|
@Output() |
||||
|
tagRemoved = new EventEmitter(); |
||||
|
|
||||
|
availableTags: string[]; |
||||
|
|
||||
|
tagFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
modelValue: IotSvgTag; |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.availableTags = (this.tags || []).concat(); |
||||
|
this.tagFormGroup = this.fb.group({ |
||||
|
tag: [null, [Validators.required]], |
||||
|
stateRenderFunction: [null, []], |
||||
|
clickAction: [null, []] |
||||
|
}); |
||||
|
this.tagFormGroup.valueChanges.subscribe( |
||||
|
() => this.updateModel() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (['tags'].includes(propName)) { |
||||
|
this.availableTags = (this.tags || []).concat(); |
||||
|
if (this.modelValue?.tag && !this.availableTags.includes(this.modelValue?.tag)) { |
||||
|
this.availableTags.push(this.modelValue?.tag); |
||||
|
this.availableTags.sort(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.tagFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.tagFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: IotSvgTag): void { |
||||
|
this.modelValue = value; |
||||
|
if (value?.tag && !(this.tags || []).includes(value?.tag)) { |
||||
|
this.availableTags = (this.tags || []).concat(value.tag).sort(); |
||||
|
} |
||||
|
const clickAction = value?.actions && value?.actions.click ? value.actions.click.actionFunction : null; |
||||
|
this.tagFormGroup.patchValue( |
||||
|
{ |
||||
|
tag: value?.tag, |
||||
|
stateRenderFunction: value?.stateRenderFunction, |
||||
|
clickAction |
||||
|
}, {emitEvent: false} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value = this.tagFormGroup.value; |
||||
|
if (value.tag && !this.modelValue?.tag) { |
||||
|
this.expansionPanel.open(); |
||||
|
} |
||||
|
this.modelValue = { |
||||
|
tag: value.tag, |
||||
|
stateRenderFunction: value.stateRenderFunction |
||||
|
}; |
||||
|
if (value.clickAction) { |
||||
|
this.modelValue.actions = { |
||||
|
click: { |
||||
|
actionFunction: value.clickAction |
||||
|
} |
||||
|
}; |
||||
|
} else { |
||||
|
this.modelValue.actions = null; |
||||
|
} |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-form-panel tb-scada-symbol-metadata-tags"> |
||||
|
<div *ngIf="tagsFormArray().controls.length; else noTags" class="tb-scada-symbol-metadata-tags-list"> |
||||
|
<div *ngFor="let tagControl of tagsFormArray().controls; trackBy: trackByTag; let $index = index; let $last = last"> |
||||
|
<tb-scada-symbol-metadata-tag fxFlex |
||||
|
[formControl]="tagControl" |
||||
|
[tags]="availableTags" |
||||
|
(tagRemoved)="removeTag($index)"> |
||||
|
</tb-scada-symbol-metadata-tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div> |
||||
|
<button type="button" mat-stroked-button color="primary" (click)="addTag()"> |
||||
|
{{ 'scada.add-tag' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #noTags> |
||||
|
<span fxLayoutAlign="start center" class="tb-prompt tb-no-tags">{{ 'scada.no-tags' | translate }}</span> |
||||
|
</ng-template> |
||||
@ -0,0 +1,25 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
.tb-scada-symbol-metadata-tags { |
||||
|
.tb-scada-symbol-metadata-tags-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
.tb-no-tags { |
||||
|
height: 60px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,171 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator |
||||
|
} from '@angular/forms'; |
||||
|
import { IotSvgTag } from '@home/components/widget/lib/svg/iot-svg.models'; |
||||
|
|
||||
|
const tagValidator = (control: AbstractControl): ValidationErrors | null => { |
||||
|
const tag: IotSvgTag = control.value; |
||||
|
if (!tag.tag) { |
||||
|
return { |
||||
|
tag: true |
||||
|
}; |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-scada-symbol-metadata-tags', |
||||
|
templateUrl: './scada-symbol-metadata-tags.component.html', |
||||
|
styleUrls: ['./scada-symbol-metadata-tags.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
tags: string[]; |
||||
|
|
||||
|
availableTags: string[]; |
||||
|
|
||||
|
tagsFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.tagsFormGroup = this.fb.group({ |
||||
|
tags: this.fb.array([]) |
||||
|
}); |
||||
|
this.updateAvailableTags(); |
||||
|
this.tagsFormGroup.valueChanges.subscribe( |
||||
|
() => { |
||||
|
let tags: IotSvgTag[] = this.tagsFormGroup.get('tags').value; |
||||
|
if (tags) { |
||||
|
tags = tags.filter(t => !!t.tag); |
||||
|
} |
||||
|
this.updateAvailableTags(); |
||||
|
this.propagateChange(tags); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (['tags'].includes(propName)) { |
||||
|
this.updateAvailableTags(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.tagsFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.tagsFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: IotSvgTag[] | undefined): void { |
||||
|
const tags= value || []; |
||||
|
this.tagsFormGroup.setControl('tags', this.prepareTagsFormArray(tags), {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
public validate(c: UntypedFormControl) { |
||||
|
const valid = this.tagsFormGroup.valid; |
||||
|
return valid ? null : { |
||||
|
tags: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
tagsFormArray(): UntypedFormArray { |
||||
|
return this.tagsFormGroup.get('tags') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
trackByTag(index: number, tagControl: AbstractControl): any { |
||||
|
return tagControl; |
||||
|
} |
||||
|
|
||||
|
removeTag(index: number) { |
||||
|
(this.tagsFormGroup.get('tags') as UntypedFormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
addTag() { |
||||
|
const tag: IotSvgTag = { |
||||
|
tag: null |
||||
|
}; |
||||
|
const tagsArray = this.tagsFormGroup.get('tags') as UntypedFormArray; |
||||
|
const tagControl = this.fb.control(tag, [tagValidator]); |
||||
|
tagsArray.push(tagControl); |
||||
|
} |
||||
|
|
||||
|
private prepareTagsFormArray(tags: IotSvgTag[] | undefined): UntypedFormArray { |
||||
|
const tagsControls: Array<AbstractControl> = []; |
||||
|
if (tags) { |
||||
|
tags.forEach((tag) => { |
||||
|
tagsControls.push(this.fb.control(tag, [tagValidator])); |
||||
|
}); |
||||
|
} |
||||
|
return this.fb.array(tagsControls); |
||||
|
} |
||||
|
|
||||
|
private updateAvailableTags() { |
||||
|
const svgTags: IotSvgTag[] = this.tagsFormGroup.get('tags').value; |
||||
|
const usedTags = (svgTags || []).filter(t => !!t.tag).map(t => t.tag); |
||||
|
this.availableTags = (this.tags || []).filter(t => !usedTags.includes(t)); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue