From b911deaa43f176d5c0e90d624b53e59de93e0ad7 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 28 Jan 2026 17:07:20 +0300 Subject: [PATCH 01/16] SED-4491 Implement basic grid --- .../grid-layout/grid-layout.component.html | 2 + .../grid-layout/grid-layout.component.scss | 40 +++++ .../grid-layout/grid-layout.component.ts | 49 ++++++ .../grid-resizer/grid-resizer.component.html | 1 + .../grid-resizer/grid-resizer.component.scss | 6 + .../grid-resizer/grid-resizer.component.ts | 24 +++ .../directives/grid-dimensions.directive.ts | 94 +++++++++++ .../directives/grid-drag-handle.directive.ts | 21 +++ .../directives/grid-element.directive.ts | 88 ++++++++++ .../lib/modules/editable-grid-layout/index.ts | 16 ++ .../injectables/grid-dimensions.service.ts | 9 + .../injectables/grid-element-drag.service.ts | 122 ++++++++++++++ .../grid-element-resizer.service.ts | 129 +++++++++++++++ .../widgets-positions-state.service.ts | 155 ++++++++++++++++++ .../types/column-correction.ts | 4 + .../types/grid-element-id.ts | 1 + .../types/row-correction.ts | 4 + .../types/widget-position.ts | 117 +++++++++++++ .../injectables/view-registry.service.ts | 15 +- .../step-core/src/lib/step-core.module.ts | 4 + projects/step-frontend/src/lib/app.module.ts | 2 + .../alt-report-widget.component.html | 9 +- .../alt-report-widget.component.ts | 9 +- .../alt-report-widget-title.directive.ts | 8 + .../lib/modules/execution/execution.module.ts | 23 ++- .../grid-view-test.component.html | 33 ++++ .../grid-view-test.component.scss | 19 +++ .../grid-view-test.component.ts | 11 ++ .../grid-view-test.initializer.ts | 16 ++ .../src/lib/modules/grid-view-test/index.ts | 2 + 30 files changed, 1013 insertions(+), 20 deletions(-) create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.html create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.scss create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/index.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/types/grid-element-id.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts create mode 100644 projects/step-frontend/src/lib/modules/execution/directives/alt-report-widget-title.directive.ts create mode 100644 projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html create mode 100644 projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss create mode 100644 projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts create mode 100644 projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts create mode 100644 projects/step-frontend/src/lib/modules/grid-view-test/index.ts diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html new file mode 100644 index 0000000000..579e0a5270 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html @@ -0,0 +1,2 @@ + +
diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss new file mode 100644 index 0000000000..9aaf7685f3 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss @@ -0,0 +1,40 @@ +@use 'projects/step-core/styles/core-variables' as var; + +step-grid-layout { + $cols-count: var(--style__cols-count); + + display: grid; + grid-template-columns: repeat($cols-count, calc((100% / $cols-count) - 0.5rem)); + grid-auto-rows: 14.6rem; + gap: 0.5rem; + position: relative; + + .step-grid-element { + position: relative; + + step-grid-resizer { + position: absolute; + bottom: 0; + right: 0; + } + + .step-grid-drag-handle { + cursor: grab; + } + } + + .preview { + border-color: 0.1rem solid var.$blue-600; + background: var.$blue-50; + opacity: 0.5; + border-radius: 0.5rem; + position: absolute; + display: none; + } + + &.show-preview { + .preview { + display: block; + } + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts new file mode 100644 index 0000000000..11a50e4a6a --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts @@ -0,0 +1,49 @@ +import { + AfterViewInit, + Component, + computed, + ElementRef, + inject, + untracked, + viewChild, + ViewEncapsulation, +} from '@angular/core'; +import { StepBasicsModule } from '../../../basics/step-basics.module'; +import { WidgetsPositionsStateService } from '../../injectables/widgets-positions-state.service'; +import { GridDimensionsService } from '../../injectables/grid-dimensions.service'; +import { GridDimensionsDirective } from '../../directives/grid-dimensions.directive'; +import { GridElementResizerService } from '../../injectables/grid-element-resizer.service'; +import { GridElementDragService } from '../../injectables/grid-element-drag.service'; + +@Component({ + selector: 'step-grid-layout', + imports: [StepBasicsModule], + templateUrl: './grid-layout.component.html', + styleUrl: './grid-layout.component.scss', + encapsulation: ViewEncapsulation.None, + host: { + '[class.show-preview]': 'showPreview()', + '[style.--style__cols-count]': '_gridDimensions.COL_COUNT', + }, + hostDirectives: [GridDimensionsDirective], + providers: [WidgetsPositionsStateService, GridElementResizerService, GridElementDragService], +}) +export class GridLayoutComponent implements AfterViewInit { + protected _gridDimensions = inject(GridDimensionsService); + private _gridElementResizer = inject(GridElementResizerService); + private _gridElementDragService = inject(GridElementDragService); + + private readonly preview = viewChild>('preview'); + + protected readonly showPreview = computed(() => { + const isResize = this._gridElementResizer.resizeInProgress(); + const isDrag = this._gridElementDragService.dragInProgress(); + return !!isResize || !!isDrag; + }); + + ngAfterViewInit(): void { + const previewElement = untracked(() => this.preview())!.nativeElement; + this._gridElementResizer.setupPreviewElement(previewElement); + this._gridElementDragService.setupPreviewElement(previewElement); + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.html b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.html new file mode 100644 index 0000000000..a3c788a4d4 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.html @@ -0,0 +1 @@ + diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.scss b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.scss new file mode 100644 index 0000000000..0568904245 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.scss @@ -0,0 +1,6 @@ +step-icon { + transform: rotateX(180deg); + &:hover { + cursor: se-resize; + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts new file mode 100644 index 0000000000..4c0160aacf --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts @@ -0,0 +1,24 @@ +import { Component, inject } from '@angular/core'; +import { StepIconsModule } from '../../../step-icons/step-icons.module'; +import { GridElementDirective } from '../../directives/grid-element.directive'; +import { GridElementResizerService } from '../../injectables/grid-element-resizer.service'; + +@Component({ + selector: 'step-grid-resizer', + imports: [StepIconsModule], + templateUrl: './grid-resizer.component.html', + styleUrl: './grid-resizer.component.scss', + host: { + '(mousedown)': 'handleMouseDown($event)', + }, +}) +export class GridResizerComponent { + private _gridElement = inject(GridElementDirective); + private _gridElementResizer = inject(GridElementResizerService); + + protected handleMouseDown(event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + this._gridElementResizer.resizeStart(this._gridElement._elRef.nativeElement); + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts new file mode 100644 index 0000000000..9a356e8db6 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts @@ -0,0 +1,94 @@ +import { computed, Directive, ElementRef, forwardRef, inject, untracked } from '@angular/core'; +import { ElementSizeDirective } from '../../basics/step-basics.module'; +import { DOCUMENT } from '@angular/common'; +import { GridDimensionsService } from '../injectables/grid-dimensions.service'; + +@Directive({ + selector: '[stepGridDimensions]', + hostDirectives: [ElementSizeDirective], + providers: [ + { + provide: GridDimensionsService, + useExisting: forwardRef(() => GridDimensionsDirective), + }, + ], +}) +export class GridDimensionsDirective implements GridDimensionsService { + private _elementRef = inject>(ElementRef); + private _elementSize = inject(ElementSizeDirective, { self: true }); + private _doc = inject(DOCUMENT); + + readonly COL_COUNT = 8; + + private readonly gridStyles = computed(() => { + const width = this._elementSize.width(); + return this._doc?.defaultView?.getComputedStyle(this._elementRef.nativeElement); + }); + + private readonly gridDimensions = computed(() => { + const gridStyles = this.gridStyles(); + const columnGapStr = gridStyles?.['columnGap'] ?? ''; + let columnGap = parseFloat(columnGapStr); + columnGap = isNaN(columnGap) ? 0 : columnGap; + + const rowGapStr = gridStyles?.['rowGap'] ?? ''; + let rowGap = parseFloat(rowGapStr); + rowGap = isNaN(rowGap) ? 0 : rowGap; + + const templateColumns = gridStyles?.['gridTemplateColumns']; + const colWidthStr = templateColumns?.split?.(' ')?.[0] ?? ''; + let colWidth = parseFloat(colWidthStr); + colWidth = isNaN(colWidth) ? 0 : colWidth; + + const templateRows = gridStyles?.['gridTemplateRows']; + const rowHeightStr = templateRows?.split?.(' ')?.[0] ?? ''; + let rowHeight = parseFloat(rowHeightStr); + rowHeight = isNaN(rowHeight) ? 0 : rowHeight; + return { + columnGap, + rowGap, + colWidth, + rowHeight, + }; + }); + + get columnGap(): number { + return untracked(() => this.gridDimensions()).columnGap; + } + + get rowGap(): number { + return untracked(() => this.gridDimensions()).rowGap; + } + + determineCellsWidth(colIndex: number): number { + const { colWidth, columnGap } = untracked(() => this.gridDimensions()); + const size = colWidth + columnGap; + return colIndex * size; + } + + determineCellsHeight(rowIndex: number): number { + const { rowHeight, rowGap } = untracked(() => this.gridDimensions()); + const size = rowHeight + rowGap; + return rowIndex * size; + } + + determineCellColumn(x: number): number { + const { colWidth, columnGap } = untracked(() => this.gridDimensions()); + const size = colWidth + columnGap; + let col = Math.floor(x / size); + if (x % size !== 0) { + col++; + } + return col; + } + + determineCellRow(y: number): number { + const { rowHeight, rowGap } = untracked(() => this.gridDimensions()); + const size = rowHeight + rowGap; + let row = Math.floor(y / size); + if (y % size !== 0) { + row++; + } + return row; + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts new file mode 100644 index 0000000000..9667d37e40 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts @@ -0,0 +1,21 @@ +import { Directive, inject } from '@angular/core'; +import { GridElementDirective } from '@exense/step-core'; +import { GridElementDragService } from '../injectables/grid-element-drag.service'; + +@Directive({ + selector: '[stepGridDragHandle]', + host: { + class: 'step-grid-drag-handle', + '(mousedown)': 'handleMouseDown($event)', + }, +}) +export class GridDragHandleDirective { + private _gridElement = inject(GridElementDirective); + private _gridElementDrag = inject(GridElementDragService); + + protected handleMouseDown(event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + this._gridElementDrag.dragStart(this._gridElement._elRef.nativeElement); + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts new file mode 100644 index 0000000000..1fb76ba9a6 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts @@ -0,0 +1,88 @@ +import { + afterNextRender, + computed, + Directive, + effect, + ElementRef, + inject, + input, + signal, + untracked, +} from '@angular/core'; +import { WidgetsPositionsStateService } from '../injectables/widgets-positions-state.service'; +import { WidgetPosition } from '../types/widget-position'; +import { GridDimensionsService } from '../injectables/grid-dimensions.service'; + +@Directive({ + selector: '[stepGridElement]', + host: { + class: 'step-grid-element', + '[attr.data-grid-element-id]': 'elementId()', + '[style.grid-column]': 'gridColumn()', + '[style.grid-row]': 'gridRow()', + }, +}) +export class GridElementDirective { + private _gridDimensions = inject(GridDimensionsService); + private _positionsState = inject(WidgetsPositionsStateService); + + private readonly isRenderComplete = signal(false); + + readonly _elRef = inject>(ElementRef); + readonly elementId = input.required({ alias: 'stepGridElement' }); + + private readonly position = computed(() => { + const elementId = this.elementId(); + const positions = this._positionsState.positions(); + return positions[elementId]; + }); + + private effectInitializePosition = effect(() => { + const position = this.position(); + const isRenderComplete = this.isRenderComplete(); + if (isRenderComplete && !position) { + this.determineInitialPosition(); + } + }); + + protected readonly gridColumn = computed(() => { + const position = this.position(); + if (!position) { + return undefined; + } + const { column, widthInCells } = position; + if (widthInCells === 1) { + return column.toString(); + } + return `${column} / ${column + widthInCells}`; + }); + + protected readonly gridRow = computed(() => { + const position = this.position(); + if (!position) { + return undefined; + } + const { row, heightInCells } = position; + if (heightInCells === 1) { + return row.toString(); + } + return `${row} / ${row + heightInCells}`; + }); + + constructor() { + afterNextRender(() => this.isRenderComplete.set(true)); + } + + private determineInitialPosition(): void { + const element = this._elRef.nativeElement; + const id = untracked(() => this.elementId()); + if (!id) { + return; + } + const column = this._gridDimensions.determineCellColumn(element.offsetLeft + this._gridDimensions.columnGap); + const row = this._gridDimensions.determineCellRow(element.offsetTop + this._gridDimensions.rowGap); + + const position = new WidgetPosition(id, { column, row, widthInCells: 1, heightInCells: 1 }); + this._positionsState.updatePosition(position); + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/index.ts b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts new file mode 100644 index 0000000000..d89094c8d8 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts @@ -0,0 +1,16 @@ +import { GridLayoutComponent } from './components/grid-layout/grid-layout.component'; +import { GridResizerComponent } from './components/grid-resizer/grid-resizer.component'; +import { GridElementDirective } from './directives/grid-element.directive'; +import { GridDragHandleDirective } from './directives/grid-drag-handle.directive'; + +export const EDITABLE_GIRD_LAYOUT_EXPORTS = [ + GridLayoutComponent, + GridResizerComponent, + GridDragHandleDirective, + GridElementDirective, +]; + +export * from './components/grid-layout/grid-layout.component'; +export * from './components/grid-resizer/grid-resizer.component'; +export * from './directives/grid-element.directive'; +export * from './directives/grid-drag-handle.directive'; diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts new file mode 100644 index 0000000000..51c44b710c --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts @@ -0,0 +1,9 @@ +export abstract class GridDimensionsService { + abstract readonly COL_COUNT: number; + abstract readonly columnGap: number; + abstract readonly rowGap: number; + abstract determineCellsWidth(colIndex: number): number; + abstract determineCellsHeight(rowIndex: number): number; + abstract determineCellColumn(x: number): number; + abstract determineCellRow(y: number): number; +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts new file mode 100644 index 0000000000..0b703fc40f --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts @@ -0,0 +1,122 @@ +import { computed, ElementRef, inject, Injectable, OnDestroy, Renderer2, signal, untracked } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { WidgetsPositionsStateService } from './widgets-positions-state.service'; +import { GridDimensionsService } from './grid-dimensions.service'; +import { gridElementId } from '../types/grid-element-id'; +import { WidgetPosition } from '../types/widget-position'; + +@Injectable() +export class GridElementDragService implements OnDestroy { + private _elementRef = inject>(ElementRef); + private _renderer = inject(Renderer2); + private _doc = inject(DOCUMENT); + private _widgetsPositions = inject(WidgetsPositionsStateService); + private _gridDimensions = inject(GridDimensionsService); + + private previewElement?: HTMLElement; + private stopDragMove?: () => void; + private stopDragEnd?: () => void; + + private readonly draggedElement = signal(undefined); + + readonly dragInProgress = computed(() => { + const draggedElement = this.draggedElement(); + return !!draggedElement; + }); + + ngOnDestroy(): void { + this.stopDragMove?.(); + this.stopDragEnd?.(); + } + + setupPreviewElement(previewElement: HTMLElement): void { + this.previewElement = previewElement; + } + + dragStart(element: HTMLElement): void { + if (!!untracked(() => this.draggedElement())) { + return; + } + this.draggedElement.set(element); + + const preview = this.previewElement; + if (preview) { + preview.style.top = `${element.offsetTop}px`; + preview.style.left = `${element.offsetLeft}px`; + preview.style.width = `${element.clientWidth}px`; + preview.style.height = `${element.clientHeight}px`; + } + + this.stopDragMove = this._renderer.listen(this._doc.body, 'mousemove', (event: MouseEvent) => this.dragMove(event)); + this.stopDragEnd = this._renderer.listen(this._doc.body, 'mouseup', (event: MouseEvent) => this.dragEnd(event)); + } + + private dragMove(event: MouseEvent): void { + const element = untracked(() => this.draggedElement()); + const preview = this.previewElement; + if (!element || !preview) { + return; + } + + let left = `${element.offsetLeft}px`; + let top = `${element.offsetTop}px`; + let width = `${element.clientWidth}px`; + let height = `${element.clientHeight}px`; + + const position = this.determineDragPosition(element, event.clientX, event.clientY); + if (position) { + const newLeft = this._gridDimensions.determineCellsWidth(position.column - 1); + const newTop = this._gridDimensions.determineCellsHeight(position.row - 1); + const newWidth = this._gridDimensions.determineCellsWidth(position.widthInCells); + const newHeight = this._gridDimensions.determineCellsHeight(position.heightInCells); + left = `${newLeft}px`; + top = `${newTop}px`; + width = `${newWidth}px`; + height = `${newHeight}px`; + } + + preview.style.left = left; + preview.style.top = top; + preview.style.width = width; + preview.style.height = height; + } + + private dragEnd(event: MouseEvent): void { + const element = untracked(() => this.draggedElement()); + if (!element) { + return; + } + const id = gridElementId(element); + if (!id) { + return; + } + + const position = this.determineDragPosition(element, event.clientX, event.clientY); + if (position) { + if (position.id !== id) { + this._widgetsPositions.swapPositions(id, position.id); + } else { + this._widgetsPositions.updatePosition(position); + } + } + + this.stopDragMove?.(); + this.stopDragMove = undefined; + this.stopDragEnd?.(); + this.stopDragEnd = undefined; + this.draggedElement.set(undefined); + } + + private determineDragPosition(element: HTMLElement, mouseX: number, mouseY: number): WidgetPosition | undefined { + const id = gridElementId(element); + if (!id) { + return undefined; + } + const gridRect = this._elementRef.nativeElement.getBoundingClientRect(); + const column = this._gridDimensions.determineCellColumn(mouseX - gridRect.x); + const row = this._gridDimensions.determineCellRow(mouseY - gridRect.y); + const result = this._widgetsPositions.findAvailablePositionForElement(id, column, row); + result.applyLimits(this._gridDimensions.COL_COUNT); + return result; + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts new file mode 100644 index 0000000000..b578475a4c --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts @@ -0,0 +1,129 @@ +import { computed, inject, Injectable, OnDestroy, Renderer2, signal, untracked } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { WidgetsPositionsStateService } from './widgets-positions-state.service'; +import { GridDimensionsService } from './grid-dimensions.service'; +import { WidgetPosition } from '../types/widget-position'; +import { gridElementId } from '../types/grid-element-id'; + +@Injectable() +export class GridElementResizerService implements OnDestroy { + private _renderer = inject(Renderer2); + private _doc = inject(DOCUMENT); + private _widgetsPositions = inject(WidgetsPositionsStateService); + private _gridDimensions = inject(GridDimensionsService); + + private previewElement?: HTMLElement; + private stopResizeMove?: () => void; + private stopResizeEnd?: () => void; + + private readonly resizedElement = signal(undefined); + + readonly resizeInProgress = computed(() => { + const resizedElement = this.resizedElement(); + return !!resizedElement; + }); + + ngOnDestroy(): void { + this.stopResizeMove?.(); + this.stopResizeEnd?.(); + } + + setupPreviewElement(previewElement: HTMLElement): void { + this.previewElement = previewElement; + } + + resizeStart(element: HTMLElement): void { + if (!!untracked(() => this.resizedElement())) { + return; + } + this.resizedElement.set(element); + + const preview = this.previewElement; + if (preview) { + preview.style.top = `${element.offsetTop}px`; + preview.style.left = `${element.offsetLeft}px`; + preview.style.width = `${element.clientWidth}px`; + preview.style.height = `${element.clientHeight}px`; + } + + this.stopResizeMove = this._renderer.listen(this._doc.body, 'mousemove', (event: MouseEvent) => + this.resizeMove(event), + ); + this.stopResizeEnd = this._renderer.listen(this._doc.body, 'mouseup', (event: MouseEvent) => this.resizeEnd(event)); + } + + private resizeMove(event: MouseEvent): void { + const element = untracked(() => this.resizedElement()); + const preview = this.previewElement; + if (!element || !preview) { + return; + } + + let left = `${element.offsetLeft}px`; + let top = `${element.offsetTop}px`; + let width = `${element.clientWidth}px`; + let height = `${element.clientHeight}px`; + + const position = this.determineResizedPosition(element, event.clientX, event.clientY); + if (position) { + const newWidth = this._gridDimensions.determineCellsWidth(position.widthInCells); + const newHeight = this._gridDimensions.determineCellsHeight(position.heightInCells); + width = `${newWidth}px`; + height = `${newHeight}px`; + } + + preview.style.left = left; + preview.style.top = top; + preview.style.width = width; + preview.style.height = height; + } + + private resizeEnd(event: MouseEvent): void { + const element = untracked(() => this.resizedElement()); + if (!element) { + return; + } + + const position = this.determineResizedPosition(element, event.clientX, event.clientY); + if (position) { + this._widgetsPositions.updatePosition(position); + } + + this.stopResizeMove?.(); + this.stopResizeMove = undefined; + this.stopResizeEnd?.(); + this.stopResizeEnd = undefined; + this.resizedElement.set(undefined); + } + + private determineResizedPosition(element: HTMLElement, mouseX: number, mouseY: number): WidgetPosition | undefined { + const id = gridElementId(element); + if (!id) { + return undefined; + } + const rect = element.getBoundingClientRect(); + const distanceX = mouseX - rect.x; + const distanceY = mouseY - rect.y; + + if (distanceX <= 0 || distanceY <= 0) { + return undefined; + } + + const column = this._gridDimensions.determineCellColumn(element.offsetLeft + this._gridDimensions.columnGap); + const row = this._gridDimensions.determineCellRow(element.offsetTop + this._gridDimensions.rowGap); + const widthInCells = this._gridDimensions.determineCellColumn(distanceX); + const heightInCells = this._gridDimensions.determineCellRow(distanceY); + + const widgetPosition = new WidgetPosition(id, { column, row, widthInCells, heightInCells }); + + const rowCorrection = this._widgetsPositions.overlapsInsideRow(widgetPosition); + widgetPosition.applyRowCorrection(rowCorrection); + + const columnCorrection = this._widgetsPositions.overlapsInsideColumn(widgetPosition); + widgetPosition.applyColumnCorrection(columnCorrection); + + widgetPosition.applyLimits(this._gridDimensions.COL_COUNT); + + return widgetPosition; + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts new file mode 100644 index 0000000000..277ecc59a2 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts @@ -0,0 +1,155 @@ +import { Injectable, signal, untracked } from '@angular/core'; +import { WidgetPosition } from '../types/widget-position'; +import { RowCorrection } from '../types/row-correction'; +import { ColumnCorrection } from '../types/column-correction'; + +@Injectable() +export class WidgetsPositionsStateService { + private readonly positionsStateInternal = signal>({}); + + readonly positions = this.positionsStateInternal.asReadonly(); + + updatePosition(position: WidgetPosition): void { + this.positionsStateInternal.update((value) => ({ + ...value, + [position.id]: position, + })); + } + + swapPositions(aElementId: string, bElementId: string): void { + const positions = untracked(() => this.positions()); + const positionA = positions[aElementId]; + const positionB = positions[bElementId]; + if (!positionA || !positionB) { + return; + } + const newPositionA = new WidgetPosition(aElementId, positionB); + const newPositionB = new WidgetPosition(bElementId, positionA); + this.positionsStateInternal.update((value) => ({ + ...value, + [newPositionA.id]: newPositionA, + [newPositionB.id]: newPositionB, + })); + } + + findAvailablePositionForElement(elementId: string, column: number, row: number): WidgetPosition { + const positions = untracked(() => this.positions()); + const positionItems = Object.values(positions); + + const otherWidgetPosition = positionItems.find((pos) => { + return column >= pos.leftEdge && column <= pos.rightEdge && row >= pos.topEdge && row <= pos.bottomEdge; + }); + + if (!!otherWidgetPosition) { + return otherWidgetPosition; + } + + const currentElementPosition = positions[elementId]; + const possiblePosition = new WidgetPosition(elementId, { + column, + row, + widthInCells: currentElementPosition?.widthInCells ?? 1, + heightInCells: currentElementPosition?.heightInCells ?? 1, + }); + + const rowCorrections = this.overlapsInsideRow(possiblePosition); + const columnCorrections = this.overlapsInsideColumn(possiblePosition); + + const fixedByRow = possiblePosition.clone(); + fixedByRow.applyRowCorrection(rowCorrections); + + const fixedByColumn = possiblePosition.clone(); + fixedByColumn.applyColumnCorrection(columnCorrections); + + const fixedByBoth = possiblePosition.clone(); + fixedByBoth.applyRowCorrection(rowCorrections); + fixedByColumn.applyColumnCorrection(columnCorrections); + + const positionVariants = [fixedByRow, fixedByColumn, fixedByBoth] + .map((pos) => ({ + pos, + distance: pos.distance(possiblePosition), + })) + .sort((a, b) => a.distance - b.distance); + + return positionVariants[0].pos; + } + + overlapsInsideRow(checkPosition: WidgetPosition): RowCorrection { + const positions = Object.values(untracked(() => this.positions())); + + const lefts: number[] = []; + const rights: number[] = []; + + positions.forEach((pos) => { + if ( + pos.id === checkPosition.id || + checkPosition.topEdge > pos.bottomEdge || + pos.topEdge > checkPosition.bottomEdge || + checkPosition.leftEdge > pos.rightEdge || + pos.leftEdge > checkPosition.rightEdge + ) { + return; + } + + if (checkPosition.leftEdge >= pos.leftEdge && checkPosition.rightEdge <= pos.rightEdge) { + lefts.push(checkPosition.column); + rights.push(checkPosition.widthInCells); + return; + } + + if (checkPosition.rightEdge >= pos.leftEdge) { + rights.push(checkPosition.rightEdge - pos.leftEdge + 1); + return; + } + + if (checkPosition.leftEdge >= pos.leftEdge) { + debugger; + lefts.push(Math.abs(checkPosition.rightEdge - pos.leftEdge) + 1); + } + }); + + const left = !lefts.length ? 0 : Math.max(...lefts); + const right = !rights.length ? 0 : Math.max(...rights); + + return { left, right }; + } + + overlapsInsideColumn(checkPosition: WidgetPosition): ColumnCorrection { + const positions = Object.values(untracked(() => this.positions())); + + const tops: number[] = []; + const bottoms: number[] = []; + + positions.forEach((pos) => { + if ( + pos.id === checkPosition.id || + checkPosition.leftEdge > pos.rightEdge || + pos.leftEdge > checkPosition.rightEdge || + checkPosition.topEdge > pos.bottomEdge || + pos.topEdge > checkPosition.bottomEdge + ) { + return; + } + + if (checkPosition.topEdge >= pos.topEdge && checkPosition.bottomEdge <= pos.bottomEdge) { + tops.push(checkPosition.row); + bottoms.push(checkPosition.heightInCells); + return; + } + + if (checkPosition.bottomEdge >= pos.topEdge) { + bottoms.push(checkPosition.bottomEdge - pos.topEdge + 1); + return; + } + + if (checkPosition.topEdge >= pos.topEdge) { + tops.push(Math.abs(checkPosition.bottomEdge - pos.topEdge) + 1); + } + }); + + const top = !tops.length ? 0 : Math.max(...tops); + const bottom = !bottoms.length ? 0 : Math.max(...bottoms); + return { top, bottom }; + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts new file mode 100644 index 0000000000..385a7ef08a --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts @@ -0,0 +1,4 @@ +export interface ColumnCorrection { + top: number; + bottom: number; +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/grid-element-id.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/grid-element-id.ts new file mode 100644 index 0000000000..95e7ebe185 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/types/grid-element-id.ts @@ -0,0 +1 @@ +export const gridElementId = (element: HTMLElement): string | undefined => element.dataset?.['gridElementId']; diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts new file mode 100644 index 0000000000..7790f09bcc --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts @@ -0,0 +1,4 @@ +export interface RowCorrection { + left: number; + right: number; +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts new file mode 100644 index 0000000000..c4b1c74f50 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts @@ -0,0 +1,117 @@ +import { RowCorrection } from './row-correction'; +import { ColumnCorrection } from './column-correction'; + +interface WidgetPositionParams { + column: number; + row: number; + widthInCells: number; + heightInCells: number; +} + +export class WidgetPosition implements WidgetPositionParams { + column: number; + row: number; + widthInCells: number; + heightInCells: number; + + constructor( + public readonly id: string, + params: WidgetPositionParams, + ) { + this.column = params.column; + this.row = params.row; + this.widthInCells = params.widthInCells; + this.heightInCells = params.heightInCells; + } + + get leftEdge(): number { + return this.column; + } + + get rightEdge(): number { + return this.column + this.widthInCells - 1; + } + + get topEdge(): number { + return this.row; + } + + get bottomEdge(): number { + return this.row + this.heightInCells - 1; + } + + private get centerCol(): number { + return this.column + this.widthInCells / 2; + } + + private get centerRow(): number { + return this.row + this.heightInCells / 2; + } + + applyLimits(maxWidthInCells: number, maxHeightInCells?: number): void { + if (this.row <= 0) { + this.row = 1; + } + + if (this.column <= 0) { + this.column = 1; + } + + if (this.rightEdge > maxWidthInCells) { + const diff = Math.abs(maxWidthInCells - this.rightEdge); + this.widthInCells -= diff; + } + + if (maxHeightInCells !== undefined) { + if (this.bottomEdge > maxHeightInCells) { + const diff = Math.abs(maxHeightInCells - this.bottomEdge); + this.heightInCells -= diff; + } + } + } + + applyRowCorrection({ left, right }: RowCorrection): void { + this.column += left; + this.widthInCells -= right; + if (this.widthInCells < 1) { + this.widthInCells = 1; + } + } + + applyColumnCorrection({ top, bottom }: ColumnCorrection): void { + this.row += top; + this.heightInCells -= bottom; + if (this.heightInCells < 1) { + this.heightInCells = 1; + } + } + + clone(): WidgetPosition { + return new WidgetPosition(this.id, this); + } + + distance(pos: WidgetPosition): number { + const x = Math.abs(this.centerCol - pos.centerCol); + const y = Math.abs(this.centerRow - pos.centerRow); + return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); + } + + logPosition(prefix: string): void { + console.log( + 'POSITION', + prefix, + 'COL:', + this.column, + 'ROW:', + this.row, + 'WIDTH:', + this.widthInCells, + 'HEIGHT:', + this.heightInCells, + 'RIGHT:', + this.rightEdge, + 'BOTTOM:', + this.bottomEdge, + ); + } +} diff --git a/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts b/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts index 31e967a01f..c8f1f4ede0 100644 --- a/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts +++ b/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts @@ -40,10 +40,10 @@ export class ViewRegistryService implements OnDestroy { private isNavigationInitializedInternal$ = new BehaviorSubject(false); readonly isNavigationInitialized$ = this.isNavigationInitializedInternal$.asObservable(); - registeredViews: { [key: string]: CustomView } = {}; + registeredViews: Record = {}; registeredMenuEntries: MenuEntry[] = []; registeredMenuIds: string[] = []; - registeredDashlets: { [key: string]: Dashlet[] | undefined } = {}; + registeredDashlets: Record = {}; private static registeredRoutes: string[] = []; @@ -69,7 +69,7 @@ export class ViewRegistryService implements OnDestroy { /** * Registers basic set of main- and submenu entries */ - registerStandardMenuEntries() { + registerStandardMenuEntries(): void { // Main Menus this.registerMenuEntry('Design', 'automation-root', 'edit', { weight: 10 }); this.registerMenuEntry('Reporting', 'execute-root', 'file-check-03', { weight: 20 }); @@ -81,6 +81,7 @@ export class ViewRegistryService implements OnDestroy { this.registerMenuEntry('Keywords', 'functions', 'keyword', { weight: 10, parentId: 'automation-root' }); this.registerMenuEntry('Plans', 'plans', 'plan', { weight: 30, parentId: 'automation-root' }); this.registerMenuEntry('Parameters', 'parameters', 'list', { weight: 40, parentId: 'automation-root' }); + this.registerMenuEntry('Grid View', 'grid-view', 'grid', { weight: 50, parentId: 'automation-root' }); this.registerMenuEntry('Schedules', 'scheduler', 'clock', { weight: 100, parentId: 'automation-root' }); // Sub Menus Execute @@ -135,7 +136,7 @@ export class ViewRegistryService implements OnDestroy { * @deprecated use getCustomView instead * @param view */ - getViewTemplate(view: string) { + getViewTemplate(view: string): string { return this.getCustomView(view).template; } @@ -143,7 +144,7 @@ export class ViewRegistryService implements OnDestroy { * @deprecated use getCustomView instead * @param view */ - isPublicView(view: string) { + isPublicView(view: string): boolean { return this.getCustomView(view).isPublicView; } @@ -151,7 +152,7 @@ export class ViewRegistryService implements OnDestroy { * @deprecated use getCustomView instead * @param view */ - isStaticView(view: string) { + isStaticView(view: string): boolean | undefined { return this.getCustomView(view).isStaticView; } @@ -320,7 +321,7 @@ export class ViewRegistryService implements OnDestroy { const dashlets = this.getDashletsInternal(path); // weightless dashlets should be last - const normalizeWeight = (weight?: number) => weight || Infinity; + const normalizeWeight = (weight?: number): number => weight || Infinity; const result = dashlets.sort((a, b) => normalizeWeight(a.weight) - normalizeWeight(b.weight)); return result; diff --git a/projects/step-core/src/lib/step-core.module.ts b/projects/step-core/src/lib/step-core.module.ts index dbddae5e05..faff32e105 100644 --- a/projects/step-core/src/lib/step-core.module.ts +++ b/projects/step-core/src/lib/step-core.module.ts @@ -79,6 +79,7 @@ import { KEYWORDS_COMMON_IMPORTS } from './modules/keywords-common'; import { RESOURCE_INPUT_IMPORTS } from './modules/resource-input'; import { ScreenInputTypePipe } from './pipes/screen-input-type.pipe'; import { ScreenInputOptionsPipe } from './pipes/screen-input-options.pipe'; +import { EDITABLE_GIRD_LAYOUT_EXPORTS } from './modules/editable-grid-layout'; @NgModule({ declarations: [ @@ -154,6 +155,7 @@ import { ScreenInputOptionsPipe } from './pipes/screen-input-options.pipe'; ARTEFACTS_COMMON_EXPORTS, ATTACHMENTS_EXPORTS, SEARCH_EXPORTS, + EDITABLE_GIRD_LAYOUT_EXPORTS, ClampFadeDirective, ], exports: [ @@ -225,6 +227,7 @@ import { ScreenInputOptionsPipe } from './pipes/screen-input-options.pipe'; ExtractUrlPipe, ExtractQueryParamsPipe, IncludesStringPipe, + EDITABLE_GIRD_LAYOUT_EXPORTS, ], providers: [ CORE_INITIALIZER, @@ -360,3 +363,4 @@ export * from './pipes/extract-url.pipe'; export * from './pipes/extract-query-params.pipe'; export * from './modules/search'; export * from './shared/no-access-entity-error'; +export * from './modules/editable-grid-layout'; diff --git a/projects/step-frontend/src/lib/app.module.ts b/projects/step-frontend/src/lib/app.module.ts index f005e9b872..cf3482945e 100644 --- a/projects/step-frontend/src/lib/app.module.ts +++ b/projects/step-frontend/src/lib/app.module.ts @@ -31,6 +31,7 @@ import { AUTOMATION_PACKAGE_IMPORTS, AUTOMATION_PACKAGE_INITIALIZER } from './mo import { ERRORS_VIEW_IMPORTS, ERRORS_VIEW_INITIALIZER } from './modules/errors-view'; import { RESOURCE_IMPORTS, RESOURCES_INITIALIZER } from './modules/resources'; import { InProgressComponent } from './components/in-progress/in-progress.component'; +import { GRID_VIEW_TEST_INITIALIZER } from './modules/grid-view-test'; Settings.defaultLocale = 'en'; @@ -39,6 +40,7 @@ const MODULES_INITIALIZERS = [ AUTOMATION_PACKAGE_INITIALIZER, ERRORS_VIEW_INITIALIZER, RESOURCES_INITIALIZER, + GRID_VIEW_TEST_INITIALIZER, ]; @NgModule({ diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.html b/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.html index ecd1457d94..471760cd27 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.html @@ -2,9 +2,12 @@ -
- {{ title() }} -
+ @if (!hasTitleLayout()) { +
+ {{ title() }} +
+ } +
diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.ts b/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.ts index 0dac6d999f..20e71fa8d9 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.ts +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-report-widget/alt-report-widget.component.ts @@ -1,4 +1,5 @@ -import { Component, input, ViewEncapsulation } from '@angular/core'; +import { Component, computed, contentChild, input, ViewEncapsulation } from '@angular/core'; +import { AltReportWidgetTitleDirective } from '../../directives/alt-report-widget-title.directive'; @Component({ selector: 'step-alt-report-widget', @@ -12,6 +13,12 @@ import { Component, input, ViewEncapsulation } from '@angular/core'; standalone: false, }) export class AltReportWidgetComponent { + private readonly titleDirective = contentChild(AltReportWidgetTitleDirective); + protected readonly hasTitleLayout = computed(() => { + const titleDirective = this.titleDirective(); + return !!titleDirective; + }); + /** @Input() **/ readonly title = input(); diff --git a/projects/step-frontend/src/lib/modules/execution/directives/alt-report-widget-title.directive.ts b/projects/step-frontend/src/lib/modules/execution/directives/alt-report-widget-title.directive.ts new file mode 100644 index 0000000000..513b9b5148 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/execution/directives/alt-report-widget-title.directive.ts @@ -0,0 +1,8 @@ +import { Directive } from '@angular/core'; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'step-alt-report-widget-title', + standalone: false, +}) +export class AltReportWidgetTitleDirective {} diff --git a/projects/step-frontend/src/lib/modules/execution/execution.module.ts b/projects/step-frontend/src/lib/modules/execution/execution.module.ts index 8421584adf..16a9844f42 100644 --- a/projects/step-frontend/src/lib/modules/execution/execution.module.ts +++ b/projects/step-frontend/src/lib/modules/execution/execution.module.ts @@ -169,6 +169,7 @@ import { AggregatedTreeNodeHistoryComponent } from './components/aggregated-tree import { AggregatedTreeNodeStatusesPiechartComponent } from './components/aggregated-tree-node-history/execution-piechart/aggregated-tree-node-statuses-piechart.component'; import { DOCUMENT } from '@angular/common'; import { AltExecutionTimePopoverTitleDirective } from './components/alt-execution-time/alt-execution-time-popover-title.directive'; +import { AltReportWidgetTitleDirective } from './directives/alt-report-widget-title.directive'; @NgModule({ declarations: [ @@ -217,6 +218,7 @@ import { AltExecutionTimePopoverTitleDirective } from './components/alt-executio AltReportWidgetComponent, AltReportWidgetFilterDirective, AltReportWidgetSortDirective, + AltReportWidgetTitleDirective, AltReportWidgetFooterDirective, AltReportNodeKeywordsComponent, AltReportNodesTestcasesComponent, @@ -327,6 +329,9 @@ import { AltExecutionTimePopoverTitleDirective } from './components/alt-executio AltExecutionTreeNodeAddonDirective, ExecutionAgentsListComponent, StatusCountBadgeComponent, + AltReportWidgetTitleDirective, + AltReportWidgetContentDirective, + AltReportWidgetFooterDirective, ], providers: [ { @@ -641,9 +646,9 @@ export class ExecutionModule { }, canActivate: [ () => { - const ctx = inject(AggregatedReportViewTreeStateContextService); - const treeState = inject(AGGREGATED_TREE_WIDGET_STATE); - ctx.setState(treeState); + const _ctx = inject(AggregatedReportViewTreeStateContextService); + const _treeState = inject(AGGREGATED_TREE_WIDGET_STATE); + _ctx.setState(_treeState); return true; }, ], @@ -676,9 +681,9 @@ export class ExecutionModule { path: 'tree', canActivate: [ () => { - const ctx = inject(AggregatedReportViewTreeStateContextService); - const treeState = inject(AGGREGATED_TREE_TAB_STATE); - ctx.setState(treeState); + const _ctx = inject(AggregatedReportViewTreeStateContextService); + const _treeState = inject(AGGREGATED_TREE_TAB_STATE); + _ctx.setState(_treeState); return true; }, ], @@ -700,9 +705,9 @@ export class ExecutionModule { ], canActivate: [ () => { - const ctx = inject(AggregatedReportViewTreeStateContextService); - const treeState = inject(AggregatedReportViewTreeStateService); - ctx.setState(treeState); + const _ctx = inject(AggregatedReportViewTreeStateContextService); + const _treeState = inject(AggregatedReportViewTreeStateService); + _ctx.setState(_treeState); return true; }, ], diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html new file mode 100644 index 0000000000..5f6e397401 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html @@ -0,0 +1,33 @@ + + + +
WIDGET 1
+
+ + Content widget 1 + + +
+ + +
WIDGET 2
+
+ +
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ +
+
+ + +
WIDGET 3
+
+ +
Data 1
+
Data 2
+
Data 3
+ +
+
+
diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss new file mode 100644 index 0000000000..0693950463 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss @@ -0,0 +1,19 @@ +:host { + padding: 1em 1rem 0 1rem; + display: block; +} + +pre { + white-space: break-spaces; +} + +step-alt-report-widget { + height: 100%; + min-height: unset; +} + +step-alt-report-widget-content { + min-height: 100%; + overflow: auto; + height: 0; +} diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts new file mode 100644 index 0000000000..4ad0424345 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { StepCoreModule } from '@exense/step-core'; +import { ExecutionModule } from '../../../execution/execution.module'; + +@Component({ + selector: 'step-grid-view-test', + imports: [StepCoreModule, ExecutionModule], + templateUrl: './grid-view-test.component.html', + styleUrl: './grid-view-test.component.scss', +}) +export class GridViewTestComponent {} diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts b/projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts new file mode 100644 index 0000000000..4570ab7022 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts @@ -0,0 +1,16 @@ +import { inject, Injector, provideAppInitializer, runInInjectionContext } from '@angular/core'; +import { ViewRegistryService } from '@exense/step-core'; +import { GridViewTestComponent } from './components/grid-view-test/grid-view-test.component'; + +const registerRoutes = (): void => { + const _viewRegistry = inject(ViewRegistryService); + _viewRegistry.registerRoute({ + path: 'grid-view', + component: GridViewTestComponent, + }); +}; + +export const GRID_VIEW_TEST_INITIALIZER = provideAppInitializer(() => { + const _injector = inject(Injector); + runInInjectionContext(_injector, registerRoutes); +}); diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/index.ts b/projects/step-frontend/src/lib/modules/grid-view-test/index.ts new file mode 100644 index 0000000000..a051b2e967 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/grid-view-test/index.ts @@ -0,0 +1,2 @@ +export * from './grid-view-test.initializer'; +export * from './components/grid-view-test/grid-view-test.component'; From 6dcc9ca1fd621f53433add2494628571e61c28e0 Mon Sep 17 00:00:00 2001 From: dvladir Date: Fri, 30 Jan 2026 14:15:33 +0300 Subject: [PATCH 02/16] SED-4417 Drag/Resize algorythm improvements --- .../grid-layout/grid-layout.component.scss | 7 + .../grid-layout/grid-layout.component.ts | 6 +- .../directives/grid-dimensions.directive.ts | 2 - .../injectables/grid-column-count.token.ts | 8 + .../injectables/grid-dimensions.service.ts | 1 - .../injectables/grid-element-drag.service.ts | 15 +- .../grid-element-resizer.service.ts | 11 +- .../widgets-positions-state.service.ts | 239 +++++++++++------- .../types/column-correction.ts | 4 - .../types/row-correction.ts | 4 - .../types/widget-position.ts | 52 +--- 11 files changed, 174 insertions(+), 175 deletions(-) create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-column-count.token.ts delete mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts delete mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss index 9aaf7685f3..cd6838c96d 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss @@ -30,6 +30,13 @@ step-grid-layout { border-radius: 0.5rem; position: absolute; display: none; + + $animation-duration: 0.2s; + transition: + top $animation-duration ease, + left $animation-duration ease, + width $animation-duration ease, + height $animation-duration ease; } &.show-preview { diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts index 11a50e4a6a..6b35685799 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts @@ -10,10 +10,10 @@ import { } from '@angular/core'; import { StepBasicsModule } from '../../../basics/step-basics.module'; import { WidgetsPositionsStateService } from '../../injectables/widgets-positions-state.service'; -import { GridDimensionsService } from '../../injectables/grid-dimensions.service'; import { GridDimensionsDirective } from '../../directives/grid-dimensions.directive'; import { GridElementResizerService } from '../../injectables/grid-element-resizer.service'; import { GridElementDragService } from '../../injectables/grid-element-drag.service'; +import { GRID_COLUMN_COUNT } from '../../injectables/grid-column-count.token'; @Component({ selector: 'step-grid-layout', @@ -23,13 +23,13 @@ import { GridElementDragService } from '../../injectables/grid-element-drag.serv encapsulation: ViewEncapsulation.None, host: { '[class.show-preview]': 'showPreview()', - '[style.--style__cols-count]': '_gridDimensions.COL_COUNT', + '[style.--style__cols-count]': '_colCount', }, hostDirectives: [GridDimensionsDirective], providers: [WidgetsPositionsStateService, GridElementResizerService, GridElementDragService], }) export class GridLayoutComponent implements AfterViewInit { - protected _gridDimensions = inject(GridDimensionsService); + protected readonly _colCount = inject(GRID_COLUMN_COUNT); private _gridElementResizer = inject(GridElementResizerService); private _gridElementDragService = inject(GridElementDragService); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts index 9a356e8db6..aa3a1236f1 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-dimensions.directive.ts @@ -18,8 +18,6 @@ export class GridDimensionsDirective implements GridDimensionsService { private _elementSize = inject(ElementSizeDirective, { self: true }); private _doc = inject(DOCUMENT); - readonly COL_COUNT = 8; - private readonly gridStyles = computed(() => { const width = this._elementSize.width(); return this._doc?.defaultView?.getComputedStyle(this._elementRef.nativeElement); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-column-count.token.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-column-count.token.ts new file mode 100644 index 0000000000..b0f6fc73c6 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-column-count.token.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from '@angular/core'; + +const COLUMN_COUNT = 8; + +export const GRID_COLUMN_COUNT = new InjectionToken('Grid column count', { + providedIn: 'root', + factory: () => COLUMN_COUNT, +}); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts index 51c44b710c..a0f9831001 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-dimensions.service.ts @@ -1,5 +1,4 @@ export abstract class GridDimensionsService { - abstract readonly COL_COUNT: number; abstract readonly columnGap: number; abstract readonly rowGap: number; abstract determineCellsWidth(colIndex: number): number; diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts index 0b703fc40f..b13be7dcc8 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts @@ -112,11 +112,20 @@ export class GridElementDragService implements OnDestroy { if (!id) { return undefined; } + + const originalPosition = this._widgetsPositions.getPosition(id); + if (!originalPosition) { + return undefined; + } + const gridRect = this._elementRef.nativeElement.getBoundingClientRect(); const column = this._gridDimensions.determineCellColumn(mouseX - gridRect.x); const row = this._gridDimensions.determineCellRow(mouseY - gridRect.y); - const result = this._widgetsPositions.findAvailablePositionForElement(id, column, row); - result.applyLimits(this._gridDimensions.COL_COUNT); - return result; + + const position = originalPosition.clone(); + position.column = column; + position.row = row; + + return this._widgetsPositions.correctPositionForDrag(id, position); } } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts index b578475a4c..28a7674e70 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts @@ -115,15 +115,6 @@ export class GridElementResizerService implements OnDestroy { const heightInCells = this._gridDimensions.determineCellRow(distanceY); const widgetPosition = new WidgetPosition(id, { column, row, widthInCells, heightInCells }); - - const rowCorrection = this._widgetsPositions.overlapsInsideRow(widgetPosition); - widgetPosition.applyRowCorrection(rowCorrection); - - const columnCorrection = this._widgetsPositions.overlapsInsideColumn(widgetPosition); - widgetPosition.applyColumnCorrection(columnCorrection); - - widgetPosition.applyLimits(this._gridDimensions.COL_COUNT); - - return widgetPosition; + return this._widgetsPositions.correctPositionForResize(id, widgetPosition); } } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts index 277ecc59a2..321e447820 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts @@ -1,14 +1,29 @@ -import { Injectable, signal, untracked } from '@angular/core'; +import { computed, inject, Injectable, signal, untracked } from '@angular/core'; import { WidgetPosition } from '../types/widget-position'; -import { RowCorrection } from '../types/row-correction'; -import { ColumnCorrection } from '../types/column-correction'; +import { GRID_COLUMN_COUNT } from './grid-column-count.token'; @Injectable() export class WidgetsPositionsStateService { + private _colCount = inject(GRID_COLUMN_COUNT); + private readonly positionsStateInternal = signal>({}); readonly positions = this.positionsStateInternal.asReadonly(); + private readonly filedState = computed(() => { + const positions = Object.values(this.positions()); + const fieldBottom = Math.max(...positions.map((item) => item.bottomEdge)); + const size = this._colCount * fieldBottom; + + const field = new Uint8Array(size); + positions.forEach((item) => this.fillPosition(field, item, 1)); + return field; + }); + + getPosition(elementId: string): WidgetPosition | undefined { + return untracked(() => this.positions())[elementId]; + } + updatePosition(position: WidgetPosition): void { this.positionsStateInternal.update((value) => ({ ...value, @@ -32,124 +47,152 @@ export class WidgetsPositionsStateService { })); } - findAvailablePositionForElement(elementId: string, column: number, row: number): WidgetPosition { - const positions = untracked(() => this.positions()); - const positionItems = Object.values(positions); + correctPositionForDrag(elementId: string, position: WidgetPosition): WidgetPosition | undefined { + // Erase information about original position to avoid conflicts for current element + if (!this.clearElementPosition(elementId)) { + return undefined; + } - const otherWidgetPosition = positionItems.find((pos) => { - return column >= pos.leftEdge && column <= pos.rightEdge && row >= pos.topEdge && row <= pos.bottomEdge; - }); + //If position is taken by other widget, return other's widgets original position + if (this.isCellTaken(position.row, position.column)) { + const otherWidgetPosition = Object.values(untracked(() => this.positions())).find( + (pos) => pos.id !== elementId && pos.includesPoint(position.row, position.column), + ); - if (!!otherWidgetPosition) { return otherWidgetPosition; } - const currentElementPosition = positions[elementId]; - const possiblePosition = new WidgetPosition(elementId, { - column, - row, - widthInCells: currentElementPosition?.widthInCells ?? 1, - heightInCells: currentElementPosition?.heightInCells ?? 1, - }); - - const rowCorrections = this.overlapsInsideRow(possiblePosition); - const columnCorrections = this.overlapsInsideColumn(possiblePosition); + const result = position.clone(); + result.applyLimits(this._colCount); + + // Determine width / height + let row = 0; + let column = 0; + let heightInCells = 0; + let widthInCells = 0; + for (let r = result.topEdge; r <= result.bottomEdge; r++) { + row = r; + let widthInCellPerRow = 0; + for (let c = result.leftEdge; c <= result.rightEdge; c++) { + column = c; + if (this.isCellTaken(row, column)) { + break; + } + widthInCellPerRow++; + } + widthInCells = Math.max(widthInCells, widthInCellPerRow); + if (this.isCellTaken(row, column)) { + if (heightInCells === 0) { + heightInCells = result.heightInCells; + } + if (widthInCells === 0) { + widthInCells = result.widthInCells; + } + break; + } + heightInCells++; + } - const fixedByRow = possiblePosition.clone(); - fixedByRow.applyRowCorrection(rowCorrections); + if (heightInCells <= 0 || widthInCells <= 0) { + return undefined; + } + result.widthInCells = widthInCells; + result.heightInCells = heightInCells; - const fixedByColumn = possiblePosition.clone(); - fixedByColumn.applyColumnCorrection(columnCorrections); + return result; + } - const fixedByBoth = possiblePosition.clone(); - fixedByBoth.applyRowCorrection(rowCorrections); - fixedByColumn.applyColumnCorrection(columnCorrections); + correctPositionForResize(elementId: string, position: WidgetPosition): WidgetPosition | undefined { + // Erase information about original position to avoid conflicts for current element + if (!this.clearElementPosition(elementId)) { + return undefined; + } - const positionVariants = [fixedByRow, fixedByColumn, fixedByBoth] - .map((pos) => ({ - pos, - distance: pos.distance(possiblePosition), - })) - .sort((a, b) => a.distance - b.distance); + const result = position.clone(); + result.applyLimits(this._colCount); - return positionVariants[0].pos; - } + let r: number; + let c: number; - overlapsInsideRow(checkPosition: WidgetPosition): RowCorrection { - const positions = Object.values(untracked(() => this.positions())); - - const lefts: number[] = []; - const rights: number[] = []; - - positions.forEach((pos) => { - if ( - pos.id === checkPosition.id || - checkPosition.topEdge > pos.bottomEdge || - pos.topEdge > checkPosition.bottomEdge || - checkPosition.leftEdge > pos.rightEdge || - pos.leftEdge > checkPosition.rightEdge - ) { - return; + // Check top edge + r = result.row; + for (c = result.leftEdge; c <= result.rightEdge; c++) { + while (this.isCellTaken(r, c) && r <= result.bottomEdge + 1) { + r++; } + } + if (r > result.bottomEdge) { + return undefined; + } + result.row = r; - if (checkPosition.leftEdge >= pos.leftEdge && checkPosition.rightEdge <= pos.rightEdge) { - lefts.push(checkPosition.column); - rights.push(checkPosition.widthInCells); - return; + // Check right edge + c = result.rightEdge; + for (r = result.topEdge; r <= result.bottomEdge; r++) { + while (this.isCellTaken(r, c) && c >= result.leftEdge - 1) { + c--; } + } + if (c < result.leftEdge) { + return undefined; + } + result.widthInCells -= result.rightEdge - c; - if (checkPosition.rightEdge >= pos.leftEdge) { - rights.push(checkPosition.rightEdge - pos.leftEdge + 1); - return; + // Check bottom edge + r = result.bottomEdge; + for (c = result.leftEdge; c <= result.rightEdge; c++) { + while (this.isCellTaken(r, c) && r >= result.topEdge - 1) { + r--; } + } + if (r < result.topEdge) { + return undefined; + } + result.heightInCells -= result.bottomEdge - r; - if (checkPosition.leftEdge >= pos.leftEdge) { - debugger; - lefts.push(Math.abs(checkPosition.rightEdge - pos.leftEdge) + 1); + // Check left edge + c = result.column; + for (r = result.topEdge; r <= result.bottomEdge; r++) { + while (this.isCellTaken(r, c) && c <= result.rightEdge + 1) { + c++; } - }); - - const left = !lefts.length ? 0 : Math.max(...lefts); - const right = !rights.length ? 0 : Math.max(...rights); - - return { left, right }; + } + if (c > result.rightEdge) { + return undefined; + } + result.column = c; + return result; } - overlapsInsideColumn(checkPosition: WidgetPosition): ColumnCorrection { - const positions = Object.values(untracked(() => this.positions())); - - const tops: number[] = []; - const bottoms: number[] = []; - - positions.forEach((pos) => { - if ( - pos.id === checkPosition.id || - checkPosition.leftEdge > pos.rightEdge || - pos.leftEdge > checkPosition.rightEdge || - checkPosition.topEdge > pos.bottomEdge || - pos.topEdge > checkPosition.bottomEdge - ) { - return; - } - - if (checkPosition.topEdge >= pos.topEdge && checkPosition.bottomEdge <= pos.bottomEdge) { - tops.push(checkPosition.row); - bottoms.push(checkPosition.heightInCells); - return; - } + private isCellTaken(row: number, column: number): boolean { + const field = untracked(() => this.filedState()); + const index = this.getFieldIndex(row, column); + if (index >= field.length) { + return false; + } + return !!field[index]; + } - if (checkPosition.bottomEdge >= pos.topEdge) { - bottoms.push(checkPosition.bottomEdge - pos.topEdge + 1); - return; + private fillPosition(field: Uint8Array, position: WidgetPosition, fillValue: number): void { + for (let r = position.topEdge; r <= position.bottomEdge; r++) { + for (let c = position.leftEdge; c <= position.rightEdge; c++) { + const index = this.getFieldIndex(r, c); + field[index] = fillValue; } + } + } - if (checkPosition.topEdge >= pos.topEdge) { - tops.push(Math.abs(checkPosition.bottomEdge - pos.topEdge) + 1); - } - }); + private clearElementPosition(elementId: string): boolean { + const originalPosition = untracked(() => this.positions())[elementId]; + if (!originalPosition) { + return false; + } + const filed = untracked(() => this.filedState()); + this.fillPosition(filed, originalPosition, 0); + return true; + } - const top = !tops.length ? 0 : Math.max(...tops); - const bottom = !bottoms.length ? 0 : Math.max(...bottoms); - return { top, bottom }; + private getFieldIndex(row: number, column: number): number { + return (row - 1) * this._colCount + (column - 1); } } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts deleted file mode 100644 index 385a7ef08a..0000000000 --- a/projects/step-core/src/lib/modules/editable-grid-layout/types/column-correction.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ColumnCorrection { - top: number; - bottom: number; -} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts deleted file mode 100644 index 7790f09bcc..0000000000 --- a/projects/step-core/src/lib/modules/editable-grid-layout/types/row-correction.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RowCorrection { - left: number; - right: number; -} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts index c4b1c74f50..228293c83e 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts @@ -1,6 +1,3 @@ -import { RowCorrection } from './row-correction'; -import { ColumnCorrection } from './column-correction'; - interface WidgetPositionParams { column: number; row: number; @@ -40,12 +37,8 @@ export class WidgetPosition implements WidgetPositionParams { return this.row + this.heightInCells - 1; } - private get centerCol(): number { - return this.column + this.widthInCells / 2; - } - - private get centerRow(): number { - return this.row + this.heightInCells / 2; + includesPoint(row: number, column: number): boolean { + return row >= this.topEdge && row <= this.bottomEdge && column >= this.leftEdge && column <= this.rightEdge; } applyLimits(maxWidthInCells: number, maxHeightInCells?: number): void { @@ -70,48 +63,7 @@ export class WidgetPosition implements WidgetPositionParams { } } - applyRowCorrection({ left, right }: RowCorrection): void { - this.column += left; - this.widthInCells -= right; - if (this.widthInCells < 1) { - this.widthInCells = 1; - } - } - - applyColumnCorrection({ top, bottom }: ColumnCorrection): void { - this.row += top; - this.heightInCells -= bottom; - if (this.heightInCells < 1) { - this.heightInCells = 1; - } - } - clone(): WidgetPosition { return new WidgetPosition(this.id, this); } - - distance(pos: WidgetPosition): number { - const x = Math.abs(this.centerCol - pos.centerCol); - const y = Math.abs(this.centerRow - pos.centerRow); - return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); - } - - logPosition(prefix: string): void { - console.log( - 'POSITION', - prefix, - 'COL:', - this.column, - 'ROW:', - this.row, - 'WIDTH:', - this.widthInCells, - 'HEIGHT:', - this.heightInCells, - 'RIGHT:', - this.rightEdge, - 'BOTTOM:', - this.bottomEdge, - ); - } } From 4be1b408d36287a4506ecd9b1d0fe838cfc2ac1a Mon Sep 17 00:00:00 2001 From: dvladir Date: Fri, 30 Jan 2026 15:32:23 +0300 Subject: [PATCH 03/16] SED-4417 Fix resize --- .../widgets-positions-state.service.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts index 321e447820..3f2e7e8275 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts @@ -161,6 +161,32 @@ export class WidgetsPositionsStateService { return undefined; } result.column = c; + + // Check inside cells + let row = 0; + let column = 0; + let newHeight = 0; + let newWidth = -1; + for (r = result.topEdge; r <= result.bottomEdge; r++) { + row = r; + let localNewWidth = 0; + for (c = result.leftEdge; c <= result.rightEdge; c++) { + column = c; + if (this.isCellTaken(row, column)) { + break; + } + localNewWidth++; + } + newWidth = newWidth < 0 ? localNewWidth : Math.min(newWidth, localNewWidth); + if (this.isCellTaken(row, column)) { + break; + } + newHeight++; + } + + result.widthInCells = newWidth; + result.heightInCells = newHeight; + return result; } From 1f4f7d35acf7e04cc0615138876f1a359c1bd0c1 Mon Sep 17 00:00:00 2001 From: dvladir Date: Mon, 2 Feb 2026 11:55:51 +0300 Subject: [PATCH 04/16] SED-4417 Fix circular dependency --- .../directives/grid-drag-handle.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts index 9667d37e40..84529d6ad0 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts @@ -1,6 +1,6 @@ import { Directive, inject } from '@angular/core'; -import { GridElementDirective } from '@exense/step-core'; import { GridElementDragService } from '../injectables/grid-element-drag.service'; +import { GridElementDirective } from './grid-element.directive'; @Directive({ selector: '[stepGridDragHandle]', From 18a51b215e042a49a63746a7b3cce238f359be3a Mon Sep 17 00:00:00 2001 From: dvladir Date: Mon, 2 Feb 2026 12:13:16 +0300 Subject: [PATCH 05/16] SED-4417 Fix circular dependency --- projects/step-core/src/lib/modules/editable-grid-layout/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/index.ts b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts index d89094c8d8..d0b1299dbb 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/index.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts @@ -14,3 +14,4 @@ export * from './components/grid-layout/grid-layout.component'; export * from './components/grid-resizer/grid-resizer.component'; export * from './directives/grid-element.directive'; export * from './directives/grid-drag-handle.directive'; +export * from './directives/grid-dimensions.directive'; From 5d9b9326f5d80a5dd915958a619322d6cf82e19d Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 4 Feb 2026 13:29:18 +0300 Subject: [PATCH 06/16] SED-4417 Collapse empty rows with hidden widgets --- .../grid-element-title.component.html | 1 + .../grid-element-title.component.scss | 0 .../grid-element-title.component.ts | 20 +++ .../grid-layout/grid-layout.component.html | 9 ++ .../grid-layout/grid-layout.component.scss | 39 +++++- .../grid-layout/grid-layout.component.ts | 57 ++++++++- .../grid-resizer/grid-resizer.component.ts | 8 +- .../directives/grid-drag-handle.directive.ts | 8 +- .../directives/grid-editable.directive.ts | 15 +++ .../directives/grid-element.directive.ts | 13 +- .../lib/modules/editable-grid-layout/index.ts | 6 +- .../injectables/grid-editable.service.ts | 5 + .../injectables/grid-element-drag.service.ts | 4 +- .../grid-element-resizer.service.ts | 4 +- .../injectables/grid-layout-config.token.ts | 22 ++++ .../widgets-positions-state.service.ts | 116 ++++++++++++++++-- .../editable-grid-layout/types/widget-ids.ts | 25 ++++ .../types/widget-position.ts | 2 +- .../grid-view-test.component.html | 80 +++++++++--- .../grid-view-test.component.scss | 14 ++- .../grid-view-test.component.ts | 26 +++- 21 files changed, 419 insertions(+), 55 deletions(-) create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.html create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.scss create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-editable.directive.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-editable.service.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/types/widget-ids.ts diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.html b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.html new file mode 100644 index 0000000000..76eefdf65c --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.html @@ -0,0 +1 @@ +{{ displayTitle() }} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.scss b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts new file mode 100644 index 0000000000..56fc905dfd --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts @@ -0,0 +1,20 @@ +import { Component, computed, inject } from '@angular/core'; +import { GRID_LAYOUT_CONFIG } from '../../injectables/grid-layout-config.token'; +import { GridElementDirective } from '../../directives/grid-element.directive'; + +@Component({ + selector: 'step-grid-element-title', + imports: [], + templateUrl: './grid-element-title.component.html', + styleUrl: './grid-element-title.component.scss', +}) +export class GridElementTitleComponent { + private _gridConfig = inject(GRID_LAYOUT_CONFIG); + private _gridElement = inject(GridElementDirective); + + protected readonly displayTitle = computed(() => { + const id = this._gridElement.elementId(); + const title = this._gridConfig.defaultElementParams?.[id]?.title; + return title ?? id; + }); +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html index 579e0a5270..e3e705a1d6 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.html @@ -1,2 +1,11 @@ +@if (_gridEditable.editMode()) { + @for (hiddenWidget of hiddenWidgets(); track hiddenWidget) { +
+
+ + +
+ } +}
diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss index cd6838c96d..c85ca35587 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss @@ -4,23 +4,21 @@ step-grid-layout { $cols-count: var(--style__cols-count); display: grid; - grid-template-columns: repeat($cols-count, calc((100% / $cols-count) - 0.5rem)); + grid-template-columns: repeat($cols-count, calc((100% / $cols-count) - 1.5rem)); grid-auto-rows: 14.6rem; - gap: 0.5rem; + row-gap: 2.4rem; + column-gap: 1.5rem; position: relative; .step-grid-element { position: relative; step-grid-resizer { + display: none; position: absolute; bottom: 0; right: 0; } - - .step-grid-drag-handle { - cursor: grab; - } } .preview { @@ -44,4 +42,33 @@ step-grid-layout { display: block; } } + + &.edit-mode .step-grid-element { + step-grid-resizer { + display: block; + } + .step-grid-drag-handle { + cursor: grab; + } + } + + .hidden-placeholder { + border: dashed 0.1rem var.$black; + display: flex; + flex-direction: column; + align-items: center; + + step-grid-element-title { + $size: 1.6rem; + font-size: $size; + position: absolute; + top: calc(50% - ($size / 2)); + } + + .drag-handle { + background: var.$gray-50; + min-height: 3rem; + width: 100%; + } + } } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts index 6b35685799..673c845389 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts @@ -1,9 +1,13 @@ import { + afterNextRender, AfterViewInit, Component, computed, + contentChildren, + effect, ElementRef, inject, + signal, untracked, viewChild, ViewEncapsulation, @@ -14,25 +18,49 @@ import { GridDimensionsDirective } from '../../directives/grid-dimensions.direct import { GridElementResizerService } from '../../injectables/grid-element-resizer.service'; import { GridElementDragService } from '../../injectables/grid-element-drag.service'; import { GRID_COLUMN_COUNT } from '../../injectables/grid-column-count.token'; +import { GridEditableDirective } from '../../directives/grid-editable.directive'; +import { GridEditableService } from '../../injectables/grid-editable.service'; +import { GRID_LAYOUT_CONFIG } from '../../injectables/grid-layout-config.token'; +import { GridElementDirective } from '../../directives/grid-element.directive'; +import { GridResizerComponent } from '../grid-resizer/grid-resizer.component'; +import { GridDragHandleDirective } from '../../directives/grid-drag-handle.directive'; +import { GridElementTitleComponent } from '../grid-element-title/grid-element-title.component'; @Component({ selector: 'step-grid-layout', - imports: [StepBasicsModule], + imports: [ + StepBasicsModule, + GridElementDirective, + GridResizerComponent, + GridDragHandleDirective, + GridElementTitleComponent, + ], templateUrl: './grid-layout.component.html', styleUrl: './grid-layout.component.scss', encapsulation: ViewEncapsulation.None, host: { '[class.show-preview]': 'showPreview()', + '[class.edit-mode]': '_gridEditable.editMode()', '[style.--style__cols-count]': '_colCount', }, - hostDirectives: [GridDimensionsDirective], + hostDirectives: [ + GridDimensionsDirective, + { + directive: GridEditableDirective, + inputs: ['editMode'], + }, + ], providers: [WidgetsPositionsStateService, GridElementResizerService, GridElementDragService], }) export class GridLayoutComponent implements AfterViewInit { protected readonly _colCount = inject(GRID_COLUMN_COUNT); + protected readonly _gridEditable = inject(GridEditableService); + private _gridLayoutConfig = inject(GRID_LAYOUT_CONFIG); private _gridElementResizer = inject(GridElementResizerService); private _gridElementDragService = inject(GridElementDragService); + private _widgetPositions = inject(WidgetsPositionsStateService); + private readonly isRenderComplete = signal(false); private readonly preview = viewChild>('preview'); protected readonly showPreview = computed(() => { @@ -41,6 +69,31 @@ export class GridLayoutComponent implements AfterViewInit { return !!isResize || !!isDrag; }); + private allWidgets = Object.keys(this._gridLayoutConfig.defaultElementParams); + private readonly renderedWidgets = contentChildren(GridElementDirective); + private readonly renderedWidgetsIds = computed(() => { + const renderedWidgets = this.renderedWidgets(); + const ids = renderedWidgets.map((item) => untracked(() => item.elementId())); + return new Set(ids); + }); + + protected readonly hiddenWidgets = computed(() => { + const renderedWidgetsIds = this.renderedWidgetsIds(); + return this.allWidgets.filter((id) => !renderedWidgetsIds.has(id)); + }); + + private effectHiddenWidgetsChange = effect(() => { + const hiddenWidgets = this.hiddenWidgets(); + const isRenderComplete = this.isRenderComplete(); + if (isRenderComplete) { + this._widgetPositions.setHiddenWidgets(hiddenWidgets); + } + }); + + constructor() { + afterNextRender(() => this.isRenderComplete.set(true)); + } + ngAfterViewInit(): void { const previewElement = untracked(() => this.preview())!.nativeElement; this._gridElementResizer.setupPreviewElement(previewElement); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts index 4c0160aacf..4aabd9dc19 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-resizer/grid-resizer.component.ts @@ -1,7 +1,8 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, untracked } from '@angular/core'; import { StepIconsModule } from '../../../step-icons/step-icons.module'; import { GridElementDirective } from '../../directives/grid-element.directive'; import { GridElementResizerService } from '../../injectables/grid-element-resizer.service'; +import { GridEditableService } from '../../injectables/grid-editable.service'; @Component({ selector: 'step-grid-resizer', @@ -15,8 +16,13 @@ import { GridElementResizerService } from '../../injectables/grid-element-resize export class GridResizerComponent { private _gridElement = inject(GridElementDirective); private _gridElementResizer = inject(GridElementResizerService); + private _gridEditable = inject(GridEditableService); protected handleMouseDown(event: MouseEvent): void { + const isEditMode = untracked(() => this._gridEditable.editMode()); + if (!isEditMode) { + return; + } event.preventDefault(); event.stopPropagation(); this._gridElementResizer.resizeStart(this._gridElement._elRef.nativeElement); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts index 84529d6ad0..ad2802b4fa 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-drag-handle.directive.ts @@ -1,5 +1,6 @@ -import { Directive, inject } from '@angular/core'; +import { Directive, inject, untracked } from '@angular/core'; import { GridElementDragService } from '../injectables/grid-element-drag.service'; +import { GridEditableService } from '../injectables/grid-editable.service'; import { GridElementDirective } from './grid-element.directive'; @Directive({ @@ -12,8 +13,13 @@ import { GridElementDirective } from './grid-element.directive'; export class GridDragHandleDirective { private _gridElement = inject(GridElementDirective); private _gridElementDrag = inject(GridElementDragService); + private _gridEditable = inject(GridEditableService); protected handleMouseDown(event: MouseEvent): void { + const isEditMode = untracked(() => this._gridEditable.editMode()); + if (!isEditMode) { + return; + } event.preventDefault(); event.stopPropagation(); this._gridElementDrag.dragStart(this._gridElement._elRef.nativeElement); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-editable.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-editable.directive.ts new file mode 100644 index 0000000000..dfa43529da --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-editable.directive.ts @@ -0,0 +1,15 @@ +import { Directive, forwardRef, input } from '@angular/core'; +import { GridEditableService } from '../injectables/grid-editable.service'; + +@Directive({ + selector: '[stepGridEditable]', + providers: [ + { + provide: GridEditableService, + useExisting: forwardRef(() => GridEditableDirective), + }, + ], +}) +export class GridEditableDirective implements GridEditableService { + readonly editMode = input(false); +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts index 1fb76ba9a6..cf6d26e57e 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts @@ -12,6 +12,7 @@ import { import { WidgetsPositionsStateService } from '../injectables/widgets-positions-state.service'; import { WidgetPosition } from '../types/widget-position'; import { GridDimensionsService } from '../injectables/grid-dimensions.service'; +import { GRID_LAYOUT_CONFIG } from '../injectables/grid-layout-config.token'; @Directive({ selector: '[stepGridElement]', @@ -41,7 +42,7 @@ export class GridElementDirective { const position = this.position(); const isRenderComplete = this.isRenderComplete(); if (isRenderComplete && !position) { - this.determineInitialPosition(); + this.updatePositionIfRequired(); } }); @@ -73,16 +74,22 @@ export class GridElementDirective { afterNextRender(() => this.isRenderComplete.set(true)); } - private determineInitialPosition(): void { + private updatePositionIfRequired(): void { const element = this._elRef.nativeElement; const id = untracked(() => this.elementId()); if (!id) { return; } + let position = this._positionsState.getPosition(id); + if (!!position) { + return; + } const column = this._gridDimensions.determineCellColumn(element.offsetLeft + this._gridDimensions.columnGap); const row = this._gridDimensions.determineCellRow(element.offsetTop + this._gridDimensions.rowGap); + const widthInCells = 1; + const heightInCells = 1; - const position = new WidgetPosition(id, { column, row, widthInCells: 1, heightInCells: 1 }); + position = new WidgetPosition(id, { column, row, widthInCells, heightInCells }); this._positionsState.updatePosition(position); } } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/index.ts b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts index d0b1299dbb..f263338650 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/index.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts @@ -1,13 +1,15 @@ import { GridLayoutComponent } from './components/grid-layout/grid-layout.component'; import { GridResizerComponent } from './components/grid-resizer/grid-resizer.component'; -import { GridElementDirective } from './directives/grid-element.directive'; import { GridDragHandleDirective } from './directives/grid-drag-handle.directive'; +import { GridElementDirective } from './directives/grid-element.directive'; +import { GridElementTitleComponent } from './components/grid-element-title/grid-element-title.component'; export const EDITABLE_GIRD_LAYOUT_EXPORTS = [ GridLayoutComponent, GridResizerComponent, GridDragHandleDirective, GridElementDirective, + GridElementTitleComponent, ]; export * from './components/grid-layout/grid-layout.component'; @@ -15,3 +17,5 @@ export * from './components/grid-resizer/grid-resizer.component'; export * from './directives/grid-element.directive'; export * from './directives/grid-drag-handle.directive'; export * from './directives/grid-dimensions.directive'; +export * from './injectables/grid-layout-config.token'; +export * from './components/grid-element-title/grid-element-title.component'; diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-editable.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-editable.service.ts new file mode 100644 index 0000000000..199bf61066 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-editable.service.ts @@ -0,0 +1,5 @@ +import { Signal } from '@angular/core'; + +export abstract class GridEditableService { + abstract readonly editMode: Signal; +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts index b13be7dcc8..b5ebe55778 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-drag.service.ts @@ -67,8 +67,8 @@ export class GridElementDragService implements OnDestroy { if (position) { const newLeft = this._gridDimensions.determineCellsWidth(position.column - 1); const newTop = this._gridDimensions.determineCellsHeight(position.row - 1); - const newWidth = this._gridDimensions.determineCellsWidth(position.widthInCells); - const newHeight = this._gridDimensions.determineCellsHeight(position.heightInCells); + const newWidth = this._gridDimensions.determineCellsWidth(position.widthInCells) - this._gridDimensions.columnGap; + const newHeight = this._gridDimensions.determineCellsHeight(position.heightInCells) - this._gridDimensions.rowGap; left = `${newLeft}px`; top = `${newTop}px`; width = `${newWidth}px`; diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts index 28a7674e70..7192a03028 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-element-resizer.service.ts @@ -66,8 +66,8 @@ export class GridElementResizerService implements OnDestroy { const position = this.determineResizedPosition(element, event.clientX, event.clientY); if (position) { - const newWidth = this._gridDimensions.determineCellsWidth(position.widthInCells); - const newHeight = this._gridDimensions.determineCellsHeight(position.heightInCells); + const newWidth = this._gridDimensions.determineCellsWidth(position.widthInCells) - this._gridDimensions.columnGap; + const newHeight = this._gridDimensions.determineCellsHeight(position.heightInCells) - this._gridDimensions.rowGap; width = `${newWidth}px`; height = `${newHeight}px`; } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts new file mode 100644 index 0000000000..bca839ff00 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts @@ -0,0 +1,22 @@ +import { WidgetPositionParams } from '../types/widget-position'; +import { InjectionToken, Provider } from '@angular/core'; + +export interface WidgetInfo { + title: string; + position: WidgetPositionParams; +} + +export interface GridLayoutConfig { + gridId: string; + defaultElementParams: Record; +} + +export const GRID_LAYOUT_CONFIG = new InjectionToken('Grid layout config'); + +export const provideGridLayoutConfig = ( + gridId: string, + defaultElementParams: Record = {}, +): Provider => ({ + provide: GRID_LAYOUT_CONFIG, + useFactory: () => ({ gridId, defaultElementParams }), +}); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts index 3f2e7e8275..e3ac3fdc50 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts @@ -1,27 +1,52 @@ -import { computed, inject, Injectable, signal, untracked } from '@angular/core'; +import { computed, inject, Injectable, OnDestroy, signal, untracked } from '@angular/core'; import { WidgetPosition } from '../types/widget-position'; import { GRID_COLUMN_COUNT } from './grid-column-count.token'; +import { GridEditableService } from './grid-editable.service'; +import { WidgetIDs } from '../types/widget-ids'; +import { GRID_LAYOUT_CONFIG } from './grid-layout-config.token'; + +const EMPTY = 0; @Injectable() -export class WidgetsPositionsStateService { +export class WidgetsPositionsStateService implements OnDestroy { + private _gridConfig = inject(GRID_LAYOUT_CONFIG); private _colCount = inject(GRID_COLUMN_COUNT); + private _gridEditable = inject(GridEditableService); + + private widgetIDs = new WidgetIDs(Object.keys(this._gridConfig.defaultElementParams)); - private readonly positionsStateInternal = signal>({}); + protected readonly hiddenWidgets = signal([]); + private readonly positionsStateInternal = signal>(this.determineInitialPositions()); - readonly positions = this.positionsStateInternal.asReadonly(); + readonly positions = computed(() => { + const positions = this.positionsStateInternal(); + const hiddenWidgets = this.hiddenWidgets(); + const isEditMode = this._gridEditable.editMode(); + if (isEditMode) { + return positions; + } + return this.realignPositionsWithHiddenWidgets(positions, hiddenWidgets); + }); private readonly filedState = computed(() => { - const positions = Object.values(this.positions()); + const positions = Object.values(this.positionsStateInternal()); + if (!positions.length) { + return new Uint8Array(0); + } const fieldBottom = Math.max(...positions.map((item) => item.bottomEdge)); const size = this._colCount * fieldBottom; const field = new Uint8Array(size); - positions.forEach((item) => this.fillPosition(field, item, 1)); + positions.forEach((item) => this.fillPosition(field, item)); return field; }); + ngOnDestroy(): void { + this.widgetIDs.destroy(); + } + getPosition(elementId: string): WidgetPosition | undefined { - return untracked(() => this.positions())[elementId]; + return untracked(() => this.positionsStateInternal())[elementId]; } updatePosition(position: WidgetPosition): void { @@ -32,7 +57,7 @@ export class WidgetsPositionsStateService { } swapPositions(aElementId: string, bElementId: string): void { - const positions = untracked(() => this.positions()); + const positions = untracked(() => this.positionsStateInternal()); const positionA = positions[aElementId]; const positionB = positions[bElementId]; if (!positionA || !positionB) { @@ -55,7 +80,7 @@ export class WidgetsPositionsStateService { //If position is taken by other widget, return other's widgets original position if (this.isCellTaken(position.row, position.column)) { - const otherWidgetPosition = Object.values(untracked(() => this.positions())).find( + const otherWidgetPosition = Object.values(untracked(() => this.positionsStateInternal())).find( (pos) => pos.id !== elementId && pos.includesPoint(position.row, position.column), ); @@ -190,6 +215,10 @@ export class WidgetsPositionsStateService { return result; } + setHiddenWidgets(widgetsIds: string[]): void { + this.hiddenWidgets.set(widgetsIds); + } + private isCellTaken(row: number, column: number): boolean { const field = untracked(() => this.filedState()); const index = this.getFieldIndex(row, column); @@ -199,7 +228,9 @@ export class WidgetsPositionsStateService { return !!field[index]; } - private fillPosition(field: Uint8Array, position: WidgetPosition, fillValue: number): void { + private fillPosition(field: Uint8Array, position: WidgetPosition, isClear?: boolean): void { + const fillValue = isClear ? EMPTY : this.widgetIDs.getNumericIdByString(position.id); + for (let r = position.topEdge; r <= position.bottomEdge; r++) { for (let c = position.leftEdge; c <= position.rightEdge; c++) { const index = this.getFieldIndex(r, c); @@ -209,16 +240,77 @@ export class WidgetsPositionsStateService { } private clearElementPosition(elementId: string): boolean { - const originalPosition = untracked(() => this.positions())[elementId]; + const originalPosition = untracked(() => this.positionsStateInternal())[elementId]; if (!originalPosition) { return false; } const filed = untracked(() => this.filedState()); - this.fillPosition(filed, originalPosition, 0); + this.fillPosition(filed, originalPosition, true); return true; } private getFieldIndex(row: number, column: number): number { return (row - 1) * this._colCount + (column - 1); } + + private realignPositionsWithHiddenWidgets( + originalPositions: Record, + hiddenWidgets: string[], + ): Record { + const positions = Object.values(originalPositions); + if (!positions.length || !hiddenWidgets?.length) { + return originalPositions; + } + const filed = untracked(() => this.filedState()); + const hiddenWidgetsNumIds = new Set(hiddenWidgets.map((idStr) => this.widgetIDs.getNumericIdByString(idStr))); + const fieldBottom = Math.max(...positions.map((item) => item.bottomEdge)); + + // This arrays show, how many rows are skipped above each row + const hiddenRowsState: number[] = new Array(fieldBottom); + + for (let row = 1; row <= fieldBottom; row++) { + let hiddenColCount = 0; + for (let col = 1; col <= this._colCount; col++) { + const index = this.getFieldIndex(row, col); + const widgetId = filed[index]; + if (hiddenWidgetsNumIds.has(widgetId) || widgetId === EMPTY) { + hiddenColCount++; + } + } + const rowIndex = row - 1; + hiddenRowsState[rowIndex] = rowIndex === 0 ? 0 : hiddenRowsState[rowIndex - 1]; + if (hiddenColCount === this._colCount) { + hiddenRowsState[rowIndex]++; + } + } + + const result = positions.reduce( + (res, position) => { + const idNum = this.widgetIDs.getNumericIdByString(position.id); + if (hiddenWidgetsNumIds.has(idNum)) { + return res; + } + + const updatesPos = position.clone(); + const hiddenRows = hiddenRowsState[updatesPos.row - 1]; + updatesPos.row -= hiddenRows; + res[position.id] = updatesPos; + + return res; + }, + {} as Record, + ); + + return result; + } + + private determineInitialPositions(): Record { + return Object.entries(this._gridConfig.defaultElementParams).reduce( + (res, [key, info]) => { + res[key] = new WidgetPosition(key, info.position); + return res; + }, + {} as Record, + ); + } } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-ids.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-ids.ts new file mode 100644 index 0000000000..4d2156c0c6 --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-ids.ts @@ -0,0 +1,25 @@ +export class WidgetIDs { + private widgetIdsNumStr = new Map(); + private widgetIdsStrNum = new Map(); + + constructor(widgetKeys: string[]) { + widgetKeys.forEach((idStr, index) => { + const idNum = index + 1; + this.widgetIdsNumStr.set(idNum, idStr); + this.widgetIdsStrNum.set(idStr, idNum); + }); + } + + destroy(): void { + this.widgetIdsNumStr.clear(); + this.widgetIdsStrNum.clear(); + } + + getNumericIdByString(idString: string): number { + return this.widgetIdsStrNum.get(idString) ?? -1; + } + + getStringIdByNumber(idNumber: number): string { + return this.widgetIdsNumStr.get(idNumber) ?? ''; + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts index 228293c83e..0793e8956d 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/types/widget-position.ts @@ -1,4 +1,4 @@ -interface WidgetPositionParams { +export interface WidgetPositionParams { column: number; row: number; widthInCells: number; diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html index 5f6e397401..85dd414043 100644 --- a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.html @@ -1,32 +1,72 @@ - - - -
WIDGET 1
-
- - Content widget 1 - - -
- +
+
+ Edit Mode + +
+
+ + @if (areErrorsVisible()) { + + +
+ +
+
+ + Some inline errors + + +
+ } + @if (areTestCasesVisible()) { + + +
+ +
+
+ +
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ +
+
+ + +
+ +
+
+ + Summary content + + +
+ } + -
WIDGET 2
+
+ +
-
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
Data 1
+
Data 2
+
Data 3
- + -
WIDGET 3
+
+ +
-
Data 1
-
Data 2
-
Data 3
+
aaa
+
bbb
+
ccc
diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss index 0693950463..8406a0a6db 100644 --- a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.scss @@ -1,6 +1,18 @@ :host { padding: 1em 1rem 0 1rem; - display: block; + display: flex; + flex-direction: column; + gap: 1.5rem; + + & > section { + display: flex; + + & > div { + display: flex; + gap: 1rem; + align-items: center; + } + } } pre { diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts index 4ad0424345..24ec1126e0 100644 --- a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts @@ -1,5 +1,5 @@ -import { Component } from '@angular/core'; -import { StepCoreModule } from '@exense/step-core'; +import { Component, model, signal } from '@angular/core'; +import { provideGridLayoutConfig, StepCoreModule } from '@exense/step-core'; import { ExecutionModule } from '../../../execution/execution.module'; @Component({ @@ -7,5 +7,25 @@ import { ExecutionModule } from '../../../execution/execution.module'; imports: [StepCoreModule, ExecutionModule], templateUrl: './grid-view-test.component.html', styleUrl: './grid-view-test.component.scss', + providers: [ + provideGridLayoutConfig('gridTest', { + errorsWidget: { title: 'Errors widget', position: { row: 1, column: 1, widthInCells: 8, heightInCells: 1 } }, + testCases: { title: 'Test Cases', position: { row: 2, column: 1, widthInCells: 6, heightInCells: 3 } }, + testCasesSummary: { + title: 'Summary: Test Cases', + position: { row: 2, column: 7, widthInCells: 2, heightInCells: 3 }, + }, + keywordsSummary: { + title: 'Summary: Keyword Calls', + position: { row: 5, column: 1, widthInCells: 2, heightInCells: 3 }, + }, + keywordsList: { title: 'Keywords', position: { row: 5, column: 3, widthInCells: 6, heightInCells: 3 } }, + }), + ], }) -export class GridViewTestComponent {} +export class GridViewTestComponent { + readonly editMode = model(false); + + readonly areErrorsVisible = signal(false); + readonly areTestCasesVisible = signal(true); +} From 221b5e12dbfb63ce0e8ca0e2ec9139bec4ab8e38 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 4 Feb 2026 13:42:24 +0300 Subject: [PATCH 07/16] SED-4417 Fix exports --- .../step-core/src/lib/modules/editable-grid-layout/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/index.ts b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts index f263338650..8fdb8ea3c0 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/index.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/index.ts @@ -3,6 +3,7 @@ import { GridResizerComponent } from './components/grid-resizer/grid-resizer.com import { GridDragHandleDirective } from './directives/grid-drag-handle.directive'; import { GridElementDirective } from './directives/grid-element.directive'; import { GridElementTitleComponent } from './components/grid-element-title/grid-element-title.component'; +import { GridEditableDirective } from './directives/grid-editable.directive'; export const EDITABLE_GIRD_LAYOUT_EXPORTS = [ GridLayoutComponent, @@ -10,12 +11,14 @@ export const EDITABLE_GIRD_LAYOUT_EXPORTS = [ GridDragHandleDirective, GridElementDirective, GridElementTitleComponent, + GridEditableDirective, ]; export * from './components/grid-layout/grid-layout.component'; export * from './components/grid-resizer/grid-resizer.component'; export * from './directives/grid-element.directive'; export * from './directives/grid-drag-handle.directive'; +export * from './directives/grid-editable.directive'; export * from './directives/grid-dimensions.directive'; export * from './injectables/grid-layout-config.token'; export * from './components/grid-element-title/grid-element-title.component'; From 48509fe693e831b18c8143ba248936a49a142b08 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 4 Feb 2026 15:14:20 +0300 Subject: [PATCH 08/16] SED-4417 Autodetermine default positions based on width --- .../grid-element-title.component.ts | 2 +- .../grid-layout/grid-layout.component.ts | 2 +- .../directives/grid-element.directive.ts | 12 +-- .../injectables/grid-layout-config.token.ts | 14 ++-- .../widgets-positions-state.service.ts | 80 +++++++++++++++---- .../services/executions-panels.service.ts | 7 +- .../single-execution-panels.service.ts | 5 +- .../grid-view-test.component.ts | 20 ++--- 8 files changed, 93 insertions(+), 49 deletions(-) diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts index 56fc905dfd..ad96364bdc 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts @@ -14,7 +14,7 @@ export class GridElementTitleComponent { protected readonly displayTitle = computed(() => { const id = this._gridElement.elementId(); - const title = this._gridConfig.defaultElementParams?.[id]?.title; + const title = this._gridConfig.defaultElementParams?.find((item) => item.id === id)?.title; return title ?? id; }); } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts index 673c845389..7646d36962 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts @@ -69,7 +69,7 @@ export class GridLayoutComponent implements AfterViewInit { return !!isResize || !!isDrag; }); - private allWidgets = Object.keys(this._gridLayoutConfig.defaultElementParams); + private allWidgets = this._gridLayoutConfig.defaultElementParams.map((item) => item.id); private readonly renderedWidgets = contentChildren(GridElementDirective); private readonly renderedWidgetsIds = computed(() => { const renderedWidgets = this.renderedWidgets(); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts index cf6d26e57e..6ca72910f9 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts @@ -11,8 +11,6 @@ import { } from '@angular/core'; import { WidgetsPositionsStateService } from '../injectables/widgets-positions-state.service'; import { WidgetPosition } from '../types/widget-position'; -import { GridDimensionsService } from '../injectables/grid-dimensions.service'; -import { GRID_LAYOUT_CONFIG } from '../injectables/grid-layout-config.token'; @Directive({ selector: '[stepGridElement]', @@ -24,7 +22,6 @@ import { GRID_LAYOUT_CONFIG } from '../injectables/grid-layout-config.token'; }, }) export class GridElementDirective { - private _gridDimensions = inject(GridDimensionsService); private _positionsState = inject(WidgetsPositionsStateService); private readonly isRenderComplete = signal(false); @@ -75,7 +72,6 @@ export class GridElementDirective { } private updatePositionIfRequired(): void { - const element = this._elRef.nativeElement; const id = untracked(() => this.elementId()); if (!id) { return; @@ -84,12 +80,8 @@ export class GridElementDirective { if (!!position) { return; } - const column = this._gridDimensions.determineCellColumn(element.offsetLeft + this._gridDimensions.columnGap); - const row = this._gridDimensions.determineCellRow(element.offsetTop + this._gridDimensions.rowGap); - const widthInCells = 1; - const heightInCells = 1; - - position = new WidgetPosition(id, { column, row, widthInCells, heightInCells }); + const positionParams = this._positionsState.findProperPosition(1, 1); + position = new WidgetPosition(id, positionParams); this._positionsState.updatePosition(position); } } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts index bca839ff00..9774217274 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts @@ -1,22 +1,22 @@ -import { WidgetPositionParams } from '../types/widget-position'; import { InjectionToken, Provider } from '@angular/core'; export interface WidgetInfo { + id: string; title: string; - position: WidgetPositionParams; + weight: number; + widthInCells: number; + heightInCells: number; + //position: WidgetPositionParams; } export interface GridLayoutConfig { gridId: string; - defaultElementParams: Record; + defaultElementParams: WidgetInfo[]; } export const GRID_LAYOUT_CONFIG = new InjectionToken('Grid layout config'); -export const provideGridLayoutConfig = ( - gridId: string, - defaultElementParams: Record = {}, -): Provider => ({ +export const provideGridLayoutConfig = (gridId: string, defaultElementParams: WidgetInfo[] = []): Provider => ({ provide: GRID_LAYOUT_CONFIG, useFactory: () => ({ gridId, defaultElementParams }), }); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts index e3ac3fdc50..9dd3bf0547 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts @@ -1,5 +1,5 @@ import { computed, inject, Injectable, OnDestroy, signal, untracked } from '@angular/core'; -import { WidgetPosition } from '../types/widget-position'; +import { WidgetPosition, WidgetPositionParams } from '../types/widget-position'; import { GRID_COLUMN_COUNT } from './grid-column-count.token'; import { GridEditableService } from './grid-editable.service'; import { WidgetIDs } from '../types/widget-ids'; @@ -13,10 +13,10 @@ export class WidgetsPositionsStateService implements OnDestroy { private _colCount = inject(GRID_COLUMN_COUNT); private _gridEditable = inject(GridEditableService); - private widgetIDs = new WidgetIDs(Object.keys(this._gridConfig.defaultElementParams)); + private widgetIDs = new WidgetIDs(this._gridConfig.defaultElementParams.map((item) => item.id)); protected readonly hiddenWidgets = signal([]); - private readonly positionsStateInternal = signal>(this.determineInitialPositions()); + private readonly positionsStateInternal = signal>({}); readonly positions = computed(() => { const positions = this.positionsStateInternal(); @@ -28,12 +28,20 @@ export class WidgetsPositionsStateService implements OnDestroy { return this.realignPositionsWithHiddenWidgets(positions, hiddenWidgets); }); - private readonly filedState = computed(() => { + private readonly fieldBottom = computed(() => { const positions = Object.values(this.positionsStateInternal()); if (!positions.length) { + return 0; + } + return Math.max(...positions.map((item) => item.bottomEdge)); + }); + + private readonly filedState = computed(() => { + const positions = Object.values(this.positionsStateInternal()); + const fieldBottom = this.fieldBottom(); + if (!positions.length || !fieldBottom) { return new Uint8Array(0); } - const fieldBottom = Math.max(...positions.map((item) => item.bottomEdge)); const size = this._colCount * fieldBottom; const field = new Uint8Array(size); @@ -41,6 +49,10 @@ export class WidgetsPositionsStateService implements OnDestroy { return field; }); + constructor() { + this.determineInitialPositions(); + } + ngOnDestroy(): void { this.widgetIDs.destroy(); } @@ -215,6 +227,45 @@ export class WidgetsPositionsStateService implements OnDestroy { return result; } + findProperPosition(widthInCells: number, heightInCells: number): WidgetPositionParams { + // First search for last widget + const field = untracked(() => this.filedState()); + const widgetPositions = untracked(() => this.positionsStateInternal()); + + let widgetId: string | undefined = undefined; + + for (let index = field.length - 1; index >= 0; index--) { + if (field[index] !== EMPTY) { + widgetId = this.widgetIDs.getStringIdByNumber(field[index]); + break; + } + } + + // If there is no widgets, define new one at first position + if (widgetId === undefined) { + return { row: 1, column: 1, widthInCells, heightInCells }; + } + + const lastWidgetPosition = widgetPositions[widgetId]; + + // If last widget heights smaller the new widget's height - allocate new widget on the new row + const nextRow = lastWidgetPosition.bottomEdge + 1; + if (lastWidgetPosition.heightInCells < heightInCells) { + return { row: nextRow, column: 1, widthInCells, heightInCells }; + } + + // Check if there is enough available cells in width after last widget + const column = lastWidgetPosition.rightEdge + 1; + const rightEdge = column + widthInCells - 1; + if (rightEdge > this._colCount) { + //If not allocate new widget on the new row + return { row: nextRow, column: 1, widthInCells, heightInCells }; + } + + // In case of available space, allocate new widget just after the last one + return { row: lastWidgetPosition.row, column, widthInCells, heightInCells }; + } + setHiddenWidgets(widgetsIds: string[]): void { this.hiddenWidgets.set(widgetsIds); } @@ -262,8 +313,8 @@ export class WidgetsPositionsStateService implements OnDestroy { return originalPositions; } const filed = untracked(() => this.filedState()); + const fieldBottom = untracked(() => this.fieldBottom()); const hiddenWidgetsNumIds = new Set(hiddenWidgets.map((idStr) => this.widgetIDs.getNumericIdByString(idStr))); - const fieldBottom = Math.max(...positions.map((item) => item.bottomEdge)); // This arrays show, how many rows are skipped above each row const hiddenRowsState: number[] = new Array(fieldBottom); @@ -304,13 +355,14 @@ export class WidgetsPositionsStateService implements OnDestroy { return result; } - private determineInitialPositions(): Record { - return Object.entries(this._gridConfig.defaultElementParams).reduce( - (res, [key, info]) => { - res[key] = new WidgetPosition(key, info.position); - return res; - }, - {} as Record, - ); + private determineInitialPositions(): void { + for (const info of this._gridConfig.defaultElementParams) { + if (!!this.getPosition(info.id)) { + continue; + } + const positionParams = this.findProperPosition(info.widthInCells, info.heightInCells); + const position = new WidgetPosition(info.id, positionParams); + this.updatePosition(position); + } } } diff --git a/projects/step-frontend/src/lib/modules/execution/services/executions-panels.service.ts b/projects/step-frontend/src/lib/modules/execution/services/executions-panels.service.ts index c0f845e3c8..9b1115e858 100644 --- a/projects/step-frontend/src/lib/modules/execution/services/executions-panels.service.ts +++ b/projects/step-frontend/src/lib/modules/execution/services/executions-panels.service.ts @@ -7,6 +7,9 @@ type FieldsAccessor = Mutable; type Panel = { id: string; label: string }; +/** + * @deprecated Relates to legacy execution's view + * **/ @Injectable({ providedIn: 'root', }) @@ -75,7 +78,7 @@ export class ExecutionsPanelsService { return !!this.getPanel(viewId, executionId)?.enabled; } - toggleShowPanel(viewId: string, executionId: string) { + toggleShowPanel(viewId: string, executionId: string): void { this.setShowPanel(viewId, !this.isShowPanel(viewId, executionId), executionId); } @@ -92,7 +95,7 @@ export class ExecutionsPanelsService { return this.getPanel(viewId, executionId)?.label || ''; } - updateObservable(viewId: string, executionId: string) { + updateObservable(viewId: string, executionId: string): void { if (this._panels$[executionId] && this._panels$[executionId][viewId]) { this._panels$[executionId][viewId].next(this._panels[executionId][viewId]); } diff --git a/projects/step-frontend/src/lib/modules/execution/services/single-execution-panels.service.ts b/projects/step-frontend/src/lib/modules/execution/services/single-execution-panels.service.ts index 6da9e7e970..2a1e5e5f6f 100644 --- a/projects/step-frontend/src/lib/modules/execution/services/single-execution-panels.service.ts +++ b/projects/step-frontend/src/lib/modules/execution/services/single-execution-panels.service.ts @@ -6,6 +6,9 @@ import { ItemInfo, Mutable } from '@exense/step-core'; type FieldAccessor = Mutable>; +/** + * @deprecated Relates to legacy execution's view + * **/ @Injectable() export class SingleExecutionPanelsService { private _executionsPanelService = inject(ExecutionsPanelsService); @@ -62,7 +65,7 @@ export class SingleExecutionPanelsService { this._executionsPanelService.setShowPanel(viewId, show, this.executionId); } - toggleShowPanel(viewId: string) { + toggleShowPanel(viewId: string): void { if (!this.executionId) { return; } diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts index 24ec1126e0..003b0da7dd 100644 --- a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts @@ -8,19 +8,13 @@ import { ExecutionModule } from '../../../execution/execution.module'; templateUrl: './grid-view-test.component.html', styleUrl: './grid-view-test.component.scss', providers: [ - provideGridLayoutConfig('gridTest', { - errorsWidget: { title: 'Errors widget', position: { row: 1, column: 1, widthInCells: 8, heightInCells: 1 } }, - testCases: { title: 'Test Cases', position: { row: 2, column: 1, widthInCells: 6, heightInCells: 3 } }, - testCasesSummary: { - title: 'Summary: Test Cases', - position: { row: 2, column: 7, widthInCells: 2, heightInCells: 3 }, - }, - keywordsSummary: { - title: 'Summary: Keyword Calls', - position: { row: 5, column: 1, widthInCells: 2, heightInCells: 3 }, - }, - keywordsList: { title: 'Keywords', position: { row: 5, column: 3, widthInCells: 6, heightInCells: 3 } }, - }), + provideGridLayoutConfig('gridTest', [ + { id: 'errorsWidget', title: 'Errors widget', widthInCells: 8, heightInCells: 1, weight: 1 }, + { id: 'testCases', title: 'Test Cases', widthInCells: 6, heightInCells: 3, weight: 1 }, + { id: 'testCasesSummary', title: 'Summary: Test Cases', widthInCells: 2, heightInCells: 3, weight: 1 }, + { id: 'keywordsSummary', title: 'Summary: Keyword Calls', widthInCells: 2, heightInCells: 3, weight: 1 }, + { id: 'keywordsList', title: 'Keywords', widthInCells: 6, heightInCells: 3, weight: 1 }, + ]), ], }) export class GridViewTestComponent { From c5d2edd11d3ff57f50ec6701cd7b324b4e1242db Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 4 Feb 2026 15:56:13 +0300 Subject: [PATCH 09/16] SED-4417 Persistence storage --- .../grid-layout/grid-layout.component.scss | 4 ++ .../grid-layout/grid-layout.component.ts | 2 + .../directives/grid-element.directive.ts | 3 +- .../grid-persistence-positions.service.ts | 54 +++++++++++++++++++ .../grid-session-storage.service.ts | 12 +++++ .../widgets-positions-state.service.ts | 39 ++++++++++++-- 6 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-persistence-positions.service.ts create mode 100644 projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-session-storage.service.ts diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss index c85ca35587..d059220901 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss @@ -10,6 +10,10 @@ step-grid-layout { column-gap: 1.5rem; position: relative; + &.hidden { + visibility: hidden; + } + .step-grid-element { position: relative; diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts index 7646d36962..cfdfe8a6ce 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.ts @@ -41,6 +41,7 @@ import { GridElementTitleComponent } from '../grid-element-title/grid-element-ti host: { '[class.show-preview]': 'showPreview()', '[class.edit-mode]': '_gridEditable.editMode()', + '[class.hidden]': '!isInitialised()', '[style.--style__cols-count]': '_colCount', }, hostDirectives: [ @@ -62,6 +63,7 @@ export class GridLayoutComponent implements AfterViewInit { private readonly isRenderComplete = signal(false); private readonly preview = viewChild>('preview'); + protected readonly isInitialised = computed(() => this._widgetPositions.isInitialized()); protected readonly showPreview = computed(() => { const isResize = this._gridElementResizer.resizeInProgress(); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts index 6ca72910f9..07d7c1cb9f 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/directives/grid-element.directive.ts @@ -38,7 +38,8 @@ export class GridElementDirective { private effectInitializePosition = effect(() => { const position = this.position(); const isRenderComplete = this.isRenderComplete(); - if (isRenderComplete && !position) { + const isPositionStateInitialized = this._positionsState.isInitialized(); + if (isRenderComplete && isPositionStateInitialized && !position) { this.updatePositionIfRequired(); } }); diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-persistence-positions.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-persistence-positions.service.ts new file mode 100644 index 0000000000..1279daa36d --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-persistence-positions.service.ts @@ -0,0 +1,54 @@ +import { inject, Injectable } from '@angular/core'; +import { WidgetPosition, WidgetPositionParams } from '../types/widget-position'; +import { catchError, map, Observable, of, timer } from 'rxjs'; +import { GridSessionStorageService } from './grid-session-storage.service'; + +@Injectable({ + providedIn: 'root', +}) +export class GridPersistencePositionsService { + private _storage = inject(GridSessionStorageService); + + savePositions(gridId: string, position: Record): Observable { + const positionParams = Object.entries(position).reduce( + (res, [key, position]) => { + res[key] = { + row: position.row, + column: position.column, + widthInCells: position.widthInCells, + heightInCells: position.heightInCells, + }; + return res; + }, + {} as Record, + ); + const paramsJson = JSON.stringify(positionParams); + this._storage.setItem(gridId, paramsJson); + return timer(100).pipe(map(() => {})); + } + + loadPositions(gridId: string): Observable> { + return timer(250).pipe( + map(() => gridId), + map((gridId) => this._storage.getItem(gridId)), + map((paramsJson) => { + if (!paramsJson) { + return {} as Record; + } + return JSON.parse(paramsJson) as Record; + }), + catchError((err) => { + return of({} as Record); + }), + map((params) => + Object.entries(params).reduce( + (res, [key, positionParam]) => { + res[key] = new WidgetPosition(key, positionParam); + return res; + }, + {} as Record, + ), + ), + ); + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-session-storage.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-session-storage.service.ts new file mode 100644 index 0000000000..e61ef99e6a --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-session-storage.service.ts @@ -0,0 +1,12 @@ +import { Inject, Injectable } from '@angular/core'; +import { SESSION_STORAGE, StorageProxy } from '../../basics/step-basics.module'; + +@Injectable({ + providedIn: 'root', +}) +export class GridSessionStorageService extends StorageProxy { + // eslint-disable-next-line @angular-eslint/prefer-inject + constructor(@Inject(SESSION_STORAGE) _sessionStorage: Storage) { + super(_sessionStorage, 'GRID'); + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts index 9dd3bf0547..4808071017 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/widgets-positions-state.service.ts @@ -1,9 +1,12 @@ -import { computed, inject, Injectable, OnDestroy, signal, untracked } from '@angular/core'; +import { computed, DestroyRef, inject, Injectable, OnDestroy, signal, untracked } from '@angular/core'; import { WidgetPosition, WidgetPositionParams } from '../types/widget-position'; import { GRID_COLUMN_COUNT } from './grid-column-count.token'; import { GridEditableService } from './grid-editable.service'; import { WidgetIDs } from '../types/widget-ids'; import { GRID_LAYOUT_CONFIG } from './grid-layout-config.token'; +import { GridPersistencePositionsService } from './grid-persistence-positions.service'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { debounceTime, switchMap } from 'rxjs'; const EMPTY = 0; @@ -12,12 +15,19 @@ export class WidgetsPositionsStateService implements OnDestroy { private _gridConfig = inject(GRID_LAYOUT_CONFIG); private _colCount = inject(GRID_COLUMN_COUNT); private _gridEditable = inject(GridEditableService); + private _gridPersistencePositions = inject(GridPersistencePositionsService); + private _destroyRef = inject(DestroyRef); private widgetIDs = new WidgetIDs(this._gridConfig.defaultElementParams.map((item) => item.id)); - protected readonly hiddenWidgets = signal([]); + private readonly isInitializedInternal = signal(false); + readonly isInitialized = this.isInitializedInternal.asReadonly(); + + private readonly hiddenWidgets = signal([]); private readonly positionsStateInternal = signal>({}); + private readonly positionsState$ = toObservable(this.positionsStateInternal); + readonly positions = computed(() => { const positions = this.positionsStateInternal(); const hiddenWidgets = this.hiddenWidgets(); @@ -50,7 +60,7 @@ export class WidgetsPositionsStateService implements OnDestroy { }); constructor() { - this.determineInitialPositions(); + this.initialize(); } ngOnDestroy(): void { @@ -270,6 +280,19 @@ export class WidgetsPositionsStateService implements OnDestroy { this.hiddenWidgets.set(widgetsIds); } + private initialize(): void { + const isInitialized = untracked(() => this.isInitialized()); + if (isInitialized) { + return; + } + this._gridPersistencePositions.loadPositions(this._gridConfig.gridId).subscribe((positions) => { + this.positionsStateInternal.set(positions); + this.determineInitialPositions(); + this.isInitializedInternal.set(true); + this.setupPositionsSync(); + }); + } + private isCellTaken(row: number, column: number): boolean { const field = untracked(() => this.filedState()); const index = this.getFieldIndex(row, column); @@ -365,4 +388,14 @@ export class WidgetsPositionsStateService implements OnDestroy { this.updatePosition(position); } } + + private setupPositionsSync(): void { + this.positionsState$ + .pipe( + debounceTime(300), + switchMap((positions) => this._gridPersistencePositions.savePositions(this._gridConfig.gridId, positions)), + takeUntilDestroyed(this._destroyRef), + ) + .subscribe(); + } } From 3a24aff4ffeb10ba5fc95b3572ccd016fd791af4 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 4 Feb 2026 17:10:29 +0300 Subject: [PATCH 10/16] SED-4417 Grid dynamic panels registration --- .../custom-registries.module.ts | 1 + .../grid-settings-registry.service.ts | 27 ++++++++++++ .../grid-element-title.component.ts | 2 +- .../injectables/grid-layout-config.token.ts | 34 +++++++++------ .../grid-view-test.component.ts | 10 +---- .../grid-view-test.initializer.ts | 43 ++++++++++++++++++- .../grid-view-test/shared/grid-test.ts | 1 + 7 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 projects/step-core/src/lib/modules/custom-registeries/services/grid-settings-registry.service.ts create mode 100644 projects/step-frontend/src/lib/modules/grid-view-test/shared/grid-test.ts diff --git a/projects/step-core/src/lib/modules/custom-registeries/custom-registries.module.ts b/projects/step-core/src/lib/modules/custom-registeries/custom-registries.module.ts index 087c0ce4a3..ea4cb54210 100644 --- a/projects/step-core/src/lib/modules/custom-registeries/custom-registries.module.ts +++ b/projects/step-core/src/lib/modules/custom-registeries/custom-registries.module.ts @@ -53,6 +53,7 @@ export * from './services/wizard-registry.service'; export * from './services/execution-custom-panel-registry.service'; export * from './services/entity-menu-items-registry.service'; export * from './services/automation-package-entity-table-registry.service'; +export * from './services/grid-settings-registry.service'; export { ItemInfo } from './services/base-registry.service'; export * from './shared/custom-registry-item'; export * from './shared/custom-registry-type.enum'; diff --git a/projects/step-core/src/lib/modules/custom-registeries/services/grid-settings-registry.service.ts b/projects/step-core/src/lib/modules/custom-registeries/services/grid-settings-registry.service.ts new file mode 100644 index 0000000000..a838d4912a --- /dev/null +++ b/projects/step-core/src/lib/modules/custom-registeries/services/grid-settings-registry.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; + +export interface GridElementInfo { + id: string; + title: string; + weight: number; + widthInCells: number; + heightInCells: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class GridSettingsRegistryService { + private girdsSettings: Map = new Map(); + + register(gridId: string, gridElement: GridElementInfo): void { + if (!this.girdsSettings.has(gridId)) { + this.girdsSettings.set(gridId, []); + } + this.girdsSettings.get(gridId)!.push(gridElement); + } + + getSettings(gridId: string): ReadonlyArray { + return (this.girdsSettings.get(gridId) ?? []).map((item) => ({ ...item })); + } +} diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts index ad96364bdc..a7fc3383e9 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-element-title/grid-element-title.component.ts @@ -14,7 +14,7 @@ export class GridElementTitleComponent { protected readonly displayTitle = computed(() => { const id = this._gridElement.elementId(); - const title = this._gridConfig.defaultElementParams?.find((item) => item.id === id)?.title; + const title = this._gridConfig.defaultElementParamsMap[id]?.title; return title ?? id; }); } diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts index 9774217274..0b59595927 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts +++ b/projects/step-core/src/lib/modules/editable-grid-layout/injectables/grid-layout-config.token.ts @@ -1,22 +1,30 @@ -import { InjectionToken, Provider } from '@angular/core'; - -export interface WidgetInfo { - id: string; - title: string; - weight: number; - widthInCells: number; - heightInCells: number; - //position: WidgetPositionParams; -} +import { inject, InjectionToken, Provider } from '@angular/core'; +import { GridElementInfo, GridSettingsRegistryService } from '../../custom-registeries/custom-registries.module'; export interface GridLayoutConfig { gridId: string; - defaultElementParams: WidgetInfo[]; + defaultElementParams: GridElementInfo[]; + defaultElementParamsMap: Record; } export const GRID_LAYOUT_CONFIG = new InjectionToken('Grid layout config'); -export const provideGridLayoutConfig = (gridId: string, defaultElementParams: WidgetInfo[] = []): Provider => ({ +export const provideGridLayoutConfig = (gridId: string, params: GridElementInfo[] = []): Provider => ({ provide: GRID_LAYOUT_CONFIG, - useFactory: () => ({ gridId, defaultElementParams }), + useFactory: () => { + const _gridSettingsRegistry = inject(GridSettingsRegistryService); + const defaultElementParams = [..._gridSettingsRegistry.getSettings(gridId), ...params].sort( + (a, b) => a.weight - b.weight, + ); + + const defaultElementParamsMap = defaultElementParams.reduce( + (res, item) => { + res[item.id] = item; + return res; + }, + {} as Record, + ); + + return { gridId, defaultElementParams, defaultElementParamsMap }; + }, }); diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts index 003b0da7dd..e10a02fedc 100644 --- a/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts +++ b/projects/step-frontend/src/lib/modules/grid-view-test/components/grid-view-test/grid-view-test.component.ts @@ -7,15 +7,7 @@ import { ExecutionModule } from '../../../execution/execution.module'; imports: [StepCoreModule, ExecutionModule], templateUrl: './grid-view-test.component.html', styleUrl: './grid-view-test.component.scss', - providers: [ - provideGridLayoutConfig('gridTest', [ - { id: 'errorsWidget', title: 'Errors widget', widthInCells: 8, heightInCells: 1, weight: 1 }, - { id: 'testCases', title: 'Test Cases', widthInCells: 6, heightInCells: 3, weight: 1 }, - { id: 'testCasesSummary', title: 'Summary: Test Cases', widthInCells: 2, heightInCells: 3, weight: 1 }, - { id: 'keywordsSummary', title: 'Summary: Keyword Calls', widthInCells: 2, heightInCells: 3, weight: 1 }, - { id: 'keywordsList', title: 'Keywords', widthInCells: 6, heightInCells: 3, weight: 1 }, - ]), - ], + providers: [provideGridLayoutConfig('gridTest')], }) export class GridViewTestComponent { readonly editMode = model(false); diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts b/projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts index 4570ab7022..965dda9e6a 100644 --- a/projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts +++ b/projects/step-frontend/src/lib/modules/grid-view-test/grid-view-test.initializer.ts @@ -1,6 +1,7 @@ import { inject, Injector, provideAppInitializer, runInInjectionContext } from '@angular/core'; -import { ViewRegistryService } from '@exense/step-core'; +import { GridSettingsRegistryService, ViewRegistryService } from '@exense/step-core'; import { GridViewTestComponent } from './components/grid-view-test/grid-view-test.component'; +import { GRID_TEST } from './shared/grid-test'; const registerRoutes = (): void => { const _viewRegistry = inject(ViewRegistryService); @@ -10,7 +11,47 @@ const registerRoutes = (): void => { }); }; +const registerGridLayout = (): void => { + const _gridSettings = inject(GridSettingsRegistryService); + _gridSettings.register(GRID_TEST, { + id: 'errorsWidget', + title: 'Errors widget', + widthInCells: 8, + heightInCells: 1, + weight: 1, + }); + _gridSettings.register(GRID_TEST, { + id: 'testCases', + title: 'Test Cases', + widthInCells: 6, + heightInCells: 3, + weight: 1, + }); + _gridSettings.register(GRID_TEST, { + id: 'testCasesSummary', + title: 'Summary: Test Cases', + widthInCells: 2, + heightInCells: 3, + weight: 1, + }); + _gridSettings.register(GRID_TEST, { + id: 'keywordsSummary', + title: 'Summary: Keyword Calls', + widthInCells: 2, + heightInCells: 3, + weight: 1, + }); + _gridSettings.register(GRID_TEST, { + id: 'keywordsList', + title: 'Keywords', + widthInCells: 6, + heightInCells: 3, + weight: 1, + }); +}; + export const GRID_VIEW_TEST_INITIALIZER = provideAppInitializer(() => { const _injector = inject(Injector); runInInjectionContext(_injector, registerRoutes); + runInInjectionContext(_injector, registerGridLayout); }); diff --git a/projects/step-frontend/src/lib/modules/grid-view-test/shared/grid-test.ts b/projects/step-frontend/src/lib/modules/grid-view-test/shared/grid-test.ts new file mode 100644 index 0000000000..8524b136bd --- /dev/null +++ b/projects/step-frontend/src/lib/modules/grid-view-test/shared/grid-test.ts @@ -0,0 +1 @@ +export const GRID_TEST = 'gridTest'; From 994953581107dd9539741c1d7824c67e1016f0a7 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 4 Feb 2026 19:15:02 +0300 Subject: [PATCH 11/16] SED-4417 Add new grid to execution view --- .../grid-layout/grid-layout.component.scss | 2 +- .../src/lib/modules/execution-common/index.ts | 1 + .../types/execution-report-grid.ts | 1 + .../injectables/view-registry.service.ts | 1 - projects/step-frontend/src/lib/app.module.ts | 2 - ...alt-execution-errors-widget.component.html | 1 + .../alt-execution-progress.component.ts | 17 +++-- ...t-execution-report-controls.component.html | 14 ++++ ...t-execution-report-controls.component.scss | 15 ++++ ...alt-execution-report-controls.component.ts | 8 +++ .../alt-execution-report.component.html | 45 +++++++----- .../alt-execution-report.component.scss | 37 +++++----- .../alt-execution-report.component.ts | 13 ++-- .../alt-execution-tree-widget.component.html | 8 ++- ...t-report-current-operations.component.html | 8 ++- ...alt-report-current-operations.component.ts | 2 +- .../alt-report-node-keywords.component.html | 1 - .../alt-report-node-list.component.html | 8 ++- .../alt-report-node-list.component.ts | 10 +-- .../alt-report-node-summary.component.html | 14 +++- .../alt-report-node-summary.component.ts | 17 +++-- .../alt-report-nodes-testcases.component.html | 2 +- ...-performance-overview-chart.component.html | 8 ++- .../lib/modules/execution/execution.module.ts | 70 +++++++++++++++++++ .../services/alt-execution-state.service.ts | 3 +- 25 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 projects/step-core/src/lib/modules/execution-common/types/execution-report-grid.ts diff --git a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss index d059220901..bf11c63a8c 100644 --- a/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss +++ b/projects/step-core/src/lib/modules/editable-grid-layout/components/grid-layout/grid-layout.component.scss @@ -5,7 +5,7 @@ step-grid-layout { display: grid; grid-template-columns: repeat($cols-count, calc((100% / $cols-count) - 1.5rem)); - grid-auto-rows: 14.6rem; + grid-auto-rows: 13rem; row-gap: 2.4rem; column-gap: 1.5rem; position: relative; diff --git a/projects/step-core/src/lib/modules/execution-common/index.ts b/projects/step-core/src/lib/modules/execution-common/index.ts index 1e92c1bc27..2832ade935 100644 --- a/projects/step-core/src/lib/modules/execution-common/index.ts +++ b/projects/step-core/src/lib/modules/execution-common/index.ts @@ -8,5 +8,6 @@ export * from './types/execution-params-config'; export * from './types/execution-view-mode'; export * from './pipes/execution-url.pipe'; export * from './pipes/execution-name.pipe'; +export * from './types/execution-report-grid'; export const EXECUTION_COMMON_EXPORTS = [ExecutionUrlPipe, ExecutionNamePipe]; diff --git a/projects/step-core/src/lib/modules/execution-common/types/execution-report-grid.ts b/projects/step-core/src/lib/modules/execution-common/types/execution-report-grid.ts new file mode 100644 index 0000000000..ba68d7d5c1 --- /dev/null +++ b/projects/step-core/src/lib/modules/execution-common/types/execution-report-grid.ts @@ -0,0 +1 @@ +export const EXECUTION_REPORT_GRID = 'executionReportGrid'; diff --git a/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts b/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts index c8f1f4ede0..2c82782eb0 100644 --- a/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts +++ b/projects/step-core/src/lib/modules/routing/injectables/view-registry.service.ts @@ -81,7 +81,6 @@ export class ViewRegistryService implements OnDestroy { this.registerMenuEntry('Keywords', 'functions', 'keyword', { weight: 10, parentId: 'automation-root' }); this.registerMenuEntry('Plans', 'plans', 'plan', { weight: 30, parentId: 'automation-root' }); this.registerMenuEntry('Parameters', 'parameters', 'list', { weight: 40, parentId: 'automation-root' }); - this.registerMenuEntry('Grid View', 'grid-view', 'grid', { weight: 50, parentId: 'automation-root' }); this.registerMenuEntry('Schedules', 'scheduler', 'clock', { weight: 100, parentId: 'automation-root' }); // Sub Menus Execute diff --git a/projects/step-frontend/src/lib/app.module.ts b/projects/step-frontend/src/lib/app.module.ts index cf3482945e..f005e9b872 100644 --- a/projects/step-frontend/src/lib/app.module.ts +++ b/projects/step-frontend/src/lib/app.module.ts @@ -31,7 +31,6 @@ import { AUTOMATION_PACKAGE_IMPORTS, AUTOMATION_PACKAGE_INITIALIZER } from './mo import { ERRORS_VIEW_IMPORTS, ERRORS_VIEW_INITIALIZER } from './modules/errors-view'; import { RESOURCE_IMPORTS, RESOURCES_INITIALIZER } from './modules/resources'; import { InProgressComponent } from './components/in-progress/in-progress.component'; -import { GRID_VIEW_TEST_INITIALIZER } from './modules/grid-view-test'; Settings.defaultLocale = 'en'; @@ -40,7 +39,6 @@ const MODULES_INITIALIZERS = [ AUTOMATION_PACKAGE_INITIALIZER, ERRORS_VIEW_INITIALIZER, RESOURCES_INITIALIZER, - GRID_VIEW_TEST_INITIALIZER, ]; @NgModule({ diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors-widget/alt-execution-errors-widget.component.html b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors-widget/alt-execution-errors-widget.component.html index c55c4e8b60..0c5eca547f 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors-widget/alt-execution-errors-widget.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors-widget/alt-execution-errors-widget.component.html @@ -26,5 +26,6 @@ } } +
diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-progress/alt-execution-progress.component.ts b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-progress/alt-execution-progress.component.ts index 33f5b97295..8f59443a17 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-progress/alt-execution-progress.component.ts +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-progress/alt-execution-progress.component.ts @@ -9,6 +9,7 @@ import { signal, viewChild, ViewEncapsulation, + model, } from '@angular/core'; import { catchError, @@ -175,7 +176,9 @@ export class AltExecutionProgressComponent private _treeLoader = inject(AggregatedTreeDataLoaderService); protected readonly isSmallScreen = toSignal(this._isSmallScreen$); - private toggleRequestWarning = viewChild('requestWarningRef', { read: ToggleRequestWarningDirective }); + private readonly toggleRequestWarning = viewChild('requestWarningRef', { read: ToggleRequestWarningDirective }); + + readonly gridEditMode = model(false); readonly timeRangeOptions: TimeRangePickerSelection[] = [ { type: 'FULL' }, @@ -200,7 +203,7 @@ export class AltExecutionProgressComponent takeUntilDestroyed(), ); - private execution = toSignal(this.execution$, { initialValue: undefined }); + private readonly execution = toSignal(this.execution$, { initialValue: undefined }); protected isAnalyticsRoute$ = this._router.events.pipe( filter((event) => event instanceof NavigationEnd), @@ -209,7 +212,7 @@ export class AltExecutionProgressComponent shareReplay(1), ); - protected isAnalyticsRoute = toSignal(this.isAnalyticsRoute$); + protected readonly isAnalyticsRoute = toSignal(this.isAnalyticsRoute$); /** Active execution's range selection data stream **/ readonly timeRangeSelection$ = this.activeExecution$.pipe( @@ -238,7 +241,7 @@ export class AltExecutionProgressComponent } }); - protected handleTimeRangeChange(selection: TimeRangePickerSelection) { + protected handleTimeRangeChange(selection: TimeRangePickerSelection): void { this.updateTimeRangeSelection(selection); } @@ -254,8 +257,8 @@ export class AltExecutionProgressComponent return execution.parameters as unknown as Array> | undefined; }), ); - protected isResolvedParametersVisible = signal(false); - protected isAgentsVisible = signal(false); + protected readonly isResolvedParametersVisible = signal(false); + protected readonly isAgentsVisible = signal(false); readonly displayStatus$ = this.execution$.pipe( map((execution) => (execution?.status === 'ENDED' ? execution?.result : execution?.status)), @@ -423,7 +426,7 @@ export class AltExecutionProgressComponent readonly currentEntity = this.execution; - private setupNavigationHistoryChange() { + private setupNavigationHistoryChange(): void { // subscribe to back and forward events this._router.events .pipe( diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.html b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.html index 0eb22ffb18..98fd8d8de0 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.html @@ -3,6 +3,20 @@ +@if (!(_isSmallScreen$ | async)) { + + + +} diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.scss b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.scss index e69de29bb2..9a23645336 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.scss +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.scss @@ -0,0 +1,15 @@ +@use 'projects/step-core/styles/core-variables' as var; + +:host { + display: flex; + align-items: center; + gap: 1rem; +} + +button.active { + background: var.$blue-10; + + step-icon { + color: var.$dark-blue; + } +} diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.ts b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.ts index f1f676e228..34a356360b 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.ts +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report-controls/alt-execution-report-controls.component.ts @@ -1,5 +1,7 @@ import { Component, inject } from '@angular/core'; import { AltExecutionReportPrintService } from '../../services/alt-execution-report-print.service'; +import { AltExecutionStateService } from '../../services/alt-execution-state.service'; +import { IS_SMALL_SCREEN } from '@exense/step-core'; @Component({ selector: 'step-alt-execution-report-controls', @@ -8,5 +10,11 @@ import { AltExecutionReportPrintService } from '../../services/alt-execution-rep standalone: false, }) export class AltExecutionReportControlsComponent { + protected _executionsState = inject(AltExecutionStateService); protected _printService = inject(AltExecutionReportPrintService); + protected readonly _isSmallScreen$ = inject(IS_SMALL_SCREEN); + + protected toggleGridEdit(): void { + this._executionsState.gridEditMode.update((value) => !value); + } } diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report/alt-execution-report.component.html b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report/alt-execution-report.component.html index 5913c857f0..6811171b30 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report/alt-execution-report.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-report/alt-execution-report.component.html @@ -1,8 +1,14 @@ + + @if (_state.errors$ | async; as errors) { } @if (_testCasesState.summary$ | async; as testCasesSummary) { } @if (_mode === ViewMode.VIEW) { - + } @if (_mode === ViewMode.VIEW) { } @if (_state.executionId$ | async; as executionId) { } - + + +
+ +
+
+
@if (_state.currentOperations$ | async; as currentOperations) { - + } @if (_state.execution$ | async; as execution) { @for (panel of customPanels; track panel.type) { } } + +
+ @if (_mode === ViewMode.PRINT) {