@ -1,3 +1,3 @@ |
|||
## Contribution |
|||
|
|||
See the [contribution guide](docs/en/Contribution/Index.md). |
|||
The contribution guide is available at [contribution guide](docs/en/contribution/index.md). |
|||
|
|||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 44 KiB |
@ -0,0 +1,143 @@ |
|||
# Entity Filters |
|||
|
|||
Every CRUD page includes some sort of inputs to filter the listed data. Some of the inputs are common among all of the entities like the `Search` box. In addition, every entity has its own advanced filters depending on its fields. To reduce the amount of code written on every CRUD page, the Angular UI of ABP Commercial introduces a new type of component called `abp-advanced-entity-filters` |
|||
|
|||
## Setup |
|||
|
|||
The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the entity filter components. |
|||
You can either import the `CommercialUiModule` which contains other components as well as `AdvancedEntityFilters` or you can directly import the `AdvancedEntityFiltersModule` if you do not need other components. Here is how you import them in your Angular module: |
|||
|
|||
```javascript |
|||
import { |
|||
CommercialUiModule, |
|||
AdvancedEntityFiltersModule, |
|||
} from "@volo/abp.commercial.ng.ui"; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
CommercialUiModule, |
|||
|
|||
// OR |
|||
|
|||
AdvancedEntityFiltersModule, |
|||
], |
|||
// rest of the module metadata |
|||
}) |
|||
export class YourModule {} |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
Let's take a look at the `Users` page from the `Identity` module. |
|||
|
|||
 |
|||
|
|||
As shown in the screenshot, `abp-advanced-entity-filters` usually contain two parts, an entity filter (common among entities), i.e. `abp-entity-filter`, and entity-specific filters which are encapsulated within the `abp-advanced-entity-filters-form` component. |
|||
|
|||
`users.component.html` |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
<abp-advanced-entity-filters-form> |
|||
<form #filterForm (keyup.enter)="list.get()"> |
|||
<div class="row"> |
|||
<!-- Form elements are omitted --> |
|||
|
|||
<div class="col-12 col-sm-auto align-self-end mb-3"> |
|||
<div class="row"> |
|||
<div class="col-6 col-sm-auto d-grid"> |
|||
<button |
|||
type="button" |
|||
class="btn btn-outline-primary" |
|||
(click)="clearFilters()" |
|||
> |
|||
<span>{%{{{ 'AbpUi::Clear' | abpLocalization }}}%}</span> |
|||
</button> |
|||
</div> |
|||
<div class="col-6 col-sm-auto d-grid"> |
|||
<button |
|||
type="button" |
|||
class="btn btn-primary" |
|||
(click)="list.get()" |
|||
> |
|||
<span>{%{{{ 'AbpUi::Refresh' | abpLocalization }}}%}</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</abp-advanced-entity-filters-form> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
The `abp-advanced-entity-filters` already contains the `abp-entity-filter` component so you do not need to pass it. However, the `abp-entity-filter` component needs an instance of `ListService` which is usually stored in the `list` field of the page. You can also change the placeholder of the component via `entityFilterPlaceholder` input which is passed into the `abpLocalization` pipe so that it uses the translated text. Default is `'AbpUi::PagerSearch'` |
|||
|
|||
E.g |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters |
|||
[list]="list" |
|||
entityFilterPlaceholder="AbpUi::PagerSearch" |
|||
> |
|||
<!-- ... --> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
### Inputs |
|||
|
|||
- `list`: an instance of `ListService` |
|||
- `entityFilterPlaceholder`: the placeholder of `abp-entity-filter` component. Default: `'AbpUi::PagerSearch'` |
|||
- `localizationSourceName`: the localization source of the current page. E.g: `AbpIdentity` |
|||
|
|||
### Inner components |
|||
|
|||
Some entities are simple and do not require any filter other than the `abp-entity-filter`. In this case, you can simply use the `abp-advanced-entity-filters` without anything in between. |
|||
|
|||
E.g. |
|||
|
|||
Let's remove `form` from the `Users` page |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
 |
|||
|
|||
If your component needs other filters, you can pass your own `form` within the `abp-advanced-entity-filters-form` component. This will render your form as well as a toggle (`abp-advanced-entity-filters-toggle`) to show and hide the form |
|||
|
|||
E.g. |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
<abp-advanced-entity-filters-form> |
|||
<form> |
|||
<!-- Content is omitted for sake of simplicity --> |
|||
</form> |
|||
</abp-advanced-entity-filters-form> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
 |
|||
|
|||
Last but not least, if you need to render some content above the `abp-entity-filter` component, you can use the `abp-advanced-entity-filters-above-search`. |
|||
|
|||
E.g. |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
<abp-advanced-entity-filters-above-search> |
|||
<h3>Custom Content above entity-filter</h3> |
|||
</abp-advanced-entity-filters-above-search> |
|||
|
|||
<abp-advanced-entity-filters-form> |
|||
<form> |
|||
<!-- Content is omitted for sake of simplicity --> |
|||
</form> |
|||
</abp-advanced-entity-filters-form> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
 |
|||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,98 @@ |
|||
# Lookup Components |
|||
|
|||
The Angular UI of ABP Commercial introduces some components with `abp-lookup-...` selector prefix. These components are used for retrieving relational entity data. |
|||
|
|||
## Setup |
|||
|
|||
The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the lookup components. Here is how you import them in your Angular module: |
|||
|
|||
```javascript |
|||
import { CommercialUiModule } from '@volo/abp.commercial.ng.ui'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
CommercialUiModule, |
|||
], |
|||
// rest of the module metadata |
|||
}) |
|||
export class YourModule {} |
|||
``` |
|||
|
|||
Now you can use the lookup components in your components declared by this module. |
|||
|
|||
## Lookup HTTP Requests |
|||
|
|||
The lookup requests are used by all lookup components to get the related entity records. Because of lexical this, _they must be arrow functions_. |
|||
|
|||
```javascript |
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class AuthorService { |
|||
getCountryLookup = (input: LookupRequestDto) => |
|||
this.restService.request<any, PagedResultDto<LookupDto<string>>>({ |
|||
method: 'GET', |
|||
url: '/api/app/authors/country-lookup', |
|||
params: { filter: input.filter, skipCount: input.skipCount, maxResultCount: input.maxResultCount }, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
// rest of the service is removed for brevity |
|||
} |
|||
``` |
|||
|
|||
## Lookup Typeahead Component |
|||
|
|||
Typeahead is a good choice when you have an unknown number of records for the related entity or you want to improve the UX with a search ability. Although not the best scenario, the country picker below shows how the lookup typeahead works: |
|||
|
|||
 |
|||
|
|||
Here is how it is used in the template. |
|||
|
|||
```html |
|||
<abp-lookup-typeahead |
|||
cid="author-country-id" |
|||
formControlName="countryId" |
|||
displayNameProp="name" |
|||
[editingData]="selected?.country" |
|||
[getFn]="service.getCountryLookup" |
|||
></abp-lookup-typeahead> |
|||
``` |
|||
|
|||
The available properties are as follows: |
|||
|
|||
- **cid:** The id of the form control (e.g. an input or a select element) inside the lookup component. Lets form controls respond to `<label>` events. |
|||
- **editingData:** The related entity data if a record is being updated. |
|||
- **displayNameProp:** The property of the updated record to use as a display name in the form control. |
|||
- **lookupNameProp:** The property of the entity to use as a display name in options. Should macth the lookup HTTP request interface. _(default: displayName)_ |
|||
- **lookupIdProp:** The property of the entity to use as the unique key in options. Should macth the lookup HTTP request interface. _(default: id)_ |
|||
- **maxResultCount:** The maximum number of options to display. _(default: 10)_ |
|||
- **getFn:** A function to get the related entity records with HTTP requests. Because of lexical this, _it must be a an arrow function_. |
|||
- **disabled:** This property lets you disable/enable a lookup component. _(default: false)_. |
|||
|
|||
## Lookup Select Component |
|||
|
|||
Select is a good choice when you have a low (and usually fixed) number of records for the related entity and search is not necessary. The country picker below shows how the lookup select works: |
|||
|
|||
 |
|||
|
|||
Here is how it is used in the template. |
|||
|
|||
```html |
|||
<abp-lookup-select |
|||
cid="author-country-id" |
|||
formControlName="countryId" |
|||
displayNameProp="name" |
|||
[getFn]="service.getCountryLookup" |
|||
></abp-lookup-select> |
|||
``` |
|||
|
|||
The available properties are as follows: |
|||
|
|||
- **cid:** The id of the form control (e.g. an input or a select element) inside the lookup component. Lets form controls respond to `<label>` events. |
|||
- **displayNameProp:** The property of the updated record to use as a display name in the form control. |
|||
- **lookupNameProp:** The property of the entity to use as a display name in options. Should macth the lookup HTTP request interface. _(default: displayName)_ |
|||
- **lookupIdProp:** The property of the entity to use as the unique key in options. Should macth the lookup HTTP request interface. _(default: id)_ |
|||
- **getFn:** A function to get the related entity records with HTTP requests. Because of lexical this, _it must be a an arrow function_. |
|||
- **disabled:** This property lets you disable/enable a lookup component. _(default: false)_. |
|||
@ -0,0 +1,77 @@ |
|||
# Manage Profile Page Tabs |
|||
|
|||
 |
|||
|
|||
The tabs in the manage profile page can be managed via `ManageProfileTabsService` which is exposed by the `@volo/abp.ng.account/public/config` package. You can add, remove, or edit a tab with using this service. |
|||
|
|||
See the example below, covers all features: |
|||
|
|||
```ts |
|||
// manage-profile-tabs.provider.ts |
|||
|
|||
import { APP_INITIALIZER, Component } from "@angular/core"; |
|||
import { TwoFactorTabComponent } from "@volo/abp.ng.account/public"; |
|||
import { |
|||
eAccountManageProfileTabNames, |
|||
ManageProfileTabsService, |
|||
} from "@volo/abp.ng.account/public/config"; |
|||
import { MyAwesomeTabComponent } from "./my-awesome-tab/my-awesome-tab.component"; |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: "abp-my-awesome-tab", |
|||
template: `My Awesome Tab`, |
|||
}) |
|||
class MyAwesomeTabComponent {} |
|||
|
|||
export const MANAGE_PROFILE_TAB_PROVIDER = { |
|||
provide: APP_INITIALIZER, |
|||
useFactory: configureManageProfileTabs, |
|||
deps: [ManageProfileTabsService], |
|||
multi: true, |
|||
}; |
|||
|
|||
export function configureManageProfileTabs(tabs: ManageProfileTabsService) { |
|||
return () => { |
|||
tabs.add([ |
|||
{ |
|||
name: "::MyAwesomeTab", // supports localization keys |
|||
order: 5, |
|||
component: MyAwesomeTabComponent, |
|||
}, |
|||
]); |
|||
|
|||
tabs.patch(eAccountManageProfileTabNames.TwoFactor, { |
|||
name: "Two factor authentication", |
|||
component: TwoFactorTabComponent, |
|||
}); |
|||
|
|||
tabs.remove([eAccountManageProfileTabNames.ProfilePicture]); |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
//app.module.ts |
|||
|
|||
import { MANAGE_PROFILE_TAB_PROVIDER } from "./manage-profile-tabs.provider"; |
|||
|
|||
@NgModule({ |
|||
providers: [MANAGE_PROFILE_TAB_PROVIDER], |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
What we have done above; |
|||
|
|||
- Created the `manage-profile-page-tabs.provider.ts`. |
|||
- Determined the `configureManageProfileTabs` function to perform manage profile tabs actions. |
|||
- Added a new tab labeled "My awesome tab". |
|||
- Renamed the "Two factor" tab label. |
|||
- Removed the "Profile picture" tab. |
|||
- Determined the `MANAGE_PROFILE_TAB_PROVIDER` to be able to run the `configureManageProfileTabs` function on initialization. |
|||
- Registered the `MANAGE_PROFILE_TAB_PROVIDER` to the `AppModule` providers. |
|||
|
|||
See the result: |
|||
|
|||
 |
|||
@ -0,0 +1,14 @@ |
|||
# Angular UI |
|||
|
|||
[Angular](https://angular.dev/) is a framework for building interactive, client-side web UIs using [TypeScript](https://www.typescriptlang.org) and [NodeJS](https://nodejs.org). |
|||
|
|||
ABP Angular provides the infrastructure to communicate with the ABP backend and offers utilities to simplify frontend development. We’ll explore and dive into the details under the following main topics: |
|||
|
|||
- Development |
|||
- Core Functionality |
|||
- Utilities |
|||
- Customization |
|||
- Components |
|||
|
|||
|
|||
You can also make a [Quick Start](./quick-start.md) |
|||
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 47 KiB |
@ -0,0 +1,96 @@ |
|||
// <auto-generated/>
|
|||
#pragma warning disable 1591
|
|||
namespace Volo.Abp.AspNetCore.RazorViews |
|||
{ |
|||
#line hidden
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
#nullable restore |
|||
#line 1 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
using System.Globalization; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
#nullable restore |
|||
#line 2 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
using Volo.Abp.AspNetCore.MultiTenancy.Views; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
#nullable restore |
|||
#line 3 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
using Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
internal class MultiTenancyMiddlewareErrorPage : AbpCompilationRazorPageBase |
|||
{ |
|||
#pragma warning disable 1998
|
|||
public async override global::System.Threading.Tasks.Task ExecuteAsync() |
|||
{ |
|||
#nullable restore |
|||
#line 5 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
|
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 404; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("\n"); |
|||
WriteLiteral("\n<html"); |
|||
BeginWriteAttribute("lang", " lang=\"", 453, "\"", 512, 1); |
|||
#nullable restore |
|||
#line 19 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
WriteAttributeValue("", 460, HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name), 460, 52, false); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
EndWriteAttribute(); |
|||
WriteLiteral(">\n <head>\n <meta charset=\"utf-8\" />\n <title>"); |
|||
#nullable restore |
|||
#line 22 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
Write(HtmlEncoder.Encode(Model.Message)); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("</title>\n </head>\n <body>\n <h3>"); |
|||
#nullable restore |
|||
#line 25 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
Write(HtmlEncoder.Encode(Model.Message)); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("</h3>\n <p>"); |
|||
#nullable restore |
|||
#line 26 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
Write(HtmlEncoder.Encode(Model.Details)); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("<p/>\n </body>\n</html>\n"); |
|||
} |
|||
#pragma warning restore 1998
|
|||
#nullable restore |
|||
#line 10 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
|
|||
public MultiTenancyMiddlewareErrorPage(MultiTenancyMiddlewareErrorPageModel model) |
|||
{ |
|||
Model = model; |
|||
} |
|||
|
|||
public MultiTenancyMiddlewareErrorPageModel Model { get; set; } |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
} |
|||
} |
|||
#pragma warning restore 1591
|
|||
@ -0,0 +1,28 @@ |
|||
@using System.Globalization |
|||
@using Volo.Abp.AspNetCore.MultiTenancy.Views |
|||
@using Volo.Abp.AspNetCore.RazorViews |
|||
@inherits AbpCompilationRazorPageBase |
|||
@{ |
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 404; |
|||
} |
|||
|
|||
@functions{ |
|||
public MultiTenancyMiddlewareErrorPage(MultiTenancyMiddlewareErrorPageModel model) |
|||
{ |
|||
Model = model; |
|||
} |
|||
|
|||
public MultiTenancyMiddlewareErrorPageModel Model { get; set; } |
|||
} |
|||
|
|||
<html lang="@HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name)"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<title>@HtmlEncoder.Encode(Model.Message)</title> |
|||
</head> |
|||
<body> |
|||
<h3>@HtmlEncoder.Encode(Model.Message)</h3> |
|||
<p>@HtmlEncoder.Encode(Model.Details)<p/> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,14 @@ |
|||
namespace Volo.Abp.AspNetCore.MultiTenancy.Views; |
|||
|
|||
public class MultiTenancyMiddlewareErrorPageModel |
|||
{ |
|||
public string Message { get; set; } |
|||
|
|||
public string Details { get; set; } |
|||
|
|||
public MultiTenancyMiddlewareErrorPageModel(string message, string details) |
|||
{ |
|||
Message = message; |
|||
Details = details; |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
// <auto-generated/>
|
|||
#pragma warning disable 1591
|
|||
namespace Volo.Abp.AspNetCore.RazorViews |
|||
{ |
|||
#line hidden
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
#nullable restore |
|||
#line 1 "AbpMvcLibsErrorPage.cshtml"
|
|||
using Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
internal class AbpMvcLibsErrorPage : AbpCompilationRazorPageBase |
|||
{ |
|||
#pragma warning disable 1998
|
|||
public async override global::System.Threading.Tasks.Task ExecuteAsync() |
|||
{ |
|||
#nullable restore |
|||
#line 3 "AbpMvcLibsErrorPage.cshtml"
|
|||
|
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 500; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral(@"
|
|||
<html> |
|||
<head> |
|||
<meta charset=""utf-8"" /> |
|||
<title>Error - The Libs folder is missing!</title> |
|||
</head> |
|||
<body> |
|||
<h1> ⚠️ The Libs folder under the <code style=""background-color: #e7e7e7;"">wwwroot/libs</code> directory is empty!</h1> |
|||
|
|||
<p>The Libs folder contains mandatory NPM Packages for running the project.</p> |
|||
|
|||
<p>Make sure you run the <code style=""background-color: #e7e7e7;"">abp install-libs</code> CLI tool command.</p> |
|||
|
|||
<p>For more information, check out the <a href=""https://abp.io/docs/latest/CLI#install-libs"">ABP CLI documentation</a></p>
|
|||
</body> |
|||
</html> |
|||
");
|
|||
} |
|||
#pragma warning restore 1998
|
|||
} |
|||
} |
|||
#pragma warning restore 1591
|
|||
@ -0,0 +1,22 @@ |
|||
@using Volo.Abp.AspNetCore.RazorViews |
|||
@inherits AbpCompilationRazorPageBase |
|||
@{ |
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 500; |
|||
} |
|||
|
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<title>Error - The Libs folder is missing!</title> |
|||
</head> |
|||
<body> |
|||
<h1> ⚠️ The Libs folder under the <code style="background-color: #e7e7e7;">wwwroot/libs</code> directory is empty!</h1> |
|||
|
|||
<p>The Libs folder contains mandatory NPM Packages for running the project.</p> |
|||
|
|||
<p>Make sure you run the <code style="background-color: #e7e7e7;">abp install-libs</code> CLI tool command.</p> |
|||
|
|||
<p>For more information, check out the <a href="https://abp.io/docs/latest/CLI#install-libs">ABP CLI documentation</a></p> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,283 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Globalization; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Text.Encodings.Web; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
|
|||
namespace Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
public abstract class AbpCompilationRazorPageBase |
|||
{ |
|||
private readonly static Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); |
|||
private readonly static char[] NewLineChars = new[] { '\r', '\n' }; |
|||
private readonly Stack<TextWriter> _textWriterStack = new Stack<TextWriter>(); |
|||
|
|||
/// <summary>
|
|||
/// The request context
|
|||
/// </summary>
|
|||
protected HttpContext Context { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// The request
|
|||
/// </summary>
|
|||
protected HttpRequest Request { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// The response
|
|||
/// </summary>
|
|||
protected HttpResponse Response { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// The output stream
|
|||
/// </summary>
|
|||
protected TextWriter Output { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// Html encoder used to encode content.
|
|||
/// </summary>
|
|||
protected HtmlEncoder HtmlEncoder { get; set; } = HtmlEncoder.Default; |
|||
|
|||
/// <summary>
|
|||
/// Url encoder used to encode content.
|
|||
/// </summary>
|
|||
protected UrlEncoder UrlEncoder { get; set; } = UrlEncoder.Default; |
|||
|
|||
/// <summary>
|
|||
/// JavaScript encoder used to encode content.
|
|||
/// </summary>
|
|||
protected JavaScriptEncoder JavaScriptEncoder { get; set; } = JavaScriptEncoder.Default; |
|||
|
|||
/// <summary>
|
|||
/// Execute an individual request
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream to write to</param>
|
|||
public async Task ExecuteAsync(Stream stream) |
|||
{ |
|||
// We technically don't need this intermediate buffer if this method accepts a memory stream.
|
|||
var buffer = new MemoryStream(); |
|||
Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true); |
|||
await ExecuteAsync(); |
|||
await Output.FlushAsync(); |
|||
await Output.DisposeAsync(); |
|||
buffer.Seek(0, SeekOrigin.Begin); |
|||
await buffer.CopyToAsync(stream); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute an individual request
|
|||
/// </summary>
|
|||
/// <param name="context"></param>
|
|||
public async Task ExecuteAsync(HttpContext context) |
|||
{ |
|||
Context = context; |
|||
Request = Context.Request; |
|||
Response = Context.Response; |
|||
var buffer = new MemoryStream(); |
|||
Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true); |
|||
await ExecuteAsync(); |
|||
await Output.FlushAsync(); |
|||
await Output.DisposeAsync(); |
|||
buffer.Seek(0, SeekOrigin.Begin); |
|||
await buffer.CopyToAsync(Response.Body); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute an individual request
|
|||
/// </summary>
|
|||
public abstract Task ExecuteAsync(); |
|||
|
|||
protected virtual void PushWriter(TextWriter writer) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(writer); |
|||
|
|||
_textWriterStack.Push(Output); |
|||
Output = writer; |
|||
} |
|||
|
|||
protected virtual TextWriter PopWriter() |
|||
{ |
|||
Output = _textWriterStack.Pop(); |
|||
return Output; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write the given value without HTML encoding directly to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="object"/> to write.</param>
|
|||
protected void WriteLiteral(object value) |
|||
{ |
|||
WriteLiteral(Convert.ToString(value, CultureInfo.InvariantCulture)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write the given value without HTML encoding directly to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="string"/> to write.</param>
|
|||
protected void WriteLiteral(string? value) |
|||
{ |
|||
if (!string.IsNullOrEmpty(value)) |
|||
{ |
|||
Output.Write(value); |
|||
} |
|||
} |
|||
|
|||
private List<string>? AttributeValues { get; set; } |
|||
|
|||
protected void WriteAttributeValue(string thingy, int startPostion, object value, int endValue, int dealyo, bool yesno) |
|||
{ |
|||
if (AttributeValues == null) |
|||
{ |
|||
AttributeValues = new List<string>(); |
|||
} |
|||
|
|||
AttributeValues.Add(value.ToString()!); |
|||
} |
|||
|
|||
private string? AttributeEnding { get; set; } |
|||
|
|||
protected void BeginWriteAttribute(string name, string beginning, int startPosition, string ending, int endPosition, int thingy) |
|||
{ |
|||
Debug.Assert(string.IsNullOrEmpty(AttributeEnding)); |
|||
|
|||
Output.Write(beginning); |
|||
AttributeEnding = ending; |
|||
} |
|||
|
|||
protected void EndWriteAttribute() |
|||
{ |
|||
Debug.Assert(AttributeValues != null); |
|||
Debug.Assert(!string.IsNullOrEmpty(AttributeEnding)); |
|||
|
|||
var attributes = string.Join(" ", AttributeValues); |
|||
Output.Write(attributes); |
|||
AttributeValues = null; |
|||
|
|||
Output.Write(AttributeEnding); |
|||
AttributeEnding = null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the given attribute to the given writer
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the attribute to write</param>
|
|||
/// <param name="leader">The value of the prefix</param>
|
|||
/// <param name="trailer">The value of the suffix</param>
|
|||
/// <param name="values">The <see cref="AttributeValue"/>s to write.</param>
|
|||
protected void WriteAttribute( |
|||
string name, |
|||
string leader, |
|||
string trailer, |
|||
params AttributeValue[] values) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(name); |
|||
ArgumentNullException.ThrowIfNull(leader); |
|||
ArgumentNullException.ThrowIfNull(trailer); |
|||
|
|||
WriteLiteral(leader); |
|||
foreach (var value in values) |
|||
{ |
|||
WriteLiteral(value.Prefix); |
|||
|
|||
// The special cases here are that the value we're writing might already be a string, or that the
|
|||
// value might be a bool. If the value is the bool 'true' we want to write the attribute name
|
|||
// instead of the string 'true'. If the value is the bool 'false' we don't want to write anything.
|
|||
// Otherwise the value is another object (perhaps an HtmlString) and we'll ask it to format itself.
|
|||
string? stringValue; |
|||
if (value.Value is bool) |
|||
{ |
|||
if ((bool)value.Value) |
|||
{ |
|||
stringValue = name; |
|||
} |
|||
else |
|||
{ |
|||
continue; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
stringValue = value.Value as string; |
|||
} |
|||
|
|||
// Call the WriteTo(string) overload when possible
|
|||
if (value.Literal && stringValue != null) |
|||
{ |
|||
WriteLiteral(stringValue); |
|||
} |
|||
else if (value.Literal) |
|||
{ |
|||
WriteLiteral(value.Value); |
|||
} |
|||
else if (stringValue != null) |
|||
{ |
|||
Write(stringValue); |
|||
} |
|||
else |
|||
{ |
|||
Write(value.Value); |
|||
} |
|||
} |
|||
WriteLiteral(trailer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="HelperResult.WriteTo(TextWriter)"/> is invoked
|
|||
/// </summary>
|
|||
/// <param name="result">The <see cref="HelperResult"/> to invoke</param>
|
|||
protected void Write(HelperResult result) |
|||
{ |
|||
result.WriteTo(Output); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the specified <paramref name="value"/> to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="object"/> to write.</param>
|
|||
/// <remarks>
|
|||
/// <see cref="HelperResult.WriteTo(TextWriter)"/> is invoked for <see cref="HelperResult"/> types.
|
|||
/// For all other types, the encoded result of <see cref="object.ToString"/> is written to
|
|||
/// <see cref="Output"/>.
|
|||
/// </remarks>
|
|||
protected void Write(object value) |
|||
{ |
|||
if (value is HelperResult helperResult) |
|||
{ |
|||
helperResult.WriteTo(Output); |
|||
} |
|||
else |
|||
{ |
|||
Write(Convert.ToString(value, CultureInfo.InvariantCulture)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="string"/> to write.</param>
|
|||
protected void Write(string? value) |
|||
{ |
|||
if (!string.IsNullOrEmpty(value)) |
|||
{ |
|||
WriteLiteral(HtmlEncoder.Encode(value)); |
|||
} |
|||
} |
|||
|
|||
protected string HtmlEncodeAndReplaceLineBreaks(string input) |
|||
{ |
|||
if (string.IsNullOrEmpty(input)) |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
|
|||
// Split on line breaks before passing it through the encoder.
|
|||
return string.Join("<br />" + Environment.NewLine, |
|||
input.Split("\r\n", StringSplitOptions.None) |
|||
.SelectMany(s => s.Split(NewLineChars, StringSplitOptions.None)) |
|||
.Select(HtmlEncoder.Encode)); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
public class AttributeValue |
|||
{ |
|||
public AttributeValue(string prefix, object value, bool literal) |
|||
{ |
|||
Prefix = prefix; |
|||
Value = value; |
|||
Literal = literal; |
|||
} |
|||
|
|||
public string Prefix { get; } |
|||
|
|||
public object Value { get; } |
|||
|
|||
public bool Literal { get; } |
|||
|
|||
public static AttributeValue FromTuple(Tuple<string, object, bool> value) |
|||
{ |
|||
return new AttributeValue(value.Item1, value.Item2, value.Item3); |
|||
} |
|||
|
|||
public static AttributeValue FromTuple(Tuple<string, string, bool> value) |
|||
{ |
|||
return new AttributeValue(value.Item1, value.Item2, value.Item3); |
|||
} |
|||
|
|||
public static implicit operator AttributeValue(Tuple<string, object, bool> value) |
|||
{ |
|||
return FromTuple(value); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
public class HelperResult |
|||
{ |
|||
public HelperResult(Action<TextWriter> action) |
|||
{ |
|||
WriteAction = action; |
|||
} |
|||
|
|||
public Action<TextWriter> WriteAction { get; } |
|||
|
|||
public void WriteTo(TextWriter writer) |
|||
{ |
|||
WriteAction(writer); |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
<SnackbarStack @ref="SnackbarStack" Location="SnackbarStackLocation.End" Multiline="true" DelayCloseOnClick="true" DelayCloseOnClickInterval="10000" Closed="@OnSnackbarClosed" /> |
|||
<SnackbarStack @ref="SnackbarStack" Location="SnackbarStackLocation.BottomEnd" Multiline="true" DelayCloseOnClick="true" DelayCloseOnClickInterval="10000" Closed="@OnSnackbarClosed" /> |
|||
@ -0,0 +1,227 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Razor.Language; |
|||
using Microsoft.AspNetCore.Razor.Language.Extensions; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.Cli.Args; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Cli.Commands; |
|||
|
|||
public class GenerateRazorPage : IConsoleCommand, ITransientDependency |
|||
{ |
|||
public const string Name = "generate-razor-page"; |
|||
|
|||
public ILogger<GenerateRazorPage> Logger { get; set; } |
|||
|
|||
public GenerateRazorPage() |
|||
{ |
|||
Logger = NullLogger<GenerateRazorPage>.Instance; |
|||
} |
|||
|
|||
public Task ExecuteAsync(CommandLineArgs commandLineArgs) |
|||
{ |
|||
var targetProjectDirectory = Directory.GetCurrentDirectory(); |
|||
var projectEngine = CreateProjectEngine(targetProjectDirectory); |
|||
|
|||
var results = MainCore(projectEngine, targetProjectDirectory); |
|||
|
|||
foreach (var result in results) |
|||
{ |
|||
File.WriteAllText(result.FilePath, result.GeneratedCode); |
|||
} |
|||
|
|||
Logger.LogInformation($"{results.Count} files successfully generated."); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public string GetUsageInfo() |
|||
{ |
|||
var sb = new StringBuilder(); |
|||
|
|||
sb.AppendLine(""); |
|||
sb.AppendLine("Usage:"); |
|||
sb.AppendLine("abp generate-razor-page"); |
|||
sb.AppendLine(""); |
|||
sb.AppendLine("See the documentation for more info: https://abp.io/docs/latest/cli"); |
|||
|
|||
return sb.ToString(); |
|||
} |
|||
|
|||
public string GetShortDescription() |
|||
{ |
|||
return "Generates code files for Razor page."; |
|||
} |
|||
|
|||
private RazorProjectEngine CreateProjectEngine(string targetProjectDirectory, Action<RazorProjectEngineBuilder>? configure = null) |
|||
{ |
|||
var fileSystem = RazorProjectFileSystem.Create(targetProjectDirectory); |
|||
var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => |
|||
{ |
|||
builder |
|||
.SetNamespace("Volo.Abp.AspNetCore.RazorViews") |
|||
.ConfigureClass((document, @class) => |
|||
{ |
|||
@class.ClassName = Path.GetFileNameWithoutExtension(document.Source.FilePath); |
|||
@class.Modifiers.Clear(); |
|||
@class.Modifiers.Add("internal"); |
|||
}); |
|||
|
|||
SectionDirective.Register(builder); |
|||
|
|||
builder.Features.Add(new SuppressChecksumOptionsFeature()); |
|||
builder.Features.Add(new SuppressMetadataAttributesFeature()); |
|||
|
|||
if (configure != null) |
|||
{ |
|||
configure(builder); |
|||
} |
|||
|
|||
builder.AddDefaultImports(@"
|
|||
@using System |
|||
@using System.Threading.Tasks");
|
|||
}); |
|||
|
|||
return projectEngine; |
|||
} |
|||
|
|||
private List<RazorPageGeneratorResult> MainCore(RazorProjectEngine projectEngine, string targetProjectDirectory) |
|||
{ |
|||
var results = new List<RazorPageGeneratorResult>(); |
|||
Logger.LogInformation("Generating code files for pages in {0}", targetProjectDirectory); |
|||
|
|||
var cshtmlFiles = projectEngine.FileSystem.EnumerateItems(targetProjectDirectory) |
|||
.Where(x => File.ReadAllText(x.PhysicalPath).Contains("@inherits AbpCompilationRazorPageBase")) |
|||
.ToList(); |
|||
|
|||
if (!cshtmlFiles.Any()) |
|||
{ |
|||
Logger.LogInformation("No .cshtml or .razor files were found."); |
|||
return results; |
|||
} |
|||
|
|||
foreach (var item in cshtmlFiles) |
|||
{ |
|||
Logger.LogInformation(" Generating code file for page {0} ...", item.FileName); |
|||
|
|||
results.Add(GenerateCodeFile(projectEngine, item)); |
|||
|
|||
Logger.LogInformation(" Done!"); |
|||
} |
|||
|
|||
return results; |
|||
} |
|||
|
|||
private RazorPageGeneratorResult GenerateCodeFile(RazorProjectEngine projectEngine, RazorProjectItem projectItem) |
|||
{ |
|||
var projectItemWrapper = new FileSystemRazorProjectItemWrapper(Logger, projectItem); |
|||
var codeDocument = projectEngine.Process(projectItemWrapper); |
|||
var cSharpDocument = codeDocument.GetCSharpDocument(); |
|||
if (cSharpDocument.Diagnostics.Any()) |
|||
{ |
|||
var diagnostics = string.Join(Environment.NewLine, cSharpDocument.Diagnostics); |
|||
Logger.LogInformation($"One or more parse errors encountered. This will not prevent the generator from continuing: {Environment.NewLine}{diagnostics}."); |
|||
} |
|||
|
|||
var generatedCodeFilePath = Path.ChangeExtension(projectItem.PhysicalPath, ".Designer.cs"); |
|||
return new RazorPageGeneratorResult |
|||
{ |
|||
FilePath = generatedCodeFilePath, |
|||
GeneratedCode = cSharpDocument.GeneratedCode, |
|||
}; |
|||
} |
|||
|
|||
private class SuppressChecksumOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature |
|||
{ |
|||
public int Order { get; set; } |
|||
|
|||
public void Configure(RazorCodeGenerationOptionsBuilder options) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
|
|||
options.SuppressChecksum = true; |
|||
} |
|||
} |
|||
|
|||
private class SuppressMetadataAttributesFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature |
|||
{ |
|||
public int Order { get; set; } |
|||
|
|||
public void Configure(RazorCodeGenerationOptionsBuilder options) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
options.SuppressMetadataAttributes = true; |
|||
} |
|||
} |
|||
|
|||
private class FileSystemRazorProjectItemWrapper : RazorProjectItem |
|||
{ |
|||
private readonly ILogger<GenerateRazorPage> _logger; |
|||
private readonly RazorProjectItem _source; |
|||
|
|||
public FileSystemRazorProjectItemWrapper(ILogger<GenerateRazorPage> logger, RazorProjectItem item) |
|||
{ |
|||
_logger = logger; |
|||
_source = item; |
|||
|
|||
// Mask the full name since we don't want a developer's local file paths to be committed.
|
|||
PhysicalPath = $"{_source.FileName}"; |
|||
} |
|||
|
|||
public override string BasePath => _source.BasePath; |
|||
|
|||
public override string FilePath => _source.FilePath; |
|||
|
|||
public override string PhysicalPath { get; } |
|||
|
|||
public override bool Exists => _source.Exists; |
|||
|
|||
public override Stream Read() |
|||
{ |
|||
var processedContent = ProcessFileIncludes(); |
|||
return new MemoryStream(Encoding.UTF8.GetBytes(processedContent)); |
|||
} |
|||
|
|||
private string ProcessFileIncludes() |
|||
{ |
|||
var basePath = Path.GetDirectoryName(_source.PhysicalPath); |
|||
var cshtmlContent = File.ReadAllText(_source.PhysicalPath); |
|||
|
|||
var startMatch = "<%$ include: "; |
|||
var endMatch = " %>"; |
|||
var startIndex = 0; |
|||
while (startIndex < cshtmlContent.Length) |
|||
{ |
|||
startIndex = cshtmlContent.IndexOf(startMatch, startIndex, StringComparison.Ordinal); |
|||
if (startIndex == -1) |
|||
{ |
|||
break; |
|||
} |
|||
var endIndex = cshtmlContent.IndexOf(endMatch, startIndex, StringComparison.Ordinal); |
|||
if (endIndex == -1) |
|||
{ |
|||
throw new InvalidOperationException($"Invalid include file format in {_source.PhysicalPath}. Usage example: <%$ include: ErrorPage.js %>"); |
|||
} |
|||
var includeFileName = cshtmlContent.Substring(startIndex + startMatch.Length, endIndex - (startIndex + startMatch.Length)); |
|||
_logger.LogInformation(" Inlining file {0}", includeFileName); |
|||
var includeFileContent = File.ReadAllText(Path.Combine(basePath, includeFileName)); |
|||
cshtmlContent = string.Concat(cshtmlContent.Substring(0, startIndex), includeFileContent, cshtmlContent.Substring(endIndex + endMatch.Length)); |
|||
startIndex += includeFileContent.Length; |
|||
} |
|||
return cshtmlContent; |
|||
} |
|||
} |
|||
|
|||
private class RazorPageGeneratorResult |
|||
{ |
|||
public string FilePath { get; set; } |
|||
|
|||
public string GeneratedCode { get; set; } |
|||
} |
|||
} |
|||