Showing
34 changed files
with
1151 additions
and
360 deletions
... | ... | @@ -3767,9 +3767,9 @@ |
3767 | 3767 | } |
3768 | 3768 | }, |
3769 | 3769 | "date-fns": { |
3770 | - "version": "2.5.0", | |
3771 | - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.5.0.tgz", | |
3772 | - "integrity": "sha512-I6Tkis01//nRcmvMQw/MRE1HAtcuA5Ie6jGPb8bJZJub7494LGOObqkV3ParnsSVviAjk5C8mNKDqYVBzCopWg==" | |
3770 | + "version": "2.1.0", | |
3771 | + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.1.0.tgz", | |
3772 | + "integrity": "sha512-eKeLk3sLCnxB/0PN4t1+zqDtSs4jb4mXRSTZ2okmx/myfWyDqeO4r5nnmA5LClJiCwpuTMeK2v5UQPuE4uMaxA==" | |
3773 | 3773 | }, |
3774 | 3774 | "date-format": { |
3775 | 3775 | "version": "2.1.0", |
... | ... | @@ -4164,9 +4164,9 @@ |
4164 | 4164 | "dev": true |
4165 | 4165 | }, |
4166 | 4166 | "electron-to-chromium": { |
4167 | - "version": "1.3.284", | |
4168 | - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.284.tgz", | |
4169 | - "integrity": "sha512-duOA4IWKH4R8ttiE8q/7xfg6eheRvMKlGqOOcGlDukdHEDJ26Wf7cMrCiK9Am11mswR6E/a23jXVA4UPDthTIw==", | |
4167 | + "version": "1.3.285", | |
4168 | + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.285.tgz", | |
4169 | + "integrity": "sha512-DYR9KW723sUbGK++DCmCmM95AbNXT4Q0tlCFMcYijFjayhuDqlGYR68OemlP8MJj0gjkwdeItIUfd0oLCgw+4A==", | |
4170 | 4170 | "dev": true |
4171 | 4171 | }, |
4172 | 4172 | "elliptic": { | ... | ... |
... | ... | @@ -41,7 +41,7 @@ |
41 | 41 | "base64-js": "^1.3.1", |
42 | 42 | "compass-sass-mixins": "^0.12.7", |
43 | 43 | "core-js": "^3.1.4", |
44 | - "date-fns": "^2.5.0", | |
44 | + "date-fns": "2.1.0", | |
45 | 45 | "deep-equal": "^1.0.1", |
46 | 46 | "flot": "git://github.com/thingsboard/flot.git#0.9-work", |
47 | 47 | "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", | ... | ... |
... | ... | @@ -21,7 +21,7 @@ |
21 | 21 | #entityAliasInput |
22 | 22 | formControlName="entityAlias" |
23 | 23 | (focusin)="onFocus()" |
24 | - [required]="required" | |
24 | + [required]="tbRequired" | |
25 | 25 | (keydown)="entityAliasEnter($event)" |
26 | 26 | (keypress)="entityAliasEnter($event)" |
27 | 27 | [matAutocomplete]="entityAliasAutocomplete"> |
... | ... | @@ -54,7 +54,7 @@ |
54 | 54 | </div> |
55 | 55 | </mat-option> |
56 | 56 | </mat-autocomplete> |
57 | - <mat-error *ngIf="!modelValue && required"> | |
57 | + <mat-error *ngIf="!modelValue && tbRequired"> | |
58 | 58 | {{ 'entity.alias-required' | translate }} |
59 | 59 | </mat-error> |
60 | 60 | </mat-form-field> | ... | ... |
... | ... | @@ -75,11 +75,11 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, |
75 | 75 | |
76 | 76 | |
77 | 77 | private requiredValue: boolean; |
78 | - get required(): boolean { | |
78 | + get tbRequired(): boolean { | |
79 | 79 | return this.requiredValue; |
80 | 80 | } |
81 | 81 | @Input() |
82 | - set required(value: boolean) { | |
82 | + set tbRequired(value: boolean) { | |
83 | 83 | this.requiredValue = coerceBooleanProperty(value); |
84 | 84 | } |
85 | 85 | |
... | ... | @@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, |
151 | 151 | |
152 | 152 | isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
153 | 153 | const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
154 | - const customErrorState = this.required && !this.modelValue; | |
154 | + const customErrorState = this.tbRequired && !this.modelValue; | |
155 | 155 | return originalErrorState || customErrorState; |
156 | 156 | } |
157 | 157 | ... | ... |
... | ... | @@ -16,9 +16,8 @@ |
16 | 16 | |
17 | 17 | --> |
18 | 18 | <mat-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}" |
19 | - [formGroup]="dataKeyFormGroup" | |
20 | 19 | class="tb-datakey-config"> |
21 | - <mat-tab label="{{ 'datakey.settings' | translate }}"> | |
20 | + <mat-tab [formGroup]="dataKeyFormGroup" label="{{ 'datakey.settings' | translate }}"> | |
22 | 21 | <div class="mat-content mat-padding" fxLayout="column"> |
23 | 22 | <mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function"> |
24 | 23 | <mat-label>{{ 'entity.key' | translate }}</mat-label> |
... | ... | @@ -94,12 +93,10 @@ |
94 | 93 | </section> |
95 | 94 | </div> |
96 | 95 | </mat-tab> |
97 | - <mat-tab label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced"> | |
96 | + <mat-tab [formGroup]="dataKeySettingsFormGroup" label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced"> | |
98 | 97 | <div class="mat-content mat-padding" fxLayout="column"> |
99 | 98 | <div style="overflow: auto;"> |
100 | 99 | <tb-json-form |
101 | - [schema]="dataKeySchema" | |
102 | - [form]="dataKeyForm" | |
103 | 100 | formControlName="settings"> |
104 | 101 | </tb-json-form> |
105 | 102 | </div> | ... | ... |
... | ... | @@ -39,6 +39,7 @@ import { Observable, of } from 'rxjs'; |
39 | 39 | import { map, mergeMap, tap } from 'rxjs/operators'; |
40 | 40 | import { alarmFields } from '@shared/models/alarm.models'; |
41 | 41 | import { JsFuncComponent } from '@shared/components/js-func.component'; |
42 | +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; | |
42 | 43 | |
43 | 44 | @Component({ |
44 | 45 | selector: 'tb-data-key-config', |
... | ... | @@ -77,15 +78,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con |
77 | 78 | |
78 | 79 | displayAdvanced = false; |
79 | 80 | |
80 | - dataKeySchema: any; | |
81 | - dataKeyForm: any; | |
82 | - | |
83 | 81 | private modelValue: DataKey; |
84 | 82 | |
85 | 83 | private propagateChange = null; |
86 | 84 | |
87 | 85 | public dataKeyFormGroup: FormGroup; |
88 | 86 | |
87 | + public dataKeySettingsFormGroup: FormGroup; | |
88 | + | |
89 | + private dataKeySettingsData: JsonFormComponentData; | |
90 | + | |
89 | 91 | private alarmKeys: Array<DataKey>; |
90 | 92 | |
91 | 93 | filteredKeys: Observable<Array<string>>; |
... | ... | @@ -112,8 +114,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con |
112 | 114 | } |
113 | 115 | if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) { |
114 | 116 | this.displayAdvanced = true; |
115 | - this.dataKeySchema = this.dataKeySettingsSchema.schema; | |
116 | - this.dataKeyForm = this.dataKeySettingsSchema.form || ['*']; | |
117 | + this.dataKeySettingsData = { | |
118 | + schema: this.dataKeySettingsSchema.schema, | |
119 | + form: this.dataKeySettingsSchema.form || ['*'] | |
120 | + }; | |
121 | + this.dataKeySettingsFormGroup = this.fb.group({ | |
122 | + settings: [null, []] | |
123 | + }); | |
124 | + this.dataKeySettingsFormGroup.valueChanges.subscribe(() => { | |
125 | + this.updateModel(); | |
126 | + }); | |
117 | 127 | } |
118 | 128 | this.dataKeyFormGroup = this.fb.group({ |
119 | 129 | name: [null, []], |
... | ... | @@ -123,8 +133,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con |
123 | 133 | decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], |
124 | 134 | funcBody: [null, []], |
125 | 135 | usePostProcessing: [null, []], |
126 | - postFuncBody: [null, []], | |
127 | - settings: [null, []] | |
136 | + postFuncBody: [null, []] | |
128 | 137 | }); |
129 | 138 | |
130 | 139 | this.dataKeyFormGroup.valueChanges.subscribe(() => { |
... | ... | @@ -162,10 +171,19 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con |
162 | 171 | this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); |
163 | 172 | this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []); |
164 | 173 | this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); |
174 | + if (this.displayAdvanced) { | |
175 | + this.dataKeySettingsData.model = this.modelValue.settings; | |
176 | + this.dataKeySettingsFormGroup.patchValue({ | |
177 | + settings: this.dataKeySettingsData | |
178 | + }, {emitEvent: false}); | |
179 | + } | |
165 | 180 | } |
166 | 181 | |
167 | 182 | private updateModel() { |
168 | 183 | this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; |
184 | + if (this.displayAdvanced) { | |
185 | + this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model; | |
186 | + } | |
169 | 187 | this.propagateChange(this.modelValue); |
170 | 188 | } |
171 | 189 | |
... | ... | @@ -222,6 +240,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con |
222 | 240 | } |
223 | 241 | }; |
224 | 242 | } |
243 | + if (this.displayAdvanced && !this.dataKeySettingsFormGroup.valid) { | |
244 | + return { | |
245 | + dataKeySettings: { | |
246 | + valid: false | |
247 | + } | |
248 | + }; | |
249 | + } | |
225 | 250 | return null; |
226 | 251 | } |
227 | 252 | } | ... | ... |
... | ... | @@ -14,15 +14,6 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | |
17 | -@mixin tb-checkered-bg() { | |
18 | - background-color: #fff; | |
19 | - background-image: | |
20 | - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), | |
21 | - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); | |
22 | - background-position: 0 0, 4px 4px; | |
23 | - background-size: 8px 8px; | |
24 | -} | |
25 | - | |
26 | 17 | :host { |
27 | 18 | .mat-chip.mat-standard-chip { |
28 | 19 | .tb-attribute-chip { |
... | ... | @@ -56,27 +47,6 @@ |
56 | 47 | color: inherit; |
57 | 48 | opacity: inherit; |
58 | 49 | } |
59 | - | |
60 | - .tb-color-preview { | |
61 | - cursor: pointer; | |
62 | - box-sizing: border-box; | |
63 | - position: relative; | |
64 | - width: 24px; | |
65 | - min-width: 24px; | |
66 | - height: 24px; | |
67 | - overflow: hidden; | |
68 | - content: ""; | |
69 | - border: 2px solid #fff; | |
70 | - border-radius: 50%; | |
71 | - box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); | |
72 | - | |
73 | - @include tb-checkered-bg(); | |
74 | - | |
75 | - .tb-color-result { | |
76 | - width: 100%; | |
77 | - height: 100%; | |
78 | - } | |
79 | - } | |
80 | 50 | } |
81 | 51 | } |
82 | 52 | } | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | </mat-checkbox> |
32 | 32 | </div> |
33 | 33 | <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> |
34 | - <span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span> | |
34 | + <span [ngClass]="{'tb-disabled-label': dataSettings.get('useDashboardTimewindow').value}" translate style="padding-right: 8px;">widget-config.timewindow</span> | |
35 | 35 | <tb-timewindow asButton="true" |
36 | 36 | aggregation="{{ widgetType === widgetTypes.timeseries }}" |
37 | 37 | fxFlex formControlName="timewindow"></tb-timewindow> |
... | ... | @@ -64,12 +64,12 @@ |
64 | 64 | <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc && |
65 | 65 | widgetType !== widgetTypes.alarm && |
66 | 66 | widgetType !== widgetTypes.static && |
67 | - isDataEnabled" [expanded]="true"> | |
67 | + modelValue?.isDataEnabled" [expanded]="true"> | |
68 | 68 | <mat-expansion-panel-header> |
69 | 69 | <mat-panel-title fxLayout="column"> |
70 | 70 | <div class="tb-panel-title" translate>widget-config.datasources</div> |
71 | - <div *ngIf="typeParameters && typeParameters.maxDatasources > -1" | |
72 | - class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: typeParameters.maxDatasources} }}</div> | |
71 | + <div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1" | |
72 | + class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div> | |
73 | 73 | </mat-panel-title> |
74 | 74 | </mat-expansion-panel-header> |
75 | 75 | <div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate"> |
... | ... | @@ -90,7 +90,7 @@ |
90 | 90 | <div style="overflow: auto; padding-bottom: 15px;"> |
91 | 91 | <div fxFlex fxLayout="row" fxLayoutAlign="start center" |
92 | 92 | formArrayName="datasources" |
93 | - *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index"> | |
93 | + *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index;"> | |
94 | 94 | <span fxFlex="5">{{$index + 1}}.</span> |
95 | 95 | <div [formGroupName]="$index" class="mat-elevation-z4" fxFlex |
96 | 96 | fxLayout="row" |
... | ... | @@ -120,7 +120,7 @@ |
120 | 120 | </ng-template> |
121 | 121 | <ng-template [ngSwitchCase]="datasourceType.entity"> |
122 | 122 | <tb-entity-alias-select |
123 | - [required]="datasourceControl.get('type').value === datasourceType.entity" | |
123 | + [tbRequired]="datasourceControl.get('type').value === datasourceType.entity" | |
124 | 124 | [aliasController]="aliasController" |
125 | 125 | formControlName="entityAliasId" |
126 | 126 | [callbacks]="widgetConfigCallbacks"> |
... | ... | @@ -130,25 +130,15 @@ |
130 | 130 | <tb-data-keys class="tb-data-keys" fxFlex |
131 | 131 | [widgetType]="widgetType" |
132 | 132 | [datasourceType]="datasourceControl.get('type').value" |
133 | - [maxDataKeys]="typeParameters?.maxDataKeys" | |
134 | - [optDataKeys]="typeParameters?.dataKeysOptional" | |
133 | + [maxDataKeys]="modelValue?.typeParameters?.maxDataKeys" | |
134 | + [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional" | |
135 | 135 | [aliasController]="aliasController" |
136 | - [datakeySettingsSchema]="dataKeySettingsSchema" | |
136 | + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" | |
137 | 137 | [callbacks]="widgetConfigCallbacks" |
138 | 138 | [entityAliasId]="datasourceControl.get('entityAliasId').value" |
139 | 139 | formControlName="dataKeys"> |
140 | 140 | </tb-data-keys> |
141 | 141 | </section> |
142 | - <!--tb-datasource fxFlex | |
143 | - [widgetType]="widgetType" | |
144 | - [maxDataKeys]="typeParameters?.maxDataKeys" | |
145 | - [optDataKeys]="typeParameters?.dataKeysOptional" | |
146 | - [aliasController]="aliasController" | |
147 | - [functionsOnly]="functionsOnly" | |
148 | - [datakeySettingsSchema]="dataKeySettingsSchema" | |
149 | - [callbacks]="widgetConfigCallbacks" | |
150 | - [formControl]="datasourceControl" | |
151 | - ></tb-datasource--> | |
152 | 142 | <button [disabled]="isLoading$ | async" |
153 | 143 | mat-button mat-icon-button color="primary" |
154 | 144 | style="min-width: 40px;" |
... | ... | @@ -164,8 +154,8 @@ |
164 | 154 | <div fxFlex fxLayout="row" fxLayoutAlign="start center"> |
165 | 155 | <button [disabled]="isLoading$ | async" |
166 | 156 | mat-button mat-raised-button color="primary" |
167 | - [fxShow]="typeParameters && | |
168 | - (typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < typeParameters.maxDatasources)" | |
157 | + [fxShow]="modelValue?.typeParameters && | |
158 | + (modelValue?.typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < modelValue?.typeParameters.maxDatasources)" | |
169 | 159 | (click)="addDatasource()" |
170 | 160 | matTooltip="{{ 'widget-config.add-datasource' | translate }}" |
171 | 161 | matTooltipPosition="above"> |
... | ... | @@ -175,7 +165,7 @@ |
175 | 165 | </div> |
176 | 166 | </mat-expansion-panel> |
177 | 167 | <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc && |
178 | - isDataEnabled" [expanded]="true"> | |
168 | + modelValue?.isDataEnabled" [expanded]="true"> | |
179 | 169 | <mat-expansion-panel-header> |
180 | 170 | <mat-panel-title> |
181 | 171 | {{ 'widget-config.target-device' | translate }} |
... | ... | @@ -183,7 +173,7 @@ |
183 | 173 | </mat-expansion-panel-header> |
184 | 174 | <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;"> |
185 | 175 | <tb-entity-alias-select fxFlex |
186 | - [required]="!widgetEditMode" | |
176 | + [tbRequired]="!widgetEditMode" | |
187 | 177 | [aliasController]="aliasController" |
188 | 178 | [allowedEntityTypes]="[entityTypes.DEVICE]" |
189 | 179 | [callbacks]="widgetConfigCallbacks" |
... | ... | @@ -193,6 +183,14 @@ |
193 | 183 | </mat-expansion-panel> |
194 | 184 | </div> |
195 | 185 | </mat-tab> |
196 | - <mat-tab label="{{ 'widget-config.settings' | translate }}"> | |
186 | + <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}"> | |
187 | + <div [formGroup]="advancedSettings" class="mat-content mat-padding tb-advanced-widget-config" | |
188 | + fxLayout="column"> | |
189 | + <tb-json-form | |
190 | + formControlName="settings"> | |
191 | + </tb-json-form> | |
192 | + </div> | |
193 | + </mat-tab> | |
194 | + <mat-tab label="{{ 'widget-config.actions' | translate }}"> | |
197 | 195 | </mat-tab> |
198 | 196 | </mat-tab-group> | ... | ... |
... | ... | @@ -25,7 +25,6 @@ import { |
25 | 25 | LegendConfig, |
26 | 26 | WidgetActionDescriptor, |
27 | 27 | WidgetActionSource, |
28 | - WidgetConfigSettings, | |
29 | 28 | widgetType, |
30 | 29 | WidgetTypeParameters |
31 | 30 | } from '@shared/models/widget.models'; |
... | ... | @@ -38,7 +37,7 @@ import { |
38 | 37 | FormGroup, |
39 | 38 | NG_VALIDATORS, |
40 | 39 | NG_VALUE_ACCESSOR, |
41 | - Validator, | |
40 | + Validator, ValidatorFn, | |
42 | 41 | Validators |
43 | 42 | } from '@angular/forms'; |
44 | 43 | import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
... | ... | @@ -59,6 +58,16 @@ import { |
59 | 58 | import { tap, mergeMap, map, catchError } from 'rxjs/operators'; |
60 | 59 | import { MatDialog } from '@angular/material/dialog'; |
61 | 60 | import { EntityService } from '@core/http/entity.service'; |
61 | +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; | |
62 | + | |
63 | +const emptySettingsSchema = { | |
64 | + type: 'object', | |
65 | + properties: {} | |
66 | +}; | |
67 | +const emptySettingsGroupInfoes = []; | |
68 | +const defaultSettingsForm = [ | |
69 | + '*' | |
70 | +]; | |
62 | 71 | |
63 | 72 | @Component({ |
64 | 73 | selector: 'tb-widget-config', |
... | ... | @@ -89,27 +98,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
89 | 98 | forceExpandDatasources: boolean; |
90 | 99 | |
91 | 100 | @Input() |
92 | - isDataEnabled: boolean; | |
93 | - | |
94 | - @Input() | |
95 | - typeParameters: WidgetTypeParameters; | |
96 | - | |
97 | - @Input() | |
98 | - actionSources: {[key: string]: WidgetActionSource}; | |
99 | - | |
100 | - @Input() | |
101 | 101 | aliasController: IAliasController; |
102 | 102 | |
103 | 103 | @Input() |
104 | 104 | entityAliases: EntityAliases; |
105 | 105 | |
106 | 106 | @Input() |
107 | - widgetSettingsSchema: any; | |
108 | - | |
109 | - @Input() | |
110 | - dataKeySettingsSchema: any; | |
111 | - | |
112 | - @Input() | |
113 | 107 | functionsOnly: boolean; |
114 | 108 | |
115 | 109 | @Input() disabled: boolean; |
... | ... | @@ -149,37 +143,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
149 | 143 | legendConfig: LegendConfig; |
150 | 144 | actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; |
151 | 145 | alarmSource: Datasource; |
152 | - settings: WidgetConfigSettings; | |
153 | 146 | mobileOrder: number; |
154 | 147 | mobileHeight: number; |
155 | 148 | |
156 | - emptySettingsSchema = { | |
157 | - type: 'object', | |
158 | - properties: {} | |
159 | - }; | |
160 | - | |
161 | - emptySettingsGroupInfoes = []; | |
162 | - | |
163 | - defaultSettingsForm = [ | |
164 | - '*' | |
165 | - ]; | |
166 | - | |
167 | - currentSettingsSchema = deepClone(this.emptySettingsSchema); | |
168 | - | |
169 | - currentSettings: WidgetConfigSettings = {}; | |
170 | - currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes); | |
171 | - | |
172 | - currentSettingsForm: any; | |
173 | - | |
174 | 149 | private modelValue: WidgetConfigComponentData; |
175 | 150 | |
176 | 151 | private propagateChange = null; |
177 | 152 | |
178 | 153 | public dataSettings: FormGroup; |
179 | 154 | public targetDeviceSettings: FormGroup; |
155 | + public advancedSettings: FormGroup; | |
180 | 156 | |
181 | 157 | private dataSettingsChangesSubscription: Subscription; |
182 | 158 | private targetDeviceSettingsSubscription: Subscription; |
159 | + private advancedSettingsSubscription: Subscription; | |
183 | 160 | |
184 | 161 | constructor(protected store: Store<AppState>, |
185 | 162 | private utils: UtilsService, |
... | ... | @@ -207,6 +184,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
207 | 184 | this.targetDeviceSettingsSubscription.unsubscribe(); |
208 | 185 | this.targetDeviceSettingsSubscription = null; |
209 | 186 | } |
187 | + if (this.advancedSettingsSubscription) { | |
188 | + this.advancedSettingsSubscription.unsubscribe(); | |
189 | + this.advancedSettingsSubscription = null; | |
190 | + } | |
210 | 191 | } |
211 | 192 | |
212 | 193 | private createChangeSubscriptions() { |
... | ... | @@ -216,11 +197,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
216 | 197 | this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( |
217 | 198 | () => this.updateTargetDeviceSettings() |
218 | 199 | ); |
200 | + this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe( | |
201 | + () => this.updateAdvancedSettings() | |
202 | + ); | |
219 | 203 | } |
220 | 204 | |
221 | 205 | private buildForms() { |
222 | 206 | this.dataSettings = this.fb.group({}); |
223 | 207 | this.targetDeviceSettings = this.fb.group({}); |
208 | + this.advancedSettings = this.fb.group({}); | |
224 | 209 | if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { |
225 | 210 | this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); |
226 | 211 | this.dataSettings.addControl('displayTimewindow', this.fb.control(null)); |
... | ... | @@ -240,7 +225,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
240 | 225 | [Validators.required, Validators.min(1)])); |
241 | 226 | } |
242 | 227 | } |
243 | - if (this.isDataEnabled) { | |
228 | + if (this.modelValue.isDataEnabled) { | |
244 | 229 | if (this.widgetType !== widgetType.rpc && |
245 | 230 | this.widgetType !== widgetType.alarm && |
246 | 231 | this.widgetType !== widgetType.static) { |
... | ... | @@ -252,6 +237,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
252 | 237 | this.widgetEditMode ? [] : [Validators.required])); |
253 | 238 | } |
254 | 239 | } |
240 | + this.advancedSettings.addControl('settings', | |
241 | + this.fb.control(null, [])); | |
255 | 242 | } |
256 | 243 | |
257 | 244 | registerOnChange(fn: any): void { |
... | ... | @@ -323,7 +310,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
323 | 310 | { timewindow: config.timewindow }, {emitEvent: false} |
324 | 311 | ); |
325 | 312 | } |
326 | - if (this.isDataEnabled) { | |
313 | + if (this.modelValue.isDataEnabled) { | |
327 | 314 | if (this.widgetType !== widgetType.rpc && |
328 | 315 | this.widgetType !== widgetType.alarm && |
329 | 316 | this.widgetType !== widgetType.static) { |
... | ... | @@ -366,9 +353,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
366 | 353 | } |
367 | 354 | } |
368 | 355 | } |
369 | - this.settings = config.settings; | |
370 | 356 | |
371 | - this.updateSchemaForm(); | |
357 | + this.updateSchemaForm(config.settings); | |
372 | 358 | |
373 | 359 | if (layout) { |
374 | 360 | this.mobileOrder = layout.mobileOrder; |
... | ... | @@ -383,7 +369,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
383 | 369 | } |
384 | 370 | |
385 | 371 | private buildDatasourceForm(datasource?: Datasource): AbstractControl { |
386 | - const dataKeysRequired = !this.typeParameters || !this.typeParameters.dataKeysOptional; | |
372 | + const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; | |
387 | 373 | const datasourceFormGroup = this.fb.group( |
388 | 374 | { |
389 | 375 | type: [datasource ? datasource.type : null, [Validators.required]], |
... | ... | @@ -402,18 +388,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
402 | 388 | return datasourceFormGroup; |
403 | 389 | } |
404 | 390 | |
405 | - private updateSchemaForm() { | |
406 | - if (this.widgetSettingsSchema && this.widgetSettingsSchema.schema) { | |
407 | - this.currentSettingsSchema = this.widgetSettingsSchema.schema; | |
408 | - this.currentSettingsForm = this.widgetSettingsSchema.form || deepClone(this.defaultSettingsForm); | |
409 | - this.currentSettingsGroupInfoes = this.widgetSettingsSchema.groupInfoes; | |
410 | - this.currentSettings = this.settings; | |
391 | + private updateSchemaForm(settings?: any) { | |
392 | + const widgetSettingsFormData: JsonFormComponentData = {}; | |
393 | + if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) { | |
394 | + widgetSettingsFormData.schema = this.modelValue.settingsSchema.schema; | |
395 | + widgetSettingsFormData.form = this.modelValue.settingsSchema.form || deepClone(defaultSettingsForm); | |
396 | + widgetSettingsFormData.groupInfoes = this.modelValue.settingsSchema.groupInfoes; | |
397 | + widgetSettingsFormData.model = settings; | |
411 | 398 | } else { |
412 | - this.currentSettingsForm = deepClone(this.defaultSettingsForm); | |
413 | - this.currentSettingsSchema = deepClone(this.emptySettingsSchema); | |
414 | - this.currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes); | |
415 | - this.currentSettings = {}; | |
399 | + widgetSettingsFormData.schema = deepClone(emptySettingsSchema); | |
400 | + widgetSettingsFormData.form = deepClone(defaultSettingsForm); | |
401 | + widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes); | |
402 | + widgetSettingsFormData.model = {}; | |
416 | 403 | } |
404 | + this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false}); | |
417 | 405 | } |
418 | 406 | |
419 | 407 | private updateDataSettings() { |
... | ... | @@ -439,8 +427,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
439 | 427 | } |
440 | 428 | } |
441 | 429 | |
430 | + private updateAdvancedSettings() { | |
431 | + if (this.modelValue) { | |
432 | + if (this.modelValue.config) { | |
433 | + const settings = this.advancedSettings.get('settings').value.model; | |
434 | + this.modelValue.config.settings = settings; | |
435 | + } | |
436 | + this.propagateChange(this.modelValue); | |
437 | + } | |
438 | + } | |
439 | + | |
442 | 440 | public displayAdvanced(): boolean { |
443 | - return this.widgetSettingsSchema && this.widgetSettingsSchema.schema; | |
441 | + return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema; | |
444 | 442 | } |
445 | 443 | |
446 | 444 | public removeDatasource(index: number) { |
... | ... | @@ -450,7 +448,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
450 | 448 | public addDatasource() { |
451 | 449 | let newDatasource: Datasource; |
452 | 450 | if (this.functionsOnly) { |
453 | - newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); | |
451 | + newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema)); | |
454 | 452 | newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)]; |
455 | 453 | } else { |
456 | 454 | newDatasource = { type: DatasourceType.entity, |
... | ... | @@ -489,8 +487,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
489 | 487 | result.funcBody = 'return prevValue + 1;'; |
490 | 488 | } |
491 | 489 | } |
492 | - if (isDefined(this.dataKeySettingsSchema.schema)) { | |
493 | - result.settings = this.utils.generateObjectFromJsonSchema(this.dataKeySettingsSchema.schema); | |
490 | + if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) { | |
491 | + result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema); | |
494 | 492 | } |
495 | 493 | return result; |
496 | 494 | } |
... | ... | @@ -605,9 +603,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
605 | 603 | valid: false |
606 | 604 | } |
607 | 605 | }; |
606 | + } else if (!this.advancedSettings.valid) { | |
607 | + return { | |
608 | + advancedSettings: { | |
609 | + valid: false | |
610 | + } | |
611 | + }; | |
608 | 612 | } else { |
609 | 613 | const config = this.modelValue.config; |
610 | - if (this.widgetType === widgetType.rpc && this.isDataEnabled) { | |
614 | + if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) { | |
611 | 615 | if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) { |
612 | 616 | return { |
613 | 617 | targetDeviceAliasIds: { |
... | ... | @@ -615,7 +619,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
615 | 619 | } |
616 | 620 | }; |
617 | 621 | } |
618 | - } else if (this.widgetType === widgetType.alarm && this.isDataEnabled) { | |
622 | + } else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) { | |
619 | 623 | if (!config.alarmSource) { |
620 | 624 | return { |
621 | 625 | alarmSource: { |
... | ... | @@ -623,7 +627,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
623 | 627 | } |
624 | 628 | }; |
625 | 629 | } |
626 | - } else if (this.widgetType !== widgetType.static && this.isDataEnabled) { | |
630 | + } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) { | |
627 | 631 | if (!config.datasources || !config.datasources.length) { |
628 | 632 | return { |
629 | 633 | datasources: { | ... | ... |
... | ... | @@ -22,7 +22,6 @@ import { |
22 | 22 | WidgetActionDescriptor, |
23 | 23 | WidgetActionSource, |
24 | 24 | WidgetConfig, |
25 | - WidgetConfigSettings, | |
26 | 25 | WidgetControllerDescriptor, |
27 | 26 | WidgetType, |
28 | 27 | widgetType, |
... | ... | @@ -74,7 +73,7 @@ export interface WidgetContext { |
74 | 73 | isMobile?: boolean; |
75 | 74 | dashboard?: IDashboardComponent; |
76 | 75 | widgetConfig?: WidgetConfig; |
77 | - settings?: WidgetConfigSettings; | |
76 | + settings?: any; | |
78 | 77 | units?: string; |
79 | 78 | decimals?: number; |
80 | 79 | subscriptions?: {[id: string]: IWidgetSubscription}; |
... | ... | @@ -122,6 +121,11 @@ export interface WidgetConfigComponentData { |
122 | 121 | config: WidgetConfig; |
123 | 122 | layout: WidgetLayout; |
124 | 123 | widgetType: widgetType; |
124 | + typeParameters: WidgetTypeParameters; | |
125 | + actionSources: {[key: string]: WidgetActionSource}; | |
126 | + isDataEnabled: boolean; | |
127 | + settingsSchema: any; | |
128 | + dataKeySettingsSchema: any; | |
125 | 129 | } |
126 | 130 | |
127 | 131 | export const MissingWidgetType: WidgetInfo = { | ... | ... |
... | ... | @@ -172,7 +172,7 @@ |
172 | 172 | [opened]="isEditingWidget" |
173 | 173 | mode="over" |
174 | 174 | position="end"> |
175 | - <tb-details-panel fxFlex | |
175 | + <tb-details-panel *ngIf="isEditingWidget" fxFlex | |
176 | 176 | headerTitle="{{editingWidget?.config.title}}" |
177 | 177 | headerSubtitle="{{ editingWidgetSubtitle }}" |
178 | 178 | [isReadOnly]="false" |
... | ... | @@ -180,20 +180,17 @@ |
180 | 180 | (closeDetails)="onEditWidgetClosed()" |
181 | 181 | (toggleDetailsEditMode)="onRevertWidgetEdit()" |
182 | 182 | (applyDetails)="saveWidget()" |
183 | - [theForm]="widgetForm"> | |
183 | + [theForm]="tbEditWidget.widgetForm"> | |
184 | 184 | <div class="details-buttons"> |
185 | 185 | <div [tb-help]="helpLinkIdForWidgetType()"></div> |
186 | 186 | </div> |
187 | - <form #widgetForm="ngForm" [formGroup]="editingWidgetFormGroup"> | |
188 | - <tb-edit-widget *ngIf="isEditingWidget" | |
189 | - [dashboard]="dashboard" | |
190 | - [aliasController]="dashboardCtx.aliasController" | |
191 | - [widgetEditMode]="widgetEditMode" | |
192 | - [widget]="editingWidget" | |
193 | - [widgetLayout]="editingWidgetLayout" | |
194 | - [widgetFormGroup]="editingWidgetFormGroup"> | |
195 | - </tb-edit-widget> | |
196 | - </form> | |
187 | + <tb-edit-widget #tbEditWidget | |
188 | + [dashboard]="dashboard" | |
189 | + [aliasController]="dashboardCtx.aliasController" | |
190 | + [widgetEditMode]="widgetEditMode" | |
191 | + [widget]="editingWidget" | |
192 | + [widgetLayout]="editingWidgetLayout"> | |
193 | + </tb-edit-widget> | |
197 | 194 | </tb-details-panel> |
198 | 195 | </mat-drawer> |
199 | 196 | </mat-drawer-container> | ... | ... |
... | ... | @@ -71,6 +71,7 @@ import { |
71 | 71 | EntityAliasesDialogData |
72 | 72 | } from '@home/components/alias/entity-aliases-dialog.component'; |
73 | 73 | import { EntityAliases } from '@app/shared/models/alias.models'; |
74 | +import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component'; | |
74 | 75 | |
75 | 76 | @Component({ |
76 | 77 | selector: 'tb-dashboard-page', |
... | ... | @@ -109,7 +110,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
109 | 110 | editingWidgetLayoutOriginal: WidgetLayout = null; |
110 | 111 | editingWidgetSubtitle: string = null; |
111 | 112 | editingLayoutCtx: DashboardPageLayoutContext = null; |
112 | - editingWidgetFormGroup: FormGroup; | |
113 | 113 | |
114 | 114 | thingsboardVersion: string = env.tbVersion; |
115 | 115 | |
... | ... | @@ -188,6 +188,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
188 | 188 | set rightLayoutOpened(rightLayoutOpened: boolean) { |
189 | 189 | } |
190 | 190 | |
191 | + @ViewChild('tbEditWidget', {static: false}) editWidgetComponent: EditWidgetComponent; | |
192 | + | |
191 | 193 | constructor(protected store: Store<AppState>, |
192 | 194 | @Inject(WINDOW) private window: Window, |
193 | 195 | private breakpointObserver: BreakpointObserver, |
... | ... | @@ -205,10 +207,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
205 | 207 | private dialog: MatDialog) { |
206 | 208 | super(store); |
207 | 209 | |
208 | - this.editingWidgetFormGroup = this.fb.group({ | |
209 | - widgetConfig: [null] | |
210 | - }); | |
211 | - | |
212 | 210 | this.rxSubscriptions.push(this.route.data.subscribe( |
213 | 211 | (data) => { |
214 | 212 | this.init(data); |
... | ... | @@ -630,15 +628,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
630 | 628 | } |
631 | 629 | |
632 | 630 | onRevertWidgetEdit() { |
633 | - if (this.editingWidgetFormGroup.dirty) { | |
634 | - this.editingWidgetFormGroup.markAsPristine(); | |
631 | + if (this.editWidgetComponent.widgetFormGroup.dirty) { | |
632 | + this.editWidgetComponent.widgetFormGroup.markAsPristine(); | |
635 | 633 | this.editingWidget = deepClone(this.editingWidgetOriginal); |
636 | 634 | this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal); |
637 | 635 | } |
638 | 636 | } |
639 | 637 | |
640 | 638 | saveWidget() { |
641 | - this.editingWidgetFormGroup.markAsPristine(); | |
639 | + this.editWidgetComponent.widgetFormGroup.markAsPristine(); | |
642 | 640 | const widget = deepClone(this.editingWidget); |
643 | 641 | const widgetLayout = deepClone(this.editingWidgetLayout); |
644 | 642 | const id = this.editingWidgetOriginal.id; | ... | ... |
... | ... | @@ -15,14 +15,9 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<form [formGroup]="widgetFormGroup"> | |
18 | +<form #widgetForm="ngForm" [formGroup]="widgetFormGroup"> | |
19 | 19 | <fieldset [disabled]="isLoading$ | async"> |
20 | 20 | <tb-widget-config |
21 | - [typeParameters]="typeParameters" | |
22 | - [actionSources]="actionSources" | |
23 | - [isDataEnabled]="isDataEnabled" | |
24 | - [widgetSettingsSchema]="settingsSchema" | |
25 | - [dataKeySettingsSchema]="dataKeySettingsSchema" | |
26 | 21 | [aliasController]="aliasController" |
27 | 22 | [functionsOnly]="widgetEditMode" |
28 | 23 | [entityAliases]="dashboard.configuration.entityAliases" | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | |
17 | +import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; | |
18 | 18 | import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; |
... | ... | @@ -29,7 +29,7 @@ import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models |
29 | 29 | import { WidgetComponentService } from '@home/components/widget/widget-component.service'; |
30 | 30 | import { WidgetConfigComponentData } from '../../models/widget-component.models'; |
31 | 31 | import { deepClone, isDefined, isString } from '@core/utils'; |
32 | -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
32 | +import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; | |
33 | 33 | import { EntityType } from '@shared/models/entity-type.models'; |
34 | 34 | import { Observable, of } from 'rxjs'; |
35 | 35 | import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; |
... | ... | @@ -66,21 +66,20 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan |
66 | 66 | @Input() |
67 | 67 | widgetLayout: WidgetLayout; |
68 | 68 | |
69 | - @Input() | |
69 | + @ViewChild('widgetForm', {static: true}) widgetForm: NgForm; | |
70 | + | |
70 | 71 | widgetFormGroup: FormGroup; |
71 | 72 | |
72 | 73 | widgetConfig: WidgetConfigComponentData; |
73 | - typeParameters: WidgetTypeParameters; | |
74 | - actionSources: {[key: string]: WidgetActionSource}; | |
75 | - isDataEnabled: boolean; | |
76 | - settingsSchema: any; | |
77 | - dataKeySettingsSchema: any; | |
78 | - functionsOnly: boolean; | |
79 | 74 | |
80 | 75 | constructor(protected store: Store<AppState>, |
81 | 76 | private dialog: MatDialog, |
77 | + private fb: FormBuilder, | |
82 | 78 | private widgetComponentService: WidgetComponentService) { |
83 | 79 | super(store); |
80 | + this.widgetFormGroup = this.fb.group({ | |
81 | + widgetConfig: [null] | |
82 | + }); | |
84 | 83 | } |
85 | 84 | |
86 | 85 | ngOnInit(): void { |
... | ... | @@ -104,27 +103,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan |
104 | 103 | |
105 | 104 | private loadWidgetConfig() { |
106 | 105 | const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); |
107 | - this.widgetConfig = { | |
108 | - config: this.widget.config, | |
109 | - layout: this.widgetLayout, | |
110 | - widgetType: this.widget.type | |
111 | - }; | |
112 | - const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; | |
113 | - const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; | |
114 | - this.typeParameters = widgetInfo.typeParameters; | |
115 | - this.actionSources = widgetInfo.actionSources; | |
116 | - this.isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; | |
117 | - if (!settingsSchema || settingsSchema === '') { | |
118 | - this.settingsSchema = {}; | |
106 | + const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; | |
107 | + const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; | |
108 | + const typeParameters = widgetInfo.typeParameters; | |
109 | + const actionSources = widgetInfo.actionSources; | |
110 | + const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; | |
111 | + let settingsSchema; | |
112 | + if (!rawSettingsSchema || rawSettingsSchema === '') { | |
113 | + settingsSchema = {}; | |
119 | 114 | } else { |
120 | - this.settingsSchema = isString(settingsSchema) ? JSON.parse(settingsSchema) : settingsSchema; | |
115 | + settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema; | |
121 | 116 | } |
122 | - if (!dataKeySettingsSchema || dataKeySettingsSchema === '') { | |
123 | - this.dataKeySettingsSchema = {}; | |
117 | + let dataKeySettingsSchema; | |
118 | + if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') { | |
119 | + dataKeySettingsSchema = {}; | |
124 | 120 | } else { |
125 | - this.dataKeySettingsSchema = isString(dataKeySettingsSchema) ? JSON.parse(dataKeySettingsSchema) : dataKeySettingsSchema; | |
121 | + dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema; | |
126 | 122 | } |
127 | - this.functionsOnly = this.dashboard ? false : true; | |
123 | + this.widgetConfig = { | |
124 | + config: this.widget.config, | |
125 | + layout: this.widgetLayout, | |
126 | + widgetType: this.widget.type, | |
127 | + typeParameters, | |
128 | + actionSources, | |
129 | + isDataEnabled, | |
130 | + settingsSchema, | |
131 | + dataKeySettingsSchema | |
132 | + }; | |
128 | 133 | this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); |
129 | 134 | } |
130 | 135 | ... | ... |
... | ... | @@ -13,37 +13,9 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | -@mixin tb-checkered-bg() { | |
17 | - background-color: #fff; | |
18 | - background-image: | |
19 | - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), | |
20 | - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); | |
21 | - background-position: 0 0, 4px 4px; | |
22 | - background-size: 8px 8px; | |
23 | -} | |
24 | 16 | |
25 | 17 | :host { |
26 | 18 | .mat-form-field { |
27 | 19 | width: 100%; |
28 | 20 | } |
29 | - .tb-color-preview { | |
30 | - cursor: pointer; | |
31 | - box-sizing: border-box; | |
32 | - position: relative; | |
33 | - width: 24px; | |
34 | - min-width: 24px; | |
35 | - height: 24px; | |
36 | - overflow: hidden; | |
37 | - content: ""; | |
38 | - border: 2px solid #fff; | |
39 | - border-radius: 50%; | |
40 | - box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); | |
41 | - | |
42 | - @include tb-checkered-bg(); | |
43 | - | |
44 | - .tb-color-result { | |
45 | - width: 100%; | |
46 | - height: 100%; | |
47 | - } | |
48 | - } | |
49 | 21 | } | ... | ... |
... | ... | @@ -18,8 +18,8 @@ import { |
18 | 18 | Directive, |
19 | 19 | ElementRef, |
20 | 20 | EventEmitter, |
21 | - Input, | |
22 | - Output, | |
21 | + Input, OnChanges, | |
22 | + Output, SimpleChanges, | |
23 | 23 | ViewContainerRef |
24 | 24 | } from '@angular/core'; |
25 | 25 | import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
... | ... | @@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
29 | 29 | @Directive({ |
30 | 30 | selector: '[tb-fullscreen]' |
31 | 31 | }) |
32 | -export class FullscreenDirective { | |
32 | +export class FullscreenDirective implements OnChanges { | |
33 | 33 | |
34 | 34 | fullscreenValue = false; |
35 | 35 | |
... | ... | @@ -37,16 +37,10 @@ export class FullscreenDirective { |
37 | 37 | private parentElement: HTMLElement; |
38 | 38 | |
39 | 39 | @Input() |
40 | - set fullscreen(fullscreen: boolean) { | |
41 | - if (this.fullscreenValue !== fullscreen) { | |
42 | - this.fullscreenValue = fullscreen; | |
43 | - if (this.fullscreenValue) { | |
44 | - this.enterFullscreen(); | |
45 | - } else { | |
46 | - this.exitFullscreen(); | |
47 | - } | |
48 | - } | |
49 | - } | |
40 | + fullscreen: boolean; | |
41 | + | |
42 | + @Input() | |
43 | + fullscreenElement: HTMLElement; | |
50 | 44 | |
51 | 45 | @Output() |
52 | 46 | fullscreenChanged = new EventEmitter<boolean>(); |
... | ... | @@ -56,11 +50,30 @@ export class FullscreenDirective { |
56 | 50 | private overlay: Overlay) { |
57 | 51 | } |
58 | 52 | |
53 | + ngOnChanges(changes: SimpleChanges): void { | |
54 | + let updateFullscreen = false; | |
55 | + for (const propName of Object.keys(changes)) { | |
56 | + const change = changes[propName]; | |
57 | + if (!change.firstChange && change.currentValue !== change.previousValue) { | |
58 | + if (propName === 'fullscreen') { | |
59 | + updateFullscreen = true; | |
60 | + } | |
61 | + } | |
62 | + } | |
63 | + if (updateFullscreen) { | |
64 | + if (this.fullscreen) { | |
65 | + this.enterFullscreen(); | |
66 | + } else { | |
67 | + this.exitFullscreen(); | |
68 | + } | |
69 | + } | |
70 | + } | |
71 | + | |
59 | 72 | enterFullscreen() { |
60 | - const targetElement = this.elementRef; | |
61 | - this.parentElement = targetElement.nativeElement.parentElement; | |
62 | - this.parentElement.removeChild(targetElement.nativeElement); | |
63 | - targetElement.nativeElement.classList.add('tb-fullscreen'); | |
73 | + const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; | |
74 | + this.parentElement = targetElement.parentElement; | |
75 | + this.parentElement.removeChild(targetElement); | |
76 | + targetElement.classList.add('tb-fullscreen'); | |
64 | 77 | const position = this.overlay.position(); |
65 | 78 | const config = new OverlayConfig({ |
66 | 79 | hasBackdrop: false, |
... | ... | @@ -73,19 +86,19 @@ export class FullscreenDirective { |
73 | 86 | |
74 | 87 | this.overlayRef = this.overlay.create(config); |
75 | 88 | this.overlayRef.attach(new EmptyPortal()); |
76 | - this.overlayRef.overlayElement.append( targetElement.nativeElement ); | |
89 | + this.overlayRef.overlayElement.append( targetElement ); | |
77 | 90 | this.fullscreenChanged.emit(true); |
78 | 91 | } |
79 | 92 | |
80 | 93 | exitFullscreen() { |
81 | - const targetElement = this.elementRef; | |
94 | + const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; | |
82 | 95 | if (this.parentElement) { |
83 | - this.overlayRef.overlayElement.removeChild( targetElement.nativeElement ); | |
84 | - this.parentElement.append(targetElement.nativeElement); | |
96 | + this.overlayRef.overlayElement.removeChild( targetElement ); | |
97 | + this.parentElement.append(targetElement); | |
85 | 98 | this.parentElement = null; |
86 | 99 | } |
87 | - targetElement.nativeElement.classList.remove('tb-fullscreen'); | |
88 | - if (this.elementRef !== targetElement) { | |
100 | + targetElement.classList.remove('tb-fullscreen'); | |
101 | + if (this.elementRef) { | |
89 | 102 | this.elementRef.nativeElement.classList.remove('tb-fullscreen'); |
90 | 103 | } |
91 | 104 | this.overlayRef.dispose(); | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | + | |
18 | +export interface JsonFormComponentData { | |
19 | + model?: any; | |
20 | + schema?: any; | |
21 | + form?: any; | |
22 | + groupInfoes?: any[]; | |
23 | +} | ... | ... |
... | ... | @@ -15,8 +15,8 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreen]="isFullscreen" | |
19 | - (fullscreenChanged)="onFullscreenChanged()"> | |
18 | +<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreenElement]="targetFullscreenElement" | |
19 | + [fullscreen]="isFullscreen" | |
20 | + (fullscreenChanged)="onFullscreenChanged($event)"> | |
20 | 21 | <div #reactRoot></div> |
21 | - <div>{{ model | json }}</div> | |
22 | 22 | </div> | ... | ... |
... | ... | @@ -40,6 +40,7 @@ import * as React from 'react'; |
40 | 40 | import * as ReactDOM from 'react-dom'; |
41 | 41 | import ReactSchemaForm from './react/json-form-react'; |
42 | 42 | import JsonFormUtils from './react/json-form-utils'; |
43 | +import { JsonFormComponentData } from './json-form-component.models'; | |
43 | 44 | |
44 | 45 | @Component({ |
45 | 46 | selector: 'tb-json-form', |
... | ... | @@ -64,12 +65,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato |
64 | 65 | @ViewChild('reactRoot', {static: true}) |
65 | 66 | reactRootElmRef: ElementRef<HTMLElement>; |
66 | 67 | |
67 | - @Input() schema: any; | |
68 | - | |
69 | - @Input() form: any; | |
70 | - | |
71 | - @Input() groupInfoes: any[]; | |
72 | - | |
73 | 68 | private readonlyValue: boolean; |
74 | 69 | get readonly(): boolean { |
75 | 70 | return this.readonlyValue; |
... | ... | @@ -91,11 +86,18 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato |
91 | 86 | onToggleFullscreen: this.onToggleFullscreen.bind(this) |
92 | 87 | }; |
93 | 88 | |
89 | + data: JsonFormComponentData; | |
90 | + | |
94 | 91 | model: any; |
92 | + schema: any; | |
93 | + form: any; | |
94 | + groupInfoes: any[]; | |
95 | 95 | |
96 | 96 | isModelValid = true; |
97 | 97 | |
98 | 98 | isFullscreen = false; |
99 | + targetFullscreenElement: HTMLElement; | |
100 | + fullscreenFinishFn: () => void; | |
99 | 101 | |
100 | 102 | private propagateChange = null; |
101 | 103 | |
... | ... | @@ -128,77 +130,91 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato |
128 | 130 | }; |
129 | 131 | } |
130 | 132 | |
131 | - writeValue(value: any): void { | |
132 | - this.model = value || {}; | |
133 | + writeValue(data: JsonFormComponentData): void { | |
134 | + this.data = data; | |
135 | + this.schema = this.data && this.data.schema ? deepClone(this.data.schema) : { | |
136 | + type: 'object' | |
137 | + }; | |
138 | + this.schema.strict = true; | |
139 | + this.form = this.data && this.data.form ? deepClone(this.data.form) : [ '*' ]; | |
140 | + this.groupInfoes = this.data && this.data.groupInfoes ? deepClone(this.data.groupInfoes) : []; | |
141 | + this.model = this.data && this.data.model || {}; | |
133 | 142 | this.model = inspector.sanitize(this.schema, this.model).data; |
134 | 143 | this.updateAndRender(); |
135 | 144 | this.isModelValid = this.validateModel(); |
136 | 145 | if (!this.isModelValid) { |
137 | 146 | this.updateView(); |
138 | 147 | } |
139 | - } | |
148 | +} | |
140 | 149 | |
141 | 150 | updateView() { |
142 | - this.propagateChange(this.model); | |
151 | + if (this.data) { | |
152 | + this.data.model = this.model; | |
153 | + this.propagateChange(this.data); | |
154 | + } | |
143 | 155 | } |
144 | 156 | |
145 | 157 | ngOnChanges(changes: SimpleChanges): void { |
146 | 158 | for (const propName of Object.keys(changes)) { |
147 | 159 | const change = changes[propName]; |
148 | 160 | if (!change.firstChange && change.currentValue !== change.previousValue) { |
149 | - if (propName === 'schema') { | |
150 | - this.model = inspector.sanitize(this.schema, this.model).data; | |
151 | - this.isModelValid = this.validateModel(); | |
152 | - } | |
153 | - if (['readonly', 'schema', 'form', 'groupInfoes'].includes(propName)) { | |
161 | + if (propName === 'readonly') { | |
154 | 162 | this.updateAndRender(); |
155 | 163 | } |
156 | 164 | } |
157 | 165 | } |
158 | 166 | } |
159 | 167 | |
160 | - onFullscreenChanged() {} | |
161 | - | |
162 | 168 | private onModelChange(key: (string | number)[], val: any) { |
163 | 169 | if (isString(val) && val === '') { |
164 | 170 | val = undefined; |
165 | 171 | } |
166 | 172 | if (JsonFormUtils.updateValue(key, this.model, val)) { |
167 | 173 | this.formProps.model = this.model; |
174 | + this.isModelValid = this.validateModel(); | |
168 | 175 | this.updateView(); |
169 | 176 | } |
170 | 177 | } |
171 | 178 | |
172 | - private onColorClick(event: MouseEvent, key: string, val: string) { | |
179 | + private onColorClick(key: (string | number)[], | |
180 | + val: tinycolor.ColorFormats.RGBA, | |
181 | + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { | |
173 | 182 | this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { |
174 | - const e = event as any; | |
175 | - if (e.data && e.data.onValueChanged) { | |
176 | - e.data.onValueChanged(tinycolor(color).toRgb()); | |
183 | + if (colorSelectedFn) { | |
184 | + colorSelectedFn(tinycolor(color).toRgb()); | |
177 | 185 | } |
178 | 186 | }); |
179 | 187 | } |
180 | 188 | |
181 | - private onToggleFullscreen() { | |
189 | + private onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) { | |
190 | + this.targetFullscreenElement = element; | |
182 | 191 | this.isFullscreen = !this.isFullscreen; |
183 | - this.formProps.isFullscreen = this.isFullscreen; | |
192 | + this.fullscreenFinishFn = fullscreenFinishFn; | |
193 | + } | |
194 | + | |
195 | + onFullscreenChanged(fullscreen: boolean) { | |
196 | + this.formProps.isFullscreen = fullscreen; | |
197 | + this.renderReactSchemaForm(false); | |
198 | + if (this.fullscreenFinishFn) { | |
199 | + this.fullscreenFinishFn(); | |
200 | + this.fullscreenFinishFn = null; | |
201 | + } | |
184 | 202 | } |
185 | 203 | |
186 | 204 | private updateAndRender() { |
187 | - const schema = this.schema ? deepClone(this.schema) : { | |
188 | - type: 'object' | |
189 | - }; | |
190 | - schema.strict = true; | |
191 | - const form = this.form ? deepClone(this.form) : [ '*' ]; | |
192 | - const groupInfoes = this.groupInfoes ? deepClone(this.groupInfoes) : []; | |
205 | + | |
193 | 206 | this.formProps.option.formDefaults.readonly = this.readonly; |
194 | - this.formProps.schema = schema; | |
195 | - this.formProps.form = form; | |
196 | - this.formProps.groupInfoes = groupInfoes; | |
207 | + this.formProps.schema = this.schema; | |
208 | + this.formProps.form = this.form; | |
209 | + this.formProps.groupInfoes = this.groupInfoes; | |
197 | 210 | this.formProps.model = deepClone(this.model); |
198 | 211 | this.renderReactSchemaForm(); |
199 | 212 | } |
200 | 213 | |
201 | - private renderReactSchemaForm() { | |
214 | + private renderReactSchemaForm(destroy: boolean = true) { | |
215 | + if (destroy) { | |
216 | + this.destroyReactSchemaForm(); | |
217 | + } | |
202 | 218 | ReactDOM.render(React.createElement(ReactSchemaForm, this.formProps), this.reactRootElmRef.nativeElement); |
203 | 219 | } |
204 | 220 | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +import * as React from 'react'; | |
18 | +import ThingsboardBaseComponent from './json-form-base-component'; | |
19 | +import reactCSS from 'reactcss'; | |
20 | +import ReactAce from 'react-ace'; | |
21 | +import Button from '@material-ui/core/Button'; | |
22 | +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; | |
23 | +import * as tinycolor from 'tinycolor2'; | |
24 | +import { IEditorProps } from 'react-ace/src/types'; | |
25 | + | |
26 | +interface ThingsboardAceEditorProps extends JsonFormFieldProps { | |
27 | + mode: string; | |
28 | + onTidy: (value: string) => string; | |
29 | +} | |
30 | + | |
31 | +interface ThingsboardAceEditorState extends JsonFormFieldState { | |
32 | + isFull: boolean; | |
33 | + focused: boolean; | |
34 | +} | |
35 | + | |
36 | +class ThingsboardAceEditor extends React.Component<ThingsboardAceEditorProps, ThingsboardAceEditorState> { | |
37 | + | |
38 | + hostElement: HTMLElement; | |
39 | + private aceEditor: IEditorProps; | |
40 | + | |
41 | + constructor(props) { | |
42 | + super(props); | |
43 | + this.onValueChanged = this.onValueChanged.bind(this); | |
44 | + this.onBlur = this.onBlur.bind(this); | |
45 | + this.onFocus = this.onFocus.bind(this); | |
46 | + this.onTidy = this.onTidy.bind(this); | |
47 | + this.onLoad = this.onLoad.bind(this); | |
48 | + this.onToggleFull = this.onToggleFull.bind(this); | |
49 | + const value = props.value ? props.value + '' : ''; | |
50 | + this.state = { | |
51 | + isFull: false, | |
52 | + value, | |
53 | + focused: false | |
54 | + }; | |
55 | + } | |
56 | + | |
57 | + onValueChanged(value) { | |
58 | + this.setState({ | |
59 | + value | |
60 | + }); | |
61 | + this.props.onChangeValidate({ | |
62 | + target: { | |
63 | + value | |
64 | + } | |
65 | + }); | |
66 | + } | |
67 | + | |
68 | + onBlur() { | |
69 | + this.setState({ focused: false }); | |
70 | + } | |
71 | + | |
72 | + onFocus() { | |
73 | + this.setState({ focused: true }); | |
74 | + } | |
75 | + | |
76 | + onTidy() { | |
77 | + if (!this.props.form.readonly) { | |
78 | + let value = this.state.value; | |
79 | + value = this.props.onTidy(value); | |
80 | + this.setState({ | |
81 | + value | |
82 | + }); | |
83 | + this.props.onChangeValidate({ | |
84 | + target: { | |
85 | + value | |
86 | + } | |
87 | + }); | |
88 | + } | |
89 | + } | |
90 | + | |
91 | + onLoad(editor: IEditorProps) { | |
92 | + this.aceEditor = editor; | |
93 | + } | |
94 | + | |
95 | + onToggleFull() { | |
96 | + this.setState({ isFull: !this.state.isFull }); | |
97 | + this.props.onToggleFullscreen(this.hostElement, () => { | |
98 | + if (this.aceEditor) { | |
99 | + this.aceEditor.resize(); | |
100 | + this.aceEditor.renderer.updateFull(); | |
101 | + } | |
102 | + }); | |
103 | + } | |
104 | + | |
105 | + componentDidUpdate() { | |
106 | + } | |
107 | + | |
108 | + render() { | |
109 | + | |
110 | + const styles = reactCSS({ | |
111 | + default: { | |
112 | + tidyButtonStyle: { | |
113 | + color: '#7B7B7B', | |
114 | + minWidth: '32px', | |
115 | + minHeight: '15px', | |
116 | + lineHeight: '15px', | |
117 | + fontSize: '0.800rem', | |
118 | + margin: '0', | |
119 | + padding: '4px', | |
120 | + height: '23px', | |
121 | + borderRadius: '5px', | |
122 | + marginLeft: '5px' | |
123 | + } | |
124 | + } | |
125 | + }); | |
126 | + | |
127 | + let labelClass = 'tb-label'; | |
128 | + if (this.props.form.required) { | |
129 | + labelClass += ' tb-required'; | |
130 | + } | |
131 | + if (this.props.form.readonly) { | |
132 | + labelClass += ' tb-readonly'; | |
133 | + } | |
134 | + if (this.state.focused) { | |
135 | + labelClass += ' tb-focused'; | |
136 | + } | |
137 | + let containerClass = 'tb-container'; | |
138 | + const style = this.props.form.style || {width: '100%'}; | |
139 | + if (this.state.isFull) { | |
140 | + containerClass += ' fullscreen-form-field'; | |
141 | + } | |
142 | + return ( | |
143 | + <div> | |
144 | + <div className='tb-json-form' ref={c => (this.hostElement = c)}> | |
145 | + <div className={containerClass}> | |
146 | + <label className={labelClass}>{this.props.form.title}</label> | |
147 | + <div className='json-form-ace-editor'> | |
148 | + <div className='title-panel'> | |
149 | + <label>{this.props.mode}</label> | |
150 | + <Button style={ styles.tidyButtonStyle } | |
151 | + className='tidy-button' onClick={this.onTidy}>Tidy</Button> | |
152 | + <Button style={ styles.tidyButtonStyle } | |
153 | + className='tidy-button' onClick={this.onToggleFull}> | |
154 | + {this.state.isFull ? | |
155 | + 'Exit fullscreen' : 'Fullscreen'} | |
156 | + </Button> | |
157 | + </div> | |
158 | + <ReactAce mode={this.props.mode} | |
159 | + height={this.state.isFull ? '100%' : '150px'} | |
160 | + width={this.state.isFull ? '100%' : '300px'} | |
161 | + theme='github' | |
162 | + onChange={this.onValueChanged} | |
163 | + onFocus={this.onFocus} | |
164 | + onBlur={this.onBlur} | |
165 | + onLoad={this.onLoad} | |
166 | + name={this.props.form.title} | |
167 | + value={this.state.value} | |
168 | + readOnly={this.props.form.readonly} | |
169 | + editorProps={{$blockScrolling: Infinity}} | |
170 | + enableBasicAutocompletion={true} | |
171 | + enableSnippets={true} | |
172 | + enableLiveAutocompletion={true} | |
173 | + style={style}/> | |
174 | + </div> | |
175 | + <div className='json-form-error' | |
176 | + style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div> | |
177 | + </div> | |
178 | + </div> | |
179 | + </div> | |
180 | + ); | |
181 | + } | |
182 | +} | |
183 | + | |
184 | +export default ThingsboardBaseComponent(ThingsboardAceEditor); | ... | ... |
... | ... | @@ -17,7 +17,8 @@ import * as React from 'react'; |
17 | 17 | import JsonFormUtils from './json-form-utils'; |
18 | 18 | import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
19 | 19 | |
20 | -export default ThingsboardBaseComponent => class extends React.Component<JsonFormFieldProps, JsonFormFieldState> { | |
20 | +export default ThingsboardBaseComponent => class<P extends JsonFormFieldProps> | |
21 | + extends React.Component<P, JsonFormFieldState> { | |
21 | 22 | |
22 | 23 | constructor(props) { |
23 | 24 | super(props); | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +import * as React from 'react'; | |
17 | +import * as ReactDOM from 'react-dom'; | |
18 | +import ThingsboardBaseComponent from './json-form-base-component'; | |
19 | +import reactCSS from 'reactcss'; | |
20 | +import * as tinycolor from 'tinycolor2'; | |
21 | +import TextField from '@material-ui/core/TextField'; | |
22 | +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; | |
23 | +import IconButton from '@material-ui/core/IconButton'; | |
24 | +import ClearIcon from '@material-ui/icons/Clear'; | |
25 | +import Tooltip from '@material-ui/core/Tooltip'; | |
26 | + | |
27 | +interface ThingsboardColorState extends JsonFormFieldState { | |
28 | + color: tinycolor.ColorFormats.RGBA | null; | |
29 | + focused: boolean; | |
30 | +} | |
31 | + | |
32 | +class ThingsboardColor extends React.Component<JsonFormFieldProps, ThingsboardColorState> { | |
33 | + | |
34 | + constructor(props) { | |
35 | + super(props); | |
36 | + this.onBlur = this.onBlur.bind(this); | |
37 | + this.onFocus = this.onFocus.bind(this); | |
38 | + this.onValueChanged = this.onValueChanged.bind(this); | |
39 | + this.onSwatchClick = this.onSwatchClick.bind(this); | |
40 | + this.onClear = this.onClear.bind(this); | |
41 | + const value = props.value ? props.value + '' : null; | |
42 | + const color = value != null ? tinycolor(value).toRgb() : null; | |
43 | + this.state = { | |
44 | + color, | |
45 | + focused: false | |
46 | + }; | |
47 | + } | |
48 | + | |
49 | + onBlur() { | |
50 | + this.setState({focused: false}); | |
51 | + } | |
52 | + | |
53 | + onFocus() { | |
54 | + this.setState({focused: true}); | |
55 | + } | |
56 | + | |
57 | + componentDidMount() { | |
58 | + const node = ReactDOM.findDOMNode(this); | |
59 | + const colContainer = $(node).children('#color-container'); | |
60 | + colContainer.click((event) => { | |
61 | + if (!this.props.form.readonly) { | |
62 | + this.onSwatchClick(event); | |
63 | + } | |
64 | + }); | |
65 | + } | |
66 | + | |
67 | + componentWillUnmount() { | |
68 | + const node = ReactDOM.findDOMNode(this); | |
69 | + const colContainer = $(node).children('#color-container'); | |
70 | + colContainer.off( 'click' ); | |
71 | + } | |
72 | + | |
73 | + onValueChanged(value: tinycolor.ColorFormats.RGBA | null) { | |
74 | + let color: tinycolor.Instance = null; | |
75 | + if (value != null) { | |
76 | + color = tinycolor(value); | |
77 | + } | |
78 | + this.setState({ | |
79 | + color: value | |
80 | + }); | |
81 | + let colorValue = ''; | |
82 | + if (color != null && color.getAlpha() !== 1) { | |
83 | + colorValue = color.toRgbString(); | |
84 | + } else if (color != null) { | |
85 | + colorValue = color.toHexString(); | |
86 | + } | |
87 | + this.props.onChangeValidate({ | |
88 | + target: { | |
89 | + value: colorValue | |
90 | + } | |
91 | + }); | |
92 | + } | |
93 | + | |
94 | + onSwatchClick(event) { | |
95 | + this.props.onColorClick(this.props.form.key, this.state.color, | |
96 | + (color) => { | |
97 | + this.onValueChanged(color); | |
98 | + } | |
99 | + ); | |
100 | + } | |
101 | + | |
102 | + onClear(event) { | |
103 | + if (event) { | |
104 | + event.stopPropagation(); | |
105 | + } | |
106 | + this.onValueChanged(null); | |
107 | + } | |
108 | + | |
109 | + render() { | |
110 | + | |
111 | + let background = 'rgba(0,0,0,0)'; | |
112 | + if (this.state.color != null) { | |
113 | + background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`; | |
114 | + } | |
115 | + | |
116 | + const styles = reactCSS({ | |
117 | + default: { | |
118 | + color: { | |
119 | + background: `${ background }` | |
120 | + }, | |
121 | + swatch: { | |
122 | + display: 'inline-block', | |
123 | + marginRight: '10px', | |
124 | + marginTop: 'auto', | |
125 | + marginBottom: 'auto', | |
126 | + cursor: 'pointer', | |
127 | + opacity: `${ this.props.form.readonly ? '0.6' : '1' }` | |
128 | + }, | |
129 | + swatchText: { | |
130 | + width: '100%' | |
131 | + }, | |
132 | + container: { | |
133 | + display: 'flex', | |
134 | + flexDirection: 'row', | |
135 | + alignItems: 'center' | |
136 | + }, | |
137 | + colorContainer: { | |
138 | + display: 'flex', | |
139 | + width: '100%' | |
140 | + } | |
141 | + }, | |
142 | + }); | |
143 | + | |
144 | + let fieldClass = 'tb-field'; | |
145 | + if (this.props.form.required) { | |
146 | + fieldClass += ' tb-required'; | |
147 | + } | |
148 | + if (this.props.form.readonly) { | |
149 | + fieldClass += ' tb-readonly'; | |
150 | + } | |
151 | + if (this.state.focused) { | |
152 | + fieldClass += ' tb-focused'; | |
153 | + } | |
154 | + | |
155 | + let stringColor = ''; | |
156 | + if (this.state.color != null) { | |
157 | + const color = tinycolor(this.state.color); | |
158 | + stringColor = color.toRgbString(); | |
159 | + } | |
160 | + | |
161 | + return ( | |
162 | + <div style={ styles.container }> | |
163 | + <div id='color-container' style={ styles.colorContainer }> | |
164 | + <div className='tb-color-preview' style={ styles.swatch }> | |
165 | + <div className='tb-color-result' style={ styles.color }/> | |
166 | + </div> | |
167 | + <TextField | |
168 | + className={fieldClass} | |
169 | + label={this.props.form.title} | |
170 | + error={!this.props.valid} | |
171 | + helperText={this.props.valid ? this.props.form.placeholder : this.props.error} | |
172 | + value={stringColor} | |
173 | + disabled={this.props.form.readonly} | |
174 | + onFocus={this.onFocus} | |
175 | + onBlur={this.onBlur} | |
176 | + style={ styles.swatchText }/> | |
177 | + </div> | |
178 | + <Tooltip title='Clear' placement='top'><IconButton onClick={this.onClear}><ClearIcon/></IconButton></Tooltip> | |
179 | + </div> | |
180 | + ); | |
181 | + } | |
182 | +} | |
183 | + | |
184 | +export default ThingsboardBaseComponent(ThingsboardColor); | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +import * as React from 'react'; | |
17 | +import ThingsboardAceEditor from './json-form-ace-editor'; | |
18 | +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; | |
19 | +import { css_beautify } from 'js-beautify'; | |
20 | + | |
21 | +class ThingsboardCss extends React.Component<JsonFormFieldProps, JsonFormFieldState> { | |
22 | + | |
23 | + constructor(props) { | |
24 | + super(props); | |
25 | + this.onTidyCss = this.onTidyCss.bind(this); | |
26 | + } | |
27 | + | |
28 | + onTidyCss(css: string): string { | |
29 | + return css_beautify(css, {indent_size: 4}); | |
30 | + } | |
31 | + | |
32 | + render() { | |
33 | + return ( | |
34 | + <ThingsboardAceEditor {...this.props} mode='css' onTidy={this.onTidyCss} {...this.state}></ThingsboardAceEditor> | |
35 | + ); | |
36 | + } | |
37 | +} | |
38 | + | |
39 | +export default ThingsboardCss; | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +import * as React from 'react'; | |
17 | +import ThingsboardAceEditor from './json-form-ace-editor'; | |
18 | +import { html_beautify } from 'js-beautify'; | |
19 | +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; | |
20 | + | |
21 | +class ThingsboardHtml extends React.Component<JsonFormFieldProps, JsonFormFieldState> { | |
22 | + | |
23 | + constructor(props) { | |
24 | + super(props); | |
25 | + this.onTidyHtml = this.onTidyHtml.bind(this); | |
26 | + } | |
27 | + | |
28 | + onTidyHtml(html: string): string { | |
29 | + return html_beautify(html, {indent_size: 4}); | |
30 | + } | |
31 | + | |
32 | + render() { | |
33 | + return ( | |
34 | + <ThingsboardAceEditor {...this.props} mode='html' onTidy={this.onTidyHtml} {...this.state}></ThingsboardAceEditor> | |
35 | + ); | |
36 | + } | |
37 | +} | |
38 | + | |
39 | +export default ThingsboardHtml; | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +import React, {useCallback} from 'react'; | |
17 | +import { DropzoneState, useDropzone } from 'react-dropzone'; | |
18 | +import ThingsboardBaseComponent from './json-form-base-component'; | |
19 | +import Dropzone from 'react-dropzone'; | |
20 | +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; | |
21 | +import IconButton from '@material-ui/core/IconButton'; | |
22 | +import ClearIcon from '@material-ui/icons/Clear'; | |
23 | +import Tooltip from '@material-ui/core/Tooltip'; | |
24 | + | |
25 | +interface ThingsboardImageState extends JsonFormFieldState { | |
26 | + imageUrl: string; | |
27 | +} | |
28 | + | |
29 | +class ThingsboardImage extends React.Component<JsonFormFieldProps, ThingsboardImageState> { | |
30 | + | |
31 | + constructor(props) { | |
32 | + super(props); | |
33 | + this.onDrop = this.onDrop.bind(this); | |
34 | + this.onClear = this.onClear.bind(this); | |
35 | + const value = props.value ? props.value + '' : null; | |
36 | + this.state = { | |
37 | + imageUrl: value | |
38 | + }; | |
39 | + } | |
40 | + | |
41 | + onDrop(acceptedFiles: File[]) { | |
42 | + const reader = new FileReader(); | |
43 | + reader.onload = () => { | |
44 | + this.onValueChanged(reader.result); | |
45 | + }; | |
46 | + reader.readAsDataURL(acceptedFiles[0]); | |
47 | + } | |
48 | + | |
49 | + onValueChanged(value) { | |
50 | + this.setState({ | |
51 | + imageUrl: value | |
52 | + }); | |
53 | + this.props.onChangeValidate({ | |
54 | + target: { | |
55 | + value | |
56 | + } | |
57 | + }); | |
58 | + } | |
59 | + | |
60 | + onClear(event) { | |
61 | + if (event) { | |
62 | + event.stopPropagation(); | |
63 | + } | |
64 | + this.onValueChanged(''); | |
65 | + } | |
66 | + | |
67 | + render() { | |
68 | + | |
69 | + let labelClass = 'tb-label'; | |
70 | + if (this.props.form.required) { | |
71 | + labelClass += ' tb-required'; | |
72 | + } | |
73 | + if (this.props.form.readonly) { | |
74 | + labelClass += ' tb-readonly'; | |
75 | + } | |
76 | + | |
77 | + let previewComponent; | |
78 | + if (this.state.imageUrl) { | |
79 | + previewComponent = <img className='tb-image-preview' src={this.state.imageUrl} />; | |
80 | + } else { | |
81 | + previewComponent = <div>No image selected</div>; | |
82 | + } | |
83 | + | |
84 | + return ( | |
85 | + <div className='tb-container'> | |
86 | + <label className={labelClass}>{this.props.form.title}</label> | |
87 | + <div className='tb-image-select-container'> | |
88 | + <div className='tb-image-preview-container'>{previewComponent}</div> | |
89 | + <div className='tb-image-clear-container'> | |
90 | + <Tooltip title='Clear' placement='top'> | |
91 | + <IconButton className='tb-image-clear-btn' onClick={this.onClear}><ClearIcon/></IconButton> | |
92 | + </Tooltip> | |
93 | + </div> | |
94 | + <Dropzone onDrop={this.onDrop} | |
95 | + accept='image/*' multiple={false}> | |
96 | + {({getRootProps, getInputProps}) => ( | |
97 | + <div className='tb-dropzone' {...getRootProps()}> | |
98 | + <div>Drop an image or click to select a file to upload.</div> | |
99 | + <input {...getInputProps()} /> | |
100 | + </div> | |
101 | + )} | |
102 | + </Dropzone> | |
103 | + </div> | |
104 | + </div> | |
105 | + ); | |
106 | + } | |
107 | +} | |
108 | + | |
109 | +export default ThingsboardBaseComponent(ThingsboardImage); | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +import * as React from 'react'; | |
17 | +import ThingsboardAceEditor from './json-form-ace-editor'; | |
18 | +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; | |
19 | + | |
20 | +class ThingsboardJavaScript extends React.Component<JsonFormFieldProps, JsonFormFieldState> { | |
21 | + | |
22 | + constructor(props) { | |
23 | + super(props); | |
24 | + this.onTidyJavascript = this.onTidyJavascript.bind(this); | |
25 | + } | |
26 | + | |
27 | + onTidyJavascript(javascript: string): string { | |
28 | + return js_beautify(javascript, {indent_size: 4, wrap_line_length: 60}); | |
29 | + } | |
30 | + | |
31 | + render() { | |
32 | + return ( | |
33 | + <ThingsboardAceEditor {...this.props} mode='javascript' onTidy={this.onTidyJavascript} {...this.state}></ThingsboardAceEditor> | |
34 | + ); | |
35 | + } | |
36 | +} | |
37 | + | |
38 | +export default ThingsboardJavaScript; | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +import * as React from 'react'; | |
17 | +import ThingsboardAceEditor from './json-form-ace-editor'; | |
18 | +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; | |
19 | + | |
20 | +class ThingsboardJson extends React.Component<JsonFormFieldProps, JsonFormFieldState> { | |
21 | + | |
22 | + constructor(props) { | |
23 | + super(props); | |
24 | + this.onTidyJson = this.onTidyJson.bind(this); | |
25 | + } | |
26 | + | |
27 | + onTidyJson(json: string): string { | |
28 | + return js_beautify(json, {indent_size: 4}); | |
29 | + } | |
30 | + | |
31 | + render() { | |
32 | + return ( | |
33 | + <ThingsboardAceEditor {...this.props} mode='json' onTidy={this.onTidyJson} {...this.state}></ThingsboardAceEditor> | |
34 | + ); | |
35 | + } | |
36 | +} | |
37 | + | |
38 | +export default ThingsboardJson; | ... | ... |
... | ... | @@ -17,24 +17,25 @@ import * as React from 'react'; |
17 | 17 | import JsonFormUtils from './json-form-utils'; |
18 | 18 | |
19 | 19 | import ThingsboardArray from './json-form-array'; |
20 | -/*import ThingsboardJavaScript from './json-form-javascript.jsx'; | |
21 | -import ThingsboardJson from './json-form-json.jsx'; | |
22 | -import ThingsboardHtml from './json-form-html.jsx'; | |
23 | -import ThingsboardCss from './json-form-css.jsx'; | |
24 | -import ThingsboardColor from './json-form-color.jsx'*/ | |
20 | +import ThingsboardJavaScript from './json-form-javascript'; | |
21 | +import ThingsboardJson from './json-form-json'; | |
22 | +import ThingsboardHtml from './json-form-html'; | |
23 | +import ThingsboardCss from './json-form-css'; | |
24 | +import ThingsboardColor from './json-form-color'; | |
25 | 25 | import ThingsboardRcSelect from './json-form-rc-select'; |
26 | 26 | import ThingsboardNumber from './json-form-number'; |
27 | 27 | import ThingsboardText from './json-form-text'; |
28 | 28 | import ThingsboardSelect from './json-form-select'; |
29 | 29 | import ThingsboardRadios from './json-form-radios'; |
30 | 30 | import ThingsboardDate from './json-form-date'; |
31 | -/*import ThingsboardImage from './json-form-image.jsx';*/ | |
31 | +import ThingsboardImage from './json-form-image'; | |
32 | 32 | import ThingsboardCheckbox from './json-form-checkbox'; |
33 | 33 | import ThingsboardHelp from './json-form-help'; |
34 | 34 | import ThingsboardFieldSet from './json-form-fieldset'; |
35 | -import { JsonFormProps, GroupInfo, JsonFormData } from './json-form.models'; | |
35 | +import { JsonFormProps, GroupInfo, JsonFormData, onChangeFn, OnColorClickFn } from './json-form.models'; | |
36 | 36 | |
37 | 37 | import _ from 'lodash'; |
38 | +import * as tinycolor from 'tinycolor2'; | |
38 | 39 | |
39 | 40 | class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { |
40 | 41 | |
... | ... | @@ -52,15 +53,15 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { |
52 | 53 | select: ThingsboardSelect, |
53 | 54 | radios: ThingsboardRadios, |
54 | 55 | date: ThingsboardDate, |
55 | - // image: ThingsboardImage, | |
56 | + image: ThingsboardImage, | |
56 | 57 | checkbox: ThingsboardCheckbox, |
57 | 58 | help: ThingsboardHelp, |
58 | 59 | array: ThingsboardArray, |
59 | - // javascript: ThingsboardJavaScript, | |
60 | - // json: ThingsboardJson, | |
61 | - // html: ThingsboardHtml, | |
62 | - // css: ThingsboardCss, | |
63 | - // color: ThingsboardColor, | |
60 | + javascript: ThingsboardJavaScript, | |
61 | + json: ThingsboardJson, | |
62 | + html: ThingsboardHtml, | |
63 | + css: ThingsboardCss, | |
64 | + color: ThingsboardColor, | |
64 | 65 | 'rc-select': ThingsboardRcSelect, |
65 | 66 | fieldset: ThingsboardFieldSet |
66 | 67 | }; |
... | ... | @@ -78,20 +79,21 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { |
78 | 79 | } |
79 | 80 | } |
80 | 81 | |
81 | - onColorClick(event, key, val) { | |
82 | - this.props.onColorClick(event, key, val); | |
82 | + onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA, | |
83 | + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { | |
84 | + this.props.onColorClick(key, val, colorSelectedFn); | |
83 | 85 | } |
84 | 86 | |
85 | - onToggleFullscreen() { | |
86 | - this.props.onToggleFullscreen(); | |
87 | + onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) { | |
88 | + this.props.onToggleFullscreen(element, fullscreenFinishFn); | |
87 | 89 | } |
88 | 90 | |
89 | 91 | |
90 | 92 | builder(form: JsonFormData, |
91 | 93 | model: any, |
92 | 94 | index: number, |
93 | - onChange: (key: (string | number)[], val: any) => void, | |
94 | - onColorClick: (event: MouseEvent, key: string, val: string) => void, | |
95 | + onChange: onChangeFn, | |
96 | + onColorClick: OnColorClickFn, | |
95 | 97 | onToggleFullscreen: () => void, |
96 | 98 | mapper: {[type: string]: any}): JSX.Element { |
97 | 99 | const type = form.type; |
... | ... | @@ -173,9 +175,9 @@ class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps |
173 | 175 | } |
174 | 176 | |
175 | 177 | render() { |
176 | - const theCla = 'pull-right fa fa-chevron-down md-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled'); | |
178 | + const theCla = 'pull-right fa fa-chevron-down tb-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled'); | |
177 | 179 | return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}> |
178 | - <div className='SchemaGroupname md-button-toggle' | |
180 | + <div className='SchemaGroupname tb-button-toggle' | |
179 | 181 | onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div> |
180 | 182 | <div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div> |
181 | 183 | </section>); | ... | ... |
... | ... | @@ -544,11 +544,13 @@ function traverseSchema(schema: JsonSchemaData, fn: (prop: any, path: string[]) |
544 | 544 | |
545 | 545 | const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => { |
546 | 546 | $fn($schema, $path); |
547 | - for (const k of Object.keys($schema.properties)) { | |
548 | - if ($schema.properties.hasOwnProperty(k)) { | |
549 | - const currentPath = $path.slice(); | |
550 | - currentPath.push(k); | |
551 | - traverse($schema.properties[k], $fn, currentPath); | |
547 | + if ($schema.properties) { | |
548 | + for (const k of Object.keys($schema.properties)) { | |
549 | + if ($schema.properties.hasOwnProperty(k)) { | |
550 | + const currentPath = $path.slice(); | |
551 | + currentPath.push(k); | |
552 | + traverse($schema.properties[k], $fn, currentPath); | |
553 | + } | |
552 | 554 | } |
553 | 555 | } |
554 | 556 | if (!ignoreArrays && $schema.items) { | ... | ... |
... | ... | @@ -18,12 +18,13 @@ import { isUndefined, isDefined, isString } from '@app/core/utils'; |
18 | 18 | import * as equal from 'deep-equal'; |
19 | 19 | import ObjectPath from 'objectpath'; |
20 | 20 | import * as React from 'react'; |
21 | +import * as tinycolor from 'tinycolor2'; | |
21 | 22 | |
22 | 23 | export interface SchemaValidationResult { |
23 | 24 | valid: boolean; |
24 | 25 | error?: { |
25 | 26 | message?: string; |
26 | - } | |
27 | + }; | |
27 | 28 | } |
28 | 29 | |
29 | 30 | export interface FormOption { |
... | ... | @@ -47,6 +48,11 @@ export interface GroupInfo { |
47 | 48 | GroupTitle: string; |
48 | 49 | } |
49 | 50 | |
51 | +export type onChangeFn = (key: (string | number)[], val: any) => void; | |
52 | +export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA, | |
53 | + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void; | |
54 | +export type onToggleFullscreenFn = (element: HTMLElement, fullscreenFinishFn?: () => void) => void; | |
55 | + | |
50 | 56 | export interface JsonFormProps { |
51 | 57 | model?: any; |
52 | 58 | schema?: any; |
... | ... | @@ -55,9 +61,9 @@ export interface JsonFormProps { |
55 | 61 | isFullscreen: boolean; |
56 | 62 | ignore?: {[key: string]: boolean}; |
57 | 63 | option: FormOption; |
58 | - onModelChange?: (key: (string | number)[], val: any) => void; | |
59 | - onColorClick?: (event: MouseEvent, key: string, val: string) => void; | |
60 | - onToggleFullscreen?: () => void; | |
64 | + onModelChange?: onChangeFn; | |
65 | + onColorClick?: OnColorClickFn; | |
66 | + onToggleFullscreen?: onToggleFullscreenFn; | |
61 | 67 | mapper?: {[type: string]: any}; |
62 | 68 | } |
63 | 69 | |
... | ... | @@ -98,22 +104,24 @@ export interface JsonFormData { |
98 | 104 | [key: string]: any; |
99 | 105 | } |
100 | 106 | |
107 | +export type ComponentBuilderFn = (form: JsonFormData, | |
108 | + model: any, | |
109 | + index: number, | |
110 | + onChange: onChangeFn, | |
111 | + onColorClick: OnColorClickFn, | |
112 | + onToggleFullscreen: onToggleFullscreenFn, | |
113 | + mapper: {[type: string]: any}) => JSX.Element; | |
114 | + | |
101 | 115 | export interface JsonFormFieldProps { |
102 | 116 | value: any; |
103 | 117 | model: any; |
104 | 118 | form: JsonFormData; |
105 | - builder: (form: JsonFormData, | |
106 | - model: any, | |
107 | - index: number, | |
108 | - onChange: (key: (string | number)[], val: any) => void, | |
109 | - onColorClick: (event: MouseEvent, key: string, val: string) => void, | |
110 | - onToggleFullscreen: () => void, | |
111 | - mapper: {[type: string]: any}) => JSX.Element; | |
119 | + builder: ComponentBuilderFn; | |
112 | 120 | mapper?: {[type: string]: any}; |
113 | - onChange?: (key: (string | number)[], val: any) => void; | |
114 | - onColorClick?: (event: MouseEvent, key: string, val: string) => void; | |
121 | + onChange?: onChangeFn; | |
122 | + onColorClick?: OnColorClickFn; | |
115 | 123 | onChangeValidate?: (e: any) => void; |
116 | - onToggleFullscreen?: () => void; | |
124 | + onToggleFullscreen?: onToggleFullscreenFn; | |
117 | 125 | valid?: boolean; |
118 | 126 | error?: string; |
119 | 127 | options?: { | ... | ... |
... | ... | @@ -21,47 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; |
21 | 21 | $input-label-float-offset: 6px !default; |
22 | 22 | $input-label-float-scale: .75 !default; |
23 | 23 | |
24 | -.tb-json-form { | |
25 | - &.tb-fullscreen { | |
26 | - [name="ReactSchemaForm"] { | |
27 | - .SchemaForm { | |
28 | - &.SchemaFormFullscreen { | |
29 | - position: absolute; | |
30 | - top: 0; | |
31 | - right: 0; | |
32 | - bottom: 0; | |
33 | - left: 0; | |
34 | - | |
35 | - > div:not(.fullscreen-form-field) { | |
36 | - display: none !important; | |
37 | - } | |
38 | - | |
39 | - > div.fullscreen-form-field { | |
40 | - position: relative; | |
41 | - width: 100%; | |
42 | - height: 100%; | |
43 | - } | |
44 | - } | |
45 | - } | |
24 | +$previewSize: 100px !default; | |
46 | 25 | |
47 | - > div { | |
48 | - > section { | |
49 | - margin: 0 !important; | |
50 | - box-shadow: none !important; | |
51 | - | |
52 | - .SchemaGroupname { | |
53 | - display: none !important; | |
54 | - } | |
26 | +.tb-json-form { | |
55 | 27 | |
56 | - > div { | |
57 | - padding: 0 !important; | |
58 | - } | |
59 | - } | |
60 | - } | |
28 | + &.tb-fullscreen { | |
29 | + background: #fff; | |
30 | + position: absolute; | |
31 | + top: 0; | |
32 | + right: 0; | |
33 | + bottom: 0; | |
34 | + left: 0; | |
35 | + > div.fullscreen-form-field { | |
36 | + position: relative; | |
37 | + width: 100%; | |
38 | + height: 100%; | |
61 | 39 | } |
62 | 40 | } |
63 | 41 | |
64 | - | |
65 | 42 | .json-form-error { |
66 | 43 | position: relative; |
67 | 44 | bottom: -5px; |
... | ... | @@ -74,6 +51,7 @@ $input-label-float-scale: .75 !default; |
74 | 51 | |
75 | 52 | .tb-container { |
76 | 53 | position: relative; |
54 | + box-sizing: border-box; | |
77 | 55 | padding: 10px 0; |
78 | 56 | margin-top: 32px; |
79 | 57 | } |
... | ... | @@ -144,6 +122,7 @@ $input-label-float-scale: .75 !default; |
144 | 122 | |
145 | 123 | .tb-head-label { |
146 | 124 | color: rgba(0, 0, 0, .54); |
125 | + padding-bottom: 15px; | |
147 | 126 | } |
148 | 127 | |
149 | 128 | .SchemaGroupname { |
... | ... | @@ -154,4 +133,130 @@ $input-label-float-scale: .75 !default; |
154 | 133 | .invisible { |
155 | 134 | display: none; |
156 | 135 | } |
136 | + | |
137 | + span.tb-toggle-icon { | |
138 | + padding-top: 12px; | |
139 | + padding-bottom: 12px; | |
140 | + } | |
141 | + | |
142 | + .tb-button-toggle .tb-toggle-icon { | |
143 | + display: inline-block; | |
144 | + width: 15px; | |
145 | + margin: auto 0 auto auto; | |
146 | + background-size: 100% auto; | |
147 | + | |
148 | + transition: transform .3s, ease-in-out; | |
149 | + } | |
150 | + | |
151 | + .tb-button-toggle .tb-toggle-icon.tb-toggled { | |
152 | + transform: rotateZ(180deg); | |
153 | + } | |
154 | + | |
155 | + .fullscreen-form-field { | |
156 | + .json-form-ace-editor { | |
157 | + height: calc(100% - 60px); | |
158 | + } | |
159 | + } | |
160 | + | |
161 | + .json-form-ace-editor { | |
162 | + position: relative; | |
163 | + height: 100%; | |
164 | + border: 1px solid #c0c0c0; | |
165 | + | |
166 | + .title-panel { | |
167 | + position: absolute; | |
168 | + top: 10px; | |
169 | + right: 20px; | |
170 | + z-index: 5; | |
171 | + font-size: .8rem; | |
172 | + font-weight: 500; | |
173 | + | |
174 | + label { | |
175 | + padding: 4px; | |
176 | + color: #00acc1; | |
177 | + text-transform: uppercase; | |
178 | + background: rgba(220, 220, 220, .35); | |
179 | + border-radius: 5px; | |
180 | + } | |
181 | + | |
182 | + button.tidy-button { | |
183 | + background: rgba(220, 220, 220, .35) !important; | |
184 | + | |
185 | + span { | |
186 | + padding: 0 !important; | |
187 | + font-size: 12px !important; | |
188 | + } | |
189 | + } | |
190 | + } | |
191 | + } | |
192 | + | |
193 | + .tb-image-select-container { | |
194 | + position: relative; | |
195 | + width: 100%; | |
196 | + height: $previewSize; | |
197 | + } | |
198 | + | |
199 | + .tb-image-preview { | |
200 | + width: auto; | |
201 | + max-width: $previewSize; | |
202 | + height: auto; | |
203 | + max-height: $previewSize; | |
204 | + } | |
205 | + | |
206 | + .tb-image-preview-container { | |
207 | + position: relative; | |
208 | + float: left; | |
209 | + width: $previewSize; | |
210 | + height: $previewSize; | |
211 | + margin-right: 12px; | |
212 | + vertical-align: top; | |
213 | + border: solid 1px; | |
214 | + | |
215 | + div { | |
216 | + width: 100%; | |
217 | + font-size: 18px; | |
218 | + text-align: center; | |
219 | + } | |
220 | + | |
221 | + div, .tb-image-preview { | |
222 | + position: absolute; | |
223 | + top: 50%; | |
224 | + left: 50%; | |
225 | + transform: translate(-50%, -50%); | |
226 | + } | |
227 | + } | |
228 | + | |
229 | + .tb-dropzone { | |
230 | + outline: none; | |
231 | + position: relative; | |
232 | + height: $previewSize; | |
233 | + padding: 0 8px; | |
234 | + overflow: hidden; | |
235 | + vertical-align: top; | |
236 | + border: dashed 2px; | |
237 | + | |
238 | + div { | |
239 | + position: absolute; | |
240 | + top: 50%; | |
241 | + left: 50%; | |
242 | + width: 100%; | |
243 | + font-size: 24px; | |
244 | + text-align: center; | |
245 | + transform: translate(-50%, -50%); | |
246 | + } | |
247 | + } | |
248 | + | |
249 | + .tb-image-clear-container { | |
250 | + position: relative; | |
251 | + float: right; | |
252 | + width: 48px; | |
253 | + height: $previewSize; | |
254 | + } | |
255 | + | |
256 | + .tb-image-clear-btn { | |
257 | + position: absolute !important; | |
258 | + top: 50%; | |
259 | + transform: translate(0%, -50%) !important; | |
260 | + } | |
261 | + | |
157 | 262 | } | ... | ... |
... | ... | @@ -283,11 +283,6 @@ export interface LegendData { |
283 | 283 | data: Array<LegendKeyData>; |
284 | 284 | } |
285 | 285 | |
286 | -export interface WidgetConfigSettings { | |
287 | - [key: string]: any; | |
288 | - // TODO: | |
289 | -} | |
290 | - | |
291 | 286 | export enum WidgetActionType { |
292 | 287 | openDashboardState = 'openDashboardState', |
293 | 288 | updateDashboardState = 'updateDashboardState', |
... | ... | @@ -347,7 +342,7 @@ export interface WidgetConfig { |
347 | 342 | units?: string; |
348 | 343 | decimals?: number; |
349 | 344 | actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; |
350 | - settings?: WidgetConfigSettings; | |
345 | + settings?: any; | |
351 | 346 | alarmSource?: Datasource; |
352 | 347 | alarmSearchStatus?: AlarmSearchStatus; |
353 | 348 | alarmsPollingInterval?: number; | ... | ... |
... | ... | @@ -39,3 +39,12 @@ |
39 | 39 | margin: auto; |
40 | 40 | } |
41 | 41 | } |
42 | + | |
43 | +@mixin tb-checkered-bg() { | |
44 | + background-color: #fff; | |
45 | + background-image: | |
46 | + linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), | |
47 | + linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); | |
48 | + background-position: 0 0, 4px 4px; | |
49 | + background-size: 8px 8px; | |
50 | +} | ... | ... |
... | ... | @@ -852,4 +852,25 @@ mat-label { |
852 | 852 | } |
853 | 853 | } |
854 | 854 | } |
855 | + | |
856 | + .tb-color-preview { | |
857 | + cursor: pointer; | |
858 | + box-sizing: border-box; | |
859 | + position: relative; | |
860 | + width: 24px; | |
861 | + min-width: 24px; | |
862 | + height: 24px; | |
863 | + overflow: hidden; | |
864 | + content: ""; | |
865 | + border: 2px solid #fff; | |
866 | + border-radius: 50%; | |
867 | + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); | |
868 | + | |
869 | + @include tb-checkered-bg(); | |
870 | + | |
871 | + .tb-color-result { | |
872 | + width: 100%; | |
873 | + height: 100%; | |
874 | + } | |
875 | + } | |
855 | 876 | } | ... | ... |