Browse Source

Merge pull request #15237 from abpframework/auto-merge/rel-7-0/1618

Merge branch dev with rel-7.0
pull/15258/head
liangshiwei 4 years ago
committed by GitHub
parent
commit
052e511bec
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs
  2. 44
      modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureManagementModal.razor
  3. 3
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/SettingManagementFeatureDefinitionProvider.cs
  4. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Web/Settings/EmailingPageContributor.cs
  5. 2
      npm/ng-packs/packages/identity/src/lib/defaults/default-users-form-props.ts
  6. 2
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/karma.conf.js.template
  7. 109
      npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts
  8. 15
      npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json
  9. 2
      npm/ng-packs/packages/tenant-management/src/lib/defaults/default-tenants-form-props.ts
  10. 15
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html
  11. 7
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts
  12. 3
      npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/components.ts
  13. 1
      npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts
  14. 1
      npm/ng-packs/packages/theme-shared/extensions/src/public-api.ts
  15. 1
      npm/ng-packs/packages/theme-shared/src/lib/components/index.ts
  16. 19
      npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.html
  17. 28
      npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts
  18. 6
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

27
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs

@ -132,11 +132,11 @@ public abstract class AppTemplateBase : TemplateInfo
case UiFramework.BlazorServer:
ConfigureWithBlazorServerUi(context, steps);
break;
case UiFramework.MauiBlazor:
ConfigureWithMauiBlazorUi(context, steps);
break;
case UiFramework.Mvc:
case UiFramework.NotSpecified:
ConfigureWithMvcUi(context, steps);
@ -213,14 +213,14 @@ public abstract class AppTemplateBase : TemplateInfo
{
steps.Add(new ChangeThemeStyleStep());
}
RemoveThemeLogoFolders(context, steps);
if (IsDefaultThemeForTemplate(context.BuildArgs.Theme.Value))
{
return;
}
steps.Add(new ChangeThemeStep());
RemoveLeptonXThemePackagesFromPackageJsonFiles(steps, isProTemplate: IsPro(), uiFramework: context.BuildArgs.UiFramework);
}
@ -240,15 +240,15 @@ public abstract class AppTemplateBase : TemplateInfo
private static bool IsDefaultThemeForTemplate(Theme theme)
{
var defaultThemesForTemplates = new[]
var defaultThemesForTemplates = new[]
{
AppTemplate.DefaultTheme, AppProTemplate.DefaultTheme,
AppTemplate.DefaultTheme, AppProTemplate.DefaultTheme,
AppNoLayersTemplate.DefaultTheme, AppNoLayersProTemplate.DefaultTheme
};
return defaultThemesForTemplates.Any(defaultTheme => defaultTheme == theme);
}
private static void RemoveLeptonXThemePackagesFromPackageJsonFiles(List<ProjectBuildPipelineStep> steps, bool isProTemplate, UiFramework uiFramework)
{
var mvcUiPackageName = isProTemplate ? "@volo/abp.aspnetcore.mvc.ui.theme.leptonx" : "@abp/aspnetcore.mvc.ui.theme.leptonxlite";
@ -263,7 +263,7 @@ public abstract class AppTemplateBase : TemplateInfo
"/MyCompanyName.MyProjectName.Host/package.json",
"/MyCompanyName.MyProjectName.Host.Mongo/package.json"
};
foreach (var packageJsonFilePath in packageJsonFilePaths)
{
steps.Add(new RemoveDependencyFromPackageJsonFileStep(packageJsonFilePath, mvcUiPackageName));
@ -278,7 +278,7 @@ public abstract class AppTemplateBase : TemplateInfo
"/MyCompanyName.MyProjectName.Blazor.Server.Tiered/package.json",
"/MyCompanyName.MyProjectName.Blazor.Server.Mongo/package.json"
};
foreach (var blazorServerPackageJsonFilePath in blazorServerPackageJsonFilePaths)
{
steps.Add(new RemoveDependencyFromPackageJsonFileStep(blazorServerPackageJsonFilePath, mvcUiPackageName));
@ -288,11 +288,11 @@ public abstract class AppTemplateBase : TemplateInfo
else if (uiFramework == UiFramework.Angular)
{
var ngUiPackageName = isProTemplate ? "@volosoft/abp.ng.theme.lepton-x" : "@abp/ng.theme.lepton-x";
var angularPackageJsonFilePaths = new List<string>
var angularPackageJsonFilePaths = new List<string>
{
"/angular/package.json"
};
foreach (var angularPackageJsonFilePath in angularPackageJsonFilePaths)
{
steps.Add(new RemoveDependencyFromPackageJsonFileStep(angularPackageJsonFilePath, ngUiPackageName));
@ -527,6 +527,7 @@ public abstract class AppTemplateBase : TemplateInfo
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web.Host"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web.Tests", projectFolderPath: "/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests"));
steps.Add(new RemoveFileStep("/aspnet-core/src/MyCompanyName.MyProjectName.MauiBlazor/Directory.Build.props"));
if (context.BuildArgs.ExtraProperties.ContainsKey("separate-identity-server") ||
context.BuildArgs.ExtraProperties.ContainsKey("separate-auth-server"))

44
modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureManagementModal.razor

@ -6,15 +6,15 @@
<ModalContent Size="ModalSize.Large" Centered="true">
<ModalHeader>
<ModalTitle>@L["Features"]</ModalTitle>
<CloseButton Clicked="CloseModal"/>
<CloseButton Clicked="CloseModal" />
</ModalHeader>
<ModalBody MaxHeight="50">
@if (Groups == null || !Groups.Any())
{
<span>@L["NoFeatureFoundMessage"]</span>
}
else
{
@if (Groups == null || !Groups.Any())
{
<span class="m-3">@L["NoFeatureFoundMessage"]</span>
}
else
{
<ModalBody MaxHeight="50">
<Tabs TabPosition="TabPosition.Start" Pills="true" @bind-SelectedTab="@SelectedTabName">
<Items>
@foreach (var group in Groups)
@ -30,7 +30,7 @@
var index = i;
<TabPanel Name="@GetNormalizedGroupName(Groups[index].Name)">
<h4>@Groups[index].DisplayName</h4>
<hr class="mt-2 mb-3"/>
<hr class="mt-2 mb-3" />
@foreach (var feature in Groups[index].Features)
{
<div class="mt-2">
@ -42,8 +42,8 @@
<Field Style="@GetFeatureStyles(feature)">
<FieldLabel>@feature.DisplayName</FieldLabel>
<TextEdit Disabled="@disabled"
Text="@feature.Value"
TextChanged="@(async (v) => await OnFeatureValueChangedAsync(v, feature))"/>
Text="@feature.Value"
TextChanged="@(async (v) => await OnFeatureValueChangedAsync(v, feature))" />
@if (feature.Description != null)
{
<div class="form-text">@feature.Description</div>
@ -53,12 +53,12 @@
if (feature.ValueType is SelectionStringValueType)
{
var items = ((SelectionStringValueType) feature.ValueType).ItemSource.Items;
var items = ((SelectionStringValueType)feature.ValueType).ItemSource.Items;
<Field Style="@GetFeatureStyles(feature)">
<FieldLabel>@feature.DisplayName</FieldLabel>
<Select TValue="string"
@bind-SelectedValue="@SelectionStringValues[feature.Name]">
@bind-SelectedValue="@SelectionStringValues[feature.Name]">
@foreach (var item in items)
{
<SelectItem Value="@item.Value">
@ -76,8 +76,7 @@
if (feature.ValueType is ToggleStringValueType)
{
<Field Style="@GetFeatureStyles(feature)">
<Check
TValue="bool" Checked="@ToggleValues[feature.Name]" CheckedChanged="@(async (v) => await OnSelectedValueChangedAsync(v, feature))">
<Check TValue="bool" Checked="@ToggleValues[feature.Name]" CheckedChanged="@(async (v) => await OnSelectedValueChangedAsync(v, feature))">
@feature.DisplayName
</Check>
@if (feature.Description != null)
@ -94,12 +93,13 @@
}
</Content>
</Tabs>
}
</ModalBody>
<ModalFooter>
<Button Color="Color.Primary" Clicked="@(async () => await DeleteAsync(ProviderName,ProviderKey))">@L["ResetToDefault"]</Button>
<Button Color="Color.Secondary" Clicked="CloseModal">@L["Cancel"]</Button>
<Button Color="Color.Primary" Clicked="SaveAsync">@L["Save"]</Button>
</ModalFooter>
</ModalBody>
<ModalFooter>
<Button Color="Color.Primary" Clicked="@(async () => await DeleteAsync(ProviderName,ProviderKey))">@L["ResetToDefault"]</Button>
<Button Color="Color.Secondary" Clicked="CloseModal">@L["Cancel"]</Button>
<Button Color="Color.Primary" Clicked="SaveAsync">@L["Save"]</Button>
</ModalFooter>
}
</ModalContent>
</Modal>

3
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/SettingManagementFeatureDefinitionProvider.cs

@ -17,7 +17,8 @@ public class SettingManagementFeatureDefinitionProvider : FeatureDefinitionProvi
"true",
L("Feature:SettingManagementEnable"),
L("Feature:SettingManagementEnableDescription"),
new ToggleStringValueType());
new ToggleStringValueType(),
isAvailableToHost: false);
settingEnableFeature.CreateChild(
SettingManagementFeatures.AllowChangingEmailSettings,

2
modules/setting-management/src/Volo.Abp.SettingManagement.Web/Settings/EmailingPageContributor.cs

@ -11,7 +11,7 @@ public class EmailingPageContributor : SettingPageContributorBase
{
public EmailingPageContributor()
{
RequiredFeatures(SettingManagementFeatures.Enable);
RequiredTenantSideFeatures(SettingManagementFeatures.Enable);
RequiredTenantSideFeatures(SettingManagementFeatures.AllowChangingEmailSettings);
RequiredPermissions(SettingManagementPermissions.Emailing);
}

2
npm/ng-packs/packages/identity/src/lib/defaults/default-users-form-props.ts

@ -12,7 +12,7 @@ export const DEFAULT_USERS_CREATE_FORM_PROPS = FormProp.createMany<IdentityUserD
validators: () => [Validators.required, Validators.maxLength(256)],
},
{
type: ePropType.Password,
type: ePropType.PasswordInputGroup,
name: 'password',
displayName: 'AbpIdentity::Password',
id: 'password',

2
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/karma.conf.js.template

@ -25,7 +25,7 @@ module.exports = function (config) {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, '../../coverage/my-project-name'),
dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'),
subdir: '.',
reporters: [
{ type: 'html' },

109
npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts

@ -8,25 +8,31 @@ import {
Tree,
url,
} from '@angular-devkit/schematics';
import { GenerateLibSchema } from './models/generate-lib-schema';
import * as ts from 'typescript';
import { join, normalize } from '@angular-devkit/core';
import {
addImportToModule,
addRouteDeclarationToModule,
applyWithOverwrite,
camel,
getFirstApplication,
getWorkspace,
InsertChange,
interpolate,
isLibrary,
JSONFile,
kebab,
pascal,
readWorkspaceSchema,
resolveProject,
updateWorkspace,
} from '../../utils';
import * as cases from '../../utils/text';
import { Exception } from '../../enums';
import { join, normalize } from '@angular-devkit/core';
import {
ProjectDefinition,
WorkspaceDefinition,
} from '@angular-devkit/core/src/workspace/definitions';
import { ProjectDefinition, WorkspaceDefinition } from '../../utils/angular/workspace';
import { addLibToWorkspaceFile } from '../../utils/angular-schematic/generate-lib';
import * as cases from '../../utils/text';
import { Exception } from '../../enums/exception';
import { GenerateLibSchema } from './models/generate-lib-schema';
export default function (schema: GenerateLibSchema) {
return async (tree: Tree) => {
@ -120,6 +126,8 @@ export function addLibToWorkspaceIfNotExist(name: string, packagesDir: string):
: noop(),
addLibToWorkspaceFile(projectRoot, packageName),
updateTsConfig(packageName, pathImportLib),
importConfigModuleToDefaultProjectAppModule(workspace, packageName),
addRoutingToAppRoutingModule(workspace, packageName),
]);
};
}
@ -134,7 +142,9 @@ export function updateTsConfig(packageName: string, path: string) {
const file = new JSONFile(host, tsConfig);
const jsonPath = ['compilerOptions', 'paths', packageName];
const jsonPathConfig = ['compilerOptions', 'paths', `${packageName}/config`];
file.modify(jsonPath, [`${path}/src/public-api.ts`]);
file.modify(jsonPathConfig, [`${path}/config/src/public-api.ts`]);
};
}
@ -154,3 +164,86 @@ export async function createLibSecondaryEntry(tree: Tree, options: GenerateLibSc
]),
]);
}
export function importConfigModuleToDefaultProjectAppModule(
workspace: WorkspaceDefinition,
packageName: string,
) {
return (tree: Tree) => {
const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!;
const project = workspace.projects.get(projectName);
const appModulePath = `${project?.sourceRoot}/app/app.module.ts`;
const appModule = tree.read(appModulePath);
if (!appModule) {
return;
}
const appModuleContent = appModule.toString();
if (appModuleContent.includes(`${camel(packageName)}ConfigModule`)) {
return;
}
const forRootStatement = `${pascal(packageName)}ConfigModule.forRoot()`;
const text = tree.read(appModulePath);
if (!text) {
return;
}
const sourceText = text.toString();
if (sourceText.includes(forRootStatement)) {
return;
}
const source = ts.createSourceFile(appModulePath, sourceText, ts.ScriptTarget.Latest, true);
const changes = addImportToModule(
source,
appModulePath,
forRootStatement,
`${kebab(packageName)}/config`,
);
const recorder = tree.beginUpdate(appModulePath);
for (const change of changes) {
if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd);
}
}
tree.commitUpdate(recorder);
return;
};
}
export function addRoutingToAppRoutingModule(workspace: WorkspaceDefinition, packageName: string) {
return (tree: Tree) => {
const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!;
const project = workspace.projects.get(projectName);
const appRoutingModulePath = `${project?.sourceRoot}/app/app-routing.module.ts`;
const appRoutingModule = tree.read(appRoutingModulePath);
if (!appRoutingModule) {
return;
}
const appRoutingModuleContent = appRoutingModule.toString();
const moduleName = `${pascal(packageName)}Module`;
if (appRoutingModuleContent.includes(moduleName)) {
return;
}
const source = ts.createSourceFile(
appRoutingModulePath,
appRoutingModuleContent,
ts.ScriptTarget.Latest,
true,
);
const importPath = `${kebab(packageName)}`;
const importStatement = `() => import('${importPath}').then(m => m.${moduleName}.forLazy())`;
const routeDefinition = `{ path: '${kebab(packageName)}', loadChildren: ${importStatement} }`;
const change = addRouteDeclarationToModule(source, `${kebab(packageName)}`, routeDefinition);
const recorder = tree.beginUpdate(appRoutingModulePath);
if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd);
}
tree.commitUpdate(recorder);
return;
};
}

15
npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json

@ -16,19 +16,28 @@
"isSecondaryEntrypoint": {
"description": "Is secondary entrypoint",
"type": "boolean",
"$default": false,
"$default": {
"$source": "argv",
"index": 1
},
"x-prompt": "Is secondary entrypoint?"
},
"isModuleTemplate": {
"description": "Is module template",
"type": "boolean",
"$default": true,
"$default": {
"$source": "argv",
"index": 2
},
"x-prompt": "Is module template?"
},
"override": {
"description": "Override existing files",
"type": "boolean",
"$default": false,
"$default": {
"$source": "argv",
"index": 3
},
"x-prompt": "Override existing files?"
}
},

2
npm/ng-packs/packages/tenant-management/src/lib/defaults/default-tenants-form-props.ts

@ -21,7 +21,7 @@ export const DEFAULT_TENANTS_CREATE_FORM_PROPS = FormProp.createMany<
validators: () => [Validators.required, Validators.maxLength(256), Validators.email],
},
{
type: ePropType.Password,
type: ePropType.PasswordInputGroup,
name: 'adminPassword',
displayName: 'AbpTenantManagement::DisplayName:AdminPassword',
id: 'admin-password',

15
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html

@ -138,6 +138,21 @@
class="form-control"
></textarea>
</ng-template>
<ng-template ngSwitchCase="passwordinputgroup">
<ng-template [ngTemplateOutlet]="label"></ng-template>
<abp-password
*abpReplaceableTemplate="{
componentKey: passwordKey,
inputs: {
inputId: { value: prop.id },
formControlName: { value: prop.name }
}
}"
[inputId]="prop.id"
[formControlName]="prop.name"
></abp-password>
</ng-template>
</div>
</ng-container>

7
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -32,6 +32,7 @@ import { FormProp } from '../../models/form-props';
import { PropData } from '../../models/props';
import { selfFactory } from '../../utils/factory.util';
import { addTypeaheadTextSuffix } from '../../utils/typeahead.util';
import { eThemeSharedComponents } from '../../enums/components';
@Component({
selector: 'abp-extensible-form-prop',
@ -70,6 +71,8 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
typeaheadModel: any;
passwordKey = eThemeSharedComponents.PasswordComponent;
private readonly form: UntypedFormGroup;
disabledFn = (data: PropData) => false;
@ -161,6 +164,8 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
return 'time';
case ePropType.Typeahead:
return 'typeahead';
case ePropType.PasswordInputGroup:
return 'passwordinputgroup';
default:
return prop.options ? 'select' : 'input';
}
@ -179,6 +184,8 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
return 'email';
case ePropType.Password:
return 'password';
case ePropType.PasswordInputGroup:
return 'passwordinputgroup';
default:
return 'hidden';
}

3
npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/components.ts

@ -0,0 +1,3 @@
export const enum eThemeSharedComponents {
PasswordComponent = 'ThemeShared.Extensions.PasswordComponent',
}

1
npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts

@ -8,6 +8,7 @@ export const enum ePropType {
MultiSelect = 'multiselect',
Number = 'number',
Password = 'password',
PasswordInputGroup = 'passwordinputgroup',
String = 'string',
Text = 'text',
Time = 'time',

1
npm/ng-packs/packages/theme-shared/extensions/src/public-api.ts

@ -11,6 +11,7 @@ export * from './lib/constants/extra-properties';
export * from './lib/directives/disabled.directive';
export * from './lib/directives/prop-data.directive';
export * from './lib/enums/props.enum';
export * from './lib/enums/components';
export {
ActionCallback,
ActionList,

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

@ -10,3 +10,4 @@ export * from './modal/modal-ref.service';
export * from './modal/modal.component';
export * from './toast-container/toast-container.component';
export * from './toast/toast.component';
export * from './password/password.component';

19
npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.html

@ -0,0 +1,19 @@
<div class="input-group" validationTarget>
<input
[type]="fieldTextType ? 'text' : 'password'"
class="form-control"
[id]="inputId"
[(ngModel)]="value"
/>
<button class="btn btn-secondary" type="button">
<i
class="fa"
[ngClass]="{
'fa-eye-slash': !fieldTextType,
'fa-eye': fieldTextType
}"
(click)="toggleFieldTextType()"
></i>
</button>
</div>

28
npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts

@ -0,0 +1,28 @@
import { Component, forwardRef, Injector, Input } from '@angular/core';
import { AbstractNgModelComponent } from '@abp/ng.core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'abp-password',
templateUrl: `password.component.html`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => PasswordComponent),
multi: true,
},
],
})
export class PasswordComponent extends AbstractNgModelComponent {
@Input() inputId!: string;
@Input() formControlName!: string;
fieldTextType: boolean;
constructor(injector: Injector) {
super(injector);
}
toggleFieldTextType() {
this.fieldTextType = !this.fieldTextType;
}
}

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

@ -32,9 +32,10 @@ import { RootParams } from './models/common';
import { NG_BOOTSTRAP_CONFIG_PROVIDERS } from './providers';
import { THEME_SHARED_ROUTE_PROVIDERS } from './providers/route.provider';
import { THEME_SHARED_APPEND_CONTENT } from './tokens/append-content.token';
import { httpErrorConfigFactory, HTTP_ERROR_CONFIG } from './tokens/http-error.token';
import { HTTP_ERROR_CONFIG, httpErrorConfigFactory } from './tokens/http-error.token';
import { DateParserFormatter } from './utils/date-parser-formatter';
import { CONFIRMATION_ICONS, DEFAULT_CONFIRMATION_ICONS } from './tokens/confirmation-icons.token';
import { PasswordComponent } from './components/password/password.component';
const declarationsWithExports = [
BreadcrumbComponent,
@ -46,6 +47,7 @@ const declarationsWithExports = [
ModalComponent,
ToastComponent,
ToastContainerComponent,
PasswordComponent,
NgxDatatableDefaultDirective,
NgxDatatableListDirective,
LoadingDirective,
@ -61,7 +63,7 @@ const declarationsWithExports = [
EllipsisModule,
],
declarations: [...declarationsWithExports, HttpErrorWrapperComponent],
exports: [NgxDatatableModule, EllipsisModule, ...declarationsWithExports],
exports: [NgxDatatableModule, EllipsisModule, NgxValidateCoreModule, ...declarationsWithExports],
providers: [DatePipe],
})
export class BaseThemeSharedModule {}

Loading…
Cancel
Save