diff --git a/docs/en/low-code/custom-endpoints.md b/docs/en/low-code/custom-endpoints.md index 02f40d11f7..ad0057df70 100644 --- a/docs/en/low-code/custom-endpoints.md +++ b/docs/en/low-code/custom-endpoints.md @@ -61,18 +61,36 @@ Inside custom endpoint scripts, you have access to: | Variable | Description | |----------|-------------| +| `request` | Full request object | | `route` | Route parameter values (e.g., `route.id`) | +| `params` | Alias for route parameters | | `query` | Query string parameters (e.g., `query.q`, `query.page`) | | `body` | Request body (for POST/PUT) | -| `user` | Current user (same as `context.currentUser` in interceptors) | +| `headers` | Request headers | +| `user` | Current user (same as `context.currentUser` in [Interceptors](interceptors.md)) | +| `email` | Email sender (same as `context.emailSender` in [Interceptors](interceptors.md)) | ### Response Helpers | Function | HTTP Status | Description | |----------|-------------|-------------| | `ok(data)` | 200 | Success response with data | -| `notFound(message)` | 404 | Not found response | +| `created(data)` | 201 | Created response with data | +| `noContent()` | 204 | No content response | | `badRequest(message)` | 400 | Bad request response | +| `unauthorized(message)` | 401 | Unauthorized response | +| `forbidden(message)` | 403 | Forbidden response | +| `notFound(message)` | 404 | Not found response | +| `error(message)` | 500 | Internal server error response | +| `response(statusCode, data, error)` | Custom | Custom status code response | + +### Logging + +| Function | Description | +|----------|-------------| +| `log(message)` | Log an informational message | +| `logWarning(message)` | Log a warning message | +| `logError(message)` | Log an error message | ### Database API diff --git a/docs/en/low-code/fluent-api.md b/docs/en/low-code/fluent-api.md index fbdbd729ae..ff384ce294 100644 --- a/docs/en/low-code/fluent-api.md +++ b/docs/en/low-code/fluent-api.md @@ -248,6 +248,24 @@ public class Organization } ```` +### Enum Localization + +Enum values can be localized using ABP's localization system. Add localization keys in the format `Enum:{EnumTypeName}.{ValueName}` to your localization JSON files: + +```json +{ + "culture": "en", + "texts": { + "Enum:OrganizationType.Corporate": "Corporate", + "Enum:OrganizationType.Enterprise": "Enterprise", + "Enum:OrganizationType.Startup": "Startup", + "Enum:OrganizationType.Consulting": "Consulting" + } +} +``` + +The Blazor UI automatically uses these localization keys for enum dropdowns and display values. If no localization key is found, the enum member name is used as-is. + ## Fluent API The Fluent API has the **highest priority** in the configuration system. Use `AbpDynamicEntityConfig.EntityConfigurations` to override any attribute or JSON setting programmatically. @@ -263,47 +281,98 @@ public override void ConfigureServices(ServiceConfigurationContext context) "MyApp.Products.Product", entity => { - entity.SetDisplayProperty("Name"); + entity.DefaultDisplayPropertyName = "Name"; - entity.ConfigureProperty("Price", prop => - { - prop.SetRequired(true); - prop.SetUI(ui => - { - ui.SetDisplayName("Unit Price"); - ui.SetCreationFormAvailability(EntityPropertyUIFormAvailability.Available); - }); - }); - - entity.ConfigureProperty("InternalNotes", prop => + var priceProperty = entity.AddOrGetProperty("Price"); + priceProperty.AsRequired(); + priceProperty.UI = new EntityPropertyUIDescriptor { - prop.SetServerOnly(true); - }); + DisplayName = "Unit Price", + CreationFormAvailability = EntityPropertyUIFormAvailability.Available + }; + + entity.AddOrGetProperty("InternalNotes").AsServerOnly(); } ); } ```` -### Entity Configuration Methods +You can also use the generic overload with a type parameter: + +````csharp +AbpDynamicEntityConfig.EntityConfigurations.Configure(entity => +{ + entity.DefaultDisplayPropertyName = "Name"; +}); +```` + +### Entity Configuration -| Method | Description | +The `Configure` method provides an `EntityDescriptor` instance. You can set its properties directly: + +| Property / Method | Description | |--------|-------------| -| `SetDisplayProperty(name)` | Set the display property for lookups | -| `SetParent(entityName)` | Set parent entity for nesting | -| `SetUI(action)` | Configure entity-level UI | -| `ConfigureProperty(name, action)` | Configure a specific property | -| `AddInterceptor(name, type, js)` | Add a JavaScript interceptor. `name`: `"Create"`, `"Update"`, or `"Delete"`. `type`: `Pre`, `Post`, or `Replace`. `Replace-Create` must return the new entity's Id | +| `DefaultDisplayPropertyName` | Set the display property for lookups | +| `Parent` | Set parent entity name for nesting | +| `UI` | Set entity-level UI (`EntityUIDescriptor` with `PageTitle`) | +| `AddOrGetProperty(name)` | Get or create a property descriptor for configuration | +| `FindProperty(name)` | Find a property descriptor by name (returns `null` if not found) | +| `GetProperty(name)` | Get a property descriptor by name (throws if not found) | +| `Interceptors` | List of `CommandInterceptorDescriptor` — add interceptors directly | + +### Property Configuration -### Property Configuration Methods +`AddOrGetProperty` returns an `EntityPropertyDescriptor`. Configure it using direct property assignment and extension methods: -| Method | Description | +| Property / Extension Method | Description | |--------|-------------| -| `SetRequired(bool)` | Mark as required | -| `SetUnique(bool)` | Mark as unique | -| `SetServerOnly(bool)` | Hide from clients | -| `SetAllowSetByClients(bool)` | Allow client writes | -| `SetForeignKey(entityName, displayProp, access)` | Configure foreign key | -| `SetUI(action)` | Configure property UI | +| `.AsRequired(bool)` | Mark as required (extension method, returns the descriptor for chaining) | +| `.AsServerOnly(bool)` | Hide from clients (extension method, returns the descriptor for chaining) | +| `.MapToDbField(bool)` | Control if property is stored in DB (extension method, returns the descriptor for chaining) | +| `.IsUnique` | Set to `true` to mark as unique | +| `.AllowSetByClients` | Set to `false` to prevent client writes | +| `.ForeignKey` | Set a `ForeignKeyDescriptor` to configure foreign key relationship | +| `.UI` | Set an `EntityPropertyUIDescriptor` to configure property UI | + +### Chaining Extension Methods + +The extension methods `AsRequired()`, `AsServerOnly()`, and `MapToDbField()` return the property descriptor, enabling fluent chaining: + +````csharp +entity.AddOrGetProperty("InternalNotes") + .AsServerOnly() + .AsRequired() + .MapToDbField(); +```` + +### Configuring Foreign Keys + +````csharp +AbpDynamicEntityConfig.EntityConfigurations.Configure( + "MyApp.Orders.Order", + entity => + { + var customerIdProperty = entity.AddOrGetProperty("CustomerId"); + customerIdProperty.ForeignKey = new ForeignKeyDescriptor + { + EntityName = "MyApp.Customers.Customer", + DisplayPropertyName = "Name", + Access = ForeignAccess.Edit + }; + } +); +```` + +### Adding Interceptors + +````csharp +entity.Interceptors.Add(new CommandInterceptorDescriptor +{ + CommandName = "Create", + Type = InterceptorType.Pre, + Javascript = "if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }" +}); +```` ## Assembly Registration diff --git a/docs/en/low-code/index.md b/docs/en/low-code/index.md index 9adf40614b..86b2a34416 100644 --- a/docs/en/low-code/index.md +++ b/docs/en/low-code/index.md @@ -170,6 +170,74 @@ See [model.json Structure](model-json.md) for the full specification. | **Scripting API** | Server-side JavaScript for database queries and CRUD | [Scripting API](scripting-api.md) | | **Custom Endpoints** | REST APIs with JavaScript handlers | [Custom Endpoints](custom-endpoints.md) | | **Foreign Access** | View/Edit related entities from the parent's UI | [Foreign Access](foreign-access.md) | +| **Export** | Export entity data to Excel (XLSX) or CSV | See below | + +## Export (Excel / CSV) + +The Low-Code System provides built-in export functionality for all dynamic entities. Users can export filtered data to **Excel (XLSX)** or **CSV** directly from the Blazor UI. + +### How It Works + +1. The client calls `GET /api/low-code/entities/{entityName}/download-token` to obtain a single-use download token (valid for 30 seconds). +2. The client calls `GET /api/low-code/entities/{entityName}/export-as-excel` or `GET /api/low-code/entities/{entityName}/export-as-csv` with the token and optional filters. + +### API Endpoints + +| Endpoint | Description | +|----------|-------------| +| `GET /api/low-code/entities/{entityName}/download-token` | Get a single-use download token | +| `GET /api/low-code/entities/{entityName}/export-as-excel` | Export as Excel (.xlsx) | +| `GET /api/low-code/entities/{entityName}/export-as-csv` | Export as CSV (.csv) | + +Export requests accept the same filtering, sorting, and search parameters as the list endpoint. Server-only properties are automatically excluded, and foreign key columns display the referenced entity's display value instead of the raw ID. + +## Custom Commands and Queries + +The Low-Code System allows you to replace or extend the default CRUD operations by implementing custom command and query handlers in C#. + +### Custom Commands + +Create a class that implements `ILcCommand` and decorate it with `[CustomCommand]`: + +````csharp +[CustomCommand("Create", "MyApp.Products.Product")] +public class CustomProductCreateCommand : LcCommandBase +{ + public override async Task ExecuteWithResultAsync(DynamicCommandArgs commandArgs) + { + // Your custom create logic here + // ... + } +} +```` + +| Parameter | Description | +|-----------|-------------| +| `commandName` | The command to replace: `"Create"`, `"Update"`, or `"Delete"` | +| `entityName` | Full entity name (e.g., `"MyApp.Products.Product"`) | + +### Custom Queries + +Create a class that implements `ILcQuery` and decorate it with `[CustomQuery]`: + +````csharp +[CustomQuery("List", "MyApp.Products.Product")] +public class CustomProductListQuery : ILcQuery +{ + public async Task ExecuteAsync(DynamicQueryArgs queryArgs) + { + // Your custom list query logic here + // ... + } +} +```` + +| Parameter | Description | +|-----------|-------------| +| `queryName` | The query to replace: `"List"` or `"Single"` | +| `entityName` | Full entity name (e.g., `"MyApp.Products.Product"`) | + +Custom commands and queries are automatically discovered and registered at startup. They completely replace the default handler for the specified entity and operation. ## Internals @@ -182,8 +250,12 @@ See [model.json Structure](model-json.md) for the full specification. ### Application Layer -* `DynamicEntityAppService`: CRUD operations for all dynamic entities. -* `DynamicEntityUIAppService`: UI definitions, menu items, and page configurations. +* `DynamicEntityAppService`: CRUD operations for all dynamic entities (Get, GetList, Create, Update, Delete, Export). +* `DynamicEntityUIAppService`: UI definitions, menu items, and page configurations. Provides: + * `GetUiDefinitionAsync(entityName)` — Full UI definition (filters, columns, forms, children, foreign access actions, permissions) + * `GetUiCreationFormDefinitionAsync(entityName)` — Creation form fields with validation rules + * `GetUiEditFormDefinitionAsync(entityName)` — Edit form fields with validation rules + * `GetMenuItemsAsync()` — Menu items for all entities that have a `pageTitle` configured (filtered by permissions) * `DynamicPermissionDefinitionProvider`: Auto-generates permissions per entity. * `CustomEndpointExecutor`: Executes JavaScript-based custom endpoints. diff --git a/docs/en/low-code/interceptors.md b/docs/en/low-code/interceptors.md index 3353f44cc2..78d8bd0837 100644 --- a/docs/en/low-code/interceptors.md +++ b/docs/en/low-code/interceptors.md @@ -78,32 +78,51 @@ Inside interceptor scripts, you have access to: ### `context.commandArgs` -| Property | Type | Description | +| Property / Method | Type | Description | |----------|------|-------------| | `data` | object | Entity data dictionary (for Create/Update) | | `entityId` | string | Entity ID (for Update/Delete) | +| `commandName` | string | Command name (`"Create"`, `"Update"`, or `"Delete"`) | +| `entityName` | string | Full entity name | | `getValue(name)` | function | Get a property value | | `setValue(name, value)` | function | Set a property value (Pre-interceptors only) | +| `hasValue(name)` | function | Check if a property exists in the data | +| `removeValue(name)` | function | Remove a property from the data | ### `context.currentUser` -| Property | Type | Description | +| Property / Method | Type | Description | |----------|------|-------------| | `isAuthenticated` | bool | Whether user is logged in | +| `id` | string | User ID | | `userName` | string | Username | | `email` | string | Email address | +| `name` | string | First name | +| `surName` | string | Last name | +| `phoneNumber` | string | Phone number | +| `phoneNumberVerified` | bool | Whether phone is verified | +| `emailVerified` | bool | Whether email is verified | +| `tenantId` | string | Tenant ID (for multi-tenant apps) | | `roles` | string[] | User's role names | -| `id` | string | User ID | +| `isInRole(roleName)` | function | Check if user has a specific role | ### `context.emailSender` -| Method | Description | +| Property / Method | Description | |--------|-------------| -| `sendAsync(to, subject, body)` | Send an email | +| `isAvailable` | Whether the email sender is configured and available | +| `sendAsync(to, subject, body)` | Send a plain-text email | +| `sendHtmlAsync(to, subject, htmlBody)` | Send an HTML email | -### `context.log(message)` +### Logging + +| Method | Description | +|--------|-------------| +| `context.log(message)` | Log an informational message | +| `context.logWarning(message)` | Log a warning message | +| `context.logError(message)` | Log an error message | -Log a message (use instead of `console.log`). +> Use these methods instead of `console.log` (which is blocked in the sandbox). ### `db` (Database API) diff --git a/docs/en/low-code/model-json.md b/docs/en/low-code/model-json.md index 1e87c3c0dc..6a5b1ca024 100644 --- a/docs/en/low-code/model-json.md +++ b/docs/en/low-code/model-json.md @@ -206,13 +206,52 @@ Add validation rules to properties: } ``` -| Validator | Parameters | Description | -|-----------|------------|-------------| -| `required` | — | Value is required | -| `minLength` | `length` | Minimum string length | -| `maxLength` | `length` | Maximum string length | -| `emailAddress` | — | Must be a valid email | -| `range` | `min`, `max` | Numeric range | +Additional validator examples: + +```json +{ + "name": "Website", + "validators": [ + { "type": "url", "message": "Please enter a valid URL" } + ] +}, +{ + "name": "PhoneNumber", + "validators": [ + { "type": "phone" } + ] +}, +{ + "name": "ProductCode", + "validators": [ + { "type": "regularExpression", "pattern": "^[A-Z]{3}-\\d{4}$", "message": "Code must be in format ABC-1234" } + ] +}, +{ + "name": "Price", + "type": "decimal", + "validators": [ + { "type": "range", "minimum": 0.01, "maximum": 99999.99 } + ] +} +``` + +| Validator | Parameters | Applies To | Description | +|-----------|------------|------------|-------------| +| `required` | `allowEmptyStrings` (optional) | All types | Value is required | +| `minLength` | `length` | String | Minimum string length | +| `maxLength` | `length` | String | Maximum string length | +| `stringLength` | `minimumLength`, `maximumLength` | String | String length range (min and max together) | +| `emailAddress` | — | String | Must be a valid email | +| `phone` | — | String | Must be a valid phone number | +| `url` | — | String | Must be a valid URL | +| `creditCard` | — | String | Must be a valid credit card number | +| `regularExpression` | `pattern` | String | Must match the regex pattern | +| `range` | `minimum`, `maximum` | Numeric | Numeric range | +| `min` | `minimum` | Numeric | Minimum numeric value | +| `max` | `maximum` | Numeric | Maximum numeric value | + +> All validators accept an optional `message` parameter for a custom error message. The `regularExpression` validator also accepts the alias `pattern`, and `emailAddress` also accepts `email`. ## UI Configuration diff --git a/docs/en/low-code/reference-entities.md b/docs/en/low-code/reference-entities.md index ae9a062bcd..717189c207 100644 --- a/docs/en/low-code/reference-entities.md +++ b/docs/en/low-code/reference-entities.md @@ -118,8 +118,6 @@ if (user) { | Entity | Name for `entityName` | Typical Display Property | |--------|----------------------|--------------------------| | ABP Identity User | `Volo.Abp.Identity.IdentityUser` | `UserName` | -| ABP Identity Role | `Volo.Abp.Identity.IdentityRole` | `Name` | -| ABP Tenant | `Volo.Saas.Tenant` | `Name` | ## See Also diff --git a/docs/en/low-code/scripting-api.md b/docs/en/low-code/scripting-api.md index ba7b9ffde2..5b5b7de9ef 100644 --- a/docs/en/low-code/scripting-api.md +++ b/docs/en/low-code/scripting-api.md @@ -57,7 +57,6 @@ var result = await db.query('LowCodeDemo.Products.Product') | `thenByDescending(x => x.Property)` | Secondary sort descending | `QueryBuilder` | | `skip(n)` | Skip n records | `QueryBuilder` | | `take(n)` | Take n records | `QueryBuilder` | -| `reverse()` | Reverse sort order | `QueryBuilder` | | `toList()` | Execute and return array | `Promise` | | `count()` | Return count | `Promise` | | `any()` | Check if any matches exist | `Promise` | @@ -71,7 +70,6 @@ var result = await db.query('LowCodeDemo.Products.Product') | `select(x => projection)` | Project to custom shape | `QueryBuilder` | | `join(entity, alias, condition)` | Inner join | `QueryBuilder` | | `leftJoin(entity, alias, condition)` | Left join | `QueryBuilder` | -| `chunk(size)` | Split into chunks | `Promise` | ### Supported Operators in Lambda @@ -279,6 +277,7 @@ Direct CRUD methods on the `db` object: |--------|-------------|---------| | `db.get(entityName, id)` | Get by ID | `Promise` | | `db.getCount(entityName)` | Get count | `Promise` | +| `db.exists(entityName)` | Check if any records exist | `Promise` | | `db.insert(entityName, entity)` | Insert new | `Promise` | | `db.update(entityName, entity)` | Update existing | `Promise` | | `db.delete(entityName, id)` | Delete by ID | `Promise` | @@ -311,12 +310,16 @@ Available in [interceptors](interceptors.md): | Property | Type | Description | |----------|------|-------------| -| `context.commandArgs` | object | Command arguments (data, entityId) | +| `context.commandArgs` | object | Command arguments (data, entityId, commandName, entityName) | | `context.commandArgs.getValue(name)` | function | Get property value | | `context.commandArgs.setValue(name, value)` | function | Set property value | -| `context.currentUser` | object | Current user (isAuthenticated, userName, email, roles, id) | -| `context.emailSender` | object | Email sending (`sendAsync(to, subject, body)`) | -| `context.log(msg)` | function | Logging | +| `context.commandArgs.hasValue(name)` | function | Check if a property exists | +| `context.commandArgs.removeValue(name)` | function | Remove a property value | +| `context.currentUser` | object | Current user info (see [Interceptors](interceptors.md) for full list) | +| `context.emailSender` | object | Email sending (`sendAsync`, `sendHtmlAsync`) | +| `context.log(msg)` | function | Log an informational message | +| `context.logWarning(msg)` | function | Log a warning message | +| `context.logError(msg)` | function | Log an error message | ## Security