From 788b06146884dcc41c065563cb7000a33f854eeb Mon Sep 17 00:00:00 2001 From: Luis Pignataro Date: Fri, 24 Jan 2020 16:01:59 -0300 Subject: [PATCH 001/130] Correcciones en los textos --- .../Volo/Abp/Localization/Resources/AbpValidation/es.json | 2 +- .../src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpValidation/es.json b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpValidation/es.json index 25ed44d7ad..87d1316a38 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpValidation/es.json +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpValidation/es.json @@ -11,7 +11,7 @@ "The {0} field is not a valid phone number.": "{0} no es un número de teléfono válido.", "The field {0} must be between {1} and {2}.": "El campo {0} debe ser entre {1} y {2}.", "The field {0} must match the regular expression '{1}'.": "El campo {0} debe coincidir con la expresión regular '{1}'.", - "The {0} field is required.": "La {0} campo es obligatorio.", + "The {0} field is required.": "El campo {0} es obligatorio.", "The field {0} must be a string with a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud máxima de {1}.", "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud mínima de {2} y una longitud máxima de {1}.", "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "El campo {0} no es una dirección URL válida http, https o ftp.", diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json index 4997156c8d..46263a716c 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -38,8 +38,8 @@ "PagerPrevious": "Anterior", "PagerFirst": "Primero", "PagerLast": "Último", - "PagerInfo": "Mostrando desde _START_ hasta _END_ total _TOTAL_", - "PagerInfoEmpty": "Mostrando desde 0 hasta 0 de 0 registros", + "PagerInfo": "Mostrando registros del _START_ al _END_ de un total de _TOTAL_ registros", + "PagerInfoEmpty": "Mostrando registros del 0 al 0 de un total de 0 registros", "PagerInfoFiltered": "(filtrado de un total de _MAX_ registros)", "NoDataAvailableInDatatable": "No hay datos", "PagerShowMenuEntries": "Mostrar _MENU_ registros", From d941b1adeab5a77d49ed9b0601cf2b2e8d20a7f1 Mon Sep 17 00:00:00 2001 From: Luis Pignataro Date: Fri, 6 Mar 2020 11:07:39 -0300 Subject: [PATCH 002/130] Change special characters to unicode --- .../Volo/Abp/Emailing/Localization/es.json | 23 +++++++++ .../Navigation/Localization/Resource/es.json | 2 +- .../Localization/Resources/AbpUi/es.json | 36 +++++++------- .../Volo/Abp/Validation/Localization/es.json | 48 +++++++++---------- .../TestResources/Base/CountryNames/es.json | 2 +- .../TestResources/Base/Validation/es.json | 4 +- .../Localization/TestResources/Source/es.json | 2 +- 7 files changed, 70 insertions(+), 47 deletions(-) create mode 100644 framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json new file mode 100644 index 0000000000..ae3a29f901 --- /dev/null +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json @@ -0,0 +1,23 @@ +{ + "culture": "es", + "texts": { + "DisplayName:Abp.Mailing.DefaultFromAddress": "Direcci\u00F3n de env\u00EDo", + "DisplayName:Abp.Mailing.DefaultFromDisplayName": "Nombre de env\u00EDo", + "DisplayName:Abp.Mailing.Smtp.Host": "Host", + "DisplayName:Abp.Mailing.Smtp.Port": "Puerto", + "DisplayName:Abp.Mailing.Smtp.UserName": "Nombre de usuario", + "DisplayName:Abp.Mailing.Smtp.Password": "Contrase\\00F1", + "DisplayName:Abp.Mailing.Smtp.Domain": "Dominio", + "DisplayName:Abp.Mailing.Smtp.EnableSsl": "Habilitar SSL", + "DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "Usar las credenciales por defecto", + "Description:Abp.Mailing.DefaultFromAddress": "La direcci\u00F3n de env\u00EDo por defecto", + "Description:Abp.Mailing.DefaultFromDisplayName": "El nombre de env\u00EDo por defecto", + "Description:Abp.Mailing.Smtp.Host": "El nombre o direcci\u00F3n del host para transacciones SMTP.", + "Description:Abp.Mailing.Smtp.Port": "El puerto usado para transacciones SMTP.", + "Description:Abp.Mailing.Smtp.UserName": "Nombre de usuario asociado a las credenciales.", + "Description:Abp.Mailing.Smtp.Password": "La contraseña del ususario asociado a las credenciales.", + "Description:Abp.Mailing.Smtp.Domain": "El dominio o equipo que valida las credenciales.", + "Description:Abp.Mailing.Smtp.EnableSsl": "Si SmtpClient usa Secure Sockets Layer (SSL) para encriptar la conecci\u00F3n.", + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si las credenciales por defecto se env\u00EDan con los request." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json index ba27a9193c..caa51f8d1d 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json @@ -1,6 +1,6 @@ { "culture": "es", "texts": { - "Menu:Administration": "Administración" + "Menu:Administration": "Administraci\u00F3n" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json index 46263a716c..4ed8edc8e0 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -1,20 +1,20 @@ { "culture": "es", "texts": { - "InternalServerErrorMessage": "Ocurrió un error interno en su pedido!", - "ValidationErrorMessage": "Su pedido no es válido!", - "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.", - "DefaultErrorMessage": "Ocurrió un error!", - "DefaultErrorMessageDetail": "El servidor no envió el detalle del error.", - "DefaultErrorMessage401": "Usted no ha iniciado sessión!", - "DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.", - "DefaultErrorMessage403": "Usted no está autorizado!", - "DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!", - "DefaultErrorMessage404": "No se encontró el recurso!", - "DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!", + "InternalServerErrorMessage": "Ocurri\u00F3 un error interno en su pedido!", + "ValidationErrorMessage": "Su pedido no es v\u00E1lido!", + "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validaci\u00F3n.", + "DefaultErrorMessage": "Ocurri\u00F3 un error!", + "DefaultErrorMessageDetail": "El servidor no envi\u00F3 el detalle del error.", + "DefaultErrorMessage401": "Usted no ha iniciado sessi\u00F3n!", + "DefaultErrorMessage401Detail": "Debe iniciar sessi\u00F3n para ejecutar esta operaci\u00F3n.", + "DefaultErrorMessage403": "Usted no est\u00E1 autorizado!", + "DefaultErrorMessage403Detail": "Usted no puede hacer esta operaci\u00F3n!", + "DefaultErrorMessage404": "No se encontr\u00F3 el recurso!", + "DefaultErrorMessage404Detail": "El recurso solicitado no se encontr\u00F3 en el servidor!", "EntityNotFoundErrorMessage": "No hay una entidad {0} con el id = {1}!", "Error": "Error", - "AreYouSure": "¿Está seguro?", + "AreYouSure": "¿Est\u00E1 seguro?", "Cancel": "Cancelar", "Yes": "Si", "No": "No", @@ -30,14 +30,14 @@ "Welcome": "Bienvenido", "Login": "Iniciar session", "Register": "Registrarse", - "Logout": "Cerrar sessión", + "Logout": "Cerrar sessi\u00F3n", "Submit": "Enviar", - "Back": "Atrás", + "Back": "Atr\u00E1s", "PagerSearch": "Buscar", "PagerNext": "Siguiente", "PagerPrevious": "Anterior", "PagerFirst": "Primero", - "PagerLast": "Último", + "PagerLast": "\u00DCltimo", "PagerInfo": "Mostrando registros del _START_ al _END_ de un total de _TOTAL_ registros", "PagerInfoEmpty": "Mostrando registros del 0 al 0 de un total de 0 registros", "PagerInfoFiltered": "(filtrado de un total de _MAX_ registros)", @@ -47,12 +47,12 @@ "ChangePassword": "Cambiar contraseña", "PersonalInfo": "My perfil", "AreYouSureYouWantToCancelEditingWarningMessage": "Tiene cambios sin guardar.", - "UnhandledException": "Excepción no controlada!", + "UnhandledException": "Excepci\u00F3n no controlada!", "401Message": "No autorizado", "403Message": "Prohibido", - "404Message": "La página no existe", + "404Message": "La p\u00E1gina no existe", "500Message": "Error interno del servidor", - "GoHomePage": "Ir a la página principal", + "GoHomePage": "Ir a la p\u00E1gina principal", "GoBack": "Atras" } } diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json index 87d1316a38..befab1cba8 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json @@ -2,33 +2,33 @@ "culture": "es", "texts": { "'{0}' and '{1}' do not match.": "'{0} \" y \"{1} \" no coinciden.", - "The {0} field is not a valid credit card number.": "{0} no es un número de tarjeta de crédito.", - "{0} is not valid.": "{0} no es válido.", - "The {0} field is not a valid e-mail address.": "{0} no es una dirección de correo electrónico válida.", - "The {0} field only accepts files with the following extensions: {1}": "{0} campo sólo acepta archivos con las siguientes extensiones: {1}", - "The field {0} must be a string or array type with a maximum length of '{1}'.": "El campo {0} debe ser una cadena o un array con una longitud máxima de '{1}'.", - "The field {0} must be a string or array type with a minimum length of '{1}'.": "El campo {0} debe ser una cadena o un array con una longitud mínima de '{1}'.", - "The {0} field is not a valid phone number.": "{0} no es un número de teléfono válido.", + "The {0} field is not a valid credit card number.": "{0} no es un n\u00FCmero de tarjeta de cr\u00E9dito.", + "{0} is not valid.": "{0} no es v\u00E1lido.", + "The {0} field is not a valid e-mail address.": "{0} no es una direcci\u00F3n de correo electr\u00F3nico v\u00E1lida.", + "The {0} field only accepts files with the following extensions: {1}": "{0} campo s\u00F3lo acepta archivos con las siguientes extensiones: {1}", + "The field {0} must be a string or array type with a maximum length of '{1}'.": "El campo {0} debe ser una cadena o un array con una longitud m\u00E1xima de '{1}'.", + "The field {0} must be a string or array type with a minimum length of '{1}'.": "El campo {0} debe ser una cadena o un array con una longitud m\u00EDnima de '{1}'.", + "The {0} field is not a valid phone number.": "{0} no es un n\u00FCmero de tel\u00E9fono v\u00E1lido.", "The field {0} must be between {1} and {2}.": "El campo {0} debe ser entre {1} y {2}.", - "The field {0} must match the regular expression '{1}'.": "El campo {0} debe coincidir con la expresión regular '{1}'.", + "The field {0} must match the regular expression '{1}'.": "El campo {0} debe coincidir con la expresi\u00F3n regular '{1}'.", "The {0} field is required.": "El campo {0} es obligatorio.", - "The field {0} must be a string with a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud máxima de {1}.", - "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud mínima de {2} y una longitud máxima de {1}.", - "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "El campo {0} no es una dirección URL válida http, https o ftp.", - "The field {0} is invalid.": "El campo {0} no es válido.", - "ThisFieldIsNotAValidCreditCardNumber.": "Este campo no es un número de tarjeta de crédito.", - "ThisFieldIsNotValid.": "Este campo no es válido.", - "ThisFieldIsNotAValidEmailAddress.": "Este campo no es una dirección de correo electrónico válida.", - "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "Este campo sólo acepta archivos con las siguientes extensiones: {0}", - "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthoOf{0}": "Este campo debe ser una cadena o un array con una longitud máxima de '{0}'.", - "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "Este campo debe ser una cadena o un array con una longitud mínima de '{0}'.", - "ThisFieldIsNotAValidPhoneNumber.": "Este campo no es un número de teléfono válido.", + "The field {0} must be a string with a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud m\u00E1xima de {1}.", + "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud m\u00EDnima de {2} y una longitud m\u00E1xima de {1}.", + "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "El campo {0} no es una direcci\u00F3n URL v\u00E1lida http, https o ftp.", + "The field {0} is invalid.": "El campo {0} no es v\u00E1lido.", + "ThisFieldIsNotAValidCreditCardNumber.": "Este campo no es un n\u00FCmero de tarjeta de cr\u00E9dito.", + "ThisFieldIsNotValid.": "Este campo no es v\u00E1lido.", + "ThisFieldIsNotAValidEmailAddress.": "Este campo no es una direcci\u00F3n de correo electr\u00F3nico v\u00E1lida.", + "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "Este campo s\u00F3lo acepta archivos con las siguientes extensiones: {0}", + "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthoOf{0}": "Este campo debe ser una cadena o un array con una longitud m\u00E1xima de '{0}'.", + "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "Este campo debe ser una cadena o un array con una longitud m\u00EDnima de '{0}'.", + "ThisFieldIsNotAValidPhoneNumber.": "Este campo no es un n\u00FCmero de tel\u00E9fono v\u00E1lido.", "ThisFieldMustBeBetween{0}And{1}": "Este campo debe tener un valor entre {0} y {1}.", - "ThisFieldMustMatchTheRegularExpression{0}": "Este campo debe coincidir con la expresión regular '{0}'.", + "ThisFieldMustMatchTheRegularExpression{0}": "Este campo debe coincidir con la expresi\u00F3n regular '{0}'.", "ThisFieldIsRequired.": "Este campo es obligatorio.", - "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "Este campo debe ser una cadena de caracteres con una longitud máxima de {0}.", - "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "Este campo debe ser una cadena de caracteres con una longitud mínima de {1} y una longitud máxima de {0}.", - "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "Este campo no es una dirección URL válida http, https o ftp.", - "ThisFieldIsInvalid.": "Este campo no es válido." + "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "Este campo debe ser una cadena de caracteres con una longitud m\u00E1xima de {0}.", + "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "Este campo debe ser una cadena de caracteres con una longitud m\u00EDnima de {1} y una longitud m\u00E1xima de {0}.", + "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "Este campo no es una direcci\u00F3n URL v\u00E1lida http, https o ftp.", + "ThisFieldIsInvalid.": "Este campo no es v\u00E1lido." } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json index d9b29895b8..625ca51545 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json @@ -1,7 +1,7 @@ { "culture": "es", "texts": { - "USA": "Estados unidos de América", + "USA": "Estados unidos de Am\u00E9rica", "Brazil": "Brasil" } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json index 04e85b5f12..df5bdbd8f7 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json @@ -1,7 +1,7 @@ { "culture": "es", "texts": { - "ThisFieldIsRequired": "El campo no puede estar vacío", - "MaxLenghtErrorMessage": "El campo puede tener un máximo de '{0}' caracteres" + "ThisFieldIsRequired": "El campo no puede estar vac\u00EDo", + "MaxLenghtErrorMessage": "El campo puede tener un m\u00E1ximo de '{0}' caracteres" } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json index 7b68ff6f2c..6b45b36efd 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json @@ -4,7 +4,7 @@ "Hello {0}.": "Hola {0}.", "Car": "Auto", "CarPlural": "Autos", - "MaxLenghtErrorMessage": "El campo puede tener un máximo de '{0}' caracteres", + "MaxLenghtErrorMessage": "El campo puede tener un m\u00E1ximo de '{0}' caracteres", "Universe": "Universo", "FortyTwo": "Curenta y dos" } From 2689af617424c08ca5e24c95dc73f81eb3c69cc7 Mon Sep 17 00:00:00 2001 From: Luis Pignataro Date: Mon, 23 Mar 2020 13:30:37 -0300 Subject: [PATCH 003/130] Spanish Localization Fixes --- framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj | 9 ++++++++- .../Volo.Abp.Localization/Volo.Abp.Localization.csproj | 7 +++++++ .../Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj | 7 +++++++ framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj | 7 +++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj b/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj index be953a1f26..a840763492 100644 --- a/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj +++ b/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj @@ -25,7 +25,7 @@ - + @@ -35,4 +35,11 @@ + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj index 0fd10d94ca..c785de3f18 100644 --- a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj +++ b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj @@ -29,4 +29,11 @@ + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj b/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj index 5236bf0404..f5272a2c31 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj +++ b/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj @@ -23,4 +23,11 @@ + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj b/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj index acff7d004b..e11709841d 100644 --- a/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj +++ b/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj @@ -22,5 +22,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers + + From 64e8590691a464bc5752c01a295c15178608343b Mon Sep 17 00:00:00 2001 From: Luis Pignataro Date: Mon, 4 May 2020 17:22:37 -0300 Subject: [PATCH 004/130] Ignore package-lock.json --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index eb4dbedfae..9a855fd898 100644 --- a/.gitignore +++ b/.gitignore @@ -293,3 +293,10 @@ samples/MicroserviceDemo/applications/ConsoleClientDemo/Logs/logs.txt modules/docs/app/Volo.DocsTestApp/Logs/logs.txt framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Logs/logs.txt samples/MicroserviceDemo/microservices/TenantManagementService.Host/Logs/logs.txt +/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json +/samples/BookStore/src/Acme.BookStore.Web/package-lock.json +/samples/DashboardDemo/src/DashboardDemo.Web/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/package-lock.json From 7293ce35c677ead6117a5eefe6eeb1dcca4bdae6 Mon Sep 17 00:00:00 2001 From: Luis Pignataro Date: Mon, 4 May 2020 19:07:28 -0300 Subject: [PATCH 005/130] Change file to UTF-8 --- .../Volo/Abp/Emailing/Localization/es.json | 16 ++++---- .../Navigation/Localization/Resource/es.json | 2 +- .../Localization/Resources/AbpUi/es.json | 38 +++++++++---------- .../Volo/Abp/Validation/Localization/es.json | 4 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json index ae3a29f901..33c59be8b5 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json @@ -1,23 +1,23 @@ { "culture": "es", "texts": { - "DisplayName:Abp.Mailing.DefaultFromAddress": "Direcci\u00F3n de env\u00EDo", - "DisplayName:Abp.Mailing.DefaultFromDisplayName": "Nombre de env\u00EDo", + "DisplayName:Abp.Mailing.DefaultFromAddress": "Dirección de envío", + "DisplayName:Abp.Mailing.DefaultFromDisplayName": "Nombre de envío", "DisplayName:Abp.Mailing.Smtp.Host": "Host", "DisplayName:Abp.Mailing.Smtp.Port": "Puerto", "DisplayName:Abp.Mailing.Smtp.UserName": "Nombre de usuario", - "DisplayName:Abp.Mailing.Smtp.Password": "Contrase\\00F1", + "DisplayName:Abp.Mailing.Smtp.Password": "Contraseña", "DisplayName:Abp.Mailing.Smtp.Domain": "Dominio", "DisplayName:Abp.Mailing.Smtp.EnableSsl": "Habilitar SSL", "DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "Usar las credenciales por defecto", - "Description:Abp.Mailing.DefaultFromAddress": "La direcci\u00F3n de env\u00EDo por defecto", - "Description:Abp.Mailing.DefaultFromDisplayName": "El nombre de env\u00EDo por defecto", - "Description:Abp.Mailing.Smtp.Host": "El nombre o direcci\u00F3n del host para transacciones SMTP.", + "Description:Abp.Mailing.DefaultFromAddress": "La dirección de envío por defecto", + "Description:Abp.Mailing.DefaultFromDisplayName": "El nombre de envío por defecto", + "Description:Abp.Mailing.Smtp.Host": "El nombre o dirección del host para transacciones SMTP.", "Description:Abp.Mailing.Smtp.Port": "El puerto usado para transacciones SMTP.", "Description:Abp.Mailing.Smtp.UserName": "Nombre de usuario asociado a las credenciales.", "Description:Abp.Mailing.Smtp.Password": "La contraseña del ususario asociado a las credenciales.", "Description:Abp.Mailing.Smtp.Domain": "El dominio o equipo que valida las credenciales.", - "Description:Abp.Mailing.Smtp.EnableSsl": "Si SmtpClient usa Secure Sockets Layer (SSL) para encriptar la conecci\u00F3n.", - "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si las credenciales por defecto se env\u00EDan con los request." + "Description:Abp.Mailing.Smtp.EnableSsl": "Si SmtpClient usa Secure Sockets Layer (SSL) para encriptar la conección.", + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si las credenciales por defecto se envían con los request." } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json index caa51f8d1d..0fc6660fac 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json @@ -1,6 +1,6 @@ { "culture": "es", "texts": { - "Menu:Administration": "Administraci\u00F3n" + "Menu:Administration": "Administración" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json index b52c56510b..be137f013b 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -1,20 +1,20 @@ { "culture": "es", "texts": { - "InternalServerErrorMessage": "Ocurrió un error interno en su pedido!", - "ValidationErrorMessage": "Su pedido no es válido!", - "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.", - "DefaultErrorMessage": "Ocurrió un error!", - "DefaultErrorMessageDetail": "El servidor no envió el detalle del error.", - "DefaultErrorMessage401": "Usted no ha iniciado sessión!", - "DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.", - "DefaultErrorMessage403": "Usted no está autorizado!", - "DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!", - "DefaultErrorMessage404": "No se encontró el recurso!", - "DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!", + "InternalServerErrorMessage": "Ocurrió un error interno en su pedido!", + "ValidationErrorMessage": "Su pedido no es válido!", + "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.", + "DefaultErrorMessage": "Ocurrió un error!", + "DefaultErrorMessageDetail": "El servidor no envió el detalle del error.", + "DefaultErrorMessage401": "Usted no ha iniciado sessión!", + "DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.", + "DefaultErrorMessage403": "Usted no está autorizado!", + "DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!", + "DefaultErrorMessage404": "No se encontró el recurso!", + "DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!", "EntityNotFoundErrorMessage": "No hay una entidad {0} con el id = {1}!", "Error": "Error", - "AreYouSure": "¿Está seguro?", + "AreYouSure": "¿Está seguro?", "Cancel": "Cancelar", "Yes": "Si", "No": "No", @@ -30,14 +30,14 @@ "Welcome": "Bienvenido", "Login": "Iniciar session", "Register": "Registrarse", - "Logout": "Cerrar sessión", + "Logout": "Cerrar sessión", "Submit": "Enviar", - "Back": "Atrás", + "Back": "Atrás", "PagerSearch": "Buscar", "PagerNext": "Siguiente", "PagerPrevious": "Anterior", "PagerFirst": "Primero", - "PagerLast": "Último", + "PagerLast": "Último", "PagerInfo": "Mostrando desde _START_ hasta _END_ total _TOTAL_", "PagerInfo{0}{1}{2}": "Mostrando desde {0} hasta {1} total {2}", "PagerInfoEmpty": "Mostrando desde 0 hasta 0 de 0 registros", @@ -45,15 +45,15 @@ "NoDataAvailableInDatatable": "No hay datos", "PagerShowMenuEntries": "Mostrar _MENU_ registros", "DatatableActionDropdownDefaultText": "Acciones", - "ChangePassword": "Cambiar contraseña", + "ChangePassword": "Cambiar contraseña", "PersonalInfo": "My perfil", "AreYouSureYouWantToCancelEditingWarningMessage": "Tiene cambios sin guardar.", - "UnhandledException": "Excepción no controlada!", + "UnhandledException": "Excepción no controlada!", "401Message": "No autorizado", "403Message": "Prohibido", - "404Message": "La página no existe", + "404Message": "La página no existe", "500Message": "Error interno del servidor", - "GoHomePage": "Ir a la página principal", + "GoHomePage": "Ir a la página principal", "GoBack": "Atras" } } diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json index 1615d7db42..26097e0e87 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json @@ -1,4 +1,4 @@ -{ +{ "culture": "es", "texts": { "'{0}' and '{1}' do not match.": "'{0} \" y \"{1} \" no coinciden.", @@ -11,7 +11,7 @@ "The {0} field is not a valid phone number.": "{0} no es un número de teléfono válido.", "The field {0} must be between {1} and {2}.": "El campo {0} debe ser entre {1} y {2}.", "The field {0} must match the regular expression '{1}'.": "El campo {0} no coincide con el formato solicitado.", - "The {0} field is required.": "La {0} campo es obligatorio.", + "The {0} field is required.": "El {0} campo es obligatorio.", "The field {0} must be a string with a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud máxima de {1}.", "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud mínima de {2} y una longitud máxima de {1}.", "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "El campo {0} no es una dirección URL válida http, https o ftp.", From 7f52cb3e09baa31cad987c9596d6a1f8ea52d48a Mon Sep 17 00:00:00 2001 From: Luis Pignataro Date: Tue, 5 May 2020 12:31:36 -0300 Subject: [PATCH 006/130] Spanish localization for Account Module --- .../Account/Localization/Resources/es.json | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json new file mode 100644 index 0000000000..43554b2311 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json @@ -0,0 +1,45 @@ +{ + "culture": "es", + "texts": { + "UserName": "Nombre de usuario", + "EmailAddress": "Dirección de correo electrónico", + "UserNameOrEmailAddress": "Nombre de usuario o dirección de correo electrónico", + "Password": "Contraseña", + "RememberMe": "Recuérdame", + "UseAnotherServiceToLogin": "Usar otro servicio para iniciar sesión", + "UserLockedOutMessage": "La cuenta de usuario ha sido bloqueada debido a los intentos de inicio de sesión no válidos. Por favor, espere unos minutos y vuelve a intentarlo.", + "InvalidUserNameOrPassword": "El nombre de usuario o la contraseña no son válidos", + "LoginIsNotAllowed": "No está permitido el inicio de sesión! Usted tendrá que confirmar su correo electrónico o número de teléfono.", + "SelfRegistrationDisabledMessage": "El autoregistro de usuario está deshabilitado para esta aplicación. Póngase en contacto con el administrador de la aplicación para registrar un nuevo usuario.", + "LocalLoginDisabledMessage": "No está habilitado el login local para esta aplicación.", + "Login": "Iniciar sesión", + "Cancel": "Cancelar", + "Register": "Registrarse", + "AreYouANewUser": "¿Usted es un usuario nuevo?", + "AlreadyRegistered": "¿Está registrado en esta aplicación?", + "InvalidLoginRequest": "Solicitud de inicio de sesión no válido", + "ThereAreNoLoginSchemesConfiguredForThisClient": "No hay ningún esquema de inicio de sesión configurado para este cliente.", + "LogInUsingYourProviderAccount": "Inicia sesión con tu cuenta de {0} ", + "DisplayName:CurrentPassword": "Contraseña actual", + "DisplayName:NewPassword": "Nueva contraseña", + "DisplayName:NewPasswordConfirm": "Confirmar nueva contraseña", + "PasswordChangedMessage": "Su contraseña ha sido cambiada con éxito.", + "DisplayName:UserName": "Nombre de usuario", + "DisplayName:Email": "Correo electrónico", + "DisplayName:Name": "Nombre", + "DisplayName:Surname": "Apellido", + "DisplayName:Password": "Contraseña", + "DisplayName:EmailAddress": "Dirección de correo electrónico", + "DisplayName:PhoneNumber": "Número de teléfono", + "PersonalSettings": "Configuración Personal", + "PersonalSettingsSaved": "Ajustes personales guardados", + "PasswordChanged": "Cambiar la contraseña", + "NewPasswordConfirmFailed": "Por favor, confirme la nueva contraseña.", + "Manage": "Administrar", + "ManageYourProfile": "Gestionar su perfil", + "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Habilitar el registro de usuario", + "Description:Abp.Account.IsSelfRegistrationEnabled": "Si está habilitado los usuarios pueden crear una cuenta mediante el registro automático.", + "DisplayName:Abp.Account.EnableLocalLogin": "Habilitar cuenta local", + "Description:Abp.Account.EnableLocalLogin": "Indica que el servidor permite iniciar sessión con una cuenta local." + } +} \ No newline at end of file From 919a2e2e01892f70722a4eb6ba8fd19016e33da1 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Fri, 8 May 2020 23:18:08 +0300 Subject: [PATCH 007/130] 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"}}

+ + + +``` + +## 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"}}

+ + +``` + +### 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.

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

Şifrenizi Değiştirin

+ +

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

+ + +``` + +## 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 008/130] 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 009/130] 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 010/130] 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 011/130] 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 012/130] 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 8a235cb25cdd8957716035f10318fcb9abf80efa Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Mon, 11 May 2020 02:09:06 +0300 Subject: [PATCH 013/130] fix(core): condition bug resolves #3886 --- npm/ng-packs/packages/core/src/lib/states/config.state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 af706586bb..546b3d0b1e 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 @@ -324,7 +324,7 @@ function patchRouteDeep( ): ABP.FullRoute[] { routes = routes.map(route => { if (route.name === name) { - newValue.url = `${parentUrl}/${(!newValue.path && newValue.path === '' + newValue.url = `${parentUrl}/${(!newValue.path || newValue.path === '' ? route.path : newValue.path) || ''}`; From 7eadf380d4146bb0a43fcdb958f784085ece2ad5 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 11 May 2020 13:28:30 +0800 Subject: [PATCH 014/130] Add dependency of @abp/clipboard in @abp/prismjs package. Resolve #3861 --- npm/packs/prismjs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/npm/packs/prismjs/package.json b/npm/packs/prismjs/package.json index 1c53989fe5..ea92abe767 100644 --- a/npm/packs/prismjs/package.json +++ b/npm/packs/prismjs/package.json @@ -6,6 +6,7 @@ }, "dependencies": { "@abp/core": "^2.7.0", + "@abp/clipboard": "^2.7.0", "prismjs": "^1.17.1" }, "gitHead": "0ea3895f3b0b489e3ea81fc88f8f0896b22b61bd" From 82e35727c412f95173c5ba3f938d656c344e5bbd Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 11 May 2020 14:15:45 +0800 Subject: [PATCH 015/130] Add error page in template projects. --- .../MyProjectNameHttpApiHostModule.cs | 11 +++++++++++ .../MyProjectNameWebModule.cs | 3 +-- .../MyProjectNameWebModule.cs | 4 ++-- .../MyProjectNameHttpApiHostModule.cs | 10 +++++++++- .../MyProjectNameIdentityServerModule.cs | 10 +++++++++- .../MyProjectNameWebUnifiedModule.cs | 3 ++- 6 files changed, 34 insertions(+), 7 deletions(-) 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 671d0edbb2..2526aec04d 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 @@ -15,6 +15,7 @@ using Microsoft.OpenApi.Models; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; using Volo.Abp.Caching; @@ -162,6 +163,16 @@ namespace MyCompanyName.MyProjectName public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseErrorPage(); + } app.UseCorrelationId(); app.UseVirtualFiles(); 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 7419ff4a7d..0809a70cc4 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 @@ -213,8 +213,6 @@ namespace MyCompanyName.MyProjectName.Web var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); - app.UseCorrelationId(); - if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -224,6 +222,7 @@ namespace MyCompanyName.MyProjectName.Web app.UseErrorPage(); } + app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); app.UseAuthentication(); 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 0b2fe70094..8330951dfc 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -192,8 +192,6 @@ namespace MyCompanyName.MyProjectName.Web var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); - app.UseCorrelationId(); - if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -202,6 +200,8 @@ namespace MyCompanyName.MyProjectName.Web { app.UseErrorPage(); } + + app.UseCorrelationId(); app.UseVirtualFiles(); app.UseRouting(); app.UseAuthentication(); 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 f961f2a1f8..e425512797 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 @@ -15,6 +15,7 @@ using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; @@ -147,11 +148,18 @@ namespace MyCompanyName.MyProjectName public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); - if (!context.GetEnvironment().IsDevelopment()) + if (env.IsDevelopment()) { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseErrorPage(); app.UseHsts(); } + app.UseHttpsRedirection(); app.UseCorrelationId(); app.UseVirtualFiles(); 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 6e66cc7500..9d0cb8397c 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/MyProjectNameIdentityServerModule.cs @@ -17,6 +17,7 @@ using Volo.Abp.AspNetCore.Authentication.JwtBearer; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Auditing; using Volo.Abp.AuditLogging.EntityFrameworkCore; @@ -168,11 +169,18 @@ namespace MyCompanyName.MyProjectName public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); - if (!context.GetEnvironment().IsDevelopment()) + if (env.IsDevelopment()) { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseErrorPage(); app.UseHsts(); } + app.UseHttpsRedirection(); app.UseCorrelationId(); app.UseVirtualFiles(); 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 a3480fa8cf..48b0667350 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 @@ -112,8 +112,9 @@ namespace MyCompanyName.MyProjectName public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); - if (context.GetEnvironment().IsDevelopment()) + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } From 54e9eb5ef8211067885f8dd19fda2aa29418af19 Mon Sep 17 00:00:00 2001 From: xyfy Date: Mon, 11 May 2020 18:07:10 +0800 Subject: [PATCH 016/130] Update Index.md --- docs/zh-Hans/Modules/Index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh-Hans/Modules/Index.md b/docs/zh-Hans/Modules/Index.md index fb07b5b001..fa7b063004 100644 --- a/docs/zh-Hans/Modules/Index.md +++ b/docs/zh-Hans/Modules/Index.md @@ -28,4 +28,4 @@ ABP是一个 **模块化的应用程序框架** 由十多个 **nuget packages** ## 商业应用模块 -[ABP商业](https://commercial.abp.io/)许可证在ABP框架上提供了额外的预构建应用程序模块. 参见ABP商业版提供的[模块列表](https://commercial.abp.io/module). +[ABP商业](https://commercial.abp.io/)许可证在ABP框架上提供了额外的预构建应用程序模块. 参见ABP商业版提供的[模块列表](https://commercial.abp.io/modules). From d3e7a50f9f52f2b03547ea4a720b3aceff78df91 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Mon, 11 May 2020 19:51:03 +0800 Subject: [PATCH 017/130] Add AbpAspNetCoreMvcModule to microservices host module dependencies --- .../BloggingService.Host/BloggingServiceHostModule.cs | 2 ++ .../IdentityService.Host/IdentityServiceHostModule.cs | 2 ++ .../ProductService.Host/ProductServiceHostModule.cs | 2 ++ .../TenantManagementServiceHostModule.cs | 2 ++ 4 files changed, 8 insertions(+) diff --git a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs index dc87e7c71e..692d4c3d61 100644 --- a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs @@ -13,6 +13,7 @@ using MsDemo.Shared; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Auditing; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; @@ -40,6 +41,7 @@ namespace BloggingService.Host { [DependsOn( typeof(AbpAutofacModule), + typeof(AbpAspNetCoreMvcModule), typeof(AbpEventBusRabbitMqModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpAuditLoggingEntityFrameworkCoreModule), diff --git a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs index 71858d497a..982f4c09af 100644 --- a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs @@ -9,6 +9,7 @@ using Microsoft.OpenApi.Models; using MsDemo.Shared; using Volo.Abp; using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Auditing; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; @@ -29,6 +30,7 @@ namespace IdentityService.Host { [DependsOn( typeof(AbpAutofacModule), + typeof(AbpAspNetCoreMvcModule), typeof(AbpEventBusRabbitMqModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpAuditLoggingEntityFrameworkCoreModule), diff --git a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs index d3b370df0c..44e23eeb78 100644 --- a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs @@ -12,6 +12,7 @@ using MsDemo.Shared; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Auditing; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; @@ -32,6 +33,7 @@ namespace ProductService.Host { [DependsOn( typeof(AbpAutofacModule), + typeof(AbpAspNetCoreMvcModule), typeof(AbpEventBusRabbitMqModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpAuditLoggingEntityFrameworkCoreModule), diff --git a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs index 2952fe65c3..f9a818234f 100644 --- a/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/TenantManagementService.Host/TenantManagementServiceHostModule.cs @@ -9,6 +9,7 @@ using MsDemo.Shared; using StackExchange.Redis; using Volo.Abp; using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Auditing; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; @@ -29,6 +30,7 @@ namespace TenantManagementService.Host { [DependsOn( typeof(AbpAutofacModule), + typeof(AbpAspNetCoreMvcModule), typeof(AbpEventBusRabbitMqModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpAuditLoggingEntityFrameworkCoreModule), From 74fb8d2e737aee2a117320682eb731cc2ac06015 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 11 May 2020 22:18:41 +0800 Subject: [PATCH 018/130] Add Email property to CurrentUserDto. --- .../Mvc/ApplicationConfigurations/CurrentUserDto.cs | 4 +++- .../AbpApplicationConfigurationAppService.cs | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs index 702af56dba..2197d7b08e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs @@ -12,5 +12,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public Guid? TenantId { get; set; } public string UserName { get; set; } + + public string Email { get; set; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index 98fd94882d..a68a38b274 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -107,7 +107,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations IsAuthenticated = _currentUser.IsAuthenticated, Id = _currentUser.Id, TenantId = _currentUser.TenantId, - UserName = _currentUser.UserName + UserName = _currentUser.UserName, + Email = _currentUser.Email }; } @@ -226,4 +227,4 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations return result; } } -} \ No newline at end of file +} From 519e6d7ffdf98d98d8a9390ac34074787bb1478f Mon Sep 17 00:00:00 2001 From: Luis Pignataro Date: Mon, 11 May 2020 16:12:06 -0300 Subject: [PATCH 019/130] Resolve some errors for maliming check --- .../Volo.Abp.Localization/Volo.Abp.Localization.csproj | 8 -------- .../Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj | 7 ------- framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj | 7 ------- .../Localization/TestResources/Base/CountryNames/es.json | 4 ++-- .../Localization/TestResources/Base/Validation/es.json | 6 +++--- .../Volo/Abp/Localization/TestResources/Source/es.json | 4 ++-- 6 files changed, 7 insertions(+), 29 deletions(-) diff --git a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj index c785de3f18..88c3bf0e81 100644 --- a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj +++ b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj @@ -28,12 +28,4 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj b/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj index f5272a2c31..5236bf0404 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj +++ b/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj @@ -23,11 +23,4 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - diff --git a/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj b/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj index e11709841d..acff7d004b 100644 --- a/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj +++ b/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj @@ -22,12 +22,5 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json index 625ca51545..13a6c2db61 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json @@ -1,7 +1,7 @@ -{ +{ "culture": "es", "texts": { - "USA": "Estados unidos de Am\u00E9rica", + "USA": "Estados unidos de América", "Brazil": "Brasil" } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json index df5bdbd8f7..a83091e2c8 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json @@ -1,7 +1,7 @@ -{ +{ "culture": "es", "texts": { - "ThisFieldIsRequired": "El campo no puede estar vac\u00EDo", - "MaxLenghtErrorMessage": "El campo puede tener un m\u00E1ximo de '{0}' caracteres" + "ThisFieldIsRequired": "El campo no puede estar vacío", + "MaxLenghtErrorMessage": "El campo puede tener un máximo de '{0}' caracteres" } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json index 6b45b36efd..f68ed052c3 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json @@ -1,10 +1,10 @@ -{ +{ "culture": "es", "texts": { "Hello {0}.": "Hola {0}.", "Car": "Auto", "CarPlural": "Autos", - "MaxLenghtErrorMessage": "El campo puede tener un m\u00E1ximo de '{0}' caracteres", + "MaxLenghtErrorMessage": "El campo puede tener un máximo de '{0}' caracteres", "Universe": "Universo", "FortyTwo": "Curenta y dos" } From 669592ac5c45ae8510144249e8c2b032e8db5c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 11 May 2020 22:54:33 +0300 Subject: [PATCH 020/130] Created samples index document --- ...tributed-Event-Bus-RabbitMQ-Integration.md | 3 ++ docs/en/Samples/Index.md | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md create mode 100644 docs/en/Samples/Index.md diff --git a/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md b/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md new file mode 100644 index 0000000000..dc3d5c172f --- /dev/null +++ b/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md @@ -0,0 +1,3 @@ +# Distributed Event Bus RabbitMQ Integration + +TODO \ No newline at end of file diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md new file mode 100644 index 0000000000..bddbd6eaae --- /dev/null +++ b/docs/en/Samples/Index.md @@ -0,0 +1,50 @@ +# Sample Applications + +Here, a list of official samples built with the ABP Framework. Most of these samples are located under the [abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub repository. + +### Microservice Demo + +A complete solution to demonstrate how to build systems based on the microservice architecture. + +* [The complete documentation for this sample](Microservice-Demo.md) +* [Source code](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo) +* [Microservice architecture document](../Microservice-Architecture.md) + +### Book Store + +A simple CRUD application to show basic principles of developing an application with the ABP Framework. The same sample was implemented with different technologies: + +* **Book Store: Razor Pages UI & Entity Framework Core** + + * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) + * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore) + +* **Book Store: Angular UI & MongoDB** + + * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) + * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +* **Book Store: Modular application (Razor Pages UI & EF Core)** + + * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) + +While there is no Razor Pages & MongoDB combination, you can check both documents to understand it since DB & UI selection don't effect each other. + +### Other Samples + +* **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) +* **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [Widget documentation](../UI/AspNetCore/Widgets.md) +* **RabbitMQ Event Bus Demo**: A solution consists of two applications communicating to each other via distributed events with RabbitMQ integration. + * [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) +* **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: + * [Azure Active Directory Authentication](../How-To/Azure-Active-Directory-Authentication-MVC.md) + * [Customize the Login Page](../How-To/Customize-Login-Page-MVC.md) + * [Customize the SignIn Manager](../How-To/Customize-SignIn-Manager.md) \ No newline at end of file From 3f2bc3ffcf94a85b478d94e9d3299b5c94264b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 11 May 2020 22:57:02 +0300 Subject: [PATCH 021/130] Added All Samples to the doc nav --- 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 c0515c18de..430fdf6b47 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -505,6 +505,10 @@ { "text": "Samples", "items": [ + { + "text": "All Samples", + "path": "Samples/Index.md" + }, { "text": "Microservice Demo", "path": "Samples/Microservice-Demo.md" From c61ef6c428de723483ac7c83576dfa0ee0e513a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 11 May 2020 23:35:44 +0300 Subject: [PATCH 022/130] Added empty samples --- docs/en/Samples/Index.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index bddbd6eaae..0c9ce68886 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -47,4 +47,10 @@ While there is no Razor Pages & MongoDB combination, you can check both document * Related "[How To](../How-To/Index.md)" documents: * [Azure Active Directory Authentication](../How-To/Azure-Active-Directory-Authentication-MVC.md) * [Customize the Login Page](../How-To/Customize-Login-Page-MVC.md) - * [Customize the SignIn Manager](../How-To/Customize-SignIn-Manager.md) \ No newline at end of file + * [Customize the SignIn Manager](../How-To/Customize-SignIn-Manager.md) +* **Empty ASP.NET Core Application**: The most basic ASP.NET Core application with the ABP Framework installed. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) + * [Documentation](../Getting-Started-AspNetCore-Application.md) +* **Empty Console Application**: The most basic console application with the ABP Framework installed. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) + * [Documentation](../Getting-Started-Console-Application.md) \ No newline at end of file From cae569f9c390ad53e7bb70236e50b328dbb4fe92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 12 May 2020 01:16:48 +0300 Subject: [PATCH 023/130] Resolved #3909: Deprecate UsePostgreSql() and rename it to UseNpgsql() for the Volo.Abp.EntityFrameworkCore.PostgreSql package. --- docs/cs/Entity-Framework-Core-PostgreSQL.md | 4 +-- docs/en/Entity-Framework-Core-PostgreSQL.md | 4 +-- .../Entity-Framework-Core-PostgreSQL.md | 4 +-- ...onfigurationContextPostgreSqlExtensions.cs | 8 ++++++ ...AbpDbContextOptionsPostgreSqlExtensions.cs | 27 +++++++++++++++++-- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/cs/Entity-Framework-Core-PostgreSQL.md b/docs/cs/Entity-Framework-Core-PostgreSQL.md index 3a39c7485d..e09880c37c 100644 --- a/docs/cs/Entity-Framework-Core-PostgreSQL.md +++ b/docs/cs/Entity-Framework-Core-PostgreSQL.md @@ -10,9 +10,9 @@ Projekt `.EntityFrameworkCore` v řešení závisí na NuGet balíku [Volo.Abp.E Najděte třídu ***YourProjectName*EntityFrameworkCoreModule** v projektu `.EntityFrameworkCore`, odstraňte `typeof(AbpEntityFrameworkCoreSqlServerModule)` z atributu `DependsOn`, přidejte `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (také nahraďte `using Volo.Abp.EntityFrameworkCore.SqlServer;` za `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). -## UsePostgreSql() +## UseNpgsql() -Najděte volání `UseSqlServer()` v *YourProjectName*EntityFrameworkCoreModule.cs uvnitř projektu `.EntityFrameworkCore` a nahraďte za `UsePostgreSql()`. +Najděte volání `UseSqlServer()` v *YourProjectName*EntityFrameworkCoreModule.cs uvnitř projektu `.EntityFrameworkCore` a nahraďte za `UseNpgsql()`. Najděte volání `UseSqlServer()` v *YourProjectName*MigrationsDbContextFactory.cs uvnitř projektu `.EntityFrameworkCore.DbMigrations` a nahraďte za `UseNpgsql()`. diff --git a/docs/en/Entity-Framework-Core-PostgreSQL.md b/docs/en/Entity-Framework-Core-PostgreSQL.md index 328ca4d416..4c6099b809 100644 --- a/docs/en/Entity-Framework-Core-PostgreSQL.md +++ b/docs/en/Entity-Framework-Core-PostgreSQL.md @@ -10,9 +10,9 @@ This document explains how to switch to the **PostgreSQL** database provider for Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). -## UsePostgreSql() +## UseNpgsql() -Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UsePostgreSql()`. +Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UseNpgsql()`. Find `UseSqlServer()` call in *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project and replace with `UseNpgsql()`. diff --git a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md index 08f8f405d1..350cff65b0 100644 --- a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md @@ -10,9 +10,9 @@ 在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). -## UsePostgreSql() +## UseNpgsql() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UsePostgreSql()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseNpgsql()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextPostgreSqlExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextPostgreSqlExtensions.cs index 1f86dfa480..9ab28dddcb 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextPostgreSqlExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextPostgreSqlExtensions.cs @@ -8,9 +8,17 @@ namespace Volo.Abp.EntityFrameworkCore { public static class AbpDbContextConfigurationContextPostgreSqlExtensions { + [Obsolete("Use 'UseNpgsql(...)' method instead. This will be removed in future versions.")] public static DbContextOptionsBuilder UsePostgreSql( [NotNull] this AbpDbContextConfigurationContext context, [CanBeNull] Action postgreSqlOptionsAction = null) + { + return context.UseNpgsql(postgreSqlOptionsAction); + } + + public static DbContextOptionsBuilder UseNpgsql( + [NotNull] this AbpDbContextConfigurationContext context, + [CanBeNull] Action postgreSqlOptionsAction = null) { if (context.ExistingConnection != null) { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsPostgreSqlExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsPostgreSqlExtensions.cs index 47967de15d..5fd6d0d6b9 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsPostgreSqlExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsPostgreSqlExtensions.cs @@ -6,16 +6,18 @@ namespace Volo.Abp.EntityFrameworkCore { public static class AbpDbContextOptionsPostgreSqlExtensions { + [Obsolete("Use 'UseNpgsql(...)' method instead. This will be removed in future versions.")] public static void UsePostgreSql( [NotNull] this AbpDbContextOptions options, [CanBeNull] Action postgreSqlOptionsAction = null) { options.Configure(context => { - context.UsePostgreSql(postgreSqlOptionsAction); + context.UseNpgsql(postgreSqlOptionsAction); }); } + [Obsolete("Use 'UseNpgsql(...)' method instead. This will be removed in future versions.")] public static void UsePostgreSql( [NotNull] this AbpDbContextOptions options, [CanBeNull] Action postgreSqlOptionsAction = null) @@ -23,7 +25,28 @@ namespace Volo.Abp.EntityFrameworkCore { options.Configure(context => { - context.UsePostgreSql(postgreSqlOptionsAction); + context.UseNpgsql(postgreSqlOptionsAction); + }); + } + + public static void UseNpgsql( + [NotNull] this AbpDbContextOptions options, + [CanBeNull] Action postgreSqlOptionsAction = null) + { + options.Configure(context => + { + context.UseNpgsql(postgreSqlOptionsAction); + }); + } + + public static void UseNpgsql( + [NotNull] this AbpDbContextOptions options, + [CanBeNull] Action postgreSqlOptionsAction = null) + where TDbContext : AbpDbContext + { + options.Configure(context => + { + context.UseNpgsql(postgreSqlOptionsAction); }); } } From 211a4c5039048c2e3973161f5f755ca6b78e03c9 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 01:30:15 +0300 Subject: [PATCH 024/130] 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 025/130] 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 026/130] 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 027/130] 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 029/130] 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 030/130] 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 e85498d597e809f3af1549fc95e17aa14f6094f9 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 14:01:56 +0300 Subject: [PATCH 031/130] refactor(ci): add force-publish to lerna version command --- npm/ng-packs/scripts/publish.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/scripts/publish.ts b/npm/ng-packs/scripts/publish.ts index 5570602813..9228a752ca 100644 --- a/npm/ng-packs/scripts/publish.ts +++ b/npm/ng-packs/scripts/publish.ts @@ -28,7 +28,15 @@ const publish = async () => { await execa( 'yarn', - ['lerna', 'version', program.nextVersion, '--yes', '--no-commit-hooks', '--skip-git'], + [ + 'lerna', + 'version', + program.nextVersion, + '--yes', + '--no-commit-hooks', + '--skip-git', + '--force-publish', + ], { stdout: 'inherit', cwd: '../' }, ); From 48ac4d2bb7010af56323746cee02dcd59d5156e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 12 May 2020 15:03:14 +0300 Subject: [PATCH 032/130] #3915: Check if in lock --- .../LocalizedTemplateContentReaderFactory.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs index b30e5771f4..092815ff6e 100644 --- a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs +++ b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -42,12 +41,18 @@ namespace Volo.Abp.TextTemplating.VirtualFiles } finally { - _lock.ExitWriteLock(); + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } } } finally { - _lock.ExitUpgradeableReadLock(); + if (_lock.IsUpgradeableReadLockHeld) + { + _lock.ExitUpgradeableReadLock(); + } } } From 7246b227bed0fcd93e083e4a0ce4bb5a937c7053 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 16:34:10 +0300 Subject: [PATCH 033/130] feat(core): add email property to current user type --- .../packages/core/src/lib/models/application-configuration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts b/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts index 0a4377f4d2..b52afe9faf 100644 --- a/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts +++ b/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts @@ -66,5 +66,6 @@ export namespace ApplicationConfiguration { id: string; tenantId: string; userName: string; + email: string; } } From 11a73493cee432ba7abab620d9969f4e76af5645 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 12 May 2020 16:58:32 +0300 Subject: [PATCH 034/130] test: fix testing erros --- .../packages/core/src/lib/tests/config-state.service.spec.ts | 1 + npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts index d019bda84b..17de9efb9e 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts @@ -122,6 +122,7 @@ const CONFIG_STATE_DATA = { id: null, tenantId: null, userName: null, + email: null, }, features: { values: {}, 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 3df3abc63e..aa3740218c 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 @@ -146,6 +146,7 @@ export const CONFIG_STATE_DATA = { id: null, tenantId: null, userName: null, + email: null, }, features: { values: {}, From 340e163b498ec14b137e73dd28eacd95a7bd1861 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 13 May 2020 00:15:25 +0800 Subject: [PATCH 035/130] Update document. --- ...Application-Modules-Overriding-Services.md | 19 +- .../Getting-Started-AspNetCore-Application.md | 15 +- ...ure-Active-Directory-Authentication-MVC.md | 24 +- docs/zh-Hans/Samples/Index.md | 56 +++ .../Tutorials/AspNetCore-Mvc/Part-II.md | 434 +----------------- docs/zh-Hans/docs-nav.json | 4 + 6 files changed, 104 insertions(+), 448 deletions(-) create mode 100644 docs/zh-Hans/Samples/Index.md diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md index caa448a14a..94a2120059 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md @@ -107,22 +107,25 @@ public class MyIdentityUserAppService : IdentityUserAppService public class MyIdentityUserManager : IdentityUserManager { public MyIdentityUserManager( - IdentityUserStore store, + IdentityUserStore store, + IIdentityRoleRepository roleRepository, + IIdentityUserRepository userRepository, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, ILogger logger, - ICancellationTokenProvider cancellationTokenProvider - ) : base( - store, + ICancellationTokenProvider cancellationTokenProvider) : + base(store, + roleRepository, + userRepository, optionsAccessor, passwordHasher, userValidators, - passwordValidators, + passwordValidators, keyNormalizer, errors, services, diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md index 9d195562fd..0334609bd3 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md @@ -30,10 +30,8 @@ ABP是一个模块化框架,它需要一个**启动 (根) 模块**继承自``Abp ````C# using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Volo.Abp; -using Volo.Abp.AspNetCore.Modularity; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Modularity; @@ -42,7 +40,8 @@ namespace BasicAspNetCoreApplication [DependsOn(typeof(AbpAspNetCoreMvcModule))] public class AppModule : AbpModule { - public override void OnApplicationInitialization(ApplicationInitializationContext context) + public override void OnApplicationInitialization( + ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); @@ -51,8 +50,14 @@ namespace BasicAspNetCoreApplication { app.UseDeveloperExceptionPage(); } + else + { + app.UseExceptionHandler("/Error"); + } - app.UseMvcWithDefaultRoute(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseConfiguredEndpoints(); } } } diff --git a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md index 8b282bbe56..34aab9fa46 100644 --- a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md +++ b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -157,6 +157,16 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); ```` +* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! + + * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 + + ````csharp + options.Scope.Add("email"); + ```` + + 到你的 openid 配置. + * Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! * 如果你在appsettings设置 **CallbackPath** 为: @@ -170,15 +180,17 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi 你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的域, 而不仅是 `/signin-azuread-oidc`. -* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! +* Help! 我一直得到 ***AADSTS700051: The response_type 'token' is not enabled for the application.*** 错误! - * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 + * 当你请求**token**(访问令牌)和**id_token**时没有在Azure门户应用程序启用访问令牌时会发生这个错误,只需勾选ID令牌顶部的**访问令牌**复选框即可同时请求令牌. - ````csharp - options.Scope.Add("email"); - ```` +* Help! 我一直得到 ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** 错误! - 到你的 openid 配置. + * 当你与 **id_token** 一起请求 **code**时,你需要在Azure门户的**证书和机密**菜单下添加**证书和机密**. 然后你需要添加openid配置选项: + + ````csharp + options.ClientSecret = "Value of your secret on azure portal"; + ```` * 如何**调试/监视**在映射之前获得的声明? diff --git a/docs/zh-Hans/Samples/Index.md b/docs/zh-Hans/Samples/Index.md new file mode 100644 index 0000000000..774d6467ef --- /dev/null +++ b/docs/zh-Hans/Samples/Index.md @@ -0,0 +1,56 @@ +# 示例应用 + +这些是ABP框架创建的官方示例. 这些示例大部分在[abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub 仓库. + +### 微服务示例 + +演示如何基于微服务体系结构构建系统的完整解决方案. + +* [示例的文档](Microservice-Demo.md) +* [源码](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo) +* [微服务架构文档](../Microservice-Architecture.md) + +### Book Store + +一个简单的CRUD应用程序,展示了使用ABP框架开发应用程序的基本原理. 使用不同的技术实现了相同的示例: + +* **Book Store: Razor Pages UI & Entity Framework Core** + + * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore) + +* **Book Store: Angular UI & MongoDB** + + * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +* **Book Store: Modular application (Razor Pages UI & EF Core)** + + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) + +如果没有Razor Pages & MongoDB 结合,但你可以检查两个文档来理解它,因为DB和UI不会互相影响. + +### 其他示例 + +* **Entity Framework 迁移**: 演示如何将应用程序拆分为多个数据库的解决方案. 每个数据库包含不同的模块. + * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [EF Core数据库迁移文档](../Entity-Framework-Core-Migrations.md) +* **Dashboard Demo**: 一个简单的应用程序,展示了如何在ASP.NET Core MVC UI中使用widget系统. + * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [Widget 文档](../UI/AspNetCore/Widgets.md) +* **RabbitMQ 事件总线 Demo**: 由两个通过RabbitMQ集成的分布式事件相互通信的应用程序组成的解决方案. + * [源码](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) + * [分布式事件总线文档](../Distributed-Event-Bus.md) + * [RabbitMQ 分布式事件总线集成文档](../Distributed-Event-Bus-RabbitMQ-Integration.md) +* **自定义认证**: 如何为ASP.NET Core MVC / Razor Pages应用程序自定义身份验证的解决方案. + * [源码](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization) + * 相关 "[How To](../How-To/Index.md)" 文档: + * [Azure Active Directory 认证](../How-To/Azure-Active-Directory-Authentication-MVC.md) + * [自定义登录页面](../How-To/Customize-Login-Page-MVC.md) + * [自定义 SignIn Manager](../How-To/Customize-SignIn-Manager.md) +* **空的ASP.NET Core应用程序**: 从基本的ASP.NET Core应用程序使用ABP框架. + * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) + * [文档](../Getting-Started-AspNetCore-Application.md) +* **空的控制台应用程序**: 从基本的控制台应用程序安装ABP框架. + * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) + * [文档](../Getting-Started-Console-Application.md) \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md index 2d4b344afa..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md @@ -1,432 +1,8 @@ -## ASP.NET Core MVC 教程 - 第二章 +# 教程 -### 关于本教程 +## 应用程序开发 -这是ASP.NET Core MVC教程系列的第二章. 查看其它章节 +* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [Angular UI](../Part-1?UI=NG) -* [Part I: 创建项目和书籍列表页面](Part-I.md) -* **Part II: 创建,编辑,删除书籍(本章)** -* [Part III: 集成测试](Part-III.md) - -你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**. - -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). - -### 新增 Book 实体 - -通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: - -![bookstore-create-dialog](images/bookstore-create-dialog-2.png) - -#### 新建 modal form - -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: - -![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png) - -##### CreateModal.cshtml.cs - -展开 `CreateModal.cshtml`,打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: - -````C# -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} -```` - -* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. -* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. -* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. - -##### CreateModal.cshtml - -打开 `CreateModal.cshtml` 文件并粘贴如下代码: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model Acme.BookStore.Web.Pages.Books.CreateModalModel -@{ - Layout = null; -} - - - - - - - - - -```` - -* 这个 modal 使用 `abp-dynamic-form` Tag Helper 根据 `CreateBookViewModel` 类自动构建了表单. - * `abp-model` 指定了 `Book` 属性为模型对象. - * `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发. - * `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). - -#### 添加 "New book" 按钮 - -打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : - -````html - - - -

@L["Books"]

-
- - - -
-
-```` - -如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: - -![bookstore-new-book-button](images/bookstore-new-book-button.png) - -打开 `Pages/books/index.js` 在datatable配置代码后面添加如下代码: - -````js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - -createModal.onResult(function () { - dataTable.ajax.reload(); -}); - -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -```` - -* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. - -现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. - -### 编辑更新已存在的 Book 实体 - -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: - -![bookstore-add-edit-dialog](images/bookstore-add-edit-dialog.png) - -#### EditModal.cshtml.cs - -展开 `EditModal.cshtml`,打开 `EditModal.cshtml.cs` 文件( `EditModalModel` 类) 并替换成以下代码: - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } - - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - var bookDto = await _bookAppService.GetAsync(Id); - Book = ObjectMapper.Map(bookDto); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } - } -} -```` - -* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. -* 在 `OnGetAsync` 方法中,将 `BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. -* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. - -#### BookDto到CreateUpdateBookDto对象映射 - -为了执行`BookDto`到`CreateUpdateBookDto`对象映射,请打开`Acme.BookStore.Web`项目中的`BookStoreWebAutoMapperProfile.cs`并更改它,如下所示: - -````csharp -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -* 刚刚添加了`CreateMap();`作为映射定义. - -#### EditModal.cshtml - -将 `EditModal.cshtml` 页面内容替换成如下代码: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Acme.BookStore.Web.Pages.Books -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@{ - Layout = null; -} - - - - - - - - - - -```` - -这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: - -* 它包含`id`属性的`abp-input`, 用于存储编辑书的id(它是隐藏的Input) -* 此页面指定的post地址是`Books/EditModal`, 并用文本 *Update* 作为 modal 标题. - -#### 为表格添加 "操作(Actions)" 下拉菜单 - -我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: - -![bookstore-books-table-actions](images/bookstore-books-table-actions.png) - -打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: - -````html - - - - @L["Actions"] - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - -```` - -* 只是为"Actions"增加了一个 `th` 标签. - -打开 `Pages/books/index.js` 并用以下内容进行替换: - -````js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -* 通过 `abp.localization.getResource('BookStore')` 可以在客户端使用服务器端定义的相同的本地化语言文本. -* 添加了一个名为 `createModal` 的新的 `ModalManager` 来打开创建用的 modal 对话框. -* 添加了一个名为 `editModal` 的新的 `ModalManager` 来打开编辑用的 modal 对话框. -* 在 `columnDefs` 起始处新增一列用于显示 "Actions" 下拉按钮. -* "New Book"动作只需调用`createModal.open`来打开创建对话框. -* "Edit" 操作只是简单调用 `editModal.open` 来打开编辑对话框. - -现在,你可以运行程序,通过编辑操作来更新任一个book实体. - -### 删除一个已有的Book实体 - -打开 `Pages/books/index.js` 文件,在 `rowAction` `items` 下新增一项: - -````js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -```` - -* `confirmMessage` 用来在实际执行 `action` 之前向用户进行确认. -* 通过javascript代理方法 `acme.bookStore.book.delete` 执行一个AJAX请求来删除一个book实体. -* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. - -最终的 `index.js` 文件内容如下所示: - -````js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -打开`Acme.BookStore.Domain.Shared`项目中的`en.json`并添加以下行: - -````json -"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", -"SuccessfullyDeleted": "Successfully deleted" -```` - -运行程序并尝试删除一个book实体. - -### 下一章 - -查看本教程的 [下一章](Part-III.md) . + \ No newline at end of file diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 9f7d69a784..d61aa63ce6 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -495,6 +495,10 @@ { "text": "示例", "items": [ + { + "text": "所有示例", + "path": "Samples/Index.md" + }, { "text": "微服务示例", "path": "Samples/Microservice-Demo.md" From 10733a68f4a9579305a4f4a9a382f2d4a49e0275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 12 May 2020 19:47:43 +0300 Subject: [PATCH 036/130] Initial SignalR-Integration document --- docs/en/SignalR-Integration.md | 102 ++++++++++++++++++++++++++++++ docs/en/images/signal-js-file.png | Bin 0 -> 57358 bytes 2 files changed, 102 insertions(+) create mode 100644 docs/en/SignalR-Integration.md create mode 100644 docs/en/images/signal-js-file.png diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md new file mode 100644 index 0000000000..cbfa165083 --- /dev/null +++ b/docs/en/SignalR-Integration.md @@ -0,0 +1,102 @@ +## SignalR Integration + +> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages that simplify the integration and usage. + +## Installation + +### Server Side + +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 your project (.csproj file) and type the following command: + +```bash +abp add-package Volo.Abp.AspNetCore.SignalR +``` + +> You typically want to add this package to the web or API layer of your application, depending on your architecture. + +#### Manual Installation + +If you want to manually install; + +1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project: + + ``` + Install-Package Volo.Abp.BackgroundJobs.HangFire + ``` + + Or use the Visual Studio NuGet package management UI to install it. + +2. Add the `AbpAspNetCoreSignalRModule` to the dependency list of your module: + +```csharp +[DependsOn( + //...other dependencies + typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +``` + +### Client Side + +Client side installation depends on your UI framework. + +#### ASP.NET Core MVC / Razor Pages UI + +Run the following command in the root folder of your web project: + +````bash +yarn add @abp/signalr +```` + +> This requires to [install yarn](https://yarnpkg.com/) if you haven't install before. + +This will add the `@abp/signalr` to the dependencies in the `package.json` of your project: + +````json +{ + ... + "dependencies": { + ... + "@abp/signalr": "~2.7.0" + } +} +```` + +Run the `gulp` in the root folder of your web project: + +````bash +gulp +```` + +This will copy the SignalR JavaScript files into your project: + +![signal-js-file](D:\Github\abp\docs\en\images\signal-js-file.png) + +Finally, add the following code to your page/view to include the `signalr.js` file + +````xml +@section scripts { + +} +```` + +It requires to add `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` to your page/view. + +> You could add the `signalr.js` file in a standard way. But using the `SignalRBrowserScriptContributor` has additional benefits. See the [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md) and [Bundling & Minification](UI/AspNetCore/Bundling-Minification.md) documents for details. + +That's all. you can use the [SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client) in your page. + +#### Other UI Frameworks / Clients + +Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) for other type of clients. + +## The ABP Framework Integration + +When you use the ABP Framework integration, you have some additional benefits. + diff --git a/docs/en/images/signal-js-file.png b/docs/en/images/signal-js-file.png new file mode 100644 index 0000000000000000000000000000000000000000..19fc6cefe3480204c2f0a31131cb50f1b2690d1d GIT binary patch literal 57358 zcmYhC1yCDbp!FAbErsBe;!vQtLveR^cXxMbp|}-yC~n2w-QC^Y9lreEn>X`iW+%x` zWGC6X_nh-P_m{k^7z!dDA^-p=65_&&005y2-X7s0!FwW+G26j+2=?L{P5^-P>AwvE zNKFR;00|%={6pD2{dCRDTS)~UxICyh&6nYtjy*5*n*4!lvs8+Vn@gpIfdP#}Vk@%x zGPm$V6P`O5(}on1l%&v{?RCLMC=t)X?bb>hK1?MPYO?wf%^)Lp&MPq#&zu!RmH4C6 zuv#5Q@TD1&4*SHlHxw2-Vh*Xbk`@3)U3ih2hU)aI)enwdCjEDWjg5nAYj0SS*we;1 zIh(K}_LAnq`FoO^JnAdG$;YkNabB9Xi6rYHfGYKlkwQYR4jVxyebb5(We<1W)h-J+v_3 z-v{gD^nu zkYRzHBzc9RP`kerW5Sv_%M1Yg5g_YS2>zEUt%?c2XZlZcS+w-&*X!JB2Qp;dooaoL zdgkr$b7HJ~N?J&CX=zT$2_X6Du+v|mdH2e`;XPvok;-7u9p;c5mItuag>yE+sHi-= z9!K-nv$3!+A#no3kdwaVaoyO7pc5!X{M}!%9s7&p1!wPq+(A_I)^`6Y97B~5=hW;fJ>wR?C z`EhFwyIlD_?OgPS{M@?G%UEo+S$O9S(;Q>ZBTB|$dkd+Xh+1IJXUMF}T}382``8P8 zAk7FVVsC5O4$=|bjPkqZuUy}#G+xni+E{m(a#Bc#564ZXy5R9T)Ld~WYnhfoLg9## zOJ%VbitB@(dTHPqWBhH$FMFz9!xZ-Yh|z?H*_^BH#R*_c8!NZ5Ide_G&+2x#%jNLy z&1YV~`cAI#XD&F`&+m!}Sx75dam^rShi>ju#%_Jx0{>&E7dDfzA{p1ORm|i%b?+u7_&~*H*^%l<_dbM#LhCxS&N-YP3UU94IUVyOFZzjJvIX`f4T8y? z!i-n7Tco=lMVHYkzc$Y;FgA8xDcwIf?`xjSWO|-Pme4c*)P{rMg!Fe4KixE!v-zx8 zN&^jesBZ^%8GsDAe5s^3?Hg(T*{r0QCP2#)atfH2EybyD8$X(><+NQdzJD#C0qjwK zOW|UU3_x^hr@B6>t615&Efo0PFKc-@1^<|7_^6kpyFpVJmh@T{nvdGp(7St!Ra~Vu zoGWE0B7=#_;B}I-JijWW@BYDOC>;- z#X=PvG{uHb{-E4UYy8>L%&r)8i&8Yg3S>*CGVzSU09@f{b|WR67fgY@L$%#|kUmx( z$6d&Ecb0w-^J{w)|NZt4olt0_Da=}Am~T(&c3H;hBs@XT?FQ23kTME_s0+4isi{(D z%k$w$u3LE%-;Y(Ttjg=O#FCBix?z`Tk%A2n4iBH|%ZYc8o$3w+>Ri}TM*qcYblXKwk#-eI^c9~Bat2>#M9}wdK|Sm0calGFVmWqFaQ5>Xhaceq zp(WGb1eecVS?M%ZA1`g$E=x~yJrwRCA)?|=*MWkN<%E$18S|im`02SO&E1_n8XIa( z_3{SftefpxrvVbr#8`|WGijHP+a;u7pn|v%_h1XT3cFPDhJBvM;iEW1x_Nars+T|TdPp;G^jsk z&CUfPnCP4qNJIuWjwwiFHj7ydP_zKaI8W7@xe=8}Coh=@$|TsC{1JqmzN9uNzN z8IaSwIj0nav`5VX?5{5!{R5@WZR|?R4vo_T6UJRcD5KKc}EKb5)5VVit|UAjll$ zm)sJVH6wn#AF-oBxs6q%c?~Pt%?S!L={T@>_l!S!PJKmP6GpS^@P}r%!yp(LTlAVX zyZU7G6&B@Q7)x+jQB^beaRcYHcZ(a@+wV;kwhuT6K=cj)jK~ZDDEYc{9d}!@It?=pJG=MZcbZcCpx{&#}MRofF`E zU$b)NF7$m_Q3BBvq?Twh#fz=$TVj zT?YV`E$hDn`D>y&Ec_)Gic97|O>HT@KO8WQFq7%$9G;{9BF25={i$#w6D#C)orNj= zaj!!!S!&}^e=qxcE%7&6jklFzsbT#t1t`c>!cJ{HZ8@4SLH>{&mFK5u z8BC;{T8^S6-C~BB*dXs$F+A_Cl46?D9!!D|@cI9Gw{dX<4FJ=V>)G{?K+hywX{3Sw zzh&xB2muAxhB4m#P(dgWg@S^D1$9skwU$;}{V|;gy!nN=%^vP>_Kz~yU{aZKm%v2~ zicIs;Qh{u+R zNd>jMYSHFFVqa5?MAdb>>unNBWX%d^1vUPHuMn{Mz@Kb;6FZ$>9mSn}X) z3a0XLQd750dxEnc6JU_)21=2K(8e7hsOZFL)q+S3?emlKa~NwIhXBIFC6n@=K*%0Q z1TT@}Eo>-%F5VVui$K2)^w=iXk0aPki3zqp6do0B$_Xs2dH&FV@*&G! zmj|puz{aG-yLVKJ`o#MqxV$lR?01xc^3^*E(kxon%6pKtiQkD zek1$P$v62Jj~B`!!soG`PtCb+ezufJu00KRu^po}uSZk6&1!DelN7P2`&?0z+HyK* zSKMb*6f8Wd!BPEu0M@<1J3mw8euIZ|%iL;e%S~Gyoby`$zYVsc-|&%%1LeAea6tX@ zju*S$j3$-JT#gI=C*5>j9+UNHEFd&T`i_&|TJi#RXtl4D2zN8K-F0t7kFd`FdqY)t z6N4fd>B#U%IW|5pdw_M+56AKIu$8%q3oz2?-{&>LmWgb!JD+)g04$bXr{0|!s+Yfu ztU6W7?S6r`FkgOOS+B4vCODzLW;oB$2lW~PQVb(Vm(PMiT^NRo$W~6Q+uH#K942*; zdeXP}wp^`zUluF|jpP*tp0|G`}Q<3SK zE*3CA`ubxRqRi2tukO#B=S$q}xdydx8`bc_(elsS4Yt^B$t&V`Agb}j>guS$QpJYk z3+*>9HlAf6B~K2WJw6L}bu)>kfPWdQ4GIVPG$jj=7Brj=`thpT>ITZ3!&t*$rR-&IEgs#2C+D_;th0U)N` zDI!}|H%4Qzwk@BP+ht7@pV;~{E++*o6o6v(V54pq>)0G^M+*&1mn>br$2DIM#3x_^ z*;+_^>yFq5h_a_Ei{xykrh88ha+l~1rO|d9sVXk<@1Ls9L4!7Nd%rNaw;YTbtBu@c z9h=(%9AmW7N^Lk_Ql{w`)F|P?Iu7ux*|cXvG9;Q_C zV48ljm?XvAI}HoX%jmP?x$!>spoLVYQ;lxlNO*e)OaJ%XrcPQ6DlivWlF%4G$f-yGxcfrmCgowRGr@NuRm?rc{V_hq(;M zOY8sTyWB%7e)fA%;qZ8NlqM{p-(l@z6sts-A>vtz_1zCdw*NCb<2DNAWTb&hMs3;iQKBV%W0=g!l|$Vh48ot95;b9*~3 zE$v`yOIJgqSQ+-|`I&`kgapHw-=X`#CReus-%j3V%@#^-Tk@gvfyMUnE?&WfsWqp z{uYTs3lXoG3h0C{8hr191V~yUCK8Uz$n1rGbbT2&L=`y>uZ{3xn?uZaBMn@wzS*Fk zCp?TIwgT(R)5Y)bf#)j?9ZgLkxm2MBpwKSH1d^Ybwcj+gv{sgv!K1gs_pMA-%FAm& z1m{XtG6gH#=Fz}urH``S-)r`=Knj~<H9_EQ@bu|VU%sd;xD8^=q$dtQcCJ}ugljW-QW9juAi={YY#|O zC`)He(Ocxw9#&_e6_i@Yspuf5{({6d8iUQXS7sLrE$plc^531Ze5phAN(Xd`Ml-`y zvr3fH`W!o8N_A1{7p#87@Lc6^*j+6dagSff%L^WS>FFQNq3HN~^s2T4`hFckj^^}R zhVlbMT#jB;7-Qi?q+Lm={u6Xci~xg zFRXJ=R$*!Y^ukQ~rF*!_TeUK(8#>>-_MIBd&ph4a8>gsBl zs+F%VL>2=)GBO=Cbz7Cz7es{L+0K36lJny>#ovg1TrCEc3-k!BVElbx%DMS;HT7N$ zn^Ei&Xe2itZYoGI{arsc0#kw%^MaO1kH41cYcukDIE(Ed%Mr0@~Z@+5k)BtyGjKFuMVb@05O z@d2{jc#n&L#RHjo{I6FyqpD-*^%5b(el66@ zFVYQ5<1T0#O_mmY$x^-?ONcQttvBPhzo7WICn14%#HsKUAsQ~i!uH4W#n?R8J)0?g zT{OAq5x(1yikFFglt8}6UHqXctM$||pvsOI@Hg6SRq27T(tW4u=<{YNy^C;>6WddX zb@=+mSwQQzz5r8Y_agH`Tl?32I6x5 zSz1a9WK)IjJd)!#pUmg-bDsnn;6cm-NQ>seuXQy^hqo(U;a>POj z8P6>SJHi^m%JvKVv5Qt|&hHQqnBWn9aoHa(3rrT6eSQq?_iVf-gzSNOT+>)G`>=E- z`8m=F8j9_CI#?6tp`ws1Etv+v{}S##aGO0Uc;k5-`*!EO@g=b6MpT4d*N5km*-VHSncCL@cQ+L{(~RJbp9Y$J2~3?LVwp`6e- z<|azDj;GO9gha1na-N*mb}<}eUN_66V{zNWLe|)e0*ry^ikOEMAtqut>SA^~<9b zely*I!m^b)LSj6Cgpv}jr|R*sadA;Sj?96;%EE%vtG=qGMT<1{kF;sIDn2GA0TGcd zcfqYy7VH-%pen|pbVVlF@?D<3=afzCsV{hZuII6D9$ zMv_eMJw(g`K!?WhuUU}*4*zmw=v1O4am38CwKaKtuxFWAexRCZQ&!$&iO=W^$kmU7 zSOrcuQ|f%zo@R28m2?WWX$U6xQ%9F`rvBomF8DmF%!I_&vw9tbErl`B?zdm=wr*xI zk5pdC7_OO~Kcd0_hj-7nx3?RM14!({#(oNPSFN21ss6x9!f6i3Aq@3E{B%JoM%ZnQgUYSxrO9csm#ap9Jvt& zWpED}1T0RjzoXHC8%0=7q`#{@a5h9k@dIEZ9PVTUi<(Qy$BN5p?U>CR;Rfs4_z1rbUK!geB$u#N*09>n{&x5phDw}7byz`7W$PM2^xba$ zA4X4@$lz8(<%iO$Lz@(hy3d*I_%&Jl(EfNvd}d~5aBWZM$x?D z;^HN9mKw0Qe-=C6SA_~B0fpY3ww|6eSE;-GX)y@hb_9hstwte$Wxa`84>C+dU-Qx8 zB$$hNcKr_yi)cqife+LN;isvWttjaEG8HIV7#fm>>ZCgPgAGGlq# zf8#}JrgW8#BZ`ue60Cvmt;eh_Eyo838Pk@G!|3J{Z?AUAql}ym#&ay5qxs((uX-=H zwzk^a+kwfcDTn=0+?F*|02E4{IJ~&Hn7J?E;4t*-6Qym>q(3Il7r5`KqseOpKIJ&w zkKTMX%o=B>mZtsYJmsq0NnMPu_LYzJ02goM1Fw^RTX4UMIxr6Ls; zd>{)On;|H4$6e06QTjt9mBZk#U@z}Od=vBAqJzpp2Oi(9QPSHp;J1GLfIr7R?;jV| zadobR(n0^sbeCldWmNq>|B$l zb%4CGvNMO@AlgM+;lj6-m??Lk#3N=`B_Y-{vH+L7XFeA2cpq_bcJ6i1E>K(>^bPw& zDVxse1PfTRrS%zN{1OKAYc;~JXCx8jU|{d0q$Q`1{w+AlM;7nj_LoIj?8LE~y^4O~ ziSC6{m6bpKCV-F6=G%}XY~)N_cNm$oSYb)_T|oAyFd=90hV6P$9R$7o@65-+S7;0N z5OXQV1)JqL)517xAN#D$S!yZ!vZBc;jJ2Dd57hmI7(kG60#}X)hm=L}chmSbihTtV z35tZbwP-k$>fPE0|W1pRiDPn`}sC@0MTOkdsH z9yDrAkM5pNN59EQ*Rhl+pWNr~?Hy>9v*k^>&y6tUw-prPUEmxd2vG7pp0753v^E_( zXY9S3b@;eCEOa+~bcRe1_BOVDx+s6APQGA1okPn?nbW*AX{LD(C?bwN^u0M-(3F@E zDdl$;*1KD;Ck>b_|JeL)!BFnemd|T=T%#gKZ|%L+Cm3<^HnM(O{PcW=!if!+MXC3) zt8H!{;r?D6QVz%GZe#WL| zHrTeS{#PaZ_nVO}!Pua*-*#}TGZN?Y`;h3ZU1fYS+b;BS^Z&TYf1U?$b%l0 z#Z$AtnH1=W1f+{^MDdAEN&a6KKqUJ2J|qCLbaW-;w}Lpd99&OUh-b9SZ|J;xf60x0 zuP?7aDqH&oHzOB@On4Lt1^mMy;8C9yo~~yeh_bR*(_<)>>zw=rbtr>>Iz_8vs6>I^!vU8E#f!xomCifNzLJ1R@-=@zLX#)^%#+yzV*|D#qa+ z{b#;*x@mnFpj|8G56z~-%0k7;$NsjN;0qe3$KeApT7vd~*LX9|t%LcbdGmaQj@RyB zY?>o$swo%uoQcUEFXyJLSyniLdgTGZxoxNN|kp|33a6E(Z~_ z28~&MPhYP-k0<0ju)Az0*`e9grWadTD!Zs(l+I{rf3o&Ef@%8vxos^lca*Gu4{90( z8MyS|RVqptGqw80dp%WZ#lRshcC^02`i~H=7Ov}vb@}`6K2zI`=RV38t7ZDfzV{E_ z){mm9-Y{MFcN@6%V+L2O)k9Cr`L(|c+*q(dTPKwr!-Ip3u@UNYNxy#^d4V-xt(lyR zOr&JN@c1}AE$zwS;h{Azb6E$tHX&lnW?1Uzj1?{2S+HH|hi+e?-|6Gj6FIPO!4EZ= zt~X7Hi9xda1@PDB$dbCnaza6Vi06U?_D{1`Cn~p3Q&4M;tx-Dk64E;XKt6rsu(2)0WJ6fp8=BorIIA-~#)j+nL7K{o~aq zc6W^Up@h^_&-bsqLI_lX_h>mnke}hh<+00An6EJCsMn7ME#`7g*YQvZ6s)f~dWE0> z#|?W7jh8KaepOc}fZ%Imp zeO1BMBxZx}$^8A3TpQ_z+BB>ogP+dxM4BACo+C2=*w|dj@t^57UmG>depSth7oF}wuUT9jB^)0^oh|;-&s);qOEMIZhU$GmLX2>ro&C?%jg6(a>>%+AS%oBn z8Wu!$`L{b1yu3N9Yorg{T#u)hJWxNx^z`gvHfyHVFty{=eWDZqP#L+1w58+{@<19p zdzF{q0|3sGXUp+2Uh{7W^XgdSVGMVVmv9d%An(H5+Hz)w z2hIRsT_3jh4ooB^00dSv=kiUN`D#7g7D8bu;T)$}Sg2(Y{TI!@gvW3g=1@7>HQ%=o z_M+G=_-9U59oAiNRq%ZpHeB;~_6^;}o;s|)Xi#XGW&n-knVZjKvv8mG8|sYd;NyW{ zEf%)V8myPwOBwIrBIoRdR=U8taY1J#2b?Ig=U%7@)qtN*t`Xyb4MTK=VfA})i)$G~ z52KlxS%D&Tq9k>ANyUFRQ@JvO>IwzDP}pkq)14h9MMcLMw9K#YhMS!MLqihl>+4{n z&XNj#Wl2d%nimfC3H^LBAtD@H*g>Wj{g*FW`xJ@&jC6E#*4@EZ5D*aB^lFt0#60d* zRaG&$RM0;>>+Qg?H|@#J#4qjh=j#y$6e6BS3RJkCecNAz;&imN<*`^n$YA+A5M_dg z3=3RdUdq!XT084kSCm`~;Gq(7-JaG?ELzv6r>B<|_zIb4!tWqJ{4kHes0R5T6;P7K z5ApEwdhvZAgYy8g9!9EgPcJW-OdhZrXJeU78^igAey%~~Q*kxDwlteR{WgM>rya_B zKjglCbK$*EVuB-8^-Sbi;QXuMk1%Npi^Vn^v9~!>is^WI!JMU)l~tvDrlYe^*mxgi z(bw3oARIf%Qo+bJI*aSOyI&CzJ6G;N=a&7hqgsaS$etru^noxiau!Z*Zjk6DbO;}> z)$Bw$A!UBGp6+SB%^O_z4P-(m5OW0Mc+HaU+DI@>x$5Ui!4)P+)iT zvb>>xXzS<{$z|DAtM58;hUao+R6OL*u?d}5`=mw9L2kMvB~G+fF+OZ9q}g6uwf!V< zo;);Y{R%e|UJmp(x0bs!-22ka*PXmf0sxi>SOZIt8M?VOj9?3XZ*HH|%!p`EV`ec~ zsAtxOSl0^cHfBnZF6{6XM&GSWik&hqD=lr&YzR@VQ(c6KZzJ%>VYdp@8tHOfNVy8E z2&(j>U^jEI=DjYJ16}_=lxW`u$rOw>}b}I zr#5`cS?%k`^Du1%tNVEDV%Kr}x`o7V!uf!IGtXS>=2v6ICt+SLuJQ4Ss-&+gS{;0vMJ)vA@<2x5olS)J04yv*m# z6&yX^BPANg{!~tdtk!=#dbIc%Se|PaY!NPz&%R}RTwI>uDqR>_k5D|T}>K-~I#9{ta(`yWP)UdQru864xh-J3_+ zO!icOA_l;?_6?uidVx{DHo^d0U7N#}FtM?T8|{3~lvOkvUYsrdie5@_XOF#NtiNXI zvtj9ezhvfizWVg`u%WlpP5F<4c6ALmx)IelW_^YgjMy!}1;8=v}w3Zff#47|$puFkz7~0!hvw zizuiKsoP8uEkExR8YO3t8kmPuz|#v%gkk=$$FerXM|njG&w-a+p4HJzEgY5GZGD;Z=R`1(mDle-&2j!ZK9>RlpBONTO z4wUEB=Tttpcy!n!&vzZ?>uoQZvRxt)5p{+BEJxJgu&Ja-%tPZ8RVjb~MSLVKGX?dL zwpUJjyTL+*PG286nFJ}kpaNN^jJjOGF_pz`6y826oF)X|UL($KC6lra?Y${Mux30y zIg3CwKuLz38jg4Rt-xj9xLZ*{)42UKyJ*e49`Yd!37dxVG!wIE{@9dVP7G8ok|W9d4fI==c<8y5^->J8b!z*5fL#Ffwb_G33qO8PbKvn z>G?8$ZdOxGVQTWnG=My(=~*I7tNi@j$mKXu_+C%hl4%NP3l#}p`Yku>ZG9r!(Y&{N zCNFIz1dxxLKY5sphwHmFX%AOt4iBr~mnY2SmsHYK+BJYX9HIRsveGne4s0&7^~HCo z`^(qOu4yV+e#iNlm$wg&ZVqv**rthdr8b>6D$gw=Itn&@&3bHBbf+~`^W`oBW*&m* z#XB#NKVL6PoAx!-#jRXMXDgNRRH3MZ6qvtkRn9cZS_W-c z=1Jm*bUnfD8e9Mr0$1S!#b9m^s1o4Amr`k_Qnh=hQ2?-(GcYhmxD>~^s(g~W&~B!+x|=JBVy3Hi%rFiI{&z&s6dKqy8=IMlH~91X{QUX~iH3%TB{DKHauGa9;(1kCnU3d#RGIJ)($}o= z+&(uMKYv%Zpc@e#-6HrMwvQ^I%J@QbB$_#Yj(`nIo}dpt&=jl{PfqY9rAE-94M2y- z*Sy{wW7cd?{^Dc=yq?mww%}R;uFQ;sL$$4~tu}qbK?5&i;K|X6nQi$w<08aPGj*%L zO7`>;&`8vt$svRZ`p3rdM;wR>nqH|cs2(yeu#cLZ5VOvACs6VdN=q^oINjbEKC7^u z2C`A<5vhw>G@@Uc;We9)`D~}_Lq+SRBsrSlJca?F?D=|m-^pVYU3vfL;JgzGh^Vh? z)(E-@)?IfvXUZI>?997bWwOUU*_eGWLebULrB$mC7eD+sxN=Q?;u~lpNQ(_)(l()E zaE<|l;3J5BpreNM>|9Aq%PL@rD3D2{N}IN|YZc9$ID2MPtXS49pSi}7QHL|7$v}63 z&)!$0Ji#K3QodByP8gt(K44`1b$e(?fdH^a^t(@7=$8RCJl7>ERDk)wl+rs`n_@zx zLv)g#2Rvl?xi6-Fw!;3sy9EGwIBhRaCr*7pCq}f_Yh>RJq)B!Bx}VqB_JL~I%l?`| z4iVFbFOpg-pOLb#@oDxzria6L{$nPz!uzioUG;YNMgO0J0iSd|U7jw*8R+G9i81N6 z9M|3wGBb%9_9M-zEAkn%z%jYImluOxyZ2@%gxVUt#FcO>M0`lV+wZ#q_;7^J0ipef zQcj#U^YU1@`1n$B#x^$P@$t$uiQ#>Q;kf_^4(^xoF%@>i>)Ts`q=~w^x|Gz2$k6^7 zm(Kdp^bi6<@77snxH+{!TtB6k?L-|kEXQP>#YWN7EivdXGRNF>EDM>g-8PvREozdv z;0v5;248|(jluJXgW))R{2DLcGQ!>fvb;c{0v=JYsqJ5=N^!WRBCUOfybG@Z!-G-X zo1~Q^xsBy?qr52_%9D?W<-BrMmjUDeDI6K@`|-dG6H~;?+6=jlx7o73313I|gNg*s zuQ$=>{65Xk*zYLnWg~|pli=~2^i7LRU!XfAOkS5>l*E4vUY5jc#=i%ulB5$J=(TNV zsH=m4h2`RI8QgQ!)6+u@ON(OyQ=@5?Z?WRu-rmMcMdRbBrE{S>!LmiQOZ1~n!+&V4 zXrL7g+Plx9z64~1p=Aq^nv645sQqj!$e)^buJ0nk-;_x==zg8b)4J&v41!qBeI=xI zRV!gRiyX}d;f(txixgvR4!g_ZEF*aCMusY$HT{mAYo&xZ*PA;oVvZg zM-34KTT})H2Fjs`nxZMzB3b8gcN!KJ4wiZHvoHqUH$$Vhwjl`L0&d^BK=PN(qobQ= zxFilL6p@K^o}2M(gxD#X0)ZB`7FDZQLjIKu4whk3kgCt)$i@*hZMV^DMz;-2(Mq#4 z50Exz_&p5((20_nEKdDo*wyJ=D>`apmGuunqoOw0$z z|GTx%Pb1sZ{*e*QW3@yMB>A~y$&Me80}3rJFQ2ijtFHe3a!*LbQ28?|iVZI^Bt(QN zyuYtcRc#?DF;R*v&i5@hEiH|fhQ=!;b5bp#R??EF(bxry$=;AaW}MArcJ6`m(#yOegB&a0Ps&i-=v;> zLDzU&6yItuXm8Lan6&06(>7QteWKRiaF@$r{iC9yz|P_2j03W5TqwSZ%brx|@=sp=rZFc?h{!Gaa!*46!GAO)zT~-k7i$DOD5j(Ad@loo2(ZGbBS_X zcb2JIla|f86vXM1UWo-9ghhHzie>rzZESA6?3`j6NI@Imz62hx4R(~cqA#dfQz4d* zS2~vh7Nkrx%Cs(AH5Tku;Dx@L5>Br@J@Ch)8kht6AkosfSTbB(TnzNhXp+r?gN6Bd zY%DCYM6Ttt`uu#dU&3)oNvgWKvTUuBM^VzyUM3AiI|2-xc(a()H}yUo((!HvB`-^|uwr-+;G= zKx_1-vZ`nrgny1oBFr~bga~3;+;kpN!PVEb2mE0zWc|71WUOJh2~QG}!cqDHool9NBY>{udT05_#1%0LoG$k$Jw;=!}z+S=Ok%8CdnM$`^i zUDVaN3`7x@R#tW_|7Zm3sPq;}F!Bn|g$(E%lQ{>Y#m&v1BtK<~tnBQ-;GPLj#(EeQ z`($}dB_{{2hY=LI#eoWtNz`C~7wn<>bjUv!3ZVxV9$9hHMfVx%j1w{ob%7Hqng6Xr zHX?Cju~&dG+bP(tdm-ZEd2XEr>)^pzlR3N2xP}`TL7(V$hK7Qr#LhuDgY;FyB7+7* zSLgan$*QH2-fTqQb3=8`79A$RGg5l~od()vLES3-03@Id`Krxf#Ws65io@U57frc)x1NYT;sUnGtd9aq{!_-e*2df2NEfuI}`L`m6jZPUH-!=10es5{U7=gj{p~wCeQKYOOnKOZR!5t#MQo9wNKp zgdwRUx~-chHL6e&e-(lOaaz(?CVzRDmz)+PfCKt?k7nQLmwpJ8eB1A@@>pxL295VY zB)vj7ANMitOH#zM5ZT%ev%4YGiVWPOH}}ewI&U7HZOhc=mwC*T+z#cgtBHTt9=p}V z-mk1otKUDn*@M}k_3PZlPm&Oy?0yQVeW-t|>>z^5q4$@1&_78L0!|Uu zeOQN3l@ERXM9IeWx?wiFEbjc|(Bx9skX>n}MXziwwmVGs$^I(trAF0;=xWn?p`Osr z+pY8Gh zAU=`0c)^frn{4a;d16iCaZ^4dV%Kw>ytLx$S7seCMd0~VDD3d3LABtrX;J!HPms2V z{@iS}*+iACk(*`dkDNyT=mSd)_*2WKRWWwhC3e>M>Yt%N_|KWJQ-i1E1_3*dR zMciF9DhUbM*sP_p+UZzXgjOrC^c^fKCgn1zh2~nM9&@4{NS9Yvt9-(=U;1xdY?Zo* ztjR;I-2NHUI8x=~V@$NL<4=w7AWq0eSQ-?5dOrTHTq2==0k`Qi!l zOJgUaDs(kukaMP#g~-zN=lvi)DB<|=8d#>bm+m3_Ch^z2-QJj z#>OSGc_o(m=Ai~WCc=M{g{fNgcM7Am7*f7S9821KN~gTO{*?)?Yatf^ofT@t5^J7? zWfFFr>}&y7kk}RO-I}cr@)^f?6HlKxIBq~j$?K!2c2yNUykPXvvgQjG{>VMxh@Kkr z7dXn9vvSr4!3r~Z&*uHjD=jZ2d~h~>%^l+X5Sfz!ki$D8CwTN5t^23R+u80ah}St+ z^q~RDiAM({Rat!B%e*X9Hn%09^E(sQGSt)cOi$vmRV$6%>h?mO7JFOE`QjL{t;rhH zIe3_Wy{`fs8qw|5mGAA87$PDfxbWinO>#@SrWA$y9WK}QpTt_qCRr*vIy@x%-pQY@ zpzm(QSzI?GdeKY5<6jKg|HxEUyPiM4)}4##dK$)^&t*kpMA?36CxQ%EWarRUQCOL^ zfQ;%K8IkJl3CQ8nI$3ER1vU>cHh-IQnTyFG*yOK5@(>VOT*RQ8_!!wFf4;fAjzxqA z4$r~S#Eb)(Y$m;-icdx?Gzl5RdQJVlXi!MEJsiK^Kw&dQL?mXT`%{x7Fj!I8kDY-4 z#$lc>G`-pcR9aR}L4_;JPI_|GXn^%|{cyi5Bkat=|8)TXAVB$u;+$oXM?2j=XCF|M!f|M2Vy%;|oaA3YTbenURp9n~6>K~%Z*(NB;qW7v?h%-yu zYFL#IOX`8-2_WI%O8ZCPdQth+|#GFKYHR} zg`pdd=I}C_i;HJi47*MuM4!|_nSLD(P88HqvN&!jXL(uA6U;qrU(fYkM|b^)X}EIz z@{w7OWbzBkiQ>_`-5qn)Nt|yE`9G@j`_pSZ`!?NlwHJLo)!I-S&)@E z?>WXb#*8-I z8#%iNe1yubM=I1(*E$ep3?7CFO+i25QoKJoWIS9mlK7_%xg0MWK?a|k5$Tv%R;sSWz zPmftkrKG22FDCK8N;z$1Q$iB5kowm4a|!Qw5qF73`*B6%$&p)}0maMC>#P8^Z@oV_ ztK2V3JVxjR*HM5E*08QFmd_40V>j+0hpTS?6JzZrqB_9f1Y^@X)wCwo-;wcSb0UNA zVccy%wa~IBrw0#d0s?vz}r&TlQ1N512fVl=NJTWLB zd(kXGZN6x2lsjGs-}xSix26Kg|25k#ApPHCB&0$D6FGGx`Q_foX>q#rESQFE8JLZ& zX&Y$K22mZ~c4_|z!{z(CJQ4M2*dPKl;T-t)04e;Hqly1ABq2c z+-0nId}dDr)-4#IGU_RFTE7fY#Nch5qI?&tFYx-6_2%5Za2djXdvZW~_R!_JQxqz$ zh2Cymc=C&awx}dX2+D;pCi*SQapB)IK>?M*4T`h-+XRrGm2{d3JR{j zzyv$s;t%v;=24WeD;RmvA_KpdK@cAZC#dM@{Tr@A5sT=%tDD6tH`sDadt4uM;S`i8 z9XMjL(qO%qBO0=TA)6}4TM=v&As1+NjH~m#?28<9xiR#YoSgdaOS4e3Ych;VA#TU% z`3!itXC($Vwr9UL^2Akj+g0w4(1Ke%6X%xKp(86O!Ti5qE~T7A(9Lfx)GkawbMA=_ zJ&B*c**wl8!kfaI>ng?0lM>G_CRYu2Q~^oVY6T_VYVOCJ zC>fm))J<(Zt=@oE5YE42hW!p(s7wO$rQ5LUMarWvW<)RSQ#VGVd|hjfibcE$ z=#D`f6~Sji1%gQIR>qc*e+^6!63uF zd0F_>bR_P1;@=p{nZz|o2Ev<}qrU`-R)Co8Xre}6DD8X3ZvX&)oSv-Y`+JOwNn;Ku z>guXgao|oIw2#x9U(^=Yix529`9@bk8Ry+CqHXKV z%*TKOzu?41d^$S16_P(EnAz6bZ)?p7pwrWy$a~dc-Eukgrg&WbOhm(h+G*yUYO3IQ zd!xz2pIZG-zDDD^1z5lzi;yAOcz%JF_~R-b+C^2hp;J*?8hye0^;O&Wu#!L)d{kPn z<#>1k9hja|S7YHok&)+j^X}g{mJzuA>bq66*}2N6}nq-|AKi2SN1wZ)2YxZfr6>H zrPlWH-dV(yr`dM@==hT1(>Wn(r)1`b&D%$#l7zpovYw_b3j_#f($rS0WJ?1BBrXY%C47$*mBY%q3Mc^kxg?|5g zGROSzm)ycwGtCc93qKQwC26`xer~QO57tySQe<_K5(L2zMJKw_WQxKD*4Ar0+b;$L z7sAmK1?T7FSQ2RAgPb=8AsGlHq)4cvnqcIR#>j$aW`ly6Zj&8xV~JJ~t^iU4dZDk) zP*|jrrP5-lyauA!NE4sn^ohZSeZi)trXa>eFp|~8+UDu#PIwX>Y}Ud`ks%yJ;z`&i zY%ni09Y1^*y=@15F0rH(MIJW6NwDr3`5L?)?1ls(7eLD#&+= zG1!xZ7j|};*D2u-Cv!=D6jh>(A)2-#Ev@xk#@E*>>V+ak=zOygxwQUt%RvPQ+ut3# zG+U^P;x51bJYV6KIHWq4=K%MwlfAnI&4yNT#6Cs#+BfnmxtvmBM1{&694Iv#K{Fsz z$au8>czq9*@?e2U^K!0-xsdkoa^rRleoSxH)l4JSD ziHa3_#KfQi05p_J4bUhQbILMR^|e=6TBI2l5W$D%%4Z4Of_R9gbv<`fS$w!S^lx}W z-ZrSi3IX}NHb@0S|YSuhN-T~SxHsC@@DIkmaci!)15U|=|9Qw({^+|zs* zu!&r(n*KRQubR7=bAHL&JIshBy)EDu30(j9VyVP3yAV{BL8YfVNf>FuaX#R9)6 z5nsa9U_#3bQ-M-gacyz<`xca#>beXG1Br_M>B&i~K#BbK#!m5kdmJL*MT8o)QL$PvP5L2vi&8zKCMqGUXh4-|l?Ne}GFRGV zMnW)zG45G`4|FfRl>LbNe(R2(X8!wrQek>}x*p8T*!b3d@mJwG>fp@TE9E_ry@;sj zE$p4f|1O5eQ2(uo7M9sLIW+*k=ruz%kGb$G?{vGQMhOQbBqSylmL*3Tb;t+N5)T`X z$`)y@0c{1llKLh6*vW)*fOZ-VIQ_Z8-P!W(d6PLpP;V6Dg8n4o`s{j7k6PeOx}YSs z;N0fORBB)K%&Xk(j$uIsG-6+&Y$_QhJg5q%G{Y7qJYYf7?7NPN-H@kNCIvsD5%La&a!hCeaztaOxYjiu# zj(#bmJs3C42!RO+3-=60rtdjEzdhfAa*F#r;kl5Fe}H>Tedj zuy5uyy(Y5CKYohkJE0z}m?F;Wv4|g{*(1Wmfz9c!xg6=)DsOiN7It7BzyhUf1cJmJ z^O)oDNCfNxOPa8lvEKL7LBEx>H|?tA(ny;eGH)hdV^jZqL*C4%$T(>K`Ue3(dWM3{Kxq zrZ1#M!hBwkupDqzTWW@)H}c(_H3`&OL&(U;K<3Th(9lFWH!~@z!TSpmXd8=0pvr&{ z(+#NuC;#U!DgTqCv93u`F=8$mOzLS&^NYX(&xc7DbX@L*S;C<@?r>6kvR=nf^chyJ zB(K|*MS}c_)5jaDD%`&T-h=Gatj=Yr+t-f{_4raz{Y=iD~_ zqWEyUOUBRcMygv^X>kwE`Mjpp59UUl_T;hq(6#D|zI>n4C_Ndm=GxJ?vwAO&&W<*m z%+0*!nlZo{b3m7r6*pWzqpukJUHK=_-A}A3v(F=+z2~n#Qi- z6z7eusbjYEx_wp@`y^;4DX?S0*J z2i`n~+C(zx+so7;(T+d2!9CZU+2mq1S*3wdV_xQAI=ER^p3j z1qan=nCedq>g;4RMrw1`umSlBh!F18ulQpA6tE!76LLtwe~_6mAX_LdI%V{j&dK-L z(bQ96=?brI2pL(k-q&1lVpl!{2Fd$=eQ{0RZnNvsu64t7ZkrieqZlIqd|p9+(GW-K zR>YuyD7?0|7W8i!92{(<*HSDQ4OLIvPtWj999Ry&<0);#EJhwvp-*hP#{Wcrb&C@g zH#F3=6=(fs;jcT})`#xqNCrQHUpnJhn=hdIj<3xKYsQS?fAjh*PAb)qle)>Vtl)Ti zFcKM|uJ(Kvkq{2l^L7lZ)UHFkg$4X}awx$M*e1q)V(H)x9H^~pu;=T@kWhD+ct=Et zL`nN(Q(LiW1{p*0i+aXl?KG*S9?k z_ix^V>3M!Uz9=vp! z^8;=~^@(H3J%#~DBz@J?+?_PxrHW|{gA!SfXZ!-VN(smiKKa0CuAsbbi8?ow z#{)h$&oD9ofBBa$OJTzqP+xJo`soKFAeLtQ12pU9UTX$xA8tUjy`b-h9V-2SfOf&z z7Q$`dmcJY;hXznby+fi-nCFu9Tb)NLSHEk{C)T-16GS5t@HUivBXMp}-$7`F9Lr_W zDXETJtJ#Sx7VI)#3aPp7 z%YOw27utgMZOfp>3twqRIT$a^>2`x@B6xg!e1z!==>Pec5fT+O2BX(qyke+nCxEcD zMH3UfB{VLW0kIw@r>7HUPD9vZ-K71IjN6*SG1eT4kKupdZ9^(@@^d^2LEG&gVt(lg zH4;JJUJIKB*kjtOYGYZF6H^w^f?Tm0SwzBqn-foAo5EIyOLC+Mrzp@#Ch%9+^Bs0Kmn? z#dz?~KdT0iE6kpn`PU#Rw^(8jfhPRqY%rB&TD^N090>OhjjqF8TH4Sf55wL?q5jFdB)Y1i8?l&D2(NGB0I$BLa;T{V+mS3n|tUiBa-9 zEIY6ifoJ!?N1VLMx2890sT-VH%{RJ*&uS8rb3|M4t7qrs$s?(-h+ZzKjefGT$3j=w zA@W4?oM!0fW@aDELL#3$J`2_A&{Po_eO}KM_pJ9Wp#;n;Lz3yN?%^~T`FUyx^YqO0 zwZ{1#rI@0Lh)A#7rVqEpbT%kO3kqHg4Jh)#Ic*UX_xDUI1l~}(e_G~8A_S2F{(L0Q zC%;{VNvcvVY3oU)BCCF+b^Ua1e(lmw&Fnwy!$Nqob@zK6p6QKofQ;7jWo)+d3hw}C{m;;w69D_c!9L^k>1*)Mc>4}Sio3;hqnaZA;;Z< z)tCHp5IZS!fDh@;G_)fFrq#{#<=m&s4d4R$$o^8=WDoD$J-r6qgTqX1ba;(@k0XYF zkwH+(yEp*UH@bdMV*?h}c13$0&?pZMsMexhh^#-+(J#Z^J;PKi8*E6_&E_kbWh>R) zQS`q7+0Lqqx#-S7che>DnWBo$-XTuEp;2k++V8eyY^_@Wv6F*=p*JZy6)C*SBy0o$ z@f=i!2IhbAwwM2htuaHpO5pvhf2Z;t`r1*sYIA}EEkU_xc#=s3&^0^DXop5(X#vY< zi<>WD2$6S$OJ`Xe(EVc#06E4}q{;0G&C;BJe?Kb=!(1Yifdx%en4GO%YImk+Myl$+ z#W-i&S@!(4EC>{#c;|h!mY&}THhm(Zbhq&X^1hEdcCJeVk6DAB+DJgR0gHGfKFF+i zUU%EX!ot%0BZ>;S9sUFk#_SDSc3Jku)aQ<_{ip9#$X>GT$^nCX-5I_^W~q5V=I}*| zMTqsYXBKCQknwv0e4WKzIf@zL>9`1L_f_&ixwltyl~%!AwGWO@D~RXh|F?%EWsyLa zIN0q3?`=O4j#{IX!}a7G9s}zTDQ!vyo2QniVhJY%-C48OkysC@R@ zZ}MEYKW)XwpDdnx-8@_2vEa*Bo0lWZq8m-gx27#8SJ@%Xz)%ygcuaP8n)blI=a72$ z@?MiHKn@UkwYR(WoI0|`f5#Ih-_T1`cOqD!+=kV+qwXGkaZ|=yuO+18IT2?Uqz)F1mqHa#w!YOY!xNf= z`wN#b(TrO1)mGl=aTe)UGnzchr?BVNJS%wJo%S*HfF)`cIe{`gumLn>m9`1ce`#Zu zBVgn+2}sFIHss&_N;n~p5%sp);D4yV>FPiN3aUkyC)9$yL!gq|X49j&E`GLMWQ}9xe>|^mFM$K&Q|vn9#U<=gIyw}9f?Ble%}*Ys zTxvMwy8+`n*O>?74v-AVhC6>Sk96@{(D4{)ZbYA zy)T*-N*rSV`{pM`pzx6k3i~*Hj03FNfTeOI5gOx4Y-a$C;d zMT?wCku8wXnw`#)8!hqL3m29@@vjdLe#x2V)9mypDF$$${~odMk4x#JH%1nu5#0%6 zm9^3>19fM3@?8+jc2f0`OcWPfZN|8a(Z1FjYs z*i1Q6;qtbtEQzR?j0|5ExHP>xGh!<$ndsRSFoE9w7Q$HLEHv`83%!HHh(~y-QDPeD zsyZG65o0|1MCe56`~Bo}5PrK>X^|nVtSMsHTym-rDUY&(E}dsVSaF2PBZ9VJK|GPv z7B;fBRI5d)MSr#)6g|il1Nb}f+2nB4waV-3QJoO&4f5(+0Duaai`YpmJg3kyZaQ-< zg*3YBnWTUVrP=$Z&(h=7L>^tovQ16nU-=I5*gy2EzWfz1fd{sS3uB(1WfH)xUUwn8%Al>^Ae=nqoxw6y(e(26T|9`lmne zSuCYZc8i@+B##R?#SzQYJrMNZMgjUe(xNrVHN!APu|S{kr%0_HeZ&BtS97;Kirm zxe4p2My~LkUiMYpAe9@f_cv*%mia;n7L{%v28pv%KsP zRsH6PnEixph2Aa;7SvyWAr1=dT)td`H=bM;->3@&wmU$$; z_)BDMBk@IHEwZRj@|nD(w`VQk64n(z+A$~K2v}>Pae$|XG#UjGy8o92VCmKeb<}XG zTG@qm(PKa)SMKVa9{zKe53H%xnTVD72^0^vVYnox@Oh~i;~N}?3ZoIriH1W^jl(%4 z0;_hzcZ+x>>haRnB$-u}9Jjp{!FVL{4t1trKLgraeOVAKWy8MS&0ikdGwSn>uYO@Z z*0M@`-?Q}jZ36(G!#TcD(dn}VmIy1O`z5H3Xx!R=N9x;2i@st??W^Krrb#rJ26xs2DtiuYz^~@!GG#@^`)k4Gj_v|?n37X4>CS>D^)pVVA#8h&l0o{#{sXUgsSwtbZ zF-L*vm4((56hWlrlYM#_)2rGU824U$oR%}BU!+l~jOTI~|7mIkl@0RwR*+Y|w_Eb9W$|oC<6DA1* zBp>@KxjnrbDB~Ts-LzGRdrE~bf7319ak^gEwNa?0bs-8nK7#qjLgx#J#_G=LrC)9q z3H-NuAgDXoNQKgII$Sm>*++@Fj1BV<#AC86+MAqS|KWGOJm-qD3 z$=(`{;vBIGNiLj%OUj{uQq^1pMI&qfZCqC1%_{rq`@YvKBUlm#fkZ8}H~PjJqvU8V zZ);w*-A|^)Pn0R?TaEI1QDIThSuW5yXnI=1|3YE99{g}gCaAPk+IEQH*ouTk)QCQB zE)l7Ib>bF`5eZb?h@Jfn^S_Vk8Ok41=qp@F2VMT4lS$O>xr6IV8!4=$JI&aP9HnIO zqC&KT7Kp02s8+JXWy$vQ^c0tp3fs?m@(WK1Y+srBC*Mytj6FaGnW8<^E?C z18L&+KX*1g{q+wXJD1H911IPG$iH(4(VVufZ0+p6px>qWz1Z4skTxdnDdVbxlT7T> z=g|Mjq>0eLPX`ChdW-Fsdjdx%r`1_k3tIvNz|Pe@npA=*9ST6i1lM`yiKlPEa^zwB zJDGwQ6YBosA*i?IfeYNlC-MLf9=11r#Q|0ij zxp`-2XTD5Ox5>`O>;S_%Mv4lD(B~)9rpdH2R!eas7)UgPkE?$gUs?m!v>vX0AFrJA3gQBltjt%o&H~xF#&D z6vIW7)|8^kByT_>QiVyHZoHg(9Q4X{p(@SXf=hpJV<&>?sBO;5!WZs067Eg)d7avT z%X)gS7X9W0%a>ev8z%7JXMPcjfz1$#8b`Gy<$<*-xTy#mr(QUP;9TwG>65Ab{p|um z!o}|WFgolz9%eFmsr0wo^9zK?yga_cY&??4yVzKU!Y|qQ_NHDU>-ePbK;Ye6KRB$VJ6NFzYBy#;T#ip( z%5RKvfK{7b%pQ18OJ$Qi{e7z|dejd+;rPfX2@G84nUqt>mZ_G}la3i#Rh39YNnHa!`Ne3X94}%AQ0dj78hQ35%u`~GMa6|e5N_dAW*(@SZ z36|l6W51TJemQNrlZUYe76#l&n&Mp{$U|L za8aqW3?JN}W8$ZL_4Hr&j~Agj-u%xyWd-aT0mR zAPqa~VZ;<ppj2^XvLP; zn18fML{WsL&AkS3fvt=GIWqn6tF8*=^k0M8cQHM)0=&~1d-qEg?%r_(cnh|6#;|V$ z8R}yGC3lm@?gb_W7N{RvRy=%%$r7HK&-W@V=zmV)ZWk}0?+NyTApl98_p_z!3VJ#Q zbpfT++9#()M>@Ef6i4z#uqKyS86Cb}i93J()jUj31@!^_ma2={yohd3K|5yRg;gt1 zfoHo1yV>bN1-w|BZ#Qc1Z(90aM@xuSWMBmh3W6&J!mR1SvxU*HNoLd(6pNO!-%ndq zf0G~a`ORZ`NjqG-Cuo^hqyDOHL(Jmx^_TF`8$4$S>bYCQ43oaDrmRvaMz#NPx?Rrz z;pA2w|Hu8XkEMYa(BNV+kThgDb^W+{y=hTifO(XP=6`#SU7e7z)-=HI?rhUI;&EDE z5ih}L8;$WoocrIWebv)#cXv2lu(fT%!Ox+Y`5u$7G;%dVrehF-P9f$x7bjj3)&2aZ z(Ct^B5hvBUD+1obq)Aqg@!?@ATr~=bpoeoAM8U3r=TPO<)u=tQ1;F>aV>(oV{As58 z@$hl;_Cg~0woLd4Utqyxggmxy;C_Je9;HZ)R@!=c;qR00*vQ!p_AhSa$qt95Z$@i7 zrV$6}f~$^C@~ik#DGgM*pw5u8TJ6lri9m}?^78s1H&B+EQM=PJnh6wZ;}G?-D|)yg zrg%?#*kMj%7tDo{5=sN}CzXn0Q8sBKrdWO{;umC!|61=yjSK-g7GtB+?bLyA(Nshs zRBRw1*!6h-eEMkEs@%G`Na61tsiSK{ySiKYpo40X5Sf875WEr$4QXnm_R``N z`=3Q=w6m$P95|0DBqw}3-XzCV0~@DW*yYNdJmqu7z4yf|fT4V@QR~r!uj5@JTGhTX z?NTYfuetIeVHpwFhr;X zlkvtxvh2cNfKQlqwj{UGy4mFW)n{@ws}=Ha0etfx>*??VdkGgwxnCaF*TsH^JJz+Y zs6Nqg9tW@Fou19R?>!xhNAeFt>nDU%QIyHvfT7TB_Q%84;+fEH6zWrRh%LYHS<~W&_oZ`ZLCj;IFSp z`Iu@(#`c=XM}Yu2p;ZOVGBz^A!Z|hZlCsk|@yxkj5*#X=mFa{W6I`d`;%dU_JXxVSmiZp}SJ3!}o`o1h#Yh#34$khb_3E`w30CLp0^ z>JUmf5bKftFWmWXxd~GIK{#E5(_w+wss;hr!6}&;s{;tpKEk8vNhJ(w{Y9Z=;hzcw zh_IIDm4cl1@g5r+n~!teH-zuszbjyt%pOA{;itqob|H}@i_`ppLzj@HAYb-oNk{EP z1a+I-KOA;)j&|7>w|ixmmvzZ9VK^?y3UWJe3$EII`0@Ap*w_yI{XHK31}~~j1V8m5 z=-Zh>6FyQhCn#`qEL5|N;P>}f+wp5j?!c`p7(JJ59sSq8YWL-nZzgXN?>EN;Vk7Ew zD;*8JgZwm0srssA*=?3YQS)3sOHNKsDzuwwG|Im^9q{n*)MJer#C>#B!)0Y-B(YO5 z2&%(pAp1P>WX)=HiqnXEFrKMTh(trVJq)=j5E*J9Nyb6 z4c77pdfg-5S~#T_8GoYr@R?3k7_$$xIaLv7tY@|>--l(pPp7EA@YWs^X-&b>6Ir`A z_h=Q(f-CS5KRUEgqe12%YAK%?mXn3#sK0XueD>eHU2?a;@R~w`O^;$Dy4B}^2fIyCzO-%T1$h( zm3vSf+@=W$(Rm5mlP!Wj2F)v8F&ez424n$)i1Q(o3+MOI4&g6E_iEGi-Btyloghb{+4-2-sK)(x#xyZ8`qE_V zec+uMVvyyyUKSb0->TUGQDvUUh=72@b27kiMlP#-l=EiD@2W**ZfW5Fy3P38VkIp_ zci!Wl`7r@2jp{2knN`F(&6Zmc(9hQvBm5i|8(&r7wJ)~` zxEagzuUu7V!GKO5s&HuADkGTfLxi4cB)=G66&sujjr;Pqwb5=F-qBo$Bk` z)VbWt+!N3xMKUM-H;JoC-)|It2-nH>!Vb6dMxUQ(w^+`mK@_a(Y1f*}x2wu59H!)p znlqD3b#++Hs++ZaWL0W~`P3ZE#oHJ^M+wY&j^g;elmzeBc^?j%>g?oYH0gi1+g7!b z%c$AUI=sKW9s__z<9kuZnDh#cD!Ks8?+ZB)fYd1Z3l_2$q!ur&j5gY>t0^hXh|`Q< z8Bc!D=v&D~i}gPs9s<$^5Ja;%)mc2I-S5T{bqPX8^F{JmB}!b#LZz3PZ7QP%t7H3S zrcw|)b0g|E!?C|hwr-vwrHu}EdYL54?vUZOs)?WDQfhZ9i{>p&qnHTGnYy%=DC;c| zb1yyycAK0g3?w@|Ip#@EiO{dEB&mkX)|!cHx=6;0!EJ5-wUj-Iz#vsicyM_eC&C{Z zWYfq=kGrdg6U;D?L{6}xwKk8b&+~EHbLUiO7pX&kJA1#-D1f(eGR|}Q)8YBsNtF^BiOFl9Vo+BWANB-biRIEX~h8cH2}@QyM$kq3D?w_fj`t{RY+SaP^V5c~K5}#X{;d zq{G~?V33EH->;Obwv?Ofbb0a8KZi~E7^Y2FP%#Z~sW5_h!j{*myU<_(z_F$OSG{SY zYzDbA0`b2w6i_SGzYzHXh@-jD)?i*x`n0%=k?P6-#`1kRK3UE8s)yIStGSK)F-xYK zzqRw**SERFti3YX%+I63*?JhziFO z7WcMi4=V8A9xf#c?$h;<@#FaCoxE6LILRK?&gO~Gc6G}oO10E_qldA)X~&p>-sMPi zW0}WJHj4UW=%?rH;z%kIY5L*({LB_blyPvW^nRb}E@sL&)x>-HVkHh&VIhxOcXo-Q zVxeG{2ldHgwQv09n|tJ5$kc=Gq=i?;SE)2uC(-d`C%N;QI`c^UpqV+cLaO_fs5xuD z_f0ZC+yKMoWer_z7>`9{#%8~HZj1gk6!ligCOzt2mJR{Oo3~1gUzc05D2)7^eE$p(q!6-rC= z)7wgpc)-E_$>3ov;6Ic+md#a=uVY&HH^4{7HVD-HK^3WXYXF`_?G`(4ez2#$fYFU` zjNKh19xYxVJ&W1XO|%ETz7e;F@<_j=TW|9;y!JFM2qswA(O)4vwE; z*<&#}QL^{-XTre10Hc*OeknaXGvOK)!UxxQUm_BotsmOyNgbVwalQiKYA$ZDc$uxJ z`d6}&wS_O8&EPC+k`xO@CKlk~ zW_sSJIMVhR?Br|xoqHzw^301`C~i4TQJ{))yh<%IG)ybsp80T4h5^H3pXi47kw#8H zEKeuN?=|taAYx4E4qu><;o<7J8D0882!C$>wQHT<`Xugaz&b4!=>yVX0g-^W#LY&n z0Ij+bqW7bL+VOn8^XP`xd}~#Gk>G|m84iPDJl5mvMV&pvMnAt#t_s|ZJX)f5_M*rFLgAOycM!IgkM#dfN3>q?34Gi5p6eiL^CCW zQ#9<0sTs9FY6=YSMW!#lHqdj4CX2hs?)M#Y=nYOql3!!(tB=oyVWw5SuS+lB!EJ?# zii#4j<-(tU0VV>vt?pw0@TY;hYF!cSZ|E-{Q(3_O=1)q#H`E|n@NYMEoS_0t+WidA zuXyacbs%cw6rwCXaA>iKjGfIZ!_^$ zS1$-PqPa`9@9m55<%qzlr*GpeCF*-sQs++)_NpI%;GHX;O-;;2i1eMvEjMjhfNs3IZtKo>b%Qf#bQ zd691A7QQ7k0&SoG04Yp?9+sS}?&|95<3o=y3&WbQ>`#^1H`f3gC`?N~((jbtn3H!h z?f2nk9QlfE6Fu-#ZD~of%Ee9XSdNAOXZ6X9@a3v)Ox51EQPlmqjuD3qOlZOB2B#{- zugP#E1AUA4MKgrV1O_HEA*ZS zkzIa*%8m24?=X)C2<;mjCpoz}e`#g3ADwH6#Sq zUpaf=vlG{4ecwqydfZ|*tNm{6$gF0_*Gbc?|H*FdMrCp|)WQBOS4PNrn;%2yDw}DZ zwQGK6jLu>ua^96jh$-2AsXC{<-KZ}Z$;HJsbx4_kiO%6WY_GQ^N_cikf2`4M&yIJW z;E>(SUMv)=d3;1Y5QFM2NBQ_DGl~NbY4&EH^LbRohN2bzQ)A86OZdNG6o=l zNrUPXcLFv;F6;Zz_Dl83QR@ea#jnL4e+n;rZNU5#st$!ML{_;_8prSBEnXKQr z9aGlp9%0n~2tfkA5J6v$uR4EMM+L)gQj%J+jZp-Pv)!!qt5d2;g}cR zFA2oJiPObLv(MCO9aoD<|DvU^1$lF=xz`}Mn3BohdE4l+@p6q|f$HpS9)}+aNK=R+ ztBRxPR9c*TUl3zlcfNT=2ADSG>pg(@?b4MZNKsvAaSI0yrafau`qNjU$Rcx zY0Dg2PagpYzB?UKFB(8trU6p!M@y|3M}{}^Uv0u{P%jT%fCc(ZaRG^UN%Fdeac=?dy9wz>1a4uZO+zhUO&WT=;GT=_2HyU zL}LvX`1qpOxY5pkDFAkb0QRr&E)Y`n(*QddWRQ`DglbmsK`IDd1OO2G4{F!T@@=)c zJ*XNT9bG{#WT6T*p@|nuk;^U@{=*Bp9f@u&@O=f=z6SzybQME|#M_*zYY^oWi{*}s z&`w?hEw1)qOkJH9k}x_mg1;6Y3%G|6UB;sMy$n9F#>CY*)WSd%i>j_}htF#?NNdVs zH=jfid{a=oUg`+L43v7g?Sr*Z}-h!Nk!G&NvnpI=8 zz5UX>{jG#)pBDAyAwyu&>2W2I8-e3TRACKOdF!S2|D~0fo3jSz(kNLB9kHcJMsg8u z8cFj-KZ~WExATs2HS%#jXekMYVBVt`jO5#h6e&%&3f&b50-E1DMf6K5G|J zkis;93z%NGWy6L?s`Sf1k!AH~)Cy8N%FtKTS%rIv8}bJt-jdU09{`^3VSIeX@PsOSCh?Mm_cO zeMw*K3_7pG9TuEB9*!Gv$3)um2z?`_R;;JxXU^%p^G4?96^*86d{XkL~>gD-I?Y5>j3N(pb}&`6UDK8k7k>YP0Lf?@LemJ7s~( z=X5_*^`|-Qojrg?H?VggNRAv$HjJ;_Y0H!gm~tIo!xF8r&a&eR9}X$kY(Rgtk*W{A zBw6tZg~`-abEM8IT$Fi#5%Vm{93=w+hC25Yoa&=26t_#8^YpjYs>~rT*6Qh6h*^fR zCr*>7E_CZ}jB=LjhjFacZ^QOs>~MIWgHf)KU74EW9x?Ir`OI&_jr5>H*xC^+80rO3GujH5Nc~LFI238rnwbZ$`+&X zWs-fF$m5#OrOa(YAvl}tKweA~RHW3aaHee)Sy0pa_Y8rQm`%05{tfoC0?F-8k{|OH z*}U|;g}A5l4eSUYqgdwa?ebt%G?AbO1r;?%d;0H4VAf5D^j1rRDy+sdY7$gp!^AYn z##*%xb@lScD=^+|=ex=mfMaj%>D@@UE4Bj^1t#S%t0IA@<_(b3~Yh;Q6vuIi%WcLZ$itQ1zBUakSyqZX?0noe(s* zyA#}lySux)1rNc3TX1)`V8PujxO;Fr{eF9Yba14rM@s&ieKN{FzU7_X=*NI8~!T#;&%Uu*%3p^%^FD*UDeD#0*RfyfR@34 zgN#PW_uRh|!j?k88CIDJ5dXokC!<&_p@GOMW~ruUURhfGjlOGrY(8@J(`Tli zZpd5(n42U#b7_=qEi4z(eIZdx{S~#ZZM;_|i1VQ4w!K|LY-1V0TXkBy_N!HkH%HXR zmFM+AS(Q9~D3X?euK>3=APpIerGBJRj}Yp+fQiVGHXNnBzQd)2Xa9P4@Rz}5Uw&S1 zxB{8@u8ufy1UeV)Cd-BTx$}K6L;MTb=X1VKKf=@?gw>A6*I}WG==CT0{EEIJGiLpc zW;#>w-o?#kUvG6XS=ilkzxR)3^4X43G}$;8SU_mkq^tC#9}gmp4J=ol0KMv49gS%KBN zszRy)7xV2J^Q4bYtC-R_Vc!{CQ17t{DK~5^ld!Ypn=h3J1!rxQHoGzPs-OW!)Uy-Q zkvM4v5D2#FhI&UU6Km%?T&-i!vHSK~$1BX-Y7k2FIZpO{F~wopl^%3r9MZ&?m#7J- zWn>JnFXC{ebNy%(FHMd53IW*q4RZ2H>u--olv>u2zKA54s=p*x?3-C(!*elm20(hG z%HAg9o}2Sm1^udK{8nmz*ImTP_;gLq@stQs*U)0k>0_4?H0C?OonY+1~l-7 z%!4f@>1F4Wk21?WLbmi4lKKUiUow2^4;iaX&Gc^Mb?XN%FE7){Rlbz$(GvfEAL{ey zg+`U$CyQZQCbIN%uIrT2o_2!hR*K8WW0OVWIsUJF=RXzC|5P1QCdkAnW=^H1HW4)7 z0_Z;>V1iW1*HXSURRXczMRZ&zhK2z-bK3f~iiO#tT6QKFu%p}=>ekiK@i|}PQORik z`Nn9^I#{7BNb!+_0dr;xZwWy&q8hi~yo}W19xPsT!Qv%w=K+><%z`xyOf=@oc=rnT zA`;`ag^2=%^CnYtQncvTaU1l~4AUH_m^DVKxIQF9#^DIy$3g#3uQO%cI9nvokrrby z*;EcTugbjru^e69@qt4h04#{ez+rP1iQ9j8GprvNqJqDNP@1YWF)q~Kw^UVRhNNBy z4HWXbpcPN3w98BwM3ot}#S)_>xIF7xTSd=f*v3~S_CZrK=~k- zz*t+A?%8q5h!{2NSTOrZx)Wo5vdj})?KM#dksP7mGrXf9r;0pFMZqqY=vJ7_?wZ5W z%R)tU=QQ)t$#nvHx;-if)#?yS^pt^fBQ<^^*y_JC<;jlj7e-aZP zfQXWVKsQB9szWeKn(v(LT>Z6ta_%31RU@p8fHh!n{yIV`f89CiqL?`8mvigYRJH84 z4EMe1BKm%{+Z5^SzYh-yW&=`DQ9V6ADx|SK9?zGDg@xgWNynHmPWCx7=3gU({E0+4 zl`)~m5?R?RqP~K6DH@rHCIEM&?g;Szwzj{u)h>Md7R!NOryb%mq)^e;5lp}VINIuk ztPRw4$c3Q*B3ZtDzQpi=-&SP|Z6(AG!V4d?6|D;#bpVMaIug@}lwG>5{I@k;n5eoc z3#$wUM2|bfWF?N}I%_s$fT%eTsA}WxmO0tRJ*M1(2rSB@U~50^YQizX-9AxQ`?+HJ zM1c%78bR8D2MO4AjfbKN z5fkq*;6zJy+zvl6bra1Fp!Y8KZN!ppE*E^(B%1M^VFbrve?ODvK?`Qb9+Q%%{x`RX+x?QyR){5e2l|oHQ>V(=?VIQ zmv-j0a{eLqv;@a7bzxCsgK5rj4)j0Sn8$XyOftnbGuWtSut8j<_hH0^-0>m3`#GL~ zon~`8`b;lo#A76Auzj9dG?&N68WcejrP;1w=1gV6K+xizae7$2F^H4^m^nShVCgK7 zK#bl1gx(NCbijJ-5KQ1VCzn(PEoLuE3s}B`H%tpZ=E9s@z2o#Pr<#ol2=Q7l4Ph)> zzyj3C1iV~H(&hb>2nD-aTiMfyWcy5b4v{-*`?xpz;i`?PFU z?5lcV--?5FJ{_WGH7=8bs_Ki8kkRtB{d#+r z)Vr*F6zsoyMKI}S+SMRHa=Nrz)x_cp_qhzsSJ#+R+#^-y`wCsu>)7&{bjT6g6s9yh z4_=$0K4gY$_R}-1SqFbmoS#krAbg&jfXdh?U3(sAE9#EJRgo0Tc4Tq8Izx=iia>T9 zYjdZ)szd)9Hvuu~u$x(XSL!0T{d8mhu|j@=y#AXxA2u@G9(pYHv*j=ov91!Yuc3&yfR+dk>4Qxj;Wk zOlxZ`8vd@XFcP#eN4nm<9~D*{tI)nN>|%%^P*=lM5hZbCbodLa^Ic_(u_5Y zo1ZWk3S31ni(WE;?Q`zz5vTpj+vQfDT@>eGLliH8Sw>rTej%FDoQgN{!UVa)2tn(# zH$Tkif&mYk3d+(FAb^IPv z_}vXtKS@?M2>G`X*!XhvnbQmFB`S|f-dfWJ5kogs)9L2NTrmMmGh|~*b%Ugj?Nf%gZsOm8@7}g{Dh9_W~)<{6tk98enTY4PA&OQ z%O9we5`IvCIB)9lxb*j#`PS}{J-98?m{o~rbzA2XE!{$<&9Zr$@vjYs1W!3s+me$*AlQihR#t?c8@HiFiG<{zb!EFK1)#C7_{v z9d{dt89uH#HDt-iDA%op2L56bKf?y_p1utFM5i1zjKp-*mrB&Gz=eA+qmG)#`=5ox z$ZU8}HCE+k`_&U47zy$`YW4irgU zj)hO;!wDn0!C*|o!51?LyPY&l*k83M1fa!)>wRtZ_BB-~{lLTU9>>kisY@LKcnaSA zaN^|<M1yvhyMD(ks?U`9rO zfq_B%zfUvZ1lbUxs5)=m$kqh^jTBUz8`dx5tm9{6Ktout$kZ^D1g8C>BeWuCx+1Jd7GKyIuuUEX5Dwi@CvW^X(suMQHuUTQ->#;< zK7-q7yG*UTO0RuE|Da#_+a2y>>ncf`u2;F{>9|h4`PuEs;_8q8a)-_i2l1ztRfHWO0I}Zjguge-EX0Lmg7>#vfALzDYO8hX zVv`?2z5X_GIp>U56uU#Bw@Y}VY4LawtTE1Qb~bUB%QzXhAT{5|xbFNi!?i??uaYK= zHw@^y)=n|I$PB>qxlqC_kxJCkH@Rl7ITh$z=RR6~)b0ylRc_cok2+a&AmVek?G1j+ z%gc*LqT&(drHR89h(S1#hbKzHAA^nD_djYDk)jo8ThwO%=grocZDn9 z3(OMVlJ@0~4}q7pq=SmlHj{`xFu4$~pxpP63dRv7OTfW@Qg|8)B3PkM=-o1^+%7}C z!h`7TE~*(ukb1q&sK8>}t8})WyZ&z&dijC8d##s5`9ltmdIraocc(p=n+_A=1Of!X zTtse{{g?sd&z9*hUC0X_yyrNUs&vbc=-AqJf^Pz4C6Ku+SRy&+bpF=2Nh*qN!2v11 zU8+01&#zET;tgJs4c?llR+n}!Ht?t3C~XWbClYZK-CH{yZz1*)vqk-%GB%1=>*d~= z!PyoZ@N;}$=>0|IQ*FROR`boxjT!6c1~i2R9E}3iORzdN9t8>7Oa|6cEkXI}KyA># zPQ6u#gaZr!ZE@qJiv{t6eL7HCqH^Pl3!I?!%n>;L&gG8yYv{e6g`H`e9XKiqLDQ*i z^NR+0z!O@8z>Ow40s|{iYh#%7DtQ+6kobaD^$dUajECh~Xk4>3$?1I!+qTdt4 z`eWOUVV@F2iaE)NNEA+_Lk$bGD}%;&(%lHirJ;bq{VPZ9@>vyjLwd$u6LQdJm_$JQ zd;3}|mkp1HU$Ar$@4TXI;88SAq?|1gYh)jx#THokwRn`>T}xLEc81`-@%7~e%q^M7 z762(GW-KrwXrZ69!WNaV&@7k;L_RMgPnEBhBXw?Ua!*{|#8gl0==$eaks^Cg%kFAN zHs=)E^u*>AR|@D0_?>ZPm_%!k3_}Cbi&ume?)vSA>!Xw(@1ni#W2O1c(4~GImml~) z?tIstF_MN7TKtjwCRD5lon#303_Di$ zCi`qUax|WdyZz>&HIaO|PNs2b8n%?D{_OI{1wJV28zEi0e8)|QU`eNaYw3(>w zx`&gDK5sXKDL~ck(a_}UXMp=QyVffBWC&4EVdqP=7E3kjE4TbILF+`8E;eCGfJbEm zi4y(O9%}Iybn#@`y#9!FNBi}~XPa9p5Uc*B^Uc2k+06AEO*6;!a%dCf35jS`==W%H zUlyxMx~pmZC+iIhf$3T2=V9fgQ&cWg5PCcEBQjw32>57uIyBnO*-?L3?)}N}NM>l1rmf77w-$=_t)FpY``%BZx$sX1$cv83;rNxJ_W2Cv;JO& z8mM_nToqP^SLKW2%(Zu_RyfYpt9=~Xh_(1%T>Md;y2pz1&lrhKXawiZ`tSb(YW>{ z%r2Rf0epspSh0)Mdojo8HXf|qK{)-f=fzUpy80$0|K2)C_WK8N&Ss~6qzY+Yjr^I& zv29%%M0~NR*|3))CCw3!#@XobI*B0lpR;7I{4&E$xV8Gxn<1+OnS)SS6p4O9Pbl^7 zx6@(n&$V(~1e;jkdrn50f%2kEL#m=`sK?G$Uz#`DAt>lXh+)Yi_M`z*bqu=Sl9QCb z{_y_0RQdOC;!c!^DBoBwR{99Pr1E6yN(D5qv$*WrgtS9j(G;=JU8=<_IDE`=XlmzvY(PA20oO;Wi!kt3LmQ;R9UR~NouI6lb*RJ7LD>`S0k|W&f`c{73u zc2{ly?8!I*NS8n*<^@e^gX-5|`yYkL+2JRx**F<{!@a{gV%gXq`Ah#SoQzMVaA0Rq zQj(*t7+j(<*Dq2_o#``o?kp&g)O+Oz_AjJuY{w571&duTy^bFevb&Z;

8I!%c$hRIgn-%j%aJBV60br_34fRVR!N4tIF{>60fH&t%-avzb zC4St9YAFyg*@unn=Hao!cH+vL{bzW;cXFnzks@PUl;j7Dnn}p?Qk34uwA7YcP%u=i zkj`csetx+S`<#dXdb2!UPdNs9Ch~~OPJ*FY!i0)Sm4fCrHvA7WNWYZ)0|KwU%poEqcpDNh+&as?HspjivHig^v_ZGHIk%vnuqMXQHp5Xf%t*r z=2tD5DO_971=58oxbJw~6xGYos7r&#Np&`lp7$h1kJ%!vxI)!jP=Q@dbroJ?@c zpHZSnn~G8asqn71{NvwDCfuOp4pG(e?%14KpG4g$TBTZVwKth0Sf# z3SN)Gmk>syy1|k%Hp$U(VI`T$qOAv1gZBizjS-%Ay^01Ar+vBYaAZQ&GFS(BoL)qS zLwBb63%$6^NAdU7uV+=1n(i3i!EnN~&xy*4>8qE_N8^>;Vxr?=h>uyY$jHuHkiw3| z8zLmeK(3U_-L3ED5}pp{U)>B6mT720s6dS6y%>a4GBICx93ZeVIzI8C_sV4?m-lti zDaYUBiI*e0%igF78#uhKUQ_4o5VVuKD%@#BAP3Bf&}w6m31Pd$E<@2-7^KDYZqIaX z1*=gIcXxWQed4|$!U8}#g*4};{uoqK&*OY*#9A_pmsex>N6#fcgXSkO*!cD@C~-(= zkN_F-IpLQ3(P`Miit;$Km77+nnTSBXIKANw@w2NJgq(*s2c^nhc`I<5`z+a5w zSI|+F&9`ZaMzG`~m!t{@w?Aj*sYv}gpo1?lrKY-A%l#4|FXVQMoaPnvtp!dHMYmSt z;t(WS2J*GEAS`nWzc$h>5=!G7ycI-MXL7U;trTc;*UE!JasmKE1U!)%ZfQZk- zfqHaS|4qDt-G+vyrlYe^TuBYN#5z~&;&&Dpo69=tI>d;L){){b5+L@&Riv#s$$F_4bP2k~F?J^FkCRM}^6IW_ceao{T2Hs&`YL7*{{jGO#kVE_=Lwyhs&BpM zE+YP=8`IZLPfs&v1_4|SSKGn8)2$H9CEz z!+*&9^?TUW9zwE--nQdL)m)qUYm#|Gt(T&F4YsBJ`9u5j4 z>OkP>4F1l~*67qnlGn47{W_VGllPS_0qf;wx)&Jh+hd7erOCYr=N$f;)Ua98{acp` zb}O2I;p%D0Q+n_}TKW-3f%;q=Kq}zQtZqVFetNQqnp{ zR_F8b@^h6@0eY^0074WQd{r}q#B#XAKT$2|l9h?^RkjWsm+rL3W^n+sBkzUld%l@MqpgI~h+^y21JPyNGz zGbo+e=||zqX*M&@eth<5X(e5XgYDVMOGkTy4Qk7OFU-r^PS8~qSpji9t9@CMy)=nk z72F1;umGC0AJKOhs4erp56opZ(K5FlgC1Jv5O2mfUv*N7jd@z(<-`lsJS6@9&x@Qg zM`|3}H70~nGUdxnjOPkXc#!6uWevYHO`vs8Pfen+lGO>Mfuy~|C0W}WAr%_M(=+t+W%luT4roVOkp14gzSoUn zsw$8~JDXHh%PGXw{`Dxg%lKW)znP=7S$rO5NjePA;c%L z+Oh5P_RI&pVObrOH04}u$36eeGzhVjm8a{?$A3ssoOvoh2ce-(=m!G*$L(dIc%&sbUikKk6S~6fgp#FI!!F; znRJPMFk~XVsIRjX6Xp^AgD)TqXmUq=`XlSqh5C?82Ed6@7i(#n#Xd1 zT`V3vdpz53wmf~cU!_MBXs__K=}#a9BWA2sxS{$4DmZ%!K{VS!mEOTAeHM>PQAmiG zO&(q}!eMaNKNj9MOxLkbyuKgD+v^!&HY1QQMmTJXVmdIko)TH`P&igcj>f7yCb ztMnpq6wiYMSEqC`Bf2ynY{YYM-lr+lRX=9t7gD1}6E96C zWA&eT``!t`c?mlJ2OZq%BIN4g@_gCX3dU|R49Y;+wZ%(ZI+=%%99#w#5hS99lvU~3 znTQZg-QEn;GrnAIaHU%F8&g+nwePp13UV8i!IQqetqOSA?4)7^_wB|nAUw+Fo6k04 z2B^IJz%cxP>gBU_tu?yur+$G}zkL%QKNQcGX@e4ZY$O5hy_y(RB=0n@hjGJxUMvbR zWaT9UI9;mq;bd&RW0!*%1(J9!5Yj3n)a}syT2)d~lB^_S7~vC^-^3XH+f{XY;u@hq z9A#<(8Nq1nP~&j5Jzl>OBJhWe+n+p@ zgGnM;hIt@Rxwx{7(S}!|T-D@M{fqz~@7%ai3+j)lIJio4$Y7kpl1~(kEIq-CauA#T z$M*A5P8qA_%10$%329(VA|#FCGr_=xLP<{zpB^%a5tR8PzMk((PaBhZ)uWMtbO8wv z6Ez~l<F9#*Sgi{ZJ=Mg9`moOo`_XRrWv#LhI z+LOhh{+g4ErbCE~oZNm8H~W;gHyykGbIi3;^c}3>J*M?G@5TN}=5$=iJmw|wrq+-N zL=s*-+gr=;POn>RdfsMDkK{!4b_KlK6Uo5>YS_b%`>mJ}&<2(0NO){=e?%a9*1H|< zaW5OjSjWhIcWsYUt-u(1HC%z0nk2B+*^8E&d+AcSBJ4t5(xy+DUGdR`F3}(idY+A)?=7ojcrM&%An{Cj>NP7C)1^S*mR@aqq_Gply69@e+Zv+ zb4aJKcx3NB5(O3()hByG*Ox`s%zhJo0SRdm29$gt^%X1TFk(bI0fA9bQTmZm!)Lf=liIU@$V#J9@i!V zSQ1n5vzaP12w2(JK(=wc?ef>)zZWZw z&ZuKFe$-%5t4wf^eEu>`5h=QeSC*E>D#7bFA>s%*t7 zyEzknWz~Cs%cIBLQ{KO_;2E=S+bT>@U!Bv=U;_X{e@W0^uI1`IGy>p%5>#i&(pG3R zJCNa5z?M%Ca1%|mIPH8QF92_L8rs^{EGQs4hxc2i0QCBhBTNTl3?cQu%b`&BvomD) zbG`6RUYe+QD4@o!u%M`Oa{!U8^jIH{4=+$-?7_?7-_R|VBXl*BCI>6t_@6&C z3p{=`rv9ixl7Qm{r}bU%z)ry#0&6|<#T#h=zut)bChLEP1~`8YHwuA8CE&6@1wrT- zn3&Ra10v!hz0tE~6m%Wk3FPLINLy~i;3Hk7CmEcO|3%qyx+64{U7_A{J6NQ6~5UVpZz6_4tO;SkkLMV+YDIbc0EBKd51+9 zz6|(JfZX5TA4B?ny=dS7LM2>vw$aUqy{#_lSAz1zOM+!#v&HkBolgedTLcN_IbA1-5Dl)Xh24i z1_J{FE=-> zUK~Fk^fs)J^ijLJ?iT@u^u>|Yu^JQ({O6C@82GQhj9++!Idm{?FkU_&;Uw|L;&VEW81A z^R=hvPjiMex!=OrW&^)t@n=ke^=JTiF#i56iil`dx!lZ;&dV|w*ie9|g0?Tb z9lzWJx63eC;t{B%gF8+dl}{b9ts!83f{x(+-ly#vN5If%N}xYwd@Ff=#0GnR-w2xU zpLu#1+n^HCJO2MR2Jesw2?^YBWN;Yx_-WbMdGppf@x{tSq3%D0%U1Yode`O|SXEu^O|9$qWZvpGX>~#!q2HcRc6Io= zCz)8rHmT9*l5x#AGbJ94>m>U|TM^K)1I>aN@aWhBG61l(Zwd->DAm(BfGMze%;0(_ zA|(|9IiWfJPo-dqKcEyN{y6kkPa=&E*u_kPWkw)cL@u>H+c znDSSL4XZm1b@dX}k;_X*ln4_Z6cMR{xDH={ZnVE4*3>GzmA{#|Gg~yga~hiwTRQyT zzrB+Y`H6pBK9g6+lDFI03q&adpg${ojCd9l73g$W^~GVx*9*95eu+=j_xOIc z98lPH6qZZIHt6aOYbp0)HiWOm!B|7e;`H?_GKUGP6cEN>akYlF8Oj!7pq%;U@bJf} zfE*5JJ)J2sc)P~G({3R%lN2pWgZbUKLstN^`R3v$gC95*FqmN?@I`k@^}XwW>!SB2 zT>NPXMqcv}%gyydG4)36FG;84F&YUWl-W^Jmd`b9QV;r_{}IP?88o}2X$hi6R>s4? z-CS3qek7vf47;iQtFuR8;Rw3dQ0I-d0VH5Oqv5bpy^sN6`z+^M=mBeyi^Qey@}|+| zdl-s;JlENgo3K5?)*inxCHqpWNVG$_6_j_?|3)mGHeWg4C7DV&3scWb)8_Ack&~(h z^wb{-nOjc+e86`-zJIHESj2%or%NndBPIj=;SW|(GPuPPYW8f6$bls{aPA$x)cPEcK>61F`ue2)+|A(eFraCbD}O(ouOYOjphcPel4iFFU!pT~cO9MtT zg>P4{()Hjm*jw>>n55fiuYHexc%bBC`-XiY811?ZF>w`r`nXTX79H=PdGmU~3584i zXcs0qF#Pb~VA>=g^m<4MCYgQiuN+-M&gsg0c>VEUW`j9HO9T9s-qg9@qdABI-;b_) z>1#ez3>{qb8JFAfvy%vCAfQ9=Z#=bJYq@)1h-ule#ch56`l?SQTT?&2$&5Xn}&@J=a_+@L&3# zC;vv$=R|P9-Sv9co?^S7kqhnKqCsPQVoW0^NqJ6R&8)pU6&E+tXf=e%iX{fVp@+-3MxvUDtVGLbRz1VcgU6K1y^zzEb)N*@% z5E-2Rv@m>w4@ZU;i8ZW2@X7dG6KAOgTc`V#)_-r`xG|wc#|aGVm8L1P&*t_l41^v} z4)KwN@2(0GWKet7Q|dn4(px*eCvBj2I*vakhf|G#)B!9vn0=tE3HV(~&4eXt+>m~UDHf)d;-O&Uylp!!zaB|u5$qD} zKm$giok1<#NYpV*rCou(1q?7^2#td%uGFRRFt=5!O9Y{0uo`e_{ggK9hvVDs^w>pi zF_9xgUcLt%@u0TjCpa&fo&1uOonDGkz(tL zQU_~$B{z;om<3H#_&NL+Z{(<$sAHLhq-E$9C&CLN-LwF<>INLZF;DDW*#wG+8*ue{ zS{93J1(cMNd@L-!hy_(-UmO@gc~v_6ifZTfnKdi5O6QwQwjS>jfr%|^Ys$B3kuFf( z!9Z-!7vKY%Gt;_!u$^j_$F+^^?zeL+fdn@;AM+#EQ~c>;LLvT3Zw7sevZZz(#LoOA+(cboHjmC;*cYQ zPQOaWVQ9iXNrcog?68%+jBfgRwJswjX9&JOWZF!PlFAV;l<%w}`g7pPTiWO#s8ERS z(Brrd6{Gel#jq2~*&R-cDQB|IbN#WCWb&*|JYSh4OgTti5SM>4wl9VymycD04S3zZ zI3U`3z1ZQ-FE5>jo&yEu-_{0JukY==<~7FU5bxyY5C0qN~XSYtI=0nAp1w|KHe) z*SG7UFB!THLFCAen4{M&bBgS)Hc1-4im82Djl~KAQWc11E5{&la||t3Q>=yk87f;$ zilq*yW*u}7&-Uh(e(JCP7K)-n<5}hV1OytM2Pu1sdnXKUEDlMexbYP?Odth+FA3z_ zC?XbkJ^#nZEglL-z()7JasQJ&ZP&RoOGTx~tNLx+EcwGnc;qfN`ax7gSDRJIR~JpI zh3`Lqf~gO+5Wr=fYa1=@xjGJVU`8yQ6%6#}>P1|vsNd?2MYsa5$4|j2!i59-h zn=Kc@(gJi8bNfr@m|@bzyHExhQ*;N`$+hnf({a!UfV+ZTIEu5J{AcD>=vG>`yA(CZ zLzj94iQBk1xbiVtdj9sE2$YNQY@V_w*OZOw6WO;iwj82K*1V>8uPsf_$prg~l8FQnyAmfo&))eTXeg9s_P*NmHfFGq`}6^jZ&$cMwmxo>b7QH$5^c-B9U4IL zlCE6-H#L-woGznPRUz{MRP!z=Y5kz4;{pIX^-;Qd=P24J=X#mluUrm%kOD5d!q)!& z_EmDFEQeV0k6vEL+AAf>NjUpS^Mq0bO>Nvl-Es<0fc$Y%W7y-Pn~a~hf_HMl$@ccA zTx@DgjOgndHpJ0e)L+Z&TG?)tmDzhITgSVm1+)_rH06uzNks>$++N;%JiNcM*L@f& zKT=aSi^#6DKYO)s^F}7@@C9^ePr(7P175MZk@cM|wQVgO4fIFe>E#;VqvwWoy-c1r zOc02Xg$d`ce*|YDw4wu21yxNA%-t0e(83w9*Xch_Ul1_8avalx&W;Xtj!*wgC!a61 zmCJ}jTFJHgDo0Ac@avuSSNtAph=Pe#S5D2z(wesf&*G9!?^^`(owGB>J%Uk>i#4^c z4jmf9V7BqfuT%{jGhDTd7%DHPm1(*U#E?)#k3}jz9+~46(791F)e8j^B#d!HUV|x6 z2ePu)H?qf41zkn#LXzrcz7IMfF2B8Ff2nkJ=R4lIpj+J}`oFwZ49Y>sAMb*f_tBX_ zD=k$&&!oMj4FPR_I5c!SxMM8Xxl%vDd#^}jcl-GC+x&W0#cflS@JF_t~v?P^EJYD#p zvRnY`NBXO%cb9Fqeo_JwnEU0&ZIL`7>aw>P`af%{!2f;v-}c-O`7F*%Nc&eTp$RK9 zKPeb_(9+sEKR4Ilx1d63WP)}!*^Fz9IvUvf#ei;#vjh*8D*XLFCmR&ZKd!VfpZjwT zJZ%G*8_x&sotx^Cb=OfVBnm4H#zA8f`?sL2$KU@+eL?1| z_idC4E#dv?@@iAchu6S(Sov9_><%YCs4z%Kkh>YmpBZLS;PWKNOa`*9MkT)1mH!r6|3sW8Rr$hzW zBC6chRCKWBi2Jmr_M03||Hjd$Htd6sq8VXyt?zFi&jd+QS%P0d=qN>XF~12gdKgPK zpA(<9exciTemm8p^A}L5ww_GE>U$?u=kt_?5>`-{XmdLu|MZDUKK%(~JHmu!4%COaGF;eKhwDwE zJAEY~96O%dH8ooLHu3f~Qa)apDXNMphf#{xf9UC}vOBNJ@}%UPV3@_j`#@Pe3`zu> z{aVYPKP6yJWpYZ2_J6FHP?S#=s3P|)nGn^8JA?27FD!X_d|qPP1?Ehoi;;;53Cd+y zPhQIqQ9l3h?x*JDoXGF9$B^)@ z2ALQ07lpR27m@*h)bqbwfDP)!^x4^22tZiBaO$!=y1vdi139IJ&oyIevGXP3R$Nif zIsDz_KLZjK)!^dd#Yr_+rcgi^!_~Jsl+Fg+m*0Ults2{rCDLshmm_OobKy+Y!y|}v zSa+OTWcJ~z{VWt;-~frNiTTmV6dy%`7jKEB6ygFtm=Du{aS;3$!MD2~f8dU8e}xd} zl{0<|eeEHeHSps(ol2FR;6Hb_L0X9T|Bh4%cdY);K$Y_XT$u-Xy@ykBthe9e-A#YF z2jP+nBV$CF1GlGB=nl?06>Uunl(LWairM4l^(|E!y}h~wfH0_{=*B(bUSIw;Y-(?p zPieqvxbouBR@YSd;<<@DDo+g)z7HNW-~&wUX6JdlE)atdE>KF|hnV1uxK-g85m8nw zKMu4n-ft`7Jn9b`wP2LfZ+aRa*)kxdt5PNpvp{|QZ1chw?lF1NSXc7(Lf7;4z7sdS zwUF#6tjvhSp6Img@j^pB+&q!4Eynk~QvXjKezg4`hM(zd%=|GRIaO3t)Wl?`U}X?)ra_1#X(y^v4sA^n#-&Ng6mto<8r+`>{n`I*(0ogu&462O zS5jRytjMjyaeA9NyZHXo05uLhL@ai=_??p}nVLjOo7+u$EqS+}pKXg|W)qdilZs() zxAzKh124~iKn2&Lf0O^_o32pyr>sS(6*O z_Ep<-k8)h=dk2KstfV6k$u5Q36&tA(4=!<7S`p0~tlh6BWU$7JyG0W}Z+-hK(U>4x z`uQH|2qf%zss=$4t^E-r$N@HTBLUdJS$5@%k=$PWgmzrfuvZg zSl5trb2-pVLdQHam2F6aaW;8@e;l}R0U7x*m+k8_V-oJ-Pdqon2gM#KF`rMLrj|fy zkBpx6k)KMjxUQ*GXh?OEhPp*Q9;nE8hl~MLEg#`9@bIL5n1WLvSr&GAp)~_Tx-ZY6 zE4)jQ>9z&Xy8U0$6K4r+lJ4OHza!rIbGC$?eI|i4;cNHWPQR|>JxkObhH@0&V7x=d zt{aqOx;8XyK{XGB1aie9P;U0eUBJ+$&w-{lR_}}z3a=$S_DMZeN8+G7VyzV&qKPU* zrd-*YW2CHY5c6~6tu_C_gLwttK0j3B&U4$u#+Ui#~vica2R{aBiS-rjam zJt_xQ)DJXrV~gwG4wm9oy}g9+CpI{I zwR5*{;t+s*NE)PzPo;a&=guOtRUeq}0ECnOc7~tG$#EHV5BC?0`To0+ipWv%-(p!S zk$vJ@`BgNdFqKHD9-BuGN6#k>>JzV~38>;p;^T@+jy#Gjv@NQ&hT~45XxkkPzk!jBQtDt&$l9E_ z%XMl4icxE6G*mE_Gf7Xt>Osr@YF5jkv@-$Nk%Zj@b(_YPoH&D`MC*LrD(t?{}tDI)@ZnWf151|4#tn86M_3p;2SsA1X{;uxOit z;&h+*QKV0&dxyE(ahI=IRiMEJzO{7b^r1N`x7KRO>|?*2%PrLb$9u#(pT9Xaf9ba+ z_kUP)MEIUf@u}%~Os6EnBeBwM$WEO)EnN6ivvLbG`^ zf|S?ks~fajHfks0*jsvdyVwltEU_12EuY(LG$U>yQEq%Ns;Vn$o8Qa5Umosv{@A5@ z1CS=exr>(?RJwcb-8y@uK%oVbD&@j0p@$biumRLXxuyJo=&|Eo?CNGGboCUA9<;H% zabnA!qsdvtb>l72HPReL#Awaq5s~=|17{6q7OjYL5b-yMwGo=&qL0zgj&aKej!drYR#y(Kar| z;U5}=4k$^{6a_TUG+fVXjO(QIbd^90n^}qY+`bXg+_DyX7He;xT=;H+d(glS<6nQj zQNH@fy+(jWXJ!jn?4~C?E*C>vN%4s8FHh`XOq;uSdm{ilv?B{FNCN{y+p$JzcwF$3 zH0^=)fMEzt>rxP)DYPsHq09hKEjOp;*SI(cv4>Yp+{N9A-zSXE!AY(D;YrebCude& zm7+BFLQ3w7Av~hl|3j9PiVPbZeLk&3O>?_Ncd-+-l$!tmAP9nU2o48WnB%CCeR!aJ znoDM2V5H&EVDfU1mqW9};9lo})<~aAtw(4Ri`81MjYg|4u2N^08OR4)`O-H&eczN{ zV&X`gOjoxj+|s$%SBM3^bY8n$oaE^eBYBYL^rn!WW!$N@~wFWbnjcTrsn@FRjY{5~ooUjjn{o|Ot7e)Q437aL4bLy#$A8pGTvAx7;1aG~ zdprKT?CS=@zg;EKAQ!6X0hIpW#cG2I0s6iN zO|@(`>Dl$0ExU%d$#jB&;qhRyhNpth=TDh3WyFZ#J9g}_7(j^}kmwOuU{|2bxIt&S zauCfti_kOx?P|w9pQk&2{f?CbO@qcjwY0dKf#Dy9k%lLSAc(i#dTZRcv1kiU3KR{r zG!SzSGM?Oos2d=tCl=PnRB)Z8R{|g?0$L*|b!Z0Pr{%n}rh`+(y8*c&yQ6)*9MiQ;lLaLA~aU z>?tA$#E`f1o8PhqV=;jXWKFkPSg%aI&>=%_I-c!>bxRbScp?Vq<$HqW!i zxc3M6YDu~}E%~%*;j4WtNm0CQuq`Yp!;S%am z;=tkwdq)PYSiQDD!JoWr`Ld7ciHo*yY^4!#eQoScUAmUmKyms{|7akQvS#&{r8>Kw zQ|ErOXv)0Lf2(ce^y%Wi>uR~JvrpI97>Ts#*x7;xcdwXG*S*{8hrGG2m*&{VOOL2I z-d+7nfFQxc-4CGmnp|I{Wx@?!9yG>@!JbvP~9}kdO_+7Ir8K z2n19N){5_|_)$@?Ra-T$t^Lp!wJmM+Emr$#3k3m}@={rp0)i}Ije!JWA|YfUWSbJ+wFGycM-J9571_$<3Lf%!CcV-dfjeYX<;^jx+_EN+e-;U45<*oQ_Y-Ul>;dR@N3ph13QRmM2 zVP5t;08mAy7!3}i4gjFOve+Xp75_dWOT(=p2*RE{d-w1E^19b~zH4_jEMY;yeBwAx z8!kv#kgg0x@*?az-I5R!Nm5-+jRpXKq!?39wsX&^)?T)aBp3>}HZ@By^1Bx$%nAy% zo*=%hMM)F>Mi(2rR8rdVNOH!#5yPZCTZ@Re+(lW#n@5J0ZfF94=Hk71PaKU&ob|%5 zUjDUN`QPui@c0a&k}9ZMWgL$L7cH+rHil9dp4vHXZl@_o7G^JTeDJy5s0s1bBYW7CAPZO1S6;ddo7(2%WM?j#`^ zLa**&Zz})*QNlq^YWH+CR$+*lf7e8(wK_3lqDBGDWrubZR0BXr>eTtk2+xjv4%KV z9b|fsEDimNf>y(RRo>UN{j$5xW$^F z+An<<0Fnp?MA*}ccD6B+LdlBIq6oq0CJ`cO*8eu0{!5X)*4GPnshg3W7QRs7{X?n0 zy8`1zMb0C=zd0-WJPc4|X}ANdJYfCNsF1ab;(xKf=~RQSyU>AgHHK8}R)&W&X|oYY zam^Q1HM={8%?_AG0APeA5&$spQr82}4uL4wk8&o?!X$%9T2O`b+$y9XhZ||tO_p@@ zmiTz%5|z+{H0U(+Fopc{8@R~1hC6`WFT8fJxw?aWb6!k(c()dEnuf1bMxA=x97*5> zcL$v|Ntu|YX*ol3z0Df%s*s>5C16esh9+XNYZJ<=2v*19&#dL{J_s3lLw-U1vZK!a z`7D`#0a>>13>|*qodVncJ)HQ=1asaOjjYhcjbGDTEJeil4}D11oJIl{7&Zp9p^T%6 z;Qg4o$#vcvbb8gi1QiVcL3V4EPgJY!k0kQzd=4H+ip);XC#ew#e5nf!SMWP(14@H> zZWt7K(ab0YuvFgQEpZ4K02syH!?aOK1Q=Iay+!SO=d4DUNjuFT6?xFiaPn}a$I4wf z(_t}&;iCUQGY<%*SajpUC<36l&3niyyG#xUm2D#7w0|M-Y9pjg=|NuqMZn#)x(v-921S6Nrp7yfe!juL2;@qN$cw zM`}1$G{uB`@KEBr(~JP1iwjx5c-ULh3^*X_BSO|MwxlZ&0HntnrWr|lK*W^#&0Nbw z{WX1(gk;mZ^J4#WU(7pmBT_qcS0;~-{$-XL0J6tKzdhA}1ksL|`;u;Z>_(=KkfVk> zfW<)nWRe*GJ-wr@!P)IiQc|eMQFm)HCi|*OupnxZv;57MaQg>+(FBxf(PuKjm8V?I z0DH>;0InL*SrW`IXE3WiuG;4UsBjUX`j@`_nP@>+f7EuMU0|$iR($f4sfrg%xbo`u z*DpIZ(Ej-3U|F!MmMG?r&|2%7x1Vzd!L_$9&P}FIkfq@c z@S8c&j)1u0v--O3b)hrp{7J<5Au$60kjSgsD*%A-+beiXpkRZK5JXApG!TjqrV;LO z<|D4@@Ow28f1GVzeb!a$;yvF*F2vXf4dM1)wR)>^a$vghx6Ki#4+;Rla#FKbh*44i zoQk1hjfzJnM<>!a#iD3zyZ%Sj+~WT1Qg>r9LSa*INp|$lT;i4nw%#Lj<8&v}-^kK% z2e1dk6`$2#a`f7XIV4KrSwI8;5WGL4a#tE**GYj|fwTs3K!PlF5Ca4ybRnP!N{B$9 zAu-Y8JcfqajwPRqKOSND%#2Yu@BwxBOaXJLooo;%g zQCPlk`1Bi&Vg*T%y1U}>z7q}0ouX6< zdssKo*63W-=>BJ8*v7@-Gx8lruDpy}eH>CVNeZap0RW|k_LTBONWEw{`PN6RpEj_R ziirGaG|m7-NY#rpf?_wBkCOc1nV9Hip9`h(<#{F*f?$AK_Hj)G zheCr}b@2EVUIL|?{bSIsk+(gbcz^rKiyjQ z(u9bQ;<2N{`{DsxqFVrfwl?1hh6ezb)1TjnFCRq^M3j-?+dHcj4huKFpJ9>!?5*~P z^BwY3hXDeDS7n!{K?cG3Ox1fbTJlF@tD2Cm>o7)1GF;2d0@|Z6yO`@ zH6x4&_}4_?O6}B=A4IJXB>)tu5LX{)DroC%C#V-Nzqmg#L&H>_s{7C?OT)lLwvCmM zk?8^yJ2B@@w?RfmX23xy?eeZ%N<(gLZhd`yVPT;l^kMEDEFt8w1&K}!))!a%gnqJ0k&(GAhNv`Xwc4Nm{FT+Ke|Pfa$-b(SB4B)q-V(0gexmW~OV=1N z>6PgIiIE{XO72z23>Jo{Gynii)33bpUu)K^Id<&0DBj#{8Do57zF<^5vv69HUZVm4 zf`AC4Qms_X85ci&)HNLkl7KKhb+*dZhwZ<8v~eTHhWWlKy~xR4(=sCdH@(qei3dTsq03zp)BI z?C*|*LaC-CF35G`Aq5aV;ei(>G?Z0ba(`%uFk}tjFw3&rx9|Am zlfN%pwk#(nhoZVxNsKYp7|A(LDaJ40qSL9-_mJ~eNXDou`$3=qJMzFReBt*$UoyFi zJ+1yFhD~_jf8Tk<+()&^419)c)PMj$5QN>kcSlD@J^l2ubLY<0*4B1eRHRi93`qOe zB3m67YJv10h)TTzwe-y<kl>RI3D7oV>KFebZjj4bl9BT zTFA?MZy2)D002M`L}q42c6RpOy?dLRo4YK75JHGhv|J#yM$qUmB5+#`Cvwn7(L&aO z<^MTJT~+7MgeAC3H`gRR`-I`>v%mj`mxI~M-&`@`>mNP!wwKb*duH`C-{u$JJ3eC0 z(jU)>E3a&f8adMT`Q{G~pLX*QKXvi*i$>Sg)EZMWya%^$+JAiHv{|W^&`{O5*|V6V z2S0CN#y$)Rd!%k!Noo~cbS3A(3J*+(8!UQd3kyH`S}|*e9*%* z2g#DI%~=kTUz}^Jn?GJp0Km5py^Y8z1!_i8T)~Xepp}s)#YL=Gy z+{>B9jfqkiiR@@N;nk(3GGDz_qspBB$`7+X{nP7vOWQQ&Deu1jE^gm=}9&FHf=r@1W+&~8{R8&d4q`1Jp9`GkKLP^_qW2U zjZlwX^y>YI>)-tK!Sfwq@pIPv;fYgKziI@~ghh^!wm-V`Z|$~@m`O`BqAVY;UE6xw z=PWZQ7_zwrNJ&Y_%gZ}@^w_3Nn|q%Agh>i9YVf<@@P~-?Aw~sMH~uKMjIZ5GO_;wh zMGqq9_4qiq{agc)FxtdGOlE}rY+*TWqD)xnhUP$>=o$A_dUCGVd6LW zJ3m}klD#xB*U())1 zHupvsup|Zr5gQ`8oacj@FeHeQB=*tPkia&5^WN%}=D11EuX^FB1(mF?T^~?#g zW{T$X8!N1ySbFaGStesm=`jz-`+UCEnu@nqzSk))LQjeJ*qciJymIxm29Cx~f5+VS zqj9I#|7OFX3QUk9qrCRN&ij0R>-mB|uH4jl-i3KeADyAh;N<@SB2lxfRNOPB00000 LNkvXXu0mjf{{oRT literal 0 HcmV?d00001 From 51435d0c3d6c53e78b86e10edc2c88fd508126ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 12 May 2020 20:05:30 +0300 Subject: [PATCH 037/130] Create HubRouteAttribute.GetRoutePatternForType() --- .../Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs index 5de916a89d..0373fbf691 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs @@ -13,6 +13,11 @@ namespace Volo.Abp.AspNetCore.SignalR RoutePattern = routePattern; } + public virtual string GetRoutePatternForType(Type hubType) + { + return RoutePattern; + } + public static string GetRoutePattern() where THub : Hub { @@ -24,7 +29,7 @@ namespace Volo.Abp.AspNetCore.SignalR var routeAttribute = hubType.GetSingleAttributeOrNull(); if (routeAttribute != null) { - return routeAttribute.RoutePattern; + return routeAttribute.GetRoutePatternForType(hubType); } return "/signalr-hubs/" + hubType.Name.RemovePostFix("Hub").ToKebabCase(); From 5651bbb8d088d63fed23405d7bd7f75e8cd02444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 12 May 2020 20:05:40 +0300 Subject: [PATCH 038/130] Update SignalR-Integration.md --- docs/en/SignalR-Integration.md | 54 +++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md index cbfa165083..585b47d76d 100644 --- a/docs/en/SignalR-Integration.md +++ b/docs/en/SignalR-Integration.md @@ -42,6 +42,8 @@ public class YourModule : AbpModule } ``` +> You don't need to use the `services.AddSignalR()` and the `app.UseEndpoints(...)`, it's done by the `AbpAspNetCoreSignalRModule`. + ### Client Side Client side installation depends on your UI framework. @@ -98,5 +100,55 @@ Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/asp ## The ABP Framework Integration -When you use the ABP Framework integration, you have some additional benefits. +This section covers the additional benefits when you use the ABP Framework integration packages. + +### Hub Route & Mapping + +ABP automatically registers Hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. + +Example: + +````csharp +public class MessagingHub : Hub +{ + //... +} +```` + +The hub route will be `/signalr-hubs/messasing` for the `MessasingHub`: + +* Adding a standard `/signalr-hubs/` prefix +* Continue with the **camel case** hub name, without the `Hub` postfix. + +If you want to specify the route, you can use the `HubRoute` attribute: + +````csharp +[HubRoute("/my-messasing-hub")] +public class MessagingHub : Hub +{ + //... +} +```` + +### AbpHub Base Class + +Instead of the standard `Hub` class, you can inherit from the `AbpHub` which has useful base properties, like `CurrentUser`. + +Example: + +````csharp +public class MessagingHub : AbpHub +{ + public async Task SendMessage(string targetUserName, string message) + { + var currentUserName = CurrentUser.UserName; //Access to the current user info + var txt = L["MyText"]; //Localization + } +} +```` + + + +## Example Application +See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messaged between (authenticated) users. \ No newline at end of file From 22938921d303b0940ac5cc86833e57bcfbd4ce18 Mon Sep 17 00:00:00 2001 From: ChangYinShung Date: Wed, 13 May 2020 01:57:46 +0800 Subject: [PATCH 039/130] add Identity Module's Localize for zh-hant add description of RequireConfirmedPhoneNumber in zh-hant --- .../Volo/Abp/Identity/Localization/zh-Hant.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json index 240b7babdf..8ad18c7c1c 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json @@ -82,6 +82,7 @@ "DisplayName:Abp.Identity.Lockout.LockoutDuration": "鎖定時間(秒)", "DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "最大失敗存取嘗試次數", "DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "要求驗證的電子信箱", + "DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "啟用手機號碼驗證", "DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "要求驗證的手機號碼", "DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "啟用使用者名稱更新", "DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "啟用電子信箱更新", @@ -95,9 +96,9 @@ "Description:Abp.Identity.Lockout.LockoutDuration": "當鎖定發生時使用者被鎖定的時間(秒).", "Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "如果啟用鎖定,當使用者被鎖定前失敗的存取嘗試次數.", "Description:Abp.Identity.SignIn.RequireConfirmedEmail": "登入時是否需要驗證電子信箱.", + "Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "使用者手機號碼是否需要驗證.", "Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "登入時是否需要驗證手機號碼.", "Description:Abp.Identity.User.IsUserNameUpdateEnabled": "是否允許使用者更新使用者名稱.", "Description:Abp.Identity.User.IsEmailUpdateEnabled": "是否允許使用者更新電子信箱." - } -} \ No newline at end of file +} From 0e492d23df67ff3a8cd7f1e4f850ccf684bc1697 Mon Sep 17 00:00:00 2001 From: Ahmet Date: Tue, 12 May 2020 21:05:25 +0300 Subject: [PATCH 040/130] 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 041/130] 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 042/130] 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 043/130] 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 044/130] 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 8dd8805c82edde1b37cfb98bb66987974d08fea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 01:23:13 +0300 Subject: [PATCH 045/130] Resolved #3912: Finalize the signalr integration document. --- docs/en/SignalR-Integration.md | 83 +++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md index 585b47d76d..5b4176b123 100644 --- a/docs/en/SignalR-Integration.md +++ b/docs/en/SignalR-Integration.md @@ -1,4 +1,4 @@ -## SignalR Integration +# SignalR Integration > It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages that simplify the integration and usage. @@ -46,7 +46,7 @@ public class YourModule : AbpModule ### Client Side -Client side installation depends on your UI framework. +Client side installation depends on your UI framework / client type. #### ASP.NET Core MVC / Razor Pages UI @@ -104,7 +104,7 @@ This section covers the additional benefits when you use the ABP Framework integ ### Hub Route & Mapping -ABP automatically registers Hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. +ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. Example: @@ -118,7 +118,7 @@ public class MessagingHub : Hub The hub route will be `/signalr-hubs/messasing` for the `MessasingHub`: * Adding a standard `/signalr-hubs/` prefix -* Continue with the **camel case** hub name, without the `Hub` postfix. +* Continue with the **camel case** hub name, without the `Hub` suffix. If you want to specify the route, you can use the `HubRoute` attribute: @@ -130,9 +130,9 @@ public class MessagingHub : Hub } ```` -### AbpHub Base Class +### AbpHub Base Classes -Instead of the standard `Hub` class, you can inherit from the `AbpHub` which has useful base properties, like `CurrentUser`. +Instead of the standard `Hub` and `Hub` classes, you can inherit from the `AbpHub` or `AbpHub` which hve useful base properties like `CurrentUser`. Example: @@ -147,7 +147,78 @@ public class MessagingHub : AbpHub } ```` +> While you could inject the same properties into your hub constructor, this way simplifies your hub class. +### Manual Registration / Mapping + +ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) as a **transient service**. If you want to **disable auto dependency injection** registration for your hub class, just add a `DisableConventionalRegistration` attribute. You can still register your hub class to dependency injection in the `ConfigureServices` method of your module if you like: + +````csharp +context.Services.AddTransient(); +```` + +When **you or ABP** register the class to the dependency injection, it is automatically mapped to the endpoint route configuration just as described in the previous sections. You can use `DisableAutoHubMap` attribute if you want to manually map your hub class. + +For manual mapping, you have two options: + +1. Use the `AbpSignalROptions` to add your map configuration (in the `ConfigureServices` method of your [module](Module-Development-Basics.md)), so ABP still performs the endpoint mapping for your hub: + +````csharp +Configure(options => +{ + options.Hubs.Add( + new HubConfig( + typeof(MessagingHub), //Hub type + "/my-messaging/route", //Hub route (URL) + hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + } + ) + ); +}); +```` + +This is a good way to provide additional SignalR options. + +If you don't want to disable auto hub map, but still want to perform additional SignalR configuration, use the `options.Hubs.AddOrUpdate(...)` method: + +````csharp +Configure(options => +{ + options.Hubs.AddOrUpdate( + typeof(MessagingHub), //Hub type + config => //Additional configuration + { + config.RoutePattern = "/my-messaging-hub"; //override the default route + config.ConfigureActions.Add(hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); + } + ); +}); +```` + +This is the way you can modify the options of a hub class defined in a depended module (where you don't have the source code access). + +2. Change `app.UseConfiguredEndpoints` in the `OnApplicationInitialization` method of your [module](Module-Development-Basics.md) as shown below (added a lambda method as the parameter). + +````csharp +app.UseConfiguredEndpoints(endpoints => +{ + endpoints.MapHub("/my-messaging-hub", options => + { + options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); +}); +```` + +### UserIdProvider + +ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP framework (see [the current user service](CurrentUser.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it. ## Example Application From 2619eada343b2a66f46323d7becde90edfe5bafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 01:24:51 +0300 Subject: [PATCH 046/130] Added HubConfigList and DisableAutoHubMapAttribute for the signalr integration. --- .../SignalR/AbpAspNetCoreSignalRModule.cs | 21 ++++++++++++---- .../AspNetCore/SignalR/AbpSignalROptions.cs | 8 +++---- .../SignalR/DisableAutoHubMapAttribute.cs | 9 +++++++ .../Volo/Abp/AspNetCore/SignalR/HubConfig.cs | 8 ++++++- .../Abp/AspNetCore/SignalR/HubConfigList.cs | 24 +++++++++++++++++++ 5 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/DisableAutoHubMapAttribute.cs create mode 100644 framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfigList.cs diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs index e26de184af..b89e5e9158 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.SignalR @@ -30,7 +31,7 @@ namespace Volo.Abp.AspNetCore.SignalR public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddSignalR(); - + Configure(options => { options.EndpointConfigureActions.Add(endpointContext => @@ -65,7 +66,7 @@ namespace Volo.Abp.AspNetCore.SignalR services.OnRegistred(context => { - if (typeof(Hub).IsAssignableFrom(context.ImplementationType)) + if (IsHubClass(context) && !IsDisabledForAutoMap(context)) { hubTypes.Add(context.ImplementationType); } @@ -80,7 +81,17 @@ namespace Volo.Abp.AspNetCore.SignalR }); } - private void MapHubType( + private static bool IsHubClass(IOnServiceRegistredContext context) + { + return typeof(Hub).IsAssignableFrom(context.ImplementationType); + } + + private static bool IsDisabledForAutoMap(IOnServiceRegistredContext context) + { + return context.ImplementationType.IsDefined(typeof(DisableAutoHubMapAttribute), true); + } + + private void MapHubType( Type hubType, IEndpointRouteBuilder endpoints, string pattern, @@ -101,8 +112,8 @@ namespace Volo.Abp.AspNetCore.SignalR // ReSharper disable once UnusedMember.Local (used via reflection) private static void MapHub( - IEndpointRouteBuilder endpoints, - string pattern, + IEndpointRouteBuilder endpoints, + string pattern, Action configureOptions) where THub : Hub { diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs index c0a07a2ea3..9adbe8d7a6 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; - -namespace Volo.Abp.AspNetCore.SignalR +namespace Volo.Abp.AspNetCore.SignalR { public class AbpSignalROptions { - public List Hubs { get; } + public HubConfigList Hubs { get; } public AbpSignalROptions() { - Hubs = new List(); + Hubs = new HubConfigList(); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/DisableAutoHubMapAttribute.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/DisableAutoHubMapAttribute.cs new file mode 100644 index 0000000000..2d2489bda9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/DisableAutoHubMapAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public class DisableAutoHubMapAttribute : Attribute + { + + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs index 59c9342fce..be2e2a6877 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs @@ -19,11 +19,17 @@ namespace Volo.Abp.AspNetCore.SignalR public HubConfig( [NotNull] Type hubType, - [NotNull] string routePattern) + [NotNull] string routePattern, + [CanBeNull] Action configureAction = null) { HubType = Check.NotNull(hubType, nameof(hubType)); RoutePattern = Check.NotNullOrWhiteSpace(routePattern, nameof(routePattern)); ConfigureActions = new List>(); + + if (configureAction != null) + { + ConfigureActions.Add(configureAction); + } } public static HubConfig Create() diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfigList.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfigList.cs new file mode 100644 index 0000000000..434e50e515 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfigList.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public class HubConfigList : List + { + public void AddOrUpdate(Action configAction = null) + { + AddOrUpdate(typeof(THub)); + } + + public void AddOrUpdate(Type hubType, Action configAction = null) + { + var hubConfig = this.GetOrAdd( + c => c.HubType == hubType, + () => HubConfig.Create(hubType) + ); + + configAction?.Invoke(hubConfig); + } + } +} \ No newline at end of file From 383d9fc59d8be2d257f04af23a8de749818271ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 02:09:08 +0300 Subject: [PATCH 047/130] Add signalr demo to samples. --- docs/en/Samples/Index.md | 3 +++ docs/en/SignalR-Integration.md | 6 ++++-- docs/en/images/signalr-demo-chat.png | Bin 0 -> 46010 bytes 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 docs/en/images/signalr-demo-chat.png diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index 0c9ce68886..12095bb2c9 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -35,6 +35,9 @@ While there is no Razor Pages & MongoDB combination, you can check both document * **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules. * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) +* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) + * [Signal Integration document](../SignalR-Integration.md) * **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI. * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [Widget documentation](../UI/AspNetCore/Widgets.md) diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md index 5b4176b123..415b585f3f 100644 --- a/docs/en/SignalR-Integration.md +++ b/docs/en/SignalR-Integration.md @@ -78,7 +78,7 @@ gulp This will copy the SignalR JavaScript files into your project: -![signal-js-file](D:\Github\abp\docs\en\images\signal-js-file.png) +![signal-js-file](images/signal-js-file.png) Finally, add the following code to your page/view to include the `signalr.js` file @@ -222,4 +222,6 @@ ABP implements SignalR's `IUserIdProvider` interface to provide the current user ## Example Application -See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messaged between (authenticated) users. \ No newline at end of file +See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messages between (authenticated) users. + +![signalr-demo-chat](images/signalr-demo-chat.png) \ No newline at end of file diff --git a/docs/en/images/signalr-demo-chat.png b/docs/en/images/signalr-demo-chat.png new file mode 100644 index 0000000000000000000000000000000000000000..120d7eda6ed54fd4e6a90ef31cfd93f4fc9e4b43 GIT binary patch literal 46010 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 C2)D3+kz51_-t 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 G`+ot40&F<| literal 0 HcmV?d00001 From cd3d1e57bd3d0f4ccd09c1de38a967a141c19828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 02:49:13 +0300 Subject: [PATCH 048/130] Create signalr test project. --- framework/Volo.Abp.sln | 7 +++++ .../Volo.Abp.AspNetCore.SignalR.Tests.csproj | 20 ++++++++++++++ .../SignalR/AbpAspNetCoreSignalRTestBase.cs | 12 +++++++++ .../SignalR/AbpAspNetCoreSignalRTestModule.cs | 15 +++++++++++ .../SignalR/AbpSignalROptions_Tests.cs | 26 +++++++++++++++++++ .../SampleHubs/DisableAutoHubMapHub.cs | 10 +++++++ .../DisableConventionalRegistrationHub.cs | 11 ++++++++ .../SignalR/SampleHubs/RegularAbpHub.cs | 6 +++++ .../SignalR/SampleHubs/RegularHub.cs | 8 ++++++ 9 files changed, 115 insertions(+) create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestModule.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions_Tests.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableAutoHubMapHub.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableConventionalRegistrationHub.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularAbpHub.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularHub.cs diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 3ff151439f..2db0442389 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -287,6 +287,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Validation.Abstrac EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR", "src\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj", "{B64FCE08-E9D2-4984-BF12-FE199F257416}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.SignalR.Tests", "test\Volo.Abp.AspNetCore.SignalR.Tests\Volo.Abp.AspNetCore.SignalR.Tests.csproj", "{8B758716-DCC9-4223-8421-5588D1597487}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -853,6 +855,10 @@ Global {B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.Build.0 = Debug|Any CPU {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.ActiveCfg = Release|Any CPU {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.Build.0 = Release|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -998,6 +1004,7 @@ Global {251C7FD3-D313-4BCE-8068-352EC7EEA275} = {447C8A77-E5F0-4538-8687-7383196D04EA} {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {B64FCE08-E9D2-4984-BF12-FE199F257416} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {8B758716-DCC9-4223-8421-5588D1597487} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj new file mode 100644 index 0000000000..8e1ab29329 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj @@ -0,0 +1,20 @@ + + + + + + netcoreapp3.1 + + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs new file mode 100644 index 0000000000..c7a31c8b8a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Testing; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public class AbpAspNetCoreSignalRTestBase : AbpIntegratedTest + { + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestModule.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestModule.cs new file mode 100644 index 0000000000..eee1dd6ed3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestModule.cs @@ -0,0 +1,15 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.SignalR +{ + [DependsOn( + typeof(AbpAspNetCoreSignalRModule), + typeof(AbpTestBaseModule), + typeof(AbpAutofacModule) + )] + public class AbpAspNetCoreSignalRTestModule : AbpModule + { + + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions_Tests.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions_Tests.cs new file mode 100644 index 0000000000..38e262abe3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions_Tests.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.AspNetCore.SignalR.SampleHubs; +using Xunit; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public class AbpSignalROptions_Tests : AbpAspNetCoreSignalRTestBase + { + private readonly AbpSignalROptions _options; + + public AbpSignalROptions_Tests() + { + _options = GetRequiredService>().Value; + } + + [Fact] + public void Should_Auto_Add_Maps() + { + _options.Hubs.ShouldContain(h => h.HubType == typeof(RegularHub)); + _options.Hubs.ShouldContain(h => h.HubType == typeof(RegularAbpHub)); + _options.Hubs.ShouldNotContain(h => h.HubType == typeof(DisableConventionalRegistrationHub)); + _options.Hubs.ShouldNotContain(h => h.HubType == typeof(DisableAutoHubMapHub)); + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableAutoHubMapHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableAutoHubMapHub.cs new file mode 100644 index 0000000000..3ceae70af4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableAutoHubMapHub.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + [DisableAutoHubMap] + public class DisableAutoHubMapHub : Hub + { + + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableConventionalRegistrationHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableConventionalRegistrationHub.cs new file mode 100644 index 0000000000..ff28b6eca9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableConventionalRegistrationHub.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.SignalR; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + [DisableConventionalRegistration] + public class DisableConventionalRegistrationHub : Hub + { + + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularAbpHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularAbpHub.cs new file mode 100644 index 0000000000..2afceffa8d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularAbpHub.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + public class RegularAbpHub : AbpHub + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularHub.cs new file mode 100644 index 0000000000..8d3987f314 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularHub.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + public class RegularHub : Hub + { + } +} \ No newline at end of file From 3154fbcbfe620ee6e9014fec5f71bff04be15c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 02:50:33 +0300 Subject: [PATCH 049/130] Make AbpAspNetCoreSignalRTestBase abstract. --- .../Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs index c7a31c8b8a..eb3cb0c265 100644 --- a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs @@ -2,7 +2,7 @@ namespace Volo.Abp.AspNetCore.SignalR { - public class AbpAspNetCoreSignalRTestBase : AbpIntegratedTest + public abstract class AbpAspNetCoreSignalRTestBase : AbpIntegratedTest { protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) { From 9ddd879b6727b41b8f0aafe92f47f63569ad42f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 05:22:33 +0300 Subject: [PATCH 050/130] Fixed #3915: Use SemaphoreSlim in the LocalizedTemplateContentReaderFactory --- .../LocalizedTemplateContentReaderFactory.cs | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs index 092815ff6e..b403b37068 100644 --- a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs +++ b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs @@ -1,6 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; +using Nito.AsyncEx; using Volo.Abp.DependencyInjection; using Volo.Abp.VirtualFileSystem; @@ -9,50 +10,33 @@ namespace Volo.Abp.TextTemplating.VirtualFiles public class LocalizedTemplateContentReaderFactory : ILocalizedTemplateContentReaderFactory, ISingletonDependency { private readonly IVirtualFileProvider _virtualFileProvider; - private readonly Dictionary _readerCache; - private readonly ReaderWriterLockSlim _lock; + private readonly ConcurrentDictionary _readerCache; + protected SemaphoreSlim SyncObj; public LocalizedTemplateContentReaderFactory(IVirtualFileProvider virtualFileProvider) { _virtualFileProvider = virtualFileProvider; - _readerCache = new Dictionary(); - _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + _readerCache = new ConcurrentDictionary(); + SyncObj = new SemaphoreSlim(1, 1); } public async Task CreateAsync(TemplateDefinition templateDefinition) { - _lock.EnterUpgradeableReadLock(); + if (_readerCache.TryGetValue(templateDefinition.Name, out var reader)) + { + return reader; + } - try + using (await SyncObj.LockAsync()) { - var reader = _readerCache.GetOrDefault(templateDefinition.Name); - if (reader != null) + if (_readerCache.TryGetValue(templateDefinition.Name, out reader)) { return reader; } - _lock.EnterWriteLock(); - - try - { - reader = await CreateInternalAsync(templateDefinition); - _readerCache[templateDefinition.Name] = reader; - return reader; - } - finally - { - if (_lock.IsWriteLockHeld) - { - _lock.ExitWriteLock(); - } - } - } - finally - { - if (_lock.IsUpgradeableReadLockHeld) - { - _lock.ExitUpgradeableReadLock(); - } + reader = await CreateInternalAsync(templateDefinition); + _readerCache[templateDefinition.Name] = reader; + return reader; } } From eb59629d21f90accd3f36867b97b379981a8f17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 13 May 2020 05:25:43 +0300 Subject: [PATCH 051/130] LocalizedTemplateContentReaderFactory: Make CreateAsync method virtual and private fields protected to easier extend. --- .../LocalizedTemplateContentReaderFactory.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs index b403b37068..02b857bf5d 100644 --- a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs +++ b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs @@ -9,33 +9,33 @@ namespace Volo.Abp.TextTemplating.VirtualFiles { public class LocalizedTemplateContentReaderFactory : ILocalizedTemplateContentReaderFactory, ISingletonDependency { - private readonly IVirtualFileProvider _virtualFileProvider; - private readonly ConcurrentDictionary _readerCache; + protected IVirtualFileProvider VirtualFileProvider { get; } + protected ConcurrentDictionary ReaderCache { get; } protected SemaphoreSlim SyncObj; public LocalizedTemplateContentReaderFactory(IVirtualFileProvider virtualFileProvider) { - _virtualFileProvider = virtualFileProvider; - _readerCache = new ConcurrentDictionary(); + VirtualFileProvider = virtualFileProvider; + ReaderCache = new ConcurrentDictionary(); SyncObj = new SemaphoreSlim(1, 1); } - public async Task CreateAsync(TemplateDefinition templateDefinition) + public virtual async Task CreateAsync(TemplateDefinition templateDefinition) { - if (_readerCache.TryGetValue(templateDefinition.Name, out var reader)) + if (ReaderCache.TryGetValue(templateDefinition.Name, out var reader)) { return reader; } using (await SyncObj.LockAsync()) { - if (_readerCache.TryGetValue(templateDefinition.Name, out reader)) + if (ReaderCache.TryGetValue(templateDefinition.Name, out reader)) { return reader; } reader = await CreateInternalAsync(templateDefinition); - _readerCache[templateDefinition.Name] = reader; + ReaderCache[templateDefinition.Name] = reader; return reader; } } @@ -49,7 +49,7 @@ namespace Volo.Abp.TextTemplating.VirtualFiles return NullLocalizedTemplateContentReader.Instance; } - var fileInfo = _virtualFileProvider.GetFileInfo(virtualPath); + var fileInfo = VirtualFileProvider.GetFileInfo(virtualPath); if (!fileInfo.Exists) { throw new AbpException("Could not find a file/folder at the location: " + virtualPath); @@ -58,7 +58,7 @@ namespace Volo.Abp.TextTemplating.VirtualFiles if (fileInfo.IsDirectory) { var folderReader = new VirtualFolderLocalizedTemplateContentReader(); - await folderReader.ReadContentsAsync(_virtualFileProvider, virtualPath); + await folderReader.ReadContentsAsync(VirtualFileProvider, virtualPath); return folderReader; } else //File From cc1408521f0b422cf73cb298b8b647262e58d4ba Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 13 May 2020 12:03:08 +0800 Subject: [PATCH 052/130] Make AbpPaginationTagHelper generate correct href attribute --- .../en/UI/AspNetCore/Tag-Helpers/Paginator.md | 2 +- .../UI/AspNetCore/Tag-Helpers/Paginator.md | 2 +- .../AbpPaginationTagHelperService.cs | 21 ++++++++++++++++++- .../TagHelpers/Pagination/PagerModel.cs | 5 +++++ .../Pages/Components/Paginator.cshtml | 6 +++--- .../Pages/Components/Paginator.cshtml.cs | 4 ++-- .../Pages/Components/Paginator/Index.cshtml | 7 +------ .../Components/Paginator/Index.cshtml.cs | 4 ++-- .../Pages/Components/Paginator/Index.js | 9 -------- 9 files changed, 35 insertions(+), 25 deletions(-) delete mode 100644 framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md index 0cc63d49e8..9969bcf3cc 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md index 7a9b3de126..945da6fa67 100644 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } 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 2ab9ccef96..76bfcb3ef1 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 @@ -1,4 +1,6 @@ -using System.Text; +using System; +using System.Linq; +using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using Localization.Resources.AbpUi; @@ -123,6 +125,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination var tagHelperOutput = await anchorTagHelper.ProcessAndGetOutputAsync(attributeList, context, "a", TagMode.StartTagAndEndTag); + SetHrefAttribute(currentPage, attributeList); + tagHelperOutput.Content.SetHtmlContent(localizer[localizationKey]); var renderedHtml = tagHelperOutput.Render(_encoder); @@ -172,5 +176,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination " \r\n" + "

\r\n"; } + + protected virtual void SetHrefAttribute(string currentPage, TagHelperAttributeList attributeList) + { + var hrefAttribute = attributeList.FirstOrDefault(x => x.Name.Equals("href", StringComparison.OrdinalIgnoreCase)); + + if (hrefAttribute != null) + { + var pageUrl = TagHelper.Model.PageUrl; + var routeValue = $"currentPage={currentPage}{(TagHelper.Model.Sort.IsNullOrWhiteSpace()? "" : "&sort="+TagHelper.Model.Sort)}"; + pageUrl += pageUrl.Contains("?") ? "&" + routeValue : "?" + routeValue; + + attributeList.Remove(hrefAttribute); + attributeList.Add(new TagHelperAttribute("href", pageUrl, hrefAttribute.ValueStyle)); + } + } } } \ No newline at end of file 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 8c6eeb34a4..d071db223a 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 @@ -38,6 +38,11 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination PageSize = pageSize; TotalPageCount = (int)Math.Ceiling(Convert.ToDouble((decimal)TotalItemsCount / PageSize)); Sort = sort; + + if (pageUrl.IsNullOrWhiteSpace() || !pageUrl.StartsWith("/")) + { + pageUrl = "/" + pageUrl; + } PageUrl = pageUrl; if (currentPage > TotalPageCount) diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml index be26fb5251..b13e1bc4ef 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml @@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } @@ -60,7 +60,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components

-<div class="row mt-3">    
+<div class="row mt-3">
     <div class="col-sm-12 col-md-5">
         Showing 80 to 90 of 100 entries.
     </div>
@@ -105,4 +105,4 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
             
         
     
-
\ No newline at end of file +
diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs index 15b31db6da..3723631381 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml index 6eb48dcd43..d987bce207 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml @@ -7,14 +7,9 @@ @{ PageLayout.Content.Title = "Paginator"; } -@section scripts { - - - -}

Paginator

Check the ABP Documentation.

-@await Component.InvokeAsync(typeof(PaginatorDemoViewComponent), new { pagerModel = Model.PagerModel }) \ No newline at end of file +@await Component.InvokeAsync(typeof(PaginatorDemoViewComponent), new { pagerModel = Model.PagerModel }) diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs index 50659a0ab4..bc51f80f0a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Paginator public void OnGet(int currentPage = 1, string sort = null) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js deleted file mode 100644 index 843e0dea00..0000000000 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js +++ /dev/null @@ -1,9 +0,0 @@ -$(function () { - var links = $("a.page-link"); - - $.each(links, function (key, value) { - var oldUrl = links[key].getAttribute("href"); - var value = Number(oldUrl.match(/currentPage=(\d+)&page/)[1]); - links[key].setAttribute("href", "/Components/Paginator?currentPage=" + value); - }) -}); \ 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 053/130] 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 ae3ae56d6d09d7b928d1bdd5858f63225a7025de Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 13 May 2020 16:48:50 +0800 Subject: [PATCH 054/130] Update PagerModel.cs --- .../TagHelpers/Pagination/PagerModel.cs | 6 +----- 1 file changed, 1 insertion(+), 5 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 d071db223a..db74266f38 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 @@ -39,11 +39,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination TotalPageCount = (int)Math.Ceiling(Convert.ToDouble((decimal)TotalItemsCount / PageSize)); Sort = sort; - if (pageUrl.IsNullOrWhiteSpace() || !pageUrl.StartsWith("/")) - { - pageUrl = "/" + pageUrl; - } - PageUrl = pageUrl; + PageUrl = pageUrl?.EnsureStartsWith('/') ?? "/"; if (currentPage > TotalPageCount) { From 5b91c8954beae73903cb9a405ffe42d5374b86fa Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 13 May 2020 18:13:05 +0800 Subject: [PATCH 055/130] 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 056/130] 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 057/130] 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 058/130] 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 059/130] 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 060/130] 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 061/130] 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 062/130] 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 0f254f11b04d579f4afd888b5ceb69e79a3c5647 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Thu, 14 May 2020 00:17:13 +0800 Subject: [PATCH 063/130] Translate signalr integration document --- docs/en/Samples/Index.md | 2 +- docs/en/SignalR-Integration.md | 2 +- docs/zh-Hans/Samples/Index.md | 3 + docs/zh-Hans/SignalR-Integration.md | 227 ++++++++++++++++++++++ docs/zh-Hans/images/signal-js-file.png | Bin 0 -> 57358 bytes docs/zh-Hans/images/signalr-demo-chat.png | Bin 0 -> 46010 bytes 6 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 docs/zh-Hans/SignalR-Integration.md create mode 100644 docs/zh-Hans/images/signal-js-file.png create mode 100644 docs/zh-Hans/images/signalr-demo-chat.png diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index 12095bb2c9..b5fae6cbce 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -37,7 +37,7 @@ While there is no Razor Pages & MongoDB combination, you can check both document * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) * **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. * [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) - * [Signal Integration document](../SignalR-Integration.md) + * [SignalR Integration document](../SignalR-Integration.md) * **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI. * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [Widget documentation](../UI/AspNetCore/Widgets.md) diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md index 415b585f3f..0a2ebdaa54 100644 --- a/docs/en/SignalR-Integration.md +++ b/docs/en/SignalR-Integration.md @@ -25,7 +25,7 @@ If you want to manually install; 1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project: ``` - Install-Package Volo.Abp.BackgroundJobs.HangFire + Install-Package Volo.Abp.AspNetCore.SignalR ``` Or use the Visual Studio NuGet package management UI to install it. diff --git a/docs/zh-Hans/Samples/Index.md b/docs/zh-Hans/Samples/Index.md index 774d6467ef..97569f2ae2 100644 --- a/docs/zh-Hans/Samples/Index.md +++ b/docs/zh-Hans/Samples/Index.md @@ -35,6 +35,9 @@ * **Entity Framework 迁移**: 演示如何将应用程序拆分为多个数据库的解决方案. 每个数据库包含不同的模块. * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [EF Core数据库迁移文档](../Entity-Framework-Core-Migrations.md) +* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. + * [源码](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) + * [SignalR 集成文档](../SignalR-Integration.md) * **Dashboard Demo**: 一个简单的应用程序,展示了如何在ASP.NET Core MVC UI中使用widget系统. * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [Widget 文档](../UI/AspNetCore/Widgets.md) diff --git a/docs/zh-Hans/SignalR-Integration.md b/docs/zh-Hans/SignalR-Integration.md new file mode 100644 index 0000000000..8045bbd394 --- /dev/null +++ b/docs/zh-Hans/SignalR-Integration.md @@ -0,0 +1,227 @@ +# SignalR 集成 + +> 你可以按照[标准的微软教程](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signal)添加[SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)到你的应用程序,但ABP提供了简化集成的SignalR集成包. + +## 安装 + +### 服务器端 + +建议使用[ABP CLI](CLI.md)安装包. + +#### 使用 ABP CLI + +在项目的文件夹(.csproj文件)中打开命令行窗口,然后输入以下命令: + +```bash +abp add-package Volo.Abp.AspNetCore.SignalR +``` + +> 你通常需要将此软件包添加到应用程序的Web或API层,具体取决于你的架构. + +#### 手动安装 + +如果你想手动安装: + +1. 添加[Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR)NuGet包到你的项目: + + ``` + Install-Package Volo.Abp.AspNetCore.SignalR + ``` + + 或者使用VisualStudio提供的UI安装 + +2. 添加 `AbpAspNetCoreSignalRModule` 到你的模块的依赖列表. + +```csharp +[DependsOn( + //...other dependencies + typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +``` + +> 你不需要 `services.AddSignalR()` 和 `app.UseEndpoints(...)`,它们在 `AbpAspNetCoreSignalRModule` 中已经添加了. + +### 客户端 + +客户端安装取决于你的UI框架/客户端类型. + +#### ASP.NET Core MVC / Razor Pages UI + +在你的Web项目的根文件夹中运行以下命令: + +````bash +yarn add @abp/signalr +```` + +> 需要 [yarn](https://yarnpkg.com/) 环境. + +它会添加 `@abp/signalr` 到你的项目中的 `package.json` 依赖项: + +````json +{ + ... + "dependencies": { + ... + "@abp/signalr": "~2.7.0" + } +} +```` + +在你的Web项目的根文件夹中运行 `gulp`: + +````bash +gulp +```` + +它会将SignalR JavaScript文件拷贝到你的项目: + +![signal-js-file](images/signal-js-file.png) + +最后将以下代码添加到页面/视图中, 添加包含 `signalr.js` 文件: + +````xml +@section scripts { + +} +```` + +它需要将 `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` 添加到你的页面/视图. + +> 你可以用标准方式添加 `signalr.js` 文件. 但是使用 `SignalRBrowserScriptContributor` 具有其他好处. 有关详细信息,请参见[客户端程序包管理](UI/AspNetCore/Client-Side-Package-Management.md)和[捆绑和压缩文档](UI/AspNetCore/Bundling-Minification.md). + +这就是全部了,你可以在你的页面使用[SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client). + +#### 其他的UI框架/客户端 + +其他类型的客户端请参考[微软文档](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction). + +## ABP框架集成 + +本节介绍了使用ABP框架集成包的其他好处. + +### Hub 路由与Mapping + +ABP自动将所有集线器注册到[依赖注入](Dependency-Injection.md)(做为transient)并映射集线器端点. 因此你不需要使用 `app.UseEndpoints(...)` 即可映射你的集线器.集线器路由(URL)通常是根据你的集线器名称确定. + +示例: + +````csharp +public class MessagingHub : Hub +{ + //... +} +```` + +`MessasingHub` 集线器的路由为 `/signalr-hubs/messasing`: + +* 添加了标准 `/signalr-hubs/` 前缀. +* 使用**驼峰命名**集线器名称,不包含 `Hub` 后缀. + +如果你想指定路由,你可以使用 `HubRoute` attribute: + +````csharp +[HubRoute("/my-messasing-hub")] +public class MessagingHub : Hub +{ + //... +} +```` + +### AbpHub 基类 + +你可以从 `AbpHub` 或 `AbpHub` 继承标准的 `Hub` 和 `Hub` 类,它们具有实用的基本属性,如 `CurrentUser`. + +示例: + +````csharp +public class MessagingHub : AbpHub +{ + public async Task SendMessage(string targetUserName, string message) + { + var currentUserName = CurrentUser.UserName; //Access to the current user info + var txt = L["MyText"]; //Localization + } +} +```` + +> 虽然可以将相同的属性注入到集线器构造函数中,但是这种方式简化了集线器类. + +### 手动注册/Mapping + +ABP会自动将所有集线器注册到[依赖注入](Dependency-Injection.md)作为**transient service**. 如果想要禁用集线器类**自动添加依赖注入**,只需要使用 `DisableConventionalRegistration` attribute. 如果愿意,你仍然可以在模块的 `ConfigureServices` 方法中注册集线器类: + +````csharp +context.Services.AddTransient(); +```` + +当**你或ABP**将类注册到依赖注入时,如前几节所述,它会自动映射到端点路由配置. 如果要手动映射集线器类,你可以使用 `DisableAutoHubMap` attribute. + +对于手动映射,你有两个选择: + +1. 使用 `AbpSignalROptions` 添加map配置(在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中),ABP会为集线器执行端点映射: + +````csharp +Configure(options => +{ + options.Hubs.Add( + new HubConfig( + typeof(MessagingHub), //Hub type + "/my-messaging/route", //Hub route (URL) + hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + } + ) + ); +}); +```` + +这是提供其他SignalR选项的好方式. + +如果你不想禁用自动集线器map,但仍想执行其他SignalR配置,可以使用 `options.Hubs.AddOrUpdate(...)` 方法: + +````csharp +Configure(options => +{ + options.Hubs.AddOrUpdate( + typeof(MessagingHub), //Hub type + config => //Additional configuration + { + config.RoutePattern = "/my-messaging-hub"; //override the default route + config.ConfigureActions.Add(hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); + } + ); +}); +```` + +你可以通过这种方式修改在依赖模块(没有源代码访问权限)中定义的集线器类的选项. + +2. 在[模块](Module-Development-Basics.md)的 `OnApplicationInitialization` 方法中更改 `app.UseConfiguredEndpoints`(添加了lambda方法作为参数). + +````csharp +app.UseConfiguredEndpoints(endpoints => +{ + endpoints.MapHub("/my-messaging-hub", options => + { + options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); +}); +```` + +### UserIdProvider + +ABP实现 `SignalR` 的 `IUserIdProvider` 接口,从ABP框架的 `ICurrentUser` 服务提供当前用户ID(请参阅[当前用户服务](CurrentUser.md)),它将集成到应用程序的身份验证系统中,实现类是 `AbpSignalRUserIdProvider` (如果你想更改/覆盖它). + +## 示例应用程序 + +参阅 [SignalR集成Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo),它有一个简单的聊天页面,可以在(经过身份验证的)用户之间发送消息. + +![signalr-demo-chat](images/signalr-demo-chat.png) \ No newline at end of file diff --git a/docs/zh-Hans/images/signal-js-file.png b/docs/zh-Hans/images/signal-js-file.png new file mode 100644 index 0000000000000000000000000000000000000000..19fc6cefe3480204c2f0a31131cb50f1b2690d1d GIT binary patch literal 57358 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 LNkvXXu0mjf{{oRT literal 0 HcmV?d00001 diff --git a/docs/zh-Hans/images/signalr-demo-chat.png b/docs/zh-Hans/images/signalr-demo-chat.png new file mode 100644 index 0000000000000000000000000000000000000000..120d7eda6ed54fd4e6a90ef31cfd93f4fc9e4b43 GIT binary patch literal 46010 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 C2)D3+kz51_-t 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 G`+ot40&F<| literal 0 HcmV?d00001 From 8d2dc2fa65f36758174f9b58cf84d8578ef945bf Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 13 May 2020 19:43:13 +0300 Subject: [PATCH 064/130] 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 065/130] 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 066/130] 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 067/130] 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 068/130] 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 069/130] 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 070/130] 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 071/130] 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 072/130] 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 073/130] 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 074/130] 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 075/130] 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 076/130] 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 077/130] 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 078/130] 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 079/130] 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 080/130] 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 081/130] 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 082/130] 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 083/130] 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 084/130] 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 085/130] 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 086/130] 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 087/130] 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 088/130] 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 089/130] 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 090/130] 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 091/130] 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 092/130] 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 093/130] 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 094/130] 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 095/130] 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 096/130] 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 097/130] 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 098/130] 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 099/130] 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 100/130] 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 101/130] 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 102/130] 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 103/130] 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 104/130] 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 105/130] 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 106/130] 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 107/130] 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 108/130] 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 109/130] 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 110/130] 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 111/130] 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 112/130] 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 113/130] 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 114/130] 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 115/130] #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 116/130] 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 117/130] 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 118/130] 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 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 119/130] 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 120/130] 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 121/130] 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 122/130] 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 123/130] 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 124/130] 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 125/130] 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 126/130] 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 127/130] 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 128/130] 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 129/130] 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 130/130] 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; } + } +}