From 919a2e2e01892f70722a4eb6ba8fd19016e33da1 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Fri, 8 May 2020 23:18:08 +0300 Subject: [PATCH 001/110] Text-Templating initial content. --- docs/en/Text-Templating.md | 156 ++++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 2 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index efde511323..49b59b347e 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -1,3 +1,155 @@ -# Text-Templating +# Text Templating -TODO \ No newline at end of file +In ABP Framework, `text template` is a mixture of text blocks and control logic that can generate a `string` result. [Scriban](https://github.com/lunet-io/scriban) is used for the control logic and [Abp.Localization](Localization.md) is used to make content easily localizable. The generated string can be text of any kind, such as a web page, an e-mail content etc. + +> **stored content** +```html +
    +{{~ for $i in 0..3 ~}} +
  1. {{ L "WelcomeMessage" }}
  2. +{{~ endfor ~}} +
+``` +> **result** (for en culture) +``` +1. Welcome to the abp.io! +2. Welcome to the abp.io! +3. Welcome to the abp.io! +4. Welcome to the abp.io! +``` + +## 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 +{ +} +```` +## Logic + +A Text Template is a combination of two object. +- `TemplateDefinition` +- `TemplateContent` + +### Template Definition + +Template Definition is an object that contains some information about your `Text Templates`. Template Definition object contains the following properties. + +- `Name` *(string)* +- `IsLayout` *(boolean)* +- `Layout` *(string)* contains the name of layout template +- `LocalizationResource` *(Type)* Inline Localized +- `IsInlineLocalized`*(boolean)* describes that the template is inline localized or not +- `DefaultCultureName` *(string)* defines the default culture for the template + +### Template Content + +This is a simple content for your templates. For default, template contents stored as `Virtual File`. + +> Example: ForgotPasswordEmail.tpl + +```html +

{{L "PasswordReset"}}

+ +

{{L "PasswordResetInfoInEmail"}}

+ +
+ {{L "ResetMyPassword"}} +
+ +``` + +## Localization + +You can localize your Text Templates by choosing two different method. + +- `Inline Localization` +- `Multiple Content Localization` + +### Inline Localization + +Inline localized Text Templates is using only one content resource, and it is using the `Abp.Localization` to get content in different languages/cultures. + +> Example Inline Localized Text Template: +> +> ForgotPasswordEmail.tpl + +```html +

{{L "PasswordReset"}}

+ +

{{L "PasswordResetInfoInEmail"}}

+ +
+ {{L "ResetMyPassword"}} +
+``` + +### Multiple Content Localization + +You can store your Text Templates for any culture in different content resource. + +**Examples given by using `Virtual File` contents.** + +> Example Multiple Content Localization + +> ForgotPasswordEmail / en.tpl + +```html +

Reset Your Password

+ +

Hello, this is a password changing email.

+ +
+ Click To Reset Your Password +
+``` + +> ForgotPasswordEmail / tr.tpl + +```html +

Şifrenizi Değiştirin

+ +

Merhaba, bu bir şifre yenileme e postasıdır.

+ +
+ Şifrenizi Yenilemek İçin Tıklayınız +
+``` + +## Definition a Text Template + +## Rendering + +## Getting Template Definitions + +### Template Definition Manager + +## Getting Template Contents + +### Template Content Contributor From 0ce59cd1dc8a551b2e085af5d8bbdc7ddbc55fbc Mon Sep 17 00:00:00 2001 From: Ahmet Date: Fri, 8 May 2020 23:29:26 +0300 Subject: [PATCH 002/110] Update Text-Templating.md Layout System Described --- docs/en/Text-Templating.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 49b59b347e..531a365f69 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -1,6 +1,6 @@ # Text Templating -In ABP Framework, `text template` is a mixture of text blocks and control logic that can generate a `string` result. [Scriban](https://github.com/lunet-io/scriban) is used for the control logic and [Abp.Localization](Localization.md) is used to make content easily localizable. The generated string can be text of any kind, such as a web page, an e-mail content etc. +In ABP Framework, `text template` is a mixture of text blocks and control logic that can generate a `string` result. An open source package [Scriban](https://github.com/lunet-io/scriban) is used for the control logic and [Abp.Localization](Localization.md) is used to make content easily localizable. The generated string can be text of any kind, such as a web page, an e-mail content etc. > **stored content** ```html @@ -36,9 +36,9 @@ 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 - ```` +```` +Install-Package Volo.Abp.TextTemplating +```` 2. Add the `AbpTextTemplatingModule` to the dependency list of your module: @@ -142,9 +142,29 @@ You can store your Text Templates for any culture in different content resource. ``` -## Definition a Text Template +## Layout System -## Rendering +It is typical to use the same layout for all emails. So, you can define a layout template. + +A text template can be layout for different text templates and also a text template may use a layout. + +A layout Text Template must have `{{content}}` area to render the child content. _(just like the `RenderBody()` in the MVC)_ + +> Example Email Layout Text Template + +```html + + + + + + + {{content}} + + +``` + +## Definition a Text Template ## Getting Template Definitions @@ -153,3 +173,5 @@ You can store your Text Templates for any culture in different content resource. ## Getting Template Contents ### Template Content Contributor + +## Rendering From 386923c4a603b6fcf74b373ab4fc042077474f71 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Fri, 8 May 2020 23:31:40 +0300 Subject: [PATCH 003/110] Update Text-Templating.md Layout system moved to under Logic section --- docs/en/Text-Templating.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 531a365f69..e63053d815 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -85,14 +85,14 @@ This is a simple content for your templates. For default, template contents stor ``` -## Localization +### Localization You can localize your Text Templates by choosing two different method. - `Inline Localization` - `Multiple Content Localization` -### Inline Localization +#### Inline Localization Inline localized Text Templates is using only one content resource, and it is using the `Abp.Localization` to get content in different languages/cultures. @@ -110,7 +110,7 @@ Inline localized Text Templates is using only one content resource, and it is us ``` -### Multiple Content Localization +#### Multiple Content Localization You can store your Text Templates for any culture in different content resource. @@ -142,7 +142,7 @@ You can store your Text Templates for any culture in different content resource. ``` -## Layout System +### Layout System It is typical to use the same layout for all emails. So, you can define a layout template. From 455ed693cfca77e43ee83b76922db2f08be073d4 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Fri, 8 May 2020 23:39:03 +0300 Subject: [PATCH 004/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index e63053d815..c6f684ff8f 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -114,8 +114,6 @@ Inline localized Text Templates is using only one content resource, and it is us You can store your Text Templates for any culture in different content resource. -**Examples given by using `Virtual File` contents.** - > Example Multiple Content Localization > ForgotPasswordEmail / en.tpl From ef0ddbd66552772c184a5deeeda81042d396dabc Mon Sep 17 00:00:00 2001 From: Ahmet Date: Sat, 9 May 2020 00:38:09 +0300 Subject: [PATCH 005/110] Update Text-Templating.md adding the definition is started --- docs/en/Text-Templating.md | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index c6f684ff8f..6b8357ac7d 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -51,6 +51,7 @@ public class YourModule : AbpModule { } ```` + ## Logic A Text Template is a combination of two object. @@ -164,6 +165,47 @@ A layout Text Template must have `{{content}}` area to render the child content. ## Definition a Text Template +First of all, create a class that inherited from `TemplateDefinitionProvider` abstract class and create `Define` method that derived from the base class. + +`Define` method requires a context that is `ITemplateDefinitionContext`. This `context` is a storage for template definitions and we will add our template definitions to the context. + +> For default, ABP uses **`Virtual Files`** for text templates. All given examples are for `Virtual File Text Template Definitions`. + +```csharp +public class MyTemplateDefinitionProvider : TemplateDefinitionProvider + { + public override void Define(ITemplateDefinitionContext context) + { + // Layout Text Template + context.Add( + new TemplateDefinition( + name: "MySampleTemplateLayout", // Template Definition Name + isLayout: true + ).WithVirtualFilePath("/SampleTemplates/SampleTemplateLayout.tpl", true) + ); + + // Inline Localized Text Template + context.Add( + new TemplateDefinition( + name: "ForgotPasswordEmail", + localizationResource: typeof(MyLocalizationResource), + layout: TestTemplates.TestTemplateLayout1 + ).WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail.tpl", true) + ); + + // Multiple File Localized Text Template + context.Add( + new TemplateDefinition( + name: "ForgotPasswordEmail", + defaultCultureName: "en" + ).WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail", false) + ); + } + } +``` + +### Context + ## Getting Template Definitions ### Template Definition Manager From 43ea247fd0a8a43ea2e1729c8b1941320d931b88 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Sat, 9 May 2020 00:58:26 +0300 Subject: [PATCH 006/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 6b8357ac7d..d94dc6931c 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -143,7 +143,7 @@ You can store your Text Templates for any culture in different content resource. ### Layout System -It is typical to use the same layout for all emails. So, you can define a layout template. +It is typical to use the same layout for some different Text Templates. So, you can define a layout template. A text template can be layout for different text templates and also a text template may use a layout. @@ -204,7 +204,9 @@ public class MyTemplateDefinitionProvider : TemplateDefinitionProvider } ``` -### Context +As you see in the given example all Text Templates are added with `(ITemplateDefinitionContext)context.Add` method. This method requires a `TemplateDefinition` object. Then we call `WithVirtualFilePath` method with chaining for the describe where is the virtual files. + + ## Getting Template Definitions From 211a4c5039048c2e3973161f5f755ca6b78e03c9 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 01:30:15 +0300 Subject: [PATCH 007/110] feat(theme-shared): add dismissible property to confirmation options #3686 --- .../components/confirmation/confirmation.component.html | 5 ++++- .../packages/theme-shared/src/lib/models/confirmation.ts | 8 +++++++- .../theme-shared/src/lib/services/confirmation.service.ts | 7 +++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html index 8987cda2f9..47c555a16b 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html @@ -1,5 +1,8 @@
-
+
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts index 815fd65c20..18dceb320d 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts @@ -3,13 +3,19 @@ import { Config } from '@abp/ng.core'; export namespace Confirmation { export interface Options { id?: any; - closable?: boolean; + dismissible?: boolean; messageLocalizationParams?: string[]; titleLocalizationParams?: string[]; hideCancelBtn?: boolean; hideYesBtn?: boolean; cancelText?: Config.LocalizationParam; yesText?: Config.LocalizationParam; + + /** + * + * @deprecated To be deleted in v2.9 + */ + closable?: boolean; } export interface DialogData { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts index 2672a93a2a..70ad0d0e2b 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts @@ -68,7 +68,7 @@ export class ConfirmationService { message: Config.LocalizationParam, title: Config.LocalizationParam, severity?: Confirmation.Severity, - options?: Partial, + options = {} as Partial, ): Observable { if (!this.containerComponentRef) this.setContainer(); @@ -78,8 +78,11 @@ export class ConfirmationService { severity: severity || 'neutral', options, }); + this.status$ = new Subject(); - this.listenToEscape(); + const { dismissible = true } = options; + if (dismissible) this.listenToEscape(); + return this.status$; } From 11f836706fb0d34d959fec84a3ec8c4ac79f14d8 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 01:31:25 +0300 Subject: [PATCH 008/110] docs: explain dismissible option #3686 --- docs/en/UI/Angular/Confirmation-Service.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md index 2d1c5c1843..9bc015ced5 100644 --- a/docs/en/UI/Angular/Confirmation-Service.md +++ b/docs/en/UI/Angular/Confirmation-Service.md @@ -53,7 +53,7 @@ this.confirmation - `Confirmation.Status` is an enum and has three properties; - `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button. - `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button. - - `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape. + - `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape or clicking the backdrop. If you are not interested in the confirmation status, you do not have to subscribe to the returned observable: @@ -70,6 +70,7 @@ Options can be passed as the third parameter to `success`, `warn`, `error`, and const options: Partial = { hideCancelBtn: false, hideYesBtn: false, + dismissible: false, cancelText: 'Close', yesText: 'Confirm', messageLocalizationParams: ['Demo'], @@ -83,10 +84,11 @@ this.confirmation.warn( ); ``` -- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false` -- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false` -- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel` -- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes` +- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false`. +- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false`. +- `dismissible` option allows dismissing the confirmation overlay by pressing escape or clicking the backdrop. Default value is `true`. +- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel`. +- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. - `messageLocalizationParams` is the interpolation parameters for the localization of the message. - `titleLocalizationParams` is the interpolation parameters for the localization of the title. From aa1eeed4e1e2d1182ea4d7295c3129b00ab6fad0 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 01:33:15 +0300 Subject: [PATCH 009/110] docs: change a word --- docs/en/UI/Angular/Confirmation-Service.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md index 9bc015ced5..16dfa9fd94 100644 --- a/docs/en/UI/Angular/Confirmation-Service.md +++ b/docs/en/UI/Angular/Confirmation-Service.md @@ -86,7 +86,7 @@ this.confirmation.warn( - `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false`. - `hideYesBtn` option hides the confirmation button when `true`. Default value is `false`. -- `dismissible` option allows dismissing the confirmation overlay by pressing escape or clicking the backdrop. Default value is `true`. +- `dismissible` option allows dismissing the confirmation popup by pressing escape or clicking the backdrop. Default value is `true`. - `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel`. - `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. - `messageLocalizationParams` is the interpolation parameters for the localization of the message. From a5ee78910afb9fd68b86f7892ba0c85378650ce1 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 12:50:13 +0300 Subject: [PATCH 010/110] fix(theme-shared): fix loader bar problem --- .../components/loader-bar/loader-bar.component.scss | 5 ++++- .../components/loader-bar/loader-bar.component.ts | 13 +++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss index 2dd3482e27..19a4f2ee8f 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss @@ -16,6 +16,9 @@ left: 0; position: fixed; top: 0; - transition: width 0.4s ease; + + &.progressing { + transition: width 0.4s ease; + } } } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts index 441359e476..de37ca18f7 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -12,6 +12,7 @@ import { filter } from 'rxjs/operators';
Date: Tue, 12 May 2020 12:52:19 +0300 Subject: [PATCH 012/110] chore: change fn name resolves #3650 --- .../src/lib/components/loader-bar/loader-bar.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts index de37ca18f7..426f3bb0f2 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -90,7 +90,7 @@ export class LoaderBarComponent implements OnDestroy, OnInit { this.isLoading = true; - const progress = () => { + const moveOn = () => { if (this.progressLevel < 75) { this.progressLevel += Math.random() * 10; } else if (this.progressLevel < 90) { @@ -103,8 +103,8 @@ export class LoaderBarComponent implements OnDestroy, OnInit { this.cdRef.detectChanges(); }; - progress(); - this.interval = interval(this.intervalPeriod).subscribe(() => progress()); + moveOn(); + this.interval = interval(this.intervalPeriod).subscribe(() => moveOn()); } stopLoading() { From 2a21eb7639c1b64d5d8bf149b36b35b4871fd3d5 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 12:56:49 +0300 Subject: [PATCH 013/110] refactor(theme-shared): improve code quality --- .../src/lib/components/loader-bar/loader-bar.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts index 426f3bb0f2..61f4d2f663 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -104,7 +104,7 @@ export class LoaderBarComponent implements OnDestroy, OnInit { }; moveOn(); - this.interval = interval(this.intervalPeriod).subscribe(() => moveOn()); + this.interval = interval(this.intervalPeriod).subscribe(moveOn); } stopLoading() { From 0e492d23df67ff3a8cd7f1e4f850ccf684bc1697 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Tue, 12 May 2020 21:05:25 +0300 Subject: [PATCH 014/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 43 +++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index d94dc6931c..4a1b99f75d 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -169,7 +169,9 @@ First of all, create a class that inherited from `TemplateDefinitionProvider` ab `Define` method requires a context that is `ITemplateDefinitionContext`. This `context` is a storage for template definitions and we will add our template definitions to the context. -> For default, ABP uses **`Virtual Files`** for text templates. All given examples are for `Virtual File Text Template Definitions`. +> **NOTE!** For default, ABP uses **Virtual File System** for text templates. Do not forget to register your files as an `Embedded Resource`. Please check the [Virtual File System Documentation](Virtual-File-System.md) for more details. + +> All given examples are for `Virtual File Text Template Definitions`. ```csharp public class MyTemplateDefinitionProvider : TemplateDefinitionProvider @@ -204,9 +206,42 @@ public class MyTemplateDefinitionProvider : TemplateDefinitionProvider } ``` -As you see in the given example all Text Templates are added with `(ITemplateDefinitionContext)context.Add` method. This method requires a `TemplateDefinition` object. Then we call `WithVirtualFilePath` method with chaining for the describe where is the virtual files. +As you see in the given example, all Text Templates are added with `(ITemplateDefinitionContext)context.Add` method. This method requires a `TemplateDefinition` object. Then we call `WithVirtualFilePath` method with chaining for the describe where is the virtual files. + +`WithVirtualFilePath` is requires one `tpl` file path for the `Inline Localized` Text Templates. If your Text Tempalte is `Multi Localized` you should create a folder and store each different culture files under that. So you can send the folder path as a parameter to `WithVirtualFilePath`. + +> Inline Localized File + +``` +/ Folder / ForgotPasswordEmail.tpl +``` + +> Multi Content Localization + +``` +/ Folder / ForgotPasswordEmail / en.tpl +/ Folder / ForgotPasswordEmail / tr.tpl +``` + + +## Rendering + +When one template is registered, it is easy to render and get the result with `ITemplateRenderer` service. + +`ITemplateRenderer` service has one method that named `RenderAsync` and to render your content and it is requires some parametres. +- `templateName` (string) **_Not Null_** +- `model` (object) **_Can Be Null_** +- `cultureName` (string) **_Can Be Null_** +- `globalContext` (dictionary) **_Can Be Null_** +`templateName` is exactly same with Template Definition Name. + +`model` is a dynamic object. This is using to put dynamic data into template. For more information, please check by [Scriban Documentation](https://github.com/lunet-io/scriban). + +`cultureName` is your rendering destination culture. When it is not exist, it will use the default culture. + +`globalContext` = TODO ## Getting Template Definitions @@ -214,6 +249,4 @@ As you see in the given example all Text Templates are added with `(ITemplateDef ## Getting Template Contents -### Template Content Contributor - -## Rendering +### Template Content Contributor \ No newline at end of file From 6b2d6ef636d4dcc158b97b3c293f6bffa9ed4d1b Mon Sep 17 00:00:00 2001 From: Ahmet Date: Tue, 12 May 2020 21:42:10 +0300 Subject: [PATCH 015/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 4a1b99f75d..d042f06967 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -237,16 +237,31 @@ When one template is registered, it is easy to render and get the result with `I `templateName` is exactly same with Template Definition Name. -`model` is a dynamic object. This is using to put dynamic data into template. For more information, please check by [Scriban Documentation](https://github.com/lunet-io/scriban). +`model` is a dynamic object. This is using to put dynamic data into template. For more information, please look at [Scriban Documentation](https://github.com/lunet-io/scriban). `cultureName` is your rendering destination culture. When it is not exist, it will use the default culture. +> If `cultureName` has a language tag it will try to find exact culture with tag, if it is not exist it will use the language family. + +> If you try to render content with _"es-MX"_ it will search your template with _"es-MX"_ culture, when it fails to find, it will try to render _"es"_ culture content. If still can't find it will render the default culture content that you defined. + `globalContext` = TODO -## Getting Template Definitions +## Template Definition Manager + +When you want to get your `Template Definitions`, you can use a singleton service that named `Template Definition Manager` in runtime. + +To use it, inject `ITemplateDefinitionManager` service. + +It has three method that you can get your Template Definitions. + +- `Get` +- `GetOrNull` +- `GetAll` -### Template Definition Manager +`Get` and `GetOrNull` requires a string parameter that name of template definition. `Get` will throw error when it is not exist but `GetOrNull` returns `null`. -## Getting Template Contents +`GetAll` returns you all registered template definitions. + +## Template Content Contributor -### Template Content Contributor \ No newline at end of file From e869d5446b121e75cae71abdbb5845540ee378b8 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Tue, 12 May 2020 22:05:41 +0300 Subject: [PATCH 016/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index d042f06967..4ffce45dd8 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -247,6 +247,27 @@ When one template is registered, it is easy to render and get the result with `I `globalContext` = TODO +## Template Content Provider + +When you want to get your stored template content you can use `ITemplateContentProvider`. + +`ITemplateContentProvider` has one method that named `GetContentOrNullAsync` with two different overriding, and it returns you a string of template content or null. (**without rendering**) + +- `templateName` (_string_) or `templateDefinition` (_`TemplateDefinition`_) +- `cultureName` (_string_) +- `tryDefaults` (_bool_) +- `useCurrentCultureIfCultureNameIsNull` (_bool_) + +### Usage + +First parametres of `GetContentOrNullAsync` (`templateName` or `templateDefinition`) are required, the other three parametres can be null. + +If you want to get exact culture content, set `tryDefaults` and `useCurrentCultureIfCultureNameIsNull` as a `false`. Because the `GetContentOrNullAsync` tries to return content of template. + +> Example Scenario + +> If you have a template content that culture "`es`", when you try to get template content with "`es-MX`" it will try to return first "`es-MX`", if it fails it will return "`es`" content. If you set `tryDefaults` and `useCurrentCultureIfCultureNameIsNull` as `false` it will return `null`. + ## Template Definition Manager When you want to get your `Template Definitions`, you can use a singleton service that named `Template Definition Manager` in runtime. @@ -265,3 +286,6 @@ It has three method that you can get your Template Definitions. ## Template Content Contributor +You can store your `Template Contents` in any resource. To make it, just create a class that implements `ITemplateContentContributor` interface. + +`ITemplateContentContributor` has a one method that named `GetOrNullAsync`. This method must return content **without rendering**. \ No newline at end of file From a3d4d2e2a9c3ff6b9c17577799e0f59d42a86b86 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Tue, 12 May 2020 22:11:43 +0300 Subject: [PATCH 017/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 4ffce45dd8..f508fdaece 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -95,7 +95,7 @@ You can localize your Text Templates by choosing two different method. #### Inline Localization -Inline localized Text Templates is using only one content resource, and it is using the `Abp.Localization` to get content in different languages/cultures. +Inline localized Text Templates is using only one content resource, and it is using the [Abp.Localization](Localization.md) to get content in different languages/cultures. > Example Inline Localized Text Template: > @@ -288,4 +288,4 @@ It has three method that you can get your Template Definitions. You can store your `Template Contents` in any resource. To make it, just create a class that implements `ITemplateContentContributor` interface. -`ITemplateContentContributor` has a one method that named `GetOrNullAsync`. This method must return content **without rendering**. \ No newline at end of file +`ITemplateContentContributor` has a one method that named `GetOrNullAsync`. This method must return content **without rendering** if it finds in your resource or returns `null`. \ No newline at end of file From b51c2204dd9e62e4a4fdaa928f55d31ab8791b17 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Wed, 13 May 2020 00:24:53 +0300 Subject: [PATCH 018/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index f508fdaece..5451b1f45e 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -230,20 +230,20 @@ When one template is registered, it is easy to render and get the result with `I `ITemplateRenderer` service has one method that named `RenderAsync` and to render your content and it is requires some parametres. -- `templateName` (string) **_Not Null_** -- `model` (object) **_Can Be Null_** -- `cultureName` (string) **_Can Be Null_** -- `globalContext` (dictionary) **_Can Be Null_** +- `templateName` (_string_) +- `model` (_object_) +- `cultureName` (_string_) +- `globalContext` (_dictionary_) `templateName` is exactly same with Template Definition Name. `model` is a dynamic object. This is using to put dynamic data into template. For more information, please look at [Scriban Documentation](https://github.com/lunet-io/scriban). -`cultureName` is your rendering destination culture. When it is not exist, it will use the default culture. +`cultureName` is your destination rendering culture. When it is not exist, it will use the default culture. > If `cultureName` has a language tag it will try to find exact culture with tag, if it is not exist it will use the language family. -> If you try to render content with _"es-MX"_ it will search your template with _"es-MX"_ culture, when it fails to find, it will try to render _"es"_ culture content. If still can't find it will render the default culture content that you defined. +> Example: If you try to render content with _"es-MX"_ it will search your template with _"es-MX"_ culture, when it fails to find, it will try to render _"es"_ culture content. If still can't find it will render the default culture content that you defined. `globalContext` = TODO @@ -288,4 +288,4 @@ It has three method that you can get your Template Definitions. You can store your `Template Contents` in any resource. To make it, just create a class that implements `ITemplateContentContributor` interface. -`ITemplateContentContributor` has a one method that named `GetOrNullAsync`. This method must return content **without rendering** if it finds in your resource or returns `null`. \ No newline at end of file +`ITemplateContentContributor` has a one method that named `GetOrNullAsync`. This method must return content **without rendering** if that is exist in your resource or must return `null`. \ No newline at end of file From bb68bdcdc599f0cedc3269e1cca2095921ab11c5 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 13 May 2020 14:49:09 +0800 Subject: [PATCH 019/110] Move localization middleware above authentication middleware --- .../applications/AuthServer.Host/AuthServerHostModule.cs | 2 +- .../BackendAdminApp.Host/BackendAdminAppHostModule.cs | 2 +- .../PublicWebSite.Host/PublicWebSiteHostModule.cs | 2 +- .../BloggingService.Host/BloggingServiceHostModule.cs | 2 +- .../IdentityService.Host/IdentityServiceHostModule.cs | 2 +- .../ProductService.Host/ProductServiceHostModule.cs | 2 +- .../TenantManagementServiceHostModule.cs | 2 +- .../MyProjectNameHttpApiHostModule.cs | 2 +- .../MyProjectNameHttpApiHostModule.cs | 2 +- .../MyProjectNameIdentityServerModule.cs | 4 ++-- .../MyProjectNameWebModule.cs | 6 ++---- .../MyProjectNameWebModule.cs | 2 +- .../MyProjectNameWebTestModule.cs | 4 ++-- .../MyProjectNameHttpApiHostModule.cs | 2 +- .../MyProjectNameIdentityServerModule.cs | 2 +- .../MyProjectNameWebHostModule.cs | 4 ++-- .../MyProjectNameWebUnifiedModule.cs | 2 +- 17 files changed, 21 insertions(+), 23 deletions(-) diff --git a/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs b/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs index 08b6689cea..095f3c5ddc 100644 --- a/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs +++ b/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs @@ -95,12 +95,12 @@ namespace AuthServer.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } app.UseIdentityServer(); - app.UseAbpRequestLocalization(); app.UseAuditing(); app.UseConfiguredEndpoints(); diff --git a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs index b0aba5af53..4414dc73ae 100644 --- a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs +++ b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs @@ -118,13 +118,13 @@ namespace BackendAdminApp.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); app.UseAuthentication(); if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs index bc0bb83021..9ea7543228 100644 --- a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs +++ b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs @@ -96,13 +96,13 @@ namespace PublicWebSite.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); app.UseAuthentication(); if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseConfiguredEndpoints(); } } diff --git a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs index 692d4c3d61..c546c57ae5 100644 --- a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs @@ -120,6 +120,7 @@ namespace BloggingService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); app.Use(async (ctx, next) => @@ -140,7 +141,6 @@ namespace BloggingService.Host { app.UseMultiTenancy(); } - app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs index 982f4c09af..49c0bbd933 100644 --- a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs @@ -101,6 +101,7 @@ namespace IdentityService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); if (MsDemoConsts.IsMultiTenancyEnabled) { @@ -121,7 +122,6 @@ namespace IdentityService.Host await next(); }); - app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs index 44e23eeb78..9fb074fe76 100644 --- a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs @@ -104,6 +104,7 @@ namespace ProductService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); app.Use(async (ctx, next) => @@ -124,7 +125,6 @@ namespace ProductService.Host { app.UseMultiTenancy(); } - app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs index f9a818234f..f068c2adf9 100644 --- a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs @@ -105,6 +105,7 @@ namespace TenantManagementService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); app.Use(async (ctx, next) => @@ -125,7 +126,6 @@ namespace TenantManagementService.Host { app.UseMultiTenancy(); } - app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index 2526aec04d..f560ac7e0a 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -178,13 +178,13 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); + app.UseAbpRequestLocalization(); app.UseAuthentication(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseSwagger(); app.UseSwaggerUI(options => diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs index 7a40f2eef5..06f52defb1 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs @@ -168,6 +168,7 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); + app.UseAbpRequestLocalization(); app.UseAuthentication(); app.UseJwtTokenMiddleware(); @@ -178,7 +179,6 @@ namespace MyCompanyName.MyProjectName app.UseIdentityServer(); app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseSwagger(); app.UseSwaggerUI(options => diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs index 0a87b21a31..e3d6d5aa67 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs @@ -135,7 +135,7 @@ namespace MyCompanyName.MyProjectName }); }); } - + public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); @@ -154,6 +154,7 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); + app.UseAbpRequestLocalization(); app.UseAuthentication(); if (MultiTenancyConsts.IsEnabled) { @@ -161,7 +162,6 @@ namespace MyCompanyName.MyProjectName } app.UseIdentityServer(); app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseAuditing(); app.UseAbpSerilogEnrichers(); app.UseConfiguredEndpoints(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs index 0809a70cc4..86738e3e1b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs @@ -67,7 +67,7 @@ namespace MyCompanyName.MyProjectName.Web ); }); } - + public override void ConfigureServices(ServiceConfigurationContext context) { var hostingEnvironment = context.Services.GetHostingEnvironment(); @@ -225,6 +225,7 @@ namespace MyCompanyName.MyProjectName.Web app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); app.UseAuthentication(); if (MultiTenancyConsts.IsEnabled) @@ -234,9 +235,6 @@ namespace MyCompanyName.MyProjectName.Web app.UseAuthorization(); - - app.UseAbpRequestLocalization(); - app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs index 8330951dfc..541a2c5e3a 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -204,6 +204,7 @@ namespace MyCompanyName.MyProjectName.Web app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); app.UseAuthentication(); app.UseJwtTokenMiddleware(); @@ -213,7 +214,6 @@ namespace MyCompanyName.MyProjectName.Web } app.UseIdentityServer(); app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs index 20f7e793c2..edde134027 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs @@ -89,10 +89,10 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); app.UseAuthentication(); app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.Use(async (ctx, next) => { @@ -110,4 +110,4 @@ namespace MyCompanyName.MyProjectName app.UseConfiguredEndpoints(); } } -} \ No newline at end of file +} diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index e425512797..8de889e88e 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -165,13 +165,13 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); + app.UseAbpRequestLocalization(); app.UseAuthentication(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs index 9d0cb8397c..4bfd5f689a 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs @@ -186,6 +186,7 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); + app.UseAbpRequestLocalization(); app.UseAuthentication(); app.UseJwtTokenMiddleware(); if (MultiTenancyConsts.IsEnabled) @@ -194,7 +195,6 @@ namespace MyCompanyName.MyProjectName } app.UseIdentityServer(); app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs index 385b840e75..6d16829e25 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs @@ -73,7 +73,7 @@ namespace MyCompanyName.MyProjectName ); }); } - + public override void ConfigureServices(ServiceConfigurationContext context) { var hostingEnvironment = context.Services.GetHostingEnvironment(); @@ -234,6 +234,7 @@ namespace MyCompanyName.MyProjectName app.UseHttpsRedirection(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); app.UseAuthentication(); if (MultiTenancyConsts.IsEnabled) @@ -243,7 +244,6 @@ namespace MyCompanyName.MyProjectName app.UseAuthorization(); - app.UseAbpRequestLocalization(); app.UseSwagger(); app.UseSwaggerUI(options => diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs index 48b0667350..57b77e5f50 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs @@ -127,6 +127,7 @@ namespace MyCompanyName.MyProjectName app.UseHttpsRedirection(); app.UseVirtualFiles(); app.UseRouting(); + app.UseAbpRequestLocalization(); app.UseAuthentication(); app.UseAuthorization(); if (MultiTenancyConsts.IsEnabled) @@ -140,7 +141,6 @@ namespace MyCompanyName.MyProjectName options.SwaggerEndpoint("/swagger/v1/swagger.json", "Support APP API"); }); - app.UseAbpRequestLocalization(); app.UseAuditing(); app.UseAbpSerilogEnrichers(); app.UseConfiguredEndpoints(); From 5b91c8954beae73903cb9a405ffe42d5374b86fa Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 13 May 2020 18:13:05 +0800 Subject: [PATCH 020/110] Check whether MethodInfo's DeclaringType has auditing disabled. --- .../Volo/Abp/Auditing/AuditingHelper.cs | 19 ++++++++++--------- .../Auditing/AuditingInterceptorRegistrar.cs | 10 +++++----- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs index e53ba5b9a6..4c3ede4ca8 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs @@ -36,7 +36,7 @@ namespace Volo.Abp.Auditing IClock clock, IAuditingStore auditingStore, ILogger logger, - IServiceProvider serviceProvider, + IServiceProvider serviceProvider, ICorrelationIdProvider correlationIdProvider) { Options = options.Value; @@ -77,9 +77,10 @@ namespace Volo.Abp.Auditing var classType = methodInfo.DeclaringType; if (classType != null) { - if (AuditingInterceptorRegistrar.ShouldAuditTypeByDefault(classType)) + var shouldAudit = AuditingInterceptorRegistrar.ShouldAuditTypeByDefaultOrNull(classType); + if (shouldAudit != null) { - return true; + return shouldAudit.Value; } } @@ -123,7 +124,7 @@ namespace Volo.Abp.Auditing return defaultValue; } - + public virtual AuditLogInfo CreateAuditLogInfo() { var auditInfo = new AuditLogInfo @@ -147,8 +148,8 @@ namespace Volo.Abp.Auditing public virtual AuditLogActionInfo CreateAuditLogAction( AuditLogInfo auditLog, - Type type, - MethodInfo method, + Type type, + MethodInfo method, object[] arguments) { return CreateAuditLogAction(auditLog, type, method, CreateArgumentsDictionary(method, arguments)); @@ -156,8 +157,8 @@ namespace Volo.Abp.Auditing public virtual AuditLogActionInfo CreateAuditLogAction( AuditLogInfo auditLog, - Type type, - MethodInfo method, + Type type, + MethodInfo method, IDictionary arguments) { var actionInfo = new AuditLogActionInfo @@ -240,4 +241,4 @@ namespace Volo.Abp.Auditing return dictionary; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptorRegistrar.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptorRegistrar.cs index a407335f3d..54607a55ca 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptorRegistrar.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptorRegistrar.cs @@ -21,8 +21,8 @@ namespace Volo.Abp.Auditing { return false; } - - if (ShouldAuditTypeByDefault(type)) + + if (ShouldAuditTypeByDefaultOrNull(type) == true) { return true; } @@ -36,7 +36,7 @@ namespace Volo.Abp.Auditing } //TODO: Move to a better place - public static bool ShouldAuditTypeByDefault(Type type) + public static bool? ShouldAuditTypeByDefaultOrNull(Type type) { //TODO: In an inheritance chain, it would be better to check the attributes on the top class first. @@ -55,7 +55,7 @@ namespace Volo.Abp.Auditing return true; } - return false; + return null; } } -} \ No newline at end of file +} From ba248a5a4a4f4088c89c7a6a3bb8c06e8000b33c Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 13:36:05 +0300 Subject: [PATCH 021/110] refactor: remove localization property from environment #3791 --- .../apps/dev-app/src/environments/environment.prod.ts | 3 --- .../apps/dev-app/src/environments/environment.ts | 3 --- .../app/angular/src/environments/environment.prod.ts | 11 ++++------- templates/app/angular/src/environments/environment.ts | 11 ++++------- .../angular/src/environments/environment.prod.ts | 3 --- .../module/angular/src/environments/environment.ts | 3 --- 6 files changed, 8 insertions(+), 26 deletions(-) diff --git a/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts b/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts index 5b42ebace3..ae0e6aba2c 100644 --- a/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts +++ b/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts @@ -19,7 +19,4 @@ export const environment = { url: 'https://localhost:44305', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, }; diff --git a/npm/ng-packs/apps/dev-app/src/environments/environment.ts b/npm/ng-packs/apps/dev-app/src/environments/environment.ts index ca462ff043..e678b55a74 100644 --- a/npm/ng-packs/apps/dev-app/src/environments/environment.ts +++ b/npm/ng-packs/apps/dev-app/src/environments/environment.ts @@ -19,7 +19,4 @@ export const environment = { url: 'https://localhost:44305', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, }; diff --git a/templates/app/angular/src/environments/environment.prod.ts b/templates/app/angular/src/environments/environment.prod.ts index bf1c0ea488..ada2b74c97 100644 --- a/templates/app/angular/src/environments/environment.prod.ts +++ b/templates/app/angular/src/environments/environment.prod.ts @@ -2,7 +2,7 @@ export const environment = { production: true, application: { name: 'MyProjectName', - logoUrl: '' + logoUrl: '', }, oAuthConfig: { issuer: 'https://localhost:44305', @@ -11,14 +11,11 @@ export const environment = { scope: 'MyProjectName', showDebugInformation: true, oidc: false, - requireHttps: true + requireHttps: true, }, apis: { default: { - url: 'https://localhost:44305' - } + url: 'https://localhost:44305', + }, }, - localization: { - defaultResourceName: 'MyProjectName' - } }; diff --git a/templates/app/angular/src/environments/environment.ts b/templates/app/angular/src/environments/environment.ts index 6f2a182746..d3676e2d08 100644 --- a/templates/app/angular/src/environments/environment.ts +++ b/templates/app/angular/src/environments/environment.ts @@ -2,7 +2,7 @@ export const environment = { production: false, application: { name: 'MyProjectName', - logoUrl: '' + logoUrl: '', }, oAuthConfig: { issuer: 'https://localhost:44305', @@ -11,14 +11,11 @@ export const environment = { scope: 'MyProjectName', showDebugInformation: true, oidc: false, - requireHttps: true + requireHttps: true, }, apis: { default: { - url: 'https://localhost:44305' - } + url: 'https://localhost:44305', + }, }, - localization: { - defaultResourceName: 'MyProjectName' - } }; diff --git a/templates/module/angular/src/environments/environment.prod.ts b/templates/module/angular/src/environments/environment.prod.ts index ce43f45c24..23d41cf375 100644 --- a/templates/module/angular/src/environments/environment.prod.ts +++ b/templates/module/angular/src/environments/environment.prod.ts @@ -18,7 +18,4 @@ export const environment = { url: 'https://localhost:44300', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, }; diff --git a/templates/module/angular/src/environments/environment.ts b/templates/module/angular/src/environments/environment.ts index f1c21c40ac..32d3aa3d4c 100644 --- a/templates/module/angular/src/environments/environment.ts +++ b/templates/module/angular/src/environments/environment.ts @@ -18,7 +18,4 @@ export const environment = { url: 'https://localhost:44300', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, }; From 99c5fb6a5c4e0d0ffdb254fdd39636a9141c9b2b Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 13:36:42 +0300 Subject: [PATCH 022/110] refactor(core): make localization in environment optional #3791 --- npm/ng-packs/packages/core/src/lib/models/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/config.ts b/npm/ng-packs/packages/core/src/lib/models/config.ts index 0f7405380b..2169f75643 100644 --- a/npm/ng-packs/packages/core/src/lib/models/config.ts +++ b/npm/ng-packs/packages/core/src/lib/models/config.ts @@ -16,7 +16,7 @@ export namespace Config { hmr?: boolean; oAuthConfig: AuthConfig; apis: Apis; - localization: { defaultResourceName: string }; + localization?: { defaultResourceName?: string }; } export interface Application { From c79944b3a8d81301c8a94a01fd21183bd8d4b458 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 13:37:17 +0300 Subject: [PATCH 023/110] feat(core): add isLocalized method to LocalizationService #3791 --- .../src/lib/services/localization.service.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index 0e89732e56..95ea86e251 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -1,11 +1,12 @@ import { Injectable, NgZone, Optional, SkipSelf } from '@angular/core'; import { ActivatedRouteSnapshot, Router } from '@angular/router'; -import { Store, Actions, ofActionSuccessful } from '@ngxs/store'; +import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; import { noop, Observable } from 'rxjs'; +import { SetLanguage } from '../actions/session.actions'; +import { ApplicationConfiguration } from '../models/application-configuration'; +import { Config } from '../models/config'; import { ConfigState } from '../states/config.state'; import { registerLocale } from '../utils/initial-utils'; -import { Config } from '../models/config'; -import { SetLanguage } from '../actions/session.actions'; type ShouldReuseRoute = (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) => boolean; @@ -75,4 +76,31 @@ export class LocalizationService { instant(key: string | Config.LocalizationWithDefault, ...interpolateParams: string[]): string { return this.store.selectSnapshot(ConfigState.getLocalization(key, ...interpolateParams)); } + + isLocalized(key, sourceName) { + if (sourceName === '_') { + //A convention to suppress the localization + return true; + } + + const localization = this.store.selectSnapshot( + ConfigState.getOne('localization'), + ) as ApplicationConfiguration.Localization; + sourceName = sourceName || localization.defaultResourceName; + if (!sourceName) { + return false; + } + + const source = localization.values[sourceName]; + if (!source) { + return false; + } + + const value = source[key]; + if (value === undefined) { + return false; + } + + return true; + } } From ff09572736f2e0fe4ec82d67c6044f89e89d1fb9 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 13:38:51 +0300 Subject: [PATCH 024/110] refactor(core): improve the getLocalization selector of config state #3791 --- .../core/src/lib/states/config.state.ts | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/states/config.state.ts b/npm/ng-packs/packages/core/src/lib/states/config.state.ts index 546b3d0b1e..25f2a69fbe 100644 --- a/npm/ng-packs/packages/core/src/lib/states/config.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/config.state.ts @@ -155,33 +155,45 @@ export class ConfigState { } const keys = key.split('::') as string[]; - const selector = createSelector([ConfigState], (state: Config.State) => { - if (!state.localization) return defaultValue || key; - - const defaultResourceName = snq(() => state.environment.localization.defaultResourceName); - if (keys[0] === '') { - if (!defaultResourceName) { - throw new Error( - `Please check your environment. May you forget set defaultResourceName? - Here is the example: - { production: false, - localization: { - defaultResourceName: 'MyProjectName' - } - }`, - ); - } + const selector = createSelector([ConfigState], (state: Config.State): string => { + const warn = (message: string) => { + if (!state.environment.production) console.warn(message); + }; - keys[0] = defaultResourceName; + if (keys.length < 2) { + warn('The localization source separator (::) not found.'); + return key as string; } + if (!state.localization) return defaultValue || keys[1]; - let localization = (keys as any).reduce((acc, val) => { - if (acc) { - return acc[val]; - } + const sourceName = + keys[0] || + snq(() => state.environment.localization.defaultResourceName) || + state.localization.defaultResourceName; + const sourceKey = keys[1]; - return undefined; - }, state.localization.values); + if (sourceName === '_') { + return sourceKey; + } + + if (!sourceName) { + warn( + 'Localization source name is not specified and the defaultResourceName was not defined!', + ); + + return sourceKey; + } + + const source = state.localization.values[sourceName]; + if (!source) { + warn('Could not find localization source: ' + sourceName); + return sourceKey; + } + + let localization = source[sourceKey]; + if (typeof localization === 'undefined') { + return sourceKey; + } interpolateParams = interpolateParams.filter(params => params != null); if (localization && interpolateParams && interpolateParams.length) { @@ -191,7 +203,7 @@ export class ConfigState { } if (typeof localization !== 'string') localization = ''; - return localization || defaultValue || key; + return localization || defaultValue || (key as string); }); return selector; From 5368a09afb4526d9e414d75337e59b9f8957b252 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 13:47:20 +0300 Subject: [PATCH 025/110] fix: lint error --- .../packages/core/src/lib/services/localization.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index 95ea86e251..8adef08f46 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -79,7 +79,7 @@ export class LocalizationService { isLocalized(key, sourceName) { if (sourceName === '_') { - //A convention to suppress the localization + // A convention to suppress the localization return true; } From d403d9424c92072ecf599eef9287e03e0e0d9c45 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 14:11:04 +0300 Subject: [PATCH 026/110] test: fix testing errors --- .../packages/core/src/lib/states/config.state.ts | 10 +++++----- .../packages/core/src/lib/tests/config.state.spec.ts | 11 ++++------- .../core/src/lib/tests/localization.service.spec.ts | 8 +------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/states/config.state.ts b/npm/ng-packs/packages/core/src/lib/states/config.state.ts index 25f2a69fbe..3527e5445d 100644 --- a/npm/ng-packs/packages/core/src/lib/states/config.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/config.state.ts @@ -162,7 +162,7 @@ export class ConfigState { if (keys.length < 2) { warn('The localization source separator (::) not found.'); - return key as string; + return defaultValue || (key as string); } if (!state.localization) return defaultValue || keys[1]; @@ -173,7 +173,7 @@ export class ConfigState { const sourceKey = keys[1]; if (sourceName === '_') { - return sourceKey; + return defaultValue || sourceKey; } if (!sourceName) { @@ -181,18 +181,18 @@ export class ConfigState { 'Localization source name is not specified and the defaultResourceName was not defined!', ); - return sourceKey; + return defaultValue || sourceKey; } const source = state.localization.values[sourceName]; if (!source) { warn('Could not find localization source: ' + sourceName); - return sourceKey; + return defaultValue || sourceKey; } let localization = source[sourceKey]; if (typeof localization === 'undefined') { - return sourceKey; + return defaultValue || sourceKey; } interpolateParams = interpolateParams.filter(params => params != null); diff --git a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts index aa3740218c..e823328f73 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts @@ -272,7 +272,7 @@ describe('ConfigState', () => { ); expect(ConfigState.getLocalization('AbpIdentity::NoIdentity')(CONFIG_STATE_DATA)).toBe( - 'AbpIdentity::NoIdentity', + 'NoIdentity', ); expect( @@ -287,18 +287,15 @@ describe('ConfigState', () => { )(CONFIG_STATE_DATA), ).toBe('first and second do not match.'); - try { + expect( ConfigState.getLocalization('::Test')({ ...CONFIG_STATE_DATA, environment: { ...CONFIG_STATE_DATA.environment, localization: {} as any, }, - }); - expect(false).toBeTruthy(); // fail - } catch (error) { - expect((error as Error).message).toContain('Please check your environment'); - } + }), + ).toBe('Test'); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts index 5600e3aa39..8c5b859f9f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts @@ -44,13 +44,7 @@ describe('LocalizationService', () => { describe('#instant', () => { it('should be return a localization', () => { - store.selectSnapshot.andCallFake( - (selector: (state: any, ...states: any[]) => Observable) => { - return selector({ - ConfigState: { getLocalization: (keys, ...interpolateParams) => keys }, - }); - }, - ); + store.selectSnapshot.andReturn('AbpTest'); expect(service.instant('AbpTest')).toBe('AbpTest'); }); From e28db049c9b232cfa5ccbab87a267e719896006d Mon Sep 17 00:00:00 2001 From: ChangYinShung Date: Thu, 14 May 2020 00:04:15 +0800 Subject: [PATCH 027/110] add Blog Module's Localize for zh-hant 1.add ShareOn , TitleLengthWarning in zh-hant --- .../Volo/Blogging/Localization/Resources/zh-Hant.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/zh-Hant.json b/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/zh-Hant.json index fcafc171de..b5781d305e 100644 --- a/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/zh-Hant.json +++ b/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/zh-Hant.json @@ -42,6 +42,8 @@ "CreationTime": "建立時間", "Description": "描述", "Blogs": "部落格", - "Tags": "標籤" + "Tags": "標籤", + "ShareOn": "分享在", + "TitleLengthWarning": "為了優化搜索引擎,標題建議保持在60個字元以內" } } \ No newline at end of file From 8d2dc2fa65f36758174f9b58cf84d8578ef945bf Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 19:43:13 +0300 Subject: [PATCH 028/110] fix: set html lang when the language change --- .../packages/core/src/lib/services/localization.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index 0e89732e56..a3b452de11 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -33,9 +33,10 @@ export class LocalizationService { } private listenToSetLanguage() { - this.actions - .pipe(ofActionSuccessful(SetLanguage)) - .subscribe(({ payload }) => this.registerLocale(payload)); + this.actions.pipe(ofActionSuccessful(SetLanguage)).subscribe(({ payload }) => { + this.registerLocale(payload); + document.documentElement.lang = payload; + }); } setRouteReuse(reuse: ShouldReuseRoute) { From d22ba8a91967836538cac8436503197286841ce7 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 20:44:16 +0300 Subject: [PATCH 029/110] fix(core): set language when call app config api --- .../packages/core/src/lib/actions/session.actions.ts | 2 +- .../packages/core/src/lib/services/localization.service.ts | 7 +++---- npm/ng-packs/packages/core/src/lib/states/config.state.ts | 6 +++++- npm/ng-packs/packages/core/src/lib/states/session.state.ts | 7 +++++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts b/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts index 463e9a202d..a7d2cd9f3c 100644 --- a/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts +++ b/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts @@ -2,7 +2,7 @@ import { ABP } from '../models'; export class SetLanguage { static readonly type = '[Session] Set Language'; - constructor(public payload: string) {} + constructor(public payload: string, public dispatchAppConfiguration?: boolean) {} } export class SetTenant { static readonly type = '[Session] Set Tenant'; diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index a3b452de11..0e89732e56 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -33,10 +33,9 @@ export class LocalizationService { } private listenToSetLanguage() { - this.actions.pipe(ofActionSuccessful(SetLanguage)).subscribe(({ payload }) => { - this.registerLocale(payload); - document.documentElement.lang = payload; - }); + this.actions + .pipe(ofActionSuccessful(SetLanguage)) + .subscribe(({ payload }) => this.registerLocale(payload)); } setRouteReuse(reuse: ShouldReuseRoute) { diff --git a/npm/ng-packs/packages/core/src/lib/states/config.state.ts b/npm/ng-packs/packages/core/src/lib/states/config.state.ts index 546b3d0b1e..29535be0ab 100644 --- a/npm/ng-packs/packages/core/src/lib/states/config.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/config.state.ts @@ -219,9 +219,13 @@ export class ConfigState { defaultLang = defaultLang.split(';')[0]; } + document.documentElement.setAttribute( + 'lang', + configuration.localization.currentCulture.cultureName, + ); return this.store.selectSnapshot(SessionState.getLanguage) ? of(null) - : dispatch(new SetLanguage(defaultLang)); + : dispatch(new SetLanguage(defaultLang, false)); }), catchError(err => { dispatch(new RestOccurError(new HttpErrorResponse({ status: 0, error: err }))); diff --git a/npm/ng-packs/packages/core/src/lib/states/session.state.ts b/npm/ng-packs/packages/core/src/lib/states/session.state.ts index 6acbf1d60d..0c81a46c58 100644 --- a/npm/ng-packs/packages/core/src/lib/states/session.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/session.state.ts @@ -69,12 +69,15 @@ export class SessionState { } @Action(SetLanguage) - setLanguage({ patchState, dispatch }: StateContext, { payload }: SetLanguage) { + setLanguage( + { patchState, dispatch }: StateContext, + { payload, dispatchAppConfiguration = true }: SetLanguage, + ) { patchState({ language: payload, }); - return dispatch(new GetAppConfiguration()); + if (dispatchAppConfiguration) return dispatch(new GetAppConfiguration()); } @Action(SetTenant) From d47c7e686ab1ec8d3a5d4fa6f519ac407b146d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 23:40:32 +0300 Subject: [PATCH 030/110] Revise text template document --- docs/en/Text-Templating.md | 221 +++++++++++++++++++++++++----- docs/en/images/hello-template.png | Bin 0 -> 64613 bytes 2 files changed, 185 insertions(+), 36 deletions(-) create mode 100644 docs/en/images/hello-template.png diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 5451b1f45e..e421f6a8c6 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -1,22 +1,44 @@ # Text Templating -In ABP Framework, `text template` is a mixture of text blocks and control logic that can generate a `string` result. An open source package [Scriban](https://github.com/lunet-io/scriban) is used for the control logic and [Abp.Localization](Localization.md) is used to make content easily localizable. The generated string can be text of any kind, such as a web page, an e-mail content etc. +## Introduction -> **stored content** -```html -
    -{{~ for $i in 0..3 ~}} -
  1. {{ L "WelcomeMessage" }}
  2. -{{~ endfor ~}} -
-``` -> **result** (for en culture) -``` -1. Welcome to the abp.io! -2. Welcome to the abp.io! -3. Welcome to the abp.io! -4. Welcome to the abp.io! -``` +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 => RENDERED CONTENT + +It is very similar to an ASP.NET Core Razor View (or Page): + +RAZOR VIEW (or PAGE) + MODEL => HTML CONTENT + +### 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. ## Installation @@ -52,21 +74,151 @@ public class YourModule : AbpModule } ```` -## Logic +## 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: + +![hello-template](images/hello-template.png) + +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(options => +{ + options.FileSets.AddEmbedded("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: + +````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 an anonymous object for simple cases: + +````csharp +var result = await _templateRenderer.RenderAsync( + "Hello", //the template name + 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 template as a convention. + + -A Text Template is a combination of two object. -- `TemplateDefinition` -- `TemplateContent` + + + + + + + + + + + + + + + + + + + + + +## Logic + +A Text Template is a combination of two parts: template definition and template content. ### Template Definition -Template Definition is an object that contains some information about your `Text Templates`. Template Definition object contains the following properties. +Template Definition is an object that contains some information about your text templates. Template Definition object contains the following properties. -- `Name` *(string)* -- `IsLayout` *(boolean)* +- `Name` *(string)*: Unique name of the template. It is then used to render the template. +- `IsLayout` *(boolean)*: - `Layout` *(string)* contains the name of layout template -- `LocalizationResource` *(Type)* Inline Localized -- `IsInlineLocalized`*(boolean)* describes that the template is inline localized or not +- `LocalizationResource` *(Type)* The localization resource type that is used if this template is inline localized. +- `IsInlineLocalized` *(boolean)* describes that the template is inline localized or not - `DefaultCultureName` *(string)* defines the default culture for the template ### Template Content @@ -83,23 +235,20 @@ This is a simple content for your templates. For default, template contents stor - ``` ### Localization -You can localize your Text Templates by choosing two different method. +You can localize your Text Templates by choosing two different methods. - `Inline Localization` - `Multiple Content Localization` #### Inline Localization -Inline localized Text Templates is using only one content resource, and it is using the [Abp.Localization](Localization.md) to get content in different languages/cultures. +An inline localized text template is using only one content resource, and it is using the [Abp.Localization](Localization.md) to get content in different languages/cultures. -> Example Inline Localized Text Template: -> -> ForgotPasswordEmail.tpl +Example Inline Localized Text Template content: ```html

{{L "PasswordReset"}}

@@ -143,13 +292,13 @@ You can store your Text Templates for any culture in different content resource. ### Layout System -It is typical to use the same layout for some different Text Templates. So, you can define a layout template. +It is typical to use the same layout for some different text templates. So, you can define a layout template. A text template can be layout for different text templates and also a text template may use a layout. A layout Text Template must have `{{content}}` area to render the child content. _(just like the `RenderBody()` in the MVC)_ -> Example Email Layout Text Template +Example Email Layout Text Template ```html @@ -163,7 +312,7 @@ A layout Text Template must have `{{content}}` area to render the child content. ``` -## Definition a Text Template +## Definition of a Text Template First of all, create a class that inherited from `TemplateDefinitionProvider` abstract class and create `Define` method that derived from the base class. @@ -228,7 +377,7 @@ As you see in the given example, all Text Templates are added with `(ITemplateDe When one template is registered, it is easy to render and get the result with `ITemplateRenderer` service. -`ITemplateRenderer` service has one method that named `RenderAsync` and to render your content and it is requires some parametres. +`ITemplateRenderer` service has one method that named `RenderAsync` and to render your content and it is requires some parameters. - `templateName` (_string_) - `model` (_object_) @@ -283,7 +432,7 @@ It has three method that you can get your Template Definitions. `Get` and `GetOrNull` requires a string parameter that name of template definition. `Get` will throw error when it is not exist but `GetOrNull` returns `null`. `GetAll` returns you all registered template definitions. - + ## Template Content Contributor You can store your `Template Contents` in any resource. To make it, just create a class that implements `ITemplateContentContributor` interface. diff --git a/docs/en/images/hello-template.png b/docs/en/images/hello-template.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3ce2816a8a26253d78c150a5537b992d86d927 GIT binary patch literal 64613 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

J{oCDaPtz0 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 literal 0 HcmV?d00001 From 05c59b6fd346f442605adf6f3db2213909b2b322 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Thu, 14 May 2020 00:36:34 +0300 Subject: [PATCH 031/110] test: fix testing error --- npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts index e823328f73..189a9ab6d9 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts @@ -306,6 +306,7 @@ describe('ConfigState', () => { const configuration = { setting: { values: { 'Abp.Localization.DefaultLanguage': 'tr;TR' } }, + localization: { currentCulture: {} }, }; const res$ = new ReplaySubject(1); @@ -324,7 +325,7 @@ describe('ConfigState', () => { timer(0).subscribe(() => { expect(patchStateArg).toEqual(configuration); expect(dispatchArg instanceof SetLanguage).toBeTruthy(); - expect(dispatchArg).toEqual({ payload: 'tr' }); + expect(dispatchArg).toEqual({ payload: 'tr', dispatchAppConfiguration: false }); done(); }); }); From d2c199da628eabd31053d502e7649f72b5ced68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 00:41:29 +0300 Subject: [PATCH 032/110] Revise the text templating --- docs/en/Text-Templating.md | 116 +++++++++++++++++++--- docs/en/images/multiple-file-template.png | Bin 0 -> 35207 bytes 2 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 docs/en/images/multiple-file-template.png diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index e421f6a8c6..62ecaa6bd5 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -125,7 +125,9 @@ Configure(options => ## Rendering the Template -`ITemplateRenderer` service is used to render a template content. Example: +`ITemplateRenderer` service is used to render a template content. + +### Example: Rendering a Simple Template ````csharp public class HelloDemo : ITransientDependency @@ -165,11 +167,11 @@ Hello John :) ### Anonymous Model -While it is suggested to create model classes for the templates, it would be practical (and possible) to use an anonymous object for simple cases: +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", //the template name + "Hello", new { Name = "John" @@ -179,25 +181,115 @@ var result = await _templateRenderer.RenderAsync( In this case, we haven't created a model class, but created an anonymous object as the model. -### PascalCase vs CamelCase +### 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: + +```` +{{L "ResetMyPassword"}} +```` + +`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: -PascalCase property names (like `UserName`) is used as camelCase (like `userName`) in the template as a convention. +````csharp +Click here to reset your password +```` +> 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 +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: +![multiple-file-template](D:\Github\abp\docs\en\images\multiple-file-template.png) +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" +); +```` @@ -251,13 +343,7 @@ An inline localized text template is using only one content resource, and it is Example Inline Localized Text Template content: ```html -

{{L "PasswordReset"}}

- -

{{L "PasswordResetInfoInEmail"}}

- - +{{L "ResetMyPassword"}} ``` #### Multiple Content Localization @@ -396,6 +482,12 @@ When one template is registered, it is easy to render and get the result with `I `globalContext` = TODO + + + + + + ## Template Content Provider When you want to get your stored template content you can use `ITemplateContentProvider`. diff --git a/docs/en/images/multiple-file-template.png b/docs/en/images/multiple-file-template.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e7327301a0bb1edb9c5fd6202650556d996004 GIT binary patch literal 35207 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 Date: Thu, 14 May 2020 00:49:24 +0300 Subject: [PATCH 033/110] test: add a dismissible option test to confirmation.service.spec --- .../src/lib/tests/confirmation.service.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts index e70b88061f..1413e034d7 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts @@ -105,6 +105,14 @@ describe('ConfirmationService', () => { selectConfirmationElement('button#cancel').click(); }); + + test('should not call the listenToEscape method when dismissible is false', () => { + const spy = spyOn(service as any, 'listenToEscape'); + + service.info('', '', { dismissible: false }); + + expect(spy.calls.count()).toBe(0); + }); }); function clearElements(selector = '.confirmation') { From b7edf2f96ae3da6998119d889d66e2458e2143c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 00:54:12 +0300 Subject: [PATCH 034/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 180 ++++++++----------------------------- 1 file changed, 35 insertions(+), 145 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 62ecaa6bd5..84d1fff3d1 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -245,7 +245,7 @@ You will see the localized result: 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 +#### 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. @@ -291,102 +291,17 @@ var result = await _templateRenderer.RenderAsync( ); ```` +## 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: - - - -## Logic - -A Text Template is a combination of two parts: template definition and template content. -### Template Definition - -Template Definition is an object that contains some information about your text templates. Template Definition object contains the following properties. - -- `Name` *(string)*: Unique name of the template. It is then used to render the template. -- `IsLayout` *(boolean)*: -- `Layout` *(string)* contains the name of layout template -- `LocalizationResource` *(Type)* The localization resource type that is used if this template is inline localized. -- `IsInlineLocalized` *(boolean)* describes that the template is inline localized or not -- `DefaultCultureName` *(string)* defines the default culture for the template - -### Template Content - -This is a simple content for your templates. For default, template contents stored as `Virtual File`. - -> Example: ForgotPasswordEmail.tpl - -```html -

{{L "PasswordReset"}}

- -

{{L "PasswordResetInfoInEmail"}}

- -
-``` - -### Localization - -You can localize your Text Templates by choosing two different methods. - -- `Inline Localization` -- `Multiple Content Localization` - -#### Inline Localization - -An inline localized text template is using only one content resource, and it is using the [Abp.Localization](Localization.md) to get content in different languages/cultures. - -Example Inline Localized Text Template content: - -```html -{{L "ResetMyPassword"}} -``` - -#### Multiple Content Localization - -You can store your Text Templates for any culture in different content resource. - -> Example Multiple Content Localization - -> ForgotPasswordEmail / en.tpl - -```html -

Reset Your Password

- -

Hello, this is a password changing email.

- - -``` - -> ForgotPasswordEmail / tr.tpl - -```html -

Şifrenizi Değiştirin

- -

Merhaba, bu bir şifre yenileme e postasıdır.

- - -``` - -### Layout System - -It is typical to use the same layout for some different text templates. So, you can define a layout template. - -A text template can be layout for different text templates and also a text template may use a layout. - -A layout Text Template must have `{{content}}` area to render the child content. _(just like the `RenderBody()` in the MVC)_ - -Example Email Layout Text Template - -```html +````xml @@ -396,67 +311,42 @@ Example Email Layout Text Template {{content}} -``` - -## Definition of a Text Template - -First of all, create a class that inherited from `TemplateDefinitionProvider` abstract class and create `Define` method that derived from the base class. - -`Define` method requires a context that is `ITemplateDefinitionContext`. This `context` is a storage for template definitions and we will add our template definitions to the context. - -> **NOTE!** For default, ABP uses **Virtual File System** for text templates. Do not forget to register your files as an `Embedded Resource`. Please check the [Virtual File System Documentation](Virtual-File-System.md) for more details. +```` -> All given examples are for `Virtual File Text Template Definitions`. +* A layout template must have a **{{content}}** part as a place holder for the rendered child content. -```csharp -public class MyTemplateDefinitionProvider : TemplateDefinitionProvider - { - public override void Define(ITemplateDefinitionContext context) - { - // Layout Text Template - context.Add( - new TemplateDefinition( - name: "MySampleTemplateLayout", // Template Definition Name - isLayout: true - ).WithVirtualFilePath("/SampleTemplates/SampleTemplateLayout.tpl", true) - ); - - // Inline Localized Text Template - context.Add( - new TemplateDefinition( - name: "ForgotPasswordEmail", - localizationResource: typeof(MyLocalizationResource), - layout: TestTemplates.TestTemplateLayout1 - ).WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail.tpl", true) - ); - - // Multiple File Localized Text Template - context.Add( - new TemplateDefinition( - name: "ForgotPasswordEmail", - defaultCultureName: "en" - ).WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail", false) - ); - } - } -``` +The register your template in the template definition provider: -As you see in the given example, all Text Templates are added with `(ITemplateDefinitionContext)context.Add` method. This method requires a `TemplateDefinition` object. Then we call `WithVirtualFilePath` method with chaining for the describe where is the virtual files. +````csharp +context.Add( + new TemplateDefinition( + "EmailLayout", + isLayout: true //SET isLayout! + ).WithVirtualFilePath( + "/Demos/EmailLayout/EmailLayout.tpl", + isInlineLocalized: true + ) +); +```` -`WithVirtualFilePath` is requires one `tpl` file path for the `Inline Localized` Text Templates. If your Text Tempalte is `Multi Localized` you should create a folder and store each different culture files under that. So you can send the folder path as a parameter to `WithVirtualFilePath`. +Now, you can use this template as the layout of any other template: -> Inline Localized File +````csharp +context.Add( + new TemplateDefinition( + name: "WelcomeEmail", + defaultCultureName: "en", + layout: "EmailLayout" //Set the LAYOUT + ).WithVirtualFilePath( + "/Demos/WelcomeEmail/Templates", + isInlineLocalized: false + ) +); +```` -``` -/ Folder / ForgotPasswordEmail.tpl -``` +## Global Context -> Multi Content Localization -``` -/ Folder / ForgotPasswordEmail / en.tpl -/ Folder / ForgotPasswordEmail / tr.tpl -``` ## Rendering From 3c6f1ebf8c163b4750d86bca4baeda67895a0bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 01:09:10 +0300 Subject: [PATCH 035/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 44 ++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 84d1fff3d1..c722d2650e 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -10,6 +10,8 @@ It is very similar to an ASP.NET Core Razor View (or Page): RAZOR VIEW (or PAGE) + MODEL => HTML CONTENT +You can use the rendered output for any purpose, like sending emails or preparing some reports. + ### Example Here, a simple template: @@ -346,35 +348,31 @@ context.Add( ## 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}} +```` -## Rendering - -When one template is registered, it is easy to render and get the result with `ITemplateRenderer` service. - -`ITemplateRenderer` service has one method that named `RenderAsync` and to render your content and it is requires some parameters. - -- `templateName` (_string_) -- `model` (_object_) -- `cultureName` (_string_) -- `globalContext` (_dictionary_) - -`templateName` is exactly same with Template Definition Name. - -`model` is a dynamic object. This is using to put dynamic data into template. For more information, please look at [Scriban Documentation](https://github.com/lunet-io/scriban). - -`cultureName` is your destination rendering culture. When it is not exist, it will use the default culture. - -> If `cultureName` has a language tag it will try to find exact culture with tag, if it is not exist it will use the language family. - -> Example: If you try to render content with _"es-MX"_ it will search your template with _"es-MX"_ culture, when it fails to find, it will try to render _"es"_ culture content. If still can't find it will render the default culture content that you defined. - -`globalContext` = TODO - +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 + { + {"myGlobalObject", "TEST VALUE"} + } +); +```` +The rendering result will be: +```` +A global object value: TEST VALUE +```` From 4226f18186de85c3b521002dfaa125f0f4fcca3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 01:25:32 +0300 Subject: [PATCH 036/110] Finalize the Text-Templating document. --- docs/en/Text-Templating.md | 77 +++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index c722d2650e..db2f25a807 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -4,11 +4,11 @@ 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 => RENDERED CONTENT +***TEMPLATE + MODEL ==render==> RENDERED CONTENT*** It is very similar to an ASP.NET Core Razor View (or Page): -RAZOR VIEW (or PAGE) + MODEL => HTML CONTENT +*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT* You can use the rendered output for any purpose, like sending emails or preparing some reports. @@ -374,47 +374,72 @@ 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 +### Template Content Provider -When you want to get your stored template content you can use `ITemplateContentProvider`. +`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` has one method that named `GetContentOrNullAsync` with two different overriding, and it returns you a string of template content or null. (**without rendering**) +> `ITemplateContentProvider` is internally used by the `ITemplateRenderer` to get the raw template contents. -- `templateName` (_string_) or `templateDefinition` (_`TemplateDefinition`_) -- `cultureName` (_string_) -- `tryDefaults` (_bool_) -- `useCurrentCultureIfCultureNameIsNull` (_bool_) +Example: -### Usage +````csharp +public class TemplateContentDemo : ITransientDependency +{ + private readonly ITemplateContentProvider _templateContentProvider; -First parametres of `GetContentOrNullAsync` (`templateName` or `templateDefinition`) are required, the other three parametres can be null. + public TemplateContentDemo(ITemplateContentProvider templateContentProvider) + { + _templateContentProvider = templateContentProvider; + } -If you want to get exact culture content, set `tryDefaults` and `useCurrentCultureIfCultureNameIsNull` as a `false`. Because the `GetContentOrNullAsync` tries to return content of template. + public async Task RunAsync() + { + var result = await _templateContentProvider + .GetContentOrNullAsync("Hello"); -> Example Scenario + Console.WriteLine(result); + } +} +```` -> If you have a template content that culture "`es`", when you try to get template content with "`es-MX`" it will try to return first "`es-MX`", if it fails it will return "`es`" content. If you set `tryDefaults` and `useCurrentCultureIfCultureNameIsNull` as `false` it will return `null`. +The result will be the raw template content: -## Template Definition Manager +```` +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). -When you want to get your `Template Definitions`, you can use a singleton service that named `Template Definition Manager` in runtime. +### Template Content Contributor -To use it, inject `ITemplateDefinitionManager` service. +`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. -It has three method that you can get your Template Definitions. +You can implement the `ITemplateContentContributor` to read raw template contents from another source. -- `Get` -- `GetOrNull` -- `GetAll` +Example: + +````csharp +public class MyTemplateContentProvider + : ITemplateContentContributor, ITransientDependency +{ + public async Task GetOrNullAsync(TemplateContentContributorContext context) + { + var templateName = context.TemplateDefinition.Name; -`Get` and `GetOrNull` requires a string parameter that name of template definition. `Get` will throw error when it is not exist but `GetOrNull` returns `null`. + //TODO: Try to find content from another source + return null; + } +} -`GetAll` returns you all registered template definitions. +```` -## Template Content Contributor +Return `null` if your source can not find the content, so `ITemplateContentProvider` fallbacks to the next contributor. -You can store your `Template Contents` in any resource. To make it, just create a class that implements `ITemplateContentContributor` interface. +### Template Definition Manager -`ITemplateContentContributor` has a one method that named `GetOrNullAsync`. This method must return content **without rendering** if that is exist in your resource or must return `null`. \ No newline at end of file +`ITemplateDefinitionManager` service can be used to get the template definitions (created by the template definition providers). \ No newline at end of file From 03e4913f10898b8b739c9bf043388963009c3cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 01:35:48 +0300 Subject: [PATCH 037/110] add Text Templating to the navigation menu. --- docs/en/docs-nav.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 430fdf6b47..5f6203054e 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -179,6 +179,10 @@ "text": "Object to object mapping", "path": "Object-To-Object-Mapping.md" }, + { + "text": "Text Templating", + "path": "Text-Templating.md" + }, { "text": "Object Serialization" }, From 659baff96db010f1882c41efaa06be1f05a4fb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 01:36:07 +0300 Subject: [PATCH 038/110] reference the sample text templating source code --- docs/en/Samples/Index.md | 3 +++ docs/en/Text-Templating.md | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index 0c9ce68886..a42d74c1c3 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -42,6 +42,9 @@ While there is no Razor Pages & MongoDB combination, you can check both document * [Source code](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) * [Distributed event bus document](../Distributed-Event-Bus.md) * [RabbitMQ distributed event bus integration document](../Distributed-Event-Bus-RabbitMQ-Integration.md) +* **Text Templates Demo**: Shows different use cases of the text templating system. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) + * [Text templating documentation](../Text-Templating.md) * **Authentication Customization**: A solution to show how to customize the authentication for ASP.NET Core MVC / Razor Pages applications. * [Source code](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization) * Related "[How To](../How-To/Index.md)" documents: diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index db2f25a807..925d26ed6f 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -42,6 +42,10 @@ Template rendering engine is very powerful; * 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. @@ -442,4 +446,10 @@ Return `null` if your source can not find the content, so `ITemplateContentProvi ### Template Definition Manager -`ITemplateDefinitionManager` service can be used to get the template definitions (created by the template definition providers). \ No newline at end of file +`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). \ No newline at end of file From 7f1af28e77c57f2206c306a368ba1c408dc9174c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 01:37:32 +0300 Subject: [PATCH 039/110] Update Text-Templating.md --- docs/en/Text-Templating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 925d26ed6f..9cad6598f1 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -257,7 +257,7 @@ Assuming that you want to send a welcome email to your users, but want to define First, create a folder and put your templates inside it, like `en.tpl`, `tr.tpl`... one for each culture you support: -![multiple-file-template](D:\Github\abp\docs\en\images\multiple-file-template.png) +![multiple-file-template](images/multiple-file-template.png) Then add your template definition in the template definition provider class: From 2143e904343707902f16fdf5da71bb5c63e54fc1 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Thu, 14 May 2020 02:11:02 +0300 Subject: [PATCH 040/110] fixes #3944 --- .../EfCoreIdentityUserRepository.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs index cab891bc94..dc94dd35e8 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs @@ -19,7 +19,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task FindByNormalizedUserNameAsync( - string normalizedUserName, + string normalizedUserName, bool includeDetails = true, CancellationToken cancellationToken = default) { @@ -32,7 +32,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task> GetRoleNamesAsync( - Guid id, + Guid id, CancellationToken cancellationToken = default) { var query = from userRole in DbContext.Set() @@ -44,8 +44,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task FindByLoginAsync( - string loginProvider, - string providerKey, + string loginProvider, + string providerKey, bool includeDetails = true, CancellationToken cancellationToken = default) { @@ -77,7 +77,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task> GetListByNormalizedRoleNameAsync( - string normalizedRoleName, + string normalizedRoleName, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -97,10 +97,10 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task> GetListAsync( - string sorting = null, + string sorting = null, int maxResultCount = int.MaxValue, - int skipCount = 0, - string filter = null, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -110,7 +110,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore !filter.IsNullOrWhiteSpace(), u => u.UserName.Contains(filter) || - u.Email.Contains(filter) + u.Email.Contains(filter) || + (u.PhoneNumber != null && u.PhoneNumber.Contains(filter)) ) .OrderBy(sorting ?? nameof(IdentityUser.UserName)) .PageBy(skipCount, maxResultCount) @@ -131,7 +132,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task GetCountAsync( - string filter = null, + string filter = null, CancellationToken cancellationToken = default) { return await this.WhereIf( From 0881683b663842c9c36504526a3c94276b9a1a82 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Thu, 14 May 2020 02:32:45 +0300 Subject: [PATCH 041/110] closes #3946 --- .../Volo/Abp/Identity/Localization/tr.json | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json index 8b8b936c0a..1f1042549b 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json @@ -71,6 +71,34 @@ "Permission:Delete": "Silme", "Permission:ChangePermissions": "İzinleri değiştirme", "Permission:UserManagement": "Kullanıcı yönetimi", - "Permission:UserLookup": "Kullanıcı sorgulama" + "Permission:UserLookup": "Kullanıcı sorgulama", + "DisplayName:Abp.Identity.Password.RequiredLength": "Uzunluk gerekli", + "DisplayName:Abp.Identity.Password.RequiredUniqueChars": "Tekil karakter gerekli", + "DisplayName:Abp.Identity.Password.RequireNonAlphanumeric": "Alfasayısal olmayan karakter gerekli", + "DisplayName:Abp.Identity.Password.RequireLowercase": "Küçük harf gerekli", + "DisplayName:Abp.Identity.Password.RequireUppercase": "Büyük harf gerekli", + "DisplayName:Abp.Identity.Password.RequireDigit": "Rakam gerekli", + "DisplayName:Abp.Identity.Lockout.AllowedForNewUsers": "Yeni kullanıcılar için aktif", + "DisplayName:Abp.Identity.Lockout.LockoutDuration": "Kilitli kalma süresi (saniye)", + "DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "Maksimum başarısız giriş denemesi", + "DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "Onaylı e-posta gerekli", + "DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "Telefon numarası onayını etkin", + "DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "Onaylı telefon numarası gerekli", + "DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "Kullanıcı adı güncellenebilir", + "DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "E-posta güncellenebilir", + "Description:Abp.Identity.Password.RequiredLength": "Minimum parola uzunluğu.", + "Description:Abp.Identity.Password.RequiredUniqueChars": Bir parolanın içermesi gereken minimum tekil karakter sayısı.", + "Description:Abp.Identity.Password.RequireNonAlphanumeric": "Parolaların alfasayısal olmayan bir karakter içermesi gerekiyorsa.", + "Description:Abp.Identity.Password.RequireLowercase": "Parolaların küçük harfli bir ASCII karakteri içermesi gerekiyorsa.", + "Description:Abp.Identity.Password.RequireUppercase": "Parolaların büyük harfli bir ASCII karakteri içermesi gerekiyorsa.", + "Description:Abp.Identity.Password.RequireDigit": "Parolaların bir rakam içermesi gerekiyorsa.", + "Description:Abp.Identity.Lockout.AllowedForNewUsers": "Yeni kullanıcılar kilitlenebilir.", + "Description:Abp.Identity.Lockout.LockoutDuration": "Kilitlenme olduğunda, ne kadar kilitli kalacağı.", + "Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "Kilitleme etkin olduğunda, kullanıcıya kilitlenmeden önce izin verilen başarısız giriş sayısı.", + "Description:Abp.Identity.SignIn.RequireConfirmedEmail": "Oturum açmak için onaylanmış bir e-posta adresinin gerekli olup olmadığı.", + "Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "Oturum açmak için telefon numarası gerekli", + "Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "Oturum açmak için onaylanmış bir telefon numarasının gerekli olup olmadığı.", + "Description:Abp.Identity.User.IsUserNameUpdateEnabled": "Kullanıcı adının, kullanıcının kendisi tarafından güncellenebilirliği.", + "Description:Abp.Identity.User.IsEmailUpdateEnabled": "E-posta alanının, kullanıcının kendisi tarafından güncellenebilirliği" } } From 500f9ff8c87e10786a5e26a048305d4a91f42e84 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 14 May 2020 08:31:26 +0800 Subject: [PATCH 042/110] Fix tr.json. --- .../Volo/Abp/Identity/Localization/tr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json index 1f1042549b..92328ad596 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json @@ -87,7 +87,7 @@ "DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "Kullanıcı adı güncellenebilir", "DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "E-posta güncellenebilir", "Description:Abp.Identity.Password.RequiredLength": "Minimum parola uzunluğu.", - "Description:Abp.Identity.Password.RequiredUniqueChars": Bir parolanın içermesi gereken minimum tekil karakter sayısı.", + "Description:Abp.Identity.Password.RequiredUniqueChars": "Bir parolanın içermesi gereken minimum tekil karakter sayısı.", "Description:Abp.Identity.Password.RequireNonAlphanumeric": "Parolaların alfasayısal olmayan bir karakter içermesi gerekiyorsa.", "Description:Abp.Identity.Password.RequireLowercase": "Parolaların küçük harfli bir ASCII karakteri içermesi gerekiyorsa.", "Description:Abp.Identity.Password.RequireUppercase": "Parolaların büyük harfli bir ASCII karakteri içermesi gerekiyorsa.", From 23f304253674e2313c6793aa00bb284940ace568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 May 2020 04:11:03 +0300 Subject: [PATCH 043/110] Implemented SearchAsync for the user lookup --- .../Identity/IIdentityUserLookupAppService.cs | 3 ++ .../Abp/Identity/UserLookupSearchInputDto.cs | 11 ++++++ .../Identity/IdentityUserLookupAppService.cs | 17 +++++++++ ...sitoryExternalUserLookupServiceProvider.cs | 19 ++++++++++ .../HttpClientIdentityUserLookupService.cs | 19 ++++++++++ .../Identity/IdentityUserLookupController.cs | 8 +++++ .../IdentityUserLookupAppService_Tests.cs | 35 +++++++++++++++++-- .../IExternalUserLookupServiceProvider.cs | 9 ++++- .../Volo/Abp/Users/IUserLookupService.cs | 7 +++- .../Volo/Abp/Users/IUserRepository.cs | 8 +++++ .../Volo/Abp/Users/UserLookupService.cs | 13 +++++++ .../EfCoreAbpUserRepositoryBase.cs | 20 +++++++++++ .../Users/MongoDB/MongoUserRepositoryBase.cs | 17 +++++++++ 13 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/UserLookupSearchInputDto.cs diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserLookupAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserLookupAppService.cs index 1ab79f400e..f3c3a3bc53 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserLookupAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserLookupAppService.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Users; @@ -10,5 +11,7 @@ namespace Volo.Abp.Identity Task FindByIdAsync(Guid id); Task FindByUserNameAsync(string userName); + + Task> SearchAsync(UserLookupSearchInputDto input); } } diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/UserLookupSearchInputDto.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/UserLookupSearchInputDto.cs new file mode 100644 index 0000000000..a2413e64ea --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/UserLookupSearchInputDto.cs @@ -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; } + } +} \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserLookupAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserLookupAppService.cs index 48d42c4579..696ce864ce 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserLookupAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserLookupAppService.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Dtos; using Volo.Abp.Users; namespace Volo.Abp.Identity @@ -37,5 +39,20 @@ namespace Volo.Abp.Identity return new UserData(userData); } + + public async Task> SearchAsync(UserLookupSearchInputDto input) + { + var users = await UserLookupServiceProvider.SearchAsync( + input.Sorting, + input.Filter, + input.MaxResultCount + ); + + return new ListResultDto( + users + .Select(u => new UserData(u)) + .ToList() + ); + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserRepositoryExternalUserLookupServiceProvider.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserRepositoryExternalUserLookupServiceProvider.cs index 7fe729b325..9d8cd1dab4 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserRepositoryExternalUserLookupServiceProvider.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserRepositoryExternalUserLookupServiceProvider.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; @@ -45,5 +47,22 @@ namespace Volo.Abp.Identity ) )?.ToAbpUserData(); } + + public virtual async Task> SearchAsync( + string sorting, + string filter, + int maxResultCount, + CancellationToken cancellationToken = default) + { + var users = await UserRepository.GetListAsync( + sorting: sorting, + maxResultCount: maxResultCount, + filter: filter, + includeDetails: false, + cancellationToken: cancellationToken + ); + + return users.Select(u => u.ToAbpUserData()).ToList(); + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/Volo/Abp/Identity/HttpClientIdentityUserLookupService.cs b/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/Volo/Abp/Identity/HttpClientIdentityUserLookupService.cs index ec1dca264f..6d5fdd0f92 100644 --- a/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/Volo/Abp/Identity/HttpClientIdentityUserLookupService.cs +++ b/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/Volo/Abp/Identity/HttpClientIdentityUserLookupService.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -25,5 +27,22 @@ namespace Volo.Abp.Identity { return await UserLookupAppService.FindByUserNameAsync(userName); } + + public async Task> SearchAsync( + string sorting, + string filter, + int maxResultCount, + CancellationToken cancellationToken = default) + { + var result = await UserLookupAppService.SearchAsync( + new UserLookupSearchInputDto + { + Filter = filter, + MaxResultCount = maxResultCount + } + ); + + return result.Items.Cast().ToList(); + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserLookupController.cs b/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserLookupController.cs index 2f8011bf7f..70dcaaa300 100644 --- a/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserLookupController.cs +++ b/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserLookupController.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Application.Dtos; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Users; @@ -32,5 +33,12 @@ namespace Volo.Abp.Identity { return LookupAppService.FindByUserNameAsync(userName); } + + [HttpGet] + [Route("search")] + public Task> SearchAsync(UserLookupSearchInputDto input) + { + return LookupAppService.SearchAsync(input); + } } } diff --git a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserLookupAppService_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserLookupAppService_Tests.cs index 227977cf15..fc5a18cbd3 100644 --- a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserLookupAppService_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserLookupAppService_Tests.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Shouldly; @@ -52,5 +50,38 @@ namespace Volo.Abp.Identity var user = await _identityUserLookupAppService.FindByUserNameAsync(Guid.NewGuid().ToString()); user.ShouldBeNull(); } + + [Fact] + public async Task Search_Without_Filter_And_Sorting() + { + var result = await _identityUserLookupAppService.SearchAsync(new UserLookupSearchInputDto()); + result.Items.Count.ShouldBeGreaterThanOrEqualTo(3); + result.Items.ShouldContain(u => u.UserName == "john.nash"); + } + + [Fact] + public async Task Search_With_Filter() + { + var result = await _identityUserLookupAppService.SearchAsync( + new UserLookupSearchInputDto + { + Filter = "a" + } + ); + + result.Items.Count.ShouldBeGreaterThanOrEqualTo(2); + result.Items.ShouldContain(u => u.UserName == "john.nash"); + result.Items.ShouldContain(u => u.UserName == "david"); + + result = await _identityUserLookupAppService.SearchAsync( + new UserLookupSearchInputDto + { + Filter = "neo" + } + ); + + result.Items.Count.ShouldBeGreaterThanOrEqualTo(1); + result.Items.ShouldContain(u => u.UserName == "neo"); + } } } diff --git a/modules/users/src/Volo.Abp.Users.Abstractions/Volo/Abp/Users/IExternalUserLookupServiceProvider.cs b/modules/users/src/Volo.Abp.Users.Abstractions/Volo/Abp/Users/IExternalUserLookupServiceProvider.cs index 8f498e32c3..b973033be6 100644 --- a/modules/users/src/Volo.Abp.Users.Abstractions/Volo/Abp/Users/IExternalUserLookupServiceProvider.cs +++ b/modules/users/src/Volo.Abp.Users.Abstractions/Volo/Abp/Users/IExternalUserLookupServiceProvider.cs @@ -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 FindByIdAsync(Guid id, CancellationToken cancellationToken = default); Task FindByUserNameAsync(string userName, CancellationToken cancellationToken = default); + + Task> SearchAsync( + string sorting, + string filter, + int maxResultCount, + CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserLookupService.cs b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserLookupService.cs index 2c50d7795f..44e84eb350 100644 --- a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserLookupService.cs +++ b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserLookupService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,10 @@ namespace Volo.Abp.Users Task FindByUserNameAsync(string userName, CancellationToken cancellationToken = default); - //TODO: More... + Task> SearchAsync( + string sorting, + string filter, + int maxResultCount, + CancellationToken cancellationToken = default); } } diff --git a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserRepository.cs b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserRepository.cs index 14eea6a3fe..fc0412c734 100644 --- a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserRepository.cs +++ b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/IUserRepository.cs @@ -13,5 +13,13 @@ namespace Volo.Abp.Users Task FindByUserNameAsync(string userName, CancellationToken cancellationToken = default); Task> GetListAsync(IEnumerable ids, CancellationToken cancellationToken = default); + + Task> SearchAsync( + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, + CancellationToken cancellationToken = default + ); } } \ No newline at end of file diff --git a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs index 2c28d71b80..7621f3f63e 100644 --- a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs +++ b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -138,6 +140,17 @@ namespace Volo.Abp.Users return await _userRepository.FindAsync(externalUser.Id, cancellationToken: cancellationToken); } + public async Task> SearchAsync(string sorting, string filter, int maxResultCount, CancellationToken cancellationToken = default) + { + if (ExternalUserLookupServiceProvider != null) + { + return await ExternalUserLookupServiceProvider.SearchAsync(sorting, filter, maxResultCount, cancellationToken); + } + + var localUsers = await _userRepository.SearchAsync(sorting, maxResultCount, 0, filter, cancellationToken); + return localUsers.Cast().ToList(); + } + protected abstract TUser CreateUser(IUserData externalUser); private async Task WithNewUowAsync(Func func) diff --git a/modules/users/src/Volo.Abp.Users.EntityFrameworkCore/Volo/Abp/Users/EntityFrameworkCore/EfCoreAbpUserRepositoryBase.cs b/modules/users/src/Volo.Abp.Users.EntityFrameworkCore/Volo/Abp/Users/EntityFrameworkCore/EfCoreAbpUserRepositoryBase.cs index 5d0958101c..f5eb013c93 100644 --- a/modules/users/src/Volo.Abp.Users.EntityFrameworkCore/Volo/Abp/Users/EntityFrameworkCore/EfCoreAbpUserRepositoryBase.cs +++ b/modules/users/src/Volo.Abp.Users.EntityFrameworkCore/Volo/Abp/Users/EntityFrameworkCore/EfCoreAbpUserRepositoryBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Dynamic.Core; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,5 +29,24 @@ namespace Volo.Abp.Users.EntityFrameworkCore { return await DbSet.Where(u => ids.Contains(u.Id)).ToListAsync(GetCancellationToken(cancellationToken)); } + + public async Task> SearchAsync( + string sorting = null, + int maxResultCount = Int32.MaxValue, + int skipCount = 0, + string filter = null, + CancellationToken cancellationToken = default) + { + return await DbSet + .WhereIf( + !filter.IsNullOrWhiteSpace(), + u => + u.UserName.Contains(filter) || + u.Email.Contains(filter) + ) + .OrderBy(sorting ?? nameof(IUser.UserName)) + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } } } \ No newline at end of file diff --git a/modules/users/src/Volo.Abp.Users.MongoDB/Volo/Abp/Users/MongoDB/MongoUserRepositoryBase.cs b/modules/users/src/Volo.Abp.Users.MongoDB/Volo/Abp/Users/MongoDB/MongoUserRepositoryBase.cs index 50d892d8dd..3cb6e567ae 100644 --- a/modules/users/src/Volo.Abp.Users.MongoDB/Volo/Abp/Users/MongoDB/MongoUserRepositoryBase.cs +++ b/modules/users/src/Volo.Abp.Users.MongoDB/Volo/Abp/Users/MongoDB/MongoUserRepositoryBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Dynamic.Core; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; @@ -29,5 +30,21 @@ namespace Volo.Abp.Users.MongoDB { return await GetMongoQueryable().Where(u => ids.Contains(u.Id)).ToListAsync(GetCancellationToken(cancellationToken)); } + + public async Task> SearchAsync(string sorting = null, int maxResultCount = Int32.MaxValue, int skipCount = 0, string filter = null, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + u => + u.UserName.Contains(filter) || + u.Email.Contains(filter) + ) + .OrderBy(sorting ?? nameof(IUserData.UserName)) + .As>() + .PageBy>(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } } } \ No newline at end of file From f5e5edae820ac0e3b35feb4f0714530fd0844cc3 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Thu, 14 May 2020 09:49:49 +0800 Subject: [PATCH 044/110] Search phone number --- .../MongoDB/MongoIdentityUserRepository.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs index 9eb1664bd5..52a345b0c4 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs @@ -15,13 +15,13 @@ namespace Volo.Abp.Identity.MongoDB { public class MongoIdentityUserRepository : MongoDbRepository, IIdentityUserRepository { - public MongoIdentityUserRepository(IMongoDbContextProvider dbContextProvider) + public MongoIdentityUserRepository(IMongoDbContextProvider dbContextProvider) : base(dbContextProvider) { } public virtual async Task FindByNormalizedUserNameAsync( - string normalizedUserName, + string normalizedUserName, bool includeDetails = true, CancellationToken cancellationToken = default) { @@ -33,7 +33,7 @@ namespace Volo.Abp.Identity.MongoDB } public virtual async Task> GetRoleNamesAsync( - Guid id, + Guid id, CancellationToken cancellationToken = default) { var user = await GetAsync(id, cancellationToken: GetCancellationToken(cancellationToken)); @@ -42,8 +42,8 @@ namespace Volo.Abp.Identity.MongoDB } public virtual async Task FindByLoginAsync( - string loginProvider, - string providerKey, + string loginProvider, + string providerKey, bool includeDetails = true, CancellationToken cancellationToken = default) { @@ -71,7 +71,7 @@ namespace Volo.Abp.Identity.MongoDB } public virtual async Task> GetListByNormalizedRoleNameAsync( - string normalizedRoleName, + string normalizedRoleName, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -89,9 +89,9 @@ namespace Volo.Abp.Identity.MongoDB public virtual async Task> GetListAsync( string sorting = null, - int maxResultCount = int.MaxValue, - int skipCount = 0, - string filter = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -100,7 +100,8 @@ namespace Volo.Abp.Identity.MongoDB !filter.IsNullOrWhiteSpace(), u => u.UserName.Contains(filter) || - u.Email.Contains(filter) + u.Email.Contains(filter) || + (u.PhoneNumber != null && u.PhoneNumber.Contains(filter)) ) .OrderBy(sorting ?? nameof(IdentityUser.UserName)) .As>() @@ -132,4 +133,4 @@ namespace Volo.Abp.Identity.MongoDB .LongCountAsync(GetCancellationToken(cancellationToken)); } } -} \ No newline at end of file +} From 72e8a686f62344f11c0deb6015cfc0ece4b02b46 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 10:15:24 +0300 Subject: [PATCH 045/110] test: add else case for listenToEscape call spec --- .../lib/tests/confirmation.service.spec.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts index 1413e034d7..3cbdcbd7de 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts @@ -106,13 +106,20 @@ describe('ConfirmationService', () => { selectConfirmationElement('button#cancel').click(); }); - test('should not call the listenToEscape method when dismissible is false', () => { - const spy = spyOn(service as any, 'listenToEscape'); - - service.info('', '', { dismissible: false }); - - expect(spy.calls.count()).toBe(0); - }); + test.each` + dismissible | count + ${true} | ${1} + ${false} | ${0} + `( + 'should call the listenToEscape method $count times when dismissible is $dismissible', + ({ dismissible, count }) => { + const spy = spyOn(service as any, 'listenToEscape'); + + service.info('', '', { dismissible }); + + expect(spy).toHaveBeenCalledTimes(count); + }, + ); }); function clearElements(selector = '.confirmation') { From e5f68db556030332682a4ffa98e84e822694a4fd Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Thu, 14 May 2020 18:44:37 +0800 Subject: [PATCH 046/110] Fix PageModel.cs --- .../TagHelpers/Pagination/PagerModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs index db74266f38..980aacee8a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs @@ -54,8 +54,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination CurrentPage = currentPage; } - ShowingFrom = totalCount == 0 ? 0 : CurrentPage * PageSize; - ShowingTo = totalCount == 0 ? 0 : ShowingFrom + PageSize; + ShowingFrom = totalCount == 0 ? 0 : (CurrentPage - 1) * PageSize + 1; + ShowingTo = totalCount == 0 ? 0 : (int)Math.Min(ShowingFrom + PageSize - 1 , totalCount); PreviousPage = CurrentPage <= 1 ? 1 : CurrentPage - 1; NextPage = CurrentPage >= TotalPageCount ? CurrentPage : CurrentPage + 1; Pages = CalculatePageNumbers(); From 0c51afc37112237cd73e4fc10f31dc2a2d95a93b Mon Sep 17 00:00:00 2001 From: Ahmet Date: Thu, 14 May 2020 16:35:20 +0300 Subject: [PATCH 047/110] Added escape blocks for text templating module --- docs/en/Text-Templating.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 9cad6598f1..18a360c5c8 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -17,7 +17,7 @@ You can use the rendered output for any purpose, like sending emails or preparin Here, a simple template: ```` -Hello {{model.name}} :) +Hello {%{{{model.name}}}%} :) ```` You can define a class with a `Name` property to render this template: @@ -114,7 +114,7 @@ public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider Example `Hello.tpl` content is shown below: ```` -Hello {{model.name}} :) +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: @@ -204,7 +204,7 @@ Inline localization uses the [localization system](Localization.md) to localize Assuming you need to send an email to a user to reset her/his password. Here, the template content: ```` -{{L "ResetMyPassword"}} +{%{{{L "ResetMyPassword"}}}%} ```` `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: @@ -314,12 +314,12 @@ First, create a template file just like before: - {{content}} + {%{{{content}}}%} ```` -* A layout template must have a **{{content}}** part as a place holder for the rendered child content. +* 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: @@ -357,7 +357,7 @@ ABP passes the `model` that can be used to access to the model inside the templa An example template content: ```` -A global object value: {{myGlobalObject}} +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: @@ -413,7 +413,7 @@ public class TemplateContentDemo : ITransientDependency The result will be the raw template content: ```` -Hello {{model.name}} :) +Hello {%{{{model.name}}}%} :) ```` * `GetContentOrNullAsync` returns `null` if no content defined for the requested template. From e9159fd1b969f8caa164926a9a50859b168e46f4 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Thu, 14 May 2020 16:39:01 +0300 Subject: [PATCH 048/110] Added escape blocks for text templating documentation --- docs/en/Text-Templating.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 9cad6598f1..18a360c5c8 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -17,7 +17,7 @@ You can use the rendered output for any purpose, like sending emails or preparin Here, a simple template: ```` -Hello {{model.name}} :) +Hello {%{{{model.name}}}%} :) ```` You can define a class with a `Name` property to render this template: @@ -114,7 +114,7 @@ public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider Example `Hello.tpl` content is shown below: ```` -Hello {{model.name}} :) +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: @@ -204,7 +204,7 @@ Inline localization uses the [localization system](Localization.md) to localize Assuming you need to send an email to a user to reset her/his password. Here, the template content: ```` -{{L "ResetMyPassword"}} +{%{{{L "ResetMyPassword"}}}%} ```` `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: @@ -314,12 +314,12 @@ First, create a template file just like before: - {{content}} + {%{{{content}}}%} ```` -* A layout template must have a **{{content}}** part as a place holder for the rendered child content. +* 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: @@ -357,7 +357,7 @@ ABP passes the `model` that can be used to access to the model inside the templa An example template content: ```` -A global object value: {{myGlobalObject}} +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: @@ -413,7 +413,7 @@ public class TemplateContentDemo : ITransientDependency The result will be the raw template content: ```` -Hello {{model.name}} :) +Hello {%{{{model.name}}}%} :) ```` * `GetContentOrNullAsync` returns `null` if no content defined for the requested template. From 41d63f6b7880dffee7dc598a1c3d915d045f9a44 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Thu, 14 May 2020 17:41:55 +0300 Subject: [PATCH 049/110] chore: add AuthGuard to setting-managent-routing.module --- .../src/lib/setting-management-routing.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/npm/ng-packs/packages/setting-management/src/lib/setting-management-routing.module.ts b/npm/ng-packs/packages/setting-management/src/lib/setting-management-routing.module.ts index f394abc962..55a056c411 100644 --- a/npm/ng-packs/packages/setting-management/src/lib/setting-management-routing.module.ts +++ b/npm/ng-packs/packages/setting-management/src/lib/setting-management-routing.module.ts @@ -2,6 +2,7 @@ import { DynamicLayoutComponent, ReplaceableComponents, ReplaceableRouteContainerComponent, + AuthGuard, } from '@abp/ng.core'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; @@ -12,6 +13,7 @@ const routes: Routes = [ { path: '', component: DynamicLayoutComponent, + canActivate: [AuthGuard], children: [ { path: '', From d3543c7c4c33fb0b262200d90ea42bc1b5a571ca Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 18:46:14 +0300 Subject: [PATCH 050/110] refactor: move ops to seperate methods and properties --- .../loader-bar/loader-bar.component.ts | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts index 61f4d2f663..41a5850131 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -3,7 +3,7 @@ import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular import { NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router'; import { takeUntilDestroy } from '@ngx-validate/core'; import { Actions, ofActionSuccessful } from '@ngxs/store'; -import { interval, Subscription, timer } from 'rxjs'; +import { Subscription, timer } from 'rxjs'; import { filter } from 'rxjs/operators'; @Component({ @@ -30,6 +30,10 @@ export class LoaderBarComponent implements OnDestroy, OnInit { @Input() color = '#77b6ff'; + @Input() + filter = (action: StartLoader | StopLoader) => + action.payload.url.indexOf('openid-configuration') < 0; + @Input() isLoading = false; @@ -43,9 +47,23 @@ export class LoaderBarComponent implements OnDestroy, OnInit { stopDelay = 800; - @Input() - filter = (action: StartLoader | StopLoader) => - action.payload.url.indexOf('openid-configuration') < 0; + private readonly clearProgress = () => { + this.progressLevel = 0; + this.cdRef.detectChanges(); + }; + + private readonly reportProgress = () => { + if (this.progressLevel < 75) { + this.progressLevel += 1 + Math.random() * 9; + } else if (this.progressLevel < 90) { + this.progressLevel += 0.4; + } else if (this.progressLevel < 100) { + this.progressLevel += 0.1; + } else { + this.interval.unsubscribe(); + } + this.cdRef.detectChanges(); + }; get boxShadow(): string { return `0 0 10px rgba(${this.color}, 0.5)`; @@ -53,7 +71,7 @@ export class LoaderBarComponent implements OnDestroy, OnInit { constructor(private actions: Actions, private router: Router, private cdRef: ChangeDetectorRef) {} - ngOnInit() { + private subscribeToLoadActions() { this.actions .pipe( ofActionSuccessful(StartLoader, StopLoader), @@ -64,7 +82,9 @@ export class LoaderBarComponent implements OnDestroy, OnInit { if (action instanceof StartLoader) this.startLoading(); else this.stopLoading(); }); + } + private subscribeToRouterEvents() { this.router.events .pipe( filter( @@ -81,6 +101,11 @@ export class LoaderBarComponent implements OnDestroy, OnInit { }); } + ngOnInit() { + this.subscribeToLoadActions(); + this.subscribeToRouterEvents(); + } + ngOnDestroy() { if (this.interval) this.interval.unsubscribe(); } @@ -90,32 +115,17 @@ export class LoaderBarComponent implements OnDestroy, OnInit { this.isLoading = true; - const moveOn = () => { - if (this.progressLevel < 75) { - this.progressLevel += Math.random() * 10; - } else if (this.progressLevel < 90) { - this.progressLevel += 0.4; - } else if (this.progressLevel < 100) { - this.progressLevel += 0.1; - } else { - this.interval.unsubscribe(); - } - this.cdRef.detectChanges(); - }; - - moveOn(); - this.interval = interval(this.intervalPeriod).subscribe(moveOn); + this.interval = timer(0, this.intervalPeriod).subscribe(this.reportProgress); } stopLoading() { if (this.interval) this.interval.unsubscribe(); + this.progressLevel = 100; this.isLoading = false; + if (this.timer && !this.timer.closed) return; - this.timer = timer(this.stopDelay).subscribe(() => { - this.progressLevel = 0; - this.cdRef.detectChanges(); - }); + this.timer = timer(this.stopDelay).subscribe(this.clearProgress); } } From 7678e39edd5231358ddae6ce0954af1f7d36cc0b Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 19:36:18 +0300 Subject: [PATCH 051/110] feat: add a token for list query debounce time --- npm/ng-packs/packages/core/src/lib/tokens/index.ts | 1 + npm/ng-packs/packages/core/src/lib/tokens/list.token.ts | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tokens/list.token.ts diff --git a/npm/ng-packs/packages/core/src/lib/tokens/index.ts b/npm/ng-packs/packages/core/src/lib/tokens/index.ts index 683bc4b3db..8d23d581a5 100644 --- a/npm/ng-packs/packages/core/src/lib/tokens/index.ts +++ b/npm/ng-packs/packages/core/src/lib/tokens/index.ts @@ -1 +1,2 @@ +export * from './list.token'; export * from './options.token'; diff --git a/npm/ng-packs/packages/core/src/lib/tokens/list.token.ts b/npm/ng-packs/packages/core/src/lib/tokens/list.token.ts new file mode 100644 index 0000000000..b51ffb9fb3 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tokens/list.token.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const LIST_QUERY_DEBOUNCE_TIME = new InjectionToken('LIST_QUERY_DEBOUNCE_TIME'); From 2d694e9e39a886abe4bd601f0896611cf48c1441 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 19:36:40 +0300 Subject: [PATCH 052/110] feat: add a service for easily querying lists --- .../packages/core/src/lib/services/index.ts | 1 + .../core/src/lib/services/list.service.ts | 96 +++++++++++ .../core/src/lib/tests/list.service.spec.ts | 150 ++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/services/list.service.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index f8b016bbd1..f01dc876de 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -4,6 +4,7 @@ export * from './config-state.service'; export * from './content-projection.service'; export * from './dom-insertion.service'; export * from './lazy-load.service'; +export * from './list.service'; export * from './localization.service'; export * from './profile-state.service'; export * from './profile.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/list.service.ts b/npm/ng-packs/packages/core/src/lib/services/list.service.ts new file mode 100644 index 0000000000..1e0b8e22b9 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/list.service.ts @@ -0,0 +1,96 @@ +import { Inject, Injectable, Optional } from '@angular/core'; +import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs'; +import { debounceTime, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { ABP } from '../models/common'; +import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens/list.token'; +import { takeUntilDestroy } from '../utils/rxjs-utils'; + +@Injectable() +export class ListService { + 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) { + console.log(value); + 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(1); + + get query$(): Observable { + return this._query$ + .asObservable() + .pipe(debounceTime(this.delay || 300), shareReplay({ bufferSize: 1, refCount: true })); + } + + private _isLoading$ = new BehaviorSubject(false); + + get isLoading$(): Observable { + return this._isLoading$.asObservable(); + } + + constructor(@Optional() @Inject(LIST_QUERY_DEBOUNCE_TIME) private delay: number) { + this.get(); + } + + 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, + }); + } + + hookToQuery(streamCreatorCallback: QueryStreamCreatorCallback): Observable { + this._isLoading$.next(true); + + return this.query$.pipe( + switchMap(streamCreatorCallback), + tap(() => this._isLoading$.next(false)), + takeUntilDestroy(this), + ); + } + + ngOnDestroy() {} +} + +export type QueryStreamCreatorCallback = (query: ABP.PageQueryParams) => Observable; diff --git a/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts new file mode 100644 index 0000000000..b7e2cc8450 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts @@ -0,0 +1,150 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; +import { bufferCount, take } from 'rxjs/operators'; +import { ListService } from '../services/list.service'; +import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens'; + +describe('ListService', () => { + let spectator: SpectatorService; + 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 = query => of(query); + + service.hookToQuery(callback).subscribe(query => { + expect(query).toEqual({ + filter: undefined, + maxResultCount: 10, + skipCount: 0, + sorting: undefined, + }); + + done(); + }); + }); + + it('should emit isLoading as side effect', done => { + const callback = query => of(query); + + 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(); + }); + }); +}); From 0744544259a853a785c891de46f9ebff75746fa3 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 21:24:09 +0300 Subject: [PATCH 053/110] feat: shareReplay response and improve callback type --- .../core/src/lib/services/list.service.ts | 20 ++++++++++++------- .../core/src/lib/tests/list.service.spec.ts | 11 ++++++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/list.service.ts b/npm/ng-packs/packages/core/src/lib/services/list.service.ts index 1e0b8e22b9..c41b1f3ff6 100644 --- a/npm/ng-packs/packages/core/src/lib/services/list.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/list.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable, 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'; @@ -67,25 +68,28 @@ export class ListService { return this._isLoading$.asObservable(); } - constructor(@Optional() @Inject(LIST_QUERY_DEBOUNCE_TIME) private delay: number) { - this.get(); - } - - get() { + 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(streamCreatorCallback: QueryStreamCreatorCallback): Observable { + hookToQuery( + streamCreatorCallback: QueryStreamCreatorCallback, + ): Observable> { this._isLoading$.next(true); return this.query$.pipe( switchMap(streamCreatorCallback), tap(() => this._isLoading$.next(false)), + shareReplay({ bufferSize: 1, refCount: true }), takeUntilDestroy(this), ); } @@ -93,4 +97,6 @@ export class ListService { ngOnDestroy() {} } -export type QueryStreamCreatorCallback = (query: ABP.PageQueryParams) => Observable; +export type QueryStreamCreatorCallback = ( + query: ABP.PageQueryParams, +) => Observable>; diff --git a/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts index b7e2cc8450..c2118d7f3a 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts @@ -1,7 +1,8 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; import { bufferCount, take } from 'rxjs/operators'; -import { ListService } from '../services/list.service'; +import { ABP } from '../models'; +import { ListService, QueryStreamCreatorCallback } from '../services/list.service'; import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens'; describe('ListService', () => { @@ -119,9 +120,10 @@ describe('ListService', () => { describe('#hookToQuery', () => { it('should call given callback with the query', done => { - const callback = query => of(query); + const callback: QueryStreamCreatorCallback = query => + of({ items: [query], totalCount: 1 }); - service.hookToQuery(callback).subscribe(query => { + service.hookToQuery(callback).subscribe(({ items: [query] }) => { expect(query).toEqual({ filter: undefined, maxResultCount: 10, @@ -134,7 +136,8 @@ describe('ListService', () => { }); it('should emit isLoading as side effect', done => { - const callback = query => of(query); + const callback: QueryStreamCreatorCallback = query => + of({ items: [query], totalCount: 1 }); service.isLoading$.pipe(bufferCount(3)).subscribe(([idle, init, end]) => { expect(idle).toBe(false); From e3034a7f949e846d15ed8dd0fb86048461fbd3ec Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 21:24:49 +0300 Subject: [PATCH 054/110] docs: describe how ListService is used --- docs/en/UI/Angular/List-Service.md | 155 +++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 docs/en/UI/Angular/List-Service.md diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md new file mode 100644 index 0000000000..b6a4dadf3e --- /dev/null +++ b/docs/en/UI/Angular/List-Service.md @@ -0,0 +1,155 @@ +# 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 + + + + + + + {%{{{ '::Name' | abpLocalization }}}%} + + + + + + + + {%{{{ data.name }}}%} + + +``` + +## 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 + + + + + + +``` + +...or... + + +```ts + @Select(BookState.getBooks) + books$: Observable; + + @Select(BookState.getBookCount) + bookCount$: Observable; + + ngOnInit() { + this.list.hookToQuery((query) => this.store.dispatch(new GetBooks(query))).subscribe(); + } +``` + +```html + + + + +``` + +## 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 + }) +``` From eca8cde624cbfbe4e353e4b956489403bb5c0168 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 21:25:21 +0300 Subject: [PATCH 055/110] docs: add links to ListService document --- docs/en/UI/Angular/Track-By-Service.md | 6 ++++++ docs/en/docs-nav.json | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/docs/en/UI/Angular/Track-By-Service.md b/docs/en/UI/Angular/Track-By-Service.md index e6b560a2eb..447cc4505a 100644 --- a/docs/en/UI/Angular/Track-By-Service.md +++ b/docs/en/UI/Angular/Track-By-Service.md @@ -111,3 +111,9 @@ class DemoComponent { trackByTenantAccountId = trackByDeep('tenant', 'account', 'id'); } ``` + + + +## What's Next? + +- [ListService](./List-Service.md) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 5f6203054e..47789023c4 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -383,6 +383,10 @@ { "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" + }, + { + "text": "ListService", + "path": "UI/Angular/List-Service.md" } ] }, From 7141295830895a5c8b13eded6eba95a3cebdc950 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 21:30:09 +0300 Subject: [PATCH 056/110] docs: describe how search is done using ListService --- docs/en/UI/Angular/List-Service.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md index b6a4dadf3e..b3c8571866 100644 --- a/docs/en/UI/Angular/List-Service.md +++ b/docs/en/UI/Angular/List-Service.md @@ -153,3 +153,13 @@ this.bookService.createByInput(form.value) // 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 + + + +``` From d4cdbd4e8a0fa2f12b171140a787304513508a51 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 14 May 2020 21:36:10 +0300 Subject: [PATCH 057/110] fix: avoid lint errors --- npm/ng-packs/packages/core/src/lib/services/list.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/list.service.ts b/npm/ng-packs/packages/core/src/lib/services/list.service.ts index c41b1f3ff6..944368ad88 100644 --- a/npm/ng-packs/packages/core/src/lib/services/list.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/list.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Optional } from '@angular/core'; +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'; @@ -7,7 +7,7 @@ import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens/list.token'; import { takeUntilDestroy } from '../utils/rxjs-utils'; @Injectable() -export class ListService { +export class ListService implements OnDestroy { private _filter = ''; set filter(value: string) { this._filter = value; @@ -37,7 +37,6 @@ export class ListService { private _sortKey = ''; set sortKey(value: string) { - console.log(value); this._sortKey = value; this.get(); } From e0dffacd5c86f204c511ad8afaba43d703ce402f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 15 May 2020 00:42:42 +0300 Subject: [PATCH 058/110] Send all attributes to clients for extra properties. --- .../ExtensionPropertyAttributeDto.cs | 25 +---- .../CachedObjectExtensionsDtoService.cs | 22 +++-- .../ExtensionPropertyAttributeDtoFactory.cs | 95 +++++++++++++++++++ .../ICachedObjectExtensionsDtoService.cs | 0 .../IExtensionPropertyAttributeDtoFactory.cs | 9 ++ 5 files changed, 119 insertions(+), 32 deletions(-) rename framework/src/{Volo.Abp.AspNetCore.Mvc.Contracts => Volo.Abp.AspNetCore.Mvc}/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs (89%) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDtoFactory.cs rename framework/src/{Volo.Abp.AspNetCore.Mvc.Contracts => Volo.Abp.AspNetCore.Mvc}/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs (100%) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/IExtensionPropertyAttributeDtoFactory.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs index c1c19350c2..12941a3b89 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs @@ -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 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() - }; - - if (attribute is StringLengthAttribute stringLengthAttribute) - { - dto.Configuration["MaximumLength"] = stringLengthAttribute.MaximumLength; - dto.Configuration["MinimumLength"] = stringLengthAttribute.MinimumLength; - } - - //TODO: Others! - - return dto; - } + public Dictionary Config { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs similarity index 89% rename from framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs rename to framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index f87fef31aa..6e72a6863e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -10,23 +10,29 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending { public class CachedObjectExtensionsDtoService : ICachedObjectExtensionsDtoService, ISingletonDependency { - private volatile ObjectExtensionsDto _cachedValue; - private readonly object _syncLock = new object(); + protected IExtensionPropertyAttributeDtoFactory ExtensionPropertyAttributeDtoFactory { get; } + protected volatile ObjectExtensionsDto CachedValue; + protected readonly object SyncLock = new object(); + + public CachedObjectExtensionsDtoService(IExtensionPropertyAttributeDtoFactory extensionPropertyAttributeDtoFactory) + { + ExtensionPropertyAttributeDtoFactory = extensionPropertyAttributeDtoFactory; + } public virtual ObjectExtensionsDto Get() { - if (_cachedValue == null) + if (CachedValue == null) { - lock (_syncLock) + lock (SyncLock) { - if (_cachedValue == null) + if (CachedValue == null) { - _cachedValue = GenerateCacheValue(); + CachedValue = GenerateCacheValue(); } } } - return _cachedValue; + return CachedValue; } protected virtual ObjectExtensionsDto GenerateCacheValue() @@ -137,7 +143,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending foreach (var attribute in propertyConfig.Attributes) { extensionPropertyDto.Attributes.Add( - ExtensionPropertyAttributeDto.Create(attribute) + ExtensionPropertyAttributeDtoFactory.Create(attribute) ); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDtoFactory.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDtoFactory.cs new file mode 100644 index 0000000000..18c5f62591 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDtoFactory.cs @@ -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 CreateConfiguration(Attribute attribute) + { + var configuration = new Dictionary(); + + AddPropertiesToConfiguration(attribute, configuration); + + return configuration; + } + + protected virtual void AddPropertiesToConfiguration(Attribute attribute, Dictionary 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; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs similarity index 100% rename from framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs rename to framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/IExtensionPropertyAttributeDtoFactory.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/IExtensionPropertyAttributeDtoFactory.cs new file mode 100644 index 0000000000..5e01077604 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/IExtensionPropertyAttributeDtoFactory.cs @@ -0,0 +1,9 @@ +using System; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending +{ + public interface IExtensionPropertyAttributeDtoFactory + { + ExtensionPropertyAttributeDto Create(Attribute attribute); + } +} \ No newline at end of file From 25085b2959bf315814507cef9835d9e2a8d673a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 15 May 2020 03:10:40 +0300 Subject: [PATCH 059/110] Improve extension property model binding. --- .../AbpExtraPropertyModelBinder.cs | 4 ++- .../ObjectExtendingPropertyInfoExtensions.cs | 36 ++++++++++++++----- .../Volo/Abp/Reflection/TypeHelper.cs | 13 +++++++ .../Volo/Abp/Reflection/TypeHelper_Tests.cs | 8 +++++ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs index d3f570b1ce..c94ad08e74 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs @@ -1,7 +1,9 @@ using System; +using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; using Volo.Abp.ObjectExtending; +using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc.ModelBinding { @@ -59,7 +61,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding return value; } - return Convert.ChangeType(value, propertyInfo.Type); + return TypeHelper.ConvertFromString(propertyInfo.Type, value); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs index 9bb6c903a9..04339eedd3 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; @@ -6,6 +7,31 @@ namespace Volo.Abp.ObjectExtending { public static class ObjectExtensionPropertyInfoAspNetCoreMvcExtensions { + private static readonly HashSet NumberTypes = new HashSet { + typeof(int), + typeof(long), + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(int?), + typeof(long?), + typeof(byte?), + typeof(sbyte?), + typeof(short?), + typeof(ushort?), + typeof(uint?), + typeof(long?), + typeof(ulong?), + typeof(float?), + typeof(double?), + }; + public static string GetInputType(this ObjectExtensionPropertyInfo propertyInfo) { foreach (var attribute in propertyInfo.Attributes) @@ -79,15 +105,7 @@ namespace Volo.Abp.ObjectExtending 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)) + if (NumberTypes.Contains(type)) { return "number"; } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 11f43f852e..63b3538c3a 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -264,5 +265,17 @@ namespace Volo.Abp.Reflection return type.FullName; } + + public static object ConvertFromString(string value) + { + return ConvertFromString(typeof(TTargetType), value); + } + + public static object ConvertFromString(Type targetType, string value) + { + return TypeDescriptor + .GetConverter(targetType) + .ConvertFromString(value); + } } } diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs index 23eba093a6..02056aab20 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs @@ -75,6 +75,14 @@ namespace Volo.Abp.Reflection TypeHelper.GetDefaultValue(typeof(string)).ShouldBeNull(); } + [Fact] + public void ConvertFromString() + { + TypeHelper.ConvertFromString("42").ShouldBe(42); + TypeHelper.ConvertFromString("42").ShouldBe((int?)42); + TypeHelper.ConvertFromString(null).ShouldBeNull(); + } + public class MyDictionary : Dictionary { From e97f07b011b1786255ad31684579e87330b8f7ae Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 15 May 2020 12:21:29 +0800 Subject: [PATCH 060/110] Put UseAbpRequestLocalization just after the UseAuthentication --- .../BackendAdminApp.Host/BackendAdminAppHostModule.cs | 4 ++-- .../PublicWebSite.Host/PublicWebSiteHostModule.cs | 4 ++-- .../BloggingService.Host/BloggingServiceHostModule.cs | 4 ++-- .../IdentityService.Host/IdentityServiceHostModule.cs | 7 ++++--- .../ProductService.Host/ProductServiceHostModule.cs | 4 ++-- .../TenantManagementServiceHostModule.cs | 4 ++-- .../MyProjectNameHttpApiHostModule.cs | 4 ++-- .../MyProjectNameHttpApiHostModule.cs | 6 +++--- .../MyProjectNameIdentityServerModule.cs | 6 ++++-- .../MyProjectNameWebModule.cs | 4 ++-- .../MyProjectNameWebModule.cs | 4 ++-- .../MyProjectNameWebTestModule.cs | 6 +++--- .../MyProjectNameHttpApiHostModule.cs | 8 +++++--- .../MyProjectNameIdentityServerModule.cs | 8 +++++--- .../MyProjectNameWebHostModule.cs | 2 +- .../MyProjectNameWebUnifiedModule.cs | 3 ++- 16 files changed, 43 insertions(+), 35 deletions(-) diff --git a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs index 4414dc73ae..0976699f07 100644 --- a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs +++ b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -118,8 +118,8 @@ namespace BackendAdminApp.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); diff --git a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs index 9ea7543228..1b2c0de935 100644 --- a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs +++ b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -96,8 +96,8 @@ namespace PublicWebSite.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); diff --git a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs index c546c57ae5..97c351e93e 100644 --- a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -120,7 +120,6 @@ namespace BloggingService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); app.Use(async (ctx, next) => @@ -137,6 +136,7 @@ namespace BloggingService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); + app.UseAbpRequestLocalization(); //TODO: localization? if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); diff --git a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs index 49c0bbd933..b950da980f 100644 --- a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Builder; @@ -101,12 +101,14 @@ namespace IdentityService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); + app.UseAbpRequestLocalization(); //TODO: localization? + if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } + app.Use(async (ctx, next) => { var currentPrincipalAccessor = ctx.RequestServices.GetRequiredService(); @@ -121,7 +123,6 @@ namespace IdentityService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); - app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs index 9fb074fe76..6bb74c1f2b 100644 --- a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Builder; @@ -104,7 +104,6 @@ namespace ProductService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); app.Use(async (ctx, next) => @@ -121,6 +120,7 @@ namespace ProductService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); + app.UseAbpRequestLocalization(); //TODO: localization? if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); diff --git a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs index f068c2adf9..61c0d5105a 100644 --- a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Builder; @@ -105,7 +105,6 @@ namespace TenantManagementService.Host app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); //TODO: localization? app.UseAuthentication(); app.Use(async (ctx, next) => @@ -122,6 +121,7 @@ namespace TenantManagementService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); + app.UseAbpRequestLocalization(); //TODO: localization? if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index f560ac7e0a..5368ac81ed 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using Microsoft.AspNetCore.Builder; @@ -178,8 +178,8 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); - app.UseAbpRequestLocalization(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs index 06f52defb1..5b950c1d23 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Net.Http; @@ -168,10 +168,10 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); - app.UseAbpRequestLocalization(); app.UseAuthentication(); app.UseJwtTokenMiddleware(); - + app.UseAbpRequestLocalization(); + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs index e3d6d5aa67..6835c537dc 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using Localization.Resources.AbpUi; @@ -154,12 +154,14 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); - app.UseAbpRequestLocalization(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseIdentityServer(); app.UseAuthorization(); app.UseAuditing(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs index 86738e3e1b..12c20e9ffe 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Builder; @@ -225,8 +225,8 @@ namespace MyCompanyName.MyProjectName.Web app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs index 541a2c5e3a..1a578ac994 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Localization.Resources.AbpUi; using Microsoft.AspNetCore; @@ -204,9 +204,9 @@ namespace MyCompanyName.MyProjectName.Web app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); app.UseAuthentication(); app.UseJwtTokenMiddleware(); + app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs index edde134027..28b5e3b55b 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyProjectNameWebTestModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using Localization.Resources.AbpUi; @@ -88,9 +88,9 @@ namespace MyCompanyName.MyProjectName }); app.UseVirtualFiles(); - app.UseRouting(); - app.UseAbpRequestLocalization(); + app.UseRouting(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); app.UseAuthorization(); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index 8de889e88e..845126a67b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using IdentityModel; @@ -164,13 +164,15 @@ namespace MyCompanyName.MyProjectName app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseCors(DefaultCorsPolicyName); - app.UseAbpRequestLocalization(); + app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); + app.UseAbpRequestLocalization(); + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAuthorization(); app.UseSwagger(); app.UseSwaggerUI(options => diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs index 4bfd5f689a..11627c93b9 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; @@ -185,14 +185,16 @@ namespace MyCompanyName.MyProjectName app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); - app.UseCors(DefaultCorsPolicyName); - app.UseAbpRequestLocalization(); + app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); app.UseJwtTokenMiddleware(); + app.UseAbpRequestLocalization(); + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseIdentityServer(); app.UseAuthorization(); app.UseSwagger(); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs index 6d16829e25..cf306f5277 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs @@ -234,8 +234,8 @@ namespace MyCompanyName.MyProjectName app.UseHttpsRedirection(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs index 57b77e5f50..653738d8ac 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs @@ -127,9 +127,10 @@ namespace MyCompanyName.MyProjectName app.UseHttpsRedirection(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAbpRequestLocalization(); app.UseAuthentication(); + app.UseAbpRequestLocalization(); app.UseAuthorization(); + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); From d0ef06e9287c84352d70de97353c5b5dfba52067 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 15 May 2020 14:44:27 +0800 Subject: [PATCH 061/110] Put UseAbpRequestLocalization after the UseMultiTenancy --- .../BackendAdminApp.Host/BackendAdminAppHostModule.cs | 4 +++- .../PublicWebSite.Host/PublicWebSiteHostModule.cs | 4 +++- .../BloggingService.Host/BloggingServiceHostModule.cs | 4 +++- .../IdentityService.Host/IdentityServiceHostModule.cs | 2 +- .../ProductService.Host/ProductServiceHostModule.cs | 4 +++- .../TenantManagementServiceHostModule.cs | 4 +++- .../MyProjectNameHttpApiHostModule.cs | 4 +++- .../MyProjectNameHttpApiHostModule.cs | 4 ++-- .../MyProjectNameIdentityServerModule.cs | 2 +- .../MyProjectNameWebModule.cs | 2 +- .../MyProjectNameWebModule.cs | 3 ++- .../MyProjectNameHttpApiHostModule.cs | 2 +- .../MyProjectNameIdentityServerModule.cs | 4 ++-- .../MyProjectNameWebHostModule.cs | 5 ++--- .../MyProjectNameWebUnifiedModule.cs | 7 ++++--- 15 files changed, 34 insertions(+), 21 deletions(-) diff --git a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs index 0976699f07..b351a33fb6 100644 --- a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs +++ b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs @@ -119,11 +119,13 @@ namespace BackendAdminApp.Host app.UseVirtualFiles(); app.UseRouting(); app.UseAuthentication(); - app.UseAbpRequestLocalization(); + if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } + + app.UseAbpRequestLocalization(); app.UseAuthorization(); app.UseSwagger(); app.UseSwaggerUI(options => diff --git a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs index 1b2c0de935..e1c0b8c215 100644 --- a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs +++ b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs @@ -97,11 +97,13 @@ namespace PublicWebSite.Host app.UseVirtualFiles(); app.UseRouting(); app.UseAuthentication(); - app.UseAbpRequestLocalization(); + if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } + + app.UseAbpRequestLocalization(); app.UseAuthorization(); app.UseConfiguredEndpoints(); } diff --git a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs index 97c351e93e..bc9209c72a 100644 --- a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs @@ -136,11 +136,13 @@ namespace BloggingService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); - app.UseAbpRequestLocalization(); //TODO: localization? + if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } + + app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs index b950da980f..457950cba4 100644 --- a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs @@ -102,7 +102,6 @@ namespace IdentityService.Host app.UseVirtualFiles(); app.UseRouting(); app.UseAuthentication(); - app.UseAbpRequestLocalization(); //TODO: localization? if (MsDemoConsts.IsMultiTenancyEnabled) { @@ -123,6 +122,7 @@ namespace IdentityService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); + app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs index 6bb74c1f2b..4968ce75a4 100644 --- a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs @@ -120,11 +120,13 @@ namespace ProductService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); - app.UseAbpRequestLocalization(); //TODO: localization? + if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } + + app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs index 61c0d5105a..504713c047 100644 --- a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs @@ -121,11 +121,13 @@ namespace TenantManagementService.Host currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); await next(); }); - app.UseAbpRequestLocalization(); //TODO: localization? + if (MsDemoConsts.IsMultiTenancyEnabled) { app.UseMultiTenancy(); } + + app.UseAbpRequestLocalization(); //TODO: localization? app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index 5368ac81ed..591b92ae44 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -179,11 +179,13 @@ namespace MyCompanyName.MyProjectName app.UseRouting(); app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); - app.UseAbpRequestLocalization(); + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + + app.UseAbpRequestLocalization(); app.UseAuthorization(); app.UseSwagger(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs index 5b950c1d23..0cd3762047 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyProjectNameHttpApiHostModule.cs @@ -169,14 +169,14 @@ namespace MyCompanyName.MyProjectName app.UseRouting(); app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); - app.UseJwtTokenMiddleware(); - app.UseAbpRequestLocalization(); + app.UseJwtTokenMiddleware(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAbpRequestLocalization(); app.UseIdentityServer(); app.UseAuthorization(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs index 6835c537dc..8d23b73d0f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs @@ -155,13 +155,13 @@ namespace MyCompanyName.MyProjectName app.UseRouting(); app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); - app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAbpRequestLocalization(); app.UseIdentityServer(); app.UseAuthorization(); app.UseAuditing(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs index 12c20e9ffe..9e1930a260 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs @@ -226,13 +226,13 @@ namespace MyCompanyName.MyProjectName.Web app.UseVirtualFiles(); app.UseRouting(); app.UseAuthentication(); - app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAbpRequestLocalization(); app.UseAuthorization(); app.UseSwagger(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs index 1a578ac994..8015f9ec45 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -206,12 +206,13 @@ namespace MyCompanyName.MyProjectName.Web app.UseRouting(); app.UseAuthentication(); app.UseJwtTokenMiddleware(); - app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + + app.UseAbpRequestLocalization(); app.UseIdentityServer(); app.UseAuthorization(); app.UseSwagger(); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index 845126a67b..ab77d8050b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -166,13 +166,13 @@ namespace MyCompanyName.MyProjectName app.UseRouting(); app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); - app.UseAbpRequestLocalization(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAbpRequestLocalization(); app.UseAuthorization(); app.UseSwagger(); app.UseSwaggerUI(options => diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs index 11627c93b9..41d31efc53 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs @@ -188,13 +188,13 @@ namespace MyCompanyName.MyProjectName app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); app.UseJwtTokenMiddleware(); - app.UseAbpRequestLocalization(); - + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAbpRequestLocalization(); app.UseIdentityServer(); app.UseAuthorization(); app.UseSwagger(); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs index cf306f5277..7381787bc3 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs @@ -234,17 +234,16 @@ namespace MyCompanyName.MyProjectName app.UseHttpsRedirection(); app.UseVirtualFiles(); app.UseRouting(); - app.UseAuthentication(); - app.UseAbpRequestLocalization(); + app.UseAuthentication(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAbpRequestLocalization(); app.UseAuthorization(); - app.UseSwagger(); app.UseSwaggerUI(options => { diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs index 653738d8ac..246db32e3e 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyProjectNameWebUnifiedModule.cs @@ -128,14 +128,15 @@ namespace MyCompanyName.MyProjectName app.UseVirtualFiles(); app.UseRouting(); app.UseAuthentication(); - app.UseAbpRequestLocalization(); - app.UseAuthorization(); - + if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } + app.UseAbpRequestLocalization(); + app.UseAuthorization(); + app.UseSwagger(); app.UseSwaggerUI(options => { From 6ce85f2df7fdb2951b7cd1c9ed47119069eb3e44 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 15 May 2020 19:45:34 +0800 Subject: [PATCH 062/110] Fix modal open event bug --- .../bootstrap/modal-manager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js index 21a8e0ce21..cc4122a373 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js @@ -95,8 +95,11 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f }); _$modal.on('shown.bs.modal', function () { - //focuses first element if it's a typeable input. + //focuses first element if it's a typeable input. var $firstVisibleInput = _$modal.find('input:not([type=hidden]):first'); + + _onOpenCallbacks.triggerAll(_publicApi); + if ($firstVisibleInput.hasClass("datepicker")) { return; //don't pop-up date pickers... } @@ -107,7 +110,6 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f } $firstVisibleInput.focus(); - _onOpenCallbacks.triggerAll(_publicApi); }); var modalClass = abp.modals[options.modalClass]; From 72b9b43e9130c098a085dbe563a231e8eb6e2f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 15 May 2020 23:29:42 +0300 Subject: [PATCH 063/110] Resolved #3963: Create a jstree package & bundle contributor. --- .../JsTree/JQueryFormScriptContributor.cs | 16 ++++++++++++ .../Mvc/UI/Packages/JsTree/JsTreeOptions.cs | 13 ++++++++++ .../Packages/JsTree/JsTreeStyleContributor.cs | 26 +++++++++++++++++++ npm/packs/jstree/abp.resourcemapping.js | 5 ++++ npm/packs/jstree/package.json | 12 +++++++++ 5 files changed, 72 insertions(+) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JQueryFormScriptContributor.cs create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeOptions.cs create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeStyleContributor.cs create mode 100644 npm/packs/jstree/abp.resourcemapping.js create mode 100644 npm/packs/jstree/package.json diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JQueryFormScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JQueryFormScriptContributor.cs new file mode 100644 index 0000000000..ca131c8aac --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JQueryFormScriptContributor.cs @@ -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"); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeOptions.cs new file mode 100644 index 0000000000..d1dbe812bf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeOptions.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JsTree +{ + public class JsTreeOptions + { + /// + /// 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". + /// + public string StylePath { get; set; } = "/libs/jstree/themes/default/style.min.css"; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeStyleContributor.cs new file mode 100644 index 0000000000..5944310c2c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JsTree/JsTreeStyleContributor.cs @@ -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>() + .Value; + + if (options.StylePath.IsNullOrEmpty()) + { + return; + } + + context.Files.AddIfNotContains(options.StylePath); + } + } +} \ No newline at end of file diff --git a/npm/packs/jstree/abp.resourcemapping.js b/npm/packs/jstree/abp.resourcemapping.js new file mode 100644 index 0000000000..89b3e09d64 --- /dev/null +++ b/npm/packs/jstree/abp.resourcemapping.js @@ -0,0 +1,5 @@ +module.exports = { + mappings: { + "@node_modules/jstree/dist/**/*.*": "@libs/jstree/" + } +} \ No newline at end of file diff --git a/npm/packs/jstree/package.json b/npm/packs/jstree/package.json new file mode 100644 index 0000000000..feee4a81e2 --- /dev/null +++ b/npm/packs/jstree/package.json @@ -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" +} From 61c5186cd1b01b67032d30cb5dad36ee179d3375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 15 May 2020 23:58:13 +0300 Subject: [PATCH 064/110] Update badges --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 14b044f8b1..f89a8a86aa 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # ABP -[![Build Status](http://vjenkins.dynu.net:5480/job/abp/badge/icon)](http://ci.volosoft.com:5480/blue/organizations/jenkins/abp/activity) [![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) -[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) [![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) +[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) This project is the next generation of the [ASP.NET Boilerplate](https://aspnetboilerplate.com/) web application framework. See [the announcement](https://blog.abp.io/abp/Abp-vNext-Announcement). From dc3360f92139df91f2ef4bf999840368d1ca903c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 00:03:54 +0300 Subject: [PATCH 065/110] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f89a8a86aa..7dd11a5362 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ABP +[![Build & Test](https://github.com/abpframework/abp/workflows/Main/badge.svg)](https://github.com/abpframework/abp/actions?query=workflow%3AMain) [![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) [![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) [![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) From 687fac36c83791681356f42c3b7db2205c7c12ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 02:04:29 +0300 Subject: [PATCH 066/110] add claims mapping middleware to the MyProjectNameHttpApiHostModule --- .../MyProjectNameHttpApiHostModule.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index e425512797..c5f72ff5b7 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Claims; using IdentityModel; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; @@ -170,6 +172,20 @@ namespace MyCompanyName.MyProjectName { app.UseMultiTenancy(); } + app.Use(async (ctx, next) => + { + var currentPrincipalAccessor = ctx.RequestServices.GetRequiredService(); + var map = new Dictionary() + { + { "sub", AbpClaimTypes.UserId }, + { "role", AbpClaimTypes.Role }, + { "email", AbpClaimTypes.Email }, + //any other map + }; + var mapClaims = currentPrincipalAccessor.Principal.Claims.Where(p => map.Keys.Contains(p.Type)).ToList(); + currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)))); + await next(); + }); app.UseAuthorization(); app.UseAbpRequestLocalization(); app.UseSwagger(); From 73bf96c86f898be10bf51ef626b23547462fe1e3 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Sat, 16 May 2020 02:10:41 +0300 Subject: [PATCH 067/110] Update en.json --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 14a4eb345c..46cd1cb470 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -127,7 +127,7 @@ "Generate": "Generate", "MissingQuantityField": "The quantity field is required!", "MissingPriceField": "The Price field is required!", - "CodeUsageStatus": "Code Usage Status", + "CodeUsageStatus": "Status", "Country": "Country", "DeveloperCount": "Developer Count", "RequestCode": "Request Code", @@ -147,6 +147,7 @@ "EmailSent": "Email Sent", "SuccessfullySent": "Successfully Sent", "SuccessfullyDeleted": "Successfully Deleted", - "DiscountRequestDeletionWarningMessage": "Discount request will be deleted" + "DiscountRequestDeletionWarningMessage": "Discount request will be deleted" , + "BusinessType": "Business Type" } } \ No newline at end of file From 94bb704bb64ced26cf1ad4fa644578381427f408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 02:39:10 +0300 Subject: [PATCH 068/110] Delete MyProjectNamePage.cs --- .../Pages/MyProjectNamePage.cs | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/MyProjectNamePage.cs diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/MyProjectNamePage.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/MyProjectNamePage.cs deleted file mode 100644 index 5fe044221d..0000000000 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/MyProjectNamePage.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Localization; -using Microsoft.AspNetCore.Mvc.Razor.Internal; -using MyCompanyName.MyProjectName.Localization; -using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; - -namespace MyCompanyName.MyProjectName.Web.Pages -{ - /* Inherit your UI Pages from this class. To do that, add this line to your Pages (.cshtml files under the Page folder): - * @inherits MyCompanyName.MyProjectName.Web.Pages.MyProjectNamePage - */ - public abstract class MyProjectNamePage : AbpPage - { - [RazorInject] - public IHtmlLocalizer L { get; set; } - } -} From 146ec7309cdae1976f87c276b29cc3dab0b3fffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 02:39:36 +0300 Subject: [PATCH 069/110] Implement #3967 for AbpPageModel. --- .../Mvc/UI/RazorPages/AbpPageModel.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs index 8d62a4e188..f616a2f32d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Volo.Abp.AspNetCore.Mvc.UI.Alerts; using Volo.Abp.AspNetCore.Mvc.Validation; using Volo.Abp.Guids; +using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; using Volo.Abp.ObjectMapping; using Volo.Abp.Settings; @@ -90,17 +91,29 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.RazorPages { if (_localizer == null) { - if (LocalizationResourceType == null) - { - throw new AbpException($"{nameof(LocalizationResourceType)} should be set before using the {nameof(L)} object!"); - } - - _localizer = StringLocalizerFactory.Create(LocalizationResourceType); + _localizer = CreateLocalizer(); } return _localizer; } } + + protected virtual IStringLocalizer CreateLocalizer() + { + if (LocalizationResourceType != null) + { + return StringLocalizerFactory.Create(LocalizationResourceType); + } + + var localizer = StringLocalizerFactory.CreateDefaultOrNull(); + if (localizer == null) + { + throw new AbpException($"Set {nameof(LocalizationResourceType)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); + } + + return localizer; + } + private IStringLocalizer _localizer; protected Type LocalizationResourceType { get; set; } From c122fdfad6d8fb8f670db060eaaf6ff55473fb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 02:48:29 +0300 Subject: [PATCH 070/110] Resolved #3967: Fallback to the default localization resource for base classes with the L property --- .../Mvc/UI/RazorPages/AbpPageModel.cs | 32 +++++----- .../Volo/Abp/AspNetCore/Mvc/AbpController.cs | 29 +++++++++- .../Volo/Abp/AspNetCore/SignalR/AbpHub.cs | 58 ++++++++++++++++++- .../Services/ApplicationService.cs | 29 +++++++++- 4 files changed, 128 insertions(+), 20 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs index f616a2f32d..db7b9041b7 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/RazorPages/AbpPageModel.cs @@ -98,22 +98,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.RazorPages } } - protected virtual IStringLocalizer CreateLocalizer() - { - if (LocalizationResourceType != null) - { - return StringLocalizerFactory.Create(LocalizationResourceType); - } - - var localizer = StringLocalizerFactory.CreateDefaultOrNull(); - if (localizer == null) - { - throw new AbpException($"Set {nameof(LocalizationResourceType)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); - } - - return localizer; - } - private IStringLocalizer _localizer; protected Type LocalizationResourceType { get; set; } @@ -165,5 +149,21 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.RazorPages TempData = TempData }; } + + protected virtual IStringLocalizer CreateLocalizer() + { + if (LocalizationResourceType != null) + { + return StringLocalizerFactory.Create(LocalizationResourceType); + } + + var localizer = StringLocalizerFactory.CreateDefaultOrNull(); + if (localizer == null) + { + throw new AbpException($"Set {nameof(LocalizationResourceType)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); + } + + return localizer; + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs index 2a3a25373e..52f01e0b2c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs @@ -101,7 +101,18 @@ namespace Volo.Abp.AspNetCore.Mvc public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory); private IStringLocalizerFactory _stringLocalizerFactory; - public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource)); + public IStringLocalizer L + { + get + { + if (_localizer == null) + { + _localizer = CreateLocalizer(); + } + + return _localizer; + } + } private IStringLocalizer _localizer; protected Type LocalizationResource @@ -121,5 +132,21 @@ namespace Volo.Abp.AspNetCore.Mvc { ModelValidator?.Validate(ModelState); } + + protected virtual IStringLocalizer CreateLocalizer() + { + if (LocalizationResource != null) + { + return StringLocalizerFactory.Create(LocalizationResource); + } + + var localizer = StringLocalizerFactory.CreateDefaultOrNull(); + if (localizer == null) + { + throw new AbpException($"Set {nameof(LocalizationResource)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); + } + + return localizer; + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs index a1dc1437ea..0a58adf098 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpHub.cs @@ -57,7 +57,18 @@ namespace Volo.Abp.AspNetCore.SignalR public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory); private IStringLocalizerFactory _stringLocalizerFactory; - public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource)); + public IStringLocalizer L + { + get + { + if (_localizer == null) + { + _localizer = CreateLocalizer(); + } + + return _localizer; + } + } private IStringLocalizer _localizer; protected Type LocalizationResource @@ -70,6 +81,22 @@ namespace Volo.Abp.AspNetCore.SignalR } } private Type _localizationResource = typeof(DefaultResource); + + protected virtual IStringLocalizer CreateLocalizer() + { + if (LocalizationResource != null) + { + return StringLocalizerFactory.Create(LocalizationResource); + } + + var localizer = StringLocalizerFactory.CreateDefaultOrNull(); + if (localizer == null) + { + throw new AbpException($"Set {nameof(LocalizationResource)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); + } + + return localizer; + } } public abstract class AbpHub : Hub @@ -118,7 +145,18 @@ namespace Volo.Abp.AspNetCore.SignalR public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory); private IStringLocalizerFactory _stringLocalizerFactory; - public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource)); + public IStringLocalizer L + { + get + { + if (_localizer == null) + { + _localizer = CreateLocalizer(); + } + + return _localizer; + } + } private IStringLocalizer _localizer; protected Type LocalizationResource @@ -131,5 +169,21 @@ namespace Volo.Abp.AspNetCore.SignalR } } private Type _localizationResource = typeof(DefaultResource); + + protected virtual IStringLocalizer CreateLocalizer() + { + if (LocalizationResource != null) + { + return StringLocalizerFactory.Create(LocalizationResource); + } + + var localizer = StringLocalizerFactory.CreateDefaultOrNull(); + if (localizer == null) + { + throw new AbpException($"Set {nameof(LocalizationResource)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); + } + + return localizer; + } } } diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs index 32c90677b8..552aae426d 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs @@ -109,7 +109,18 @@ namespace Volo.Abp.Application.Services public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(ref _stringLocalizerFactory); private IStringLocalizerFactory _stringLocalizerFactory; - public IStringLocalizer L => _localizer ?? (_localizer = StringLocalizerFactory.Create(LocalizationResource)); + public IStringLocalizer L + { + get + { + if (_localizer == null) + { + _localizer = CreateLocalizer(); + } + + return _localizer; + } + } private IStringLocalizer _localizer; protected Type LocalizationResource @@ -147,5 +158,21 @@ namespace Volo.Abp.Application.Services await AuthorizationService.CheckAsync(policyName); } + + protected virtual IStringLocalizer CreateLocalizer() + { + if (LocalizationResource != null) + { + return StringLocalizerFactory.Create(LocalizationResource); + } + + var localizer = StringLocalizerFactory.CreateDefaultOrNull(); + if (localizer == null) + { + throw new AbpException($"Set {nameof(LocalizationResource)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); + } + + return localizer; + } } } \ No newline at end of file From 94a5bffebefb4e04ba45ee8d111aaa92d6bc50b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 02:51:35 +0300 Subject: [PATCH 071/110] Fix Index.cshtml of the app template --- .../src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml index e3df314288..a2fc159766 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml @@ -1,6 +1,10 @@ @page -@inherits MyCompanyName.MyProjectName.Web.Pages.MyProjectNamePage +@using Microsoft.AspNetCore.Mvc.Localization +@using MyCompanyName.MyProjectName.Localization +@using Volo.Abp.Users @model MyCompanyName.MyProjectName.Web.Pages.IndexModel +@inject IHtmlLocalizer +@inject ICurrentUser CurrentUser @section styles { From 8ad6a3a124fa55e61a3171e5c4ab8b9c74b62d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 02:51:54 +0300 Subject: [PATCH 072/110] Update Index.cshtml --- .../src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml index a2fc159766..7329ce019b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/Index.cshtml @@ -3,7 +3,7 @@ @using MyCompanyName.MyProjectName.Localization @using Volo.Abp.Users @model MyCompanyName.MyProjectName.Web.Pages.IndexModel -@inject IHtmlLocalizer +@inject IHtmlLocalizer L @inject ICurrentUser CurrentUser @section styles { From 67a230d3a0e4f92ab65ffee6ebb8c5cbc307e7eb Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Sat, 16 May 2020 14:02:47 +0800 Subject: [PATCH 073/110] Rename main.yml to build-and-test.yml Resolve #3965 --- .github/workflows/{main.yml => build-and-test.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{main.yml => build-and-test.yml} (96%) diff --git a/.github/workflows/main.yml b/.github/workflows/build-and-test.yml similarity index 96% rename from .github/workflows/main.yml rename to .github/workflows/build-and-test.yml index e33a07a197..395f88351a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/build-and-test.yml @@ -1,4 +1,4 @@ -name: "Main" +name: "build and test" on: pull_request: paths: From 37490be9a0fb16a859ba77d80d926096f65dfeca Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Sat, 16 May 2020 14:12:04 +0800 Subject: [PATCH 074/110] Update README.md https://github.com/abpframework/abp/issues/3965 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7dd11a5362..72a47ddc4b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ABP -[![Build & Test](https://github.com/abpframework/abp/workflows/Main/badge.svg)](https://github.com/abpframework/abp/actions?query=workflow%3AMain) +![build and test](https://github.com/abpframework/abp/workflows/build%20and%20test/badge.svg) [![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) [![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) [![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) From 54c685f80170d5689b2fdcd43e8fe306d7b6fe5f Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Sat, 16 May 2020 17:31:05 +0800 Subject: [PATCH 075/110] Change HttpApiClient project target to netstandard2.0 --- .../MyCompanyName.MyProjectName.HttpApi.Client.csproj | 2 +- .../MyCompanyName.MyProjectName.HttpApi.Client.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj index 4c70886aaa..fa83d6c25e 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + netstandard2.0 MyCompanyName.MyProjectName diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj index 1466849524..4fb9f73c33 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Client/MyCompanyName.MyProjectName.HttpApi.Client.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + netstandard2.0 MyCompanyName.MyProjectName From 5f20c7726ee9c9213ac8abb6bd5d0fdc11d396b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 23:14:48 +0300 Subject: [PATCH 076/110] Resolved #3978: ObjectExtensionManager should automatically add RequiredAttribute & EnumDataTypeAttribute when needed --- .../Volo/Abp/Reflection/TypeHelper.cs | 24 ++++++ .../ExtensionPropertyHelper.cs | 23 ++++++ .../ExtensionPropertyConfiguration.cs | 2 + .../ModuleExtensionConfigurationHelper.cs | 1 + .../ObjectExtensionPropertyInfo.cs | 3 +- .../Volo/Abp/Reflection/TypeHelper_Tests.cs | 7 ++ .../ObjectExtensionManager_Tests.cs | 74 ++++++++++++++++++- 7 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 63b3538c3a..87b94774e7 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -10,6 +10,30 @@ namespace Volo.Abp.Reflection { public static class TypeHelper { + private static readonly HashSet NonNullablePrimitiveTypes = new HashSet + { + typeof(byte), + typeof(short), + typeof(int), + typeof(long), + typeof(sbyte), + typeof(ushort), + typeof(uint), + typeof(ulong), + typeof(bool), + typeof(float), + typeof(decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid) + }; + + public static bool IsNonNullablePrimitiveType(Type type) + { + return NonNullablePrimitiveTypes.Contains(type); + } + public static bool IsFunc(object obj) { if (obj == null) diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs new file mode 100644 index 0000000000..507f67997a --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Reflection; + +namespace Volo.Abp.ObjectExtending +{ + public static class ExtensionPropertyHelper + { + public static IEnumerable GetDefaultAttributes(Type type) + { + if (TypeHelper.IsNonNullablePrimitiveType(type) || type.IsEnum) + { + yield return new RequiredAttribute(); + } + + if (type.IsEnum) + { + yield return new EnumDataTypeAttribute(type); + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs index 0ff3941f4e..47882fdf93 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs @@ -61,6 +61,8 @@ namespace Volo.Abp.ObjectExtending.Modularity Entity = new ExtensionPropertyEntityConfiguration(); UI = new ExtensionPropertyUiConfiguration(); Api = new ExtensionPropertyApiConfiguration(); + + Attributes.AddRange(ExtensionPropertyHelper.GetDefaultAttributes(Type)); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs index 1f1edd757e..d819918ac5 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs @@ -148,6 +148,7 @@ namespace Volo.Abp.ObjectExtending.Modularity propertyConfig.Name, property => { + property.Attributes.Clear(); property.Attributes.AddRange(propertyConfig.Attributes); property.DisplayName = propertyConfig.DisplayName; property.Validators.AddRange(propertyConfig.Validators); diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs index 5d55325477..e89598e6d2 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; -using Microsoft.Extensions.Localization; using Volo.Abp.Localization; namespace Volo.Abp.ObjectExtending @@ -61,6 +60,8 @@ namespace Volo.Abp.ObjectExtending ValidationAttributes = new List(); Attributes = new List(); Validators = new List>(); + + Attributes.AddRange(ExtensionPropertyHelper.GetDefaultAttributes(Type)); } } } diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs index 02056aab20..978bf9de54 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs @@ -7,6 +7,13 @@ namespace Volo.Abp.Reflection { public class TypeHelper_Tests { + [Fact] + public void IsNonNullablePrimitiveType() + { + TypeHelper.IsNonNullablePrimitiveType(typeof(int)).ShouldBeTrue(); + TypeHelper.IsNonNullablePrimitiveType(typeof(string)).ShouldBeFalse(); + } + [Fact] public void Should_Generic_Type_From_Nullable() { diff --git a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs index 857e2fd155..cb5b308c64 100644 --- a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs +++ b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.ComponentModel.DataAnnotations; +using System.Linq; using Shouldly; using Xunit; @@ -22,7 +23,7 @@ namespace Volo.Abp.ObjectExtending var objectExtension = _objectExtensionManager.GetOrNull(); objectExtension.ShouldNotBeNull(); - + var properties = objectExtension.GetProperties(); properties.Count.ShouldBe(1); properties.FirstOrDefault(p => p.Name == "TestProp").ShouldNotBeNull(); @@ -55,9 +56,78 @@ namespace Volo.Abp.ObjectExtending property.Configuration["TestConfig2"].ShouldBe("TestConfig2-Value"); } + [Fact] + public void Should_Automatically_Add_RequiredAttribute_To_Non_Nullable_Types_And_Enums() + { + _objectExtensionManager + .AddOrUpdateProperty("IntProp") + .AddOrUpdateProperty("BoolProp") + .AddOrUpdateProperty("NullableIntProp") + .AddOrUpdateProperty("StringProp") + .AddOrUpdateProperty("EnumProp"); + + _objectExtensionManager + .GetPropertyOrNull("IntProp") + .Attributes + .ShouldContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("BoolProp") + .Attributes + .ShouldContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("EnumProp") + .Attributes + .ShouldContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("NullableIntProp") + .Attributes + .ShouldNotContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("StringProp") + .Attributes + .ShouldNotContain(x => x is RequiredAttribute); + } + + [Fact] + public void Should_Automatically_Add_EnumDataTypeAttribute_For_Enums() + { + _objectExtensionManager + .AddOrUpdateProperty("EnumProp"); + + _objectExtensionManager + .GetPropertyOrNull("EnumProp") + .Attributes + .ShouldContain(x => x is EnumDataTypeAttribute); + } + + [Fact] + public void Should_Be_Able_To_Clear_Auto_Added_Attributes() + { + _objectExtensionManager + .AddOrUpdateProperty("IntProp", property => + { + property.Attributes.Clear(); + }); + + _objectExtensionManager + .GetPropertyOrNull("IntProp") + .Attributes + .ShouldNotContain(x => x is RequiredAttribute); + } + private class MyExtensibleObject : ExtensibleObject { } + + private enum MyTestEnum + { + EnumValue1, + EnumValue2, + } } } From 3345f7e988c599caa5e246912c0e8dca2cb46234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 23:18:49 +0300 Subject: [PATCH 077/110] Normalize UI/API configuration of an extension property if possible. --- .../EntityExtensionConfiguration.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs index ce84cb19e5..d03696b642 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs @@ -50,6 +50,8 @@ namespace Volo.Abp.ObjectExtending.Modularity configureAction?.Invoke(propertyInfo); + NormalizeProperty(propertyInfo); + return this; } @@ -58,5 +60,23 @@ namespace Volo.Abp.ObjectExtending.Modularity { return Properties.Values.ToImmutableList(); } + + private static void NormalizeProperty(ExtensionPropertyConfiguration propertyInfo) + { + if (!propertyInfo.Api.OnGet.IsAvailable) + { + propertyInfo.UI.OnTable.IsVisible = false; + } + + if (!propertyInfo.Api.OnCreate.IsAvailable) + { + propertyInfo.UI.OnCreateForm.IsVisible = false; + } + + if (!propertyInfo.Api.OnUpdate.IsAvailable) + { + propertyInfo.UI.OnEditForm.IsVisible = false; + } + } } } \ No newline at end of file From fa0a59ee44a2c87d84d62dc5f25a018dd8adaa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sat, 16 May 2020 23:24:43 +0300 Subject: [PATCH 078/110] Document Default Validation Attributes #3978 --- docs/en/Object-Extensions.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index bee229dc27..21391170d3 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -208,6 +208,15 @@ ObjectExtensionManager.Instance With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided. +#### Default Validation Attributes + +There are some attributes **automatically added** when you create certain type of properties; + +* `RequiredAttribute` is added for non nullable primitive property types (e.g. `int`, `bool`, `DateTime`...) and `enum` types. +* `EnumDataTypeAttribute` is added for enum types, to prevent to set invalid enum values. + +Use `options.Attributes.Clear();` if you don't want these attributes. + ### Custom Validation If you need, you can add a custom action that is executed to validate the extra properties. Example: From 2cd8311792de2505f3aca422cd348738205f241b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sun, 17 May 2020 00:11:19 +0300 Subject: [PATCH 079/110] #3979: Allow to set custom "default value" for extra properties. --- docs/en/Object-Extensions.md | 38 ++++++++++++++ .../ObjectExtending/ExtensionPropertyDto.cs | 2 + .../CachedObjectExtensionsDtoService.cs | 1 + .../Abp/Data/HasExtraPropertiesExtensions.cs | 9 +++- .../ExtensionPropertyHelper.cs | 16 +++++- .../ExtensionPropertyConfiguration.cs | 20 ++++++++ .../ModuleExtensionConfigurationHelper.cs | 2 + .../ObjectExtensionPropertyInfo.cs | 20 ++++++++ .../ObjectExtensionManager_Tests.cs | 50 +++++++++++++++++++ 9 files changed, 155 insertions(+), 3 deletions(-) diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index 21391170d3..40a7ddd5ea 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -174,6 +174,44 @@ ObjectExtensionManager.Instance The following sections explain the fundamental property configuration options. +#### Default Value + +A default value is automatically set for the new property, which is the natural default value for the property type, like `null` for `string`, `false` for `bool` or `0` for `int`. + +There are two ways to override the default value: + +##### DefaultValue Option + +`DefaultValue` option can be set to any value: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyIntProperty", + options => + { + options.DefaultValue = 42; + }); +```` + +##### DefaultValueFactory Options + +`DefaultValueFactory` can be set to a function that returns the default value: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyIntProperty", + options => + { + options.DefaultValueFactory = () => 42; + }); +```` + +`options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . + +> Tip: Use `DefaultValueFactory` option only if the default value may change over the time. If it is a constant value, then use the `DefaultValue` option. + #### CheckPairDefinitionOnMapping Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better. diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs index 74214914b9..b1ba6f7ea9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs @@ -21,5 +21,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending public List Attributes { get; set; } public Dictionary Configuration { get; set; } + + public object DefaultValue { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index 6e72a6863e..c6e200f45d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -108,6 +108,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending Attributes = new List(), DisplayName = CreateDisplayNameDto(propertyConfig), Configuration = new Dictionary(), + DefaultValue = propertyConfig.GetDefaultValue(), Api = new ExtensionPropertyApiDto { OnGet = new ExtensionPropertyApiGetDto diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs index ca9cc0a4c1..6abb350328 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs @@ -65,8 +65,13 @@ namespace Volo.Abp.Data public static TSource SetDefaultsForExtraProperties(this TSource source, Type objectType = null) where TSource : IHasExtraProperties { + if (objectType == null) + { + objectType = typeof(TSource); + } + var properties = ObjectExtensionManager.Instance - .GetProperties(objectType ?? typeof(TSource)); + .GetProperties(objectType); foreach (var property in properties) { @@ -75,7 +80,7 @@ namespace Volo.Abp.Data continue; } - source.ExtraProperties[property.Name] = TypeHelper.GetDefaultValue(property.Type); + source.ExtraProperties[property.Name] = property.GetDefaultValue(); } return source; diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs index 507f67997a..ee36b397c8 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs @@ -5,7 +5,7 @@ using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending { - public static class ExtensionPropertyHelper + internal static class ExtensionPropertyHelper { public static IEnumerable GetDefaultAttributes(Type type) { @@ -19,5 +19,19 @@ namespace Volo.Abp.ObjectExtending yield return new EnumDataTypeAttribute(type); } } + + public static object GetDefaultValue( + Type propertyType, + Func defaultValueFactory, + object defaultValue) + { + if (defaultValueFactory != null) + { + return defaultValueFactory(); + } + + return defaultValue ?? + TypeHelper.GetDefaultValue(propertyType); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs index 47882fdf93..6d66e51337 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Volo.Abp.Localization; +using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending.Modularity { @@ -45,6 +46,19 @@ namespace Volo.Abp.ObjectExtending.Modularity [NotNull] public ExtensionPropertyApiConfiguration Api { get; } + /// + /// Uses as the default value if was not set. + /// + [CanBeNull] + public object DefaultValue { get; set; } + + /// + /// Used with the first priority to create the default value for the property. + /// Uses to the if this was not set. + /// + [CanBeNull] + public Func DefaultValueFactory { get; set; } + public ExtensionPropertyConfiguration( [NotNull] EntityExtensionConfiguration entityExtensionConfiguration, [NotNull] Type type, @@ -63,6 +77,12 @@ namespace Volo.Abp.ObjectExtending.Modularity Api = new ExtensionPropertyApiConfiguration(); Attributes.AddRange(ExtensionPropertyHelper.GetDefaultAttributes(Type)); + DefaultValue = TypeHelper.GetDefaultValue(Type); + } + + public object GetDefaultValue() + { + return ExtensionPropertyHelper.GetDefaultValue(Type, DefaultValueFactory, DefaultValue); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs index d819918ac5..cb5be17c1f 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs @@ -152,6 +152,8 @@ namespace Volo.Abp.ObjectExtending.Modularity property.Attributes.AddRange(propertyConfig.Attributes); property.DisplayName = propertyConfig.DisplayName; property.Validators.AddRange(propertyConfig.Validators); + property.DefaultValue = propertyConfig.DefaultValue; + property.DefaultValueFactory = propertyConfig.DefaultValueFactory; } ); } diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs index e89598e6d2..12b32ab241 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; using Volo.Abp.Localization; +using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending { @@ -47,6 +48,19 @@ namespace Volo.Abp.ObjectExtending [NotNull] public Dictionary Configuration { get; } + /// + /// Uses as the default value if was not set. + /// + [CanBeNull] + public object DefaultValue { get; set; } + + /// + /// Used with the first priority to create the default value for the property. + /// Uses to the if this was not set. + /// + [CanBeNull] + public Func DefaultValueFactory { get; set; } + public ObjectExtensionPropertyInfo( [NotNull] ObjectExtensionInfo objectExtension, [NotNull] Type type, @@ -62,6 +76,12 @@ namespace Volo.Abp.ObjectExtending Validators = new List>(); Attributes.AddRange(ExtensionPropertyHelper.GetDefaultAttributes(Type)); + DefaultValue = TypeHelper.GetDefaultValue(Type); + } + + public object GetDefaultValue() + { + return ExtensionPropertyHelper.GetDefaultValue(Type, DefaultValueFactory, DefaultValue); } } } diff --git a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs index cb5b308c64..6386dc41ff 100644 --- a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs +++ b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs @@ -119,6 +119,56 @@ namespace Volo.Abp.ObjectExtending .ShouldNotContain(x => x is RequiredAttribute); } + [Fact] + public void Should_Set_DefaultValues() + { + _objectExtensionManager + .AddOrUpdateProperty("IntProp") + .AddOrUpdateProperty("IntPropWithCustomDefaultValue", property => + { + property.DefaultValue = 42; + }) + .AddOrUpdateProperty("BoolProp") + .AddOrUpdateProperty("NullableIntProp") + .AddOrUpdateProperty("NullableIntPropWithCustomDefaultValueFactory", property => + { + property.DefaultValueFactory = () => 2; + }) + .AddOrUpdateProperty("StringProp") + .AddOrUpdateProperty("StringPropWithCustomDefaultValue", property => + { + property.DefaultValue = "custom-value"; + }); + + _objectExtensionManager + .GetPropertyOrNull("IntProp") + .DefaultValue.ShouldBe(0); + + _objectExtensionManager + .GetPropertyOrNull("IntPropWithCustomDefaultValue") + .DefaultValue.ShouldBe(42); + + _objectExtensionManager + .GetPropertyOrNull("BoolProp") + .DefaultValue.ShouldBe(false); + + _objectExtensionManager + .GetPropertyOrNull("NullableIntProp") + .DefaultValue.ShouldBeNull(); + + var propWithDefaultValueFactory = _objectExtensionManager + .GetPropertyOrNull("NullableIntPropWithCustomDefaultValueFactory"); + propWithDefaultValueFactory.GetDefaultValue().ShouldBe(2); + + _objectExtensionManager + .GetPropertyOrNull("StringProp") + .DefaultValue.ShouldBeNull(); + + _objectExtensionManager + .GetPropertyOrNull("StringPropWithCustomDefaultValue") + .DefaultValue.ShouldBe("custom-value"); + } + private class MyExtensibleObject : ExtensibleObject { From 2f074af4a86ac20e7bfea345aaffdb7a87f55cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sun, 17 May 2020 00:31:55 +0300 Subject: [PATCH 080/110] Change TypeSimple name to enum for enum members. --- .../ObjectExtending/CachedObjectExtensionsDtoService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index c6e200f45d..aab28b3cb9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -104,7 +104,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending var extensionPropertyDto = new ExtensionPropertyDto { Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(propertyConfig.Type), - TypeSimple = TypeHelper.GetSimplifiedName(propertyConfig.Type), + TypeSimple = propertyConfig.Type.IsEnum + ? "enum" + : TypeHelper.GetSimplifiedName(propertyConfig.Type), Attributes = new List(), DisplayName = CreateDisplayNameDto(propertyConfig), Configuration = new Dictionary(), From 6fefed6aadc7339342be3697a7c233d8f2609fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sun, 17 May 2020 02:03:14 +0300 Subject: [PATCH 081/110] AbpSelectTagHelperService: Support providing enum as object. --- .../TagHelpers/Form/AbpSelectTagHelperService.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs index 3ed4f63f82..3c2805566f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Reflection; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; @@ -102,7 +101,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form return TagHelper.AspItems.ToList(); } - if (TagHelper.AspFor.ModelExplorer.Metadata.IsEnum) + if (IsEnum()) { return GetSelectItemsFromEnum(context, output, TagHelper.AspFor.ModelExplorer); } @@ -116,6 +115,17 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form throw new Exception("No items provided for select attribute."); } + private bool IsEnum() + { + var value = TagHelper.AspFor.Model; + if (value != null && value.GetType().IsEnum) + { + return true; + } + + return TagHelper.AspFor.ModelExplorer.Metadata.IsEnum; + } + protected virtual async Task GetLabelAsHtmlAsync(TagHelperContext context, TagHelperOutput output, TagHelperOutput selectTag) { if (!string.IsNullOrEmpty(TagHelper.Label)) From 6c949053d7fb05a578421c5a15fb89ff5aa34946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sun, 17 May 2020 02:05:15 +0300 Subject: [PATCH 082/110] Handle enum conversion on the ExtraPropertiesValueConverter --- .../Volo/Abp/Reflection/TypeHelper.cs | 12 +++++ .../Abp/EntityFrameworkCore/AbpDbContext.cs | 1 + .../ExtraPropertiesValueConverter.cs | 45 +++++++++++++++++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 87b94774e7..b388ef791d 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -301,5 +301,17 @@ namespace Volo.Abp.Reflection .GetConverter(targetType) .ConvertFromString(value); } + + public static object ConvertFrom(object value) + { + return ConvertFrom(typeof(TTargetType), value); + } + + public static object ConvertFrom(Type targetType, object value) + { + return TypeDescriptor + .GetConverter(targetType) + .ConvertFrom(value); + } } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 7f914fb1a0..2b35523916 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -194,6 +194,7 @@ namespace Volo.Abp.EntityFrameworkCore { continue; } + /* Checking "currentValue != null" has a good advantage: * Assume that you we already using a named extra property, * then decided to create a field (entity extension) for it. diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs index ddcdb942ba..c1c8491dd6 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; using Volo.Abp.ObjectExtending; @@ -11,7 +12,7 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters public ExtraPropertiesValueConverter(Type entityType) : base( d => SerializeObject(d, entityType), - s => DeserializeObject(s)) + s => DeserializeObject(s, entityType)) { } @@ -38,9 +39,47 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters return JsonConvert.SerializeObject(copyDictionary, Formatting.None); } - private static Dictionary DeserializeObject(string extraPropertiesAsJson) + private static Dictionary DeserializeObject(string extraPropertiesAsJson, Type entityType) { - return JsonConvert.DeserializeObject>(extraPropertiesAsJson); + var dictionary = JsonConvert.DeserializeObject>(extraPropertiesAsJson); + + if (entityType != null) + { + var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType); + if (objectExtension != null) + { + foreach (var property in objectExtension.GetProperties()) + { + dictionary[property.Name] = GetNormalizedValue(dictionary, property); + } + } + } + + return dictionary; + } + + private static object GetNormalizedValue(Dictionary dictionary, ObjectExtensionPropertyInfo property) + { + var value = dictionary.GetOrDefault(property.Name); + if (value == null) + { + return null; + } + + try + { + if (property.Type.IsEnum) + { + return Enum.Parse(property.Type, value.ToString(), true); + } + + //return Convert.ChangeType(value, property.Type); + return value; + } + catch + { + return value; + } } } } \ No newline at end of file From 5e84b6ad15dac16184cd08daf1185301bf14badf Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Sun, 17 May 2020 02:09:42 +0300 Subject: [PATCH 083/110] add TotalQuestionCount --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 46cd1cb470..b5e5d8d5b2 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -148,6 +148,7 @@ "SuccessfullySent": "Successfully Sent", "SuccessfullyDeleted": "Successfully Deleted", "DiscountRequestDeletionWarningMessage": "Discount request will be deleted" , - "BusinessType": "Business Type" + "BusinessType": "Business Type", + "TotalQuestionCount": "Total question count" } } \ No newline at end of file From 9a19517d174b5c597b2d479144bb661c22b2a657 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Sun, 17 May 2020 02:10:43 +0300 Subject: [PATCH 084/110] add RemainingQuestionCount --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index b5e5d8d5b2..6ce9c8c1bf 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -149,6 +149,7 @@ "SuccessfullyDeleted": "Successfully Deleted", "DiscountRequestDeletionWarningMessage": "Discount request will be deleted" , "BusinessType": "Business Type", - "TotalQuestionCount": "Total question count" + "TotalQuestionCount": "Total question count", + "RemainingQuestionCount": "Remaining question count" } } \ No newline at end of file From e3e3ecd71233a1bf4173df791bd079d1ba204742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sun, 17 May 2020 04:20:16 +0300 Subject: [PATCH 085/110] Refactor AbpTagHelperLocalizer. Also, improve enum localization on select. --- .../TagHelpers/AbpTagHelperLocalizer.cs | 38 +++++------ .../Form/AbpRadioInputTagHelperService.cs | 2 +- .../Form/AbpSelectTagHelperService.cs | 65 ++++++++++++++++--- .../TagHelpers/IAbpTagHelperLocalizer.cs | 6 +- .../AbpPaginationTagHelperService.cs | 13 +++- .../AbpDictionaryBasedStringLocalizer.cs | 2 - 6 files changed, 85 insertions(+), 41 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs index 9f047c5b3c..700a5f4ee9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs @@ -21,38 +21,30 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers public string GetLocalizedText(string text, ModelExplorer explorer) { - var resourceType = GetResourceTypeFromModelExplorer(explorer); - var localizer = GetStringLocalizer(resourceType); - - return localizer == null ? text : localizer[text].Value; - } - - public IStringLocalizer GetLocalizer(ModelExplorer explorer) - { - var resourceType = GetResourceTypeFromModelExplorer(explorer); - return GetStringLocalizer(resourceType); - } - - public IStringLocalizer GetLocalizer(Assembly assembly) - { - var resourceType = _options.AssemblyResources.GetOrDefault(assembly); - return GetStringLocalizer(resourceType); + var localizer = GetLocalizerOrNull(explorer); + return localizer == null + ? text + : localizer[text].Value; } - public IStringLocalizer GetLocalizer(Type resourceType) + public IStringLocalizer GetLocalizerOrNull(ModelExplorer explorer) { - return GetStringLocalizer(resourceType); + return GetLocalizerOrNull(explorer.Container.ModelType.Assembly); } - private IStringLocalizer GetStringLocalizer(Type resourceType) + public IStringLocalizer GetLocalizerOrNull(Assembly assembly) { - return resourceType == null ? null : _stringLocalizerFactory.Create(resourceType); + var resourceType = GetResourceType(assembly); + return resourceType == null + ? _stringLocalizerFactory.CreateDefaultOrNull() + : _stringLocalizerFactory.Create(resourceType); } - private Type GetResourceTypeFromModelExplorer(ModelExplorer explorer) + private Type GetResourceType(Assembly assembly) { - var assembly = explorer.Container.ModelType.Assembly; - return _options.AssemblyResources.GetOrDefault(assembly); + return _options + .AssemblyResources + .GetOrDefault(assembly); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs index 010cc1d075..bcfa5bd7d1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs @@ -90,7 +90,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual List GetSelectItemsFromEnum(TagHelperContext context, TagHelperOutput output, ModelExplorer explorer) { - var localizer = _tagHelperLocalizer.GetLocalizer(explorer); + var localizer = _tagHelperLocalizer.GetLocalizerOrNull(explorer); var selectItems = explorer.Metadata.IsEnum ? explorer.ModelType.GetTypeInfo().GetMembers(BindingFlags.Public | BindingFlags.Static) .Select((t, i) => new SelectListItem { Value = i.ToString(), Text = GetLocalizedPropertyName(localizer, explorer.ModelType, t.Name) }).ToList() : null; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs index 3c2805566f..bb79740a41 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs @@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; +using Volo.Abp.DynamicProxy; +using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form { @@ -196,8 +198,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual List GetSelectItemsFromEnum(TagHelperContext context, TagHelperOutput output, ModelExplorer explorer) { - var localizer = _tagHelperLocalizer.GetLocalizer(explorer); - var selectItems = new List(); var isNullableType = Nullable.GetUnderlyingType(explorer.ModelType) != null; var enumType = explorer.ModelType; @@ -208,26 +208,75 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form selectItems.Add(new SelectListItem()); } + var containerLocalizer = _tagHelperLocalizer.GetLocalizerOrNull(explorer.Container.ModelType.Assembly); + IStringLocalizer modelObjectLocalizer = null; + if (explorer.Model != null) + { + modelObjectLocalizer = _tagHelperLocalizer.GetLocalizerOrNull(ProxyHelper.UnProxy(explorer.Model).GetType().Assembly); + } + selectItems.AddRange(enumType.GetEnumNames() .Select(enumName => new SelectListItem { Value = Convert.ToUInt64(Enum.Parse(enumType, enumName)).ToString(), - Text = GetLocalizedPropertyName(localizer, enumType, enumName) + Text = GetLocalizedEnumFieldName(containerLocalizer, modelObjectLocalizer, enumType, enumName) })); return selectItems; } - protected virtual string GetLocalizedPropertyName(IStringLocalizer localizer, Type enumType, string propertyName) + protected virtual string GetLocalizedEnumFieldName( + IStringLocalizer containerLocalizer, + IStringLocalizer modelObjectLocalizer, + Type enumType, + string fieldName) { - if (localizer == null) + LocalizedString localizedString; + + //Look for the enum name + enum field name + + var localizationKey = enumType.Name + "." + fieldName; + if (containerLocalizer != null) + { + localizedString = containerLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } + } + + if (modelObjectLocalizer != null) + { + localizedString = modelObjectLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } + } + + //Look for the enum field name + + localizationKey = fieldName; + + if (containerLocalizer != null) { - return propertyName; + localizedString = containerLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } } - var localizedString = localizer[enumType.Name + "." + propertyName]; + if (modelObjectLocalizer != null) + { + localizedString = modelObjectLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } + } - return !localizedString.ResourceNotFound ? localizedString.Value : localizer[propertyName].Value; + return fieldName; } protected virtual List GetSelectItemsFromAttribute( diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs index 8351e47082..2a73777b00 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs @@ -10,10 +10,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers { string GetLocalizedText(string text, ModelExplorer explorer); - IStringLocalizer GetLocalizer(ModelExplorer explorer); + IStringLocalizer GetLocalizerOrNull(ModelExplorer explorer); - IStringLocalizer GetLocalizer(Assembly assembly); - - IStringLocalizer GetLocalizer(Type resourceType); + IStringLocalizer GetLocalizerOrNull(Assembly assembly); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs index 76bfcb3ef1..9c37e77767 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs @@ -7,6 +7,7 @@ using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; @@ -17,12 +18,18 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination private readonly IHtmlGenerator _generator; private readonly HtmlEncoder _encoder; private readonly IAbpTagHelperLocalizer _tagHelperLocalizer; + private readonly IStringLocalizerFactory _stringLocalizerFactory; - public AbpPaginationTagHelperService(IHtmlGenerator generator, HtmlEncoder encoder, IAbpTagHelperLocalizer tagHelperLocalizer) + public AbpPaginationTagHelperService( + IHtmlGenerator generator, + HtmlEncoder encoder, + IAbpTagHelperLocalizer tagHelperLocalizer, + IStringLocalizerFactory stringLocalizerFactory) { _generator = generator; _encoder = encoder; _tagHelperLocalizer = tagHelperLocalizer; + _stringLocalizerFactory = stringLocalizerFactory; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) @@ -119,7 +126,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination protected virtual async Task RenderAnchorTagHelperLinkHtmlAsync(TagHelperContext context, TagHelperOutput output, string currentPage, string localizationKey) { - var localizer = _tagHelperLocalizer.GetLocalizer(typeof(AbpUiResource)); + var localizer = _stringLocalizerFactory.Create(typeof(AbpUiResource)); var anchorTagHelper = GetAnchorTagHelper(currentPage, out var attributeList); @@ -156,7 +163,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination protected virtual string GetOpeningTags(TagHelperContext context, TagHelperOutput output) { - var localizer = _tagHelperLocalizer.GetLocalizer(typeof(AbpUiResource)); + var localizer = _stringLocalizerFactory.Create(typeof(AbpUiResource)); var pagerInfo = (TagHelper.ShowInfo ?? false) ? "
" + localizer["PagerInfo{0}{1}{2}", TagHelper.Model.ShowingFrom, TagHelper.Model.ShowingTo, TagHelper.Model.TotalItemsCount] + "
\r\n" diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs index 53a2d7714e..7d9056ca4d 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs @@ -177,9 +177,7 @@ namespace Volo.Abp.Localization return allStrings.Values.ToImmutableList(); } - - public class CultureWrapperStringLocalizer : IStringLocalizer, IStringLocalizerSupportsInheritance { private readonly string _cultureName; From 2f0a7af7b3b4b849a392ad21572565789055bc22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sun, 17 May 2020 22:56:15 +0300 Subject: [PATCH 086/110] Resolved #3984 Expose enum type & members to client side for extension properties. --- .../ObjectExtending/EntityExtensionDto.cs | 1 - .../ObjectExtending/ExtensionEnumDto.cs | 13 +++ .../ObjectExtending/ExtensionEnumFieldDto.cs | 12 +++ .../ObjectExtending/ObjectExtensionsDto.cs | 2 + .../Form/AbpSelectTagHelperService.cs | 95 ++++++------------- .../ui-extensions.js | 88 +++++++++++++++-- .../CachedObjectExtensionsDtoService.cs | 45 ++++++++- .../Volo/Abp/AbpInitializationException.cs | 46 +++++++++ .../Volo/Abp/Modularity/ModuleLoader.cs | 27 +++++- .../Volo/Abp/Modularity/ModuleManager.cs | 22 ++++- .../Volo/Abp/Reflection/TypeHelper.cs | 2 +- .../ExtraPropertiesValueConverter.cs | 6 +- .../Abp/Localization/LocalizableString.cs | 0 .../LocalizationResourceNameAttribute.cs | 0 .../Abp/Localization/StringLocalizerHelper.cs | 50 ++++++++++ ...xtensionPropertyConfigurationExtensions.cs | 32 +++++++ .../Volo/Abp/Reflection/TypeHelper_Tests.cs | 7 ++ .../ObjectExtensionManager_Tests.cs | 20 ++-- 18 files changed, 380 insertions(+), 88 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumDto.cs create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumFieldDto.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/AbpInitializationException.cs rename framework/src/{Volo.Abp.Localization => Volo.Abp.Localization.Abstractions}/Volo/Abp/Localization/LocalizableString.cs (100%) rename framework/src/{Volo.Abp.Localization => Volo.Abp.Localization.Abstractions}/Volo/Abp/Localization/LocalizationResourceNameAttribute.cs (100%) create mode 100644 framework/src/Volo.Abp.Localization/Volo/Abp/Localization/StringLocalizerHelper.cs create mode 100644 framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs index 83093e0370..d15870e7c1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs @@ -9,6 +9,5 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending public Dictionary Properties { get; set; } public Dictionary Configuration { get; set; } - } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumDto.cs new file mode 100644 index 0000000000..4f2dc526eb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending +{ + [Serializable] + public class ExtensionEnumDto + { + public List Fields { get; set; } + + public string LocalizationResource { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumFieldDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumFieldDto.cs new file mode 100644 index 0000000000..ddc7c349d4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumFieldDto.cs @@ -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; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs index fd9665eab1..0338bbd4dc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs @@ -7,5 +7,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending public class ObjectExtensionsDto { public Dictionary Modules { get; set; } + + public Dictionary Enums { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs index bb79740a41..21f6c498a9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Linq.Dynamic.Core; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; @@ -12,6 +13,7 @@ using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; using Volo.Abp.DynamicProxy; +using Volo.Abp.Localization; using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form @@ -21,12 +23,18 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form private readonly IHtmlGenerator _generator; private readonly HtmlEncoder _encoder; private readonly IAbpTagHelperLocalizer _tagHelperLocalizer; + private readonly IStringLocalizerFactory _stringLocalizerFactory; - public AbpSelectTagHelperService(IHtmlGenerator generator, HtmlEncoder encoder, IAbpTagHelperLocalizer tagHelperLocalizer) + public AbpSelectTagHelperService( + IHtmlGenerator generator, + HtmlEncoder encoder, + IAbpTagHelperLocalizer tagHelperLocalizer, + IStringLocalizerFactory stringLocalizerFactory) { _generator = generator; _encoder = encoder; _tagHelperLocalizer = tagHelperLocalizer; + _stringLocalizerFactory = stringLocalizerFactory; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) @@ -209,74 +217,33 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form } var containerLocalizer = _tagHelperLocalizer.GetLocalizerOrNull(explorer.Container.ModelType.Assembly); - IStringLocalizer modelObjectLocalizer = null; - if (explorer.Model != null) - { - modelObjectLocalizer = _tagHelperLocalizer.GetLocalizerOrNull(ProxyHelper.UnProxy(explorer.Model).GetType().Assembly); - } - - selectItems.AddRange(enumType.GetEnumNames() - .Select(enumName => new SelectListItem - { - Value = Convert.ToUInt64(Enum.Parse(enumType, enumName)).ToString(), - Text = GetLocalizedEnumFieldName(containerLocalizer, modelObjectLocalizer, enumType, enumName) - })); - - return selectItems; - } - - protected virtual string GetLocalizedEnumFieldName( - IStringLocalizer containerLocalizer, - IStringLocalizer modelObjectLocalizer, - Type enumType, - string fieldName) - { - LocalizedString localizedString; - - //Look for the enum name + enum field name - - var localizationKey = enumType.Name + "." + fieldName; - if (containerLocalizer != null) - { - localizedString = containerLocalizer[localizationKey]; - if (!localizedString.ResourceNotFound) - { - return localizedString.Value; - } - } - if (modelObjectLocalizer != null) + foreach (var enumValue in enumType.GetEnumValues()) { - localizedString = modelObjectLocalizer[localizationKey]; - if (!localizedString.ResourceNotFound) + var memberName = enumType.GetEnumName(enumValue); + var localizedMemberName = AbpInternalLocalizationHelper.LocalizeWithFallback( + new[] + { + containerLocalizer, + _stringLocalizerFactory.CreateDefaultOrNull() + }, + new[] + { + $"Enum:{enumType.Name}.{memberName}", + $"{enumType.Name}.{memberName}", + memberName + }, + memberName + ); + + selectItems.Add(new SelectListItem { - return localizedString.Value; - } - } - - //Look for the enum field name - - localizationKey = fieldName; - - if (containerLocalizer != null) - { - localizedString = containerLocalizer[localizationKey]; - if (!localizedString.ResourceNotFound) - { - return localizedString.Value; - } - } - - if (modelObjectLocalizer != null) - { - localizedString = modelObjectLocalizer[localizationKey]; - if (!localizedString.ResourceNotFound) - { - return localizedString.Value; - } + Value = enumValue.ToString(), + Text = localizedMemberName + }); } - return fieldName; + return selectItems; } protected virtual List GetSelectItemsFromAttribute( diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js index 9ee545615c..dbf73618ad 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js @@ -99,6 +99,26 @@ function initializeObjectExtensions() { + var getShortEnumTypeName = function (enumType) { + var lastDotIndex = enumType.lastIndexOf('.'); + if (lastDotIndex < 0) { + return enumType; + } + + return enumType.substr(lastDotIndex + 1); + }; + + var getEnumMemberName = function (enumInfo, enumMemberValue) { + for (var i = 0; i < enumInfo.fields.length; i++) { + var enumField = enumInfo.fields[i]; + if (enumField.value == enumMemberValue) { + return enumField.name; + } + } + + return null; + }; + function localizeDisplayName(propertyName, displayName) { if (displayName && displayName.name) { return abp.localization.localize(displayName.name, displayName.resource); @@ -111,6 +131,48 @@ return abp.localization.localize(propertyName); } + function localizeWithFallback(localizationResources, keys, defaultValue) { + for (var i = 0; i < localizationResources.length; i++) { + var localizationResource = localizationResources[i]; + if (!localizationResource) { + continue; + } + + for (var j = 0; j < keys.length; j++) { + var key = keys[j]; + + if (abp.localization.isLocalized(key, localizationResource)) { + return abp.localization.localize(key, localizationResource); + } + } + } + + return defaultValue; + } + + function localizeEnumMember(property, row) { + var enumType = property.config.type; + var enumInfo = abp.objectExtensions.enums[enumType]; + var enumMemberValue = row.extraProperties[property.name]; + var enumMemberName = getEnumMemberName(enumInfo, enumMemberValue); + + if (!enumMemberName) { + return enumMemberValue; + } + + var shortEnumType = getShortEnumTypeName(enumType); + + return localizeWithFallback( + [enumInfo.localizationResource, abp.localization.defaultResourceName], + [ + 'Enum:' + shortEnumType + '.' + enumMemberName, + shortEnumType + '.' + enumMemberName, + enumMemberName + ], + enumMemberName + ); + } + function configureTableColumns(tableName, columnConfigs) { abp.ui.extensions.tableColumns.get(tableName) .addContributor( @@ -137,16 +199,30 @@ return tableProperties; } + function convertPropertyToColumnConfig(property) { + var columnConfig = { + title: localizeDisplayName(property.name, property.config.displayName), + data: "extraProperties." + property.name, + orderable: false + }; + + if (property.config.typeSimple === 'enum') { + columnConfig.render = function (data, type, row) { + return localizeEnumMember( + property, + row + ); + } + } + + return columnConfig; + } + function convertPropertiesToColumnConfigs(properties) { var columnConfigs = []; for (var i = 0; i < properties.length; i++) { - var tableProperty = properties[i]; - columnConfigs.push({ - title: localizeDisplayName(tableProperty.name, tableProperty.config.displayName), - data: "extraProperties." + tableProperty.name, - orderable: false - }); + columnConfigs.push(convertPropertyToColumnConfig(properties[i])); } return columnConfigs; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index aab28b3cb9..870ff0368e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -39,7 +39,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending { var objectExtensionsDto = new ObjectExtensionsDto { - Modules = new Dictionary() + Modules = new Dictionary(), + Enums = new Dictionary() }; foreach (var moduleConfig in ObjectExtensionManager.Instance.Modules()) @@ -47,6 +48,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending objectExtensionsDto.Modules[moduleConfig.Key] = CreateModuleExtensionDto(moduleConfig.Value); } + FillEnums(objectExtensionsDto); + return objectExtensionsDto; } @@ -183,5 +186,45 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending return null; } + + protected virtual void FillEnums(ObjectExtensionsDto objectExtensionsDto) + { + var enumProperties = ObjectExtensionManager.Instance.Modules().Values + .SelectMany( + m => m.Entities.Values.SelectMany( + e => e.GetProperties() + ) + ) + .Where(p => p.Type.IsEnum) + .ToList(); + + foreach (var enumProperty in enumProperties) + { + // ReSharper disable once AssignNullToNotNullAttribute (enumProperty.Type.FullName can not be null for this case) + objectExtensionsDto.Enums[enumProperty.Type.FullName] = CreateExtensionEnumDto(enumProperty); + } + } + + protected virtual ExtensionEnumDto CreateExtensionEnumDto(ExtensionPropertyConfiguration enumProperty) + { + var extensionEnumDto = new ExtensionEnumDto + { + Fields = new List(), + LocalizationResource = enumProperty.GetLocalizationResourceNameOrNull() + }; + + foreach (var enumValue in enumProperty.Type.GetEnumValues()) + { + extensionEnumDto.Fields.Add( + new ExtensionEnumFieldDto + { + Name = enumProperty.Type.GetEnumName(enumValue), + Value = enumValue + } + ); + } + + return extensionEnumDto; + } } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpInitializationException.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpInitializationException.cs new file mode 100644 index 0000000000..1bcd513168 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpInitializationException.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.Serialization; + +namespace Volo.Abp +{ + public class AbpInitializationException : AbpException + { + /// + /// Creates a new object. + /// + public AbpInitializationException() + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + public AbpInitializationException(string message) + : base(message) + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + /// Inner exception + public AbpInitializationException(string message, Exception innerException) + : base(message, innerException) + { + + } + + /// + /// Constructor for serializing. + /// + public AbpInitializationException(SerializationInfo serializationInfo, StreamingContext context) + : base(serializationInfo, context) + { + + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs index 30bf523c1d..374e1adb78 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs @@ -105,7 +105,14 @@ namespace Volo.Abp.Modularity //PreConfigureServices foreach (var module in modules.Where(m => m.Instance is IPreConfigureServices)) { - ((IPreConfigureServices)module.Instance).PreConfigureServices(context); + try + { + ((IPreConfigureServices)module.Instance).PreConfigureServices(context); + } + catch (Exception ex) + { + throw new AbpInitializationException($"An error occurred during {nameof(IPreConfigureServices.PreConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex); + } } //ConfigureServices @@ -119,13 +126,27 @@ namespace Volo.Abp.Modularity } } - module.Instance.ConfigureServices(context); + try + { + module.Instance.ConfigureServices(context); + } + catch (Exception ex) + { + throw new AbpInitializationException($"An error occurred during {nameof(IAbpModule.ConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex); + } } //PostConfigureServices foreach (var module in modules.Where(m => m.Instance is IPostConfigureServices)) { - ((IPostConfigureServices)module.Instance).PostConfigureServices(context); + try + { + ((IPostConfigureServices)module.Instance).PostConfigureServices(context); + } + catch (Exception ex) + { + throw new AbpInitializationException($"An error occurred during {nameof(IPostConfigureServices.PostConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex); + } } foreach (var module in modules) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleManager.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleManager.cs index 8f25389b63..e594a24603 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleManager.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleManager.cs @@ -34,11 +34,18 @@ namespace Volo.Abp.Modularity { LogListOfModules(); - foreach (var Contributor in _lifecycleContributors) + foreach (var contributor in _lifecycleContributors) { foreach (var module in _moduleContainer.Modules) { - Contributor.Initialize(context, module.Instance); + try + { + contributor.Initialize(context, module.Instance); + } + catch (Exception ex) + { + throw new AbpInitializationException($"An error occurred during the initialize {contributor.GetType().FullName} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex); + } } } @@ -59,11 +66,18 @@ namespace Volo.Abp.Modularity { var modules = _moduleContainer.Modules.Reverse().ToList(); - foreach (var Contributor in _lifecycleContributors) + foreach (var contributor in _lifecycleContributors) { foreach (var module in modules) { - Contributor.Shutdown(context, module.Instance); + try + { + contributor.Shutdown(context, module.Instance); + } + catch (Exception ex) + { + throw new AbpInitializationException($"An error occurred during the shutdown {contributor.GetType().FullName} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex); + } } } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index b388ef791d..192043a800 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -169,7 +169,7 @@ namespace Volo.Abp.Reflection { return Activator.CreateInstance(type); } - + return null; } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs index c1c8491dd6..2fa00e3caf 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs @@ -58,12 +58,14 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters return dictionary; } - private static object GetNormalizedValue(Dictionary dictionary, ObjectExtensionPropertyInfo property) + private static object GetNormalizedValue( + Dictionary dictionary, + ObjectExtensionPropertyInfo property) { var value = dictionary.GetOrDefault(property.Name); if (value == null) { - return null; + return property.GetDefaultValue(); } try diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableString.cs b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableString.cs similarity index 100% rename from framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableString.cs rename to framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableString.cs diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceNameAttribute.cs b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizationResourceNameAttribute.cs similarity index 100% rename from framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceNameAttribute.cs rename to framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizationResourceNameAttribute.cs diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/StringLocalizerHelper.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/StringLocalizerHelper.cs new file mode 100644 index 0000000000..b6daf4a44d --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/StringLocalizerHelper.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.Localization +{ + /// + /// This class is designed to be used internal by the framework. + /// + public static class AbpInternalLocalizationHelper + { + /// + /// Searches an array of keys in an array of localizers. + /// + /// + /// An array of localizers. Search the keys on the localizers. + /// Can contain null items in the array. + /// + /// + /// An array of keys. Search the keys on the localizers. + /// Should not contain null items in the array. + /// + /// + /// Return value if none of the localizers has none of the keys. + /// + /// + 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; + } + } +} diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs new file mode 100644 index 0000000000..946fc04274 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs @@ -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; + } + } +} diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs index 978bf9de54..b12a52e935 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs @@ -80,6 +80,7 @@ namespace Volo.Abp.Reflection TypeHelper.GetDefaultValue(typeof(byte)).ShouldBe(0); TypeHelper.GetDefaultValue(typeof(int)).ShouldBe(0); TypeHelper.GetDefaultValue(typeof(string)).ShouldBeNull(); + TypeHelper.GetDefaultValue(typeof(MyEnum)).ShouldBe(MyEnum.EnumValue0); } [Fact] @@ -94,5 +95,11 @@ namespace Volo.Abp.Reflection { } + + public enum MyEnum + { + EnumValue0, + EnumValue1, + } } } diff --git a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs index 6386dc41ff..a751fdf20f 100644 --- a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs +++ b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs @@ -138,23 +138,27 @@ namespace Volo.Abp.ObjectExtending .AddOrUpdateProperty("StringPropWithCustomDefaultValue", property => { property.DefaultValue = "custom-value"; + }) + .AddOrUpdateProperty("EnumProp", property => + { + property.DefaultValue = MyTestEnum.EnumValue2; }); _objectExtensionManager .GetPropertyOrNull("IntProp") - .DefaultValue.ShouldBe(0); + .GetDefaultValue().ShouldBe(0); _objectExtensionManager .GetPropertyOrNull("IntPropWithCustomDefaultValue") - .DefaultValue.ShouldBe(42); + .GetDefaultValue().ShouldBe(42); _objectExtensionManager .GetPropertyOrNull("BoolProp") - .DefaultValue.ShouldBe(false); + .GetDefaultValue().ShouldBe(false); _objectExtensionManager .GetPropertyOrNull("NullableIntProp") - .DefaultValue.ShouldBeNull(); + .GetDefaultValue().ShouldBeNull(); var propWithDefaultValueFactory = _objectExtensionManager .GetPropertyOrNull("NullableIntPropWithCustomDefaultValueFactory"); @@ -162,11 +166,15 @@ namespace Volo.Abp.ObjectExtending _objectExtensionManager .GetPropertyOrNull("StringProp") - .DefaultValue.ShouldBeNull(); + .GetDefaultValue().ShouldBeNull(); _objectExtensionManager .GetPropertyOrNull("StringPropWithCustomDefaultValue") - .DefaultValue.ShouldBe("custom-value"); + .GetDefaultValue().ShouldBe("custom-value"); + + _objectExtensionManager + .GetPropertyOrNull("EnumProp") + .GetDefaultValue().ShouldBe(MyTestEnum.EnumValue2); } private class MyExtensibleObject : ExtensibleObject From 9071221dcf45a1f782826d9fbf6c464cca4c8068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 18 May 2020 01:20:38 +0300 Subject: [PATCH 087/110] Update TagHelperExtensions.cs --- .../TagHelpers/Extensions/TagHelperExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs index d925fd11e8..af961a2bf6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.TagHelpers; using System.Text.Encodings.Web; -using Volo.Abp.Threading; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions { From 4aa07db3d27f4b4bea6e291415f3e244af4e472f Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Mon, 18 May 2020 01:44:15 +0300 Subject: [PATCH 088/110] fix sentences... --- docs/en/Dapper.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md index d8b17838eb..25bba118ec 100644 --- a/docs/en/Dapper.md +++ b/docs/en/Dapper.md @@ -1,22 +1,23 @@ # Dapper Integration -Because Dapper's idea is that the sql statement takes precedence, and mainly provides some extension methods for the `IDbConnection` interface. +Dapper is a light-weight and simple database provider. The major benefit of using Dapper is writing T-SQL queries. It provides some extension methods for `IDbConnection` interface. -Abp does not encapsulate too many functions for Dapper. Abp Dapper provides a `DapperRepository` base class based on Abp EntityFrameworkCore, which provides the `IDbConnection` and `IDbTransaction` properties required by Dapper. +ABP does not encapsulate many functions for Dapper. ABP Dapper library provides a `DapperRepository` base class based on ABP EntityFrameworkCore module, which provides the `IDbConnection` and `IDbTransaction` properties required by Dapper. -These two properties can work well with [Unit-Of-Work](Unit-Of-Work.md). +`IDbConnection` and `IDbTransaction` works well with the [ABP Unit-Of-Work](Unit-Of-Work.md). ## Installation -Please install and configure EF Core according to [EF Core's integrated documentation](Entity-Framework-Core.md). +Install and configure EF Core according to [EF Core's integrated documentation](Entity-Framework-Core.md). -`Volo.Abp.Dapper` is the main nuget package for the Dapper integration. Install it to your project (for a layered application, to your data/infrastructure layer): +`Volo.Abp.Dapper` is the main nuget package for the Dapper integration. +Install it to your project (for a layered application, to your data/infrastructure layer): ```shell Install-Package Volo.Abp.Dapper ``` -Then add `AbpDapperModule` module dependency (`DependsOn` attribute) to your [module](Module-Development-Basics.md): +Then add `AbpDapperModule` module dependency (with `DependsOn` attribute) to your [module](Module-Development-Basics.md): ````C# using Volo.Abp.Dapper; @@ -34,9 +35,10 @@ namespace MyCompany.MyProject ## Implement Dapper Repository -The following code implements the `Person` repository, which requires EF Core's `DbContext` (MyAppDbContext). You can inject `PersonDapperRepository` to call its methods. +The following code creates the `PersonRepository`, which requires EF Core's `DbContext` (MyAppDbContext). +You can inject `PersonDapperRepository` to your services for your database operations. -`DbConnection` and `DbTransaction` are from the `DapperRepository` base class. +`DbConnection` and `DbTransaction` comes from the `DapperRepository` base class. ```C# public class PersonDapperRepository : DapperRepository, ITransientDependency From cc9fd339f9517af66b7a101083bf468c8c526ac2 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Mon, 18 May 2020 01:45:11 +0300 Subject: [PATCH 089/110] Update Dapper.md --- docs/en/Dapper.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md index 25bba118ec..12660f21aa 100644 --- a/docs/en/Dapper.md +++ b/docs/en/Dapper.md @@ -11,6 +11,7 @@ ABP does not encapsulate many functions for Dapper. ABP Dapper library provides Install and configure EF Core according to [EF Core's integrated documentation](Entity-Framework-Core.md). `Volo.Abp.Dapper` is the main nuget package for the Dapper integration. +You can find it on NuGet Gallery: https://www.nuget.org/packages/Volo.Abp.Dapper Install it to your project (for a layered application, to your data/infrastructure layer): ```shell From fbfdbb1c5311cb59de70ee460ef0aa3b11922cd6 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Mon, 18 May 2020 01:45:32 +0300 Subject: [PATCH 090/110] Update Dapper.md --- docs/en/Dapper.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md index 12660f21aa..0cb1a645d7 100644 --- a/docs/en/Dapper.md +++ b/docs/en/Dapper.md @@ -11,7 +11,9 @@ ABP does not encapsulate many functions for Dapper. ABP Dapper library provides Install and configure EF Core according to [EF Core's integrated documentation](Entity-Framework-Core.md). `Volo.Abp.Dapper` is the main nuget package for the Dapper integration. + You can find it on NuGet Gallery: https://www.nuget.org/packages/Volo.Abp.Dapper + Install it to your project (for a layered application, to your data/infrastructure layer): ```shell From 1c713d42013414599f4496dc529d294058759f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 18 May 2020 01:46:36 +0300 Subject: [PATCH 091/110] Change the example. --- docs/en/Object-Extensions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index 40a7ddd5ea..8c7cca4037 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -200,17 +200,17 @@ ObjectExtensionManager.Instance ````csharp ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "MyIntProperty", + .AddOrUpdateProperty( + "MyDateTimeProperty", options => { - options.DefaultValueFactory = () => 42; + options.DefaultValueFactory = () => DateTime.Now; }); ```` `options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . -> Tip: Use `DefaultValueFactory` option only if the default value may change over the time. If it is a constant value, then use the `DefaultValue` option. +> Tip: Use `DefaultValueFactory` option only if the default value may change over the time (like `DateTime.Now` in this example). If it is a constant value, then use the `DefaultValue` option. #### CheckPairDefinitionOnMapping From 7d8be4f62c106c8399f3a6d54808a9f207aa27dd Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Mon, 18 May 2020 01:47:01 +0300 Subject: [PATCH 092/110] Update Dapper.md --- docs/en/Dapper.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md index 0cb1a645d7..850847ebbb 100644 --- a/docs/en/Dapper.md +++ b/docs/en/Dapper.md @@ -2,9 +2,7 @@ Dapper is a light-weight and simple database provider. The major benefit of using Dapper is writing T-SQL queries. It provides some extension methods for `IDbConnection` interface. -ABP does not encapsulate many functions for Dapper. ABP Dapper library provides a `DapperRepository` base class based on ABP EntityFrameworkCore module, which provides the `IDbConnection` and `IDbTransaction` properties required by Dapper. - -`IDbConnection` and `IDbTransaction` works well with the [ABP Unit-Of-Work](Unit-Of-Work.md). +ABP does not encapsulate many functions for Dapper. ABP Dapper library provides a `DapperRepository` base class based on ABP EntityFrameworkCore module, which provides the `IDbConnection` and `IDbTransaction` properties required by Dapper. `IDbConnection` and `IDbTransaction` works well with the [ABP Unit-Of-Work](Unit-Of-Work.md). ## Installation From 27009ed76ebbfea0e080b526f56fdbadab33aab0 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Mon, 18 May 2020 01:48:02 +0300 Subject: [PATCH 093/110] Update Dapper.md --- docs/en/Dapper.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md index 850847ebbb..daf8ceb13a 100644 --- a/docs/en/Dapper.md +++ b/docs/en/Dapper.md @@ -8,9 +8,7 @@ ABP does not encapsulate many functions for Dapper. ABP Dapper library provides Install and configure EF Core according to [EF Core's integrated documentation](Entity-Framework-Core.md). -`Volo.Abp.Dapper` is the main nuget package for the Dapper integration. - -You can find it on NuGet Gallery: https://www.nuget.org/packages/Volo.Abp.Dapper +`Volo.Abp.Dapper` is the library for the Dapper integration. You can find it on NuGet Gallery: https://www.nuget.org/packages/Volo.Abp.Dapper Install it to your project (for a layered application, to your data/infrastructure layer): From b36b948b698dfee2b192c2b30edda710cd40c04b Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Mon, 18 May 2020 01:48:18 +0300 Subject: [PATCH 094/110] Update Dapper.md --- docs/en/Dapper.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md index daf8ceb13a..26fbf97e66 100644 --- a/docs/en/Dapper.md +++ b/docs/en/Dapper.md @@ -8,7 +8,9 @@ ABP does not encapsulate many functions for Dapper. ABP Dapper library provides Install and configure EF Core according to [EF Core's integrated documentation](Entity-Framework-Core.md). -`Volo.Abp.Dapper` is the library for the Dapper integration. You can find it on NuGet Gallery: https://www.nuget.org/packages/Volo.Abp.Dapper +`Volo.Abp.Dapper` is the library for the Dapper integration. + +You can find it on NuGet Gallery: https://www.nuget.org/packages/Volo.Abp.Dapper Install it to your project (for a layered application, to your data/infrastructure layer): From dade0f28170be9d5efe1a710e7c8a1979b641fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Mon, 18 May 2020 13:49:10 +0300 Subject: [PATCH 095/110] added en localizaiton file for Volo.Abp.Emailing.Tests project --- .../Volo/Abp/Emailing/Localization/en.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 framework/test/Volo.Abp.Emailing.Tests/Volo/Abp/Emailing/Localization/en.json diff --git a/framework/test/Volo.Abp.Emailing.Tests/Volo/Abp/Emailing/Localization/en.json b/framework/test/Volo.Abp.Emailing.Tests/Volo/Abp/Emailing/Localization/en.json new file mode 100644 index 0000000000..70d0b2b52e --- /dev/null +++ b/framework/test/Volo.Abp.Emailing.Tests/Volo/Abp/Emailing/Localization/en.json @@ -0,0 +1,6 @@ +{ + "culture": "de", + "texts": { + "hello": "Hello" + } +} \ No newline at end of file From ce6dc02ade5140796d79a9833b610be99c856505 Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Mon, 18 May 2020 16:29:57 +0300 Subject: [PATCH 096/110] bloging linkedin meta tags --- .../src/Volo.Blogging.Web/BloggingTwitterOptions.cs | 12 ------------ .../Pages/Blogs/Posts/Detail.cshtml | 6 ++++++ .../SocialMedia/BloggingTwitterOptions.cs | 7 +++++++ 3 files changed, 13 insertions(+), 12 deletions(-) delete mode 100644 modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs create mode 100644 modules/blogging/src/Volo.Blogging.Web/SocialMedia/BloggingTwitterOptions.cs diff --git a/modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs b/modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs deleted file mode 100644 index 8c1b1fbf7d..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/BloggingTwitterOptions.cs +++ /dev/null @@ -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; } - } -} diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml index 63cfbf8a8e..db6427e2a7 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Detail.cshtml @@ -8,6 +8,7 @@ @using Volo.Blogging.Pages.Blog.Posts @using Volo.Blogging.Areas.Blog.Helpers.TagHelpers @using Volo.Abp.AspNetCore.Mvc.UI.Packages.Prismjs +@using Volo.Blogging.SocialMedia @inject IAuthorizationService Authorization @inject IOptionsSnapshot twitterOptions @model DetailModel @@ -21,6 +22,11 @@ ViewBag.TwitterDescription = Model.Post.Description; ViewBag.TwitterImage = $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Model.Post.CoverImage}"; + ViewBag.LinkedInUrl = Request.GetEncodedUrl(); + ViewBag.LinkedInTitle = Model.Post.Title; + ViewBag.LinkedInDescription = Model.Post.Description; + ViewBag.LinkedInImage = $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Model.Post.CoverImage}"; + var hasCommentingPermission = CurrentUser.IsAuthenticated; //TODO: Apply real policy! } @section scripts { diff --git a/modules/blogging/src/Volo.Blogging.Web/SocialMedia/BloggingTwitterOptions.cs b/modules/blogging/src/Volo.Blogging.Web/SocialMedia/BloggingTwitterOptions.cs new file mode 100644 index 0000000000..5960be21b9 --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Web/SocialMedia/BloggingTwitterOptions.cs @@ -0,0 +1,7 @@ +namespace Volo.Blogging.SocialMedia +{ + public class BloggingTwitterOptions + { + public string Site { get; set; } + } +} From 832cd5d47754bdba36a7188026ce9254773755ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ak=C4=B1n=20Sabri=20=C3=87am?= Date: Mon, 18 May 2020 18:22:33 +0300 Subject: [PATCH 097/110] added localization keys fororganizations in abpio admin --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 14a4eb345c..c172c1b302 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -147,6 +147,8 @@ "EmailSent": "Email Sent", "SuccessfullySent": "Successfully Sent", "SuccessfullyDeleted": "Successfully Deleted", - "DiscountRequestDeletionWarningMessage": "Discount request will be deleted" + "DiscountRequestDeletionWarningMessage": "Discount request will be deleted", + "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount !", + "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero !" } } \ No newline at end of file From 465c751ea9f5e4aa6235364785bea04057b7f863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 00:16:52 +0300 Subject: [PATCH 098/110] Fixed #3999: Fix BootstrapDatepicker Script/Style Contributor namespace --- .../BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs | 2 +- .../BootstrapDatepicker/BootstrapDatepickerStyleContributor.cs | 2 +- .../Bundling/SharedThemeGlobalScriptContributor.cs | 1 + .../Bundling/SharedThemeGlobalStyleContributor.cs | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs index 7e21c8cef5..5a32cb7ca0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs @@ -4,7 +4,7 @@ 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.Timeago +namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDatepicker { [DependsOn(typeof(JQueryScriptContributor))] public class BootstrapDatepickerScriptContributor : BundleContributor diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerStyleContributor.cs index bc345e34b3..0b9b555e97 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerStyleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerStyleContributor.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Select2 +namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDatepicker { public class BootstrapDatepickerStyleContributor : BundleContributor { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs index 892f0d497a..4ff7430553 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs @@ -1,5 +1,6 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Bootstrap; +using Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDatepicker; using Volo.Abp.AspNetCore.Mvc.UI.Packages.DatatablesNetBs4; using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryForm; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs index d1703113d0..49b8c4893e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs @@ -1,5 +1,6 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Bootstrap; +using Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDatepicker; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core; using Volo.Abp.AspNetCore.Mvc.UI.Packages.DatatablesNetBs4; using Volo.Abp.AspNetCore.Mvc.UI.Packages.FontAwesome; From 4a56efdc48ca479de832d8c43617d3332c9de296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 00:17:19 +0300 Subject: [PATCH 099/110] Show icon for boolean extension properties on datatables. --- .../ui-extensions.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js index dbf73618ad..40e22d3da9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js @@ -150,10 +150,9 @@ return defaultValue; } - function localizeEnumMember(property, row) { + function localizeEnumMember(property, enumMemberValue) { var enumType = property.config.type; var enumInfo = abp.objectExtensions.enums[enumType]; - var enumMemberValue = row.extraProperties[property.name]; var enumMemberName = getEnumMemberName(enumInfo, enumMemberValue); if (!enumMemberName) { @@ -199,6 +198,10 @@ return tableProperties; } + function getValueFromRow(property, row) { + return row.extraProperties[property.name];; + } + function convertPropertyToColumnConfig(property) { var columnConfig = { title: localizeDisplayName(property.name, property.config.displayName), @@ -208,10 +211,18 @@ if (property.config.typeSimple === 'enum') { columnConfig.render = function (data, type, row) { - return localizeEnumMember( - property, - row - ); + var value = getValueFromRow(property, row); + return localizeEnumMember(property, value); + } + } + else if (property.config.typeSimple === 'boolean') { + columnConfig.render = function (data, type, row) { + var value = getValueFromRow(property, row); + if (value) { + return ''; + } else { + return ''; + } } } From 7abcc0fdf8e9f285881c0acb6e1147d48376d9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 00:55:33 +0300 Subject: [PATCH 100/110] Add Format (asp-format), Name and Value properties to AbpInputTagHelper --- .../TagHelpers/Form/AbpInputTagHelper.cs | 7 +++++++ .../TagHelpers/Form/AbpInputTagHelperService.cs | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs index f264d21cd5..16a700f4bc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs @@ -33,6 +33,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form [ViewContext] public ViewContext ViewContext { get; set; } + [HtmlAttributeName("asp-format")] + public string Format { get; set; } + + public string Name { get; set; } + + public string Value { get; set; } + public AbpInputTagHelper(AbpInputTagHelperService tagHelperService) : base(tagHelperService) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index a2bb92a14d..b03b0c8b69 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -110,7 +110,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form return new TextAreaTagHelper(_generator) { For = TagHelper.AspFor, - ViewContext = TagHelper.ViewContext + ViewContext = TagHelper.ViewContext, + Name = TagHelper.Name }; } @@ -118,7 +119,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form { For = TagHelper.AspFor, InputTypeName = TagHelper.InputTypeName, - ViewContext = TagHelper.ViewContext + ViewContext = TagHelper.ViewContext, + Format = TagHelper.Format, + Name = TagHelper.Name, + Value = TagHelper.Value }; } From 67e68297e2b31fca8147997dc4eec0ff32567e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 00:57:52 +0300 Subject: [PATCH 101/110] Create GetInputFormatOrNull extension method for ObjectExtensionPropertyInfo --- ...UiObjectExtensionPropertyInfoExtensions.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs new file mode 100644 index 0000000000..bac7787028 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Volo.Abp.ObjectExtending +{ + public static class MvcUiObjectExtensionPropertyInfoExtensions + { + private static readonly Type[] DateTypes = new[] + { + typeof(DateTime), typeof(DateTimeOffset) + }; + + public static string GetInputFormatOrNull(this ObjectExtensionPropertyInfo property) + { + if (IsDate(property)) + { + return "{0:yyyy-MM-dd}"; + } + + return null; + } + + private static bool IsDate(ObjectExtensionPropertyInfo property) + { + if (!DateTypes.Contains(property.Type)) + { + return false; + } + + var dataTypeAttribute = property + .Attributes + .OfType() + .FirstOrDefault(); + + if (dataTypeAttribute == null) + { + return false; + } + + return dataTypeAttribute.DataType == DataType.Date; + } + } +} From 17891a959ad50e576a37214a153c71d12159ae00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 01:41:14 +0300 Subject: [PATCH 102/110] Add Default Renderers for datatables. --- .../datatables/datatables-extensions.js | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js index cd31e73bc4..40aebbc71f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js @@ -8,7 +8,7 @@ }; /************************************************************************ - * RECORD-ACTIONS extension for datatables * + * RECORD-ACTIONS extension for datatables * *************************************************************************/ (function () { if (!$.fn.dataTableExt) { @@ -328,4 +328,32 @@ })(); + /************************************************************************ + * Default Renderers * + *************************************************************************/ + + datatables.defaultRenderers = datatables.defaultRenderers || {}; + + datatables.defaultRenderers['boolean'] = function(value) { + if (value) { + return ''; + } else { + return ''; + } + }; + + datatables.defaultRenderers['date'] = function (value) { + return luxon + .DateTime + .fromISO(value, { locale: abp.localization.currentCulture.name }) + .toLocaleString(); + }; + + datatables.defaultRenderers['datetime'] = function (value) { + return luxon + .DateTime + .fromISO(value, { locale: abp.localization.currentCulture.name }) + .toLocaleString(luxon.DateTime.DATETIME_SHORT); + }; + })(jQuery); \ No newline at end of file From 2d55cbf1942ae6d56e6b2c6ef53b2b5331d31dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 01:41:18 +0300 Subject: [PATCH 103/110] Update LuxonScriptContributor.cs --- .../Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs index e605f6e6ae..3d2a1dc774 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Luxon From f160b368a99590e5064b4e3c69800e82b9db92c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 01:41:47 +0300 Subject: [PATCH 104/110] handle dates and datetimes on datatables MVC UI --- .../ui-extensions.js | 19 ++- ...UiObjectExtensionPropertyInfoExtensions.cs | 114 +++++++++++++++--- .../CachedObjectExtensionsDtoService.cs | 24 +++- .../ObjectExtendingPropertyInfoExtensions.cs | 113 +++-------------- .../IBasicObjectExtensionPropertyInfo.cs | 38 ++++++ .../ExtensionPropertyConfiguration.cs | 2 +- .../ModuleExtensionConfigurationHelper.cs | 1 + .../ObjectExtensionPropertyInfo.cs | 2 +- 8 files changed, 185 insertions(+), 128 deletions(-) create mode 100644 framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/IBasicObjectExtensionPropertyInfo.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js index 40e22d3da9..eacfecc867 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js @@ -96,7 +96,7 @@ get: _get }; })(); - + function initializeObjectExtensions() { var getShortEnumTypeName = function (enumType) { @@ -209,19 +209,18 @@ orderable: false }; + if (property.config.typeSimple === 'enum') { - columnConfig.render = function (data, type, row) { + columnConfig.render = function(data, type, row) { var value = getValueFromRow(property, row); return localizeEnumMember(property, value); } - } - else if (property.config.typeSimple === 'boolean') { - columnConfig.render = function (data, type, row) { - var value = getValueFromRow(property, row); - if (value) { - return ''; - } else { - return ''; + } else { + var defaultRenderer = abp.libs.datatables.defaultRenderers[property.config.typeSimple]; + if (defaultRenderer) { + columnConfig.render = function (data, type, row) { + var value = getValueFromRow(property, row); + return defaultRenderer(value); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs index bac7787028..608374e9e8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs @@ -1,19 +1,40 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; +using Microsoft.AspNetCore.Mvc; namespace Volo.Abp.ObjectExtending { public static class MvcUiObjectExtensionPropertyInfoExtensions { - private static readonly Type[] DateTypes = new[] - { - typeof(DateTime), typeof(DateTimeOffset) + private static readonly HashSet NumberTypes = new HashSet { + typeof(int), + typeof(long), + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(int?), + typeof(long?), + typeof(byte?), + typeof(sbyte?), + typeof(short?), + typeof(ushort?), + typeof(uint?), + typeof(long?), + typeof(ulong?), + typeof(float?), + typeof(double?), }; - public static string GetInputFormatOrNull(this ObjectExtensionPropertyInfo property) + public static string GetInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property) { - if (IsDate(property)) + if (property.IsDate()) { return "{0:yyyy-MM-dd}"; } @@ -21,24 +42,85 @@ namespace Volo.Abp.ObjectExtending return null; } - private static bool IsDate(ObjectExtensionPropertyInfo property) + 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 (!DateTypes.Contains(property.Type)) + if (attribute is EmailAddressAttribute) { - return false; + return "email"; } - var dataTypeAttribute = property - .Attributes - .OfType() - .FirstOrDefault(); + if (attribute is UrlAttribute) + { + return "url"; + } + + if (attribute is HiddenInputAttribute) + { + return "hidden"; + } + + if (attribute is PhoneAttribute) + { + return "tel"; + } - if (dataTypeAttribute == null) + if (attribute is DataTypeAttribute dataTypeAttribute) { - return false; + 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 dataTypeAttribute.DataType == DataType.Date; + 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; } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index 870ff0368e..76d73820b2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -107,9 +107,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending var extensionPropertyDto = new ExtensionPropertyDto { Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(propertyConfig.Type), - TypeSimple = propertyConfig.Type.IsEnum - ? "enum" - : TypeHelper.GetSimplifiedName(propertyConfig.Type), + TypeSimple = GetSimpleTypeName(propertyConfig), Attributes = new List(), DisplayName = CreateDisplayNameDto(propertyConfig), Configuration = new Dictionary(), @@ -161,6 +159,26 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending return extensionPropertyDto; } + protected virtual string GetSimpleTypeName(ExtensionPropertyConfiguration propertyConfig) + { + if (propertyConfig.Type.IsEnum) + { + return "enum"; + } + + if (propertyConfig.IsDate()) + { + return "date"; + } + + if (propertyConfig.IsDateTime()) + { + return "datetime"; + } + + return TypeHelper.GetSimplifiedName(propertyConfig.Type); + } + protected virtual LocalizableStringDto CreateDisplayNameDto(ExtensionPropertyConfiguration propertyConfig) { if (propertyConfig.DisplayName == null) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs index 04339eedd3..dc8a28e89f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs @@ -1,116 +1,35 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Mvc; +using System.Linq; namespace Volo.Abp.ObjectExtending { public static class ObjectExtensionPropertyInfoAspNetCoreMvcExtensions { - private static readonly HashSet NumberTypes = new HashSet { - typeof(int), - typeof(long), - typeof(byte), - typeof(sbyte), - typeof(short), - typeof(ushort), - typeof(uint), - typeof(long), - typeof(ulong), - typeof(float), - typeof(double), - typeof(int?), - typeof(long?), - typeof(byte?), - typeof(sbyte?), - typeof(short?), - typeof(ushort?), - typeof(uint?), - typeof(long?), - typeof(ulong?), - typeof(float?), - typeof(double?), + private static readonly Type[] DateTimeTypes = + { + typeof(DateTime), + typeof(DateTimeOffset) }; - public static string GetInputType(this ObjectExtensionPropertyInfo propertyInfo) + public static bool IsDate(this IBasicObjectExtensionPropertyInfo property) { - foreach (var attribute in propertyInfo.Attributes) - { - var inputTypeByAttribute = GetInputTypeFromAttributeOrNull(attribute); - if (inputTypeByAttribute != null) - { - return inputTypeByAttribute; - } - } - - return GetInputTypeFromTypeOrNull(propertyInfo.Type) - ?? "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 "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 (NumberTypes.Contains(type)) - { - return "number"; - } - - return null; + return property + .Attributes + .OfType() + .FirstOrDefault()?.DataType; } } } diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/IBasicObjectExtensionPropertyInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/IBasicObjectExtensionPropertyInfo.cs new file mode 100644 index 0000000000..de9bb35b01 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/IBasicObjectExtensionPropertyInfo.cs @@ -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 Attributes { get; } + + [NotNull] + public List> Validators { get; } + + [CanBeNull] + public ILocalizableString DisplayName { get; } + + /// + /// Uses as the default value if was not set. + /// + [CanBeNull] + public object DefaultValue { get; set; } + + /// + /// Used with the first priority to create the default value for the property. + /// Uses to the if this was not set. + /// + [CanBeNull] + public Func DefaultValueFactory { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs index 6d66e51337..0a19a82018 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs @@ -6,7 +6,7 @@ using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending.Modularity { - public class ExtensionPropertyConfiguration : IHasNameWithLocalizableDisplayName + public class ExtensionPropertyConfiguration : IHasNameWithLocalizableDisplayName, IBasicObjectExtensionPropertyInfo { [NotNull] public EntityExtensionConfiguration EntityExtensionConfiguration { get; } diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs index cb5be17c1f..f4a5b149d6 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using JetBrains.Annotations; namespace Volo.Abp.ObjectExtending.Modularity diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs index 12b32ab241..ddd8fbbc3c 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs @@ -7,7 +7,7 @@ using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending { - public class ObjectExtensionPropertyInfo : IHasNameWithLocalizableDisplayName + public class ObjectExtensionPropertyInfo : IHasNameWithLocalizableDisplayName, IBasicObjectExtensionPropertyInfo { [NotNull] public ObjectExtensionInfo ObjectExtension { get; } From 9ef9d142ef7171881463c81a788fda08fc56fcb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 01:51:29 +0300 Subject: [PATCH 105/110] Handle formatting for DateTime inputs for extension properties. --- .../MvcUiObjectExtensionPropertyInfoExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs index 608374e9e8..72338a021d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs @@ -39,6 +39,11 @@ namespace Volo.Abp.ObjectExtending return "{0:yyyy-MM-dd}"; } + if (property.IsDateTime()) + { + return "{0:yyyy-MM-ddTHH:mm}"; + } + return null; } From 0df437d876b3758d10d7d4f3dc83887edd5c0126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 02:12:17 +0300 Subject: [PATCH 106/110] Add decimal to NumberTypes for extension properties --- .../MvcUiObjectExtensionPropertyInfoExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs index 72338a021d..1ebe486068 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs @@ -19,6 +19,7 @@ namespace Volo.Abp.ObjectExtending typeof(ulong), typeof(float), typeof(double), + typeof(decimal), typeof(int?), typeof(long?), typeof(byte?), @@ -30,6 +31,7 @@ namespace Volo.Abp.ObjectExtending typeof(ulong?), typeof(float?), typeof(double?), + typeof(decimal?) }; public static string GetInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property) From c17868e5acef0ce3135e22d6ce07c90e5c25face Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 05:17:15 +0300 Subject: [PATCH 107/110] Handle floating numbers on extension properties. --- .../Extensions/TagHelperExtensions.cs | 20 +++++-- .../Form/AbpInputTagHelperService.cs | 40 +++++++++++-- ...UiObjectExtensionPropertyInfoExtensions.cs | 17 ++++++ ...PropertiesDictionaryModelBinderProvider.cs | 1 - .../AbpExtraPropertyModelBinder.cs | 6 +- .../Volo/Abp/Reflection/TypeHelper.cs | 56 +++++++++++++++++-- 6 files changed, 122 insertions(+), 18 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs index af961a2bf6..761b72ec99 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs @@ -7,14 +7,26 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions { public static class TagHelperExtensions { - public static async Task ProcessAndGetOutputAsync(this TagHelper tagHelper, TagHelperAttributeList attributeList, TagHelperContext context, string tagName = "div", TagMode tagMode = TagMode.SelfClosing) + public static async Task ProcessAndGetOutputAsync( + this TagHelper tagHelper, + TagHelperAttributeList attributeList, + TagHelperContext context, + string tagName = "div", + TagMode tagMode = TagMode.SelfClosing) { - var innerOutput = new TagHelperOutput(tagName, attributeList, (useCachedResult, encoder) => Task.Run(() => new DefaultTagHelperContent())) + var innerOutput = new TagHelperOutput( + tagName, + attributeList, + (useCachedResult, encoder) => Task.Run(() => new DefaultTagHelperContent())) { TagMode = tagMode }; - - var innerContext = new TagHelperContext(attributeList, context.Items, Guid.NewGuid().ToString()); + + var innerContext = new TagHelperContext( + attributeList, + context.Items, + Guid.NewGuid().ToString() + ); tagHelper.Init(context); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index b03b0c8b69..932c6d5864 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -115,22 +115,40 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form }; } - return new InputTagHelper(_generator) + var inputTagHelper = new InputTagHelper(_generator) { For = TagHelper.AspFor, InputTypeName = TagHelper.InputTypeName, - ViewContext = TagHelper.ViewContext, - Format = TagHelper.Format, - Name = TagHelper.Name, - Value = TagHelper.Value + ViewContext = TagHelper.ViewContext }; + + if (!TagHelper.Format.IsNullOrEmpty()) + { + inputTagHelper.Format = TagHelper.Format; + } + + if (!TagHelper.Name.IsNullOrEmpty()) + { + inputTagHelper.Name = TagHelper.Name; + } + + if (!TagHelper.Value.IsNullOrEmpty()) + { + inputTagHelper.Value = TagHelper.Value; + } + + return inputTagHelper; } protected virtual async Task<(TagHelperOutput, bool)> GetInputTagHelperOutputAsync(TagHelperContext context, TagHelperOutput output) { var tagHelper = GetInputTagHelper(context, output); - var inputTagHelperOutput = await tagHelper.ProcessAndGetOutputAsync(GetInputAttributes(context, output), context, "input"); + var inputTagHelperOutput = await tagHelper.ProcessAndGetOutputAsync( + GetInputAttributes(context, output), + context, + "input" + ); ConvertToTextAreaIfTextArea(inputTagHelperOutput); AddDisabledAttribute(inputTagHelperOutput); @@ -349,6 +367,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form attrList.Add("type", TagHelper.InputTypeName); } + if (!TagHelper.Name.IsNullOrEmpty() && !attrList.ContainsName("name")) + { + attrList.Add("name", TagHelper.Name); + } + + if (!TagHelper.Value.IsNullOrEmpty() && !attrList.ContainsName("value")) + { + attrList.Add("value", TagHelper.Value); + } + return attrList; } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs index 1ebe486068..7aea3cf9f2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending { @@ -49,6 +50,22 @@ namespace Volo.Abp.ObjectExtending 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) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertiesDictionaryModelBinderProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertiesDictionaryModelBinderProvider.cs index 3ed1eda3a1..812ca30f06 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertiesDictionaryModelBinderProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertiesDictionaryModelBinderProvider.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs index c94ad08e74..d09da73a59 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; using Volo.Abp.ObjectExtending; @@ -61,7 +60,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding return value; } - return TypeHelper.ConvertFromString(propertyInfo.Type, value); + return TypeHelper.ConvertFromString( + propertyInfo.Type, + value + ); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 192043a800..83182fc965 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -2,14 +2,23 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Reflection; using JetBrains.Annotations; +using Volo.Abp.Localization; namespace Volo.Abp.Reflection { public static class TypeHelper { + private static readonly HashSet FloatingTypes = new HashSet + { + typeof(float), + typeof(double), + typeof(decimal) + }; + private static readonly HashSet NonNullablePrimitiveTypes = new HashSet { typeof(byte), @@ -40,7 +49,7 @@ namespace Volo.Abp.Reflection { return false; } - + var type = obj.GetType(); if (!type.GetTypeInfo().IsGenericType) { @@ -169,7 +178,7 @@ namespace Volo.Abp.Reflection { return Activator.CreateInstance(type); } - + return null; } @@ -297,9 +306,39 @@ namespace Volo.Abp.Reflection public static object ConvertFromString(Type targetType, string value) { - return TypeDescriptor - .GetConverter(targetType) - .ConvertFromString(value); + if (value == null) + { + return null; + } + + var converter = TypeDescriptor.GetConverter(targetType); + + if (IsFloatingType(targetType)) + { + using (CultureHelper.Use(CultureInfo.InvariantCulture)) + { + return converter.ConvertFromString(value.Replace(',', '.')); + } + } + + return converter.ConvertFromString(value); + } + + public static bool IsFloatingType(Type type, bool includeNullable = true) + { + if (FloatingTypes.Contains(type)) + { + return true; + } + + if (includeNullable && + IsNullable(type) && + FloatingTypes.Contains(type.GenericTypeArguments[0])) + { + return true; + } + + return false; } public static object ConvertFrom(object value) @@ -313,5 +352,12 @@ namespace Volo.Abp.Reflection .GetConverter(targetType) .ConvertFrom(value); } + + public static Type StripNullable(Type type) + { + return IsNullable(type) + ? type.GenericTypeArguments[0] + : type; + } } } From d3a42228b2f983e45d648a1f59bc7354edbd1814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 19 May 2020 05:26:45 +0300 Subject: [PATCH 108/110] Set name only if there is a valid value. --- .../Form/AbpInputTagHelperService.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index 932c6d5864..4ddf44d2d6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -91,7 +91,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form inputHtml + label : label + inputHtml; - return innerContent + infoHtml + validation; + return innerContent + infoHtml + validation; } protected virtual string SurroundInnerHtmlAndGet(TagHelperContext context, TagHelperOutput output, string innerHtml, bool isCheckbox) @@ -103,16 +103,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual TagHelper GetInputTagHelper(TagHelperContext context, TagHelperOutput output) { - var textAreaAttribute = TagHelper.AspFor.ModelExplorer.GetAttribute