Browse Source

feature(theme-shared): add setting-management package

pull/1701/head
mehmet-erim 7 years ago
parent
commit
b2f2dec03e
  1. 35
      npm/ng-packs/angular.json
  2. 21
      npm/ng-packs/packages/account/src/lib/constants/routes.ts
  3. 2
      npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
  4. 6
      npm/ng-packs/packages/core/src/lib/plugins/config/config.plugin.ts
  5. 30
      npm/ng-packs/packages/core/src/lib/states/config.state.ts
  6. 4
      npm/ng-packs/packages/core/src/lib/utils/common-utils.ts
  7. 1
      npm/ng-packs/packages/core/src/lib/utils/index.ts
  8. 43
      npm/ng-packs/packages/identity/src/lib/constants/routes.ts
  9. 1
      npm/ng-packs/packages/setting-management/README.md
  10. 32
      npm/ng-packs/packages/setting-management/karma.conf.js
  11. 8
      npm/ng-packs/packages/setting-management/ng-package.json
  12. 10
      npm/ng-packs/packages/setting-management/package.json
  13. 1
      npm/ng-packs/packages/setting-management/src/lib/components/index.ts
  14. 17
      npm/ng-packs/packages/setting-management/src/lib/components/services/initial.service.ts
  15. 176
      npm/ng-packs/packages/setting-management/src/lib/components/setting/setting.component.html
  16. 20
      npm/ng-packs/packages/setting-management/src/lib/components/setting/setting.component.ts
  17. 1
      npm/ng-packs/packages/setting-management/src/lib/constants/index.ts
  18. 15
      npm/ng-packs/packages/setting-management/src/lib/constants/routes.ts
  19. 18
      npm/ng-packs/packages/setting-management/src/lib/setting-management-routing.module.ts
  20. 15
      npm/ng-packs/packages/setting-management/src/lib/setting-management.module.ts
  21. 3
      npm/ng-packs/packages/setting-management/src/public-api.ts
  22. 21
      npm/ng-packs/packages/setting-management/src/test.ts
  23. 26
      npm/ng-packs/packages/setting-management/tsconfig.lib.json
  24. 17
      npm/ng-packs/packages/setting-management/tsconfig.spec.json
  25. 17
      npm/ng-packs/packages/setting-management/tslint.json
  26. 37
      npm/ng-packs/packages/tenant-management/src/lib/constants/routes.ts
  27. 7
      npm/ng-packs/packages/theme-shared/src/lib/animations/fade.animations.ts
  28. 3
      npm/ng-packs/packages/theme-shared/src/lib/animations/index.ts
  29. 1
      npm/ng-packs/packages/theme-shared/src/lib/models/index.ts
  30. 8
      npm/ng-packs/packages/theme-shared/src/lib/models/setting-management.ts
  31. 3
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

35
npm/ng-packs/angular.json

@ -253,6 +253,41 @@
}
}
}
},
"setting-management": {
"projectType": "library",
"root": "packages/setting-management",
"sourceRoot": "packages/setting-management/src",
"prefix": "abp",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "packages/setting-management/tsconfig.lib.json",
"project": "packages/setting-management/ng-package.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "packages/setting-management/src/test.ts",
"tsConfig": "packages/setting-management/tsconfig.spec.json",
"karmaConfig": "packages/setting-management/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"packages/setting-management/tsconfig.lib.json",
"packages/setting-management/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "core"

21
npm/ng-packs/packages/account/src/lib/constants/routes.ts

@ -1,11 +1,14 @@
import { ABP, eLayoutType } from '@abp/ng.core';
export const ACCOUNT_ROUTES = [
{
name: 'Account',
path: 'account',
invisible: true,
layout: eLayoutType.application,
children: [{ path: 'login', name: 'Login', order: 1 }, { path: 'register', name: 'Register', order: 2 }],
},
] as ABP.FullRoute[];
export const ACCOUNT_ROUTES = {
routes: [
{
name: 'Account',
path: 'account',
invisible: true,
layout: eLayoutType.application,
children: [{ path: 'login', name: 'Login', order: 1 }, { path: 'register', name: 'Register', order: 2 }],
},
] as ABP.FullRoute[],
settings: [],
};

2
npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts

@ -57,7 +57,7 @@ function findLayout(segments: UrlSegment[], routes: ABP.FullRoute[]): eLayoutTyp
if (route.children && route.children.length && segments.length > 1) {
const child = route.children.find(c => c.path === segments[1].path);
if (child.layout) {
if (child && child.layout) {
layout = child.layout;
}
}

6
npm/ng-packs/packages/core/src/lib/plugins/config/config.plugin.ts

@ -38,9 +38,9 @@ export class ConfigPlugin implements NgxsPlugin {
function transformRoutes(routes: Routes = [], wrappers: ABP.FullRoute[] = []): any {
const abpRoutes: ABP.FullRoute[] = routes
.filter(route => {
return snq(() => route.data.routes.find(r => r.path === route.path), false);
return snq(() => route.data.routes.routes.find(r => r.path === route.path), false);
})
.reduce((acc, val) => [...acc, ...val.data.routes], []);
.reduce((acc, val) => [...acc, ...val.data.routes.routes], []);
wrappers = abpRoutes.filter(ar => ar.wrapper);
const transformed = [] as ABP.FullRoute[];
@ -48,7 +48,7 @@ function transformRoutes(routes: Routes = [], wrappers: ABP.FullRoute[] = []): a
.filter(route => (route.data || {}).routes && (route.component || route.loadChildren))
.forEach(route => {
const abpPackage = abpRoutes.find(
abp => abp.path.toLowerCase() === route.path.toLowerCase() && snq(() => route.data.routes.length, false),
abp => abp.path.toLowerCase() === route.path.toLowerCase() && snq(() => route.data.routes.routes.length, false),
);
const { length } = transformed;

30
npm/ng-packs/packages/core/src/lib/states/config.state.ts

@ -60,6 +60,17 @@ export class ConfigState {
return selector;
}
static getRoute(path?: string, name?: string) {
const selector = createSelector(
[ConfigState],
function(state: Config.State) {
return findRoute(state.routes, path, name);
},
);
return selector;
}
static getApiUrl(key?: string) {
const selector = createSelector(
[ConfigState],
@ -229,3 +240,22 @@ function patchRouteDeep(
return organizeRoutes(routes);
}
function findRoute(routes: ABP.FullRoute[], path?: string, name?: string) {
let foundRoute;
routes.forEach(route => {
if (foundRoute) return;
if (path && route.path === path) {
foundRoute = route;
} else if (name && route.name === name) {
foundRoute = route;
return;
} else if (route.children && route.children.length) {
foundRoute = findRoute(route.children, path, name);
return;
}
});
return foundRoute;
}

4
npm/ng-packs/packages/core/src/lib/utils/common-utils.ts

@ -0,0 +1,4 @@
export function noop() {
const fn = function() {};
return fn;
}

1
npm/ng-packs/packages/core/src/lib/utils/index.ts

@ -1,3 +1,4 @@
export * from './common-utils';
export * from './generator-utils';
export * from './initial-utils';
export * from './route-utils';

43
npm/ng-packs/packages/identity/src/lib/constants/routes.ts

@ -1,22 +1,25 @@
import { eLayoutType, ABP } from '@abp/ng.core';
export const IDENTITY_ROUTES = [
{
name: 'AbpUiNavigation::Menu:Administration',
path: '',
order: 1,
wrapper: true,
},
{
name: 'AbpIdentity::Menu:IdentityManagement',
path: 'identity',
order: 1,
parentName: 'AbpUiNavigation::Menu:Administration',
layout: eLayoutType.application,
iconClass: 'fa fa-id-card-o',
children: [
{ path: 'roles', name: 'AbpIdentity::Roles', order: 2, requiredPolicy: 'AbpIdentity.Roles' },
{ path: 'users', name: 'AbpIdentity::Users', order: 1, requiredPolicy: 'AbpIdentity.Users' },
],
},
] as ABP.FullRoute[];
export const IDENTITY_ROUTES = {
routes: [
{
name: 'AbpUiNavigation::Menu:Administration',
path: '',
order: 1,
wrapper: true,
},
{
name: 'AbpIdentity::Menu:IdentityManagement',
path: 'identity',
order: 1,
parentName: 'AbpUiNavigation::Menu:Administration',
layout: eLayoutType.application,
iconClass: 'fa fa-id-card-o',
children: [
{ path: 'roles', name: 'AbpIdentity::Roles', order: 2, requiredPolicy: 'AbpIdentity.Roles' },
{ path: 'users', name: 'AbpIdentity::Users', order: 1, requiredPolicy: 'AbpIdentity.Users' },
],
},
] as ABP.FullRoute[],
settings: [],
};

1
npm/ng-packs/packages/setting-management/README.md

@ -0,0 +1 @@
<h1> @abp/ng.setting-management </h1>

32
npm/ng-packs/packages/setting-management/karma.conf.js

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/setting-management'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

8
npm/ng-packs/packages/setting-management/ng-package.json

@ -0,0 +1,8 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/setting-management",
"lib": {
"entryFile": "src/public-api.ts"
},
"whitelistedNonPeerDependencies": ["@abp/ng.theme.shared"]
}

10
npm/ng-packs/packages/setting-management/package.json

@ -0,0 +1,10 @@
{
"name": "@abp/ng.setting-management",
"version": "0.0.1",
"dependencies": {
"@abp/ng.theme.shared": "^0.8.3"
},
"publishConfig": {
"access": "public"
}
}

1
npm/ng-packs/packages/setting-management/src/lib/components/index.ts

@ -0,0 +1 @@
export * from './setting/setting.component';

17
npm/ng-packs/packages/setting-management/src/lib/components/services/initial.service.ts

@ -0,0 +1,17 @@
import { SettingTab } from '@abp/ng.theme.shared';
import { Injectable, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import snq from 'snq';
@Injectable()
export class InitialService {
public settings: SettingTab[];
constructor(private router: Router) {
this.settings = this.router.config
.map(route => snq(() => route.data.routes.settings))
.filter(settings => settings && settings.length)
.reduce((acc, val) => [...acc, ...val], [])
.sort((a, b) => a.order - b.order);
}
}

176
npm/ng-packs/packages/setting-management/src/lib/components/setting/setting.component.html

@ -0,0 +1,176 @@
<div class="row entry-row">
<div class="col-auto">
<h1 class="content-header-title">{{'AbpSettingManagement::Settings' | abpLocalization}}</h1>
</div>
<div id="breadcrumb" class="col-md-auto pl-md-0">
<abp-breadcrumb></abp-breadcrumb>
</div>
<div class="col">
<div class="text-lg-right pt-2" id="AbpContentToolbar"></div>
</div>
</div>
<div id="SettingManagementWrapper">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-3">
<ul class="nav flex-column nav-pills" id="T515ccf3324254f41a4a9a6b34b0dae56" role="tablist">
<li *ngFor="let setting of settings" (click)="selected = setting" class="nav-item">
<a
class="nav-link"
[id]="setting.name + '-tab'"
role="tab"
[class.active]="setting.name === selected.name"
>{{ setting.name | abpLocalization }}</a
>
</li>
</ul>
</div>
<div class="col-9">
<div class="tab-content">
<div class="tab-pane fade show active" [id]="selected.name + '-tab'" role="tabpanel">
<h2>{{ selected.name | abpLocalization }}</h2>
<hr class="my-4" />
<ng-container *ngComponentOutlet="selected.component"></ng-container>
</div>
<div
class="tab-pane fade "
id="T515ccf3324254f41a4a9a6b34b0dae56_1"
role="tabpanel"
aria-labelledby="T515ccf3324254f41a4a9a6b34b0dae56_1-tab"
>
<h2>Lepton theme settings</h2>
<hr class="my-4" />
<form id="LeptonThemeSettingsForm" novalidate="novalidate">
<div class="row">
<div class="col col-md-12">
<div>
<div class="form-group">
<label for="MenuPlacement">Menu placement</label>
<select
class="custom-select form-control"
data-val="true"
data-val-required="The Menu placement field is required."
id="MenuPlacement"
name="MenuPlacement"
><option selected="selected" value="0">Left</option>
<option value="1">Top</option>
</select>
<span
class="text-danger field-validation-valid"
data-valmsg-for="MenuPlacement"
data-valmsg-replace="true"
></span>
</div>
<div class="form-group">
<label for="MenuStatus">Menu status</label>
<select
class="custom-select form-control"
data-val="true"
data-val-required="The Menu status field is required."
id="MenuStatus"
name="MenuStatus"
><option selected="selected" value="0">Always opened</option>
<option value="1">Open on hover</option>
</select>
<span
class="text-danger field-validation-valid"
data-valmsg-for="MenuStatus"
data-valmsg-replace="true"
></span>
</div>
<div class="custom-checkbox custom-control mb-2">
<input
type="checkbox"
data-val="true"
data-val-required="The Boxed layout field is required."
id="BoxedLayout"
name="BoxedLayout"
value="true"
class="custom-control-input "
/><label class="custom-control-label" for="BoxedLayout">Boxed layout</label>
</div>
</div>
<hr class="my-4" />
<div>
<button type="submit" class="btn btn-primary" data-busy-text="Processing...">
<i class="fa fa-save"></i> Save
</button>
</div>
</div>
</div>
<input name="BoxedLayout" type="hidden" value="false" />
</form>
</div>
<div
class="tab-pane fade "
id="T515ccf3324254f41a4a9a6b34b0dae56_2"
role="tabpanel"
aria-labelledby="T515ccf3324254f41a4a9a6b34b0dae56_2-tab"
>
<h2>Account</h2>
<hr class="my-4" />
<form id="AccountSettingsForm" novalidate="novalidate">
<div class="row">
<div class="col-md-12">
<div>
<div class="custom-checkbox custom-control mb-2">
<input
type="checkbox"
checked="checked"
data-val="true"
data-val-required="The Is self registration enabled field is required."
id="IsSelfRegistrationEnabled"
name="IsSelfRegistrationEnabled"
value="true"
class="custom-control-input "
/><label class="custom-control-label" for="IsSelfRegistrationEnabled"
>Is self registration enabled</label
>
</div>
</div>
<hr class="my-4" />
<div>
<h4>Two factor login settings</h4>
<div class="custom-checkbox custom-control mb-2">
<input
type="checkbox"
checked="checked"
data-val="true"
data-val-required="The Is remember browser enabled field is required."
id="IsRememberBrowserEnabled"
name="IsRememberBrowserEnabled"
value="true"
class="custom-control-input "
/><label class="custom-control-label" for="IsRememberBrowserEnabled"
>Is remember browser enabled</label
>
</div>
</div>
<hr class="my-4" />
<div>
<button type="submit" class="btn btn-primary" data-busy-text="Processing...">
<i class="fa fa-save"></i> Save
</button>
</div>
</div>
</div>
<input name="IsSelfRegistrationEnabled" type="hidden" value="false" /><input
name="IsRememberBrowserEnabled"
type="hidden"
value="false"
/>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

20
npm/ng-packs/packages/setting-management/src/lib/components/setting/setting.component.ts

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { SettingTab } from '@abp/ng.theme.shared';
import { InitialService } from '../services/initial.service';
@Component({
selector: 'abp-setting',
templateUrl: './setting.component.html',
})
export class SettingComponent implements OnInit {
settings: SettingTab[];
selected = {} as SettingTab;
constructor(private initialService: InitialService) {}
ngOnInit() {
this.settings = this.initialService.settings;
this.selected = this.settings[0];
}
}

1
npm/ng-packs/packages/setting-management/src/lib/constants/index.ts

@ -0,0 +1 @@
export * from './routes';

15
npm/ng-packs/packages/setting-management/src/lib/constants/routes.ts

@ -0,0 +1,15 @@
import { ABP, eLayoutType } from '@abp/ng.core';
export const SETTING_MANAGEMENT_ROUTES = {
routes: [
{
name: 'Settings',
path: 'setting-management',
parentName: 'AbpUiNavigation::Menu:Administration',
layout: eLayoutType.application,
order: 6,
iconClass: 'fa fa-cog',
},
] as ABP.FullRoute[],
settings: [],
};

18
npm/ng-packs/packages/setting-management/src/lib/setting-management-routing.module.ts

@ -0,0 +1,18 @@
import { DynamicLayoutComponent } from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SettingComponent } from './components/setting/setting.component';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [{ path: '', component: SettingComponent }],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SettingManagementRoutingModule {}

15
npm/ng-packs/packages/setting-management/src/lib/setting-management.module.ts

@ -0,0 +1,15 @@
import { CoreModule, noop } from '@abp/ng.core';
import { NgModule, ModuleWithProviders, APP_INITIALIZER, Self } from '@angular/core';
import { SettingComponent } from './components/setting/setting.component';
import { SettingManagementRoutingModule } from './setting-management-routing.module';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { InitialService } from './components/services/initial.service';
@NgModule({
declarations: [SettingComponent],
imports: [SettingManagementRoutingModule, CoreModule, ThemeSharedModule],
providers: [InitialService],
})
export class SettingManagementModule {
constructor(@Self() initialService: InitialService) {}
}

3
npm/ng-packs/packages/setting-management/src/public-api.ts

@ -0,0 +1,3 @@
export * from './lib/setting-management.module';
export * from './lib/components';
export * from './lib/constants';

21
npm/ng-packs/packages/setting-management/src/test.ts

@ -0,0 +1,21 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

26
npm/ng-packs/packages/setting-management/tsconfig.lib.json

@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

17
npm/ng-packs/packages/setting-management/tsconfig.spec.json

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

17
npm/ng-packs/packages/setting-management/tslint.json

@ -0,0 +1,17 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"abp",
"camelCase"
],
"component-selector": [
true,
"element",
"abp",
"kebab-case"
]
}
}

37
npm/ng-packs/packages/tenant-management/src/lib/constants/routes.ts

@ -1,19 +1,22 @@
import { ABP, eLayoutType } from '@abp/ng.core';
export const TENANT_MANAGEMENT_ROUTES = [
{
name: 'AbpTenantManagement::Menu:TenantManagement',
path: 'tenant-management',
parentName: 'AbpUiNavigation::Menu:Administration',
layout: eLayoutType.application,
iconClass: 'fa fa-users',
children: [
{
path: 'tenants',
name: 'AbpTenantManagement::Tenants',
order: 1,
requiredPolicy: 'AbpTenantManagement.Tenants',
},
],
},
] as ABP.FullRoute[];
export const TENANT_MANAGEMENT_ROUTES = {
routes: [
{
name: 'AbpTenantManagement::Menu:TenantManagement',
path: 'tenant-management',
parentName: 'AbpUiNavigation::Menu:Administration',
layout: eLayoutType.application,
iconClass: 'fa fa-users',
children: [
{
path: 'tenants',
name: 'AbpTenantManagement::Tenants',
order: 1,
requiredPolicy: 'AbpTenantManagement.Tenants',
},
],
},
] as ABP.FullRoute[],
settings: [],
};

7
npm/ng-packs/packages/theme-shared/src/lib/animations/fade.animations.ts

@ -0,0 +1,7 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
export const fade = trigger('fade', [
state('void', style({ opacity: 1 })),
transition(':enter', [style({ opacity: 0 }), animate(250)]),
transition(':leave', animate(250, style({ opacity: 0 }))),
]);

3
npm/ng-packs/packages/theme-shared/src/lib/animations/index.ts

@ -1 +1,2 @@
export * from './slide.animation';
export * from './fade.animations';
export * from './slide.animations';

1
npm/ng-packs/packages/theme-shared/src/lib/models/index.ts

@ -1,2 +1,3 @@
export * from './confirmation';
export * from './toaster';
export * from './setting-management';

8
npm/ng-packs/packages/theme-shared/src/lib/models/setting-management.ts

@ -0,0 +1,8 @@
import { Type } from '@angular/core';
export interface SettingTab {
name: string;
order: number;
component: Type<any>;
requiredPolicy?: string;
}

3
npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

@ -16,6 +16,7 @@ import { ButtonComponent } from './components/button/button.component';
import { ValidationErrorComponent } from './components/errors/validation-error.component';
import { ChangePasswordComponent } from './components/change-password/change-password.component';
import { ProfileComponent } from './components/profile/profile.component';
import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
export function appendScript(injector: Injector) {
const fn = function() {
@ -63,6 +64,7 @@ export function appendScript(injector: Injector) {
ValidationErrorComponent,
ChangePasswordComponent,
ProfileComponent,
BreadcrumbComponent,
],
exports: [
ButtonComponent,
@ -72,6 +74,7 @@ export function appendScript(injector: Injector) {
LoaderBarComponent,
ChangePasswordComponent,
ProfileComponent,
BreadcrumbComponent,
],
entryComponents: [ErrorComponent, ValidationErrorComponent],
})

Loading…
Cancel
Save