diff --git a/VirtoCommerce.LiquidThemeEngine/LiquidThemeEngineOptions.cs b/VirtoCommerce.LiquidThemeEngine/LiquidThemeEngineOptions.cs index ec57876d3..894628357 100644 --- a/VirtoCommerce.LiquidThemeEngine/LiquidThemeEngineOptions.cs +++ b/VirtoCommerce.LiquidThemeEngine/LiquidThemeEngineOptions.cs @@ -10,11 +10,31 @@ public class LiquidThemeEngineOptions public IList TemplatesDiscoveryFolders { get; set; } = new List() { "templates", "snippets", "layout", "assets" }; public string ThemesAssetsRelativeUrl { get; set; } = "~/themes/assets"; public bool RethrowLiquidRenderErrors { get; set; } = false; + + /// + /// The path to the base theme that will be used to discover the theme resources not found by the path of theme for current store. + /// This parameter can be used for theme inheritance logic. + /// Example values: + /// Electronics/default -> wwwroot/cms-content/Themes/Electronics/default + /// default-> wwwroot/cms-content/Themes/default + /// + public string BaseThemePath { get; set; } + /// + /// Original description: /// The name of the base theme that will be used to discover the theme resources not found by the path of theme for current store. /// This parameter can be used for theme inheritance logic. /// Example values: default_theme -> wwwroot/cms-content/default_theme + /// + /// How it actually worked: + /// Storefront used this parameter as a store name, i.e. Electronics -> wwwroot/cms-content/Themes/Electronics/default /// + [Obsolete("Obsolete. Use BaseThemePath instead.")] public string BaseThemeName { get; set; } + + /// + /// Set to true if you want to merge current theme settings with base theme settings instead of placement + /// + public bool MergeBaseSettings { get; set; } } } diff --git a/VirtoCommerce.LiquidThemeEngine/ShopifyLiquidThemeEngine.cs b/VirtoCommerce.LiquidThemeEngine/ShopifyLiquidThemeEngine.cs index 3d0468e7a..cc2bfb116 100644 --- a/VirtoCommerce.LiquidThemeEngine/ShopifyLiquidThemeEngine.cs +++ b/VirtoCommerce.LiquidThemeEngine/ShopifyLiquidThemeEngine.cs @@ -94,7 +94,12 @@ public ShopifyLiquidThemeEngine(IStorefrontMemoryCache memoryCache, IWorkContext private string CurrentThemePath => Path.Combine("Themes", WorkContext.CurrentStore.Id, CurrentThemeName); //Relative path to the discovery of theme resources that weren't found by the current path. - private string BaseThemePath => !string.IsNullOrEmpty(_options.BaseThemeName) ? Path.Combine("Themes", _options.BaseThemeName, "default") : null; + private string BaseThemePath => + !string.IsNullOrEmpty(_options.BaseThemePath) ? Path.Combine("Themes", _options.BaseThemePath) : +#pragma warning disable 618 + // We need to use obsolete value here for backward compatibility. + !string.IsNullOrEmpty(_options.BaseThemeName) ? Path.Combine("Themes", _options.BaseThemeName, "default") : null; +#pragma warning restore 618 private string BaseThemeSettingPath => BaseThemePath != null ? Path.Combine(BaseThemePath, "config", "settings_data.json") : null; public string BaseThemeLocalePath => BaseThemePath != null ? Path.Combine(BaseThemePath, "locales") : null; @@ -347,44 +352,28 @@ public ValueTask RenderTemplateAsync(string templateContent, string temp public IDictionary GetSettings(string defaultValue = null) { var cacheKey = CacheKey.With(GetType(), "GetSettings", CurrentThemeSettingPath, defaultValue); - return _memoryCache.GetOrCreateExclusive(cacheKey, (cacheItem) => + return _memoryCache.GetOrCreateExclusive(cacheKey, cacheItem => { cacheItem.AddExpirationToken(new CompositeChangeToken(new[] { ThemeEngineCacheRegion.CreateChangeToken(), _themeBlobProvider.Watch(CurrentThemeSettingPath) })); - var retVal = new Dictionary().WithDefaultValue(defaultValue); - //Load all data from current theme config - var resultSettings = InnerGetAllSettings(_themeBlobProvider, CurrentThemeSettingPath); - if (resultSettings == null && BaseThemeSettingPath != null) + + JObject result; + var baseThemeSettings = new JObject(); + var currentThemeSettings = result = GetCurrentSettingsPreset(InnerGetAllSettings(_themeBlobProvider, CurrentThemeSettingPath)); + + //Try to load settings from base theme path and merge them with resources for local theme + if ((_options.MergeBaseSettings || currentThemeSettings == null) && !string.IsNullOrEmpty(BaseThemeSettingPath)) { - resultSettings = InnerGetAllSettings(_themeBlobProvider, BaseThemeSettingPath); + cacheItem.AddExpirationToken(new CompositeChangeToken(new[] { ThemeEngineCacheRegion.CreateChangeToken(), _themeBlobProvider.Watch(BaseThemeSettingPath) })); + result = baseThemeSettings = GetCurrentSettingsPreset(InnerGetAllSettings(_themeBlobProvider, BaseThemeSettingPath)); } - if (resultSettings != null) - { - //Get actual preset from merged config - var currentPreset = resultSettings.GetValue("current"); - if (currentPreset is JValue) - { - var currentPresetName = ((JValue)currentPreset).Value.ToString(); - if (!(resultSettings.GetValue("presets") is JObject presets) || !presets.Children().Any()) - { - throw new StorefrontException("Setting presets not defined"); - } - IList allPresets = presets.Children().Cast().ToList(); - resultSettings = allPresets.FirstOrDefault(p => p.Name == currentPresetName).Value as JObject; - if (resultSettings == null) - { - throw new StorefrontException($"Setting preset with name '{currentPresetName}' not found"); - } - } - if (currentPreset is JObject) - { - resultSettings = (JObject)currentPreset; - } - - retVal = resultSettings.ToObject>().ToDictionary(x => x.Key, x => x.Value).WithDefaultValue(defaultValue); + if (_options.MergeBaseSettings) + { + result = baseThemeSettings; + result.Merge(currentThemeSettings ?? new JObject(), new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Merge }); } - return retVal; + return result.ToObject>().ToDictionary(x => x.Key, x => x.Value).WithDefaultValue(defaultValue); }); } @@ -475,16 +464,47 @@ private static JObject InnerGetAllSettings(IContentBlobProvider themeBlobProvide throw new ArgumentNullException(nameof(settingsPath)); } - JObject retVal = null; + var result = new JObject(); if (themeBlobProvider.PathExists(settingsPath)) { using (var stream = themeBlobProvider.OpenRead(settingsPath)) { - retVal = JsonConvert.DeserializeObject(stream.ReadToString()); + result = JsonConvert.DeserializeObject(stream.ReadToString()); } } - return retVal; + return result; + } + + /// + /// Get actual preset from config + /// + /// + private static JObject GetCurrentSettingsPreset(JObject allSettings) + { + var result = allSettings; + var currentPreset = allSettings.GetValue("current"); + if (currentPreset is JValue currentPresetValue) + { + var currentPresetName = currentPresetValue.Value.ToString(); + if (!(allSettings.GetValue("presets") is JObject presets) || !presets.Children().Any()) + { + throw new StorefrontException("Setting presets not defined"); + } + + IList allPresets = presets.Children().Cast().ToList(); + result = allPresets.FirstOrDefault(p => p.Name == currentPresetName)?.Value as JObject; + if (result == null) + { + throw new StorefrontException($"Setting preset with name '{currentPresetName}' not found"); + } + } + if (currentPreset is JObject preset) + { + result = preset; + } + + return result; } private string ReadTemplateByPath(string templatePath) diff --git a/VirtoCommerce.Storefront/appsettings.json b/VirtoCommerce.Storefront/appsettings.json index faba7a60d..37d329c9a 100644 --- a/VirtoCommerce.Storefront/appsettings.json +++ b/VirtoCommerce.Storefront/appsettings.json @@ -38,10 +38,12 @@ }, "LiquidThemeEngine": { "RethrowLiquidRenderErrors": false, - //The name of the base theme that will be used to discover the theme resources not found by the path of theme for current store. - //This parameter can be used for theme inheritance logic. - // Example values: 'default_theme' will map to this path 'wwwroot/cms-content/default_theme' - "BaseThemeName": "" + // The path to the base theme that will be used to discover the theme resources not found by the path of theme for current store. + // This parameter can be used for theme inheritance logic. + // Example values: Electronics/default_theme -> wwwroot/cms-content/Themes/Electronics/default_theme + "BaseThemePath": "", + // Set to true if you want to merge current theme settings with base theme settings instead of placement + "MergeBaseSettings": false }, "RequireHttps": { "Enabled": false,