9 changed files with 1132 additions and 142 deletions
@ -0,0 +1,220 @@ |
|||
using System.Collections.Immutable; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Features; |
|||
|
|||
namespace LINGYUN.Abp.MicroService.AdminService.Overrides; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergeFeatureDefinitionManager : FeatureDefinitionManager |
|||
{ |
|||
public MergeFeatureDefinitionManager( |
|||
IStaticFeatureDefinitionStore staticStore, |
|||
IDynamicFeatureDefinitionStore dynamicStore) |
|||
: base(staticStore, dynamicStore) |
|||
{ |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<FeatureDefinition>> GetAllAsync() |
|||
{ |
|||
var staticFeatures = await StaticStore.GetFeaturesAsync(); |
|||
var dynamicFeatures = await DynamicStore.GetFeaturesAsync(); |
|||
|
|||
var mergedFeatures = new Dictionary<string, FeatureDefinition>(); |
|||
|
|||
foreach (var staticFeature in staticFeatures) |
|||
{ |
|||
mergedFeatures[staticFeature.Name] = staticFeature; |
|||
} |
|||
|
|||
foreach (var dynamicFeature in dynamicFeatures) |
|||
{ |
|||
if (mergedFeatures.TryGetValue(dynamicFeature.Name, out var existingFeature)) |
|||
{ |
|||
MergeFeatureMetadata(existingFeature, dynamicFeature); |
|||
|
|||
foreach (var child in dynamicFeature.Children) |
|||
{ |
|||
MergeChildFeature(existingFeature, child); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
mergedFeatures[dynamicFeature.Name] = dynamicFeature; |
|||
} |
|||
} |
|||
|
|||
return mergedFeatures.Values.ToImmutableList(); |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync() |
|||
{ |
|||
var staticGroups = await StaticStore.GetGroupsAsync(); |
|||
var dynamicGroups = await DynamicStore.GetGroupsAsync(); |
|||
|
|||
var mergedGroups = new Dictionary<string, FeatureGroupDefinition>(); |
|||
|
|||
foreach (var staticGroup in staticGroups) |
|||
{ |
|||
mergedGroups[staticGroup.Name] = staticGroup; |
|||
} |
|||
|
|||
foreach (var dynamicGroup in dynamicGroups) |
|||
{ |
|||
if (mergedGroups.TryGetValue(dynamicGroup.Name, out var existingGroup)) |
|||
{ |
|||
MergeGroupFeatures(existingGroup, dynamicGroup); |
|||
} |
|||
else |
|||
{ |
|||
mergedGroups[dynamicGroup.Name] = dynamicGroup; |
|||
} |
|||
} |
|||
|
|||
return mergedGroups.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static void MergeGroupFeatures(FeatureGroupDefinition target, FeatureGroupDefinition source) |
|||
{ |
|||
foreach (var sourceFeature in source.Features) |
|||
{ |
|||
var existingFeature = target.GetFeatureOrNull(sourceFeature.Name); |
|||
|
|||
if (existingFeature == null) |
|||
{ |
|||
var newFeature = target.AddFeature( |
|||
sourceFeature.Name, |
|||
sourceFeature.DefaultValue, |
|||
sourceFeature.DisplayName, |
|||
sourceFeature.Description, |
|||
sourceFeature.ValueType, |
|||
sourceFeature.IsVisibleToClients, |
|||
sourceFeature.IsAvailableToHost |
|||
); |
|||
|
|||
CopyFeatureDetails(sourceFeature, newFeature); |
|||
|
|||
foreach (var child in sourceFeature.Children) |
|||
{ |
|||
AddChildFeatureRecursively(newFeature, child); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergeFeatureMetadata(existingFeature, sourceFeature); |
|||
|
|||
foreach (var child in sourceFeature.Children) |
|||
{ |
|||
MergeChildFeature(existingFeature, child); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void AddChildFeatureRecursively(FeatureDefinition parent, FeatureDefinition sourceChild) |
|||
{ |
|||
var newChild = parent.CreateChild( |
|||
sourceChild.Name, |
|||
sourceChild.DefaultValue, |
|||
sourceChild.DisplayName, |
|||
sourceChild.Description, |
|||
sourceChild.ValueType, |
|||
sourceChild.IsVisibleToClients, |
|||
sourceChild.IsAvailableToHost |
|||
); |
|||
|
|||
CopyFeatureDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildFeatureRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
|
|||
private static void MergeChildFeature(FeatureDefinition parent, FeatureDefinition sourceChild) |
|||
{ |
|||
var existingChild = parent.Children.FirstOrDefault(c => c.Name == sourceChild.Name); |
|||
|
|||
if (existingChild == null) |
|||
{ |
|||
var newChild = parent.CreateChild( |
|||
sourceChild.Name, |
|||
sourceChild.DefaultValue, |
|||
sourceChild.DisplayName, |
|||
sourceChild.Description, |
|||
sourceChild.ValueType, |
|||
sourceChild.IsVisibleToClients, |
|||
sourceChild.IsAvailableToHost |
|||
); |
|||
CopyFeatureDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildFeatureRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergeFeatureMetadata(existingChild, sourceChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
MergeChildFeature(existingChild, grandchild); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void CopyFeatureDetails(FeatureDefinition source, FeatureDefinition target) |
|||
{ |
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.AllowedProviders) |
|||
{ |
|||
if (!target.AllowedProviders.Contains(provider)) |
|||
{ |
|||
target.AllowedProviders.Add(provider); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void MergeFeatureMetadata(FeatureDefinition target, FeatureDefinition source) |
|||
{ |
|||
if (source.DisplayName != null) |
|||
{ |
|||
target.DisplayName = source.DisplayName; |
|||
} |
|||
|
|||
if (source.Description != null) |
|||
{ |
|||
target.Description = source.Description; |
|||
} |
|||
|
|||
if (source.DefaultValue != null) |
|||
{ |
|||
target.DefaultValue = source.DefaultValue; |
|||
} |
|||
|
|||
if (source.ValueType != null) |
|||
{ |
|||
target.ValueType = source.ValueType; |
|||
} |
|||
|
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.AllowedProviders) |
|||
{ |
|||
if (!target.AllowedProviders.Contains(provider)) |
|||
{ |
|||
target.AllowedProviders.Add(provider); |
|||
} |
|||
} |
|||
|
|||
target.IsVisibleToClients = target.IsVisibleToClients || source.IsVisibleToClients; |
|||
target.IsAvailableToHost = target.IsAvailableToHost || source.IsAvailableToHost; |
|||
} |
|||
} |
|||
@ -0,0 +1,186 @@ |
|||
using System.Collections.Immutable; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LINGYUN.Abp.MicroService.AdminService.Overrides; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergePermissionDefinitionManager : PermissionDefinitionManager |
|||
{ |
|||
private readonly IStaticPermissionDefinitionStore _staticStore; |
|||
private readonly IDynamicPermissionDefinitionStore _dynamicStore; |
|||
|
|||
public MergePermissionDefinitionManager( |
|||
IStaticPermissionDefinitionStore staticStore, |
|||
IDynamicPermissionDefinitionStore dynamicStore) : base(staticStore, dynamicStore) |
|||
{ |
|||
_staticStore = staticStore; |
|||
_dynamicStore = dynamicStore; |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync() |
|||
{ |
|||
var staticGroups = await _staticStore.GetGroupsAsync(); |
|||
var dynamicGroups = await _dynamicStore.GetGroupsAsync(); |
|||
|
|||
var mergedGroups = new Dictionary<string, PermissionGroupDefinition>(); |
|||
|
|||
foreach (var staticGroup in staticGroups) |
|||
{ |
|||
mergedGroups[staticGroup.Name] = staticGroup; |
|||
} |
|||
|
|||
foreach (var dynamicGroup in dynamicGroups) |
|||
{ |
|||
if (mergedGroups.TryGetValue(dynamicGroup.Name, out var existingGroup)) |
|||
{ |
|||
MergeGroupPermissions(existingGroup, dynamicGroup); |
|||
} |
|||
else |
|||
{ |
|||
mergedGroups[dynamicGroup.Name] = dynamicGroup; |
|||
} |
|||
} |
|||
|
|||
return mergedGroups.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static void MergeGroupPermissions(PermissionGroupDefinition target, PermissionGroupDefinition source) |
|||
{ |
|||
foreach (var sourcePermission in source.Permissions) |
|||
{ |
|||
var existingPermission = target.GetPermissionOrNull(sourcePermission.Name); |
|||
|
|||
if (existingPermission == null) |
|||
{ |
|||
var newPermission = target.AddPermission( |
|||
sourcePermission.Name, |
|||
sourcePermission.DisplayName, |
|||
sourcePermission.MultiTenancySide, |
|||
sourcePermission.IsEnabled |
|||
); |
|||
|
|||
CopyPermissionDetails(sourcePermission, newPermission); |
|||
|
|||
foreach (var child in sourcePermission.Children) |
|||
{ |
|||
AddChildPermissionRecursively(newPermission, child); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergePermissionMetadata(existingPermission, sourcePermission); |
|||
|
|||
foreach (var sourceChild in sourcePermission.Children) |
|||
{ |
|||
MergeChildPermissions(existingPermission, sourceChild); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void AddChildPermissionRecursively(PermissionDefinition parent, PermissionDefinition sourceChild) |
|||
{ |
|||
var newChild = parent.AddChild( |
|||
sourceChild.Name, |
|||
sourceChild.DisplayName, |
|||
sourceChild.MultiTenancySide, |
|||
sourceChild.IsEnabled |
|||
); |
|||
|
|||
CopyPermissionDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildPermissionRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
|
|||
private static void MergeChildPermissions(PermissionDefinition parent, PermissionDefinition sourceChild) |
|||
{ |
|||
var existingChild = parent.Children.FirstOrDefault(c => c.Name == sourceChild.Name); |
|||
|
|||
if (existingChild == null) |
|||
{ |
|||
var newChild = parent.AddChild( |
|||
sourceChild.Name, |
|||
sourceChild.DisplayName, |
|||
sourceChild.MultiTenancySide, |
|||
sourceChild.IsEnabled |
|||
); |
|||
CopyPermissionDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildPermissionRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergePermissionMetadata(existingChild, sourceChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
MergeChildPermissions(existingChild, grandchild); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void CopyPermissionDetails(PermissionDefinition source, PermissionDefinition target) |
|||
{ |
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.Providers) |
|||
{ |
|||
if (!target.Providers.Contains(provider)) |
|||
{ |
|||
target.Providers.Add(provider); |
|||
} |
|||
} |
|||
|
|||
foreach (var checker in source.StateCheckers) |
|||
{ |
|||
if (!target.StateCheckers.Contains(checker)) |
|||
{ |
|||
target.StateCheckers.Add(checker); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void MergePermissionMetadata(PermissionDefinition target, PermissionDefinition source) |
|||
{ |
|||
target.DisplayName = source.DisplayName; |
|||
|
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.Providers) |
|||
{ |
|||
if (!target.Providers.Contains(provider)) |
|||
{ |
|||
target.Providers.Add(provider); |
|||
} |
|||
} |
|||
|
|||
foreach (var checker in source.StateCheckers) |
|||
{ |
|||
if (!target.StateCheckers.Contains(checker)) |
|||
{ |
|||
target.StateCheckers.Add(checker); |
|||
} |
|||
} |
|||
|
|||
if (source.MultiTenancySide != MultiTenancySides.Both) |
|||
{ |
|||
target.MultiTenancySide |= source.MultiTenancySide; |
|||
} |
|||
|
|||
target.IsEnabled = target.IsEnabled || source.IsEnabled; |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
using System.Collections.Immutable; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.MicroService.AdminService.Overrides; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergeSettingDefinitionManager : SettingDefinitionManager |
|||
{ |
|||
private readonly IStaticSettingDefinitionStore _staticStore; |
|||
private readonly IDynamicSettingDefinitionStore _dynamicStore; |
|||
|
|||
public MergeSettingDefinitionManager( |
|||
IStaticSettingDefinitionStore staticStore, |
|||
IDynamicSettingDefinitionStore dynamicStore) |
|||
: base(staticStore, dynamicStore) |
|||
{ |
|||
_staticStore = staticStore; |
|||
_dynamicStore = dynamicStore; |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<SettingDefinition>> GetAllAsync() |
|||
{ |
|||
var staticSettings = await _staticStore.GetAllAsync(); |
|||
var dynamicSettings = await _dynamicStore.GetAllAsync(); |
|||
|
|||
var mergedSettings = new Dictionary<string, SettingDefinition>(); |
|||
|
|||
foreach (var staticSetting in staticSettings) |
|||
{ |
|||
mergedSettings[staticSetting.Name] = staticSetting; |
|||
} |
|||
|
|||
foreach (var dynamicSetting in dynamicSettings) |
|||
{ |
|||
if (mergedSettings.TryGetValue(dynamicSetting.Name, out var existingSetting)) |
|||
{ |
|||
MergeSetting(existingSetting, dynamicSetting); |
|||
} |
|||
else |
|||
{ |
|||
mergedSettings[dynamicSetting.Name] = dynamicSetting; |
|||
} |
|||
} |
|||
|
|||
return mergedSettings.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static void MergeSetting(SettingDefinition target, SettingDefinition source) |
|||
{ |
|||
target.DisplayName = source.DisplayName; |
|||
target.Description = source.Description ?? target.Description; |
|||
target.DefaultValue = source.DefaultValue ?? target.DefaultValue; |
|||
target.IsVisibleToClients = target.IsVisibleToClients || source.IsVisibleToClients; |
|||
target.IsInherited = target.IsInherited || source.IsInherited; |
|||
target.IsEncrypted = target.IsEncrypted || source.IsEncrypted; |
|||
|
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.Providers) |
|||
{ |
|||
if (!target.Providers.Contains(provider)) |
|||
{ |
|||
target.Providers.Add(provider); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,84 @@ |
|||
using System.Collections.Immutable; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.TextTemplating; |
|||
|
|||
namespace LINGYUN.Abp.MicroService.AdminService.Overrides; |
|||
|
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergeTemplateDefinitionManager : TemplateDefinitionManager |
|||
{ |
|||
public MergeTemplateDefinitionManager( |
|||
IStaticTemplateDefinitionStore staticStore, |
|||
IDynamicTemplateDefinitionStore dynamicStore) |
|||
: base(staticStore, dynamicStore) |
|||
{ |
|||
} |
|||
public async override Task<IReadOnlyList<TemplateDefinition>> GetAllAsync() |
|||
{ |
|||
var staticTemplates = await StaticStore.GetAllAsync(); |
|||
var dynamicTemplates = await DynamicStore.GetAllAsync(); |
|||
|
|||
var mergedTemplates = new Dictionary<string, TemplateDefinition>(); |
|||
|
|||
foreach (var staticTemplate in staticTemplates) |
|||
{ |
|||
mergedTemplates[staticTemplate.Name] = staticTemplate; |
|||
} |
|||
|
|||
foreach (var dynamicTemplate in dynamicTemplates) |
|||
{ |
|||
if (mergedTemplates.TryGetValue(dynamicTemplate.Name, out var existingTemplate)) |
|||
{ |
|||
MergeTemplate(existingTemplate, dynamicTemplate); |
|||
} |
|||
else |
|||
{ |
|||
mergedTemplates[dynamicTemplate.Name] = dynamicTemplate; |
|||
} |
|||
} |
|||
|
|||
return mergedTemplates.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static TemplateDefinition MergeTemplate(TemplateDefinition staticTemplate, TemplateDefinition dynamicTemplate) |
|||
{ |
|||
var localizationResourceName = dynamicTemplate.LocalizationResourceName ?? staticTemplate.LocalizationResourceName; |
|||
var defaultCultureName = dynamicTemplate.DefaultCultureName ?? staticTemplate.DefaultCultureName; |
|||
var displayName = dynamicTemplate.DisplayName ?? staticTemplate.DisplayName; |
|||
var isLayout = dynamicTemplate.IsLayout || staticTemplate.IsLayout; |
|||
var layout = dynamicTemplate.Layout ?? staticTemplate.Layout; |
|||
|
|||
var mergedTemplate = new TemplateDefinition( |
|||
staticTemplate.Name, |
|||
localizationResourceName, |
|||
displayName, |
|||
isLayout, |
|||
layout, |
|||
defaultCultureName |
|||
); |
|||
|
|||
foreach (var property in staticTemplate.Properties) |
|||
{ |
|||
mergedTemplate.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var property in dynamicTemplate.Properties) |
|||
{ |
|||
mergedTemplate.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
if (!string.IsNullOrEmpty(dynamicTemplate.RenderEngine)) |
|||
{ |
|||
mergedTemplate.RenderEngine = dynamicTemplate.RenderEngine; |
|||
} |
|||
else if (!string.IsNullOrEmpty(staticTemplate.RenderEngine)) |
|||
{ |
|||
mergedTemplate.RenderEngine = staticTemplate.RenderEngine; |
|||
} |
|||
|
|||
mergedTemplate.IsInlineLocalized = dynamicTemplate.IsInlineLocalized || staticTemplate.IsInlineLocalized; |
|||
|
|||
return mergedTemplate; |
|||
} |
|||
} |
|||
@ -1,142 +0,0 @@ |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.AspNetCore.RequestLocalization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; |
|||
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending; |
|||
using Volo.Abp.Authorization; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Features; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.Timing; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace LY.MicroService.BackendAdmin.ApplicationConfigurations; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class AbpApplicationConfigurationAppService : Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationAppService |
|||
{ |
|||
private readonly IServiceProvider _serviceProvider; |
|||
private readonly AbpApplicationConfigurationOptions _options; |
|||
private readonly ICachedObjectExtensionsDtoService _cachedObjectExtensionsDtoService; |
|||
|
|||
private readonly ISettingProvider _settingProvider; |
|||
private readonly ISettingDefinitionManager _settingDefinitionManager; |
|||
|
|||
public AbpApplicationConfigurationAppService( |
|||
IOptions<AbpLocalizationOptions> localizationOptions, |
|||
IOptions<AbpRequestLocalizationOptions> requestLocalizationOptions, |
|||
IOptions<AbpMultiTenancyOptions> multiTenancyOptions, |
|||
IServiceProvider serviceProvider, |
|||
IAbpAuthorizationPolicyProvider abpAuthorizationPolicyProvider, |
|||
IPermissionDefinitionManager permissionDefinitionManager, |
|||
DefaultAuthorizationPolicyProvider defaultAuthorizationPolicyProvider, |
|||
IPermissionChecker permissionChecker, |
|||
IAuthorizationService authorizationService, |
|||
ICurrentUser currentUser, |
|||
ISettingProvider settingProvider, |
|||
ISettingDefinitionManager settingDefinitionManager, |
|||
IFeatureDefinitionManager featureDefinitionManager, |
|||
ILanguageProvider languageProvider, |
|||
ITimezoneProvider timezoneProvider, |
|||
IOptions<AbpClockOptions> abpClockOptions, |
|||
ICachedObjectExtensionsDtoService cachedObjectExtensionsDtoService, |
|||
IOptions<AbpApplicationConfigurationOptions> options) |
|||
: base( |
|||
localizationOptions, |
|||
requestLocalizationOptions, |
|||
multiTenancyOptions, |
|||
serviceProvider, |
|||
abpAuthorizationPolicyProvider, |
|||
permissionDefinitionManager, |
|||
defaultAuthorizationPolicyProvider, |
|||
permissionChecker, |
|||
authorizationService, |
|||
currentUser, |
|||
settingProvider, |
|||
settingDefinitionManager, |
|||
featureDefinitionManager, |
|||
languageProvider, |
|||
timezoneProvider, |
|||
abpClockOptions, |
|||
cachedObjectExtensionsDtoService, |
|||
options) |
|||
{ |
|||
_options = options.Value; |
|||
_serviceProvider = serviceProvider; |
|||
_cachedObjectExtensionsDtoService = cachedObjectExtensionsDtoService; |
|||
|
|||
_settingProvider = settingProvider; |
|||
_settingDefinitionManager = settingDefinitionManager; |
|||
} |
|||
|
|||
public override async Task<ApplicationConfigurationDto> GetAsync(ApplicationConfigurationRequestOptions options) |
|||
{ |
|||
//TODO: Optimize & cache..?
|
|||
|
|||
Logger.LogDebug("Executing AbpApplicationConfigurationAppService.GetAsync()..."); |
|||
|
|||
var result = new ApplicationConfigurationDto |
|||
{ |
|||
Auth = await GetAuthConfigAsync(), |
|||
Features = await GetFeaturesConfigAsync(), |
|||
GlobalFeatures = await GetGlobalFeaturesConfigAsync(), |
|||
Localization = await GetLocalizationConfigAsync(options), |
|||
CurrentUser = GetCurrentUser(), |
|||
Setting = await GetSettingConfigAsync(), |
|||
MultiTenancy = GetMultiTenancy(), |
|||
CurrentTenant = GetCurrentTenant(), |
|||
Timing = await GetTimingConfigAsync(), |
|||
Clock = GetClockConfig(), |
|||
ObjectExtensions = _cachedObjectExtensionsDtoService.Get(), |
|||
ExtraProperties = new ExtraPropertyDictionary() |
|||
}; |
|||
|
|||
if (_options.Contributors.Any()) |
|||
{ |
|||
using (var scope = _serviceProvider.CreateScope()) |
|||
{ |
|||
var context = new ApplicationConfigurationContributorContext(scope.ServiceProvider, result); |
|||
foreach (var contributor in _options.Contributors) |
|||
{ |
|||
await contributor.ContributeAsync(context); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Logger.LogDebug("Executed AbpApplicationConfigurationAppService.GetAsync()."); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
protected async virtual Task<ApplicationSettingConfigurationDto> GetSettingConfigAsync() |
|||
{ |
|||
var result = new ApplicationSettingConfigurationDto |
|||
{ |
|||
Values = new Dictionary<string, string>() |
|||
}; |
|||
|
|||
var settingDefinitions = (await _settingDefinitionManager.GetAllAsync()).Where(x => x.IsVisibleToClients); |
|||
var settingNames = settingDefinitions.Select(x => x.Name).ToArray(); |
|||
if (settingNames.Length > 0) |
|||
{ |
|||
var settingValues = await _settingProvider.GetAllAsync(settingNames); |
|||
|
|||
foreach (var settingValue in settingValues) |
|||
{ |
|||
result.Values[settingValue.Name] = settingValue.Value; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,223 @@ |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Features; |
|||
|
|||
namespace LY.MicroService.BackendAdmin.Overrides; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergeFeatureDefinitionManager : FeatureDefinitionManager |
|||
{ |
|||
public MergeFeatureDefinitionManager( |
|||
IStaticFeatureDefinitionStore staticStore, |
|||
IDynamicFeatureDefinitionStore dynamicStore) |
|||
: base(staticStore, dynamicStore) |
|||
{ |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<FeatureDefinition>> GetAllAsync() |
|||
{ |
|||
var staticFeatures = await StaticStore.GetFeaturesAsync(); |
|||
var dynamicFeatures = await DynamicStore.GetFeaturesAsync(); |
|||
|
|||
var mergedFeatures = new Dictionary<string, FeatureDefinition>(); |
|||
|
|||
foreach (var staticFeature in staticFeatures) |
|||
{ |
|||
mergedFeatures[staticFeature.Name] = staticFeature; |
|||
} |
|||
|
|||
foreach (var dynamicFeature in dynamicFeatures) |
|||
{ |
|||
if (mergedFeatures.TryGetValue(dynamicFeature.Name, out var existingFeature)) |
|||
{ |
|||
MergeFeatureMetadata(existingFeature, dynamicFeature); |
|||
|
|||
foreach (var child in dynamicFeature.Children) |
|||
{ |
|||
MergeChildFeature(existingFeature, child); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
mergedFeatures[dynamicFeature.Name] = dynamicFeature; |
|||
} |
|||
} |
|||
|
|||
return mergedFeatures.Values.ToImmutableList(); |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync() |
|||
{ |
|||
var staticGroups = await StaticStore.GetGroupsAsync(); |
|||
var dynamicGroups = await DynamicStore.GetGroupsAsync(); |
|||
|
|||
var mergedGroups = new Dictionary<string, FeatureGroupDefinition>(); |
|||
|
|||
foreach (var staticGroup in staticGroups) |
|||
{ |
|||
mergedGroups[staticGroup.Name] = staticGroup; |
|||
} |
|||
|
|||
foreach (var dynamicGroup in dynamicGroups) |
|||
{ |
|||
if (mergedGroups.TryGetValue(dynamicGroup.Name, out var existingGroup)) |
|||
{ |
|||
MergeGroupFeatures(existingGroup, dynamicGroup); |
|||
} |
|||
else |
|||
{ |
|||
mergedGroups[dynamicGroup.Name] = dynamicGroup; |
|||
} |
|||
} |
|||
|
|||
return mergedGroups.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static void MergeGroupFeatures(FeatureGroupDefinition target, FeatureGroupDefinition source) |
|||
{ |
|||
foreach (var sourceFeature in source.Features) |
|||
{ |
|||
var existingFeature = target.GetFeatureOrNull(sourceFeature.Name); |
|||
|
|||
if (existingFeature == null) |
|||
{ |
|||
var newFeature = target.AddFeature( |
|||
sourceFeature.Name, |
|||
sourceFeature.DefaultValue, |
|||
sourceFeature.DisplayName, |
|||
sourceFeature.Description, |
|||
sourceFeature.ValueType, |
|||
sourceFeature.IsVisibleToClients, |
|||
sourceFeature.IsAvailableToHost |
|||
); |
|||
|
|||
CopyFeatureDetails(sourceFeature, newFeature); |
|||
|
|||
foreach (var child in sourceFeature.Children) |
|||
{ |
|||
AddChildFeatureRecursively(newFeature, child); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergeFeatureMetadata(existingFeature, sourceFeature); |
|||
|
|||
foreach (var child in sourceFeature.Children) |
|||
{ |
|||
MergeChildFeature(existingFeature, child); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void AddChildFeatureRecursively(FeatureDefinition parent, FeatureDefinition sourceChild) |
|||
{ |
|||
var newChild = parent.CreateChild( |
|||
sourceChild.Name, |
|||
sourceChild.DefaultValue, |
|||
sourceChild.DisplayName, |
|||
sourceChild.Description, |
|||
sourceChild.ValueType, |
|||
sourceChild.IsVisibleToClients, |
|||
sourceChild.IsAvailableToHost |
|||
); |
|||
|
|||
CopyFeatureDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildFeatureRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
|
|||
private static void MergeChildFeature(FeatureDefinition parent, FeatureDefinition sourceChild) |
|||
{ |
|||
var existingChild = parent.Children.FirstOrDefault(c => c.Name == sourceChild.Name); |
|||
|
|||
if (existingChild == null) |
|||
{ |
|||
var newChild = parent.CreateChild( |
|||
sourceChild.Name, |
|||
sourceChild.DefaultValue, |
|||
sourceChild.DisplayName, |
|||
sourceChild.Description, |
|||
sourceChild.ValueType, |
|||
sourceChild.IsVisibleToClients, |
|||
sourceChild.IsAvailableToHost |
|||
); |
|||
CopyFeatureDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildFeatureRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergeFeatureMetadata(existingChild, sourceChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
MergeChildFeature(existingChild, grandchild); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void CopyFeatureDetails(FeatureDefinition source, FeatureDefinition target) |
|||
{ |
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.AllowedProviders) |
|||
{ |
|||
if (!target.AllowedProviders.Contains(provider)) |
|||
{ |
|||
target.AllowedProviders.Add(provider); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void MergeFeatureMetadata(FeatureDefinition target, FeatureDefinition source) |
|||
{ |
|||
if (source.DisplayName != null) |
|||
{ |
|||
target.DisplayName = source.DisplayName; |
|||
} |
|||
|
|||
if (source.Description != null) |
|||
{ |
|||
target.Description = source.Description; |
|||
} |
|||
|
|||
if (source.DefaultValue != null) |
|||
{ |
|||
target.DefaultValue = source.DefaultValue; |
|||
} |
|||
|
|||
if (source.ValueType != null) |
|||
{ |
|||
target.ValueType = source.ValueType; |
|||
} |
|||
|
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.AllowedProviders) |
|||
{ |
|||
if (!target.AllowedProviders.Contains(provider)) |
|||
{ |
|||
target.AllowedProviders.Add(provider); |
|||
} |
|||
} |
|||
|
|||
target.IsVisibleToClients = target.IsVisibleToClients || source.IsVisibleToClients; |
|||
target.IsAvailableToHost = target.IsAvailableToHost || source.IsAvailableToHost; |
|||
} |
|||
} |
|||
@ -0,0 +1,189 @@ |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LY.MicroService.BackendAdmin.Overrides; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergePermissionDefinitionManager : PermissionDefinitionManager |
|||
{ |
|||
private readonly IStaticPermissionDefinitionStore _staticStore; |
|||
private readonly IDynamicPermissionDefinitionStore _dynamicStore; |
|||
|
|||
public MergePermissionDefinitionManager( |
|||
IStaticPermissionDefinitionStore staticStore, |
|||
IDynamicPermissionDefinitionStore dynamicStore) : base(staticStore, dynamicStore) |
|||
{ |
|||
_staticStore = staticStore; |
|||
_dynamicStore = dynamicStore; |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync() |
|||
{ |
|||
var staticGroups = await _staticStore.GetGroupsAsync(); |
|||
var dynamicGroups = await _dynamicStore.GetGroupsAsync(); |
|||
|
|||
var mergedGroups = new Dictionary<string, PermissionGroupDefinition>(); |
|||
|
|||
foreach (var staticGroup in staticGroups) |
|||
{ |
|||
mergedGroups[staticGroup.Name] = staticGroup; |
|||
} |
|||
|
|||
foreach (var dynamicGroup in dynamicGroups) |
|||
{ |
|||
if (mergedGroups.TryGetValue(dynamicGroup.Name, out var existingGroup)) |
|||
{ |
|||
MergeGroupPermissions(existingGroup, dynamicGroup); |
|||
} |
|||
else |
|||
{ |
|||
mergedGroups[dynamicGroup.Name] = dynamicGroup; |
|||
} |
|||
} |
|||
|
|||
return mergedGroups.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static void MergeGroupPermissions(PermissionGroupDefinition target, PermissionGroupDefinition source) |
|||
{ |
|||
foreach (var sourcePermission in source.Permissions) |
|||
{ |
|||
var existingPermission = target.GetPermissionOrNull(sourcePermission.Name); |
|||
|
|||
if (existingPermission == null) |
|||
{ |
|||
var newPermission = target.AddPermission( |
|||
sourcePermission.Name, |
|||
sourcePermission.DisplayName, |
|||
sourcePermission.MultiTenancySide, |
|||
sourcePermission.IsEnabled |
|||
); |
|||
|
|||
CopyPermissionDetails(sourcePermission, newPermission); |
|||
|
|||
foreach (var child in sourcePermission.Children) |
|||
{ |
|||
AddChildPermissionRecursively(newPermission, child); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergePermissionMetadata(existingPermission, sourcePermission); |
|||
|
|||
foreach (var sourceChild in sourcePermission.Children) |
|||
{ |
|||
MergeChildPermissions(existingPermission, sourceChild); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void AddChildPermissionRecursively(PermissionDefinition parent, PermissionDefinition sourceChild) |
|||
{ |
|||
var newChild = parent.AddChild( |
|||
sourceChild.Name, |
|||
sourceChild.DisplayName, |
|||
sourceChild.MultiTenancySide, |
|||
sourceChild.IsEnabled |
|||
); |
|||
|
|||
CopyPermissionDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildPermissionRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
|
|||
private static void MergeChildPermissions(PermissionDefinition parent, PermissionDefinition sourceChild) |
|||
{ |
|||
var existingChild = parent.Children.FirstOrDefault(c => c.Name == sourceChild.Name); |
|||
|
|||
if (existingChild == null) |
|||
{ |
|||
var newChild = parent.AddChild( |
|||
sourceChild.Name, |
|||
sourceChild.DisplayName, |
|||
sourceChild.MultiTenancySide, |
|||
sourceChild.IsEnabled |
|||
); |
|||
CopyPermissionDetails(sourceChild, newChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
AddChildPermissionRecursively(newChild, grandchild); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
MergePermissionMetadata(existingChild, sourceChild); |
|||
|
|||
foreach (var grandchild in sourceChild.Children) |
|||
{ |
|||
MergeChildPermissions(existingChild, grandchild); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void CopyPermissionDetails(PermissionDefinition source, PermissionDefinition target) |
|||
{ |
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.Providers) |
|||
{ |
|||
if (!target.Providers.Contains(provider)) |
|||
{ |
|||
target.Providers.Add(provider); |
|||
} |
|||
} |
|||
|
|||
foreach (var checker in source.StateCheckers) |
|||
{ |
|||
if (!target.StateCheckers.Contains(checker)) |
|||
{ |
|||
target.StateCheckers.Add(checker); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void MergePermissionMetadata(PermissionDefinition target, PermissionDefinition source) |
|||
{ |
|||
target.DisplayName = source.DisplayName; |
|||
|
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.Providers) |
|||
{ |
|||
if (!target.Providers.Contains(provider)) |
|||
{ |
|||
target.Providers.Add(provider); |
|||
} |
|||
} |
|||
|
|||
foreach (var checker in source.StateCheckers) |
|||
{ |
|||
if (!target.StateCheckers.Contains(checker)) |
|||
{ |
|||
target.StateCheckers.Add(checker); |
|||
} |
|||
} |
|||
|
|||
if (source.MultiTenancySide != MultiTenancySides.Both) |
|||
{ |
|||
target.MultiTenancySide |= source.MultiTenancySide; |
|||
} |
|||
|
|||
target.IsEnabled = target.IsEnabled || source.IsEnabled; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LY.MicroService.BackendAdmin.Overrides; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergeSettingDefinitionManager : SettingDefinitionManager |
|||
{ |
|||
private readonly IStaticSettingDefinitionStore _staticStore; |
|||
private readonly IDynamicSettingDefinitionStore _dynamicStore; |
|||
|
|||
public MergeSettingDefinitionManager( |
|||
IStaticSettingDefinitionStore staticStore, |
|||
IDynamicSettingDefinitionStore dynamicStore) |
|||
: base(staticStore, dynamicStore) |
|||
{ |
|||
_staticStore = staticStore; |
|||
_dynamicStore = dynamicStore; |
|||
} |
|||
|
|||
public async override Task<IReadOnlyList<SettingDefinition>> GetAllAsync() |
|||
{ |
|||
var staticSettings = await _staticStore.GetAllAsync(); |
|||
var dynamicSettings = await _dynamicStore.GetAllAsync(); |
|||
|
|||
var mergedSettings = new Dictionary<string, SettingDefinition>(); |
|||
|
|||
foreach (var staticSetting in staticSettings) |
|||
{ |
|||
mergedSettings[staticSetting.Name] = staticSetting; |
|||
} |
|||
|
|||
foreach (var dynamicSetting in dynamicSettings) |
|||
{ |
|||
if (mergedSettings.TryGetValue(dynamicSetting.Name, out var existingSetting)) |
|||
{ |
|||
MergeSetting(existingSetting, dynamicSetting); |
|||
} |
|||
else |
|||
{ |
|||
mergedSettings[dynamicSetting.Name] = dynamicSetting; |
|||
} |
|||
} |
|||
|
|||
return mergedSettings.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static void MergeSetting(SettingDefinition target, SettingDefinition source) |
|||
{ |
|||
target.DisplayName = source.DisplayName; |
|||
target.Description = source.Description ?? target.Description; |
|||
target.DefaultValue = source.DefaultValue ?? target.DefaultValue; |
|||
target.IsVisibleToClients = target.IsVisibleToClients || source.IsVisibleToClients; |
|||
target.IsInherited = target.IsInherited || source.IsInherited; |
|||
target.IsEncrypted = target.IsEncrypted || source.IsEncrypted; |
|||
|
|||
foreach (var property in source.Properties) |
|||
{ |
|||
target.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var provider in source.Providers) |
|||
{ |
|||
if (!target.Providers.Contains(provider)) |
|||
{ |
|||
target.Providers.Add(provider); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.TextTemplating; |
|||
|
|||
namespace LY.MicroService.BackendAdmin.Overrides; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MergeTemplateDefinitionManager : TemplateDefinitionManager |
|||
{ |
|||
public MergeTemplateDefinitionManager( |
|||
IStaticTemplateDefinitionStore staticStore, |
|||
IDynamicTemplateDefinitionStore dynamicStore) |
|||
: base(staticStore, dynamicStore) |
|||
{ |
|||
} |
|||
public async override Task<IReadOnlyList<TemplateDefinition>> GetAllAsync() |
|||
{ |
|||
var staticTemplates = await StaticStore.GetAllAsync(); |
|||
var dynamicTemplates = await DynamicStore.GetAllAsync(); |
|||
|
|||
var mergedTemplates = new Dictionary<string, TemplateDefinition>(); |
|||
|
|||
foreach (var staticTemplate in staticTemplates) |
|||
{ |
|||
mergedTemplates[staticTemplate.Name] = staticTemplate; |
|||
} |
|||
|
|||
foreach (var dynamicTemplate in dynamicTemplates) |
|||
{ |
|||
if (mergedTemplates.TryGetValue(dynamicTemplate.Name, out var existingTemplate)) |
|||
{ |
|||
MergeTemplate(existingTemplate, dynamicTemplate); |
|||
} |
|||
else |
|||
{ |
|||
mergedTemplates[dynamicTemplate.Name] = dynamicTemplate; |
|||
} |
|||
} |
|||
|
|||
return mergedTemplates.Values.ToImmutableList(); |
|||
} |
|||
|
|||
private static TemplateDefinition MergeTemplate(TemplateDefinition staticTemplate, TemplateDefinition dynamicTemplate) |
|||
{ |
|||
var localizationResourceName = dynamicTemplate.LocalizationResourceName ?? staticTemplate.LocalizationResourceName; |
|||
var defaultCultureName = dynamicTemplate.DefaultCultureName ?? staticTemplate.DefaultCultureName; |
|||
var displayName = dynamicTemplate.DisplayName ?? staticTemplate.DisplayName; |
|||
var isLayout = dynamicTemplate.IsLayout || staticTemplate.IsLayout; |
|||
var layout = dynamicTemplate.Layout ?? staticTemplate.Layout; |
|||
|
|||
var mergedTemplate = new TemplateDefinition( |
|||
staticTemplate.Name, |
|||
localizationResourceName, |
|||
displayName, |
|||
isLayout, |
|||
layout, |
|||
defaultCultureName |
|||
); |
|||
|
|||
foreach (var property in staticTemplate.Properties) |
|||
{ |
|||
mergedTemplate.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
foreach (var property in dynamicTemplate.Properties) |
|||
{ |
|||
mergedTemplate.Properties[property.Key] = property.Value; |
|||
} |
|||
|
|||
if (!string.IsNullOrEmpty(dynamicTemplate.RenderEngine)) |
|||
{ |
|||
mergedTemplate.RenderEngine = dynamicTemplate.RenderEngine; |
|||
} |
|||
else if (!string.IsNullOrEmpty(staticTemplate.RenderEngine)) |
|||
{ |
|||
mergedTemplate.RenderEngine = staticTemplate.RenderEngine; |
|||
} |
|||
|
|||
mergedTemplate.IsInlineLocalized = dynamicTemplate.IsInlineLocalized || staticTemplate.IsInlineLocalized; |
|||
|
|||
return mergedTemplate; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue