diff --git a/docs/en/UI/Angular/Form-Validation.md b/docs/en/UI/Angular/Form-Validation.md index 38e7969eea..a178fa213c 100644 --- a/docs/en/UI/Angular/Form-Validation.md +++ b/docs/en/UI/Angular/Form-Validation.md @@ -6,23 +6,52 @@ Reactive forms in ABP Angular UI are validated by [ngx-validate](https://www.npm ## How to Add New Error Messages -You can add a new error message by providing the `VALIDATION_BLUEPRINTS` injection token from your root module. +You can add a new error message by passing validation options to the `ThemeSharedModule` in your root module. ```js import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; @NgModule({ + imports: [ + ThemeSharedModule.forRoot({ + validation: { + blueprints: { + uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", + }, + }, + + // rest of theme shared config + }), + + // other imports + ], + // rest of the module metadata +}) +export class AppModule {} +``` + +Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ providers: [ - // other providers { provide: VALIDATION_BLUEPRINTS, useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", }, }, + + // other providers ], + + // rest of the module metadata }) export class AppModule {} ``` @@ -40,7 +69,7 @@ In this example; ## How to Change Existing Error Messages -You can overwrite an existing error message by providing `VALIDATION_BLUEPRINTS` injection token from your root module. Let's imagine you have a custom localization resource for required inputs. +You can overwrite an existing error message by passing validation options to the `ThemeSharedModule` in your root module. Let's imagine you have a custom localization resource for required inputs. ```json "RequiredInput": "Oops! We need this input." @@ -50,19 +79,48 @@ To use this instead of the built-in required input message, all you need to do i ```js import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; @NgModule({ + imports: [ + ThemeSharedModule.forRoot({ + validation: { + blueprints: { + required: "::RequiredInput", + }, + }, + + // rest of theme shared config + }), + + // other imports + ], + // rest of the module metadata +}) +export class AppModule {} +``` + +Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ providers: [ - // other providers { provide: VALIDATION_BLUEPRINTS, useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, required: "::RequiredInput", }, }, + + // other providers ], + + // rest of the module metadata }) export class AppModule {} ``` diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs index ee9040df66..8cfbe5cb94 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs @@ -205,9 +205,15 @@ namespace Volo.Abp.Cli.ProjectModification string postFix) { var srcPath = Path.Combine(Path.GetDirectoryName(moduleSolutionFile), targetFolder); + + if (!Directory.Exists(srcPath)) + { + return; + } + var projectFolderPath = Directory.GetDirectories(srcPath).FirstOrDefault(d=> d.EndsWith(postFix)); - if(projectFolderPath == null) + if (projectFolderPath == null) { return; } @@ -218,7 +224,6 @@ namespace Volo.Abp.Cli.ProjectModification { Directory.Delete(projectFolderPath, true); } - } private async Task ChangeDomainTestReferenceToMongoDB(ModuleWithMastersInfo module, string moduleSolutionFile) diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs index 31c9de0e18..69fae63d71 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs @@ -85,6 +85,7 @@ namespace Volo.Abp.Identity (await UserManager.CreateAsync(user, input.Password)).CheckErrors(); await UpdateUserByInput(user, input); + (await UserManager.UpdateAsync(user)).CheckErrors(); await CurrentUnitOfWork.SaveChangesAsync(); @@ -174,6 +175,7 @@ namespace Volo.Abp.Identity user.Name = input.Name; user.Surname = input.Surname; + (await UserManager.UpdateAsync(user)).CheckErrors(); if (input.RoleNames != null) { diff --git a/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts b/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts index a947809623..be08f78e5b 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts @@ -2,18 +2,22 @@ import { CoreModule } from '@abp/ng.core'; import { ThemeSharedModule } from '@abp/ng.theme.shared'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { NgbCollapseModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; -import { NgxValidateCoreModule } from '@ngx-validate/core'; +import { + NgxValidateCoreModule, + VALIDATION_ERROR_TEMPLATE, + VALIDATION_TARGET_SELECTOR, +} from '@ngx-validate/core'; import { AccountLayoutComponent } from './components/account-layout/account-layout.component'; import { ApplicationLayoutComponent } from './components/application-layout/application-layout.component'; import { EmptyLayoutComponent } from './components/empty-layout/empty-layout.component'; import { LogoComponent } from './components/logo/logo.component'; +import { CurrentUserComponent } from './components/nav-items/current-user.component'; +import { LanguagesComponent } from './components/nav-items/languages.component'; import { NavItemsComponent } from './components/nav-items/nav-items.component'; import { RoutesComponent } from './components/routes/routes.component'; import { ValidationErrorComponent } from './components/validation-error/validation-error.component'; -import { CurrentUserComponent } from './components/nav-items/current-user.component'; -import { LanguagesComponent } from './components/nav-items/languages.component'; -import { BASIC_THEME_STYLES_PROVIDERS } from './providers/styles.provider'; import { BASIC_THEME_NAV_ITEM_PROVIDERS } from './providers/nav-item.provider'; +import { BASIC_THEME_STYLES_PROVIDERS } from './providers/styles.provider'; export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, EmptyLayoutComponent]; @@ -45,37 +49,28 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt ], entryComponents: [...LAYOUTS, ValidationErrorComponent, CurrentUserComponent, LanguagesComponent], }) +export class BaseThemeBasicModule {} + +@NgModule({ + exports: [BaseThemeBasicModule], + imports: [BaseThemeBasicModule], +}) export class ThemeBasicModule { static forRoot(): ModuleWithProviders { return { - ngModule: RootThemeBasicModule, - providers: [BASIC_THEME_NAV_ITEM_PROVIDERS, BASIC_THEME_STYLES_PROVIDERS], + ngModule: ThemeBasicModule, + providers: [ + BASIC_THEME_NAV_ITEM_PROVIDERS, + BASIC_THEME_STYLES_PROVIDERS, + { + provide: VALIDATION_ERROR_TEMPLATE, + useValue: ValidationErrorComponent, + }, + { + provide: VALIDATION_TARGET_SELECTOR, + useValue: '.form-group', + }, + ], }; } } - -@NgModule({ - imports: [ - NgxValidateCoreModule.forRoot({ - targetSelector: '.form-group', - blueprints: { - creditCard: 'AbpValidation::ThisFieldIsNotAValidCreditCardNumber.', - email: 'AbpValidation::ThisFieldIsNotAValidEmailAddress.', - invalid: 'AbpValidation::ThisFieldIsNotValid.', - max: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]', - maxlength: - 'AbpValidation::ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}[{{ requiredLength }}]', - min: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]', - minlength: - 'AbpValidation::ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}[{{ requiredLength }}]', - ngbDate: 'AbpValidation::ThisFieldIsNotValid.', - passwordMismatch: 'AbpIdentity::Volo.Abp.Identity:PasswordConfirmationFailed', - range: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]', - required: 'AbpValidation::ThisFieldIsRequired.', - url: 'AbpValidation::ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl', - }, - errorTemplate: ValidationErrorComponent, - }), - ], -}) -export class RootThemeBasicModule {} diff --git a/npm/ng-packs/packages/theme-basic/testing/ng-package.json b/npm/ng-packs/packages/theme-basic/testing/ng-package.json new file mode 100644 index 0000000000..c539597da7 --- /dev/null +++ b/npm/ng-packs/packages/theme-basic/testing/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/theme-basic/testing", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/npm/ng-packs/packages/theme-basic/testing/src/lib/theme-basic-testing.module.ts b/npm/ng-packs/packages/theme-basic/testing/src/lib/theme-basic-testing.module.ts new file mode 100644 index 0000000000..7df6d40231 --- /dev/null +++ b/npm/ng-packs/packages/theme-basic/testing/src/lib/theme-basic-testing.module.ts @@ -0,0 +1,32 @@ +import { + BaseThemeBasicModule, + BASIC_THEME_NAV_ITEM_PROVIDERS, + BASIC_THEME_STYLES_PROVIDERS, + ValidationErrorComponent, +} from '@abp/ng.theme.basic'; +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { VALIDATION_ERROR_TEMPLATE, VALIDATION_TARGET_SELECTOR } from '@ngx-validate/core'; + +@NgModule({ + exports: [BaseThemeBasicModule], + imports: [BaseThemeBasicModule], +}) +export class ThemeBasicTestingModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: ThemeBasicTestingModule, + providers: [ + BASIC_THEME_NAV_ITEM_PROVIDERS, + BASIC_THEME_STYLES_PROVIDERS, + { + provide: VALIDATION_ERROR_TEMPLATE, + useValue: ValidationErrorComponent, + }, + { + provide: VALIDATION_TARGET_SELECTOR, + useValue: '.form-group', + }, + ], + }; + } +} diff --git a/npm/ng-packs/packages/theme-basic/testing/src/public-api.ts b/npm/ng-packs/packages/theme-basic/testing/src/public-api.ts new file mode 100644 index 0000000000..24a3ac7586 --- /dev/null +++ b/npm/ng-packs/packages/theme-basic/testing/src/public-api.ts @@ -0,0 +1 @@ +export * from './lib/theme-basic-testing.module'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/constants/validation.ts b/npm/ng-packs/packages/theme-shared/src/lib/constants/validation.ts new file mode 100644 index 0000000000..463a7b01dc --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/constants/validation.ts @@ -0,0 +1,16 @@ +export const DEFAULT_VALIDATION_BLUEPRINTS = { + creditCard: 'AbpValidation::ThisFieldIsNotAValidCreditCardNumber.', + email: 'AbpValidation::ThisFieldIsNotAValidEmailAddress.', + invalid: 'AbpValidation::ThisFieldIsNotValid.', + max: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]', + maxlength: + 'AbpValidation::ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}[{{ requiredLength }}]', + min: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]', + minlength: + 'AbpValidation::ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}[{{ requiredLength }}]', + ngbDate: 'AbpValidation::ThisFieldIsNotValid.', + passwordMismatch: 'AbpIdentity::Volo.Abp.Identity:PasswordConfirmationFailed', + range: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]', + required: 'AbpValidation::ThisFieldIsRequired.', + url: 'AbpValidation::ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl', +}; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts index 09171179c4..64047e0db7 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts @@ -1,7 +1,9 @@ import { Type } from '@angular/core'; +import { Validation } from '@ngx-validate/core'; export interface RootParams { httpErrorConfig: HttpErrorConfig; + validation?: Partial; } export type ErrorScreenErrorCodes = 401 | 403 | 404 | 500; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts index 6775c15e5a..037faee882 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts @@ -2,7 +2,13 @@ import { CoreModule, noop } from '@abp/ng.core'; import { DatePipe } from '@angular/common'; import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core'; import { NgbDateParserFormatter, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; -import { NgxValidateCoreModule } from '@ngx-validate/core'; +import { + defaultMapErrorsFn, + NgxValidateCoreModule, + VALIDATION_BLUEPRINTS, + VALIDATION_MAP_ERRORS_FN, + VALIDATION_VALIDATE_ON_SUBMIT, +} from '@ngx-validate/core'; import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component'; import { ButtonComponent } from './components/button/button.component'; @@ -18,6 +24,7 @@ import { TableEmptyMessageComponent } from './components/table-empty-message/tab import { TableComponent } from './components/table/table.component'; import { ToastContainerComponent } from './components/toast-container/toast-container.component'; import { ToastComponent } from './components/toast/toast.component'; +import { DEFAULT_VALIDATION_BLUEPRINTS } from './constants/validation'; import { LoadingDirective } from './directives/loading.directive'; import { NgxDatatableDefaultDirective } from './directives/ngx-datatable-default.directive'; import { NgxDatatableListDirective } from './directives/ngx-datatable-list.directive'; @@ -69,7 +76,9 @@ export class BaseThemeSharedModule {} exports: [BaseThemeSharedModule], }) export class ThemeSharedModule { - static forRoot(options = {} as RootParams): ModuleWithProviders { + static forRoot( + { httpErrorConfig, validation = {} } = {} as RootParams, + ): ModuleWithProviders { return { ngModule: ThemeSharedModule, providers: [ @@ -92,13 +101,28 @@ export class ThemeSharedModule { deps: [Injector], useFactory: initLazyStyleHandler, }, - { provide: HTTP_ERROR_CONFIG, useValue: options.httpErrorConfig }, + { provide: HTTP_ERROR_CONFIG, useValue: httpErrorConfig }, { provide: 'HTTP_ERROR_CONFIG', useFactory: httpErrorConfigFactory, deps: [HTTP_ERROR_CONFIG], }, { provide: NgbDateParserFormatter, useClass: DateParserFormatter }, + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, + ...(validation.blueprints || {}), + }, + }, + { + provide: VALIDATION_MAP_ERRORS_FN, + useValue: validation.mapErrorsFn || defaultMapErrorsFn, + }, + { + provide: VALIDATION_VALIDATE_ON_SUBMIT, + useValue: validation.validateOnSubmit, + }, ], }; } diff --git a/npm/ng-packs/packages/theme-shared/src/public-api.ts b/npm/ng-packs/packages/theme-shared/src/public-api.ts index 7cc8e821d5..5a4f9efe95 100644 --- a/npm/ng-packs/packages/theme-shared/src/public-api.ts +++ b/npm/ng-packs/packages/theme-shared/src/public-api.ts @@ -5,6 +5,7 @@ export * from './lib/animations'; export * from './lib/components'; export { BOOTSTRAP } from './lib/constants/styles'; +export * from './lib/constants/validation'; export * from './lib/directives'; export * from './lib/enums'; export { ErrorHandler } from './lib/handlers'; diff --git a/npm/ng-packs/packages/theme-shared/testing/src/lib/models/config.ts b/npm/ng-packs/packages/theme-shared/testing/src/lib/models/config.ts new file mode 100644 index 0000000000..1b4825cf9f --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/testing/src/lib/models/config.ts @@ -0,0 +1,5 @@ +import { Validation } from '@ngx-validate/core'; + +export interface Config { + validation?: Partial; +} diff --git a/npm/ng-packs/packages/theme-shared/testing/src/lib/models/index.ts b/npm/ng-packs/packages/theme-shared/testing/src/lib/models/index.ts new file mode 100644 index 0000000000..f03c2281a9 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/testing/src/lib/models/index.ts @@ -0,0 +1 @@ +export * from './config'; diff --git a/npm/ng-packs/packages/theme-shared/testing/src/lib/theme-shared-testing.module.ts b/npm/ng-packs/packages/theme-shared/testing/src/lib/theme-shared-testing.module.ts index 323281046a..52abb12ffe 100644 --- a/npm/ng-packs/packages/theme-shared/testing/src/lib/theme-shared-testing.module.ts +++ b/npm/ng-packs/packages/theme-shared/testing/src/lib/theme-shared-testing.module.ts @@ -1,11 +1,19 @@ import { BaseThemeSharedModule, DateParserFormatter, + DEFAULT_VALIDATION_BLUEPRINTS, THEME_SHARED_ROUTE_PROVIDERS, } from '@abp/ng.theme.shared'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap'; +import { + defaultMapErrorsFn, + VALIDATION_BLUEPRINTS, + VALIDATION_MAP_ERRORS_FN, + VALIDATION_VALIDATE_ON_SUBMIT, +} from '@ngx-validate/core'; +import { Config } from './models/config'; /** * ThemeSharedTestingModule is the module that will be used in tests @@ -15,12 +23,29 @@ import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap'; imports: [RouterTestingModule, BaseThemeSharedModule], }) export class ThemeSharedTestingModule { - static withConfig(): ModuleWithProviders { + static withConfig( + { validation = {} } = {} as Config, + ): ModuleWithProviders { return { ngModule: ThemeSharedTestingModule, providers: [ THEME_SHARED_ROUTE_PROVIDERS, { provide: NgbDateParserFormatter, useClass: DateParserFormatter }, + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, + ...(validation.blueprints || {}), + }, + }, + { + provide: VALIDATION_MAP_ERRORS_FN, + useValue: validation.mapErrorsFn || defaultMapErrorsFn, + }, + { + provide: VALIDATION_VALIDATE_ON_SUBMIT, + useValue: validation.validateOnSubmit, + }, ], }; } diff --git a/npm/ng-packs/packages/theme-shared/testing/src/public-api.ts b/npm/ng-packs/packages/theme-shared/testing/src/public-api.ts index 14355a8a28..dc639b1462 100644 --- a/npm/ng-packs/packages/theme-shared/testing/src/public-api.ts +++ b/npm/ng-packs/packages/theme-shared/testing/src/public-api.ts @@ -1 +1,3 @@ +import * as ThemeSharedTesting from './lib/models'; export * from './lib/theme-shared-testing.module'; +export { ThemeSharedTesting };