mirror of https://github.com/abpframework/abp.git
107 changed files with 2245 additions and 462 deletions
@ -1,4 +1,4 @@ |
|||||
name: "Main" |
name: "build and test" |
||||
on: |
on: |
||||
pull_request: |
pull_request: |
||||
paths: |
paths: |
||||
@ -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"> |
||||
|
``` |
||||
@ -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; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.ComponentModel.DataAnnotations; |
|
||||
using Volo.Abp.Reflection; |
|
||||
|
|
||||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
||||
{ |
{ |
||||
[Serializable] |
[Serializable] |
||||
public class ExtensionPropertyAttributeDto |
public class ExtensionPropertyAttributeDto |
||||
{ |
{ |
||||
public string Type { get; set; } |
|
||||
public string TypeSimple { get; set; } |
public string TypeSimple { get; set; } |
||||
public Dictionary<string, object> Configuration { get; set; } |
|
||||
|
|
||||
public static ExtensionPropertyAttributeDto Create(Attribute attribute) |
public Dictionary<string, object> Config { get; set; } |
||||
{ |
|
||||
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; |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
@ -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; |
||||
using System.ComponentModel.DataAnnotations; |
using System.ComponentModel.DataAnnotations; |
||||
using Microsoft.AspNetCore.Mvc; |
using System.Linq; |
||||
|
|
||||
namespace Volo.Abp.ObjectExtending |
namespace Volo.Abp.ObjectExtending |
||||
{ |
{ |
||||
public static class ObjectExtensionPropertyInfoAspNetCoreMvcExtensions |
public static class ObjectExtensionPropertyInfoAspNetCoreMvcExtensions |
||||
{ |
{ |
||||
public static string GetInputType(this ObjectExtensionPropertyInfo propertyInfo) |
private static readonly Type[] DateTimeTypes = |
||||
{ |
{ |
||||
foreach (var attribute in propertyInfo.Attributes) |
typeof(DateTime), |
||||
{ |
typeof(DateTimeOffset) |
||||
var inputTypeByAttribute = GetInputTypeFromAttributeOrNull(attribute); |
}; |
||||
if (inputTypeByAttribute != null) |
|
||||
{ |
|
||||
return inputTypeByAttribute; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return GetInputTypeFromTypeOrNull(propertyInfo.Type) |
public static bool IsDate(this IBasicObjectExtensionPropertyInfo property) |
||||
?? "text"; //default
|
{ |
||||
|
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 DateTimeTypes.Contains(property.Type) && |
||||
{ |
!property.IsDate(); |
||||
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) |
public static DataType? GetDataTypeOrNull(this IBasicObjectExtensionPropertyInfo property) |
||||
{ |
{ |
||||
if (type == typeof(bool)) |
return property |
||||
{ |
.Attributes |
||||
return "checkbox"; |
.OfType<DataTypeAttribute>() |
||||
} |
.FirstOrDefault()?.DataType; |
||||
|
|
||||
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; |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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,101 @@ |
|||||
|
import { Inject, Injectable, OnDestroy, Optional } from '@angular/core'; |
||||
|
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs'; |
||||
|
import { debounceTime, shareReplay, switchMap, tap } from 'rxjs/operators'; |
||||
|
import { ABP } from '../models/common'; |
||||
|
import { PagedResultDto } from '../models/dtos'; |
||||
|
import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens/list.token'; |
||||
|
import { takeUntilDestroy } from '../utils/rxjs-utils'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class ListService implements OnDestroy { |
||||
|
private _filter = ''; |
||||
|
set filter(value: string) { |
||||
|
this._filter = value; |
||||
|
this.get(); |
||||
|
} |
||||
|
get filter(): string { |
||||
|
return this._filter; |
||||
|
} |
||||
|
|
||||
|
private _maxResultCount = 10; |
||||
|
set maxResultCount(value: number) { |
||||
|
this._maxResultCount = value; |
||||
|
this.get(); |
||||
|
} |
||||
|
get maxResultCount(): number { |
||||
|
return this._maxResultCount; |
||||
|
} |
||||
|
|
||||
|
private _page = 1; |
||||
|
set page(value: number) { |
||||
|
this._page = value; |
||||
|
this.get(); |
||||
|
} |
||||
|
get page(): number { |
||||
|
return this._page; |
||||
|
} |
||||
|
|
||||
|
private _sortKey = ''; |
||||
|
set sortKey(value: string) { |
||||
|
this._sortKey = value; |
||||
|
this.get(); |
||||
|
} |
||||
|
get sortKey(): string { |
||||
|
return this._sortKey; |
||||
|
} |
||||
|
|
||||
|
private _sortOrder = ''; |
||||
|
set sortOrder(value: string) { |
||||
|
this._sortOrder = value; |
||||
|
this.get(); |
||||
|
} |
||||
|
get sortOrder(): string { |
||||
|
return this._sortOrder; |
||||
|
} |
||||
|
|
||||
|
private _query$ = new ReplaySubject<ABP.PageQueryParams>(1); |
||||
|
|
||||
|
get query$(): Observable<ABP.PageQueryParams> { |
||||
|
return this._query$ |
||||
|
.asObservable() |
||||
|
.pipe(debounceTime(this.delay || 300), shareReplay({ bufferSize: 1, refCount: true })); |
||||
|
} |
||||
|
|
||||
|
private _isLoading$ = new BehaviorSubject(false); |
||||
|
|
||||
|
get isLoading$(): Observable<boolean> { |
||||
|
return this._isLoading$.asObservable(); |
||||
|
} |
||||
|
|
||||
|
get = () => { |
||||
|
this._query$.next({ |
||||
|
filter: this._filter || undefined, |
||||
|
maxResultCount: this._maxResultCount, |
||||
|
skipCount: (this._page - 1) * this._maxResultCount, |
||||
|
sorting: this._sortOrder ? `${this._sortKey} ${this._sortOrder}` : undefined, |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
constructor(@Optional() @Inject(LIST_QUERY_DEBOUNCE_TIME) private delay: number) { |
||||
|
this.get(); |
||||
|
} |
||||
|
|
||||
|
hookToQuery<T extends any>( |
||||
|
streamCreatorCallback: QueryStreamCreatorCallback<T>, |
||||
|
): Observable<PagedResultDto<T>> { |
||||
|
this._isLoading$.next(true); |
||||
|
|
||||
|
return this.query$.pipe( |
||||
|
switchMap(streamCreatorCallback), |
||||
|
tap(() => this._isLoading$.next(false)), |
||||
|
shareReplay({ bufferSize: 1, refCount: true }), |
||||
|
takeUntilDestroy(this), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() {} |
||||
|
} |
||||
|
|
||||
|
export type QueryStreamCreatorCallback<T> = ( |
||||
|
query: ABP.PageQueryParams, |
||||
|
) => Observable<PagedResultDto<T>>; |
||||
@ -0,0 +1,153 @@ |
|||||
|
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
||||
|
import { of } from 'rxjs'; |
||||
|
import { bufferCount, take } from 'rxjs/operators'; |
||||
|
import { ABP } from '../models'; |
||||
|
import { ListService, QueryStreamCreatorCallback } from '../services/list.service'; |
||||
|
import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens'; |
||||
|
|
||||
|
describe('ListService', () => { |
||||
|
let spectator: SpectatorService<ListService>; |
||||
|
let service: ListService; |
||||
|
|
||||
|
const createService = createServiceFactory({ |
||||
|
service: ListService, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: LIST_QUERY_DEBOUNCE_TIME, |
||||
|
useValue: 0, |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createService(); |
||||
|
service = spectator.service; |
||||
|
}); |
||||
|
|
||||
|
describe('#filter', () => { |
||||
|
it('should initially be empty string', () => { |
||||
|
expect(service.filter).toBe(''); |
||||
|
}); |
||||
|
|
||||
|
it('should be changed', () => { |
||||
|
service.filter = 'foo'; |
||||
|
|
||||
|
expect(service.filter).toBe('foo'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('#maxResultCount', () => { |
||||
|
it('should initially be 10', () => { |
||||
|
expect(service.maxResultCount).toBe(10); |
||||
|
}); |
||||
|
|
||||
|
it('should be changed', () => { |
||||
|
service.maxResultCount = 20; |
||||
|
|
||||
|
expect(service.maxResultCount).toBe(20); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('#page', () => { |
||||
|
it('should initially be 1', () => { |
||||
|
expect(service.page).toBe(1); |
||||
|
}); |
||||
|
|
||||
|
it('should be changed', () => { |
||||
|
service.page = 9; |
||||
|
|
||||
|
expect(service.page).toBe(9); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('#sortKey', () => { |
||||
|
it('should initially be empty string', () => { |
||||
|
expect(service.sortKey).toBe(''); |
||||
|
}); |
||||
|
|
||||
|
it('should be changed', () => { |
||||
|
service.sortKey = 'foo'; |
||||
|
|
||||
|
expect(service.sortKey).toBe('foo'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('#sortOrder', () => { |
||||
|
it('should initially be empty string', () => { |
||||
|
expect(service.sortOrder).toBe(''); |
||||
|
}); |
||||
|
|
||||
|
it('should be changed', () => { |
||||
|
service.sortOrder = 'foo'; |
||||
|
|
||||
|
expect(service.sortOrder).toBe('foo'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('#query$', () => { |
||||
|
it('should initially emit default query', done => { |
||||
|
service.query$.pipe(take(1)).subscribe(query => { |
||||
|
expect(query).toEqual({ |
||||
|
filter: undefined, |
||||
|
maxResultCount: 10, |
||||
|
skipCount: 0, |
||||
|
sorting: undefined, |
||||
|
}); |
||||
|
|
||||
|
done(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should emit a query based on params set', done => { |
||||
|
service.filter = 'foo'; |
||||
|
service.sortKey = 'bar'; |
||||
|
service.sortOrder = 'baz'; |
||||
|
service.maxResultCount = 20; |
||||
|
service.page = 9; |
||||
|
|
||||
|
service.query$.pipe(take(1)).subscribe(query => { |
||||
|
expect(query).toEqual({ |
||||
|
filter: 'foo', |
||||
|
sorting: 'bar baz', |
||||
|
maxResultCount: 20, |
||||
|
skipCount: 160, |
||||
|
}); |
||||
|
|
||||
|
done(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('#hookToQuery', () => { |
||||
|
it('should call given callback with the query', done => { |
||||
|
const callback: QueryStreamCreatorCallback<ABP.PageQueryParams> = query => |
||||
|
of({ items: [query], totalCount: 1 }); |
||||
|
|
||||
|
service.hookToQuery(callback).subscribe(({ items: [query] }) => { |
||||
|
expect(query).toEqual({ |
||||
|
filter: undefined, |
||||
|
maxResultCount: 10, |
||||
|
skipCount: 0, |
||||
|
sorting: undefined, |
||||
|
}); |
||||
|
|
||||
|
done(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should emit isLoading as side effect', done => { |
||||
|
const callback: QueryStreamCreatorCallback<ABP.PageQueryParams> = query => |
||||
|
of({ items: [query], totalCount: 1 }); |
||||
|
|
||||
|
service.isLoading$.pipe(bufferCount(3)).subscribe(([idle, init, end]) => { |
||||
|
expect(idle).toBe(false); |
||||
|
expect(init).toBe(true); |
||||
|
expect(end).toBe(false); |
||||
|
|
||||
|
done(); |
||||
|
}); |
||||
|
|
||||
|
service.hookToQuery(callback).subscribe(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -1 +1,2 @@ |
|||||
|
export * from './list.token'; |
||||
export * from './options.token'; |
export * from './options.token'; |
||||
|
|||||
@ -0,0 +1,3 @@ |
|||||
|
import { InjectionToken } from '@angular/core'; |
||||
|
|
||||
|
export const LIST_QUERY_DEBOUNCE_TIME = new InjectionToken<number>('LIST_QUERY_DEBOUNCE_TIME'); |
||||
@ -0,0 +1,5 @@ |
|||||
|
module.exports = { |
||||
|
mappings: { |
||||
|
"@node_modules/jstree/dist/**/*.*": "@libs/jstree/" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
{ |
||||
|
"version": "2.7.0", |
||||
|
"name": "@abp/jstree", |
||||
|
"publishConfig": { |
||||
|
"access": "public" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@abp/jquery": "^2.7.0", |
||||
|
"jstree": "^3.3.9" |
||||
|
}, |
||||
|
"gitHead": "0ea3895f3b0b489e3ea81fc88f8f0896b22b61bd" |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue