From 5287d16fe8d576bfb81c0a61ccde7a59d728e7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20M=C3=B8nster?= Date: Mon, 13 Mar 2023 15:48:28 +0100 Subject: [PATCH 1/3] Create .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..358e4f59 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file From fed17f1b747ded3904fac4c1236bc859cc0fd146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20M=C3=B8nster?= Date: Thu, 16 Mar 2023 22:16:23 +0100 Subject: [PATCH 2/3] Adds uSync functionality --- .gitignore | 3 +- .../IMetaFieldsValueRepository.cs | 3 + .../MetaFieldsDatabaseRepository.cs | 8 + .../MetaFieldsValueService.cs | 7 + .../SeoToolkit.Umbraco.Site.csproj | 2 + .../appsettings-schema.json | 3 + .../appsettings-schema.usync.json | 278 ++++++++++++++++++ src/SeoToolkit.Umbraco.Site/appsettings.json | 2 +- src/SeoToolkit.Umbraco.sln | 9 + .../Composers/USyncComposer.cs | 19 ++ .../Constants/Serialization.cs | 9 + .../Handlers/MetaFieldSettingsHandler.cs | 52 ++++ .../Handlers/MetaFieldValuesHandler.cs | 57 ++++ .../Handlers/SeoToolKitSyncHandlerBase.cs | 32 ++ src/SeoToolkit.uSync/SeoToolkit.uSync.csproj | 19 ++ .../MetaFieldSettingsSerializer.cs | 150 ++++++++++ .../Serializers/MetaFieldValuesSerializer.cs | 228 ++++++++++++++ .../XmlTrackers/SeoToolkitXmlTracker.cs | 24 ++ 18 files changed, 903 insertions(+), 2 deletions(-) create mode 100644 src/SeoToolkit.Umbraco.Site/appsettings-schema.usync.json create mode 100644 src/SeoToolkit.uSync/Composers/USyncComposer.cs create mode 100644 src/SeoToolkit.uSync/Constants/Serialization.cs create mode 100644 src/SeoToolkit.uSync/Handlers/MetaFieldSettingsHandler.cs create mode 100644 src/SeoToolkit.uSync/Handlers/MetaFieldValuesHandler.cs create mode 100644 src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs create mode 100644 src/SeoToolkit.uSync/SeoToolkit.uSync.csproj create mode 100644 src/SeoToolkit.uSync/Serializers/MetaFieldSettingsSerializer.cs create mode 100644 src/SeoToolkit.uSync/Serializers/MetaFieldValuesSerializer.cs create mode 100644 src/SeoToolkit.uSync/XmlTrackers/SeoToolkitXmlTracker.cs diff --git a/.gitignore b/.gitignore index f7be36c7..faad736f 100644 --- a/.gitignore +++ b/.gitignore @@ -486,4 +486,5 @@ $RECYCLE.BIN/ # Nuget package Umbraco.Cms.StaticAssets will copy them in during dotnet build **/wwwroot/umbraco/ -**/SeoToolkit.Umbraco.Site/App_Plugins/ \ No newline at end of file +**/SeoToolkit.Umbraco.Site/App_Plugins/ +src/SeoToolkit.Umbraco.Site/uSync/ diff --git a/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/IMetaFieldsValueRepository.cs b/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/IMetaFieldsValueRepository.cs index 0cdf43ce..06b4503c 100644 --- a/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/IMetaFieldsValueRepository.cs +++ b/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/IMetaFieldsValueRepository.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using SeoToolkit.Umbraco.MetaFields.Core.Models.SeoSettings.Database; namespace SeoToolkit.Umbraco.MetaFields.Core.Repositories.SeoValueRepository { @@ -10,5 +12,6 @@ public interface IMetaFieldsValueRepository bool Exists(int nodeId, string fieldAlias, string culture); Dictionary GetAllValues(int nodeId, string culture); + IEnumerable> GetAll(); } } diff --git a/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs b/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs index d22154e7..af88c1c3 100644 --- a/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs +++ b/src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs @@ -74,5 +74,13 @@ public Dictionary GetAllValues(int nodeId, string culture) .ToDictionary(it => it.Alias, it => JsonConvert.DeserializeObject(it.UserValue)); } } + + public IEnumerable> GetAll() + { + using var scope = _scopeProvider.CreateScope(); + var values = scope.Database + .Fetch(scope.SqlContext.Sql()).GroupBy(it => it.NodeId); + return values; + } } } diff --git a/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs b/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs index 048559f3..e21eeaca 100644 --- a/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs +++ b/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; using SeoToolkit.Umbraco.MetaFields.Core.Interfaces.Services; +using SeoToolkit.Umbraco.MetaFields.Core.Models.SeoSettings.Database; using SeoToolkit.Umbraco.MetaFields.Core.Repositories.SeoValueRepository; namespace SeoToolkit.Umbraco.MetaFields.Core.Services.SeoValueService @@ -44,6 +46,11 @@ public void AddValues(int nodeId, Dictionary values, string cult ClearCache(nodeId); } + public IEnumerable> GetAll() + { + return _repository.GetAll(); + } + public void Delete(int nodeId, string fieldAlias, string culture = null) { _repository.Delete(nodeId, fieldAlias, culture.IfNullOrWhiteSpace(GetCulture())); diff --git a/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj b/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj index 6e4ecc2c..9d1f71a4 100644 --- a/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj +++ b/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj @@ -11,6 +11,7 @@ + @@ -46,6 +47,7 @@ + diff --git a/src/SeoToolkit.Umbraco.Site/appsettings-schema.json b/src/SeoToolkit.Umbraco.Site/appsettings-schema.json index 223ea1c3..45f0c9ba 100644 --- a/src/SeoToolkit.Umbraco.Site/appsettings-schema.json +++ b/src/SeoToolkit.Umbraco.Site/appsettings-schema.json @@ -3883,6 +3883,9 @@ }, { "$ref": "appsettings-schema.Umbraco.Cms.json#" + }, + { + "$ref": "appsettings-schema.usync.json#" } ] } \ No newline at end of file diff --git a/src/SeoToolkit.Umbraco.Site/appsettings-schema.usync.json b/src/SeoToolkit.Umbraco.Site/appsettings-schema.usync.json new file mode 100644 index 00000000..cd76483f --- /dev/null +++ b/src/SeoToolkit.Umbraco.Site/appsettings-schema.usync.json @@ -0,0 +1,278 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "uSyncAppSettings", + "type": "object", + "properties": { + "uSync": { + "$ref": "#/definitions/USyncuSyncDefinition" + } + }, + "definitions": { + "USyncuSyncDefinition": { + "type": "object", + "description": "Configuration of uSync settings", + "properties": { + "Settings": { + "description": "uSync settings", + "oneOf": [ + { + "$ref": "#/definitions/USyncBackOfficeConfigurationuSyncSettings" + } + ] + }, + "ForceFips": { + "type": "boolean", + "description": "Force uSync to use FIPS compliant hashing algorthims when comparing files" + }, + "Sets": { + "description": "Settings of Handler sets", + "oneOf": [ + { + "$ref": "#/definitions/USyncuSyncSetsDefinition" + } + ] + }, + "AutoTemplates": { + "description": "Settings for the AutoTemplates package, (dynamic adding of templates based on files on disk)", + "oneOf": [ + { + "$ref": "#/definitions/USyncAutoTemplatesDefinition" + } + ] + } + } + }, + "USyncBackOfficeConfigurationuSyncSettings": { + "type": "object", + "description": "uSync Settings", + "properties": { + "RootFolder": { + "type": "string", + "description": "Location where all uSync files are saved by default", + "default": "uSync/v9/" + }, + "DefaultSet": { + "type": "string", + "description": "The default handler set to use on all notification triggered events", + "default": "Default" + }, + "ImportAtStartup": { + "type": "string", + "description": "Import when Umbraco boots (can be group name or 'All' so everything is done, blank or 'none' == off)", + "default": "None" + }, + "ExportAtStartup": { + "type": "string", + "description": "Export when Umbraco boots", + "default": "None" + }, + "ExportOnSave": { + "type": "string", + "description": "Export when an item is saved in Umbraco", + "default": "All" + }, + "UiEnabledGroups": { + "type": "string", + "description": "The handler groups that are enabled in the UI.", + "default": "All" + }, + "ReportDebug": { + "type": "boolean", + "description": "Debug reports (creates an export into a temp folder for comparison)", + "default": false + }, + "AddOnPing": { + "type": "boolean", + "description": "Ping the AddOnUrl to get the json used to show the addons dashboard", + "default": true + }, + "RebuildCacheOnCompletion": { + "type": "boolean", + "description": "Pre Umbraco 8.4 - rebuild the cache was needed after content was imported", + "default": false + }, + "FailOnMissingParent": { + "type": "boolean", + "description": "Fail if the items parent is not in umbraco or part of the batch being imported", + "default": false + }, + "CacheFolderKeys": { + "type": "boolean", + "description": "Should folder keys be cached (for speed)", + "default": true + }, + "ShowVersionCheckWarning": { + "type": "boolean", + "description": "Show a version check warning to the user if the folder version is less than the version expected by uSync.", + "default": true + }, + "CustomMappings": { + "type": "object", + "description": "Custom mapping keys, allows users to add a simple config mapping to make one property type to behave like an existing one", + "additionalProperties": { + "type": "string" + } + }, + "SignalRRoot": { + "type": "string", + "description": "location of SignalR hub script", + "default": "" + }, + "EnableHistory": { + "type": "boolean", + "description": "Should the history view be on of off ? ", + "default": true + }, + "DefaultExtension": { + "type": "string", + "description": "Default file extension for the uSync files. ", + "default": "config" + }, + "ImportOnFirstBoot": { + "type": "boolean", + "description": "Import the uSync folder on the first boot. ", + "default": false + }, + "FirstBootGroup": { + "type": "string", + "description": "Handler group(s) to run on first boot, default is All (so full import)", + "default": "All" + }, + "DisableDashboard": { + "type": "boolean", + "description": "Disable the default dashboard (so people can't accedently press the buttons).", + "default": "false" + }, + "SummaryDashboard": { + "type": "boolean", + "description": "summerize results (for when there are loads and loads of items)\n ", + "default": "false" + }, + "SummaryLimit": { + "type": "integer", + "description": "limit of items to display before flicking to summary view. (this is per handler)\n ", + "format": "int32", + "default": 1000 + } + } + }, + "USyncuSyncSetsDefinition": { + "type": "object", + "properties": { + "Default": { + "$ref": "#/definitions/USyncBackOfficeConfigurationuSyncHandlerSetSettings" + } + } + }, + "USyncBackOfficeConfigurationuSyncHandlerSetSettings": { + "type": "object", + "description": "Settings for a handler set (group of handlers)", + "properties": { + "Enabled": { + "type": "boolean", + "description": "Is this handler set enabled", + "default": true + }, + "HandlerGroups": { + "type": "array", + "description": "List of groups handlers can belong to.", + "items": { + "type": "string" + } + }, + "DisabledHandlers": { + "type": "array", + "description": "List of disabled handlers", + "items": { + "type": "string" + } + }, + "HandlerDefaults": { + "description": "Default settings for all handlers", + "oneOf": [ + { + "$ref": "#/definitions/USyncBackOfficeConfigurationHandlerSettings" + } + ] + }, + "Handlers": { + "type": "object", + "description": "Settings for named handlers ", + "additionalProperties": { + "$ref": "#/definitions/USyncBackOfficeConfigurationHandlerSettings" + } + }, + "IsSelectable": { + "type": "boolean", + "description": "for handlers to appear in the drop down on the dashboard they have to be selectable\n " + } + } + }, + "USyncBackOfficeConfigurationHandlerSettings": { + "type": "object", + "description": "Settings to control who a handler works", + "properties": { + "Enabled": { + "type": "boolean", + "description": "Is handler enabled or disabled", + "default": true + }, + "Actions": { + "type": "array", + "description": "List of actions the handler is configured for. ", + "items": { + "type": "string" + } + }, + "UseFlatStructure": { + "type": "boolean", + "description": "Should use a flat folder structure when exporting items", + "default": true + }, + "GuidNames": { + "type": "boolean", + "description": "Items should be saved with their guid/key value as the filename", + "default": false + }, + "FailOnMissingParent": { + "type": "boolean", + "description": "Imports should fail if the parent item is missing (if false, item be importated go a close as possible to location)", + "default": false + }, + "Group": { + "type": "string", + "description": "Override the group the handler belongs too.", + "default": "" + }, + "Settings": { + "type": "object", + "description": "Additional settings for the handler", + "additionalProperties": { + "type": "string" + } + } + } + }, + "USyncAutoTemplatesDefinition": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean", + "description": "Enable AutoTemplates feature", + "default": false + }, + "Delete": { + "type": "boolean", + "description": "Delete templates from Umbraco if the file is missing from disk", + "default": false + }, + "Delay": { + "type": "integer", + "description": "Amount of time (milliseconds) to wait after file change event before applying changes", + "format": "int32", + "default": 1000 + } + } + } + } +} \ No newline at end of file diff --git a/src/SeoToolkit.Umbraco.Site/appsettings.json b/src/SeoToolkit.Umbraco.Site/appsettings.json index b759e357..92a8300e 100644 --- a/src/SeoToolkit.Umbraco.Site/appsettings.json +++ b/src/SeoToolkit.Umbraco.Site/appsettings.json @@ -11,7 +11,7 @@ } }, "ConnectionStrings": { - "umbracoDbDSN": "Server=localhost;Database=uSeoToolkitV11;Integrated Security=true" + "umbracoDbDSN": "Server=(localdb)\\MSSQLLocalDB;Database=uSeoToolkit;Integrated Security=true" }, "Umbraco": { "CMS": { diff --git a/src/SeoToolkit.Umbraco.sln b/src/SeoToolkit.Umbraco.sln index 86d06fb2..3cfa9e05 100644 --- a/src/SeoToolkit.Umbraco.sln +++ b/src/SeoToolkit.Umbraco.sln @@ -49,6 +49,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeoToolkit.Umbraco.Redirect EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeoToolkit.Umbraco.Redirects.Core", "SeoToolkit.Umbraco.Redirects.Core\SeoToolkit.Umbraco.Redirects.Core.csproj", "{566D1DBF-EC21-405C-BB92-424C4A1C1B88}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "uSync", "uSync", "{782F3EFF-9063-4C1D-AB3F-1E45734EFB27}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeoToolkit.uSync", "SeoToolkit.uSync\SeoToolkit.uSync.csproj", "{3B981A49-B278-4D40-B8E2-EB62266FC80D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,6 +127,10 @@ Global {566D1DBF-EC21-405C-BB92-424C4A1C1B88}.Debug|Any CPU.Build.0 = Debug|Any CPU {566D1DBF-EC21-405C-BB92-424C4A1C1B88}.Release|Any CPU.ActiveCfg = Release|Any CPU {566D1DBF-EC21-405C-BB92-424C4A1C1B88}.Release|Any CPU.Build.0 = Release|Any CPU + {3B981A49-B278-4D40-B8E2-EB62266FC80D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B981A49-B278-4D40-B8E2-EB62266FC80D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B981A49-B278-4D40-B8E2-EB62266FC80D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B981A49-B278-4D40-B8E2-EB62266FC80D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -140,6 +148,7 @@ Global {5B460335-8786-4FA9-911B-AEC5A752B88C} = {5641B436-FCCB-4D1A-9D25-6A39B95031CB} {7C9ABE0D-106C-48C0-B0CF-DBBCD8AF334E} = {B0B49325-8248-4DB8-889D-211D9408D7A5} {566D1DBF-EC21-405C-BB92-424C4A1C1B88} = {B0B49325-8248-4DB8-889D-211D9408D7A5} + {3B981A49-B278-4D40-B8E2-EB62266FC80D} = {782F3EFF-9063-4C1D-AB3F-1E45734EFB27} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {551ADBA5-3DFB-4300-929C-2BE0DB346636} diff --git a/src/SeoToolkit.uSync/Composers/USyncComposer.cs b/src/SeoToolkit.uSync/Composers/USyncComposer.cs new file mode 100644 index 00000000..466a3e1b --- /dev/null +++ b/src/SeoToolkit.uSync/Composers/USyncComposer.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using SeoToolkit.Umbraco.uSync.Core.Serializers; +using SeoToolkit.Umbraco.uSync.Core.XmlTrackers; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using uSync.Core.Tracking; + +namespace SeoToolkit.Umbraco.uSync.Core.Composers; + +public class USyncComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + } +} \ No newline at end of file diff --git a/src/SeoToolkit.uSync/Constants/Serialization.cs b/src/SeoToolkit.uSync/Constants/Serialization.cs new file mode 100644 index 00000000..4b584702 --- /dev/null +++ b/src/SeoToolkit.uSync/Constants/Serialization.cs @@ -0,0 +1,9 @@ +namespace SeoToolkit.Umbraco.uSync.Core.Constants; + +public static class Serialization +{ + public const string DocumentTypeSettingsDto = "DocumentTypeSettingsDto"; + public const string MetaFieldValues = "MetaFieldValues"; + + public const string RootName = "Content"; +} \ No newline at end of file diff --git a/src/SeoToolkit.uSync/Handlers/MetaFieldSettingsHandler.cs b/src/SeoToolkit.uSync/Handlers/MetaFieldSettingsHandler.cs new file mode 100644 index 00000000..d39d1ee7 --- /dev/null +++ b/src/SeoToolkit.uSync/Handlers/MetaFieldSettingsHandler.cs @@ -0,0 +1,52 @@ +using SeoToolkit.Umbraco.MetaFields.Core.Models.DocumentTypeSettings.Business; +using uSync.BackOffice; + + +using Microsoft.Extensions.Logging; +using SeoToolkit.Umbraco.Common.Core.Interfaces; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Strings; + +using uSync.BackOffice.Configuration; +using uSync.BackOffice.Services; +using uSync.BackOffice.SyncHandlers; +using uSync.Core; +using uSyncConstants = uSync.BackOffice.uSyncConstants; + +namespace SeoToolkit.uSync.Handlers +{ + [SyncHandler("seoToolkitTest", "seoToolkit DocTypes", "seoToolkitContentTypes", uSyncConstants.Priorites.ContentTypes, + IsTwoPass = true, Icon = "icon-item-arrangement", EntityType = Constants.UdiEntityType.DocumentType)] + public class MetaFieldSettingsHandler : SeoToolKitSyncHandlerBase, ISyncHandler + { + private readonly IRepository _repository; + + //TODO should ideally be implemented + protected override IEnumerable DeleteMissingItems(DocumentTypeSettingsDto parent, IEnumerable keysToKeep, bool reportOnly) + { + return new List(); + } + + + protected override IEnumerable GetChildItems(DocumentTypeSettingsDto parent) + { + return parent == null! ? _repository.GetAll().ToList() : new List(); + } + + protected override string GetItemName(DocumentTypeSettingsDto item) + { + return item.Content.Name ?? ""; + } + + public IEnumerable ProcessPostImport(string folder, IEnumerable actions, HandlerSettings config) + { + return new List(); + } + + public MetaFieldSettingsHandler(IRepository repository, ILogger logger, AppCaches appCaches, IShortStringHelper shortStringHelper, SyncFileService syncFileService, uSyncEventService mutexService, uSyncConfigService uSyncConfig, ISyncItemFactory itemFactory) : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, itemFactory) + { + _repository = repository; + } + } +} \ No newline at end of file diff --git a/src/SeoToolkit.uSync/Handlers/MetaFieldValuesHandler.cs b/src/SeoToolkit.uSync/Handlers/MetaFieldValuesHandler.cs new file mode 100644 index 00000000..78fa921e --- /dev/null +++ b/src/SeoToolkit.uSync/Handlers/MetaFieldValuesHandler.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Logging; +using SeoToolkit.Umbraco.MetaFields.Core.Models.SeoSettings.Database; +using SeoToolkit.Umbraco.MetaFields.Core.Repositories.SeoValueRepository; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Strings; +using uSync.BackOffice; +using uSync.BackOffice.Configuration; +using uSync.BackOffice.Services; +using uSync.BackOffice.SyncHandlers; +using uSync.Core; +using uSyncConstants = uSync.BackOffice.uSyncConstants; + +namespace SeoToolkit.uSync.Handlers; + +[SyncHandler("seoToolkitMetaFieldValuesHandler", "SeoToolkit Meta Fields", "SeoToolkitMetaFields", uSyncConstants.Priorites.Content + , Icon = "icon-list", IsTwoPass = false, EntityType = Constants.UdiEntityType.Document)] +public class MetaFieldValuesHandler : SeoToolKitSyncHandlerBase>?>, ISyncHandler +{ + private readonly IMetaFieldsValueRepository _metaFieldsValueRepository; + + public MetaFieldValuesHandler(IMetaFieldsValueRepository metaFieldsValueRepository, ILogger logger, AppCaches appCaches, IShortStringHelper shortStringHelper, SyncFileService syncFileService, uSyncEventService mutexService, uSyncConfigService uSyncConfig, ISyncItemFactory itemFactory) : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, itemFactory) + { + this.ItemType = "SeoToolkit Meta"; + _metaFieldsValueRepository = metaFieldsValueRepository; + } + + //TODO this should ideally be implemented + protected override IEnumerable DeleteMissingItems(KeyValuePair>? parent, IEnumerable keysToKeep, bool reportOnly) + { + return new List(); + } + + public IEnumerable ProcessPostImport(string folder, IEnumerable actions, HandlerSettings config) + { + return new List(); + } + + protected override IEnumerable>?> GetChildItems(KeyValuePair>? parent) + { + if (parent != null!) return new List>?>(); + var allField = _metaFieldsValueRepository.GetAll().Select(metaFieldGroup => + { + KeyValuePair>? kv = new KeyValuePair>(metaFieldGroup.Key, metaFieldGroup.ToList()); + return kv; + }); + return allField; + } + + protected override string GetItemName(KeyValuePair>? item) + { + if (item == null) + return ""; + return item.Value.Key.ToString(); + } + +} \ No newline at end of file diff --git a/src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs b/src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs new file mode 100644 index 00000000..c8d764a3 --- /dev/null +++ b/src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs @@ -0,0 +1,32 @@ +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Strings; +using uSync.BackOffice.Configuration; +using uSync.BackOffice.Services; +using uSync.BackOffice.SyncHandlers; +using uSync.Core; + + +namespace SeoToolkit.uSync.Handlers; +public abstract class SeoToolKitSyncHandlerBase : SyncHandlerRoot +{ + protected override IEnumerable GetFolders(TObject parent) + => Enumerable.Empty(); + protected override TObject GetFromService(TObject item) + => item; + + protected override bool ShouldImport(XElement node, HandlerSettings config) + { + return base.ShouldImport(node, config); + } + + protected override bool ShouldExport(XElement node, HandlerSettings config) + { + return base.ShouldExport(node, config); + } + + protected SeoToolKitSyncHandlerBase(ILogger> logger, AppCaches appCaches, IShortStringHelper shortStringHelper, SyncFileService syncFileService, uSyncEventService mutexService, uSyncConfigService uSyncConfig, ISyncItemFactory itemFactory) : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, itemFactory) + { + } +} \ No newline at end of file diff --git a/src/SeoToolkit.uSync/SeoToolkit.uSync.csproj b/src/SeoToolkit.uSync/SeoToolkit.uSync.csproj new file mode 100644 index 00000000..d61a6c3e --- /dev/null +++ b/src/SeoToolkit.uSync/SeoToolkit.uSync.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/SeoToolkit.uSync/Serializers/MetaFieldSettingsSerializer.cs b/src/SeoToolkit.uSync/Serializers/MetaFieldSettingsSerializer.cs new file mode 100644 index 00000000..e65731ff --- /dev/null +++ b/src/SeoToolkit.uSync/Serializers/MetaFieldSettingsSerializer.cs @@ -0,0 +1,150 @@ +using System.Text.Json; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using SeoToolkit.Umbraco.Common.Core.Interfaces; +using SeoToolkit.Umbraco.MetaFields.Core.Constants; +using SeoToolkit.Umbraco.MetaFields.Core.Interfaces.SeoField; +using SeoToolkit.Umbraco.MetaFields.Core.Models.Converters; +using SeoToolkit.Umbraco.MetaFields.Core.Models.DocumentTypeSettings.Business; +using SeoToolkit.Umbraco.MetaFields.Core.Models.SeoField; +using SeoToolkit.Umbraco.MetaFields.Core.Services.DocumentTypeSettings; +using Umbraco.Cms.Core.Web; +using uSync.Core; +using uSync.Core.Models; +using uSync.Core.Serialization; + +namespace SeoToolkit.Umbraco.uSync.Core.Serializers; + +[SyncSerializer("dce705fa-7259-4754-8d6b-1eeeb05f5f12", "SeoToolkit.MetaFieldSerializer", + Constants.Serialization.MetaFieldValues)] +public class MetaFieldSettingsSerializer : SyncSerializerRoot, ISyncSerializer +{ + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly IRepository _repository; + private readonly IMetaFieldsSettingsService _metaFieldsSettingsService; + + public MetaFieldSettingsSerializer(IMetaFieldsSettingsService metaFieldsSettingsService, + IUmbracoContextFactory umbracoContextFactory, ILogger logger, + IRepository repository) : base(logger) + { + _metaFieldsSettingsService = metaFieldsSettingsService; + _umbracoContextFactory = umbracoContextFactory; + _repository = repository; + } + + private const string Value = "Value"; + private const string Fields = "Fields"; + private const string Editor = "Editor"; + private const string EditEditor = "EditEditor"; + private const string SeoField = "SeoField"; + + protected override SyncAttempt SerializeCore(DocumentTypeSettingsDto item, SyncSerializerOptions options) + { + var node = InitializeNode(item); + + var settingsDto = _repository.Get(item.Content.Id); + + if (settingsDto != null) + { + foreach (var el in from field in settingsDto.Fields let value = JsonSerializer.Serialize(field.Value) let editor = JsonSerializer.Serialize(field.Key.Editor) let editEditor = JsonSerializer.Serialize(field.Key.EditEditor) select new XElement(field.Key.Alias, + new XElement(SeoField, field.Key.Alias), + new XElement(Value, value), + new XElement(Editor, editor), + new XElement(EditEditor, editEditor))) + { + node.Add(el); + } + } + else + { + return SyncAttempt.Succeed(item.Content.Name, node, typeof(DocumentTypeSettingsDto), + ChangeType.NoChange); + } + + + return SyncAttempt.Succeed(item.Content.Name, node, typeof(DocumentTypeSettingsDto), + ChangeType.Export); + } + + + private XElement InitializeNode(DocumentTypeSettingsDto item) + { + return new XElement(Constants.Serialization.RootName, new XAttribute("Key", ItemKey(item))); + } + + protected override SyncAttempt DeserializeCore(XElement node, + SyncSerializerOptions options) + { + var attempt = FindItem(node); + + attempt.Fields = node.Elements().Select(element => + { + var seoFieldAlias = element.Element(SeoField)?.Value; + var valueJson = element.Element(Value)?.Value; + if (seoFieldAlias == null || valueJson == null) return new KeyValuePair(); + ISeoField field = seoFieldAlias switch + { + SeoFieldAliasConstants.Title => new SeoTitleField(), + SeoFieldAliasConstants.CanonicalUrl => new CanonicalUrlField(), + SeoFieldAliasConstants.MetaDescription => new SeoDescriptionField(), + SeoFieldAliasConstants.OpenGraphDescription => new OpenGraphDescriptionField(), + SeoFieldAliasConstants.OpenGraphImage => new OpenGraphImageField(_umbracoContextFactory), + SeoFieldAliasConstants.OpenGraphTitle => new OpenGraphTitleField(), + _ => new CanonicalUrlField() + }; + + var value = JsonSerializer.Deserialize(valueJson); + if (value?.Value != null && seoFieldAlias != SeoFieldAliasConstants.CanonicalUrl) + value.Value = JsonSerializer.Deserialize(value.Value?.ToString()!); + + return new KeyValuePair(field, value!); + + }).ToDictionary(x => x.Key, x => x.Value); + + + return SyncAttempt.Succeed(node.GetAlias(), attempt, + typeof(DocumentTypeSettingsDto), ChangeType.Import); + } + + public override bool IsValid(XElement node) + { + return true; + } + + public override DocumentTypeSettingsDto FindItem(int id) + { + return _repository.Get(id); + } + + public override DocumentTypeSettingsDto FindItem(Guid key) + { + return _repository.GetAll().FirstOrDefault(setting => setting.Content.Key == key)!; + } + + public override DocumentTypeSettingsDto FindItem(string alias) + { + return _repository.GetAll().FirstOrDefault(setting => setting.Content.Alias == alias)!; + } + + + public override void SaveItem(DocumentTypeSettingsDto item) + { + _metaFieldsSettingsService.Set(item); + } + + //TODO implement this + public override void DeleteItem(DocumentTypeSettingsDto item) + { + _repository.Delete(item.Content.Id); + } + + public override string ItemAlias(DocumentTypeSettingsDto item) + { + return item.Content.Alias; + } + + public override Guid ItemKey(DocumentTypeSettingsDto item) + { + return item.Content.Key; + } +} \ No newline at end of file diff --git a/src/SeoToolkit.uSync/Serializers/MetaFieldValuesSerializer.cs b/src/SeoToolkit.uSync/Serializers/MetaFieldValuesSerializer.cs new file mode 100644 index 00000000..d9cf9b39 --- /dev/null +++ b/src/SeoToolkit.uSync/Serializers/MetaFieldValuesSerializer.cs @@ -0,0 +1,228 @@ +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using SeoToolkit.Umbraco.MetaFields.Core.Interfaces.Services; +using SeoToolkit.Umbraco.MetaFields.Core.Models.SeoSettings.Database; +using SeoToolkit.Umbraco.MetaFields.Core.Repositories.SeoValueRepository; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using uSync.Core; +using uSync.Core.Models; +using uSync.Core.Serialization; + +namespace SeoToolkit.Umbraco.uSync.Core.Serializers; + +[SyncSerializer("cfeaa2d0-8af5-4ef4-9dd6-3b696df18189", "SeoToolkit.MetaFieldSerializer", + Constants.Serialization.MetaFieldValues)] +public class MetaFieldValuesSerializer : SyncSerializerRoot>?>, ISyncSerializer>?> +{ + private readonly IMetaFieldsValueRepository _metaFieldsValueRepository; + private readonly IMetaFieldsValueService _metaFieldsValueService; + private readonly IContentService _contentService; + private readonly ILocalizationService _localizationService; + + + + public MetaFieldValuesSerializer(IMetaFieldsValueService metaFieldsValueService, IMetaFieldsValueRepository metaFieldsValueRepository,IContentService contentService ,ILocalizationService localizationService ,ILogger logger) : base(logger) + { + _metaFieldsValueService = metaFieldsValueService; + _metaFieldsValueRepository = metaFieldsValueRepository; + _contentService = contentService; + _localizationService = localizationService; + } + + static Dictionary MetaFieldsValueEntitiesToDictionary(IEnumerable items, string culture) + { + return items.Where(it => it.Culture == culture).ToDictionary(it => it.Alias, it =>(object?)it.UserValue); + } + + + private XElement InitializeNode(KeyValuePair>? item) + { + return new XElement(Constants.Serialization.RootName, new XAttribute("Key", ItemKey(item)), + new XAttribute("Alias", ItemAlias(item))); + } + + private const string Value = "Value"; + private const string Culture = "Culture"; + protected override SyncAttempt SerializeCore(KeyValuePair>? item, SyncSerializerOptions options) + { + var node = InitializeNode(item); + + var languageVariants = _localizationService.GetAllLanguages(); + + if(item == null) + return SyncAttempt.Fail("NaN", ChangeType.NoChange, "No Meta Fields"); + + foreach (var languageVariant in languageVariants) + { + var fields = MetaFieldsValueEntitiesToDictionary(item.Value.Value, languageVariant.IsoCode); + + if (!fields.Any()) + { + return SyncAttempt.Fail(item.Value.Key.ToString(), ChangeType.NoChange, "No Meta Fields"); + } + + foreach (var field in fields) + { + var currentNode = node.Element(field.Key); + if (currentNode == null) + { + var element = new XElement(field.Key, new XElement(Value, field.Value, new XAttribute(Culture, languageVariant.IsoCode))); + node.Add(element); + } + else + { + currentNode.Add(new XElement(Value, field.Value, new XAttribute(Culture, languageVariant.IsoCode))); + } + + } + } + + + return SyncAttempt.Succeed(item.Value.Key.ToString(), node, typeof(IContent), ChangeType.Export); + } + + protected override SyncAttempt>?> CanDeserialize(XElement node, SyncSerializerOptions options) + { + return base.CanDeserialize(node, options); + } + + protected override SyncAttempt>?> DeserializeCore(XElement node, SyncSerializerOptions options) + { + var id = GetId(node); + var list = new List(); + KeyValuePair>? kv = new KeyValuePair>(id, list); + var lookup = new Dictionary>(); + + var currentNode = node.FirstNode as XElement; + while (currentNode != null) + { + if (lookup.TryGetValue(currentNode!.Name.LocalName, out var fieldsDictionary)) + { + foreach (var nodeValue in currentNode.Nodes()) + { + var elementValue = nodeValue as XElement; + if(elementValue == null) + continue; + fieldsDictionary.Add(elementValue.FirstAttribute!.Value, elementValue.Value); + } + } + else + { + var tempKvList = new Dictionary(); + foreach (var nodeValue in currentNode.Nodes()) + { + var elementValue = nodeValue as XElement; + if(elementValue == null) + continue; + tempKvList.Add(elementValue.FirstAttribute!.Value, elementValue.Value); + } + lookup.Add(currentNode!.Name.LocalName, tempKvList); + } + currentNode = currentNode.NextNode as XElement; + } + + var first = lookup.FirstOrDefault().Value.ToList(); + for (int i = 0; i < first.Count; i++) + { + var culture = first[i].Key; + foreach (var keyValuePair in lookup) + { + var entity = new MetaFieldsValueEntity() + { + Alias = keyValuePair.Key, + NodeId = id, + UserValue = keyValuePair.Value[culture], + Culture = culture + }; + list.Add(entity); + } + } + + + + return SyncAttempt>?>.Succeed(node.GetAlias(), kv, + typeof(KeyValuePair>?), ChangeType.Import); + + + } + + private const string Alias = "Alias"; + + public static int GetId(XElement node) + { + if (int.TryParse(node.Attribute(Alias)?.Value, out var id)) + { + return id; + } + + return -1; + } + + public override KeyValuePair>? FindItem(int id) + { + var value = _metaFieldsValueRepository.GetAll().Where(it => it.Key == id).SelectMany(it => it); + return new KeyValuePair>(id, value); + } + + public override KeyValuePair>? FindItem(Guid key) + { + var content = _contentService.GetById(key); + if (content == null) + return null!; + return FindItem(content.Id); + } + + public override KeyValuePair>? FindItem(string alias) + { + throw new NotImplementedException(); + } + + public override void SaveItem(KeyValuePair>? item) + { + if(item == null) + return; + var id = item.Value.Key; + foreach (var group in item.Value.Value.GroupBy(it => it.Culture)) + { + var culture = group.Key; + var values = MetaFieldsValueEntitiesToDictionary(group.ToList(), culture); + _metaFieldsValueService.AddValues(id, values, culture); + } + + } + + public override void DeleteItem(KeyValuePair>? item) + { + if(item == null) + return; + var id = item.Value.Key; + foreach (var group in item.Value.Value.GroupBy(it => it.Culture)) + { + var culture = group.Key; + var aliases = MetaFieldsValueEntitiesToDictionary(group.ToList(), culture).Select(metaField => metaField.Key ); + foreach (var alias in aliases) + { + _metaFieldsValueService.Delete(id, alias, culture); + } + } + } + public override bool IsValid(XElement node) + { + return true; + } + public override string ItemAlias(KeyValuePair>? item) + { + if(item == null) + return ""; + return item.Value.Key.ToString(); + } + + public override Guid ItemKey(KeyValuePair>? item) + { + if(item == null) + return Guid.Empty; + var content = _contentService.GetById(item.Value.Key); + return content?.Key ?? Guid.Empty; + } +} \ No newline at end of file diff --git a/src/SeoToolkit.uSync/XmlTrackers/SeoToolkitXmlTracker.cs b/src/SeoToolkit.uSync/XmlTrackers/SeoToolkitXmlTracker.cs new file mode 100644 index 00000000..d8ae8c97 --- /dev/null +++ b/src/SeoToolkit.uSync/XmlTrackers/SeoToolkitXmlTracker.cs @@ -0,0 +1,24 @@ +using SeoToolkit.Umbraco.MetaFields.Core.Models.SeoSettings.Database; +using SeoToolkit.Umbraco.uSync.Core.Serializers; +using uSync.Core.Serialization; +using uSync.Core.Tracking; + +namespace SeoToolkit.Umbraco.uSync.Core.XmlTrackers; + +public class SeoToolkitXmlTracker : + SyncXmlTracker>?>, + ISyncTracker>?> +{ + public override List TrackingItems => new List() + { + TrackingItem.Many("Property - *", "/*/Value", "@Culture"), + + }; + + public SeoToolkitXmlTracker(SyncSerializerCollection serializers, MetaFieldValuesSerializer serializer) : base(serializers) + { + this.serializer = serializer; + } + + +} \ No newline at end of file From afaae4a063b1da09840599e14237118b547d367f Mon Sep 17 00:00:00 2001 From: patrickdemooij9 Date: Thu, 11 May 2023 19:51:30 +0200 Subject: [PATCH 3/3] Move SeoToolkit items to their own group --- .../Handlers/SeoToolKitSyncHandlerBase.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs b/src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs index c8d764a3..1f331c1b 100644 --- a/src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs +++ b/src/SeoToolkit.uSync/Handlers/SeoToolKitSyncHandlerBase.cs @@ -11,6 +11,12 @@ namespace SeoToolkit.uSync.Handlers; public abstract class SeoToolKitSyncHandlerBase : SyncHandlerRoot { + public override string Group => "SeoToolkit"; + + protected SeoToolKitSyncHandlerBase(ILogger> logger, AppCaches appCaches, IShortStringHelper shortStringHelper, SyncFileService syncFileService, uSyncEventService mutexService, uSyncConfigService uSyncConfig, ISyncItemFactory itemFactory) : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, itemFactory) + { + } + protected override IEnumerable GetFolders(TObject parent) => Enumerable.Empty(); protected override TObject GetFromService(TObject item) @@ -25,8 +31,4 @@ protected override bool ShouldExport(XElement node, HandlerSettings config) { return base.ShouldExport(node, config); } - - protected SeoToolKitSyncHandlerBase(ILogger> logger, AppCaches appCaches, IShortStringHelper shortStringHelper, SyncFileService syncFileService, uSyncEventService mutexService, uSyncConfigService uSyncConfig, ISyncItemFactory itemFactory) : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, itemFactory) - { - } } \ No newline at end of file