Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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