@ -0,0 +1 @@ |
|||
Please describe the feature need! |
|||
@ -0,0 +1,71 @@ |
|||
# For most projects, this workflow file will not need changing; you simply need |
|||
# to commit it to your repository. |
|||
# |
|||
# You may wish to alter this file to override the set of languages analyzed, |
|||
# or to provide custom queries or build logic. |
|||
name: "CodeQL" |
|||
|
|||
on: |
|||
push: |
|||
branches: [dev, rel-1.0, rel-1.1, rel-2.0, rel-2.1, rel-2.2, rel-2.3, rel-2.4, rel-2.5, rel-2.6, rel-2.7, rel-2.8, rel-2.9, rel-3.0, rel-3.1, rel-3.2] |
|||
pull_request: |
|||
# The branches below must be a subset of the branches above |
|||
branches: [dev] |
|||
schedule: |
|||
- cron: '0 8 * * 6' |
|||
|
|||
jobs: |
|||
analyze: |
|||
name: Analyze |
|||
runs-on: ubuntu-latest |
|||
|
|||
strategy: |
|||
fail-fast: false |
|||
matrix: |
|||
# Override automatic language detection by changing the below list |
|||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] |
|||
language: ['csharp', 'javascript'] |
|||
# Learn more... |
|||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection |
|||
|
|||
steps: |
|||
- name: Checkout repository |
|||
uses: actions/checkout@v2 |
|||
with: |
|||
# We must fetch at least the immediate parents so that if this is |
|||
# a pull request then we can checkout the head. |
|||
fetch-depth: 2 |
|||
|
|||
# If this run was triggered by a pull request event, then checkout |
|||
# the head of the pull request instead of the merge commit. |
|||
- run: git checkout HEAD^2 |
|||
if: ${{ github.event_name == 'pull_request' }} |
|||
|
|||
# Initializes the CodeQL tools for scanning. |
|||
- name: Initialize CodeQL |
|||
uses: github/codeql-action/init@v1 |
|||
with: |
|||
languages: ${{ matrix.language }} |
|||
# If you wish to specify custom queries, you can do so here or in a config file. |
|||
# By default, queries listed here will override any specified in a config file. |
|||
# Prefix the list here with "+" to use these queries and those in the config file. |
|||
# queries: ./path/to/local/query, your-org/your-repo/queries@main |
|||
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). |
|||
# If this step fails, then you should remove it and run the build manually (see below) |
|||
- name: Autobuild |
|||
uses: github/codeql-action/autobuild@v1 |
|||
|
|||
# ℹ️ Command-line programs to run using the OS shell. |
|||
# 📚 https://git.io/JvXDl |
|||
|
|||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines |
|||
# and modify them (or add more) to build your code if your project |
|||
# uses a compiled language |
|||
|
|||
#- run: | |
|||
# make bootstrap |
|||
# make release |
|||
|
|||
- name: Perform CodeQL Analysis |
|||
uses: github/codeql-action/analyze@v1 |
|||
@ -0,0 +1,15 @@ |
|||
{ |
|||
"grammarly.userWords": [ |
|||
"api", |
|||
"apiName", |
|||
"cli", |
|||
"defaultProject", |
|||
"formatter", |
|||
"md", |
|||
"monorepo", |
|||
"ngsw", |
|||
"npx", |
|||
"pwa", |
|||
"rootNamespace" |
|||
] |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
# ABP Framework 3.2 Final Has Been Released |
|||
|
|||
ABP Framework & ABP Commercial 3.2 have been released today. |
|||
|
|||
Since all the new features are already explained in details with the [3.2 RC Announcement Post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI), I will not repeat all the details again. Please read [the RC post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI) for **new feature and changes** you may need to do for your solution while upgrading to the version 3.2. |
|||
|
|||
## Creating New Solutions |
|||
|
|||
You can create a new solution with the ABP Framework version 3.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). |
|||
|
|||
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. |
|||
|
|||
## How to Upgrade an Existing Solution |
|||
|
|||
### Install/Update the ABP CLI |
|||
|
|||
First of all, install the ABP CLI or upgrade to the latest version. |
|||
|
|||
If you haven't installed yet: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
To update an existing installation: |
|||
|
|||
```bash |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
### ABP UPDATE Command |
|||
|
|||
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: |
|||
|
|||
````bash |
|||
abp update |
|||
```` |
|||
|
|||
After the update command, check [the RC blog post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI) to learn if you need to make any changes in your solution. |
|||
|
|||
> You may want to see the new [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). |
|||
|
|||
## About the Next Versions |
|||
|
|||
The next two versions (3.3 & 4.0) will be mostly related to completing the Blazor UI features and upgrading the ABP Framework & ecosystem to the .NET 5.0. |
|||
|
|||
The ultimate goal is to complete the version 4.0 with a stable Blazor UI with the fundamental features implemented and publish it just after the Microsoft lunches .NET 5 in this November. |
|||
@ -0,0 +1,317 @@ |
|||
# Using DevExtreme Angular Components With the ABP Framework |
|||
|
|||
In this article, I will show you how to integrate the [DevExpress Angular components](https://js.devexpress.com/Documentation/Guide/Angular_Components/DevExtreme_Angular_Components/) to a project created using the ABP Framework startup templates. Then I will use the [DataGrid](https://js.devexpress.com/Documentation/Guide/Widgets/DataGrid/Overview/) component to show a list of users on the UI. |
|||
|
|||
## Create the Project |
|||
|
|||
Let's create a new web application with the Angular UI using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI#new): |
|||
|
|||
```shell |
|||
abp new DevExtremeAngular -u angular |
|||
``` |
|||
|
|||
> For detail information about how to generate and start up a project, please refer to the [official docs](https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&DB=EF&Tiered=No). For the scope of this post, we will not go into details of the backend applications. |
|||
|
|||
## Running the Solution |
|||
|
|||
### The Server Side |
|||
|
|||
Server side contains multiple projects in the solution: |
|||
|
|||
 |
|||
|
|||
Run following projects in order; |
|||
|
|||
* Run `DevExtremeAngular.DbMigrator` to create the database and seed the initial data. |
|||
* Run `DevExtremeAngular.HttpApi.Host` project to make the backend API up & running. |
|||
|
|||
### The Angular Application |
|||
|
|||
Open a command line terminal and navigate to `angular` folder then run `yarn` or `npm install` based on which package you are using. |
|||
|
|||
After installation process is done, you can start your angular project by running `yarn start` or `npm start`. This command should serve the application and open the application in your default browser. If it doesn't open, you can navigate to http://localhost:4200 in your browser: |
|||
|
|||
 |
|||
|
|||
You can login to the application by using following credentials: |
|||
|
|||
> _Default admin username is **admin** and password is **1q2w3E\***_ |
|||
|
|||
 |
|||
|
|||
After successful login, you should be redirected to home page. |
|||
|
|||
## Install DevExtreme |
|||
|
|||
You can follow [the guide](https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Add_DevExtreme_to_an_Angular_CLI_Application/) provided by **DevExtreme** team or apply the following steps. |
|||
|
|||
* `npm install devextreme devextreme-angular` or `yarn add devextreme devextreme-angular` |
|||
* Import given two following styles in `angular.json` file: |
|||
|
|||
```javascript |
|||
// ... |
|||
"styles": [ |
|||
// ... |
|||
"src/styles.scss", |
|||
|
|||
"node_modules/devextreme/dist/css/dx.common.css", |
|||
"node_modules/devextreme/dist/css/dx.light.css" |
|||
] |
|||
``` |
|||
|
|||
* Add `dx-viewport` to classes of `body` in `index.html` |
|||
|
|||
```html |
|||
<body class="bg-light dx-viewport"> |
|||
<app-root> |
|||
<div class="donut centered"></div> |
|||
</app-root> |
|||
</body> |
|||
``` |
|||
|
|||
After completing these steps, you need to restart the angular application. |
|||
|
|||
## Create a lazy Angular Module for DevExtreme Demo |
|||
|
|||
Let's create a module which will be loaded lazily. |
|||
|
|||
Open up a terminal and navigate to `angular` to run following command. |
|||
|
|||
```shell |
|||
ng g m dev-extreme --route dev-extreme --module app |
|||
``` |
|||
|
|||
...or with `npx`, if you do not have `angular-cli` installed... |
|||
|
|||
```shell |
|||
npx ng g m dev-extreme --route dev-extreme --module app |
|||
``` |
|||
|
|||
Your terminal should log the following output: |
|||
|
|||
```shell |
|||
CREATE src/app/dev-extreme/dev-extreme-routing.module.ts (361 bytes) |
|||
CREATE src/app/dev-extreme/dev-extreme.module.ts (379 bytes) |
|||
CREATE src/app/dev-extreme/dev-extreme.component.scss (0 bytes) |
|||
CREATE src/app/dev-extreme/dev-extreme.component.html (26 bytes) |
|||
CREATE src/app/dev-extreme/dev-extreme.component.spec.ts (655 bytes) |
|||
CREATE src/app/dev-extreme/dev-extreme.component.ts (295 bytes) |
|||
UPDATE src/app/app-routing.module.ts (362 bytes) |
|||
``` |
|||
|
|||
The Angular CLI has created a module and configured it to lazy-load at `/dev-extreme` path. |
|||
|
|||
The last step to be able to see our newly created module in the browser, open `route.provider.ts` and edit the array being added into the routes. |
|||
|
|||
```typescript |
|||
// ... |
|||
routes.add([ |
|||
{ |
|||
path: '/', |
|||
name: '::Menu:Home', |
|||
iconClass: 'fas fa-home', |
|||
order: 1, |
|||
layout: eLayoutType.application, |
|||
}, |
|||
{ |
|||
path: '/dev-extreme', |
|||
name: 'Dev Extreme', |
|||
order: 2, |
|||
layout: eLayoutType.application, |
|||
}, |
|||
]); |
|||
// ... |
|||
``` |
|||
|
|||
After completing the steps above, you should be able to see `Dev Extreme` on the header and when you click on it, you should be redirected to `/dev-extreme` page and see the following message on the screen. |
|||
|
|||
 |
|||
|
|||
## Display users on the dev-extreme page |
|||
|
|||
For this demo, we will list users on the screen. We already have `admin` as our first user. |
|||
|
|||
Let's add couple of more to the list in `Administration -> Identity Management -> Users` page. |
|||
|
|||
 |
|||
|
|||
Now we are ready to fetch our users and display them on `/dev-extreme` page. |
|||
|
|||
Firstly, let's create a service for our component. |
|||
|
|||
Navigate to the `dev-extreme` folder and run following command. If you run this command at the root, the service will be generated next to `app.module.ts` |
|||
|
|||
```shell |
|||
ng g s dev-extreme |
|||
``` |
|||
|
|||
Following files should be created |
|||
|
|||
```shell |
|||
CREATE src/app/dev-extreme/dev-extreme.service.spec.ts (378 bytes) |
|||
CREATE src/app/dev-extreme/dev-extreme.service.ts (139 bytes) |
|||
``` |
|||
|
|||
Let's import and inject `IdentityService` as dependency in `dev-extreme.service.ts`. After then, let's create a stream called `users$` to retrieve the users. |
|||
|
|||
`identityService.getUsers` returns `ABP.PagedResponse` which contains two fields, `items` and `totalCount`. We are only interested in `items` for now. |
|||
|
|||
When we apply the steps described above, the final version of `dev-extreme.service` should be as follows |
|||
|
|||
```typescript |
|||
import { Injectable } from '@angular/core'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { IdentityService } from '@abp/ng.identity'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class DevExtremeService { |
|||
users$ = this.service.getUsers().pipe(map((result) => result.items)); |
|||
|
|||
constructor(private service: IdentityService) {} |
|||
} |
|||
``` |
|||
|
|||
Now we can simply inject `DevExtremeService` as public and utilize `users$` stream in `dev-extreme.component.ts` as follows: |
|||
|
|||
```typescript |
|||
import { Component } from '@angular/core'; |
|||
import { DevExtremeService } from './dev-extreme.service'; |
|||
|
|||
@Component({ |
|||
selector: 'app-dev-extreme', |
|||
templateUrl: './dev-extreme.component.html', |
|||
styleUrls: ['./dev-extreme.component.scss'], |
|||
}) |
|||
export class DevExtremeComponent { |
|||
constructor(public service: DevExtremeService) {} |
|||
} |
|||
``` |
|||
|
|||
And use it within `dev-extreme.component.html` |
|||
|
|||
```html |
|||
<ng-container *ngIf="service.users$ | async as users"> |
|||
<ul> |
|||
<li *ngFor="let user of users"> |
|||
{{ user.name }} |
|||
</li> |
|||
</ul> |
|||
</ng-container> |
|||
``` |
|||
|
|||
This should list names of the users on the screen |
|||
|
|||
 |
|||
|
|||
## Use DxDataGrid to list the users |
|||
|
|||
You can take a look at [demo](https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/ColumnCustomization/Angular/Light/) provided by **DevExtreme** team or apply the following steps. |
|||
|
|||
Now, our application is ready to use `dx-data-grid` in `dev-extreme.component.ts` |
|||
|
|||
Firstly, we need to import `DxDataGridModule` in our module as follows. |
|||
|
|||
```typescript |
|||
// ... |
|||
|
|||
import { DxDataGridModule } from 'devextreme-angular'; |
|||
|
|||
@NgModule({ |
|||
// ... |
|||
imports: [ |
|||
// ... |
|||
DxDataGridModule |
|||
], |
|||
}) |
|||
export class DevExtremeModule {} |
|||
``` |
|||
|
|||
At this point `dx-data-grid` is avaliable within our module and we can use it in our template. |
|||
|
|||
Change `dev-extreme.component.html` to the following |
|||
|
|||
```html |
|||
<ng-container *ngIf="service.users$ | async as users"> |
|||
<dx-data-grid [dataSource]="users"></dx-data-grid> |
|||
</ng-container> |
|||
``` |
|||
|
|||
It should display a table on the screen |
|||
|
|||
 |
|||
|
|||
Since, we did not specify any columns, `dx-data-grid` displayed every column avaliable. Let's pick some columns to make it more readable. |
|||
|
|||
Change `dev-extreme.component.html` to the following: |
|||
|
|||
```html |
|||
<ng-container *ngIf="service.users$ | async as users"> |
|||
<dx-data-grid [dataSource]="users"> |
|||
<dxi-column dataField="userName"></dxi-column> |
|||
<dxi-column dataField="name"></dxi-column> |
|||
<dxi-column dataField="surname"></dxi-column> |
|||
<dxi-column dataField="email"></dxi-column> |
|||
<dxi-column dataField="phoneNumber"></dxi-column> |
|||
</dx-data-grid> |
|||
</ng-container> |
|||
``` |
|||
|
|||
which will display following table on the screen |
|||
|
|||
 |
|||
|
|||
We can also utilize `abpLocalization` pipe to translate the headers of the table. To use `abpLocalization` pipe in our templates, we need to import `CoreModule` from `@abp/ng.core` into our module. |
|||
|
|||
```typescript |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
|
|||
@NgModule({ |
|||
// ... |
|||
imports: [ |
|||
// ... |
|||
CoreModule |
|||
], |
|||
}) |
|||
export class DevExtremeModule {} |
|||
``` |
|||
|
|||
And change the template to the following: |
|||
|
|||
```html |
|||
<ng-container *ngIf="service.users$ | async as users"> |
|||
<dx-data-grid [dataSource]="users"> |
|||
<dxi-column |
|||
dataField="userName" |
|||
[caption]="'AbpIdentity::DisplayName:UserName' | abpLocalization" |
|||
></dxi-column> |
|||
<dxi-column |
|||
dataField="name" |
|||
[caption]="'AbpIdentity::DisplayName:Name' | abpLocalization" |
|||
></dxi-column> |
|||
<dxi-column |
|||
dataField="surname" |
|||
[caption]="'AbpIdentity::DisplayName:Surname' | abpLocalization" |
|||
></dxi-column> |
|||
<dxi-column |
|||
dataField="email" |
|||
[caption]="'AbpIdentity::DisplayName:Email' | abpLocalization" |
|||
></dxi-column> |
|||
<dxi-column |
|||
dataField="phoneNumber" |
|||
[caption]="'AbpIdentity::DisplayName:PhoneNumber' | abpLocalization" |
|||
></dxi-column> |
|||
</dx-data-grid> |
|||
</ng-container> |
|||
``` |
|||
|
|||
The headers should change when a new language is selected; |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, we have seen how to integrate `DevExtreme` angular components into a project generated by `ABP CLI`. |
|||
|
|||
You can download source code of [the demo here](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Angular). |
|||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 828 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,43 @@ |
|||
# Redis Cache |
|||
|
|||
ABP Framework [Caching System](Caching.md) extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). So, **any provider** supported by the standard ASP.NET Core distributed cache can be usable in your application and can be configured just like **documented by Microsoft**. |
|||
|
|||
However, ABP provides an **integration package** for Redis Cache: [Volo.Abp.Caching.StackExchangeRedis](https://www.nuget.org/packages/Volo.Abp.Caching.StackExchangeRedis). There are two reasons for using this package, instead of the standard [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis/) package. |
|||
|
|||
1. It implements `SetManyAsync` and `GetManyAsync` methods. These are not standard methods of the Microsoft Caching library, but added by the ABP Framework [Caching](Caching.md) system. They **significiantly increases the performance** when you need to set/get multiple cache items with a single method call. |
|||
2. It **simplifies** the Redis cache **configuration** (will be explained below). |
|||
|
|||
> Volo.Abp.Caching.StackExchangeRedis is already uses the Microsoft.Extensions.Caching.StackExchangeRedis package, but extends and improves it. |
|||
|
|||
## Installation |
|||
|
|||
> This package is already installed in the application startup template if it is using Redis. |
|||
|
|||
Open a command line in the folder of your `.csproj` file and type the following ABP CLI command: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.Caching.StackExchangeRedis |
|||
```` |
|||
|
|||
## Configuration |
|||
|
|||
Volo.Abp.Caching.StackExchangeRedis package automatically gets the redis [configuration](Configuration.md) from the `IConfiguration`. So, for example, you can set your configuration inside the `appsettings.json`: |
|||
|
|||
````js |
|||
"Redis": { |
|||
"Configuration": "127.0.0.1" |
|||
} |
|||
```` |
|||
|
|||
Alternatively you can configure the standard [RedisCacheOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscacheoptions) [options](Options.md) class in the `ConfigureServices` method of your [module](Module-Development-Basics.md): |
|||
|
|||
````csharp |
|||
Configure<RedisCacheOptions>(options => |
|||
{ |
|||
//... |
|||
}); |
|||
```` |
|||
|
|||
## See Also |
|||
|
|||
* [Caching](Caching.md) |
|||
@ -0,0 +1,98 @@ |
|||
# About Feature Libraries |
|||
|
|||
ABP has an ever-growing number of feature modules and [introducing a new one](../../Module-Development-Basics.md) is always possible. When the UI is Angular, these features have modular Angular libraries accompanying them. |
|||
|
|||
## Feature Library Content |
|||
|
|||
Each library has at least two modules: |
|||
|
|||
1. The main module contains all components, services, types, enums, etc. to deliver the required UI when the feature is loaded. From here on, we will refer to these modules as **"feature module"**. |
|||
2. There is also a **"config module"** per library which helps us configure applications to run these modules or make them accessible. |
|||
|
|||
## How to Add a Feature Library to Your Project |
|||
|
|||
<!-- TODO: Insert info on CLI `add-module` command here when the schematic is ready. --> |
|||
|
|||
The manual setup of a feature library has three steps: |
|||
|
|||
### 1. Install the Library |
|||
|
|||
Feature libraries are usually published as an npm package. If a library you want to use does not exist in your project, you may install it via the following command: |
|||
|
|||
```shell |
|||
yarn add @my-company-name/my-project-name |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
```shell |
|||
npm install @my-company-name/my-project-name |
|||
``` |
|||
|
|||
The `my-company-name` and `my-project-name` parts are going to change according to the package you want to use. For example, if we want to install the ABP Identity module, the package installation will be as seen below: |
|||
|
|||
```shell |
|||
yarn add @abp/ng.identity |
|||
``` |
|||
|
|||
> Identity is used just as an example. If you have initiated your project with ABP CLI or ABP Suite, the identity library will already be installed and configured in your project. |
|||
|
|||
### 2. Import the Config Module |
|||
|
|||
As of ABP v3.0, every lazy-loaded module has a config module available via a secondary entry point on the same package. Importing them in your root module looks like this: |
|||
|
|||
```js |
|||
import { IdentityConfigModule } from "@abp/ng.identity/config"; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
IdentityConfigModule.forRoot(), |
|||
], |
|||
// providers, declarations, and bootstrap |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
We need the config modules for actions required before feature modules are loaded (lazily). For example, the above import configures the menu to display links to identity pages. |
|||
|
|||
Furthermore, depending on the library, the `.forRoot` static method may receive some options that configure how the feature works. |
|||
|
|||
### 3. Import the Feature Module |
|||
|
|||
Finally, the feature module should be [loaded lazily via Angular router](https://angular.io/guide/lazy-loading-ngmodules). If you open the `/src/app/app-routing.module.ts` file, you should see `IdentityModule` is loaded exactly as follows: |
|||
|
|||
```js |
|||
import { NgModule } from "@angular/core"; |
|||
import { RouterModule, Routes } from "@angular/router"; |
|||
|
|||
const routes: Routes = [ |
|||
// other routes |
|||
{ |
|||
path: "identity", |
|||
loadChildren: () => |
|||
import("@abp/ng.identity").then((m) => m.IdentityModule.forLazy()), |
|||
}, |
|||
// other routes |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forRoot(routes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class AppRoutingModule {} |
|||
``` |
|||
|
|||
When you load the identity feature like this, the "Users" page, for example, will have a route path of `/identity/users`. <sup id="a-modify-route">[1](#f-modify-route)</sup> |
|||
|
|||
Depending on the library, the `.forLazy` static method may also receive some options that configure how the feature works. |
|||
|
|||
--- |
|||
|
|||
<sup id="f-modify-route"><b>1</b></sup> _Libraries expect to work at a predefined path. Please check [how to patch a navigation element](./Modifying-the-Menu.md#how-to-patch-or-remove-a-navigation-element), if you want to use a different path from the default one (e.g. '/identity')._ <sup>[↩](#a-modify-route)</sup> |
|||
|
|||
--- |
|||
|
|||
## What's Next? |
|||
|
|||
- [Service Proxies](./Service-Proxies.md) |
|||
@ -0,0 +1,346 @@ |
|||
# PWA Configuration |
|||
|
|||
[Progressive Web Apps](https://web.dev/progressive-web-apps/) are web applications which, although not as integrated to the OS as a native app, can take advantage of native features. They can be discovered via search engines, installed on devices with a single tap or click, and shared via a regular link. They also can work offline and get updates when new content is available. |
|||
|
|||
Converting your Angular application to a PWA is easy. |
|||
|
|||
## 1. Install Angular PWA |
|||
|
|||
Run the following command in the root folder of your Angular application: |
|||
|
|||
```shell |
|||
yarn ng add @angular/pwa |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
```shell |
|||
npm run ng add @angular/pwa |
|||
``` |
|||
|
|||
This will install the `@angular/service-worker` package and make your default app a PWA. Alternatively, you may add `project` parameter to target a specific app in your workspace: |
|||
|
|||
```shell |
|||
yarn ng add @angular/pwa --project MyProjectName |
|||
``` |
|||
|
|||
Here is the output of the command: |
|||
|
|||
<img alt="Angular PWA updates and creates files" src="./images/pwa-configuration-ng-add.png" width="400px" style="max-width:100%"> |
|||
|
|||
So, Angular CLI updates some files and add a few others: |
|||
|
|||
- **ngsw-config.json** is where the [service worker configuration](https://angular.io/guide/service-worker-config) is placed. Not all PWAs have this file. It is specific to Angular. |
|||
- **manifest.webmanifest** is a [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) and provides information about your app in JSON format. |
|||
- **icons** are placeholder icons that are referred to in your web app manifest. We will replace these in a minute. |
|||
- **angular.json** has following modifications: |
|||
- `assets` include _manifest.webmanifest_. |
|||
- `serviceWorker` is `true` in production build. |
|||
- `ngswConfigPath` refers to _ngsw-config.json_. |
|||
- **package.json** has _@angular/service-worker_ as a new dependency. |
|||
- **app.module.ts** imports `ServiceWorkerModule` and registers a service worker filename. |
|||
- **index.html** has following modifications: |
|||
- A `<link>` element that refers to _manifest.webmanifest_. |
|||
- A `<meta>` tag that sets a theme color. |
|||
|
|||
## 2. Update the Web App Manifest |
|||
|
|||
### 2.1. Set the Name of Your App |
|||
|
|||
The `name` and the `short_name` properties in the generated manifest are derived from your project name. Let's change them. |
|||
|
|||
Open the _manifest.webmanifest_ file and update `name` and `short_name` props: |
|||
|
|||
```json |
|||
{ |
|||
/* rest of the manifest meta data */ |
|||
"short_name": "My Project", |
|||
"name": "My Project: My Catch-Phrase" |
|||
} |
|||
``` |
|||
|
|||
The short name must be really short because it will be displayed on anywhere with limited space, like the launcher and the home screen. |
|||
|
|||
### 2.2. Add a Description |
|||
|
|||
The `@angular/pwa` schematic we just added does not insert a description to your manifest file, but, according to [web app manifest standards](https://www.w3.org/TR/appmanifest/#description-member), you should. |
|||
|
|||
So, open the _manifest.webmanifest_ file and place the description as seen below: |
|||
|
|||
```json |
|||
{ |
|||
/* rest of the manifest meta data */ |
|||
"description": "My short project description giving a slightly better idea about my app" |
|||
} |
|||
``` |
|||
|
|||
As a bonus, providing a description [along with other criteria](https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-edgehtml/microsoft-store#criteria-for-automatic-submission) helps Bing web crawler to index your app and automatically submit your app to Microsoft Store in `.appx` format. |
|||
|
|||
### 2.3. Set App Colors |
|||
|
|||
Angular generates the manifest file with a default `theme_color` and `background_color`. Change these according to your brand identity: |
|||
|
|||
Open the _manifest.webmanifest_ file and update `theme_color` and `background_color` properties: |
|||
|
|||
```json |
|||
{ |
|||
/* rest of the manifest meta data */ |
|||
"theme_color": "#000000", |
|||
"background_color": "#ffffff" |
|||
} |
|||
``` |
|||
|
|||
Then open _index.html_ and change the theme color meta tag as below: |
|||
|
|||
```html |
|||
<meta name="theme-color" content="#000000" /> |
|||
``` |
|||
|
|||
### 2.4. Replace App Icons & Add Splash Screens |
|||
|
|||
We need to update the icons and add some splash screens. This normally is time-consuming, but we will use the marvelous [pwa-asset-generator](https://github.com/onderceylan/pwa-asset-generator#readme) library. |
|||
|
|||
First, open the _manifest.webmanifest_ file and remove all elements in the `icons` property: |
|||
|
|||
```json |
|||
{ |
|||
/* rest of the manifest meta data */ |
|||
"icons": [] |
|||
} |
|||
``` |
|||
|
|||
Then, run the following command in your terminal (changing the path of course): |
|||
|
|||
```shell |
|||
npx pwa-asset-generator /path/to/your/logo.png ./src/assets/pwa -i ./src/index.html -m ./src/manifest.webmanifest |
|||
``` |
|||
|
|||
Open the _manifest.webmanifest_ file again. You will see this: |
|||
|
|||
```json |
|||
{ |
|||
/* rest of the manifest meta data */ |
|||
"icons": [ |
|||
{ |
|||
"src": "../manifest-icon-192.png", |
|||
"sizes": "192x192", |
|||
"type": "image/png", |
|||
"purpose": "maskable any" |
|||
}, |
|||
{ |
|||
"src": "../manifest-icon-512.png", |
|||
"sizes": "512x512", |
|||
"type": "image/png", |
|||
"purpose": "maskable any" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
In addition to updated icons, the library will generate splash screens. However, Apple requires all splash screens to be added in your _index.html_ and displays a blank screen at startup otherwise. So, the following tags will be inserted into the _index.html_ file: |
|||
|
|||
```html |
|||
<link |
|||
rel="apple-touch-icon" |
|||
sizes="180x180" |
|||
href="assets/pwa/apple-icon-180.jpg" |
|||
/> |
|||
<link |
|||
rel="apple-touch-icon" |
|||
sizes="167x167" |
|||
href="assets/pwa/apple-icon-167.jpg" |
|||
/> |
|||
<link |
|||
rel="apple-touch-icon" |
|||
sizes="152x152" |
|||
href="assets/pwa/apple-icon-152.jpg" |
|||
/> |
|||
<link |
|||
rel="apple-touch-icon" |
|||
sizes="120x120" |
|||
href="assets/pwa/apple-icon-120.jpg" |
|||
/> |
|||
|
|||
<meta name="apple-mobile-web-app-capable" content="yes" /> |
|||
|
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2048-2732.jpg" |
|||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2732-2048.jpg" |
|||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1668-2388.jpg" |
|||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2388-1668.jpg" |
|||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1536-2048.jpg" |
|||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2048-1536.jpg" |
|||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1668-2224.jpg" |
|||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2224-1668.jpg" |
|||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1620-2160.jpg" |
|||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2160-1620.jpg" |
|||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1242-2688.jpg" |
|||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2688-1242.jpg" |
|||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1125-2436.jpg" |
|||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-2436-1125.jpg" |
|||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-828-1792.jpg" |
|||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1792-828.jpg" |
|||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1080-1920.jpg" |
|||
media="(device-width: 360px) and (device-height: 640px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1920-1080.jpg" |
|||
media="(device-width: 360px) and (device-height: 640px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-750-1334.jpg" |
|||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1334-750.jpg" |
|||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-640-1136.jpg" |
|||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
|||
/> |
|||
<link |
|||
rel="apple-touch-startup-image" |
|||
href="assets/pwa/apple-splash-1136-640.jpg" |
|||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
|||
/> |
|||
``` |
|||
|
|||
## 3. Configure Service Worker |
|||
|
|||
### 3.1 Modify Asset Groups |
|||
|
|||
Angular has defined some static files to be cached by the service worker, but they are not 100% accurate. Let's change it. |
|||
|
|||
Open _ngsw-config.json_ file and replace its content with this: |
|||
|
|||
```json |
|||
{ |
|||
"$schema": "./node_modules/@angular/service-worker/config/schema.json", |
|||
"index": "/index.html", |
|||
"assetGroups": [ |
|||
{ |
|||
"name": "app", |
|||
"installMode": "prefetch", |
|||
"resources": { |
|||
"files": [ |
|||
"/favicon.ico", |
|||
"/index.html", |
|||
"/manifest.webmanifest", |
|||
"/*.css", |
|||
"/common-es2015.*.js", |
|||
"/main-es2015.*.js", |
|||
"/polyfills-es2015.*.js", |
|||
"/runtime-es2015.*.js", |
|||
"/vendor-es2015.*.js" |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"name": "modules", |
|||
"installMode": "lazy", |
|||
"updateMode": "prefetch", |
|||
"resources": { |
|||
"files": [ |
|||
"/*-es2015.*.js", |
|||
"!/common-es2015.*.js", |
|||
"!/main-es2015.*.js", |
|||
"!/polyfills-es2015.*.js", |
|||
"!/runtime-es2015.*.js", |
|||
"!/vendor-es2015.*.js" |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"name": "assets", |
|||
"installMode": "lazy", |
|||
"updateMode": "prefetch", |
|||
"resources": { |
|||
"files": [ |
|||
"/assets/**", |
|||
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" |
|||
] |
|||
} |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
In case you want to cache other static files, please refer to the [service worker configuration document](https://angular.io/guide/service-worker-config#assetgroups) on Angular.io. |
|||
|
|||
### 3.2 Set Data Groups |
|||
|
|||
This part is unique to your project. We recommend being very careful about which endpoints to cache. Please refer to [service worker configuration document](https://angular.io/guide/service-worker-config#datagroups) on Angular.io for details. |
|||
|
|||
## What's Next? |
|||
|
|||
- [Config State](./Config-State.md) |
|||
|
After Width: | Height: | Size: 247 KiB |
@ -0,0 +1,206 @@ |
|||
# ASP.NET Core MVC / Razor Pages: Forms & Validation |
|||
|
|||
ABP Framework provides infrastructure and conventions to make easier to create forms, localize display names for the form elements and handle server & client side validation; |
|||
|
|||
* [abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper automates **creating a complete form** from a C# model class: Creates the input elements, handles localization and client side validation. |
|||
* [ABP Form tag helpers](Tag-Helpers/Form-elements.md) (`abp-input`, `abp-select`, `abp-radio`...) render **a single form element** with handling localization and client side validation. |
|||
* ABP Framework automatically **localizes the display name** of a form element without needing to add a `[DisplayName]` attribute. |
|||
* **Validation errors** are automatically localized based on the user culture. |
|||
|
|||
> This document is for the **client side validation** and it doesn't cover the server side validation. Check the [validation document](../../Validation.md) for server side validation infrastructure. |
|||
|
|||
## The Classic Way |
|||
|
|||
In a typical Bootstrap based ASP.NET Core MVC / Razor Pages UI, you [need to write](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation#client-side-validation) such a boilerplate code to create a simple form element: |
|||
|
|||
````html |
|||
<div class="form-group"> |
|||
<label asp-for="Movie.ReleaseDate" class="control-label"></label> |
|||
<input asp-for="Movie.ReleaseDate" class="form-control" /> |
|||
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span> |
|||
</div> |
|||
```` |
|||
|
|||
You can continue to use this approach if you need or prefer it. However, ABP Form tag helpers can produce the same output with a minimal code. |
|||
|
|||
## ABP Dynamic Forms |
|||
|
|||
[abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper completely automates the form creation. Take this model class as an example: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; |
|||
|
|||
namespace MyProject.Web.Pages |
|||
{ |
|||
public class MovieViewModel |
|||
{ |
|||
[Required] |
|||
[StringLength(256)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
[DataType(DataType.Date)] |
|||
public DateTime ReleaseDate { get; set; } |
|||
|
|||
[Required] |
|||
[TextArea] |
|||
[StringLength(1000)] |
|||
public string Description { get; set; } |
|||
|
|||
public Genre Genre { get; set; } |
|||
|
|||
public float? Price { get; set; } |
|||
|
|||
public bool PreOrder { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
It uses the data annotation attributes to define validation rules and UI styles for the properties. `Genre`, is an `enum` in this example: |
|||
|
|||
````csharp |
|||
namespace MyProject.Web.Pages |
|||
{ |
|||
public enum Genre |
|||
{ |
|||
Classic, |
|||
Action, |
|||
Fiction, |
|||
Fantasy, |
|||
Animation |
|||
} |
|||
} |
|||
```` |
|||
|
|||
In order to create the form in a razor page, create a property in your `PageModel` class: |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
|
|||
namespace MyProject.Web.Pages |
|||
{ |
|||
public class CreateMovieModel : PageModel |
|||
{ |
|||
[BindProperty] |
|||
public MovieViewModel Movie { get; set; } |
|||
|
|||
public void OnGet() |
|||
{ |
|||
Movie = new MovieViewModel(); |
|||
} |
|||
|
|||
public async Task OnPostAsync() |
|||
{ |
|||
if (ModelState.IsValid) |
|||
{ |
|||
//TODO: Save the Movie |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Then you can render the form in the `.cshtml` file: |
|||
|
|||
```html |
|||
@page |
|||
@model MyProject.Web.Pages.CreateMovieModel |
|||
|
|||
<h2>Create a new Movie</h2> |
|||
|
|||
<abp-dynamic-form abp-model="Movie" submit-button="true" /> |
|||
``` |
|||
|
|||
The result is shown below: |
|||
|
|||
 |
|||
|
|||
See the *Localization & Validation* section below to localize the field display names and see how the validation works. |
|||
|
|||
> See [its own document](Tag-Helpers/Dynamic-Forms.md) for all options of the `abp-dynamic-form` tag helper. |
|||
|
|||
## ABP Form Tag Helpers |
|||
|
|||
`abp-dynamic-form` covers most of the scenarios and allows you to control and customize the form using the attributes. |
|||
|
|||
However, if you want to **render the form body yourself** (for example, you may want to fully control the **form layout**), you can directly use the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md). The same auto-generated form above can be created using the ABP Form Tag Helpers as shown below: |
|||
|
|||
```html |
|||
@page |
|||
@model MyProject.Web.Pages.CreateMovieModel |
|||
|
|||
<h2>Create a new Movie</h2> |
|||
|
|||
<form method="post"> |
|||
<abp-input asp-for="Movie.Name"/> |
|||
<abp-input asp-for="Movie.ReleaseDate"/> |
|||
<abp-input asp-for="Movie.Description"/> |
|||
<abp-select asp-for="Movie.Genre"/> |
|||
<abp-input asp-for="Movie.Price"/> |
|||
<abp-input asp-for="Movie.PreOrder"/> |
|||
<abp-button button-type="Primary" type="submit">Save</abp-button> |
|||
</form> |
|||
``` |
|||
|
|||
> See the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md) document for details of these tag helpers and their options. |
|||
|
|||
## Validation & Localization |
|||
|
|||
Both of the Dynamic Form and the Form Tag Helpers **automatically validate** the input based on the data annotation attributes and shows validation error messages on the user interface. Error messages are **automatically localized** based on the current culture. |
|||
|
|||
**Example: User leaves empty a required string property** |
|||
|
|||
 |
|||
|
|||
The error message below is shown if the language is French: |
|||
|
|||
 |
|||
|
|||
Validation errors are already [translated](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization) a lot of languages. You can [contribute](../../Contribution/Index.md) to the translation for your own language or override the texts for your own application by following the [localization](../../Localization.md) documentation. |
|||
|
|||
## Display Name Localization |
|||
|
|||
ABP Framework uses the property name as the field name on the user interface. You typically want to [localize](../../Localization.md) this name based on the current culture. |
|||
|
|||
ABP Framework can conventionally localize the fields on the UI when you add the localization keys to the localization JSON files. |
|||
|
|||
Example: French localization for the *Name* property (add into the `fr.json` in the application): |
|||
|
|||
````js |
|||
"Name": "Nom" |
|||
```` |
|||
|
|||
Then the UI will use the given name for French language: |
|||
|
|||
 |
|||
|
|||
### Using the `DisplayName:` Prefix |
|||
|
|||
Directly using the property name as the localization key may be a problem if you need to use the property name for other purpose, which a different translation value. In this case, use the `DisplayName:` prefix for the localization key: |
|||
|
|||
````js |
|||
"DisplayName:Name": "Nom" |
|||
```` |
|||
|
|||
ABP prefers to use the `DisplayName:Name` key over the `Name` key if it does exists. |
|||
|
|||
### Using a Custom Localization Key |
|||
|
|||
If you need, you can use the `[DisplayName]` attribute to specify the localization key for a specific property: |
|||
|
|||
````csharp |
|||
[DisplayName("MyNameKey")] |
|||
public string Name { get; set; } |
|||
```` |
|||
|
|||
In this case, you can add an entry to the localization file using the key `MyNameKey`. |
|||
|
|||
> If you use the `[DisplayName]` but not add a corresponding entity to the localization file, then ABP Framework shows the given key as the field name, `MyNameKey` for this case. So, it provides a way to specify a hard coded display name even if you don't need to use the localization system. |
|||
|
|||
## See Also |
|||
|
|||
* [Server Side Validation](../../Validation.md) |
|||
@ -1,3 +1,205 @@ |
|||
# Navigation Menu |
|||
# ASP.NET Core MVC / Razor Pages UI: Navigation Menu |
|||
|
|||
Every application has a main menu to allow users to navigate to pages/screens of the application. Some applications may contain more than one menu in different sections of the UI. |
|||
|
|||
ABP Framework is a [modular](../../Module-Development-Basics.md) application development framework. **Every module may need to add items to the menu**. |
|||
|
|||
So, ABP Framework **provides a menu infrastructure** where; |
|||
|
|||
* The application or the modules can add items to a menu, without knowing how the menu is rendered. |
|||
* The [theme](Theming.md) properly renders the menu. |
|||
|
|||
## Adding Menu Items |
|||
|
|||
In order to add menu items (or manipulate the existing items) you need to create a class implementing the `IMenuContributor` interface. |
|||
|
|||
**Example: Add a *CRM* menu item with *Customers* and *Orders* sub menu items** |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using MyProject.Localization; |
|||
using Volo.Abp.UI.Navigation; |
|||
|
|||
namespace MyProject.Web.Menus |
|||
{ |
|||
public class MyProjectMenuContributor : IMenuContributor |
|||
{ |
|||
public async Task ConfigureMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
if (context.Menu.Name == StandardMenus.Main) |
|||
{ |
|||
await ConfigureMainMenuAsync(context); |
|||
} |
|||
} |
|||
|
|||
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
var l = context.GetLocalizer<MyProjectResource>(); |
|||
|
|||
context.Menu.AddItem( |
|||
new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"]) |
|||
.AddItem(new ApplicationMenuItem( |
|||
name: "MyProject.Crm.Customers", |
|||
displayName: l["Menu:Customers"], |
|||
url: "/crm/customers") |
|||
).AddItem(new ApplicationMenuItem( |
|||
name: "MyProject.Crm.Orders", |
|||
displayName: l["Menu:Orders"], |
|||
url: "/crm/orders") |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* This example adds items only to the main menu (`StandardMenus.Main`: see the *Standard Menus* section below). |
|||
* It gets a `IStringLocalizer` from `context` to [localize](../../Localization.md) the display names of the menu items. |
|||
* Adds the Customers and Orders as children of the CRM menu. |
|||
|
|||
Once you create a menu contributor, you need to add it to the `AbpNavigationOptions` in the `ConfigureServices` method of your module: |
|||
|
|||
````csharp |
|||
Configure<AbpNavigationOptions>(options => |
|||
{ |
|||
options.MenuContributors.Add(new MyProjectMenuContributor()); |
|||
}); |
|||
```` |
|||
|
|||
When you run the application, you will see the menu items added to the main menu: |
|||
|
|||
 |
|||
|
|||
> The menu is rendered by the current UI theme. So, the look of the main menu can be completely different based on your theme. |
|||
|
|||
Here, a few notes on the menu contributors; |
|||
|
|||
* ABP Framework calls the `ConfigureMenuAsync` method **whenever need to render** the menu. |
|||
* Every menu item can have **children**. So, you can add menu items with **unlimited depth** (however, your UI theme may not support unlimited depth). |
|||
* Only leaf menu items have `url`s normally. When you click to a parent menu, its sub menu is opened or closed, you don't navigate the `url` of a parent menu item. |
|||
* If a menu item has no children and has no `url` defined, then it is not rendered on the UI. This simplifies to authorize the menu items: You only authorize the child items (see the next section). If none of the children are authorized, then the parent automatically disappears. |
|||
|
|||
### Menu Item Properties |
|||
|
|||
There are more options of a menu item (the constructor of the `ApplicationMenuItem` class). Here, the list of all available options; |
|||
|
|||
* `name` (`string`, required): The unique name of the menu item. |
|||
* `displayName` (`string`, required): Display name/text of the menu item. You can [localize](../../Localization.md) this as shown before. |
|||
* `url` (`string`): The URL of the menu item. |
|||
* `icon` (`string`): An icon name. Free [Font Awesome](https://fontawesome.com/) icon classes are supported out of the box. Example: `fa fa-book`. You can use any CSS font icon class as long as you include the necessary CSS files to your application. |
|||
* `order` (`int`): The order of the menu item. Default value is `1000`. Items are sorted by the adding order unless you specify an order value. |
|||
* `customData` (`object`): A custom object that you can associate to the menu item and use it while rendering the menu item. |
|||
* `target` (`string`): Target of the menu item. Can be `null` (default), "_blank", "_*self*", "_parent", "_*top*" or a frame name for web applications. |
|||
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute. |
|||
* `cssClass` (`string`): Additional string classes for the menu item. |
|||
|
|||
### Authorization |
|||
|
|||
As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source. |
|||
|
|||
One use case is the [authorization](Authorization.md). You typically want to add menu items by checking a permission. |
|||
|
|||
**Example: Check if the current user has a permission** |
|||
|
|||
````csharp |
|||
if (await context.IsGrantedAsync("MyPermissionName")) |
|||
{ |
|||
//...add menu items |
|||
} |
|||
```` |
|||
|
|||
> You can use `context.AuthorizationService` to directly access to the `IAuthorizationService`. |
|||
|
|||
### Resolving Dependencies |
|||
|
|||
`context.ServiceProvider` can be used to resolve any service dependency. |
|||
|
|||
**Example: Get a service** |
|||
|
|||
````csharp |
|||
var myService = context.ServiceProvider.GetRequiredService<IMyService>(); |
|||
//...use the service |
|||
```` |
|||
|
|||
> You don't need to care about releasing/disposing services. ABP Framework handles it. |
|||
|
|||
### The Administration Menu |
|||
|
|||
There is a special menu item in the menu menu that is added by the ABP Framework: The *Administration* menu. It is typically used by the pre-built admin [application modules](../../Modules/Index.md): |
|||
|
|||
 |
|||
|
|||
If you want to add menu items under the *Administration* menu item, you can use the `context.Menu.GetAdministration()` extension method: |
|||
|
|||
````csharp |
|||
context.Menu.GetAdministration().AddItem(...) |
|||
```` |
|||
|
|||
### Manipulating the Existing Menu Items |
|||
|
|||
ABP Framework executes the menu contributors by the [module dependency order](../../Module-Development-Basics.md). So, you can manipulate the menu items that your application or module (directly or indirectly) depends on. |
|||
|
|||
**Example: Set an icon for the `Users` menu item added by the [Identity Module](../../Modules/Identity.md)** |
|||
|
|||
````csharp |
|||
var userMenu = context.Menu.FindMenuItem(IdentityMenuNames.Users); |
|||
userMenu.Icon = "fa fa-users"; |
|||
```` |
|||
|
|||
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors. |
|||
|
|||
## Standard Menus |
|||
|
|||
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus: |
|||
|
|||
* `Main`: The main menu of the application. Contains links to the page of the application. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.Main`. |
|||
* `User`: User profile menu. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.User`. |
|||
|
|||
The `Main` menu already covered above. The `User` menu is available when a user has logged in: |
|||
|
|||
 |
|||
|
|||
You can add items to the `User` menu by checking the `context.Menu.Name` as shown below: |
|||
|
|||
```csharp |
|||
if (context.Menu.Name == StandardMenus.User) |
|||
{ |
|||
//...add items |
|||
} |
|||
``` |
|||
|
|||
## IMenuManager |
|||
|
|||
`IMenuManager` is generally used by the UI [theme](Theming.md) to render the menu items on the UI. So, **you generally don't need to directly use** the `IMenuManager`. |
|||
|
|||
**Example: Getting the `Main` menu items** |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
using Volo.Abp.UI.Navigation; |
|||
|
|||
namespace MyProject.Web.Pages |
|||
{ |
|||
public class IndexModel : PageModel |
|||
{ |
|||
private readonly IMenuManager _menuManager; |
|||
|
|||
public IndexModel(IMenuManager menuManager) |
|||
{ |
|||
_menuManager = menuManager; |
|||
} |
|||
|
|||
public async Task OnGetAsync() |
|||
{ |
|||
var mainMenu = await _menuManager.GetAsync(StandardMenus.Main); |
|||
|
|||
foreach (var menuItem in mainMenu.Items) |
|||
{ |
|||
//... |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
TODO |
|||
@ -0,0 +1,6 @@ |
|||
# Blazor UI for the ABP Framework |
|||
|
|||
The detailed documentation for the Blazor UI is in progress. However, you can follow the documents below to start with the Blazor UI today. |
|||
|
|||
* [Get started](https://docs.abp.io/en/abp/latest/Getting-Started?UI=Blazor) with the Blazor UI for the ABP Framework. |
|||
* [Web Application Development Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=Blazor) with the Blazor UI. |
|||
@ -0,0 +1,3 @@ |
|||
# Blazor UI Notification |
|||
|
|||
`UiNotificationService` is used to show toastr style notifications on the user interface. The documentation is in progress... |
|||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 948 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
@ -0,0 +1,20 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic; |
|||
using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme |
|||
{ |
|||
public class BasicThemeToolbarContributor : IToolbarContributor |
|||
{ |
|||
public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) |
|||
{ |
|||
if (context.Toolbar.Name == StandardToolbars.Main) |
|||
{ |
|||
context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitch))); |
|||
context.Toolbar.Items.Add(new ToolbarItem(typeof(LoginDisplay))); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||
@ -1,18 +1,50 @@ |
|||
using System.Threading.Tasks; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Localization.Resources.AbpUi; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.AspNetCore.Components.Routing; |
|||
using Microsoft.Extensions.Localization; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Http.Client; |
|||
using Volo.Abp.UI.Navigation; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic |
|||
{ |
|||
public partial class LoginDisplay |
|||
public partial class LoginDisplay : IDisposable |
|||
{ |
|||
[Inject] protected IMenuManager MenuManager { get; set; } |
|||
|
|||
[Inject] |
|||
protected IMenuManager MenuManager { get; set; } |
|||
|
|||
[Inject] |
|||
protected IStringLocalizer<AbpUiResource> UiLocalizer { get; set; } |
|||
|
|||
[Inject] |
|||
protected IOptions<AbpRemoteServiceOptions> RemoteServiceOptions { get; set; } |
|||
|
|||
protected ApplicationMenu Menu { get; set; } |
|||
|
|||
protected string ServerUrl { get; set; } |
|||
protected string ServerAccountUrl { get; set; } |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
Menu = await MenuManager.GetAsync(StandardMenus.User); |
|||
|
|||
ServerUrl = RemoteServiceOptions.Value.RemoteServices.Default?.BaseUrl?.TrimEnd('/'); |
|||
ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; |
|||
|
|||
Navigation.LocationChanged += OnLocationChanged; |
|||
} |
|||
|
|||
protected virtual void OnLocationChanged(object sender, LocationChangedEventArgs e) |
|||
{ |
|||
ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; |
|||
StateHasChanged(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Navigation.LocationChanged -= OnLocationChanged; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
<ul class="navbar-nav"> |
|||
@foreach (var render in ToolbarItemRenders) |
|||
{ |
|||
<li class="nav-item"> |
|||
@render |
|||
</li> |
|||
} |
|||
</ul> |
|||
@ -0,0 +1,32 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic |
|||
{ |
|||
public partial class NavToolbar |
|||
{ |
|||
[Inject] |
|||
private IToolbarManager ToolbarManager { get; set; } |
|||
|
|||
private List<RenderFragment> ToolbarItemRenders { get; set; } = new List<RenderFragment>(); |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
var toolbar = await ToolbarManager.GetAsync(StandardToolbars.Main); |
|||
|
|||
ToolbarItemRenders.Clear(); |
|||
|
|||
foreach (var item in toolbar.Items) |
|||
{ |
|||
ToolbarItemRenders.Add(builder => |
|||
{ |
|||
builder.OpenComponent(0, item.ComponentType); |
|||
builder.CloseComponent(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.JSInterop; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming |
|||
{ |
|||
public class AbpUtilsService : IAbpUtilsService, ITransientDependency |
|||
{ |
|||
protected IJSRuntime JsRuntime { get; } |
|||
|
|||
public AbpUtilsService(IJSRuntime jsRuntime) |
|||
{ |
|||
JsRuntime = jsRuntime; |
|||
} |
|||
|
|||
public ValueTask AddClassToTagAsync(string tagName, string className) |
|||
{ |
|||
return JsRuntime.InvokeVoidAsync("abp.utils.addClassToTag", tagName, className); |
|||
} |
|||
|
|||
public ValueTask RemoveClassFromTagAsync(string tagName, string className) |
|||
{ |
|||
return JsRuntime.InvokeVoidAsync("abp.utils.removeClassFromTag", tagName, className); |
|||
} |
|||
|
|||
public ValueTask<bool> HasClassOnTagAsync(string tagName, string className) |
|||
{ |
|||
return JsRuntime.InvokeAsync<bool>("abp.utils.hasClassOnTag", tagName, className); |
|||
} |
|||
|
|||
public ValueTask ReplaceLinkHrefByIdAsync(string linkId, string hrefValue) |
|||
{ |
|||
return JsRuntime.InvokeVoidAsync("abp.utils.replaceLinkHrefById", linkId, hrefValue); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming |
|||
{ |
|||
public interface IAbpUtilsService |
|||
{ |
|||
ValueTask AddClassToTagAsync(string tagName, string className); |
|||
|
|||
ValueTask RemoveClassFromTagAsync(string tagName, string className); |
|||
|
|||
ValueTask<bool> HasClassOnTagAsync(string tagName, string className); |
|||
|
|||
ValueTask ReplaceLinkHrefByIdAsync(string linkId, string hrefValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public class AbpToolbarOptions |
|||
{ |
|||
[NotNull] |
|||
public List<IToolbarContributor> Contributors { get; } |
|||
|
|||
public AbpToolbarOptions() |
|||
{ |
|||
Contributors = new List<IToolbarContributor>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.Extensions.Localization; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public interface IToolbarConfigurationContext : IServiceProviderAccessor |
|||
{ |
|||
Toolbar Toolbar { get; } |
|||
|
|||
IAuthorizationService AuthorizationService { get; } |
|||
|
|||
IStringLocalizerFactory StringLocalizerFactory { get; } |
|||
|
|||
Task<bool> IsGrantedAsync(string policyName); |
|||
|
|||
[CanBeNull] |
|||
IStringLocalizer GetDefaultLocalizer(); |
|||
|
|||
[NotNull] |
|||
public IStringLocalizer GetLocalizer<T>(); |
|||
|
|||
[NotNull] |
|||
public IStringLocalizer GetLocalizer(Type resourceType); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public interface IToolbarContributor |
|||
{ |
|||
Task ConfigureToolbarAsync(IToolbarConfigurationContext context); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public interface IToolbarManager |
|||
{ |
|||
Task<Toolbar> GetAsync(string name); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public static class StandardToolbars |
|||
{ |
|||
public const string Main = "Main"; |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public class Toolbar |
|||
{ |
|||
public string Name { get; } |
|||
|
|||
public List<ToolbarItem> Items { get; } |
|||
|
|||
public Toolbar([NotNull] string name) |
|||
{ |
|||
Name = Check.NotNull(name, nameof(name)); |
|||
Items = new List<ToolbarItem>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public class ToolbarConfigurationContext : IToolbarConfigurationContext |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; } |
|||
private readonly object _serviceProviderLock = new object(); |
|||
|
|||
private TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
lock (_serviceProviderLock) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
reference = (TRef)ServiceProvider.GetRequiredService(serviceType); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return reference; |
|||
} |
|||
|
|||
public IAuthorizationService AuthorizationService => LazyGetRequiredService(typeof(IAuthorizationService), ref _authorizationService); |
|||
private IAuthorizationService _authorizationService; |
|||
|
|||
private IStringLocalizerFactory _stringLocalizerFactory; |
|||
public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(typeof(IStringLocalizerFactory),ref _stringLocalizerFactory); |
|||
|
|||
public Toolbar Toolbar { get; } |
|||
|
|||
public ToolbarConfigurationContext(Toolbar toolbar, IServiceProvider serviceProvider) |
|||
{ |
|||
Toolbar = toolbar; |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
|
|||
public Task<bool> IsGrantedAsync(string policyName) |
|||
{ |
|||
return AuthorizationService.IsGrantedAsync(policyName); |
|||
} |
|||
|
|||
[CanBeNull] |
|||
public IStringLocalizer GetDefaultLocalizer() |
|||
{ |
|||
return StringLocalizerFactory.CreateDefaultOrNull(); |
|||
} |
|||
|
|||
[NotNull] |
|||
public IStringLocalizer GetLocalizer<T>() |
|||
{ |
|||
return StringLocalizerFactory.Create<T>(); |
|||
} |
|||
|
|||
[NotNull] |
|||
public IStringLocalizer GetLocalizer(Type resourceType) |
|||
{ |
|||
return StringLocalizerFactory.Create(resourceType); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public class ToolbarItem |
|||
{ |
|||
public Type ComponentType |
|||
{ |
|||
get => _componentType; |
|||
set => _componentType = Check.NotNull(value, nameof(value)); |
|||
} |
|||
private Type _componentType; |
|||
|
|||
public int Order { get; set; } |
|||
|
|||
public ToolbarItem([NotNull] Type componentType, int order = 0) |
|||
{ |
|||
Order = order; |
|||
ComponentType = Check.NotNull(componentType, nameof(componentType)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars |
|||
{ |
|||
public class ToolbarManager : IToolbarManager, ITransientDependency |
|||
{ |
|||
protected AbpToolbarOptions Options { get; } |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public ToolbarManager( |
|||
IOptions<AbpToolbarOptions> options, |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public async Task<Toolbar> GetAsync(string name) |
|||
{ |
|||
var toolbar = new Toolbar(name); |
|||
|
|||
using (var scope = ServiceProvider.CreateScope()) |
|||
{ |
|||
var context = new ToolbarConfigurationContext(toolbar, scope.ServiceProvider); |
|||
|
|||
foreach (var contributor in Options.Contributors) |
|||
{ |
|||
await contributor.ConfigureToolbarAsync(context); |
|||
} |
|||
} |
|||
|
|||
return toolbar; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
var abp = abp || {}; |
|||
(function () { |
|||
abp.utils = abp.utils || {}; |
|||
|
|||
abp.utils.addClassToTag = function (tagName, className) { |
|||
var tags = document.getElementsByTagName(tagName); |
|||
for (var i = 0; i < tags.length; i++) { |
|||
tags[i].classList.add(className); |
|||
} |
|||
}; |
|||
|
|||
abp.utils.removeClassFromTag = function (tagName, className) { |
|||
var tags = document.getElementsByTagName(tagName); |
|||
for (var i = 0; i < tags.length; i++) { |
|||
tags[i].classList.remove(className); |
|||
} |
|||
}; |
|||
|
|||
abp.utils.hasClassOnTag = function (tagName, className) { |
|||
var tags = document.getElementsByTagName(tagName); |
|||
if (tags.length) { |
|||
return tags[0].classList.contains(className); |
|||
} |
|||
|
|||
return false; |
|||
}; |
|||
|
|||
abp.utils.replaceLinkHrefById = function (linkId, hrefValue) { |
|||
var link = document.getElementById(linkId); |
|||
|
|||
if (link && link.href !== hrefValue) { |
|||
link.href = hrefValue; |
|||
} |
|||
}; |
|||
})(); |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly |
|||
{ |
|||
public interface IUiNotificationService |
|||
{ |
|||
Task Info(string message); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly |
|||
{ |
|||
public class NullUiNotificationService : IUiNotificationService, ITransientDependency |
|||
{ |
|||
public Task Info(string message) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly |
|||
{ |
|||
[Dependency(ReplaceServices = true)] |
|||
public class WebAssemblyCurrentTenantAccessor : ICurrentTenantAccessor, ISingletonDependency |
|||
{ |
|||
public BasicTenantInfo Current { get; set; } |
|||
} |
|||
} |
|||