Merge branch 'master' of github.com:thingsboard/thingsboard
This commit is contained in:
commit
e211e5759f
@ -19,7 +19,7 @@
|
|||||||
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
|
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
|
||||||
"hasBasicMode": true,
|
"hasBasicMode": true,
|
||||||
"basicModeDirective": "tb-flot-basic-config",
|
"basicModeDirective": "tb-flot-basic-config",
|
||||||
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}"
|
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}"
|
||||||
},
|
},
|
||||||
"externalId": null,
|
"externalId": null,
|
||||||
"tags": null
|
"tags": null
|
||||||
|
|||||||
@ -236,6 +236,15 @@ export class WidgetService {
|
|||||||
return this.http.get<PageData<WidgetTypeInfo>>(url, defaultHttpOptionsFromConfig(config));
|
return this.http.get<PageData<WidgetTypeInfo>>(url, defaultHttpOptionsFromConfig(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addWidgetFqnToWidgetBundle(widgetsBundleId: string, fqn: string, config?: RequestConfig) {
|
||||||
|
return this.getBundleWidgetTypeFqns(widgetsBundleId, config).pipe(
|
||||||
|
mergeMap(widgetsBundleFqn => {
|
||||||
|
widgetsBundleFqn.push(fqn);
|
||||||
|
return this.updateWidgetsBundleWidgetFqns(widgetsBundleId, widgetsBundleFqn, config);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public getWidgetTemplate(widgetTypeParam: widgetType,
|
public getWidgetTemplate(widgetTypeParam: widgetType,
|
||||||
config?: RequestConfig): Observable<WidgetInfo> {
|
config?: RequestConfig): Observable<WidgetInfo> {
|
||||||
const templateWidgetType = widgetTypesData.get(widgetTypeParam);
|
const templateWidgetType = widgetTypesData.get(widgetTypeParam);
|
||||||
|
|||||||
@ -110,6 +110,7 @@
|
|||||||
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
|
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
|
||||||
<tb-widgets-bundle-select fxFlexOffset="5"
|
<tb-widgets-bundle-select fxFlexOffset="5"
|
||||||
fxFlex
|
fxFlex
|
||||||
|
required
|
||||||
[selectFirstBundle]="false"
|
[selectFirstBundle]="false"
|
||||||
[selectBundleAlias]="selectedWidgetsBundleAlias"
|
[selectBundleAlias]="selectedWidgetsBundleAlias"
|
||||||
[ngModel]="null"
|
[ngModel]="null"
|
||||||
|
|||||||
@ -297,7 +297,7 @@ export class ImportExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public importWidgetType(): Observable<WidgetType> {
|
public importWidgetType(): Observable<WidgetTypeDetails> {
|
||||||
return this.openImportDialog('widget.import', 'widget-type.widget-file').pipe(
|
return this.openImportDialog('widget.import', 'widget-type.widget-file').pipe(
|
||||||
mergeMap((widgetTypeDetails: WidgetTypeDetails) => {
|
mergeMap((widgetTypeDetails: WidgetTypeDetails) => {
|
||||||
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
|
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
|
||||||
@ -309,9 +309,7 @@ export class ImportExportService {
|
|||||||
return this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails);
|
return this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError((err) => {
|
catchError(() => of(null))
|
||||||
return of(null);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.ctx.$scope.aggregatedValueCardWidget = this;
|
this.ctx.$scope.aggregatedValueCardWidget = this;
|
||||||
this.settings = {...aggregatedValueCardDefaultSettings, ...this.ctx.settings};
|
this.settings = {...aggregatedValueCardDefaultSettings, ...this.ctx.settings};
|
||||||
this.showSubtitle = this.settings.showSubtitle;
|
this.showSubtitle = this.settings.showSubtitle && this.ctx.datasources?.length > 0;
|
||||||
const subtitle = this.settings.subtitle;
|
const subtitle = this.settings.subtitle;
|
||||||
this.subtitle$ = this.ctx.registerLabelPattern(subtitle, this.subtitle$);
|
this.subtitle$ = this.ctx.registerLabelPattern(subtitle, this.subtitle$);
|
||||||
this.subtitleStyle = textStyle(this.settings.subtitleFont);
|
this.subtitleStyle = textStyle(this.settings.subtitleFont);
|
||||||
@ -156,7 +156,7 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
if (this.showChart) {
|
if (this.showChart && this.ctx.datasources?.length) {
|
||||||
const settings = {
|
const settings = {
|
||||||
shadowSize: 0,
|
shadowSize: 0,
|
||||||
enableSelection: false,
|
enableSelection: false,
|
||||||
|
|||||||
@ -60,8 +60,10 @@ export class FlotWidgetComponent implements OnInit {
|
|||||||
this.settings = this.ctx.settings;
|
this.settings = this.ctx.settings;
|
||||||
this.chartType = this.chartType || 'line';
|
this.chartType = this.chartType || 'line';
|
||||||
this.configureLegend();
|
this.configureLegend();
|
||||||
|
if (this.ctx.datasources?.length) {
|
||||||
this.flot = new TbFlot(this.ctx, this.chartType, $(this.flotElement.nativeElement));
|
this.flot = new TbFlot(this.ctx, this.chartType, $(this.flotElement.nativeElement));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private configureLegend(): void {
|
private configureLegend(): void {
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<div class="fixed-title-width" translate>date.format</div>
|
<div class="fixed-title-width" translate>date.format</div>
|
||||||
<mat-form-field class="tb-date-format-input" required fxFlex appearance="outline" subscriptSizing="dynamic">
|
<mat-form-field class="tb-date-format-input" required fxFlex appearance="outline" subscriptSizing="dynamic">
|
||||||
<input matInput [formControl]="dateFormatFormControl" placeholder="{{ 'widget-config.set' | translate }}">
|
<input matInput [formControl]="dateFormatFormControl" placeholder="{{ 'widget-config.set' | translate }}">
|
||||||
<div matSuffix tb-help-popup="date/date-format" [tb-help-popup-style]="{width: '800px'}"></div>
|
<div matIconSuffix tb-help-popup="date/date-format" [tb-help-popup-style]="{width: '800px'}"></div>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|||||||
@ -48,6 +48,7 @@
|
|||||||
}
|
}
|
||||||
.mat-mdc-form-field.tb-date-format-input {
|
.mat-mdc-form-field.tb-date-format-input {
|
||||||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
|
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
|
||||||
|
.mat-mdc-form-field-flex {
|
||||||
.mat-mdc-form-field-icon-suffix {
|
.mat-mdc-form-field-icon-suffix {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -56,6 +57,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.tb-date-format-settings-panel-buttons {
|
.tb-date-format-settings-panel-buttons {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -479,7 +479,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
|
|||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
padding: this.padding,
|
padding: this.padding,
|
||||||
margin: this.margin,
|
margin: this.margin,
|
||||||
borderRadius: this.borderRadius};
|
borderRadius: this.borderRadius || 'unset' };
|
||||||
if (this.widget.config.widgetStyle) {
|
if (this.widget.config.widgetStyle) {
|
||||||
this.style = {...this.style, ...this.widget.config.widgetStyle};
|
this.style = {...this.style, ...this.widget.config.widgetStyle};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
<form [formGroup]="saveWidgetTypeAsFormGroup" (ngSubmit)="saveAs()">
|
<form [formGroup]="saveWidgetTypeAsFormGroup" (ngSubmit)="saveAs()" style="width: 360px">
|
||||||
<mat-toolbar color="primary">
|
<mat-toolbar color="primary">
|
||||||
<h2 translate>widget.save-widget-as</h2>
|
<h2 translate>widget.save-widget-as</h2>
|
||||||
<span fxFlex></span>
|
<span fxFlex></span>
|
||||||
@ -29,7 +29,6 @@
|
|||||||
</mat-progress-bar>
|
</mat-progress-bar>
|
||||||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
|
||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<fieldset>
|
|
||||||
<span translate>widget.save-widget-as-text</span>
|
<span translate>widget.save-widget-as-text</span>
|
||||||
<mat-form-field class="mat-block">
|
<mat-form-field class="mat-block">
|
||||||
<mat-label translate>widget.title</mat-label>
|
<mat-label translate>widget.title</mat-label>
|
||||||
@ -38,7 +37,10 @@
|
|||||||
{{ 'widget.title-required' | translate }}
|
{{ 'widget.title-required' | translate }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</fieldset>
|
<tb-widgets-bundle-select
|
||||||
|
formControlName="widgetsBundle"
|
||||||
|
bundlesScope="{{bundlesScope}}">
|
||||||
|
</tb-widgets-bundle-select>
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions fxLayoutAlign="end center">
|
<div mat-dialog-actions fxLayoutAlign="end center">
|
||||||
<button mat-button color="primary"
|
<button mat-button color="primary"
|
||||||
|
|||||||
@ -18,12 +18,15 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { MatDialogRef } from '@angular/material/dialog';
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { DialogComponent } from '@shared/components/dialog.component';
|
import { DialogComponent } from '@shared/components/dialog.component';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||||
|
import { Authority } from '@shared/models/authority.enum';
|
||||||
|
|
||||||
export interface SaveWidgetTypeAsDialogResult {
|
export interface SaveWidgetTypeAsDialogResult {
|
||||||
widgetName: string;
|
widgetName: string;
|
||||||
|
widgetBundleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -34,18 +37,27 @@ export interface SaveWidgetTypeAsDialogResult {
|
|||||||
export class SaveWidgetTypeAsDialogComponent extends
|
export class SaveWidgetTypeAsDialogComponent extends
|
||||||
DialogComponent<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult> implements OnInit {
|
DialogComponent<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult> implements OnInit {
|
||||||
|
|
||||||
saveWidgetTypeAsFormGroup: UntypedFormGroup;
|
saveWidgetTypeAsFormGroup: FormGroup;
|
||||||
|
bundlesScope: string;
|
||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>,
|
public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>,
|
||||||
public fb: UntypedFormBuilder) {
|
public fb: FormBuilder) {
|
||||||
super(store, router, dialogRef);
|
super(store, router, dialogRef);
|
||||||
|
|
||||||
|
const authUser = getCurrentAuthUser(store);
|
||||||
|
if (authUser.authority === Authority.TENANT_ADMIN) {
|
||||||
|
this.bundlesScope = 'tenant';
|
||||||
|
} else {
|
||||||
|
this.bundlesScope = 'system';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.saveWidgetTypeAsFormGroup = this.fb.group({
|
this.saveWidgetTypeAsFormGroup = this.fb.group({
|
||||||
title: [null, [Validators.required]]
|
title: [null, [Validators.required]],
|
||||||
|
widgetsBundle: [null]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +67,10 @@ export class SaveWidgetTypeAsDialogComponent extends
|
|||||||
|
|
||||||
saveAs(): void {
|
saveAs(): void {
|
||||||
const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value;
|
const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value;
|
||||||
|
const widgetBundleId: string = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value?.id?.id;
|
||||||
const result: SaveWidgetTypeAsDialogResult = {
|
const result: SaveWidgetTypeAsDialogResult = {
|
||||||
widgetName
|
widgetName,
|
||||||
|
widgetBundleId
|
||||||
};
|
};
|
||||||
this.dialogRef.close(result);
|
this.dialogRef.close(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ import {
|
|||||||
SaveWidgetTypeAsDialogComponent,
|
SaveWidgetTypeAsDialogComponent,
|
||||||
SaveWidgetTypeAsDialogResult
|
SaveWidgetTypeAsDialogResult
|
||||||
} from '@home/pages/widget/save-widget-type-as-dialog.component';
|
} from '@home/pages/widget/save-widget-type-as-dialog.component';
|
||||||
import { forkJoin, Subscription } from 'rxjs';
|
import { forkJoin, mergeMap, of, Subscription } from 'rxjs';
|
||||||
import { ResizeObserver } from '@juggle/resize-observer';
|
import { ResizeObserver } from '@juggle/resize-observer';
|
||||||
import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models';
|
import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
@ -563,7 +563,17 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
|
|||||||
private commitSaveWidget() {
|
private commitSaveWidget() {
|
||||||
const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined;
|
const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined;
|
||||||
const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined;
|
const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined;
|
||||||
this.widgetService.saveWidgetTypeDetails(this.widget, id, createdTime).subscribe({
|
this.widgetService.saveWidgetTypeDetails(this.widget, id, createdTime).pipe(
|
||||||
|
mergeMap((widgetTypeDetails) => {
|
||||||
|
const widgetsBundleId = this.route.snapshot.params.widgetsBundleId as string;
|
||||||
|
if (widgetsBundleId && !id) {
|
||||||
|
return this.widgetService.addWidgetFqnToWidgetBundle(widgetsBundleId, widgetTypeDetails.fqn).pipe(
|
||||||
|
map(() => widgetTypeDetails)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return of(widgetTypeDetails);
|
||||||
|
})
|
||||||
|
).subscribe({
|
||||||
next: (widgetTypeDetails) => {
|
next: (widgetTypeDetails) => {
|
||||||
this.saveWidgetPending = false;
|
this.saveWidgetPending = false;
|
||||||
if (!this.widgetTypeDetails?.id) {
|
if (!this.widgetTypeDetails?.id) {
|
||||||
@ -595,11 +605,25 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
|
|||||||
config.title = this.widget.widgetName;
|
config.title = this.widget.widgetName;
|
||||||
this.widget.defaultConfig = JSON.stringify(config);
|
this.widget.defaultConfig = JSON.stringify(config);
|
||||||
this.isDirty = false;
|
this.isDirty = false;
|
||||||
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined).subscribe(
|
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined).pipe(
|
||||||
|
mergeMap((widget) => {
|
||||||
|
if (saveWidgetAsData.widgetBundleId) {
|
||||||
|
return this.widgetService.addWidgetFqnToWidgetBundle(saveWidgetAsData.widgetBundleId, widget.fqn).pipe(
|
||||||
|
map(() => widget)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return of(widget);
|
||||||
|
})
|
||||||
|
).subscribe(
|
||||||
{
|
{
|
||||||
next: (widgetTypeDetails) => {
|
next: (widgetTypeDetails) => {
|
||||||
this.saveWidgetAsPending = false;
|
this.saveWidgetAsPending = false;
|
||||||
this.router.navigate(['..', widgetTypeDetails.id.id], {relativeTo: this.route});
|
if (saveWidgetAsData.widgetBundleId) {
|
||||||
|
this.router.navigate(['resources', 'widgets-library', 'widgets-bundles',
|
||||||
|
saveWidgetAsData.widgetBundleId, widgetTypeDetails.id.id]);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['resources', 'widgets-library', 'widget-types', widgetTypeDetails.id.id]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.saveWidgetAsPending = false;
|
this.saveWidgetAsPending = false;
|
||||||
|
|||||||
@ -27,6 +27,23 @@
|
|||||||
</button>
|
</button>
|
||||||
<span class="mat-headline-5">{{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }}</span>
|
<span class="mat-headline-5">{{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }}</span>
|
||||||
<span fxFlex></span>
|
<span fxFlex></span>
|
||||||
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
||||||
|
*ngIf="!isReadOnly"
|
||||||
|
matTooltip="{{ 'widget.add-new-widget' | translate }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
[matMenuTriggerFor]="addWidgetMenu">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #addWidgetMenu="matMenu" xPosition="before">
|
||||||
|
<button mat-menu-item (click)="addWidgetType($event)">
|
||||||
|
<mat-icon>insert_drive_file</mat-icon>
|
||||||
|
<span>{{ 'dashboard.create-new-widget' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="importWidgetType()">
|
||||||
|
<mat-icon>file_upload</mat-icon>
|
||||||
|
<span>{{ 'widget.import' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
<button mat-icon-button
|
<button mat-icon-button
|
||||||
matTooltip="{{ 'widgets-bundle.export' | translate }}"
|
matTooltip="{{ 'widgets-bundle.export' | translate }}"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
@ -83,18 +100,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="editMode && !addMode && widgets?.length" class="tb-add-widget-button"
|
<div *ngIf="editMode && !addMode && widgets?.length" class="tb-add-widget-button"
|
||||||
matTooltip="{{ 'widget.add' | translate }}"
|
matTooltip="{{ 'widget.add-existing-widget' | translate }}"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
(click)="addWidgetMode()">
|
(click)="addWidgetMode()">
|
||||||
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon>
|
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="editMode && !addMode && !widgets?.length" class="tb-add-widget-button-panel">
|
<div *ngIf="editMode && !addMode && !widgets?.length" class="tb-add-widget-button-panel">
|
||||||
<div class="tb-add-widget-button-with-text"
|
<div class="tb-add-widget-button-with-text" (click)="addWidgetMode()">
|
||||||
matTooltip="{{ 'widget.add' | translate }}"
|
|
||||||
matTooltipPosition="above"
|
|
||||||
(click)="addWidgetMode()">
|
|
||||||
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon>
|
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon>
|
||||||
<div class="tb-add-widget-button-text" translate>widget.add</div>
|
<div class="tb-add-widget-button-text" translate>widget.add-existing-widget</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isReadOnly && !widgets?.length" class="tb-no-data-available">
|
<div *ngIf="isReadOnly && !widgets?.length" class="tb-no-data-available">
|
||||||
|
|||||||
@ -24,13 +24,17 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
|||||||
import { Authority } from '@shared/models/authority.enum';
|
import { Authority } from '@shared/models/authority.enum';
|
||||||
import { NULL_UUID } from '@shared/models/id/has-uuid';
|
import { NULL_UUID } from '@shared/models/id/has-uuid';
|
||||||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
|
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
|
||||||
import { WidgetTypeInfo } from '@shared/models/widget.models';
|
import { widgetType as WidgetDataType, WidgetTypeInfo } from '@shared/models/widget.models';
|
||||||
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||||
import { ImportExportService } from '@home/components/import-export/import-export.service';
|
import { ImportExportService } from '@home/components/import-export/import-export.service';
|
||||||
import { WidgetService } from '@core/http/widget.service';
|
import { WidgetService } from '@core/http/widget.service';
|
||||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||||
import { isDefinedAndNotNull } from '@core/utils';
|
import { isDefinedAndNotNull } from '@core/utils';
|
||||||
import { FormControl, Validators } from '@angular/forms';
|
import { FormControl, Validators } from '@angular/forms';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
|
||||||
|
|
||||||
|
type WidgetTypeBundle = WithOptional<WidgetTypeInfo, 'widgetType'>;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-widgets-bundle-widget',
|
selector: 'tb-widgets-bundle-widget',
|
||||||
@ -47,7 +51,7 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn
|
|||||||
isDirty = false;
|
isDirty = false;
|
||||||
|
|
||||||
widgetsBundle: WidgetsBundle;
|
widgetsBundle: WidgetsBundle;
|
||||||
widgets: Array<WidgetTypeInfo>;
|
widgets: Array<WidgetTypeBundle>;
|
||||||
excludeWidgetTypeIds: Array<string>;
|
excludeWidgetTypeIds: Array<string>;
|
||||||
|
|
||||||
addWidgetFormControl = new FormControl(null, [Validators.required]);
|
addWidgetFormControl = new FormControl(null, [Validators.required]);
|
||||||
@ -58,7 +62,8 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn
|
|||||||
private widgetsService: WidgetService,
|
private widgetsService: WidgetService,
|
||||||
private importExport: ImportExportService,
|
private importExport: ImportExportService,
|
||||||
private sanitizer: DomSanitizer,
|
private sanitizer: DomSanitizer,
|
||||||
private cd: ChangeDetectorRef) {
|
private cd: ChangeDetectorRef,
|
||||||
|
private dialog: MatDialog) {
|
||||||
super(store);
|
super(store);
|
||||||
this.authUser = getCurrentAuthUser(this.store);
|
this.authUser = getCurrentAuthUser(this.store);
|
||||||
this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
|
this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
|
||||||
@ -88,7 +93,7 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn
|
|||||||
return '/assets/widget-preview-empty.svg';
|
return '/assets/widget-preview-empty.svg';
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByWidget(index: number, widget: WidgetTypeInfo): any {
|
trackByWidget(index: number, widget: WidgetTypeBundle): any {
|
||||||
return widget;
|
return widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,27 +114,27 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn
|
|||||||
this.addMode = false;
|
this.addMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addWidget(newWidget: WidgetTypeInfo) {
|
private addWidget(newWidget: WidgetTypeBundle) {
|
||||||
this.widgets.push(newWidget);
|
this.widgets.push(newWidget);
|
||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
this.addMode = false;
|
this.addMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
openWidgetEditor($event: Event, widgetType: WidgetTypeInfo) {
|
openWidgetEditor($event: Event, widgetType: WidgetTypeBundle) {
|
||||||
if ($event) {
|
if ($event) {
|
||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
}
|
}
|
||||||
this.router.navigate([widgetType.id.id], {relativeTo: this.route}).then(()=> {});
|
this.router.navigate([widgetType.id.id], {relativeTo: this.route}).then(()=> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
exportWidgetType($event: Event, widgetType: WidgetTypeInfo) {
|
exportWidgetType($event: Event, widgetType: WidgetTypeBundle) {
|
||||||
if ($event) {
|
if ($event) {
|
||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
}
|
}
|
||||||
this.importExport.exportWidgetType(widgetType.id.id);
|
this.importExport.exportWidgetType(widgetType.id.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeWidgetType($event: Event, widgetType: WidgetTypeInfo) {
|
removeWidgetType($event: Event, widgetType: WidgetTypeBundle) {
|
||||||
if ($event) {
|
if ($event) {
|
||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
}
|
}
|
||||||
@ -176,4 +181,44 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addWidgetType($event: Event): void {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
this.dialog.open<SelectWidgetTypeDialogComponent, any,
|
||||||
|
WidgetDataType>(SelectWidgetTypeDialogComponent, {
|
||||||
|
disableClose: true,
|
||||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
|
||||||
|
}).afterClosed().subscribe(
|
||||||
|
(type) => {
|
||||||
|
if (type) {
|
||||||
|
this.router.navigate([type], {relativeTo: this.route}).then(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
importWidgetType() {
|
||||||
|
this.importExport.importWidgetType().subscribe(
|
||||||
|
(widgetType) => {
|
||||||
|
if (widgetType) {
|
||||||
|
const isExistWidget = this.widgets.some(widget => widget.id.id === widgetType.id.id);
|
||||||
|
if (isExistWidget) {
|
||||||
|
this.widgets = this.widgets.map(widget => widget.id.id !== widgetType.id.id ? widget : widgetType);
|
||||||
|
}
|
||||||
|
if (this.editMode) {
|
||||||
|
this.widgets.push(widgetType);
|
||||||
|
this.isDirty = true;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
} else if (!isExistWidget) {
|
||||||
|
this.widgetsService.addWidgetFqnToWidgetBundle(this.widgetsBundle.id.id, widgetType.fqn).subscribe(() => {
|
||||||
|
this.widgets.push(widgetType);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,9 @@
|
|||||||
<mat-select [required]="required"
|
<mat-select [required]="required"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[(ngModel)]="widgetsBundle"
|
[(ngModel)]="widgetsBundle"
|
||||||
matInput
|
|
||||||
panelClass="tb-widgets-bundle-select"
|
panelClass="tb-widgets-bundle-select"
|
||||||
placeholder="{{ 'widget.select-widgets-bundle' | translate }}"
|
placeholder="{{ 'widget.select-widgets-bundle' | translate }}"
|
||||||
|
(blur)="onTouched()"
|
||||||
(ngModelChange)="widgetsBundleChanged()">
|
(ngModelChange)="widgetsBundleChanged()">
|
||||||
<mat-select-trigger>
|
<mat-select-trigger>
|
||||||
<div class="tb-bundle-item">
|
<div class="tb-bundle-item">
|
||||||
@ -29,6 +29,8 @@
|
|||||||
<span translate class="tb-bundle-system" *ngIf="isSystem(widgetsBundle)">widgets-bundle.system</span>
|
<span translate class="tb-bundle-system" *ngIf="isSystem(widgetsBundle)">widgets-bundle.system</span>
|
||||||
</div>
|
</div>
|
||||||
</mat-select-trigger>
|
</mat-select-trigger>
|
||||||
|
<mat-option [value]="null" *ngIf="!required">
|
||||||
|
</mat-option>
|
||||||
<mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle">
|
<mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle">
|
||||||
<div class="tb-bundle-item">
|
<div class="tb-bundle-item">
|
||||||
<span>{{widgetsBundle.title}}</span>
|
<span>{{widgetsBundle.title}}</span>
|
||||||
|
|||||||
@ -20,12 +20,12 @@ import { Observable } from 'rxjs';
|
|||||||
import { map, share, tap } from 'rxjs/operators';
|
import { map, share, tap } from 'rxjs/operators';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@app/core/core.state';
|
import { AppState } from '@app/core/core.state';
|
||||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
||||||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
|
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
|
||||||
import { WidgetService } from '@core/http/widget.service';
|
import { WidgetService } from '@core/http/widget.service';
|
||||||
import { isDefined } from '@core/utils';
|
import { isDefined } from '@core/utils';
|
||||||
import { NULL_UUID } from '@shared/models/id/has-uuid';
|
import { NULL_UUID } from '@shared/models/id/has-uuid';
|
||||||
import { getCurrentAuthState } from '@core/auth/auth.selectors';
|
import { getCurrentAuthState } from '@core/auth/auth.selectors';
|
||||||
|
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-widgets-bundle-select',
|
selector: 'tb-widgets-bundle-select',
|
||||||
@ -44,19 +44,15 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni
|
|||||||
bundlesScope: 'system' | 'tenant';
|
bundlesScope: 'system' | 'tenant';
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
|
@coerceBoolean()
|
||||||
selectFirstBundle: boolean;
|
selectFirstBundle: boolean;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
selectBundleAlias: string;
|
selectBundleAlias: string;
|
||||||
|
|
||||||
private requiredValue: boolean;
|
|
||||||
get required(): boolean {
|
|
||||||
return this.requiredValue;
|
|
||||||
}
|
|
||||||
@Input()
|
@Input()
|
||||||
set required(value: boolean) {
|
@coerceBoolean()
|
||||||
this.requiredValue = coerceBooleanProperty(value);
|
required: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@ -70,7 +66,8 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni
|
|||||||
|
|
||||||
widgetsBundle: WidgetsBundle | null;
|
widgetsBundle: WidgetsBundle | null;
|
||||||
|
|
||||||
private propagateChange = (v: any) => { };
|
onTouched = () => {};
|
||||||
|
private propagateChange: (value: any) => void = () => {};
|
||||||
|
|
||||||
constructor(private store: Store<AppState>,
|
constructor(private store: Store<AppState>,
|
||||||
private widgetService: WidgetService) {
|
private widgetService: WidgetService) {
|
||||||
@ -81,6 +78,7 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerOnTouched(fn: any): void {
|
registerOnTouched(fn: any): void {
|
||||||
|
this.onTouched = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|||||||
@ -4388,6 +4388,7 @@
|
|||||||
"volt": "Volt",
|
"volt": "Volt",
|
||||||
"kilovolts": "Kilovolts",
|
"kilovolts": "Kilovolts",
|
||||||
"dbmV": "dBmV",
|
"dbmV": "dBmV",
|
||||||
|
"dbm": "dBm",
|
||||||
"volt-meter": "Volt-Meter",
|
"volt-meter": "Volt-Meter",
|
||||||
"kilovolt-meter": "Kilovolt-Meter",
|
"kilovolt-meter": "Kilovolt-Meter",
|
||||||
"megavolt-meter": "Megavolt-Meter",
|
"megavolt-meter": "Megavolt-Meter",
|
||||||
@ -4759,7 +4760,7 @@
|
|||||||
"no-widgets-text": "No widgets found",
|
"no-widgets-text": "No widgets found",
|
||||||
"management": "Widget management",
|
"management": "Widget management",
|
||||||
"editor": "Widget Editor",
|
"editor": "Widget Editor",
|
||||||
"confirm-to-exit-editor-html": "You have unsaved default widget settings.<br>Are you sure you want to leave this page?",
|
"confirm-to-exit-editor-html": "You have unsaved widget settings.<br>Are you sure you want to leave this page?",
|
||||||
"widget-type-not-found": "Problem loading widget configuration.<br>Probably associated\n widget type was removed.",
|
"widget-type-not-found": "Problem loading widget configuration.<br>Probably associated\n widget type was removed.",
|
||||||
"widget-type-load-error": "Widget wasn't loaded due to the following errors:",
|
"widget-type-load-error": "Widget wasn't loaded due to the following errors:",
|
||||||
"remove": "Remove widget",
|
"remove": "Remove widget",
|
||||||
@ -4827,6 +4828,8 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"widget-details": "Widget details",
|
"widget-details": "Widget details",
|
||||||
"add": "Add Widget",
|
"add": "Add Widget",
|
||||||
|
"add-existing-widget": "Add existing widget",
|
||||||
|
"add-new-widget": "Add new widget",
|
||||||
"search-widgets": "Search widgets",
|
"search-widgets": "Search widgets",
|
||||||
"selected-widgets": "{ count, plural, =1 {1 widget} other {# widgets} } selected",
|
"selected-widgets": "{ count, plural, =1 {1 widget} other {# widgets} } selected",
|
||||||
"undo": "Undo widget changes",
|
"undo": "Undo widget changes",
|
||||||
|
|||||||
@ -938,6 +938,11 @@
|
|||||||
"symbol": "dBmV",
|
"symbol": "dBmV",
|
||||||
"tags": ["decibels millivolt","voltage level","signal","dBmV"]
|
"tags": ["decibels millivolt","voltage level","signal","dBmV"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "unit.dbm",
|
||||||
|
"symbol": "dBm",
|
||||||
|
"tags": ["decibel milliwatts","output power","signal","dBm"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "unit.volt-meter",
|
"name": "unit.volt-meter",
|
||||||
"symbol": "V·m",
|
"symbol": "V·m",
|
||||||
|
|||||||
6
ui-ngx/src/typings/utils.d.ts
vendored
6
ui-ngx/src/typings/utils.d.ts
vendored
@ -19,3 +19,9 @@ type NestedKeyOf<ObjectType extends object> =
|
|||||||
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]> extends infer U extends string ? U : never}`
|
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]> extends infer U extends string ? U : never}`
|
||||||
: `${Key}`
|
: `${Key}`
|
||||||
}[keyof ObjectType & (string | number)];
|
}[keyof ObjectType & (string | number)];
|
||||||
|
|
||||||
|
type AllKeyOf<T> = T extends never ? never : keyof T;
|
||||||
|
|
||||||
|
type Optional<T, K> = { [P in Extract<keyof T, K>]?: T[P] };
|
||||||
|
|
||||||
|
type WithOptional<T, K extends AllKeyOf<T>> = T extends never ? never : Omit<T, K> & Optional<T, K>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user