mirror of https://github.com/abpframework/abp.git
146 changed files with 3062 additions and 559 deletions
@ -1,4 +1,4 @@ |
|||
name: "Main" |
|||
name: "build and test" |
|||
on: |
|||
pull_request: |
|||
paths: |
|||
@ -1,3 +1,455 @@ |
|||
# Text-Templating |
|||
# Text Templating |
|||
|
|||
TODO |
|||
## Introduction |
|||
|
|||
ABP Framework provides a simple, yet efficient text template system. Text templating is used to dynamically render contents based on a template and a model (a data object): |
|||
|
|||
***TEMPLATE + MODEL ==render==> RENDERED CONTENT*** |
|||
|
|||
It is very similar to an ASP.NET Core Razor View (or Page): |
|||
|
|||
*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT* |
|||
|
|||
You can use the rendered output for any purpose, like sending emails or preparing some reports. |
|||
|
|||
### Example |
|||
|
|||
Here, a simple template: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
You can define a class with a `Name` property to render this template: |
|||
|
|||
````csharp |
|||
public class HelloModel |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
If you render the template with a `HelloModel` with the `Name` is `John`, the rendered output is will be: |
|||
|
|||
```` |
|||
Hello John :) |
|||
```` |
|||
|
|||
Template rendering engine is very powerful; |
|||
|
|||
* It is based on the [Scriban](https://github.com/lunet-io/scriban) library, so it supports **conditional logics**, **loops** and much more. |
|||
* Template content **can be localized**. |
|||
* You can define **layout templates** to be used as the layout while rendering other templates. |
|||
* You can pass arbitrary objects to the template context (beside the model) for advanced scenarios. |
|||
|
|||
### Source Code |
|||
|
|||
Get [the source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. |
|||
|
|||
## Installation |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
### Using the ABP CLI |
|||
|
|||
Open a command line window in the folder of the project (.csproj file) and type the following command: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.TextTemplating |
|||
```` |
|||
|
|||
### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) NuGet package to your project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.TextTemplating |
|||
```` |
|||
|
|||
2. Add the `AbpTextTemplatingModule` to the dependency list of your module: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpTextTemplatingModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
## Defining Templates |
|||
|
|||
Before rendering a template, you should define it. Create a class inheriting from the `TemplateDefinitionProvider` base class: |
|||
|
|||
````csharp |
|||
public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider |
|||
{ |
|||
public override void Define(ITemplateDefinitionContext context) |
|||
{ |
|||
context.Add( |
|||
new TemplateDefinition("Hello") //template name: "Hello" |
|||
.WithVirtualFilePath( |
|||
"/Demos/Hello/Hello.tpl", //template content path |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `context` object is used to add new templates or get the templates defined by depended modules. Used `context.Add(...)` to define a new template. |
|||
* `TemplateDefinition` is the class represents a template. Each template must have a unique name (that will be used while you are rendering the template). |
|||
* `/Demos/Hello/Hello.tpl` is the path of the template file. |
|||
* `isInlineLocalized` is used to declare if you are using a single template for all languages (`true`) or different templates for each language (`false`). See the Localization section below for more. |
|||
|
|||
### The Template Content |
|||
|
|||
`WithVirtualFilePath` indicates that we are using the [Virtual File System](Virtual-File-System.md) to store the template content. Create a `Hello.tpl` file inside your project and mark it as "**embedded resource**" on the properties window: |
|||
|
|||
 |
|||
|
|||
Example `Hello.tpl` content is shown below: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
The [Virtual File System](Virtual-File-System.md) requires to add your files in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class: |
|||
|
|||
````csharp |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<TextTemplateDemoModule>("TextTemplateDemo"); |
|||
}); |
|||
```` |
|||
|
|||
* `TextTemplateDemoModule` is the module class that you define your template in. |
|||
* `TextTemplateDemo` is the root namespace of your project. |
|||
|
|||
## Rendering the Template |
|||
|
|||
`ITemplateRenderer` service is used to render a template content. |
|||
|
|||
### Example: Rendering a Simple Template |
|||
|
|||
````csharp |
|||
public class HelloDemo : ITransientDependency |
|||
{ |
|||
private readonly ITemplateRenderer _templateRenderer; |
|||
|
|||
public HelloDemo(ITemplateRenderer templateRenderer) |
|||
{ |
|||
_templateRenderer = templateRenderer; |
|||
} |
|||
|
|||
public async Task RunAsync() |
|||
{ |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"Hello", //the template name |
|||
new HelloModel |
|||
{ |
|||
Name = "John" |
|||
} |
|||
); |
|||
|
|||
Console.WriteLine(result); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `HelloDemo` is a simple class that injects the `ITemplateRenderer` in its constructor and uses it inside the `RunAsync` method. |
|||
* `RenderAsync` gets two fundamental parameters: |
|||
* `templateName`: The name of the template to be rendered (`Hello` in this example). |
|||
* `model`: An object that is used as the `model` inside the template (a `HelloModel` object in this example). |
|||
|
|||
The result shown below for this example: |
|||
|
|||
````csharp |
|||
Hello John :) |
|||
```` |
|||
|
|||
### Anonymous Model |
|||
|
|||
While it is suggested to create model classes for the templates, it would be practical (and possible) to use anonymous objects for simple cases: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"Hello", |
|||
new |
|||
{ |
|||
Name = "John" |
|||
} |
|||
); |
|||
```` |
|||
|
|||
In this case, we haven't created a model class, but created an anonymous object as the model. |
|||
|
|||
### PascalCase vs camelCase |
|||
|
|||
PascalCase property names (like `UserName`) is used as camelCase (like `userName`) in the templates. |
|||
|
|||
## Localization |
|||
|
|||
It is possible to localize a template content based on the current culture. There are two types of localization options described in the following sections. |
|||
|
|||
### Inline localization |
|||
|
|||
Inline localization uses the [localization system](Localization.md) to localize texts inside templates. |
|||
|
|||
#### Example: Reset Password Link |
|||
|
|||
Assuming you need to send an email to a user to reset her/his password. Here, the template content: |
|||
|
|||
```` |
|||
<a href="{%{{{model.link}}}%}">{%{{{L "ResetMyPassword"}}}%}</a> |
|||
```` |
|||
|
|||
`L` function is used to localize the given key based on the current user culture. You need to define the `ResetMyPassword` key inside your localization file: |
|||
|
|||
````json |
|||
"ResetMyPassword": "Click here to reset your password" |
|||
```` |
|||
|
|||
You also need to declare the localization resource to be used with this template, inside your template definition provider class: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
"PasswordReset", //Template name |
|||
typeof(DemoResource) //LOCALIZATION RESOURCE |
|||
).WithVirtualFilePath( |
|||
"/Demos/PasswordReset/PasswordReset.tpl", //template content path |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
```` |
|||
|
|||
That's all. When you render this template like that: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"PasswordReset", //the template name |
|||
new PasswordResetModel |
|||
{ |
|||
Link = "https://abp.io/example-link?userId=123&token=ABC" |
|||
} |
|||
); |
|||
```` |
|||
|
|||
You will see the localized result: |
|||
|
|||
````csharp |
|||
<a href="https://abp.io/example-link?userId=123&token=ABC">Click here to reset your password</a> |
|||
```` |
|||
|
|||
> If you define the [default localization resource](Localization.md) for your application, then no need to declare the resource type for the template definition. |
|||
|
|||
### Multiple Contents Localization |
|||
|
|||
Instead of a single template that uses the localization system to localize the template, you may want to create different template files for each language. It can be needed if the template should be completely different for a specific culture rather than simple text localizations. |
|||
|
|||
#### Example: Welcome Email Template |
|||
|
|||
Assuming that you want to send a welcome email to your users, but want to define a completely different template based on the user culture. |
|||
|
|||
First, create a folder and put your templates inside it, like `en.tpl`, `tr.tpl`... one for each culture you support: |
|||
|
|||
 |
|||
|
|||
Then add your template definition in the template definition provider class: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
name: "WelcomeEmail", |
|||
defaultCultureName: "en" |
|||
) |
|||
.WithVirtualFilePath( |
|||
"/Demos/WelcomeEmail/Templates", //template content folder |
|||
isInlineLocalized: false |
|||
) |
|||
); |
|||
```` |
|||
|
|||
* Set **default culture name**, so it fallbacks to the default culture if there is no template for the desired culture. |
|||
* Specify **the template folder** rather than a single template file. |
|||
* Set `isInlineLocalized` to `false` for this case. |
|||
|
|||
That's all, you can render the template for the current culture: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync("WelcomeEmail"); |
|||
```` |
|||
|
|||
> Skipped the modal for this example to keep it simple, but you can use models as just explained before. |
|||
|
|||
### Specify the Culture |
|||
|
|||
`ITemplateRenderer` service uses the current culture (`CultureInfo.CurrentUICulture`) if not specified. If you need, you can specify the culture as the `cultureName` parameter: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"WelcomeEmail", |
|||
cultureName: "en" |
|||
); |
|||
```` |
|||
|
|||
## Layout Templates |
|||
|
|||
Layout templates are used to create shared layouts among other templates. It is similar to the layout system in the ASP.NET Core MVC / Razor Pages. |
|||
|
|||
### Example: Email HTML Layout Template |
|||
|
|||
For example, you may want to create a single layout for all of your email templates. |
|||
|
|||
First, create a template file just like before: |
|||
|
|||
````xml |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
</head> |
|||
<body> |
|||
{%{{{content}}}%} |
|||
</body> |
|||
</html> |
|||
```` |
|||
|
|||
* A layout template must have a **{%{{{content}}}%}** part as a place holder for the rendered child content. |
|||
|
|||
The register your template in the template definition provider: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
"EmailLayout", |
|||
isLayout: true //SET isLayout! |
|||
).WithVirtualFilePath( |
|||
"/Demos/EmailLayout/EmailLayout.tpl", |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
```` |
|||
|
|||
Now, you can use this template as the layout of any other template: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
name: "WelcomeEmail", |
|||
defaultCultureName: "en", |
|||
layout: "EmailLayout" //Set the LAYOUT |
|||
).WithVirtualFilePath( |
|||
"/Demos/WelcomeEmail/Templates", |
|||
isInlineLocalized: false |
|||
) |
|||
); |
|||
```` |
|||
|
|||
## Global Context |
|||
|
|||
ABP passes the `model` that can be used to access to the model inside the template. You can pass more global variables if you need. |
|||
|
|||
An example template content: |
|||
|
|||
```` |
|||
A global object value: {%{{{myGlobalObject}}}%} |
|||
```` |
|||
|
|||
This template assumes that that is a `myGlobalObject` object in the template rendering context. You can provide it like shown below: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"GlobalContextUsage", |
|||
globalContext: new Dictionary<string, object> |
|||
{ |
|||
{"myGlobalObject", "TEST VALUE"} |
|||
} |
|||
); |
|||
```` |
|||
|
|||
The rendering result will be: |
|||
|
|||
```` |
|||
A global object value: TEST VALUE |
|||
```` |
|||
|
|||
## Advanced Features |
|||
|
|||
This section covers some internals and more advanced usages of the text templating system. |
|||
|
|||
### Template Content Provider |
|||
|
|||
`ITemplateRenderer` is used to render the template, which is what you want for most of the cases. However, you can use the `ITemplateContentProvider` to get the raw (not rendered) template contents. |
|||
|
|||
> `ITemplateContentProvider` is internally used by the `ITemplateRenderer` to get the raw template contents. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class TemplateContentDemo : ITransientDependency |
|||
{ |
|||
private readonly ITemplateContentProvider _templateContentProvider; |
|||
|
|||
public TemplateContentDemo(ITemplateContentProvider templateContentProvider) |
|||
{ |
|||
_templateContentProvider = templateContentProvider; |
|||
} |
|||
|
|||
public async Task RunAsync() |
|||
{ |
|||
var result = await _templateContentProvider |
|||
.GetContentOrNullAsync("Hello"); |
|||
|
|||
Console.WriteLine(result); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The result will be the raw template content: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
* `GetContentOrNullAsync` returns `null` if no content defined for the requested template. |
|||
* It can get a `cultureName` parameter that is used if template has different files for different cultures (see Multiple Contents Localization section above). |
|||
|
|||
### Template Content Contributor |
|||
|
|||
`ITemplateContentProvider` service uses `ITemplateContentContributor` implementations to find template contents. There is a single pre-implemented content contributor, `VirtualFileTemplateContentContributor`, which gets template contents from the virtual file system as described above. |
|||
|
|||
You can implement the `ITemplateContentContributor` to read raw template contents from another source. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MyTemplateContentProvider |
|||
: ITemplateContentContributor, ITransientDependency |
|||
{ |
|||
public async Task<string> GetOrNullAsync(TemplateContentContributorContext context) |
|||
{ |
|||
var templateName = context.TemplateDefinition.Name; |
|||
|
|||
//TODO: Try to find content from another source |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
Return `null` if your source can not find the content, so `ITemplateContentProvider` fallbacks to the next contributor. |
|||
|
|||
### Template Definition Manager |
|||
|
|||
`ITemplateDefinitionManager` service can be used to get the template definitions (created by the template definition providers). |
|||
|
|||
## See Also |
|||
|
|||
* [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. |
|||
* [Localization system](Localization.md). |
|||
* [Virtual File System](Virtual-File-System.md). |
|||
@ -0,0 +1,165 @@ |
|||
# Querying Lists Easily with ListService |
|||
|
|||
`ListService` is a utility service to provide an easy pagination, sorting, and search implementation. |
|||
|
|||
|
|||
|
|||
## Getting Started |
|||
|
|||
`ListService` is **not provided in root**. The reason is, this way, it will clear any subscriptions on component destroy. You may use the optional `LIST_QUERY_DEBOUNCE_TIME` token to adjust the debounce behavior. |
|||
|
|||
```js |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { BookDto } from '../models'; |
|||
import { BookService } from '../services'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
providers: [ |
|||
// [Required] |
|||
ListService, |
|||
|
|||
// [Optional] |
|||
// Provide this token if you want a different debounce time. |
|||
// Default is 300. Cannot be 0. Any value below 100 is not recommended. |
|||
{ provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 500 }, |
|||
], |
|||
template: ` |
|||
|
|||
`, |
|||
}) |
|||
class BookComponent { |
|||
items: BookDto[] = []; |
|||
count = 0; |
|||
|
|||
constructor( |
|||
public readonly list: ListService, |
|||
private bookService: BookService, |
|||
) {} |
|||
|
|||
ngOnInit() { |
|||
// A function that gets query and returns an observable |
|||
const bookStreamCreator = query => this.bookService.getList(query); |
|||
|
|||
this.list.hookToQuery(bookStreamCreator).subscribe( |
|||
response => { |
|||
this.items = response.items; |
|||
this.count = response.count; |
|||
// If you use OnPush change detection strategy, |
|||
// call detectChanges method of ChangeDetectorRef here. |
|||
} |
|||
); // Subscription is auto-cleared on destroy. |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` properties directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component properties instead. |
|||
|
|||
Place `ListService` properties into the template like this: |
|||
|
|||
```html |
|||
<abp-table |
|||
[value]="book.items" |
|||
[(page)]="list.page" |
|||
[rows]="list.maxResultCount" |
|||
[totalRecords]="book.totalCount" |
|||
[headerTemplate]="tableHeader" |
|||
[bodyTemplate]="tableBody" |
|||
[abpLoading]="list.isLoading$ | async" |
|||
> |
|||
</abp-table> |
|||
|
|||
<ng-template #tableHeader> |
|||
<tr> |
|||
<th (click)="nameSort.sort('name')"> |
|||
{%{{{ '::Name' | abpLocalization }}}%} |
|||
<abp-sort-order-icon |
|||
#nameSort |
|||
sortKey="name" |
|||
[(selectedSortKey)]="list.sortKey" |
|||
[(order)]="list.sortOrder" |
|||
></abp-sort-order-icon> |
|||
</th> |
|||
</tr> |
|||
</ng-template> |
|||
|
|||
<ng-template #tableBody let-data> |
|||
<tr> |
|||
<td>{%{{{ data.name }}}%}</td> |
|||
</tr> |
|||
</ng-template> |
|||
``` |
|||
|
|||
## Usage with Observables |
|||
|
|||
You may use observables in combination with [AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe) of Angular instead. Here are some possibilities: |
|||
|
|||
```ts |
|||
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query)); |
|||
``` |
|||
|
|||
```html |
|||
<!-- simplified representation of the template --> |
|||
|
|||
<abp-table |
|||
[value]="(book$ | async)?.items || []" |
|||
[totalRecords]="(book$ | async)?.totalCount" |
|||
> |
|||
</abp-table> |
|||
|
|||
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE --> |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
|
|||
```ts |
|||
@Select(BookState.getBooks) |
|||
books$: Observable<BookDto[]>; |
|||
|
|||
@Select(BookState.getBookCount) |
|||
bookCount$: Observable<number>; |
|||
|
|||
ngOnInit() { |
|||
this.list.hookToQuery((query) => this.store.dispatch(new GetBooks(query))).subscribe(); |
|||
} |
|||
``` |
|||
|
|||
```html |
|||
<!-- simplified representation of the template --> |
|||
|
|||
<abp-table |
|||
[value]="books$ | async" |
|||
[totalRecords]="bookCount$ | async" |
|||
> |
|||
</abp-table> |
|||
``` |
|||
|
|||
## How to Refresh Table on Create/Update/Delete |
|||
|
|||
`ListService` exposes a `get` method to trigger a request with the current query. So, basically, whenever a create, update, or delete action resolves, you can call `this.list.get();` and it will call hooked stream creator again. |
|||
|
|||
```ts |
|||
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get); |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
```ts |
|||
this.bookService.createByInput(form.value) |
|||
.subscribe(() => { |
|||
this.list.get(); |
|||
|
|||
// Other subscription logic here |
|||
}) |
|||
``` |
|||
|
|||
## How to Implement Server-Side Search in a Table |
|||
|
|||
`ListService` exposes a `filter` property that will trigger a request with the current query and the given search string. All you need to do is to bind it to an input element with two-way binding. |
|||
|
|||
```html |
|||
<!-- simplified representation --> |
|||
|
|||
<input type="text" name="search" [(ngModel)]="list.filter"> |
|||
``` |
|||
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 34 KiB |
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
[Serializable] |
|||
public class ExtensionEnumDto |
|||
{ |
|||
public List<ExtensionEnumFieldDto> Fields { get; set; } |
|||
|
|||
public string LocalizationResource { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
[Serializable] |
|||
public class ExtensionEnumFieldDto |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public object Value { get; set; } |
|||
} |
|||
} |
|||
@ -1,36 +1,13 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
[Serializable] |
|||
public class ExtensionPropertyAttributeDto |
|||
{ |
|||
public string Type { get; set; } |
|||
public string TypeSimple { get; set; } |
|||
public Dictionary<string, object> Configuration { get; set; } |
|||
|
|||
public static ExtensionPropertyAttributeDto Create(Attribute attribute) |
|||
{ |
|||
var attributeType = attribute.GetType(); |
|||
var dto = new ExtensionPropertyAttributeDto |
|||
{ |
|||
Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(attributeType), |
|||
TypeSimple = TypeHelper.GetSimplifiedName(attributeType), |
|||
Configuration = new Dictionary<string, object>() |
|||
}; |
|||
|
|||
if (attribute is StringLengthAttribute stringLengthAttribute) |
|||
{ |
|||
dto.Configuration["MaximumLength"] = stringLengthAttribute.MaximumLength; |
|||
dto.Configuration["MinimumLength"] = stringLengthAttribute.MinimumLength; |
|||
} |
|||
|
|||
//TODO: Others!
|
|||
|
|||
return dto; |
|||
} |
|||
public Dictionary<string, object> Config { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JsTree |
|||
{ |
|||
[DependsOn(typeof(JQueryScriptContributor))] |
|||
public class JsTreeScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/jstree/jstree.min.js"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JsTree |
|||
{ |
|||
public class JsTreeOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Path of the style file for the JsTree library.
|
|||
/// Setting to null ignores the style file.
|
|||
///
|
|||
/// Default value: "/libs/jstree/themes/default/style.min.css".
|
|||
/// </summary>
|
|||
public string StylePath { get; set; } = "/libs/jstree/themes/default/style.min.css"; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JsTree |
|||
{ |
|||
public class JsTreeStyleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
var options = context |
|||
.ServiceProvider |
|||
.GetRequiredService<IOptions<JsTreeOptions>>() |
|||
.Value; |
|||
|
|||
if (options.StylePath.IsNullOrEmpty()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
context.Files.AddIfNotContains(options.StylePath); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.ObjectExtending |
|||
{ |
|||
public static class MvcUiObjectExtensionPropertyInfoExtensions |
|||
{ |
|||
private static readonly HashSet<Type> NumberTypes = new HashSet<Type> { |
|||
typeof(int), |
|||
typeof(long), |
|||
typeof(byte), |
|||
typeof(sbyte), |
|||
typeof(short), |
|||
typeof(ushort), |
|||
typeof(uint), |
|||
typeof(long), |
|||
typeof(ulong), |
|||
typeof(float), |
|||
typeof(double), |
|||
typeof(decimal), |
|||
typeof(int?), |
|||
typeof(long?), |
|||
typeof(byte?), |
|||
typeof(sbyte?), |
|||
typeof(short?), |
|||
typeof(ushort?), |
|||
typeof(uint?), |
|||
typeof(long?), |
|||
typeof(ulong?), |
|||
typeof(float?), |
|||
typeof(double?), |
|||
typeof(decimal?) |
|||
}; |
|||
|
|||
public static string GetInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property) |
|||
{ |
|||
if (property.IsDate()) |
|||
{ |
|||
return "{0:yyyy-MM-dd}"; |
|||
} |
|||
|
|||
if (property.IsDateTime()) |
|||
{ |
|||
return "{0:yyyy-MM-ddTHH:mm}"; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public static string GetInputValueOrNull(this IBasicObjectExtensionPropertyInfo property, object value) |
|||
{ |
|||
if (value == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (TypeHelper.IsFloatingType(property.Type)) |
|||
{ |
|||
return value.ToString()?.Replace(',', '.'); |
|||
} |
|||
|
|||
/* Let the ASP.NET Core handle it! */ |
|||
return null; |
|||
} |
|||
|
|||
public static string GetInputType(this ObjectExtensionPropertyInfo propertyInfo) |
|||
{ |
|||
foreach (var attribute in propertyInfo.Attributes) |
|||
{ |
|||
var inputTypeByAttribute = GetInputTypeFromAttributeOrNull(attribute); |
|||
if (inputTypeByAttribute != null) |
|||
{ |
|||
return inputTypeByAttribute; |
|||
} |
|||
} |
|||
|
|||
return GetInputTypeFromTypeOrNull(propertyInfo.Type) |
|||
?? "text"; //default
|
|||
} |
|||
|
|||
private static string GetInputTypeFromAttributeOrNull(Attribute attribute) |
|||
{ |
|||
if (attribute is EmailAddressAttribute) |
|||
{ |
|||
return "email"; |
|||
} |
|||
|
|||
if (attribute is UrlAttribute) |
|||
{ |
|||
return "url"; |
|||
} |
|||
|
|||
if (attribute is HiddenInputAttribute) |
|||
{ |
|||
return "hidden"; |
|||
} |
|||
|
|||
if (attribute is PhoneAttribute) |
|||
{ |
|||
return "tel"; |
|||
} |
|||
|
|||
if (attribute is DataTypeAttribute dataTypeAttribute) |
|||
{ |
|||
switch (dataTypeAttribute.DataType) |
|||
{ |
|||
case DataType.Password: |
|||
return "password"; |
|||
case DataType.Date: |
|||
return "date"; |
|||
case DataType.Time: |
|||
return "time"; |
|||
case DataType.EmailAddress: |
|||
return "email"; |
|||
case DataType.Url: |
|||
return "url"; |
|||
case DataType.PhoneNumber: |
|||
return "tel"; |
|||
case DataType.DateTime: |
|||
return "datetime-local"; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private static string GetInputTypeFromTypeOrNull(Type type) |
|||
{ |
|||
if (type == typeof(bool)) |
|||
{ |
|||
return "checkbox"; |
|||
} |
|||
|
|||
if (type == typeof(DateTime)) |
|||
{ |
|||
return "datetime-local"; |
|||
} |
|||
|
|||
if (NumberTypes.Contains(type)) |
|||
{ |
|||
return "number"; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Reflection; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
public class ExtensionPropertyAttributeDtoFactory : IExtensionPropertyAttributeDtoFactory, ITransientDependency |
|||
{ |
|||
public virtual ExtensionPropertyAttributeDto Create(Attribute attribute) |
|||
{ |
|||
return new ExtensionPropertyAttributeDto |
|||
{ |
|||
TypeSimple = GetSimplifiedName(attribute), |
|||
Config = CreateConfiguration(attribute) |
|||
}; |
|||
} |
|||
|
|||
protected virtual string GetSimplifiedName(Attribute attribute) |
|||
{ |
|||
return attribute.GetType().Name.ToCamelCase().RemovePostFix("Attribute"); |
|||
} |
|||
|
|||
protected virtual Dictionary<string, object> CreateConfiguration(Attribute attribute) |
|||
{ |
|||
var configuration = new Dictionary<string, object>(); |
|||
|
|||
AddPropertiesToConfiguration(attribute, configuration); |
|||
|
|||
return configuration; |
|||
} |
|||
|
|||
protected virtual void AddPropertiesToConfiguration(Attribute attribute, Dictionary<string, object> configuration) |
|||
{ |
|||
var properties = attribute |
|||
.GetType() |
|||
.GetProperties(BindingFlags.Instance | BindingFlags.Public); |
|||
|
|||
foreach (var property in properties) |
|||
{ |
|||
if (IgnoreProperty(attribute, property)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var value = GetPropertyValue(attribute, property); |
|||
if (value == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
configuration[property.Name.ToCamelCase()] = value; |
|||
} |
|||
} |
|||
|
|||
protected virtual bool IgnoreProperty(Attribute attribute, PropertyInfo property) |
|||
{ |
|||
if (property.DeclaringType == null || |
|||
property.DeclaringType.IsIn(typeof(ValidationAttribute), typeof(Attribute), typeof(object))) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (property.PropertyType == typeof(DisplayFormatAttribute)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
protected virtual object GetPropertyValue(Attribute attribute, PropertyInfo property) |
|||
{ |
|||
var value = property.GetValue(attribute); |
|||
if (value == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (property.PropertyType.IsEnum) |
|||
{ |
|||
return Enum.GetName(property.PropertyType, value); |
|||
} |
|||
|
|||
if (property.PropertyType == typeof(Type)) |
|||
{ |
|||
return TypeHelper.GetSimplifiedName((Type) value); |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
public interface IExtensionPropertyAttributeDtoFactory |
|||
{ |
|||
ExtensionPropertyAttributeDto Create(Attribute attribute); |
|||
} |
|||
} |
|||
@ -1,98 +1,35 @@ |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using System.Linq; |
|||
|
|||
namespace Volo.Abp.ObjectExtending |
|||
{ |
|||
public static class ObjectExtensionPropertyInfoAspNetCoreMvcExtensions |
|||
{ |
|||
public static string GetInputType(this ObjectExtensionPropertyInfo propertyInfo) |
|||
private static readonly Type[] DateTimeTypes = |
|||
{ |
|||
foreach (var attribute in propertyInfo.Attributes) |
|||
{ |
|||
var inputTypeByAttribute = GetInputTypeFromAttributeOrNull(attribute); |
|||
if (inputTypeByAttribute != null) |
|||
{ |
|||
return inputTypeByAttribute; |
|||
} |
|||
} |
|||
typeof(DateTime), |
|||
typeof(DateTimeOffset) |
|||
}; |
|||
|
|||
return GetInputTypeFromTypeOrNull(propertyInfo.Type) |
|||
?? "text"; //default
|
|||
public static bool IsDate(this IBasicObjectExtensionPropertyInfo property) |
|||
{ |
|||
return DateTimeTypes.Contains(property.Type) && |
|||
property.GetDataTypeOrNull() == DataType.Date; |
|||
} |
|||
|
|||
private static string GetInputTypeFromAttributeOrNull(Attribute attribute) |
|||
public static bool IsDateTime(this IBasicObjectExtensionPropertyInfo property) |
|||
{ |
|||
if (attribute is EmailAddressAttribute) |
|||
{ |
|||
return "email"; |
|||
} |
|||
|
|||
if (attribute is UrlAttribute) |
|||
{ |
|||
return "url"; |
|||
} |
|||
|
|||
if (attribute is HiddenInputAttribute) |
|||
{ |
|||
return "hidden"; |
|||
} |
|||
|
|||
if (attribute is PhoneAttribute) |
|||
{ |
|||
return "tel"; |
|||
} |
|||
|
|||
if (attribute is DataTypeAttribute dataTypeAttribute) |
|||
{ |
|||
switch (dataTypeAttribute.DataType) |
|||
{ |
|||
case DataType.Password: |
|||
return "password"; |
|||
case DataType.Date: |
|||
return "date"; |
|||
case DataType.Time: |
|||
return "time"; |
|||
case DataType.EmailAddress: |
|||
return "email"; |
|||
case DataType.Url: |
|||
return "url"; |
|||
case DataType.PhoneNumber: |
|||
return "tel"; |
|||
case DataType.DateTime: |
|||
return "datetime-local"; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
return DateTimeTypes.Contains(property.Type) && |
|||
!property.IsDate(); |
|||
} |
|||
|
|||
private static string GetInputTypeFromTypeOrNull(Type type) |
|||
public static DataType? GetDataTypeOrNull(this IBasicObjectExtensionPropertyInfo property) |
|||
{ |
|||
if (type == typeof(bool)) |
|||
{ |
|||
return "checkbox"; |
|||
} |
|||
|
|||
if (type == typeof(DateTime)) |
|||
{ |
|||
return "datetime-local"; |
|||
} |
|||
|
|||
if (type == typeof(int) || |
|||
type == typeof(long) || |
|||
type == typeof(byte) || |
|||
type == typeof(sbyte) || |
|||
type == typeof(short) || |
|||
type == typeof(ushort) || |
|||
type == typeof(uint) || |
|||
type == typeof(long) || |
|||
type == typeof(ulong)) |
|||
{ |
|||
return "number"; |
|||
} |
|||
|
|||
return null; |
|||
return property |
|||
.Attributes |
|||
.OfType<DataTypeAttribute>() |
|||
.FirstOrDefault()?.DataType; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using System.Runtime.Serialization; |
|||
|
|||
namespace Volo.Abp |
|||
{ |
|||
public class AbpInitializationException : AbpException |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpException"/> object.
|
|||
/// </summary>
|
|||
public AbpInitializationException() |
|||
{ |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="message">Exception message</param>
|
|||
public AbpInitializationException(string message) |
|||
: base(message) |
|||
{ |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="message">Exception message</param>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpInitializationException(string message, Exception innerException) |
|||
: base(message, innerException) |
|||
{ |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Constructor for serializing.
|
|||
/// </summary>
|
|||
public AbpInitializationException(SerializationInfo serializationInfo, StreamingContext context) |
|||
: base(serializationInfo, context) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using Microsoft.Extensions.Localization; |
|||
|
|||
namespace Volo.Abp.Localization |
|||
{ |
|||
/// <summary>
|
|||
/// This class is designed to be used internal by the framework.
|
|||
/// </summary>
|
|||
public static class AbpInternalLocalizationHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Searches an array of keys in an array of localizers.
|
|||
/// </summary>
|
|||
/// <param name="localizers">
|
|||
/// An array of localizers. Search the keys on the localizers.
|
|||
/// Can contain null items in the array.
|
|||
/// </param>
|
|||
/// <param name="keys">
|
|||
/// An array of keys. Search the keys on the localizers.
|
|||
/// Should not contain null items in the array.
|
|||
/// </param>
|
|||
/// <param name="defaultValue">
|
|||
/// Return value if none of the localizers has none of the keys.
|
|||
/// </param>
|
|||
/// <returns></returns>
|
|||
public static string LocalizeWithFallback( |
|||
IStringLocalizer[] localizers, |
|||
string[] keys, |
|||
string defaultValue) |
|||
{ |
|||
foreach (var key in keys) |
|||
{ |
|||
foreach (var localizer in localizers) |
|||
{ |
|||
if (localizer == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var localizedString = localizer[key]; |
|||
if (!localizedString.ResourceNotFound) |
|||
{ |
|||
return localizedString.Value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return defaultValue; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.ObjectExtending |
|||
{ |
|||
internal static class ExtensionPropertyHelper |
|||
{ |
|||
public static IEnumerable<Attribute> GetDefaultAttributes(Type type) |
|||
{ |
|||
if (TypeHelper.IsNonNullablePrimitiveType(type) || type.IsEnum) |
|||
{ |
|||
yield return new RequiredAttribute(); |
|||
} |
|||
|
|||
if (type.IsEnum) |
|||
{ |
|||
yield return new EnumDataTypeAttribute(type); |
|||
} |
|||
} |
|||
|
|||
public static object GetDefaultValue( |
|||
Type propertyType, |
|||
Func<object> defaultValueFactory, |
|||
object defaultValue) |
|||
{ |
|||
if (defaultValueFactory != null) |
|||
{ |
|||
return defaultValueFactory(); |
|||
} |
|||
|
|||
return defaultValue ?? |
|||
TypeHelper.GetDefaultValue(propertyType); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Volo.Abp.ObjectExtending |
|||
{ |
|||
public interface IBasicObjectExtensionPropertyInfo |
|||
{ |
|||
[NotNull] |
|||
public string Name { get; } |
|||
|
|||
[NotNull] |
|||
public Type Type { get; } |
|||
|
|||
[NotNull] |
|||
public List<Attribute> Attributes { get; } |
|||
|
|||
[NotNull] |
|||
public List<Action<ObjectExtensionPropertyValidationContext>> Validators { get; } |
|||
|
|||
[CanBeNull] |
|||
public ILocalizableString DisplayName { get; } |
|||
|
|||
/// <summary>
|
|||
/// Uses as the default value if <see cref="DefaultValueFactory"/> was not set.
|
|||
/// </summary>
|
|||
[CanBeNull] |
|||
public object DefaultValue { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Used with the first priority to create the default value for the property.
|
|||
/// Uses to the <see cref="DefaultValue"/> if this was not set.
|
|||
/// </summary>
|
|||
[CanBeNull] |
|||
public Func<object> DefaultValueFactory { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Volo.Abp.ObjectExtending.Modularity |
|||
{ |
|||
public static class ExtensionPropertyConfigurationExtensions |
|||
{ |
|||
public static string GetLocalizationResourceNameOrNull( |
|||
this ExtensionPropertyConfiguration property) |
|||
{ |
|||
var resourceType = property.GetLocalizationResourceTypeOrNull(); |
|||
if (resourceType == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return LocalizationResourceNameAttribute.GetName(resourceType); |
|||
} |
|||
|
|||
public static Type GetLocalizationResourceTypeOrNull( |
|||
this ExtensionPropertyConfiguration property) |
|||
{ |
|||
if (property.DisplayName != null && |
|||
property.DisplayName is LocalizableString localizableString) |
|||
{ |
|||
return localizableString.ResourceType; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"culture": "de", |
|||
"texts": { |
|||
"hello": "Hello" |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Blogging |
|||
{ |
|||
public class BloggingTwitterOptions |
|||
{ |
|||
public string Site { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Volo.Blogging.SocialMedia |
|||
{ |
|||
public class BloggingTwitterOptions |
|||
{ |
|||
public string Site { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Volo.Abp.Identity |
|||
{ |
|||
public class UserLookupSearchInputDto : LimitedResultRequestDto, ISortedResultRequest |
|||
{ |
|||
public string Sorting { get; set; } |
|||
|
|||
public string Filter { get; set; } |
|||
} |
|||
} |
|||
@ -1,13 +1,20 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Users |
|||
{ |
|||
public interface IExternalUserLookupServiceProvider //TODO: Consider to inherit from IUserLookupService
|
|||
public interface IExternalUserLookupServiceProvider |
|||
{ |
|||
Task<IUserData> FindByIdAsync(Guid id, CancellationToken cancellationToken = default); |
|||
|
|||
Task<IUserData> FindByUserNameAsync(string userName, CancellationToken cancellationToken = default); |
|||
|
|||
Task<List<IUserData>> SearchAsync( |
|||
string sorting, |
|||
string filter, |
|||
int maxResultCount, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue