Serenity 10.3.0 Release Notes

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:

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:

// Use the static helper to create the action column
const actionColumn = GridEditController.createActionsColumn({
    width: 50,
    cssClass: "my-actions"
});

Keyboard Shortcuts

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:

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:

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:

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:

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:

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

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
}

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:

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:

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:

Updated NuGet packages:

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.