Merge pull request #6947 from abpframework/auto-merge/rel-4-1/28

Merge branch dev with rel-4.1
pull/6978/head
Halil İbrahim Kalkan 4 years ago committed by GitHub
commit 5756a36522
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -43,17 +43,59 @@ In any case, you can create a **separate solution** for the desired module and d
#### Publishing the Customized Module as Packages
One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company.
One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company, for example.
## Module Customization / Extending Approaches
This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways:
This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways.
### Module Entity Extension System
> Module entity extension system is the **main and high level extension system** that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point.
See the [Module Entity Extensions document](Module-Entity-Extensions.md) to learn how to use it.
### Extending Entities
If you only need to get/set extra data on an existing entity, follow the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document.
### Overriding Services/Components
In addition to the extensibility systems, you can partially or completely override any service or user interface page/component.
* [Extending Entities](Customizing-Application-Modules-Extending-Entities.md)
* [Overriding Services](Customizing-Application-Modules-Overriding-Services.md)
* [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md)
### See Also
### Additional UI Extensibility Points
There are some low level systems that you can control entity actions, table columns and page toolbar of a page defined by a module.
#### Entity Actions
Entity action extension system allows you to add a new action to the action menu for an entity on the user interface;
* [Entity Action Extensions for ASP.NET Core UI](UI/AspNetCore/Entity-Action-Extensions.md)
* [Entity Action Extensions for Angular](UI/Angular/Entity-Action-Extensions.md)
#### Data Table Column Extensions
Data table column extension system allows you to add a new column in the data table on the user interface;
* [Data Table Column Extensions for ASP.NET Core UI](UI/AspNetCore/Data-Table-Column-Extensions.md)
* [Data Table Column Extensions for Angular](UI/Angular/Data-Table-Column-Extensions.md)
#### Page Toolbar
Page toolbar system allows you to add components to the toolbar of a page;
* [Page Toolbar Extensions for ASP.NET Core UI](UI/AspNetCore/Page-Toolbar-Extensions.md)
* [Page Toolbar Extensions for Angular](UI/Angular/Page-Toolbar-Extensions.md)
#### Others
* [Dynamic Form Extensions for Angular](UI/Angular/Dynamic-Form-Extensions.md)
## See Also
Also, see the following documents:

@ -0,0 +1,327 @@
# Data Table Column (or Entity Prop) Extensions for Angular UI
## Introduction
Entity prop extension system allows you to add a new column to the data table for an entity or change/remove an already existing one. A "Name" column was added to the user management page below:
![Entity Prop Extension Example: "Name" Column](images/user-prop-extension-name-column-ng.png)
You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells.
## How to Set Up
In this example, we will add a "Name" column and display the value of the `name` field in the user management page of the [Identity Module](../../Modules/Identity.md).
### Step 1. Create Entity Prop Contributors
The following code prepares a constant named `identityEntityPropContributors`, ready to be imported and used in your root module:
```js
// entity-prop-contributors.ts
import { EntityProp, EntityPropList, ePropType } from '@abp/ng.theme.shared/extensions';
import { IdentityEntityPropContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const nameProp = new EntityProp<IdentityUserDto>({
type: ePropType.String,
name: 'name',
displayName: 'AbpIdentity::Name',
sortable: true,
columnWidth: 250,
});
export function namePropContributor(propList: EntityPropList<IdentityUserDto>) {
propList.addAfter(
nameProp,
'userName',
(value, name) => value.name === name,
);
}
export const identityEntityPropContributors: IdentityEntityPropContributors = {
'Identity.UsersComponent': [namePropContributor],
};
```
The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addAfter` method, which adds a node with given value after the first node that has the previous value. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `namePropContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityEntityPropContributors: IdentityEntityPropContributors = {
'Identity.UsersComponent': [ namePropContributor ],
};
/* OR */
const identityContributors: IdentityEntityPropContributors = {};
identityContributors[eIdentityComponents.Users] = [ namePropContributor ];
export const identityEntityPropContributors = identityContributors;
```
### Step 2. Import and Use Entity Prop Contributors
Import `identityEntityPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import { identityEntityPropContributors } from './entity-prop-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
entityPropContributors: identityEntityPropContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `nameProp` entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (`UsersComponent`) of the `IdentityModule`.
## How to Render Custom HTML in Cells
You can use the `valueResolver` to render an HTML string in the table. Imagine we want to show a red times icon (❌) next to unconfirmed emails and phones, instead of showing a green check icon next to confirmed emails and phones. The contributors below would do that for you.
```js
// entity-prop-contributors.ts
import { EntityProp, EntityPropList, ePropType } from '@abp/ng.theme.shared/extensions';
import { IdentityUserDto } from '@volo/abp.ng.identity';
import { IdentityEntityPropContributors } from '@volo/abp.ng.identity/config';
export function emailPropContributor(propList: EntityPropList<IdentityUserDto>) {
const index = propList.indexOf('email', (value, name) => value.name === name);
const droppedNode = propList.dropByIndex(index);
const emailProp = new EntityProp<IdentityUserDto>({
...droppedNode.value,
valueResolver: data => {
const { email, emailConfirmed } = data.record;
const icon = email && !emailConfirmed ? `<i class="fa fa-times text-danger ml-1"></i>` : '';
return of((email || '') + icon); // should return an observable
},
});
propList.addByIndex(emailProp, index);
}
export function phonePropContributor(propList: EntityPropList<IdentityUserDto>) {
const index = propList.indexOf('phoneNumber', (value, name) => value.name === name);
const droppedNode = propList.dropByIndex(index);
const phoneProp = new EntityProp<IdentityUserDto>({
...droppedNode.value,
valueResolver: data => {
const { phoneNumber, phoneNumberConfirmed } = data.record;
const icon =
phoneNumber && !phoneNumberConfirmed ? `<i class="fa fa-times text-danger ml-1"></i>` : '';
return of((phoneNumber || '') + icon); // should return an observable
},
});
propList.addByIndex(phoneProp, index);
}
export const identityEntityPropContributors: IdentityEntityPropContributors = {
'Identity.UsersComponent': [emailPropContributor, phonePropContributor],
};
```
> The `valueResolver` method should return an observable. You can wrap your return values with `of` from RxJS for that.
## Object Extensions
Extra properties defined on an existing entity will be included in the table based on their configuration. The values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior.
## API
### PropData\<R = any\>
`PropData` is the shape of the parameter passed to all callbacks or predicates in an `EntityProp`.
It has the following properties:
- **record** is the row data, i.e. current value rendered in the table.
```js
{
type: ePropType.String,
name: 'name',
valueResolver: data => {
const name = data.record.name || '';
return of(name.toUpperCase());
},
}
```
- **index** is the table index where the record is at.
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleTableComponent`, including, but not limited to, its parent component.
```js
{
type: ePropType.String,
name: 'name',
valueResolver: data => {
const restService = data.getInjected(RestService);
const usersComponent = data.getInjected(UsersComponent);
// Use restService and usersComponent public props and methods here
},
}
```
### PropCallback\<T, R = any\>
`PropCallback` is the type of the callback function that can be passed to an `EntityProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type PropCallback<T, R = any> = (data?: PropData<T>) => R;
```
### PropPredicate\<T\>
`PropPredicate` is the type of the predicate function that can be passed to an `EntityProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation:
```js
type PropPredicate<T> = (data?: PropData<T>) => boolean;
```
### EntityPropOptions\<R = any\>
`EntityPropOptions` is the type that defines required and optional properties you have to pass in order to create an entity prop.
Its type definition is as follows:
```js
type EntityPropOptions<R = any> = {
type: ePropType;
name: string;
displayName?: string;
valueResolver?: PropCallback<R, Observable<any>>;
sortable?: boolean;
columnWidth?: number;
permission?: string;
visible?: PropPredicate<R>;
};
```
As you see, passing `type` and `name` is enough to create an entity prop. Here is what each property is good for:
- **type** is the type of the prop value. It is used for custom rendering in the table. (_required_)
- **name** is the property name (or key) which will be used to read the value of the prop. (_required_)
- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`)
- **valueResolver** is a callback that is called when the cell is rendered. It must return an observable. (_default:_ `data => of(data.record[options.name])`)
- **sortable** defines if the table is sortable based on this entity prop. Sort icons are shown based on it. (_default:_ `false`)
- **columnWidth** defines a minimum width for the column. Good for horizontal scroll. (_default:_ `undefined`)
- **permission** is the permission context which will be used to decide if a column for this entity prop should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if this entity prop should be displayed on the table or not. (_default:_ `() => true`)
> Important Note: Do not use record in visibility predicates. First of all, the table header checks it too and the record will be `undefined`. Second, if some cells are displayed and others are not, the table will be broken. Use the `valueResolver` and render an empty cell when you need to hide a specific cell.
You may find a full example below.
### EntityProp\<R = any\>
`EntityProp` is the class that defines your entity props. It takes an `EntityPropOptions` and sets the default values to the properties, creating an entity prop that can be passed to an entity contributor.
```js
const options: EntityPropOptions<IdentityUserDto> = {
type: ePropType.String,
name: 'email',
displayName: 'AbpIdentity::EmailAddress',
valueResolver: data => {
const { email, emailConfirmed } = data.record;
return of(
(email || '') + (emailConfirmed ? `<i class="fa fa-check text-success ml-1"></i>` : ''),
);
},
sortable: true,
columnWidth: 250,
permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical
visible: data => {
const store = data.getInjected(Store);
const selectSensitiveDataVisibility = ConfigState.getSetting(
'Abp.Identity.IsSensitiveDataVisible' // hypothetical
);
return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true';
}
};
const prop = new EntityProp(options);
```
It also has two static methods to create its instances:
- **EntityProp.create\<R = any\>\(options: EntityPropOptions\<R\>\)** is used to create an instance of `EntityProp`.
```js
const prop = EntityProp.create(options);
```
- **EntityProp.createMany\<R = any\>\(options: EntityPropOptions\<R\>\[\]\)** is used to create multiple instances of `EntityProp` with given array of `EntityPropOptions`.
```js
const props = EntityProp.createMany(optionsArray);
```
### EntityPropList\<R = any\>
`EntityPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
propList: EntityPropList<IdentityUserDto>,
) {
// drop email node
const emailPropNode = propList.dropByValue(
'AbpIdentity::EmailAddress',
(prop, text) => prop.text === text,
);
// add it back after phoneNumber
propList.addAfter(
emailPropNode.value,
'phoneNumber',
(value, name) => value.name === name,
);
}
```
### EntityPropContributorCallback\<R = any\>
`EntityPropContributorCallback` is the type that you can pass as entity prop contributor callbacks to static `forLazy` methods of the modules.
```js
export function isLockedOutPropContributor(
propList: EntityPropList<IdentityUserDto>,
) {
// add isLockedOutProp as 2nd column
propList.add(isLockedOutProp).byIndex(1);
}
export const identityEntityPropContributors = {
[eIdentityComponents.Users]: [isLockedOutPropContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

@ -0,0 +1,325 @@
# Dynamic Form (or Form Prop) Extensions for Angular UI
## Introduction
Form prop extension system allows you to add a new field to the create and/or edit forms for a form or change/remove an already existing one. A "Date of Birth" field was added to the user management page below:
![Form Prop Extension Example: "Date of Birth" Field](images/user-prop-extension-date-of-birth-field-ng.png)
You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contibutor for an edit form.
## How to Set Up
In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../Modules/Identity.md) and validate it.
### Step 1. Create Form Prop Contributors
The following code prepares two constants named `identityCreateFormPropContributors` and `identityEditFormPropContributors`, ready to be imported and used in your root module:
```js
// form-prop-contributors.ts
import { Validators } from '@angular/forms';
import { ePropType, FormProp, FormPropList } from '@abp/ng.theme.shared/extensions';
import { IdentityCreateFormPropContributors, IdentityEditFormPropContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const birthdayProp = new FormProp<IdentityUserDto>({
type: ePropType.Date,
name: 'birthday',
displayName: 'Date of Birth',
validators: () => [Validators.required],
});
export function birthdayPropContributor(propList: FormPropList<IdentityUserDto>) {
propList.addByIndex(birthdayProp, 4);
}
export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = {
'Identity.UsersComponent': [birthdayPropContributor],
};
export const identityEditFormPropContributors: IdentityEditFormPropContributors = {
'Identity.UsersComponent': [birthdayPropContributor],
};
```
The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addByIndex` method, which adds the given value to the specified index of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `birthdayPropContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = {
'Identity.UsersComponent': [ birthdayPropContributor ],
};
/* OR */
const identityCreateContributors: IdentityCreateFormPropContributors = {};
identityCreateContributors[eIdentityComponents.Users] = [ birthdayPropContributor ];
export const identityCreateFormPropContributors = identityCreateContributors;
```
### Step 2. Import and Use Form Prop Contributors
Import `identityCreateFormPropContributors` and `identityEditFormPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import {
identityCreateFormPropContributors,
identityEditFormPropContributors,
} from './form-prop-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
createFormPropContributors: identityCreateFormPropContributors,
editFormPropContributors: identityEditFormPropContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `IdentityModule`.
## Object Extensions
Extra properties defined on an existing entity will be included in the create and edit forms and validated based on their configuration. The form values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior.
## API
### PropData\<R = any\>
`PropData` is the shape of the parameter passed to all callbacks or predicates in a `FormProp`.
It has the following properties:
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleFormPropComponent`, including, but not limited to, its parent components.
```js
{
type: ePropType.Enum,
name: 'myField',
options: data => {
const restService = data.getInjected(RestService);
const usersComponent = data.getInjected(UsersComponent);
// Use restService and usersComponent public props and methods here
}
},
```
- **record** is the row data, i.e. current value of the selected item to edit. This property is _available only on edit forms_.
```js
{
type: ePropType.String,
name: 'myProp',
readonly: data => data.record.someOtherProp,
}
```
### PropCallback\<T, R = any\>
`PropCallback` is the type of the callback function that can be passed to a `FormProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type PropCallback<T, R = any> = (data?: PropData<T>) => R;
```
### PropPredicate\<T\>
`PropPredicate` is the type of the predicate function that can be passed to a `FormProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation:
```js
type PropPredicate<T> = (data?: PropData<T>) => boolean;
```
### FormPropOptions\<R = any\>
`FormPropOptions` is the type that defines required and optional properties you have to pass in order to create a form prop.
Its type definition is as follows:
```js
type FormPropOptions<R = any> = {
type: ePropType;
name: string;
displayName?: string;
id?: string;
permission?: string;
visible?: PropPredicate<R>;
readonly?: PropPredicate<R>;
disabled?: PropPredicate<R>;
validators?: PropCallback<R, ValidatorFn[]>;
asyncValidators?: PropCallback<R, AsyncValidatorFn[]>;
defaultValue?: boolean | number | string | Date;
options?: PropCallback<R, Observable<ABP.Option<any>[]>>;
autocomplete?: string;
isExtra? boolean;
};
```
As you see, passing `type` and `name` is enough to create a form prop. Here is what each property is good for:
- **type** is the type of the prop value. It defines which input is rendered for the prop in the form. (_required_)
- **name** is the property name (or key) which will be used to read the value of the prop. (_required_)
- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`)
- **id** will be set as the `for` attribute of the label and the `id` attribute of the input for the field. (_default:_ `options.name`)
- **permission** is the permission context which will be used to decide if a column for this form prop should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if this prop should be displayed on the form or not. (_default:_ `() => true`)
- **readonly** is a predicate that will be used to decide if this prop should be readonly or not. (_default:_ `() => false`)
- **disabled** is a predicate that will be used to decide if this prop should be disabled or not. (_default:_ `() => false`)
- **validators** is a callback that returns validators for the prop. (_default:_ `() => []`)
- **asyncValidators** is a callback that returns async validators for the prop. (_default:_ `() => []`)
- **defaultValue** is the initial value the field will have. (_default:_ `null`)
- **options** is a callback that is called when a dropdown is needed. It must return an observable. (_default:_ `undefined`)
- **autocomplete** will be set as the `autocomplete` attribute of the input for the field. Please check [possible values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#Values). (_default:_ `'off'`)
- **isExtra** indicates this prop is an object extension. When `true`, the value of the field will be mapped from and to `extraProperties` of the entity. (_default:_ `undefined`)
> Important Note: Do not use `record` property of `PropData` in create form predicates and callbacks, because it will be `undefined`. You can use it on edit form contributors though.
You may find a full example below.
### FormProp\<R = any\>
`FormProp` is the class that defines your form props. It takes a `FormPropOptions` and sets the default values to the properties, creating a form prop that can be passed to a form contributor.
```js
const options: FormPropOptions<IdentityUserDto> = {
type: ePropType.Enum,
name: 'myProp',
displayName: 'Default::MyPropName',
id: 'my-prop',
permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical
visible: data => {
const store = data.getInjected(Store);
const selectSensitiveDataVisibility = ConfigState.getSetting(
'Abp.Identity.IsSensitiveDataVisible' // hypothetical
);
return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true';
},
readonly: data => data.record.someProp,
disabled: data => data.record.someOtherProp,
validators: () => [Validators.required],
asyncValidators: data => {
const http = data.getInjected(HttpClient);
function validate(control: AbstractControl): Observable<ValidationErrors | null> {
if (control.pristine) return of(null);
return http
.get('https://api.my-brand.io/hypothetical/endpoint/' + control.value)
.pipe(map(response => (response.valid ? null : { invalid: true })));
}
return [validate];
},
defaultValue: 0,
options: data => {
const service = data.getInjected(MyIdentityService);
return service.getMyPropOptions()
.pipe(
map(({items}) => items.map(
item => ({key: item.name, value: item.id })
)),
);
},
autocomplete: 'off',
isExtra: true,
};
const prop = new FormProp(options);
```
It also has two static methods to create its instances:
- **FormProp.create\<R = any\>\(options: FormPropOptions\<R\>\)** is used to create an instance of `FormProp`.
```js
const prop = FormProp.create(options);
```
- **FormProp.createMany\<R = any\>\(options: FormPropOptions\<R\>\[\]\)** is used to create multiple instances of `FormProp` with given array of `FormPropOptions`.
```js
const props = FormProp.createMany(optionsArray);
```
### FormPropList\<R = any\>
`FormPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
propList: FormPropList<IdentityUserDto>,
) {
// drop email node
const emailPropNode = propList.dropByValue(
'AbpIdentity::EmailAddress',
(prop, displayName) => prop.displayName === displayName,
);
// add it back after phoneNumber
propList.addAfter(
emailPropNode.value,
'phoneNumber',
(value, name) => value.name === name,
);
}
```
### CreateFormPropContributorCallback\<R = any\>
`CreateFormPropContributorCallback` is the type that you can pass as **create form** prop contributor callbacks to static `forLazy` methods of the modules.
```js
export function myPropCreateContributor(
propList: FormPropList<IdentityUserDto>,
) {
// add myProp as 2nd field from the start
propList.add(myProp).byIndex(1);
}
export const identityCreateFormPropContributors = {
[eIdentityComponents.Users]: [myPropCreateContributor],
};
```
### EditFormPropContributorCallback\<R = any\>
`EditFormPropContributorCallback` is the type that you can pass as **edit form** prop contributor callbacks to static `forLazy` methods of the modules.
```js
export function myPropEditContributor(
propList: FormPropList<IdentityUserDto>,
) {
// add myProp as 2nd field from the end
propList.add(myProp).byIndex(-1);
}
export const identityEditFormPropContributors = {
[eIdentityComponents.Users]: [myPropEditContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

@ -0,0 +1,442 @@
# Entity Action Extensions for Angular UI
## Introduction
Entity action extension system allows you to add a new action to the action menu for an entity. A "Click Me" action was added to the user management page below:
![Entity Action Extension Example: "Click Me!" Action](images/user-action-extension-click-me-ng.png)
You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can access to the current entity in your code.
## How to Set Up
In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../Modules/Identity.md).
### Step 1. Create Entity Action Contributors
The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module:
```js
// entity-action-contributors.ts
import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions';
import { IdentityEntityActionContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const alertUserName = new EntityAction<IdentityUserDto>({
text: 'Click Me!',
action: data => {
// Replace alert with your custom code
alert(data.record.userName);
},
// See EntityActionOptions in API section for all options
});
export function alertUserNameContributor(
actionList: EntityActionList<IdentityUserDto>,
) {
actionList.addTail(alertUserName);
}
export const identityEntityActionContributors: IdentityEntityActionContributors = {
// enum indicates the page to add contributors to
[eIdentityComponents.Users]: [
alertUserNameContributor,
// You can add more contributors here
],
};
```
The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addTail` method, which adds the given value to the end of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `alertUserNameContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityEntityActionContributors: IdentityEntityActionContributors = {
'Identity.UsersComponent': [ alertUserNameContributor ],
};
/* OR */
const identityContributors: IdentityEntityActionContributors = {};
identityContributors[eIdentityComponents.Users] = [ alertUserNameContributor ];
export const identityEntityActionContributors = identityContributors;
```
### Step 2. Import and Use Entity Action Contributors
Import `identityEntityActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import { identityEntityActionContributors } from './entity-action-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
entityActionContributors: identityEntityActionContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the users page (`UsersComponent`) of the `IdentityModule`.
## How to Place a Custom Modal and Trigger It by Entity Actions
Incase you need to place a custom modal that will be triggered by an entity action, there are two ways to do it: A quick one and an elaborate one.
### The Quick Solution
1. Place your custom modal inside `AppComponent` template.
```html
<abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader>
<h3><!-- YOUR TITLE HERE --></h3>
</ng-template>
<ng-template #abpBody>
<!-- YOUR CONTENT HERE -->
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpIdentity::Cancel' | abpLocalization }}}%}
</button>
<!-- YOUR CONFIRMATION BUTTON HERE -->
</ng-template>
</abp-modal>
```
2. Add the following inside your `AppComponent` class:
```js
isModalOpen: boolean;
openModal(/* may take parameters */) {
/* and set things before showing the modal */
this.isModalOpen = true;
}
```
3. Add an entity action similar to this:
```js
const customModalAction = new EntityAction<IdentityUserDto>({
text: 'Custom Modal Action',
action: data => {
const component = data.getInjected(AppComponent);
component.openModal(/* you may pass parameters */);
},
});
```
That should work. However, there is a longer but lazy-loading solution, and we are going to use NGXS for it.
### The Elaborate Solution
Consider the modal will be displayed in the Identity module. How can we lazy-load it too?
1. Create a folder called `identity-extended` inside your app folder.
2. Create a file called `identity-popups.store.ts` in it.
3. Insert the following code in the new file:
```js
import { Action, Selector, State, StateContext } from '@ngxs/store';
export class ToggleIdentityPopup {
static readonly type = '[IdentityPopups] Toggle';
constructor(public readonly payload: boolean) {}
}
@State<IdentityPopupsStateModel>({
name: 'IdentityPopups',
defaults: {
isVisible: false,
},
})
export class IdentityPopupsState {
@Selector()
static isVisible(state: IdentityPopupsStateModel) {
return state.isVisible;
}
@Action(ToggleIdentityPopup)
toggleModal(
context: StateContext<IdentityPopupsStateModel>,
{ payload }: ToggleIdentityPopup,
) {
context.patchState({ isVisible: payload });
}
}
interface IdentityPopupsStateModel {
isVisible: boolean;
}
```
4. Create a file called `identity-extended.module.ts` in the same folder.
5. Insert the following code in the new file:
```js
import { CoreModule } from '@abp/ng.core';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { Component, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NgxsModule, Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { IdentityPopupsState, ToggleIdentityPopup } from './identity-popups.store';
@Component({
template: `
<router-outlet></router-outlet>
<router-outlet name="popup"></router-outlet>
`,
})
export class IdentityOutletComponent {}
@Component({
template: `
<abp-modal [visible]="isVisible$ | async" (disappear)="onDisappear()">
<ng-template #abpHeader>
<h3><!-- YOUR TITLE HERE --></h3>
</ng-template>
<ng-template #abpBody>
<!-- YOUR CONTENT HERE -->
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpIdentity::Cancel' | abpLocalization }}}%}
</button>
<!-- YOUR CONFIRMATION BUTTON HERE -->
</ng-template>
</abp-modal>
`,
})
export class IdentityPopupsComponent {
@Select(IdentityPopupsState.isVisible)
isVisible$: Observable<boolean>;
constructor(private store: Store) {}
onDisappear() {
this.store.dispatch(new ToggleIdentityPopup(false));
}
}
@NgModule({
declarations: [IdentityPopupsComponent, IdentityOutletComponent],
imports: [
CoreModule,
ThemeSharedModule,
NgxsModule.forFeature([IdentityPopupsState]),
RouterModule.forChild([
{
path: '',
component: IdentityOutletComponent,
children: [
{
path: '',
outlet: 'popup',
component: IdentityPopupsComponent,
},
{
path: '',
loadChildren: () => import('@volo/abp.ng.identity').then(m => m.IdentityModule),
},
],
},
]),
],
})
export class IdentityExtendedModule {}
```
6. Change the `identity` path in your `AppRoutingModule` to this:
```js
{
path: 'identity',
loadChildren: () =>
import('./identity-extended/identity-extended.module').then(m => m.IdentityExtendedModule),
},
```
7. Add an entity action similar to this:
```js
const customModalAction = new EntityAction<IdentityUserDto>({
text: 'Custom Modal Action',
action: data => {
const store = data.getInjected(Store);
store.dispatch(new ToggleIdentityPopup(true));
},
});
```
It should now be working well with lazy-loading. The files are compact in the description to make it quicker to explain. You may split the files as you wish.
## API
### ActionData\<R = any\>
`ActionData` is the shape of the parameter passed to all callbacks or predicates in an `EntityAction`.
It has the following properties:
- **record** is the row data, i.e. current value rendered in the table.
```js
{
text: 'Click Me!',
action: data => {
alert(data.record.userName);
},
}
```
- **index** is the table index where the record is at.
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `GridActionsComponent`, including, but not limited to, its parent component.
```js
{
text: 'Click Me!',
action: data => {
const restService = data.getInjected(RestService);
// Use restService public props and methods here
},
visible: data => {
const usersComponent = data.getInjected(UsersComponent);
// Use usersComponent public props and methods here
},
}
```
### ActionCallback\<T, R = any\>
`ActionCallback` is the type of the callback function that can be passed to an `EntityAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type ActionCallback<T, R = any> = (data?: ActionData<T>) => R;
```
### ActionPredicate\<T\>
`ActionPredicate` is the type of the predicate function that can be passed to an `EntityAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation:
```js
type ActionPredicate<T> = (data?: ActionData<T>) => boolean;
```
### EntityActionOptions\<R = any\>
`EntityActionOptions` is the type that defines required and optional properties you have to pass in order to create an entity action.
Its type definition is as follows:
```js
type EntityActionOptions<R = any> = {
action: ActionCallback<R>,
text: string,
icon?: string,
permission?: string,
visible?: ActionPredicate<R>,
};
```
As you see, passing `action` and `text` is enough to create an entity action. Here is what each property is good for:
- **action** is a callback that is called when the grid action is clicked. (_required_)
- **text** is the button text which will be localized. (_required_)
- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`)
- **permission** is the permission context which will be used to decide if this type of grid action should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if the current record should have this grid action or not. (_default:_ `() => true`)
You may find a full example below.
### EntityAction\<R = any\>
`EntityAction` is the class that defines your entity actions. It takes an `EntityActionOptions` and sets the default values to the properties, creating an entity action that can be passed to an entity contributor.
```js
const options: EntityActionOptions<IdentityUserDto> = {
action: data => {
const component = data.getInjected(UsersComponent);
component.unlock(data.record.id);
},
text: 'AbpIdentity::Unlock',
icon: 'fa fa-unlock',
permission: 'AbpIdentity.Users.Update',
visible: data => data.record.isLockedOut,
};
const action = new EntityAction(options);
```
It also has two static methods to create its instances:
- **EntityAction.create\<R = any\>\(options: EntityActionOptions\<R\>\)** is used to create an instance of `EntityAction`.
```js
const action = EntityAction.create(options);
```
- **EntityAction.createMany\<R = any\>\(options: EntityActionOptions\<R\>\[\]\)** is used to create multiple instances of `EntityAction` with given array of `EntityActionOptions`.
```js
const actions = EntityAction.createMany(optionsArray);
```
### EntityActionList\<R = any\>
`EntityActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
actionList: EntityActionList<IdentityUserDto>,
) {
// drop "Unlock" button
const unlockActionNode = actionList.dropByValue(
'AbpIdentity::Unlock',
(action, text) => action.text === text,
);
// add it back to the head of the list
actionList.addHead(unlockActionNode.value);
}
```
### EntityActionContributorCallback\<R = any\>
`EntityActionContributorCallback` is the type that you can pass as entity action contributor callbacks to static `forLazy` methods of the modules.
```js
// lockUserContributor should have EntityActionContributorCallback<IdentityUserDto> type
export function lockUserContributor(
actionList: EntityActionList<IdentityUserDto>,
) {
// add lockUser as 3rd action
actionList.add(lockUser).byIndex(2);
}
export const identityEntityActionContributors = {
[eIdentityComponents.Users]: [lockUserContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

@ -0,0 +1,420 @@
# Page Toolbar Extensions for Angular UI
## Introduction
Page toolbar extension system allows you to add a new action to the toolbar of a page. A "Click Me" action was added to the user management page below:
![Page Toolbar Extension Example: "Click Me!" Action](images/user-page-toolbar-extension-click-me-ng.png)
You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access to page data (the main record, usually an entity list) in your code. Additionally, you can pass in custom components instead of using the default button.
## How to Add an Action to Page Toolbar
In this example, we will add a "Click Me!" action and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console.
### Step 1. Create Toolbar Action Contributors
The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module:
```js
// toolbar-action-contributors.ts
import { ToolbarActionList, ToolbarAction } from '@abp/ng.theme.shared/extensions';
import { IdentityToolbarActionContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const logUserNames = new ToolbarAction<IdentityUserDto[]>({
text: 'Click Me!',
action: data => {
// Replace log with your custom code
data.record.forEach(user => console.log(user.userName));
},
// See ToolbarActionOptions in API section for all options
});
export function logUserNamesContributor(
actionList: ToolbarActionList<IdentityUserDto[]>
) {
actionList.addHead(logUserNames);
}
export const identityToolbarActionContributors: IdentityToolbarActionContributors = {
// enum indicates the page to add contributors to
[eIdentityComponents.Users]: [
logUserNamesContributor,
// You can add more contributors here
],
};
```
The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `logUserNamesContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
### Step 2. Import and Use Toolbar Action Contributors
Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import { identityToolbarActionContributors } from './toolbar-action-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
toolbarActionContributors: identityToolbarActionContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule`.
## How to Add a Custom Component to Page Toolbar
In this example, we will add a custom "Click Me!" button and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console.
### Step 1. Create A Custom Component
We need to have a component before we can pass it to the toolbar action contributors:
```js
// click-me-button.component.ts
import { Component, Inject } from '@angular/core';
import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.theme.shared/extensions';
import { IdentityUserDto } from '@volo/abp.ng.identity';
@Component({
selector: 'app-click-me-button',
template: `
<button class="btn btn-warning" (click)="handleClick()">Click Me!</button>
`,
})
export class ClickMeButtonComponent {
constructor(
@Inject(EXTENSIONS_ACTION_DATA)
private data: ActionData<IdentityUserDto[]>
) {}
handleClick() {
this.data.record.forEach(user => console.log(user.userName));
}
}
```
Here, `EXTENSIONS_ACTION_DATA` token provides us the context from the page toolbar. Therefore, we are able to reach the page data via `record`, which is an array of users, i.e. `IdentityUserDto[]`.
> We could also import `EXTENSIONS_ACTION_CALLBACK` from **@abp/ng.theme.shared/extensions** package, which is a higher order function that triggers the predefined `action` when called. It passes `ActionData` as the first parameter, so you do not have to pass it explicitly. In other words, `EXTENSIONS_ACTION_CALLBACK` can be called without any parameters and it will not fail.
### Step 2. Create Toolbar Action Contributors
The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in:
```js
// toolbar-action-contributors.ts
import { ToolbarActionList, ToolbarComponent } from '@abp/ng.theme.shared/extensions';
import { IdentityUserDto } from '@volo/abp.ng.identity';
import { IdentityToolbarActionContributors } from '@volo/abp.ng.identity/config';
import { ClickMeButtonComponent } from './click-me-button.component';
const logUserNames = new ToolbarComponent<IdentityUserDto[]>({
component: ClickMeButtonComponent,
// See ToolbarActionOptions in API section for all options
});
export function logUserNamesContributor(
actionList: ToolbarActionList<IdentityUserDto[]>
) {
actionList.addHead(logUserNames);
}
export const identityToolbarActionContributors: IdentityToolbarActionContributors = {
// enum indicates the page to add contributors to
[eIdentityComponents.Users]: [
logUserNamesContributor,
// You can add more contributors here
],
};
```
The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `logUserNamesContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityToolbarActionContributors: IdentityToolbarActionContributors = {
'Identity.UsersComponent': [ logUserNamesContributor ],
};
/* OR */
const identityContributors: IdentityToolbarActionContributors = {};
identityContributors[eIdentityComponents.Users] = [ logUserNamesContributor ];
export const identityToolbarActionContributors = identityContributors;
```
### Step 3. Import and Use Toolbar Action Contributors
Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below. If Ivy is not enabled in your project, do not forget putting `ClickMeButtonComponent` into `entryComponents`:
```js
import { identityToolbarActionContributors } from './toolbar-action-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
toolbarActionContributors: identityToolbarActionContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule` and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components.
![Page Toolbar Extension Example: Custom "Click Me!" Button](images/user-page-toolbar-extension-custom-click-me-ng.png)
## How to Place a Custom Modal and Trigger It by Toolbar Actions
Please check the same topic in [entity action extensions document](Entity-Action-Extensions.md) and replace entity action with a toolbar action.
## API
### ActionData\<R = any\>
`ActionData` is the shape of the parameter passed to all callbacks or predicates in a `ToolbarAction`.
It has the following properties:
- **record** is the page data, the main record on a page, usually an entity list (e.g. list of users).
```js
{
text: 'Click Me!',
action: data => {
data.record.forEach(user => {
console.lof(user.userName);
});
},
}
```
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `PageToolbarComponent`, including, but not limited to, its parent component.
```js
{
text: 'Click Me!',
action: data => {
const restService = data.getInjected(RestService);
// Use restService public props and methods here
},
visible: data => {
const usersComponent = data.getInjected(UsersComponent);
// Use usersComponent public props and methods here
},
}
```
### ActionCallback\<T, R = any\>
`ActionCallback` is the type of the callback function that can be passed to a `ToolbarAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type ActionCallback<T, R = any> = (data?: ActionData<T>) => R;
```
### ActionPredicate\<T\>
`ActionPredicate` is the type of the predicate function that can be passed to a `ToolbarAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation:
```js
type ActionPredicate<T> = (data?: ActionData<T>) => boolean;
```
### ToolbarActionOptions\<R = any\>
`ToolbarActionOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar action.
Its type definition is as follows:
```js
type ToolbarActionOptions<R = any> = {
action: ActionCallback<R>,
text: string,
icon?: string,
permission?: string,
visible?: ActionPredicate<R>,
};
```
As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for:
- **action** is a callback that is called when the toolbar action is clicked. (_required_)
- **text** is the button text which will be localized. (_required_)
- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`)
- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`)
You may find a full example below.
### ToolbarAction\<R = any\>
`ToolbarAction` is the class that defines your toolbar actions. It takes an `ToolbarActionOptions` and sets the default values to the properties, creating an toolbar action that can be passed to an toolbar contributor.
```js
const options: ToolbarActionOptions<IdentityUserDto[]> = {
action: data => {
const service = data.getInjected(MyCustomIdentityService);
const lockedUsers = data.record.filter(user => user.isLockedOut);
service.unlockAll(lockedUsers);
},
text: 'MyProjectName::UnlockAll',
icon: 'fa fa-unlock',
permission: 'AbpIdentity.Users.Update',
visible: data => data.record.some(user => user.isLockedOut),
};
const action = new ToolbarAction(options);
```
It also has two static methods to create its instances:
- **ToolbarAction.create\<R = any\>\(options: ToolbarActionOptions\<R\>\)** is used to create an instance of `ToolbarAction`.
```js
const action = ToolbarAction.create(options);
```
- **ToolbarAction.createMany\<R = any\>\(options: ToolbarActionOptions\<R\>\[\]\)** is used to create multiple instances of `ToolbarAction` with given array of `ToolbarActionOptions`.
### ToolbarComponentOptions\<R = any\>
`ToolbarComponentOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar component.
Its type definition is as follows:
```js
type ToolbarComponentOptions<R = any> = {
component: Type<any>,
action?: ActionCallback<R>,
permission?: string,
visible?: ActionPredicate<R>,
};
```
As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for:
- **component** is the constructor of the component to be projected. (_required_)
- **action** is a predefined callback that you can reach in your component via `EXTENSIONS_ACTION_CALLBACK` token and trigger. (_optional_)
- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`)
You may find a full example below.
### ToolbarComponent\<R = any\>
`ToolbarComponent` is the class that defines toolbar actions which project a custom component. It takes an `ToolbarComponentOptions` and sets the default values to the properties, creating a toolbar action that can be passed to an toolbar contributor.
```js
const options: ToolbarComponentOptions<IdentityUserDto[]> = {
component: UnlockAllButton,
action: data => {
const service = data.getInjected(MyCustomIdentityService);
const lockedUsers = data.record.filter(user => user.isLockedOut);
service.unlockAll(lockedUsers);
},
permission: 'AbpIdentity.Users.Update',
visible: data => data.record.some(user => user.isLockedOut),
};
const action = new ToolbarComponent(options);
```
It also has two static methods to create its instances:
- **ToolbarComponent.create\<R = any\>\(options: ToolbarComponentOptions\<R\>\)** is used to create an instance of `ToolbarComponent`.
```js
const action = ToolbarComponent.create(options);
```
- **ToolbarComponent.createMany\<R = any\>\(options: ToolbarComponentOptions\<R\>\[\]\)** is used to create multiple instances of `ToolbarComponent` with given array of `ToolbarComponentOptions`.
```js
const actions = ToolbarComponent.createMany(optionsArray);
```
### ToolbarActionList\<R = any\>
`ToolbarActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
actionList: ToolbarActionList<IdentityUserDto[]>,
) {
// drop "New User" button
const newUserActionNode = actionList.dropByValue(
'AbpIdentity::NewUser',
(action, text) => action['text'] === text,
);
// add it back to the head of the list
actionList.addHead(newUserActionNode.value);
}
export const identityEntityActionContributors = {
[eIdentityComponents.Users]: [
logUserNamesContributor,
reorderUserContributors,
],
};
```
### ToolbarActionContributorCallback\<R = any\>
`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `forLazy` methods of the modules.
```js
// exportUsersContributor should have ToolbarActionContributorCallback<IdentityUserDto[]> type
export function exportUsersContributor(
actionList: ToolbarActionList<IdentityUserDto[]>,
) {
// add exportUsers just before the last action
actionList.add(exportUsers).byIndex(-1);
}
export const identityEntityActionContributors = {
[eIdentityComponents.Users]: [exportUsersContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

@ -0,0 +1,163 @@
# Page Toolbar Extensions for ASP.NET Core UI
Page toolbar system allows you to add components to the toolbar of any page. The page toolbar is the area right to the header of a page. A button ("Import users from excel") was added to the user management page below:
![page-toolbar-button](../../images/page-toolbar-button.png)
You can add any type of view component item to the page toolbar or modify existing items.
## How to Set Up
In this example, we will add an "Import users from excel" button and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md).
### Add a New Button to the User Management Page
Write the following code inside the `ConfigureServices` of your web module class:
````csharp
Configure<AbpPageToolbarOptions>(options =>
{
options.Configure<Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel>(toolbar =>
{
toolbar.AddButton(
LocalizableString.Create<MyProjectNameResource>("ImportFromExcel"),
icon: "file-import",
id: "ImportUsersFromExcel",
type: AbpButtonType.Secondary
);
});
});
````
`AddButton` is a shortcut to simply add a button component. Note that you need to add the `ImportFromExcel` to your localization dictionary (json file) to localize the text.
When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `order` to set the order of the button component relative to the other components).
### Create a JavaScript File
Now, we can go to the client side to handle click event of the new button. First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project:
![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png)
Here, the content of this JavaScript file:
````js
$(function () {
$('#ImportUsersFromExcel').click(function (e) {
e.preventDefault();
alert('TODO: import users from excel');
});
});
````
In the `click` event, you can do anything you need to do.
### Add the File to the User Management Page
Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](Bundling-Minification.md).
Write the following code inside the `ConfigureServices` of your module class:
````csharp
Configure<AbpBundlingOptions>(options =>
{
options.ScriptBundles.Configure(
typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName,
bundleConfiguration =>
{
bundleConfiguration.AddFiles(
"/Pages/Identity/Users/my-user-extensions.js"
);
});
});
````
This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules.
## Advanced Use Cases
While you typically want to add a button action to the page toolbar, it is possible to add any type of component.
### Add View Component to a Page Toolbar
First, create a new view component in your project:
![page-toolbar-custom-component](../../images/page-toolbar-custom-component.png)
For this example, we've created a `MyToolbarItem` view component under the `/Pages/Identity/Users/MyToolbarItem` folder.
`MyToolbarItemViewComponent.cs` content:
````csharp
public class MyToolbarItemViewComponent : AbpViewComponent
{
public IViewComponentResult Invoke()
{
return View("~/Pages/Identity/Users/MyToolbarItem/Default.cshtml");
}
}
````
`Default.cshtml` content:
````xml
<span>
<button type="button" class="btn btn-dark">CLICK ME</button>
</span>
````
* `.cshtml` file can contain any type of component(s). It is a typical view component.
* `MyToolbarItemViewComponent` can inject and use any service if you need.
Then you can add the `MyToolbarItemViewComponent` to the user management page:
````csharp
Configure<AbpPageToolbarOptions>(options =>
{
options.Configure<Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel>(
toolbar =>
{
toolbar.AddComponent<MyToolbarItemViewComponent>();
}
);
});
````
* If your component accepts arguments (in the `Invoke`/`InvokeAsync` method), you can pass them to the `AddComponent` method as an anonymous object.
#### Permissions
If your button/component should be available based on a [permission/policy](../../Authorization.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods.
### Add a Page Toolbar Contributor
If you perform advanced custom logic while adding an item to a page toolbar, you can create a class that implements the `IPageToolbarContributor` interface or inherits from the `PageToolbarContributor` class:
````csharp
public class MyToolbarContributor : PageToolbarContributor
{
public override Task ContributeAsync(PageToolbarContributionContext context)
{
context.Items.Insert(0, new PageToolbarItem(typeof(MyToolbarItemViewComponent)));
return Task.CompletedTask;
}
}
````
* You can use `context.ServiceProvider` to resolve dependencies if you need.
Then add your class to the `Contributors` list:
````csharp
Configure<AbpPageToolbarOptions>(options =>
{
options.Configure<Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel>(
toolbar =>
{
toolbar.Contributors.Add(new MyToolbarContributor());
}
);
});
````

@ -353,22 +353,8 @@
"path": "PlugIn-Modules.md"
},
{
"text": "Customizing the Application Modules",
"path": "Customizing-Application-Modules-Guide.md",
"items": [
{
"text": "Extending Entities",
"path": "Customizing-Application-Modules-Extending-Entities.md"
},
{
"text": "Overriding Services",
"path": "Customizing-Application-Modules-Overriding-Services.md"
},
{
"text": "Overriding the User Interface",
"path": "Customizing-Application-Modules-Overriding-User-Interface.md"
}
]
"text": "Customizing/Extending Modules",
"path": "Customizing-Application-Modules-Guide.md"
},
{
"text": "Best Practices",

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Loading…
Cancel
Save