Skip to content
13 changes: 12 additions & 1 deletion docs/en/studio/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@

This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version.

## 3.1.0 (2026-08-24) Latest
## 3.1.1 (2026-08-28) Latest

* Refactor release version resolution logic to ensure proper tag handling
* Fix chmod command syntax in set-executable-permissions scripts for Li…
* Restore macOS signing env values used by the 3.0 pipelines
* Show preparing-solution banner only after creation
* Add top spacing above the browser address bar
* Improve fullscreen flyout interaction and small-window clipping
* Fix Admin Console command input export
* Prefer Docker Compose v2 when starting containers

## 3.1.0 (2026-08-24)

* Clarify CLI tracked-process stop method: //github.com/volosoft/abp-studio/pull/4517
* Action of studio template test is working with self hosted agent
Expand Down
2 changes: 1 addition & 1 deletion docs/en/studio/version-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This document provides a general overview of the relationship between various ve

| **ABP Studio Version** | **ABP Version of Startup Template** |
|------------------------|---------------------------|
| 3.0.9 - 3.1.0 | 10.6.0 |
| 3.0.9 - 3.1.1 | 10.6.0 |
| 3.0.6 - 3.0.8 | 10.5.0 |
| 3.0.4 - 3.0.5 | 10.4.1 |
| 3.0.3 | 10.4.0 |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
afterNextRender,
AfterViewInit,
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
Injector,
LOCALE_ID,
Expand All @@ -17,9 +19,10 @@ import {
contentChild,
viewChild,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';

import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs';
import { Observable, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs';

import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import {
Expand Down Expand Up @@ -92,6 +95,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
})
export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestroy {
readonly #injector = inject(Injector);
readonly #destroyRef = inject(DestroyRef);
readonly getInjected = this.#injector.get.bind(this.#injector);
protected readonly locale = inject(LOCALE_ID);
protected readonly config = inject(ConfigStateService);
Expand Down Expand Up @@ -186,6 +190,10 @@ export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestr
},
);

private horizontalScrollOffset = 0;

private hasPendingHorizontalScrollOffset = false;

hasAtLeastOnePermittedAction: boolean;

readonly propList: EntityPropList<R>;
Expand Down Expand Up @@ -232,6 +240,7 @@ export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestr
}

this._data.set(dataValue.map((record, index) => this.prepareRecord(record, index)));
this.restoreHorizontalScrollOffset();
});
}

Expand Down Expand Up @@ -363,13 +372,66 @@ export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestr
ngAfterViewInit(): void {
if (!this.infiniteScroll()) {
this.list()
?.requestStatus$?.pipe(filter(status => status === 'loading'))
.subscribe(() => {
this._data.set([]);
?.requestStatus$?.pipe(takeUntilDestroyed(this.#destroyRef))
.subscribe(status => {
if (status === 'loading') {
this.rememberHorizontalScrollOffset();
this._data.set([]);
return;
}

// A failed request never reaches the data input, restore from here instead
if (status === 'error') {
this.restoreHorizontalScrollOffset();
}
});
}
}

private getBodyElement(): HTMLElement | null {
if (!this.isBrowser) {
return null;
}

return this.table()?.element?.querySelector('datatable-body') ?? null;
}

// The element carrying the column width is gone while loading without rows, so the browser
// resets the horizontal scroll position and the header keeps the offset it had before
private rememberHorizontalScrollOffset(): void {
const body = this.getBodyElement();
this.hasPendingHorizontalScrollOffset = true;

// Not scrollable while a previous request is still in flight, keep the offset taken back then
if (body && body.scrollWidth > body.clientWidth) {
this.horizontalScrollOffset = body.scrollLeft;
}
}

private restoreHorizontalScrollOffset(): void {
if (!this.hasPendingHorizontalScrollOffset) {
return;
}

this.hasPendingHorizontalScrollOffset = false;

afterNextRender(
() => {
const body = this.getBodyElement();

if (!body) {
return;
}

body.scrollLeft = this.horizontalScrollOffset;

// The header only follows a scroll event, scrolling to the same position does not raise one
body.dispatchEvent(new Event('scroll'));
},
{ injector: this.#injector },
);
}

ngOnDestroy(): void {
this.loadMoreSubscription.unsubscribe();
}
Expand Down
Loading