mirror of https://github.com/abpframework/abp.git
66 changed files with 1965 additions and 135 deletions
@ -0,0 +1,101 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Configure ABP IdentityModel clients for server-to-server access tokens, tenant-aware client selection, and request customization." |
|||
} |
|||
``` |
|||
|
|||
# IdentityModel Clients |
|||
|
|||
The `Volo.Abp.IdentityModel` package obtains access tokens for server-to-server HTTP calls. `AbpIdentityModelModule` binds the `IdentityClients` configuration section to `AbpIdentityClientOptions`. |
|||
|
|||
## Installation |
|||
|
|||
Install the `Volo.Abp.IdentityModel` NuGet package in the project that obtains the tokens: |
|||
|
|||
````shell |
|||
abp add-package Volo.Abp.IdentityModel |
|||
```` |
|||
|
|||
The command adds the package and the `AbpIdentityModelModule` dependency to the module class. |
|||
|
|||
## Configure Identity Clients |
|||
|
|||
Define a `Default` client and any named clients in the application configuration: |
|||
|
|||
````json |
|||
{ |
|||
"IdentityClients": { |
|||
"Default": { |
|||
"GrantType": "client_credentials", |
|||
"ClientId": "MyProject_Backend", |
|||
"ClientSecret": "your-client-secret", |
|||
"Authority": "https://localhost:44301/", |
|||
"Scope": "MyProject" |
|||
}, |
|||
"Reporting": { |
|||
"GrantType": "client_credentials", |
|||
"ClientId": "MyProject_Reporting", |
|||
"ClientSecret": "your-reporting-client-secret", |
|||
"Authority": "https://localhost:44301/", |
|||
"Scope": "Reporting" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
When a client name is requested for the current tenant, ABP selects the first available configuration in this order: |
|||
|
|||
1. `<client-name>.<tenant-id>` |
|||
2. `<client-name>.<tenant-name>` |
|||
3. `<client-name>` |
|||
4. `Default` |
|||
|
|||
If no client name is supplied, ABP uses `Default` as the client name. Tenant-specific entries let a tenant use different credentials without changing the consuming service. For example, `Reporting.8e6fcd0a-75ab-4d94-90f4-9a2503d0e70c` overrides the `Reporting` client for that tenant ID. |
|||
|
|||
Pass the client name to `TryAuthenticateAsync` when you authenticate an `HttpClient` directly: |
|||
|
|||
````csharp |
|||
var client = _httpClientFactory.CreateClient(); |
|||
|
|||
if (!await _authenticationService.TryAuthenticateAsync(client, "Reporting")) |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"The Reporting identity client is not configured." |
|||
); |
|||
} |
|||
|
|||
var response = await client.GetAsync("https://reporting.example.com/api/reports"); |
|||
```` |
|||
|
|||
Install `Volo.Abp.Http.Client.IdentityModel` when dynamic HTTP client proxies should obtain tokens automatically. Its `AbpHttpClientIdentityModelModule` integration uses the remote service's `IdentityClient` value when configured, then the remote service name, and finally the `Default` identity client fallback described above: |
|||
|
|||
````json |
|||
{ |
|||
"RemoteServices": { |
|||
"Reporting": { |
|||
"BaseUrl": "https://reporting.example.com/", |
|||
"IdentityClient": "Reporting" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Customize Discovery and Token Requests |
|||
|
|||
Use `IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage` to add headers or otherwise customize the request messages: |
|||
|
|||
````csharp |
|||
Configure<IdentityModelHttpRequestMessageOptions>(options => |
|||
{ |
|||
options.ConfigureHttpRequestMessage = request => |
|||
{ |
|||
request.Headers.TryAddWithoutValidation( |
|||
"X-Internal-Client", |
|||
"MyProject" |
|||
); |
|||
}; |
|||
}); |
|||
```` |
|||
|
|||
The callback runs for discovery, client-credentials, password and device-authorization request messages created by the default authentication service. |
|||
@ -0,0 +1,77 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Learn how to use ABP's in-memory database provider, register a MemoryDb context and repositories, and customize entity serialization." |
|||
} |
|||
``` |
|||
|
|||
# In-Memory Database Provider |
|||
|
|||
The `Volo.Abp.MemoryDb` package implements ABP repositories with an in-process database. It is useful for tests and other non-durable scenarios. Data is kept in the application process and is lost when the process stops. |
|||
|
|||
## Installation |
|||
|
|||
Install the `Volo.Abp.MemoryDb` NuGet package in the data-access project: |
|||
|
|||
````shell |
|||
abp add-package Volo.Abp.MemoryDb |
|||
```` |
|||
|
|||
The command adds the package and the `AbpMemoryDbModule` dependency to the module class. You can also configure the dependency manually as shown in the next section. |
|||
|
|||
## Configure the Module |
|||
|
|||
Add `AbpMemoryDbModule` as a dependency of your module: |
|||
|
|||
````csharp |
|||
[DependsOn(typeof(AbpMemoryDbModule))] |
|||
public class MyDataModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
## Create a MemoryDb Context |
|||
|
|||
Derive a class from `MemoryDbContext` and return the entity types managed by the context: |
|||
|
|||
````csharp |
|||
public class MyMemoryDbContext : MemoryDbContext |
|||
{ |
|||
private static readonly Type[] EntityTypes = |
|||
[ |
|||
typeof(Book), |
|||
typeof(Author) |
|||
]; |
|||
|
|||
public override IReadOnlyList<Type> GetEntityTypes() |
|||
{ |
|||
return EntityTypes; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Register the context in the `ConfigureServices` method of your module: |
|||
|
|||
````csharp |
|||
context.Services.AddMemoryDbContext<MyMemoryDbContext>(options => |
|||
{ |
|||
options.AddDefaultRepositories(); |
|||
}); |
|||
```` |
|||
|
|||
`AddDefaultRepositories()` registers default repositories for the aggregate roots returned by the context. Pass `includeAllEntities: true` when default repositories are also needed for other entity types. |
|||
|
|||
MemoryDb repositories use ABP's unit-of-work-aware database provider. Repository operations require an active [unit of work](../../architecture/domain-driven-design/unit-of-work.md). |
|||
|
|||
## JSON Serialization |
|||
|
|||
MemoryDb stores serialized entity values. Configure `Utf8JsonMemoryDbSerializerOptions` to customize the underlying `System.Text.Json` options: |
|||
|
|||
````csharp |
|||
Configure<Utf8JsonMemoryDbSerializerOptions>(options => |
|||
{ |
|||
options.JsonSerializerOptions.Converters.Add( |
|||
new MyEntityJsonConverter() |
|||
); |
|||
}); |
|||
```` |
|||
@ -0,0 +1,88 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Learn how to use ABP Commercial Angular date range controls, standalone UI configuration and the public testing entrypoint." |
|||
} |
|||
``` |
|||
|
|||
# Commercial UI Components |
|||
|
|||
The `@volo/abp.commercial.ng.ui` package provides shared ABP Commercial Angular controls in addition to the separately documented [lookup components](./lookup-components.md) and [entity filters](./entity-filters.md). The package is included in ABP Commercial Angular application templates. |
|||
|
|||
## Date Range Controls |
|||
|
|||
`DateRangePickerComponent` and `DatetimeRangePickerComponent` are Angular form controls. `startDateProp` and `endDateProp` specify the two properties updated in the bound model: |
|||
|
|||
```ts |
|||
import { Component } from '@angular/core'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { |
|||
DateRangePickerModule, |
|||
DatetimeRangePickerComponent, |
|||
} from '@volo/abp.commercial.ng.ui'; |
|||
|
|||
@Component({ |
|||
selector: 'app-report-range', |
|||
templateUrl: './report-range.component.html', |
|||
imports: [ |
|||
FormsModule, |
|||
DateRangePickerModule, |
|||
DatetimeRangePickerComponent, |
|||
], |
|||
}) |
|||
export class ReportRangeComponent { |
|||
dateRange: { |
|||
startDate: string | Date | null; |
|||
endDate: string | Date | null; |
|||
} = { |
|||
startDate: null, |
|||
endDate: null, |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
```html |
|||
<abp-date-range-picker |
|||
[(ngModel)]="dateRange" |
|||
startDateProp="startDate" |
|||
endDateProp="endDate" |
|||
labelText="Reports::DateRange" |
|||
/> |
|||
``` |
|||
|
|||
Use `abp-datetime-range-picker` with the same inputs when the model also needs start and end times. |
|||
|
|||
## Standalone Configuration |
|||
|
|||
Register commercial UI configuration in the application providers. The following example enables flag icons: |
|||
|
|||
```ts |
|||
import { ApplicationConfig } from '@angular/core'; |
|||
import { |
|||
provideCommercialUiConfig, |
|||
withEnableFlagIcon, |
|||
} from '@volo/abp.commercial.ng.ui/config'; |
|||
|
|||
export const appConfig: ApplicationConfig = { |
|||
providers: [ |
|||
provideCommercialUiConfig( |
|||
withEnableFlagIcon(true), |
|||
), |
|||
], |
|||
}; |
|||
``` |
|||
|
|||
Calling `provideCommercialUiConfig()` also registers the shared profile-picture, impersonation and tenant-switching providers. Flag icons are disabled unless `withEnableFlagIcon(true)` is supplied. |
|||
|
|||
## Testing |
|||
|
|||
`CommercialUiTestingModule` imports and exports `BaseCommercialUiModule`, making its declarations available from the public testing entrypoint. Its `withConfig()` method returns the module registration without adding test doubles or providers: |
|||
|
|||
```ts |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { CommercialUiTestingModule } from '@volo/abp.commercial.ng.ui/testing'; |
|||
|
|||
await TestBed.configureTestingModule({ |
|||
imports: [CommercialUiTestingModule.withConfig()], |
|||
}).compileComponents(); |
|||
``` |
|||
@ -1,41 +1,117 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Learn how to easily format dates in Angular using DateTime format pipes for shortDate, shortTime, and shortDateTime with culture settings." |
|||
"Description": "Format dates and handle clock-aware timezone conversion in ABP Angular applications with pipes, TimeService, and TimezoneService." |
|||
} |
|||
``` |
|||
|
|||
{%{ |
|||
# DateTime Format Pipes |
|||
# Date and Time |
|||
|
|||
You can format date by Date pipe of angular. |
|||
ABP Angular provides culture-aware format pipes, clock-aware UTC conversion, timezone selection, and date-time services. These APIs use the culture, clock, and timezone values from the application configuration. |
|||
|
|||
## Culture-Aware Format Pipes |
|||
|
|||
Angular's built-in `DatePipe` can format a date directly: |
|||
|
|||
Example |
|||
|
|||
```html |
|||
<span>{{today | date 'dd/mm/yy'}}</span> |
|||
<span>{{ today | date:'dd/MM/yy' }}</span> |
|||
``` |
|||
|
|||
ShortDate, ShortTime and ShortDateTime format data like angular's data pipe but easier. Also the pipes get format from config service by culture. |
|||
The ABP pipes below use the short date and time patterns returned in the application localization configuration. |
|||
|
|||
## ShortDate Pipe |
|||
### `shortDate` |
|||
|
|||
```html |
|||
<span> {{today | shortDate }}</span> |
|||
<span>{{ today | shortDate }}</span> |
|||
``` |
|||
|
|||
### `shortTime` |
|||
|
|||
## ShortTime Pipe |
|||
```html |
|||
<span>{{ today | shortTime }}</span> |
|||
``` |
|||
|
|||
### `shortDateTime` |
|||
|
|||
```html |
|||
<span> {{today | shortTime }}</span> |
|||
<span>{{ today | shortDateTime }}</span> |
|||
``` |
|||
|
|||
These pipes extend Angular's `DatePipe`. They select the format pattern from `ConfigStateService`; they do not apply ABP's clock-aware timezone selection. Use `abpUtcToLocal` when the value also needs to follow the application's clock and timezone. |
|||
|
|||
## Clock-Aware UTC Conversion |
|||
|
|||
## ShortDateTime Pipe |
|||
The `abpUtcToLocal` pipe accepts `date`, `time`, or `datetime` as its format type: |
|||
|
|||
```html |
|||
<span> {{today | shortDateTime }}</span> |
|||
<span>{{ order.creationTime | abpUtcToLocal:'datetime' }}</span> |
|||
``` |
|||
|
|||
Its behavior depends on the clock configuration returned by the backend: |
|||
|
|||
- With the UTC clock enabled, it converts the input to `TimezoneService.timezone`, including the daylight-saving-time offset for that date. |
|||
- With a non-UTC clock, it formats the value without applying the configured timezone conversion. |
|||
- Empty or invalid input produces an empty string. |
|||
|
|||
The output pattern is still taken from the current application's short date and time formats. |
|||
|
|||
## `TimezoneService` |
|||
|
|||
Inject `TimezoneService` to read or persist the timezone used by the Angular application: |
|||
|
|||
```ts |
|||
import { TimezoneService } from '@abp/ng.core'; |
|||
import { Component, inject } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'app-timezone-selector', |
|||
template: `<button type="button" (click)="selectTimezone()">Use Istanbul time</button>`, |
|||
}) |
|||
export class TimezoneSelectorComponent { |
|||
private readonly timezoneService = inject(TimezoneService); |
|||
|
|||
selectTimezone(): void { |
|||
this.timezoneService.setTimezone('Europe/Istanbul'); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`timezone` returns: |
|||
|
|||
- the browser timezone when the backend clock is not UTC; |
|||
- the `Abp.Timing.TimeZone` setting when the clock is UTC and the setting has a value; |
|||
- the browser timezone as a fallback when the UTC setting is empty. |
|||
|
|||
`setTimezone` writes the selected IANA timezone to the `__timezone` cookie only when the UTC clock is enabled. |
|||
|
|||
When you configure the application with `provideAbpCore`, its built-in `timezoneInterceptor` adds the effective timezone to outgoing `HttpClient` requests as the `__timezone` header. It does not add the header when the UTC clock is disabled. |
|||
|
|||
## `TimeService` |
|||
|
|||
`TimeService` returns [Luxon](https://moment.github.io/luxon/#/) `DateTime` values and formats dates with the current Angular locale: |
|||
|
|||
```ts |
|||
import { TimeService } from '@abp/ng.core'; |
|||
import { inject, Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class ScheduleFormatter { |
|||
private readonly timeService = inject(TimeService); |
|||
|
|||
formatForIstanbul(value: string): string { |
|||
return this.timeService.format(value, 'ff', 'Europe/Istanbul'); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
| Method | Behavior | |
|||
| --- | --- | |
|||
| `now(zone = 'local')` | Returns the current time in the requested IANA timezone. | |
|||
| `toZone(value, zone)` | Parses an ISO string or `Date` and returns a Luxon value in the requested timezone. | |
|||
| `format(value, format = 'ff', zone = 'local')` | Converts to the timezone, applies its DST rules, and formats with the current locale. | |
|||
| `formatDateWithStandardOffset(value, format = 'ff', zone?)` | Applies the zone's January 1 offset and formats without any further timezone or DST conversion. | |
|||
| `formatWithoutTimeZone(value, format = 'ff')` | Formats the parsed ISO clock fields without shifting them to another timezone. | |
|||
|
|||
}%} |
|||
|
|||
@ -0,0 +1,94 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Learn how to use the generic ABP Angular lookup search component with remote searches, two-way values and custom templates." |
|||
} |
|||
``` |
|||
|
|||
# Lookup Search Component |
|||
|
|||
`LookupSearchComponent` is a generic standalone search control exported by `@abp/ng.components/lookup`. A search function returns observable lookup items, and the component manages debouncing, loading, selection and clearing. |
|||
|
|||
This component is part of the open-source `@abp/ng.components` package. It is different from the [commercial lookup component family](./lookup-components.md), which provides form-oriented typeahead, select and table controls. |
|||
|
|||
The package is included in the Angular application templates. Install it if the application does not already reference it: |
|||
|
|||
```bash |
|||
npm install @abp/ng.components |
|||
``` |
|||
|
|||
```ts |
|||
import { Component, inject, signal } from '@angular/core'; |
|||
import { |
|||
LookupItem, |
|||
LookupSearchComponent, |
|||
LookupSearchFn, |
|||
} from '@abp/ng.components/lookup'; |
|||
import { map } from 'rxjs'; |
|||
|
|||
interface BookLookupItem extends LookupItem { |
|||
authorName: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'app-book-lookup', |
|||
templateUrl: './book-lookup.component.html', |
|||
imports: [LookupSearchComponent], |
|||
}) |
|||
export class BookLookupComponent { |
|||
private readonly bookService = inject(BookService); |
|||
|
|||
readonly selectedBookId = signal(''); |
|||
readonly selectedBookName = signal(''); |
|||
|
|||
readonly searchBooks: LookupSearchFn<BookLookupItem> = filter => |
|||
this.bookService.search(filter).pipe( |
|||
map(books => |
|||
books.map(book => ({ |
|||
key: book.id, |
|||
displayName: book.name, |
|||
authorName: book.authorName, |
|||
})), |
|||
), |
|||
); |
|||
|
|||
onBookSelected(book: BookLookupItem) { |
|||
console.log(book.key); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
{%{ |
|||
```html |
|||
<abp-lookup-search |
|||
label="Books::Book" |
|||
placeholder="Books::SearchBooks" |
|||
[searchFn]="searchBooks" |
|||
[minSearchLength]="2" |
|||
[selectedValue]="selectedBookId()" |
|||
(selectedValueChange)="selectedBookId.set($event)" |
|||
[displayValue]="selectedBookName()" |
|||
(displayValueChange)="selectedBookName.set($event)" |
|||
(itemSelected)="onBookSelected($event)" |
|||
> |
|||
<ng-template #itemTemplate let-book> |
|||
<strong>{{ book.displayName }}</strong> |
|||
<small>{{ book.authorName }}</small> |
|||
</ng-template> |
|||
</abp-lookup-search> |
|||
``` |
|||
}%} |
|||
|
|||
Each lookup item uses `key` as its selected value and `displayName` as its displayed value by default. Set `valueKey` or `displayKey` to use another item property. |
|||
|
|||
Searches use a 300 millisecond debounce by default. Configure `debounceTime` and `minSearchLength` when the remote endpoint needs different behavior. `label` and `placeholder` values are passed through ABP localization. |
|||
|
|||
`selectedValue` and `displayValue` are model inputs and support two-way binding. The component also emits `searchChanged` for input changes and `itemSelected` after selection. |
|||
|
|||
Add an `#itemTemplate` template to customize each result, as in the previous example. Add a `#noResultsTemplate` template to replace the default no-results content: |
|||
|
|||
```html |
|||
<ng-template #noResultsTemplate> |
|||
<div class="list-group-item text-muted">No matching books</div> |
|||
</ng-template> |
|||
``` |
|||
@ -0,0 +1,96 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Configure localized Angular route titles, the application-name suffix, and a custom TitleStrategy in an ABP application." |
|||
} |
|||
``` |
|||
|
|||
# Document Title Strategy |
|||
|
|||
`provideAbpCore` registers `AbpTitleStrategy` as Angular's default `TitleStrategy`. It reads the deepest active route title, localizes it, and updates the browser document title. |
|||
|
|||
## Setting a Route Title |
|||
|
|||
Use Angular's `title` route property. The value can be an ABP localization key: |
|||
|
|||
```ts |
|||
import { Routes } from '@angular/router'; |
|||
import { BooksComponent } from './books.component'; |
|||
|
|||
export const routes: Routes = [ |
|||
{ |
|||
path: 'books', |
|||
component: BooksComponent, |
|||
title: 'BookStore::Menu:Books', |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
With an application name of `Book Store`, the resulting title is: |
|||
|
|||
```text |
|||
Books | Book Store |
|||
``` |
|||
|
|||
The strategy resolves the application name from the `::AppName` localization key. If a route has no title, it uses only the application name. It also recalculates the active title when the language changes. |
|||
|
|||
## Removing the Application-Name Suffix |
|||
|
|||
Provide `DISABLE_PROJECT_NAME` with `true` to omit the application name from routes that have a title: |
|||
|
|||
```ts |
|||
import { DISABLE_PROJECT_NAME } from '@abp/ng.core'; |
|||
import { ApplicationConfig } from '@angular/core'; |
|||
|
|||
export const appConfig: ApplicationConfig = { |
|||
providers: [ |
|||
{ |
|||
provide: DISABLE_PROJECT_NAME, |
|||
useValue: true, |
|||
}, |
|||
], |
|||
}; |
|||
``` |
|||
|
|||
The route above then produces `Books`. A route without a title still falls back to the application name. |
|||
|
|||
## Replacing the Strategy |
|||
|
|||
Create an Angular `TitleStrategy` and pass it to the `withTitleStrategy` feature when the application needs a different title convention: |
|||
|
|||
```ts |
|||
import { Title } from '@angular/platform-browser'; |
|||
import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; |
|||
import { inject, Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class CustomTitleStrategy extends TitleStrategy { |
|||
private readonly title = inject(Title); |
|||
|
|||
override updateTitle(routerState: RouterStateSnapshot): void { |
|||
const routeTitle = this.buildTitle(routerState); |
|||
this.title.setTitle(routeTitle ? `My Application - ${routeTitle}` : 'My Application'); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Register it together with the normal ABP Core options: |
|||
|
|||
```ts |
|||
import { provideAbpCore, withOptions, withTitleStrategy } from '@abp/ng.core'; |
|||
import { registerLocaleForEsBuild } from '@abp/ng.core/locale'; |
|||
import { ApplicationConfig } from '@angular/core'; |
|||
import { environment } from '../environments/environment'; |
|||
|
|||
export const appConfig: ApplicationConfig = { |
|||
providers: [ |
|||
provideAbpCore( |
|||
withOptions({ |
|||
environment, |
|||
registerLocaleFn: registerLocaleForEsBuild(), |
|||
}), |
|||
withTitleStrategy(CustomTitleStrategy), |
|||
), |
|||
], |
|||
}; |
|||
``` |
|||
@ -0,0 +1,138 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Learn how to use the ABP Angular tree component for hierarchical data, selection, templates and drag-and-drop." |
|||
} |
|||
``` |
|||
|
|||
# Tree Component |
|||
|
|||
`TreeComponent` is a standalone tree control exported by `@abp/ng.components/tree`. It supports selection, checkboxes, expansion, context-menu templates and drag-and-drop. |
|||
|
|||
The `@abp/ng.components` package is included in the Angular application templates. Install it if the application does not already reference it: |
|||
|
|||
```bash |
|||
npm install @abp/ng.components |
|||
``` |
|||
|
|||
Import it into a standalone component and provide nodes in the format expected by the underlying NG-ZORRO tree: |
|||
|
|||
```ts |
|||
import { Component, signal } from '@angular/core'; |
|||
import { |
|||
DropEvent, |
|||
ExpandedIconTemplateDirective, |
|||
TreeComponent, |
|||
TreeNodeTemplateDirective, |
|||
} from '@abp/ng.components/tree'; |
|||
import { of } from 'rxjs'; |
|||
|
|||
interface Category { |
|||
id: string; |
|||
name: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'app-category-tree', |
|||
templateUrl: './category-tree.component.html', |
|||
imports: [ |
|||
TreeComponent, |
|||
TreeNodeTemplateDirective, |
|||
ExpandedIconTemplateDirective, |
|||
], |
|||
}) |
|||
export class CategoryTreeComponent { |
|||
readonly nodes = signal([ |
|||
{ |
|||
key: 'books', |
|||
title: 'Books', |
|||
entity: { id: 'books', name: 'Books' } as Category, |
|||
children: [], |
|||
isLeaf: true, |
|||
}, |
|||
]); |
|||
|
|||
readonly expandedKeys = signal<string[]>([]); |
|||
readonly selectedCategory = signal<Category | null>(null); |
|||
|
|||
readonly allowDrop = () => of(true); |
|||
|
|||
handleDrop(event: DropEvent) { |
|||
console.log(event.dragNode?.key); |
|||
} |
|||
|
|||
edit(key: string) { |
|||
console.log(key); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```html |
|||
<abp-tree |
|||
[nodes]="nodes()" |
|||
[expandedKeys]="expandedKeys()" |
|||
[selectedNode]="selectedCategory()" |
|||
[draggable]="true" |
|||
[beforeDrop]="allowDrop" |
|||
(expandedKeysChange)="expandedKeys.set($event)" |
|||
(selectedNodeChange)="selectedCategory.set($event)" |
|||
(dropOver)="handleDrop($event)" |
|||
> |
|||
<ng-template #menu let-node> |
|||
<button type="button" class="dropdown-item" (click)="edit(node.key)"> |
|||
Edit |
|||
</button> |
|||
</ng-template> |
|||
</abp-tree> |
|||
``` |
|||
|
|||
The main inputs are: |
|||
|
|||
- `nodes`, `checkedKeys`, `expandedKeys` and `selectedNode` for tree state. |
|||
- `draggable`, `checkable`, `checkStrictly` and `noAnimation` for tree behavior. |
|||
- `changeCheckboxWithNode` to update checked keys when a node is selected. |
|||
- `isNodeSelected` to replace the default selected-node comparison. |
|||
- `beforeDrop` to approve or reject a drag-and-drop operation. |
|||
|
|||
State changes are exposed through `checkedKeysChange`, `expandedKeysChange`, `selectedNodeChange`, `dropOver` and `nzExpandChange`. |
|||
|
|||
The default `beforeDrop` handler rejects drops. Supply a handler that returns an observable accepted by the underlying tree control when drag-and-drop is enabled. |
|||
|
|||
## Templates |
|||
|
|||
Use the `#menu` template, as in the previous example, to add a context menu for each node. Use `abpTreeNodeTemplate` and `abpTreeExpandedIconTemplate` to replace the node and expanded-icon templates: |
|||
|
|||
{%{ |
|||
```html |
|||
<abp-tree [nodes]="nodes()"> |
|||
<ng-template abpTreeNodeTemplate let-node> |
|||
<strong>{{ node.title }}</strong> |
|||
</ng-template> |
|||
|
|||
<ng-template abpTreeExpandedIconTemplate let-node> |
|||
<span>{{ node.isExpanded ? '−' : '+' }}</span> |
|||
</ng-template> |
|||
</abp-tree> |
|||
``` |
|||
}%} |
|||
|
|||
The component example imports `TreeNodeTemplateDirective` and `ExpandedIconTemplateDirective` because Angular must see each directive used by a standalone component template. If you use only one of these templates, import only its corresponding directive. |
|||
|
|||
## Flat-List Adapter |
|||
|
|||
`TreeAdapter<T>` converts a flat list of `BaseNode` values into tree nodes. Each item requires an `id` and a nullable `parentId`; its `displayName` or `name` becomes the default node title. Use `getTree()`, `handleDrop()`, `handleRemove()` and `handleUpdate()` to keep the flat list and tree representations synchronized. |
|||
|
|||
## Style Loading |
|||
|
|||
The component loads `ng-zorro-antd-tree.css` when it is initialized. Provide `DISABLE_TREE_STYLE_LOADING_TOKEN` with `true` only when the application already includes that stylesheet: |
|||
|
|||
```ts |
|||
import { DISABLE_TREE_STYLE_LOADING_TOKEN } from '@abp/ng.components/tree'; |
|||
|
|||
export const providers = [ |
|||
{ |
|||
provide: DISABLE_TREE_STYLE_LOADING_TOKEN, |
|||
useValue: true, |
|||
}, |
|||
]; |
|||
``` |
|||
@ -0,0 +1,57 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Use ABP's MVC JavaScript Clock API for time-zone detection, date normalization, localized display, and the browser time-zone cookie." |
|||
} |
|||
``` |
|||
|
|||
# ASP.NET Core MVC / Razor Pages UI: JavaScript Clock API |
|||
|
|||
The `abp.clock` namespace provides date, time-zone and browser-time-zone helpers for MVC / Razor Pages applications. The application configuration script sets `abp.clock.kind` from the server-side ABP clock configuration. |
|||
|
|||
## Time-Zone Support |
|||
|
|||
`abp.clock.supportsMultipleTimezone()` returns `true` when the configured clock kind is `Utc`. `abp.clock.timeZone()` returns the value of the `Abp.Timing.TimeZone` setting when it is available and otherwise returns the browser's IANA time-zone name. |
|||
|
|||
````js |
|||
if (abp.clock.supportsMultipleTimezone()) { |
|||
console.log(abp.clock.timeZone()); |
|||
} |
|||
```` |
|||
|
|||
## Normalize Date Values |
|||
|
|||
Use `normalizeToString` before sending a date value to the server and `normalizeToLocaleString` before displaying a server value: |
|||
|
|||
````js |
|||
const requestValue = abp.clock.normalizeToString(new Date()); |
|||
const displayValue = abp.clock.normalizeToLocaleString(requestValue); |
|||
```` |
|||
|
|||
`normalizeToString` returns an ISO-compatible date-time string. For a non-UTC clock, the core implementation uses `yyyy-MM-ddTHH:mm:ss`. Both normalization methods return empty or invalid values unchanged. |
|||
|
|||
The standard shared MVC theme bundle loads Luxon and replaces the core implementations of `normalizeToString` and `normalizeToLocaleString`. When multiple time zones are supported, this Luxon implementation interprets the input in the configured IANA time zone, converts it to UTC and returns an ISO value ending in `Z`. The output can include milliseconds (for example, `2026-07-17T08:30:00.000Z`), so do not require an exact string length when consuming it. |
|||
|
|||
If an application uses the core scripts without the shared theme's Luxon contributor, the fallback implementation produces a `Z`-suffixed transport value by detecting a numeric browser offset. It is not a full IANA time-zone conversion and does not account for fractional-hour offsets or an offset change between the current date and the input date. Include the Luxon contributor when those cases must be handled. |
|||
|
|||
`normalizeToLocaleString` accepts standard `Intl.DateTimeFormat` options. When no options are supplied, it uses `abp.clock.toLocaleStringOptions`. The default options include the numeric year, long month, numeric day, hour, minute and second. You can replace them to define application-wide display defaults: |
|||
|
|||
````js |
|||
abp.clock.toLocaleStringOptions = { |
|||
year: 'numeric', |
|||
month: '2-digit', |
|||
day: '2-digit', |
|||
hour: '2-digit', |
|||
minute: '2-digit' |
|||
}; |
|||
```` |
|||
|
|||
## Browser Time-Zone Cookie |
|||
|
|||
After application configuration is initialized, ABP writes the browser's time-zone name to the `__timezone` cookie when multiple time zones are supported. Set the following flag before configuration initialization to disable this behavior: |
|||
|
|||
````js |
|||
abp.clock.trySetBrowserTimeZoneToCookie = false; |
|||
```` |
|||
|
|||
Use `abp.clock.browserTimeZone()` to read the browser time zone or `abp.clock.setBrowserTimeZoneToCookie()` to refresh the cookie explicitly. |
|||
@ -1,6 +1,99 @@ |
|||
# Multi Lingual Entities |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Use ABP multi-lingual objects to select translations by culture, apply fallback rules, and process translations in bulk." |
|||
} |
|||
``` |
|||
|
|||
This feature is still under development. |
|||
Follow the below link to get information about the development status |
|||
# Multi-Lingual Objects |
|||
|
|||
https://github.com/abpframework/abp/issues/11698 |
|||
The `Volo.Abp.MultiLingualObject` package provides a contract and a selection service for objects that store one translation per language. Persistence mapping is application-specific; the package does not create a database relationship for the translations. |
|||
|
|||
## Installation |
|||
|
|||
Install the package in the project that defines the consuming module: |
|||
|
|||
```bash |
|||
abp add-package Volo.Abp.MultiLingualObject |
|||
``` |
|||
|
|||
Add `AbpMultiLingualObjectsModule` as a dependency of that module when the package is installed manually: |
|||
|
|||
````csharp |
|||
[DependsOn(typeof(AbpMultiLingualObjectsModule))] |
|||
public class MyApplicationModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
## Define a Multi-Lingual Object |
|||
|
|||
Implement `IMultiLingualObject<TTranslation>` on the object and `IObjectTranslation` on its translation type: |
|||
|
|||
```csharp |
|||
using Volo.Abp.MultiLingualObjects; |
|||
|
|||
public class Product : IMultiLingualObject<ProductTranslation> |
|||
{ |
|||
public ICollection<ProductTranslation> Translations { get; set; } = |
|||
new List<ProductTranslation>(); |
|||
} |
|||
|
|||
public class ProductTranslation : IObjectTranslation |
|||
{ |
|||
public string Language { get; set; } = string.Empty; |
|||
|
|||
public string Name { get; set; } = string.Empty; |
|||
} |
|||
``` |
|||
|
|||
`Language` stores a culture name such as `en`, `en-US` or `tr`. |
|||
|
|||
## Select a Translation |
|||
|
|||
Inject `IMultiLingualObjectManager` and call `GetTranslationAsync`: |
|||
|
|||
```csharp |
|||
public class ProductService |
|||
: ITransientDependency |
|||
{ |
|||
private readonly IMultiLingualObjectManager _multiLingualObjectManager; |
|||
|
|||
public ProductService( |
|||
IMultiLingualObjectManager multiLingualObjectManager) |
|||
{ |
|||
_multiLingualObjectManager = multiLingualObjectManager; |
|||
} |
|||
|
|||
public Task<ProductTranslation?> GetTranslationAsync(Product product) |
|||
{ |
|||
return _multiLingualObjectManager |
|||
.GetTranslationAsync<Product, ProductTranslation>(product); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
With the default arguments, the manager uses `CultureInfo.CurrentUICulture.Name`. It selects translations in this order: |
|||
|
|||
1. An exact translation for the current UI culture. |
|||
2. A translation for a parent of the current UI culture when `fallbackToParentCultures` is `true`. |
|||
3. A translation for the language configured by `LocalizationSettingNames.DefaultLanguage`. |
|||
4. The first available translation. |
|||
|
|||
The method returns `null` when the collection is null or empty. To disable only the parent-culture fallback, pass `culture` and set `fallbackToParentCultures` to `false`. The default-language and first-available fallbacks still apply. |
|||
|
|||
## Select Translations in Bulk |
|||
|
|||
Use `GetBulkTranslationsAsync` to select translations for multiple objects with the same culture and fallback settings: |
|||
|
|||
```csharp |
|||
var results = await _multiLingualObjectManager |
|||
.GetBulkTranslationsAsync<Product, ProductTranslation>(products); |
|||
|
|||
foreach (var (product, translation) in results) |
|||
{ |
|||
// Use product and its selected translation. |
|||
} |
|||
``` |
|||
|
|||
Each result keeps the source object together with its selected translation. An object with no translations gets a `null` translation. |
|||
|
|||
Loading…
Reference in new issue