Serenity 10.3.0 Release Notes (2026-04-14)

These release notes detail the significant changes made in Serenity and StartSharp from version 10.2.0 to 10.3.0. For a complete list of changes, please refer to the Serenity Change Log.

Grid Editor Controller (StartSharp) — Inline Row Adding and Action Column

The GridEditController has been significantly enhanced with new inline editing capabilities, making it easier to build spreadsheet-like editing experiences.

Inline Row Adding

GridEditController now supports inline row adding directly in the grid, without opening a separate dialog. This is controlled via the addRow option:

import { GridEditController } from "@serenity-is/pro.extensions";

const controller = new GridEditController({
    grid: myGrid,
    addRow: "bottom",       // "bottom" (default), "top", or false
    addRowFrozen: true,    // pin the add row (requires EnhancedLayout)
    autoEdit: true,         // auto-enter edit mode on new row
    addRowInit: (item) => {
        item.SomeDefaultValue = "pre-filled";
    }
});

Key features:

  • Placement: Add row can be at the top ("top") or bottom ("bottom") of the grid
  • Frozen rows: When addRowFrozen is true, the add row stays visible during scrolling (requires EnhancedLayout)
  • Auto-add: When the user moves to another row while editing the add row without committing, a new add row is automatically created
  • Custom initialization: The addRowInit callback lets you pre-fill default values for new rows
  • isAddRowSymbol: A Symbol (Symbol.for("Serenity.isAddRow")) is used to identify add-row items internally

Action Column

GridEditController now includes an action column (default ID "RowEditActions") with per-row action buttons:

Option Type Default Description
actionsColumn string "RowEditActions" Column ID for action buttons
showDeleteAction boolean true Show delete icon for data rows
showDeleteConfirmation boolean \| () => boolean true Confirm before delete

The action column renders:

  • Green + icon for uninitialized add rows → creates a new item
  • Red undo + green check icons for initialized add rows (cancel/save)
  • Red trash icon for data rows (delete)
// Use the static helper to create the action column
const actionColumn = GridEditController.createActionsColumn({
    width: 50,
    cssClass: "my-actions"
});

Keyboard Shortcuts

  • Ctrl+Enter — Commit the current row changes (when commitRowViaKey is true, default)
  • Ctrl+Delete — Delete the current row (when deleteRowViaKey is true, default false)
  • Double Escape — Undo changes for the current row (when undoRowDblEsc is true, default)

Auto-Save and Dirty Tracking

new GridEditController({
    grid: myGrid,
    autoSave: true,     // automatically save changes on cell edit
    // Only saves when the item is actually dirty:
    shouldAutoSave: ({ item }) => controller.isItemDirty(item)
});

The controller tracks dirty state per item and per field:

  • isItemDirty(item) — checks if any field has changed
  • isFieldDirty(item, field) — checks if a specific field has changed
  • Modified cells get the slick-cell-dirty CSS class

Validation of Invisible Columns

When saving an add row, the controller now validates ALL columns (including invisible ones). If validation fails on an invisible column, a notification is shown instead of trying to set the active cell on a hidden column.

Conversion to Widget

GridEditController was rewritten from a plain class to Widget<GridEditOptions<TItem>>, giving it proper lifecycle management via destroy():

// Auto-registration
const controller = GridEditController.enableAutoRegister(myGrid, options);

// Later, clean up
controller.destroy();

Renamed: SelectableAttributeShowSelectionAttribute

The SelectableAttribute has been renamed to ShowSelectionAttribute to better identify its intent and usage in SleekGrid. It only controls whether the "selected" CSS class should be applied to cells, not whether a cell can actually be selected (which is controlled by focusable).

// Old (deprecated, still works):
[Selectable(false)]
public string MyColumn { get; set; }

// New:
[ShowSelection(false)]
public string MyColumn { get; set; }

The PropertyItem.selectable property has been renamed to showSelection accordingly. The old SelectableAttribute is deprecated but still functional.

New TabbableAttribute

A new TabbableAttribute has been introduced to control tab navigation behavior for editable grid columns:

// Prevent tabbing to this column's cells
[Tabbable(false)]
public decimal LineTotal { get; set; }

This maps to the new tabbable option in SleekGrid columns (default true). If a column has focusable: false, the tabbable property has no effect (tab cannot navigate to a non-focusable cell).

SleekGrid: Individual EditorLock Per Grid

Previously, all grids shared the same GlobalEditorLock by default, which could cause conflicts between editable grids in different unrelated dialogs. Now, if options.editorLock is set to null, SleekGrid automatically creates a new EditorLock instance for each grid:

// In ScriptInit.ts or equivalent:
gridDefaults.editorLock = null; // Each grid gets its own EditorLock

This resolves issues where editing in one grid could interfere with another grid's editing state.

Additionally, setOptions() now calls prepareForOptionsChange() which properly handles the case where the edit controller is active but commit fails, gracefully cancelling the current edit.

IIntrinsicPropertyAttributeProvider — Combination Attributes

A new IIntrinsicPropertyAttributeProvider interface has been introduced, allowing an attribute to "inject" additional attributes onto a property during code generation:

public interface IIntrinsicPropertyAttributeProvider
{
    object PropertyAttributes { get; }
}

This enables creating combination attributes that apply multiple properties at once:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RowEditActionsColumnAttribute : Attribute, IIntrinsicPropertyAttributeProvider
{
    [DisplayName("Controls.EntityGrid.RowEditActionsTitle"),
     FixedWidth(40), Focusable(false), ShowSelection(false), Unbound]
    public virtual object PropertyAttributes { get; }
}

When a property has [RowEditActionsColumn], it effectively behaves as if [DisplayName(...), FixedWidth(40), Focusable(false), ShowSelection(false), Unbound] were all applied.

GetAttributesWithIntrinsic() Helper

A new GetAttributesWithIntrinsic() helper method complements this by returning both direct attributes and any intrinsic attributes provided via IIntrinsicPropertyAttributeProvider. This is used by:

  • Sergen/Pro.Coder for code generation (both Cecil and Roslyn paths)
  • Property item providers for form/column generation
  • BasedOnRowPropertyNameAnalyzer for Roslyn analysis

AttributeOrigin Enhancement

The AttributeOrigin enum now includes an Intrinsic = 2 flag:

[Flags]
public enum AttributeOrigin
{
    Explicit = 0,
    Inherit = 1,
    Intrinsic = 2,
    Annotation = 4,
    BasedOnField = 8,
    All = Inherit | Intrinsic | Annotation | BasedOnField,
}

IPropertyInfo.GetAttribute() and GetAttributes() now accept an origin argument to selectively include/exclude intrinsic, inherited, and field-based attributes.

FocusableAttribute and ShowSelectionAttribute

Two new attributes for finer control over grid column behavior:

// Focusable(false) prevents cells from being entered/edited
[Focusable(false)]
public string ReadOnlyDisplayColumn { get; set; }

// ShowSelection(false) prevents the "selected" CSS class
[ShowSelection(false)]
public string NoHighlightColumn { get; set; }

These were previously available but are now properly exposed as first-class attributes, and the FocusableAttribute now has the correct [AttributeUsage(AttributeTargets.Property)].

IPropertySource Derives from IPropertyInfo

IPropertySource now derives from IPropertyInfo, making it a proper extension of the property information hierarchy:

// Before: IPropertySource had its own Property member
// After: IPropertySource extends IPropertyInfo, so Property can be removed in the future

The ReflectedType is now available through IPropertySource, making it easier to work with reflected property metadata without relying on IPropertySource.Property (which may be removed in the future to allow dynamic/mock properties).

TypeScript 6 Migration

The project has been migrated to TypeScript 6.0.2/6.0.3. This brings several changes:

Strict Mode Default

TypeScript 6 now has strict: true by default. Since many Serenity patterns rely on non-strict behavior, we explicitly set "strict": false in tsconfig.json files:

{
    "compilerOptions": {
        "strict": false,
        "noUncheckedSideEffectImports": false
    }
}

rootDir Set to "."

TypeScript 6 errors when rootDir is not explicitly set (even though "." is the default). We added "rootDir": "." to all tsconfig files.

noUncheckedSideEffectImports: false

TypeScript 6 enables noUncheckedSideEffectImports by default, which causes errors for side-effect-only imports (e.g., import "./my-module"). We explicitly disable this.

noImplicitOverride: true

The noImplicitOverride option is now enabled, requiring the override keyword when overriding methods in derived classes.

esModuleInterop: true

Ensures compatibility with CommonJS modules.

If you are upgrading your project, add these settings to your tsconfig.json:

{
    "compilerOptions": {
        "strict": false,
        "noUncheckedSideEffectImports": false,
        "noImplicitOverride": true,
        "rootDir": "."
    }
}

RemoteView.onRecalcRows Event

A new onRecalcRows event has been added to RemoteView, allowing interception of the row recalculation process before row diffs are computed. This is useful for injecting special rows (like fixed-location rows or add-row placeholders) that should be ignored in filtering/grouping calculations:

const view = myGrid.view as RemoteView;

view.onRecalcRows.subscribe((e, args: ArgsRecalcRows) => {
    // Inject a special row at a specific position
    args.rows.splice(0, 0, specialRow);
});

The itemMetadataCallback can now also be get/set via getItemMetadataCallback() and setItemMetadataCallback() methods on IRemoteView.

Async commitEdits for Property Grid

A new async commitEdits method has been added to the property grid, allowing pending edits to be asynchronously committed before saving. This is particularly useful for inline grid editors where the editor dialog itself needs to ensure all pending changes are saved before the dialog closes:

// In an entity dialog, before saving:
async save() {
    // Commit any pending edits in inline editors
    await this.commitEdits?.();
    // Then proceed with the normal save
    await super.save();
}

Improved Error Handling for Async Errors

The error handling system has been significantly improved to better handle unhandledRejection (Promise) errors:

  • runtimeErrorHandler now accepts both string messages and ErrorEvent objects
  • Unhandled promise rejections are now reported in development mode with proper distinction from regular script errors
  • Error notifications show "UNCAUGHT ERROR! (in promise)!" for promise rejections vs "SCRIPT ERROR!" for regular errors
  • Service call errors with origin == "serviceCall" are handled separately and not reported as unhandled rejections
import { ErrorHandling } from "@serenity-is/corelib";

// In development mode, async errors now show a proper notification:
// "UNCAUGHT ERROR! (in promise)! See browser console (F12) for details."

DeleteRowActionFormatter and GridEditorBase Service Methods

DeleteRowActionFormatter

A new DeleteRowActionFormatter has been added to @serenity-is/extensions for adding a delete action column to grid editors:

// In your columns definition:
[DeleteRowActionFormatter, DisplayName(""), FixedWidth(24), ReadOnly(true), Unbound]
public object DeleteRow { get; set; }

The formatter renders a trash icon for valid data rows and handles the click event:

// In your grid editor:
protected override onClick(e: Event, row: number, cell: number) {
    super.onClick(e, row, cell);

    const action = (e.target as HTMLElement).closest(".inline-action")
        ?.getAttribute("data-action");
    if (action === "delete-row") {
        e.preventDefault();
        confirmDialog(EntityDialogTexts.DeleteConfirmation, async () => {
            await this.delete({ request: { EntityId: this.itemId(this.itemAt(row)) } });
        });
    }
}

GridEditorBase Service Methods

GridEditorBase now provides three new methods for service endpoint resolution:

protected getCreateServiceMethod() {
    return this.getService() + '/Create';
}

protected getDeleteServiceMethod() {
    return this.getService() + '/Delete';
}

protected getUpdateServiceMethod() {
    return this.getService() + '/Update';
}

These can be overridden to customize the service URLs used by the save() and delete() methods.

HeaderFiltersMixin Using onSetViewParams Event

The HeaderFiltersMixin now uses the onSetViewParams event in addition to (or instead of) relying on onSubmitting:

// The mixin now subscribes to:
this.dataGrid.onSetViewParams.subscribe(handleGridSetViewParams);
this.dataGrid.onFiltering.subscribe(handleGridFiltering);

This fixes an issue where ExcelExportHelper could not set its filters properly when onViewSubmit was manually called instead of prepareSubmit.

Key improvements:

  • NotIn optimization: When more than 20 values are checked (configurable via useNotInThreshold), it switches to NOT IN with unchecked values, reducing request size
  • Null handling: Separates null values from non-null for IN/NOT IN queries using isNull/isNotNull
  • Skip column pattern: Properly handles the skip column to avoid recursing into setViewParams during filter value fetching

Column Picker: FilterOnly and ReadPermission Filtering

Columns with [FilterOnly] attribute or columns that lack the required ReadPermission are now properly excluded from the column picker:

// This column will NOT appear in the column picker
[FilterOnly]
public int SearchOnlyField { get; set; }

// This column will NOT appear if the user lacks Admin permission
[ReadPermission("Administration:Admin")]
public int RestrictedField { get; set; }

Previously, such columns could accidentally appear in the column picker's hidden columns list.

Data Grid Persistence with Always Visible/Hidden Columns

The logic for always visible and always hidden columns has been extended to work with data grid persistence:

  • Columns with the AlwaysVisible attribute are now properly persisted as visible
  • Columns with the Hidden attribute are properly persisted as hidden
  • This ensures that column visibility state is maintained correctly across sessions

Bootstrap Tooltip Workaround

A workaround was added for a Bootstrap 5 bug (#37474) where tooltip instances could fail to dispose properly:

// workaround for https://github.com/twbs/bootstrap/issues/37474
instance._activeTrigger = {};
instance._element = document.createElement('noscript'); // placeholder with no behavior

This prevents errors when tooltips are rapidly shown/hidden or when elements with tooltips are removed from the DOM.

Switch to Playwright from Selenium

The functional test infrastructure has been migrated from Selenium WebDriver to Playwright (via Microsoft.Playwright.Xunit.v3), and from xUnit v2 to xUnit v3:

Benefits

  • Faster test execution — Playwright's modern architecture is significantly faster
  • Better debugging — Playwright's tracing and screenshot capabilities
  • Simpler setup — No need for separate WebDriver binaries
  • Automatic browser management — Playwright handles browser installation

PlaywrightSettings

A programmatic configuration system for Playwright settings was implemented:

public class PlaywrightSettings
{
    public string Browser { get; set; } = "Chromium";
    public BrowserTypeLaunchOptions LaunchOptions { get; set; }

    public static void Initialize()
    {
        new PlaywrightSettings()
        {
            LaunchOptions = new() {
                Headless = Environment.OSVersion.Platform != PlatformID.Win32NT
            }
        }.Install();
    }
}

Key behavior: Headless mode is enabled only on non-Windows (Linux CI). On Windows, tests run with a visible browser for easier debugging.

DataExplorer: Criteria and Distinct Support

The DataExplorer feature now supports criteria and distinct query modes:

Criteria Support

A new DataExplorerCriteriaReplacer validates and processes user-supplied criteria:

public class DataExplorerCriteriaReplacer : BaseCriteriaVisitor
{
    public void Validate(BaseCriteria criteria)  // throws on invalid fields
    public BaseCriteria Process(BaseCriteria criteria)  // transforms to actual field names
}
  • Checks each field expression against known columns
  • Converts values to proper types (Int32, Int64, Decimal, etc.)
  • Rejects param-type criteria

Distinct Support

The DistinctFields option in DataExplorerEndpoint enables distinct value queries, which power the HeaderFiltersMixin dropdowns:

// Server returns distinct values for the specified fields
request.DistinctFields = ["Country", "City"];

Recaptcha CSP Integration (StartSharp)

Content Security Policy (CSP) support has been enhanced for pages using Google Recaptcha:

<!-- Recaptcha URLs are now added to CSP directives automatically -->
script-src:
  https://www.google.com/recaptcha/
  https://www.gstatic.com/recaptcha/

frame-src:
  https://www.google.com/recaptcha/

The CSP framework (HtmlCspExtensions) now supports:

  • Nonce generation: CspNonce() generates a 32-byte random nonce for inline scripts
  • Auto-quoting: Values matching [A-Za-z0-9_-]+ are auto-quoted (e.g., unsafe-inline'unsafe-inline')
  • Import map integration: CSP entries added via import maps

BasedOnRowPropertyNameAnalyzer — Intrinsic Attribute Awareness

The BasedOnRowPropertyNameAnalyzer Roslyn diagnostic analyzer now uses GetAttributesWithIntrinsic() to properly detect [SkipNameCheck], [IgnoreUIField], [NotMapped], [IgnoreName], and other attributes even when applied through intrinsic attributes (e.g., RowEditActionsColumnAttribute which provides [Unbound] intrinsically).

Diagnostics

Code Severity Description
SRN0001 Error "Property 'X' does not match any property name in the RowType"
SRN0002 Error "Property 'X' does match any property name (did you mean 'Y'?)" (case mismatch)

Code Fixes

The BasedOnRowPropertyNameCodeFixProvider offers two fixes:

  1. Rename the property to match the row property name
  2. Add [SkipNameCheck] attribute

Other Serenity Framework Changes

ServerTypingsGenerator: Non-Local Module Filtering

Types from non-local modules in node_modules are now skipped in addition to types from declaration files. This prevents generating duplicate editor, formatter, and other .attributes for types in project package references.

onSetViewParams Event on DataGrid

A new onSetViewParams event has been added to DataGrid, allowing subscribers to modify view parameters before a list request is submitted:

dataGrid.onSetViewParams.subscribe((e, args) => {
    const request = args.view.params as ListRequest;
    request.Criteria = Criteria.and(request.Criteria, someAdditionalCriteria);
});

The ChartInDialog sample now includes:

  • A maximize/restore button for modals
  • Dragging capability for modals
  • Absolute positioning for the modal-dialog element to simplify calculations
  • The modal stays in view when resizing the window

RemoteView.getItemMetadata Type Fix

The type of getItemMetadata has been changed from ItemMetadata<TItem> to ItemMetadata<any> to resolve type compatibility issues.

SleekGrid.canCellBeActive Now Accepts tab Parameter

The canCellBeActive method now accepts an optional tab parameter to distinguish between tab navigation and click navigation:

canCellBeActive(row: number, cell: number, tab?: boolean): boolean

When tab is true, columns with tabbable: false are skipped during tab navigation.

Bug Fixes (10.2.1 through 10.3.0)

Null Reference in Grid Inline Editor (10.2.9)

Fixed a null reference issue when a grid inline editor's in-place add button is clicked while another inline editor grid is opened.

GetAttribute Default inherit Parameter (10.2.6)

The deprecated GetAttribute method had inherit: false as default, whereas GetCustomAttribute (the recommended replacement) has inherit: true by default. This caused issues with [DefaultHandler] when a base class uses it.

ServerTypingsGenerator: Non-Local Module Types (10.2.1)

Types from non-local modules in node_modules are now properly skipped in addition to declaration file types.

DateOnlyField Format String (10.2.3)

Fixed an issue with DateOnlyField due to an extra quote in the format string. Sergen reverted to generating DateTime fields for SQL date columns as DateOnlyField is not yet stable.

Column FilterOnly/ReadPermission in Column Picker (10.2.2)

Fixed: Columns with [FilterOnly] attribute or columns lacking the required ReadPermission are no longer shown in the column picker.

Fixed an issue where the summary footer value was not shown the first time when no columns have summaries and one is set via the column header menu.

Header Filters Dropdown Reuse (10.2.2)

Fixed an issue where header filter dropdown contents were not emptied before appending new search results, causing stale data.

Validation Rule Disposal (10.2.3)

Added a disposing listener to the target element during addValidationRule to resolve issues when editors with custom validation rules are disposed in inline grid editors while validation callbacks are still executing.

Bootstrap Tooltip Dispose Issue (10.2.3)

Fixed a Bootstrap 5 tooltip disposal issue that could cause errors when tooltips are rapidly shown/hidden.

HeaderFiltersPlugin.getFilterValue Null Check (10.3.3+)

Ensured HeaderFiltersPlugin.getFilterValue does not fail when value is null/undefined.

applyFormatterResultToCellNode innerHTML vs innerText (10.3.4)

Fixed: applyFormatterResultToCellNode was setting innerHTML instead of innerText when enableHtmlRendering is false, causing wrong content to be rendered on cell updates.

Package Updates

Updated NPM packages:

  • @serenity-is/tsbuild to 10.2.4 / 10.3.0
  • typescript to 6.0.2 / 7.0.2 (workspace catalog)
  • @preact/signals-core to 1.14.1
  • @preact/signals to 2.9.0
  • preact to 10.29.1
  • @tiptap packages to 3.22.2
  • vitest to 4.1.3
  • esbuild to 0.28.0
  • jspdf to 4.2.1
  • jsdom to 29.0.1
  • dompurify to 3.3.3
  • glob to 13.0.6
  • sortablejs to 1.15.7

Updated NuGet packages:

  • System/ASP.NET Core packages to 10.0.5
  • Microsoft.Data.SqlClient to 7.0.0
  • Microsoft.Build to 18.3.0
  • FluentMigrator to 8.0.1
  • MailKit to 4.15.1
  • Oracle.ManagedDataAccess.Core to 23.26.100
  • Npgsql to 10.0.2
  • coverlet.collector to 8.0.1
  • Scriban to 7.0.6
  • OpenIddict to 7.4.0
  • Microsoft.TypeScript.MSBuild to 6.0.3
  • Markdig to 1.1.2

Upgrading to 10.3.0

  1. Update NuGet packages to 10.3.0
  2. Update npm packages:
    • @serenity-is/tsbuild to 10.2.4+
  3. TypeScript 6 compatibility: Add these to your tsconfig.json:
    {
        "compilerOptions": {
            "strict": false,
            "noUncheckedSideEffectImports": false,
            "noImplicitOverride": true,
            "rootDir": "."
        }
    }
    
  4. Replace SelectableAttribute: If you use [Selectable], rename to [ShowSelection]. The old attribute still works but is deprecated.
  5. Review GridEditController options (StartSharp): If you use GridEditController, note that actionColumn has been renamed to actionsColumn and the default action column ID is now "RowEditActions".
  6. xUnit v3 migration (if using functional tests): Update test projects to xUnit v3 and switch from Selenium to Playwright.
  7. Check TabbableAttribute: If you have read-only columns that should be skipped during tab navigation, add [Tabbable(false)] to them.
  8. Review GridDefaults.editorLock: If you have multiple editable grids, consider setting gridDefaults.editorLock = null to give each grid its own EditorLock.
  9. Review HeaderFiltersMixin: If you use ExcelExportHelper with HeaderFiltersMixin, the new onSetViewParams-based approach should work more reliably.
  10. CSP compatibility: If you use Recaptcha, ensure the CSP directives are properly configured.