From 8221f8558140a1150135853535dd8a0600b2dd9d Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Mon, 17 Nov 2025 13:59:40 +0200 Subject: [PATCH 1/2] VCST-4291: Enhance performance and UI. feat: Improved performance in document processing. --- .../Extensions/ConfigurationExtensions.cs | 6 + .../Search/ContentIndexDocumentBuilder.cs | 10 +- .../ContentIndexDocumentChangesProvider.cs | 35 +- .../Controllers/Api/ContentController.cs | 37 +- .../Scripts/blades/content-main.js | 472 +++++++++--------- .../Scripts/blades/content-main.tpl.html | 40 +- .../Scripts/widgets/storeCMSWidget.tpl.html | 8 +- .../package-lock.json | 7 +- 8 files changed, 314 insertions(+), 301 deletions(-) diff --git a/src/VirtoCommerce.ContentModule.Core/Extensions/ConfigurationExtensions.cs b/src/VirtoCommerce.ContentModule.Core/Extensions/ConfigurationExtensions.cs index c4ace300..b400df99 100644 --- a/src/VirtoCommerce.ContentModule.Core/Extensions/ConfigurationExtensions.cs +++ b/src/VirtoCommerce.ContentModule.Core/Extensions/ConfigurationExtensions.cs @@ -10,5 +10,11 @@ public static bool IsContentFullTextSearchEnabled(this IConfiguration configurat var value = configuration["Search:ContentFullTextSearchEnabled"]; return value.TryParse(false); } + + public static bool IsContentStatisticEnabled(this IConfiguration configuration) + { + var value = configuration["Content:StatisticEnabled"]; + return value.TryParse(false); + } } } diff --git a/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentBuilder.cs b/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentBuilder.cs index d3e19fd4..0630e7cf 100644 --- a/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentBuilder.cs +++ b/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentBuilder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using VirtoCommerce.ContentModule.Core.Model; @@ -39,9 +41,9 @@ public Task BuildSchemaAsync(IndexDocument schema) public virtual async Task> GetDocumentsAsync(IList documentIds) { - var result = new List(); + var result = new ConcurrentBag(); - foreach (var documentId in documentIds) + await Parallel.ForEachAsync(documentIds, async (documentId, cancellationToken) => { try { @@ -59,9 +61,9 @@ public virtual async Task> GetDocumentsAsync(IList { _log.LogError(e, "Cannot create document for ID '{DocumentId}'", documentId); } - } + }); - return result; + return result.ToList(); } private async Task<(string, string, ContentFile)> GetFile(string documentId) diff --git a/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentChangesProvider.cs b/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentChangesProvider.cs index 551a53fc..5f949a73 100644 --- a/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentChangesProvider.cs +++ b/src/VirtoCommerce.ContentModule.Data/Search/ContentIndexDocumentChangesProvider.cs @@ -2,9 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; using VirtoCommerce.ContentModule.Core; using VirtoCommerce.ContentModule.Core.Model; using VirtoCommerce.ContentModule.Core.Services; +using VirtoCommerce.ContentModule.Data.Model; +using VirtoCommerce.Platform.Core.Caching; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.SearchModule.Core.Model; using VirtoCommerce.SearchModule.Core.Services; @@ -19,15 +22,18 @@ public class ContentIndexDocumentChangesProvider : IIndexDocumentChangesProvider private readonly IContentFileService _contentFileService; private readonly IStoreSearchService _storeSearchService; private readonly IContentStatisticService _contentStatisticService; + private readonly IPlatformMemoryCache _platformMemoryCache; public ContentIndexDocumentChangesProvider( IContentFileService contentFileService, IStoreSearchService storeSearchService, - IContentStatisticService contentStatisticService) + IContentStatisticService contentStatisticService, + IPlatformMemoryCache platformMemoryCache) { _contentFileService = contentFileService; _storeSearchService = storeSearchService; _contentStatisticService = contentStatisticService; + _platformMemoryCache = platformMemoryCache; } public virtual async Task> GetChangesAsync(DateTime? startDate, DateTime? endDate, long skip, long take) @@ -90,18 +96,25 @@ private async Task GetStores(int skip, int take) return stores; } - private async Task> GetFiles(Store store, string contentType, DateTime now) + private Task> GetFiles(Store store, string contentType, DateTime now) { - var filter = AbstractTypeFactory.TryCreateInstance(); - filter.ContentType = contentType; - filter.StoreId = store.Id; - var files = await _contentFileService.EnumerateFiles(filter); - return files.Select(x => (x, new IndexDocumentChange + var cacheKey = CacheKey.With(GetType(), nameof(GetFiles), store.Id, contentType); + return _platformMemoryCache.GetOrCreateAsync>(cacheKey, async (cacheEntry) => { - DocumentId = DocumentIdentifierHelper.GenerateId(store.Id, contentType, x), - ChangeType = IndexDocumentChangeType.Modified, - ChangeDate = x.ModifiedDate ?? now, - })).ToList(); + cacheEntry.AddExpirationToken(ContentCacheRegion.CreateChangeToken()); + cacheEntry.SetAbsoluteExpiration(TimeSpan.FromHours(3)); + + var filter = AbstractTypeFactory.TryCreateInstance(); + filter.ContentType = contentType; + filter.StoreId = store.Id; + var files = await _contentFileService.EnumerateFiles(filter); + return files.Select(x => (x, new IndexDocumentChange + { + DocumentId = DocumentIdentifierHelper.GenerateId(store.Id, contentType, x), + ChangeType = IndexDocumentChangeType.Modified, + ChangeDate = x.ModifiedDate ?? now, + })).ToList(); + }); } } } diff --git a/src/VirtoCommerce.ContentModule.Web/Controllers/Api/ContentController.cs b/src/VirtoCommerce.ContentModule.Web/Controllers/Api/ContentController.cs index 75aa6c2e..fc827ef6 100644 --- a/src/VirtoCommerce.ContentModule.Web/Controllers/Api/ContentController.cs +++ b/src/VirtoCommerce.ContentModule.Web/Controllers/Api/ContentController.cs @@ -53,25 +53,40 @@ public class ContentController( [Authorize(Permissions.Read)] public async Task> GetStoreContentStats(string storeId) { - var pagesTask = contentStats.GetStorePagesCountAsync(storeId); - var blogsTask = contentStats.GetStoreBlogsCountAsync(storeId); - var themesTask = contentStats.GetStoreThemesCountAsync(storeId); + var store = await storeService.GetNoCloneAsync(storeId, StoreResponseGroup.DynamicProperties.ToString()); - var storeTask = storeService.GetNoCloneAsync(storeId, StoreResponseGroup.DynamicProperties.ToString()); - - await Task.WhenAll(themesTask, blogsTask, pagesTask, storeTask); + if (store == null) + { + return NotFound(); + } - var activeThemeProperty = storeTask.Result.DynamicProperties.FirstOrDefault(x => x.Name == "DefaultThemeName"); + var activeThemeProperty = store.DynamicProperties.FirstOrDefault(x => x.Name == "DefaultThemeName"); var activeTheme = activeThemeProperty?.Values?.FirstOrDefault()?.Value?.ToString(); var result = new ContentStatistic { - PagesCount = pagesTask.Result, - BlogsCount = blogsTask.Result, - ThemesCount = themesTask.Result, + ActiveThemeName = activeTheme ?? ContentConstants.DefaultTheme, - ActiveThemeName = activeTheme ?? ContentConstants.DefaultTheme + PagesCount = -1, + BlogsCount = -1, + ThemesCount = -1, }; + + if (!configuration.IsContentStatisticEnabled()) + { + return Ok(result); + } + + var pagesTask = contentStats.GetStorePagesCountAsync(storeId); + var blogsTask = contentStats.GetStoreBlogsCountAsync(storeId); + var themesTask = contentStats.GetStoreThemesCountAsync(storeId); + + await Task.WhenAll(themesTask, blogsTask, pagesTask); + + result.PagesCount = pagesTask.Result; + result.BlogsCount = blogsTask.Result; + result.ThemesCount = themesTask.Result; + return Ok(result); } diff --git a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js index a0d11873..d7998851 100644 --- a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js +++ b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js @@ -1,240 +1,240 @@ angular.module('virtoCommerce.contentModule') - .controller('virtoCommerce.contentModule.contentMainController', [ - '$scope', '$state', '$stateParams', 'virtoCommerce.contentModule.menus', - 'virtoCommerce.contentModule.contentApi', 'virtoCommerce.storeModule.stores', - 'platformWebApp.bladeNavigationService', 'platformWebApp.dialogService', - 'platformWebApp.widgetService', 'platformWebApp.bladeUtils', - 'virtoCommerce.contentModule.fileHandlerFactory', - function ($scope, $state, $stateParams, menus, contentApi, stores, - bladeNavigationService, dialogService, widgetService, bladeUtils, fileHandlerFactory) { - var blade = $scope.blade; - - var filter = $scope.filter = {}; - - filter.criteriaChanged = function () { - if ($scope.pageSettings.currentPage > 1) { - $scope.pageSettings.currentPage = 1; - } else { - blade.refresh(); - } - }; - - blade.refresh = function () { - blade.isLoading = true; - blade.currentEntities = []; - - if ($stateParams.storeId) { - stores.get({ id: $stateParams.storeId }, blade.openThemes); - }; - - stores.search({ - keyword: filter.keyword ? filter.keyword : undefined, - skip: ($scope.pageSettings.currentPage - 1) * $scope.pageSettings.itemsPerPageCount, - take: $scope.pageSettings.itemsPerPageCount - }, function (data) { - - var loadCounter = data.results.length * 2; - $scope.pageSettings.totalItems = data.totalCount; - var finnalyFunction = function () { - blade.isLoading = --loadCounter; - }; - - blade.isLoading = _.any(data.results); - - $scope.pageSettings.totalItems = data.totalCount; - - _.each(data.results, function (x) { - blade.currentEntities.push({ - storeId: x.id, - store: x, - themesCount: '...', - pagesCount: '...', - blogsCount: '...', - listLinksCount: '...' - }); - - var statsPromise = blade.refreshWidgets(x.id, 'stats') - if (statsPromise) { - statsPromise.finally(finnalyFunction); - } - - var menusPromise = blade.refreshWidgets(x.id, 'menus'); - if (menusPromise) { - menusPromise.finally(finnalyFunction); - } - }); - }); - - $scope.thereIsWidgetToShow = _.any(widgetService.widgetsMap['contentMainListItem'], function (w) { return !angular.isFunction(w.isVisible) || w.isVisible(blade); }); - }; - - - blade.refreshWidgets = function (storeId, requestType, data) { - var entity = _.findWhere(blade.currentEntities, { storeId: storeId }); - - switch (requestType) { - case 'menus': - return menus.get({ storeId: storeId }, function (data) { - entity.listLinksCount = data.length; - }, function (error) { bladeNavigationService.setError('Error ' + error.status, blade); }).$promise; - case 'stats': - return contentApi.getStatistics({ storeId: storeId }, function (data) { - angular.extend(entity, data); - }, function (error) { bladeNavigationService.setError('Error ' + error.status, blade); }).$promise; - case 'defaultTheme': - entity.activeThemeName = data; - return null; - default: - return null; - } - }; - - $scope.$on("cms-menus-changed", function (event, storeId) { - blade.refresh(storeId, 'menus'); - }); - - $scope.$on("cms-statistics-changed", function (event, storeId) { - blade.refresh(storeId, 'stats'); - }); - - blade.openThemes = function (store) { - var newBlade = { - id: "themesListBlade", - storeId: store.id, - title: 'content.blades.themes-list.title', - titleValues: { name: store.name }, - subtitle: 'content.blades.themes-list.subtitle', - controller: 'virtoCommerce.contentModule.themesListController', - template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/themes/themes-list.tpl.html', - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.openPages = function (data) { - var newBlade = { - id: "pagesList", - contentType: 'pages', - storeId: data.storeId, - storeUrl: data.store.url, - languages: data.store.languages, - currentEntity: data, - title: data.store.name, - subtitle: 'content.blades.pages-list.subtitle-pages', - controller: 'virtoCommerce.contentModule.pagesListController', - template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/pages/pages-list.tpl.html' - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.openLinkLists = function (data) { - var newBlade = { - id: "linkListBlade", - storeId: data.storeId, - title: 'content.blades.link-lists.title', - subtitle: 'content.blades.link-lists.subtitle', - subtitleValues: { name: data.store.name }, - controller: 'virtoCommerce.contentModule.linkListsController', - template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/menu/link-lists.tpl.html', - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.openBlogs = function (data) { - var newBlade = { - id: "blogsListBlade", - contentType: 'blogs', - storeId: data.storeId, - storeUrl: data.store.url, - languages: data.store.languages, - currentEntity: data, - title: data.store.name, - subtitle: 'content.blades.pages-list.subtitle-blogs', - controller: 'virtoCommerce.contentModule.pagesListController', - template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/pages/pages-list.tpl.html' - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.addNewTheme = function (data) { - var newBlade = { - id: 'addTheme', - isNew: true, - isActivateAfterSave: !data.themesCount, - store: data.store, - storeId: data.storeId, - controller: 'virtoCommerce.contentModule.themeDetailController', - template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/themes/theme-detail.tpl.html', - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.addNewPage = function (data) { - fileHandlerFactory.handleAction('create', { blade: blade, store: data.store }); - }; - - blade.addNewLinkList = function (data) { - var newBlade = { - id: 'addMenuLinkListBlade', - storeId: data.storeId, - isNew: true, - title: 'content.blades.menu-link-list.title-new', - subtitle: 'content.blades.menu-link-list.subtitle-new', - controller: 'virtoCommerce.contentModule.menuLinkListController', - template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/menu/menu-link-list.tpl.html', - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.addBlog = function (data) { - var newBlade = { - id: 'newBlog', - contentType: 'blogs', - storeId: data.storeId, - currentEntity: {}, - isNew: true, - title: 'content.blades.edit-blog.title-new', - subtitle: 'content.blades.edit-blog.subtitle-new', - controller: 'virtoCommerce.contentModule.editBlogController', - template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/pages/edit-blog.tpl.html', - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.openTheme = function (data) { - var newBlade = { - id: 'themeAssetListBlade', - contentType: 'themes', - storeId: data.storeId, - currentEntity: { name: data.activeThemeName, url: data.activeThemeURL }, - subtitle: 'content.blades.asset-list.subtitle', - controller: 'virtoCommerce.contentModule.assetListController', - template: 'Modules/$(VirtoCommerce.Assets)/Scripts/blades/asset-list.tpl.html' - }; - bladeNavigationService.showBlade(newBlade, blade); - }; - - blade.previewTheme = function (data) { - if (data.store.url) { - window.open(data.store.url + '?previewtheme=' + data.activeThemeName, '_blank'); - } - else { - var dialog = { - id: "noUrlInStore", - title: "content.dialogs.set-store-url.title", - message: "content.dialogs.set-store-url.message" - } - dialogService.showNotificationDialog(dialog); - } - } - - $scope.openStoresModule = function () { - $state.go('workspace.storeModule'); - }; - - blade.headIcon = 'fa fa-code'; - - bladeUtils.initializePagination($scope); - $scope.pageSettings.itemsPerPageCount = 3; + .controller('virtoCommerce.contentModule.contentMainController', [ + '$scope', '$state', '$stateParams', 'virtoCommerce.contentModule.menus', + 'virtoCommerce.contentModule.contentApi', 'virtoCommerce.storeModule.stores', + 'platformWebApp.bladeNavigationService', 'platformWebApp.dialogService', + 'platformWebApp.widgetService', 'platformWebApp.bladeUtils', + 'virtoCommerce.contentModule.fileHandlerFactory', + function ($scope, $state, $stateParams, menus, contentApi, stores, + bladeNavigationService, dialogService, widgetService, bladeUtils, fileHandlerFactory) { + var blade = $scope.blade; + + var filter = $scope.filter = {}; + + filter.criteriaChanged = function () { + if ($scope.pageSettings.currentPage > 1) { + $scope.pageSettings.currentPage = 1; + } else { + blade.refresh(); + } + }; + + blade.refresh = function () { + blade.isLoading = true; + blade.currentEntities = []; + + if ($stateParams.storeId) { + stores.get({ id: $stateParams.storeId }, blade.openThemes); + }; + + stores.search({ + keyword: filter.keyword ? filter.keyword : undefined, + skip: ($scope.pageSettings.currentPage - 1) * $scope.pageSettings.itemsPerPageCount, + take: $scope.pageSettings.itemsPerPageCount + }, function (data) { + + var loadCounter = data.results.length * 2; + $scope.pageSettings.totalItems = data.totalCount; + var finnalyFunction = function () { + blade.isLoading = --loadCounter; + }; + + blade.isLoading = _.any(data.results); + + $scope.pageSettings.totalItems = data.totalCount; + + _.each(data.results, function (x) { + blade.currentEntities.push({ + storeId: x.id, + store: x, + themesCount: '...', + pagesCount: '...', + blogsCount: '...', + listLinksCount: '...' + }); + + var statsPromise = blade.refreshWidgets(x.id, 'stats') + if (statsPromise) { + statsPromise.finally(finnalyFunction); + } + + var menusPromise = blade.refreshWidgets(x.id, 'menus'); + if (menusPromise) { + menusPromise.finally(finnalyFunction); + } + }); + }); + + $scope.thereIsWidgetToShow = _.any(widgetService.widgetsMap['contentMainListItem'], function (w) { return !angular.isFunction(w.isVisible) || w.isVisible(blade); }); + }; + + + blade.refreshWidgets = function (storeId, requestType, data) { + var entity = _.findWhere(blade.currentEntities, { storeId: storeId }); + + switch (requestType) { + case 'menus': + return menus.get({ storeId: storeId }, function (data) { + entity.listLinksCount = data.length; + }, function (error) { bladeNavigationService.setError('Error ' + error.status, blade); }).$promise; + case 'stats': + return contentApi.getStatistics({ storeId: storeId }, function (data) { + angular.extend(entity, data); + }, function (error) { bladeNavigationService.setError('Error ' + error.status, blade); }).$promise; + case 'defaultTheme': + entity.activeThemeName = data; + return null; + default: + return null; + } + }; + + $scope.$on("cms-menus-changed", function (event, storeId) { + blade.refresh(storeId, 'menus'); + }); + + $scope.$on("cms-statistics-changed", function (event, storeId) { + blade.refresh(storeId, 'stats'); + }); + + blade.openThemes = function (store) { + var newBlade = { + id: "themesListBlade", + storeId: store.id, + title: 'content.blades.themes-list.title', + titleValues: { name: store.name }, + subtitle: 'content.blades.themes-list.subtitle', + controller: 'virtoCommerce.contentModule.themesListController', + template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/themes/themes-list.tpl.html', + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.openPages = function (data) { + var newBlade = { + id: "pagesList", + contentType: 'pages', + storeId: data.storeId, + storeUrl: data.store.url, + languages: data.store.languages, + currentEntity: data, + title: data.store.name, + subtitle: 'content.blades.pages-list.subtitle-pages', + controller: 'virtoCommerce.contentModule.pagesListController', + template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/pages/pages-list.tpl.html' + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.openLinkLists = function (data) { + var newBlade = { + id: "linkListBlade", + storeId: data.storeId, + title: 'content.blades.link-lists.title', + subtitle: 'content.blades.link-lists.subtitle', + subtitleValues: { name: data.store.name }, + controller: 'virtoCommerce.contentModule.linkListsController', + template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/menu/link-lists.tpl.html', + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.openBlogs = function (data) { + var newBlade = { + id: "blogsListBlade", + contentType: 'blogs', + storeId: data.storeId, + storeUrl: data.store.url, + languages: data.store.languages, + currentEntity: data, + title: data.store.name, + subtitle: 'content.blades.pages-list.subtitle-blogs', + controller: 'virtoCommerce.contentModule.pagesListController', + template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/pages/pages-list.tpl.html' + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.addNewTheme = function (data) { + var newBlade = { + id: 'addTheme', + isNew: true, + isActivateAfterSave: !data.themesCount, + store: data.store, + storeId: data.storeId, + controller: 'virtoCommerce.contentModule.themeDetailController', + template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/themes/theme-detail.tpl.html', + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.addNewPage = function (data) { + fileHandlerFactory.handleAction('create', { blade: blade, store: data.store }); + }; + + blade.addNewLinkList = function (data) { + var newBlade = { + id: 'addMenuLinkListBlade', + storeId: data.storeId, + isNew: true, + title: 'content.blades.menu-link-list.title-new', + subtitle: 'content.blades.menu-link-list.subtitle-new', + controller: 'virtoCommerce.contentModule.menuLinkListController', + template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/menu/menu-link-list.tpl.html', + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.addBlog = function (data) { + var newBlade = { + id: 'newBlog', + contentType: 'blogs', + storeId: data.storeId, + currentEntity: {}, + isNew: true, + title: 'content.blades.edit-blog.title-new', + subtitle: 'content.blades.edit-blog.subtitle-new', + controller: 'virtoCommerce.contentModule.editBlogController', + template: 'Modules/$(VirtoCommerce.Content)/Scripts/blades/pages/edit-blog.tpl.html', + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.openTheme = function (data) { + var newBlade = { + id: 'themeAssetListBlade', + contentType: 'themes', + storeId: data.storeId, + currentEntity: { name: data.activeThemeName, url: data.activeThemeURL }, + subtitle: 'content.blades.asset-list.subtitle', + controller: 'virtoCommerce.contentModule.assetListController', + template: 'Modules/$(VirtoCommerce.Assets)/Scripts/blades/asset-list.tpl.html' + }; + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.previewTheme = function (data) { + if (data.store.url) { + window.open(data.store.url + '?previewtheme=' + data.activeThemeName, '_blank'); + } + else { + var dialog = { + id: "noUrlInStore", + title: "content.dialogs.set-store-url.title", + message: "content.dialogs.set-store-url.message" + } + dialogService.showNotificationDialog(dialog); + } + } + + $scope.openStoresModule = function () { + $state.go('workspace.storeModule'); + }; + + blade.headIcon = 'fa fa-code'; + + bladeUtils.initializePagination($scope); + $scope.pageSettings.itemsPerPageCount = 3; }]); diff --git a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html index 854d464b..ccb9bc8f 100644 --- a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html +++ b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html @@ -27,50 +27,26 @@
{{ 'content.blades.content-main.labels.active-theme' | translate }} {{data.activeThemeName}}
-
{{ 'content.blades.content-main.labels.themes' | translate }}
-
{{data.themesCount}}
- {{ 'platform.commands.add' | translate }} +
{{data.themesCount}}
+
{{ 'content.blades.content-main.labels.pages' | translate }}
-
{{data.pagesCount}}
- {{ 'platform.commands.add' | translate }} +
{{data.pagesCount}}
+
{{ 'content.blades.content-main.labels.link-lists' | translate }}
-
{{data.listLinksCount}}
- {{ 'platform.commands.add' | translate }} +
{{data.listLinksCount}}
+
{{ 'content.blades.content-main.labels.blogs' | translate }}
-
{{data.blogsCount}}
- {{ 'platform.commands.add' | translate }} +
{{data.blogsCount}}
+
diff --git a/src/VirtoCommerce.ContentModule.Web/Scripts/widgets/storeCMSWidget.tpl.html b/src/VirtoCommerce.ContentModule.Web/Scripts/widgets/storeCMSWidget.tpl.html index 0ff6a214..80a48334 100644 --- a/src/VirtoCommerce.ContentModule.Web/Scripts/widgets/storeCMSWidget.tpl.html +++ b/src/VirtoCommerce.ContentModule.Web/Scripts/widgets/storeCMSWidget.tpl.html @@ -1,9 +1,9 @@ -
+
  • {{'content.widgets.store-cms.active-theme' | translate}} {{statistics.activeThemeName }}
  • -
  • {{'content.widgets.store-cms.themes' | translate}} {{statistics.themesCount}}
  • -
  • {{'content.widgets.store-cms.pages' | translate}} {{statistics.pagesCount }}
  • -
  • {{'content.widgets.store-cms.blogs' | translate}} {{statistics.blogsCount }}
  • +
  • {{'content.widgets.store-cms.themes' | translate}} {{statistics.themesCount}}
  • +
  • {{'content.widgets.store-cms.pages' | translate}} {{statistics.pagesCount }}
  • +
  • {{'content.widgets.store-cms.blogs' | translate}} {{statistics.blogsCount }}
\ No newline at end of file diff --git a/src/VirtoCommerce.ContentModule.Web/package-lock.json b/src/VirtoCommerce.ContentModule.Web/package-lock.json index 7c53ffc7..90c655e0 100644 --- a/src/VirtoCommerce.ContentModule.Web/package-lock.json +++ b/src/VirtoCommerce.ContentModule.Web/package-lock.json @@ -416,10 +416,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" From 869a400762fbac72bba7a5bb896a733d034b320d Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Mon, 17 Nov 2025 18:33:23 +0200 Subject: [PATCH 2/2] Enhance UI with new widget styles and interactivity. --- .../Content/css/themes.css | 53 ++++++++++++++- .../Scripts/blades/content-main.js | 14 ++++ .../Scripts/blades/content-main.tpl.html | 68 +++++++++++++------ 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/src/VirtoCommerce.ContentModule.Web/Content/css/themes.css b/src/VirtoCommerce.ContentModule.Web/Content/css/themes.css index 56cdcd53..a42610bf 100644 --- a/src/VirtoCommerce.ContentModule.Web/Content/css/themes.css +++ b/src/VirtoCommerce.ContentModule.Web/Content/css/themes.css @@ -1,4 +1,53 @@ .form-input.__expanded textarea { - max-height: 500px; - min-height: 500px; + max-height: 500px; + min-height: 500px; +} + +/* Content widgets (tiles with icon + text) */ +.vc-content-widget { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 16px 12px; + text-align: center; + cursor: pointer; + border-radius: 4px; + background-color: #ffffff; + transition: box-shadow 0.15s ease, transform 0.15s ease, background-color 0.15s ease; +} + +.vc-content-widget__icon { + width: 42px; + height: 42px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + /*background-color: #f0f2f5;*/ + color: #a5a5a5; + margin-bottom: 10px; + font-size: 32px; +} + +.vc-content-widget__title { + font-size: 12px; + font-weight: 500; + color: #161d25; + line-height: 1.4; + margin-bottom: 2px; +} + +.vc-content-widget__counter { + font-size: 11px; + font-weight: 600; + color: #a0a4b5; +} + +.vc-content-widget:hover { +} + +.vc-content-widget:active { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + transform: translateY(0); } diff --git a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js index d7998851..65431d4b 100644 --- a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js +++ b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.js @@ -213,6 +213,20 @@ angular.module('virtoCommerce.contentModule') bladeNavigationService.showBlade(newBlade, blade); }; + blade.openStore = function (store) { + if (store.url) { + window.open(store.url, '_blank'); + } + else { + var dialog = { + id: "noUrlInStore", + title: "content.dialogs.set-store-url.title", + message: "content.dialogs.set-store-url.message" + } + dialogService.showNotificationDialog(dialog); + } + } + blade.previewTheme = function (data) { if (data.store.url) { window.open(data.store.url + '?previewtheme=' + data.activeThemeName, '_blank'); diff --git a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html index ccb9bc8f..76bdd723 100644 --- a/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html +++ b/src/VirtoCommerce.ContentModule.Web/Scripts/blades/content-main.tpl.html @@ -14,8 +14,8 @@
-
-
{{data.store.name}}
+
+
{{data.store.name}}
@@ -23,30 +23,54 @@

{{ 'content.blades.content-main.labels.no-active-theme' | translate }}

-
-
- {{ 'content.blades.content-main.labels.active-theme' | translate }} {{data.activeThemeName}} -
+
+
+ {{ 'content.blades.content-main.labels.active-theme' | translate }} {{data.activeThemeName}} +
-
-
{{ 'content.blades.content-main.labels.themes' | translate }}
-
{{data.themesCount}}
- +
+
+ +
+
+ {{ 'content.blades.content-main.labels.themes' | translate }} +
+
+ {{data.themesCount}} +
-
-
{{ 'content.blades.content-main.labels.pages' | translate }}
-
{{data.pagesCount}}
- +
+
+ +
+
+ {{ 'content.blades.content-main.labels.pages' | translate }} +
+
+ {{data.pagesCount}} +
-
-
{{ 'content.blades.content-main.labels.link-lists' | translate }}
-
{{data.listLinksCount}}
- +
+
+ +
+
+ {{ 'content.blades.content-main.labels.blogs' | translate }} +
+
+ {{data.blogsCount}} +
-
-
{{ 'content.blades.content-main.labels.blogs' | translate }}
-
{{data.blogsCount}}
- +
+
+ +
+
+ {{ 'content.blades.content-main.labels.link-lists' | translate }} +
+
+ {{data.listLinksCount}} +