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
addRowFrozenistrue, the add row stays visible during scrolling (requiresEnhancedLayout) - 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
addRowInitcallback 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
commitRowViaKeyistrue, default) - Ctrl+Delete — Delete the current row (when
deleteRowViaKeyistrue, defaultfalse) - Double Escape — Undo changes for the current row (when
undoRowDblEscistrue, 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 changedisFieldDirty(item, field)— checks if a specific field has changed- Modified cells get the
slick-cell-dirtyCSS 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: SelectableAttribute → ShowSelectionAttribute
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
BasedOnRowPropertyNameAnalyzerfor 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:
runtimeErrorHandlernow accepts both string messages andErrorEventobjects- 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 toNOT INwith unchecked values, reducing request size - Null handling: Separates null values from non-null for
IN/NOT INqueries usingisNull/isNotNull - Skip column pattern: Properly handles the skip column to avoid recursing into
setViewParamsduring 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
AlwaysVisibleattribute are now properly persisted as visible - Columns with the
Hiddenattribute 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:
- Rename the property to match the row property name
- 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);
});
Modal Dragging Improvements
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.
Summary Footer Initial Display (10.2.2)
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/tsbuildto 10.2.4 / 10.3.0typescriptto 6.0.2 / 7.0.2 (workspace catalog)@preact/signals-coreto 1.14.1@preact/signalsto 2.9.0preactto 10.29.1@tiptappackages to 3.22.2vitestto 4.1.3esbuildto 0.28.0jspdfto 4.2.1jsdomto 29.0.1dompurifyto 3.3.3globto 13.0.6sortablejsto 1.15.7
Updated NuGet packages:
- System/ASP.NET Core packages to 10.0.5
Microsoft.Data.SqlClientto 7.0.0Microsoft.Buildto 18.3.0FluentMigratorto 8.0.1MailKitto 4.15.1Oracle.ManagedDataAccess.Coreto 23.26.100Npgsqlto 10.0.2coverlet.collectorto 8.0.1Scribanto 7.0.6OpenIddictto 7.4.0Microsoft.TypeScript.MSBuildto 6.0.3Markdigto 1.1.2
Upgrading to 10.3.0
- Update NuGet packages to 10.3.0
- Update npm packages:
@serenity-is/tsbuildto 10.2.4+
- TypeScript 6 compatibility: Add these to your
tsconfig.json:{ "compilerOptions": { "strict": false, "noUncheckedSideEffectImports": false, "noImplicitOverride": true, "rootDir": "." } } - Replace
SelectableAttribute: If you use[Selectable], rename to[ShowSelection]. The old attribute still works but is deprecated. - Review
GridEditControlleroptions (StartSharp): If you useGridEditController, note thatactionColumnhas been renamed toactionsColumnand the default action column ID is now"RowEditActions". - xUnit v3 migration (if using functional tests): Update test projects to xUnit v3 and switch from Selenium to Playwright.
- Check
TabbableAttribute: If you have read-only columns that should be skipped during tab navigation, add[Tabbable(false)]to them. - Review
GridDefaults.editorLock: If you have multiple editable grids, consider settinggridDefaults.editorLock = nullto give each grid its own EditorLock. - Review
HeaderFiltersMixin: If you useExcelExportHelperwithHeaderFiltersMixin, the newonSetViewParams-based approach should work more reliably. - CSP compatibility: If you use Recaptcha, ensure the CSP directives are properly configured.