diff --git a/ui-ngx/patches/angular-gridster2+15.0.4.patch b/ui-ngx/patches/angular-gridster2+15.0.4.patch new file mode 100644 index 0000000000..a6642928c3 --- /dev/null +++ b/ui-ngx/patches/angular-gridster2+15.0.4.patch @@ -0,0 +1,44 @@ +diff --git a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs +index cf4e220..4275d11 100644 +--- a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs ++++ b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs +@@ -208,6 +208,7 @@ const GridsterConfigService = { + useTransformPositioning: true, + scrollSensitivity: 10, + scrollSpeed: 20, ++ colWidthUpdateCallback: undefined, + initCallback: undefined, + destroyCallback: undefined, + gridSizeChangedCallback: undefined, +@@ -1243,6 +1244,9 @@ class GridsterComponent { + this.renderer.setStyle(this.el, 'padding-right', this.$options.margin + 'px'); + } + this.curColWidth = (this.curWidth - marginWidth) / this.columns; ++ if (this.options.colWidthUpdateCallback) { ++ this.curColWidth = this.options.colWidthUpdateCallback(this.curColWidth); ++ } + let marginHeight = -this.$options.margin; + if (this.$options.outerMarginTop !== null) { + marginHeight += this.$options.outerMarginTop; +@@ -1266,6 +1270,9 @@ class GridsterComponent { + } + else { + this.curColWidth = (this.curWidth + this.$options.margin) / this.columns; ++ if (this.options.colWidthUpdateCallback) { ++ this.curColWidth = this.options.colWidthUpdateCallback(this.curColWidth); ++ } + this.curRowHeight = + ((this.curHeight + this.$options.margin) / this.rows) * + this.$options.rowHeightRatio; +diff --git a/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts b/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts +index 1d7cdf0..a712b35 100644 +--- a/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts ++++ b/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts +@@ -73,6 +73,7 @@ export interface GridsterConfig { + useTransformPositioning?: boolean; + scrollSensitivity?: number | null; + scrollSpeed?: number; ++ colWidthUpdateCallback?: (colWidth: number) => number; + initCallback?: (gridster: GridsterComponentInterface) => void; + destroyCallback?: (gridster: GridsterComponentInterface) => void; + gridSizeChangedCallback?: (gridster: GridsterComponentInterface) => void; diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index ef367dfb1e..12a7e687bd 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -610,17 +610,15 @@ export class DashboardUtilsService { originalColumns = 24; } const gridSettings = layout.gridSettings; - if (!gridSettings.isScada) { - let columns = 24; - if (gridSettings && gridSettings.columns) { - columns = gridSettings.columns; - } - columns = columns * layoutCount; - if (columns !== originalColumns) { - const ratio = columns / originalColumns; - widgetLayout.sizeX *= ratio; - widgetLayout.sizeY *= ratio; - } + let columns = 24; + if (gridSettings && gridSettings.columns) { + columns = gridSettings.columns; + } + columns = columns * layoutCount; + if (columns !== originalColumns) { + const ratio = columns / originalColumns; + widgetLayout.sizeX *= ratio; + widgetLayout.sizeY *= ratio; } if (row > -1 && column > - 1) { @@ -686,35 +684,32 @@ export class DashboardUtilsService { const columns = gridSettings.columns || 24; const ratio = columns / prevColumns; layout.gridSettings = gridSettings; - if (!gridSettings.isScada) { - let maxRow = 0; - for (const w of Object.keys(layout.widgets)) { - const widget = layout.widgets[w]; - if (!widget.sizeX) { - widget.sizeX = 1; - } - if (!widget.sizeY) { - widget.sizeY = 1; - } - maxRow = Math.max(maxRow, widget.row + widget.sizeY); + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + if (!widget.sizeX) { + widget.sizeX = 1; } - const newMaxRow = Math.round(maxRow * ratio); - for (const w of Object.keys(layout.widgets)) { - const widget = layout.widgets[w]; - if (widget.row + widget.sizeY === maxRow) { - widget.row = Math.round(widget.row * ratio); - widget.sizeY = newMaxRow - widget.row; - } else { - widget.row = Math.round(widget.row * ratio); - widget.sizeY = Math.round(widget.sizeY * ratio); - } - widget.sizeX = Math.round(widget.sizeX * ratio); - widget.col = Math.round(widget.col * ratio); - if (widget.col + widget.sizeX > columns) { - widget.sizeX = columns - widget.col; - } + if (!widget.sizeY) { + widget.sizeY = 1; } } + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + widget.row = Math.round(widget.row * ratio); + widget.col = Math.round(widget.col * ratio); + widget.sizeX = Math.round(widget.sizeX * ratio); + widget.sizeY = Math.round(widget.sizeY * ratio); + } + } + + public moveWidgets(layout: DashboardLayout, cols: number, rows: number) { + cols = isDefinedAndNotNull(cols) ? Math.round(cols) : 0; + rows = isDefinedAndNotNull(rows) ? Math.round(rows) : 0; + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + widget.col = Math.max(0, widget.col + cols); + widget.row = Math.max(0, widget.row + rows); + } } public removeUnusedWidgets(dashboard: Dashboard) { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 3e96e13b0a..0ea553b508 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -150,6 +150,10 @@ import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-pag import { TbPopoverComponent } from '@shared/components/popover.component'; import { ResizeObserver } from '@juggle/resize-observer'; import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; +import { + MoveWidgetsDialogComponent, + MoveWidgetsDialogResult +} from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; // @dynamic @Component({ @@ -285,7 +289,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC gridSettings: {}, ignoreLoading: true, ctrl: null, - dashboardCtrl: this + dashboardCtrl: this, + displayGrid: 'onDrag&Resize' } }, right: { @@ -297,7 +302,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC gridSettings: {}, ignoreLoading: true, ctrl: null, - dashboardCtrl: this + dashboardCtrl: this, + displayGrid: 'onDrag&Resize' } } }; @@ -913,6 +919,28 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }); } + private moveWidgets($event: Event, layoutId: DashboardLayoutId) { + if ($event) { + $event.stopPropagation(); + } + this.layouts[layoutId].layoutCtx.displayGrid = 'always'; + this.cd.markForCheck(); + this.dialog.open(MoveWidgetsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe((result) => { + this.layouts[layoutId].layoutCtx.displayGrid = 'onDrag&Resize'; + if (result) { + const targetLayout = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts[layoutId]; + this.dashboardUtils.moveWidgets(targetLayout, result.cols, result.rows); + this.updateLayouts(); + } else { + this.cd.markForCheck(); + } + }); + } + private updateDashboardLayouts(newLayouts: DashboardStateLayouts) { this.dashboardUtils.setLayouts(this.dashboard, this.dashboardCtx.state, newLayouts); this.updateLayouts(); @@ -1422,6 +1450,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC shortcut: 'M-I' } ); + dashboardContextActions.push( + { + action: ($event) => { + this.moveWidgets($event, layoutCtx.id); + }, + enabled: true, + value: 'dashboard.move-all-widgets', + icon: 'open_with' + } + ); } return dashboardContextActions; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts index b2ed609c29..94dfaf635e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts @@ -21,6 +21,7 @@ import { IAliasController, IStateController } from '@core/api/widget-api.models' import { ILayoutController } from './layout/layout.models'; import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; import { Observable } from 'rxjs'; +import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; export declare type DashboardPageScope = 'tenant' | 'customer'; @@ -69,6 +70,7 @@ export interface DashboardPageLayoutContext { ctrl: ILayoutController; dashboardCtrl: IDashboardController; ignoreLoading: boolean; + displayGrid: displayGrids; } export interface DashboardPageLayout { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index e2d0a40cf5..50460e808c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -114,8 +114,9 @@
dashboard.layout-settings
dashboard.columns-count
- - + + + + + {{ columns }} + + + +
+
+
dashboard.min-layout-width
+ + + + warning + + dashboard.columns-suffix +
dashboard.widgets-margins
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index bea429798f..6d405e9ef1 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts @@ -54,6 +54,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent([ ['default', 'dashboard.state-controller-default'], ]); @@ -155,10 +157,17 @@ export class DashboardSettingsDialogComponent extends DialogComponent +
+ +

dashboard.move-all-widgets

+ + +
+ + +
+
+
dashboard.move-by
+
+ + + dashboard.cols + + + + dashboard.rows + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts new file mode 100644 index 0000000000..ba030c467d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; + +export interface MoveWidgetsDialogResult { + cols: number; + rows: number; +} + +@Component({ + selector: 'tb-move-widgets-dialog', + templateUrl: './move-widgets-dialog.component.html', + providers: [], + styleUrls: [] +}) +export class MoveWidgetsDialogComponent extends DialogComponent { + + moveWidgetsFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + private fb: UntypedFormBuilder, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.moveWidgetsFormGroup = this.fb.group({ + cols: [0, [Validators.required]], + rows: [0, [Validators.required]] + } + ); + } + + cancel(): void { + this.dialogRef.close(null); + } + + move(): void { + const result: MoveWidgetsDialogResult = this.moveWidgetsFormGroup.value; + this.dialogRef.close(result); + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 8b51d691a5..2c245a935e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -90,6 +90,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @Input() columns: number; + @Input() + @coerceBoolean() + colWidthInteger = false; + @Input() @coerceBoolean() setGridSize = false; @@ -256,15 +260,22 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), itemInitCallback: (item, itemComponent) => { (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; + }, + colWidthUpdateCallback: (colWidth) => { + if (this.colWidthInteger) { + return Math.floor(colWidth); + } else { + return colWidth; + } } }; - this.updateMobileOpts(); + this.updateGridOpts(); this.breakpointObserverSubscription = this.breakpointObserver .observe(MediaBreakpoints['gt-sm']).subscribe( () => { - this.updateMobileOpts(); + this.updateGridOpts(); this.notifyGridsterOptionsChanged(); } ); @@ -291,20 +302,24 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } ngOnChanges(changes: SimpleChanges): void { - let updateMobileOpts = false; - let updateLayoutOpts = false; + let updateGridOpts = false; + let updateColumnOpts = false; let updateEditingOpts = false; + let updateDisplayGridOpts = false; let updateWidgets = false; let updateDashboardTimewindow = false; for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { - if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { - updateMobileOpts = true; + if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', + 'mobileRowHeight', 'colWidthInteger'].includes(propName)) { + updateGridOpts = true; } else if (['outerMargin', 'margin', 'columns'].includes(propName)) { - updateLayoutOpts = true; + updateColumnOpts = true; } else if (['isEdit', 'isEditingWidget'].includes(propName)) { updateEditingOpts = true; + } else if (propName === 'displayGrid') { + updateDisplayGridOpts = true; } else if (['widgets', 'widgetLayouts'].includes(propName)) { updateWidgets = true; } else if (propName === 'dashboardTimewindow') { @@ -318,16 +333,19 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); } - if (updateLayoutOpts) { - this.updateLayoutOpts(); + if (updateColumnOpts) { + this.updateColumnOpts(); } - if (updateMobileOpts) { - this.updateMobileOpts(); + if (updateGridOpts) { + this.updateGridOpts(); } if (updateEditingOpts) { this.updateEditingOpts(); } - if (updateMobileOpts || updateLayoutOpts || updateEditingOpts) { + if (updateDisplayGridOpts) { + this.updateDisplayGridOpts(); + } + if (updateGridOpts || updateColumnOpts || updateEditingOpts || updateDisplayGridOpts) { this.notifyGridsterOptionsChanged(); } } @@ -544,7 +562,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo }); } - private updateMobileOpts(parentHeight?: number) { + private onGridsterParentResize() { + const parentHeight = this.gridster.el.offsetHeight; + if (this.isMobileSize && this.mobileAutofillHeight && parentHeight) { + this.updateGridOpts(parentHeight); + } + this.notifyGridsterOptionsChanged(); + } + + private updateGridOpts(parentHeight?: number) { let updateWidgetRowsAndSort = false; const isMobileSize = this.checkIsMobileSize(); if (this.isMobileSize !== isMobileSize) { @@ -568,15 +594,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - private onGridsterParentResize() { - const parentHeight = this.gridster.el.offsetHeight; - if (this.isMobileSize && this.mobileAutofillHeight && parentHeight) { - this.updateMobileOpts(parentHeight); - } - this.notifyGridsterOptionsChanged(); - } - - private updateLayoutOpts() { + private updateColumnOpts() { this.gridsterOpts.minCols = this.columns ? this.columns : 24; this.gridsterOpts.outerMargin = isDefined(this.outerMargin) ? this.outerMargin : true; this.gridsterOpts.margin = isDefined(this.margin) ? this.margin : 10; @@ -587,6 +605,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo this.gridsterOpts.draggable.enabled = this.isEdit && !this.isEditingWidget; } + private updateDisplayGridOpts() { + this.gridsterOpts.displayGrid = this.displayGrid; + } + public notifyGridsterOptionsChanged() { if (!this.optionsChangeNotificationsPaused) { if (this.gridster && this.gridster.options) { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index f1a4623511..6678497820 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -174,6 +174,7 @@ import { import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; +import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; @NgModule({ declarations: @@ -287,6 +288,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet EditWidgetComponent, DashboardWidgetSelectComponent, AddWidgetDialogComponent, + MoveWidgetsDialogComponent, ManageDashboardLayoutsDialogComponent, DashboardSettingsDialogComponent, ManageDashboardStatesDialogComponent, @@ -419,6 +421,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet EditWidgetComponent, DashboardWidgetSelectComponent, AddWidgetDialogComponent, + MoveWidgetsDialogComponent, ManageDashboardLayoutsDialogComponent, DashboardSettingsDialogComponent, ManageDashboardStatesDialogComponent, diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index a118e39b96..282f87ade3 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -52,6 +52,7 @@ export interface WidgetLayouts { export interface GridSettings { backgroundColor?: string; columns?: number; + minColumns?: number; margin?: number; outerMargin?: boolean; backgroundSizeMode?: string; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 953a04db59..5570b0419d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1163,12 +1163,18 @@ "maximum-upload-file-size": "Maximum upload file size: {{ size }}", "cannot-upload-file": "Cannot upload file", "settings": "Settings", + "move-all-widgets": "Move all widgets", + "move-by": "Move by", + "cols": "cols", + "rows": "rows", "scada-layout": "SCADA layout", "layout-settings": "Layout settings", "columns-count": "Columns count", "columns-count-required": "Columns count is required.", "min-columns-count-message": "Only 10 minimum column count is allowed.", "max-columns-count-message": "Only 1000 maximum column count is allowed.", + "min-layout-width": "Minimum layout width", + "columns-suffix": "columns", "widgets-margins": "Margin between widgets", "margin-required": "Margin value is required.", "min-margin-message": "Only 0 is allowed as minimum margin value.",